pac-vector 0.1.0__py3-none-any.whl

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.
pac_vector/server.py ADDED
@@ -0,0 +1,320 @@
1
+ """MCP tool surface: 17 tools over the document store."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from typing import Annotated, Literal
7
+
8
+ from mcp.server.fastmcp import FastMCP, Image
9
+ from mcp.types import ToolAnnotations
10
+ from pydantic import BaseModel, Field
11
+
12
+ from .document import Document, Store
13
+ from .preview import Preview
14
+
15
+ INSTRUCTIONS = """\
16
+ SVG editor over in-memory virtual documents (one per "window", like Inkscape).
17
+
18
+ Workflow: doc_create → add / update / arrange / group / mask … → render to look at the
19
+ result → doc_export to save. A live browser preview runs at the URL doc_create and
20
+ doc_list return; every edit appears there immediately with the touched elements outlined.
21
+
22
+ Model:
23
+ - Every element has an id; when markup omits one, an id like rect-3 is assigned and
24
+ returned. Tools address elements by id; layers may also be addressed by label.
25
+ - Layers are Inkscape layers (<g inkscape:groupmode="layer">). New elements go to the
26
+ current layer (the last one created or added to) unless parent= is given.
27
+ - Coordinates are user units of the document's viewBox.
28
+ - Content goes in as SVG markup. Gradients, patterns, filters, markers, symbols and
29
+ <defs> in markup are moved into <defs> automatically; reference them with url(#id).
30
+ - Every successful edit is one undo step. A failed edit changes nothing.
31
+
32
+ Granularity: put several elements in one add call; give update several ids at once;
33
+ use replace only when an element's structure changes (e.g. rewriting a path or a group).
34
+ inspect (outline) is the cheap way to see structure; render shows pixels.
35
+ """
36
+
37
+ mcp = FastMCP("pac-vector", instructions=INSTRUCTIONS)
38
+ store = Store()
39
+ preview = Preview(store, port=int(os.environ.get("PAC_VECTOR_PORT", "0") or 0))
40
+ store.on_change = lambda doc, summary: preview.publish(doc.payload(summary))
41
+ store.on_close = lambda doc_id: preview.publish({"type": "closed", "id": doc_id})
42
+
43
+ RO = ToolAnnotations(readOnlyHint=True, idempotentHint=True, openWorldHint=False)
44
+ EDIT = ToolAnnotations(readOnlyHint=False, destructiveHint=False, openWorldHint=False)
45
+ DESTRUCTIVE = ToolAnnotations(readOnlyHint=False, destructiveHint=True, openWorldHint=False)
46
+
47
+ DocId = Annotated[str, Field(description="Document id from doc_create / doc_list, e.g. 'badge__3fa9c1'.")]
48
+ Ids = Annotated[list[str], Field(description="Element ids (layers may also be given by label).")]
49
+ Position = Annotated[
50
+ str | None,
51
+ Field(description="'top' (default, frontmost) | 'bottom' | 'before:<id>' | 'after:<id>' (z-order inside the parent)."),
52
+ ]
53
+
54
+
55
+ def _foot(doc: Document) -> str:
56
+ return f"[{doc.id} v{doc.version}]"
57
+
58
+
59
+ def _done(doc: Document, summary: str) -> str:
60
+ return f"{summary}\n{_foot(doc)}"
61
+
62
+
63
+ # ── documents ────────────────────────────────────────────────────────────────
64
+
65
+
66
+ @mcp.tool(annotations=EDIT, structured_output=False)
67
+ def doc_create(
68
+ name: Annotated[str | None, Field(description="Window name; defaults to the file name or 'untitled'.")] = None,
69
+ width: Annotated[float | None, Field(description="Page width in `units` (blank default 800).")] = None,
70
+ height: Annotated[float | None, Field(description="Page height in `units` (blank default 600).")] = None,
71
+ units: Literal["px", "mm", "cm", "in", "pt", "pc"] = "px",
72
+ view_box: Annotated[str | None, Field(description="'minx miny w h'; default '0 0 width height'.")] = None,
73
+ background: Annotated[str | None, Field(description="Page colour, e.g. '#ffffff'. Omit for transparent.")] = None,
74
+ layers: Annotated[list[str] | None, Field(description="Layer labels to create, bottom to top. Default ['Layer 1'].")] = None,
75
+ source: Annotated[
76
+ str | None,
77
+ Field(description="Import instead of starting blank: a path to .svg/.svgz, or full <svg> markup."),
78
+ ] = None,
79
+ ) -> str:
80
+ """Open a new document window, blank or imported. Returns its id, structure and the live preview URL."""
81
+ url = preview.start()
82
+ doc, notes = store.create(name, width, height, units, view_box, background, layers, source)
83
+ return "\n".join([f"created {doc.id}", doc.info(), *notes, f"preview: {url}#{doc.id}"])
84
+
85
+
86
+ @mcp.tool(annotations=RO, structured_output=False)
87
+ def doc_list() -> str:
88
+ """List open document windows: id, name, size, layers, version, unsaved flag, file path, preview URL."""
89
+ if not store.docs:
90
+ return "no documents open (doc_create)"
91
+ url = preview.start()
92
+ return "\n".join([d.info() for d in store.docs.values()] + [f"preview: {url}"])
93
+
94
+
95
+ @mcp.tool(annotations=EDIT, structured_output=False)
96
+ def doc_set(
97
+ doc: DocId,
98
+ name: str | None = None,
99
+ width: Annotated[float | None, Field(description="New page width; the viewBox scales with it so content keeps its size.")] = None,
100
+ height: Annotated[float | None, Field(description="New page height (viewBox scales with it).")] = None,
101
+ units: Literal["px", "mm", "cm", "in", "pt", "pc"] | None = None,
102
+ view_box: Annotated[str | None, Field(description="'minx miny w h' — sets the coordinate system explicitly.")] = None,
103
+ background: Annotated[str | None, Field(description="Page colour; 'none' for transparent.")] = None,
104
+ fit: Annotated[Literal["content"] | None, Field(description="'content' shrinks/grows the page to the drawing's visual bounds.")] = None,
105
+ ) -> str:
106
+ """Change window properties: name, page size, units, viewBox, background, or fit the page to the content."""
107
+ d = store.get(doc)
108
+ return _done(d, d.set_props(name, width, height, units, view_box, background, fit))
109
+
110
+
111
+ @mcp.tool(annotations=EDIT, structured_output=False)
112
+ def doc_export(
113
+ doc: DocId,
114
+ path: Annotated[str | None, Field(description="Output file (.svg or .png). Relative paths resolve against PAC_VECTOR_ROOT or the server's cwd. Omit to re-save to the file it was opened from / last saved to.")] = None,
115
+ format: Annotated[Literal["svg", "png"] | None, Field(description="Defaults to the path's extension.")] = None,
116
+ scale: Annotated[float, Field(description="PNG only: pixels per CSS px (2 = retina). Capped at 2048px per side.")] = 1.0,
117
+ area: Annotated[
118
+ str | list[str],
119
+ Field(description="'page' (default), 'drawing' (crop to visible content), or a list of element ids (export only those, cropped)."),
120
+ ] = "page",
121
+ plain: Annotated[bool, Field(description="SVG only: strip Inkscape/sodipodi metadata (layers become plain <g>).")] = False,
122
+ background: Annotated[bool, Field(description="Paint the page background into the output.")] = True,
123
+ ) -> str:
124
+ """Write the document to disk as SVG (Inkscape-compatible, layers kept) or PNG."""
125
+ d = store.get(doc)
126
+ return d.export(path, format, scale, area, plain, background)
127
+
128
+
129
+ @mcp.tool(annotations=DESTRUCTIVE, structured_output=False)
130
+ def doc_close(
131
+ doc: DocId,
132
+ discard: Annotated[bool, Field(description="Close even with unsaved changes (they are lost).")] = False,
133
+ ) -> str:
134
+ """Close a document window. Refuses if it has unsaved changes unless discard=true."""
135
+ return store.close(doc, discard)
136
+
137
+
138
+ # ── reading ──────────────────────────────────────────────────────────────────
139
+
140
+
141
+ @mcp.tool(annotations=RO, structured_output=False)
142
+ def inspect(
143
+ doc: DocId,
144
+ root: Annotated[str | None, Field(description="Id of the subtree to show; omit for the whole document.")] = None,
145
+ depth: Annotated[int, Field(ge=1, le=20, description="Outline levels to expand (layers are level 1).")] = 3,
146
+ format: Annotated[
147
+ Literal["outline", "xml"],
148
+ Field(description="'outline': one compact line per element (id, geometry, paint, transform). 'xml': the subtree's markup (big data: URIs are elided)."),
149
+ ] = "outline",
150
+ ) -> str:
151
+ """Show document structure: defs, layers, groups and elements with their ids and key attributes."""
152
+ d = store.get(doc)
153
+ out = d.inspect(root, depth, format)
154
+ if root is None and format == "outline":
155
+ out += f"\npreview: {preview.start()}#{d.id}"
156
+ return out
157
+
158
+
159
+ @mcp.tool(annotations=RO, structured_output=False)
160
+ def render(
161
+ doc: DocId,
162
+ ids: Annotated[list[str] | None, Field(description="Crop to these elements (with surrounding context); omit for the whole page.")] = None,
163
+ scale: Annotated[float, Field(gt=0, le=8, description="Zoom factor; output is capped at 2048px per side.")] = 1.0,
164
+ padding: Annotated[int, Field(ge=0, le=512, description="Pixels of margin around a crop.")] = 16,
165
+ isolate: Annotated[bool, Field(description="With ids: draw only those elements.")] = False,
166
+ background: Annotated[bool, Field(description="Paint the page background.")] = True,
167
+ ) -> list:
168
+ """Rasterise the page (or a crop around elements) to a PNG you can look at. With ids it also reports their visual bounding box in user units."""
169
+ d = store.get(doc)
170
+ text, png = d.render(ids, scale, padding, background, isolate)
171
+ return [f"{text}\n{_foot(d)}", Image(data=png, format="png")]
172
+
173
+
174
+ # ── editing ──────────────────────────────────────────────────────────────────
175
+
176
+
177
+ @mcp.tool(annotations=EDIT, structured_output=False)
178
+ def add(
179
+ doc: DocId,
180
+ markup: Annotated[
181
+ str,
182
+ Field(description="One or more SVG elements, e.g. '<rect x=\"10\" y=\"10\" width=\"80\" height=\"40\" fill=\"url(#g)\"/><linearGradient id=\"g\">…</linearGradient>'. Gradients/patterns/filters/markers/symbols go to <defs> automatically. Prefixes xlink:, inkscape:, sodipodi: are pre-declared."),
183
+ ],
184
+ parent: Annotated[str | None, Field(description="Layer or group id/label to add into; default: the current layer.")] = None,
185
+ position: Position = None,
186
+ ) -> str:
187
+ """Add SVG elements to a layer or group. Returns the ids assigned to every new element."""
188
+ d = store.get(doc)
189
+ return _done(d, d.add(markup, parent, position))
190
+
191
+
192
+ class TransformSpec(BaseModel):
193
+ translate: list[float] | None = Field(None, description="[dx, dy]")
194
+ rotate: float | None = Field(None, description="Degrees, clockwise.")
195
+ scale: float | list[float] | None = Field(None, description="s or [sx, sy].")
196
+ origin: Literal["center"] | list[float] = Field(
197
+ "center", description="Pivot for rotate/scale in parent coordinates: 'center' (each element's own visual center) or [x, y]."
198
+ )
199
+ mode: Literal["append", "set"] = Field(
200
+ "append", description="'append' applies on top of the existing transform; 'set' replaces it."
201
+ )
202
+
203
+
204
+ @mcp.tool(annotations=EDIT, structured_output=False)
205
+ def update(
206
+ doc: DocId,
207
+ ids: Ids,
208
+ attrs: Annotated[
209
+ dict[str, str | float | None] | None,
210
+ Field(description="Attributes to set; null removes. Geometry, paint, href, 'xlink:href', 'inkscape:label'… 'id' renames (one element; references follow)."),
211
+ ] = None,
212
+ style: Annotated[
213
+ dict[str, str | float | None] | None,
214
+ Field(description="CSS properties merged into style=\"\" (they override attributes); null removes the property from both."),
215
+ ] = None,
216
+ text: Annotated[str | None, Field(description="Replace the text content of a <text>/<tspan>.")] = None,
217
+ transform: Annotated[
218
+ str | TransformSpec | None,
219
+ Field(description="Raw transform string (replaces; '' removes), or {translate, rotate, scale, origin, mode}. Applied as translate · rotate · scale on top of the current transform."),
220
+ ] = None,
221
+ label: Annotated[str | None, Field(description="Inkscape label ('' removes).")] = None,
222
+ hidden: Annotated[bool | None, Field(description="Hide (display:none) or show.")] = None,
223
+ locked: Annotated[bool | None, Field(description="Lock against edits by these tools, or unlock.")] = None,
224
+ ) -> str:
225
+ """Change attributes, style, text, transform, label, visibility or lock of one or many elements at once."""
226
+ d = store.get(doc)
227
+ tf = transform.model_dump() if isinstance(transform, TransformSpec) else transform
228
+ return _done(d, d.update(ids, attrs, style, text, tf, label, hidden, locked))
229
+
230
+
231
+ @mcp.tool(annotations=DESTRUCTIVE, structured_output=False)
232
+ def replace(
233
+ doc: DocId,
234
+ markup: Annotated[str, Field(description="New SVG for the element (keeps its id if the markup has none), or a whole <svg> / element list for the whole drawing.")],
235
+ id: Annotated[str | None, Field(description="Element to replace. Omit to replace the entire drawing (document properties and the preview stay).")] = None,
236
+ ) -> str:
237
+ """Swap one element's markup for new markup, or set the whole drawing's content."""
238
+ d = store.get(doc)
239
+ return _done(d, d.replace(markup, id))
240
+
241
+
242
+ @mcp.tool(annotations=DESTRUCTIVE, structured_output=False)
243
+ def remove(doc: DocId, ids: Ids) -> str:
244
+ """Delete elements (with their children). Warns about anything still referencing them."""
245
+ d = store.get(doc)
246
+ return _done(d, d.remove(ids))
247
+
248
+
249
+ @mcp.tool(annotations=EDIT, structured_output=False)
250
+ def arrange(
251
+ doc: DocId,
252
+ ids: Ids,
253
+ to: Annotated[str | None, Field(description="Layer or group to move/copy into (on-canvas position is preserved).")] = None,
254
+ position: Annotated[
255
+ str | None,
256
+ Field(description="'top' | 'bottom' | 'up' | 'down' (one step, no `to`) | 'before:<id>' | 'after:<id>'."),
257
+ ] = None,
258
+ copy: Annotated[bool, Field(description="Duplicate instead of move (copies get new ids; placed just above the originals unless to/position say otherwise).")] = False,
259
+ offset: Annotated[list[float] | None, Field(description="[dx, dy] translation applied to the moved/copied elements.")] = None,
260
+ ) -> str:
261
+ """Z-order, move between layers/groups, reorder layers, or duplicate elements."""
262
+ d = store.get(doc)
263
+ return _done(d, d.arrange(ids, to, position, copy, offset))
264
+
265
+
266
+ # ── structure ────────────────────────────────────────────────────────────────
267
+
268
+
269
+ @mcp.tool(annotations=EDIT, structured_output=False)
270
+ def group(
271
+ doc: DocId,
272
+ ids: Annotated[list[str] | None, Field(description="Elements to wrap; omit to create an empty group/layer.")] = None,
273
+ label: Annotated[str | None, Field(description="Label (layer name when layer=true).")] = None,
274
+ layer: Annotated[bool, Field(description="Make it an Inkscape layer. layer=true without ids creates a new empty layer and makes it current.")] = False,
275
+ parent: Annotated[str | None, Field(description="Where the new group goes; default: the topmost element's parent (or the root for a new layer).")] = None,
276
+ position: Position = None,
277
+ ) -> str:
278
+ """Group elements (or create a new empty group/layer). Elements keep their on-canvas position."""
279
+ d = store.get(doc)
280
+ return _done(d, d.group(ids, label, layer, parent, position))
281
+
282
+
283
+ @mcp.tool(annotations=EDIT, structured_output=False)
284
+ def ungroup(doc: DocId, ids: Ids) -> str:
285
+ """Dissolve groups or layers: children move up one level; the group's transform and inherited styles are pushed onto them."""
286
+ d = store.get(doc)
287
+ return _done(d, d.ungroup(ids))
288
+
289
+
290
+ @mcp.tool(annotations=EDIT, structured_output=False)
291
+ def mask(
292
+ doc: DocId,
293
+ targets: Ids,
294
+ source: Annotated[
295
+ str,
296
+ Field(description="Id of an existing element to turn into the mask/clip (it is moved out of the drawing), or SVG markup drawn in the targets' own canvas coordinates."),
297
+ ],
298
+ kind: Annotated[Literal["mask", "clip"], Field(description="'clip': hard edge, only the source's shape matters. 'mask': soft, uses the source's colours.")] = "mask",
299
+ mode: Annotated[
300
+ Literal["luminance", "alpha"],
301
+ Field(description="mask only — 'luminance': white shows, black hides; 'alpha': source opacity decides."),
302
+ ] = "luminance",
303
+ ) -> str:
304
+ """Apply a mask or clip path to elements; the <mask>/<clipPath> is created in <defs> and wired up."""
305
+ d = store.get(doc)
306
+ return _done(d, d.mask(targets, source, kind, mode))
307
+
308
+
309
+ @mcp.tool(annotations=EDIT, structured_output=False)
310
+ def unmask(doc: DocId, targets: Ids) -> str:
311
+ """Release masks/clip paths: their content returns to the drawing just above each target."""
312
+ d = store.get(doc)
313
+ return _done(d, d.unmask(targets))
314
+
315
+
316
+ @mcp.tool(annotations=EDIT, structured_output=False)
317
+ def undo(doc: DocId, steps: Annotated[int, Field(ge=1, le=100)] = 1) -> str:
318
+ """Undo the last edit(s) of a document."""
319
+ d = store.get(doc)
320
+ return _done(d, d.undo(steps))
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.5
2
+ Name: pac-vector
3
+ Version: 0.1.0
4
+ Summary: Local stdio MCP server for SVG editing via in-memory virtual documents, with a live browser preview
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: lxml>=5.0
7
+ Requires-Dist: mcp[cli]<2.0.0,>=1.10.0
8
+ Requires-Dist: pillow>=10.0
9
+ Requires-Dist: resvg-py>=0.5.0
10
+ Provides-Extra: dev
11
+ Requires-Dist: pytest>=8.0; extra == 'dev'
12
+ Description-Content-Type: text/markdown
13
+
14
+ # pac-vector
15
+
16
+ Local stdio MCP server: an SVG editor over in-memory **virtual documents** (one per
17
+ "window", like Inkscape), with a **live browser preview** of every edit.
18
+
19
+ - Content goes in as SVG markup; structure (layers, groups, masks, z-order) has typed tools.
20
+ - Layers are real Inkscape layers, so exported files open in Inkscape with layers intact.
21
+ - Every edit is atomic (a failed call changes nothing) and one undo step.
22
+ - `render` returns a PNG the agent can look at; rendering is resvg (no browser, no Cairo).
23
+
24
+ ## Install
25
+
26
+ Same shape as pac-pincers: a pip package, run as a module by whatever Python it
27
+ was installed into.
28
+
29
+ ```powershell
30
+ python -m pip install --upgrade pac-vector
31
+ claude mcp add pac-vector --scope user -- python -m pac_vector
32
+ ```
33
+
34
+ As a plugin `.mcp.json` entry (mirrors the datatopia plugin's pac-pincers entry):
35
+
36
+ ```json
37
+ "pac-vector": {
38
+ "type": "stdio",
39
+ "command": "${PAC_VECTOR_PYTHON:-python}",
40
+ "args": ["-m", "pac_vector"],
41
+ "env": {}
42
+ }
43
+ ```
44
+
45
+ Development:
46
+
47
+ ```powershell
48
+ python -m venv .venv
49
+ .\.venv\Scripts\python.exe -m pip install -e ".[dev]"
50
+ .\.venv\Scripts\python.exe run_tests.py -q
51
+ .\.venv\Scripts\python.exe -m build # dist/pac_vector-<ver>.tar.gz + .whl
52
+ ```
53
+
54
+ ## Tools (17)
55
+
56
+ | Group | Tools |
57
+ |---|---|
58
+ | Documents | `doc_create` (blank or import), `doc_list`, `doc_set` (size, units, viewBox, background, fit), `doc_export` (svg/png, page/drawing/ids, plain), `doc_close` |
59
+ | Reading | `inspect` (outline or xml), `render` (PNG, optional crop to ids + bbox) |
60
+ | Editing | `add` (markup → layer/group, defs auto-routed), `update` (attrs/style/text/transform/label/hidden/locked on many ids), `replace` (one element or the whole drawing), `remove`, `arrange` (z-order, move between layers, duplicate, offset) |
61
+ | Structure | `group` (also new layers), `ungroup`, `mask` (mask or clip from id or markup), `unmask` |
62
+ | History | `undo` |
63
+
64
+ ## Preview
65
+
66
+ `doc_create` / `doc_list` / `inspect` print `preview: http://127.0.0.1:<port>/#<doc>`.
67
+ The page has a tab per document, outlines the elements each edit touched, and
68
+ shows (and copies) the id of anything you click. The server lives inside the MCP
69
+ process: it starts on the first `doc_create` and dies with the session.
70
+
71
+ ## Environment
72
+
73
+ | Variable | Default | Meaning |
74
+ |---|---|---|
75
+ | `PAC_VECTOR_PORT` | `0` (random) | Pin the preview port (falls back to random if busy) |
76
+ | `PAC_VECTOR_OPEN_BROWSER` | off | `1` opens the preview in the default browser on first start |
77
+ | `PAC_VECTOR_ROOT` | server cwd | Base directory for relative export/import paths |
78
+ | `PAC_VECTOR_LOG` | `WARNING` | Log level (stderr only) |
79
+
80
+ ## Notes
81
+
82
+ - stdout is the JSON-RPC pipe: nothing may print to it. `tests/test_stdout_safety.py`
83
+ scans the package for `print()` / `sys.stdout`, and `tests/test_stdio.py` runs a real session.
84
+ - Native modules are loaded before the stdio loop starts (`render.warm`): on Windows a
85
+ first native load from inside the FastMCP loop can hang (see pac-pincers' `_excel.py`).
86
+ - Bounding boxes are measured from pixels (two-pass render), so they include strokes and
87
+ filters and work for text and curves; content further than two page-widths off-canvas isn't found.
88
+ - Version lives only in `pyproject.toml`; a release is a commit named after the version.
@@ -0,0 +1,11 @@
1
+ pac_vector/__init__.py,sha256=dcq66GafZ0X2Fk2h6bI7gjBtzlGpzN9xjCT0yhmVMOU,90
2
+ pac_vector/__main__.py,sha256=xivJhtEKPNXdY9Ihl8AvE8h4RT7ZrTdx6Y2NPrGu7O8,564
3
+ pac_vector/_xml.py,sha256=7yj-ni6Eq3LSrKzbfZWsjblYYahd3A39uqBdFT9LiNg,11253
4
+ pac_vector/document.py,sha256=8Zg6ND1I6IwtRfptYW5ukhuPJ54mfBT4Hovb1GGdD4o,57615
5
+ pac_vector/preview.py,sha256=kxjsf9i_nnqY0fXVJuW7Zbt2pUTE939z0N4jaObWID4,12145
6
+ pac_vector/render.py,sha256=WwXPX8a79REyHJQ-Hfush-4SUvmhd6LN-3Q5brVw4Rw,5850
7
+ pac_vector/server.py,sha256=lBNi42PMigL9mOywV0dqrguPnr4_FgLKESYuSuQcee8,16743
8
+ pac_vector-0.1.0.dist-info/METADATA,sha256=GjGhQiC8y-eNlNTc9Q_GAn9babRvlK5eRv-EJak5CHs,3684
9
+ pac_vector-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
10
+ pac_vector-0.1.0.dist-info/entry_points.txt,sha256=uMldFTCt33_kG6GG8hLz7ZXhcxY18s3GfVO3Iok6JgA,56
11
+ pac_vector-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pac-vector = pac_vector.__main__:main