pixie-mcp 0.1.0 → 0.2.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.
package/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # pixie-mcp
2
2
 
3
- The [Sprite](https://drawpixie.com) MCP server: ask for a sprite in words and it
3
+ The [Pixie](https://drawpixie.com) MCP server: ask for a sprite in words and it
4
4
  appears on a real layer, editable, on the undo stack.
5
5
 
6
6
  ```sh
7
- claude mcp add pixie -- npx -y pixie-mcp
7
+ claude mcp add -s user pixie -- npx -y pixie-mcp
8
8
  npx pixie-mcp login
9
9
  ```
10
10
 
@@ -28,9 +28,9 @@ not share. You can change that, or take the credential back, from
28
28
  In this order:
29
29
 
30
30
  1. `--server ws://…` or `--editor http://…`, if you say which.
31
- 2. **drawpixie.com** — the Sprite this copy came from, which is where your
31
+ 2. **drawpixie.com** — the Pixie this copy came from, which is where your
32
32
  sprites are. This is the ordinary case and needs nothing said.
33
- 3. A local editor answering on `http://127.0.0.1:4242`, for a Sprite running on
33
+ 3. A local editor answering on `http://127.0.0.1:4242`, for a Pixie running on
34
34
  this machine.
35
35
  4. A room server answering on `ws://127.0.0.1:4243`, and failing that, becoming
36
36
  one in this process.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pixie-mcp",
3
- "version": "0.1.0",
4
- "description": "MCP server for Sprite — draw pixel art from the AI client you already pay for. Holds no API key and bills nobody.",
3
+ "version": "0.2.0",
4
+ "description": "MCP server for Pixie — draw pixel art from the AI client you already pay for. Holds no API key and bills nobody.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "pixie-mcp": "pixie-mcp.mjs"
package/pixie-mcp.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- var k=(n,e)=>()=>(n&&(e=n(n=0)),e);var ie,Ve,ze,qt,ae=k(()=>{ie="[A-Za-z0-9_-]{1,64}",Ve="token|members|link|slow|project",ze=[{owner:"accounts",re:new RegExp("^/auth/")},{owner:"accounts",re:new RegExp("^/a/[A-Za-z0-9_-]{1,64}\\.png$")},{owner:"accounts",re:new RegExp("^/projects(/|$)")},{owner:"accounts",re:new RegExp(`^/documents/${ie}/(${Ve})$`)},{owner:"library",re:new RegExp("^/documents$")},{owner:"library",re:new RegExp("^/documents/(new|register|move|order|folders|folders/delete)$")},{owner:"library",re:new RegExp(`^/documents/${ie}(/(thumbnail\\.png|duplicate|rename))?$`)}],qt=ze.map(({owner:n,re:e})=>({owner:n,source:e.source}))});var q,T=k(()=>{ae();q=["read","draw","create"]});function ce(n){let e="";for(let t=0;t<n.length;t+=32768)e+=String.fromCharCode.apply(null,n.subarray(t,t+32768));return btoa(e).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}var Gt,Kt,le=k(()=>{T();Gt=336*60*60*1e3,Kt=2160*60*60*1e3});async function he(n){let e=new TextEncoder().encode(String(n));return ce(new Uint8Array(await crypto.subtle.digest("SHA-256",e)))}var Qt,ue=k(()=>{le();T();Qt=120*1e3});function E(n=Date.now()){let e="0123456789ABCDEFGHJKMNPQRSTVWXYZ",t=(BigInt(n)&0xffffffffffffn)<<80n,r=crypto.getRandomValues(new Uint8Array(10)),o=0n;for(let i of r)o=o<<8n|BigInt(i);t|=o;let s="";for(let i=0;i<26;i++)s=e[Number(t&31n)]+s,t>>=5n;return s}function x(n){return n==null?{at:0}:typeof n=="number"?{at:n}:{frame:n}}var tt,nt,rt,O,_,H,P=k(()=>{tt=new TextEncoder,nt=new TextDecoder,rt=1;O=class n{#e;#t;constructor(e){this.#e=e,this.#t=e.exports}static async load(e){let t;if(e instanceof WebAssembly.Module)t=await WebAssembly.instantiate(e,{});else if(e instanceof ArrayBuffer||ArrayBuffer.isView(e))({instance:t}=await WebAssembly.instantiate(e,{}));else{let r=e instanceof Response||typeof e?.then=="function"?e:fetch(e);({instance:t}=await WebAssembly.instantiateStreaming(r,{}))}return new n(t)}get#n(){return new Uint8Array(this.#t.memory.buffer)}call(e,t={}){let r=tt.encode(JSON.stringify({m:e,a:t})),o=this.#t.sprite_alloc(r.length);if(!o)throw new Error("sprite-core: allocation failed");let s=0;try{if(this.#n.set(r,o),s=this.#t.sprite_dispatch(o,r.length),!s)throw new Error("sprite-core: dispatch returned nothing");let i=this.#n,a=i[s]|i[s+1]<<8|i[s+2]<<16|i[s+3]<<24,c=i[s+4],l=i.slice(s+5,s+5+a);if(c===rt)return l;let h=JSON.parse(nt.decode(l));if(h&&h.error)throw new Error(h.error);return h}finally{s?this.#t.sprite_free_result(s):this.#t.sprite_dealloc(o,r.length)}}reset(){this.#t.sprite_reset()}newDocument(e={}){return this.call("doc.new",{w:32,h:32,name:"untitled",...e})}openSession(e,t,r=1){let{h:o}=this.call("session.new",{doc:e,author:t,seed:r});return new _(this,o)}openRoom(e){let{h:t}=this.call("room.new",{doc:e});return new H(this,t)}},_=class{constructor(e,t){this.core=e,this.h=t}#e(e,t={}){return this.core.call(e,{h:this.h,...t})}state(){return this.#e("session.state")}apply(e,{now:t=Date.now(),label:r="Edit",job:o=null}={}){let s={now:t,label:r,bodies:Array.isArray(e)?e:[e]};return o&&(s.job=o),this.#e("session.apply",s).ops}remote(e){return this.#e("session.remote",{sop:e})}reject(e){return this.#e("session.reject",{op:e})}undo(e=Date.now()){return this.#e("session.undo",{now:e}).ops}redo(e=Date.now()){return this.#e("session.redo",{now:e}).ops}pending(){return this.#e("session.pending")}checkpoint({doc:e=!0}={}){return this.#e("session.checkpoint",{doc:e})}save(){return this.#e("session.save")}drawArt(e,t,r={}){return this.#e("session.drawArt",{rows:e,colors:t,...r}).bodies}drawAnimation(e,t,r={}){return this.#e("session.drawAnimation",{frames:e,colors:t,...r}).bodies}readArt(e,t,r={}){return this.#e("session.readArt",{layer:e,...x(t),...r})}cel(e,t){return this.#e("session.cel",{layer:e,...x(t)})}layerImage(e,t){return this.#e("session.layerImage",{layer:e,...x(t)})}tileCel(e,t){return this.#e("session.tileCel",{layer:e,...x(t)})}tile(e,t){return this.#e("session.tile",{tileset:e,index:t})}composite(e,{include:t="export"}={}){return this.#e("session.composite",{...x(e),include:t})}png(e){return this.#e("session.png",x(e))}contactSheet({columns:e=4,scale:t=2}={}){return this.#e("session.sheet",{columns:e,scale:t})}thumbnail(e=48){return this.#e("session.thumbnail",{size:e})}card({title:e=null,frame:t=null,width:r=1200,height:o=630}={}){return this.#e("session.card",{title:e,width:r,height:o,...x(t)})}close(){this.#e("session.free")}},H=class{constructor(e,t){this.core=e,this.h=t}#e(e,t={}){return this.core.call(e,{h:this.h,...t})}state(){return this.#e("room.state")}submit(e,t=Date.now()){return this.#e("room.submit",{op:e,now:t})}since(e){return this.#e("room.since",{seq:e})}compact(){return this.#e("room.compact")}save(){return this.#e("room.save")}lease(e,t){return this.#e("room.lease",{action:e,...t})}close(){this.#e("room.free")}}});function be(n){return JSON.stringify(n)}function Ee(n){let e=n.rle instanceof Uint8Array?n.rle:null,t=new TextEncoder().encode(JSON.stringify({...n,rle:void 0}));if(t.length>ot)return null;let r=new Uint8Array(3+t.length+(e?.length??0));return r[0]=Se,r[1]=t.length&255,r[2]=t.length>>8&255,r.set(t,3),e&&r.set(e,3+t.length),r}function st(n){return n instanceof Uint8Array?n:n instanceof ArrayBuffer?new Uint8Array(n):ArrayBuffer.isView(n)?new Uint8Array(n.buffer,n.byteOffset,n.byteLength):null}function xe(n){if(typeof n!="string"){let e=st(n);if(!e||e.length<3||e[0]!==Se)return null;let t=e[1]|e[2]<<8;if(e.length<3+t)return null;try{let r=JSON.parse(new TextDecoder().decode(e.subarray(3,3+t)));if(!r||typeof r.t!="string")return null;let o=e.subarray(3+t);return r.rle=o.length?Uint8Array.from(o):null,r}catch{return null}}try{let e=JSON.parse(n);return e&&typeof e.t=="string"?e:null}catch{return null}}var g,ye,b,ge,Se,ot,ke=k(()=>{g={HELLO:"hello",SUBMIT:"submit",PRESENCE:"presence",STROKE:"stroke",LEASE:"lease",PING:"ping",PROMPT:"prompt",ANSWER:"answer",STOP:"stop"},ye={DOCUMENT:"document",LIBRARY:"library"},b={WELCOME:"welcome",REHELLO:"rehello",OPS:"ops",REJECT:"reject",PRESENCE:"presence",STROKE:"stroke",LEASE:"lease",ERROR:"error",PONG:"pong",PROMPT:"prompt",ANSWER:"answer",STOP:"stop"},ge={OPS:"ops",SNAPSHOT:"snapshot"};Se=157,ot=65535});import{readFile as _t}from"node:fs/promises";import{readFile as Ue,writeFile as De,mkdir as Be,chmod as Me}from"node:fs/promises";import{readFileSync as Fe}from"node:fs";import{homedir as qe,hostname as He}from"node:os";import{join as B}from"node:path";import{execFile as Je}from"node:child_process";var p=n=>String(n??"").replace(/\/+$/,"").replace(/^ws/,"http"),G=n=>String(n??"").replace(/\/+$/,"").replace(/^http/,"ws");async function K(n,e){if(!n?.ok||!(n.headers.get("content-type")??"").includes("json"))return null;let t=await n.json().catch(()=>null);return!t||typeof t!="object"?null:e.some(r=>r in t)?t:null}async function X(n,{timeoutMs:e=3e3,fetchImpl:t=fetch}={}){try{let r=await t(`${p(n)}/setup`,{signal:AbortSignal.timeout(e)});return await K(r,["accounts","expectedPort","install"])}catch{return null}}var D=new Map;function Y(n,{fetchImpl:e=fetch,timeoutMs:t=3e3}={}){let r=p(n);return D.has(r)||D.set(r,Ce(n,{fetchImpl:e,timeoutMs:t})),D.get(r)}async function Ce(n,{fetchImpl:e,timeoutMs:t}){try{let r=await e(`${p(n)}/config.json`,{signal:AbortSignal.timeout(t)}),s=(await K(r,["rooms","accounts"]))?.rooms;if(typeof s=="string"&&/^(wss?|https?):\/\/./.test(s))return G(s)}catch{}return G(n)}var v=process.env.SPRITE_PUBLIC_SERVER||We()||null;function We(){return"wss://drawpixie.com"}var Z=B(process.env.XDG_CONFIG_HOME||B(qe(),".config"),"sprite"),M=B(Z,"agent.json"),Q=n=>String(n??"").replace(/\/+$/,"");function ee(){return`${He().replace(/\.local$/,"")}`.slice(0,60)||"a computer"}async function te(){try{let n=JSON.parse(await Ue(M,"utf8"));return n&&typeof n=="object"&&n.servers?n:{servers:{}}}catch{return{servers:{}}}}async function F(n){return process.env.SPRITE_TOKEN?{token:process.env.SPRITE_TOKEN,from:"the environment"}:(await te()).servers[Q(n)]??null}async function ne(n,e){await Be(Z,{recursive:!0,mode:448});let t=await te();t.servers[Q(n)]={...e,at:Date.now()};let r=`${M}.tmp`;await De(r,JSON.stringify(t,null,2),{mode:384}),await Me(r,384);let{rename:o}=await import("node:fs/promises");await o(r,M)}async function re(n,{timeoutMs:e=3e3}={}){try{let t=await fetch(`${p(n)}/setup`,{signal:AbortSignal.timeout(e)});return t.ok?!!(await t.json()).accounts:!1}catch{return!1}}async function oe(n,e,{session:t=null}={}){let r=t??await F(n);if(!r?.token)return;let o=await fetch(`${p(n)}/documents/${encodeURIComponent(e)}/token`,{headers:{Authorization:`Bearer ${r.token}`}});if(o.status===401)throw new Error("this machine is signed out \u2014 pair it again");if(o.status===403)throw new Error(`no access to ${e}`);if(!o.ok)throw new Error(`could not get access to ${e}`);return(await o.json()).token}function se(n){let e=process.platform==="darwin"?"open":process.platform==="win32"?"start":"xdg-open";return new Promise(t=>Je(e,[n],()=>t()))}ue();T();import{createServer as Ke}from"node:http";import{randomBytes as pe}from"node:crypto";var Xe=300*1e3,Ye=()=>pe(32).toString("base64url"),Ze=()=>pe(16).toString("base64url");function de(n,e){return`<!doctype html><meta charset="utf-8"><title>Sprite</title><body style="background:#101014;color:#d8d8e0;font:14px/1.6 system-ui,sans-serif;display:grid;place-items:center;height:100vh;margin:0;text-align:center"><div><p style="font-size:16px">${n}</p><p style="color:#8a8a99;font-size:12px">${e}</p></div>`}async function Qe({state:n,timeoutMs:e}){let t,r,o=new Promise((c,l)=>{t=c,r=l}),s=Ke((c,l)=>{let h=new URL(c.url,"http://127.0.0.1");if(h.pathname!=="/callback"){l.writeHead(404).end();return}if(h.searchParams.get("state")!==n){l.writeHead(404).end();return}let u=h.searchParams.get("error");l.writeHead(200,{"Content-Type":"text/html; charset=utf-8"}).end(u?de("Nothing was granted.","You can close this window."):de("This computer can now draw in your sprites.","You can close this window and go back to what you were doing.")),t(h.searchParams)});await new Promise((c,l)=>{s.once("error",l),s.listen(0,"127.0.0.1",c)});let i=setTimeout(()=>r(new Error("nobody answered the browser in time")),e);i.unref?.();let{port:a}=s.address();return{redirect:`http://127.0.0.1:${a}/callback`,wait:o,close:()=>{clearTimeout(i),s.close(),s.closeAllConnections?.()}}}async function me(n,{onUrl:e=()=>{},open:t=se,label:r="the Sprite agent",scope:o=q,timeoutMs:s=Xe}={}){let i=Ye(),a=await he(i),c=Ze(),l=await Qe({state:c,timeoutMs:s});try{let h=new URL(`${p(n)}/authorize`);h.searchParams.set("challenge",a),h.searchParams.set("state",c),h.searchParams.set("redirect",l.redirect),h.searchParams.set("scope",o.join(",")),h.searchParams.set("name",r),e(h.toString()),await t(h.toString());let u=await l.wait,d=u.get("error");if(d)throw new Error(d==="access_denied"?"you said no on that screen, so nothing was granted":`that request was refused: ${d}`);let m=u.get("code");if(!m)throw new Error("the browser came back without an authorisation");let w=await fetch(`${p(n)}/auth/authorize/exchange`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:m,verifier:i,name:ee()})}),f=await w.json().catch(()=>({}));if(!w.ok||!f.token)throw new Error(f.error||"that authorisation could not be exchanged");return await ne(n,{token:f.token,name:f.name,email:f.email,scope:f.scope??null}),f}finally{l.close()}}import{realpathSync as fe}from"node:fs";import{fileURLToPath as et}from"node:url";function we(n){let e=process.argv[1];if(!e)return!1;try{return fe(et(n))===fe(e)}catch{return!1}}P();import{execFile as ct}from"node:child_process";ke();P();var it=5e3,I=class{#e=null;constructor(e,t,r){if(!r||!r.author)throw new Error("DocumentClient needs an author");if(this.core=e,this.author=r.author,this.name=r.name||"anonymous",this.agent=r.agent===!0,this.answers=r.answers===!0,this.seed=r.seed??1,this.epoch=0,this.resumed=null,r.resume){let{h:o,dropped:s}=e.call("session.resume",{doc:t,author:this.author,seed:this.seed,seq:r.resume.seq??0,pending:r.resume.pending??[]});this.session=new _(e,o),this.resumed={seq:r.resume.seq??0,dropped:s??[]}}else this.session=e.openSession(t,this.author,this.seed);this.getToken=r.getToken??null,this.transport=null,this.connected=!1,this.welcomed=!1,this.ready=new Promise(o=>{this.#e=o}),this.participants=[],this.leases=[],this.you=null,this.onChange=()=>{},this.onPresence=()=>{},this.onStroke=()=>{},this.strokePreview=!0,this.onLeases=()=>{},this.onLost=()=>{},this.onPrompt=()=>{},this.onAnswer=()=>{},this.onStop=()=>{},this.onStatus=()=>{}}apply(e,t={}){let r=this.session.apply(e,t);return this.#s(r),this.onChange(),r}undo(e=Date.now()){let t=this.session.undo(e);return this.#s(t),this.onChange(),t}redo(e=Date.now()){let t=this.session.redo(e);return this.#s(t),this.onChange(),t}setPresence(e){this.connected&&this.#o({t:g.PRESENCE,...e})}sendStroke({layer:e,frame:t,rect:r,rle:o,done:s=!1}){if(!this.connected||!this.welcomed||!this.strokePreview)return;this._strokeSentAt=Date.now();let i=Ee({t:g.STROKE,layer:e,frame:t,rect:r,rle:o,done:s});i&&this.#a(i)}lease(e,t={}){this.#o({t:g.LEASE,action:e,...t})}prompt(e,{where:t=ye.DOCUMENT}={}){if(!this.connected)return null;let r=String(e??"").trim().slice(0,2e3);if(!r)return null;let o=E();return this.#o({t:g.PROMPT,id:o,text:r,where:t}),o}answer(e,t="",{reply:r=null,made:o=null,model:s=null}={}){return this.connected?(this.#o({t:g.ANSWER,state:e,text:String(t??"").slice(0,2e3),reply:r,made:o,model:s}),!0):!1}stopAnswer(e){return!this.connected||!e?!1:(this.#o({t:g.STOP,id:e}),!0)}get state(){return this.session.state()}checkpoint({doc:e=!0}={}){return{epoch:this.epoch,author:this.author,...this.session.checkpoint({doc:e})}}composite(e){return this.session.composite(e)}connect(e){return this.transport=e,e.onopen=async()=>{this.connected=!0,this.welcomed=!1,clearInterval(this.heartbeat),this.heartbeat=setInterval(()=>{this.connected&&this.#o({t:g.PING})},it),await this.#t()},e.onmessage=t=>this.#n(t),e.onclose=()=>{this.connected=!1,this.welcomed=!1,clearInterval(this.heartbeat),this.onStatus({connected:!1})},e.readyState===1&&e.onopen(),this}disconnect(){if(this.transport){this.connected=!1,this.welcomed=!1,clearInterval(this.heartbeat);try{this.transport.close()}catch{}this.transport=null,this.onStatus({connected:!1})}}async#t(){let e=this.getToken?await this.getToken():void 0,t=this.session.state();this.#o({t:g.HELLO,author:this.author,name:this.name,token:e,since:t.seq,...this.agent?{agent:!0}:{},...this.answers?{answers:!0}:{},docId:t.doc.id})}#n(e){let t=xe(e);if(t)switch(t.t){case b.WELCOME:return this.#r(t);case b.REHELLO:return this.welcomed=!1,void this.#t();case b.OPS:return this.#c(t);case b.REJECT:return this.#l(t);case b.PRESENCE:return this.participants=t.participants||[],this.onPresence(this.participants);case b.STROKE:return this.onStroke(t);case b.LEASE:return this.leases=t.leases||[],this.onLeases(this.leases);case b.PROMPT:return this.onPrompt(t);case b.ANSWER:return this.onAnswer(t);case b.STOP:return this.onStop(t);case b.ERROR:if(this.#h(t.message)){this.strokePreview=!1;return}return this.onLost({kind:"protocol",message:t.message});default:return}}#r(e){this.you=e.you,this.welcomed=!0;let t=this.session.pending();if(e.catchup.kind===ge.SNAPSHOT){let{h:r,dropped:o}=this.core.call("session.resume",{doc:e.catchup.doc,author:this.author,seed:this.seed,seq:e.catchup.seq,pending:t});this.session.close(),this.session=new _(this.core,r),this.epoch++,o&&o.length&&this.onLost({kind:"rebased",ops:o,message:`${o.length} edit${o.length>1?"s":""} could not be replayed onto the current document`})}else for(let r of e.catchup.ops||[])this.#i(r);this.participants=e.participants||[],this.leases=e.leases||[],t.length&&this.#o({t:g.SUBMIT,ops:t}),this.onStatus({connected:!0,resent:t.length}),this.onPresence(this.participants),this.onLeases(this.leases),this.onChange(),this.#e?.(),this.#e=null}#c(e){let t=!1;for(let r of e.ops||[])this.#i(r),t=!0;t&&this.onChange()}#i(e){let t=this.session.remote(e);t.dropped&&t.dropped.length&&this.onLost({kind:"obsolete",ops:t.dropped,message:`${t.dropped.length} of your edits no longer apply and were dropped`})}#l(e){e.opId&&this.session.reject(e.opId),this.onLost({kind:e.kind,ops:e.opId?[e.opId]:[],message:e.message}),this.onChange()}#s(e){!e.length||!this.connected||!this.welcomed||this.#o({t:g.SUBMIT,ops:e})}#o(e){this.#a(be(e))}#h(e){return typeof e!="string"?!1:e===`unknown message: ${g.STROKE}`?!0:e==="unreadable message"&&Date.now()-(this._strokeSentAt??0)<2e3}#a(e){if(this.transport)try{this.transport.send(e)}catch{this.connected=!1}}close(){this.disconnect(),this.session.close()}};P();function at(n){try{return/^(localhost|127\.0\.0\.1|\[::1\]|::1)$/.test(new URL(p(n)).hostname)}catch{return!1}}function _e(n,{publicServer:e=v}={}){let t=p(n);return e&&n===e?`nothing is answering at ${t} \u2014 this machine is offline, or the site is down`:at(n)?`nothing is answering at ${t} \u2014 start the editor with \`npm start\` in the Sprite repository, then try again`:`nothing is answering at ${t} \u2014 check the address and that whatever serves it is up`}var lt=6e3,N=class{constructor(e,t){this.core=e,this.author=t.author,this.name=t.name||"agent",this.connectTransport=t.connect,this.getToken=t.getToken??null,this.rooms=new Map,this.asked=[],this.onAsked=null,this.jobs=new Map,this.log=[]}async open(e,{w:t=32,h:r=32,name:o=e}={}){let s=this.rooms.get(e);if(s)return s.client;let i=this.core.newDocument({w:t,h:r,name:o,seed:Re(),now:Date.now()}),a=new I(this.core,i,{author:this.author,name:this.name,seed:Re(),agent:!0,getToken:this.getToken?()=>this.getToken(e):null});a.onLost=u=>{this.lastRefusal=u},a.onPrompt=u=>{if(u.author===this.author)return;let d=u.where==="library";this.asked.push({text:u.text,from:u.name,at:u.at,wants:d?"a new sprite":"a change to this one",room:d?null:e,askedIn:e}),this.asked.length>32&&this.asked.shift(),this.onAsked?.()};let c=this.connectTransport(e,{w:t,h:r,name:o}),l=!1;return await new Promise((u,d)=>{let m=setTimeout(()=>d(new Error(`timed out joining ${e}`)),1e4);a.onStatus=f=>{f.connected&&(l=!0,clearTimeout(m),u())},a.connect(c);let w=c.onclose;c.onclose=()=>{w?.(),!l&&(clearTimeout(m),d(new Error(this.serverUrl?_e(this.serverUrl):"nothing is answering at the room server")))}}),this.rooms.set(e,{client:a}),a}client(e){let t=this.rooms.get(e);if(!t)throw new Error(`not in room ${e}`);return t.client}async peek(e){return this.rooms.has(e)?this.client(e):this.open(e)}async begin(e,t,{layer:r=null,open:o={}}={}){let s=await this.open(e,o),i=E();if(s.lease("acquire",{job:i,intent:t,...r?{layers:[r]}:{}}),await je(s),!s.leases.find(l=>l.job===i)){let l=s.leases[0];throw new Error(l?`"${e}" is already held by another job: ${l.intent}`:`could not claim "${e}"`)}let c={room:e,layer:r,intent:t,cancelled:!1,timer:setInterval(()=>{this.jobs.has(i)&&s.lease("renew",{job:i})},lt)};return c.timer.unref&&c.timer.unref(),this.jobs.set(i,c),this.note("begin_job",e,t),i}resolve(e){if(e){let o=this.jobs.get(e);if(!o)throw new Error(`unknown job ${e} \u2014 it may have finished`);if(o.cancelled)throw new Error("the user stopped you \u2014 do not retry; say what you were about to do and ask");return{job:e,...o}}if(this.jobs.size===0)throw new Error("no job is open \u2014 call new_sprite or begin_job first");if(this.jobs.size>1)throw new Error(`${this.jobs.size} jobs are open \u2014 pass the job argument`);let[t,r]=[...this.jobs.entries()][0];return{job:t,...r}}finish(e){let t=this.jobs.get(e);t&&(clearInterval(t.timer),this.jobs.delete(e),this.client(t.room).lease("release",{job:e}),this.note("finish_job",t.room,t.intent))}async submit(e,t,r){let o=this.resolve(e),s=this.client(o.room);if(!r.length)return{ops:0};if(this.lastRefusal=null,s.apply(r,{label:t,job:o.job,now:Date.now()}),await je(s),this.lastRefusal){let i=this.lastRefusal.message;throw this.lastRefusal.kind==="revoked"&&(o.cancelled=!0),this.lastRefusal=null,new Error(i)}return this.note(t,o.room,`${r.length} op${r.length>1?"s":""}`),{ops:r.length,seq:s.state.seq}}cancel(e){let t=this.jobs.get(e);t&&(t.cancelled=!0)}note(e,t,r){this.log.unshift({at:Date.now(),tool:e,room:t,detail:r}),this.log.length>200&&this.log.pop()}announce(){if(!this.serverUrl)return;let e=`${this.serverUrl.replace(/^ws/,"http").replace(/\/$/,"")}/agent`,t=()=>{fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:this.name})}).catch(()=>{})};t(),clearInterval(this.heartbeat),this.heartbeat=setInterval(t,15e3),this.heartbeat.unref?.()}close(){clearInterval(this.heartbeat);for(let[e]of this.jobs)this.finish(e);for(let{client:e}of this.rooms.values())e.close();this.rooms.clear()}get kind(){return"room"}get where(){return this.serverUrl??"a room server"}supports(e){return e==="open_editor"?!!this.editorUrl:ht.has(e)}async openEditor({document:e,job:t}={}){if(!this.editorUrl)return{ok:!1,message:"there is no editor to open from here"};let r=this.editorUrl;try{r=`${this.editorUrl}/d/${this.#t(e,t)}`}catch{r=`${this.editorUrl}/library`}return await dt(r),{ok:!0,message:`Opened ${r}`}}async newSprite({name:e,w:t=32,h:r=32,intent:o,palette:s}){let i=String(e??"").replace(/[^A-Za-z0-9_-]/g,"-").slice(0,64)||"sprite",a=await this.begin(i,o,{open:{w:t,h:r,name:e}});return s?.length&&await this.submit(a,"set_palette",[{t:"setPalette",colors:s}]),this.#e(i,o),{job:a,...this.#n(i)}}async#e(e,t){let r=process.env.SPRITE_ASKED_IN,o=process.env.SPRITE_REPLY_TO??null;if(r)try{(await this.open(r)).answer("thinking",t?String(t).slice(0,200):"making a new sprite",{reply:o,made:e})}catch{}}async beginJob({document:e,intent:t,layer:r=null}){return{job:await this.begin(e,t,{layer:r}),...this.#n(String(e))}}finishJob({job:e}){let t=this.resolve(e);return this.finish(t.job),{finished:t.intent,document:t.room}}#t(e,t){return e?String(e):this.resolve(t).room}async info({document:e,job:t}={}){let r=this.#t(e,t);return await this.peek(r),this.#n(r)}#n(e){let t=this.client(e),r=t.state;return{document:e,name:r.doc.name,w:r.doc.w,h:r.doc.h,layers:r.doc.layers.map(o=>o.name),frames:r.doc.frames.length,palette:r.doc.palette,tags:r.doc.tags.map(o=>o.name),seq:r.seq,participants:t.participants.map(o=>o.name)}}async readArt({document:e,job:t,layer:r,frame:o=0,trim:s=!1}){let i=this.#t(e,t),a=await this.peek(i);return{document:i,...a.session.readArt(this.#r(i,r),o,{trim:s})}}async preview({document:e,job:t,frame:r=0}){let o=await this.peek(this.#t(e,t)),s=this.#t(e,t);return{png:o.session.png(r),caption:`${s}, frame ${r+1}`}}async sheet({document:e,job:t,columns:r=4,scale:o=2}){let s=this.#t(e,t),i=await this.peek(s);return{png:i.session.contactSheet({columns:r,scale:o}),caption:`${s}, ${i.state.doc.frames.length} frames, numbered left to right`}}async documents(){let e=[...this.jobs.entries()].map(([s,i])=>({job:s,document:i.room,intent:i.intent})),t=[...this.rooms.keys()],r=null;if(this.serverUrl){let s=this.serverUrl.replace(/^ws/,"http").replace(/\/$/,"");r=await fetch(`${s}/documents`).then(i=>i.ok?i.json():null).then(i=>i?.documents?.map(a=>a.id)).catch(()=>null)}return{documents:r?[...new Set([...t,...r])]:t,jobs:e,open:t}}takeRequests(){let e=this.asked.splice(0,this.asked.length);return{requests:e,note:e.length?"Each says what it wants. Where `room` is set, answer in that document. Where it is null the request came from the library and wants a new sprite \u2014 call new_sprite and work there, and do not touch `askedIn`, which is only the room the words travelled through. The person is watching.":"Nobody has asked for anything."}}#r(e,t){let r=this.client(e).state;if(t!=null&&t!==""){let o=typeof t=="number"?r.doc.layers[t]:r.doc.layers.find(s=>s.name===t||s.id===t);if(!o)throw new Error(`no layer ${JSON.stringify(t)} in ${e}`);return o.id}return r.doc.layers[r.doc.layers.length-1].id}async drawArt({job:e,rows:t,colors:r,x:o=0,y:s=0,center:i=!1,clear:a=!1,layer:c,frame:l=0,label:h}){let u=this.resolve(e),m=this.client(u.room).session.drawArt(t,r,{layer:c!=null?this.#r(u.room,c):u.layer??this.#r(u.room,null),at:l,x:o,y:s,center:i,clear:a});return await this.submit(u.job,h||"draw_art",m),{...this.#n(u.room),drew:`${t[0]?.length??0}\xD7${t.length}`}}async drawAnimation({job:e,frames:t,colors:r,fps:o,layer:s,label:i}){let a=this.resolve(e),c=this.client(a.room),l=s!=null?this.#r(a.room,s):a.layer??this.#r(a.room,null),h=c.state.doc.frames.length,u=t.length-h;if(u>0){let m=[],w=c.state.doc.frames[h-1]?.index??"V";for(let f=0;f<u;f++)w=w+"V",m.push({t:"addFrame",id:E(),index:w,durMs:J(o)});await this.submit(a.job,"add frames",m)}if(o){let m=J(o);await this.submit(a.job,"set fps",c.state.doc.frames.map(w=>({t:"setFrameDur",id:w.id,durMs:m})))}let d=c.session.drawAnimation(t,r,{layer:l});return await this.submit(a.job,i||"draw_animation",d),{...this.#n(a.room),wrote:`${t.length} frames`}}async addLayer({job:e,name:t}){let r=this.resolve(e),o=this.client(r.room).state,s=o.doc.layers[o.doc.layers.length-1];return await this.submit(r.job,"add_layer",[{t:"addLayer",id:E(),index:(s?.index??"V")+"V",name:t||`Layer ${o.doc.layers.length+1}`,visible:!0,locked:!1,opacity:1}]),this.#n(r.room)}async addFrame({job:e,copy:t=!1}){let r=this.resolve(e),o=this.client(r.room),s=o.state,i=s.doc.frames[s.doc.frames.length-1],a=E(),c=[{t:"addFrame",id:a,index:(i?.index??"V")+"V",durMs:i?.durMs??83}];if(t&&i)for(let l of s.doc.layers){let h=o.session.cel(l.id,i.id);h.every(u=>u===0)||c.push(ve(l.id,a,s.doc,h))}return await this.submit(r.job,"add_frame",c),this.#n(r.room)}async setPalette({job:e,colors:t}){let r=this.resolve(e);return await this.submit(r.job,"set_palette",[{t:"setPalette",colors:t}]),{...this.#n(r.room),palette:t.length}}async setFps({job:e,fps:t}){let r=this.resolve(e),o=this.client(r.room).state,s=J(t);return await this.submit(r.job,"set_fps",o.doc.frames.map(i=>({t:"setFrameDur",id:i.id,durMs:s}))),{...this.#n(r.room),fps:t}}async clearCel({job:e,layer:t,frame:r=0}){let o=this.resolve(e),i=this.client(o.room).state,a=this.#r(o.room,t??o.layer),c=i.doc.frames[r];if(!c)throw new Error(`no frame ${r} in ${o.room}`);let l=new Uint8Array(i.doc.w*i.doc.h*4);return await this.submit(o.job,"clear",[ve(a,c.id,i.doc,l)]),this.#n(o.room)}async trim({job:e,margin:t=0}){let r=this.resolve(e),o=this.client(r.room),s=o.state,i=this.#r(r.room,r.layer),a=o.session.readArt(i,0,{trim:!0}),c=a.rect.w+t*2,l=a.rect.h+t*2;return c>=s.doc.w&&l>=s.doc.h?{...this.#n(r.room),trimmed:"already tight"}:(await this.submit(r.job,"trim_canvas",[{t:"resize",w:c,h:l,dx:-a.rect.x+t,dy:-a.rect.y+t}]),{...this.#n(r.room),trimmed:`${c}\xD7${l}`})}},ht=new Set(["new_sprite","begin_job","finish_job","draw_art","draw_animation","read_art","render_preview","render_animation","add_layer","add_frame","set_palette","set_fps","clear","trim_canvas","get_info","list_documents","take_requests"]);function ve(n,e,t,r){return{t:"paintRegion",layer:n,frame:e,rect:{x:0,y:0,w:t.w,h:t.h},rle:ut(r)}}function ut(n){let e=[];if(n.length){let t=[n[0],n[1],n[2],n[3]],r=0,o=()=>{let s=r;do{let i=s&127;s>>=7,e.push(s?i|128:i)}while(s);e.push(...t)};for(let s=0;s<n.length;s+=4)n[s]===t[0]&&n[s+1]===t[1]&&n[s+2]===t[2]&&n[s+3]===t[3]?r++:(o(),t=[n[s],n[s+1],n[s+2],n[s+3]],r=1);o()}return Buffer.from(Uint8Array.from(e)).toString("base64")}function dt(n){let e=process.platform==="darwin"?"open":process.platform==="win32"?"start":"xdg-open";return new Promise(t=>ct(e,[n],()=>t()))}function J(n){return!n||n<=0?83:Math.max(10,Math.round(1e3/n))}function je(n,e=400){return new Promise(t=>{let r=Date.now(),o=()=>{if(n.state.pending===0&&Date.now()-r>40||Date.now()-r>e)return t();setTimeout(o,10)};o()})}function Re(){return(Date.now()^Math.floor(Math.random()*4294967295))>>>0}import{execFile as pt}from"node:child_process";var mt=2e4,ft=5e3,j=class n{constructor({base:e,name:t="agent"}={}){this.base=String(e).replace(/\/$/,""),this.name=t,this.log=[],this.beat=setInterval(()=>void this.announce(),ft),this.beat.unref&&this.beat.unref()}get kind(){return"local"}get where(){return this.base}supports(e){return wt.has(e)}static async probe(e,{timeoutMs:t=700}={}){try{let r=await fetch(`${e.replace(/\/$/,"")}/bridge/status`,{signal:AbortSignal.timeout(t)});if(!r.ok)return null;let o=await r.json();return o?.server==="sprite"?o:null}catch{return null}}async announce(){try{await fetch(`${this.base}/bridge/agent`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:this.name,version:null}),signal:AbortSignal.timeout(2e3)})}catch{}}async call(e,t=[],r=mt){let o;try{o=await(await fetch(`${this.base}/bridge/call`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({method:e,args:t,timeoutMs:r}),signal:AbortSignal.timeout(r+2e3)})).json()}catch(s){throw new Error(`lost contact with the Sprite editor at ${this.base}: ${s.message}`)}if(!o.ok)throw new Error(o.error);return this.note(e),o.result}note(e,t=""){this.log.unshift({at:Date.now(),tool:e,room:"local",detail:t}),this.log.length>200&&this.log.pop()}newSprite({name:e,w:t=32,h:r=32,intent:o,palette:s}){return this.call("newSprite",[{w:t,h:r,name:e,palette:s,intent:o,replace:!1}])}beginJob({document:e,intent:t}){return this.call("beginJob",[{document:e,intent:t}])}finishJob({job:e}){return this.call("finishJob",[{job:e}])}async info({document:e}={}){let t=await this.call("info",[]);return{editor:this.base,...e?{asked:e}:{},...t}}readArt({document:e,layer:t,frame:r}){return this.call("readArt",[{document:e,layer:t,frame:r}])}async preview({document:e,frame:t,scale:r=8}){let o=await this.call("toDataURL",[t??null,r,e??null]);return{png:Ae(o.url),caption:o.caption}}async sheet({document:e,columns:t=0,scale:r=0}){let o=await this.call("toStripDataURL",[{document:e??null,columns:t,scale:r}]);return{png:Ae(o.url),caption:o.caption}}documents(){return this.call("listDocuments",[])}drawArt({job:e,rows:t,colors:r,x:o,y:s,center:i,clear:a,layer:c,frame:l,label:h}){return this.call("drawArt",[t,r,{job:e,x:o,y:s,center:i,clear:a,layer:c,frame:l,label:h}])}drawAnimation({job:e,frames:t,colors:r,fps:o,layer:s,label:i}){return this.call("drawAnimation",[{frames:t,colors:r,fps:o,job:e,layer:s,label:i}])}addLayer({name:e}){return this.call("addLayer",[e])}addFrame({copy:e=!1}){return this.call("addFrame",[{copy:!!e}])}setPalette({colors:e}){return this.call("setPalette",[e])}setFps({fps:e}){return this.call("setFps",[e])}clearCel({job:e,layer:t,frame:r}){return this.call("clear",[{job:e,layer:t,frame:r}])}trim({job:e,margin:t=0}){return this.call("trimCanvas",[{job:e,margin:t}])}setFrame({index:e}){return this.call("setFrame",[e])}setLayer({index:e}){return this.call("setLayer",[e])}select({document:e}){return this.call("selectDocument",[e])}closeDocument({document:e}){return this.call("closeDocument",[e??null])}undo(){return this.call("undo",[])}redo(){return this.call("redo",[])}async openEditor(){if((await n.probe(this.base))?.connected)return{ok:!0,message:`Editor already connected at ${this.base}`};await yt(this.base);let t=Date.now()+1e4;for(;Date.now()<t;){if((await n.probe(this.base))?.connected)return{ok:!0,message:`Editor connected at ${this.base}`};await new Promise(o=>setTimeout(o,200))}return{ok:!1,message:`Opened ${this.base} but nothing connected \u2014 check the browser window.`}}close(){clearInterval(this.beat)}},wt=new Set(["new_sprite","begin_job","finish_job","draw_art","draw_animation","read_art","render_preview","render_animation","add_layer","add_frame","set_palette","set_fps","clear","trim_canvas","get_info","list_documents","open_editor","select_document","close_document","set_frame","set_layer","undo","redo"]);function Ae(n){let e=String(n??"").split(",")[1];if(!e)throw new Error("the editor returned something that was not an image");return Buffer.from(e,"base64")}function yt(n){let e=process.platform==="darwin"?"open":process.platform==="win32"?"start":"xdg-open";return new Promise(t=>pt(e,[n],()=>t()))}var L={type:"string",description:"#rrggbb"},y={type:"string",description:"The job this call belongs to. May be omitted when exactly one job is open."},R={type:"string",description:"Which document to read. Omit to use the one your job owns. In a local editor a 0-based tab index or a name both work; in a room it is the room name."};var Te=n=>({content:[{type:"text",text:typeof n=="string"?n:JSON.stringify(n)}]}),$e=(n,e)=>({content:[{type:"image",data:Buffer.from(n).toString("base64"),mimeType:"image/png"},{type:"text",text:e}]}),gt=[{name:"new_sprite",description:"Create a sprite and claim it. Call this FIRST \u2014 before writing a script, computing a grid, or thinking about pixels. It costs milliseconds and puts a labelled document in front of the user immediately, so they can see what you are making and where. The tab opens in the background and never steals their focus.",inputSchema:{type:"object",properties:{name:{type:"string",description:"Document name. Shown on the tab and used for exports."},w:{type:"integer",default:32},h:{type:"integer",default:32},palette:{type:"array",items:L,description:"Optional starting palette."},intent:{type:"string",description:"One line on what you are making. Shown while you work."}},required:["name","intent"]},run:(n,e)=>e.newSprite({name:n.name,w:n.w??32,h:n.h??32,intent:n.intent,palette:n.palette})},{name:"begin_job",description:"Claim an existing document for one piece of work \u2014 the other way to start, where new_sprite is the one for new art. Refused by name if another job holds it, which is what keeps parallel requests from colliding.",inputSchema:{type:"object",properties:{document:R,intent:{type:"string"},layer:{type:"string",description:"Restrict the claim to one layer, leaving the rest free. Rooms only."}},required:["document","intent"]},run:(n,e)=>e.beginJob({document:n.document,intent:n.intent,layer:n.layer??null})},{name:"finish_job",description:"Release the document. Call this when the sprite is done, so the user knows to look.",inputSchema:{type:"object",properties:{job:y}},run:(n,e)=>e.finishJob({job:n.job})},{name:"draw_art",description:'Draw a character grid. Each string in `rows` is one row of pixels and each character maps to a colour via `colors`. "." and " " leave existing pixels alone; a colour mapped to null erases. Every character is resolved before a pixel moves, so a typo fails rather than half-drawing. Keep the palette small \u2014 four to eight colours, reused for shading \u2014 because that is what makes art read as pixel art rather than as a downscaled photograph.',inputSchema:{type:"object",properties:{rows:{type:"array",items:{type:"string"}},colors:{type:"object",additionalProperties:{oneOf:[L,{type:"null"}]}},x:{type:"integer"},y:{type:"integer"},center:{type:"boolean"},clear:{type:"boolean",description:"Erase the cel first."},layer:{type:"string",description:"Layer name or index. Defaults to the top one."},frame:{type:"integer",description:"Frame index. Defaults to the first."},label:{type:"string",description:"What this shows as in the undo history."},job:y},required:["rows","colors"]},run:(n,e)=>e.drawArt({job:n.job,rows:n.rows,colors:n.colors,x:n.x??0,y:n.y??0,center:n.center??!1,clear:n.clear??!1,layer:n.layer,frame:n.frame??0,label:n.label})},{name:"draw_animation",description:"Write every frame in ONE call, as a single undo step. Use copyPrevious so a frame carries the last one forward and sends only the pixels that moved \u2014 that is what takes a six-frame loop from four thousand tokens of restated grid to under a thousand. Never loop draw_art.",inputSchema:{type:"object",properties:{frames:{type:"array",minItems:1,items:{type:"object",properties:{rows:{type:"array",items:{type:"string"}},copyPrevious:{type:"boolean"},x:{type:"integer"},y:{type:"integer"},center:{type:"boolean"}},required:["rows"]}},colors:{type:"object",additionalProperties:{oneOf:[L,{type:"null"}]}},fps:{type:"number"},layer:{type:"string"},label:{type:"string"},job:y},required:["frames","colors"]},run:(n,e)=>e.drawAnimation({job:n.job,frames:n.frames,colors:n.colors,fps:n.fps,layer:n.layer,label:n.label})},{name:"read_art",description:"Read a cel back as a grid \u2014 the exact inverse of draw_art, so you can extend art a human drew rather than only replacing it. Reads never need a job and never wait.",inputSchema:{type:"object",properties:{document:R,layer:{type:"string"},frame:{type:"integer"},trim:{type:"boolean",description:"Return only the drawn area. Rooms only."},job:y}},run:(n,e)=>e.readArt({document:n.document,job:n.job,layer:n.layer,frame:n.frame??0,trim:n.trim??!1})},{name:"render_preview",description:"See one frame as an image, so you can check your own work rather than working blind.",inputSchema:{type:"object",properties:{document:R,frame:{type:"integer"},scale:{type:"integer"},job:y}},async run(n,e){let t=await e.preview({document:n.document,job:n.job,frame:n.frame??0,scale:n.scale??8});return $e(t.png,t.caption)}},{name:"render_animation",description:"Every frame as one numbered contact sheet. Stills inspected one call at a time, minutes apart, cannot tell you whether motion reads; a contact sheet can, in one call. Check any animation with this before calling it done.",inputSchema:{type:"object",properties:{document:R,columns:{type:"integer",default:4},scale:{type:"integer",default:2},job:y}},async run(n,e){let t=await e.sheet({document:n.document,job:n.job,columns:n.columns??4,scale:n.scale??2});return $e(t.png,t.caption)}},{name:"add_layer",description:"Add a layer above the others. Separate layers keep pieces independently editable.",inputSchema:{type:"object",properties:{name:{type:"string"},job:y}},run:(n,e)=>e.addLayer({job:n.job,name:n.name})},{name:"add_frame",description:"Add an animation frame after the last one.",inputSchema:{type:"object",properties:{copy:{type:"boolean",description:"Start from the previous frame."},job:y}},run:(n,e)=>e.addFrame({job:n.job,copy:n.copy??!1})},{name:"set_palette",description:"Replace the palette. Constraining yourself to a small fixed palette is the single biggest quality lever in pixel art.",inputSchema:{type:"object",properties:{colors:{type:"array",items:L,minItems:1},job:y},required:["colors"]},run:(n,e)=>e.setPalette({job:n.job,colors:n.colors})},{name:"set_fps",description:"Set playback speed for every frame. Eight to twelve suits character animation.",inputSchema:{type:"object",properties:{fps:{type:"integer",minimum:1,maximum:60},job:y},required:["fps"]},run:(n,e)=>e.setFps({job:n.job,fps:n.fps})},{name:"clear",description:"Erase a cel. Destructive, so it goes through a job like every other write.",inputSchema:{type:"object",properties:{layer:{type:"string"},frame:{type:"integer"},job:y}},run:(n,e)=>e.clearCel({job:n.job,layer:n.layer,frame:n.frame??0})},{name:"trim_canvas",description:"Crop the canvas to the drawn pixels. CALL THIS BEFORE finish_job ON EVERY SPRITE. You cannot know a sprite's extent before you have drawn it, so overshoot at new_sprite and trim afterwards \u2014 empty space is inherited by every export, thumbnail and sprite sheet.",inputSchema:{type:"object",properties:{margin:{type:"integer",default:0},job:y}},run:(n,e)=>e.trim({job:n.job,margin:n.margin??0})},{name:"get_info",description:"What is in a document \u2014 size, layers, frames, palette \u2014 and who else is looking at it.",inputSchema:{type:"object",properties:{document:R,job:y}},run:(n,e)=>e.info({document:n.document,job:n.job})},{name:"list_documents",description:'Every document you can reach, and every job currently open. Call this when the user names a sprite ("add a frame to the hamster") so you can pass it to begin_job. Never guess a name.',inputSchema:{type:"object",properties:{}},run:(n,e)=>e.documents()},{name:"take_requests",description:"Requests people have typed into the editor and not yet been answered. Call this when the user asks you to check, or after joining a room they are in. Each one is somebody asking for a sprite in words \u2014 treat it exactly as if they had said it to you directly. Each carries what it wants: a `room` to answer in, or `room: null` for a request typed at the library, which wants a brand new sprite rather than a change to any existing one.",inputSchema:{type:"object",properties:{}},run:(n,e)=>e.takeRequests()},{name:"open_editor",description:"Open the Sprite editor in the default browser and wait for it to connect. Run this first if nothing is connected.",inputSchema:{type:"object",properties:{}},async run(n,e){let t=await e.openEditor();return Te(t.message)}},{name:"select_document",description:"Bring a document to the front for the USER to look at. This does not give you the right to draw in it \u2014 begin_job does that.",inputSchema:{type:"object",properties:{document:{type:["string","integer"]}},required:["document"]},run:(n,e)=>e.select({document:n.document})},{name:"close_document",description:"Close a document tab. The document stays in the library, so this tidies the workspace rather than deleting anything.",inputSchema:{type:"object",properties:{document:{type:["string","integer"]}}},run:(n,e)=>e.closeDocument({document:n.document??null})},{name:"set_frame",description:"Move the user to a frame, by 0-based index.",inputSchema:{type:"object",properties:{index:{type:"integer"}},required:["index"]},run:(n,e)=>e.setFrame({index:n.index})},{name:"set_layer",description:"Move the user to a layer, by 0-based index counting from the bottom.",inputSchema:{type:"object",properties:{index:{type:"integer"}},required:["index"]},run:(n,e)=>e.setLayer({index:n.index})},{name:"undo",description:"Undo the last change, so the user can always reject generated art.",inputSchema:{type:"object",properties:{}},run:(n,e)=>e.undo()},{name:"redo",description:"Redo the last undone change.",inputSchema:{type:"object",properties:{}},run:(n,e)=>e.redo()}];function Oe(n){return gt.filter(e=>n.supports(e.name)).map(e=>({name:e.name,description:e.description,inputSchema:e.inputSchema,run:async t=>{let r=await e.run(t??{},n);return r&&typeof r=="object"&&Array.isArray(r.content)?r:Te(r)}}))}import{readFileSync as bt}from"node:fs";var St="0.1.0";function Et(){let n=new URL("./version.json",import.meta.url);try{let e=JSON.parse(bt(n,"utf8"))?.version;if(typeof e=="string"&&e)return e;throw new Error("it names no version")}catch(e){throw new Error(`${n.pathname} could not be read as a version: ${e.message}`)}}var Pe=St??Et();function W(n="room"){return`Pixel art editor with live collaboration. ${n==="local"?`Documents are tabs in a browser window on this machine. The user is
2
+ var k=(n,e)=>()=>(n&&(e=n(n=0)),e);function ue(n){let e="";for(let t=0;t<n.length;t+=32768)e+=String.fromCharCode.apply(null,n.subarray(t,t+32768));return btoa(e).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}var he=k(()=>{"use strict"});var v,Ke,Xe,Vt,de=k(()=>{"use strict";v="[A-Za-z0-9_-]{1,64}",Ke="token|members|link|slow|project",Xe=[{owner:"accounts",re:new RegExp("^/auth/")},{owner:"accounts",re:new RegExp("^/a/[A-Za-z0-9_-]{1,64}\\.png$")},{owner:"accounts",re:new RegExp("^/projects(/|$)")},{owner:"accounts",re:new RegExp(`^/documents/${v}/(${Ke})$`)},{owner:"library",re:new RegExp("^/documents$")},{owner:"library",re:new RegExp("^/documents/(new|register|move|order|folders|folders/delete)$")},{owner:"library",re:new RegExp(`^/documents/${v}(/(thumbnail\\.png|duplicate|rename))?$`)},{owner:"library",re:new RegExp(`^/documents/${v}/history/at/\\d+/thumbnail\\.png$`)},{owner:"document",re:new RegExp(`^/documents/${v}/history(/at/\\d+|/\\d+)?$`)},{owner:"document",re:new RegExp(`^/documents/${v}/comments(/[A-Za-z0-9]{1,40})?$`)},{owner:"document",re:new RegExp(`^/documents/${v}/snapshots(/enabled|/[A-Za-z0-9_-]{1,128})?$`)},{owner:"document",re:new RegExp(`^/favourites(/order|/(document|project)/${v})?$`)}],Vt=Xe.map(({owner:n,re:e})=>({owner:n,source:e.source}))});var J,W=k(()=>{"use strict";de();J=["read","draw","create"]});async function pe(n){let e=new TextEncoder().encode(String(n));return ue(new Uint8Array(await crypto.subtle.digest("SHA-256",e)))}var en,me=k(()=>{"use strict";he();W();en=120*1e3});function E(n=Date.now()){let e="0123456789ABCDEFGHJKMNPQRSTVWXYZ",t=(BigInt(n)&0xffffffffffffn)<<80n,r=crypto.getRandomValues(new Uint8Array(10)),o=0n;for(let i of r)o=o<<8n|BigInt(i);t|=o;let s="";for(let i=0;i<26;i++)s=e[Number(t&31n)]+s,t>>=5n;return s}function x(n){return n==null?{at:0}:typeof n=="number"?{at:n}:{frame:n}}var rt,ot,st,I,_,q,$=k(()=>{"use strict";rt=new TextEncoder,ot=new TextDecoder,st=1;I=class n{#e;#t;constructor(e){this.#e=e,this.#t=e.exports}static async load(e){let t;if(e instanceof WebAssembly.Module)t=await WebAssembly.instantiate(e,{});else if(e instanceof ArrayBuffer||ArrayBuffer.isView(e))({instance:t}=await WebAssembly.instantiate(e,{}));else{let r=e instanceof Response||typeof e?.then=="function"?e:fetch(e);({instance:t}=await WebAssembly.instantiateStreaming(r,{}))}return new n(t)}get#n(){return new Uint8Array(this.#t.memory.buffer)}call(e,t={}){let r=rt.encode(JSON.stringify({m:e,a:t})),o=this.#t.sprite_alloc(r.length);if(!o)throw new Error("sprite-core: allocation failed");let s=0;try{if(this.#n.set(r,o),s=this.#t.sprite_dispatch(o,r.length),!s)throw new Error("sprite-core: dispatch returned nothing");let i=this.#n,a=i[s]|i[s+1]<<8|i[s+2]<<16|i[s+3]<<24,c=i[s+4],l=i.slice(s+5,s+5+a);if(c===st)return l;let u=JSON.parse(ot.decode(l));if(u&&u.error)throw new Error(u.error);return u}finally{s?this.#t.sprite_free_result(s):this.#t.sprite_dealloc(o,r.length)}}reset(){this.#t.sprite_reset()}newDocument(e={}){return this.call("doc.new",{w:32,h:32,name:"untitled",...e})}openSession(e,t,r=1){let{h:o}=this.call("session.new",{doc:e,author:t,seed:r});return new _(this,o)}openRoom(e,{seq:t=0}={}){let{h:r}=this.call("room.new",{doc:e,...t>0?{seq:t}:{}});return new q(this,r)}},_=class{constructor(e,t){this.core=e,this.h=t}#e(e,t={}){return this.core.call(e,{h:this.h,...t})}state(){return this.#e("session.state")}apply(e,{now:t=Date.now(),label:r="Edit",job:o=null}={}){let s={now:t,label:r,bodies:Array.isArray(e)?e:[e]};return o&&(s.job=o),this.#e("session.apply",s).ops}remote(e){return this.#e("session.remote",{sop:e})}reject(e){return this.#e("session.reject",{op:e})}undo(e=Date.now()){return this.#e("session.undo",{now:e}).ops}redo(e=Date.now()){return this.#e("session.redo",{now:e}).ops}pending(){return this.#e("session.pending")}checkpoint({doc:e=!0}={}){return this.#e("session.checkpoint",{doc:e})}save(){return this.#e("session.save")}drawArt(e,t,r={}){return this.#e("session.drawArt",{rows:e,colors:t,...r}).bodies}drawAnimation(e,t,r={}){return this.#e("session.drawAnimation",{frames:e,colors:t,...r}).bodies}readArt(e,t,r={}){return this.#e("session.readArt",{layer:e,...x(t),...r})}cel(e,t){return this.#e("session.cel",{layer:e,...x(t)})}layerImage(e,t){return this.#e("session.layerImage",{layer:e,...x(t)})}tileCel(e,t){return this.#e("session.tileCel",{layer:e,...x(t)})}tile(e,t){return this.#e("session.tile",{tileset:e,index:t})}composite(e,{include:t="export"}={}){return this.#e("session.composite",{...x(e),include:t})}png(e){return this.#e("session.png",x(e))}contactSheet({columns:e=4,scale:t=2}={}){return this.#e("session.sheet",{columns:e,scale:t})}thumbnail(e=48){return this.#e("session.thumbnail",{size:e})}card({frame:e=null,width:t=1200,height:r=630,framing:o=null}={}){return this.#e("session.card",{width:t,height:r,...o??{},...x(e)})}fit({width:e,height:t,margin:r=0,inscribed:o=!1,dx:s=0,dy:i=0,frame:a=null}){return this.#e("session.fit",{width:e,height:t,margin:r,inscribed:o,dx:s,dy:i,...x(a)})}close(){this.#e("session.free")}},q=class{constructor(e,t){this.core=e,this.h=t}#e(e,t={}){return this.core.call(e,{h:this.h,...t})}state(){return this.#e("room.state")}submit(e,t=Date.now()){return this.#e("room.submit",{op:e,now:t})}since(e){return this.#e("room.since",{seq:e})}compact(){return this.#e("room.compact")}save(){return this.#e("room.save")}lease(e,t){return this.#e("room.lease",{action:e,...t})}close(){this.#e("room.free")}}});function xe(n){return JSON.stringify(n)}function ve(n){let e=n.rle instanceof Uint8Array?n.rle:null,t=new TextEncoder().encode(JSON.stringify({...n,rle:void 0}));if(t.length>it)return null;let r=new Uint8Array(3+t.length+(e?.length??0));return r[0]=ke,r[1]=t.length&255,r[2]=t.length>>8&255,r.set(t,3),e&&r.set(e,3+t.length),r}function at(n){return n instanceof Uint8Array?n:n instanceof ArrayBuffer?new Uint8Array(n):ArrayBuffer.isView(n)?new Uint8Array(n.buffer,n.byteOffset,n.byteLength):null}function _e(n){if(typeof n!="string"){let e=at(n);if(!e||e.length<3||e[0]!==ke)return null;let t=e[1]|e[2]<<8;if(e.length<3+t)return null;try{let r=JSON.parse(new TextDecoder().decode(e.subarray(3,3+t)));if(!r||typeof r.t!="string")return null;let o=e.subarray(3+t);return r.rle=o.length?Uint8Array.from(o):null,r}catch{return null}}try{let e=JSON.parse(n);return e&&typeof e.t=="string"?e:null}catch{return null}}var y,Se,S,Ee,ke,it,je=k(()=>{"use strict";y={HELLO:"hello",SUBMIT:"submit",PRESENCE:"presence",STROKE:"stroke",LEASE:"lease",PING:"ping",PROMPT:"prompt",ANSWER:"answer",STOP:"stop"},Se={DOCUMENT:"document",LIBRARY:"library"},S={WELCOME:"welcome",REHELLO:"rehello",OPS:"ops",REJECT:"reject",PRESENCE:"presence",STROKE:"stroke",LEASE:"lease",ERROR:"error",PONG:"pong",PROMPT:"prompt",ANSWER:"answer",STOP:"stop"},Ee={OPS:"ops",SNAPSHOT:"snapshot"};ke=157,it=65535});function lt(n){return String(n??"").trim().toLowerCase().replace(/[^a-z0-9_-]+/g,"-").replace(/^-+|-+$/g,"").replace(/-{2,}/g,"-").slice(0,64).replace(/-+$/,"")||null}function $e(n){return`${(lt(n)??"").slice(0,48).replace(/-+$/,"")||"sprite"}-${E().toLowerCase().slice(0,12)}`}var Re=k(()=>{"use strict";$()});import{readFile as $t}from"node:fs/promises";import{readFile as Me,writeFile as Fe,mkdir as Je,chmod as We}from"node:fs/promises";import{readFileSync as qe}from"node:fs";import{homedir as He,hostname as Ve}from"node:os";import{join as M}from"node:path";import{execFile as ze}from"node:child_process";var p=n=>String(n??"").replace(/\/+$/,"").replace(/^ws/,"http"),Y=n=>String(n??"").replace(/\/+$/,"").replace(/^http/,"ws");async function Z(n,e){if(!n?.ok||!(n.headers.get("content-type")??"").includes("json"))return null;let t=await n.json().catch(()=>null);return!t||typeof t!="object"?null:e.some(r=>r in t)?t:null}async function Q(n,{timeoutMs:e=3e3,fetchImpl:t=fetch}={}){try{let r=await t(`${p(n)}/setup`,{signal:AbortSignal.timeout(e)});return await Z(r,["accounts","expectedPort","install"])}catch{return null}}var B=new Map;function ee(n,{fetchImpl:e=fetch,timeoutMs:t=3e3}={}){let r=p(n);return B.has(r)||B.set(r,Be(n,{fetchImpl:e,timeoutMs:t})),B.get(r)}async function Be(n,{fetchImpl:e,timeoutMs:t}){try{let r=await e(`${p(n)}/config.json`,{signal:AbortSignal.timeout(t)}),s=(await Z(r,["rooms","accounts"]))?.rooms;if(typeof s=="string"&&/^(wss?|https?):\/\/./.test(s))return Y(s)}catch{}return Y(n)}var j=process.env.SPRITE_PUBLIC_SERVER||Ge()||null;function Ge(){return"wss://drawpixie.com"}var te=M(process.env.XDG_CONFIG_HOME||M(He(),".config"),"sprite"),F=M(te,"agent.json"),ne=n=>String(n??"").replace(/\/+$/,"");function re(){return`${Ve().replace(/\.local$/,"")}`.slice(0,60)||"a computer"}async function oe(){try{let n=JSON.parse(await Me(F,"utf8"));return n&&typeof n=="object"&&n.servers?n:{servers:{}}}catch{return{servers:{}}}}async function O(n){return process.env.SPRITE_TOKEN?{token:process.env.SPRITE_TOKEN,from:"the environment"}:(await oe()).servers[ne(n)]??null}async function se(n,e){await Je(te,{recursive:!0,mode:448});let t=await oe();t.servers[ne(n)]={...e,at:Date.now()};let r=`${F}.tmp`;await Fe(r,JSON.stringify(t,null,2),{mode:384}),await We(r,384);let{rename:o}=await import("node:fs/promises");await o(r,F)}async function ie(n,{timeoutMs:e=3e3}={}){try{let t=await fetch(`${p(n)}/setup`,{signal:AbortSignal.timeout(e)});return t.ok?!!(await t.json()).accounts:!1}catch{return!1}}async function ae(n,e,{session:t=null}={}){let r=t??await O(n);if(!r?.token)return;let o=await fetch(`${p(n)}/documents/${encodeURIComponent(e)}/token`,{headers:{Authorization:`Bearer ${r.token}`}});if(o.status===401)throw new Error("this machine is signed out \u2014 pair it again");if(o.status===403)throw new Error(`no access to ${e}`);if(!o.ok)throw new Error(`could not get access to ${e}`);return(await o.json()).token}async function ce(n,{name:e,w:t,h:r},{session:o=null}={}){let s=o??await O(n);if(!s?.token)return;let i=await fetch(`${p(n)}/documents/new`,{method:"POST",headers:{Authorization:`Bearer ${s.token}`,"Content-Type":"application/json"},body:JSON.stringify({name:e,w:t,h:r})});if(i.status===401)throw new Error("this machine is signed out \u2014 pair it again");if(!i.ok)throw new Error(`could not make a document: the server said ${i.status}`);let{id:a}=await i.json();if(typeof a!="string"||!a)throw new Error("could not make a document: the server named none");return a}function le(n){let e=process.platform==="darwin"?"open":process.platform==="win32"?"start":"xdg-open";return new Promise(t=>ze(e,[n],()=>t()))}me();W();import{createServer as Ye}from"node:http";import{randomBytes as we}from"node:crypto";var Ze=300*1e3,Qe=()=>we(32).toString("base64url"),et=()=>we(16).toString("base64url");function fe(n,e){return`<!doctype html><meta charset="utf-8"><title>Pixie</title><body style="background:#101014;color:#d8d8e0;font:14px/1.6 system-ui,sans-serif;display:grid;place-items:center;height:100vh;margin:0;text-align:center"><div><p style="font-size:16px">${n}</p><p style="color:#8a8a99;font-size:12px">${e}</p></div>`}async function tt({state:n,timeoutMs:e}){let t,r,o=new Promise((c,l)=>{t=c,r=l}),s=Ye((c,l)=>{let u=new URL(c.url,"http://127.0.0.1");if(u.pathname!=="/callback"){l.writeHead(404).end();return}if(u.searchParams.get("state")!==n){l.writeHead(404).end();return}let h=u.searchParams.get("error");l.writeHead(200,{"Content-Type":"text/html; charset=utf-8"}).end(h?fe("Nothing was granted.","You can close this window."):fe("This computer can now draw in your sprites.","You can close this window and go back to what you were doing.")),t(u.searchParams)});await new Promise((c,l)=>{s.once("error",l),s.listen(0,"127.0.0.1",c)});let i=setTimeout(()=>r(new Error("nobody answered the browser in time")),e);i.unref?.();let{port:a}=s.address();return{redirect:`http://127.0.0.1:${a}/callback`,wait:o,close:()=>{clearTimeout(i),s.close(),s.closeAllConnections?.()}}}async function ye(n,{onUrl:e=()=>{},open:t=le,label:r="the Pixie agent",scope:o=J,timeoutMs:s=Ze}={}){let i=Qe(),a=await pe(i),c=et(),l=await tt({state:c,timeoutMs:s});try{let u=new URL(`${p(n)}/authorize`);u.searchParams.set("challenge",a),u.searchParams.set("state",c),u.searchParams.set("redirect",l.redirect),u.searchParams.set("scope",o.join(",")),u.searchParams.set("name",r),e(u.toString()),await t(u.toString());let h=await l.wait,m=h.get("error");if(m)throw new Error(m==="access_denied"?"you said no on that screen, so nothing was granted":`that request was refused: ${m}`);let f=h.get("code");if(!f)throw new Error("the browser came back without an authorisation");let w=await fetch(`${p(n)}/auth/authorize/exchange`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:f,verifier:i,name:re()})}),d=await w.json().catch(()=>({}));if(!w.ok||!d.token)throw new Error(d.error||"that authorisation could not be exchanged");return await se(n,{token:d.token,name:d.name,email:d.email,scope:d.scope??null}),d}finally{l.close()}}import{realpathSync as ge}from"node:fs";import{fileURLToPath as nt}from"node:url";function be(n){let e=process.argv[1];if(!e)return!1;try{return ge(nt(n))===ge(e)}catch{return!1}}$();import{execFile as ht}from"node:child_process";je();$();var ct=5e3,N=class{#e=null;constructor(e,t,r){if(!r||!r.author)throw new Error("DocumentClient needs an author");if(this.core=e,this.author=r.author,this.name=r.name||"anonymous",this.agent=r.agent===!0,this.answers=r.answers===!0,this.seed=r.seed??1,this.epoch=0,this.resumed=null,r.resume){let{h:o,dropped:s}=e.call("session.resume",{doc:t,author:this.author,seed:this.seed,seq:r.resume.seq??0,pending:r.resume.pending??[]});this.session=new _(e,o),this.resumed={seq:r.resume.seq??0,dropped:s??[]}}else this.session=e.openSession(t,this.author,this.seed);this.getToken=r.getToken??null,this.transport=null,this.connected=!1,this.welcomed=!1,this.ready=new Promise(o=>{this.#e=o}),this.participants=[],this.leases=[],this.presence=null,this.you=null,this.onChange=()=>{},this.onPresence=()=>{},this.onStroke=()=>{},this.strokePreview=!0,this.onLeases=()=>{},this.onLost=()=>{},this.onPrompt=()=>{},this.onAnswer=()=>{},this.onStop=()=>{},this.onStatus=()=>{}}apply(e,t={}){let r=this.session.apply(e,t);return this.#s(r),this.onChange(),r}undo(e=Date.now()){let t=this.session.undo(e);return this.#s(t),this.onChange(),t}redo(e=Date.now()){let t=this.session.redo(e);return this.#s(t),this.onChange(),t}setPresence(e){this.presence=e,this.connected&&this.#r({t:y.PRESENCE,...e})}sendStroke({layer:e,frame:t,rect:r,rle:o,done:s=!1}){if(!this.connected||!this.welcomed||!this.strokePreview)return;this._strokeSentAt=Date.now();let i=ve({t:y.STROKE,layer:e,frame:t,rect:r,rle:o,done:s});i&&this.#a(i)}lease(e,t={}){this.#r({t:y.LEASE,action:e,...t})}prompt(e,{where:t=Se.DOCUMENT}={}){if(!this.connected)return null;let r=String(e??"").trim().slice(0,2e3);if(!r)return null;let o=E();return this.#r({t:y.PROMPT,id:o,text:r,where:t}),o}answer(e,t="",{reply:r=null,made:o=null,model:s=null}={}){return this.connected?(this.#r({t:y.ANSWER,state:e,text:String(t??"").slice(0,2e3),reply:r,made:o,model:s}),!0):!1}stopAnswer(e){return!this.connected||!e?!1:(this.#r({t:y.STOP,id:e}),!0)}get state(){return this.session.state()}checkpoint({doc:e=!0}={}){return{epoch:this.epoch,author:this.author,...this.session.checkpoint({doc:e})}}composite(e){return this.session.composite(e)}connect(e){return this.transport=e,this.pendingToken=this.getToken?this.getToken():void 0,this.pendingToken?.catch?.(()=>{}),e.onopen=async()=>{this.connected=!0,this.welcomed=!1,clearInterval(this.heartbeat),this.heartbeat=setInterval(()=>{this.connected&&this.#r({t:y.PING})},ct),await this.#t()},e.onmessage=t=>this.#n(t),e.onclose=()=>{this.connected=!1,this.welcomed=!1,clearInterval(this.heartbeat),this.onStatus({connected:!1})},e.readyState===1&&e.onopen(),this}disconnect(){if(this.transport){this.connected=!1,this.welcomed=!1,clearInterval(this.heartbeat);try{this.transport.close()}catch{}this.transport=null,this.onStatus({connected:!1})}}async#t(){let e;if(this.pendingToken!==void 0){let r=this.pendingToken;this.pendingToken=void 0,e=await r}else e=this.getToken?await this.getToken():void 0;let t=this.session.state();this.#r({t:y.HELLO,author:this.author,name:this.name,token:e,since:t.seq,...this.agent?{agent:!0}:{},...this.answers?{answers:!0}:{},docId:t.doc.id})}#n(e){let t=_e(e);if(t)switch(t.t){case S.WELCOME:return this.#o(t);case S.REHELLO:return this.welcomed=!1,void this.#t();case S.OPS:return this.#c(t);case S.REJECT:return this.#l(t);case S.PRESENCE:return this.participants=t.participants||[],this.onPresence(this.participants);case S.STROKE:return this.onStroke(t);case S.LEASE:return this.leases=t.leases||[],this.onLeases(this.leases);case S.PROMPT:return this.onPrompt(t);case S.ANSWER:return this.onAnswer(t);case S.STOP:return this.onStop(t);case S.ERROR:if(this.#u(t.message)){this.strokePreview=!1;return}return this.onLost({kind:"protocol",message:t.message});default:return}}#o(e){this.you=e.you,this.welcomed=!0;let t=this.session.pending();if(e.catchup.kind===Ee.SNAPSHOT){let{h:r,dropped:o}=this.core.call("session.resume",{doc:e.catchup.doc,author:this.author,seed:this.seed,seq:e.catchup.seq,pending:t});this.session.close(),this.session=new _(this.core,r),this.epoch++,o&&o.length&&this.onLost({kind:"rebased",ops:o,message:`${o.length} edit${o.length>1?"s":""} could not be replayed onto the current document`})}else for(let r of e.catchup.ops||[])this.#i(r);this.participants=e.participants||[],this.leases=e.leases||[],t.length&&this.#r({t:y.SUBMIT,ops:t}),this.presence&&this.#r({t:y.PRESENCE,...this.presence}),this.onStatus({connected:!0,resent:t.length}),this.onPresence(this.participants),this.onLeases(this.leases),this.onChange(),this.#e?.(),this.#e=null}#c(e){let t=!1;for(let r of e.ops||[])this.#i(r),t=!0;t&&this.onChange()}#i(e){let t=this.session.remote(e);t.dropped&&t.dropped.length&&this.onLost({kind:"obsolete",ops:t.dropped,message:`${t.dropped.length} of your edits no longer apply and were dropped`})}#l(e){e.opId&&this.session.reject(e.opId),this.onLost({kind:e.kind,ops:e.opId?[e.opId]:[],message:e.message}),this.onChange()}#s(e){!e.length||!this.connected||!this.welcomed||this.#r({t:y.SUBMIT,ops:e})}#r(e){this.#a(xe(e))}#u(e){return typeof e!="string"?!1:e===`unknown message: ${y.STROKE}`?!0:e==="unreadable message"&&Date.now()-(this._strokeSentAt??0)<2e3}#a(e){if(this.transport)try{this.transport.send(e)}catch{this.connected=!1}}close(){this.disconnect(),this.session.close()}};$();Re();function ut(n){try{return/^(localhost|127\.0\.0\.1|\[::1\]|::1)$/.test(new URL(p(n)).hostname)}catch{return!1}}function Te(n,{publicServer:e=j}={}){let t=p(n);return e&&n===e?`nothing is answering at ${t} \u2014 this machine is offline, or the site is down`:ut(n)?`nothing is answering at ${t} \u2014 start the editor with \`npm start\` in the Pixie repository, then try again`:`nothing is answering at ${t} \u2014 check the address and that whatever serves it is up`}var dt=6e3,L=class{constructor(e,t){this.core=e,this.author=t.author,this.name=t.name||"agent",this.connectTransport=t.connect,this.getToken=t.getToken??null,this.createDocument=t.createDocument??null,this.rooms=new Map,this.asked=[],this.onAsked=null,this.jobs=new Map,this.log=[]}async open(e,{w:t=32,h:r=32,name:o=e}={}){let s=this.rooms.get(e);if(s)return s.client;let i=this.core.newDocument({w:t,h:r,name:o,seed:Oe(),now:Date.now()}),a=new N(this.core,i,{author:this.author,name:this.name,seed:Oe(),agent:!0,getToken:this.getToken?()=>this.getToken(e):null});a.onLost=h=>{this.lastRefusal=h},a.onPrompt=h=>{if(h.author===this.author)return;let m=h.where==="library";this.asked.push({text:h.text,from:h.name,at:h.at,wants:m?"a new sprite":"a change to this one",room:m?null:e,askedIn:e}),this.asked.length>32&&this.asked.shift(),this.onAsked?.()};let c=this.connectTransport(e,{w:t,h:r,name:o}),l=!1;return await new Promise((h,m)=>{let f=setTimeout(()=>m(new Error(`timed out joining ${e}`)),1e4);a.onStatus=d=>{d.connected&&(l=!0,clearTimeout(f),h())},a.connect(c);let w=c.onclose;c.onclose=()=>{w?.(),!l&&(clearTimeout(f),m(new Error(this.serverUrl?Te(this.serverUrl):"nothing is answering at the room server")))}}),this.rooms.set(e,{client:a}),a}client(e){let t=this.rooms.get(e);if(!t)throw new Error(`not in room ${e}`);return t.client}async peek(e){return this.rooms.has(e)?this.client(e):this.open(e)}async begin(e,t,{layer:r=null,open:o={}}={}){let s=await this.open(e,o),i=E();if(s.lease("acquire",{job:i,intent:t,...r?{layers:[r]}:{}}),await Pe(s),!s.leases.find(l=>l.job===i)){let l=s.leases[0];throw new Error(l?`"${e}" is already held by another job: ${l.intent}`:`could not claim "${e}"`)}let c={room:e,layer:r,intent:t,cancelled:!1,timer:setInterval(()=>{this.jobs.has(i)&&s.lease("renew",{job:i})},dt)};return c.timer.unref&&c.timer.unref(),this.jobs.set(i,c),this.note("begin_job",e,t),i}resolve(e){if(e){let o=this.jobs.get(e);if(!o)throw new Error(`unknown job ${e} \u2014 it may have finished`);if(o.cancelled)throw new Error("the user stopped you \u2014 do not retry; say what you were about to do and ask");return{job:e,...o}}if(this.jobs.size===0)throw new Error("no job is open \u2014 call new_sprite or begin_job first");if(this.jobs.size>1)throw new Error(`${this.jobs.size} jobs are open \u2014 pass the job argument`);let[t,r]=[...this.jobs.entries()][0];return{job:t,...r}}finish(e){let t=this.jobs.get(e);t&&(clearInterval(t.timer),this.jobs.delete(e),this.client(t.room).lease("release",{job:e}),this.note("finish_job",t.room,t.intent))}async submit(e,t,r){let o=this.resolve(e),s=this.client(o.room);if(!r.length)return{ops:0};if(this.lastRefusal=null,s.apply(r,{label:t,job:o.job,now:Date.now()}),await Pe(s),this.lastRefusal){let i=this.lastRefusal.message;throw this.lastRefusal.kind==="revoked"&&(o.cancelled=!0),this.lastRefusal=null,new Error(i)}return this.note(t,o.room,`${r.length} op${r.length>1?"s":""}`),{ops:r.length,seq:s.state.seq}}cancel(e){let t=this.jobs.get(e);t&&(t.cancelled=!0)}note(e,t,r){this.log.unshift({at:Date.now(),tool:e,room:t,detail:r}),this.log.length>200&&this.log.pop()}announce(){if(!this.serverUrl)return;let e=`${this.serverUrl.replace(/^ws/,"http").replace(/\/$/,"")}/agent`,t=()=>{fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:this.name})}).catch(()=>{})};t(),clearInterval(this.heartbeat),this.heartbeat=setInterval(t,15e3),this.heartbeat.unref?.()}close(){clearInterval(this.heartbeat);for(let[e]of this.jobs)this.finish(e);for(let{client:e}of this.rooms.values())e.close();this.rooms.clear()}get kind(){return"room"}get where(){return this.serverUrl??"a room server"}supports(e){return e==="open_editor"?!!this.editorUrl:pt.has(e)}async openEditor({document:e,job:t}={}){if(!this.editorUrl)return{ok:!1,message:"there is no editor to open from here"};let r=this.editorUrl;try{r=`${this.editorUrl}/d/${this.#t(e,t)}`}catch{r=`${this.editorUrl}/library`}return await ft(r),{ok:!0,message:`Opened ${r}`}}async newSprite({name:e,w:t=32,h:r=32,intent:o,palette:s}){let i=await this.createDocument?.({name:e,w:t,h:r})??$e(e),a=await this.begin(i,o,{open:{w:t,h:r,name:e}});return s?.length&&await this.submit(a,"set_palette",[{t:"setPalette",colors:s}]),this.#e(i,o),{job:a,...this.#n(i)}}async#e(e,t){let r=process.env.SPRITE_ASKED_IN,o=process.env.SPRITE_REPLY_TO??null;if(r)try{(await this.open(r)).answer("thinking",t?String(t).slice(0,200):"making a new sprite",{reply:o,made:e})}catch{}}async beginJob({document:e,intent:t,layer:r=null}){return{job:await this.begin(e,t,{layer:r}),...this.#n(String(e))}}finishJob({job:e}){let t=this.resolve(e);return this.finish(t.job),{finished:t.intent,document:t.room}}#t(e,t){return e?String(e):this.resolve(t).room}async info({document:e,job:t}={}){let r=this.#t(e,t);return await this.peek(r),this.#n(r)}#n(e){let t=this.client(e),r=t.state;return{document:e,name:r.doc.name,w:r.doc.w,h:r.doc.h,layers:r.doc.layers.map(o=>o.name),frames:r.doc.frames.length,palette:r.doc.palette,tags:r.doc.tags.map(o=>o.name),seq:r.seq,participants:t.participants.map(o=>o.name)}}async readArt({document:e,job:t,layer:r,frame:o=0,trim:s=!1}){let i=this.#t(e,t),a=await this.peek(i);return{document:i,...a.session.readArt(this.#o(i,r),o,{trim:s})}}async preview({document:e,job:t,frame:r=0}){let o=await this.peek(this.#t(e,t)),s=this.#t(e,t);return{png:o.session.png(r),caption:`${s}, frame ${r+1}`}}async sheet({document:e,job:t,columns:r=4,scale:o=2}){let s=this.#t(e,t),i=await this.peek(s);return{png:i.session.contactSheet({columns:r,scale:o}),caption:`${s}, ${i.state.doc.frames.length} frames, numbered left to right`}}async documents(){let e=[...this.jobs.entries()].map(([s,i])=>({job:s,document:i.room,intent:i.intent})),t=[...this.rooms.keys()],r=null;if(this.serverUrl){let s=this.serverUrl.replace(/^ws/,"http").replace(/\/$/,"");r=await fetch(`${s}/documents`).then(i=>i.ok?i.json():null).then(i=>i?.documents?.map(a=>a.id)).catch(()=>null)}return{documents:r?[...new Set([...t,...r])]:t,jobs:e,open:t}}takeRequests(){let e=this.asked.splice(0,this.asked.length);return{requests:e,note:e.length?"Each says what it wants. Where `room` is set, answer in that document. Where it is null the request came from the library and wants a new sprite \u2014 call new_sprite and work there, and do not touch `askedIn`, which is only the room the words travelled through. The person is watching.":"Nobody has asked for anything."}}#o(e,t){let r=this.client(e).state;if(t!=null&&t!==""){let o=typeof t=="number"?r.doc.layers[t]:r.doc.layers.find(s=>s.name===t||s.id===t);if(!o)throw new Error(`no layer ${JSON.stringify(t)} in ${e}`);return o.id}return r.doc.layers[r.doc.layers.length-1].id}async drawArt({job:e,rows:t,colors:r,x:o=0,y:s=0,center:i=!1,clear:a=!1,layer:c,frame:l=0,label:u}){let h=this.resolve(e),f=this.client(h.room).session.drawArt(t,r,{layer:c!=null?this.#o(h.room,c):h.layer??this.#o(h.room,null),at:l,x:o,y:s,center:i,clear:a});return await this.submit(h.job,u||"draw_art",f),{...this.#n(h.room),drew:`${t[0]?.length??0}\xD7${t.length}`}}async drawAnimation({job:e,frames:t,colors:r,fps:o,layer:s,label:i}){let a=this.resolve(e),c=this.client(a.room),l=s!=null?this.#o(a.room,s):a.layer??this.#o(a.room,null),u=c.state.doc.frames.length,h=t.length-u;if(h>0){let f=[],w=c.state.doc.frames[u-1]?.index??"V";for(let d=0;d<h;d++)w=w+"V",f.push({t:"addFrame",id:E(),index:w,durMs:H(o)});await this.submit(a.job,"add frames",f)}if(o){let f=H(o);await this.submit(a.job,"set fps",c.state.doc.frames.map(w=>({t:"setFrameDur",id:w.id,durMs:f})))}let m=c.session.drawAnimation(t,r,{layer:l});return await this.submit(a.job,i||"draw_animation",m),{...this.#n(a.room),wrote:`${t.length} frames`}}async addLayer({job:e,name:t}){let r=this.resolve(e),o=this.client(r.room).state,s=o.doc.layers[o.doc.layers.length-1];return await this.submit(r.job,"add_layer",[{t:"addLayer",id:E(),index:(s?.index??"V")+"V",name:t||`Layer ${o.doc.layers.length+1}`,visible:!0,locked:!1,opacity:1}]),this.#n(r.room)}async addFrame({job:e,copy:t=!1}){let r=this.resolve(e),o=this.client(r.room),s=o.state,i=s.doc.frames[s.doc.frames.length-1],a=E(),c=[{t:"addFrame",id:a,index:(i?.index??"V")+"V",durMs:i?.durMs??83}];if(t&&i)for(let l of s.doc.layers){let u=o.session.cel(l.id,i.id);u.every(h=>h===0)||c.push(Ae(l.id,a,s.doc,u))}return await this.submit(r.job,"add_frame",c),this.#n(r.room)}async setPalette({job:e,colors:t}){let r=this.resolve(e);return await this.submit(r.job,"set_palette",[{t:"setPalette",colors:t}]),{...this.#n(r.room),palette:t.length}}async setFps({job:e,fps:t}){let r=this.resolve(e),o=this.client(r.room).state,s=H(t);return await this.submit(r.job,"set_fps",o.doc.frames.map(i=>({t:"setFrameDur",id:i.id,durMs:s}))),{...this.#n(r.room),fps:t}}async clearCel({job:e,layer:t,frame:r=0}){let o=this.resolve(e),i=this.client(o.room).state,a=this.#o(o.room,t??o.layer),c=i.doc.frames[r];if(!c)throw new Error(`no frame ${r} in ${o.room}`);let l=new Uint8Array(i.doc.w*i.doc.h*4);return await this.submit(o.job,"clear",[Ae(a,c.id,i.doc,l)]),this.#n(o.room)}async trim({job:e,margin:t=0}){let r=this.resolve(e),o=this.client(r.room),s=o.state,i=this.#o(r.room,r.layer),a=o.session.readArt(i,0,{trim:!0}),c=a.rect.w+t*2,l=a.rect.h+t*2;return c>=s.doc.w&&l>=s.doc.h?{...this.#n(r.room),trimmed:"already tight"}:(await this.submit(r.job,"trim_canvas",[{t:"resize",w:c,h:l,dx:-a.rect.x+t,dy:-a.rect.y+t}]),{...this.#n(r.room),trimmed:`${c}\xD7${l}`})}},pt=new Set(["new_sprite","begin_job","finish_job","draw_art","draw_animation","read_art","render_preview","render_animation","add_layer","add_frame","set_palette","set_fps","clear","trim_canvas","get_info","list_documents","take_requests"]);function Ae(n,e,t,r){return{t:"paintRegion",layer:n,frame:e,rect:{x:0,y:0,w:t.w,h:t.h},rle:mt(r)}}function mt(n){let e=[];if(n.length){let t=[n[0],n[1],n[2],n[3]],r=0,o=()=>{let s=r;do{let i=s&127;s>>=7,e.push(s?i|128:i)}while(s);e.push(...t)};for(let s=0;s<n.length;s+=4)n[s]===t[0]&&n[s+1]===t[1]&&n[s+2]===t[2]&&n[s+3]===t[3]?r++:(o(),t=[n[s],n[s+1],n[s+2],n[s+3]],r=1);o()}return Buffer.from(Uint8Array.from(e)).toString("base64")}function ft(n){let e=process.platform==="darwin"?"open":process.platform==="win32"?"start":"xdg-open";return new Promise(t=>ht(e,[n],()=>t()))}function H(n){return!n||n<=0?83:Math.max(10,Math.round(1e3/n))}function Pe(n,e=400){return new Promise(t=>{let r=Date.now(),o=()=>{if(n.state.pending===0&&Date.now()-r>40||Date.now()-r>e)return t();setTimeout(o,10)};o()})}function Oe(){return(Date.now()^Math.floor(Math.random()*4294967295))>>>0}import{execFile as wt}from"node:child_process";var yt=2e4,gt=5e3,R=class n{constructor({base:e,name:t="agent"}={}){this.base=String(e).replace(/\/$/,""),this.name=t,this.log=[],this.beat=setInterval(()=>void this.announce(),gt),this.beat.unref&&this.beat.unref()}get kind(){return"local"}get where(){return this.base}supports(e){return bt.has(e)}static async probe(e,{timeoutMs:t=700}={}){try{let r=await fetch(`${e.replace(/\/$/,"")}/bridge/status`,{signal:AbortSignal.timeout(t)});if(!r.ok)return null;let o=await r.json();return o?.server==="sprite"?o:null}catch{return null}}async announce(){try{await fetch(`${this.base}/bridge/agent`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:this.name,version:null}),signal:AbortSignal.timeout(2e3)})}catch{}}async call(e,t=[],r=yt){let o;try{o=await(await fetch(`${this.base}/bridge/call`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({method:e,args:t,timeoutMs:r}),signal:AbortSignal.timeout(r+2e3)})).json()}catch(s){throw new Error(`lost contact with the Pixie editor at ${this.base}: ${s.message}`)}if(!o.ok)throw new Error(o.error);return this.note(e),o.result}note(e,t=""){this.log.unshift({at:Date.now(),tool:e,room:"local",detail:t}),this.log.length>200&&this.log.pop()}newSprite({name:e,w:t=32,h:r=32,intent:o,palette:s}){return this.call("newSprite",[{w:t,h:r,name:e,palette:s,intent:o,replace:!1}])}beginJob({document:e,intent:t}){return this.call("beginJob",[{document:e,intent:t}])}finishJob({job:e}){return this.call("finishJob",[{job:e}])}async info({document:e}={}){let t=await this.call("info",[]);return{editor:this.base,...e?{asked:e}:{},...t}}readArt({document:e,layer:t,frame:r}){return this.call("readArt",[{document:e,layer:t,frame:r}])}async preview({document:e,frame:t,scale:r=8}){let o=await this.call("toDataURL",[t??null,r,e??null]);return{png:Ie(o.url),caption:o.caption}}async sheet({document:e,columns:t=0,scale:r=0}){let o=await this.call("toStripDataURL",[{document:e??null,columns:t,scale:r}]);return{png:Ie(o.url),caption:o.caption}}documents(){return this.call("listDocuments",[])}drawArt({job:e,rows:t,colors:r,x:o,y:s,center:i,clear:a,layer:c,frame:l,label:u}){return this.call("drawArt",[t,r,{job:e,x:o,y:s,center:i,clear:a,layer:c,frame:l,label:u}])}drawAnimation({job:e,frames:t,colors:r,fps:o,layer:s,label:i}){return this.call("drawAnimation",[{frames:t,colors:r,fps:o,job:e,layer:s,label:i}])}addLayer({name:e}){return this.call("addLayer",[e])}addFrame({copy:e=!1}){return this.call("addFrame",[{copy:!!e}])}setPalette({colors:e}){return this.call("setPalette",[e])}setFps({fps:e}){return this.call("setFps",[e])}clearCel({job:e,layer:t,frame:r}){return this.call("clear",[{job:e,layer:t,frame:r}])}trim({job:e,margin:t=0}){return this.call("trimCanvas",[{job:e,margin:t}])}setFrame({index:e}){return this.call("setFrame",[e])}setLayer({index:e}){return this.call("setLayer",[e])}select({document:e}){return this.call("selectDocument",[e])}closeDocument({document:e}){return this.call("closeDocument",[e??null])}undo(){return this.call("undo",[])}redo(){return this.call("redo",[])}async openEditor(){if((await n.probe(this.base))?.connected)return{ok:!0,message:`Editor already connected at ${this.base}`};await St(this.base);let t=Date.now()+1e4;for(;Date.now()<t;){if((await n.probe(this.base))?.connected)return{ok:!0,message:`Editor connected at ${this.base}`};await new Promise(o=>setTimeout(o,200))}return{ok:!1,message:`Opened ${this.base} but nothing connected \u2014 check the browser window.`}}close(){clearInterval(this.beat)}},bt=new Set(["new_sprite","begin_job","finish_job","draw_art","draw_animation","read_art","render_preview","render_animation","add_layer","add_frame","set_palette","set_fps","clear","trim_canvas","get_info","list_documents","open_editor","select_document","close_document","set_frame","set_layer","undo","redo"]);function Ie(n){let e=String(n??"").split(",")[1];if(!e)throw new Error("the editor returned something that was not an image");return Buffer.from(e,"base64")}function St(n){let e=process.platform==="darwin"?"open":process.platform==="win32"?"start":"xdg-open";return new Promise(t=>wt(e,[n],()=>t()))}var C={type:"string",description:"#rrggbb"},g={type:"string",description:"The job this call belongs to. May be omitted when exactly one job is open."},T={type:"string",description:"Which document to read. Omit to use the one your job owns. In a local editor a 0-based tab index or a name both work; in a room it is the room name."};var Le=n=>({content:[{type:"text",text:typeof n=="string"?n:JSON.stringify(n)}]}),Ne=(n,e)=>({content:[{type:"image",data:Buffer.from(n).toString("base64"),mimeType:"image/png"},{type:"text",text:e}]}),Et=[{name:"new_sprite",description:"Create a sprite and claim it. Call this FIRST \u2014 before writing a script, computing a grid, or thinking about pixels. It costs milliseconds and puts a labelled document in front of the user immediately, so they can see what you are making and where. The tab opens in the background and never steals their focus.",inputSchema:{type:"object",properties:{name:{type:"string",description:"Document name. Shown on the tab and used for exports."},w:{type:"integer",default:32},h:{type:"integer",default:32},palette:{type:"array",items:C,description:"Optional starting palette."},intent:{type:"string",description:"One line on what you are making. Shown while you work."}},required:["name","intent"]},run:(n,e)=>e.newSprite({name:n.name,w:n.w??32,h:n.h??32,intent:n.intent,palette:n.palette})},{name:"begin_job",description:"Claim an existing document for one piece of work \u2014 the other way to start, where new_sprite is the one for new art. Refused by name if another job holds it, which is what keeps parallel requests from colliding.",inputSchema:{type:"object",properties:{document:T,intent:{type:"string"},layer:{type:"string",description:"Restrict the claim to one layer, leaving the rest free. Rooms only."}},required:["document","intent"]},run:(n,e)=>e.beginJob({document:n.document,intent:n.intent,layer:n.layer??null})},{name:"finish_job",description:"Release the document. Call this when the sprite is done, so the user knows to look.",inputSchema:{type:"object",properties:{job:g}},run:(n,e)=>e.finishJob({job:n.job})},{name:"draw_art",description:'Draw a character grid. Each string in `rows` is one row of pixels and each character maps to a colour via `colors`. "." and " " leave existing pixels alone; a colour mapped to null erases. Every character is resolved before a pixel moves, so a typo fails rather than half-drawing. Keep the palette small \u2014 four to eight colours, reused for shading \u2014 because that is what makes art read as pixel art rather than as a downscaled photograph.',inputSchema:{type:"object",properties:{rows:{type:"array",items:{type:"string"}},colors:{type:"object",additionalProperties:{oneOf:[C,{type:"null"}]}},x:{type:"integer"},y:{type:"integer"},center:{type:"boolean"},clear:{type:"boolean",description:"Erase the cel first."},layer:{type:"string",description:"Layer name or index. Defaults to the top one."},frame:{type:"integer",description:"Frame index. Defaults to the first."},label:{type:"string",description:"What this shows as in the undo history."},job:g},required:["rows","colors"]},run:(n,e)=>e.drawArt({job:n.job,rows:n.rows,colors:n.colors,x:n.x??0,y:n.y??0,center:n.center??!1,clear:n.clear??!1,layer:n.layer,frame:n.frame??0,label:n.label})},{name:"draw_animation",description:"Write every frame in ONE call, as a single undo step. Use copyPrevious so a frame carries the last one forward and sends only the pixels that moved \u2014 that is what takes a six-frame loop from four thousand tokens of restated grid to under a thousand. Never loop draw_art.",inputSchema:{type:"object",properties:{frames:{type:"array",minItems:1,items:{type:"object",properties:{rows:{type:"array",items:{type:"string"}},copyPrevious:{type:"boolean"},x:{type:"integer"},y:{type:"integer"},center:{type:"boolean"}},required:["rows"]}},colors:{type:"object",additionalProperties:{oneOf:[C,{type:"null"}]}},fps:{type:"number"},layer:{type:"string"},label:{type:"string"},job:g},required:["frames","colors"]},run:(n,e)=>e.drawAnimation({job:n.job,frames:n.frames,colors:n.colors,fps:n.fps,layer:n.layer,label:n.label})},{name:"read_art",description:"Read a cel back as a grid \u2014 the exact inverse of draw_art, so you can extend art a human drew rather than only replacing it. Reads never need a job and never wait.",inputSchema:{type:"object",properties:{document:T,layer:{type:"string"},frame:{type:"integer"},trim:{type:"boolean",description:"Return only the drawn area. Rooms only."},job:g}},run:(n,e)=>e.readArt({document:n.document,job:n.job,layer:n.layer,frame:n.frame??0,trim:n.trim??!1})},{name:"render_preview",description:"See one frame as an image, so you can check your own work rather than working blind.",inputSchema:{type:"object",properties:{document:T,frame:{type:"integer"},scale:{type:"integer"},job:g}},async run(n,e){let t=await e.preview({document:n.document,job:n.job,frame:n.frame??0,scale:n.scale??8});return Ne(t.png,t.caption)}},{name:"render_animation",description:"Every frame as one numbered contact sheet. Stills inspected one call at a time, minutes apart, cannot tell you whether motion reads; a contact sheet can, in one call. Check any animation with this before calling it done.",inputSchema:{type:"object",properties:{document:T,columns:{type:"integer",default:4},scale:{type:"integer",default:2},job:g}},async run(n,e){let t=await e.sheet({document:n.document,job:n.job,columns:n.columns??4,scale:n.scale??2});return Ne(t.png,t.caption)}},{name:"add_layer",description:"Add a layer above the others. Separate layers keep pieces independently editable.",inputSchema:{type:"object",properties:{name:{type:"string"},job:g}},run:(n,e)=>e.addLayer({job:n.job,name:n.name})},{name:"add_frame",description:"Add an animation frame after the last one.",inputSchema:{type:"object",properties:{copy:{type:"boolean",description:"Start from the previous frame."},job:g}},run:(n,e)=>e.addFrame({job:n.job,copy:n.copy??!1})},{name:"set_palette",description:"Replace the palette. Constraining yourself to a small fixed palette is the single biggest quality lever in pixel art.",inputSchema:{type:"object",properties:{colors:{type:"array",items:C,minItems:1},job:g},required:["colors"]},run:(n,e)=>e.setPalette({job:n.job,colors:n.colors})},{name:"set_fps",description:"Set playback speed for every frame. Eight to twelve suits character animation.",inputSchema:{type:"object",properties:{fps:{type:"integer",minimum:1,maximum:60},job:g},required:["fps"]},run:(n,e)=>e.setFps({job:n.job,fps:n.fps})},{name:"clear",description:"Erase a cel. Destructive, so it goes through a job like every other write.",inputSchema:{type:"object",properties:{layer:{type:"string"},frame:{type:"integer"},job:g}},run:(n,e)=>e.clearCel({job:n.job,layer:n.layer,frame:n.frame??0})},{name:"trim_canvas",description:"Crop the canvas to the drawn pixels. CALL THIS BEFORE finish_job ON EVERY SPRITE. You cannot know a sprite's extent before you have drawn it, so overshoot at new_sprite and trim afterwards \u2014 empty space is inherited by every export, thumbnail and sprite sheet.",inputSchema:{type:"object",properties:{margin:{type:"integer",default:0},job:g}},run:(n,e)=>e.trim({job:n.job,margin:n.margin??0})},{name:"get_info",description:"What is in a document \u2014 size, layers, frames, palette \u2014 and who else is looking at it.",inputSchema:{type:"object",properties:{document:T,job:g}},run:(n,e)=>e.info({document:n.document,job:n.job})},{name:"list_documents",description:'Every document you can reach, and every job currently open. Call this when the user names a sprite ("add a frame to the hamster") so you can pass it to begin_job. Never guess a name.',inputSchema:{type:"object",properties:{}},run:(n,e)=>e.documents()},{name:"take_requests",description:"Requests people have typed into the editor and not yet been answered. Call this when the user asks you to check, or after joining a room they are in. Each one is somebody asking for a sprite in words \u2014 treat it exactly as if they had said it to you directly. Each carries what it wants: a `room` to answer in, or `room: null` for a request typed at the library, which wants a brand new sprite rather than a change to any existing one.",inputSchema:{type:"object",properties:{}},run:(n,e)=>e.takeRequests()},{name:"open_editor",description:"Open the Pixie editor in the default browser and wait for it to connect. Run this first if nothing is connected.",inputSchema:{type:"object",properties:{}},async run(n,e){let t=await e.openEditor();return Le(t.message)}},{name:"select_document",description:"Bring a document to the front for the USER to look at. This does not give you the right to draw in it \u2014 begin_job does that.",inputSchema:{type:"object",properties:{document:{type:["string","integer"]}},required:["document"]},run:(n,e)=>e.select({document:n.document})},{name:"close_document",description:"Close a document tab. The document stays in the library, so this tidies the workspace rather than deleting anything.",inputSchema:{type:"object",properties:{document:{type:["string","integer"]}}},run:(n,e)=>e.closeDocument({document:n.document??null})},{name:"set_frame",description:"Move the user to a frame, by 0-based index.",inputSchema:{type:"object",properties:{index:{type:"integer"}},required:["index"]},run:(n,e)=>e.setFrame({index:n.index})},{name:"set_layer",description:"Move the user to a layer, by 0-based index counting from the bottom.",inputSchema:{type:"object",properties:{index:{type:"integer"}},required:["index"]},run:(n,e)=>e.setLayer({index:n.index})},{name:"undo",description:"Undo the last change, so the user can always reject generated art.",inputSchema:{type:"object",properties:{}},run:(n,e)=>e.undo()},{name:"redo",description:"Redo the last undone change.",inputSchema:{type:"object",properties:{}},run:(n,e)=>e.redo()}];function Ce(n){return Et.filter(e=>n.supports(e.name)).map(e=>({name:e.name,description:e.description,inputSchema:e.inputSchema,run:async t=>{let r=await e.run(t??{},n);return r&&typeof r=="object"&&Array.isArray(r.content)?r:Le(r)}}))}import{readFileSync as xt}from"node:fs";var kt="0.2.0";function vt(){let n=new URL("./version.json",import.meta.url);try{let e=JSON.parse(xt(n,"utf8"))?.version;if(typeof e=="string"&&e)return e;throw new Error("it names no version")}catch(e){throw new Error(`${n.pathname} could not be read as a version: ${e.message}`)}}var De=kt??vt();function V(n="room"){return`Pixel art editor with live collaboration. ${n==="local"?`Documents are tabs in a browser window on this machine. The user is
3
3
  looking at it while you work, and can take the canvas back from you at any
4
4
  moment.`:`Documents are rooms; other people and other agents may be in one with
5
5
  you, and they see what you do as you do it.`}
@@ -16,7 +16,7 @@ one job open you may omit the job argument; with several you must pass it. The
16
16
  user can see your job and revoke it \u2014 if a call fails saying the user stopped
17
17
  you, do NOT retry. Say what you were about to do and ask.
18
18
 
19
- ${xt}`}var xt=`SPEED. Wall clock is round trips times payload, so:
19
+ ${_t}`}var _t=`SPEED. Wall clock is round trips times payload, so:
20
20
  - Silhouette before detail. At 32px a front-on animal is indistinguishable from
21
21
  any other animal; profile is what reads. When the subject could go several
22
22
  ways, draw 2-3 rough silhouettes as frames, look at them with
@@ -30,16 +30,16 @@ ${xt}`}var xt=`SPEED. Wall clock is round trips times payload, so:
30
30
 
31
31
  Do not ask the user abstract clarifying questions. A human round trip costs
32
32
  minutes; a tool round trip costs seconds. Commit to a good default and show
33
- something.`,kt=W("room"),C=class{constructor({name:e="sprite",version:t=Pe,tools:r=[],instructions:o=kt}={}){this.name=e,this.version=t,this.tools=r,this.instructions=o,this.client=null,this.onCall=null}async handle(e){if(!e||e.jsonrpc!=="2.0")return null;let t=e.id===void 0||e.id===null;try{let r=await this.#e(e);return t?null:{jsonrpc:"2.0",id:e.id,result:r}}catch(r){return t?null:{jsonrpc:"2.0",id:e.id,error:{code:-32603,message:r?.message??String(r)}}}}async#e(e){switch(e.method){case"initialize":return this.client=e.params?.clientInfo??null,{protocolVersion:e.params?.protocolVersion??"2024-11-05",capabilities:{tools:{}},serverInfo:{name:this.name,version:this.version},instructions:this.instructions};case"notifications/initialized":case"notifications/cancelled":return{};case"ping":return{};case"tools/list":return{tools:this.tools.map(t=>({name:t.name,description:t.description,inputSchema:t.inputSchema}))};case"tools/call":{let t=this.tools.find(r=>r.name===e.params?.name);if(!t)throw new Error(`unknown tool: ${e.params?.name}`);this.onCall?.(t.name,e.params?.arguments??{});try{return await t.run(e.params?.arguments??{})}catch(r){return{isError:!0,content:[{type:"text",text:r?.message??String(r)}]}}}case"resources/list":return{resources:[]};case"prompts/list":return{prompts:[]};default:throw new Error(`unknown method: ${e.method}`)}}serve(e=process.stdin,t=process.stdout){let r="";return e.setEncoding("utf8"),e.on("data",async o=>{r+=o;let s;for(;(s=r.indexOf(`
33
+ something.`,jt=V("room"),D=class{constructor({name:e="pixie",version:t=De,tools:r=[],instructions:o=jt}={}){this.name=e,this.version=t,this.tools=r,this.instructions=o,this.client=null,this.onCall=null}async handle(e){if(!e||e.jsonrpc!=="2.0")return null;let t=e.id===void 0||e.id===null;try{let r=await this.#e(e);return t?null:{jsonrpc:"2.0",id:e.id,result:r}}catch(r){return t?null:{jsonrpc:"2.0",id:e.id,error:{code:-32603,message:r?.message??String(r)}}}}async#e(e){switch(e.method){case"initialize":return this.client=e.params?.clientInfo??null,{protocolVersion:e.params?.protocolVersion??"2024-11-05",capabilities:{tools:{}},serverInfo:{name:this.name,version:this.version},instructions:this.instructions};case"notifications/initialized":case"notifications/cancelled":return{};case"ping":return{};case"tools/list":return{tools:this.tools.map(t=>({name:t.name,description:t.description,inputSchema:t.inputSchema}))};case"tools/call":{let t=this.tools.find(r=>r.name===e.params?.name);if(!t)throw new Error(`unknown tool: ${e.params?.name}`);this.onCall?.(t.name,e.params?.arguments??{});try{return await t.run(e.params?.arguments??{})}catch(r){return{isError:!0,content:[{type:"text",text:r?.message??String(r)}]}}}case"resources/list":return{resources:[]};case"prompts/list":return{prompts:[]};default:throw new Error(`unknown method: ${e.method}`)}}serve(e=process.stdin,t=process.stdout){let r="";return e.setEncoding("utf8"),e.on("data",async o=>{r+=o;let s;for(;(s=r.indexOf(`
34
34
  `))!==-1;){let i=r.slice(0,s).trim();if(r=r.slice(s+1),!i)continue;let a;try{a=JSON.parse(i)}catch{continue}let c=await this.handle(a);c&&t.write(JSON.stringify(c)+`
35
- `)}}),this}};var A=(n,e)=>{let t=process.argv.indexOf(`--${n}`);return t>-1?process.argv[t+1]:e},V=process.env.SPRITE_EDITOR||"http://127.0.0.1:4242",U=process.env.SPRITE_SERVER||"ws://127.0.0.1:4243",vt=new URL("./sprite_core.wasm",import.meta.url);function jt(n){let e=String(n??"").trim();return!e||!/^[a-z0-9]+([-_][a-z0-9]+)*$/.test(e)?e||"an agent":e.split(/[-_]/).map(t=>Rt.has(t)?t.toUpperCase():t[0].toUpperCase()+t.slice(1)).join(" ")}var Rt=new Set(["cli","ai","api","ide","mcp","sdk","gpt","llm"]);function At(n){let e=new WebSocket(n),t={readyState:0,onopen:null,onmessage:null,onclose:null,send:r=>e.send(r),close:()=>e.close()};return e.addEventListener("open",()=>{t.readyState=1,t.onopen?.()}),e.addEventListener("message",r=>t.onmessage?.(r.data)),e.addEventListener("close",()=>{t.readyState=3,t.onclose?.()}),e.addEventListener("error",()=>{}),t}async function $t(n,{timeoutMs:e=700}={}){if(await X(n,{timeoutMs:e}))return!0;try{let t=await fetch(`${p(n)}/health`,{signal:AbortSignal.timeout(e)});return!t.ok||!(t.headers.get("content-type")??"").includes("json")?!1:(await t.json().catch(()=>null))?.ok===!0}catch{return!1}}async function Tt({editor:n=V,server:e=U,allowHost:t=!0,publicServer:r=v}={}){if(r)return{kind:"room",url:r,why:`the Sprite this agent belongs to, at ${p(r)}`};let o=await j.probe(n);return o?{kind:"local",url:n,why:o.connected?`a Sprite editor is open at ${n}`:`a Sprite server is running at ${n}, with no editor open yet`}:await $t(e)?{kind:"room",url:e,why:`a room server is running at ${e}`}:t?{kind:"room",url:e,start:!0,why:"nothing was running, so this process is the server"}:{kind:"room",url:e,why:`nothing is running yet; waiting for one at ${e}`}}async function Ot({kind:n=null,editor:e=V,server:t=U,wasm:r=vt,name:o="agent",allowHost:s=!0}={}){let i=n?{kind:n,url:n==="local"?e:t,why:"you said so"}:await Tt({editor:e,server:t,allowHost:s}),a,c=null,l=null;if(i.kind==="local")a=new j({base:i.url,name:o}),a.announce();else{i.start;let d=Y(i.url),m=r instanceof Uint8Array?r:await _t(r);c=await O.load(m);let w=await d;a=new N(c,{author:E(),name:o,connect:(f,S={})=>{let $=new URLSearchParams;S.w&&$.set("w",String(S.w)),S.h&&$.set("h",String(S.h)),S.name&&$.set("name",S.name);let z=$.toString();return At(`${w}/rooms/${f}${z?`?${z}`:""}`)}}),a.serverUrl=w,a.getToken=async f=>{let S=await oe(i.url,f);if(S!==void 0)return S;if(await Ne(i.url))throw new Error(Le(i.url))},a.editorUrl=p(i.url),a.hosted=l}let h=new C({tools:Oe(a),instructions:W(a.kind)}),u=h.handle.bind(h);return h.handle=async d=>{let m=await u(d);return d?.method==="initialize"&&h.client?.name&&(a.name=jt(h.client.name),a.announce()),m},{core:c,backend:a,mcp:h,chosen:i}}var Ie=null;function Ne(n){return Ie??=re(n).catch(()=>!1),Ie}function Pt(n=process.argv){let e=n[1]??"";return/[\\/]pixie-mcp$/.test(e)?"pixie-mcp":/[\\/]sprite-mcp$/.test(e)?"sprite-mcp":"npx pixie-mcp"}function Le(n,e=Pt()){return`this machine has not been given a credential for ${p(n)} yet. Run \`${e} login\` once, in a terminal \u2014 it opens a browser, and one button in it is the whole of it.`}if(we(import.meta.url)){let n=A("server",null)?"room":A("editor",null)?"local":null;if(process.argv.includes("login")){let e=A("server",v||U);try{let t=await me(e,{onUrl:r=>process.stderr.write(`
35
+ `)}}),this}};var A=(n,e)=>{let t=process.argv.indexOf(`--${n}`);return t>-1?process.argv[t+1]:e},K=process.env.SPRITE_EDITOR||"http://127.0.0.1:4242",U=process.env.SPRITE_SERVER||"ws://127.0.0.1:4243",Rt=new URL("./sprite_core.wasm",import.meta.url);function Tt(n){let e=String(n??"").trim();return!e||!/^[a-z0-9]+([-_][a-z0-9]+)*$/.test(e)?e||"an agent":e.split(/[-_]/).map(t=>At.has(t)?t.toUpperCase():t[0].toUpperCase()+t.slice(1)).join(" ")}var At=new Set(["cli","ai","api","ide","mcp","sdk","gpt","llm"]);function Pt(n){let e=new WebSocket(n),t={readyState:0,onopen:null,onmessage:null,onclose:null,send:r=>e.send(r),close:()=>e.close()};return e.addEventListener("open",()=>{t.readyState=1,t.onopen?.()}),e.addEventListener("message",r=>t.onmessage?.(r.data)),e.addEventListener("close",()=>{t.readyState=3,t.onclose?.()}),e.addEventListener("error",()=>{}),t}async function Ot(n,{timeoutMs:e=700}={}){if(await Q(n,{timeoutMs:e}))return!0;try{let t=await fetch(`${p(n)}/health`,{signal:AbortSignal.timeout(e)});return!t.ok||!(t.headers.get("content-type")??"").includes("json")?!1:(await t.json().catch(()=>null))?.ok===!0}catch{return!1}}async function It({editor:n=K,server:e=U,allowHost:t=!0,publicServer:r=j}={}){if(r)return{kind:"room",url:r,why:`the Pixie this agent belongs to, at ${p(r)}`};let o=await R.probe(n);return o?{kind:"local",url:n,why:o.connected?`a Pixie editor is open at ${n}`:`a Pixie server is running at ${n}, with no editor open yet`}:await Ot(e)?{kind:"room",url:e,why:`a room server is running at ${e}`}:t?{kind:"room",url:e,start:!0,why:"nothing was running, so this process is the server"}:{kind:"room",url:e,why:`nothing is running yet; waiting for one at ${e}`}}async function Nt({kind:n=null,editor:e=K,server:t=U,wasm:r=Rt,name:o="agent",allowHost:s=!0}={}){let i=n?{kind:n,url:n==="local"?e:t,why:"you said so"}:await It({editor:e,server:t,allowHost:s}),a,c=null,l=null;if(i.kind==="local")a=new R({base:i.url,name:o}),a.announce();else{i.start;let m=ee(i.url),f=r instanceof Uint8Array?r:await $t(r);c=await I.load(f);let w=await m;a=new L(c,{author:E(),name:o,connect:(d,b={})=>{let P=new URLSearchParams;b.w&&P.set("w",String(b.w)),b.h&&P.set("h",String(b.h)),b.name&&P.set("name",b.name);let X=P.toString();return Pt(`${w}/rooms/${d}${X?`?${X}`:""}`)}}),a.serverUrl=w,a.getToken=async d=>{let b=await ae(i.url,d);if(b!==void 0)return b;if(await z(i.url))throw new Error(G(i.url))},a.createDocument=async d=>{let b=await ce(i.url,d);if(b!==void 0)return b;if(await z(i.url))throw new Error(G(i.url))},a.editorUrl=p(i.url),a.hosted=l}let u=new D({tools:Ce(a),instructions:V(a.kind)}),h=u.handle.bind(u);return u.handle=async m=>{let f=await h(m);return m?.method==="initialize"&&u.client?.name&&(a.name=Tt(u.client.name),a.announce()),f},{core:c,backend:a,mcp:u,chosen:i}}var Ue=null;function z(n){return Ue??=ie(n).catch(()=>!1),Ue}function Lt(n=process.argv){let e=n[1]??"";return/[\\/]pixie-mcp$/.test(e)?"pixie-mcp":/[\\/]sprite-mcp$/.test(e)?"sprite-mcp":"npx pixie-mcp"}function G(n,e=Lt()){return`this machine has not been given a credential for ${p(n)} yet. Run \`${e} login\` once, in a terminal \u2014 it opens a browser, and one button in it is the whole of it.`}if(be(import.meta.url)){let n=A("server",null)?"room":A("editor",null)?"local":null;if(process.argv.includes("login")){let e=A("server",j||U);try{let t=await ye(e,{onUrl:r=>process.stderr.write(`
36
36
  ${r}
37
- `),label:"the Sprite MCP server"});process.stderr.write(`
37
+ `),label:"the Pixie MCP server"});process.stderr.write(`
38
38
  This machine can now draw as ${t.email??t.name??"you"}.
39
39
 
40
40
  `)}catch(t){process.stderr.write(`
41
41
  ${t.message}
42
42
 
43
- `),process.exit(1)}}else{let{mcp:e,chosen:t}=await Ot({kind:n,editor:A("editor",V),server:A("server",U),allowHost:!process.argv.includes("--no-host")});process.stderr.write(`sprite-mcp \u2192 ${t.kind} (${t.why})
44
- `),e.serve(),t.kind==="room"&&(async()=>(await F(t.url))?.token||await Ne(t.url)&&process.stderr.write(`sprite-mcp \u2192 ${Le(t.url)}
45
- `))()}}export{V as DEFAULT_EDITOR,U as DEFAULT_SERVER,Pt as commandName,Ot as createServer,Tt as detect,jt as presentable,$t as probeRooms,Le as signInFirst,At as websocketTransport};
43
+ `),process.exit(1)}}else{let{mcp:e,chosen:t}=await Nt({kind:n,editor:A("editor",K),server:A("server",U),allowHost:!process.argv.includes("--no-host")});process.stderr.write(`sprite-mcp \u2192 ${t.kind} (${t.why})
44
+ `),e.serve(),t.kind==="room"&&(async()=>(await O(t.url))?.token||await z(t.url)&&process.stderr.write(`sprite-mcp \u2192 ${G(t.url)}
45
+ `))()}}export{K as DEFAULT_EDITOR,U as DEFAULT_SERVER,Lt as commandName,Nt as createServer,It as detect,Tt as presentable,Ot as probeRooms,G as signInFirst,Pt as websocketTransport};
package/sprite_core.wasm CHANGED
Binary file