@energy8platform/golem 0.6.0 → 0.7.0

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.
Files changed (64) hide show
  1. package/README.md +14 -0
  2. package/dist/editor.css +3 -4
  3. package/dist/editor.js +108 -61
  4. package/dist/lib/cli.js +101 -70
  5. package/dist/lib/cli.js.map +4 -4
  6. package/dist/lib/e8/agent.d.ts +35 -0
  7. package/dist/lib/e8/host.d.ts +22 -0
  8. package/dist/lib/e8/node.d.ts +66 -0
  9. package/dist/lib/e8/runtime.d.ts +10 -0
  10. package/dist/lib/e8/schema.d.ts +3 -0
  11. package/dist/lib/e8/types.d.ts +80 -0
  12. package/dist/lib/e8-agent.js +6781 -0
  13. package/dist/lib/e8-agent.js.map +7 -0
  14. package/dist/lib/e8-client.js +116 -0
  15. package/dist/lib/e8-host.js +6929 -0
  16. package/dist/lib/e8-host.js.map +7 -0
  17. package/dist/lib/e8-runtime.js +2024 -0
  18. package/dist/lib/e8-runtime.js.map +7 -0
  19. package/dist/lib/e8-schema.js +7 -0
  20. package/dist/lib/e8-schema.js.map +7 -0
  21. package/dist/lib/editor/api.d.ts +1 -0
  22. package/dist/lib/editor/embed.d.ts +9 -0
  23. package/dist/lib/editor/server.d.ts +23 -1
  24. package/dist/lib/editor/store.d.ts +2 -0
  25. package/dist/lib/editor/timeline.d.ts +15 -0
  26. package/dist/lib/editor-entry.d.ts +1 -1
  27. package/dist/lib/editor-entry.js +102 -70
  28. package/dist/lib/editor-entry.js.map +4 -4
  29. package/dist/lib/rig-import-layers.d.ts +2 -0
  30. package/dist/lib/rig-version.d.ts +12 -0
  31. package/dist/lib/tool-schema.d.ts +3 -0
  32. package/dist/lib/tools.d.ts +2 -0
  33. package/dist/lib/tools.js +49 -10
  34. package/dist/lib/tools.js.map +4 -4
  35. package/editor.html +2 -2
  36. package/package.json +38 -3
  37. package/skills/e8-golem/SKILL.md +71 -0
  38. package/skills/golem-symbol-animation/SKILL.md +87 -0
  39. package/skills/golem-symbol-animation/agents/openai.yaml +4 -0
  40. package/skills/golem-symbol-animation/references/facial-controls.md +48 -0
  41. package/skills/golem-symbol-animation/references/game-engine-integration.md +133 -0
  42. package/skills/golem-symbol-animation/references/golem-authoring.md +138 -0
  43. package/skills/golem-symbol-animation/references/interactive-editor.md +17 -0
  44. package/skills/golem-symbol-animation/references/motion-craft.md +113 -0
  45. package/skills/golem-symbol-animation/references/packed-delivery.md +88 -0
  46. package/skills/golem-symbol-animation/references/quantitative-qa.md +106 -0
  47. package/skills/golem-symbol-animation/references/spine-rive-study.md +87 -0
  48. package/skills/golem-symbol-animation/scripts/atlas_parts.py +87 -0
  49. package/skills/golem-symbol-animation/scripts/check_package.py +71 -0
  50. package/skills/golem-symbol-animation/scripts/image_gates.py +143 -0
  51. package/skills/golem-symbol-cutting/SKILL.md +70 -0
  52. package/skills/golem-symbol-cutting/agents/openai.yaml +4 -0
  53. package/skills/golem-symbol-cutting/assets/h1/h1.atlas.png +0 -0
  54. package/skills/golem-symbol-cutting/assets/h1/h1.png +0 -0
  55. package/skills/golem-symbol-cutting/references/cutting-workflow.md +92 -0
  56. package/skills/golem-symbol-cutting/references/facial-layers.md +44 -0
  57. package/skills/golem-symbol-cutting/references/generation-workflow.md +109 -0
  58. package/skills/golem-symbol-cutting/references/golem-handoff.md +62 -0
  59. package/skills/golem-symbol-cutting/references/h1-example.md +46 -0
  60. package/skills/golem-symbol-cutting/references/hybrid-workflow.md +119 -0
  61. package/skills/golem-symbol-cutting/references/registration-and-motion.md +89 -0
  62. package/skills/golem-symbol-cutting/references/spine-rive-construction.md +73 -0
  63. package/skills/golem-symbol-cutting/scripts/atlas_parts.py +87 -0
  64. package/skills/golem-symbol-cutting/scripts/hybrid_parts.py +279 -0
