pixie-mcp 0.5.2 → 0.5.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pixie-mcp",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
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": {
package/pixie-mcp.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- var $=(r,e)=>()=>(r&&(e=r(r=0)),e);function we(r){let e="";for(let t=0;t<r.length;t+=32768)e+=String.fromCharCode.apply(null,r.subarray(t,t+32768));return btoa(e).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}var ye=$(()=>{"use strict"});var j,nt,rt,ln,ge=$(()=>{"use strict";j="[A-Za-z0-9_-]{1,64}",nt="token|members|link|slow|project",rt=[{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/${j}/(${nt})$`)},{owner:"library",re:new RegExp("^/documents$")},{owner:"library",re:new RegExp("^/documents/(new|register|move|order|folders|folders/delete|trash|trash/empty)$")},{owner:"library",re:new RegExp(`^/documents/${j}(/(thumbnail\\.png|duplicate|rename|restore))?$`)},{owner:"library",re:new RegExp(`^/documents/${j}/history/at/\\d+/thumbnail\\.png$`)},{owner:"document",re:new RegExp(`^/documents/${j}/history(/at/\\d+|/\\d+)?$`)},{owner:"document",re:new RegExp(`^/documents/${j}/comments(/[A-Za-z0-9]{1,40})?$`)},{owner:"document",re:new RegExp(`^/documents/${j}/snapshots(/enabled|/[A-Za-z0-9_-]{1,128})?$`)},{owner:"document",re:new RegExp(`^/favourites(/order|/(document|project)/${j})?$`)}],ln=rt.map(({owner:r,re:e})=>({owner:r,source:e.source}))});var V,X=$(()=>{"use strict";ge();V=["read","draw","create","duplicate","organize"]});async function be(r){let e=new TextEncoder().encode(String(r));return we(new Uint8Array(await crypto.subtle.digest("SHA-256",e)))}var yn,Se=$(()=>{"use strict";ye();X();yn=120*1e3});function k(r=Date.now()){let e="0123456789ABCDEFGHJKMNPQRSTVWXYZ",t=(BigInt(r)&0xffffffffffffn)<<80n,n=crypto.getRandomValues(new Uint8Array(10)),o=0n;for(let i of n)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 v(r){return r==null?{at:0}:typeof r=="number"?{at:r}:{frame:r}}var ht,ut,dt,D,A,Z,O=$(()=>{"use strict";ht=new TextEncoder,ut=new TextDecoder,dt=1;D=class r{#e;#n;constructor(e){this.#e=e,this.#n=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 n=e instanceof Response||typeof e?.then=="function"?e:fetch(e);({instance:t}=await WebAssembly.instantiateStreaming(n,{}))}return new r(t)}get#r(){return new Uint8Array(this.#n.memory.buffer)}call(e,t={}){let n=ht.encode(JSON.stringify({m:e,a:t})),o=this.#n.sprite_alloc(n.length);if(!o)throw new Error("sprite-core: allocation failed");let s=0;try{if(this.#r.set(n,o),s=this.#n.sprite_dispatch(o,n.length),!s)throw new Error("sprite-core: dispatch returned nothing");let i=this.#r,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===dt)return l;let h=JSON.parse(ut.decode(l));if(h&&h.error)throw new Error(h.error);return h}finally{s?this.#n.sprite_free_result(s):this.#n.sprite_dealloc(o,n.length)}}reset(){this.#n.sprite_reset()}newDocument(e={}){return this.call("doc.new",{w:32,h:32,name:"untitled",...e})}decodePng(e){let t=this.call("image.decode",{png:[...e]}),n=new DataView(t.buffer,t.byteOffset,t.byteLength),o=n.getUint32(0,!0),s=n.getUint32(4,!0);return{w:o,h:s,pixels:t.subarray(8)}}openSession(e,t,n=1){let{h:o}=this.call("session.new",{doc:e,author:t,seed:n});return new A(this,o)}openRoom(e,{seq:t=0}={}){let{h:n}=this.call("room.new",{doc:e,...t>0?{seq:t}:{}});return new Z(this,n)}},A=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:n="Edit",job:o=null}={}){let s={now:t,label:n,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,n={}){return this.#e("session.drawArt",{rows:e,colors:t,...n}).bodies}drawAnimation(e,t,n={}){return this.#e("session.drawAnimation",{frames:e,colors:t,...n}).bodies}readArt(e,t,n={}){return this.#e("session.readArt",{layer:e,...v(t),...n})}cel(e,t){return this.#e("session.cel",{layer:e,...v(t)})}layerImage(e,t){return this.#e("session.layerImage",{layer:e,...v(t)})}tileCel(e,t){return this.#e("session.tileCel",{layer:e,...v(t)})}tile(e,t){return this.#e("session.tile",{tileset:e,index:t})}composite(e,{include:t="export"}={}){return this.#e("session.composite",{...v(e),include:t})}png(e,{scale:t=1}={}){return this.#e("session.png",{...v(e),scale:Math.max(1,Math.round(t)||1)})}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:n=630,framing:o=null,bleed:s=0,raw:i=!1}={}){return this.#e("session.card",{width:t,height:n,...o??{},...v(e),...s?{bleed:s}:{},...i?{raw:i}:{}})}cardLayout({frame:e=null,width:t=1200,height:n=630,framing:o=null}={}){return this.#e("session.cardLayout",{width:t,height:n,...o??{},...v(e)})}fit({width:e,height:t,margin:n=0,inscribed:o=!1,dx:s=0,dy:i=0,frame:a=null}){return this.#e("session.fit",{width:e,height:t,margin:n,inscribed:o,dx:s,dy:i,...v(a)})}close(){this.#e("session.free")}},Z=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 Ae(r){return JSON.stringify(r)}function Te(r){let e=r.rle instanceof Uint8Array?r.rle:null,t=new TextEncoder().encode(JSON.stringify({...r,rle:void 0}));if(t.length>pt)return null;let n=new Uint8Array(3+t.length+(e?.length??0));return n[0]=Re,n[1]=t.length&255,n[2]=t.length>>8&255,n.set(t,3),e&&n.set(e,3+t.length),n}function mt(r){return r instanceof Uint8Array?r:r instanceof ArrayBuffer?new Uint8Array(r):ArrayBuffer.isView(r)?new Uint8Array(r.buffer,r.byteOffset,r.byteLength):null}function Pe(r){if(typeof r!="string"){let e=mt(r);if(!e||e.length<3||e[0]!==Re)return null;let t=e[1]|e[2]<<8;if(e.length<3+t)return null;try{let n=JSON.parse(new TextDecoder().decode(e.subarray(3,3+t)));if(!n||typeof n.t!="string")return null;let o=e.subarray(3+t);return n.rle=o.length?Uint8Array.from(o):null,n}catch{return null}}try{let e=JSON.parse(r);return e&&typeof e.t=="string"?e:null}catch{return null}}var g,$e,S,je,Re,pt,Oe=$(()=>{"use strict";g={HELLO:"hello",SUBMIT:"submit",PRESENCE:"presence",STROKE:"stroke",LEASE:"lease",PING:"ping",PROMPT:"prompt",ANSWER:"answer",STOP:"stop"},$e={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"},je={OPS:"ops",SNAPSHOT:"snapshot"};Re=157,pt=65535});function wt(r){return String(r??"").trim().toLowerCase().replace(/[^a-z0-9_-]+/g,"-").replace(/^-+|-+$/g,"").replace(/-{2,}/g,"-").slice(0,64).replace(/-+$/,"")||null}function Ie(r){return`${(wt(r)??"").slice(0,48).replace(/-+$/,"")||"sprite"}-${k().toLowerCase().slice(0,12)}`}var Ne=$(()=>{"use strict";O()});import{readFile as Wt}from"node:fs/promises";import{readFile as Ge,writeFile as Ve,mkdir as Xe,chmod as Ze}from"node:fs/promises";import{readFileSync as Ke}from"node:fs";import{homedir as Ye,hostname as Qe}from"node:os";import{join as H}from"node:path";import{execFile as et}from"node:child_process";var f=r=>String(r??"").replace(/\/+$/,"").replace(/^ws/,"http"),ne=r=>String(r??"").replace(/\/+$/,"").replace(/^http/,"ws");async function re(r,e){if(!r?.ok||!(r.headers.get("content-type")??"").includes("json"))return null;let t=await r.json().catch(()=>null);return!t||typeof t!="object"?null:e.some(n=>n in t)?t:null}async function oe(r,{timeoutMs:e=3e3,fetchImpl:t=fetch}={}){try{let n=await t(`${f(r)}/setup`,{signal:AbortSignal.timeout(e)});return await re(n,["accounts","expectedPort","install"])}catch{return null}}var q=new Map;function se(r,{fetchImpl:e=fetch,timeoutMs:t=3e3}={}){let n=f(r);return q.has(n)||q.set(n,He(r,{fetchImpl:e,timeoutMs:t})),q.get(n)}async function He(r,{fetchImpl:e,timeoutMs:t}){try{let n=await e(`${f(r)}/config.json`,{signal:AbortSignal.timeout(t)}),s=(await re(n,["rooms","accounts"]))?.rooms;if(typeof s=="string"&&/^(wss?|https?):\/\/./.test(s))return ne(s)}catch{}return ne(r)}var T=process.env.SPRITE_PUBLIC_SERVER||tt()||null;function tt(){return"wss://drawpixie.com"}var ie=H(process.env.XDG_CONFIG_HOME||H(Ye(),".config"),"sprite"),G=H(ie,"agent.json"),ae=r=>String(r??"").replace(/\/+$/,"");function ce(){return`${Qe().replace(/\.local$/,"")}`.slice(0,60)||"a computer"}async function le(){try{let r=JSON.parse(await Ge(G,"utf8"));return r&&typeof r=="object"&&r.servers?r:{servers:{}}}catch{return{servers:{}}}}async function P(r){return process.env.SPRITE_TOKEN?{token:process.env.SPRITE_TOKEN,from:"the environment"}:(await le()).servers[ae(r)]??null}async function he(r,e){await Xe(ie,{recursive:!0,mode:448});let t=await le();t.servers[ae(r)]={...e,at:Date.now()};let n=`${G}.tmp`;await Ve(n,JSON.stringify(t,null,2),{mode:384}),await Ze(n,384);let{rename:o}=await import("node:fs/promises");await o(n,G)}async function ue(r,{timeoutMs:e=3e3}={}){try{let t=await fetch(`${f(r)}/setup`,{signal:AbortSignal.timeout(e)});return t.ok?!!(await t.json()).accounts:!1}catch{return!1}}async function de(r,e,{session:t=null}={}){let n=t??await P(r);if(!n?.token)return;let o=await fetch(`${f(r)}/documents/${encodeURIComponent(e)}/token`,{headers:{Authorization:`Bearer ${n.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 pe(r,{name:e,w:t,h:n},{session:o=null}={}){let s=o??await P(r);if(!s?.token)return;let i=await fetch(`${f(r)}/documents/new`,{method:"POST",headers:{Authorization:`Bearer ${s.token}`,"Content-Type":"application/json"},body:JSON.stringify({name:e,w:t,h:n})});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}async function me(r,e,{method:t="GET",body:n=null,session:o=null}={}){let s=o??await P(r);if(!s?.token)return;let i=await fetch(`${f(r)}${e}`,{method:t,headers:{Authorization:`Bearer ${s.token}`,...n!==null?{"Content-Type":"application/json"}:{}},body:n!==null?JSON.stringify(n):void 0});if(i.status===401)throw new Error("this machine is signed out \u2014 pair it again");let a=await i.json().catch(()=>({}));if(!i.ok)throw new Error(a?.error?String(a.error):`the server said ${i.status}`);return a}function fe(r){let e=process.platform==="darwin"?"open":process.platform==="win32"?"start":"xdg-open";return new Promise(t=>et(e,[r],()=>t()))}Se();X();import{createServer as ot}from"node:http";import{randomBytes as ke}from"node:crypto";var st=300*1e3,it=()=>ke(32).toString("base64url"),at=()=>ke(16).toString("base64url");function Ee(r,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">${r}</p><p style="color:#8a8a99;font-size:12px">${e}</p></div>`}async function ct({state:r,timeoutMs:e}){let t,n,o=new Promise((c,l)=>{t=c,n=l}),s=ot((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")!==r){l.writeHead(404).end();return}let u=h.searchParams.get("error");l.writeHead(200,{"Content-Type":"text/html; charset=utf-8"}).end(u?Ee("Nothing was granted.","You can close this window."):Ee("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(()=>n(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 xe(r,{onUrl:e=()=>{},open:t=fe,label:n="the Pixie agent",scope:o=V,timeoutMs:s=st}={}){let i=it(),a=await be(i),c=at(),l=await ct({state:c,timeoutMs:s});try{let h=new URL(`${f(r)}/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",n),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 p=u.get("code");if(!p)throw new Error("the browser came back without an authorization");let w=await fetch(`${f(r)}/auth/authorize/exchange`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:p,verifier:i,name:ce()})}),m=await w.json().catch(()=>({}));if(!w.ok||!m.token)throw new Error(m.error||"that authorization could not be exchanged");return await he(r,{token:m.token,name:m.name,email:m.email,scope:m.scope??null}),m}finally{l.close()}}import{realpathSync as ve}from"node:fs";import{fileURLToPath as lt}from"node:url";function _e(r){let e=process.argv[1];if(!e)return!1;try{return ve(lt(r))===ve(e)}catch{return!1}}O();import{execFile as jt}from"node:child_process";Oe();O();var ft=5e3,C=class{#e=null;constructor(e,t,n){if(!n||!n.author)throw new Error("DocumentClient needs an author");if(this.core=e,this.author=n.author,this.name=n.name||"anonymous",this.agent=n.agent===!0,this.answers=n.answers===!0,this.seed=n.seed??1,this.epoch=0,this.resumed=null,n.resume){let{h:o,dropped:s}=e.call("session.resume",{doc:t,author:this.author,seed:this.seed,seq:n.resume.seq??0,pending:n.resume.pending??[]});this.session=new A(e,o),this.resumed={seq:n.resume.seq??0,dropped:s??[]}}else this.session=e.openSession(t,this.author,this.seed);this.getToken=n.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.waiters=new Set,this.onPrompt=()=>{},this.onAnswer=()=>{},this.onStop=()=>{},this.onStatus=()=>{}}apply(e,t={}){let n=this.session.apply(e,t);return this.#a(n),this.onChange(),n}acked(e,{timeoutMs:t=3e4}={}){let n=new Set((Array.isArray(e)?e:[e]).map(o=>o?.opId??o).filter(Boolean));return new Promise((o,s)=>{let i={ids:n,resolve:o,reject:s,timer:null};i.timer=setTimeout(()=>{this.waiters.delete(i);let a=[...n].filter(c=>this.session.pending().some(l=>l.opId===c)).length;s(new Error(`${a} operation${a===1?"":"s"} not confirmed by the room after ${Math.round(t/1e3)}s \u2014 `+(this.connected?"it is answering slowly":`the connection dropped; nothing since sequence ${this.session.state().seq} has been saved`)))},t),this.waiters.add(i),this.#n()})}#n(){if(!this.waiters.size)return;let e=new Set(this.session.pending().map(t=>t.opId));for(let t of this.waiters)[...t.ids].some(n=>e.has(n))||(this.waiters.delete(t),clearTimeout(t.timer),t.resolve())}#r(e,t){for(let n of this.waiters)n.ids.has(e)&&(this.waiters.delete(n),clearTimeout(n.timer),n.reject(new Error(t)))}undo(e=Date.now()){let t=this.session.undo(e);return this.#a(t),this.onChange(),t}redo(e=Date.now()){let t=this.session.redo(e);return this.#a(t),this.onChange(),t}setPresence(e){this.presence=e,this.connected&&this.#o({t:g.PRESENCE,...e})}sendStroke({layer:e,frame:t,rect:n,rle:o,done:s=!1}){if(!this.connected||!this.welcomed||!this.strokePreview)return;this._strokeSentAt=Date.now();let i=Te({t:g.STROKE,layer:e,frame:t,rect:n,rle:o,done:s});i&&this.#l(i)}lease(e,t={}){this.#o({t:g.LEASE,action:e,...t})}prompt(e,{where:t=$e.DOCUMENT}={}){if(!this.connected)return null;let n=String(e??"").trim().slice(0,2e3);if(!n)return null;let o=k();return this.#o({t:g.PROMPT,id:o,text:n,where:t}),o}answer(e,t="",{reply:n=null,made:o=null,model:s=null}={}){return this.connected?(this.#o({t:g.ANSWER,state:e,text:String(t??"").slice(0,2e3),reply:n,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,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.#o({t:g.PING})},ft),await this.#t()},e.onmessage=t=>this.#i(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 n=this.pendingToken;this.pendingToken=void 0,e=await n}else e=this.getToken?await this.getToken():void 0;let 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})}#i(e){let t=Pe(e);if(t)switch(t.t){case S.WELCOME:return this.#s(t);case S.REHELLO:return this.welcomed=!1,void this.#t();case S.OPS:return this.#h(t);case S.REJECT:return this.#u(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.#d(t.message)){this.strokePreview=!1;return}return this.onLost({kind:"protocol",message:t.message});default:return}}#s(e){this.you=e.you,this.welcomed=!0;let t=this.session.pending();if(e.catchup.kind===je.SNAPSHOT){let{h:n,dropped:o}=this.core.call("session.resume",{doc:e.catchup.doc,author:this.author,seed:this.seed,seq:e.catchup.seq,pending:t});if(this.session.close(),this.session=new A(this.core,n),this.epoch++,o&&o.length){for(let s of o)this.#r(s,"could not be replayed onto the current document");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 n of e.catchup.ops||[])this.#c(n);this.#n(),this.participants=e.participants||[],this.leases=e.leases||[],t.length&&this.#o({t:g.SUBMIT,ops:t}),this.presence&&this.#o({t:g.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}#h(e){let t=!1;for(let n of e.ops||[])this.#c(n),t=!0;t&&(this.#n(),this.onChange())}#c(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`})}#u(e){e.opId&&this.session.reject(e.opId),e.opId&&this.#r(e.opId,e.message),this.onLost({kind:e.kind,ops:e.opId?[e.opId]:[],message:e.message}),this.onChange()}#a(e){!e.length||!this.connected||!this.welcomed||this.#o({t:g.SUBMIT,ops:e})}#o(e){this.#l(Ae(e))}#d(e){return typeof e!="string"?!1:e===`unknown message: ${g.STROKE}`?!0:e==="unreadable message"&&Date.now()-(this._strokeSentAt??0)<2e3}#l(e){if(this.transport)try{this.transport.send(e)}catch{this.connected=!1}}close(){this.disconnect(),this.session.close()}};O();Ne();var x=r=>{let e=String(r).replace("#","");return[parseInt(e.slice(0,2),16),parseInt(e.slice(2,4),16),parseInt(e.slice(4,6),16)]};var R=([r,e,t])=>.3*r+.59*e+.11*t;function U([r,e,t]){let n=Math.max(r,e,t),o=Math.min(r,e,t),s=n-o,i=0;return s&&(n===r?i=(e-t)/s%6:n===e?i=(t-r)/s+2:i=(r-e)/s+4,i*=60,i<0&&(i+=360)),[i,n?s/n:0,n/255]}var Le={luminance:(r,e)=>R(x(r))-R(x(e)),hue:(r,e)=>{let[t,n]=U(x(r)),[o,s]=U(x(e));return n<.08||s<.08?n<.08&&s<.08?R(x(r))-R(x(e)):n<.08?-1:1:Math.round(t/20)!==Math.round(o/20)?t-o:R(x(r))-R(x(e))},saturation:(r,e)=>U(x(r))[1]-U(x(e))[1]};function K(r,e="hue"){let t=Le[e]??Le.hue;return[...r].sort(t)}function yt(r){try{return/^(localhost|127\.0\.0\.1|\[::1\]|::1)$/.test(new URL(f(r)).hostname)}catch{return!1}}function De(r,{publicServer:e=T}={}){let t=f(r);return e&&r===e?`nothing is answering at ${t} \u2014 this machine is offline, or the site is down`:yt(r)?`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`}import{readFile as gt,writeFile as bt}from"node:fs/promises";var M={type:"string",description:"#rrggbb"};async function St(r,e){let t=await gt(r).catch(s=>{throw new Error(`could not read ${r}: ${s.code==="ENOENT"?"no such file":s.message}`)});if(t.length>8&&t[0]===137&&t[1]===80&&t[2]===78&&t[3]===71){if(!e.core?.decodePng)throw new Error("this server cannot decode a PNG \u2014 send rows and colors");return Et(e.core.decodePng(t))}let o;try{o=JSON.parse(t.toString("utf8"))}catch{throw new Error(`${r} is neither a PNG nor JSON`)}if(!o||typeof o!="object"||!Array.isArray(o.rows)||typeof o.colors!="object")throw new Error(`${r} needs "rows" (an array of strings) and "colors" (a map of character to #rrggbb)`);return o}function Et({w:r,h:e,pixels:t}){let n={},o=new Map,s=33,i=l=>{let h=o.get(l);if(h)return h;for(;;)if(s===34||s===46||s===92||s===127)s+=1;else if(s>=128&&s<161)s=161;else if(s>=55296&&s<57344)s=57344;else break;return h=String.fromCodePoint(s),s+=1,o.set(l,h),n[h]=l,h},a=l=>l.toString(16).padStart(2,"0"),c=[];for(let l=0;l<e;l++){let h="";for(let u=0;u<r;u++){let d=(l*r+u)*4,p=t[d+3];if(p===0){h+=".";continue}let w=p===255?`#${a(t[d])}${a(t[d+1])}${a(t[d+2])}`:`#${a(t[d])}${a(t[d+1])}${a(t[d+2])}${a(p)}`;h+=i(w)}c.push(h)}return{rows:c,colors:n}}var b={type:"string",description:"The job this call belongs to. May be omitted when exactly one job is open."},I={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 Ue=r=>({content:[{type:"text",text:typeof r=="string"?r:JSON.stringify(r)}]}),Ce=(r,e)=>({content:[{type:"image",data:Buffer.from(r).toString("base64"),mimeType:"image/png"},{type:"text",text:e}]}),E={w:80,h:42},kt="40\xD721, 80\xD742, 120\xD763, 160\xD784, 240\xD7126",xt="40\xD740, 50\xD750, 80\xD780, 100\xD7100",vt="60\xD720, 75\xD725, 150\xD750, 300\xD7100";function Me(r,e){return!r||!e?null:r===e?"This is square, the shape of a profile picture: in the editor, file \u25B8 Save as profile picture (400\xD7400) fits it inside the circle it will be cropped to. Tell the user.":r===3*e?"This is 3:1, the shape of a profile banner: in the editor, file \u25B8 Save as profile banner (1500\xD7500) exports it at the size X wants. Tell the user.":Math.abs(r/e-1200/630)<.02?"This is the link-card shape: the share button posts it edge to edge.":null}function _t(r){let e=String(r??"").trim();if(!/^[A-Za-z0-9]+([-_][A-Za-z0-9]+)+$/.test(e))return e;let t=e.split(/[-_]+/).join(" ");return t[0].toUpperCase()+t.slice(1)}var $t=[{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 labeled 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:'The title, the way a person would write it: "Blue cake", not "blue-cake". Shown on the tab and in the library; the URL is minted from it. A hyphenated lowercase name is turned into words.'},w:{type:"integer",default:E.w,description:`Width in pixels. WHAT THE USER ASKED FOR WINS: a stated size or shape is used as stated, and a named artist, piece or style is matched in scale and detail. The rest is defaults for when nothing was said: ${E.w}\xD7${E.h} \u2014 1.91:1, the ratio link cards crop to, so a piece drawn on it posts edge to edge \u2014 at any size (${kt}, 480\xD7252, 600\xD7315: examples, not limits). A PROFILE PICTURE is square and cropped to a circle \u2014 keep the subject inside it: ${xt}. A BANNER is 3:1: ${vt}. Use a game asset's own size (8\xD78, 16\xD716, 32\xD732) only when the user asks for a tile, icon, or sprite for a game.`},h:{type:"integer",default:E.h,description:"Height in pixels. See w."},palette:{type:"array",items:M,description:"Optional starting palette."},intent:{type:"string",description:"One line on what you are making. Shown while you work."},folder:{type:"string",description:'Optional folder to file it in, by path (`food/sushi`). Made if it does not exist. Use this when the user asks for sprites "in a folder" rather than moving them afterwards.'},challenge:{type:"string",enum:["daily","weekly"],description:'"daily" when the user asks for the sprite of the day / today\'s theme / the Pixel Dailies; "weekly" for the sprite of the week / this week\'s word / Weekly Pixels. The name comes from the theme itself (pass `name` only to override), and finish_job will say how to post it as an entry. Call `challenges` first if you want to see the theme before drawing.'}},required:["intent"]},run:async(r,e)=>{let t=r.name?_t(r.name):null,n=r.w??null,o=r.h??null;if(r.challenge){let s=await e.challenge(r.challenge);if(t??=s.theme,n===null&&o===null&&s.sizes?.length){let i=s.sizes[Math.floor((s.sizes.length-1)/2)],[a,c]=i.split(/[x×]/).map(Number);a&&c&&([n,o]=[a,c])}}if(!t)throw new Error("new_sprite needs a name, or a challenge to take one from");return e.newSprite({name:t,w:n??E.w,h:o??E.h,intent:r.intent,palette:r.palette,folder:r.folder??null,challenge:r.challenge??null})}},{name:"challenges",description:`The sprite of the day and the sprite of the week: Pixel Dailies sets a theme every day ("Today's theme is #Gateway") and Weekly Pixels a word every week, with sizes. When the user says "the sprite of the day", "today's theme", "the daily", "the weekly", "this week's word" or names either community, this is what they mean \u2014 read it here rather than asking them. The server reads the accounts and keeps the answer; nothing is fetched from Bluesky by you.`,inputSchema:{type:"object",properties:{}},run:(r,e)=>e.challenges()},{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:I,intent:{type:"string"},layer:{type:"string",description:"Restrict the claim to one layer, leaving the rest free. Rooms only."}},required:["document","intent"]},run:(r,e)=>e.beginJob({document:r.document,intent:r.intent,layer:r.layer??null})},{name:"finish_job",description:"Release the document. Call this when the sprite is done, so the user knows to look. The result says what to do next for a profile picture, a banner or a card \u2014 pass that on to the user.",inputSchema:{type:"object",properties:{job:b}},run:(r,e)=>e.finishJob({job:r.job})},{name:"draw_art",description:'Returns when the room has confirmed the drawing \u2014 `seq` is the room\'s number for it and `committed` is true \u2014 or fails saying why; if it says the connection dropped, wait a moment and check get_info before drawing more. Draw a character grid. Each string in `rows` is one row of pixels and each character maps to a color via `colors`. "." and " " leave existing pixels alone; a color mapped to null erases. Every character is resolved before a pixel moves, so a typo fails rather than half-drawing. With no style reference, keep the palette small \u2014 four to eight colors, reused for shading \u2014 because that is what makes a small sprite read as pixel art rather than as a downscaled photograph. When matching an artist or a piece, use as many colors as their work does. A canvas past ~300 wide goes in bands: several calls with `y`, a few dozen rows each. A LARGE OR GENERATED GRID SHOULD NOT PASS THROUGH YOU: have the script write it to a file and pass `file` instead of `rows` \u2014 a JSON file of {rows, colors, x?, y?, \u2026}, or a PNG, which is read here on the same machine. Then the call is fifty bytes, whatever the canvas.',inputSchema:{type:"object",properties:{file:{type:"string",description:"Path to a grid on this machine, instead of rows and colors: a JSON file of draw_art arguments, or a PNG. Anything given in the call as well wins."},rows:{type:"array",items:{type:"string"}},colors:{type:"object",additionalProperties:{oneOf:[M,{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:b}},run:async(r,e)=>{let t=r.file?{...await St(r.file,e),...r}:r;if(!Array.isArray(t.rows)||!t.colors)throw new Error("draw_art needs rows and colors, or a file that has them");return e.drawArt({job:t.job,rows:t.rows,colors:t.colors,x:t.x??0,y:t.y??0,center:t.center??!1,clear:t.clear??!1,layer:t.layer,frame:t.frame??0,label:t.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:[M,{type:"null"}]}},fps:{type:"number"},layer:{type:"string"},label:{type:"string"},job:b},required:["frames","colors"]},run:(r,e)=>e.drawAnimation({job:r.job,frames:r.frames,colors:r.colors,fps:r.fps,layer:r.layer,label:r.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:I,layer:{type:"string"},frame:{type:"integer"},trim:{type:"boolean",description:"Return only the drawn area. Rooms only."},file:{type:"string",description:"Write the grid to this path on this machine as JSON \u2014 the shape draw_art takes as `file` \u2014 instead of returning it, so a script can round-trip art without it passing through you. The reply says where it went and how big it is."},job:b}},run:async(r,e)=>{let t=await e.readArt({document:r.document,job:r.job,layer:r.layer,frame:r.frame??0,trim:r.trim??!1});if(!r.file)return t;let{rows:n,colors:o,...s}=t;return await bt(r.file,JSON.stringify({rows:n,colors:o},null,0)).catch(i=>{throw new Error(`could not write ${r.file}: ${i.message}`)}),{...s,file:r.file,size:`${n[0]?.length??0}\xD7${n.length}`,colors:Object.keys(o??{}).length}}},{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:I,frame:{type:"integer"},scale:{type:"integer"},job:b}},async run(r,e){let t=await e.preview({document:r.document,job:r.job,frame:r.frame??0,scale:r.scale??8});return Ce(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:I,columns:{type:"integer",default:4},scale:{type:"integer",default:2},job:b}},async run(r,e){let t=await e.sheet({document:r.document,job:r.job,columns:r.columns??4,scale:r.scale??2});return Ce(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:b}},run:(r,e)=>e.addLayer({job:r.job,name:r.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:b}},run:(r,e)=>e.addFrame({job:r.job,copy:r.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:M,minItems:1},job:b},required:["colors"]},run:(r,e)=>e.setPalette({job:r.job,colors:r.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:b},required:["fps"]},run:(r,e)=>e.setFps({job:r.job,fps:r.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:b}},run:(r,e)=>e.clearCel({job:r.job,layer:r.layer,frame:r.frame??0})},{name:"trim_canvas",description:"Crop the canvas to the drawn pixels. Call this before finish_job on every GAME ASSET \u2014 a tile, an icon, a sprite for a sheet: you cannot know its extent before you have drawn it, so overshoot at new_sprite and trim afterwards, or empty space is inherited by every export and sheet. Do NOT trim a piece made to be posted: its canvas is the card, and the space around the subject is the composition.",inputSchema:{type:"object",properties:{margin:{type:"integer",default:0},job:b}},run:(r,e)=>e.trim({job:r.job,margin:r.margin??0})},{name:"get_info",description:"What is in a document \u2014 size, layers, frames, palette \u2014 and who else is looking at it; `connected` says whether the room is on the other end and `pending` how many operations it has not confirmed. Both should be true and zero before you trust a read.",inputSchema:{type:"object",properties:{document:I,job:b}},run:(r,e)=>e.info({document:r.document,job:r.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:(r,e)=>e.documents()},{name:"create_folder",description:"Make a folder in the library, by path \u2014 `food/sushi` nests. Already existing is fine. Prefer passing `folder` to new_sprite when the sprites are new; use this for an empty folder or before move_documents.",inputSchema:{type:"object",properties:{name:{type:"string",description:"The folder path."}},required:["name"]},run:(r,e)=>e.createFolder({name:r.name})},{name:"move_documents",description:"File documents into a folder, or back to the top level with folder null. Takes document ids from list_documents. The folder is made if it does not exist.",inputSchema:{type:"object",properties:{documents:{type:"array",items:{type:"string"},description:"Document ids."},folder:{type:["string","null"],description:"Folder path, or null for the top level."}},required:["documents"]},run:(r,e)=>e.moveDocuments({documents:r.documents,folder:r.folder??null,make:!0})},{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:(r,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(r,e){let t=await e.openEditor();return Ue(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:(r,e)=>e.select({document:r.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:(r,e)=>e.closeDocument({document:r.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:(r,e)=>e.setFrame({index:r.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:(r,e)=>e.setLayer({index:r.index})},{name:"undo",description:"Undo the last change, so the user can always reject generated art.",inputSchema:{type:"object",properties:{}},run:(r,e)=>e.undo()},{name:"redo",description:"Redo the last undone change.",inputSchema:{type:"object",properties:{}},run:(r,e)=>e.redo()}];function Be(r){return $t.filter(e=>r.supports(e.name)).map(e=>({name:e.name,description:e.description,inputSchema:e.inputSchema,run:async t=>{let n=await e.run(t??{},r);return n&&typeof n=="object"&&Array.isArray(n.content)?n:Ue(n)}}))}var At=6e3,B=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.control=t.control??null,this.rooms=new Map,this.asked=[],this.onAsked=null,this.jobs=new Map,this.log=[]}async open(e,{w:t=32,h:n=32,name:o=e}={}){let s=this.rooms.get(e);if(s)return s.client;let i=this.core.newDocument({w:t,h:n,name:o,seed:ze(),now:Date.now()}),a=new C(this.core,i,{author:this.author,name:this.name,seed:ze(),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:n,name:o}),l=!1;return await new Promise((u,d)=>{let p=setTimeout(()=>d(new Error(`timed out joining ${e}`)),1e4);a.onStatus=m=>{m.connected&&(l=!0,clearTimeout(p),u())},a.connect(c);let w=c.onclose;c.onclose=()=>{if(w?.(),l)return this.#e(e,{w:t,h:n,name:o});clearTimeout(p),d(new Error(this.serverUrl?De(this.serverUrl):"nothing is answering at the room server"))}}),this.rooms.set(e,{client:a,attempts:0}),a}#e(e,t){let n=this.rooms.get(e);if(!n||n.closing)return;let o=Math.min(15e3,1e3*2**Math.min(n.attempts,4));n.attempts+=1,n.retry=setTimeout(()=>{if(!this.rooms.get(e)||n.closing)return;let s=this.connectTransport(e,t),i=!1,a=s.onclose;n.client.onStatus=l=>{l.connected&&(i=!0,n.attempts=0)},n.client.connect(s);let c=s.onclose;s.onclose=()=>{c?.(),a?.(),this.#e(e,t)}},o),n.retry.unref?.()}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:n=null,open:o={}}={}){let s=await this.open(e,o),i=k(),a=Ot(s,i);s.lease("acquire",{job:i,intent:t,...n?{layers:[n]}:{}});let c=await a;if(c!=="held"){let h=s.leases.find(u=>u.job!==i);throw new Error(c==="refused"&&this.lastRefusal?.message?`could not claim "${e}": ${this.lastRefusal.message}`:h?`"${e}" is already held by another job: ${h.intent}`:c==="silent"?`could not claim "${e}": the room did not answer in time \u2014 check get_info and try again`:`could not claim "${e}"`)}let l={room:e,layer:n,intent:t,cancelled:!1,timer:setInterval(()=>{this.jobs.has(i)&&s.lease("renew",{job:i})},At)};return l.timer.unref&&l.timer.unref(),this.jobs.set(i,l),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,n]=[...this.jobs.entries()][0];return{job:t,...n}}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,n){let o=this.resolve(e),s=this.client(o.room);if(!n.length)return{ops:0};if(!s.connected||!s.welcomed)throw new Error(`the room connection dropped \u2014 nothing since sequence ${s.state.seq} has been saved. Reconnecting; try again in a moment, and check get_info before drawing more.`);this.lastRefusal=null;let i=s.state.seq,a=s.apply(n,{label:t,job:o.job,now:Date.now()});try{await s.acked(a,{timeoutMs:3e4})}catch(l){throw this.lastRefusal?.kind==="revoked"&&(o.cancelled=!0),this.lastRefusal=null,l}if(this.lastRefusal){let l=this.lastRefusal.message;throw this.lastRefusal.kind==="revoked"&&(o.cancelled=!0),this.lastRefusal=null,new Error(l)}this.note(t,o.room,`${n.length} op${n.length>1?"s":""}`);let c=s.state.seq;return{ops:n.length,seq:c,committed:c>i}}cancel(e){let t=this.jobs.get(e);t&&(t.cancelled=!0)}note(e,t,n){this.log.unshift({at:Date.now(),tool:e,room:t,detail:n}),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(){for(let e of this.rooms.values())e.closing=!0,clearTimeout(e.retry);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:Rt.has(e)}async openEditor({document:e,job:t}={}){if(!this.editorUrl)return{ok:!1,message:"there is no editor to open from here"};let n=this.editorUrl;try{n=`${this.editorUrl}/d/${this.#r(e,t)}`}catch{n=`${this.editorUrl}/library`}return await Pt(n),{ok:!0,message:`Opened ${n}`}}async newSprite({name:e,w:t=E.w,h:n=E.h,intent:o,palette:s,folder:i=null,challenge:a=null}){let c=await this.createDocument?.({name:e,w:t,h:n})??Ie(e),l=await this.begin(c,o,{open:{w:t,h:n,name:e}});return a&&(this.jobs.get(l).challenge=a),s?.length&&await this.submit(l,"set_palette",[{t:"setPalette",colors:K(s,"hue")}]),i&&await this.moveDocuments({documents:[c],folder:i,make:!0}),this.#n(c,o),{job:l,...this.#t(c)}}async#n(e,t){let n=process.env.SPRITE_ASKED_IN,o=process.env.SPRITE_REPLY_TO??null;if(n)try{(await this.open(n)).answer("thinking",t?String(t).slice(0,200):"making a new sprite",{reply:o,made:e})}catch{}}async beginJob({document:e,intent:t,layer:n=null}){return{job:await this.begin(e,t,{layer:n}),...this.#t(String(e))}}finishJob({job:e}){let t=this.resolve(e);this.finish(t.job);let n=this.client(t.room).state,o=Me(n.doc.w,n.doc.h),i=[t.challenge?`This is ${t.challenge==="weekly"?"this week's Weekly Pixels":"today's Pixel Dailies"} entry: tell the user to post it from the share button with the ${t.challenge==="weekly"?"Weekly Pixels":"Pixel Dailies"} switch on, which puts the theme and the tags on the post.`:null,o].filter(Boolean).join(" ");return{finished:t.intent,document:t.room,...i?{next:i}:{}}}async challenges(){if(!this.newsUrl)throw new Error("no server to ask: the themes come from the Pixie server, and this session has none");if(!(this.newsAt&&Date.now()-this.newsAt<300*1e3)){let n=await fetch(this.newsUrl);if(!n.ok)throw new Error(`the server said ${n.status} for the themes`);this.news=await n.json(),this.newsAt=Date.now()}let t=(n,o)=>n?{theme:n.theme,tags:n.tags,detail:n.detail??null,sizes:n.sizes??[],postedAt:new Date(n.at).toISOString(),post:n.url}:{theme:null,note:`no ${o} theme is known right now`};return{daily:t(this.news?.dailies,"daily"),weekly:t(this.news?.weekly,"weekly"),how:'new_sprite({ challenge: "daily" | "weekly", intent }) names the sprite after the theme; finish_job says how to post it as an entry.'}}async challenge(e){let t=await this.challenges(),n=e==="weekly"?t.weekly:t.daily;if(!n?.theme)throw new Error(`no ${e} theme is known right now \u2014 ask the user what to draw`);return n}#r(e,t){return e?String(e):this.resolve(t).room}async info({document:e,job:t}={}){let n=this.#r(e,t);return await this.peek(n),this.#t(n)}#t(e){let t=this.client(e),n=t.state;return{document:e,name:n.doc.name,w:n.doc.w,h:n.doc.h,layers:n.doc.layers.map(o=>o.name),frames:n.doc.frames.length,palette:n.doc.palette,tags:n.doc.tags.map(o=>o.name),seq:n.seq,connected:!!(t.connected&&t.welcomed),pending:n.pending,participants:t.participants.map(o=>o.name)}}#i(e){let t=this.client(e),n=t.state;return n.pending===0&&t.connected?{}:{warning:n.pending?`includes ${n.pending} operation${n.pending===1?"":"s"} the room has not confirmed${t.connected?"":" \u2014 the connection dropped, reconnecting"}`:"the room connection dropped, reconnecting"}}async readArt({document:e,job:t,layer:n,frame:o=0,trim:s=!1}){let i=this.#r(e,t),a=await this.peek(i);return{document:i,...a.session.readArt(this.#s(i,n),o,{trim:s}),...this.#i(i)}}async preview({document:e,job:t,frame:n=0}){let o=await this.peek(this.#r(e,t)),s=this.#r(e,t),{warning:i}=this.#i(s);return{png:o.session.png(n),caption:`${s}, frame ${n+1}${i?` \u2014 ${i}`:""}`}}async sheet({document:e,job:t,columns:n=4,scale:o=2}){let s=this.#r(e,t),i=await this.peek(s);return{png:i.session.contactSheet({columns:n,scale:o}),caption:`${s}, ${i.state.doc.frames.length} frames, numbered left to right`}}async documents(){let e=[...this.jobs.entries()].map(([c,l])=>({job:c,document:l.room,intent:l.intent})),t=[...this.rooms.keys()],n=null,o=null,s=null,i=this.control?await this.control("/documents",{}).catch(()=>null):null;if(i)n=i.documents?.map(c=>c.id)??null,o=Array.isArray(i.folders)?i.folders.map(c=>typeof c=="string"?c:c?.path??c?.name).filter(Boolean):null,s=i.documents?Object.fromEntries(i.documents.filter(c=>c.folder).map(c=>[c.id,c.folder])):null;else if(this.serverUrl){let c=this.serverUrl.replace(/^ws/,"http").replace(/\/$/,"");n=await fetch(`${c}/documents`).then(l=>l.ok?l.json():null).then(l=>l?.documents?.map(h=>h.id)).catch(()=>null)}return{documents:n?[...new Set([...t,...n])]:t,jobs:e,open:t,...o?{folders:o}:{},...s?{filed:s}:{}}}async createFolder({name:e}){if(!this.control)throw new Error("no library here: this Sprite has no accounts, so there are no folders");let t=String(e??"").trim();if(!t)throw new Error("a folder needs a name");let n=await this.folders();if(n.includes(t))return{folder:t,created:!1,folders:n};let o=t.split("/");for(let s=1;s<=o.length;s++){let i=o.slice(0,s).join("/");n.includes(i)||(await this.control("/documents/folders",{method:"POST",body:{name:i}}),n.push(i))}return{folder:t,created:!0,folders:await this.folders()}}async moveDocuments({documents:e,folder:t=null,make:n=!1}){if(!this.control)throw new Error("no library here: this Sprite has no accounts, so there are no folders");let o=(Array.isArray(e)?e:[e]).map(String).filter(Boolean);if(!o.length)throw new Error("say which documents");let s=t===null||t===""?null:String(t).trim();s&&n&&await this.createFolder({name:s});let i=await this.control("/documents/move",{method:"POST",body:{ids:o,folder:s}});return{moved:o,folder:s,ok:i?.ok!==!1}}async folders(){let e=await this.control("/documents",{});return Array.isArray(e?.folders)?e.folders.map(t=>typeof t=="string"?t:t?.path??t?.name).filter(Boolean):[]}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."}}#s(e,t){let n=this.client(e).state;if(t!=null&&t!==""){let o=typeof t=="number"?n.doc.layers[t]:n.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 n.doc.layers[n.doc.layers.length-1].id}async drawArt({job:e,rows:t,colors:n,x:o=0,y:s=0,center:i=!1,clear:a=!1,layer:c,frame:l=0,label:h}){let u=this.resolve(e),p=this.client(u.room).session.drawArt(t,n,{layer:c!=null?this.#s(u.room,c):u.layer??this.#s(u.room,null),at:l,x:o,y:s,center:i,clear:a}),{committed:w}=await this.submit(u.job,h||"draw_art",p);return{...this.#t(u.room),drew:`${t[0]?.length??0}\xD7${t.length}`,committed:w}}async drawAnimation({job:e,frames:t,colors:n,fps:o,layer:s,label:i}){let a=this.resolve(e),c=this.client(a.room),l=s!=null?this.#s(a.room,s):a.layer??this.#s(a.room,null),h=c.state.doc.frames.length,u=t.length-h;if(u>0){let p=[],w=c.state.doc.frames[h-1]?.index??"V";for(let m=0;m<u;m++)w=w+"V",p.push({t:"addFrame",id:k(),index:w,durMs:Y(o)});await this.submit(a.job,"add frames",p)}if(o){let p=Y(o);await this.submit(a.job,"set fps",c.state.doc.frames.map(w=>({t:"setFrameDur",id:w.id,durMs:p})))}let d=c.session.drawAnimation(t,n,{layer:l});return await this.submit(a.job,i||"draw_animation",d),{...this.#t(a.room),wrote:`${t.length} frames`}}async addLayer({job:e,name:t}){let n=this.resolve(e),o=this.client(n.room).state,s=o.doc.layers[o.doc.layers.length-1];return await this.submit(n.job,"add_layer",[{t:"addLayer",id:k(),index:(s?.index??"V")+"V",name:t||`Layer ${o.doc.layers.length+1}`,visible:!0,locked:!1,opacity:1}]),this.#t(n.room)}async addFrame({job:e,copy:t=!1}){let n=this.resolve(e),o=this.client(n.room),s=o.state,i=s.doc.frames[s.doc.frames.length-1],a=k(),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(Fe(l.id,a,s.doc,h))}return await this.submit(n.job,"add_frame",c),this.#t(n.room)}async setPalette({job:e,colors:t}){let n=K(t,"hue"),o=this.resolve(e);return await this.submit(o.job,"set_palette",[{t:"setPalette",colors:n}]),{...this.#t(o.room),palette:n.length}}async setFps({job:e,fps:t}){let n=this.resolve(e),o=this.client(n.room).state,s=Y(t);return await this.submit(n.job,"set_fps",o.doc.frames.map(i=>({t:"setFrameDur",id:i.id,durMs:s}))),{...this.#t(n.room),fps:t}}async clearCel({job:e,layer:t,frame:n=0}){let o=this.resolve(e),i=this.client(o.room).state,a=this.#s(o.room,t??o.layer),c=i.doc.frames[n];if(!c)throw new Error(`no frame ${n} in ${o.room}`);let l=new Uint8Array(i.doc.w*i.doc.h*4);return await this.submit(o.job,"clear",[Fe(a,c.id,i.doc,l)]),this.#t(o.room)}async trim({job:e,margin:t=0}){let n=this.resolve(e),o=this.client(n.room),s=o.state,i=this.#s(n.room,n.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.#t(n.room),trimmed:"already tight"}:(await this.submit(n.job,"trim_canvas",[{t:"resize",w:c,h:l,dx:-a.rect.x+t,dy:-a.rect.y+t}]),{...this.#t(n.room),trimmed:`${c}\xD7${l}`})}},Rt=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","challenges","take_requests","create_folder","move_documents"]);function Fe(r,e,t,n){return{t:"paintRegion",layer:r,frame:e,rect:{x:0,y:0,w:t.w,h:t.h},rle:Tt(n)}}function Tt(r){let e=[];if(r.length){let t=[r[0],r[1],r[2],r[3]],n=0,o=()=>{let s=n;do{let i=s&127;s>>=7,e.push(s?i|128:i)}while(s);e.push(...t)};for(let s=0;s<r.length;s+=4)r[s]===t[0]&&r[s+1]===t[1]&&r[s+2]===t[2]&&r[s+3]===t[3]?n++:(o(),t=[r[s],r[s+1],r[s+2],r[s+3]],n=1);o()}return Buffer.from(Uint8Array.from(e)).toString("base64")}function Pt(r){let e=process.platform==="darwin"?"open":process.platform==="win32"?"start":"xdg-open";return new Promise(t=>jt(e,[r],()=>t()))}function Y(r){return!r||r<=0?83:Math.max(10,Math.round(1e3/r))}function Ot(r,e,t=8e3){return new Promise(n=>{let o=r.onLeases,s=r.onLost,i=!1,a=l=>{i||(i=!0,clearTimeout(c),r.onLeases=o,r.onLost=s,n(l))};r.onLeases=l=>{o?.(l),l.some(h=>h.job===e)&&a("held")},r.onLost=l=>{s?.(l),l?.ops?.length||a("refused")};let c=setTimeout(()=>a("silent"),t);r.leases.some(l=>l.job===e)&&a("held")})}function ze(){return(Date.now()^Math.floor(Math.random()*4294967295))>>>0}import{execFile as It}from"node:child_process";var Nt=2e4,Lt=5e3,N=class r{constructor({base:e,name:t="agent"}={}){this.base=String(e).replace(/\/$/,""),this.name=t,this.log=[],this.beat=setInterval(()=>void this.announce(),Lt),this.beat.unref&&this.beat.unref()}get kind(){return"local"}get where(){return this.base}supports(e){return Dt.has(e)}static async probe(e,{timeoutMs:t=700}={}){try{let n=await fetch(`${e.replace(/\/$/,"")}/bridge/status`,{signal:AbortSignal.timeout(t)});if(!n.ok)return null;let o=await n.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=[],n=Nt){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:n}),signal:AbortSignal.timeout(n+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=E.w,h:n=E.h,intent:o,palette:s}){return this.call("newSprite",[{w:t,h:n,name:e,palette:s,intent:o,replace:!1}])}async challenges(){let e=await fetch(`${this.base}/news.json`).catch(()=>null);if(!e?.ok)throw new Error("this editor does not serve the themes \u2014 ask the user what to draw");let t=await e.json(),n=(o,s)=>o?{theme:o.theme,tags:o.tags,detail:o.detail??null,sizes:o.sizes??[],postedAt:new Date(o.at).toISOString(),post:o.url}:{theme:null,note:`no ${s} theme is known right now`};return{daily:n(t?.dailies,"daily"),weekly:n(t?.weekly,"weekly")}}async challenge(e){let t=await this.challenges(),n=e==="weekly"?t.weekly:t.daily;if(!n?.theme)throw new Error(`no ${e} theme is known right now \u2014 ask the user what to draw`);return n}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:n}){return this.call("readArt",[{document:e,layer:t,frame:n}])}async preview({document:e,frame:t,scale:n=8}){let o=await this.call("toDataURL",[t??null,n,e??null]);return{png:We(o.url),caption:o.caption}}async sheet({document:e,columns:t=0,scale:n=0}){let o=await this.call("toStripDataURL",[{document:e??null,columns:t,scale:n}]);return{png:We(o.url),caption:o.caption}}documents(){return this.call("listDocuments",[])}drawArt({job:e,rows:t,colors:n,x:o,y:s,center:i,clear:a,layer:c,frame:l,label:h}){return this.call("drawArt",[t,n,{job:e,x:o,y:s,center:i,clear:a,layer:c,frame:l,label:h}])}drawAnimation({job:e,frames:t,colors:n,fps:o,layer:s,label:i}){return this.call("drawAnimation",[{frames:t,colors:n,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:n}){return this.call("clear",[{job:e,layer:t,frame:n}])}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 r.probe(this.base))?.connected)return{ok:!0,message:`Editor already connected at ${this.base}`};await Ct(this.base);let t=Date.now()+1e4;for(;Date.now()<t;){if((await r.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)}},Dt=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","challenges","open_editor","select_document","close_document","set_frame","set_layer","undo","redo"]);function We(r){let e=String(r??"").split(",")[1];if(!e)throw new Error("the editor returned something that was not an image");return Buffer.from(e,"base64")}function Ct(r){let e=process.platform==="darwin"?"open":process.platform==="win32"?"start":"xdg-open";return new Promise(t=>It(e,[r],()=>t()))}import{readFileSync as Ut}from"node:fs";var Mt="0.5.2";function Bt(){let r=new URL("./version.json",import.meta.url);try{let e=JSON.parse(Ut(r,"utf8"))?.version;if(typeof e=="string"&&e)return e;throw new Error("it names no version")}catch(e){throw new Error(`${r.pathname} could not be read as a version: ${e.message}`)}}var Je=Mt??Bt();function Q(r="room"){return`Pixel art editor with live collaboration. ${r==="local"?`Documents are tabs in a browser window on this machine. The user is
2
+ var _=(r,e)=>()=>(r&&(e=r(r=0)),e);function we(r){let e="";for(let t=0;t<r.length;t+=32768)e+=String.fromCharCode.apply(null,r.subarray(t,t+32768));return btoa(e).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}var ye=_(()=>{"use strict"});var j,ot,st,dn,ge=_(()=>{"use strict";j="[A-Za-z0-9_-]{1,64}",ot="token|members|link|slow|suggest|project",st=[{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/${j}/(${ot})$`)},{owner:"library",re:new RegExp("^/documents$")},{owner:"library",re:new RegExp("^/documents/(new|register|move|order|folders|folders/delete|trash|trash/empty)$")},{owner:"library",re:new RegExp(`^/documents/${j}(/(thumbnail\\.png|export|duplicate|rename|restore))?$`)},{owner:"library",re:new RegExp(`^/documents/${j}/history/at/\\d+/thumbnail\\.png$`)},{owner:"library",re:new RegExp("^/wall\\.png$")},{owner:"document",re:new RegExp(`^/documents/${j}/history(/at/\\d+|/\\d+)?$`)},{owner:"document",re:new RegExp(`^/documents/${j}/comments(/[A-Za-z0-9]{1,40})?$`)},{owner:"document",re:new RegExp(`^/documents/${j}/snapshots(/enabled|/[A-Za-z0-9_-]{1,128})?$`)},{owner:"document",re:new RegExp(`^/favourites(/order|/(document|project)/${j})?$`)}],dn=st.map(({owner:r,re:e})=>({owner:r,source:e.source}))});var K,Z=_(()=>{"use strict";ge();K=["read","draw","create","duplicate","organize"]});async function be(r){let e=new TextEncoder().encode(String(r));return we(new Uint8Array(await crypto.subtle.digest("SHA-256",e)))}var Sn,Se=_(()=>{"use strict";ye();Z();Sn=120*1e3});function x(r=Date.now()){let e="0123456789ABCDEFGHJKMNPQRSTVWXYZ",t=(BigInt(r)&0xffffffffffffn)<<80n,n=crypto.getRandomValues(new Uint8Array(10)),o=0n;for(let i of n)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 $(r){return r==null?{at:0}:typeof r=="number"?{at:r}:{frame:r}}var dt,pt,mt,D,A,X,O=_(()=>{"use strict";dt=new TextEncoder,pt=new TextDecoder,mt=1;D=class r{#e;#n;constructor(e){this.#e=e,this.#n=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 n=e instanceof Response||typeof e?.then=="function"?e:fetch(e);({instance:t}=await WebAssembly.instantiateStreaming(n,{}))}return new r(t)}get#i(){return new Uint8Array(this.#n.memory.buffer)}call(e,t={}){let n=dt.encode(JSON.stringify({m:e,a:t})),o=this.#n.sprite_alloc(n.length);if(!o)throw new Error("sprite-core: allocation failed");let s=0;try{if(this.#i.set(n,o),s=this.#n.sprite_dispatch(o,n.length),!s)throw new Error("sprite-core: dispatch returned nothing");let i=this.#i,a=i[s]|i[s+1]<<8|i[s+2]<<16|i[s+3]<<24,l=i[s+4],c=i.slice(s+5,s+5+a);if(l===mt)return c;let h=JSON.parse(pt.decode(c));if(h&&h.error)throw new Error(h.error);return h}finally{s?this.#n.sprite_free_result(s):this.#n.sprite_dealloc(o,n.length)}}reset(){this.#n.sprite_reset()}newDocument(e={}){return this.call("doc.new",{w:32,h:32,name:"untitled",...e})}decodePng(e){let t=this.call("image.decode",{png:[...e]}),n=new DataView(t.buffer,t.byteOffset,t.byteLength),o=n.getUint32(0,!0),s=n.getUint32(4,!0);return{w:o,h:s,pixels:t.subarray(8)}}openSession(e,t,n=1){let{h:o}=this.call("session.new",{doc:e,author:t,seed:n});return new A(this,o)}openRoom(e,{seq:t=0}={}){let{h:n}=this.call("room.new",{doc:e,...t>0?{seq:t}:{}});return new X(this,n)}},A=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:n="Edit",job:o=null}={}){let s={now:t,label:n,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,n={}){return this.#e("session.drawArt",{rows:e,colors:t,...n}).bodies}drawAnimation(e,t,n={}){return this.#e("session.drawAnimation",{frames:e,colors:t,...n}).bodies}readArt(e,t,n={}){return this.#e("session.readArt",{layer:e,...$(t),...n})}cel(e,t){return this.#e("session.cel",{layer:e,...$(t)})}layerImage(e,t){return this.#e("session.layerImage",{layer:e,...$(t)})}tileCel(e,t){return this.#e("session.tileCel",{layer:e,...$(t)})}tile(e,t){return this.#e("session.tile",{tileset:e,index:t})}composite(e,{include:t="export"}={}){return this.#e("session.composite",{...$(e),include:t})}png(e,{scale:t=1,rect:n=null}={}){return this.#e("session.png",{...$(e),scale:Math.max(1,Math.round(t)||1),...n?{rect:n}:{}})}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:n=630,framing:o=null,bleed:s=0,raw:i=!1}={}){return this.#e("session.card",{width:t,height:n,...o??{},...$(e),...s?{bleed:s}:{},...i?{raw:i}:{}})}cardLayout({frame:e=null,width:t=1200,height:n=630,framing:o=null}={}){return this.#e("session.cardLayout",{width:t,height:n,...o??{},...$(e)})}fit({width:e,height:t,margin:n=0,inscribed:o=!1,dx:s=0,dy:i=0,frame:a=null}){return this.#e("session.fit",{width:e,height:t,margin:n,inscribed:o,dx:s,dy:i,...$(a)})}close(){this.#e("session.free")}},X=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 Ae(r){return JSON.stringify(r)}function Te(r){let e=r.rle instanceof Uint8Array?r.rle:null,t=new TextEncoder().encode(JSON.stringify({...r,rle:void 0}));if(t.length>ft)return null;let n=new Uint8Array(3+t.length+(e?.length??0));return n[0]=Re,n[1]=t.length&255,n[2]=t.length>>8&255,n.set(t,3),e&&n.set(e,3+t.length),n}function wt(r){return r instanceof Uint8Array?r:r instanceof ArrayBuffer?new Uint8Array(r):ArrayBuffer.isView(r)?new Uint8Array(r.buffer,r.byteOffset,r.byteLength):null}function Pe(r){if(typeof r!="string"){let e=wt(r);if(!e||e.length<3||e[0]!==Re)return null;let t=e[1]|e[2]<<8;if(e.length<3+t)return null;try{let n=JSON.parse(new TextDecoder().decode(e.subarray(3,3+t)));if(!n||typeof n.t!="string")return null;let o=e.subarray(3+t);return n.rle=o.length?Uint8Array.from(o):null,n}catch{return null}}try{let e=JSON.parse(r);return e&&typeof e.t=="string"?e:null}catch{return null}}var g,_e,b,je,Re,ft,Oe=_(()=>{"use strict";g={HELLO:"hello",SUBMIT:"submit",PRESENCE:"presence",STROKE:"stroke",LEASE:"lease",REVIEW:"review",PING:"ping",PROMPT:"prompt",ANSWER:"answer",STOP:"stop"},_e={DOCUMENT:"document",LIBRARY:"library"},b={WELCOME:"welcome",REHELLO:"rehello",OPS:"ops",REJECT:"reject",PRESENCE:"presence",STROKE:"stroke",LEASE:"lease",INK:"ink",SUGGESTIONS:"suggestions",SUGGESTION:"suggestion",ERROR:"error",PONG:"pong",PROMPT:"prompt",ANSWER:"answer",STOP:"stop"},je={OPS:"ops",SNAPSHOT:"snapshot"};Re=157,ft=65535});function Ie(r){return x().toLowerCase().slice(0,16)}var Ne=_(()=>{"use strict";O()});import{readFile as Ht}from"node:fs/promises";import{readFile as Ke,writeFile as Ze,mkdir as Xe,chmod as Ye}from"node:fs/promises";import{readFileSync as Qe}from"node:fs";import{homedir as et,hostname as tt}from"node:os";import{join as H}from"node:path";import{execFile as nt}from"node:child_process";var w=r=>String(r??"").replace(/\/+$/,"").replace(/^ws/,"http"),ne=r=>String(r??"").replace(/\/+$/,"").replace(/^http/,"ws");async function re(r,e){if(!r?.ok||!(r.headers.get("content-type")??"").includes("json"))return null;let t=await r.json().catch(()=>null);return!t||typeof t!="object"?null:e.some(n=>n in t)?t:null}async function oe(r,{timeoutMs:e=3e3,fetchImpl:t=fetch}={}){try{let n=await t(`${w(r)}/setup`,{signal:AbortSignal.timeout(e)});return await re(n,["accounts","expectedPort","install"])}catch{return null}}var J=new Map;function se(r,{fetchImpl:e=fetch,timeoutMs:t=3e3}={}){let n=w(r);return J.has(n)||J.set(n,Ve(r,{fetchImpl:e,timeoutMs:t})),J.get(n)}async function Ve(r,{fetchImpl:e,timeoutMs:t}){try{let n=await e(`${w(r)}/config.json`,{signal:AbortSignal.timeout(t)}),s=(await re(n,["rooms","accounts"]))?.rooms;if(typeof s=="string"&&/^(wss?|https?):\/\/./.test(s))return ne(s)}catch{}return ne(r)}var T=process.env.SPRITE_PUBLIC_SERVER||rt()||null;function rt(){return"wss://drawpixie.com"}var ie=H(process.env.XDG_CONFIG_HOME||H(et(),".config"),"sprite"),V=H(ie,"agent.json"),ae=r=>String(r??"").replace(/\/+$/,"");function ce(){return`${tt().replace(/\.local$/,"")}`.slice(0,60)||"a computer"}async function le(){try{let r=JSON.parse(await Ke(V,"utf8"));return r&&typeof r=="object"&&r.servers?r:{servers:{}}}catch{return{servers:{}}}}async function P(r){return process.env.SPRITE_TOKEN?{token:process.env.SPRITE_TOKEN,from:"the environment"}:(await le()).servers[ae(r)]??null}async function he(r,e){await Xe(ie,{recursive:!0,mode:448});let t=await le();t.servers[ae(r)]={...e,at:Date.now()};let n=`${V}.tmp`;await Ze(n,JSON.stringify(t,null,2),{mode:384}),await Ye(n,384);let{rename:o}=await import("node:fs/promises");await o(n,V)}async function ue(r,{timeoutMs:e=3e3}={}){try{let t=await fetch(`${w(r)}/setup`,{signal:AbortSignal.timeout(e)});return t.ok?!!(await t.json()).accounts:!1}catch{return!1}}async function de(r,e,{session:t=null}={}){let n=t??await P(r);if(!n?.token)return;let o=await fetch(`${w(r)}/documents/${encodeURIComponent(e)}/token`,{headers:{Authorization:`Bearer ${n.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 pe(r,{name:e,w:t,h:n},{session:o=null}={}){let s=o??await P(r);if(!s?.token)return;let i=await fetch(`${w(r)}/documents/new`,{method:"POST",headers:{Authorization:`Bearer ${s.token}`,"Content-Type":"application/json"},body:JSON.stringify({name:e,w:t,h:n})});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}async function me(r,e,{method:t="GET",body:n=null,session:o=null}={}){let s=o??await P(r);if(!s?.token)return;let i=await fetch(`${w(r)}${e}`,{method:t,headers:{Authorization:`Bearer ${s.token}`,...n!==null?{"Content-Type":"application/json"}:{}},body:n!==null?JSON.stringify(n):void 0});if(i.status===401)throw new Error("this machine is signed out \u2014 pair it again");let a=await i.json().catch(()=>({}));if(!i.ok)throw new Error(a?.error?String(a.error):`the server said ${i.status}`);return a}function fe(r){let e=process.platform==="darwin"?"open":process.platform==="win32"?"start":"xdg-open";return new Promise(t=>nt(e,[r],()=>t()))}Se();Z();import{createServer as it}from"node:http";import{randomBytes as Ee}from"node:crypto";var at=300*1e3,ct=()=>Ee(32).toString("base64url"),lt=()=>Ee(16).toString("base64url");function xe(r,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">${r}</p><p style="color:#8a8a99;font-size:12px">${e}</p></div>`}async function ht({state:r,timeoutMs:e}){let t,n,o=new Promise((l,c)=>{t=l,n=c}),s=it((l,c)=>{let h=new URL(l.url,"http://127.0.0.1");if(h.pathname!=="/callback"){c.writeHead(404).end();return}if(h.searchParams.get("state")!==r){c.writeHead(404).end();return}let u=h.searchParams.get("error");c.writeHead(200,{"Content-Type":"text/html; charset=utf-8"}).end(u?xe("Nothing was granted.","You can close this window."):xe("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((l,c)=>{s.once("error",c),s.listen(0,"127.0.0.1",l)});let i=setTimeout(()=>n(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 ke(r,{onUrl:e=()=>{},open:t=fe,label:n="the Pixie agent",scope:o=K,timeoutMs:s=at}={}){let i=ct(),a=await be(i),l=lt(),c=await ht({state:l,timeoutMs:s});try{let h=new URL(`${w(r)}/authorize`);h.searchParams.set("challenge",a),h.searchParams.set("state",l),h.searchParams.set("redirect",c.redirect),h.searchParams.set("scope",o.join(",")),h.searchParams.set("name",n),e(h.toString()),await t(h.toString());let u=await c.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 p=u.get("code");if(!p)throw new Error("the browser came back without an authorization");let m=await fetch(`${w(r)}/auth/authorize/exchange`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:p,verifier:i,name:ce()})}),f=await m.json().catch(()=>({}));if(!m.ok||!f.token)throw new Error(f.error||"that authorization could not be exchanged");return await he(r,{token:f.token,name:f.name,email:f.email,scope:f.scope??null}),f}finally{c.close()}}import{realpathSync as ve}from"node:fs";import{fileURLToPath as ut}from"node:url";function $e(r){let e=process.argv[1];if(!e)return!1;try{return ve(ut(r))===ve(e)}catch{return!1}}O();import{execFile as Rt}from"node:child_process";Oe();O();var yt=5e3,C=class{#e=null;constructor(e,t,n){if(!n||!n.author)throw new Error("DocumentClient needs an author");if(this.core=e,this.author=n.author,this.name=n.name||"anonymous",this.agent=n.agent===!0,this.answers=n.answers===!0,this.seed=n.seed??1,this.epoch=0,this.resumed=null,n.resume){let{h:o,dropped:s}=e.call("session.resume",{doc:t,author:this.author,seed:this.seed,seq:n.resume.seq??0,pending:n.resume.pending??[]});this.session=new A(e,o),this.resumed={seq:n.resume.seq??0,dropped:s??[]}}else this.session=e.openSession(t,this.author,this.seed);this.getToken=n.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.ink=null,this.onInk=()=>{},this.proposals=new Map,this.suggesting=!1,this.onSuggestions=()=>{},this.onLost=()=>{},this.waiters=new Set,this.onPrompt=()=>{},this.onAnswer=()=>{},this.onStop=()=>{},this.onStatus=()=>{}}apply(e,t={}){let n=this.session.apply(e,t);return this.#l(n),this.onChange(),n}acked(e,{timeoutMs:t=3e4}={}){let n=new Set((Array.isArray(e)?e:[e]).map(o=>o?.opId??o).filter(Boolean));return new Promise((o,s)=>{let i={ids:n,resolve:o,reject:s,timer:null};i.timer=setTimeout(()=>{this.waiters.delete(i);let a=[...n].filter(l=>this.session.pending().some(c=>c.opId===l)).length;s(new Error(`${a} operation${a===1?"":"s"} not confirmed by the room after ${Math.round(t/1e3)}s \u2014 `+(this.connected?"it is answering slowly":`the connection dropped; nothing since sequence ${this.session.state().seq} has been saved`)))},t),this.waiters.add(i),this.#n()})}#n(){if(!this.waiters.size)return;let e=new Set(this.session.pending().map(t=>t.opId));for(let t of this.waiters)[...t.ids].some(n=>e.has(n))||(this.waiters.delete(t),clearTimeout(t.timer),t.resolve())}#i(e,t){for(let n of this.waiters)n.ids.has(e)&&(this.waiters.delete(n),clearTimeout(n.timer),n.reject(new Error(t)))}undo(e=Date.now()){let t=this.session.undo(e);return this.#l(t),this.onChange(),t}redo(e=Date.now()){let t=this.session.redo(e);return this.#l(t),this.onChange(),t}setPresence(e){this.presence=e,this.connected&&this.#r({t:g.PRESENCE,...e})}sendStroke({layer:e,frame:t,rect:n,rle:o,done:s=!1}){if(!this.connected||!this.welcomed||!this.strokePreview||this.suggesting)return;this._strokeSentAt=Date.now();let i=Te({t:g.STROKE,layer:e,frame:t,rect:n,rle:o,done:s});i&&this.#h(i)}suggest(e){if(!e.length||!this.connected||!this.welcomed)return[];let t=this.session.state().seq,n=e.map(o=>({opId:x(),author:this.author,parentSeq:t,...o}));return this.#r({t:g.SUBMIT,ops:n}),n}review(e,t){!this.connected||!this.welcomed||this.#r({t:g.REVIEW,author:e,verdict:t})}lease(e,t={}){this.#r({t:g.LEASE,action:e,...t})}prompt(e,{where:t=_e.DOCUMENT}={}){if(!this.connected)return null;let n=String(e??"").trim().slice(0,2e3);if(!n)return null;let o=x();return this.#r({t:g.PROMPT,id:o,text:n,where:t}),o}answer(e,t="",{reply:n=null,made:o=null,model:s=null}={}){return this.connected?(this.#r({t:g.ANSWER,state:e,text:String(t??"").slice(0,2e3),reply:n,made:o,model:s}),!0):!1}stopAnswer(e){return!this.connected||!e?!1:(this.#r({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,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:g.PING})},yt),await this.#s()},e.onmessage=t=>this.#c(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#s(){let e;if(this.pendingToken!==void 0){let n=this.pendingToken;this.pendingToken=void 0,e=await n}else e=this.getToken?await this.getToken():void 0;let t=this.session.state();this.#r({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})}#c(e){let t=Pe(e);if(t)switch(t.t){case b.WELCOME:return this.#t(t);case b.REHELLO:return this.welcomed=!1,void this.#s();case b.OPS:return this.#a(t);case b.REJECT:return this.#u(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.INK:return this.ink={left:t.left,cap:t.cap,refillMs:t.refillMs,nextInMs:t.nextInMs??0,at:Date.now()},this.onInk(this.ink);case b.SUGGESTIONS:return this.proposals=new Map((t.proposals??[]).map(n=>[n.author,{...n,ops:[...n.ops??[]]}])),this.onSuggestions(this.proposals);case b.SUGGESTION:{if(t.gone)return this.proposals.delete(t.author),this.onSuggestions(this.proposals,{author:t.author,name:t.name??null,gone:t.gone,by:t.by??null,applied:t.applied??0});let n=this.proposals.get(t.author)??{author:t.author,name:t.name??null,cc:t.cc??null,ops:[],at:t.at};if(n.name=t.name??n.name,n.at=t.at??n.at,t.replaced?.length){let o=new Set(t.replaced);n.ops=n.ops.filter(s=>!(s.rect?.w===1&&s.rect?.h===1&&o.has(`${s.rect.x},${s.rect.y}`)))}return n.ops.push(...t.ops??[]),this.proposals.set(t.author,n),this.onSuggestions(this.proposals,{author:t.author,added:t.ops?.length??0})}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.#d(t.message)){this.strokePreview=!1;return}return this.onLost({kind:t.fatal?"fatal":"protocol",message:t.message});default:return}}#t(e){this.you=e.you,this.suggesting=e.you?.suggest===!0,this.welcomed=!0;let t=this.session.pending();if(e.catchup.kind===je.SNAPSHOT){let{h:n,dropped:o}=this.core.call("session.resume",{doc:e.catchup.doc,author:this.author,seed:this.seed,seq:e.catchup.seq,pending:t});if(this.session.close(),this.session=new A(this.core,n),this.epoch++,o&&o.length){for(let s of o)this.#i(s,"could not be replayed onto the current document");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 n of e.catchup.ops||[])this.#o(n);if(this.#n(),this.participants=e.participants||[],this.leases=e.leases||[],t.length&&this.suggesting)for(let n of t)this.session.reject(n.opId);else t.length&&this.#r({t:g.SUBMIT,ops:t});this.presence&&this.#r({t:g.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}#a(e){let t=!1;for(let n of e.ops||[])this.#o(n),t=!0;t&&(this.#n(),this.onChange())}#o(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`})}#u(e){e.opId&&this.session.reject(e.opId),e.opId&&this.#i(e.opId,e.message),this.onLost({kind:e.kind,ops:e.opId?[e.opId]:[],message:e.message}),this.onChange()}#l(e){!e.length||!this.connected||!this.welcomed||this.#r({t:g.SUBMIT,ops:e})}#r(e){this.#h(Ae(e))}#d(e){return typeof e!="string"?!1:e===`unknown message: ${g.STROKE}`?!0:e==="unreadable message"&&Date.now()-(this._strokeSentAt??0)<2e3}#h(e){if(this.transport)try{this.transport.send(e)}catch{this.connected=!1}}close(){this.disconnect(),this.session.close()}};O();Ne();var k=r=>{let e=String(r).replace("#","");return[parseInt(e.slice(0,2),16),parseInt(e.slice(2,4),16),parseInt(e.slice(4,6),16)]};var R=([r,e,t])=>.3*r+.59*e+.11*t;function U([r,e,t]){let n=Math.max(r,e,t),o=Math.min(r,e,t),s=n-o,i=0;return s&&(n===r?i=(e-t)/s%6:n===e?i=(t-r)/s+2:i=(r-e)/s+4,i*=60,i<0&&(i+=360)),[i,n?s/n:0,n/255]}var Le={luminance:(r,e)=>R(k(r))-R(k(e)),hue:(r,e)=>{let[t,n]=U(k(r)),[o,s]=U(k(e));return n<.08||s<.08?n<.08&&s<.08?R(k(r))-R(k(e)):n<.08?-1:1:Math.round(t/20)!==Math.round(o/20)?t-o:R(k(r))-R(k(e))},saturation:(r,e)=>U(k(r))[1]-U(k(e))[1]};function M(r,e="hue"){let t=Le[e]??Le.hue;return[...r].sort(t)}function gt(r){try{return/^(localhost|127\.0\.0\.1|\[::1\]|::1)$/.test(new URL(w(r)).hostname)}catch{return!1}}function De(r,{publicServer:e=T}={}){let t=w(r);return e&&r===e?`nothing is answering at ${t} \u2014 this machine is offline, or the site is down`:gt(r)?`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`}import{readFile as bt,writeFile as St}from"node:fs/promises";var B={type:"string",description:"#rrggbb"};async function xt(r,e){let t=await bt(r).catch(s=>{throw new Error(`could not read ${r}: ${s.code==="ENOENT"?"no such file":s.message}`)});if(t.length>8&&t[0]===137&&t[1]===80&&t[2]===78&&t[3]===71){if(!e.core?.decodePng)throw new Error("this server cannot decode a PNG \u2014 send rows and colors");return Et(e.core.decodePng(t))}let o;try{o=JSON.parse(t.toString("utf8"))}catch{throw new Error(`${r} is neither a PNG nor JSON`)}if(!o||typeof o!="object"||!Array.isArray(o.rows)||typeof o.colors!="object")throw new Error(`${r} needs "rows" (an array of strings) and "colors" (a map of character to #rrggbb)`);return o}function Et({w:r,h:e,pixels:t}){let n={},o=new Map,s=33,i=c=>{let h=o.get(c);if(h)return h;for(;;)if(s===34||s===46||s===92||s===127)s+=1;else if(s>=128&&s<161)s=161;else if(s>=55296&&s<57344)s=57344;else break;return h=String.fromCodePoint(s),s+=1,o.set(c,h),n[h]=c,h},a=c=>c.toString(16).padStart(2,"0"),l=[];for(let c=0;c<e;c++){let h="";for(let u=0;u<r;u++){let d=(c*r+u)*4,p=t[d+3];if(p===0){h+=".";continue}let m=p===255?`#${a(t[d])}${a(t[d+1])}${a(t[d+2])}`:`#${a(t[d])}${a(t[d+1])}${a(t[d+2])}${a(p)}`;h+=i(m)}l.push(h)}return{rows:l,colors:n}}var S={type:"string",description:"The job this call belongs to. May be omitted when exactly one job is open."},I={type:"string",description:"Which document. Omit to use the one your job owns. The id \u2014 the one new_sprite, get_info and list_documents hand back \u2014 or the exact name when only one document has it. In a local editor a 0-based tab index works too."},kt=r=>({isError:!0,content:[{type:"text",text:r}]}),Ue=r=>({content:[{type:"text",text:typeof r=="string"?r:JSON.stringify(r)}]}),Ce=(r,e)=>({content:[{type:"image",data:Buffer.from(r).toString("base64"),mimeType:"image/png"},{type:"text",text:e}]}),E={w:80,h:42},vt="40\xD721, 80\xD742, 120\xD763, 160\xD784, 240\xD7126",$t="40\xD740, 50\xD750, 80\xD780, 100\xD7100",_t="60\xD720, 75\xD725, 150\xD750, 300\xD7100";function Me(r,e){return!r||!e?null:r===e?"This is square, the shape of a profile picture: in the editor, file \u25B8 Save as profile picture (400\xD7400) fits it inside the circle it will be cropped to. Tell the user.":r===3*e?"This is 3:1, the shape of a profile banner: in the editor, file \u25B8 Save as profile banner (1500\xD7500) exports it at the size X wants. Tell the user.":Math.abs(r/e-1200/630)<.02?"This is the link-card shape: the share button posts it edge to edge.":null}function jt(r){let e=String(r??"").trim();if(!/^[A-Za-z0-9]+([-_][A-Za-z0-9]+)+$/.test(e))return e;let t=e.split(/[-_]+/).join(" ");return t[0].toUpperCase()+t.slice(1)}var At=[{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 labeled 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:'The title, the way a person would write it: "Blue cake", not "blue-cake". Shown on the tab and in the library; the URL is minted from it. A hyphenated lowercase name is turned into words.'},w:{type:"integer",default:E.w,description:`Width in pixels. WHAT THE USER ASKED FOR WINS: a stated size or shape is used as stated, and a named artist, piece or style is matched in scale and detail. The rest is defaults for when nothing was said: ${E.w}\xD7${E.h} \u2014 1.91:1, the ratio link cards crop to, so a piece drawn on it posts edge to edge \u2014 at any size (${vt}, 480\xD7252, 600\xD7315: examples, not limits). A PROFILE PICTURE is square and cropped to a circle \u2014 keep the subject inside it: ${$t}. A BANNER is 3:1: ${_t}. Use a game asset's own size (8\xD78, 16\xD716, 32\xD732) only when the user asks for a tile, icon, or sprite for a game.`},h:{type:"integer",default:E.h,description:"Height in pixels. See w."},palette:{type:"array",items:B,description:"Optional starting palette."},intent:{type:"string",description:"One line on what you are making. Shown while you work."},folder:{type:"string",description:'Optional folder to file it in, by path (`food/sushi`). Made if it does not exist. Use this when the user asks for sprites "in a folder" rather than moving them afterwards.'},challenge:{type:"string",enum:["daily","weekly"],description:'"daily" when the user asks for the sprite of the day / today\'s theme / the Pixel Dailies; "weekly" for the sprite of the week / this week\'s word / Weekly Pixels. The name comes from the theme itself (pass `name` only to override), and finish_job will say how to post it as an entry. Call `challenges` first if you want to see the theme before drawing.'}},required:["intent"]},run:async(r,e)=>{let t=r.name?jt(r.name):null,n=r.w??null,o=r.h??null;if(r.challenge){let s=await e.challenge(r.challenge);if(t??=s.theme,n===null&&o===null&&s.sizes?.length){let i=s.sizes[Math.floor((s.sizes.length-1)/2)],[a,l]=i.split(/[x×]/).map(Number);a&&l&&([n,o]=[a,l])}}if(!t)throw new Error("new_sprite needs a name, or a challenge to take one from");return e.newSprite({name:t,w:n??E.w,h:o??E.h,intent:r.intent,palette:r.palette,folder:r.folder??null,challenge:r.challenge??null})}},{name:"challenges",description:`The sprite of the day and the sprite of the week: Pixel Dailies sets a theme every day ("Today's theme is #Gateway") and Weekly Pixels a word every week, with sizes. When the user says "the sprite of the day", "today's theme", "the daily", "the weekly", "this week's word" or names either community, this is what they mean \u2014 read it here rather than asking them. The server reads the accounts and keeps the answer; nothing is fetched from Bluesky by you.`,inputSchema:{type:"object",properties:{}},run:(r,e)=>e.challenges()},{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:I,intent:{type:"string"},layer:{type:"string",description:"Restrict the claim to one layer, leaving the rest free. Rooms only."}},required:["document","intent"]},run:(r,e)=>e.beginJob({document:r.document,intent:r.intent,layer:r.layer??null})},{name:"finish_job",description:"Release the document. Call this when the sprite is done, so the user knows to look. The result says what to do next for a profile picture, a banner or a card \u2014 pass that on to the user.",inputSchema:{type:"object",properties:{job:S}},run:(r,e)=>e.finishJob({job:r.job})},{name:"draw_art",description:'Returns when the room has confirmed the drawing \u2014 `seq` is the room\'s number for it and `committed` is true \u2014 or fails saying why; if it says the connection dropped, wait a moment and check get_info before drawing more. Draw a character grid. Each string in `rows` is one row of pixels and each character maps to a color via `colors`. "." and " " leave existing pixels alone; a color mapped to null erases. Every character is resolved before a pixel moves, so a typo fails rather than half-drawing. With no style reference, keep the palette small \u2014 four to eight colors, reused for shading \u2014 because that is what makes a small sprite read as pixel art rather than as a downscaled photograph. When matching an artist or a piece, use as many colors as their work does. A canvas past ~300 wide goes in bands: several calls with `y`, a few dozen rows each. A LARGE OR GENERATED GRID SHOULD NOT PASS THROUGH YOU: have the script write it to a file and pass `file` instead of `rows` \u2014 a JSON file of {rows, colors, x?, y?, \u2026}, or a PNG, which is read here on the same machine. Then the call is fifty bytes, whatever the canvas.',inputSchema:{type:"object",properties:{file:{type:"string",description:"Path to a grid on this machine, instead of rows and colors: a JSON file of draw_art arguments, or a PNG. Anything given in the call as well wins."},rows:{type:"array",items:{type:"string"}},colors:{type:"object",additionalProperties:{oneOf:[B,{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:S}},run:async(r,e)=>{let t=r.file?{...await xt(r.file,e),...r}:r;if(!Array.isArray(t.rows)||!t.colors)throw new Error("draw_art needs rows and colors, or a file that has them");return e.drawArt({job:t.job,rows:t.rows,colors:t.colors,x:t.x??0,y:t.y??0,center:t.center??!1,clear:t.clear??!1,layer:t.layer,frame:t.frame??0,label:t.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:[B,{type:"null"}]}},fps:{type:"number"},layer:{type:"string"},label:{type:"string"},job:S},required:["frames","colors"]},run:(r,e)=>e.drawAnimation({job:r.job,frames:r.frames,colors:r.colors,fps:r.fps,layer:r.layer,label:r.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:I,layer:{type:"string"},frame:{type:"integer"},trim:{type:"boolean",description:"Return only the drawn area. Rooms only."},file:{type:"string",description:"Write the grid to this path on this machine as JSON \u2014 the shape draw_art takes as `file` \u2014 instead of returning it, so a script can round-trip art without it passing through you. The reply says where it went and how big it is."},job:S}},run:async(r,e)=>{let t=await e.readArt({document:r.document,job:r.job,layer:r.layer,frame:r.frame??0,trim:r.trim??!1});if(!r.file)return t;let{rows:n,colors:o,...s}=t;return await St(r.file,JSON.stringify({rows:n,colors:o},null,0)).catch(i=>{throw new Error(`could not write ${r.file}: ${i.message}`)}),{...s,file:r.file,size:`${n[0]?.length??0}\xD7${n.length}`,colors:Object.keys(o??{}).length}}},{name:"render_preview",description:"See one frame as an image, so you can check your own work rather than working blind. Rendered at a readable size by default (about a thousand pixels on the long side); `scale` is screen pixels per sprite pixel if you want a particular one. To look closely at one part of a big canvas \u2014 a cell of a sprite sheet \u2014 pass `rect` `{x, y, w, h}` in sprite pixels. The text beside the image says exactly what was rendered and at what scale.",inputSchema:{type:"object",properties:{document:I,frame:{type:"integer"},scale:{type:"integer",description:"Screen pixels per sprite pixel. Default: whatever makes the long side about 1000px."},rect:{type:"object",description:"A window onto the canvas in sprite pixels, cropped before scaling. Omit w/h to run to the edge.",properties:{x:{type:"integer"},y:{type:"integer"},w:{type:"integer"},h:{type:"integer"}}},job:S}},async run(r,e){let t=await e.preview({document:r.document,job:r.job,frame:r.frame??0,scale:r.scale??null,rect:r.rect??null});return Ce(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:I,columns:{type:"integer",default:4},scale:{type:"integer",default:2},job:S}},async run(r,e){let t=await e.sheet({document:r.document,job:r.job,columns:r.columns??4,scale:r.scale??2});return Ce(t.png,t.caption)}},{name:"add_layer",description:"Add a layer above the others. Anything you will revise while the rest stays \u2014 a figure over a background, a caption over a figure \u2014 goes on its own layer, so a revision is one draw_art with `clear: true` on that layer and the base is never redrawn or at risk.",inputSchema:{type:"object",properties:{name:{type:"string"},job:S}},run:(r,e)=>e.addLayer({job:r.job,name:r.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:S}},run:(r,e)=>e.addFrame({job:r.job,copy:r.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 \u2014 choose it before you draw, because every draw_art adds its colors. With `prune: true` and no `colors`, drop every palette entry no pixel uses instead: the way to clean up after a session that did not. Never done for you on finish_job.",inputSchema:{type:"object",properties:{colors:{type:"array",items:B,minItems:1},prune:{type:"boolean",description:"Keep only the colors some pixel uses. `colors` is ignored."},job:S}},run:(r,e)=>!r.prune&&!r.colors?.length?kt("set_palette wants `colors`, or `prune: true`"):e.setPalette({job:r.job,colors:r.colors??[],prune:r.prune===!0})},{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:S},required:["fps"]},run:(r,e)=>e.setFps({job:r.job,fps:r.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:S}},run:(r,e)=>e.clearCel({job:r.job,layer:r.layer,frame:r.frame??0})},{name:"trim_canvas",description:"Crop the canvas to the drawn pixels. Call this before finish_job on every GAME ASSET \u2014 a tile, an icon, a sprite for a sheet: you cannot know its extent before you have drawn it, so overshoot at new_sprite and trim afterwards, or empty space is inherited by every export and sheet. Do NOT trim a piece made to be posted: its canvas is the card, and the space around the subject is the composition.",inputSchema:{type:"object",properties:{margin:{type:"integer",default:0},job:S}},run:(r,e)=>e.trim({job:r.job,margin:r.margin??0})},{name:"get_info",description:"What is in a document \u2014 size, layers, frames, palette \u2014 and who else is looking at it; `connected` says whether the room is on the other end and `pending` how many operations it has not confirmed. Both should be true and zero before you trust a read.",inputSchema:{type:"object",properties:{document:I,job:S}},run:(r,e)=>e.info({document:r.document,job:r.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:(r,e)=>e.documents()},{name:"create_folder",description:"Make a folder in the library, by path \u2014 `food/sushi` nests. Already existing is fine. Prefer passing `folder` to new_sprite when the sprites are new; use this for an empty folder or before move_documents.",inputSchema:{type:"object",properties:{name:{type:"string",description:"The folder path."}},required:["name"]},run:(r,e)=>e.createFolder({name:r.name})},{name:"move_documents",description:"File documents into a folder, or back to the top level with folder null. Takes document ids from list_documents. The folder is made if it does not exist.",inputSchema:{type:"object",properties:{documents:{type:"array",items:{type:"string"},description:"Document ids."},folder:{type:["string","null"],description:"Folder path, or null for the top level."}},required:["documents"]},run:(r,e)=>e.moveDocuments({documents:r.documents,folder:r.folder??null,make:!0})},{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:(r,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(r,e){let t=await e.openEditor();return Ue(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:(r,e)=>e.select({document:r.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:(r,e)=>e.closeDocument({document:r.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:(r,e)=>e.setFrame({index:r.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:(r,e)=>e.setLayer({index:r.index})},{name:"undo",description:"Undo the last change, so the user can always reject generated art.",inputSchema:{type:"object",properties:{}},run:(r,e)=>e.undo()},{name:"redo",description:"Redo the last undone change.",inputSchema:{type:"object",properties:{}},run:(r,e)=>e.redo()}];function Be(r){return At.filter(e=>r.supports(e.name)).map(e=>({name:e.name,description:e.description,inputSchema:e.inputSchema,run:async t=>{let n=await e.run(t??{},r);return n&&typeof n=="object"&&Array.isArray(n.content)?n:Ue(n)}}))}var Fe=/^[A-Za-z0-9_-]{1,64}$/,Tt=6e3;function Pt(r,e){return Math.max(1,Math.min(32,Math.round(1e3/Math.max(r,e,1))))}function Ot(r,e,t){let n=(l,c)=>Number.isFinite(+l)?Math.max(0,Math.round(+l)):c,o=Math.min(n(r?.x,0),e),s=Math.min(n(r?.y,0),t),i=Math.min(n(r?.w,e-o),e-o),a=Math.min(n(r?.h,t-s),t-s);return{x:o,y:s,w:i,h:a}}var F=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.control=t.control??null,this.probe=t.probe??null,this.rooms=new Map,this.asked=[],this.onAsked=null,this.jobs=new Map,this.log=[]}async open(e,{w:t=32,h:n=32,name:o=e}={}){let s=this.rooms.get(e);if(s)return s.client;let i=this.core.newDocument({w:t,h:n,name:o,seed:We(),now:Date.now()}),a=new C(this.core,i,{author:this.author,name:this.name,seed:We(),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 l=this.connectTransport(e,{w:t,h:n,name:o}),c=!1;return await new Promise((u,d)=>{let p=setTimeout(()=>d(new Error(`timed out joining ${e}`)),1e4);a.onStatus=f=>{f.connected&&(c=!0,clearTimeout(p),u())},a.connect(l);let m=l.onclose;l.onclose=()=>{if(m?.(),c)return this.#n(e,{w:t,h:n,name:o});clearTimeout(p),this.#e(e).then(f=>d(new Error(f)))}}),this.rooms.set(e,{client:a,attempts:0}),a}async#e(e){return this.serverUrl?(this.probe?await this.probe(this.serverUrl).catch(()=>!1):!1)?Fe.test(e)?`the room "${e}" would not open, and the server is up: try once more \u2014 a room can be away for a moment \u2014 and if it repeats, there is no such document or it is not this account's; list_documents names what it can`:`the room server refused "${e}": that is a name, not a document id \u2014 list_documents gives the id, and passing the exact name works when it is unique`:De(this.serverUrl):"nothing is answering at the room server"}#n(e,t){let n=this.rooms.get(e);if(!n||n.closing)return;let o=Math.min(15e3,1e3*2**Math.min(n.attempts,4));n.attempts+=1,n.retry=setTimeout(()=>{if(!this.rooms.get(e)||n.closing)return;let s=this.connectTransport(e,t),i=!1,a=s.onclose;n.client.onStatus=c=>{c.connected&&(i=!0,n.attempts=0)},n.client.connect(s);let l=s.onclose;s.onclose=()=>{l?.(),a?.(),this.#n(e,t)}},o),n.retry.unref?.()}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:n=null,open:o={}}={}){let s=await this.open(e,o),i=x(),a=Dt(s,i);s.lease("acquire",{job:i,intent:t,...n?{layers:[n]}:{}});let l=await a;if(l!=="held"){let h=s.leases.find(u=>u.job!==i);throw new Error(l==="refused"&&this.lastRefusal?.message?`could not claim "${e}": ${this.lastRefusal.message}`:h?`"${e}" is already held by another job: ${h.intent}`:l==="silent"?`could not claim "${e}": the room did not answer in time \u2014 check get_info and try again`:`could not claim "${e}"`)}let c={room:e,layer:n,intent:t,cancelled:!1,timer:setInterval(()=>{this.jobs.has(i)&&s.lease("renew",{job:i})},Tt)};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,n]=[...this.jobs.entries()][0];return{job:t,...n}}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,n){let o=this.resolve(e),s=this.client(o.room);if(!n.length)return{ops:0};if(!s.connected||!s.welcomed)throw new Error(`the room connection dropped \u2014 nothing since sequence ${s.state.seq} has been saved. Reconnecting; try again in a moment, and check get_info before drawing more.`);this.lastRefusal=null;let i=s.state.seq,a=s.apply(n,{label:t,job:o.job,now:Date.now()});try{await s.acked(a,{timeoutMs:3e4})}catch(c){throw this.lastRefusal?.kind==="revoked"&&(o.cancelled=!0),this.lastRefusal=null,c}if(this.lastRefusal){let c=this.lastRefusal.message;throw this.lastRefusal.kind==="revoked"&&(o.cancelled=!0),this.lastRefusal=null,new Error(c)}this.note(t,o.room,`${n.length} op${n.length>1?"s":""}`);let l=s.state.seq;return{ops:n.length,seq:l,committed:l>i}}cancel(e){let t=this.jobs.get(e);t&&(t.cancelled=!0)}note(e,t,n){this.log.unshift({at:Date.now(),tool:e,room:t,detail:n}),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(){for(let e of this.rooms.values())e.closing=!0,clearTimeout(e.retry);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:It.has(e)}async openEditor({document:e,job:t}={}){if(!this.editorUrl)return{ok:!1,message:"there is no editor to open from here"};let n=this.editorUrl;try{n=`${this.editorUrl}/d/${await this.#s(e,t)}`}catch{n=`${this.editorUrl}/library`}return await Lt(n),{ok:!0,message:`Opened ${n}`}}async newSprite({name:e,w:t=E.w,h:n=E.h,intent:o,palette:s,folder:i=null,challenge:a=null}){let l=await this.createDocument?.({name:e,w:t,h:n})??Ie(e),c=await this.begin(l,o,{open:{w:t,h:n,name:e}});return a&&(this.jobs.get(c).challenge=a),s?.length&&await this.submit(c,"set_palette",[{t:"setPalette",colors:M(s,"hue")}]),i&&await this.moveDocuments({documents:[l],folder:i,make:!0}),this.#i(l,o),{job:c,...this.#t(l)}}async#i(e,t){let n=process.env.SPRITE_ASKED_IN,o=process.env.SPRITE_REPLY_TO??null;if(n)try{(await this.open(n)).answer("thinking",t?String(t).slice(0,200):"making a new sprite",{reply:o,made:e})}catch{}}async beginJob({document:e,intent:t,layer:n=null}){let o=await this.#s(e,null);return{job:await this.begin(o,t,{layer:n}),...this.#t(o)}}finishJob({job:e}){let t=this.resolve(e);this.finish(t.job);let n=this.client(t.room).state,o=Me(n.doc.w,n.doc.h),i=[t.challenge?`This is ${t.challenge==="weekly"?"this week's Weekly Pixels":"today's Pixel Dailies"} entry: tell the user to post it from the share button with the ${t.challenge==="weekly"?"Weekly Pixels":"Pixel Dailies"} switch on, which puts the theme and the tags on the post.`:null,o].filter(Boolean).join(" ");return{finished:t.intent,document:t.room,...i?{next:i}:{}}}async challenges(){if(!this.newsUrl)throw new Error("no server to ask: the themes come from the Pixie server, and this session has none");if(!(this.newsAt&&Date.now()-this.newsAt<300*1e3)){let n=await fetch(this.newsUrl);if(!n.ok)throw new Error(`the server said ${n.status} for the themes`);this.news=await n.json(),this.newsAt=Date.now()}let t=(n,o)=>n?{theme:n.theme,tags:n.tags,detail:n.detail??null,sizes:n.sizes??[],postedAt:new Date(n.at).toISOString(),post:n.url}:{theme:null,note:`no ${o} theme is known right now`};return{daily:t(this.news?.dailies,"daily"),weekly:t(this.news?.weekly,"weekly"),how:'new_sprite({ challenge: "daily" | "weekly", intent }) names the sprite after the theme; finish_job says how to post it as an entry.'}}async challenge(e){let t=await this.challenges(),n=e==="weekly"?t.weekly:t.daily;if(!n?.theme)throw new Error(`no ${e} theme is known right now \u2014 ask the user what to draw`);return n}async#s(e,t){if(!e)return this.resolve(t).room;let n=String(e).trim();if(this.rooms.has(n))return n;let o=await this.#c();if(o===null||o.some(i=>i.id===n))return n;let s=o.filter(i=>(i.name??"").trim().toLowerCase()===n.toLowerCase());if(s.length===1)return s[0].id;if(s.length>1)throw new Error(`${s.length} documents are called "${n}" \u2014 pass the id: ${s.map(i=>i.id).join(", ")}`);if(Fe.test(n))return n;throw new Error(`no document is called "${n}" \u2014 list_documents names every one with its id; pass the id`)}async#c(){let e=this.control?await this.control("/documents",{}).catch(()=>null):null;if(e?.documents)return e.documents.map(n=>({id:n.id,name:n.name}));if(!this.serverUrl)return null;let t=this.serverUrl.replace(/^ws/,"http").replace(/\/$/,"");return fetch(`${t}/documents`).then(n=>n.ok?n.json():null).then(n=>n?.documents?.map(o=>({id:o.id,name:o.name}))??null).catch(()=>null)}async info({document:e,job:t}={}){let n=await this.#s(e,t);return await this.peek(n),this.#t(n)}#t(e){let t=this.client(e),n=t.state;return{document:e,name:n.doc.name,w:n.doc.w,h:n.doc.h,layers:n.doc.layers.map(o=>o.name),frames:n.doc.frames.length,palette:n.doc.palette,tags:n.doc.tags.map(o=>o.name),seq:n.seq,connected:!!(t.connected&&t.welcomed),pending:n.pending,participants:t.participants.map(o=>o.name)}}#a(e){let t=this.client(e),n=t.state;return n.pending===0&&t.connected?{}:{warning:n.pending?`includes ${n.pending} operation${n.pending===1?"":"s"} the room has not confirmed${t.connected?"":" \u2014 the connection dropped, reconnecting"}`:"the room connection dropped, reconnecting"}}async readArt({document:e,job:t,layer:n,frame:o=0,trim:s=!1}){let i=await this.#s(e,t),a=await this.peek(i);return{document:i,...a.session.readArt(this.#o(i,n),o,{trim:s}),...this.#a(i)}}async preview({document:e,job:t,frame:n=0,scale:o=null,rect:s=null}){let i=await this.#s(e,t),a=await this.peek(i),{warning:l}=this.#a(i),c=a.state.doc,h=s?Ot(s,c.w,c.h):null,u=h?h.w:c.w,d=h?h.h:c.h,p=Number.isInteger(o)&&o>0?o:Pt(u,d),m=Math.max(1,Math.min(p,Math.floor(8192/Math.max(u,d,1)))),f=a.session.png(n,{scale:m,...h?{rect:h}:{}}),y=h?`${h.w}\xD7${h.h} at (${h.x},${h.y}) of ${c.w}\xD7${c.h}`:`${c.w}\xD7${c.h}`,v=m<p?` \u2014 asked for \xD7${p}, held to \xD7${m} so a side stays under 8192px; render a smaller rect to look closer`:"";return{png:f,caption:`${i}, frame ${n+1}: ${y} at \xD7${m} (${u*m}\xD7${d*m}px)${v}${l?` \u2014 ${l}`:""}`}}async sheet({document:e,job:t,columns:n=4,scale:o=2}){let s=await this.#s(e,t),i=await this.peek(s);return{png:i.session.contactSheet({columns:n,scale:o}),caption:`${s}, ${i.state.doc.frames.length} frames, numbered left to right`}}async documents(){let e=[...this.jobs.entries()].map(([l,c])=>({job:l,document:c.room,intent:c.intent})),t=[...this.rooms.keys()],n=null,o=null,s=null,i=this.control?await this.control("/documents",{}).catch(()=>null):null;if(i)n=i.documents?.map(l=>l.id)??null,o=Array.isArray(i.folders)?i.folders.map(l=>typeof l=="string"?l:l?.path??l?.name).filter(Boolean):null,s=i.documents?Object.fromEntries(i.documents.filter(l=>l.folder).map(l=>[l.id,l.folder])):null;else if(this.serverUrl){let l=this.serverUrl.replace(/^ws/,"http").replace(/\/$/,"");n=await fetch(`${l}/documents`).then(c=>c.ok?c.json():null).then(c=>c?.documents?.map(h=>h.id)).catch(()=>null)}return{documents:n?[...new Set([...t,...n])]:t,jobs:e,open:t,...o?{folders:o}:{},...s?{filed:s}:{}}}async createFolder({name:e}){if(!this.control)throw new Error("no library here: this Sprite has no accounts, so there are no folders");let t=String(e??"").trim();if(!t)throw new Error("a folder needs a name");let n=await this.folders();if(n.includes(t))return{folder:t,created:!1,folders:n};let o=t.split("/");for(let s=1;s<=o.length;s++){let i=o.slice(0,s).join("/");n.includes(i)||(await this.control("/documents/folders",{method:"POST",body:{name:i}}),n.push(i))}return{folder:t,created:!0,folders:await this.folders()}}async moveDocuments({documents:e,folder:t=null,make:n=!1}){if(!this.control)throw new Error("no library here: this Sprite has no accounts, so there are no folders");let o=(Array.isArray(e)?e:[e]).map(String).filter(Boolean);if(!o.length)throw new Error("say which documents");let s=t===null||t===""?null:String(t).trim();s&&n&&await this.createFolder({name:s});let i=await this.control("/documents/move",{method:"POST",body:{ids:o,folder:s}});return{moved:o,folder:s,ok:i?.ok!==!1}}async folders(){let e=await this.control("/documents",{});return Array.isArray(e?.folders)?e.folders.map(t=>typeof t=="string"?t:t?.path??t?.name).filter(Boolean):[]}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 n=this.client(e).state;if(t!=null&&t!==""){let o=typeof t=="number"?n.doc.layers[t]:n.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 n.doc.layers[n.doc.layers.length-1].id}async drawArt({job:e,rows:t,colors:n,x:o=0,y:s=0,center:i=!1,clear:a=!1,layer:l,frame:c=0,label:h}){let u=this.resolve(e),p=this.client(u.room).session.drawArt(t,n,{layer:l!=null?this.#o(u.room,l):u.layer??this.#o(u.room,null),at:c,x:o,y:s,center:i,clear:a}),{committed:m}=await this.submit(u.job,h||"draw_art",p);return{...this.#t(u.room),drew:`${t[0]?.length??0}\xD7${t.length}`,committed:m}}async drawAnimation({job:e,frames:t,colors:n,fps:o,layer:s,label:i}){let a=this.resolve(e),l=this.client(a.room),c=s!=null?this.#o(a.room,s):a.layer??this.#o(a.room,null),h=l.state.doc.frames.length,u=t.length-h;if(u>0){let p=[],m=l.state.doc.frames[h-1]?.index??"V";for(let f=0;f<u;f++)m=m+"V",p.push({t:"addFrame",id:x(),index:m,durMs:Y(o)});await this.submit(a.job,"add frames",p)}if(o){let p=Y(o);await this.submit(a.job,"set fps",l.state.doc.frames.map(m=>({t:"setFrameDur",id:m.id,durMs:p})))}let d=l.session.drawAnimation(t,n,{layer:c});return await this.submit(a.job,i||"draw_animation",d),{...this.#t(a.room),wrote:`${t.length} frames`}}async addLayer({job:e,name:t}){let n=this.resolve(e),o=this.client(n.room).state,s=o.doc.layers[o.doc.layers.length-1];return await this.submit(n.job,"add_layer",[{t:"addLayer",id:x(),index:(s?.index??"V")+"V",name:t||`Layer ${o.doc.layers.length+1}`,visible:!0,locked:!1,opacity:1}]),this.#t(n.room)}async addFrame({job:e,copy:t=!1}){let n=this.resolve(e),o=this.client(n.room),s=o.state,i=s.doc.frames[s.doc.frames.length-1],a=x(),l=[{t:"addFrame",id:a,index:(i?.index??"V")+"V",durMs:i?.durMs??83}];if(t&&i)for(let c of s.doc.layers){let h=o.session.cel(c.id,i.id);h.every(u=>u===0)||l.push(ze(c.id,a,s.doc,h))}return await this.submit(n.job,"add_frame",l),this.#t(n.room)}async setPalette({job:e,colors:t,prune:n=!1}){let o=this.resolve(e),s;if(n){let i=this.client(o.room),a=i.state,l=new Set;for(let h of a.doc.layers)for(let u of a.doc.frames){let d=i.session.cel(h.id,u.id);for(let p=0;p<d.length;p+=4)d[p+3]!==0&&l.add(`#${[d[p],d[p+1],d[p+2]].map(m=>m.toString(16).padStart(2,"0")).join("")}`)}let c=a.doc.palette??[];s=c.filter(h=>l.has(String(h).toLowerCase()));for(let h of l)s.some(u=>String(u).toLowerCase()===h)||s.push(h);return s=M(s,"hue"),await this.submit(o.job,"set_palette",[{t:"setPalette",colors:s}]),{...this.#t(o.room),palette:s.length,pruned:c.length-s.length}}return s=M(t,"hue"),await this.submit(o.job,"set_palette",[{t:"setPalette",colors:s}]),{...this.#t(o.room),palette:s.length}}async setFps({job:e,fps:t}){let n=this.resolve(e),o=this.client(n.room).state,s=Y(t);return await this.submit(n.job,"set_fps",o.doc.frames.map(i=>({t:"setFrameDur",id:i.id,durMs:s}))),{...this.#t(n.room),fps:t}}async clearCel({job:e,layer:t,frame:n=0}){let o=this.resolve(e),i=this.client(o.room).state,a=this.#o(o.room,t??o.layer),l=i.doc.frames[n];if(!l)throw new Error(`no frame ${n} in ${o.room}`);let c=new Uint8Array(i.doc.w*i.doc.h*4);return await this.submit(o.job,"clear",[ze(a,l.id,i.doc,c)]),this.#t(o.room)}async trim({job:e,margin:t=0}){let n=this.resolve(e),o=this.client(n.room),s=o.state,i=this.#o(n.room,n.layer),a=o.session.readArt(i,0,{trim:!0}),l=a.rect.w+t*2,c=a.rect.h+t*2;return l>=s.doc.w&&c>=s.doc.h?{...this.#t(n.room),trimmed:"already tight"}:(await this.submit(n.job,"trim_canvas",[{t:"resize",w:l,h:c,dx:-a.rect.x+t,dy:-a.rect.y+t}]),{...this.#t(n.room),trimmed:`${l}\xD7${c}`})}},It=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","challenges","take_requests","create_folder","move_documents"]);function ze(r,e,t,n){return{t:"paintRegion",layer:r,frame:e,rect:{x:0,y:0,w:t.w,h:t.h},rle:Nt(n)}}function Nt(r){let e=[];if(r.length){let t=[r[0],r[1],r[2],r[3]],n=0,o=()=>{let s=n;do{let i=s&127;s>>=7,e.push(s?i|128:i)}while(s);e.push(...t)};for(let s=0;s<r.length;s+=4)r[s]===t[0]&&r[s+1]===t[1]&&r[s+2]===t[2]&&r[s+3]===t[3]?n++:(o(),t=[r[s],r[s+1],r[s+2],r[s+3]],n=1);o()}return Buffer.from(Uint8Array.from(e)).toString("base64")}function Lt(r){let e=process.platform==="darwin"?"open":process.platform==="win32"?"start":"xdg-open";return new Promise(t=>Rt(e,[r],()=>t()))}function Y(r){return!r||r<=0?83:Math.max(10,Math.round(1e3/r))}function Dt(r,e,t=8e3){return new Promise(n=>{let o=r.onLeases,s=r.onLost,i=!1,a=c=>{i||(i=!0,clearTimeout(l),r.onLeases=o,r.onLost=s,n(c))};r.onLeases=c=>{o?.(c),c.some(h=>h.job===e)&&a("held")},r.onLost=c=>{s?.(c),c?.ops?.length||a("refused")};let l=setTimeout(()=>a("silent"),t);r.leases.some(c=>c.job===e)&&a("held")})}function We(){return(Date.now()^Math.floor(Math.random()*4294967295))>>>0}import{execFile as Ct}from"node:child_process";var Ut=2e4,Mt=5e3,N=class r{constructor({base:e,name:t="agent"}={}){this.base=String(e).replace(/\/$/,""),this.name=t,this.log=[],this.beat=setInterval(()=>void this.announce(),Mt),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 n=await fetch(`${e.replace(/\/$/,"")}/bridge/status`,{signal:AbortSignal.timeout(t)});if(!n.ok)return null;let o=await n.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=[],n=Ut){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:n}),signal:AbortSignal.timeout(n+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=E.w,h:n=E.h,intent:o,palette:s}){return this.call("newSprite",[{w:t,h:n,name:e,palette:s,intent:o,replace:!1}])}async challenges(){let e=await fetch(`${this.base}/news.json`).catch(()=>null);if(!e?.ok)throw new Error("this editor does not serve the themes \u2014 ask the user what to draw");let t=await e.json(),n=(o,s)=>o?{theme:o.theme,tags:o.tags,detail:o.detail??null,sizes:o.sizes??[],postedAt:new Date(o.at).toISOString(),post:o.url}:{theme:null,note:`no ${s} theme is known right now`};return{daily:n(t?.dailies,"daily"),weekly:n(t?.weekly,"weekly")}}async challenge(e){let t=await this.challenges(),n=e==="weekly"?t.weekly:t.daily;if(!n?.theme)throw new Error(`no ${e} theme is known right now \u2014 ask the user what to draw`);return n}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:n}){return this.call("readArt",[{document:e,layer:t,frame:n}])}async preview({document:e,frame:t,scale:n=null,rect:o=null}){if(o)throw new Error("the local editor cannot render a region \u2014 render the whole frame");let s=await this.call("toDataURL",[t??null,Number.isInteger(n)&&n>0?n:8,e??null]);return{png:Ge(s.url),caption:s.caption}}async sheet({document:e,columns:t=0,scale:n=0}){let o=await this.call("toStripDataURL",[{document:e??null,columns:t,scale:n}]);return{png:Ge(o.url),caption:o.caption}}documents(){return this.call("listDocuments",[])}drawArt({job:e,rows:t,colors:n,x:o,y:s,center:i,clear:a,layer:l,frame:c,label:h}){return this.call("drawArt",[t,n,{job:e,x:o,y:s,center:i,clear:a,layer:l,frame:c,label:h}])}drawAnimation({job:e,frames:t,colors:n,fps:o,layer:s,label:i}){return this.call("drawAnimation",[{frames:t,colors:n,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,prune:t=!1}){if(t)throw new Error("prune works in a room \u2014 in a local editor, pass the colors to keep");return this.call("setPalette",[e])}setFps({fps:e}){return this.call("setFps",[e])}clearCel({job:e,layer:t,frame:n}){return this.call("clear",[{job:e,layer:t,frame:n}])}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 r.probe(this.base))?.connected)return{ok:!0,message:`Editor already connected at ${this.base}`};await Ft(this.base);let t=Date.now()+1e4;for(;Date.now()<t;){if((await r.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","challenges","open_editor","select_document","close_document","set_frame","set_layer","undo","redo"]);function Ge(r){let e=String(r??"").split(",")[1];if(!e)throw new Error("the editor returned something that was not an image");return Buffer.from(e,"base64")}function Ft(r){let e=process.platform==="darwin"?"open":process.platform==="win32"?"start":"xdg-open";return new Promise(t=>Ct(e,[r],()=>t()))}import{readFileSync as zt}from"node:fs";var Wt="0.5.4";function Gt(){let r=new URL("./version.json",import.meta.url);try{let e=JSON.parse(zt(r,"utf8"))?.version;if(typeof e=="string"&&e)return e;throw new Error("it names no version")}catch(e){throw new Error(`${r.pathname} could not be read as a version: ${e.message}`)}}var qe=Wt??Gt();function Q(r="room"){return`Pixel art editor with live collaboration. ${r==="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.`}
@@ -66,7 +66,7 @@ them) and filed with move_documents. If a call is refused as "may not
66
66
  organize", this machine was paired before folders were part of the default:
67
67
  tell the user to run \`pixie-mcp login\` again.
68
68
 
69
- ${Ft}`}var Ft=`SPEED. Wall clock is round trips times payload, so:
69
+ ${qt}`}var qt=`SPEED. Wall clock is round trips times payload, so:
70
70
  - Silhouette before detail. At 32px a front-on animal is indistinguishable from
71
71
  any other animal; profile is what reads. When the subject could go several
72
72
  ways, draw 2-3 rough silhouettes as frames, look at them with
@@ -81,9 +81,9 @@ ${Ft}`}var Ft=`SPEED. Wall clock is round trips times payload, so:
81
81
 
82
82
  Do not ask the user abstract clarifying questions. A human round trip costs
83
83
  minutes; a tool round trip costs seconds. Commit to a good default and show
84
- something.`,zt=Q("room"),F=class{constructor({name:e="pixie",version:t=Je,tools:n=[],instructions:o=zt}={}){this.name=e,this.version=t,this.tools=n,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 n=await this.#e(e);return t?null:{jsonrpc:"2.0",id:e.id,result:n}}catch(n){return t?null:{jsonrpc:"2.0",id:e.id,error:{code:-32603,message:n?.message??String(n)}}}}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(n=>n.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(n){return{isError:!0,content:[{type:"text",text:n?.message??String(n)}]}}}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 n="";return e.setEncoding("utf8"),e.on("data",async o=>{n+=o;let s;for(;(s=n.indexOf(`
85
- `))!==-1;){let i=n.slice(0,s).trim();if(n=n.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)+`
86
- `)}}),this}};var L=(r,e)=>{let t=process.argv.indexOf(`--${r}`);return t>-1?process.argv[t+1]:e},ee=process.env.SPRITE_EDITOR||"http://127.0.0.1:4242",J=process.env.SPRITE_SERVER||"ws://127.0.0.1:4243",Jt=new URL("./sprite_core.wasm",import.meta.url);function qt(r){let e=String(r??"").trim();return!e||!/^[a-z0-9]+([-_][a-z0-9]+)*$/.test(e)?e||"an agent":e.split(/[-_]/).map(t=>Ht.has(t)?t.toUpperCase():t[0].toUpperCase()+t.slice(1)).join(" ")}var Ht=new Set(["cli","ai","api","ide","mcp","sdk","gpt","llm"]);function Gt(r){let e=new WebSocket(r),t={readyState:0,onopen:null,onmessage:null,onclose:null,send:n=>e.send(n),close:()=>e.close()};return e.addEventListener("open",()=>{t.readyState=1,t.onopen?.()}),e.addEventListener("message",n=>t.onmessage?.(n.data)),e.addEventListener("close",()=>{t.readyState=3,t.onclose?.()}),e.addEventListener("error",()=>{}),t}async function Vt(r,{timeoutMs:e=700}={}){if(await oe(r,{timeoutMs:e}))return!0;try{let t=await fetch(`${f(r)}/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 Xt({editor:r=ee,server:e=J,allowHost:t=!0,publicServer:n=T}={}){if(n)return{kind:"room",url:n,why:`the Pixie this agent belongs to, at ${f(n)}`};let o=await N.probe(r);return o?{kind:"local",url:r,why:o.connected?`a Pixie editor is open at ${r}`:`a Pixie server is running at ${r}, with no editor open yet`}:await Vt(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 Zt({kind:r=null,editor:e=ee,server:t=J,wasm:n=Jt,name:o="agent",allowHost:s=!0}={}){let i=r?{kind:r,url:r==="local"?e:t,why:"you said so"}:await Xt({editor:e,server:t,allowHost:s}),a,c=null,l=null;if(i.kind==="local")a=new N({base:i.url,name:o}),a.announce();else{i.start;let d=se(i.url),p=n instanceof Uint8Array?n:await Wt(n);c=await D.load(p);let w=await d;a=new B(c,{author:k(),name:o,connect:(m,y={})=>{let _=new URLSearchParams;y.w&&_.set("w",String(y.w)),y.h&&_.set("h",String(y.h)),y.name&&_.set("name",y.name);let te=_.toString();return Gt(`${w}/rooms/${m}${te?`?${te}`:""}`)}}),a.serverUrl=w,a.getToken=async m=>{let y=await de(i.url,m);if(y!==void 0)return y;if(await z(i.url))throw new Error(W(i.url))},a.createDocument=async m=>{let y=await pe(i.url,m);if(y!==void 0)return y;if(await z(i.url))throw new Error(W(i.url))},a.control=async(m,y)=>{let _=await me(i.url,m,y);if(_!==void 0)return _;throw new Error(await z(i.url)?W(i.url):"no library here: this Sprite has no accounts, so there are no folders")},a.editorUrl=f(i.url),a.newsUrl=`${f(i.url)}/news.json`,a.hosted=l}let h=new F({tools:Be(a),instructions:Q(a.kind)}),u=h.handle.bind(h);return h.handle=async d=>{let p=await u(d);return d?.method==="initialize"&&h.client?.name&&(a.name=qt(h.client.name),a.announce()),p},{core:c,backend:a,mcp:h,chosen:i}}var qe=null;function z(r){return qe??=ue(r).catch(()=>!1),qe}function Kt(r=process.argv){let e=r[1]??"";return/[\\/]pixie-mcp$/.test(e)?"pixie-mcp":/[\\/]sprite-mcp$/.test(e)?"sprite-mcp":"npx pixie-mcp"}function W(r,e=Kt()){return`this machine has not been given a credential for ${f(r)} yet. Run \`${e} login\` once, in a terminal \u2014 it opens a browser, and one button in it is the whole of it.`}if(_e(import.meta.url)){let r=L("server",null)?"room":L("editor",null)?"local":null;if(process.argv.includes("login")){let e=L("server",T||J);try{let t=await xe(e,{onUrl:n=>process.stderr.write(`
84
+ something.`,Jt=Q("room"),z=class{constructor({name:e="pixie",version:t=qe,tools:n=[],instructions:o=Jt}={}){this.name=e,this.version=t,this.tools=n,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 n=await this.#e(e);return t?null:{jsonrpc:"2.0",id:e.id,result:n}}catch(n){return t?null:{jsonrpc:"2.0",id:e.id,error:{code:-32603,message:n?.message??String(n)}}}}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(n=>n.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(n){return{isError:!0,content:[{type:"text",text:n?.message??String(n)}]}}}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 n="";return e.setEncoding("utf8"),e.on("data",async o=>{n+=o;let s;for(;(s=n.indexOf(`
85
+ `))!==-1;){let i=n.slice(0,s).trim();if(n=n.slice(s+1),!i)continue;let a;try{a=JSON.parse(i)}catch{continue}let l=await this.handle(a);l&&t.write(JSON.stringify(l)+`
86
+ `)}}),this}};var L=(r,e)=>{let t=process.argv.indexOf(`--${r}`);return t>-1?process.argv[t+1]:e},ee=process.env.SPRITE_EDITOR||"http://127.0.0.1:4242",q=process.env.SPRITE_SERVER||"ws://127.0.0.1:4243",Vt=new URL("./sprite_core.wasm",import.meta.url);function Kt(r){let e=String(r??"").trim();return!e||!/^[a-z0-9]+([-_][a-z0-9]+)*$/.test(e)?e||"an agent":e.split(/[-_]/).map(t=>Zt.has(t)?t.toUpperCase():t[0].toUpperCase()+t.slice(1)).join(" ")}var Zt=new Set(["cli","ai","api","ide","mcp","sdk","gpt","llm"]);function Xt(r){let e=new WebSocket(r),t={readyState:0,onopen:null,onmessage:null,onclose:null,send:n=>e.send(n),close:()=>e.close()};return e.addEventListener("open",()=>{t.readyState=1,t.onopen?.()}),e.addEventListener("message",n=>t.onmessage?.(n.data)),e.addEventListener("close",()=>{t.readyState=3,t.onclose?.()}),e.addEventListener("error",()=>{}),t}async function He(r,{timeoutMs:e=700}={}){if(await oe(r,{timeoutMs:e}))return!0;try{let t=await fetch(`${w(r)}/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 Yt({editor:r=ee,server:e=q,allowHost:t=!0,publicServer:n=T}={}){if(n)return{kind:"room",url:n,why:`the Pixie this agent belongs to, at ${w(n)}`};let o=await N.probe(r);return o?{kind:"local",url:r,why:o.connected?`a Pixie editor is open at ${r}`:`a Pixie server is running at ${r}, with no editor open yet`}:await He(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 Qt({kind:r=null,editor:e=ee,server:t=q,wasm:n=Vt,name:o="agent",allowHost:s=!0}={}){let i=r?{kind:r,url:r==="local"?e:t,why:"you said so"}:await Yt({editor:e,server:t,allowHost:s}),a,l=null,c=null;if(i.kind==="local")a=new N({base:i.url,name:o}),a.announce();else{i.start;let d=se(i.url),p=n instanceof Uint8Array?n:await Ht(n);l=await D.load(p);let m=await d;a=new F(l,{author:x(),name:o,connect:(f,y={})=>{let v=new URLSearchParams;y.w&&v.set("w",String(y.w)),y.h&&v.set("h",String(y.h)),y.name&&v.set("name",y.name);let te=v.toString();return Xt(`${m}/rooms/${f}${te?`?${te}`:""}`)},probe:()=>He(i.url)}),a.serverUrl=m,a.getToken=async f=>{let y=await de(i.url,f);if(y!==void 0)return y;if(await W(i.url))throw new Error(G(i.url))},a.createDocument=async f=>{let y=await pe(i.url,f);if(y!==void 0)return y;if(await W(i.url))throw new Error(G(i.url))},a.control=async(f,y)=>{let v=await me(i.url,f,y);if(v!==void 0)return v;throw new Error(await W(i.url)?G(i.url):"no library here: this Sprite has no accounts, so there are no folders")},a.editorUrl=w(i.url),a.newsUrl=`${w(i.url)}/news.json`,a.hosted=c}let h=new z({tools:Be(a),instructions:Q(a.kind)}),u=h.handle.bind(h);return h.handle=async d=>{let p=await u(d);return d?.method==="initialize"&&h.client?.name&&(a.name=Kt(h.client.name),a.announce()),p},{core:l,backend:a,mcp:h,chosen:i}}var Je=null;function W(r){return Je??=ue(r).catch(()=>!1),Je}function en(r=process.argv){let e=r[1]??"";return/[\\/]pixie-mcp$/.test(e)?"pixie-mcp":/[\\/]sprite-mcp$/.test(e)?"sprite-mcp":"npx pixie-mcp"}function G(r,e=en()){return`this machine has not been given a credential for ${w(r)} yet. Run \`${e} login\` once, in a terminal \u2014 it opens a browser, and one button in it is the whole of it.`}if($e(import.meta.url)){let r=L("server",null)?"room":L("editor",null)?"local":null;if(process.argv.includes("login")){let e=L("server",T||q);try{let t=await ke(e,{onUrl:n=>process.stderr.write(`
87
87
  ${n}
88
88
  `),label:"the Pixie MCP server"});process.stderr.write(`
89
89
  This machine can now draw as ${t.email??t.name??"you"}.
@@ -91,6 +91,6 @@ something.`,zt=Q("room"),F=class{constructor({name:e="pixie",version:t=Je,tools:
91
91
  `)}catch(t){process.stderr.write(`
92
92
  ${t.message}
93
93
 
94
- `),process.exit(1)}}else{let{mcp:e,chosen:t}=await Zt({kind:r,editor:L("editor",ee),server:L("server",J),allowHost:!process.argv.includes("--no-host")});process.stderr.write(`sprite-mcp \u2192 ${t.kind} (${t.why})
95
- `),e.serve(),t.kind==="room"&&(async()=>(await P(t.url))?.token||await z(t.url)&&process.stderr.write(`sprite-mcp \u2192 ${W(t.url)}
96
- `))()}}export{ee as DEFAULT_EDITOR,J as DEFAULT_SERVER,Kt as commandName,Zt as createServer,Xt as detect,qt as presentable,Vt as probeRooms,W as signInFirst,Gt as websocketTransport};
94
+ `),process.exit(1)}}else{let{mcp:e,chosen:t}=await Qt({kind:r,editor:L("editor",ee),server:L("server",q),allowHost:!process.argv.includes("--no-host")});process.stderr.write(`sprite-mcp \u2192 ${t.kind} (${t.why})
95
+ `),e.serve(),t.kind==="room"&&(async()=>(await P(t.url))?.token||await W(t.url)&&process.stderr.write(`sprite-mcp \u2192 ${G(t.url)}
96
+ `))()}}export{ee as DEFAULT_EDITOR,q as DEFAULT_SERVER,en as commandName,Qt as createServer,Yt as detect,Kt as presentable,He as probeRooms,G as signInFirst,Xt as websocketTransport};
package/sprite_core.wasm CHANGED
Binary file