@mmmbuto/nexuscrew 0.8.32 → 0.8.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,154 @@
1
+ ---
2
+ name: fill-forms
3
+ description: Use for inspecting, filling and visually validating local PDF or DOCX forms, including AcroForm fields, coordinate-based overlays, checkboxes, character boxes and an explicitly authorised signature image. Trigger when the user asks to fill in a PDF, complete a form, prepare paperwork for signature, populate a DOCX template or inspect a form before completion. Keep processing local, preserve the original, never invent personal data, and never sign or submit a document without explicit authorisation.
4
+ ---
5
+
6
+ # Fill Forms
7
+
8
+ Fill PDF and DOCX forms locally with the bundled scripts. Prefer named PDF
9
+ fields when they exist; use coordinate overlays only for flat forms or scans.
10
+ Always preserve the blank source and visually verify a new output file.
11
+
12
+ ## Select the response language
13
+
14
+ Choose the language for questions, progress updates and explanations in this
15
+ order:
16
+
17
+ 1. the user's explicit language preference;
18
+ 2. the language of the current request;
19
+ 3. a reliable client or system locale;
20
+ 4. English.
21
+
22
+ Do not translate field values, names, identifiers, legal wording or document
23
+ text merely to match the response language. Preserve the document's language
24
+ unless the user explicitly requests a translation.
25
+
26
+ ## Apply the safety boundary
27
+
28
+ - Work on local copies. Do not upload a form or its contents to an external
29
+ service unless the user explicitly requests and authorises that transfer.
30
+ - Never invent a name, address, identifier, account number, date, selection or
31
+ legal answer. Ask for missing data or leave the field visibly unresolved.
32
+ - Treat signatures as sensitive assets. Insert a signature image only when the
33
+ authorised signer supplies it and explicitly asks for insertion into the
34
+ identified document. Never keep signature images in source control.
35
+ - Filling is not signing, sending or submitting. Obtain separate explicit
36
+ authorisation before any consequential external action.
37
+ - Never overwrite the blank source. The scripts reject identical source and
38
+ output paths and require `--overwrite` before replacing an existing output.
39
+ - Generated documents, prepared signatures and preview images are written as
40
+ owner-only files; generated preview/grid directories are owner-only.
41
+ - Keep configuration files containing personal data out of source control and
42
+ remove temporary previews when the user no longer needs them.
43
+
44
+ ## Prepare dependencies
45
+
46
+ Resolve every script relative to this `SKILL.md`. The scripts require Python 3
47
+ and do not install packages automatically. Check the environment first:
48
+
49
+ ```bash
50
+ python3 --version
51
+ python3 -c "import fitz"
52
+ ```
53
+
54
+ For PDF image/signature handling and DOCX support, the complete dependency set
55
+ is listed in `requirements.txt`. If dependencies are missing, explain what is
56
+ needed and ask before creating a virtual environment or installing packages.
57
+ Never modify the system Python implicitly.
58
+
59
+ ## Inspect first
60
+
61
+ Run the inspector before filling a PDF:
62
+
63
+ ```bash
64
+ python3 <skill-dir>/scripts/inspect_pdf.py form.pdf
65
+ ```
66
+
67
+ It reports AcroForm fields and produces coordinate-grid PNG files next to the
68
+ source. Review those images rather than guessing positions. Replacing an
69
+ existing grid requires an intentional `--overwrite`.
70
+
71
+ - If named fields exist, prefer `mode: "acroform"`.
72
+ - For a flat PDF or scan, use `mode: "overlay"`.
73
+ - Use `mode: "both"` only when the document genuinely mixes both forms.
74
+
75
+ For precise checkboxes and signature lines, anchor coordinates to nearby PDF
76
+ text rather than estimating from a full-page image. Read
77
+ [`references/overlay-technique.md`](references/overlay-technique.md) before a
78
+ non-trivial overlay.
79
+
80
+ ## Fill a PDF
81
+
82
+ Create a JSON configuration beside the working copy:
83
+
84
+ ```json
85
+ {
86
+ "src": "blank-form.pdf",
87
+ "out": "completed-form.pdf",
88
+ "mode": "overlay",
89
+ "font_size": 9,
90
+ "overlay": [
91
+ {"page": 0, "x": 220, "y": 190, "text": "Example Person"},
92
+ {"page": 0, "x": 62, "y": 332, "check": true},
93
+ {
94
+ "page": 1,
95
+ "x": 70,
96
+ "y": 410,
97
+ "text": "XX00EXAMPLE0000000000000000",
98
+ "spread": {"step": 17.5}
99
+ }
100
+ ]
101
+ }
102
+ ```
103
+
104
+ Relative paths are resolved from the configuration file. Run:
105
+
106
+ ```bash
107
+ python3 <skill-dir>/scripts/fill_pdf.py form-data.json
108
+ ```
109
+
110
+ The script creates the output and a `<output>_preview/` directory. Use
111
+ `--overwrite` only for an intentional revision of that output.
112
+
113
+ Supported overlay items:
114
+
115
+ - `text`, `x`, `y`, optional `size`;
116
+ - `check: true`, `x`, `y`, optional `size`;
117
+ - `text` plus `spread.step` and optional `spread.skip` for character boxes;
118
+ - `image` plus `rect: [x0, y0, x1, y1]` for an explicitly authorised local
119
+ image;
120
+ - optional top-level `font_file` for a user-provided TTF/OTF when the built-in
121
+ PDF font cannot represent the required text.
122
+
123
+ For AcroForm mode, provide `"acroform": {"field-name": "value"}` and booleans
124
+ for checkboxes. Unknown requested field names fail closed instead of silently
125
+ producing an incomplete form.
126
+
127
+ ## Fill a DOCX template
128
+
129
+ Inspect its paragraphs and tables:
130
+
131
+ ```bash
132
+ python3 <skill-dir>/scripts/dump_docx.py template.docx
133
+ ```
134
+
135
+ Use explicit `{{placeholder}}` tokens whenever possible, then run:
136
+
137
+ ```bash
138
+ python3 <skill-dir>/scripts/fill_docx.py template.docx completed.docx \
139
+ --data form-data.json
140
+ ```
141
+
142
+ Plain-text key replacement is available only with `--literal-keys` because it
143
+ can otherwise replace unintended text. Unused data keys fail closed unless the
144
+ user deliberately chooses `--allow-unused`.
145
+
146
+ ## Verify before handoff
147
+
148
+ 1. Review every generated preview page at full size.
149
+ 2. Zoom critical regions such as checkboxes, character boxes and signatures.
150
+ 3. Compare every populated value with the user's authoritative source.
151
+ 4. Confirm that the original file is unchanged and the output has a distinct
152
+ name.
153
+ 5. Show the final preview or document to the user before calling it ready.
154
+ 6. State clearly which fields remain empty, uncertain, unsigned or unsubmitted.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Fill Forms"
3
+ short_description: "Safely fill and verify local PDF and DOCX forms"
4
+ default_prompt: "Use fill-forms to inspect and safely complete this local form."
@@ -0,0 +1,99 @@
1
+ # Coordinate Overlay Technique
2
+
3
+ Read this reference before filling a non-trivial flat PDF. For ordinary
4
+ AcroForm documents, named fields are safer and this guide is unnecessary.
5
+
6
+ ## Coordinate system
7
+
8
+ PyMuPDF uses PDF points (1 point is 1/72 inch), with the origin at the top-left.
9
+ The x axis increases to the right and the y axis increases downward.
10
+ `insert_text((x, y), ...)` places the text baseline at `(x, y)`.
11
+
12
+ The PNG grids produced by `inspect_pdf.py` use the same coordinates. No manual
13
+ pixel-to-point conversion is needed.
14
+
15
+ ## Anchor to existing text
16
+
17
+ For a checkbox, signature line or narrow field, locate a nearby printed label
18
+ and derive an offset from its exact PDF word bounds:
19
+
20
+ ```python
21
+ import fitz
22
+
23
+ doc = fitz.open("form.pdf")
24
+ for word in doc[0].get_text("words"):
25
+ if word[4] == "Consent":
26
+ print(word[:5])
27
+ ```
28
+
29
+ A word tuple starts with `(x0, y0, x1, y1, text)`. Position the field relative
30
+ to those bounds and verify the result in a high-resolution clipped preview.
31
+ Do not treat an estimated offset as final evidence.
32
+
33
+ ## Text baselines and boxes
34
+
35
+ Use the line on which the text should sit as the initial y coordinate. If the
36
+ text falls below the line, decrease y; if it floats too high, increase y.
37
+ Adjust in small steps.
38
+
39
+ For a checkbox, place the X near the horizontal centre and use the lower part
40
+ of the box as the initial baseline. Verify at high zoom.
41
+
42
+ ## Character-by-character fields
43
+
44
+ Use `spread.step` for account numbers, dates or identifiers printed as separate
45
+ boxes:
46
+
47
+ ```json
48
+ {
49
+ "page": 0,
50
+ "x": 70,
51
+ "y": 410,
52
+ "text": "XX00EXAMPLE0000000000000000",
53
+ "spread": {"step": 17.5}
54
+ }
55
+ ```
56
+
57
+ Measure `step` from two adjacent boxes on the coordinate grid. `spread.skip`
58
+ accepts zero-based character indexes that should not be drawn while retaining
59
+ their spacing.
60
+
61
+ ## Long text and fonts
62
+
63
+ The overlay script does not wrap text automatically. Reduce the per-item
64
+ `size`, split the content into explicitly positioned lines, or leave the field
65
+ for manual completion rather than allowing text to overlap legal wording.
66
+
67
+ The built-in PDF font is suitable for common Latin text. For another script or
68
+ missing glyph, use a locally available, appropriately licensed TTF/OTF through
69
+ the top-level `font_file` configuration. Do not bundle private or unlicensed
70
+ fonts.
71
+
72
+ ## Signature images
73
+
74
+ Insert a signature only after explicit authorisation from the signer for the
75
+ specific document. Prepare the image locally with `prepare_signature.py` and
76
+ keep the original and prepared image outside source control.
77
+
78
+ Use a transparent PNG and a rectangle whose proportions are close to the
79
+ signature. Avoid stretching it unnaturally. Place it so the visible stroke
80
+ rests on the intended line, then inspect the result on a pure white background
81
+ and in a clipped high-resolution preview.
82
+
83
+ The fill script downsizes very large inserted images to the resolution needed
84
+ for the target rectangle. This avoids embedding a multi-megapixel source into
85
+ a small signature area.
86
+
87
+ ## Focused verification
88
+
89
+ Render a critical region rather than relying only on a full-page preview:
90
+
91
+ ```python
92
+ clip = fitz.Rect(x0, y0, x1, y1)
93
+ pix = doc[page_number].get_pixmap(clip=clip, dpi=240)
94
+ pix.save("verification-region.png")
95
+ ```
96
+
97
+ Check every populated field, not only representative examples. Treat the
98
+ completed file as a draft until the user has reviewed it. Filling a form does
99
+ not authorise signing, sending or submission.
@@ -0,0 +1,4 @@
1
+ PyMuPDF>=1.24,<2
2
+ Pillow>=10,<13
3
+ numpy>=1.26,<3
4
+ python-docx>=1.1,<2
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env python3
2
+ """Print paragraphs, tables, headers and footers from a DOCX template."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ from pathlib import Path
8
+
9
+
10
+ def parse_args() -> argparse.Namespace:
11
+ parser = argparse.ArgumentParser(
12
+ description="Inspect text and table structure in a local DOCX form."
13
+ )
14
+ parser.add_argument("docx", type=Path)
15
+ return parser.parse_args()
16
+
17
+
18
+ def load_docx():
19
+ try:
20
+ import docx
21
+ except ImportError as exc:
22
+ raise SystemExit(
23
+ "Missing dependency: python-docx. With user consent, install the "
24
+ "packages listed in the skill's requirements.txt."
25
+ ) from exc
26
+ return docx
27
+
28
+
29
+ def print_paragraphs(paragraphs, heading: str) -> None:
30
+ print(f"== {heading} ==")
31
+ for index, paragraph in enumerate(paragraphs):
32
+ text = paragraph.text.strip()
33
+ if text:
34
+ print(f"[{index}] {text}")
35
+
36
+
37
+ def print_tables(tables, heading: str) -> None:
38
+ for table_index, table in enumerate(tables):
39
+ print(
40
+ f"\n== {heading} TABLE {table_index} "
41
+ f"({len(table.rows)}x{len(table.columns)}) =="
42
+ )
43
+ for row_index, row in enumerate(table.rows):
44
+ cells = [cell.text.strip() for cell in row.cells]
45
+ print(f" row {row_index}: {cells}")
46
+
47
+
48
+ def main() -> None:
49
+ args = parse_args()
50
+ if not args.docx.is_file():
51
+ raise SystemExit(f"DOCX not found or not a regular file: {args.docx}")
52
+
53
+ docx = load_docx()
54
+ try:
55
+ document = docx.Document(args.docx)
56
+ except Exception as exc:
57
+ raise SystemExit(f"Could not open DOCX: {exc}") from exc
58
+
59
+ print_paragraphs(document.paragraphs, "BODY PARAGRAPHS")
60
+ print_tables(document.tables, "BODY")
61
+
62
+ for section_index, section in enumerate(document.sections):
63
+ print_paragraphs(section.header.paragraphs, f"HEADER {section_index}")
64
+ print_tables(section.header.tables, f"HEADER {section_index}")
65
+ print_paragraphs(section.footer.paragraphs, f"FOOTER {section_index}")
66
+ print_tables(section.footer.tables, f"FOOTER {section_index}")
67
+
68
+
69
+ if __name__ == "__main__":
70
+ main()
@@ -0,0 +1,207 @@
1
+ #!/usr/bin/env python3
2
+ """Fill explicit placeholders in a local DOCX template."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import os
9
+ import re
10
+ import tempfile
11
+ from collections import Counter
12
+ from pathlib import Path
13
+
14
+
15
+ def parse_args() -> argparse.Namespace:
16
+ parser = argparse.ArgumentParser(
17
+ description="Replace {{placeholders}} in a DOCX form using a JSON mapping."
18
+ )
19
+ parser.add_argument("src", type=Path)
20
+ parser.add_argument("out", type=Path)
21
+ parser.add_argument("--data", required=True, type=Path)
22
+ parser.add_argument(
23
+ "--literal-keys",
24
+ action="store_true",
25
+ help="also replace unbraced mapping keys; use only for an inspected template",
26
+ )
27
+ parser.add_argument(
28
+ "--allow-unused",
29
+ action="store_true",
30
+ help="allow mapping keys that were not found in the document",
31
+ )
32
+ parser.add_argument(
33
+ "--overwrite",
34
+ action="store_true",
35
+ help="replace an existing output file, never the source",
36
+ )
37
+ return parser.parse_args()
38
+
39
+
40
+ def load_docx():
41
+ try:
42
+ import docx
43
+ except ImportError as exc:
44
+ raise SystemExit(
45
+ "Missing dependency: python-docx. With user consent, install the "
46
+ "packages listed in the skill's requirements.txt."
47
+ ) from exc
48
+ return docx
49
+
50
+
51
+ def load_mapping(path: Path) -> dict[str, str]:
52
+ if not path.is_file():
53
+ raise SystemExit(f"Data file not found: {path}")
54
+ if path.stat().st_size > 2 * 1024 * 1024:
55
+ raise SystemExit("Data JSON exceeds the 2 MiB safety limit.")
56
+
57
+ try:
58
+ raw = json.loads(path.read_text(encoding="utf-8"))
59
+ except (OSError, UnicodeError, json.JSONDecodeError) as exc:
60
+ raise SystemExit(f"Could not read data JSON: {exc}") from exc
61
+
62
+ if not isinstance(raw, dict) or not raw:
63
+ raise SystemExit("Data JSON must be a non-empty object.")
64
+
65
+ mapping: dict[str, str] = {}
66
+ for key, value in raw.items():
67
+ if (
68
+ not isinstance(key, str)
69
+ or not key.strip()
70
+ or len(key) > 128
71
+ or "{{" in key
72
+ or "}}" in key
73
+ ):
74
+ raise SystemExit("Every placeholder key must be a plain non-empty string.")
75
+ if value is None:
76
+ mapping[key] = ""
77
+ elif isinstance(value, (str, int, float, bool)):
78
+ mapping[key] = str(value)
79
+ else:
80
+ raise SystemExit(f"Placeholder {key!r} has a non-scalar value.")
81
+ return mapping
82
+
83
+
84
+ def token_pattern(mapping: dict[str, str], literal_keys: bool):
85
+ token_to_key = {}
86
+ for key in mapping:
87
+ token_to_key[f"{{{{{key}}}}}"] = key
88
+ if literal_keys:
89
+ token_to_key[key] = key
90
+ ordered = sorted(token_to_key, key=len, reverse=True)
91
+ return re.compile("|".join(re.escape(token) for token in ordered)), token_to_key
92
+
93
+
94
+ def replace_in_paragraph(paragraph, pattern, token_to_key, mapping, counts) -> None:
95
+ original = "".join(run.text for run in paragraph.runs)
96
+ if not original:
97
+ return
98
+
99
+ def replacement(match):
100
+ key = token_to_key[match.group(0)]
101
+ counts[key] += 1
102
+ return mapping[key]
103
+
104
+ updated = pattern.sub(replacement, original)
105
+ if updated == original:
106
+ return
107
+ if not paragraph.runs:
108
+ paragraph.add_run(updated)
109
+ return
110
+ paragraph.runs[0].text = updated
111
+ for run in paragraph.runs[1:]:
112
+ run.text = ""
113
+
114
+
115
+ def process_tables(tables, process_paragraph) -> None:
116
+ for table in tables:
117
+ for row in table.rows:
118
+ for cell in row.cells:
119
+ for paragraph in cell.paragraphs:
120
+ process_paragraph(paragraph)
121
+ process_tables(cell.tables, process_paragraph)
122
+
123
+
124
+ def save_atomic(document, output: Path) -> None:
125
+ output.parent.mkdir(parents=True, exist_ok=True)
126
+ handle = tempfile.NamedTemporaryFile(
127
+ prefix=f".{output.name}.",
128
+ suffix=".tmp",
129
+ dir=output.parent,
130
+ delete=False,
131
+ )
132
+ temporary = Path(handle.name)
133
+ handle.close()
134
+ try:
135
+ document.save(temporary)
136
+ os.replace(temporary, output)
137
+ os.chmod(output, 0o600)
138
+ except Exception:
139
+ temporary.unlink(missing_ok=True)
140
+ raise
141
+
142
+
143
+ def main() -> None:
144
+ args = parse_args()
145
+ source = args.src.resolve()
146
+ output_unresolved = args.out.expanduser()
147
+ if output_unresolved.is_symlink():
148
+ raise SystemExit("Output must not be a symbolic link.")
149
+ output = output_unresolved.resolve()
150
+ data_path = args.data.resolve()
151
+
152
+ if not source.is_file():
153
+ raise SystemExit(f"DOCX not found or not a regular file: {source}")
154
+ if source == output:
155
+ raise SystemExit("Source and output must be different files.")
156
+ if output.exists() and not args.overwrite:
157
+ raise SystemExit("Output already exists. Use --overwrite for an intentional revision.")
158
+
159
+ mapping = load_mapping(data_path)
160
+ pattern, token_to_key = token_pattern(mapping, args.literal_keys)
161
+ counts = Counter()
162
+ docx = load_docx()
163
+
164
+ try:
165
+ document = docx.Document(source)
166
+ except Exception as exc:
167
+ raise SystemExit(f"Could not open DOCX: {exc}") from exc
168
+
169
+ def process(paragraph):
170
+ replace_in_paragraph(
171
+ paragraph,
172
+ pattern,
173
+ token_to_key,
174
+ mapping,
175
+ counts,
176
+ )
177
+
178
+ for paragraph in document.paragraphs:
179
+ process(paragraph)
180
+ process_tables(document.tables, process)
181
+
182
+ for section in document.sections:
183
+ for paragraph in section.header.paragraphs:
184
+ process(paragraph)
185
+ process_tables(section.header.tables, process)
186
+ for paragraph in section.footer.paragraphs:
187
+ process(paragraph)
188
+ process_tables(section.footer.tables, process)
189
+
190
+ unused = sorted(key for key in mapping if counts[key] == 0)
191
+ if unused and not args.allow_unused:
192
+ raise SystemExit(
193
+ "Unused placeholders: "
194
+ + ", ".join(repr(key) for key in unused)
195
+ + ". Inspect the template or use --allow-unused deliberately."
196
+ )
197
+
198
+ save_atomic(document, output)
199
+ print(f"Saved: {output}")
200
+ print(f"Replacements: {sum(counts.values())}")
201
+ if unused:
202
+ print("Unused placeholders allowed: " + ", ".join(repr(key) for key in unused))
203
+ print("Review the completed DOCX or export a local copy to PDF before handoff.")
204
+
205
+
206
+ if __name__ == "__main__":
207
+ main()