package/dist/lib/cli.js CHANGED
@@ -3,9 +3,9 @@ import { resolve as resolve3 } from "node:path";
3
3
 
4
4
  // src/editor/server.ts
5
5
  import { createServer } from "node:http";
6
- import { readFile as readFile10, stat as stat3, readdir as readdir5, mkdir as mkdir7, open } from "node:fs/promises";
6
+ import { readFile as readFile10, stat as stat4, readdir as readdir5, mkdir as mkdir7, open } from "node:fs/promises";
7
7
  import { watch as fsWatch } from "node:fs";
8
- import { resolve as resolve2, dirname as dirname9, basename as basename5, extname as extname2, sep, join as join10, relative as relative3 } from "node:path";
8
+ import { resolve as resolve2, dirname as dirname9, basename as basename5, extname as extname2, sep as sep2, join as join10, relative as relative4 } from "node:path";
9
9
  import { fileURLToPath as fileURLToPath3 } from "node:url";
10
10
 
11
11
  // src/rig-format.ts
@@ -571,7 +571,7 @@ function parse(input) {
571
571
  // src/rig-api.ts
572
572
  import { z as z3 } from "zod";
573
573
  import { readFile as readFile9, writeFile as writeFile7, stat as stat2, mkdir as mkdir6, readdir as readdir4 } from "node:fs/promises";
574
- import { dirname as dirname8, join as join9, relative as relative2, isAbsolute } from "node:path";
574
+ import { dirname as dirname8, join as join9, relative as relative3, isAbsolute as isAbsolute2 } from "node:path";
575
575
 
576
576
  // src/rig-io.ts
577
577
  import { mkdir, rename, rm, writeFile } from "node:fs/promises";
@@ -594,8 +594,10 @@ var saveJson = (path, value) => saveBytes(path, JSON.stringify(value, null, 2));
594
594
  // src/rig-queue.ts
595
595
  import { AsyncLocalStorage } from "node:async_hooks";
596
596
  import { resolve } from "node:path";
597
- var pending = /* @__PURE__ */ new Map();
598
- var active = new AsyncLocalStorage();
597
+ var { pending, active } = globalThis[Symbol.for("golem.rigQueue")] ??= {
598
+ pending: /* @__PURE__ */ new Map(),
599
+ active: new AsyncLocalStorage()
600
+ };
599
601
  async function withRigQueue(path, op) {
600
602
  const key = resolve(path);
601
603
  const held = active.getStore();
@@ -2011,8 +2013,9 @@ function createDoc(name, width, height, notes) {
2011
2013
  function createSymbol(name, asset, margin = 0.15, notes) {
2012
2014
  const mx = Math.ceil(asset.width * margin), my = Math.ceil(asset.height * margin);
2013
2015
  const width = asset.width + 2 * mx, height = asset.height + 2 * my;
2014
- const fromFile = asset.src.replace(/^.*\//, "").replace(/\.[^.]+$/, "").toLowerCase().replace(/[^a-z0-9_]/g, "_").replace(/^[^a-z]+/, "");
2015
- const id = asset.id ?? (fromFile || name);
2016
+ const stem = asset.src.replace(/^.*\//, "").replace(/\.[^.]+$/, "").toLowerCase().replace(/[^a-z0-9_]/g, "_");
2017
+ const fromFile = stem.replace(/^[^a-z]+/, "") || (stem ? `symbol_${stem}` : "");
2018
+ const id = asset.id ?? (ID_RE.test(fromFile) ? fromFile : ID_RE.test(name) ? name : "symbol");
2016
2019
  let doc = createDoc(name, width, height, notes);
2017
2020
  doc = { ...doc, bones: [{ ...doc.bones[0], transform: { ...doc.bones[0].transform, x: width / 2, y: height - my } }] };
2018
2021
  doc = { ...doc, assets: [RigSchema.shape.assets.element.parse({ id, src: asset.src, width: asset.width, height: asset.height })] };
@@ -2768,7 +2771,7 @@ function setGoal(doc, bone, x, y, opts) {
2768
2771
 
2769
2772
  // src/rig-import-layers.ts
2770
2773
  import { readdir, readFile } from "node:fs/promises";
2771
- import { join as join2, basename as basename2 } from "node:path";
2774
+ import { join as join2, basename as basename2, isAbsolute, relative, sep } from "node:path";
2772
2775
  var DEFAULT_PARENT = {
2773
2776
  head: "neck",
2774
2777
  neck: "torso",
@@ -2794,6 +2797,10 @@ function pngSize(buf) {
2794
2797
  if (buf.readUInt32BE(12) !== 1229472850) throw new Error("not a PNG (IHDR missing)");
2795
2798
  return [buf.readUInt32BE(16), buf.readUInt32BE(20)];
2796
2799
  }
2800
+ function outsideDir(dir, file) {
2801
+ const r = relative(dir, join2(dir, file));
2802
+ return isAbsolute(file) || r === ".." || r.startsWith(".." + sep);
2803
+ }
2797
2804
  async function importLayers(doc, dir, relPrefix = "layers") {
2798
2805
  let specs;
2799
2806
  try {
@@ -2806,6 +2813,7 @@ async function importLayers(doc, dir, relPrefix = "layers") {
2806
2813
  let next = doc;
2807
2814
  const parsed = [];
2808
2815
  for (const s2 of specs) {
2816
+ if (outsideDir(dir, s2.file)) throw new Error(`layers.json file "${s2.file}" is outside the layers folder (${dir})`);
2809
2817
  const buf = await readFile(join2(dir, s2.file));
2810
2818
  const [w, h] = pngSize(buf);
2811
2819
  const m = basename2(s2.file, ".png").match(/^(?:(\d+)_)?(.+)$/);
@@ -4062,7 +4070,7 @@ async function buildAtlas(doc, assetsDir, outDir, opts = {}) {
4062
4070
 
4063
4071
  // src/spine-import.ts
4064
4072
  import { readFile as readFile7, stat, writeFile as writeFile5 } from "node:fs/promises";
4065
- import { basename as basename4, dirname as dirname6, extname, join as join8, relative } from "node:path";
4073
+ import { basename as basename4, dirname as dirname6, extname, join as join8, relative as relative2 } from "node:path";
4066
4074
 
4067
4075
  // src/ktx2.ts
4068
4076
  import { readFile as readFile6 } from "node:fs/promises";
@@ -4211,13 +4219,13 @@ var Ids = class {
4211
4219
  var spineToDoc = (x, y, originX, originY) => [x + originX, originY - y];
4212
4220
  var r4 = (v) => Math.round(v * 1e6) / 1e6 + 0;
4213
4221
  var hex8 = (c) => `#${(c ?? "ffffffff").toLowerCase().padEnd(8, "f").slice(0, 8)}`;
4214
- function importSpine(json, atlasText, opts = {}) {
4222
+ function importSpine(json2, atlasText, opts = {}) {
4215
4223
  const warnings = [];
4216
4224
  const atlas = parseAtlas(atlasText);
4217
- const version = String(json.skeleton?.spine ?? "4.0");
4218
- if (json.physics?.length) warnings.push(`${json.physics.length} Spine physics constraints are not imported; bake their motion before delivery`);
4225
+ const version = String(json2.skeleton?.spine ?? "4.0");
4226
+ if (json2.physics?.length) warnings.push(`${json2.physics.length} Spine physics constraints are not imported; bake their motion before delivery`);
4219
4227
  const major = parseFloat(version);
4220
- const bounds = { x: json.skeleton?.x ?? 0, y: json.skeleton?.y ?? 0, w: json.skeleton?.width ?? 0, h: json.skeleton?.height ?? 0 };
4228
+ const bounds = { x: json2.skeleton?.x ?? 0, y: json2.skeleton?.y ?? 0, w: json2.skeleton?.width ?? 0, h: json2.skeleton?.height ?? 0 };
4221
4229
  const margin = opts.margin ?? 0;
4222
4230
  let originX = -bounds.x + margin, originY = bounds.y + bounds.h + margin;
4223
4231
  const toDoc = (x, y) => spineToDoc(x, y, originX, originY);
@@ -4234,7 +4242,7 @@ function importSpine(json, atlasText, opts = {}) {
4234
4242
  }
4235
4243
  return id;
4236
4244
  };
4237
- const spineBones = json.bones ?? [];
4245
+ const spineBones = json2.bones ?? [];
4238
4246
  const boneIndexByName = new Map(spineBones.map((b, i) => [b.name, i]));
4239
4247
  const bones = [{ id: ROOT, transform: { x: r4(originX), y: r4(originY) }, role: "root" }];
4240
4248
  for (const b of spineBones) {
@@ -4247,7 +4255,7 @@ function importSpine(json, atlasText, opts = {}) {
4247
4255
  ...b.color ? { color: b.color } : {}
4248
4256
  });
4249
4257
  }
4250
- const skins = Array.isArray(json.skins) ? json.skins : Object.entries(json.skins ?? {}).map(([name, attachments2]) => ({ name, attachments: attachments2 }));
4258
+ const skins = Array.isArray(json2.skins) ? json2.skins : Object.entries(json2.skins ?? {}).map(([name, attachments2]) => ({ name, attachments: attachments2 }));
4251
4259
  const selectedSkin = opts.skin ? skins.find((s2) => s2.name === opts.skin) : skins.find((s2) => s2.name === "default") ?? skins[0];
4252
4260
  if (opts.skin && !selectedSkin) throw new Error(`Spine skin "${opts.skin}" not found; available: ${skins.map((s2) => s2.name).join(", ")}`);
4253
4261
  const lookupSkins = [selectedSkin, skins.find((s2) => s2.name === "default")].filter((s2, i, list) => s2 && list.indexOf(s2) === i);
@@ -4271,7 +4279,7 @@ function importSpine(json, atlasText, opts = {}) {
4271
4279
  const resolved = skinnedName.get(`${slot} ${name}`);
4272
4280
  return resolved === void 0 ? null : attachmentId(slot, resolved);
4273
4281
  };
4274
- const spineSlots = json.slots ?? [];
4282
+ const spineSlots = json2.slots ?? [];
4275
4283
  const slotIndexByName = new Map(spineSlots.map((s2, i) => [s2.name, i]));
4276
4284
  const blendOf = (b) => b === "additive" ? "add" : b === "multiply" ? "multiply" : b === "screen" ? "screen" : "normal";
4277
4285
  const slots = spineSlots.map((s2, i) => ({
@@ -4437,7 +4445,7 @@ function importSpine(json, atlasText, opts = {}) {
4437
4445
  }
4438
4446
  }
4439
4447
  const activeConstraint = (kind, c) => !c.skin || lookupSkins.some((s2) => s2[kind]?.includes(c.name));
4440
- const ik = (json.ik ?? []).filter((c) => activeConstraint("ik", c)).map((c) => ({
4448
+ const ik = (json2.ik ?? []).filter((c) => activeConstraint("ik", c)).map((c) => ({
4441
4449
  id: ikIds.get(c.name),
4442
4450
  order: c.order ?? 0,
4443
4451
  bones: c.bones.map((b) => boneIds.get(b)),
@@ -4450,7 +4458,7 @@ function importSpine(json, atlasText, opts = {}) {
4450
4458
  stretch: !!c.stretch,
4451
4459
  uniform: !!c.uniform
4452
4460
  }));
4453
- const transform = (json.transform ?? []).filter((c) => activeConstraint("transform", c)).map((c) => ({
4461
+ const transform = (json2.transform ?? []).filter((c) => activeConstraint("transform", c)).map((c) => ({
4454
4462
  id: tcIds.get(c.name),
4455
4463
  order: c.order ?? 0,
4456
4464
  bones: c.bones.map((b) => boneIds.get(b)),
@@ -4470,7 +4478,7 @@ function importSpine(json, atlasText, opts = {}) {
4470
4478
  mixScaleY: c.mixScaleY ?? c.mixScaleX ?? c.scaleMix ?? 1,
4471
4479
  mixShearY: c.mixShearY ?? c.shearMix ?? 1
4472
4480
  }));
4473
- const path = (json.path ?? []).filter((c) => activeConstraint("path", c)).map((c) => ({
4481
+ const path = (json2.path ?? []).filter((c) => activeConstraint("path", c)).map((c) => ({
4474
4482
  id: pcIds.get(c.name),
4475
4483
  order: c.order ?? 0,
4476
4484
  bones: c.bones.map((b) => boneIds.get(b)),
@@ -4524,7 +4532,7 @@ function importSpine(json, atlasText, opts = {}) {
4524
4532
  return keys;
4525
4533
  };
4526
4534
  const animations = [];
4527
- for (const [animName, a] of Object.entries(json.animations ?? {})) {
4535
+ for (const [animName, a] of Object.entries(json2.animations ?? {})) {
4528
4536
  const tracks = [], events = [];
4529
4537
  let duration = 0;
4530
4538
  const seen = (frames) => {
@@ -4805,7 +4813,7 @@ function importSpine(json, atlasText, opts = {}) {
4805
4813
  const build = () => ({
4806
4814
  format: "energy8.rig",
4807
4815
  version: 2,
4808
- meta: { name: opts.name ?? json.skeleton?.hash ?? "spine", width, height, notes: opts.notes ?? `imported from Spine ${version}` },
4816
+ meta: { name: opts.name ?? json2.skeleton?.hash ?? "spine", width, height, notes: opts.notes ?? `imported from Spine ${version}` },
4809
4817
  assets: [...assets.values()],
4810
4818
  bones,
4811
4819
  slots,
@@ -4858,7 +4866,7 @@ async function firstExisting(candidates, what) {
4858
4866
  throw new Error(`${what} not found, tried: ${candidates.join(", ")}`);
4859
4867
  }
4860
4868
  async function importSpineProject(opts) {
4861
- const json = JSON.parse(await readFile7(opts.json, "utf8"));
4869
+ const json2 = JSON.parse(await readFile7(opts.json, "utf8"));
4862
4870
  const stem = opts.json.replace(/\.json$/i, "");
4863
4871
  const atlasPath = opts.atlas ?? await firstExisting([stem + ".atlas", stem + ".atlas.txt"], "atlas");
4864
4872
  const atlasText = await readFile7(atlasPath, "utf8");
@@ -4868,6 +4876,7 @@ async function importSpineProject(opts) {
4868
4876
  const rigDir = dirname6(opts.rigPath);
4869
4877
  const pagePaths = /* @__PURE__ */ new Map();
4870
4878
  for (const page2 of pages) {
4879
+ if (!opts.image && outsideDir(dirname6(atlasPath), page2.name)) throw new Error(`atlas page "${page2.name}" is outside the atlas folder (${dirname6(atlasPath)})`);
4871
4880
  const pageStem = join8(dirname6(atlasPath), page2.name.replace(/\.[a-z0-9]+$/i, ""));
4872
4881
  const imagePath = opts.image ?? await firstExisting([join8(dirname6(atlasPath), page2.name), pageStem + ".png", pageStem + ".ktx2", ...pages.length === 1 ? [stem + ".png", stem + ".ktx2"] : []], `atlas page image ${page2.name}`);
4873
4882
  let pngPath = imagePath;
@@ -4880,10 +4889,10 @@ async function importSpineProject(opts) {
4880
4889
  }
4881
4890
  pagePaths.set(page2.name, pngPath);
4882
4891
  }
4883
- const pageImage = (page2) => relative(rigDir, pagePaths.get(page2)).split("\\").join("/");
4892
+ const pageImage = (page2) => relative2(rigDir, pagePaths.get(page2)).split("\\").join("/");
4884
4893
  const name = opts.name ?? basename4(stem);
4885
4894
  const imagePaths = [...pagePaths.values()];
4886
- return { ...importSpine(json, atlasText, { name, skin: opts.skin, pageImage, margin: opts.margin, notes: opts.notes }), imagePath: imagePaths[0], imagePaths };
4895
+ return { ...importSpine(json2, atlasText, { name, skin: opts.skin, pageImage, margin: opts.margin, notes: opts.notes }), imagePath: imagePaths[0], imagePaths };
4887
4896
  }
4888
4897
 
4889
4898
  // src/rig-templates.ts
@@ -5573,8 +5582,8 @@ async function load(path) {
5573
5582
  }
5574
5583
  var save = (path, doc) => saveJson(path, doc);
5575
5584
  var exists2 = (p) => stat2(p).then(() => true, () => false);
5576
- var resolveFrom = (rigPath, p) => isAbsolute(p) ? p : join9(dirname8(rigPath), p);
5577
- var srcFor = (rigPath, file) => relative2(dirname8(rigPath), file).split("\\").join("/");
5585
+ var resolveFrom = (rigPath, p) => isAbsolute2(p) ? p : join9(dirname8(rigPath), p);
5586
+ var srcFor = (rigPath, file) => relative3(dirname8(rigPath), file).split("\\").join("/");
5578
5587
  var libraryFor = (rigPath, lib) => lib ? resolveFrom(rigPath, lib) : join9(dirname8(rigPath), "presets.json");
5579
5588
  async function pngInfo(rigPath, file) {
5580
5589
  const abs = resolveFrom(rigPath, file);
@@ -5825,9 +5834,9 @@ tool(
5825
5834
  margin: z3.number().optional().describe("canvas margin around the skeleton bounds, px"),
5826
5835
  notes: z3.string().optional()
5827
5836
  },
5828
- async ({ path, json, atlas, image, name, skin: skin2, margin, notes }) => {
5837
+ async ({ path, json: json2, atlas, image, name, skin: skin2, margin, notes }) => {
5829
5838
  try {
5830
- const r = await importSpineProject({ json: resolveFrom(path, json), atlas: atlas && resolveFrom(path, atlas), image: image && resolveFrom(path, image), rigPath: path, name, skin: skin2, margin, notes });
5839
+ const r = await importSpineProject({ json: resolveFrom(path, json2), atlas: atlas && resolveFrom(path, atlas), image: image && resolveFrom(path, image), rigPath: path, name, skin: skin2, margin, notes });
5831
5840
  const res = await create(path, parse(r.doc));
5832
5841
  if (r.warnings.length) res.text += `
5833
5842
  warnings:
@@ -6599,40 +6608,41 @@ tool(
6599
6608
  }
6600
6609
  );
6601
6610
 
6611
+ // src/rig-version.ts
6612
+ import { stat as stat3 } from "node:fs/promises";
6613
+ async function rigVersion(abs) {
6614
+ try {
6615
+ const s2 = await stat3(abs);
6616
+ return `${s2.mtimeMs}:${s2.size}`;
6617
+ } catch {
6618
+ return "missing";
6619
+ }
6620
+ }
6621
+
6602
6622
  // src/editor/server.ts
6603
6623
  var TYPES = { ".html": "text/html; charset=utf-8", ".js": "text/javascript", ".css": "text/css", ".map": "application/json", ".json": "application/json", ".png": "image/png", ".jpg": "image/jpeg", ".webp": "image/webp" };
6604
- async function startEditorServer(opts) {
6624
+ var json = (res, status, body) => {
6625
+ res.writeHead(status, { "content-type": "application/json" });
6626
+ res.end(JSON.stringify(body));
6627
+ };
6628
+ function createEditorHandler(opts) {
6605
6629
  const root3 = resolve2(opts.root);
6606
6630
  const assets = resolve2(opts.assets ?? fileURLToPath3(new URL("../..", import.meta.url)));
6607
6631
  const inside = (rel) => {
6608
6632
  const abs = resolve2(root3, rel);
6609
- return abs === root3 || abs.startsWith(root3 + sep) ? abs : void 0;
6610
- };
6611
- const version = async (abs) => {
6612
- try {
6613
- const s2 = await stat3(abs);
6614
- return `${s2.mtimeMs}:${s2.size}`;
6615
- } catch {
6616
- return "missing";
6617
- }
6633
+ return abs === root3 || abs.startsWith(root3 + sep2) ? abs : void 0;
6618
6634
  };
6619
6635
  const readDoc = async (abs) => RigDocument.parse(JSON.parse(await readFile10(abs, "utf8")));
6620
6636
  const readDocAt = async (abs) => {
6621
- let v = await version(abs);
6637
+ let v = await rigVersion(abs);
6622
6638
  for (let i = 0; i < 3; i++) {
6623
6639
  const doc = await readDoc(abs);
6624
- const after = await version(abs);
6640
+ const after = await rigVersion(abs);
6625
6641
  if (after === v) return { doc, version: v };
6626
6642
  v = after;
6627
6643
  }
6628
6644
  return { doc: await readDoc(abs), version: v };
6629
6645
  };
6630
- const json = (res, status, body2) => {
6631
- res.writeHead(status, { "content-type": "application/json" });
6632
- res.end(JSON.stringify(body2));
6633
- };
6634
- let allowedHosts = /* @__PURE__ */ new Set();
6635
- const hostOk = (req) => allowedHosts.has(String(req.headers.host ?? "").toLowerCase());
6636
6646
  const clients = /* @__PURE__ */ new Map();
6637
6647
  const watchers = /* @__PURE__ */ new Map();
6638
6648
  function ensureWatch(abs) {
@@ -6644,7 +6654,7 @@ async function startEditorServer(opts) {
6644
6654
  const schedule = () => {
6645
6655
  clearTimeout(entry.timer);
6646
6656
  entry.timer = setTimeout(async () => {
6647
- const v = await version(abs);
6657
+ const v = await rigVersion(abs);
6648
6658
  if (v === entry.last) return;
6649
6659
  entry.last = v;
6650
6660
  for (const res of clients.get(abs) ?? []) res.write(`event: changed
@@ -6653,7 +6663,7 @@ data: ${JSON.stringify({ version: v })}
6653
6663
  `);
6654
6664
  }, 50);
6655
6665
  };
6656
- version(abs).then((v) => {
6666
+ rigVersion(abs).then((v) => {
6657
6667
  entry.last = v;
6658
6668
  });
6659
6669
  watchers.set(abs, entry);
@@ -6679,7 +6689,7 @@ data: ${JSON.stringify({ version: v })}
6679
6689
  name = String(JSON.parse(await readFile10(abs, "utf8")).meta?.name ?? "?");
6680
6690
  } catch {
6681
6691
  }
6682
- out.push({ rig: relative3(root3, abs).split(sep).join("/"), name, mtimeMs: (await stat3(abs)).mtimeMs });
6692
+ out.push({ rig: relative4(root3, abs).split(sep2).join("/"), name, mtimeMs: (await stat4(abs)).mtimeMs });
6683
6693
  }
6684
6694
  };
6685
6695
  await walk(root3);
@@ -6713,20 +6723,22 @@ data: ${JSON.stringify({ version: v })}
6713
6723
  }
6714
6724
  return Buffer.concat(chunks);
6715
6725
  }
6716
- const server = createServer(async (req, res) => {
6726
+ async function handle(req, res, rawUrl = req.url ?? "/") {
6717
6727
  try {
6718
- if (!hostOk(req)) return json(res, 403, { isError: true, text: `ERROR: forbidden Host "${req.headers.host ?? ""}" \u2014 this server answers only on 127.0.0.1 / localhost` });
6719
- const url = new URL(req.url ?? "/", "http://x");
6728
+ const url = new URL(rawUrl, "http://x");
6720
6729
  if (url.pathname === "/") {
6721
- res.writeHead(302, { location: "/editor.html" + url.search });
6722
- return res.end();
6730
+ res.writeHead(302, { location: "editor.html" + url.search });
6731
+ return void res.end();
6732
+ }
6733
+ if (url.pathname.startsWith("/api/")) {
6734
+ await api(url, req, res);
6735
+ return;
6723
6736
  }
6724
- if (url.pathname.startsWith("/api/")) return await api(url, req, res);
6725
6737
  const STATIC = /* @__PURE__ */ new Set(["/editor.html", "/dist/editor.js", "/dist/editor.js.map", "/dist/editor.css", "/dist/editor.css.map"]);
6726
6738
  const abs = url.pathname.startsWith("/rig/") ? inside(decodeURIComponent(url.pathname.slice(5))) : STATIC.has(url.pathname) ? join10(assets, url.pathname) : void 0;
6727
6739
  if (!abs) {
6728
6740
  res.writeHead(403);
6729
- return res.end("forbidden");
6741
+ return void res.end("forbidden");
6730
6742
  }
6731
6743
  try {
6732
6744
  const data = await readFile10(abs);
@@ -6744,7 +6756,7 @@ data: ${JSON.stringify({ version: v })}
6744
6756
  }
6745
6757
  json(res, 500, { isError: true, text: `ERROR: ${e.message}` });
6746
6758
  }
6747
- });
6759
+ }
6748
6760
  async function api(url, req, res) {
6749
6761
  const rigOf = (rel) => {
6750
6762
  if (!rel) return void 0;
@@ -6761,7 +6773,7 @@ data: ${JSON.stringify({ version: v })}
6761
6773
  res.writeHead(403);
6762
6774
  return res.end();
6763
6775
  }
6764
- if (!await stat3(abs).then(() => true, () => false)) return json(res, 404, { isError: true, text: `ERROR: ${url.searchParams.get("rig")} does not exist under ${root3}` });
6776
+ if (!await stat4(abs).then(() => true, () => false)) return json(res, 404, { isError: true, text: `ERROR: ${url.searchParams.get("rig")} does not exist under ${root3}` });
6765
6777
  ensureWatch(abs);
6766
6778
  res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache", connection: "keep-alive" });
6767
6779
  res.write(`: connected
@@ -6783,7 +6795,7 @@ data: ${JSON.stringify({ version: v })}
6783
6795
  if (!abs) return json(res, 403, { isError: true, text: "rig outside root" });
6784
6796
  ensureWatch(abs);
6785
6797
  return await withRigQueue(abs, async () => {
6786
- if (baseVersion !== void 0 && await version(abs) !== baseVersion) {
6798
+ if (baseVersion !== void 0 && await rigVersion(abs) !== baseVersion) {
6787
6799
  const at = await readDocAt(abs);
6788
6800
  return json(res, 409, {
6789
6801
  isError: true,
@@ -6792,8 +6804,15 @@ data: ${JSON.stringify({ version: v })}
6792
6804
  text: `ERROR: the rig changed on disk (expected ${baseVersion}, found ${at.version}); nothing was written`
6793
6805
  });
6794
6806
  }
6795
- const r = await callTool(url.pathname.slice(10), { ...args2 ?? {}, path: abs });
6796
- return json(res, 200, { text: r.text, doc: r.doc, image: r.image?.toString("base64"), isError: r.isError, version: await version(abs) });
6807
+ const tool2 = url.pathname.slice(10);
6808
+ let given = { ...args2 ?? {}, path: abs };
6809
+ try {
6810
+ if (opts.args) given = opts.args(tool2, given);
6811
+ } catch (e) {
6812
+ return json(res, 400, { isError: true, text: `ERROR: ${e instanceof Error ? e.message : String(e)}` });
6813
+ }
6814
+ const r = await callTool(tool2, given);
6815
+ return json(res, 200, { text: r.text, doc: r.doc, image: r.image?.toString("base64"), isError: r.isError, version: await rigVersion(abs) });
6797
6816
  });
6798
6817
  }
6799
6818
  if (url.pathname === "/api/rigs" && req.method === "GET") return json(res, 200, await listRigs());
@@ -6804,11 +6823,11 @@ data: ${JSON.stringify({ version: v })}
6804
6823
  if (typeof rig !== "string" || !/(^|\/)rig\.json$/.test(rig)) return json(res, 400, { isError: true, text: "ERROR: a project is a directory with a rig.json \u2014 the path must end with /rig.json" });
6805
6824
  const abs = inside(rig);
6806
6825
  if (!abs) return json(res, 403, { isError: true, text: "rig outside root" });
6807
- if (await stat3(abs).then(() => true, () => false)) return json(res, 409, { isError: true, text: `ERROR: ${rig} already exists \u2014 open it, or pick another directory` });
6826
+ if (await stat4(abs).then(() => true, () => false)) return json(res, 409, { isError: true, text: `ERROR: ${rig} already exists \u2014 open it, or pick another directory` });
6808
6827
  await mkdir7(dirname9(abs), { recursive: true });
6809
6828
  const r = await callTool("rig_create", { path: abs, name: name ?? basename5(dirname9(abs)), width: width ?? 640, height: height ?? 700 });
6810
6829
  ensureWatch(abs);
6811
- return json(res, r.isError ? 400 : 200, { text: r.text, doc: r.doc, isError: r.isError, version: await version(abs) });
6830
+ return json(res, r.isError ? 400 : 200, { text: r.text, doc: r.doc, isError: r.isError, version: await rigVersion(abs) });
6812
6831
  }
6813
6832
  if (url.pathname === "/api/files" && req.method === "GET") {
6814
6833
  const abs = rigOf(url.searchParams.get("rig"));
@@ -6822,8 +6841,8 @@ data: ${JSON.stringify({ version: v })}
6822
6841
  if (!/^[A-Za-z0-9._-]+\.png$/i.test(name) || name.startsWith(".")) return json(res, 400, { isError: true, text: `ERROR: bad file name "${name}" \u2014 letters, digits, . _ - and a .png extension` });
6823
6842
  if (!String(req.headers["content-type"] ?? "").toLowerCase().startsWith("image/png")) return json(res, 415, { isError: true, text: "ERROR: upload needs content-type: image/png" });
6824
6843
  const target = join10(dirname9(abs), name);
6825
- if (!inside(relative3(root3, target))) return json(res, 403, { isError: true, text: "file outside root" });
6826
- if (await stat3(target).then(() => true, () => false)) return json(res, 409, { isError: true, text: `ERROR: ${name} already exists next to the rig \u2014 rename the file` });
6844
+ if (!inside(relative4(root3, target))) return json(res, 403, { isError: true, text: "file outside root" });
6845
+ if (await stat4(target).then(() => true, () => false)) return json(res, 409, { isError: true, text: `ERROR: ${name} already exists next to the rig \u2014 rename the file` });
6827
6846
  const data = await bytes(req, UPLOAD_MAX);
6828
6847
  if (!data) return json(res, 413, { isError: true, text: `ERROR: ${name} is larger than ${UPLOAD_MAX / 1048576} MB` });
6829
6848
  let size;
@@ -6837,6 +6856,22 @@ data: ${JSON.stringify({ version: v })}
6837
6856
  }
6838
6857
  json(res, 404, { isError: true, text: "unknown api route" });
6839
6858
  }
6859
+ return { handle, close() {
6860
+ for (const w of watchers.values()) {
6861
+ clearTimeout(w.timer);
6862
+ w.w.close();
6863
+ }
6864
+ for (const set of clients.values()) for (const c of set) c.end();
6865
+ } };
6866
+ }
6867
+ async function startEditorServer(opts) {
6868
+ const editor = createEditorHandler(opts);
6869
+ let allowedHosts = /* @__PURE__ */ new Set();
6870
+ const server = createServer((req, res) => {
6871
+ if (!allowedHosts.has(String(req.headers.host ?? "").toLowerCase()))
6872
+ return json(res, 403, { isError: true, text: `ERROR: forbidden Host "${req.headers.host ?? ""}" \u2014 this server answers only on 127.0.0.1 / localhost` });
6873
+ void editor.handle(req, res);
6874
+ });
6840
6875
  await new Promise((ok, fail2) => {
6841
6876
  server.once("error", fail2);
6842
6877
  server.listen(opts.port ?? 8765, "127.0.0.1", () => {
@@ -6847,11 +6882,7 @@ data: ${JSON.stringify({ version: v })}
6847
6882
  const port2 = server.address().port;
6848
6883
  allowedHosts = /* @__PURE__ */ new Set([`127.0.0.1:${port2}`, `localhost:${port2}`, `[::1]:${port2}`]);
6849
6884
  return { port: port2, url: `http://127.0.0.1:${port2}`, close: async () => {
6850
- for (const w of watchers.values()) {
6851
- clearTimeout(w.timer);
6852
- w.w.close();
6853
- }
6854
- for (const set of clients.values()) for (const c of set) c.end();
6885
+ editor.close();
6855
6886
  await new Promise((r) => server.close(() => r()));
6856
6887
  } };
6857
6888
  }