@vcad/mcp 0.9.4-main.26 → 0.9.4-main.27

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.
package/README.md CHANGED
@@ -6,4 +6,4 @@ vcad's MCP server as a self-contained bundle (server + BRep kernel WASM).
6
6
  { "mcpServers": { "vcad": { "command": "npx", "args": ["-y", "@vcad/mcp"] } } }
7
7
  ```
8
8
 
9
- Built from 178040b8a9aaccb709684c5b95e4fb42a00ae9e2 at 2026-07-25T23:14:00.956Z. Source: https://github.com/ecto/vcad
9
+ Built from cfda229f9a317556e3c9f64a43700549cd5265ef at 2026-07-25T23:24:45.817Z. Source: https://github.com/ecto/vcad
package/index.mjs CHANGED
@@ -41079,6 +41079,28 @@ var init_CHANGELOG = __esm({
41079
41079
  "build_receipt"
41080
41080
  ]
41081
41081
  },
41082
+ {
41083
+ id: "2026-07-25-gym-session-first",
41084
+ version: "0.9.4",
41085
+ date: "2026-07-25",
41086
+ category: "feat",
41087
+ title: "Sim from a session id, not a re-sent document",
41088
+ summary: "create_robot_env and batch_create_envs now take document_id, binding the env to the live session instead of a re-sent IR copy, and observations come back keyed by joint / instance id.",
41089
+ features: [
41090
+ "physics",
41091
+ "simulation",
41092
+ "mcp"
41093
+ ],
41094
+ mcpTools: [
41095
+ "create_robot_env",
41096
+ "batch_create_envs",
41097
+ "gym_reset",
41098
+ "gym_observe",
41099
+ "gym_step",
41100
+ "batch_reset",
41101
+ "batch_step"
41102
+ ]
41103
+ },
41082
41104
  {
41083
41105
  id: "2026-07-25-gerber-pad-rotation",
41084
41106
  version: "0.9.4",
@@ -74088,6 +74110,41 @@ function getSimulation(envId) {
74088
74110
  function getEnvRecord(envId) {
74089
74111
  return envRecords.get(envId) ?? null;
74090
74112
  }
74113
+ function resolveEnvDocument(args) {
74114
+ const id = typeof args.document_id === "string" ? args.document_id : "";
74115
+ const inline = args.document && typeof args.document === "object" ? args.document : null;
74116
+ if (id && inline) {
74117
+ throw new Error(
74118
+ `Pass either \`document_id\` or \`document\`, not both \u2014 they resolve to different sessions and the inline copy may be stale. Drop \`document\` to simulate session "${id}" in place.`
74119
+ );
74120
+ }
74121
+ if (!id && !inline) {
74122
+ throw new Error(
74123
+ "Pass `document_id` (from open_document) to simulate a resident session \u2014 or an inline `document` object for the stateless flow. Exactly one is required."
74124
+ );
74125
+ }
74126
+ if (id) {
74127
+ return { doc: getSession(id), documentId: id, source: "session" };
74128
+ }
74129
+ return { doc: inline, documentId: null, source: "inline" };
74130
+ }
74131
+ function labelObservation(obs, jointIds, endEffectorIds) {
74132
+ const labeled = { ...obs, joint_ids: jointIds };
74133
+ if (jointIds && jointIds.length === obs.joint_positions.length) {
74134
+ labeled.joints = jointIds.map((id, i) => ({
74135
+ id,
74136
+ position: obs.joint_positions[i],
74137
+ velocity: obs.joint_velocities[i]
74138
+ }));
74139
+ }
74140
+ if (endEffectorIds.length === obs.end_effector_poses.length) {
74141
+ labeled.end_effectors = endEffectorIds.map((id, i) => ({
74142
+ id,
74143
+ pose: obs.end_effector_poses[i]
74144
+ }));
74145
+ }
74146
+ return labeled;
74147
+ }
74091
74148
  async function createRobotEnv(input) {
74092
74149
  const args = input;
74093
74150
  const available = await isPhysicsAvailable2();
@@ -74106,17 +74163,19 @@ async function createRobotEnv(input) {
74106
74163
  }
74107
74164
  try {
74108
74165
  const envId = `sim_${randomBytes4(12).toString("base64url")}`;
74109
- const env = await PhysicsEnv.create(args.document, {
74166
+ const { doc, documentId: sessionId, source } = resolveEnvDocument(args);
74167
+ const env = await PhysicsEnv.create(doc, {
74110
74168
  endEffectorIds: args.end_effector_ids,
74111
74169
  dt: args.dt,
74112
74170
  substeps: args.substeps,
74113
74171
  maxSteps: args.max_steps
74114
74172
  });
74115
74173
  simulations.set(envId, env);
74116
- const documentId = registerSession(args.document);
74174
+ const documentId = sessionId ?? registerSession(doc);
74117
74175
  envRecords.set(envId, {
74118
- document: args.document,
74176
+ document: doc,
74119
74177
  documentId,
74178
+ endEffectorIds: args.end_effector_ids,
74120
74179
  trajectory: [],
74121
74180
  rewards: [],
74122
74181
  dones: [],
@@ -74128,7 +74187,19 @@ async function createRobotEnv(input) {
74128
74187
  });
74129
74188
  const info = {
74130
74189
  env_id: envId,
74190
+ // Binding contract, spelled out because two handles come back:
74191
+ // · env_id → gym_step / gym_reset / gym_observe / gym_close
74192
+ // · document_id → the replay viewer, get_preview_glb, and every other
74193
+ // session tool (render_view, inspect_cad, update, …)
74194
+ // On the session path document_id IS the id you passed — the env and the
74195
+ // document you keep editing are the same session. On the inline path it
74196
+ // is a NEW session minted from the IR you sent.
74131
74197
  document_id: documentId,
74198
+ document_source: source,
74199
+ binds: {
74200
+ env_id: "gym_step, gym_reset, gym_observe, gym_close",
74201
+ document_id: "replay viewer, get_preview_glb, and session tools (render_view, inspect_cad, update)"
74202
+ },
74132
74203
  num_joints: env.numJoints,
74133
74204
  // Observation ordering contract: joint_positions[i] and
74134
74205
  // joint_velocities[i] refer to joint_ids[i]. Null when the loaded
@@ -74185,8 +74256,16 @@ function gymStep(input) {
74185
74256
  record2.dones.shift();
74186
74257
  }
74187
74258
  }
74259
+ const labeled = {
74260
+ ...result,
74261
+ observation: labelObservation(
74262
+ result.observation,
74263
+ env.jointIds,
74264
+ record2?.endEffectorIds ?? []
74265
+ )
74266
+ };
74188
74267
  return {
74189
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
74268
+ content: [{ type: "text", text: JSON.stringify(labeled, null, 2) }]
74190
74269
  };
74191
74270
  } catch (err7) {
74192
74271
  const message = err7 instanceof Error ? err7.message : String(err7);
@@ -74218,7 +74297,20 @@ function gymReset(input) {
74218
74297
  record2.resetEpoch += 1;
74219
74298
  }
74220
74299
  return {
74221
- content: [{ type: "text", text: JSON.stringify(observation, null, 2) }]
74300
+ content: [
74301
+ {
74302
+ type: "text",
74303
+ text: JSON.stringify(
74304
+ labelObservation(
74305
+ observation,
74306
+ env.jointIds,
74307
+ record2?.endEffectorIds ?? []
74308
+ ),
74309
+ null,
74310
+ 2
74311
+ )
74312
+ }
74313
+ ]
74222
74314
  };
74223
74315
  } catch (err7) {
74224
74316
  const message = err7 instanceof Error ? err7.message : String(err7);
@@ -74241,8 +74333,22 @@ function gymObserve(input) {
74241
74333
  }
74242
74334
  try {
74243
74335
  const observation = env.observe();
74336
+ const record2 = envRecords.get(args.env_id);
74244
74337
  return {
74245
- content: [{ type: "text", text: JSON.stringify(observation, null, 2) }]
74338
+ content: [
74339
+ {
74340
+ type: "text",
74341
+ text: JSON.stringify(
74342
+ labelObservation(
74343
+ observation,
74344
+ env.jointIds,
74345
+ record2?.endEffectorIds ?? []
74346
+ ),
74347
+ null,
74348
+ 2
74349
+ )
74350
+ }
74351
+ ]
74246
74352
  };
74247
74353
  } catch (err7) {
74248
74354
  const message = err7 instanceof Error ? err7.message : String(err7);
@@ -74288,6 +74394,7 @@ async function batchCreateEnvs(input) {
74288
74394
  }
74289
74395
  try {
74290
74396
  const batchId = `batch_${randomBytes4(12).toString("base64url")}`;
74397
+ const { doc, documentId, source } = resolveEnvDocument(args);
74291
74398
  const envOptions = {
74292
74399
  endEffectorIds: args.end_effector_ids,
74293
74400
  dt: args.dt,
@@ -74296,22 +74403,33 @@ async function batchCreateEnvs(input) {
74296
74403
  };
74297
74404
  const envPromises = Array.from(
74298
74405
  { length: args.n_envs },
74299
- () => PhysicsEnv.create(args.document, envOptions)
74406
+ () => PhysicsEnv.create(doc, envOptions)
74300
74407
  );
74301
74408
  const envs = await Promise.all(envPromises);
74302
74409
  const actionDim = envs[0].actionDim;
74303
74410
  batchGroups.set(batchId, {
74304
74411
  envs,
74305
74412
  actionDim,
74306
- document: args.document,
74307
- options: envOptions
74413
+ document: doc,
74414
+ options: envOptions,
74415
+ jointIds: envs[0].jointIds
74308
74416
  });
74309
74417
  const info = {
74310
74418
  batch_id: batchId,
74419
+ // batch_step / batch_reset bind to batch_id. No document session is
74420
+ // minted here — a batch has no viewer; `document_id` echoes the session
74421
+ // read from, and is null on the inline path.
74422
+ document_id: documentId,
74423
+ document_source: source,
74311
74424
  n_envs: args.n_envs,
74312
74425
  action_dim: actionDim,
74313
74426
  observation_dim: envs[0].observationDim,
74314
- num_joints: envs[0].numJoints
74427
+ num_joints: envs[0].numJoints,
74428
+ // Observation ordering contract, echoed so batch callers get the same
74429
+ // labeling the single-env path returns per observation.
74430
+ joint_ids: envs[0].jointIds,
74431
+ actuated_joint_ids: envs[0].actuatedJointIds,
74432
+ end_effector_ids: args.end_effector_ids
74315
74433
  };
74316
74434
  return {
74317
74435
  content: [{ type: "text", text: JSON.stringify(info, null, 2) }]
@@ -74353,7 +74471,13 @@ function batchStep(input) {
74353
74471
  (env, i) => env.step(args.action_type, args.actions[i])
74354
74472
  );
74355
74473
  const summary = {
74356
- observations: results.map((r) => r.observation),
74474
+ observations: results.map(
74475
+ (r) => labelObservation(
74476
+ r.observation,
74477
+ group.jointIds,
74478
+ group.options.endEffectorIds
74479
+ )
74480
+ ),
74357
74481
  rewards: results.map((r) => r.reward),
74358
74482
  dones: results.map((r) => r.done)
74359
74483
  };
@@ -74380,7 +74504,13 @@ function batchReset(input) {
74380
74504
  };
74381
74505
  }
74382
74506
  try {
74383
- const observations = group.envs.map((env) => env.reset());
74507
+ const observations = group.envs.map(
74508
+ (env) => labelObservation(
74509
+ env.reset(),
74510
+ group.jointIds,
74511
+ group.options.endEffectorIds
74512
+ )
74513
+ );
74384
74514
  return {
74385
74515
  content: [{ type: "text", text: JSON.stringify({ observations }, null, 2) }]
74386
74516
  };
@@ -74392,7 +74522,7 @@ function batchReset(input) {
74392
74522
  };
74393
74523
  }
74394
74524
  }
74395
- var simulations, MAX_TRAJECTORY, envRecords, batchGroups, createRobotEnvSchema, gymStepSchema, gymResetSchema, gymObserveSchema, gymCloseSchema, batchCreateEnvsSchema, batchStepSchema, batchResetSchema, toolDefs45;
74525
+ var simulations, MAX_TRAJECTORY, envRecords, batchGroups, INLINE_DOC_DESC, SESSION_DOC_DESC, createRobotEnvSchema, gymStepSchema, gymResetSchema, gymObserveSchema, gymCloseSchema, batchCreateEnvsSchema, batchStepSchema, batchResetSchema, toolDefs45;
74396
74526
  var init_gym = __esm({
74397
74527
  "src/tools/gym.ts"() {
74398
74528
  "use strict";
@@ -74403,12 +74533,18 @@ var init_gym = __esm({
74403
74533
  MAX_TRAJECTORY = 600;
74404
74534
  envRecords = /* @__PURE__ */ new Map();
74405
74535
  batchGroups = /* @__PURE__ */ new Map();
74536
+ INLINE_DOC_DESC = "Inline Document IR describing the robot assembly, used instead of a session. Use this stateless path when no `document_id` is resident (e.g. a cold serverless instance). Exactly one of `document_id` or `document` must be given.";
74537
+ SESSION_DOC_DESC = "Session id of the assembly to simulate (from open_document / create_cad_loon). Preferred: the env binds to this same session, so no IR round-trip is needed and the sim can never run against a stale copy.";
74406
74538
  createRobotEnvSchema = {
74407
74539
  type: "object",
74408
74540
  properties: {
74541
+ document_id: {
74542
+ type: "string",
74543
+ description: SESSION_DOC_DESC
74544
+ },
74409
74545
  document: {
74410
74546
  type: "object",
74411
- description: "vcad IR Document describing the robot assembly"
74547
+ description: INLINE_DOC_DESC
74412
74548
  },
74413
74549
  end_effector_ids: {
74414
74550
  type: "array",
@@ -74428,7 +74564,7 @@ var init_gym = __esm({
74428
74564
  description: "Maximum episode length (default: 1000)"
74429
74565
  }
74430
74566
  },
74431
- required: ["document", "end_effector_ids"]
74567
+ required: ["end_effector_ids"]
74432
74568
  };
74433
74569
  gymStepSchema = {
74434
74570
  type: "object",
@@ -74483,9 +74619,13 @@ var init_gym = __esm({
74483
74619
  batchCreateEnvsSchema = {
74484
74620
  type: "object",
74485
74621
  properties: {
74622
+ document_id: {
74623
+ type: "string",
74624
+ description: SESSION_DOC_DESC
74625
+ },
74486
74626
  document: {
74487
74627
  type: "object",
74488
- description: "vcad IR Document describing the robot assembly"
74628
+ description: INLINE_DOC_DESC
74489
74629
  },
74490
74630
  n_envs: {
74491
74631
  type: "number",
@@ -74509,7 +74649,7 @@ var init_gym = __esm({
74509
74649
  description: "Maximum episode length (default: 1000)"
74510
74650
  }
74511
74651
  },
74512
- required: ["document", "n_envs", "end_effector_ids"]
74652
+ required: ["n_envs", "end_effector_ids"]
74513
74653
  };
74514
74654
  batchStepSchema = {
74515
74655
  type: "object",
@@ -74548,7 +74688,7 @@ var init_gym = __esm({
74548
74688
  {
74549
74689
  name: "create_robot_env",
74550
74690
  pack: "physics",
74551
- description: "Create a physics simulation environment from a vcad assembly. Returns an environment ID that can be used with gym_step, gym_reset, and gym_observe. The environment provides a gym-style interface for RL training. Mounts the inline 3D viewer with a play button \u2014 gym_step rollouts replay right in the chat.",
74691
+ description: "Create a physics simulation environment from a vcad assembly. Session-first: pass the `document_id` of the assembly already open \u2014 the env binds to that same session, so there is no IR round-trip and the sim can't run against a stale copy. Inline `document` IR is the stateless fallback; exactly one of the two is required. Returns env_id (for gym_step / gym_reset / gym_observe / gym_close) and document_id (what the replay viewer and the other session tools bind to). Mounts the inline 3D viewer with a play button \u2014 gym_step rollouts replay right in the chat.",
74552
74692
  inputSchema: createRobotEnvSchema,
74553
74693
  handler: (a) => createRobotEnv(a),
74554
74694
  // writesDoc: the registered session must persist durably (signed-in hosted
@@ -74559,7 +74699,7 @@ var init_gym = __esm({
74559
74699
  {
74560
74700
  name: "gym_step",
74561
74701
  pack: "physics",
74562
- description: "Step the physics simulation with an action. action_type can be 'torque' (Nm), 'position' (degrees/mm), or 'velocity' (deg/s or mm/s). Returns observation (joint positions/velocities, end effector poses), reward, and done flag.",
74702
+ description: "Step the physics simulation with an action. action_type can be 'torque' (Nm), 'position' (degrees/mm), or 'velocity' (deg/s or mm/s). Returns observation (joint positions/velocities, end effector poses), reward, and done flag. The observation carries both the bare positional arrays and id-keyed views: `joints[i] = {id, position, velocity}` and `end_effectors[i] = {id, pose}`, so no ordering has to be remembered.",
74563
74703
  inputSchema: gymStepSchema,
74564
74704
  handler: (a) => gymStep(a),
74565
74705
  behavior: behavior({})
@@ -74567,7 +74707,7 @@ var init_gym = __esm({
74567
74707
  {
74568
74708
  name: "gym_reset",
74569
74709
  pack: "physics",
74570
- description: "Reset the simulation environment to its initial state. Returns the initial observation.",
74710
+ description: "Reset the simulation environment to its initial state. Returns the initial observation, with joint values keyed by joint id (`joints`) and end effector poses keyed by instance id (`end_effectors`) alongside the bare arrays.",
74571
74711
  inputSchema: gymResetSchema,
74572
74712
  handler: (a) => gymReset(a),
74573
74713
  behavior: behavior({})
@@ -74575,7 +74715,7 @@ var init_gym = __esm({
74575
74715
  {
74576
74716
  name: "gym_observe",
74577
74717
  pack: "physics",
74578
- description: "Get the current observation from the simulation without stepping. Returns joint positions, velocities, and end effector poses.",
74718
+ description: "Get the current observation from the simulation without stepping. Returns joint positions, velocities, and end effector poses \u2014 both as bare arrays and keyed by joint / instance id.",
74579
74719
  inputSchema: gymObserveSchema,
74580
74720
  handler: (a) => gymObserve(a),
74581
74721
  behavior: behavior({})
@@ -74591,7 +74731,7 @@ var init_gym = __esm({
74591
74731
  {
74592
74732
  name: "batch_create_envs",
74593
74733
  pack: "physics",
74594
- description: "Create N parallel simulation environments from a single robot assembly. Returns a batch_id for use with batch_step and batch_reset. Enables parallel RL training across multiple environments.",
74734
+ description: "Create N parallel simulation environments from a single robot assembly. Session-first: pass the `document_id` of the open assembly, or inline `document` IR as the stateless fallback \u2014 exactly one is required. Returns a batch_id for use with batch_step and batch_reset. Enables parallel RL training across multiple environments.",
74595
74735
  inputSchema: batchCreateEnvsSchema,
74596
74736
  handler: (a) => batchCreateEnvs(a),
74597
74737
  behavior: behavior({})
@@ -77878,6 +78018,8 @@ function buildInstructions(kernelPrompt) {
77878
78018
  "",
77879
78019
  "Sessions and server restarts: a `document_id` is a handle to server-side state, NOT storage. If ANY preview goes dark, a widget stops updating, or a call fails with an unknown `document_id`, check `server_info` FIRST \u2014 a low `uptime_s` or a changed `instance_id` means the SERVER RESTARTED and the feature is fine. That is the fastest way to tell a restart from a bug, and it should be the first check for any preview-shaped complaint. On a restart every live document_id dies at once, so previously-working widgets all go dark together \u2014 that is not a renderer bug. When `server_info` reports `durable:false`, sessions are in-memory only and cannot be recovered: keep the authoring source (your `create_cad_loon` call or record of edits) so a document can be rebuilt, and snapshot long-lived work with `checkpoint_document` / `save_document`. Every tool result also carries `_meta['io.vcad/build']` with `boot_token` + `instance_id`, so a changed value flags a restart without polling.",
77880
78020
  "",
78021
+ "Physics/RL workflow: `create_robot_env` (or `batch_create_envs`) \u2192 `gym_reset` \u2192 `gym_step` \u2192 `gym_close`. Both env creators are session-first like everything else: pass the `document_id` of the open assembly \u2014 never re-send its IR. `create_robot_env` binds the env to that same session, so a `document_id` env and the document you keep editing can't drift apart; inline `document` IR stays available as the stateless fallback (it mints a NEW session, returned as `document_id`) and passing both is refused. `env_id` drives gym_step/gym_reset/gym_observe/gym_close; `document_id` is what the replay viewer, `get_preview_glb`, and the other session tools bind to. Observations come back both as bare positional arrays and keyed by id (`joints[i] = {id, position, velocity}`, `end_effectors[i] = {id, pose}`) \u2014 read the keyed views and no ordering has to be remembered.",
78022
+ "",
77881
78023
  "PCB workflow: `create_schematic` (declare connectivity as data via `nets`) \u2192 `place_components` \u2192 `route_nets` / `add_coil` / `add_coil_array` \u2192 `run_drc` \u2192 `validate_for_fab` \u2192 `export_gerber`. All take the `document_id` from create_schematic and mutate that session \u2014 never re-send the document. `validate_for_fab` is the single 'is this board ready?' gate (DRC + renderability + Gerber serialization + blockers, all fail-closed); `export_gerber` enforces a clean DRC by default and blocks a dirty board. `board_from_solid` turns a solid part (e.g. an enclosure or stator disc in a CAD session) into an outline polygon for `place_components`. For motors, plan the winding first with `winding_layout` (slots + poles \u2192 per-coil phase/polarity/winding-factor, as data \u2014 it touches no board), then realize it with `add_coil_array`. `run_drc` returns a summary by default (counts by rule + net-pair, worst clearance, a capped sample); pass `detail:'full'` for every violation. Surgical copper edits: `get_copper` lists existing traces/vias/zones (filtered by layer/net/bbox/kind) with the same indices `delete_trace`/`delete_via`/`delete_zone` accept \u2014 discover, then delete, without exporting the document. Where two nets must touch on purpose (wye neutral, split ground, shunt tap), declare it with `add_net_tie` \u2014 prefer a region-scoped tie (position+radius) so DRC stays honest away from the junction; `delete_net_tie` takes it back."
77882
78024
  ].join("\n");
77883
78025
  if (!kernelPrompt) return header;
@@ -78727,8 +78869,8 @@ var init_server3 = __esm({
78727
78869
  init_order_feed();
78728
78870
  init_animate();
78729
78871
  PKG_VERSION = (() => {
78730
- if ("0.9.4-main.26") {
78731
- return "0.9.4-main.26";
78872
+ if ("0.9.4-main.27") {
78873
+ return "0.9.4-main.27";
78732
78874
  }
78733
78875
  try {
78734
78876
  const req = createRequire2(import.meta.url);
@@ -78737,8 +78879,8 @@ var init_server3 = __esm({
78737
78879
  return "0.0.0";
78738
78880
  }
78739
78881
  })();
78740
- BUILD_SHA = "178040b8a9aaccb709684c5b95e4fb42a00ae9e2";
78741
- BUILD_TIME = "2026-07-25T23:14:00.956Z";
78882
+ BUILD_SHA = "cfda229f9a317556e3c9f64a43700549cd5265ef";
78883
+ BUILD_TIME = "2026-07-25T23:24:45.817Z";
78742
78884
  SHORT_SHA = BUILD_SHA === "unknown" ? "unknown" : BUILD_SHA.slice(0, 7);
78743
78885
  VERSION_WITH_BUILD = SHORT_SHA === "unknown" ? PKG_VERSION : `${PKG_VERSION}+${SHORT_SHA}`;
78744
78886
  INSTANCE_ID = randomUUID6().slice(0, 8);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vcad/mcp",
3
- "version": "0.9.4-main.26",
3
+ "version": "0.9.4-main.27",
4
4
  "description": "vcad MCP server — parametric CAD + PCB design tools for AI agents (self-contained: bundled server + kernel WASM)",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -19,7 +19,7 @@
19
19
  },
20
20
  "homepage": "https://vcad.io",
21
21
  "vcadBuild": {
22
- "sha": "178040b8a9aaccb709684c5b95e4fb42a00ae9e2",
23
- "builtAt": "2026-07-25T23:14:00.956Z"
22
+ "sha": "cfda229f9a317556e3c9f64a43700549cd5265ef",
23
+ "builtAt": "2026-07-25T23:24:45.817Z"
24
24
  }
25
25
  }
Binary file