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:

settingmeaning
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
toolDiameterflat end mill diameter, mm
resolutionsimulation grid cell, mm (0.1–5; smaller = more accurate, slower)
preciseEdgesrender 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; q filters 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, lineCount and content. For long programs pass lines: {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 partial settings, optional folderId.
  • update_file → replace the whole content, or apply line patches; and/or change settings and name. 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); replacement is split on newlines into whole lines, no trailing newline; toLine = fromLine − 1 inserts before fromLine; replacement: "" deletes the range. Prefer patches over whole-file rewrites for long programs. For svg/model files send the whole JSON document as content (or use set_svg_ops to change machining ops without re-sending the drawing). The previous content is kept for 24 h → revert_file undoes the last content change (one level; settings/name untouched). That last change may be the user's own workbench save, so pass expectedUpdatedAt (the file.updatedAt your update_file returned) — 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-sentence note describing the change — shown as a toast if a tab is open, dropped otherwise.
  • move_file (folder and/or name), delete_file (requires confirm: 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_view tells you whether a tab is connected (connected, connections), the camera (viewport), the regions the user marked (userHighlights with userNote, work coordinates), your own boxes (agentHighlights), the playhead (moveIndex/total/playing), the listing lines they clicked (selection with 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_moves with box: {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; with deepest: 3 the deepest cuts (ties in program order — use zAtOrBelow to get the whole floor); with lines: [118] the move a line produces. Filters intersect. To reason about the state they are looking at, keep matches with moveIndex ≤ playhead.moveIndex.
  • highlight_region draws labelled translucent boxes in their 3D view (work coordinates; focusCamera: true flies the camera there). Each call replaces your previous boxes for that file (≤ 20). A match's box from locate_moves can be passed directly (thin boxes are drawn at least 0.5 mm thick). clear: true removes your boxes.
  • set_playhead moves their simulation to a move index so they see the state right after that move (playback pauses).
  • send_note shows a one-sentence toast in their workbench. Use update_file's own note for edits and send_note only 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_svg machines every shape of an svg file that has an op (or all shapes with defaultOp) and saves a gcode file (new, or update outputFileId) with the drawing's settings and sourceFileId set. 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 = document height − 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 start G21 G90 G17 / G0 Z<safeZ> and end G0 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 and sourceFileId. 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: false for a dry run that returns the program text and its analysis without writing anything.
  • import_svg_markup creates 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 partial settings.stock (e.g. {h: 10}) keeps the drawing-derived w/d. Shapes import at the top level; <g> becomes a group. Then assign ops with set_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.

  • explore_public_designs (sort: top = most upvoted all-time | new | imported | discussed; kind; q; page) and get_public_design (with includeContent for the body) browse published designs; items carry an absolute url. There is no time-windowed sort: for "popular right now" show top and discussed and say they are all-time. vote_design toggles the user's upvote.
  • import_public_design copies a design into the user's files. publish_file shares 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_publication removes 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} }] }box is the shape highlight_region.boxes[] and locate_moves.box accept.
  • 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."

  1. Either write the program yourself (concentric rectangles 45 % of Ø6 apart, two 2.5 mm passes, starting with G0 Z5) and create_file {name: "Centre pocket", kind: "gcode", content, settings: {toolDiameter: 6}} — or draw it: create_file kind svg (120×120 document so its corner is the stock corner) with a centred rect carrying a pocket op {depth 5, stepdown 2.5}, then generate_gcode_from_svg {outputName: "Centre pocket"} (this leaves the drawing as a second file, which is useful for later edits — tell the user).
  2. Read the returned analysis: issues should be empty and simulation.removedVolumeCm3 ≈ 60×40×5/1000 = 12.
  3. 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)

  1. get_live_viewuserHighlights[0], userNote, playhead.
  2. locate_moves {fileId, box: that highlight} → the moves through it; cross-check with the analysis collisions (rapidCollisions gives moveIndex + line).
  3. highlight_region the offending move's box with a label; set_playhead to its moveIndex; explain; offer update_file with a patch (e.g. insert G0 Z5 before 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."

  1. import_svg_markup {name, svg, settings: {stock: {…}, toolDiameter}} → note the object ids in document.
  2. 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).
  3. generate_gcode_from_svg → report ops, warnings and analysis.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 (with expectedUpdatedAt) 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; parser warnings about M/S words do not occur (they are accepted), other warnings are informational.