This is the text an assistant receives from the get_help tool.
cnc.xtract.bot — working with a user's CNC files
You are connected on behalf of one signed-in user. Everything you do happens in their account: their files, their published designs, their open workbench tab. Absolute links for the user look like https://cnc.xtract.bot/app/p/{fileId} and https://cnc.xtract.bot/d/{publicationId}; tools that return a url field give you the right link — pass it on verbatim.
1. Concepts
Files live in folders (a flat list where each file/folder has a folderId/parentId, null = root). Every file has a kind:
gcode— a CNC program (text). The workbench simulates it and renders the material removed in 3D.svg— a 2D drawing (JSON document, §5). Filled shapes become pockets, profiles or engravings.model— a 3D part built from sketches and extrusions (JSON document, §6). Machined top-down (2.5D).
Every file also carries settings that place the program on a machine:
| setting | meaning |
|---|---|
machine {x,y,z} | bed size, mm |
stock {x,y,w,d,h} | the block of material: (x,y) = position of its minimum-X, minimum-Y ("front-left") corner on the bed; w along X, d along Y, h = thickness |
origin {x,y} | distance from the stock's front-left corner to program X0 Y0 — so that corner is at program X = −origin.x, Y = −origin.y |
zZero "top" | "bed" | where program Z0 is: the stock top (typical) or the bed |
toolDiameter | flat end mill diameter, mm |
resolution | simulation grid cell, mm (0.1–5; smaller = more accurate, slower) |
preciseEdges | render walls where the cutter really passed (sub-cell); costs memory/time |
When a tool takes settings it is a deep-merged partial: {stock: {w: 80}} keeps x, y, d, h at their current/default values. Defaults for new files: 600×600×200 machine, 120×120×30 stock at (0,0), origin (0,0), Ø6 tool, 0.25 mm, Z0 at the stock top.
Coordinates. Program coordinates are work coordinates in millimetres: X0 Y0 at the work origin, Z0 at the stock top when zZero is "top". Negative Z cuts into the material. With origin (0,0) the stock spans X 0…w, Y 0…d, Z −h…0. Every file response carries a workspace sentence stating exactly this for that file. highlight_region, locate_moves and get_live_view all use work coordinates.
Moves. A move is one motion segment: every G0/G1 line (including a modal line that has coordinates but no G word) and every G2/G3 arc is exactly one move; comment lines, F-only lines and G21/G90/G17/M/S/T lines produce none. Move indexes are 1-based and match the workbench's playhead: "move N / total" shows the material right after move N has been applied (0 = untouched stock). Lines are 1-based too. locate_moves converts lines ↔ move indexes ↔ geometry, and every collision report carries both moveIndex and line.
G-code supported: G0/G1, G2/G3 arcs with I/J or R (XY plane; helical and full circles allowed), G17, G20/G21 units, G90/G91, G28, modal motion, F feed rates, comments with ; or ( ). Accepted silently (no warning, no effect on geometry): M words (M3/M4/M5/M8/M9/M2/M30…), S, T, N and O words — include the spindle/coolant words a real machine needs. Anything else (other G codes, unknown letters) is ignored with a parser warning; warnings are informational, not errors. A flat end mill is assumed. The simulation starts with the tool at X0 Y0, 10 mm above the stock top (whatever zZero is); the first move is a normal move from there, so begin programs with a rapid to a safe height (e.g. G0 Z5 with zZero top). Generated programs contain no spindle command; if the user gives no rpm, add M3 S<value> with a stated assumption.
Through-cuts. To cut through, go to Z = −stock.h (zZero top; Z 0 with zZero bed). The analysis then lists "cut reaches the machine bed / stock bottom" — for an intentional through-cut relay it as expected and remind the user to use a spoilboard. There is no holding-tab support: a part cut free is loose on the last pass.
Quota. Each account may store 50 MB (file bodies + saved simulation states). list_files includes storage {usedBytes, quotaBytes}; get_storage_usage returns just that. Writes that would exceed it fail with a clear message.
2. Reading and writing files
list_files→ folders, files (id, kind, name, folderId, bytes, updatedAt, url) and storage. Start here;qfilters by name.get_open_files→ which files have a workbench tab connected right now. Use it when the user says "the file I have open" without naming it.get_file→ settings,workspace,lineCountand content. For long programs passlines: {from, to}(1-based inclusive) to get a numbered window ("118: G0 X80 Y60").create_file→ new file.kind,name,content(G-code text, or the JSON document for svg/model), optional partialsettings, optionalfolderId.update_file→ replace the wholecontent, or apply linepatches; and/or changesettingsandname. Patch semantics:{fromLine, toLine, replacement}— 1-based inclusive against the file as it was (patches are applied bottom-up, so several patches — even adjacent ones — can use original numbers);replacementis split on newlines into whole lines, no trailing newline;toLine = fromLine − 1inserts beforefromLine;replacement: ""deletes the range. Prefer patches over whole-file rewrites for long programs. For svg/model files send the whole JSON document ascontent(or useset_svg_opsto change machining ops without re-sending the drawing). The previous content is kept for 24 h →revert_fileundoes the last content change (one level; settings/name untouched). That last change may be the user's own workbench save, so passexpectedUpdatedAt(thefile.updatedAtyourupdate_filereturned) — the revert is refused if the file changed since, and you apply an inverse patch instead. If the user has the file open, their tab reloads and re-simulates automatically (a tab with unsaved local edits is asked first); pass a one-sentencenotedescribing the change — shown as a toast if a tab is open, dropped otherwise.move_file(folder and/or name),delete_file(requiresconfirm: true),create_folder,rename_folder,delete_folder(must be empty).
Analysis. create_file, update_file, revert_file and both generators already return the full analysis (parse + simulation) of the saved program — relay its issues verbatim; you do not need a second call. Use analyze_gcode for raw text (a dry run before saving, with optional settings) or to re-check a file later. The simulation uses a coarser grid than the workbench when needed (simulation.resolution), so volumes are approximate to a few percent.
3. The live view (collaborating in real time)
When the user has a file open in the workbench, its tab is connected to a live session:
get_live_viewtells you whether a tab is connected (connected,connections), the camera (viewport), the regions the user marked (userHighlightswithuserNote, work coordinates), your own boxes (agentHighlights), theplayhead(moveIndex/total/playing), the listing lines they clicked (selectionwith the text) and the current measurements (stats: pieces, largestPiece, volumes, rapidCollisions/bedCollisions with moveIndex + line). Use it whenever the user says "this", "here", "the spot I marked".locate_moveswithbox: {min: h.min, max: h.max}(a highlight can be passed as-is; extra fields are ignored) finds the moves passing through what they marked; withdeepest: 3the deepest cuts (ties in program order — usezAtOrBelowto get the whole floor); withlines: [118]the move a line produces. Filters intersect. To reason about the state they are looking at, keep matches withmoveIndex ≤ playhead.moveIndex.highlight_regiondraws labelled translucent boxes in their 3D view (work coordinates;focusCamera: trueflies the camera there). Each call replaces your previous boxes for that file (≤ 20). A match'sboxfromlocate_movescan be passed directly (thin boxes are drawn at least 0.5 mm thick).clear: trueremoves your boxes.set_playheadmoves their simulation to a move index so they see the state right after that move (playback pauses).send_noteshows a one-sentence toast in their workbench. Useupdate_file's ownnotefor edits andsend_noteonly for messages that are not edits.
If connected is false the user has no tab open; edits still save and highlights are shown when they open the file.
4. Generating G-code from drawings and models
generate_gcode_from_svgmachines every shape of an svg file that has anop(or all shapes withdefaultOp) and saves a gcode file (new, or updateoutputFileId) with the drawing's settings andsourceFileIdset. Ops:pocket(clear the filled area),profile-outside/profile-inside(outline offset by the tool radius),engrave(tool centre on the outline). Each op:depth(positive mm below the stock top),stepdown,feed,plunge(mm/min) and, for pockets,stepover(fraction of the tool diameter). Coordinates: program X = drawing x; program Y = documentheight− drawing y (the document box, not the shapes' bounds), so the document's bottom-left corner is program X0 Y0. Shapes are machined in document order; shapes narrower than the tool produce a warning. Defaults: feed 800, plunge 250, stepdown 2, stepover 0.45, safeZ 5. Pockets are cleared with concentric offset rings, innermost first, the wall pass last. Generated programs startG21 G90 G17/G0 Z<safeZ>and endG0 Z<safeZ>/G0 X0 Y0/M2, with no spindle command.generate_gcode_from_model→ waterline roughing of a model file: for each distinct top height, highest first, pocket everything (within the stock footprint) whose final surface is at or below it. Frame: model X/Y are program X/Y (the part is not re-centred); model Z0 is the part's bottom, sitting on the bed;stockTop(default stock.h) is the stock top in model Z, so anything above the part's top is faced off first.clearOutsideTo: omitted or null → the stock around the part is left untouched (safe default); a model Z (e.g. 0 = the bed) → the surrounding stock is cleared down to it and, at 0, the part is cut free on the last pass (no holding tabs — warn the user). The result inherits the model file's settings andsourceFileId. Size the stock in the file's settings before generating (a 120×120×30 default around a 100×60×12 part means a lot of facing and clearing). Waterline roughing leaves the exact 2.5D surface; there is no finishing allowance parameter.- Both accept
save: falsefor a dry run that returns the program text and its analysis without writing anything. import_svg_markupcreates an svg file from raw<svg>markup. Scale: width/height with units + viewBox; with no width, 1 user unit = 1 mm. stock.w/d default to the drawing size and stock.h stays 30 mm; a partialsettings.stock(e.g.{h: 10}) keeps the drawing-derived w/d. Shapes import at the top level;<g>becomes a group. Then assign ops withset_svg_ops{ops: [{objectId, op}]}.
5. The SVG document (kind "svg")
{ "version": 1, "units": "mm", "width": 120, "height": 120,
"objects": [
{ "id": "a1", "type": "group", "name": "Layer 1", "visible": true, "locked": false, "transform": [1,0,0,1,0,0],
"children": [
{ "id": "b2", "type": "rect", "name": "Pocket", "visible": true, "locked": false, "fill": "#3b82f6", "stroke": null,
"opacity": 1, "fillRule": "nonzero", "transform": [1,0,0,1,0,0],
"rect": { "x": 20, "y": 20, "w": 60, "h": 40, "rx": 4 },
"op": { "type": "pocket", "depth": 3, "stepdown": 1.5, "feed": 800, "plunge": 250, "stepover": 0.45 } },
{ "id": "c3", "type": "ellipse", "name": "Hole", "ellipse": { "cx": 100, "cy": 100, "rx": 5, "ry": 5 }, "op": { "type": "profile-inside", "depth": 30, "stepdown": 5, "feed": 600, "plunge": 200 } },
{ "id": "d4", "type": "path", "name": "Logo", "d": "M10 10 h20 v20 h-20 z", "fillRule": "evenodd" },
{ "id": "e5", "type": "polygon", "name": "Tri", "points": [0,0, 30,0, 15,25] }
] } ] }
Shapes may sit directly in objects or inside groups. Groups are layers and sub-objects: hidden groups are skipped, a group's op applies to children without their own, transforms nest. Circles are ellipses with rx = ry. SVG rects are corner-based (x, y with y downwards) — unlike model rects (§6). fill is cosmetic (machining follows op). width/height are required; version/units default to 1/"mm"; any object may omit fields (defaults: visible true, locked false, fill #3b82f6, opacity 1, fillRule nonzero, identity transform, op null). As content the document is sent serialised as a JSON string.
6. The model document (kind "model")
{ "version": 1, "units": "mm", "features": [
{ "id": "sk1", "type": "sketch", "name": "Base", "plane": "XY", "offsetZ": 0,
"profiles": [ { "id": "p1", "type": "rect", "cx": 50, "cy": 50, "w": 100, "h": 100 } ] },
{ "id": "ex1", "type": "extrude", "name": "Plate", "sketchId": "sk1", "profileIds": "all", "distance": 20, "direction": "up", "op": "join" },
{ "id": "sk2", "type": "sketch", "name": "Top", "plane": "XY", "offsetZ": 20,
"profiles": [ { "id": "p2", "type": "circle", "cx": 15, "cy": 15, "r": 4 }, { "id": "p3", "type": "polygon", "points": [40,40, 60,40, 50,60] } ] },
{ "id": "ex2", "type": "extrude", "name": "Holes", "sketchId": "sk2", "profileIds": ["p2"], "distance": 20, "direction": "down", "op": "cut" }
] }
The timeline is evaluated in order: a sketch defines profiles on an XY plane at offsetZ (model Z, bottom of the part = 0); an extrude turns chosen profiles into a prism of distance going up or down from the sketch plane that either joins (adds material) or cuts (removes it). Rect profiles are centre-based (cx, cy, w, h, optional rotation in degrees; unlike svg rects), circles are cx, cy, r, polygons list points. Model X/Y are program X/Y with Y pointing the same way as program Y. The kernel is 2.5D: what a 3-axis machine can cut from above.
7. The public gallery
explore_public_designs(sort: top = most upvoted all-time | new | imported | discussed; kind; q; page) andget_public_design(withincludeContentfor the body) browse published designs; items carry an absoluteurl. There is no time-windowed sort: for "popular right now" showtopanddiscussedand say they are all-time.vote_designtoggles the user's upvote.import_public_designcopies a design into the user's files.publish_fileshares one of theirs: when the user has asked to publish, do it directly with a sensible title/description and tell them both are editable (update_publication); propose first only when they did not explicitly ask.list_my_publications;set_publication_status(published | unlisted — unlisted keeps the link working but leaves the gallery);delete_publicationremoves it for good (their file is untouched).list_comments/post_comment— comments are visible to signed-in users only.
Moderators (accounts in ADMIN_EMAILS) see five extra tools in tools/list: admin_auto_moderate (read-only briefing: the pending comments — id, author, design, flag reason, body — plus the moderation policy; nothing changes until you act), admin_moderate_comments (apply decisions: [{commentId, action, note}] — hide is reversible, delete is permanent, restore makes a hidden comment visible, clear_flag dismisses the report and keeps it visible), admin_moderation_queue (the raw list without the policy), admin_set_publication_status (published | unlisted | removed with a reason) and admin_ban_user {userId, banned} (reversible with banned false). Present decisions to the moderator before applying unless they told you to act autonomously; never ban without an explicit instruction. If those tools are not in your list, this user is not a moderator — say so instead of guessing.
8. Response shapes (the fields you will read)
- file summary (in
create_file,update_file,move_file,get_file, generators):{ id, kind, name, folderId, bytes, lineCount, sourceFileId, updatedAt, settings {machine, stock, origin, toolDiameter, resolution, preciseEdges, zZero}, workspace, url }. - analysis:
{ lineCount, moveCount, rapidCount, cutCount, arcCount, rapidTravel, cutTravel (mm), bbox {minX,minY,minZ,maxX,maxY,maxZ} | null, feeds [mm/min], estimatedSeconds, minZ, warnings [strings], simulation { resolution, cells, removedVolumeCm3, remainingVolumeCm3, pieces, largestPiece {w,d,h,x,y} | null, rapidCollisions [{moveIndex, line}], bedCollisions [{moveIndex, line}], ms } | null, issues [strings] }. - locate_moves:
{ total, truncated, matches: [{ moveIndex, line, code, rapid, feed, from [x,y,z], to [x,y,z], minZ, box {min [x,y,z], max [x,y,z]}, bbox {minX…maxZ} }] }—boxis the shapehighlight_region.boxes[]andlocate_moves.boxaccept. - get_live_view:
{ connected, connections, updatedAt, viewport {position, target} | null, userHighlights [{id, label, min, max}] (oldest first), userNote (top-level string | null), agentHighlights, playhead {moveIndex, total, playing} | null, selection {fromLine, toLine, text} | null, stats {…} | null, program {moveCount, lineCount, travel, warnings} | null }. - get_open_files:
{ open: [{id, name, kind, connections, playhead {moveIndex, total, playing} | null, url}], recentlyOpened: [ids] }. set_playhead:{ moveIndex, tabsNotified }. revert_file:{ file, analysis, revertedFrom {updatedAt} }. - generators:
{ file, ops [{label, paths, depth}], warnings, cutLengthMm, moveCount, analysis }(dry run:{ saved: false, gcode, ops, warnings, cutLengthMm, moveCount, analysis }). - publication:
{ id, ownerName, title, description, kind, settings, stats {moveCount, travel, bbox, warnings}, upvotes, commentCount, importCount, status, createdAt, url }.
9. Worked examples
"Make me a 60×40 mm pocket, 5 mm deep, in the middle of my 120×120×30 blank."
- Either write the program yourself (concentric rectangles 45 % of Ø6 apart, two 2.5 mm passes, starting with
G0 Z5) andcreate_file{name: "Centre pocket", kind: "gcode", content, settings: {toolDiameter: 6}} — or draw it:create_filekind svg (120×120 document so its corner is the stock corner) with a centred rect carrying a pocket op {depth 5, stepdown 2.5}, thengenerate_gcode_from_svg{outputName: "Centre pocket"} (this leaves the drawing as a second file, which is useful for later edits — tell the user). - Read the returned
analysis:issuesshould be empty andsimulation.removedVolumeCm3≈ 60×40×5/1000 = 12. - Tell the user the file name, the passes, the estimated time and any warnings.
"Why does my part have a gouge here?" (file open, region marked)
get_live_view→userHighlights[0],userNote,playhead.locate_moves{fileId, box: that highlight} → the moves through it; cross-check with theanalysiscollisions (rapidCollisionsgives moveIndex + line).highlight_regionthe offending move'sboxwith a label;set_playheadto itsmoveIndex; explain; offerupdate_filewith a patch (e.g. insertG0 Z5before the rapid's line) and apply it when the user agrees.
"Add a safe retract before the rapid at line 118." get_file {lines: {from: 110, to: 125}} to confirm line 118 is the rapid and what Z the following cut needs → update_file {patches: [{fromLine: 118, toLine: 117, replacement: "G0 Z5"}], note: "Added a retract before the rapid on line 118"} → relay the new analysis.issues (the rapid collision should be gone) and keep file.updatedAt in case of a revert. Z5 assumes zZero top — with zZero bed use Z = stock.h + 5. If the next cutting line has no Z word, also re-plunge: insert G1 Z-3 F250 before it in the same call (both patches use original line numbers).
"Turn this SVG into toolpaths."
import_svg_markup{name, svg, settings: {stock: {…}, toolDiameter}} → note the object ids indocument.set_svg_ops{ops: [{objectId, op: {type: "pocket", depth: 4}}, {objectId, op: {type: "profile-inside", depth: stock.h}}]} (pocket for filled areas, profile-inside for holes, profile-outside for cut-outs).generate_gcode_from_svg→ reportops,warningsandanalysis.issues(a through-hole legitimately "reaches the bed").
"Point at the deepest cut and jump there." locate_moves {fileId, deepest: 1} → highlight_region {boxes: [{...match.box, label: "deepest cut Z -12"}], focusCamera: true} → set_playhead {moveIndex: match.moveIndex}.
"Undo the change you just made." revert_file {fileId, confirm: true, expectedUpdatedAt: <the updatedAt your update_file returned>}; if it is refused because the file changed since, get_file the region and apply an inverse patch.
"Publish my bracket." list_files → propose a title/description → publish_file → give the user the returned url.
10. Etiquette
- Say what you are about to change before
update_file/delete_file; never delete without the user asking;revert_file(withexpectedUpdatedAt) exists if an edit was wrong. - Prefer patches over whole-file rewrites for small edits of long programs.
- Quote lines and moves the way the workbench shows them (both 1-based: "line 118", "move 412").
- When a tool returns
issues, relay them verbatim before proposing fixes; parserwarningsabout M/S words do not occur (they are accepted), other warnings are informational.