agentcad 0.1.0__tar.gz
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.
- agentcad-0.1.0/PKG-INFO +20 -0
- agentcad-0.1.0/pyproject.toml +35 -0
- agentcad-0.1.0/setup.cfg +4 -0
- agentcad-0.1.0/src/agentcad/__init__.py +0 -0
- agentcad-0.1.0/src/agentcad/cli.py +271 -0
- agentcad-0.1.0/src/agentcad/commands/__init__.py +0 -0
- agentcad-0.1.0/src/agentcad/commands/context.py +36 -0
- agentcad-0.1.0/src/agentcad/commands/daemon_cmd.py +59 -0
- agentcad-0.1.0/src/agentcad/commands/diff.py +141 -0
- agentcad-0.1.0/src/agentcad/commands/docs.py +507 -0
- agentcad-0.1.0/src/agentcad/commands/export_cmd.py +81 -0
- agentcad-0.1.0/src/agentcad/commands/feedback.py +43 -0
- agentcad-0.1.0/src/agentcad/commands/init.py +41 -0
- agentcad-0.1.0/src/agentcad/commands/inspect_cmd.py +122 -0
- agentcad-0.1.0/src/agentcad/commands/render.py +154 -0
- agentcad-0.1.0/src/agentcad/commands/run.py +411 -0
- agentcad-0.1.0/src/agentcad/commands/skill.py +144 -0
- agentcad-0.1.0/src/agentcad/commands/view.py +143 -0
- agentcad-0.1.0/src/agentcad/daemon.py +302 -0
- agentcad-0.1.0/src/agentcad/export.py +149 -0
- agentcad-0.1.0/src/agentcad/helpers.py +741 -0
- agentcad-0.1.0/src/agentcad/manifest.py +27 -0
- agentcad-0.1.0/src/agentcad/mcp/__init__.py +0 -0
- agentcad-0.1.0/src/agentcad/mcp/__main__.py +15 -0
- agentcad-0.1.0/src/agentcad/mcp/server.py +176 -0
- agentcad-0.1.0/src/agentcad/metrics.py +105 -0
- agentcad-0.1.0/src/agentcad/render.py +193 -0
- agentcad-0.1.0/src/agentcad/session_log.py +87 -0
- agentcad-0.1.0/src/agentcad/validate.py +71 -0
- agentcad-0.1.0/src/agentcad.egg-info/PKG-INFO +20 -0
- agentcad-0.1.0/src/agentcad.egg-info/SOURCES.txt +56 -0
- agentcad-0.1.0/src/agentcad.egg-info/dependency_links.txt +1 -0
- agentcad-0.1.0/src/agentcad.egg-info/entry_points.txt +2 -0
- agentcad-0.1.0/src/agentcad.egg-info/requires.txt +5 -0
- agentcad-0.1.0/src/agentcad.egg-info/top_level.txt +1 -0
- agentcad-0.1.0/tests/test_cli.py +44 -0
- agentcad-0.1.0/tests/test_context.py +85 -0
- agentcad-0.1.0/tests/test_daemon.py +587 -0
- agentcad-0.1.0/tests/test_diff.py +291 -0
- agentcad-0.1.0/tests/test_docs.py +384 -0
- agentcad-0.1.0/tests/test_export.py +149 -0
- agentcad-0.1.0/tests/test_export_cmd.py +134 -0
- agentcad-0.1.0/tests/test_feedback.py +93 -0
- agentcad-0.1.0/tests/test_help.py +143 -0
- agentcad-0.1.0/tests/test_helpers.py +669 -0
- agentcad-0.1.0/tests/test_init.py +67 -0
- agentcad-0.1.0/tests/test_inspect.py +97 -0
- agentcad-0.1.0/tests/test_mcp_server.py +123 -0
- agentcad-0.1.0/tests/test_metrics.py +162 -0
- agentcad-0.1.0/tests/test_preamble.py +105 -0
- agentcad-0.1.0/tests/test_render.py +209 -0
- agentcad-0.1.0/tests/test_render_cmd.py +223 -0
- agentcad-0.1.0/tests/test_run.py +980 -0
- agentcad-0.1.0/tests/test_session_log.py +122 -0
- agentcad-0.1.0/tests/test_session_logging_integration.py +64 -0
- agentcad-0.1.0/tests/test_skill.py +81 -0
- agentcad-0.1.0/tests/test_validate.py +166 -0
- agentcad-0.1.0/tests/test_view.py +98 -0
agentcad-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentcad
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI CAD tool for AI agents. Write CadQuery scripts, get STEP files, renders, and metrics.
|
|
5
|
+
Author: James Dillard
|
|
6
|
+
License: BSL-1.1
|
|
7
|
+
Project-URL: Repository, https://github.com/jdilla1277/agentcad
|
|
8
|
+
Keywords: cad,agent,cadquery,3d,geometry,ai,mcp
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Requires-Python: <3.13,>=3.10
|
|
17
|
+
Requires-Dist: click>=8.0
|
|
18
|
+
Requires-Dist: cadquery>=2.0
|
|
19
|
+
Provides-Extra: mcp
|
|
20
|
+
Requires-Dist: mcp>=1.0.0; extra == "mcp"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "agentcad"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "CLI CAD tool for AI agents. Write CadQuery scripts, get STEP files, renders, and metrics."
|
|
9
|
+
requires-python = ">=3.10,<3.13"
|
|
10
|
+
license = {text = "BSL-1.1"}
|
|
11
|
+
authors = [{name = "James Dillard"}]
|
|
12
|
+
keywords = ["cad", "agent", "cadquery", "3d", "geometry", "ai", "mcp"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Environment :: Console",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Topic :: Scientific/Engineering",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
]
|
|
22
|
+
dependencies = ["click>=8.0", "cadquery>=2.0"]
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
mcp = ["mcp>=1.0.0"]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Repository = "https://github.com/jdilla1277/agentcad"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
agentcad = "agentcad.cli:cli"
|
|
33
|
+
|
|
34
|
+
[tool.setuptools.packages.find]
|
|
35
|
+
where = ["src"]
|
agentcad-0.1.0/setup.cfg
ADDED
|
File without changes
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from agentcad.session_log import SessionLogger
|
|
9
|
+
from agentcad.commands.context import context
|
|
10
|
+
from agentcad.commands.daemon_cmd import daemon
|
|
11
|
+
from agentcad.commands.diff import diff
|
|
12
|
+
from agentcad.commands.docs import docs
|
|
13
|
+
from agentcad.commands.export_cmd import export_cmd
|
|
14
|
+
from agentcad.commands.feedback import feedback
|
|
15
|
+
from agentcad.commands.init import init
|
|
16
|
+
from agentcad.commands.inspect_cmd import inspect_cmd
|
|
17
|
+
from agentcad.commands.render import render
|
|
18
|
+
from agentcad.commands.run import run
|
|
19
|
+
from agentcad.commands.skill import skill
|
|
20
|
+
from agentcad.commands.view import view
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
_BRIEFING = """\b
|
|
24
|
+
EXAMPLE SESSION
|
|
25
|
+
$ agentcad init --name myproject
|
|
26
|
+
{"command": "init", "status": "success", "project": "myproject"}
|
|
27
|
+
\b
|
|
28
|
+
Write a script — no imports needed, cq and show_object are pre-injected:
|
|
29
|
+
box = cq.Workplane('XY').box(10, 20, 5)
|
|
30
|
+
show_object(box)
|
|
31
|
+
\b
|
|
32
|
+
$ agentcad run box.py --output first --render iso --preview
|
|
33
|
+
{"command": "run", "status": "success", "version": 1, "label": "first",
|
|
34
|
+
"outputs": {"step": "v1_first/output.step", "script": "v1_first/script.py"},
|
|
35
|
+
"metrics": {"dimensions": {"x": 10.0, "y": 20.0, "z": 5.0},
|
|
36
|
+
"volume": 1000.0, "surface_area": 700.0, "face_count": 6,
|
|
37
|
+
"edge_count": 12, "is_valid": true, ...},
|
|
38
|
+
"preview": "v1_first/preview.png",
|
|
39
|
+
"renders": {"iso": "v1_first/renders/iso.png"}}
|
|
40
|
+
|
|
41
|
+
\b
|
|
42
|
+
Version directory layout:
|
|
43
|
+
v1_first/
|
|
44
|
+
output.step STEP geometry
|
|
45
|
+
script.py copy of the executed script
|
|
46
|
+
meta.json full run metadata
|
|
47
|
+
preview.png 256x256 iso preview (when --preview used)
|
|
48
|
+
renders/ PNG views (when --render used)
|
|
49
|
+
iso.png
|
|
50
|
+
|
|
51
|
+
\b
|
|
52
|
+
WRITING SCRIPTS
|
|
53
|
+
show_object(result) is required — it tells agentcad what geometry to output.
|
|
54
|
+
These names are pre-injected (no import needed):
|
|
55
|
+
cq cadquery module
|
|
56
|
+
show_object surfaces geometry to agentcad (required — at least one call)
|
|
57
|
+
translate translate(shape, x, y, z)
|
|
58
|
+
rotate rotate(shape, axis, angle_deg) — axis: 'X'/'Y'/'Z'
|
|
59
|
+
Right-hand rule: positive = counterclockwise from + axis
|
|
60
|
+
mirror_fuse mirror_fuse(shape, plane='XZ') — mirror + boolean fuse
|
|
61
|
+
loft_sections loft_sections(sections, smooth=True) — loft wires into solid
|
|
62
|
+
tapered_sweep tapered_sweep(spine, radii) — circles along spine
|
|
63
|
+
naca_wire naca_wire(y, le_x, te_x, thickness, profile='0012')
|
|
64
|
+
|
|
65
|
+
\b
|
|
66
|
+
.val().wrapped — extracting a TopoDS_Shape from CadQuery's fluent API:
|
|
67
|
+
CadQuery methods return Workplane objects. The helpers (translate, rotate,
|
|
68
|
+
etc.) operate on raw TopoDS_Shape. To bridge them:
|
|
69
|
+
part = cq.Workplane('XY').box(10, 20, 5).val().wrapped # -> TopoDS_Shape
|
|
70
|
+
moved = translate(part, 50, 0, 0) # -> TopoDS_Shape
|
|
71
|
+
|
|
72
|
+
\b
|
|
73
|
+
Showing helper output:
|
|
74
|
+
Helpers return TopoDS_Shape. To pass back to show_object:
|
|
75
|
+
shape = cq.Shape.cast(topo_shape)
|
|
76
|
+
show_object(cq.Workplane('XY').newObject([shape]))
|
|
77
|
+
Or use multiple show_object() calls — agentcad auto-compounds them.
|
|
78
|
+
|
|
79
|
+
\b
|
|
80
|
+
Explicit imports still work (adding 'import cadquery as cq' is harmless).
|
|
81
|
+
For OCP internals (e.g. OCP.gp, OCP.BRepPrimAPI), import manually.
|
|
82
|
+
|
|
83
|
+
\b
|
|
84
|
+
COMMANDS
|
|
85
|
+
agentcad init [--name NAME]
|
|
86
|
+
Initialize project. Creates agentcad.json manifest.
|
|
87
|
+
|
|
88
|
+
\b
|
|
89
|
+
agentcad run SCRIPT --output LABEL [flags]
|
|
90
|
+
Execute script, produce versioned STEP + metrics.
|
|
91
|
+
--render VIEWS PNG views: front,back,left,right,top,bottom,iso,
|
|
92
|
+
'all', custom angle az:el (e.g. 45:30),
|
|
93
|
+
or mixed (front,right,45:30).
|
|
94
|
+
--export FMT Mesh export: stl, glb (GLB auto-colors per-solid).
|
|
95
|
+
--preview Quick 256x256 iso PNG.
|
|
96
|
+
--params K=V,.. Override top-level script constants.
|
|
97
|
+
--dry-run Metrics only — no version consumed, no disk artifacts.
|
|
98
|
+
|
|
99
|
+
\b
|
|
100
|
+
agentcad render STEP --view SPEC [--zoom N] [--focus x,y,z] [--no-fit] [--name LABEL]
|
|
101
|
+
Render PNG views of an existing STEP file. Same view spec as --render.
|
|
102
|
+
Use this for post-hoc rendering with camera control (zoom, focus).
|
|
103
|
+
|
|
104
|
+
\b
|
|
105
|
+
agentcad export STEP --format stl,glb,obj
|
|
106
|
+
Export STEP to mesh formats. GLB auto-colors individual solids.
|
|
107
|
+
|
|
108
|
+
\b
|
|
109
|
+
agentcad inspect STEP
|
|
110
|
+
Topology report: solid_count, shell_count, shells (open/closed + face
|
|
111
|
+
count per shell), face_count, face_orientations (forward/reversed),
|
|
112
|
+
edge_count, free_edge_count, is_valid.
|
|
113
|
+
|
|
114
|
+
\b
|
|
115
|
+
agentcad diff REF1 REF2 Compare versions (by number or label).
|
|
116
|
+
agentcad context Project state: versions, current, tool_version.
|
|
117
|
+
agentcad view FILE Open GLB/STEP in browser (three.js). STEP auto-converts.
|
|
118
|
+
agentcad daemon start|stop|status Background worker — eliminates 3-5s cold start.
|
|
119
|
+
agentcad docs [SECTION] Deep-dive docs (15 sections).
|
|
120
|
+
|
|
121
|
+
\b
|
|
122
|
+
RESPONSE SCHEMA
|
|
123
|
+
Every command returns JSON with "command" and "status" keys.
|
|
124
|
+
"success" — completed normally.
|
|
125
|
+
"failed" — script error. Version IS consumed. Creates v{N}_{label}_failed/.
|
|
126
|
+
"error" — CLI error (bad args, missing file). No version consumed. No disk artifacts.
|
|
127
|
+
"validation_error" — static check failed (syntax, missing show_object, bad import).
|
|
128
|
+
No version consumed. No disk artifacts. Instant (<100ms).
|
|
129
|
+
|
|
130
|
+
\b
|
|
131
|
+
METRICS (in every successful run response)
|
|
132
|
+
bounding_box {x: [min,max], y: [min,max], z: [min,max]}
|
|
133
|
+
dimensions {x, y, z} bbox extents
|
|
134
|
+
volume float unit-agnostic (CadQuery defaults mm -> mm^3)
|
|
135
|
+
surface_area float
|
|
136
|
+
center_of_mass {x, y, z}
|
|
137
|
+
face_count int unique faces
|
|
138
|
+
edge_count int unique edges
|
|
139
|
+
is_valid bool BRepCheck shape validity
|
|
140
|
+
Tip: verify geometry from metrics alone — check volume, dimensions, face_count
|
|
141
|
+
before rendering. Use 'agentcad diff' to compare metrics across versions.
|
|
142
|
+
|
|
143
|
+
\b
|
|
144
|
+
PARAMETRIC SCRIPTS
|
|
145
|
+
Top-level assignments become overridable parameters:
|
|
146
|
+
length = 50.0
|
|
147
|
+
width = 20.0
|
|
148
|
+
result = cq.Workplane('XY').box(length, width, 10)
|
|
149
|
+
show_object(result)
|
|
150
|
+
$ agentcad run script.py --output v2 --params length=100,width=30
|
|
151
|
+
Types auto-coerced: bool ('true'/'false') > int > float > string.
|
|
152
|
+
Unknown param name -> "error" status with available names (no version consumed).
|
|
153
|
+
|
|
154
|
+
\b
|
|
155
|
+
CADQUERY PATTERNS
|
|
156
|
+
Build at origin, then position:
|
|
157
|
+
part = cq.Workplane('XY').box(10, 20, 5).val().wrapped
|
|
158
|
+
placed = translate(part, 50, 0, 0)
|
|
159
|
+
\b
|
|
160
|
+
Angled positioning (build along Z, rotate, translate to attachment):
|
|
161
|
+
arm = cq.Workplane('XY').circle(5).extrude(120).val().wrapped
|
|
162
|
+
tilted = rotate(arm, 'Y', 30)
|
|
163
|
+
placed = translate(tilted, 0, 0, 8)
|
|
164
|
+
\b
|
|
165
|
+
Compound vs Union:
|
|
166
|
+
cq.Compound.makeCompound([...]) spatial grouping, parts stay separate
|
|
167
|
+
.union() boolean fuse into single solid
|
|
168
|
+
\b
|
|
169
|
+
Revolve: axis is relative to workplane origin, not sketch pen position.
|
|
170
|
+
.move() shifts pen without moving origin — trap. Use .transformed(offset=(...)).
|
|
171
|
+
\b
|
|
172
|
+
Workplane stacking:
|
|
173
|
+
.transformed(offset=(x,y,z)) shift origin in local coords
|
|
174
|
+
.workplane(offset=N) stack along normal
|
|
175
|
+
.center(x, y) move pen relative to origin
|
|
176
|
+
.move(x, y) move pen only (origin stays)
|
|
177
|
+
\b
|
|
178
|
+
twistExtrude performance:
|
|
179
|
+
For profiles with >100 points, use polyline() instead of spline().
|
|
180
|
+
1000-point spline + twistExtrude can take 5+ minutes; polyline < 1 min.
|
|
181
|
+
|
|
182
|
+
\b
|
|
183
|
+
DEBUGGING
|
|
184
|
+
Geometry wrong? Check metrics first — volume and dimensions catch most issues.
|
|
185
|
+
$ agentcad run script.py --output test --dry-run # metrics, no disk artifacts
|
|
186
|
+
$ agentcad inspect v1_test/output.step # topology deep-dive
|
|
187
|
+
Hollow shape? -> free_edge_count > 0, shell not closed
|
|
188
|
+
Inverted normals? -> face_orientations imbalanced
|
|
189
|
+
Invalid? -> is_valid: false
|
|
190
|
+
$ agentcad render v1_test/output.step --view all # visual from 4 angles
|
|
191
|
+
Then iterate: fix script, run with new --output label.
|
|
192
|
+
|
|
193
|
+
\b
|
|
194
|
+
MCP INTEGRATION
|
|
195
|
+
For native tool integration with Claude Code, Cursor, Windsurf, or any
|
|
196
|
+
MCP-compatible agent, install the MCP extra and add to your .mcp.json:
|
|
197
|
+
|
|
198
|
+
pip install agentcad[mcp]
|
|
199
|
+
|
|
200
|
+
.mcp.json:
|
|
201
|
+
{"agentcad": {"command": "python", "args": ["-m", "agentcad.mcp"]}}
|
|
202
|
+
|
|
203
|
+
This exposes all agentcad commands as native agent tools — no CLI
|
|
204
|
+
parsing needed. The agent calls run(), inspect(), docs() etc. directly.
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class _LoggingGroup(click.Group):
|
|
209
|
+
"""Click Group that auto-logs every command invocation to session.jsonl."""
|
|
210
|
+
|
|
211
|
+
def invoke(self, ctx):
|
|
212
|
+
captured = []
|
|
213
|
+
original_echo = click.echo
|
|
214
|
+
|
|
215
|
+
def _capturing_echo(message=None, **kwargs):
|
|
216
|
+
if message is not None:
|
|
217
|
+
captured.append(str(message))
|
|
218
|
+
original_echo(message, **kwargs)
|
|
219
|
+
|
|
220
|
+
click.echo = _capturing_echo
|
|
221
|
+
try:
|
|
222
|
+
return super().invoke(ctx)
|
|
223
|
+
finally:
|
|
224
|
+
click.echo = original_echo
|
|
225
|
+
self._log_session(ctx, captured)
|
|
226
|
+
|
|
227
|
+
def _log_session(self, ctx, captured):
|
|
228
|
+
if os.environ.get("AGENTCAD_NO_LOG"):
|
|
229
|
+
return
|
|
230
|
+
# Find the subcommand name and args
|
|
231
|
+
cmd_name = ctx.invoked_subcommand
|
|
232
|
+
if not cmd_name or cmd_name == "feedback":
|
|
233
|
+
return
|
|
234
|
+
# Parse the last JSON output (commands may echo multiple things)
|
|
235
|
+
result = {}
|
|
236
|
+
for line in reversed(captured):
|
|
237
|
+
try:
|
|
238
|
+
result = json.loads(line)
|
|
239
|
+
break
|
|
240
|
+
except (json.JSONDecodeError, TypeError):
|
|
241
|
+
continue
|
|
242
|
+
# Collect the raw args from sys.argv
|
|
243
|
+
args = sys.argv[2:] if len(sys.argv) > 2 else []
|
|
244
|
+
try:
|
|
245
|
+
logger = SessionLogger(Path.cwd())
|
|
246
|
+
logger.log(cmd_name, {"argv": args}, result)
|
|
247
|
+
except Exception:
|
|
248
|
+
pass # Never let logging break the CLI
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@click.group(
|
|
252
|
+
cls=_LoggingGroup,
|
|
253
|
+
epilog=_BRIEFING,
|
|
254
|
+
context_settings=dict(max_content_width=120),
|
|
255
|
+
)
|
|
256
|
+
def cli():
|
|
257
|
+
"""agentcad — CLI CAD tool for AI agents. All output is JSON."""
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
cli.add_command(context)
|
|
261
|
+
cli.add_command(daemon)
|
|
262
|
+
cli.add_command(diff)
|
|
263
|
+
cli.add_command(docs)
|
|
264
|
+
cli.add_command(export_cmd)
|
|
265
|
+
cli.add_command(feedback)
|
|
266
|
+
cli.add_command(init)
|
|
267
|
+
cli.add_command(inspect_cmd)
|
|
268
|
+
cli.add_command(render)
|
|
269
|
+
cli.add_command(run)
|
|
270
|
+
cli.add_command(skill)
|
|
271
|
+
cli.add_command(view)
|
|
File without changes
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from agentcad.manifest import load_manifest
|
|
6
|
+
|
|
7
|
+
TOOL_VERSION = "0.1.0"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.command()
|
|
11
|
+
def context():
|
|
12
|
+
"""Show the current project context."""
|
|
13
|
+
manifest = load_manifest(command="context")
|
|
14
|
+
|
|
15
|
+
versions = manifest.get("versions", [])
|
|
16
|
+
current = manifest.get("current", None)
|
|
17
|
+
|
|
18
|
+
versions_summary = [
|
|
19
|
+
{
|
|
20
|
+
"version": v["version"],
|
|
21
|
+
"label": v["label"],
|
|
22
|
+
"status": v["status"],
|
|
23
|
+
"path": v["path"],
|
|
24
|
+
}
|
|
25
|
+
for v in versions
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
click.echo(json.dumps({
|
|
29
|
+
"command": "context",
|
|
30
|
+
"status": "success",
|
|
31
|
+
"project": manifest["name"],
|
|
32
|
+
"tool_version": TOOL_VERSION,
|
|
33
|
+
"current": current,
|
|
34
|
+
"version_count": len(versions),
|
|
35
|
+
"versions": versions_summary,
|
|
36
|
+
}))
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from agentcad.daemon import (
|
|
6
|
+
_default_pid_path,
|
|
7
|
+
_default_socket_path,
|
|
8
|
+
daemon_status,
|
|
9
|
+
start_daemon,
|
|
10
|
+
stop_daemon,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _socket_path():
|
|
15
|
+
return _default_socket_path()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _pid_path():
|
|
19
|
+
return _default_pid_path()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@click.group()
|
|
23
|
+
def daemon():
|
|
24
|
+
"""Manage the agentcad background daemon."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@daemon.command()
|
|
28
|
+
def start():
|
|
29
|
+
"""Start the daemon worker."""
|
|
30
|
+
result = start_daemon(
|
|
31
|
+
socket_path=_socket_path(),
|
|
32
|
+
pid_path=_pid_path(),
|
|
33
|
+
)
|
|
34
|
+
ok = result.get("started", False) or "already running" in result.get("message", "").lower()
|
|
35
|
+
output = {"command": "daemon", "status": "success" if ok else "error", **result}
|
|
36
|
+
click.echo(json.dumps(output))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@daemon.command()
|
|
40
|
+
def stop():
|
|
41
|
+
"""Stop the daemon worker."""
|
|
42
|
+
result = stop_daemon(
|
|
43
|
+
socket_path=_socket_path(),
|
|
44
|
+
pid_path=_pid_path(),
|
|
45
|
+
)
|
|
46
|
+
ok = result.get("stopped", False)
|
|
47
|
+
output = {"command": "daemon", "status": "success" if ok else "error", **result}
|
|
48
|
+
click.echo(json.dumps(output))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@daemon.command()
|
|
52
|
+
def status():
|
|
53
|
+
"""Check daemon status."""
|
|
54
|
+
result = daemon_status(
|
|
55
|
+
socket_path=_socket_path(),
|
|
56
|
+
pid_path=_pid_path(),
|
|
57
|
+
)
|
|
58
|
+
output = {"command": "daemon", "status": "success", **result}
|
|
59
|
+
click.echo(json.dumps(output))
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from agentcad.manifest import load_manifest
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _resolve_version(manifest, ref):
|
|
11
|
+
"""Resolve a version reference (number or label) to a manifest version entry."""
|
|
12
|
+
versions = manifest.get("versions", [])
|
|
13
|
+
|
|
14
|
+
# Try as version number first
|
|
15
|
+
try:
|
|
16
|
+
num = int(ref)
|
|
17
|
+
for v in versions:
|
|
18
|
+
if v["version"] == num:
|
|
19
|
+
return v
|
|
20
|
+
except ValueError:
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
# Try as label
|
|
24
|
+
for v in versions:
|
|
25
|
+
if v["label"] == ref:
|
|
26
|
+
return v
|
|
27
|
+
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _load_version_meta(version_entry):
|
|
32
|
+
"""Load meta.json for a version entry."""
|
|
33
|
+
path = Path.cwd() / version_entry["path"] / "meta.json"
|
|
34
|
+
return json.loads(path.read_text())
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _compute_set_diff(old_keys, new_keys):
|
|
38
|
+
"""Compute added/removed/unchanged between two sets of keys."""
|
|
39
|
+
old_set = set(old_keys)
|
|
40
|
+
new_set = set(new_keys)
|
|
41
|
+
return {
|
|
42
|
+
"added": sorted(new_set - old_set),
|
|
43
|
+
"removed": sorted(old_set - new_set),
|
|
44
|
+
"unchanged": sorted(old_set & new_set),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _scalar_diff(old_val, new_val):
|
|
49
|
+
"""Return None if same, {from, to} if different."""
|
|
50
|
+
if old_val == new_val:
|
|
51
|
+
return None
|
|
52
|
+
return {"from": old_val, "to": new_val}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@click.command()
|
|
56
|
+
@click.argument("ref1")
|
|
57
|
+
@click.argument("ref2")
|
|
58
|
+
def diff(ref1, ref2):
|
|
59
|
+
"""Compare two versions of a model."""
|
|
60
|
+
manifest = load_manifest(command="diff")
|
|
61
|
+
|
|
62
|
+
v1_entry = _resolve_version(manifest, ref1)
|
|
63
|
+
if v1_entry is None:
|
|
64
|
+
click.echo(json.dumps({
|
|
65
|
+
"command": "diff",
|
|
66
|
+
"status": "error",
|
|
67
|
+
"message": f"Version '{ref1}' not found",
|
|
68
|
+
}))
|
|
69
|
+
sys.exit(1)
|
|
70
|
+
|
|
71
|
+
v2_entry = _resolve_version(manifest, ref2)
|
|
72
|
+
if v2_entry is None:
|
|
73
|
+
click.echo(json.dumps({
|
|
74
|
+
"command": "diff",
|
|
75
|
+
"status": "error",
|
|
76
|
+
"message": f"Version '{ref2}' not found",
|
|
77
|
+
}))
|
|
78
|
+
sys.exit(1)
|
|
79
|
+
|
|
80
|
+
meta1 = _load_version_meta(v1_entry)
|
|
81
|
+
meta2 = _load_version_meta(v2_entry)
|
|
82
|
+
|
|
83
|
+
# Compute changes
|
|
84
|
+
changes = {
|
|
85
|
+
"label": _scalar_diff(meta1.get("label"), meta2.get("label")),
|
|
86
|
+
"status": _scalar_diff(meta1.get("status"), meta2.get("status")),
|
|
87
|
+
"outputs": _compute_set_diff(
|
|
88
|
+
meta1.get("outputs", {}).keys(),
|
|
89
|
+
meta2.get("outputs", {}).keys(),
|
|
90
|
+
),
|
|
91
|
+
"renders": _compute_set_diff(
|
|
92
|
+
meta1.get("renders", {}).keys(),
|
|
93
|
+
meta2.get("renders", {}).keys(),
|
|
94
|
+
),
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# Compare metrics if present in either version
|
|
98
|
+
m1 = meta1.get("metrics", {})
|
|
99
|
+
m2 = meta2.get("metrics", {})
|
|
100
|
+
if m1 or m2:
|
|
101
|
+
all_keys = sorted(set(m1.keys()) | set(m2.keys()))
|
|
102
|
+
changes["metrics"] = {
|
|
103
|
+
k: _scalar_diff(m1.get(k), m2.get(k)) for k in all_keys
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# Compare params if present in either version
|
|
107
|
+
p1 = meta1.get("params", {})
|
|
108
|
+
p2 = meta2.get("params", {})
|
|
109
|
+
if p1 or p2:
|
|
110
|
+
all_param_keys = sorted(set(p1.keys()) | set(p2.keys()))
|
|
111
|
+
changes["params"] = {
|
|
112
|
+
k: _scalar_diff(p1.get(k), p2.get(k)) for k in all_param_keys
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
# Compare parts if present in either version
|
|
116
|
+
parts1 = {p["name"]: p for p in meta1.get("parts", [])}
|
|
117
|
+
parts2 = {p["name"]: p for p in meta2.get("parts", [])}
|
|
118
|
+
if parts1 or parts2:
|
|
119
|
+
parts_changes = {
|
|
120
|
+
"names": _compute_set_diff(parts1.keys(), parts2.keys()),
|
|
121
|
+
}
|
|
122
|
+
shared = sorted(set(parts1.keys()) & set(parts2.keys()))
|
|
123
|
+
for name in shared:
|
|
124
|
+
p1_part = parts1[name]
|
|
125
|
+
p2_part = parts2[name]
|
|
126
|
+
part_diff = {"color": _scalar_diff(p1_part.get("color"), p2_part.get("color"))}
|
|
127
|
+
m1_metrics = p1_part.get("metrics", {})
|
|
128
|
+
m2_metrics = p2_part.get("metrics", {})
|
|
129
|
+
all_mkeys = sorted(set(m1_metrics.keys()) | set(m2_metrics.keys()))
|
|
130
|
+
for k in all_mkeys:
|
|
131
|
+
part_diff[k] = _scalar_diff(m1_metrics.get(k), m2_metrics.get(k))
|
|
132
|
+
parts_changes[name] = part_diff
|
|
133
|
+
changes["parts"] = parts_changes
|
|
134
|
+
|
|
135
|
+
click.echo(json.dumps({
|
|
136
|
+
"command": "diff",
|
|
137
|
+
"status": "success",
|
|
138
|
+
"v1": {"version": meta1["version"], "label": meta1["label"]},
|
|
139
|
+
"v2": {"version": meta2["version"], "label": meta2["label"]},
|
|
140
|
+
"changes": changes,
|
|
141
|
+
}))
|