pixie-mcp 0.7.8 → 0.7.9
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 +1 -1
- package/pixie-mcp.mjs +11 -11
package/package.json
CHANGED
package/pixie-mcp.mjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var Z=(r,e)=>()=>(r&&(e=r(r=0)),e);function bt(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 xt=Z(()=>{"use strict"});var Y,jn,_n,fo,$t=Z(()=>{"use strict";Y="[A-Za-z0-9_-]{1,64}",jn="token|members|link|slow|suggest|project",_n=[{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/${Y}/(${jn})$`)},{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/${Y}(/(thumbnail\\.png|export|duplicate|rename|restore))?$`)},{owner:"library",re:new RegExp(`^/documents/${Y}/history/at/\\d+/thumbnail\\.png$`)},{owner:"library",re:new RegExp("^/wall\\.png$")},{owner:"document",re:new RegExp(`^/documents/${Y}/history(/at/\\d+|/\\d+|/source(/[A-Za-z0-9_-]{1,64})?)?$`)},{owner:"document",re:new RegExp(`^/documents/${Y}/comments(/[A-Za-z0-9]{1,40})?$`)},{owner:"document",re:new RegExp(`^/documents/${Y}/snapshots(/enabled|/[A-Za-z0-9_-]{1,128})?$`)},{owner:"document",re:new RegExp(`^/favourites(/order|/(document|project)/${Y})?$`)}],fo=_n.map(({owner:r,re:e})=>({owner:r,source:e.source}))});var ze,Fe=Z(()=>{"use strict";$t();ze=["read","draw","create","duplicate","organize"]});async function kt(r){let e=new TextEncoder().encode(String(r));return bt(new Uint8Array(await crypto.subtle.digest("SHA-256",e)))}var ko,St=Z(()=>{"use strict";xt();Fe();ko=120*1e3});function L(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 K(r){return r==null?{at:0}:typeof r=="number"?{at:r}:{frame:r}}var In,Mn,Cn,ke,Q,Be,ce=Z(()=>{"use strict";In=new TextEncoder,Mn=new TextDecoder,Cn=1;ke=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#o(){return new Uint8Array(this.#n.memory.buffer)}call(e,t={}){let n=In.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.#o.set(n,o),s=this.#n.sprite_dispatch(o,n.length),!s)throw new Error("sprite-core: dispatch returned nothing");let i=this.#o,a=i[s]|i[s+1]<<8|i[s+2]<<16|i[s+3]<<24,c=i[s+4],h=i.slice(s+5,s+5+a);if(c===Cn)return h;let l=JSON.parse(Mn.decode(h));if(l&&l.error)throw new Error(l.error);return l}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 Q(this,o)}openRoom(e,{seq:t=0}={}){let{h:n}=this.call("room.new",{doc:e,...t>0?{seq:t}:{}});return new Be(this,n)}},Q=class{constructor(e,t){this.core=e,this.h=t}#e(e,t={}){return this.core.call(e,{h:this.h,...t})}state(){return this.#e("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,...K(t),...n})}cel(e,t){return this.#e("session.cel",{layer:e,...K(t)})}layerImage(e,t){return this.#e("session.layerImage",{layer:e,...K(t)})}tileCel(e,t){return this.#e("session.tileCel",{layer:e,...K(t)})}tile(e,t){return this.#e("session.tile",{tileset:e,index:t})}composite(e,{include:t="export"}={}){return this.#e("session.composite",{...K(e),include:t})}png(e,{scale:t=1,rect:n=null}={}){return this.#e("session.png",{...K(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??{},...K(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??{},...K(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,...K(a)})}close(){this.#e("session.free")}},Be=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})}region(e,t,n){return this.#e("room.region",{layer:e,frame:t,rect:{x:n.x??0,y:n.y??0,w:n.w,h:n.h}})}close(){this.#e("room.free")}}});function Ot(r){return JSON.stringify(r)}function Nt(r){let e=r.rle instanceof Uint8Array?r.rle:null,t=new TextEncoder().encode(JSON.stringify({...r,rle:void 0}));if(t.length>Ln)return null;let n=new Uint8Array(3+t.length+(e?.length??0));return n[0]=Pt,n[1]=t.length&255,n[2]=t.length>>8&255,n.set(t,3),e&&n.set(e,3+t.length),n}function Un(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 It(r){if(typeof r!="string"){let e=Un(r);if(!e||e.length<3||e[0]!==Pt)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 P,Rt,N,Tt,Pt,Ln,Mt=Z(()=>{"use strict";P={HELLO:"hello",SUBMIT:"submit",PRESENCE:"presence",STROKE:"stroke",LEASE:"lease",REVIEW:"review",PING:"ping",PROMPT:"prompt",ANSWER:"answer",STOP:"stop"},Rt={DOCUMENT:"document",LIBRARY:"library"},N={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"},Tt={OPS:"ops",SNAPSHOT:"snapshot"};Pt=157,Ln=65535});function Ct(r){return L().toLowerCase().slice(0,16)}var Lt=Z(()=>{"use strict";ce()});function qn(r){let e=[];if(!r.length)return new Uint8Array;let t=r[0],n=r[1],o=r[2],s=r[3],i=0,a=()=>{let c=i;do{let h=c&127;c>>>=7,e.push(c?h|128:h)}while(c);e.push(t,n,o,s)};for(let c=0;c<r.length;c+=4)r[c]===t&&r[c+1]===n&&r[c+2]===o&&r[c+3]===s?i++:(a(),t=r[c],n=r[c+1],o=r[c+2],s=r[c+3],i=1);return a(),new Uint8Array(e)}function zn(r){let e="";for(let n=0;n<r.length;n+=32768)e+=String.fromCharCode.apply(null,r.subarray(n,n+32768));return btoa(e)}function Fn(r){return zn(qn(r))}function Dt(r,e,t,n){return{t:"paintRegion",layer:r,frame:e,rect:{x:t.x,y:t.y,w:t.w,h:t.h},rle:Fn(n)}}var qt=Z(()=>{"use strict"});import{readFile as Kr}from"node:fs/promises";import{readFile as yn,writeFile as gn,mkdir as bn,chmod as xn}from"node:fs/promises";import{readFileSync as $n}from"node:fs";import{homedir as kn,hostname as Sn}from"node:os";import{join as De}from"node:path";import{execFile as En}from"node:child_process";var S=r=>String(r??"").replace(/\/+$/,"").replace(/^ws/,"http"),st=r=>String(r??"").replace(/\/+$/,"").replace(/^http/,"ws");async function it(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 at(r,{timeoutMs:e=3e3,fetchImpl:t=fetch}={}){try{let n=await t(`${S(r)}/setup`,{signal:AbortSignal.timeout(e)});return await it(n,["accounts","expectedPort","install"])}catch{return null}}var Ue=new Map;function ct(r,{fetchImpl:e=fetch,timeoutMs:t=3e3}={}){let n=S(r);return Ue.has(n)||Ue.set(n,wn(r,{fetchImpl:e,timeoutMs:t})),Ue.get(n)}async function wn(r,{fetchImpl:e,timeoutMs:t}){try{let n=await e(`${S(r)}/config.json`,{signal:AbortSignal.timeout(t)}),s=(await it(n,["rooms","accounts"]))?.rooms;if(typeof s=="string"&&/^(wss?|https?):\/\/./.test(s))return st(s)}catch{}return st(r)}var ie=process.env.SPRITE_PUBLIC_SERVER||vn()||null;function vn(){return"wss://drawpixie.com"}var ht=De(process.env.XDG_CONFIG_HOME||De(kn(),".config"),"sprite"),qe=De(ht,"agent.json"),lt=r=>String(r??"").replace(/\/+$/,"");function dt(){return`${Sn().replace(/\.local$/,"")}`.slice(0,60)||"a computer"}async function ut(){try{let r=JSON.parse(await yn(qe,"utf8"));return r&&typeof r=="object"&&r.servers?r:{servers:{}}}catch{return{servers:{}}}}async function ae(r){return process.env.SPRITE_TOKEN?{token:process.env.SPRITE_TOKEN,from:"the environment"}:(await ut()).servers[lt(r)]??null}async function pt(r,e){await bn(ht,{recursive:!0,mode:448});let t=await ut();t.servers[lt(r)]={...e,at:Date.now()};let n=`${qe}.tmp`;await gn(n,JSON.stringify(t,null,2),{mode:384}),await xn(n,384);let{rename:o}=await import("node:fs/promises");await o(n,qe)}async function ft(r,{timeoutMs:e=3e3}={}){try{let t=await fetch(`${S(r)}/setup`,{signal:AbortSignal.timeout(e)});return t.ok?!!(await t.json()).accounts:!1}catch{return!1}}async function mt(r,e,{session:t=null}={}){let n=t??await ae(r);if(!n?.token)return;let o=await fetch(`${S(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 wt(r,{name:e,w:t,h:n},{session:o=null}={}){let s=o??await ae(r);if(!s?.token)return;let i=await fetch(`${S(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 yt(r,e,{method:t="GET",body:n=null,session:o=null}={}){let s=o??await ae(r);if(!s?.token)return;let i=await fetch(`${S(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 gt(r){let e=process.platform==="darwin"?"open":process.platform==="win32"?"start":"xdg-open";return new Promise(t=>En(e,[r],()=>t()))}St();Fe();import{createServer as An}from"node:http";import{randomBytes as vt}from"node:crypto";var Rn=300*1e3,Tn=()=>vt(32).toString("base64url"),On=()=>vt(16).toString("base64url");function Et(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 Pn({state:r,timeoutMs:e}){let t,n,o=new Promise((c,h)=>{t=c,n=h}),s=An((c,h)=>{let l=new URL(c.url,"http://127.0.0.1");if(l.pathname!=="/callback"){h.writeHead(404).end();return}if(l.searchParams.get("state")!==r){h.writeHead(404).end();return}let p=l.searchParams.get("error");h.writeHead(200,{"Content-Type":"text/html; charset=utf-8"}).end(p?Et("Nothing was granted.","You can close this window."):Et("This computer can now draw in your sprites.","You can close this window and go back to what you were doing.")),t(l.searchParams)});await new Promise((c,h)=>{s.once("error",h),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 jt(r,{onUrl:e=()=>{},open:t=gt,label:n="the Pixie agent",scope:o=ze,timeoutMs:s=Rn}={}){let i=Tn(),a=await kt(i),c=On(),h=await Pn({state:c,timeoutMs:s});try{let l=new URL(`${S(r)}/authorize`);l.searchParams.set("challenge",a),l.searchParams.set("state",c),l.searchParams.set("redirect",h.redirect),l.searchParams.set("scope",o.join(",")),l.searchParams.set("name",n),e(l.toString()),await t(l.toString());let p=await h.wait,u=p.get("error");if(u)throw new Error(u==="access_denied"?"you said no on that screen, so nothing was granted":`that request was refused: ${u}`);let d=p.get("code");if(!d)throw new Error("the browser came back without an authorization");let m=await fetch(`${S(r)}/auth/authorize/exchange`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:d,verifier:i,name:dt()})}),w=await m.json().catch(()=>({}));if(!m.ok||!w.token)throw new Error(w.error||"that authorization could not be exchanged");return await pt(r,{token:w.token,name:w.name,email:w.email,scope:w.scope??null}),w}finally{h.close()}}import{realpathSync as _t}from"node:fs";import{fileURLToPath as Nn}from"node:url";function At(r){let e=process.argv[1];if(!e)return!1;try{return _t(Nn(r))===_t(e)}catch{return!1}}ce();import{execFile as cn}from"node:child_process";Mt();ce();var Dn=5e3,Se=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 Q(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.#t(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(h=>h.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())}#o(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.#t(t),this.onChange(),t}redo(e=Date.now()){let t=this.session.redo(e);return this.#t(t),this.onChange(),t}setPresence(e){this.presence=e,this.connected&&this.#r({t:P.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=Nt({t:P.STROKE,layer:e,frame:t,rect:n,rle:o,done:s});i&&this.#d(i)}suggest(e){if(!e.length||!this.connected||!this.welcomed)return[];let t=this.session.state().seq,n=e.map(o=>({opId:L(),author:this.author,parentSeq:t,...o}));return this.#r({t:P.SUBMIT,ops:n}),n}review(e,t){!this.connected||!this.welcomed||this.#r({t:P.REVIEW,author:e,verdict:t})}lease(e,t={}){this.#r({t:P.LEASE,action:e,...t})}prompt(e,{where:t=Rt.DOCUMENT}={}){if(!this.connected)return null;let n=String(e??"").trim().slice(0,2e3);if(!n)return null;let o=L();return this.#r({t:P.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:P.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:P.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:P.PING})},Dn),await this.#c()},e.onmessage=t=>this.#p(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#c(){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:P.HELLO,author:this.author,name:this.name,token:e,since:t.seq,...this.agent?{agent:!0}:{},...this.answers?{answers:!0}:{},...this.presence?.active===!1?{active:!1}:{},docId:t.doc.id})}#p(e){let t=It(e);if(t)switch(t.t){case N.WELCOME:return this.#i(t);case N.REHELLO:return this.welcomed=!1,void this.#c();case N.OPS:return this.#s(t);case N.REJECT:return this.#f(t);case N.PRESENCE:return this.participants=t.participants||[],this.onPresence(this.participants);case N.STROKE:return this.onStroke(t);case N.LEASE:return this.leases=t.leases||[],this.onLeases(this.leases);case N.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 N.SUGGESTIONS:return this.proposals=new Map((t.proposals??[]).map(n=>[n.author,{...n,ops:[...n.ops??[]]}])),this.onSuggestions(this.proposals);case N.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 N.PROMPT:return this.onPrompt(t);case N.ANSWER:return this.onAnswer(t);case N.STOP:return this.onStop(t);case N.ERROR:if(this.#m(t.message)){this.strokePreview=!1;return}return this.onLost({kind:t.fatal?"fatal":"protocol",message:t.message});default:return}}#i(e){this.you=e.you,this.suggesting=e.you?.suggest===!0,this.welcomed=!0;let t=this.session.pending();if(e.catchup.kind===Tt.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 Q(this.core,n),this.epoch++,o&&o.length){for(let s of o)this.#o(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.#l(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:P.SUBMIT,ops:t});this.presence&&this.#r({t:P.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}#s(e){let t=!1;for(let n of e.ops||[])this.#l(n),t=!0;t&&(this.#n(),this.onChange())}#l(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`})}#f(e){e.opId&&this.session.reject(e.opId),e.opId&&this.#o(e.opId,e.message),this.onLost({kind:e.kind,ops:e.opId?[e.opId]:[],message:e.message}),this.onChange()}#t(e){!e.length||!this.connected||!this.welcomed||this.#r({t:P.SUBMIT,ops:e})}#r(e){this.#d(Ot(e))}#m(e){return typeof e!="string"?!1:e===`unknown message: ${P.STROKE}`?!0:e==="unreadable message"&&Date.now()-(this._strokeSentAt??0)<2e3}#d(e){if(this.transport)try{this.transport.send(e)}catch{this.connected=!1}}close(){this.disconnect(),this.session.close()}};ce();Lt();var W=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 ee=([r,e,t])=>.3*r+.59*e+.11*t;function Ee([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 Ut={luminance:(r,e)=>ee(W(r))-ee(W(e)),hue:(r,e)=>{let[t,n]=Ee(W(r)),[o,s]=Ee(W(e));return n<.08||s<.08?n<.08&&s<.08?ee(W(r))-ee(W(e)):n<.08?-1:1:Math.round(t/20)!==Math.round(o/20)?t-o:ee(W(r))-ee(W(e))},saturation:(r,e)=>Ee(W(r))[1]-Ee(W(e))[1]};function te(r,e="hue"){let t=Ut[e]??Ut.hue;return[...r].sort(t)}qt();import{readFile as pe,writeFile as xr,mkdir as $r}from"node:fs/promises";import{access as Bn,mkdir as Wn,writeFile as Gn}from"node:fs/promises";import{dirname as Jn}from"node:path";async function ve(r,e,{overwrite:t=!1,seq:n=null}={}){let o=await Bn(r).then(()=>!0,()=>!1);if(o&&!t){let s=r.replace(/(\.png)?$/i,`-seq${n??"N"}$1`);throw new Error(`${r} already exists \u2014 a chat that showed it once will keep showing the old picture, so a preview gets a new name each time: ${s}. Pass overwrite: true only for a file nothing has shown yet.`)}return await Wn(Jn(r),{recursive:!0}),await Gn(r,e),o}function zt(r,e){let t=r.inputSchema??{},n=t.properties??{},o=Object.keys(n),s=e&&typeof e=="object"?e:{},i={};for(let[c,h]of Object.entries(s)){if(!(c in n)){let l=We(c,o),p=o.length?`${r.name} takes: ${o.join(", ")}.`:`${r.name} takes no arguments.`;throw new Error(`no such argument \`${c}\`${l?` \u2014 did you mean \`${l}\`?`:""} ${p}`)}i[c]=Hn(r.name,c,n[c],h)}let a=(t.required??[]).filter(c=>i[c]===void 0);if(a.length)throw new Error(`${r.name} needs ${a.map(c=>`\`${c}\``).join(" and ")}. It takes: ${o.join(", ")}.`);return i}function Hn(r,e,t,n){if(n==null)return n;let o=t.type;if(o==="integer"||o==="number"){let s=typeof n=="string"&&n.trim()!==""?Number(n):n;if(typeof s!="number"||!Number.isFinite(s))throw new Error(`\`${e}\` must be a number, got ${ne(n)}. ${r} takes \`${e}\` as ${o==="integer"?"an integer":"a number"}.`);if(o==="integer"&&!Number.isInteger(s))throw new Error(`\`${e}\` must be a whole number, got ${s}.`);if(t.minimum!==void 0&&s<t.minimum)throw new Error(`\`${e}\` must be at least ${t.minimum}, got ${s}.`);if(t.maximum!==void 0&&s>t.maximum)throw new Error(`\`${e}\` must be at most ${t.maximum}, got ${s}.`);return s}if(o==="boolean"){if(n==="true")return!0;if(n==="false")return!1;if(typeof n!="boolean")throw new Error(`\`${e}\` must be true or false, got ${ne(n)}.`);return n}if(o==="string"&&typeof n!="string"){if(typeof n=="number")return String(n);throw new Error(`\`${e}\` must be text, got ${ne(n)}.`)}if(o==="array"&&!Array.isArray(n))throw new Error(`\`${e}\` must be a list, got ${ne(n)}.`);if(o==="object"&&(typeof n!="object"||Array.isArray(n)))throw new Error(`\`${e}\` must be an object, got ${ne(n)}.`);if(t.enum&&!t.enum.includes(n))throw new Error(`\`${e}\` must be one of ${t.enum.map(s=>`\`${s}\``).join(", ")}, got ${ne(n)}.`);return n}function ne(r){return typeof r=="string"?`"${r.length>40?r.slice(0,37)+"\u2026":r}"`:Array.isArray(r)?"a list":r&&typeof r=="object"?"an object":String(r)}function We(r,e){let t=r.toLowerCase(),n=e.filter(i=>{let a=i.toLowerCase();return(t.startsWith(a)||a.startsWith(t))&&a!==t});if(n.length)return n.sort((i,a)=>Math.abs(i.length-t.length)-Math.abs(a.length-t.length))[0];let o=null,s=3;for(let i of e){let a=Vn(t,i.toLowerCase());a<s&&(s=a,o=i)}return o}function Vn(r,e){let t=Array.from({length:e.length+1},(n,o)=>o);for(let n=1;n<=r.length;n++){let o=t[0];t[0]=n;for(let s=1;s<=e.length;s++){let i=t[s];t[s]=Math.min(t[s]+1,t[s-1]+1,o+(r[n-1]===e[s-1]?0:1)),o=i}}return t[e.length]}import{createHash as kr}from"node:crypto";import{tmpdir as Sr}from"node:os";import{join as Ye,basename as Pe,extname as Er,resolve as vr}from"node:path";function Ge(r){try{return/^(localhost|127\.0\.0\.1|\[::1\]|::1)$/.test(new URL(S(r)).hostname)}catch{return!1}}function Ft(r,{publicServer:e=ie}={}){let t=S(r);return e&&r===e?`nothing is answering at ${t} \u2014 this machine is offline, or the site is down`:Ge(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{appendFile as lr,mkdir as Vt,readFile as dr,writeFile as He}from"node:fs/promises";import{dirname as Kt}from"node:path";import{appendFile as Kn,mkdir as Zn,readFile as Xn}from"node:fs/promises";import{homedir as Yn}from"node:os";import{join as Je}from"node:path";import{randomUUID as Qn}from"node:crypto";function Jt(){return process.env.PIXIE_TRACES||Je(process.env.XDG_CONFIG_HOME||Je(Yn(),".config"),"sprite","traces")}var Bt=r=>Je(Jt(),`${String(r).replace(/[^A-Za-z0-9_-]/g,"_")}.jsonl`),er=new Set(["pull_source"]),je=r=>er.has(r.tool)?null:r.args?.job??null,Wt=4e3,Gt=120,tr=2e3,nr=new Set(["note","unfinished","takes","intent","finished","run","label"]);function X(r,e=0,t=Gt){if(r==null||typeof r=="number"||typeof r=="boolean")return r;if(typeof r=="string")return r.length>t?`${r.slice(0,t-1)}\u2026`:r;if(Array.isArray(r)){if(r.length&&r.every(n=>typeof n=="string")){let n=new Set(r.map(o=>o.length));return`${r.length}\xD7${n.size===1?r[0].length:`${Math.min(...n)}\u2013${Math.max(...n)}`}`}return`[${r.length}]`}if(typeof r=="object"){let n=Object.keys(r),o=n.length<=8&&n.every(i=>r[i]==null||typeof r[i]!="object");if(n.length>32||e>0&&!o)return`{${n.length}}`;let s={};for(let i of n)s[i]=X(r[i],e+1,nr.has(i)&&typeof r[i]=="string"?tr:Gt);return s}return String(r)}function rr(r){if(r&&typeof r=="object"&&Array.isArray(r.content)){let e=r.content.find(n=>n.type==="image"),t=r.content.find(n=>n.type==="text")?.text??"";if(r.isError)return{ok:!1,error:X(t)};if(e)return{ok:!0,result:`image \u2014 ${X(t)}`};try{return{ok:!0,result:X(JSON.parse(t))}}catch{return{ok:!0,result:X(t)}}}return{ok:!0,result:X(r)}}var _e=class{constructor(){this.session=Qn(),this.entries=[],this.next=1,this.open=new Map,this.starts=new Map,this.idleFrom=1,this.lead=new Map}async record(e,t,n){let o=Date.now(),s={i:this.next++,t:new Date(o).toISOString(),tool:e,args:X(t??{})};try{let i=await n();return Object.assign(s,rr(i),{ms:Date.now()-o}),this.#e(s),i}catch(i){throw Object.assign(s,{ok:!1,error:X(i?.message??String(i)),ms:Date.now()-o}),this.#e(s),i}}#e(e){this.entries.push(e),this.entries.length>Wt&&this.entries.splice(0,this.entries.length-Wt)}began(e){if(!e)return;let t=this.entries.at(-1)?.i??this.next;this.open.size===0&&this.idleFrom<t&&this.lead.set(e,this.idleFrom),this.open.set(e,t),this.starts.set(t,e)}all(e=500){let t=this.entries.length,n=new Map,o=null;for(let s of this.entries){let i=this.starts.get(s.i);i&&(o=i),n.set(s.i,je(s)??o??null),s.tool==="finish_job"&&(je(s)??o)===o&&(o=null)}return{calls:t,dropped:Math.max(0,t-e),entries:this.entries.slice(-e).map(s=>n.get(s.i)?{...s,job:n.get(s.i)}:s)}}finished(e){let t=e??(this.open.size===1?[...this.open.keys()][0]:null);if(!t||!this.open.has(t))return[];let n=this.open.get(t);this.open.delete(t);let o=this.lead.get(t)??n;return this.lead.delete(t),this.open.size===0&&(this.idleFrom=this.next),this.entries.filter(s=>s.i>=o&&(je(s)==null||je(s)===t)&&(this.starts.get(s.i)??t)===t).map(s=>s.i<n?{...s,between:!0}:s)}async keepFor(e,t,n){if(!e||!n.length)return null;let o=n.map(s=>JSON.stringify({...s,job:t,document:e,session:this.session})).join(`
|
|
3
|
-
`);try{return await
|
|
4
|
-
`),null}catch(s){return s.message}}async earlier(e){let t=[];for(let n of new Set([...e].filter(Boolean))){let o=await
|
|
5
|
-
`))if(s.trim())try{let i=JSON.parse(s);i.session!==this.session&&t.push({...i,earlier:!0})}catch{}}return t.sort((n,o)=>String(n.t).localeCompare(String(o.t)))}documents(){let e=new Set;for(let t of this.entries)typeof t.args?.document=="string"&&e.add(t.args.document),t.result&&typeof t.result=="object"&&typeof t.result.document=="string"&&e.add(t.result.document);return e}};import{readFileSync as or}from"node:fs";import{execFileSync as sr}from"node:child_process";import{fileURLToPath as ir}from"node:url";import{dirname as ar}from"node:path";var Ht="0.7.8";function cr(){let r=new URL("./version.json",import.meta.url);try{let e=JSON.parse(or(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 he=Ht??cr();function hr(){if(Ht)return null;let r=ar(ir(import.meta.url)),e=(...t)=>sr("git",t,{cwd:r,stdio:["ignore","pipe","ignore"],timeout:2e3}).toString().trim();try{let t=e("rev-parse","--short=12","HEAD");return t?e("status","--porcelain","--",".","../../edge/src","../../core/js")?`${t}-dirty`:t:null}catch{return null}}var le=hr();var Ae={type:"string",description:"#rrggbb"};async function ur(r,e){let t=await dr(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 pr(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 pr({w:r,h:e,pixels:t}){let n={},o=new Map,s=33,i=h=>{let l=o.get(h);if(l)return l;for(;;)if(s===34||s===46||s===92||s===95||s===127)s+=1;else if(s>=128&&s<161)s=161;else if(s>=55296&&s<57344)s=57344;else break;return l=String.fromCodePoint(s),s+=1,o.set(h,l),n[l]=h,l},a=h=>h.toString(16).padStart(2,"0"),c=[];for(let h=0;h<e;h++){let l="";for(let p=0;p<r;p++){let u=(h*r+p)*4,d=t[u+3];if(d===0){l+=".";continue}let m=d===255?`#${a(t[u])}${a(t[u+1])}${a(t[u+2])}`:`#${a(t[u])}${a(t[u+1])}${a(t[u+2])}${a(d)}`;l+=i(m)}c.push(l)}return{rows:c,colors:n}}var _={type:"string",description:"The job this call belongs to. May be omitted when exactly one job is open."},de={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. A share link (`https://\u2026/s/<document>_<snapshot>`) or an editor link (`\u2026/d/<document>`) names its document too, so a link the user pasted can be passed straight in. In a local editor a 0-based tab index works too."},Zt={type:"integer",description:"A PAST state of the document, rebuilt from the room's history: the sequence number get_info reports and every draw_art result carries. Omit for now. This is how to compare a pass with the one it replaced \u2014 the check that catches a revision that is locally better and worse as a picture: read or render the seq before your change, render now, and stack them small and in grays (references/judge.py). A read, never a restore; the document does not move."};function oe(r){let e=String(r).trim();if(!/[:/]/.test(e))return e;let t=e;if(/^[a-z][a-z0-9+.-]*:\/\//i.test(e))try{t=new URL(e).pathname}catch{return e}let n=/(?:^|\/)s\/([A-Za-z0-9_-]{1,128})(?:\/|$)/.exec(t);if(n)return n[1].split("_")[0];let o=/(?:^|\/)d\/([A-Za-z0-9-]{1,64})(?:\/|$)/.exec(t);return o?o[1]:e}var Re=r=>r==null||r===""?null:/^\d+$/.test(String(r))?Number(r):r,fr=r=>({isError:!0,content:[{type:"text",text:r}]}),re=r=>({content:[{type:"text",text:typeof r=="string"?r:JSON.stringify(r)}]}),Xt=(r,e)=>({content:[{type:"image",data:Buffer.from(r).toString("base64"),mimeType:"image/png"},{type:"text",text:e}]}),D={w:240,h:126},Yt={w:80,h:42},mr="40\xD721, 80\xD742, 120\xD763, 160\xD784, 240\xD7126, 480\xD7252, 600\xD7315",wr="40\xD740, 50\xD750, 80\xD780, 100\xD7100",yr="60\xD720, 75\xD725, 150\xD750, 300\xD7100";function Ve(r,e){if(!r||!e)return null;let t=r/e,n=1200/630;if(Math.abs(t-n)<.15){let o=Math.round(r/n);return`${r}\xD7${e} is ${t.toFixed(2)}:1; the link-card shape at this width is ${r}\xD7${o} \u2014 resize_canvas if it is meant to be posted.`}return Math.abs(t-1)<.1?`${r}\xD7${e} is nearly square; ${Math.max(r,e)}\xD7${Math.max(r,e)} would be a profile picture.`:Math.abs(t-3)<.25?`${r}\xD7${e} is nearly 3:1; ${r}\xD7${Math.round(r/3)} would be a profile banner.`:null}function Te(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 gr(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 br=[{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. The size is `w` and `h` (not width/height), as everywhere here.",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:D.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: ${D.w}\xD7${D.h} \u2014 1.91:1, the ratio link cards crop to, so a piece drawn on it posts edge to edge, and big enough for a subject with parts (a dish with its slice and garnish, a figure with props, a room). ${Yt.w}\xD7${Yt.h} when the user asks for something SIMPLE \u2014 "simple", "quick", "small", "minimal", "a little", "basic", "rough", "doodle", "icon-like" \u2014 bold shapes and big pixels. Any multiple of 40\xD721 fills the card (${mr}: examples, not limits). A PROFILE PICTURE is square and cropped to a circle \u2014 keep the subject inside it: ${wr}. A BANNER is 3:1: ${yr}. 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:D.h,description:"Height in pixels. See w."},palette:{type:"array",items:Ae,description:"Optional starting palette. Kept in ramp order \u2014 by hue, then dark to light \u2014 not the order given, so a script that indexes the document's palette by position will be off: map by hex."},like:{type:"string",description:'Another document \u2014 its id, name or link \u2014 whose size and palette this one starts with: the next sheet of a series, "one like this". `w`, `h` and `palette` given here still win. It saves the mechanical part only; measure the reference\'s per-object budget anyway (analyze_reference.py --objects).'},intent:{type:"string",description:'One line on what you are making, including the VIEW. Shown while you work. The default view is three-quarter: the camera above and to one side, so an object shows its top and two sides and a scene has depth \u2014 unless the user says "flat", "side view", "front", "profile", "top-down", "icon" or "tile", which mean what they say and no perspective at all.'},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","inktober"],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; "inktober" for today\'s Inktober prompt, which runs the 31 days of October. The name comes from the prompt 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 prompt before drawing.'}},required:["intent"]},run:async(r,e)=>{let t=r.name?gr(r.name):null,n=r.w??null,o=r.h??null;if(r.challenge){let c=await e.challenge(r.challenge);if(t??=c.theme,n===null&&o===null&&c.sizes?.length){let h=c.sizes[Math.floor((c.sizes.length-1)/2)],[l,p]=h.split(/[x×]/).map(Number);l&&p&&([n,o]=[l,p])}}if(!t)throw new Error("new_sprite needs a name, or a challenge to take one from");let s=r.palette,i=null;if(r.like){let c=await e.info({document:oe(r.like)});if(!c.document)throw new Error("`like` reads another document, which is a room thing \u2014 in a local editor, give w, h and palette yourself");n??=c.w,o??=c.h,s??=c.palette,i=`${c.document} ("${c.name}"): ${r.w==null&&r.h==null?`its ${c.w}\xD7${c.h}`:"the size you gave"}, ${r.palette?"the palette you gave":`its ${c.palette.length} palette entries`}`}let a=await e.newSprite({name:t,w:n??D.w,h:o??D.h,intent:r.intent,palette:s,folder:r.folder??null,challenge:r.challenge??null});return i?{...a,like:i}:a}},{name:"challenges",description:`The standing prompts: Pixel Dailies sets a theme every day ("Today's theme is #Gateway"), Weekly Pixels a word every week with sizes, and Inktober a prompt for each of the 31 days of October. When the user says "the sprite of the day", "today's theme", "the daily", "the weekly", "this week's word", "Inktober" or "today's Inktober prompt" \u2014 or names any of the three \u2014 this is what they mean: read it here rather than asking them. The server reads the accounts and keeps the answer, and holds Inktober's published list; nothing is fetched from Bluesky or inktober.com by you. An entry whose \`theme\` is null is one nobody has set \u2014 Inktober's is null for the other eleven months.`,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. With no `document` and no job open, it picks up the document you last finished IN THIS SESSION \u2014 one more pass on the same sprite is begin_job({ intent }); a document from an earlier session has to be named (list_documents has them).",inputSchema:{type:"object",properties:{document:{type:"string",description:"Which document: the id, or the exact name when only one document has it. Omit, with no job open, for the one you last finished in this session; a document from an earlier one has to be named."},intent:{type:"string"},layer:{type:"string",description:"Restrict the claim to one layer, leaving the rest free. Rooms only."}},required:["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 \u2014 and carries `trace`: every call of this job, as the server saw it \u2014 and, marked `between: true`, any made with no job open since the last finish. When the task asked for a report back, do NOT assemble trace.jsonl out of these: `get_info({ trace: true, file: "\u2026/trace.jsonl" })` writes the whole session to that file itself, and three reports that retyped the record by hand lost something each \u2014 two lost `versions` and one abbreviated every result \u2014 which is a sibling of `trace` here, travels inside this call\'s own trace entry, and is what feedback.py stamps the report with. Without it a current server is labeled ancient and every friction item struck.',inputSchema:{type:"object",properties:{job:_,note:{type:"string",description:`One line on what shipped \u2014 "8 roses, hooked thorns, ivy; crown at CY 69" \u2014 which becomes the history's line for this job in place of the intent written before the work. Optional; up to 200 characters.`},trace:{type:"string",description:"Also APPEND this job's calls to this path as JSONL, as the server saw them. Seldom needed now: every finish keeps its job's calls per document on the server's side whether or not this is passed, and `get_info({ document, trace: true, file })` writes them back \u2014 earlier sessions' included \u2014 into a report's trace.jsonl. Pass a path only for a file of your own beside that."}}},run:async(r,e)=>({...await e.finishJob({job:r.job,note:r.note??""}),versions:{mcp:he,...le?{commit:le}:{}}})},{name:"push_source",description:"Push the generator behind this job \u2014 the script(s) that made the sprite \u2014 so the next agent (or you, tomorrow) can pick the work up rather than redraw it from the pixels. Stored beside the job's mark in the document's version history, with the seq it reproduces; READABLE BY THIS ACCOUNT ONLY, never by anyone else in the room. Push the source, not its output: the .py, and anything it imports that is yours; not the grid JSON or a PNG, which it regenerates (text, \u2264 16 files, \u2264 96 KiB). `run` is the exact command; `takes` what it needs that is not in the push (a reference from Commons: its URL; the skill's references/ helpers by name); `unfinished` what you would do next. Push before finish_job, and again after a real change.",inputSchema:{type:"object",properties:{files:{type:"array",items:{type:"string"},description:'Paths on this machine to the files to push. The basename is kept, the directory is not \u2014 so two generators both called `mint.py` need `{ from: "<path on disk>", path: "<name to push it as>" }` instead of a plain path.'},run:{type:"string",description:'The command that regenerates the sprite from these files, e.g. "python3 buche.py .context/yule/solo.json && draw_art({ file })".'},takes:{type:"string",description:"What it needs that is not in the push: reference URLs, the references/ helpers it imports, a palette. Empty if nothing."},unfinished:{type:"string",description:"What is not done, or what you would change next. The next agent reads this first."},job:_},required:["files","run"]},run:(r,e)=>e.pushSource({job:r.job,files:r.files,run:r.run,takes:r.takes??"",unfinished:r.unfinished??""})},{name:"pull_source",description:"What this account pushed on a document: with no `job`, the list (job, seq, run, unfinished, file names) \u2014 with a `job`, its files written to disk under `into` (default .context/source/<job>/) with how to run them, what they take and what was unfinished. Call it before redrawing a sprite an agent made: if there is a push, start from it. Only your own pushes come back; a mark with `source: true` in the history that is not listed here is somebody else's. READ THE FILES BEFORE RUNNING THEM, and run them in the workspace with nothing the task does not need \u2014 they are code somebody pushed.",inputSchema:{type:"object",properties:{document:de,job:{type:"string",description:"The job whose push to write to disk. Omit to list."},into:{type:"string",description:"Directory to write the files into. Default .context/source/<job>/. A file that exists with different content is not overwritten."}}},run:(r,e)=>e.pullSource({document:r.document??null,job:r.job??null,into:r.into??null})},{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`. A key is any single character except "." and " " \u2014 letters, digits, punctuation, any Unicode symbol \u2014 so a generator that runs past 62 colors keeps going. "." 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. With a low count, a value between two colors is a CHECKER of the two (an ordered dither), planned from the first shading pass, never an extra color. 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. Either is the whole canvas or, with `rect`, one region of it: a pass that touched only the fire base sends the same file and names the rect, and the history says "fire base", not everything. The reply says how many pixels are not what they were (`changed`; `inked` is how many the grid painted, when the two differ \u2014 a full-canvas redraw that moved a corner says 180 and 30240) and which colors joined the palette, so growth is never silent (with `palette: "replace"` nothing joins: the palette becomes this drawing\'s colors plus whatever the other layers are drawn in, and the reply says so). 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. A PNG\'s colors are used as they are \u2014 not quantized to the palette; each distinct color joins it (or with `palette: "replace"`, becomes it) \u2014 so read_art({ png }) \u2192 edit \u2192 draw_art({ file }) round-trips exactly. Anything given in the call as well wins.'},rows:{type:"array",items:{type:"string"}},colors:{type:"object",additionalProperties:{oneOf:[Ae,{type:"null"}]}},x:{type:"integer"},y:{type:"integer"},center:{type:"boolean"},clear:{type:"boolean",description:"Erase the cel first."},rect:{type:"object",description:"With `file`, JSON or PNG: replace only this region `{x, y, w, h}` of the canvas from a full-canvas file \u2014 a one-object change is not a whole-canvas redraw. With `clear`, only the region is emptied first.",properties:{x:{type:"integer"},y:{type:"integer"},w:{type:"integer"},h:{type:"integer"}}},palette:{type:"string",enum:["add","replace","keep"],description:"What this drawing's colors do to the document palette: `add` (default) joins them; `replace` makes them the whole palette \u2014 for a generated piece redrawn several times, so the palette does not pile up, and on a multi-layer document it keeps what the OTHER layers are drawn in too, since the palette is the document's; `keep` leaves the palette as it is \u2014 for a pass the user may send back (a glow tried three ways left 120 tints behind), or a generator whose blends are not colors anybody will pick."},layer:{type:"string",description:"Layer name or index. Defaults to the top one. A name no layer has makes that layer, on top \u2014 or under `below`."},below:{type:"string",description:"When `layer` makes a new layer: the existing layer (name or index) it goes directly under. A backdrop drawn after the subject goes under it, not into Layer 1."},frame:{type:"integer",description:"Frame index. Defaults to the first."},label:{type:"string",description:"What this shows as in the undo history."},job:_}},run:async(r,e)=>{let t=r.file?{...await ur(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,below:Re(t.below),frame:t.frame??0,label:t.label,palette:t.palette??"add",rect:t.rect??null})}},{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. SILHOUETTE FIRST: at 32px a front-on animal is any animal; when the subject could go several ways, draw 2-3 rough silhouettes as frames, look with render_animation, keep the one that reads, then add detail \u2014 three candidates cost barely more than one.",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:[Ae,{type:"null"}]}},fps:{type:"number"},layer:{type:"string"},label:{type:"string"},job:_},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: with no job open they read the document last finished in this session, or the `document` named. The fallback is this process's memory, so the first call of a new session names its document. `seq` reads a PAST state instead of now \u2014 the grid as it stood at that document sequence, rebuilt from the room's history \u2014 which is how to get the pixels of the pass you replaced.",inputSchema:{type:"object",properties:{document:de,layer:{type:"string"},frame:{type:"integer"},trim:{type:"boolean",description:"Return only the drawn area. Rooms only."},seq:Zt,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."},png:{type:"string",description:"Write the document to this path as a PNG, one image pixel per sprite pixel \u2014 the composite, or one `layer` \u2014 so an image tool, `references/compare.py` or `references/scene.py guides` can open the room's truth directly. A local editor writes the composite only."},job:_}},run:async(r,e)=>{if(r.png){if(!e.readPng)throw new Error("a PNG of the document is a room thing \u2014 in a local editor, read_art({ file }) and render it");let i=await e.readPng({document:r.document,job:r.job,layer:r.layer??null,frame:r.frame??0,seq:r.seq??null});return await He(r.png,i.png).catch(a=>{throw new Error(`could not write ${r.png}: ${a.message}`)}),{document:i.document,...i.at!==void 0?{at:i.at}:{},png:r.png,size:`${i.w}\xD7${i.h}`,layer:r.layer??"composite"}}let t=await e.readArt({document:r.document,job:r.job,layer:r.layer,frame:r.frame??0,trim:r.trim??!1,seq:r.seq??null});if(!r.file)return t;let{rows:n,colors:o,...s}=t;return await He(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. Needs no job: with none open it renders the document last finished in this session, or the `document` named \u2014 the first look of a new session names it, the fallback being this process's. 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, or `rects` to put several objects side by side in ONE image, which is what the per-object audit wants (an agent wrote its own tiler rather than make eight calls, and spent the audit looking at its own render instead of the room's). The text beside the image says exactly what was rendered, at what scale, and at which document seq \u2014 the room's confirmed state, never a queued or stale mirror: a render waits for the room to confirm what you sent and is refused while the connection is down. WHEN MATCHING A REFERENCE, pass `reference` (a path on this machine \u2014 PNG, or a JPEG on a Mac \u2014 or a document's id or link, so \"like this sheet\" is one call): the reference is brought to the canvas grid and shown on the LEFT of the render, same rect, same scale \u2014 the only comparison that catches an object built nothing like the original. A reference that is not the shape of the canvas is fitted whole (black where it does not reach) \u2014 into the `rect` when one is given, so one icon beside the one cell it was drawn into is the icon filling that cell; `referenceRect` brings one part of it \u2014 the object you are checking, in reference pixels \u2014 into the `rect` instead (into the whole canvas when there is no `rect`), fitted by the side that limits it, so the two sides are one object at one scale; the caption gives that scale in reference pixels per canvas pixel. That is how to compare one thing up close. To give rects in canvas pixels instead, pass the reference already resampled to the canvas's size (analyze_reference.py --resample): a reference the canvas's shape maps 1:1, so `referenceRect` is then the same numbers as `rect`. Judge per object from that, never from the whole image or from memory \u2014 and judge construction, not resemblance: count the faces, name the light, say where the shadow falls, on theirs and on yours. `values` renders both as grays \u2014 the squint test for a value plan and a focal point. ON A TRANSPARENT CANVAS pass `background` (\"checker\" or a #rrggbb): alpha is shown over white, so a white tip or a pale sparkle is invisible and cannot be judged at all.",inputSchema:{type:"object",properties:{document:de,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"}}},rects:{type:"array",description:"Several windows instead of one, tiled left to right into a single image with a magenta line between them \u2014 the per-object audit in one call rather than one call per object. Each crop is NUMBERED in the magenta strip under it, 1 upwards, so a 32-cell sheet audit can name the cell it means instead of counting along the row; give each one a `label` and the caption says which number is which. Not with `reference`.",items:{type:"object",properties:{x:{type:"integer"},y:{type:"integer"},w:{type:"integer"},h:{type:"integer"},label:{type:"string",description:'What this crop is \u2014 "cruller", "head". Said in the caption against the number drawn under the crop; a name does not fit on an icon-sized tile.'}}}},reference:{type:"string",description:"Path to a reference image on this machine \u2014 a PNG anywhere, a JPEG on a Mac (converted on the way; elsewhere convert it first) \u2014 or a Pixie document: its id, its name, or a link to it (`/d/\u2026` or a share `/s/\u2026`), read as its composite at the room's confirmed seq. Shown beside the render at the same crop and scale, fitted whole to the canvas grid and centered \u2014 or the part of it `referenceRect` names."},referenceRect:{type:"object",description:"The part of the reference to bring to the grid, in reference pixels: `{x, y, w, h}`. Omit for the whole picture. With a `rect`, this region is fitted into that rect (centered, black where it does not reach); pick the region that holds the object `rect` shows, at the same proportions, and the two sides are that object at one scale.",properties:{x:{type:"integer"},y:{type:"integer"},w:{type:"integer"},h:{type:"integer"}}},values:{type:"boolean",description:"Render as values only (grayscale) \u2014 the render, and the reference when one is given. A picture that is mid-tone everywhere shows it here and nowhere else."},background:{type:"string",description:'What the transparent pixels show as: `"checker"` or a `#rrggbb`. Default is nothing \u2014 the alpha arrives as alpha, which a viewer paints white, so white or near-white art on a transparent canvas is INVISIBLE in the preview. Pass this whenever the canvas is transparent and the art has light pixels: a white tip, a sparkle, snow, a highlight. A sparkle was once shipped unseen for want of it.'},file:{type:"string",description:"Also write the image to this path \u2014 exactly what is returned: the rect, the scale, the reference beside it, the values. For `final.png` and the crops a report keeps, for any check that wants the room's picture rather than a local re-render, and for showing the user. A path that already exists is REFUSED unless `overwrite` is true: a chat that has shown a path once keeps showing the old picture under it, so a preview meant to be seen gets a new name every time (`preview-<seq>.png`; the caption says the seq)."},overwrite:{type:"boolean",description:"Let `file` replace a file that exists. Only for a file nothing has shown yet \u2014 a report's `final.png` being redone \u2014 never for a preview embedded in a reply."},seq:Zt,job:_}},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,rects:r.rects??null,reference:r.reference??null,referenceRect:r.referenceRect??null,values:r.values===!0,background:r.background??null,file:r.file??null,overwrite:r.overwrite===!0,seq:r.seq??null});return Xt(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:de,columns:{type:"integer",default:4},scale:{type:"integer",default:2},job:_}},async run(r,e){let t=await e.sheet({document:r.document,job:r.job,columns:r.columns??4,scale:r.scale??2});return Xt(t.png,t.caption)}},{name:"add_layer",description:"Add a layer above the others \u2014 or directly `below` or `above` one that exists, for a backdrop put in after the subject. 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"},below:{type:"string",description:"A layer (name or index) to put the new one directly under \u2014 the cloth under the drumstick."},above:{type:"string",description:"A layer (name or index) to put the new one directly over."},job:_}},run:(r,e)=>e.addLayer({job:r.job,name:r.name,below:Re(r.below),above:Re(r.above)})},{name:"rename_layer",description:"Give a layer a new name. A layer is found by its name \u2014 draw_art({ layer }), read_art({ layer }) \u2014 so a name that has drifted (a room once logged the author over agents' layer names) is put back here rather than worked around.",inputSchema:{type:"object",properties:{layer:{type:"string",description:"The layer as it is called now, or its index (0 is the bottom)."},name:{type:"string",description:"What it is called from now on. Refused if another layer has it."},job:_},required:["layer","name"]},run:(r,e)=>e.renameLayer({job:r.job,layer:Re(r.layer),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:_}},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:Ae,minItems:1},prune:{type:"boolean",description:"Keep only the colors some pixel uses. `colors` is ignored."},job:_}},run:(r,e)=>!r.prune&&!r.colors?.length?fr("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:_},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:_}},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. `center: true` instead KEEPS the canvas and moves the art to the middle of it \u2014 "make it horizontally and vertically centered", which arrives on every transparent sprite; `get_info`\'s `bbox` is where the art is now. Both measure the art on EVERY layer and frame, because the crop is of the document and what falls outside it is gone.',inputSchema:{type:"object",properties:{margin:{type:"integer",default:0},center:{type:"boolean",description:"Do not crop: keep the canvas and move the drawn pixels to its center. `margin` is ignored."},job:_}},run:(r,e)=>e.trim({job:r.job,margin:r.margin??0,center:r.center===!0})},{name:"resize_canvas",description:"Change the canvas size, keeping the art \u2014 the fix for a sprite made at the wrong size, in place, on the same document. `anchor` says where the old canvas sits on the new one: `center` (default), `top-left`, `top`, `top-right`, `left`, `right`, `bottom-left`, `bottom`, `bottom-right`. Never make a second document for a size change. The old art is kept at `anchor`; pass `clear: true` to start the new size blank in the same call \u2014 otherwise the room shows the old picture in the corner until the next full draw_art. `clear` empties EVERY layer and frame: a resize is of the document, so a blank canvas is a blank one.",inputSchema:{type:"object",properties:{w:{type:"integer",minimum:1,maximum:1024},h:{type:"integer",minimum:1,maximum:1024},anchor:{type:"string"},clear:{type:"boolean",description:"Erase everything after resizing, so a full redraw at the new size does not follow an old picture in the corner."},job:_},required:["w","h"]},run:(r,e)=>e.resizeCanvas({job:r.job,w:r.w,h:r.h,anchor:r.anchor??"center",clear:r.clear===!0})},{name:"get_info",description:"What is in a document \u2014 size, layers, frames, palette \u2014 and who else is looking at it; `layers: true` makes each layer an object with its pixel count and bounding box, which is how to check a layer split without reading every layer back; `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. `trace: true` adds this session's whole call record \u2014 every call, not one job's, the same entries finish_job hands back \u2014 which is what a report's Trace section is written from when it spans several jobs. Per process: a new session starts an empty one. With `file` as well, the record is written there as JSONL and the result is a summary: that is `trace.jsonl`, in the server's own words rather than retyped through you. The result carries `versions` \u2014 which pixie-mcp this was \u2014 so the file stamps itself and a report never has to.",inputSchema:{type:"object",properties:{document:de,job:_,trace:{type:"boolean",description:"Add every call this session has made, oldest first, as `trace`, each entry carrying the `job` it belongs to \u2014 for a report over more than one job. Earlier sessions' FINISHED jobs on the same documents (this one's, or the `document` named) come first, marked `earlier: true`: every finish_job keeps its calls per document, so a report written after a restart still has the work."},layers:{type:"boolean",description:"Make `layers` an object per layer instead of a name \u2014 `{ name, pixels, bbox }`, bottom to top, counted over every frame. What verifies a layer split without reading each one back and stacking it: a pass that landed nowhere is `pixels: 0`, and a layer reaching further than the one above it is the trim you are about to regret."},file:{type:"string",description:"With `trace: true`: write the trace to this path as JSONL \u2014 one entry per line, which is exactly what a report's `trace.jsonl` is \u2014 and return a summary instead of the entries. The file is the server's copy: a trace that comes back through the model and is typed out again loses the long fields (a report's was \"the payload re-emitted, with the repeated document metadata abbreviated\"). Replaced if it exists, because a report gets rebuilt."}}},run:async(r,e)=>{if(r.file&&r.trace!==!0)throw new Error("`file` here writes the trace \u2014 pass trace: true with it. For the document as a picture or a grid, that is render_preview({ file }) and read_art({ file }).");let t=await e.info({document:r.document,job:r.job,layers:r.layers===!0});return r.trace===!0?{...t,versions:{mcp:he,...le?{commit:le}:{}}}:t}},{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 re(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: in a local editor its tab, in a room the connection and any job you hold on it. 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 Qt(r){let e=r.trace??=new _e,t=n=>n&&typeof n=="object"&&Array.isArray(n.content);return br.filter(n=>r.supports(n.name)).map(n=>({name:n.name,description:n.description,inputSchema:n.inputSchema,run:async o=>{let s=await e.record(n.name,o,()=>{let i=zt(n,o);return typeof i.document=="string"&&(i.document=oe(i.document)),n.run(i,r)});if((n.name==="new_sprite"||n.name==="begin_job")&&s?.job&&e.began(s.job),n.name==="finish_job"&&s&&typeof s=="object"&&!t(s)){let i=e.finished(s.job??o?.job),a=await e.keepFor(s.document,s.job??o?.job,i);if(a&&(s.traceUnkept=`this job's calls could not be kept for a later report: ${a}`),typeof o?.trace=="string"&&o.trace){let c=String(o.trace);return await Vt(Kt(c),{recursive:!0}).catch(()=>{}),await lr(c,`${i.map(h=>JSON.stringify(h)).join(`
|
|
2
|
+
var V=(r,e)=>()=>(r&&(e=r(r=0)),e);function xt(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 kt=V(()=>{"use strict"});var Y,Pn,In,jo,St=V(()=>{"use strict";Y="[A-Za-z0-9_-]{1,64}",Pn="token|members|link|slow|suggest|project",In=[{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/${Y}/(${Pn})$`)},{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/${Y}(/(thumbnail\\.png|export|duplicate|rename|restore))?$`)},{owner:"library",re:new RegExp(`^/documents/${Y}/history/at/\\d+/thumbnail\\.png$`)},{owner:"library",re:new RegExp("^/wall\\.png$")},{owner:"document",re:new RegExp(`^/documents/${Y}/history(/at/\\d+|/\\d+|/source(/[A-Za-z0-9_-]{1,64})?)?$`)},{owner:"document",re:new RegExp(`^/documents/${Y}/comments(/[A-Za-z0-9]{1,40})?$`)},{owner:"document",re:new RegExp(`^/documents/${Y}/snapshots(/enabled|/[A-Za-z0-9_-]{1,128})?$`)},{owner:"document",re:new RegExp(`^/favourites(/order|/(document|project)/${Y})?$`)}],jo=In.map(({owner:r,re:e})=>({owner:r,source:e.source}))});var Be,Ge=V(()=>{"use strict";St();Be=["read","draw","create","duplicate","organize"]});async function Et(r){let e=new TextEncoder().encode(String(r));return xt(new Uint8Array(await crypto.subtle.digest("SHA-256",e)))}var Io,vt=V(()=>{"use strict";kt();Ge();Io=120*1e3});function U(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 J(r){return r==null?{at:0}:typeof r=="number"?{at:r}:{frame:r}}var zn,qn,Bn,Ee,te,We,de=V(()=>{"use strict";zn=new TextEncoder,qn=new TextDecoder,Bn=1;Ee=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#o(){return new Uint8Array(this.#n.memory.buffer)}call(e,t={}){let n=zn.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.#o.set(n,o),s=this.#n.sprite_dispatch(o,n.length),!s)throw new Error("sprite-core: dispatch returned nothing");let i=this.#o,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===Bn)return l;let h=JSON.parse(qn.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 te(this,o)}openRoom(e,{seq:t=0}={}){let{h:n}=this.call("room.new",{doc:e,...t>0?{seq:t}:{}});return new We(this,n)}},te=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,...J(t),...n})}cel(e,t){return this.#e("session.cel",{layer:e,...J(t)})}layerImage(e,t){return this.#e("session.layerImage",{layer:e,...J(t)})}tileCel(e,t){return this.#e("session.tileCel",{layer:e,...J(t)})}tile(e,t){return this.#e("session.tile",{tileset:e,index:t})}composite(e,{include:t="export"}={}){return this.#e("session.composite",{...J(e),include:t})}png(e,{scale:t=1,rect:n=null}={}){return this.#e("session.png",{...J(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??{},...J(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??{},...J(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,...J(a)})}close(){this.#e("session.free")}},We=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})}region(e,t,n){return this.#e("room.region",{layer:e,frame:t,rect:{x:n.x??0,y:n.y??0,w:n.w,h:n.h}})}close(){this.#e("room.free")}}});function Pt(r){return JSON.stringify(r)}function Mt(r){let e=r.rle instanceof Uint8Array?r.rle:null,t=new TextEncoder().encode(JSON.stringify({...r,rle:void 0}));if(t.length>Gn)return null;let n=new Uint8Array(3+t.length+(e?.length??0));return n[0]=It,n[1]=t.length&255,n[2]=t.length>>8&255,n.set(t,3),e&&n.set(e,3+t.length),n}function Wn(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 Ct(r){if(typeof r!="string"){let e=Wn(r);if(!e||e.length<3||e[0]!==It)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 N,Tt,P,Nt,It,Gn,Ut=V(()=>{"use strict";N={HELLO:"hello",SUBMIT:"submit",PRESENCE:"presence",STROKE:"stroke",LEASE:"lease",REVIEW:"review",PING:"ping",PROMPT:"prompt",ANSWER:"answer",STOP:"stop"},Tt={DOCUMENT:"document",LIBRARY:"library"},P={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"},Nt={OPS:"ops",SNAPSHOT:"snapshot"};It=157,Gn=65535});function Lt(r){return U().toLowerCase().slice(0,16)}var Dt=V(()=>{"use strict";de()});function Hn(r){let e=[];if(!r.length)return new Uint8Array;let t=r[0],n=r[1],o=r[2],s=r[3],i=0,a=()=>{let c=i;do{let l=c&127;c>>>=7,e.push(c?l|128:l)}while(c);e.push(t,n,o,s)};for(let c=0;c<r.length;c+=4)r[c]===t&&r[c+1]===n&&r[c+2]===o&&r[c+3]===s?i++:(a(),t=r[c],n=r[c+1],o=r[c+2],s=r[c+3],i=1);return a(),new Uint8Array(e)}function Vn(r){let e="";for(let n=0;n<r.length;n+=32768)e+=String.fromCharCode.apply(null,r.subarray(n,n+32768));return btoa(e)}function Kn(r){return Vn(Hn(r))}function zt(r,e,t,n){return{t:"paintRegion",layer:r,frame:e,rect:{x:t.x,y:t.y,w:t.w,h:t.h},rle:Kn(n)}}var qt=V(()=>{"use strict"});import{readFile as ao}from"node:fs/promises";import{readFile as En,writeFile as vn,mkdir as jn,chmod as _n}from"node:fs/promises";import{readFileSync as An}from"node:fs";import{homedir as Rn,hostname as On}from"node:os";import{join as ze}from"node:path";import{execFile as Tn}from"node:child_process";var j=r=>String(r??"").replace(/\/+$/,"").replace(/^ws/,"http"),at=r=>String(r??"").replace(/\/+$/,"").replace(/^http/,"ws");async function ct(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 lt(r,{timeoutMs:e=3e3,fetchImpl:t=fetch}={}){try{let n=await t(`${j(r)}/setup`,{signal:AbortSignal.timeout(e)});return await ct(n,["accounts","expectedPort","install"])}catch{return null}}var Fe=new Map;function ht(r,{fetchImpl:e=fetch,timeoutMs:t=3e3}={}){let n=j(r);return Fe.has(n)||Fe.set(n,Sn(r,{fetchImpl:e,timeoutMs:t})),Fe.get(n)}async function Sn(r,{fetchImpl:e,timeoutMs:t}){try{let n=await e(`${j(r)}/config.json`,{signal:AbortSignal.timeout(t)}),s=(await ct(n,["rooms","accounts"]))?.rooms;if(typeof s=="string"&&/^(wss?|https?):\/\/./.test(s))return at(s)}catch{}return at(r)}var le=process.env.SPRITE_PUBLIC_SERVER||Nn()||null;function Nn(){return"wss://drawpixie.com"}var dt=ze(process.env.XDG_CONFIG_HOME||ze(Rn(),".config"),"sprite"),qe=ze(dt,"agent.json"),ut=r=>String(r??"").replace(/\/+$/,"");function pt(){return`${On().replace(/\.local$/,"")}`.slice(0,60)||"a computer"}async function ft(){try{let r=JSON.parse(await En(qe,"utf8"));return r&&typeof r=="object"&&r.servers?r:{servers:{}}}catch{return{servers:{}}}}async function he(r){return process.env.SPRITE_TOKEN?{token:process.env.SPRITE_TOKEN,from:"the environment"}:(await ft()).servers[ut(r)]??null}async function mt(r,e){await jn(dt,{recursive:!0,mode:448});let t=await ft();t.servers[ut(r)]={...e,at:Date.now()};let n=`${qe}.tmp`;await vn(n,JSON.stringify(t,null,2),{mode:384}),await _n(n,384);let{rename:o}=await import("node:fs/promises");await o(n,qe)}async function wt(r,{timeoutMs:e=3e3}={}){try{let t=await fetch(`${j(r)}/setup`,{signal:AbortSignal.timeout(e)});return t.ok?!!(await t.json()).accounts:!1}catch{return!1}}async function yt(r,e,{session:t=null}={}){let n=t??await he(r);if(!n?.token)return;let o=await fetch(`${j(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 gt(r,{name:e,w:t,h:n},{session:o=null}={}){let s=o??await he(r);if(!s?.token)return;let i=await fetch(`${j(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 bt(r,e,{method:t="GET",body:n=null,session:o=null}={}){let s=o??await he(r);if(!s?.token)return;let i=await fetch(`${j(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 $t(r){let e=process.platform==="darwin"?"open":process.platform==="win32"?"start":"xdg-open";return new Promise(t=>Tn(e,[r],()=>t()))}vt();Ge();import{createServer as Mn}from"node:http";import{randomBytes as _t}from"node:crypto";var Cn=300*1e3,Un=()=>_t(32).toString("base64url"),Ln=()=>_t(16).toString("base64url");function jt(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 Dn({state:r,timeoutMs:e}){let t,n,o=new Promise((c,l)=>{t=c,n=l}),s=Mn((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 p=h.searchParams.get("error");l.writeHead(200,{"Content-Type":"text/html; charset=utf-8"}).end(p?jt("Nothing was granted.","You can close this window."):jt("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 At(r,{onUrl:e=()=>{},open:t=$t,label:n="the Pixie agent",scope:o=Be,timeoutMs:s=Cn}={}){let i=Un(),a=await Et(i),c=Ln(),l=await Dn({state:c,timeoutMs:s});try{let h=new URL(`${j(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 p=await l.wait,d=p.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 u=p.get("code");if(!u)throw new Error("the browser came back without an authorization");let m=await fetch(`${j(r)}/auth/authorize/exchange`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:u,verifier:i,name:pt()})}),w=await m.json().catch(()=>({}));if(!m.ok||!w.token)throw new Error(w.error||"that authorization could not be exchanged");return await mt(r,{token:w.token,name:w.name,email:w.email,scope:w.scope??null}),w}finally{l.close()}}import{realpathSync as Rt}from"node:fs";import{fileURLToPath as Fn}from"node:url";function Ot(r){let e=process.argv[1];if(!e)return!1;try{return Rt(Fn(r))===Rt(e)}catch{return!1}}de();import{execFile as fn}from"node:child_process";Ut();de();var Jn=5e3,ve=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 te(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.#t(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())}#o(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.#t(t),this.onChange(),t}redo(e=Date.now()){let t=this.session.redo(e);return this.#t(t),this.onChange(),t}setPresence(e){this.presence=e,this.connected&&this.#r({t:N.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=Mt({t:N.STROKE,layer:e,frame:t,rect:n,rle:o,done:s});i&&this.#l(i)}suggest(e){if(!e.length||!this.connected||!this.welcomed)return[];let t=this.session.state().seq,n=e.map(o=>({opId:U(),author:this.author,parentSeq:t,...o}));return this.#r({t:N.SUBMIT,ops:n}),n}review(e,t){!this.connected||!this.welcomed||this.#r({t:N.REVIEW,author:e,verdict:t})}lease(e,t={}){this.#r({t:N.LEASE,action:e,...t})}prompt(e,{where:t=Tt.DOCUMENT}={}){if(!this.connected)return null;let n=String(e??"").trim().slice(0,2e3);if(!n)return null;let o=U();return this.#r({t:N.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:N.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:N.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:N.PING})},Jn),await this.#c()},e.onmessage=t=>this.#p(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#c(){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:N.HELLO,author:this.author,name:this.name,token:e,since:t.seq,...this.agent?{agent:!0}:{},...this.answers?{answers:!0}:{},...this.presence?.active===!1?{active:!1}:{},docId:t.doc.id})}#p(e){let t=Ct(e);if(t)switch(t.t){case P.WELCOME:return this.#i(t);case P.REHELLO:return this.welcomed=!1,void this.#c();case P.OPS:return this.#s(t);case P.REJECT:return this.#f(t);case P.PRESENCE:return this.participants=t.participants||[],this.onPresence(this.participants);case P.STROKE:return this.onStroke(t);case P.LEASE:return this.leases=t.leases||[],this.onLeases(this.leases);case P.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 P.SUGGESTIONS:return this.proposals=new Map((t.proposals??[]).map(n=>[n.author,{...n,ops:[...n.ops??[]]}])),this.onSuggestions(this.proposals);case P.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 P.PROMPT:return this.onPrompt(t);case P.ANSWER:return this.onAnswer(t);case P.STOP:return this.onStop(t);case P.ERROR:if(this.#m(t.message)){this.strokePreview=!1;return}return this.onLost({kind:t.fatal?"fatal":"protocol",message:t.message});default:return}}#i(e){this.you=e.you,this.suggesting=e.you?.suggest===!0,this.welcomed=!0;let t=this.session.pending();if(e.catchup.kind===Nt.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 te(this.core,n),this.epoch++,o&&o.length){for(let s of o)this.#o(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.#d(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:N.SUBMIT,ops:t});this.presence&&this.#r({t:N.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}#s(e){let t=!1;for(let n of e.ops||[])this.#d(n),t=!0;t&&(this.#n(),this.onChange())}#d(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`})}#f(e){e.opId&&this.session.reject(e.opId),e.opId&&this.#o(e.opId,e.message),this.onLost({kind:e.kind,ops:e.opId?[e.opId]:[],message:e.message}),this.onChange()}#t(e){!e.length||!this.connected||!this.welcomed||this.#r({t:N.SUBMIT,ops:e})}#r(e){this.#l(Pt(e))}#m(e){return typeof e!="string"?!1:e===`unknown message: ${N.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()}};de();Dt();var W=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 ne=([r,e,t])=>.3*r+.59*e+.11*t;function je([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 Ft={luminance:(r,e)=>ne(W(r))-ne(W(e)),hue:(r,e)=>{let[t,n]=je(W(r)),[o,s]=je(W(e));return n<.08||s<.08?n<.08&&s<.08?ne(W(r))-ne(W(e)):n<.08?-1:1:Math.round(t/20)!==Math.round(o/20)?t-o:ne(W(r))-ne(W(e))},saturation:(r,e)=>je(W(r))[1]-je(W(e))[1]};function Q(r,e="hue"){let t=Ft[e]??Ft.hue;return[...r].sort(t)}qt();import{readFile as we,writeFile as Tr,mkdir as Nr}from"node:fs/promises";import{access as Zn,mkdir as Xn,writeFile as Yn}from"node:fs/promises";import{dirname as Qn}from"node:path";async function ee(r,e,{overwrite:t=!1,seq:n=null}={}){let o=await Zn(r).then(()=>!0,()=>!1);if(o&&!t){let s=r.replace(/(\.png)?$/i,`-seq${n??"N"}$1`);throw new Error(`${r} already exists \u2014 a chat that showed it once will keep showing the old picture, so a preview gets a new name each time: ${s}. Pass overwrite: true only for a file nothing has shown yet.`)}return await Xn(Qn(r),{recursive:!0}),await Yn(r,e),o}function Bt(r,e){let t=r.inputSchema??{},n=t.properties??{},o=Object.keys(n),s=e&&typeof e=="object"?e:{},i={};for(let[c,l]of Object.entries(s)){if(!(c in n)){let h=Je(c,o),p=o.length?`${r.name} takes: ${o.join(", ")}.`:`${r.name} takes no arguments.`;throw new Error(`no such argument \`${c}\`${h?` \u2014 did you mean \`${h}\`?`:""} ${p}`)}i[c]=er(r.name,c,n[c],l)}let a=(t.required??[]).filter(c=>i[c]===void 0);if(a.length)throw new Error(`${r.name} needs ${a.map(c=>`\`${c}\``).join(" and ")}. It takes: ${o.join(", ")}.`);return i}function er(r,e,t,n){if(n==null)return n;let o=t.type;if(o==="integer"||o==="number"){let s=typeof n=="string"&&n.trim()!==""?Number(n):n;if(typeof s!="number"||!Number.isFinite(s))throw new Error(`\`${e}\` must be a number, got ${re(n)}. ${r} takes \`${e}\` as ${o==="integer"?"an integer":"a number"}.`);if(o==="integer"&&!Number.isInteger(s))throw new Error(`\`${e}\` must be a whole number, got ${s}.`);if(t.minimum!==void 0&&s<t.minimum)throw new Error(`\`${e}\` must be at least ${t.minimum}, got ${s}.`);if(t.maximum!==void 0&&s>t.maximum)throw new Error(`\`${e}\` must be at most ${t.maximum}, got ${s}.`);return s}if(o==="boolean"){if(n==="true")return!0;if(n==="false")return!1;if(typeof n!="boolean")throw new Error(`\`${e}\` must be true or false, got ${re(n)}.`);return n}if(o==="string"&&typeof n!="string"){if(typeof n=="number")return String(n);throw new Error(`\`${e}\` must be text, got ${re(n)}.`)}if(o==="array"&&!Array.isArray(n))throw new Error(`\`${e}\` must be a list, got ${re(n)}.`);if(o==="object"&&(typeof n!="object"||Array.isArray(n)))throw new Error(`\`${e}\` must be an object, got ${re(n)}.`);if(t.enum&&!t.enum.includes(n))throw new Error(`\`${e}\` must be one of ${t.enum.map(s=>`\`${s}\``).join(", ")}, got ${re(n)}.`);return n}function re(r){return typeof r=="string"?`"${r.length>40?r.slice(0,37)+"\u2026":r}"`:Array.isArray(r)?"a list":r&&typeof r=="object"?"an object":String(r)}function Je(r,e){let t=r.toLowerCase(),n=e.filter(i=>{let a=i.toLowerCase();return(t.startsWith(a)||a.startsWith(t))&&a!==t});if(n.length)return n.sort((i,a)=>Math.abs(i.length-t.length)-Math.abs(a.length-t.length))[0];let o=null,s=3;for(let i of e){let a=tr(t,i.toLowerCase());a<s&&(s=a,o=i)}return o}function tr(r,e){let t=Array.from({length:e.length+1},(n,o)=>o);for(let n=1;n<=r.length;n++){let o=t[0];t[0]=n;for(let s=1;s<=e.length;s++){let i=t[s];t[s]=Math.min(t[s]+1,t[s-1]+1,o+(r[n-1]===e[s-1]?0:1)),o=i}}return t[e.length]}import{createHash as Pr}from"node:crypto";import{tmpdir as Ir}from"node:os";import{join as et,basename as Pe,extname as Mr,resolve as Cr}from"node:path";function He(r){try{return/^(localhost|127\.0\.0\.1|\[::1\]|::1)$/.test(new URL(j(r)).hostname)}catch{return!1}}function Gt(r,{publicServer:e=le}={}){let t=j(r);return e&&r===e?`nothing is answering at ${t} \u2014 this machine is offline, or the site is down`:He(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{appendFile as xr,mkdir as Xt,readFile as nn,writeFile as Ke}from"node:fs/promises";import{dirname as Yt}from"node:path";import{appendFile as nr,mkdir as rr,readFile as or}from"node:fs/promises";import{homedir as sr}from"node:os";import{join as Ve}from"node:path";import{randomUUID as ir}from"node:crypto";function Vt(){return process.env.PIXIE_TRACES||Ve(process.env.XDG_CONFIG_HOME||Ve(sr(),".config"),"sprite","traces")}var Wt=r=>Ve(Vt(),`${String(r).replace(/[^A-Za-z0-9_-]/g,"_")}.jsonl`),ar=new Set(["pull_source"]),_e=r=>ar.has(r.tool)?null:r.args?.job??null,Jt=4e3,Ht=120,cr=2e3,lr=new Set(["note","unfinished","takes","intent","finished","run","label"]);function K(r,e=0,t=Ht){if(r==null||typeof r=="number"||typeof r=="boolean")return r;if(typeof r=="string")return r.length>t?`${r.slice(0,t-1)}\u2026`:r;if(Array.isArray(r)){if(r.length&&r.every(n=>typeof n=="string")){let n=new Set(r.map(o=>o.length));return`${r.length}\xD7${n.size===1?r[0].length:`${Math.min(...n)}\u2013${Math.max(...n)}`}`}return`[${r.length}]`}if(typeof r=="object"){let n=Object.keys(r),o=n.length<=8&&n.every(i=>r[i]==null||typeof r[i]!="object");if(n.length>32||e>0&&!o)return`{${n.length}}`;let s={};for(let i of n)s[i]=K(r[i],e+1,lr.has(i)&&typeof r[i]=="string"?cr:Ht);return s}return String(r)}function hr(r){if(r&&typeof r=="object"&&Array.isArray(r.content)){let e=r.content.find(n=>n.type==="image"),t=r.content.find(n=>n.type==="text")?.text??"";if(r.isError)return{ok:!1,error:K(t)};if(e)return{ok:!0,result:`image \u2014 ${K(t)}`};try{return{ok:!0,result:K(JSON.parse(t))}}catch{return{ok:!0,result:K(t)}}}return{ok:!0,result:K(r)}}var Ae=class{constructor(){this.session=ir(),this.entries=[],this.next=1,this.open=new Map,this.starts=new Map,this.idleFrom=1,this.lead=new Map}async record(e,t,n){let o=Date.now(),s={i:this.next++,t:new Date(o).toISOString(),tool:e,args:K(t??{})};try{let i=await n();return Object.assign(s,hr(i),{ms:Date.now()-o}),this.#e(s),i}catch(i){throw Object.assign(s,{ok:!1,error:K(i?.message??String(i)),ms:Date.now()-o}),this.#e(s),i}}#e(e){this.entries.push(e),this.entries.length>Jt&&this.entries.splice(0,this.entries.length-Jt)}began(e){if(!e)return;let t=this.entries.at(-1)?.i??this.next;this.open.size===0&&this.idleFrom<t&&this.lead.set(e,this.idleFrom),this.open.set(e,t),this.starts.set(t,e)}all(e=500){let t=this.entries.length,n=new Map,o=null;for(let s of this.entries){let i=this.starts.get(s.i);i&&(o=i),n.set(s.i,_e(s)??o??null),s.tool==="finish_job"&&(_e(s)??o)===o&&(o=null)}return{calls:t,dropped:Math.max(0,t-e),entries:this.entries.slice(-e).map(s=>n.get(s.i)?{...s,job:n.get(s.i)}:s)}}finished(e){let t=e??(this.open.size===1?[...this.open.keys()][0]:null);if(!t||!this.open.has(t))return[];let n=this.open.get(t);this.open.delete(t);let o=this.lead.get(t)??n;return this.lead.delete(t),this.open.size===0&&(this.idleFrom=this.next),this.entries.filter(s=>s.i>=o&&(_e(s)==null||_e(s)===t)&&(this.starts.get(s.i)??t)===t).map(s=>s.i<n?{...s,between:!0}:s)}async keepFor(e,t,n){if(!e||!n.length)return null;let o=n.map(s=>JSON.stringify({...s,job:t,document:e,session:this.session})).join(`
|
|
3
|
+
`);try{return await rr(Vt(),{recursive:!0}),await nr(Wt(e),`${o}
|
|
4
|
+
`),null}catch(s){return s.message}}async earlier(e){let t=[];for(let n of new Set([...e].filter(Boolean))){let o=await or(Wt(n),"utf8").catch(()=>"");for(let s of o.split(`
|
|
5
|
+
`))if(s.trim())try{let i=JSON.parse(s);i.session!==this.session&&t.push({...i,earlier:!0})}catch{}}return t.sort((n,o)=>String(n.t).localeCompare(String(o.t)))}documents(){let e=new Set;for(let t of this.entries)typeof t.args?.document=="string"&&e.add(t.args.document),t.result&&typeof t.result=="object"&&typeof t.result.document=="string"&&e.add(t.result.document);return e}};import{readFileSync as dr}from"node:fs";import{execFileSync as ur}from"node:child_process";import{fileURLToPath as pr}from"node:url";import{dirname as fr}from"node:path";var Kt="0.7.9";function mr(){let r=new URL("./version.json",import.meta.url);try{let e=JSON.parse(dr(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 ue=Kt??mr();function wr(){if(Kt)return null;let r=fr(pr(import.meta.url)),e=(...t)=>ur("git",t,{cwd:r,stdio:["ignore","pipe","ignore"],timeout:2e3}).toString().trim();try{let t=e("rev-parse","--short=12","HEAD");return t?e("status","--porcelain","--",".","../../edge/src","../../core/js")?`${t}-dirty`:t:null}catch{return null}}var pe=wr();import{deflateSync as yr}from"node:zlib";var gr=(()=>{let r=new Uint32Array(256);for(let e=0;e<256;e++){let t=e;for(let n=0;n<8;n++)t=t&1?3988292384^t>>>1:t>>>1;r[e]=t>>>0}return r})();function br(r){let e=4294967295;for(let t of r)e=gr[(e^t)&255]^e>>>8;return(e^4294967295)>>>0}function oe(r,e){let t=Buffer.alloc(12+e.length);return t.writeUInt32BE(e.length,0),t.write(r,4,"ascii"),Buffer.from(e).copy(t,8),t.writeUInt32BE(br(t.subarray(4,8+e.length)),8+e.length),t}function $r(r,e,t,n){let o=e*n,s=Buffer.alloc((o*4+1)*t*n),i=0;for(let a=0;a<t;a++){let c=Buffer.alloc(o*4);for(let l=0;l<e;l++){let h=(a*e+l)*4;for(let p=0;p<n;p++)c.set(r.subarray(h,h+4),(l*n+p)*4)}for(let l=0;l<n;l++)s[i++]=0,c.copy(s,i),i+=c.length}return yr(s)}function Zt(r,e,t,n=1){let o=e*n,s=t*n,i=Buffer.alloc(13);i.writeUInt32BE(o,0),i.writeUInt32BE(s,4),i[8]=8,i[9]=6;let a=Buffer.alloc(8);a.writeUInt32BE(r.length,0),a.writeUInt32BE(0,4);let c=[Buffer.from([137,80,78,71,13,10,26,10]),oe("IHDR",i),oe("acTL",a)],l=0;return r.forEach((h,p)=>{let d=Buffer.alloc(26);d.writeUInt32BE(l++,0),d.writeUInt32BE(o,4),d.writeUInt32BE(s,8),d.writeUInt16BE(Math.max(1,Math.round(h.durMs??100)),20),d.writeUInt16BE(1e3,22),d[24]=0,d[25]=0,c.push(oe("fcTL",d));let u=$r(h.pixels,e,t,n);if(p===0)c.push(oe("IDAT",u));else{let m=Buffer.alloc(4+u.length);m.writeUInt32BE(l++,0),u.copy(m,4),c.push(oe("fdAT",m))}}),c.push(oe("IEND",Buffer.alloc(0))),Buffer.concat(c)}var Re={type:"string",description:"#rrggbb"};async function rn(r,e){let t=await nn(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 kr(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 kr({w:r,h:e,pixels:t}){let n={},o=on(n),s=a=>a.toString(16).padStart(2,"0"),i=[];for(let a=0;a<e;a++){let c="";for(let l=0;l<r;l++){let h=(a*r+l)*4,p=t[h+3];if(p===0){c+=".";continue}let d=p===255?`#${s(t[h])}${s(t[h+1])}${s(t[h+2])}`:`#${s(t[h])}${s(t[h+1])}${s(t[h+2])}${s(p)}`;c+=o(d)}i.push(c)}return{rows:i,colors:n}}function on(r){let e=new Map,t=33;return n=>{let o=e.get(n);if(o)return o;for(;;)if(t===34||t===46||t===92||t===95||t===127)t+=1;else if(t>=128&&t<161)t=161;else if(t>=55296&&t<57344)t=57344;else break;return o=String.fromCodePoint(t),t+=1,e.set(n,o),r[o]=n,o}}var Sr=new Set(["."," ","_"]);async function Er(r,e){let t=r;if(r.file){let i=await nn(r.file).catch(c=>{throw new Error(`could not read ${r.file}: ${c.code==="ENOENT"?"no such file":c.message}`)}),a;try{a=JSON.parse(i.toString("utf8"))}catch{throw new Error(`${r.file} is not JSON \u2014 draw_animation's \`file\` is a JSON of the call ({ frames, colors, fps? }); a PNG per frame goes in that frame's own \`file\``)}if(!a||!Array.isArray(a.frames))throw new Error(`${r.file} needs "frames", an array of { rows } or { file }`);t={...a,...r}}if(!Array.isArray(t.frames)||!t.frames.length)throw new Error("draw_animation needs `frames` \u2014 or a `file` that has them");if(t.frames.forEach((i,a)=>{if(!i||!Array.isArray(i.rows)&&!i.file)throw new Error(`frame ${a} needs \`rows\` or a \`file\``)}),!t.frames.some(i=>i.file))return t;let n={},o=on(n),s=[];for(let[i,a]of t.frames.entries()){let c=a.file?await rn(a.file,e):{rows:a.rows,colors:t.colors??{}},l=c.rows.map(p=>[...p].map(d=>{if(Sr.has(d))return d;if(!(d in c.colors))throw new Error(`frame ${i}${a.file?` (${a.file})`:""}: "${d}" is not in its colors`);let u=c.colors[d];return o(u===null?null:String(u).toLowerCase())}).join("")),h=Object.fromEntries(["x","y","center","copyPrevious"].flatMap(p=>a[p]!==void 0?[[p,a[p]]]:c[p]!==void 0&&a.file?[[p,c[p]]]:[]));s.push({rows:l,...h})}return{...t,frames:s,colors:n}}var O={type:"string",description:"The job this call belongs to. May be omitted when exactly one job is open."},fe={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. A share link (`https://\u2026/s/<document>_<snapshot>`) or an editor link (`\u2026/d/<document>`) names its document too, so a link the user pasted can be passed straight in. In a local editor a 0-based tab index works too."},Qt={type:"integer",description:"A PAST state of the document, rebuilt from the room's history: the sequence number get_info reports and every draw_art result carries. Omit for now. This is how to compare a pass with the one it replaced \u2014 the check that catches a revision that is locally better and worse as a picture: read or render the seq before your change, render now, and stack them small and in grays (references/judge.py). A read, never a restore; the document does not move."};function ie(r){let e=String(r).trim();if(!/[:/]/.test(e))return e;let t=e;if(/^[a-z][a-z0-9+.-]*:\/\//i.test(e))try{t=new URL(e).pathname}catch{return e}let n=/(?:^|\/)s\/([A-Za-z0-9_-]{1,128})(?:\/|$)/.exec(t);if(n)return n[1].split("_")[0];let o=/(?:^|\/)d\/([A-Za-z0-9-]{1,64})(?:\/|$)/.exec(t);return o?o[1]:e}var Oe=r=>r==null||r===""?null:/^\d+$/.test(String(r))?Number(r):r,vr=r=>({isError:!0,content:[{type:"text",text:r}]}),se=r=>({content:[{type:"text",text:typeof r=="string"?r:JSON.stringify(r)}]}),en=(r,e)=>({content:[{type:"image",data:Buffer.from(r).toString("base64"),mimeType:"image/png"},{type:"text",text:e}]}),F={w:240,h:126},tn={w:80,h:42},jr="40\xD721, 80\xD742, 120\xD763, 160\xD784, 240\xD7126, 480\xD7252, 600\xD7315",_r="40\xD740, 50\xD750, 80\xD780, 100\xD7100",Ar="60\xD720, 75\xD725, 150\xD750, 300\xD7100";function Ze(r,e){if(!r||!e)return null;let t=r/e,n=1200/630;if(Math.abs(t-n)<.15){let o=Math.round(r/n);return`${r}\xD7${e} is ${t.toFixed(2)}:1; the link-card shape at this width is ${r}\xD7${o} \u2014 resize_canvas if it is meant to be posted.`}return Math.abs(t-1)<.1?`${r}\xD7${e} is nearly square; ${Math.max(r,e)}\xD7${Math.max(r,e)} would be a profile picture.`:Math.abs(t-3)<.25?`${r}\xD7${e} is nearly 3:1; ${r}\xD7${Math.round(r/3)} would be a profile banner.`:null}function Te(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 Rr(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 Or=[{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. The size is `w` and `h` (not width/height), as everywhere here.",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:F.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: ${F.w}\xD7${F.h} \u2014 1.91:1, the ratio link cards crop to, so a piece drawn on it posts edge to edge, and big enough for a subject with parts (a dish with its slice and garnish, a figure with props, a room). ${tn.w}\xD7${tn.h} when the user asks for something SIMPLE \u2014 "simple", "quick", "small", "minimal", "a little", "basic", "rough", "doodle", "icon-like" \u2014 bold shapes and big pixels. Any multiple of 40\xD721 fills the card (${jr}: examples, not limits). A PROFILE PICTURE is square and cropped to a circle \u2014 keep the subject inside it: ${_r}. A BANNER is 3:1: ${Ar}. 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:F.h,description:"Height in pixels. See w."},palette:{type:"array",items:Re,description:"Optional starting palette. Kept in ramp order \u2014 by hue, then dark to light \u2014 not the order given, so a script that indexes the document's palette by position will be off: map by hex."},like:{type:"string",description:'Another document \u2014 its id, name or link \u2014 whose size and palette this one starts with: the next sheet of a series, "one like this". `w`, `h` and `palette` given here still win. It saves the mechanical part only; measure the reference\'s per-object budget anyway (analyze_reference.py --objects).'},intent:{type:"string",description:'One line on what you are making, including the VIEW. Shown while you work. The default view is three-quarter: the camera above and to one side, so an object shows its top and two sides and a scene has depth \u2014 unless the user says "flat", "side view", "front", "profile", "top-down", "icon" or "tile", which mean what they say and no perspective at all.'},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","inktober"],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; "inktober" for today\'s Inktober prompt, which runs the 31 days of October. The name comes from the prompt 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 prompt before drawing.'}},required:["intent"]},run:async(r,e)=>{let t=r.name?Rr(r.name):null,n=r.w??null,o=r.h??null;if(r.challenge){let c=await e.challenge(r.challenge);if(t??=c.theme,n===null&&o===null&&c.sizes?.length){let l=c.sizes[Math.floor((c.sizes.length-1)/2)],[h,p]=l.split(/[x×]/).map(Number);h&&p&&([n,o]=[h,p])}}if(!t)throw new Error("new_sprite needs a name, or a challenge to take one from");let s=r.palette,i=null;if(r.like){let c=await e.info({document:ie(r.like)});if(!c.document)throw new Error("`like` reads another document, which is a room thing \u2014 in a local editor, give w, h and palette yourself");n??=c.w,o??=c.h,s??=c.palette,i=`${c.document} ("${c.name}"): ${r.w==null&&r.h==null?`its ${c.w}\xD7${c.h}`:"the size you gave"}, ${r.palette?"the palette you gave":`its ${c.palette.length} palette entries`}`}let a=await e.newSprite({name:t,w:n??F.w,h:o??F.h,intent:r.intent,palette:s,folder:r.folder??null,challenge:r.challenge??null});return i?{...a,like:i}:a}},{name:"challenges",description:`The standing prompts: Pixel Dailies sets a theme every day ("Today's theme is #Gateway"), Weekly Pixels a word every week with sizes, and Inktober a prompt for each of the 31 days of October. When the user says "the sprite of the day", "today's theme", "the daily", "the weekly", "this week's word", "Inktober" or "today's Inktober prompt" \u2014 or names any of the three \u2014 this is what they mean: read it here rather than asking them. The server reads the accounts and keeps the answer, and holds Inktober's published list; nothing is fetched from Bluesky or inktober.com by you. An entry whose \`theme\` is null is one nobody has set \u2014 Inktober's is null for the other eleven months.`,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. With no `document` and no job open, it picks up the document you last finished IN THIS SESSION \u2014 one more pass on the same sprite is begin_job({ intent }); a document from an earlier session has to be named (list_documents has them).",inputSchema:{type:"object",properties:{document:{type:"string",description:"Which document: the id, or the exact name when only one document has it. Omit, with no job open, for the one you last finished in this session; a document from an earlier one has to be named."},intent:{type:"string"},layer:{type:"string",description:"Restrict the claim to one layer, leaving the rest free. Rooms only."}},required:["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 \u2014 and carries `trace`: every call of this job, as the server saw it \u2014 and, marked `between: true`, any made with no job open since the last finish. When the task asked for a report back, do NOT assemble trace.jsonl out of these: `get_info({ trace: true, file: "\u2026/trace.jsonl" })` writes the whole session to that file itself, and three reports that retyped the record by hand lost something each \u2014 two lost `versions` and one abbreviated every result \u2014 which is a sibling of `trace` here, travels inside this call\'s own trace entry, and is what feedback.py stamps the report with. Without it a current server is labeled ancient and every friction item struck.',inputSchema:{type:"object",properties:{job:O,note:{type:"string",description:`One line on what shipped \u2014 "8 roses, hooked thorns, ivy; crown at CY 69" \u2014 which becomes the history's line for this job in place of the intent written before the work. Optional; up to 200 characters.`},trace:{type:"string",description:"Also APPEND this job's calls to this path as JSONL, as the server saw them. Seldom needed now: every finish keeps its job's calls per document on the server's side whether or not this is passed, and `get_info({ document, trace: true, file })` writes them back \u2014 earlier sessions' included \u2014 into a report's trace.jsonl. Pass a path only for a file of your own beside that."}}},run:async(r,e)=>({...await e.finishJob({job:r.job,note:r.note??""}),versions:{mcp:ue,...pe?{commit:pe}:{}}})},{name:"push_source",description:"Push the generator behind this job \u2014 the script(s) that made the sprite \u2014 so the next agent (or you, tomorrow) can pick the work up rather than redraw it from the pixels. Stored beside the job's mark in the document's version history, with the seq it reproduces; READABLE BY THIS ACCOUNT ONLY, never by anyone else in the room. Push the source, not its output: the .py, and anything it imports that is yours; not the grid JSON or a PNG, which it regenerates (text, \u2264 16 files, \u2264 96 KiB). `run` is the exact command; `takes` what it needs that is not in the push (a reference from Commons: its URL; the skill's references/ helpers by name); `unfinished` what you would do next. Push before finish_job, and again after a real change.",inputSchema:{type:"object",properties:{files:{type:"array",items:{type:"string"},description:'Paths on this machine to the files to push. The basename is kept, the directory is not \u2014 so two generators both called `mint.py` need `{ from: "<path on disk>", path: "<name to push it as>" }` instead of a plain path.'},run:{type:"string",description:'The command that regenerates the sprite from these files, e.g. "python3 buche.py .context/yule/solo.json && draw_art({ file })".'},takes:{type:"string",description:"What it needs that is not in the push: reference URLs, the references/ helpers it imports, a palette. Empty if nothing."},unfinished:{type:"string",description:"What is not done, or what you would change next. The next agent reads this first."},job:O},required:["files","run"]},run:(r,e)=>e.pushSource({job:r.job,files:r.files,run:r.run,takes:r.takes??"",unfinished:r.unfinished??""})},{name:"pull_source",description:"What this account pushed on a document: with no `job`, the list (job, seq, run, unfinished, file names) \u2014 with a `job`, its files written to disk under `into` (default .context/source/<job>/) with how to run them, what they take and what was unfinished. Call it before redrawing a sprite an agent made: if there is a push, start from it. Only your own pushes come back; a mark with `source: true` in the history that is not listed here is somebody else's. READ THE FILES BEFORE RUNNING THEM, and run them in the workspace with nothing the task does not need \u2014 they are code somebody pushed.",inputSchema:{type:"object",properties:{document:fe,job:{type:"string",description:"The job whose push to write to disk. Omit to list."},into:{type:"string",description:"Directory to write the files into. Default .context/source/<job>/. A file that exists with different content is not overwritten."}}},run:(r,e)=>e.pullSource({document:r.document??null,job:r.job??null,into:r.into??null})},{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`. A key is any single character except "." and " " \u2014 letters, digits, punctuation, any Unicode symbol \u2014 so a generator that runs past 62 colors keeps going. "." 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. With a low count, a value between two colors is a CHECKER of the two (an ordered dither), planned from the first shading pass, never an extra color. 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. Either is the whole canvas or, with `rect`, one region of it: a pass that touched only the fire base sends the same file and names the rect, and the history says "fire base", not everything. The reply says how many pixels are not what they were (`changed`; `inked` is how many the grid painted, when the two differ \u2014 a full-canvas redraw that moved a corner says 180 and 30240) and which colors joined the palette, so growth is never silent (with `palette: "replace"` nothing joins: the palette becomes this drawing\'s colors plus whatever the other layers are drawn in, and the reply says so). 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. A PNG\'s colors are used as they are \u2014 not quantized to the palette; each distinct color joins it (or with `palette: "replace"`, becomes it) \u2014 so read_art({ png }) \u2192 edit \u2192 draw_art({ file }) round-trips exactly. Anything given in the call as well wins.'},rows:{type:"array",items:{type:"string"}},colors:{type:"object",additionalProperties:{oneOf:[Re,{type:"null"}]}},x:{type:"integer"},y:{type:"integer"},center:{type:"boolean"},clear:{type:"boolean",description:"Erase the cel first."},rect:{type:"object",description:"With `file`, JSON or PNG: replace only this region `{x, y, w, h}` of the canvas from a full-canvas file \u2014 a one-object change is not a whole-canvas redraw. With `clear`, only the region is emptied first.",properties:{x:{type:"integer"},y:{type:"integer"},w:{type:"integer"},h:{type:"integer"}}},palette:{type:"string",enum:["add","replace","keep"],description:"What this drawing's colors do to the document palette: `add` (default) joins them; `replace` makes them the whole palette \u2014 for a generated piece redrawn several times, so the palette does not pile up, and on a multi-layer document it keeps what the OTHER layers are drawn in too, since the palette is the document's; `keep` leaves the palette as it is \u2014 for a pass the user may send back (a glow tried three ways left 120 tints behind), or a generator whose blends are not colors anybody will pick."},layer:{type:"string",description:"Layer name or index. Defaults to the top one. A name no layer has makes that layer, on top \u2014 or under `below`."},below:{type:"string",description:"When `layer` makes a new layer: the existing layer (name or index) it goes directly under. A backdrop drawn after the subject goes under it, not into Layer 1."},frame:{type:"integer",description:"Which frame, COUNTED FROM 0: 0 is the first, 5 the sixth. Defaults to 0. A frame that does not exist is refused \u2014 add_frame first. On a document with several frames the reply names the frame it drew. For a whole generated loop use draw_animation with a file per frame, not this once per frame."},label:{type:"string",description:"What this shows as in the undo history."},job:O}},run:async(r,e)=>{let t=r.file?{...await rn(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,below:Oe(t.below),frame:t.frame??0,label:t.label,palette:t.palette??"add",rect:t.rect??null})}},{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. A GENERATED LOOP DOES NOT PASS THROUGH YOU: give each frame a `file` (a draw_art JSON or a PNG the script wrote) in place of its rows, or the whole call as one JSON `file` \u2014 `{ frames, colors, fps? }` \u2014 and the call is a few hundred bytes. Frames are counted from 0, and frame i of the list is frame i of the document; missing frames are added. SILHOUETTE FIRST: at 32px a front-on animal is any animal; when the subject could go several ways, draw 2-3 rough silhouettes as frames, look with render_animation, keep the one that reads, then add detail \u2014 three candidates cost barely more than one.",inputSchema:{type:"object",properties:{file:{type:"string",description:"Path to a JSON of this call's arguments \u2014 `frames` (each `{ rows }` or `{ file }`), `colors`, and any of `fps`, `layer`, `label`, `palette` \u2014 instead of giving them here. Anything given in the call as well wins."},frames:{type:"array",minItems:1,items:{type:"object",properties:{rows:{type:"array",items:{type:"string"}},file:{type:"string",description:"This frame from a file instead of `rows`: a draw_art JSON ({ rows, colors, x?, y? }) or a PNG, read as draw_art({ file }) reads one. Each file's keys are its own; the frames are rekeyed onto one map by color, so a PNG per frame works as it is."},copyPrevious:{type:"boolean"},x:{type:"integer"},y:{type:"integer"},center:{type:"boolean"}}}},colors:{type:"object",additionalProperties:{oneOf:[Re,{type:"null"}]},description:"The keys of the frames given as `rows`. Frames from files bring their own."},fps:{type:"number"},layer:{type:"string"},label:{type:"string"},palette:{type:"string",enum:["add","replace","keep"],description:"As draw_art's: `add` (default) joins the frames' colors to the palette; `replace` makes the palette what some frame landed on plus what every cel this call did not write is drawn in \u2014 for a loop regenerated every pass; `keep` leaves it alone. Rooms only; a local editor adds."},job:O}},run:async(r,e)=>{let t=await Er(r,e);if(!t.colors)throw new Error("draw_animation needs `colors` for frames given as rows");return e.drawAnimation({job:t.job,frames:t.frames,colors:t.colors,fps:t.fps,layer:t.layer,label:t.label,palette:t.palette??"add"})}},{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: with no job open they read the document last finished in this session, or the `document` named. The fallback is this process's memory, so the first call of a new session names its document. `seq` reads a PAST state instead of now \u2014 the grid as it stood at that document sequence, rebuilt from the room's history \u2014 which is how to get the pixels of the pass you replaced.",inputSchema:{type:"object",properties:{document:fe,layer:{type:"string"},frame:{type:"integer"},trim:{type:"boolean",description:"Return only the drawn area. Rooms only."},seq:Qt,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."},png:{type:"string",description:"Write the document to this path as a PNG, one image pixel per sprite pixel \u2014 the composite, or one `layer` \u2014 so an image tool, `references/compare.py` or `references/scene.py guides` can open the room's truth directly. A local editor writes the composite only."},job:O}},run:async(r,e)=>{if(r.png){if(!e.readPng)throw new Error("a PNG of the document is a room thing \u2014 in a local editor, read_art({ file }) and render it");let i=await e.readPng({document:r.document,job:r.job,layer:r.layer??null,frame:r.frame??0,seq:r.seq??null});return await Ke(r.png,i.png).catch(a=>{throw new Error(`could not write ${r.png}: ${a.message}`)}),{document:i.document,...i.at!==void 0?{at:i.at}:{},png:r.png,size:`${i.w}\xD7${i.h}`,layer:r.layer??"composite"}}let t=await e.readArt({document:r.document,job:r.job,layer:r.layer,frame:r.frame??0,trim:r.trim??!1,seq:r.seq??null});if(!r.file)return t;let{rows:n,colors:o,...s}=t;return await Ke(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. Needs no job: with none open it renders the document last finished in this session, or the `document` named \u2014 the first look of a new session names it, the fallback being this process's. 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, or `rects` to put several objects side by side in ONE image, which is what the per-object audit wants (an agent wrote its own tiler rather than make eight calls, and spent the audit looking at its own render instead of the room's). The text beside the image says exactly what was rendered, at what scale, and at which document seq \u2014 the room's confirmed state, never a queued or stale mirror: a render waits for the room to confirm what you sent and is refused while the connection is down. WHEN MATCHING A REFERENCE, pass `reference` (a path on this machine \u2014 PNG, or a JPEG on a Mac \u2014 or a document's id or link, so \"like this sheet\" is one call): the reference is brought to the canvas grid and shown on the LEFT of the render, same rect, same scale \u2014 the only comparison that catches an object built nothing like the original. A reference that is not the shape of the canvas is fitted whole (black where it does not reach) \u2014 into the `rect` when one is given, so one icon beside the one cell it was drawn into is the icon filling that cell; `referenceRect` brings one part of it \u2014 the object you are checking, in reference pixels \u2014 into the `rect` instead (into the whole canvas when there is no `rect`), fitted by the side that limits it, so the two sides are one object at one scale; the caption gives that scale in reference pixels per canvas pixel. That is how to compare one thing up close. To give rects in canvas pixels instead, pass the reference already resampled to the canvas's size (analyze_reference.py --resample): a reference the canvas's shape maps 1:1, so `referenceRect` is then the same numbers as `rect`. Judge per object from that, never from the whole image or from memory \u2014 and judge construction, not resemblance: count the faces, name the light, say where the shadow falls, on theirs and on yours. `values` renders both as grays \u2014 the squint test for a value plan and a focal point. ON A TRANSPARENT CANVAS pass `background` (\"checker\" or a #rrggbb): alpha is shown over white, so a white tip or a pale sparkle is invisible and cannot be judged at all.",inputSchema:{type:"object",properties:{document:fe,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"}}},rects:{type:"array",description:"Several windows instead of one, tiled left to right into a single image with a magenta line between them \u2014 the per-object audit in one call rather than one call per object. Each crop is NUMBERED in the magenta strip under it, 1 upwards, so a 32-cell sheet audit can name the cell it means instead of counting along the row; give each one a `label` and the caption says which number is which. Not with `reference`.",items:{type:"object",properties:{x:{type:"integer"},y:{type:"integer"},w:{type:"integer"},h:{type:"integer"},label:{type:"string",description:'What this crop is \u2014 "cruller", "head". Said in the caption against the number drawn under the crop; a name does not fit on an icon-sized tile.'}}}},reference:{type:"string",description:"Path to a reference image on this machine \u2014 a PNG anywhere, a JPEG on a Mac (converted on the way; elsewhere convert it first) \u2014 or a Pixie document: its id, its name, or a link to it (`/d/\u2026` or a share `/s/\u2026`), read as its composite at the room's confirmed seq. Shown beside the render at the same crop and scale, fitted whole to the canvas grid and centered \u2014 or the part of it `referenceRect` names."},referenceRect:{type:"object",description:"The part of the reference to bring to the grid, in reference pixels: `{x, y, w, h}`. Omit for the whole picture. With a `rect`, this region is fitted into that rect (centered, black where it does not reach); pick the region that holds the object `rect` shows, at the same proportions, and the two sides are that object at one scale.",properties:{x:{type:"integer"},y:{type:"integer"},w:{type:"integer"},h:{type:"integer"}}},values:{type:"boolean",description:"Render as values only (grayscale) \u2014 the render, and the reference when one is given. A picture that is mid-tone everywhere shows it here and nowhere else."},background:{type:"string",description:'What the transparent pixels show as: `"checker"` or a `#rrggbb`. Default is nothing \u2014 the alpha arrives as alpha, which a viewer paints white, so white or near-white art on a transparent canvas is INVISIBLE in the preview. Pass this whenever the canvas is transparent and the art has light pixels: a white tip, a sparkle, snow, a highlight. A sparkle was once shipped unseen for want of it.'},file:{type:"string",description:"Also write the image to this path \u2014 exactly what is returned: the rect, the scale, the reference beside it, the values. For `final.png` and the crops a report keeps, for any check that wants the room's picture rather than a local re-render, and for showing the user. A path that already exists is REFUSED unless `overwrite` is true: a chat that has shown a path once keeps showing the old picture under it, so a preview meant to be seen gets a new name every time (`preview-<seq>.png`; the caption says the seq)."},overwrite:{type:"boolean",description:"Let `file` replace a file that exists. Only for a file nothing has shown yet \u2014 a report's `final.png` being redone \u2014 never for a preview embedded in a reply."},seq:Qt,job:O}},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,rects:r.rects??null,reference:r.reference??null,referenceRect:r.referenceRect??null,values:r.values===!0,background:r.background??null,file:r.file??null,overwrite:r.overwrite===!0,seq:r.seq??null});return en(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. `file` keeps the sheet on disk; `apng` writes the LOOP ITSELF as an animated PNG of the room's frames at their own timing \u2014 the thing to show the user in chat, instead of re-rendering the frames locally and building a GIF from your own JSON.",inputSchema:{type:"object",properties:{document:fe,columns:{type:"integer",default:4},scale:{type:"integer",default:2,description:"Screen pixels per sprite pixel, for the sheet and the `apng` both."},file:{type:"string",description:"Also write the sheet to this path \u2014 exactly the image returned. A path that exists is refused unless `overwrite`, as render_preview's is."},apng:{type:"string",description:"Also write the frames as an animated PNG to this path: every frame's composite, at its duration, looping, alpha kept. Plays in a browser and in chat. A path that exists is refused unless `overwrite`. Rooms only."},overwrite:{type:"boolean",description:"Let `file` or `apng` replace a file that exists \u2014 only one nothing has shown yet."},job:O}},async run(r,e){let t=await e.sheet({document:r.document,job:r.job,columns:r.columns??4,scale:r.scale??2}),n=t.caption;if(r.file){let o=await ee(String(r.file),t.png,{overwrite:r.overwrite===!0});n+=` \u2014 sheet written to ${r.file}${o?" (replaced)":""}`}if(r.apng){if(!e.animationFrames)throw new Error("an animated PNG is a room thing \u2014 in a local editor, render_animation({ file }) keeps the sheet");let o=await e.animationFrames({document:r.document,job:r.job}),s=Math.max(1,Math.min(r.scale??2,Math.floor(4096/Math.max(o.w,o.h,1)))),i=await ee(String(r.apng),Zt(o.frames,o.w,o.h,s),{overwrite:r.overwrite===!0}),a=o.frames.reduce((c,l)=>c+(l.durMs??100),0);n+=` \u2014 ${o.frames.length} frames as an animated PNG, \xD7${s}, ${a} ms a loop, written to ${r.apng}${i?" (replaced)":""}`}return en(t.png,n)}},{name:"add_layer",description:"Add a layer above the others \u2014 or directly `below` or `above` one that exists, for a backdrop put in after the subject. 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"},below:{type:"string",description:"A layer (name or index) to put the new one directly under \u2014 the cloth under the drumstick."},above:{type:"string",description:"A layer (name or index) to put the new one directly over."},job:O}},run:(r,e)=>e.addLayer({job:r.job,name:r.name,below:Oe(r.below),above:Oe(r.above)})},{name:"rename_layer",description:"Give a layer a new name. A layer is found by its name \u2014 draw_art({ layer }), read_art({ layer }) \u2014 so a name that has drifted (a room once logged the author over agents' layer names) is put back here rather than worked around.",inputSchema:{type:"object",properties:{layer:{type:"string",description:"The layer as it is called now, or its index (0 is the bottom)."},name:{type:"string",description:"What it is called from now on. Refused if another layer has it."},job:O},required:["layer","name"]},run:(r,e)=>e.renameLayer({job:r.job,layer:Oe(r.layer),name:r.name})},{name:"add_frame",description:"Add an animation frame after the last one; the reply says its index, counted from 0. For a whole generated loop, draw_animation adds the frames it needs \u2014 this is for one more.",inputSchema:{type:"object",properties:{copy:{type:"boolean",description:"Start from the previous frame."},job:O}},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:Re,minItems:1},prune:{type:"boolean",description:"Keep only the colors some pixel uses. `colors` is ignored."},job:O}},run:(r,e)=>!r.prune&&!r.colors?.length?vr("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:O},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:O}},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. `center: true` instead KEEPS the canvas and moves the art to the middle of it \u2014 "make it horizontally and vertically centered", which arrives on every transparent sprite; `get_info`\'s `bbox` is where the art is now. Both measure the art on EVERY layer and frame, because the crop is of the document and what falls outside it is gone.',inputSchema:{type:"object",properties:{margin:{type:"integer",default:0},center:{type:"boolean",description:"Do not crop: keep the canvas and move the drawn pixels to its center. `margin` is ignored."},job:O}},run:(r,e)=>e.trim({job:r.job,margin:r.margin??0,center:r.center===!0})},{name:"resize_canvas",description:"Change the canvas size, keeping the art \u2014 the fix for a sprite made at the wrong size, in place, on the same document. `anchor` says where the old canvas sits on the new one: `center` (default), `top-left`, `top`, `top-right`, `left`, `right`, `bottom-left`, `bottom`, `bottom-right`. Never make a second document for a size change. The old art is kept at `anchor`; pass `clear: true` to start the new size blank in the same call \u2014 otherwise the room shows the old picture in the corner until the next full draw_art. `clear` empties EVERY layer and frame: a resize is of the document, so a blank canvas is a blank one.",inputSchema:{type:"object",properties:{w:{type:"integer",minimum:1,maximum:1024},h:{type:"integer",minimum:1,maximum:1024},anchor:{type:"string"},clear:{type:"boolean",description:"Erase everything after resizing, so a full redraw at the new size does not follow an old picture in the corner."},job:O},required:["w","h"]},run:(r,e)=>e.resizeCanvas({job:r.job,w:r.w,h:r.h,anchor:r.anchor??"center",clear:r.clear===!0})},{name:"get_info",description:"What is in a document \u2014 size, layers, frames, palette \u2014 and who else is looking at it; `layers: true` makes each layer an object with its pixel count and bounding box, which is how to check a layer split without reading every layer back; `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. `trace: true` adds this session's whole call record \u2014 every call, not one job's, the same entries finish_job hands back \u2014 which is what a report's Trace section is written from when it spans several jobs. Per process: a new session starts an empty one. With `file` as well, the record is written there as JSONL and the result is a summary: that is `trace.jsonl`, in the server's own words rather than retyped through you. The result carries `versions` \u2014 which pixie-mcp this was \u2014 so the file stamps itself and a report never has to.",inputSchema:{type:"object",properties:{document:fe,job:O,trace:{type:"boolean",description:"Add every call this session has made, oldest first, as `trace`, each entry carrying the `job` it belongs to \u2014 for a report over more than one job. Earlier sessions' FINISHED jobs on the same documents (this one's, or the `document` named) come first, marked `earlier: true`: every finish_job keeps its calls per document, so a report written after a restart still has the work."},layers:{type:"boolean",description:"Make `layers` an object per layer instead of a name \u2014 `{ name, pixels, bbox }`, bottom to top, counted over every frame. What verifies a layer split without reading each one back and stacking it: a pass that landed nowhere is `pixels: 0`, and a layer reaching further than the one above it is the trim you are about to regret."},file:{type:"string",description:"With `trace: true`: write the trace to this path as JSONL \u2014 one entry per line, which is exactly what a report's `trace.jsonl` is \u2014 and return a summary instead of the entries. The file is the server's copy: a trace that comes back through the model and is typed out again loses the long fields (a report's was \"the payload re-emitted, with the repeated document metadata abbreviated\"). Replaced if it exists, because a report gets rebuilt."}}},run:async(r,e)=>{if(r.file&&r.trace!==!0)throw new Error("`file` here writes the trace \u2014 pass trace: true with it. For the document as a picture or a grid, that is render_preview({ file }) and read_art({ file }).");let t=await e.info({document:r.document,job:r.job,layers:r.layers===!0});return r.trace===!0?{...t,versions:{mcp:ue,...pe?{commit:pe}:{}}}:t}},{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 se(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: in a local editor its tab, in a room the connection and any job you hold on it. 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 sn(r){let e=r.trace??=new Ae,t=n=>n&&typeof n=="object"&&Array.isArray(n.content);return Or.filter(n=>r.supports(n.name)).map(n=>({name:n.name,description:n.description,inputSchema:n.inputSchema,run:async o=>{let s=await e.record(n.name,o,()=>{let i=Bt(n,o);return typeof i.document=="string"&&(i.document=ie(i.document)),n.run(i,r)});if((n.name==="new_sprite"||n.name==="begin_job")&&s?.job&&e.began(s.job),n.name==="finish_job"&&s&&typeof s=="object"&&!t(s)){let i=e.finished(s.job??o?.job),a=await e.keepFor(s.document,s.job??o?.job,i);if(a&&(s.traceUnkept=`this job's calls could not be kept for a later report: ${a}`),typeof o?.trace=="string"&&o.trace){let c=String(o.trace);return await Xt(Yt(c),{recursive:!0}).catch(()=>{}),await xr(c,`${i.map(l=>JSON.stringify(l)).join(`
|
|
6
6
|
`)}
|
|
7
|
-
`).catch(
|
|
7
|
+
`).catch(l=>{throw new Error(`could not append the trace to ${c}: ${l.message}`)}),se({...s,traceFile:c,traceCalls:i.length,traceNote:`${i.length} entr${i.length===1?"y":"ies"} appended to ${c}. Pass the same path on every finish_job of this piece and the file survives a session boundary; get_info({ trace: true, file }) writes this session's whole record instead.`})}return se({...s,trace:i})}if(n.name==="get_info"&&o?.trace===!0&&s&&typeof s=="object"&&!t(s)){let i=e.all(),a=[...new Set([...e.documents(),s.document].filter(Boolean).map(d=>ie(d)))],c=await e.earlier(a),l=[...c,...i.entries],h=i.calls+c.length,{dropped:p}=i;if(o.file){let d=String(o.file),u=`${l.map(w=>JSON.stringify(w)).join(`
|
|
8
8
|
`)}
|
|
9
|
-
`;await Vt(Kt(u),{recursive:!0}).catch(()=>{}),await He(u,d).catch(w=>{throw new Error(`could not write ${u}: ${w.message}`)});let m=h.filter(w=>w.ok===!1).length;return re({...s,traceFile:u,traceCalls:l,traceFailed:m,...p?{traceDropped:p}:{},...c.length?{traceEarlier:c.length}:{},traceNote:`${h.length} entr${h.length===1?"y":"ies"} written to ${u}, one JSON object per line, oldest first \u2014 ${c.length?`the first ${c.length} from earlier sessions' finished jobs on ${a.length===1?"this document":"these documents"} (marked \`earlier: true\`), then this session's; `:""}this call is the last of them, and its result carries \`versions\`, so the file says which pixie-mcp wrote it. That file is the report's Trace section; say under the heading what stands out in it.`})}return re({...s,trace:h,traceCalls:l,...p?{traceDropped:p}:{},...c.length?{traceEarlier:c.length}:{},traceNote:`every call this session made, oldest first${c.length?`, after ${c.length} from earlier sessions' finished jobs on the same documents (\`earlier: true\`)`:""} \u2014 save as trace.jsonl, one entry per line. This call is in it. Or pass \`file\` and the server writes it.`})}return t(s)?s:re(s)}}))}var en=/^[A-Za-z0-9_-]{1,64}$/,jr={daily:{label:"Pixel Dailies",whose:"today's Pixel Dailies",puts:"the theme and the tags"},weekly:{label:"Weekly Pixels",whose:"this week's Weekly Pixels",puts:"the tags and the account"},inktober:{label:"Inktober",whose:"today's Inktober",puts:"#inktober and the year's tag"}},_r=6e3;function tn(r,e){return Math.max(1,Math.min(32,Math.round(1e3/Math.max(r,e,1))))}function nn(r,e,t,n=null){let o=Ar(n,r.w,r.h),s=o.x===0&&o.y===0&&o.w===r.w&&o.h===r.h?r:Rr(r,o),i=Math.min(e/o.w,t/o.h),a=Math.max(1,Math.round(o.w*i)),c=Math.max(1,Math.round(o.h*i)),h=Or(s,a,c),l=Math.floor((e-a)/2),p=Math.floor((t-c)/2),u=new Uint8Array(e*t*4);for(let d=3;d<u.length;d+=4)u[d]=255;for(let d=0;d<c;d++)u.set(h.pixels.subarray(d*a*4,(d+1)*a*4),((p+d)*e+l)*4);return{w:e,h:t,pixels:u,placed:{x:l,y:p,w:a,h:c},region:o,by:e/o.w<=t/o.h?"width":"height"}}function Ar(r,e,t){let n=Math.max(0,Math.min(e-1,Math.floor(Number(r?.x??0))||0)),o=Math.max(0,Math.min(t-1,Math.floor(Number(r?.y??0))||0)),s=Math.max(1,Math.min(e-n,Math.floor(Number(r?.w??e-n))||e-n)),i=Math.max(1,Math.min(t-o,Math.floor(Number(r?.h??t-o))||t-o));return{x:n,y:o,w:s,h:i}}function Rr(r,e){let t=new Uint8Array(e.w*e.h*4);for(let n=0;n<e.h;n++){let o=((e.y+n)*r.w+e.x)*4;t.set(r.pixels.subarray(o,o+e.w*4),n*e.w*4)}return{w:e.w,h:e.h,pixels:t}}async function Tr(r,e){if(e.length>8&&e[0]===137&&e[1]===80&&e[2]===78&&e[3]===71)return e;let n=e[0]===255&&e[1]===216?"JPEG":e.subarray(0,4).toString()==="GIF8"?"GIF":e.subarray(8,12).toString()==="WEBP"?"WebP":"not a PNG",o="convert it first: python3 references/pngio.py in.jpg out.png (Pillow, or `sips` on a Mac), or any image tool";if(process.platform!=="darwin")throw new Error(`the reference ${r} is ${n==="not a PNG"?n:`a ${n}`}, and this server reads PNG \u2014 ${o}`);let s=Ye(Sr(),`pixie-ref-${kr("sha256").update(e).digest("hex").slice(0,16)}-${Pe(r,Er(r))}.png`),i=await pe(s).catch(()=>null);return i||(await new Promise((a,c)=>{cn("sips",["-s","format","png",r,"--out",s],(h,l,p)=>{h?c(new Error(`the reference ${r} is ${n==="not a PNG"?n:`a ${n}`}, and sips could not convert it (${String(p||h.message).trim()}) \u2014 ${o}`)):a()})}),pe(s))}function Or(r,e,t){let n=new Uint8Array(e*t*4),o=r.w/e,s=r.h/t;for(let i=0;i<t;i++){let a=Math.floor(i*s),c=Math.max(a+1,Math.floor((i+1)*s));for(let h=0;h<e;h++){let l=Math.floor(h*o),p=Math.max(l+1,Math.floor((h+1)*o)),u=0,d=0,m=0,w=0,f=0;for(let x=a;x<c&&x<r.h;x++)for(let b=l;b<p&&b<r.w;b++){let $=(x*r.w+b)*4;u+=r.pixels[$],d+=r.pixels[$+1],m+=r.pixels[$+2],w+=r.pixels[$+3],f++}let y=(i*e+h)*4;f&&n.set([Math.round(u/f),Math.round(d/f),Math.round(m/f),Math.round(w/f)],y)}}return{w:e,h:t,pixels:n}}function Oe(r,e=new Uint8Array(r.length)){for(let t=0;t<r.length;t+=4){let n=Math.round(.299*r[t]+.587*r[t+1]+.114*r[t+2]);e[t]=e[t+1]=e[t+2]=n,e[t+3]=r[t+3]}return e}function Pr(r){if(r==null||r===!1||r==="")return null;let e=String(r).trim().toLowerCase();if(e==="checker"||e==="true")return{checker:!0};let t=/^#?([0-9a-f]{6})$/.exec(e);if(!t)throw new Error(`background: "checker" or a #rrggbb color, not ${JSON.stringify(r)}`);let n=parseInt(t[1],16);return{rgb:[n>>16&255,n>>8&255,n&255]}}var Nr=[[204,204,204],[153,153,153]],Qe=4;function Ke(r,e,t,n,o){if(!o)return r;for(let s=0;s<t;s++)for(let i=0;i<e;i++){let a=(s*e+i)*4,c=r[a+3]/255;if(c===1)continue;let h=o.rgb??Nr[(((n.x+i)/Qe|0)+((n.y+s)/Qe|0))%2];for(let l=0;l<3;l++)r[a+l]=Math.round(r[a+l]*c+h[l]*(1-c));r[a+3]=255}return r}function rn(r){return r?`, on ${r.rgb?`#${r.rgb.map(t=>t.toString(16).padStart(2,"0")).join("")}`:`a ${Qe}px checker`} where the canvas is transparent`:""}function on(r,e,t){let n=new Uint8Array(t.w*t.h*4);for(let o=0;o<t.h;o++){let s=((t.y+o)*e+t.x)*4;n.set(r.subarray(s,s+t.w*4),o*t.w*4)}return n}var Ir=["###,#.#,#.#,#.#,###",".#.,##.,.#.,.#.,###","###,..#,###,#..,###","###,..#,###,..#,###","#.#,#.#,###,..#,..#","###,#..,###,..#,###","###,#..,###,#.#,###","###,..#,..#,..#,..#","###,#.#,###,#.#,###","###,#.#,###,..#,..#"].map(r=>r.split(",")),Ne=5;function Mr(r,e,t,n,o,s,i){let a=String(i).split("").map(l=>Ir[Number(l)]),c=a.length*3+(a.length-1);if(c>s||o+Ne>t)return;let h=n+Math.floor((s-c)/2);for(let l of a){for(let p=0;p<Ne;p++)for(let u=0;u<3;u++)l[p][u]==="#"&&r.set([0,0,0,255],((o+p)*e+h+u)*4);h+=4}}var hn=24;function Cr(r,e=hn,t=4){let n=a=>/^#[0-9a-f]{6}$/i.test(a)?[1,3,5].map(c=>parseInt(a.slice(c,c+2),16)):null,o=r.map(a=>[a,n(a)]).filter(([,a])=>a),s=[];for(let a=0;a<o.length;a++)for(let c=a+1;c<o.length;c++){let h=Math.hypot(...o[a][1].map((l,p)=>l-o[c][1][p]));h<e&&s.push({a:o[a][0],b:o[c][0],d:Math.round(h)})}s.sort((a,c)=>a.d-c.d);let i=new Set(s.flatMap(a=>[a.a,a.b])).size;return{pairs:s.slice(0,t),total:s.length,crowded:i}}function ue(r,e,t){let n=(c,h)=>Number.isFinite(+c)?Math.max(0,Math.round(+c)):h,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 Ie=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.shapeSaid=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:an(),now:Date.now()}),a=new Se(this.core,i,{author:this.author,name:this.name,seed:an(),agent:!0,getToken:this.getToken?()=>this.getToken(e):null});a.onLost=p=>{this.lastRefusal=p},a.onPrompt=p=>{if(p.author===this.author)return;let u=p.where==="library";this.asked.push({text:p.text,from:p.name,at:p.at,wants:u?"a new sprite":"a change to this one",room:u?null:e,askedIn:e}),this.asked.length>32&&this.asked.shift(),this.onAsked?.()};let c=this.connectTransport(e,{w:t,h:n,name:o}),h=!1;return await new Promise((p,u)=>{let d=setTimeout(()=>u(new Error(`timed out joining ${e}`)),1e4);a.onStatus=w=>{w.connected&&(h=!0,clearTimeout(d),p())},a.connect(c);let m=c.onclose;c.onclose=()=>{if(m?.(),h)return this.#n(e,{w:t,h:n,name:o});clearTimeout(d),this.#e(e).then(w=>u(new Error(w)))}}),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)?en.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`:Ft(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=h=>{h.connected&&(i=!0,n.attempts=0)},n.client.connect(s);let c=s.onclose;s.onclose=()=>{c?.(),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=L();this.lastRefusal=null;let a=await zr(s,i,{intent:t,...n?{layers:[n]}:{}});if(a!=="held"){let h=s.leases.find(l=>l.job!==i);throw new Error(a==="refused"&&this.lastRefusal?.message?`could not claim "${e}": ${this.lastRefusal.message}`:h?`"${e}" is already held by another job: ${h.intent}`:a==="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,began:s.state.seq,layer:n,intent:t,cancelled:!1,timer:setInterval(()=>{this.jobs.has(i)&&s.lease("renew",{job:i})},_r)};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(this.lastRoom?`no job is open \u2014 begin_job({ document: "${this.lastRoom}" }) picks ${this.lastRoom} up again, or new_sprite starts another`:"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,t=""){let n=this.jobs.get(e);n&&(clearInterval(n.timer),this.jobs.delete(e),this.lastRoom=n.room,this.client(n.room).lease("release",{job:e,...t?{note:t}:{}}),this.note("finish_job",n.room,t||n.intent))}async#o(e,t){if(!e&&!t&&this.jobs.size===0){if(this.lastRoom)return this.lastRoom;throw new Error(`say which document \u2014 looking needs no job, but the fallback to "the one last finished" is this session's, and nothing has been finished in it. list_documents names every one with its id.`)}return this.#s(e,t)}#c(e,t){return!t||this.shapeSaid.get(e)===t?null:(this.shapeSaid.set(e,t),t)}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(h){throw this.lastRefusal?.kind==="revoked"&&(o.cancelled=!0),this.lastRefusal=null,h}if(this.lastRefusal){let h=this.lastRefusal.message;throw this.lastRefusal.kind==="revoked"&&(o.cancelled=!0),this.lastRefusal=null,new Error(h)}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=Ge(this.serverUrl)?15e3:6e4,n=()=>{fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:this.name,every:t})}).catch(()=>{})};n(),clearInterval(this.heartbeat),this.heartbeat=setInterval(n,t),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:Lr.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.#o(e,t)}`}catch{n=`${this.editorUrl}/library`}return await Dr(n),{ok:!0,message:`Opened ${n}`}}async newSprite({name:e,w:t=D.w,h:n=D.h,intent:o,palette:s,folder:i=null,challenge:a=null}){let c=await this.createDocument?.({name:e,w:t,h:n})??Ct(e),h=await this.begin(c,o,{open:{w:t,h:n,name:e}});a&&(this.jobs.get(h).challenge=a),s?.length&&await this.submit(h,"set_palette",[{t:"setPalette",colors:te(s,"hue")}]),i&&await this.moveDocuments({documents:[c],folder:i,make:!0}),this.#p(c,o);let l=this.#c(c,Te(t,n)??Ve(t,n));return{job:h,...this.#t(c),...l?{shape:l}:{}}}async#p(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=!e&&this.jobs.size===0&&this.lastRoom?this.lastRoom:await this.#s(e,null);return{job:await this.begin(o,t,{layer:n}),...this.#t(o)}}finishJob({job:e,note:t=""}){let n=this.resolve(e),o=typeof t=="string"?t.trim().slice(0,200):"";this.finish(n.job,o);let s=this.client(n.room).state,i=this.#c(n.room,Te(s.doc.w,s.doc.h)),a=jr[n.challenge],h=[a?`This is ${a.whose} entry: tell the user to post it from the share button with the ${a.label} switch on, which puts ${a.puts} on the post.`:null,i].filter(Boolean).join(" "),l=s.seq!==n.began,p=n.pushed?{source:n.pushed}:l?{source:null,note:"nothing pushed \u2014 if a script made this, push_source before the next job, so the next agent (or you, tomorrow) can pick it up rather than redraw it"}:{source:null};return{finished:o||n.intent,...o?{intent:n.intent}:{},job:n.job,document:n.room,drew:l,...h?{next:h}:{},...p}}async#i(e,t={}){if(this.control)try{return await this.control(e,t)}catch(n){if(!this.openControl||!/no library here/.test(n.message))throw n}if(this.openControl)return this.openControl(e,t);throw new Error("no server to push to: this session has no control plane")}async pushSource({job:e,files:t,run:n,takes:o="",unfinished:s=""}){let i=this.resolve(e),a=this.client(i.room),c=(Array.isArray(t)?t:[t]).filter(d=>d!=null&&d!=="");if(!c.length)throw new Error("say which files: the generator, and anything it imports that is yours");let h=c.map(d=>{if(typeof d=="string")return{from:d,name:Pe(d)};let m=d?.from??d?.path;if(typeof m!="string"||!m)throw new Error(`each file is a path, or { from: "<path on disk>", path: "<name to push it as>" } \u2014 got ${JSON.stringify(d)}`);return{from:m,name:Pe(String(d.path??m))}}),l=[];for(let{from:d,name:m}of h){let w=await pe(d).catch(f=>{throw new Error(`could not read ${d}: ${f.code==="ENOENT"?"no such file":f.message}`)});if(w.includes(0))throw new Error(`${d} is not text \u2014 a push is source, not a PNG or a grid; the next agent regenerates those`);l.push({name:m,text:w.toString("utf8")})}let u=(await this.#i(`/documents/${encodeURIComponent(i.room)}/history/source`,{method:"POST",body:{job:i.job,seq:a.state.seq,run:String(n??""),takes:String(o??""),unfinished:String(s??""),name:this.name,files:l}}))?.source??null;return u&&this.jobs.has(i.job)&&(this.jobs.get(i.job).pushed={seq:u.seq,files:u.files.map(d=>d.name)}),this.note("push_source",i.room,`${l.length} file${l.length===1?"":"s"}`),{document:i.room,job:i.job,seq:a.state.seq,files:u?.files??l.map(d=>({name:d.name})),note:"pushed beside this job's mark in the version history; readable by this account only. pull_source({ document, job }) brings it back to disk."}}async pullSource({document:e=null,job:t=null,into:n=null}){let o=null;if(e?o=await this.#s(e,null):this.jobs.size===1?o=this.resolve(null).room:o=this.lastRoom,!o)throw new Error("say which document \u2014 none is open and none was finished in this session");let s=`/documents/${encodeURIComponent(o)}/history/source`;if(!t){let h=await this.#i(s,{}),l=Array.isArray(h?.sources)?h.sources:[];return{document:o,sources:l.map(p=>({job:p.job,seq:p.seq,at:p.at?new Date(p.at).toISOString():null,run:p.run,unfinished:p.unfinished||null,files:p.files.map(u=>u.name)})),note:l.length?"yours on this document, newest first \u2014 pull_source({ job, into }) writes one to disk; `seq` is the document state it reproduces (get_info says where the document is now)":"nothing pushed on this document by this account \u2014 a mark with `source: true` in the history was pushed by somebody else, and is theirs"}}let i=await this.#i(`${s}/${encodeURIComponent(String(t))}`,{});if(!i?.files)throw new Error(`no source of yours under job ${t} on ${o}`);let a=vr(n?String(n):Ye(".context","source",String(t)));await $r(a,{recursive:!0});let c=[];for(let h of i.files){let l=Ye(a,Pe(h.name)),p=await pe(l,"utf8").catch(()=>null);if(p!==null&&p!==h.text)throw new Error(`${l} exists and differs \u2014 pull into another directory, or move it first`);await xr(l,h.text),c.push(l)}return{document:o,job:i.job,seq:i.seq,pushedAt:i.at?new Date(i.at).toISOString():null,by:i.by?.name??null,run:i.run,takes:i.takes||null,unfinished:i.unfinished||null,files:c,note:`written to ${a}. Read the files before running them; run them from there, in the workspace, with nothing the task does not need. They reproduce seq ${i.seq} \u2014 compare get_info's seq to see what happened since.`}}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??[],...n.day?{day:n.day}:{},postedAt:new Date(n.at).toISOString(),post:n.url}:{theme:null,note:`no ${o} prompt is known right now`};return{daily:t(this.news?.dailies,"daily"),weekly:t(this.news?.weekly,"weekly"),inktober:t(this.news?.inktober,"Inktober"),how:'new_sprite({ challenge: "daily" | "weekly" | "inktober", intent }) names the sprite after the prompt; finish_job says how to post it as an entry.'}}async challenge(e){let n=(await this.challenges())[e];if(!n?.theme)throw new Error(e==="inktober"?"no Inktober prompt is known right now \u2014 it runs the 31 days of October, and the list is a year at a time. Ask the user what to draw.":`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.#l();if(o===null||o.some(a=>a.id===n))return n;let s=o.filter(a=>(a.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(a=>a.id).join(", ")}`);let i=/^([A-Za-z0-9]{8,64})_([A-Za-z0-9]{8,64})$/.exec(n);if(i&&o.some(a=>a.id===i[1]))return i[1];if(en.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#l(){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,layers:n=!1}={}){let o=await this.#o(e,t),s=await this.peek(o),i=this.#t(o);return n?{...i,layers:this.#f(s)}:i}#f(e){let t=e.state,{w:n,h:o}=t.doc;return t.doc.layers.map(s=>{let i=0,a=n,c=o,h=-1,l=-1;for(let p=0;p<t.doc.frames.length;p++){let u;try{u=e.session.layerImage(s.id,p)}catch{continue}for(let d=0;d<o;d++)for(let m=0;m<n;m++)u[(d*n+m)*4+3]!==0&&(i++,m<a&&(a=m),m>h&&(h=m),d<c&&(c=d),d>l&&(l=d))}return{name:s.name,pixels:i,bbox:h<0?null:{x:a,y:c,w:h-a+1,h:l-c+1}}})}#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,paletteUsed:this.#y(t),bbox:this.#m(t),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)}}#r(e){let t=e.state,{w:n,h:o}=t.doc,s=n,i=o,a=-1,c=-1;for(let h of t.doc.layers)for(let l=0;l<t.doc.frames.length;l++){let p;try{p=e.session.layerImage(h.id,l)}catch{continue}for(let u=0;u<o;u++)for(let d=0;d<n;d++)p[(u*n+d)*4+3]!==0&&(d<s&&(s=d),d>a&&(a=d),u<i&&(i=u),u>c&&(c=u))}return a<0?null:{x:s,y:i,w:a-s+1,h:c-i+1}}#m(e){try{let t=e.session.composite(0),{w:n,h:o}=e.state.doc,s=n,i=o,a=-1,c=-1;for(let h=0;h<o;h++)for(let l=0;l<n;l++)t[(h*n+l)*4+3]!==0&&(l<s&&(s=l),l>a&&(a=l),h<i&&(i=h),h>c&&(c=h));return a<0?null:{x:s,y:i,w:a-s+1,h:c-i+1,cx:(s+a)/2,cy:(i+c)/2}}catch{return null}}#d(e,t=null){let n=e.state,{w:o,h:s}=n.doc,i=new Set,a=c=>c.toString(16).padStart(2,"0");for(let c of n.doc.layers)if(c.id!==t)for(let h=0;h<n.doc.frames.length;h++){let l;try{l=e.session.layerImage(c.id,h)}catch{continue}for(let p=0;p<o*s*4;p+=4)l[p+3]!==0&&i.add(`#${a(l[p])}${a(l[p+1])}${a(l[p+2])}`)}return[...i]}#y(e){try{let t=e.session.composite(0),n=new Set;for(let o=0;o<t.length;o+=4)t[o+3]!==0&&n.add(t[o]<<16|t[o+1]<<8|t[o+2]);return(e.state.doc.palette??[]).filter(o=>{let s=/^#([0-9a-f]{6})/i.exec(String(o));return s&&n.has(parseInt(s[1],16))}).length}catch{return null}}#g(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#w(e,t){let n=Math.trunc(Number(t));if(!Number.isFinite(n)||n<0)throw new Error(`\`seq\` is a document sequence \u2014 the number get_info reports and every draw_art result carries; got ${JSON.stringify(t)}`);let s=(await this.peek(e)).state.seq,i=await this.#i(`/documents/${encodeURIComponent(e)}/history/at/${n}`,{}).catch(async c=>{let l=((await this.#i(`/documents/${encodeURIComponent(e)}/history`,{}).catch(()=>null))?.versions??[]).map(u=>u.seq).filter(u=>Number.isFinite(u)),p=l.length?` Replay starts from an anchor: ${[...new Set(l)].sort((u,d)=>u-d).join(", ")}.`:"";throw new Error(`seq ${n} of ${e} cannot be rebuilt (${c.message}) \u2014 the document is at seq ${s}.${p}`)});if(!i||typeof i!="object")throw new Error(`seq ${n} of ${e} came back empty \u2014 the history has the operations but no document to replay them onto`);let a=this.core.openSession(i,this.author,1);return{past:!0,now:s,state:{doc:a.state().doc,seq:n,pending:0},session:a,connected:!0,welcomed:!0,close:()=>{try{a.close()}catch{}}}}async readArt({document:e,job:t,layer:n,frame:o=0,trim:s=!1,seq:i=null}){let a=await this.#o(e,t);if(i!=null){let h=await this.#w(a,i);try{return{document:a,at:h.state.seq,...h.session.readArt(this.#a(h.state.doc,n,a),o,{trim:s}),note:`the document as it stood at seq ${h.state.seq}, rebuilt from the history; the room is now at ${h.now}. A read of the past, not a restore.`}}finally{h.close()}}let c=await this.peek(a);return{document:a,...c.session.readArt(this.#h(a,n),o,{trim:s}),...this.#g(a)}}async readPng({document:e,job:t,layer:n=null,frame:o=0,seq:s=null}){let i=await this.#o(e,t),a=s!=null?await this.#w(i,s):null,c=a??await this.peek(i);try{let h=c.state.doc,l=a?{at:a.state.seq}:{};if(n==null)return{document:i,...l,png:c.session.png(o,{scale:1}),w:h.w,h:h.h};let p=c.session.cel(this.#a(h,n,i),o);return{document:i,...l,png:this.#u(h.w,h.h,p),w:h.w,h:h.h}}finally{a?.close()}}#u(e,t,n,o=1){let s=this.core.newDocument({w:e,h:t,name:"scratch",seed:1,now:Date.now()}),i=this.core.openSession(s,L(),1);try{let a=i.state();return i.apply([Dt(a.doc.layers[0].id,a.doc.frames[0].id,{x:0,y:0,w:e,h:t},n)],{now:Date.now()}),i.png(0,{scale:o})}finally{i.close()}}async#k(e,t,n,o,s,i=!1,a=null,c=!1,h=null){let{png:l,from:p}=await this.#S(t),u=this.core.decodePng(l),d=e.state.doc,m=Math.abs(u.w/u.h-d.w/d.h)<.01,w=c&&(!!a||!m),f=w?nn(u,n.w,n.h,a):nn(u,d.w,d.h,a),y=e.session.composite(o);h&&Ke(y,d.w,d.h,{x:0,y:0},h);let x=i?Oe(y):y;i&&Oe(f.pixels,f.pixels);let b=n.w,$=n.h,E=new Uint8Array((b*2+1)*$*4),G=(b*2+1)*4;for(let C=0;C<$;C++){for(let R=0;R<b;R++){let H=n.x+R,T=n.y+C,M=w?R:H,B=w?C:T,ge=(B*f.w+M)*4,V=B<f.h&&M<f.w;E.set(V?f.pixels.subarray(ge,ge+4):[0,0,0,255],C*G+R*4);let be=(T*d.w+H)*4;E.set(x.subarray(be,be+4),C*G+(b+1+R)*4)}E.set([255,0,255,255],C*G+b*4)}let v=f.placed,I=f.region.w===u.w&&f.region.h===u.h,q=!!a&&I,J=I?q?`\`referenceRect\` ${f.region.w}\xD7${f.region.h} at (${f.region.x}, ${f.region.y}) covers the whole ${u.w}\xD7${u.h} reference, so this is all of it`:`the reference is ${u.w}\xD7${u.h}`:`the region ${f.region.w}\xD7${f.region.h} at (${f.region.x}, ${f.region.y}) of the ${u.w}\xD7${u.h} reference`,j=f.region.w/v.w,A=`${j>=10?Math.round(j):j.toFixed(1)} reference px per canvas px`,F=w?I?`fitted whole into the ${n.w}\xD7${n.h} rect by ${f.by} (${v.w}\xD7${v.h}, centered; black where it does not reach; ${A}) \u2014 the whole reference against this part of the canvas${q?", as asked; name a SMALLER `referenceRect` to bring one part of it":"; pass `referenceRect` to bring one part of it instead"}`:`fitted into the ${n.w}\xD7${n.h} rect by ${f.by} (${v.w}\xD7${v.h}, centered; black where it does not reach; ${A}) \u2014 that part of the reference against this part of the canvas, at one scale`:v.w===d.w&&v.h===d.h?`brought to the ${d.w}\xD7${d.h} canvas (${A})`:`fitted whole to ${v.w}\xD7${v.h} by ${f.by}, centered on the ${d.w}\xD7${d.h} canvas (black where it does not reach; ${A})`,z=I&&v.w===d.w&&v.h===d.h?Math.abs(j-1)<.05?"":` \u2014 ${A}`:` \u2014 ${J}, ${F}${I&&!q&&!w&&(v.w!==d.w||v.h!==d.h)?"; pass `referenceRect` to bring one part of it to the grid instead":""}`;return{png:this.#u(b*2+1,$,E,s),note:`${p}${z}`}}async#S(e){let t=String(e).trim(),n=null;if(/^[a-z][a-z0-9+.-]*:\/\//i.test(t)){if(n=oe(t),n===t)throw new Error(`could not read the reference ${t}: a URL is read only when it is a Pixie link (/d/\u2026 or /s/\u2026) \u2014 save anything else to a file and pass the path`)}else{let a=await pe(t).catch(l=>l);if(!(a instanceof Error))return{png:await Tr(t,a),from:""};let c=`could not read the reference ${t}: ${a.code==="ENOENT"?"no such file":a.message}`;if(a.code!=="ENOENT")throw new Error(c);let h=oe(t);if(h!==t)n=h;else{if(/[\\/]/.test(t)||/\.[a-z0-9]{2,4}$/i.test(t))throw new Error(c);n=await this.#s(t,null).catch(()=>{throw new Error(`${c}, and no document by that name or id either \u2014 a reference is a path on this machine, a document's id, or its link`)})}}let o=await this.#b(await this.#s(n,null)),{w:s,h:i}=o.state.doc;return{png:o.session.png(0,{scale:1}),from:` \u2014 the reference is the document ${n} at seq ${o.state.seq} (${s}\xD7${i}, frame 1)`}}async preview({document:e,job:t,frame:n=0,scale:o=null,rect:s=null,rects:i=null,reference:a=null,referenceRect:c=null,values:h=!1,background:l=null,file:p=null,overwrite:u=!1,seq:d=null}){let m=await this.#v({document:e,job:t,frame:n,scale:o,rect:s,rects:i,reference:a,referenceRect:c,values:h,background:l,seq:d});if(p){let w=String(p),f=await ve(w,m.png,{overwrite:u,seq:m.seq});m.caption+=` \u2014 written to ${w}${f?" (replaced; anything that showed this path before shows the old picture)":""}`}return m}async#b(e){let t=await this.peek(e);if(t.state.pending>0&&await Fr(t,3e3),!t.connected||!t.welcomed)throw new Error(`the room connection dropped \u2014 a render now could be behind what the room holds (last confirmed seq ${t.state.seq}). Reconnecting; try again in a moment.`);return t}#E(e,t,n,o,s,i,a){let c=o.map(f=>ue(f,t.w,t.h)),h=1,l=c.reduce((f,y)=>f+y.w,0)+h*(c.length-1),p=Math.max(...c.map(f=>f.h)),u=p+Ne+2,d=new Uint8Array(l*u*4),m=e.session.composite(n),w=0;return c.forEach((f,y)=>{let x=on(m,t.w,f);Ke(x,f.w,f.h,f,a),i&&Oe(x,x);for(let b=0;b<u;b++)for(let $=0;$<f.w;$++){let E=(b*l+w+$)*4;b>=f.h?d.set([255,0,255,255],E):d.set(x.subarray((b*f.w+$)*4,(b*f.w+$)*4+4),E)}if(Mr(d,l,u,w,p+1,f.w,y+1),w+=f.w,w<l){for(let b=0;b<u;b++)d.set([255,0,255,255],(b*l+w)*4);w+=h}}),{png:this.#u(l,u,d,s),wins:c,w:l,h:u}}async#v({document:e,job:t,frame:n=0,scale:o=null,rect:s=null,rects:i=null,reference:a=null,referenceRect:c=null,values:h=!1,background:l=null,seq:p=null}){let u=Pr(l),d=await this.#o(e,t),m=p!=null?await this.#w(d,p):null;try{return await this.#j({room:d,view:m??await this.#b(d),past:m,frame:n,scale:o,rect:s,rects:i,reference:a,referenceRect:c,values:h,bg:u})}finally{m?.close()}}async#j({room:e,view:t,past:n,frame:o,scale:s,rect:i,rects:a,reference:c,referenceRect:h,values:l,bg:p}){let u=t,{warning:d}=n?{}:this.#g(e),m=u.state.seq,w=n?` (rebuilt from the history; the room is now at ${n.now})`:"",f=u.state.doc;if(Array.isArray(a)&&a.length){if(c)throw new Error("one rect at a time against a reference \u2014 `rects` tiles crops of the render alone");let j=a.reduce((M,B)=>M+ue(B,f.w,f.h).w,0)+a.length-1,A=Math.max(...a.map(M=>ue(M,f.w,f.h).h))+Ne+2,F=Number.isInteger(s)&&s>0?s:tn(j,A),z=Math.max(1,Math.min(F,Math.floor(8192/Math.max(j,A,1)))),{png:C,wins:R}=this.#E(u,f,o,a,z,l,p),H=z<F?` \u2014 asked for \xD7${F}, held to \xD7${z} so a side stays under 8192px; fewer or smaller rects to look closer`:"",T=R.map((M,B)=>`${B+1}${a[B]?.label?` ${String(a[B].label).slice(0,48)}`:""} ${M.w}\xD7${M.h} at (${M.x},${M.y})`).join(", ");return{png:C,seq:m,caption:`${e} at seq ${m}${w}, frame ${o+1}: ${R.length} crops of ${f.w}\xD7${f.h} side by side at \xD7${z}, magenta between, each numbered in the strip under it \u2014 ${T}${l?", values only":""}${rn(p)}${H}${d?` \u2014 ${d}`:""}`}}let y=i?ue(i,f.w,f.h):null,x=y?y.w:f.w,b=y?y.h:f.h,$=Number.isInteger(s)&&s>0?s:tn(c?x*2:x,b),E=Math.max(1,Math.min($,Math.floor(8192/Math.max(c?x*2:x,b,1)))),G=y?`${y.w}\xD7${y.h} at (${y.x},${y.y}) of ${f.w}\xD7${f.h}`:`${f.w}\xD7${f.h}`,v=E<$?` \u2014 asked for \xD7${$}, held to \xD7${E} so a side stays under 8192px; render a smaller rect to look closer`:"",I=rn(p);if(c){let{png:j,note:A}=await this.#k(u,String(c),y??{x:0,y:0,w:f.w,h:f.h},o,E,l,h,!!y,p);return{png:j,seq:m,caption:`${e} at seq ${m}${w}, frame ${o+1}: reference (left) beside the render (right), ${G} at \xD7${E}${l?", values only":""}${I}${A}${v}${d?` \u2014 ${d}`:""}. Per object, say how each is built \u2014 count the faces, name the light, say where the shadow falls \u2014 and what differs; "reads as" is not a verdict.`}}let q=y??{x:0,y:0,w:f.w,h:f.h};return{png:l||p?this.#u(x,b,(()=>{let j=on(u.session.composite(o),f.w,q);return Ke(j,x,b,q,p),l?Oe(j,j):j})(),E):u.session.png(o,{scale:E,...y?{rect:y}:{}}),seq:m,caption:`${e} at seq ${m}${w}, frame ${o+1}: ${G} at \xD7${E} (${x*E}\xD7${b*E}px)${l?", values only \u2014 squint: is there a focal point, and where is the darkest dark":""}${I}${v}${d?` \u2014 ${d}`:""}`}}async sheet({document:e,job:t,columns:n=4,scale:o=2}){let s=await this.#o(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,h])=>({job:c,document:h.room,intent:h.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(h=>h.ok?h.json():null).then(h=>h?.documents?.map(l=>l.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."}}#h(e,t){return this.#a(this.client(e).state.doc,t,e)}async#x(e,t,n=null){let o=this.client(e.room).state.doc;if(typeof t!="string"||t==="")return{id:this.#a(o,t,e.room),added:null};if(o.layers.some(i=>i.name===t||i.id===t))return{id:this.#a(o,t,e.room),added:null};let s=We(t,o.layers.map(i=>i.name));if(s)throw new Error(`no layer ${JSON.stringify(t)} in ${e.room} \u2014 did you mean ${JSON.stringify(s)}? A name this close to one that exists is a typo, and making it would put this pass on a layer nobody is looking at. add_layer({ name }) if it really is a new one.`);return await this.addLayer({job:e.job,name:t,below:n}),{id:this.#h(e.room,t),added:t}}#a(e,t,n){if(t!=null&&t!==""){let o=typeof t=="number"?e.layers[t]:e.layers.find(s=>s.name===t||s.id===t);if(!o)throw new Error(`no layer ${JSON.stringify(t)} in ${n}`);return o.id}return e.layers[e.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,below:h=null,frame:l=0,label:p,palette:u="add",rect:d=null}){let m=this.resolve(e),w=this.client(m.room),f=t,y={x:o,y:s},x=a;if(d){let g=ue(d,Math.max(...t.map(O=>O.length),1),t.length);if(f=t.slice(g.y,g.y+g.h).map(O=>O.slice(g.x,g.x+g.w).padEnd(g.w,".")),y={x:g.x,y:g.y},a){let O=[..."~^`|"].find(k=>!(k in n))??"\xA1";f=f.map(k=>k.replace(/[. ]/g,O)),n={...n,[O]:null},x=!1}}let b=new Set((w.state.doc.palette??[]).map(g=>String(g).toLowerCase())),$=(()=>{try{return Uint8Array.from(w.session.composite(l))}catch{return null}})(),{id:E,added:G}=c!=null?await this.#x(m,c,h):{id:m.layer??this.#h(m.room,null),added:null},v=w.session.drawArt(f,n,{layer:E,at:l,x:y.x,y:y.y,center:d?!1:i,clear:x,addToPalette:u!=="keep"}),{committed:I}=await this.submit(m.job,p||"draw_art",v),q=(this.client(m.room).state.doc.palette??[]).map(g=>String(g).toLowerCase()).filter(g=>!b.has(g)),J=w.state.doc,j=Math.max(...f.map(g=>g.length),0),A=i&&!d?{x:Math.round((J.w-j)/2),y:Math.round((J.h-f.length)/2)}:y,F=0,z=0;for(let g=0;g<f.length;g++){let O=A.y+g;for(let k=0;k<f[g].length;k++){let se=f[g][k];if(se==="."||se===" ")continue;let $e=A.x+k;$e<0||O<0||$e>=J.w||O>=J.h?z++:F++}}let C=F;if(I&&$)try{let g=w.session.composite(l),O=0;for(let k=0;k<g.length;k+=4)(g[k]!==$[k]||g[k+1]!==$[k+1]||g[k+2]!==$[k+2]||g[k+3]!==$[k+3])&&O++;C=O}catch{}let R=[],H=w.state.doc.layers.length-1;if(u==="replace"&&I){let g=U=>typeof U=="string"&&/^#[0-9a-f]{6}$/i.test(U)?U.toLowerCase():null,O=d?null:new Set(f.flatMap(U=>[...U])),k=Object.entries(n).filter(([U,mn])=>g(mn)&&(O===null||O.has(U))),se=[...new Set(k.map(([,U])=>g(U)))],$e=new Set(se);R=[...new Set(Object.values(n).map(g).filter(U=>U&&!$e.has(U)))];let fn=H>0?this.#d(this.client(m.room),E):[],ot=[...new Set([...se,...fn])];ot.length&&await this.submit(m.job,"set_palette",[{t:"setPalette",colors:te(ot,"hue")}])}let T=(this.client(m.room).state.doc.palette??[]).length,M=this.#y(this.client(m.room))??T,B=(d?`the file's \u2014 the whole picture's, not the ${f[0]?.length??0}\xD7${f.length} that was drawn; nothing outside the rect moved`:"this drawing's")+(u==="replace"&&H>0?`, and what the other ${H} layer${H===1?" is":"s are"} drawn in \u2014 a replace is of the document, so it keeps them`:""),ge=R.length?`; ${R.length} in \`colors\` landed on no pixel and ${R.length===1?"was":"were"} left out: ${R.join(" ")}`:"",V=u==="replace"&&T>48?Cr(this.client(m.room).state.doc.palette??[]):{pairs:[],total:0,crowded:0},be=V.crowded*3>T,un=V.pairs.length&&!be?` \u2014 ${T} colors, and ${V.total} pair${V.total===1?"":"s"} of them within ${hn} RGB of each other (${V.pairs.map(g=>`${g.a}\u2248${g.b}`).join(", ")}${V.total>V.pairs.length?", \u2026":""}): ramps that read alike, and a merge if they are not separate materials`:"",Le=z?`${z} pixel${z===1?"":"s"} of the ${j}\xD7${f.length} grid at (${A.x},${A.y}) fell outside the ${J.w}\xD7${J.h} canvas and were not drawn \u2014 resize_canvas first, or generate at the canvas's size`:"",xe=u==="replace"&&I?{note:`palette replaced: ${T} color${T===1?"":"s"}, ${B}${ge}${un}`}:u==="keep"&&I?{note:`palette kept at ${T}: this drawing's colors are on the canvas and not in the palette`}:q.length?{added:te(q,"hue"),note:`${q.length} color${q.length===1?"":"s"} joined the palette${T>48||T>M*2?` \u2014 ${T} entries now and ${M} with a pixel on them`:""} \u2014 for a generated piece redrawn from a script, pass \`palette: "replace"\` on this call and the palette is exactly what you drew; set_palette({ prune: true }) is the user's document and is theirs to ask for`}:{},pn=Le?{...xe,note:xe.note?`${xe.note}; ${Le}`:Le}:xe;return{...this.#t(m.room),drew:d?`${f[0]?.length??0}\xD7${f.length} at (${y.x},${y.y})`:`${t[0]?.length??0}\xD7${t.length}`,changed:C,...G?{layerAdded:G}:{},...C!==F?{inked:F}:{},committed:I,...pn}}async resizeCanvas({job:e,w:t,h:n,anchor:o="center",clear:s=!1}){let i=this.resolve(e),a=this.client(i.room).state,c=Math.max(1,Math.min(1024,Math.round(Number(t)||a.doc.w))),h=Math.max(1,Math.min(1024,Math.round(Number(n)||a.doc.h)));if(c===a.doc.w&&h===a.doc.h)return s&&await this.#$(i),{...this.#t(i.room),resized:s?"already that size; cleared":"already that size"};let l=String(o??"center").toLowerCase(),p=l.includes("left")?0:l.includes("right")?2:1,u=l.includes("top")?0:l.includes("bottom")?2:1,d=Math.round((c-a.doc.w)*p/2),m=Math.round((h-a.doc.h)*u/2);await this.submit(i.job,"resize_canvas",[{t:"resize",w:c,h,dx:d,dy:m}]),s&&await this.#$(i);let w=this.#c(i.room,Te(c,h)??Ve(c,h));return{...this.#t(i.room),resized:`${a.doc.w}\xD7${a.doc.h} \u2192 ${c}\xD7${h}, ${s?"cleared":`the art kept at ${d},${m}`}`,...w?{shape:w}:{}}}async closeDocument({document:e}){let t=await this.#s(e,null),n=this.rooms.get(t);if(!n)return{closed:t,open:!1};for(let[o,s]of this.jobs)s.room===t&&this.finish(o);return n.closing=!0,clearTimeout(n.retry),n.client.close(),this.rooms.delete(t),{closed:t,open:!1}}async drawAnimation({job:e,frames:t,colors:n,fps:o,layer:s,label:i}){let a=this.resolve(e),c=this.client(a.room),{id:h}=s!=null?await this.#x(a,s):{id:a.layer??this.#h(a.room,null)},l=c.state.doc.frames.length,p=t.length-l;if(p>0){let d=[],m=c.state.doc.frames[l-1]?.index??"V";for(let w=0;w<p;w++)m=m+"V",d.push({t:"addFrame",id:L(),index:m,durMs:Xe(o)});await this.submit(a.job,"add frames",d)}if(o){let d=Xe(o);await this.submit(a.job,"set fps",c.state.doc.frames.map(m=>({t:"setFrameDur",id:m.id,durMs:d})))}let u=c.session.drawAnimation(t,n,{layer:h});return await this.submit(a.job,i||"draw_animation",u),{...this.#t(a.room),wrote:`${t.length} frames`}}async addLayer({job:e,name:t,below:n=null,above:o=null}){let s=this.resolve(e),i=this.client(s.room).state,a=i.doc.layers,h=(a[a.length-1]?.index??"V")+"V";if(n!=null||o!=null){if(n!=null&&o!=null)throw new Error("`below` or `above`, not both \u2014 name the one neighbour the new layer goes next to");let l=a.indexOf(a.find(d=>d.id===this.#a(i.doc,n??o,s.room))),p=n!=null?a[l-1]?.index:a[l].index,u=n!=null?a[l].index:a[l+1]?.index;h=this.core.call("frac.between",{...p?{lo:p}:{},...u?{hi:u}:{}}).index}return await this.submit(s.job,"add_layer",[{t:"addLayer",id:L(),index:h,name:t||`Layer ${i.doc.layers.length+1}`,visible:!0,locked:!1,opacity:1}]),this.#t(s.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=L(),c=[{t:"addFrame",id:a,index:(i?.index??"V")+"V",durMs:i?.durMs??83}];if(t&&i)for(let h of s.doc.layers){let l=o.session.cel(h.id,i.id);l.every(p=>p===0)||c.push(Ze(h.id,a,s.doc,l))}return await this.submit(n.job,"add_frame",c),this.#t(n.room)}async renameLayer({job:e,layer:t,name:n}){let o=this.resolve(e),s=this.client(o.room).state.doc,i=this.#a(s,t,o.room),a=String(n??"").trim();if(!a)throw new Error("a layer needs a name \u2014 pass `name`");if(s.layers.find(h=>h.name===a&&h.id!==i))throw new Error(`another layer is already called ${JSON.stringify(a)} \u2014 a layer is found by its name, so two of one name is a draw_art landing on the wrong one`);return await this.submit(o.job,"rename_layer",[{t:"setLayerProp",id:i,prop:"name",value:a}]),this.#t(o.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,c=new Set;for(let l of a.doc.layers)for(let p of a.doc.frames){let u=i.session.cel(l.id,p.id);for(let d=0;d<u.length;d+=4)u[d+3]!==0&&c.add(`#${[u[d],u[d+1],u[d+2]].map(m=>m.toString(16).padStart(2,"0")).join("")}`)}let h=a.doc.palette??[];s=h.filter(l=>c.has(String(l).toLowerCase()));for(let l of c)s.some(p=>String(p).toLowerCase()===l)||s.push(l);return s=te(s,"hue"),await this.submit(o.job,"set_palette",[{t:"setPalette",colors:s}]),{...this.#t(o.room),palette:s.length,pruned:h.length-s.length}}return s=te(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=Xe(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#$(e){let t=this.client(e.room).state,n=new Uint8Array(t.doc.w*t.doc.h*4),o=[];for(let s of t.doc.layers)for(let i of t.doc.frames)o.push(Ze(s.id,i.id,t.doc,n));o.length&&await this.submit(e.job,"clear",o)}async clearCel({job:e,layer:t,frame:n=0}){let o=this.resolve(e),i=this.client(o.room).state,a=this.#h(o.room,t??o.layer),c=i.doc.frames[n];if(!c)throw new Error(`no frame ${n} in ${o.room}`);let h=new Uint8Array(i.doc.w*i.doc.h*4);return await this.submit(o.job,"clear",[Ze(a,c.id,i.doc,h)]),this.#t(o.room)}async trim({job:e,margin:t=0,center:n=!1}){let o=this.resolve(e),s=this.client(o.room),i=s.state,a=this.#r(s),c=i.doc.layers.length>1?` every one of the ${i.doc.layers.length} layers`:"";if(n){if(!a)return{...this.#t(o.room),centered:"nothing drawn"};let p=Math.floor((i.doc.w-a.w)/2)-a.x,u=Math.floor((i.doc.h-a.h)/2)-a.y;if(!p&&!u)return{...this.#t(o.room),centered:"already centered"};await this.submit(o.job,"center_art",[{t:"resize",w:i.doc.w,h:i.doc.h,dx:p,dy:u}]);let d=[(i.doc.w-a.w)%2?"width":null,(i.doc.h-a.h)%2?"height":null].filter(Boolean);return{...this.#t(o.room),centered:`the ${a.w}\xD7${a.h} of art${c?` on${c}`:""} moved ${p},${u}${d.length?` \u2014 the ${d.join(" and ")} cannot be split evenly, so it sits half a pixel up-left`:""}`}}if(!a)return{...this.#t(o.room),trimmed:"nothing drawn"};let h=a.w+t*2,l=a.h+t*2;return h>=i.doc.w&&l>=i.doc.h?{...this.#t(o.room),trimmed:"already tight"}:(await this.submit(o.job,"trim_canvas",[{t:"resize",w:h,h:l,dx:-a.x+t,dy:-a.y+t}]),{...this.#t(o.room),trimmed:`${h}\xD7${l}${c?`, to the art on${c}`:""}`})}},Lr=new Set(["rename_layer","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","resize_canvas","close_document","get_info","list_documents","challenges","take_requests","create_folder","move_documents","push_source","pull_source"]);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:Ur(n)}}function Ur(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 Dr(r){let e=process.platform==="darwin"?"open":process.platform==="win32"?"start":"xdg-open";return new Promise(t=>cn(e,[r],()=>t()))}function Xe(r){return!r||r<=0?83:Math.max(10,Math.round(1e3/r))}function qr(r,e,t=8e3){return new Promise(n=>{let o=r.onLeases,s=r.onLost,i=!1,a=h=>{i||(i=!0,clearTimeout(c),r.onLeases=o,r.onLost=s,n(h))};r.onLeases=h=>{o?.(h),h.some(l=>l.job===e)&&a("held")},r.onLost=h=>{s?.(h),h?.ops?.length||a("refused")};let c=setTimeout(()=>a("silent"),t);r.leases.some(h=>h.job===e)&&a("held")})}function sn(r,e=8e3){return new Promise(t=>{let n=Date.now(),o=()=>{if(r.welcomed)return t(!0);if(Date.now()-n>e)return t(!1);setTimeout(o,20)};o()})}async function zr(r,e,t,{ms:n}={}){await sn(r,n);let o=()=>{let i=qr(r,e,n);return r.lease("acquire",{job:e,...t}),i},s=await o();return s==="refused"&&!r.welcomed&&await sn(r,n)&&(s=await o()),s}function Fr(r,e=400){return new Promise(t=>{let n=Date.now(),o=()=>{if(r.state.pending===0&&Date.now()-n>40||Date.now()-n>e)return t();setTimeout(o,10)};o()})}function an(){return(Date.now()^Math.floor(Math.random()*4294967295))>>>0}import{execFile as Br}from"node:child_process";var Wr=2e4,et="a past seq is a room thing \u2014 the history lives beside the document in a room, and a local editor keeps none. Save each pass with render_preview({ file }) and stack them with references/judge.py",Gr=5e3,fe=class r{constructor({base:e,name:t="agent"}={}){this.base=String(e).replace(/\/$/,""),this.name=t,this.log=[],this.beat=setInterval(()=>void this.announce(),Gr),this.beat.unref&&this.beat.unref()}get kind(){return"local"}get where(){return this.base}supports(e){return Jr.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=Wr){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=D.w,h:n=D.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??[],...o.day?{day:o.day}:{},postedAt:new Date(o.at).toISOString(),post:o.url}:{theme:null,note:`no ${s} prompt is known right now`};return{daily:n(t?.dailies,"daily"),weekly:n(t?.weekly,"weekly"),inktober:n(t?.inktober,"Inktober")}}async challenge(e){let n=(await this.challenges())[e];if(!n?.theme)throw new Error(e==="inktober"?"no Inktober prompt is known right now \u2014 it runs the 31 days of October. Ask the user what to draw.":`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,layers:t=!1}={}){if(t)throw new Error("a per-layer bbox is a room thing \u2014 the frozen editor reports no such detail. read_art({ layer, png }) per layer and measure them with references/pngio.py");let n=await this.call("info",[]);return{editor:this.base,...e?{asked:e}:{},...n}}readArt({document:e,layer:t,frame:n,seq:o=null}){if(o!=null)throw new Error(et);return this.call("readArt",[{document:e,layer:t,frame:n}])}async preview({document:e,frame:t,scale:n=null,rect:o=null,rects:s=null,reference:i=null,referenceRect:a=null,values:c=!1,background:h=null,file:l=null,overwrite:p=!1,seq:u=null}){if(o)throw new Error("the local editor cannot render a region \u2014 render the whole frame");if(s)throw new Error("the local editor cannot render regions \u2014 render the whole frame, or read_art({ png }) and tile the crops with references/compare.py");if(c)throw new Error("the local editor cannot render values only \u2014 read_art({ png }) and convert it, or run the squint test with references/compare.py");if(i||a)throw new Error("a reference beside the render is a room thing \u2014 in a local editor, read_art({ file }) and compare with references/compare.py");if(h)throw new Error("a background under the alpha is a room thing \u2014 in a local editor, read_art({ png }) and put it over one with references/render.py");if(u!=null)throw new Error(et);let d=await this.call("toDataURL",[t??null,Number.isInteger(n)&&n>0?n:8,e??null]),m=tt(d.url),w=d.caption;if(l){let f=String(l),y=await ve(f,m,{overwrite:p});w+=` \u2014 written to ${f}${y?" (replaced; anything that showed this path before shows the old picture)":""}`}return{png:m,caption:w}}async readPng({document:e,frame:t=0,layer:n=null,seq:o=null}){if(o!=null)throw new Error(et);if(n!=null)throw new Error("one layer as a PNG is a room thing \u2014 in a local editor, read_art({ layer, file }) and render it with references/render.py");let s=await this.call("toDataURL",[t??null,1,e??null]),i=/(\d+)×(\d+)/.exec(s.caption??"");return{document:e??null,png:tt(s.url),w:i?Number(i[1]):0,h:i?Number(i[2]):0}}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:tt(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,below:h=null,frame:l,label:p,rect:u=null}){if(h!=null)throw new Error("`below` is a room thing \u2014 the frozen editor makes new layers on top");if(u){let d=Math.max(0,u.x|0),m=Math.max(0,u.y|0);t=t.slice(m,m+(u.h|0)).map(w=>w.slice(d,d+(u.w|0))),o=d,s=m,i=!1}return this.call("drawArt",[t,n,{job:e,x:o,y:s,center:i,clear:a,layer:c,frame:l,label:p}])}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,below:t=null,above:n=null}){if(t!=null||n!=null)throw new Error("a layer under or over another is a room thing \u2014 the frozen editor adds on top; add it, then reorder it in the editor");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 Hr(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)}},Jr=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 tt(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 Hr(r){let e=process.platform==="darwin"?"open":process.platform==="win32"?"start":"xdg-open";return new Promise(t=>Br(e,[r],()=>t()))}function nt(r="room"){return`Pixel art editor with live collaboration. ${r==="local"?`Documents are tabs in a browser on this machine; the user watches as you
|
|
9
|
+
`;await Xt(Yt(d),{recursive:!0}).catch(()=>{}),await Ke(d,u).catch(w=>{throw new Error(`could not write ${d}: ${w.message}`)});let m=l.filter(w=>w.ok===!1).length;return se({...s,traceFile:d,traceCalls:h,traceFailed:m,...p?{traceDropped:p}:{},...c.length?{traceEarlier:c.length}:{},traceNote:`${l.length} entr${l.length===1?"y":"ies"} written to ${d}, one JSON object per line, oldest first \u2014 ${c.length?`the first ${c.length} from earlier sessions' finished jobs on ${a.length===1?"this document":"these documents"} (marked \`earlier: true\`), then this session's; `:""}this call is the last of them, and its result carries \`versions\`, so the file says which pixie-mcp wrote it. That file is the report's Trace section; say under the heading what stands out in it.`})}return se({...s,trace:l,traceCalls:h,...p?{traceDropped:p}:{},...c.length?{traceEarlier:c.length}:{},traceNote:`every call this session made, oldest first${c.length?`, after ${c.length} from earlier sessions' finished jobs on the same documents (\`earlier: true\`)`:""} \u2014 save as trace.jsonl, one entry per line. This call is in it. Or pass \`file\` and the server writes it.`})}return t(s)?s:se(s)}}))}var an=/^[A-Za-z0-9_-]{1,64}$/,Ur={daily:{label:"Pixel Dailies",whose:"today's Pixel Dailies",puts:"the theme and the tags"},weekly:{label:"Weekly Pixels",whose:"this week's Weekly Pixels",puts:"the tags and the account"},inktober:{label:"Inktober",whose:"today's Inktober",puts:"#inktober and the year's tag"}},Lr=6e3;function cn(r,e){return Math.max(1,Math.min(32,Math.round(1e3/Math.max(r,e,1))))}function ln(r,e,t,n=null){let o=Dr(n,r.w,r.h),s=o.x===0&&o.y===0&&o.w===r.w&&o.h===r.h?r:Fr(r,o),i=Math.min(e/o.w,t/o.h),a=Math.max(1,Math.round(o.w*i)),c=Math.max(1,Math.round(o.h*i)),l=qr(s,a,c),h=Math.floor((e-a)/2),p=Math.floor((t-c)/2),d=new Uint8Array(e*t*4);for(let u=3;u<d.length;u+=4)d[u]=255;for(let u=0;u<c;u++)d.set(l.pixels.subarray(u*a*4,(u+1)*a*4),((p+u)*e+h)*4);return{w:e,h:t,pixels:d,placed:{x:h,y:p,w:a,h:c},region:o,by:e/o.w<=t/o.h?"width":"height"}}function Dr(r,e,t){let n=Math.max(0,Math.min(e-1,Math.floor(Number(r?.x??0))||0)),o=Math.max(0,Math.min(t-1,Math.floor(Number(r?.y??0))||0)),s=Math.max(1,Math.min(e-n,Math.floor(Number(r?.w??e-n))||e-n)),i=Math.max(1,Math.min(t-o,Math.floor(Number(r?.h??t-o))||t-o));return{x:n,y:o,w:s,h:i}}function Fr(r,e){let t=new Uint8Array(e.w*e.h*4);for(let n=0;n<e.h;n++){let o=((e.y+n)*r.w+e.x)*4;t.set(r.pixels.subarray(o,o+e.w*4),n*e.w*4)}return{w:e.w,h:e.h,pixels:t}}async function zr(r,e){if(e.length>8&&e[0]===137&&e[1]===80&&e[2]===78&&e[3]===71)return e;let n=e[0]===255&&e[1]===216?"JPEG":e.subarray(0,4).toString()==="GIF8"?"GIF":e.subarray(8,12).toString()==="WEBP"?"WebP":"not a PNG",o="convert it first: python3 references/pngio.py in.jpg out.png (Pillow, or `sips` on a Mac), or any image tool";if(process.platform!=="darwin")throw new Error(`the reference ${r} is ${n==="not a PNG"?n:`a ${n}`}, and this server reads PNG \u2014 ${o}`);let s=et(Ir(),`pixie-ref-${Pr("sha256").update(e).digest("hex").slice(0,16)}-${Pe(r,Mr(r))}.png`),i=await we(s).catch(()=>null);return i||(await new Promise((a,c)=>{fn("sips",["-s","format","png",r,"--out",s],(l,h,p)=>{l?c(new Error(`the reference ${r} is ${n==="not a PNG"?n:`a ${n}`}, and sips could not convert it (${String(p||l.message).trim()}) \u2014 ${o}`)):a()})}),we(s))}function qr(r,e,t){let n=new Uint8Array(e*t*4),o=r.w/e,s=r.h/t;for(let i=0;i<t;i++){let a=Math.floor(i*s),c=Math.max(a+1,Math.floor((i+1)*s));for(let l=0;l<e;l++){let h=Math.floor(l*o),p=Math.max(h+1,Math.floor((l+1)*o)),d=0,u=0,m=0,w=0,f=0;for(let g=a;g<c&&g<r.h;g++)for(let $=h;$<p&&$<r.w;$++){let S=(g*r.w+$)*4;d+=r.pixels[S],u+=r.pixels[S+1],m+=r.pixels[S+2],w+=r.pixels[S+3],f++}let y=(i*e+l)*4;f&&n.set([Math.round(d/f),Math.round(u/f),Math.round(m/f),Math.round(w/f)],y)}}return{w:e,h:t,pixels:n}}function Ne(r,e=new Uint8Array(r.length)){for(let t=0;t<r.length;t+=4){let n=Math.round(.299*r[t]+.587*r[t+1]+.114*r[t+2]);e[t]=e[t+1]=e[t+2]=n,e[t+3]=r[t+3]}return e}function Br(r){if(r==null||r===!1||r==="")return null;let e=String(r).trim().toLowerCase();if(e==="checker"||e==="true")return{checker:!0};let t=/^#?([0-9a-f]{6})$/.exec(e);if(!t)throw new Error(`background: "checker" or a #rrggbb color, not ${JSON.stringify(r)}`);let n=parseInt(t[1],16);return{rgb:[n>>16&255,n>>8&255,n&255]}}var Gr=[[204,204,204],[153,153,153]],tt=4;function Xe(r,e,t,n,o){if(!o)return r;for(let s=0;s<t;s++)for(let i=0;i<e;i++){let a=(s*e+i)*4,c=r[a+3]/255;if(c===1)continue;let l=o.rgb??Gr[(((n.x+i)/tt|0)+((n.y+s)/tt|0))%2];for(let h=0;h<3;h++)r[a+h]=Math.round(r[a+h]*c+l[h]*(1-c));r[a+3]=255}return r}function hn(r){return r?`, on ${r.rgb?`#${r.rgb.map(t=>t.toString(16).padStart(2,"0")).join("")}`:`a ${tt}px checker`} where the canvas is transparent`:""}function dn(r,e,t){let n=new Uint8Array(t.w*t.h*4);for(let o=0;o<t.h;o++){let s=((t.y+o)*e+t.x)*4;n.set(r.subarray(s,s+t.w*4),o*t.w*4)}return n}var Wr=["###,#.#,#.#,#.#,###",".#.,##.,.#.,.#.,###","###,..#,###,#..,###","###,..#,###,..#,###","#.#,#.#,###,..#,..#","###,#..,###,..#,###","###,#..,###,#.#,###","###,..#,..#,..#,..#","###,#.#,###,#.#,###","###,#.#,###,..#,..#"].map(r=>r.split(",")),Ie=5;function Jr(r,e,t,n,o,s,i){let a=String(i).split("").map(h=>Wr[Number(h)]),c=a.length*3+(a.length-1);if(c>s||o+Ie>t)return;let l=n+Math.floor((s-c)/2);for(let h of a){for(let p=0;p<Ie;p++)for(let d=0;d<3;d++)h[p][d]==="#"&&r.set([0,0,0,255],((o+p)*e+l+d)*4);l+=4}}var mn=24;function Hr(r,e){let t=[];return r>0&&t.push(`the other ${r} layer${r===1?"":"s"}`),e>1&&t.push(`the other ${e-1} frame${e===2?"":"s"} of this layer`),`${t.join(" and ")} ${t.length===1&&(r===1||e===2)?"is":"are"}`}function Vr(r,e=mn,t=4){let n=a=>/^#[0-9a-f]{6}$/i.test(a)?[1,3,5].map(c=>parseInt(a.slice(c,c+2),16)):null,o=r.map(a=>[a,n(a)]).filter(([,a])=>a),s=[];for(let a=0;a<o.length;a++)for(let c=a+1;c<o.length;c++){let l=Math.hypot(...o[a][1].map((h,p)=>h-o[c][1][p]));l<e&&s.push({a:o[a][0],b:o[c][0],d:Math.round(l)})}s.sort((a,c)=>a.d-c.d);let i=new Set(s.flatMap(a=>[a.a,a.b])).size;return{pairs:s.slice(0,t),total:s.length,crowded:i}}function me(r,e,t){let n=(c,l)=>Number.isFinite(+c)?Math.max(0,Math.round(+c)):l,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 Me=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.shapeSaid=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:pn(),now:Date.now()}),a=new ve(this.core,i,{author:this.author,name:this.name,seed:pn(),agent:!0,getToken:this.getToken?()=>this.getToken(e):null});a.onLost=p=>{this.lastRefusal=p},a.onPrompt=p=>{if(p.author===this.author)return;let d=p.where==="library";this.asked.push({text:p.text,from:p.name,at:p.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((p,d)=>{let u=setTimeout(()=>d(new Error(`timed out joining ${e}`)),1e4);a.onStatus=w=>{w.connected&&(l=!0,clearTimeout(u),p())},a.connect(c);let m=c.onclose;c.onclose=()=>{if(m?.(),l)return this.#n(e,{w:t,h:n,name:o});clearTimeout(u),this.#e(e).then(w=>d(new Error(w)))}}),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)?an.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`:Gt(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=l=>{l.connected&&(i=!0,n.attempts=0)},n.client.connect(s);let c=s.onclose;s.onclose=()=>{c?.(),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=U();this.lastRefusal=null;let a=await Qr(s,i,{intent:t,...n?{layers:[n]}:{}});if(a!=="held"){let l=s.leases.find(h=>h.job!==i);throw new Error(a==="refused"&&this.lastRefusal?.message?`could not claim "${e}": ${this.lastRefusal.message}`:l?`"${e}" is already held by another job: ${l.intent}`:a==="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,began:s.state.seq,layer:n,intent:t,cancelled:!1,timer:setInterval(()=>{this.jobs.has(i)&&s.lease("renew",{job:i})},Lr)};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(this.lastRoom?`no job is open \u2014 begin_job({ document: "${this.lastRoom}" }) picks ${this.lastRoom} up again, or new_sprite starts another`:"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,t=""){let n=this.jobs.get(e);n&&(clearInterval(n.timer),this.jobs.delete(e),this.lastRoom=n.room,this.client(n.room).lease("release",{job:e,...t?{note:t}:{}}),this.note("finish_job",n.room,t||n.intent))}async#o(e,t){if(!e&&!t&&this.jobs.size===0){if(this.lastRoom)return this.lastRoom;throw new Error(`say which document \u2014 looking needs no job, but the fallback to "the one last finished" is this session's, and nothing has been finished in it. list_documents names every one with its id.`)}return this.#s(e,t)}#c(e,t){return!t||this.shapeSaid.get(e)===t?null:(this.shapeSaid.set(e,t),t)}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=He(this.serverUrl)?15e3:6e4,n=()=>{fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:this.name,every:t})}).catch(()=>{})};n(),clearInterval(this.heartbeat),this.heartbeat=setInterval(n,t),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:Kr.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.#o(e,t)}`}catch{n=`${this.editorUrl}/library`}return await Xr(n),{ok:!0,message:`Opened ${n}`}}async newSprite({name:e,w:t=F.w,h:n=F.h,intent:o,palette:s,folder:i=null,challenge:a=null}){let c=await this.createDocument?.({name:e,w:t,h:n})??Lt(e),l=await this.begin(c,o,{open:{w:t,h:n,name:e}});a&&(this.jobs.get(l).challenge=a),s?.length&&await this.submit(l,"set_palette",[{t:"setPalette",colors:Q(s,"hue")}]),i&&await this.moveDocuments({documents:[c],folder:i,make:!0}),this.#p(c,o);let h=this.#c(c,Te(t,n)??Ze(t,n));return{job:l,...this.#t(c),...h?{shape:h}:{}}}async#p(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=!e&&this.jobs.size===0&&this.lastRoom?this.lastRoom:await this.#s(e,null);return{job:await this.begin(o,t,{layer:n}),...this.#t(o)}}finishJob({job:e,note:t=""}){let n=this.resolve(e),o=typeof t=="string"?t.trim().slice(0,200):"";this.finish(n.job,o);let s=this.client(n.room).state,i=this.#c(n.room,Te(s.doc.w,s.doc.h)),a=Ur[n.challenge],l=[a?`This is ${a.whose} entry: tell the user to post it from the share button with the ${a.label} switch on, which puts ${a.puts} on the post.`:null,i].filter(Boolean).join(" "),h=s.seq!==n.began,p=n.pushed?{source:n.pushed}:h?{source:null,note:"nothing pushed \u2014 if a script made this, push_source before the next job, so the next agent (or you, tomorrow) can pick it up rather than redraw it"}:{source:null};return{finished:o||n.intent,...o?{intent:n.intent}:{},job:n.job,document:n.room,drew:h,...l?{next:l}:{},...p}}async#i(e,t={}){if(this.control)try{return await this.control(e,t)}catch(n){if(!this.openControl||!/no library here/.test(n.message))throw n}if(this.openControl)return this.openControl(e,t);throw new Error("no server to push to: this session has no control plane")}async pushSource({job:e,files:t,run:n,takes:o="",unfinished:s=""}){let i=this.resolve(e),a=this.client(i.room),c=(Array.isArray(t)?t:[t]).filter(u=>u!=null&&u!=="");if(!c.length)throw new Error("say which files: the generator, and anything it imports that is yours");let l=c.map(u=>{if(typeof u=="string")return{from:u,name:Pe(u)};let m=u?.from??u?.path;if(typeof m!="string"||!m)throw new Error(`each file is a path, or { from: "<path on disk>", path: "<name to push it as>" } \u2014 got ${JSON.stringify(u)}`);return{from:m,name:Pe(String(u.path??m))}}),h=[];for(let{from:u,name:m}of l){let w=await we(u).catch(f=>{throw new Error(`could not read ${u}: ${f.code==="ENOENT"?"no such file":f.message}`)});if(w.includes(0))throw new Error(`${u} is not text \u2014 a push is source, not a PNG or a grid; the next agent regenerates those`);h.push({name:m,text:w.toString("utf8")})}let d=(await this.#i(`/documents/${encodeURIComponent(i.room)}/history/source`,{method:"POST",body:{job:i.job,seq:a.state.seq,run:String(n??""),takes:String(o??""),unfinished:String(s??""),name:this.name,files:h}}))?.source??null;return d&&this.jobs.has(i.job)&&(this.jobs.get(i.job).pushed={seq:d.seq,files:d.files.map(u=>u.name)}),this.note("push_source",i.room,`${h.length} file${h.length===1?"":"s"}`),{document:i.room,job:i.job,seq:a.state.seq,files:d?.files??h.map(u=>({name:u.name})),note:"pushed beside this job's mark in the version history; readable by this account only. pull_source({ document, job }) brings it back to disk."}}async pullSource({document:e=null,job:t=null,into:n=null}){let o=null;if(e?o=await this.#s(e,null):this.jobs.size===1?o=this.resolve(null).room:o=this.lastRoom,!o)throw new Error("say which document \u2014 none is open and none was finished in this session");let s=`/documents/${encodeURIComponent(o)}/history/source`;if(!t){let l=await this.#i(s,{}),h=Array.isArray(l?.sources)?l.sources:[];return{document:o,sources:h.map(p=>({job:p.job,seq:p.seq,at:p.at?new Date(p.at).toISOString():null,run:p.run,unfinished:p.unfinished||null,files:p.files.map(d=>d.name)})),note:h.length?"yours on this document, newest first \u2014 pull_source({ job, into }) writes one to disk; `seq` is the document state it reproduces (get_info says where the document is now)":"nothing pushed on this document by this account \u2014 a mark with `source: true` in the history was pushed by somebody else, and is theirs"}}let i=await this.#i(`${s}/${encodeURIComponent(String(t))}`,{});if(!i?.files)throw new Error(`no source of yours under job ${t} on ${o}`);let a=Cr(n?String(n):et(".context","source",String(t)));await Nr(a,{recursive:!0});let c=[];for(let l of i.files){let h=et(a,Pe(l.name)),p=await we(h,"utf8").catch(()=>null);if(p!==null&&p!==l.text)throw new Error(`${h} exists and differs \u2014 pull into another directory, or move it first`);await Tr(h,l.text),c.push(h)}return{document:o,job:i.job,seq:i.seq,pushedAt:i.at?new Date(i.at).toISOString():null,by:i.by?.name??null,run:i.run,takes:i.takes||null,unfinished:i.unfinished||null,files:c,note:`written to ${a}. Read the files before running them; run them from there, in the workspace, with nothing the task does not need. They reproduce seq ${i.seq} \u2014 compare get_info's seq to see what happened since.`}}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??[],...n.day?{day:n.day}:{},postedAt:new Date(n.at).toISOString(),post:n.url}:{theme:null,note:`no ${o} prompt is known right now`};return{daily:t(this.news?.dailies,"daily"),weekly:t(this.news?.weekly,"weekly"),inktober:t(this.news?.inktober,"Inktober"),how:'new_sprite({ challenge: "daily" | "weekly" | "inktober", intent }) names the sprite after the prompt; finish_job says how to post it as an entry.'}}async challenge(e){let n=(await this.challenges())[e];if(!n?.theme)throw new Error(e==="inktober"?"no Inktober prompt is known right now \u2014 it runs the 31 days of October, and the list is a year at a time. Ask the user what to draw.":`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.#d();if(o===null||o.some(a=>a.id===n))return n;let s=o.filter(a=>(a.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(a=>a.id).join(", ")}`);let i=/^([A-Za-z0-9]{8,64})_([A-Za-z0-9]{8,64})$/.exec(n);if(i&&o.some(a=>a.id===i[1]))return i[1];if(an.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#d(){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,layers:n=!1}={}){let o=await this.#o(e,t),s=await this.peek(o),i=this.#t(o);return n?{...i,layers:this.#f(s)}:i}#f(e){let t=e.state,{w:n,h:o}=t.doc;return t.doc.layers.map(s=>{let i=0,a=n,c=o,l=-1,h=-1;for(let p=0;p<t.doc.frames.length;p++){let d;try{d=e.session.layerImage(s.id,p)}catch{continue}for(let u=0;u<o;u++)for(let m=0;m<n;m++)d[(u*n+m)*4+3]!==0&&(i++,m<a&&(a=m),m>l&&(l=m),u<c&&(c=u),u>h&&(h=u))}return{name:s.name,pixels:i,bbox:l<0?null:{x:a,y:c,w:l-a+1,h:h-c+1}}})}#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,paletteUsed:this.#y(t),bbox:this.#m(t),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)}}#r(e){let t=e.state,{w:n,h:o}=t.doc,s=n,i=o,a=-1,c=-1;for(let l of t.doc.layers)for(let h=0;h<t.doc.frames.length;h++){let p;try{p=e.session.layerImage(l.id,h)}catch{continue}for(let d=0;d<o;d++)for(let u=0;u<n;u++)p[(d*n+u)*4+3]!==0&&(u<s&&(s=u),u>a&&(a=u),d<i&&(i=d),d>c&&(c=d))}return a<0?null:{x:s,y:i,w:a-s+1,h:c-i+1}}#m(e){try{let t=e.session.composite(0),{w:n,h:o}=e.state.doc,s=n,i=o,a=-1,c=-1;for(let l=0;l<o;l++)for(let h=0;h<n;h++)t[(l*n+h)*4+3]!==0&&(h<s&&(s=h),h>a&&(a=h),l<i&&(i=l),l>c&&(c=l));return a<0?null:{x:s,y:i,w:a-s+1,h:c-i+1,cx:(s+a)/2,cy:(i+c)/2}}catch{return null}}#l(e,t=null){let n=e.state,{w:o,h:s}=n.doc,i=new Set,a=c=>c.toString(16).padStart(2,"0");for(let c of n.doc.layers)for(let l=0;l<n.doc.frames.length;l++){if(t&&c.id===t.layer&&t.frames.includes(l))continue;let h;try{h=e.session.layerImage(c.id,l)}catch{continue}for(let p=0;p<o*s*4;p+=4)h[p+3]!==0&&i.add(`#${a(h[p])}${a(h[p+1])}${a(h[p+2])}`)}return[...i]}#y(e){try{let t=new Set;for(let n=0;n<e.state.doc.frames.length;n++){let o=e.session.composite(n);for(let s=0;s<o.length;s+=4)o[s+3]!==0&&t.add(o[s]<<16|o[s+1]<<8|o[s+2])}return(e.state.doc.palette??[]).filter(n=>{let o=/^#([0-9a-f]{6})/i.exec(String(n));return o&&t.has(parseInt(o[1],16))}).length}catch{return null}}#g(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#w(e,t){let n=Math.trunc(Number(t));if(!Number.isFinite(n)||n<0)throw new Error(`\`seq\` is a document sequence \u2014 the number get_info reports and every draw_art result carries; got ${JSON.stringify(t)}`);let s=(await this.peek(e)).state.seq,i=await this.#i(`/documents/${encodeURIComponent(e)}/history/at/${n}`,{}).catch(async c=>{let h=((await this.#i(`/documents/${encodeURIComponent(e)}/history`,{}).catch(()=>null))?.versions??[]).map(d=>d.seq).filter(d=>Number.isFinite(d)),p=h.length?` Replay starts from an anchor: ${[...new Set(h)].sort((d,u)=>d-u).join(", ")}.`:"";throw new Error(`seq ${n} of ${e} cannot be rebuilt (${c.message}) \u2014 the document is at seq ${s}.${p}`)});if(!i||typeof i!="object")throw new Error(`seq ${n} of ${e} came back empty \u2014 the history has the operations but no document to replay them onto`);let a=this.core.openSession(i,this.author,1);return{past:!0,now:s,state:{doc:a.state().doc,seq:n,pending:0},session:a,connected:!0,welcomed:!0,close:()=>{try{a.close()}catch{}}}}async readArt({document:e,job:t,layer:n,frame:o=0,trim:s=!1,seq:i=null}){let a=await this.#o(e,t);if(i!=null){let l=await this.#w(a,i);try{return{document:a,at:l.state.seq,...l.session.readArt(this.#a(l.state.doc,n,a),o,{trim:s}),note:`the document as it stood at seq ${l.state.seq}, rebuilt from the history; the room is now at ${l.now}. A read of the past, not a restore.`}}finally{l.close()}}let c=await this.peek(a);return{document:a,...c.session.readArt(this.#h(a,n),o,{trim:s}),...this.#g(a)}}async readPng({document:e,job:t,layer:n=null,frame:o=0,seq:s=null}){let i=await this.#o(e,t),a=s!=null?await this.#w(i,s):null,c=a??await this.peek(i);try{let l=c.state.doc,h=a?{at:a.state.seq}:{};if(n==null)return{document:i,...h,png:c.session.png(o,{scale:1}),w:l.w,h:l.h};let p=c.session.cel(this.#a(l,n,i),o);return{document:i,...h,png:this.#u(l.w,l.h,p),w:l.w,h:l.h}}finally{a?.close()}}#u(e,t,n,o=1){let s=this.core.newDocument({w:e,h:t,name:"scratch",seed:1,now:Date.now()}),i=this.core.openSession(s,U(),1);try{let a=i.state();return i.apply([zt(a.doc.layers[0].id,a.doc.frames[0].id,{x:0,y:0,w:e,h:t},n)],{now:Date.now()}),i.png(0,{scale:o})}finally{i.close()}}async#k(e,t,n,o,s,i=!1,a=null,c=!1,l=null){let{png:h,from:p}=await this.#S(t),d=this.core.decodePng(h),u=e.state.doc,m=Math.abs(d.w/d.h-u.w/u.h)<.01,w=c&&(!!a||!m),f=w?ln(d,n.w,n.h,a):ln(d,u.w,u.h,a),y=e.session.composite(o);l&&Xe(y,u.w,u.h,{x:0,y:0},l);let g=i?Ne(y):y;i&&Ne(f.pixels,f.pixels);let $=n.w,S=n.h,x=new Uint8Array(($*2+1)*S*4),_=($*2+1)*4;for(let I=0;I<S;I++){for(let C=0;C<$;C++){let G=n.x+C,H=n.y+I,R=w?C:G,A=w?I:H,ae=(A*f.w+R)*4,Le=A<f.h&&R<f.w;x.set(Le?f.pixels.subarray(ae,ae+4):[0,0,0,255],I*_+C*4);let xe=(H*u.w+G)*4;x.set(g.subarray(xe,xe+4),I*_+($+1+C)*4)}x.set([255,0,255,255],I*_+$*4)}let k=f.placed,q=f.region.w===d.w&&f.region.h===d.h,L=!!a&&q,Z=q?L?`\`referenceRect\` ${f.region.w}\xD7${f.region.h} at (${f.region.x}, ${f.region.y}) covers the whole ${d.w}\xD7${d.h} reference, so this is all of it`:`the reference is ${d.w}\xD7${d.h}`:`the region ${f.region.w}\xD7${f.region.h} at (${f.region.x}, ${f.region.y}) of the ${d.w}\xD7${d.h} reference`,E=f.region.w/k.w,M=`${E>=10?Math.round(E):E.toFixed(1)} reference px per canvas px`,B=w?q?`fitted whole into the ${n.w}\xD7${n.h} rect by ${f.by} (${k.w}\xD7${k.h}, centered; black where it does not reach; ${M}) \u2014 the whole reference against this part of the canvas${L?", as asked; name a SMALLER `referenceRect` to bring one part of it":"; pass `referenceRect` to bring one part of it instead"}`:`fitted into the ${n.w}\xD7${n.h} rect by ${f.by} (${k.w}\xD7${k.h}, centered; black where it does not reach; ${M}) \u2014 that part of the reference against this part of the canvas, at one scale`:k.w===u.w&&k.h===u.h?`brought to the ${u.w}\xD7${u.h} canvas (${M})`:`fitted whole to ${k.w}\xD7${k.h} by ${f.by}, centered on the ${u.w}\xD7${u.h} canvas (black where it does not reach; ${M})`,z=q&&k.w===u.w&&k.h===u.h?Math.abs(E-1)<.05?"":` \u2014 ${M}`:` \u2014 ${Z}, ${B}${q&&!L&&!w&&(k.w!==u.w||k.h!==u.h)?"; pass `referenceRect` to bring one part of it to the grid instead":""}`;return{png:this.#u($*2+1,S,x,s),note:`${p}${z}`}}async#S(e){let t=String(e).trim(),n=null;if(/^[a-z][a-z0-9+.-]*:\/\//i.test(t)){if(n=ie(t),n===t)throw new Error(`could not read the reference ${t}: a URL is read only when it is a Pixie link (/d/\u2026 or /s/\u2026) \u2014 save anything else to a file and pass the path`)}else{let a=await we(t).catch(h=>h);if(!(a instanceof Error))return{png:await zr(t,a),from:""};let c=`could not read the reference ${t}: ${a.code==="ENOENT"?"no such file":a.message}`;if(a.code!=="ENOENT")throw new Error(c);let l=ie(t);if(l!==t)n=l;else{if(/[\\/]/.test(t)||/\.[a-z0-9]{2,4}$/i.test(t))throw new Error(c);n=await this.#s(t,null).catch(()=>{throw new Error(`${c}, and no document by that name or id either \u2014 a reference is a path on this machine, a document's id, or its link`)})}}let o=await this.#b(await this.#s(n,null)),{w:s,h:i}=o.state.doc;return{png:o.session.png(0,{scale:1}),from:` \u2014 the reference is the document ${n} at seq ${o.state.seq} (${s}\xD7${i}, frame 1)`}}async preview({document:e,job:t,frame:n=0,scale:o=null,rect:s=null,rects:i=null,reference:a=null,referenceRect:c=null,values:l=!1,background:h=null,file:p=null,overwrite:d=!1,seq:u=null}){let m=await this.#v({document:e,job:t,frame:n,scale:o,rect:s,rects:i,reference:a,referenceRect:c,values:l,background:h,seq:u});if(p){let w=String(p),f=await ee(w,m.png,{overwrite:d,seq:m.seq});m.caption+=` \u2014 written to ${w}${f?" (replaced; anything that showed this path before shows the old picture)":""}`}return m}async#b(e){let t=await this.peek(e);if(t.state.pending>0&&await eo(t,3e3),!t.connected||!t.welcomed)throw new Error(`the room connection dropped \u2014 a render now could be behind what the room holds (last confirmed seq ${t.state.seq}). Reconnecting; try again in a moment.`);return t}#E(e,t,n,o,s,i,a){let c=o.map(f=>me(f,t.w,t.h)),l=1,h=c.reduce((f,y)=>f+y.w,0)+l*(c.length-1),p=Math.max(...c.map(f=>f.h)),d=p+Ie+2,u=new Uint8Array(h*d*4),m=e.session.composite(n),w=0;return c.forEach((f,y)=>{let g=dn(m,t.w,f);Xe(g,f.w,f.h,f,a),i&&Ne(g,g);for(let $=0;$<d;$++)for(let S=0;S<f.w;S++){let x=($*h+w+S)*4;$>=f.h?u.set([255,0,255,255],x):u.set(g.subarray(($*f.w+S)*4,($*f.w+S)*4+4),x)}if(Jr(u,h,d,w,p+1,f.w,y+1),w+=f.w,w<h){for(let $=0;$<d;$++)u.set([255,0,255,255],($*h+w)*4);w+=l}}),{png:this.#u(h,d,u,s),wins:c,w:h,h:d}}async#v({document:e,job:t,frame:n=0,scale:o=null,rect:s=null,rects:i=null,reference:a=null,referenceRect:c=null,values:l=!1,background:h=null,seq:p=null}){let d=Br(h),u=await this.#o(e,t),m=p!=null?await this.#w(u,p):null;try{return await this.#j({room:u,view:m??await this.#b(u),past:m,frame:n,scale:o,rect:s,rects:i,reference:a,referenceRect:c,values:l,bg:d})}finally{m?.close()}}async#j({room:e,view:t,past:n,frame:o,scale:s,rect:i,rects:a,reference:c,referenceRect:l,values:h,bg:p}){let d=t,{warning:u}=n?{}:this.#g(e),m=d.state.seq,w=n?` (rebuilt from the history; the room is now at ${n.now})`:"",f=d.state.doc;if(Array.isArray(a)&&a.length){if(c)throw new Error("one rect at a time against a reference \u2014 `rects` tiles crops of the render alone");let E=a.reduce((R,A)=>R+me(A,f.w,f.h).w,0)+a.length-1,M=Math.max(...a.map(R=>me(R,f.w,f.h).h))+Ie+2,B=Number.isInteger(s)&&s>0?s:cn(E,M),z=Math.max(1,Math.min(B,Math.floor(8192/Math.max(E,M,1)))),{png:I,wins:C}=this.#E(d,f,o,a,z,h,p),G=z<B?` \u2014 asked for \xD7${B}, held to \xD7${z} so a side stays under 8192px; fewer or smaller rects to look closer`:"",H=C.map((R,A)=>`${A+1}${a[A]?.label?` ${String(a[A].label).slice(0,48)}`:""} ${R.w}\xD7${R.h} at (${R.x},${R.y})`).join(", ");return{png:I,seq:m,caption:`${e} at seq ${m}${w}, frame ${o+1}: ${C.length} crops of ${f.w}\xD7${f.h} side by side at \xD7${z}, magenta between, each numbered in the strip under it \u2014 ${H}${h?", values only":""}${hn(p)}${G}${u?` \u2014 ${u}`:""}`}}let y=i?me(i,f.w,f.h):null,g=y?y.w:f.w,$=y?y.h:f.h,S=Number.isInteger(s)&&s>0?s:cn(c?g*2:g,$),x=Math.max(1,Math.min(S,Math.floor(8192/Math.max(c?g*2:g,$,1)))),_=y?`${y.w}\xD7${y.h} at (${y.x},${y.y}) of ${f.w}\xD7${f.h}`:`${f.w}\xD7${f.h}`,k=x<S?` \u2014 asked for \xD7${S}, held to \xD7${x} so a side stays under 8192px; render a smaller rect to look closer`:"",q=hn(p);if(c){let{png:E,note:M}=await this.#k(d,String(c),y??{x:0,y:0,w:f.w,h:f.h},o,x,h,l,!!y,p);return{png:E,seq:m,caption:`${e} at seq ${m}${w}, frame ${o+1}: reference (left) beside the render (right), ${_} at \xD7${x}${h?", values only":""}${q}${M}${k}${u?` \u2014 ${u}`:""}. Per object, say how each is built \u2014 count the faces, name the light, say where the shadow falls \u2014 and what differs; "reads as" is not a verdict.`}}let L=y??{x:0,y:0,w:f.w,h:f.h};return{png:h||p?this.#u(g,$,(()=>{let E=dn(d.session.composite(o),f.w,L);return Xe(E,g,$,L,p),h?Ne(E,E):E})(),x):d.session.png(o,{scale:x,...y?{rect:y}:{}}),seq:m,caption:`${e} at seq ${m}${w}, frame ${o+1}: ${_} at \xD7${x} (${g*x}\xD7${$*x}px)${h?", values only \u2014 squint: is there a focal point, and where is the darkest dark":""}${q}${k}${u?` \u2014 ${u}`:""}`}}async sheet({document:e,job:t,columns:n=4,scale:o=2}){let s=await this.#o(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."}}#h(e,t){return this.#a(this.client(e).state.doc,t,e)}async#$(e,t,n=null){let o=this.client(e.room).state.doc;if(typeof t!="string"||t==="")return{id:this.#a(o,t,e.room),added:null};if(o.layers.some(i=>i.name===t||i.id===t))return{id:this.#a(o,t,e.room),added:null};let s=Je(t,o.layers.map(i=>i.name));if(s)throw new Error(`no layer ${JSON.stringify(t)} in ${e.room} \u2014 did you mean ${JSON.stringify(s)}? A name this close to one that exists is a typo, and making it would put this pass on a layer nobody is looking at. add_layer({ name }) if it really is a new one.`);return await this.addLayer({job:e.job,name:t,below:n}),{id:this.#h(e.room,t),added:t}}#a(e,t,n){if(t!=null&&t!==""){let o=typeof t=="number"?e.layers[t]:e.layers.find(s=>s.name===t||s.id===t);if(!o)throw new Error(`no layer ${JSON.stringify(t)} in ${n}`);return o.id}return e.layers[e.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,below:l=null,frame:h=0,label:p,palette:d="add",rect:u=null}){let m=this.resolve(e),w=this.client(m.room),f=w.state.doc.frames.length;if(!Number.isInteger(h)||h<0||h>=f)throw new Error(`no frame ${h}: this document has ${f}, counted from 0 (0 to ${f-1}) \u2014 add_frame first, or draw_animation for a whole loop`);let y=t,g={x:o,y:s},$=a;if(u){let b=me(u,Math.max(...t.map(T=>T.length),1),t.length);if(y=t.slice(b.y,b.y+b.h).map(T=>T.slice(b.x,b.x+b.w).padEnd(b.w,".")),g={x:b.x,y:b.y},a){let T=[..."~^`|"].find(v=>!(v in n))??"\xA1";y=y.map(v=>v.replace(/[. ]/g,T)),n={...n,[T]:null},$=!1}}let S=new Set((w.state.doc.palette??[]).map(b=>String(b).toLowerCase())),x=(()=>{try{return Uint8Array.from(w.session.composite(h))}catch{return null}})(),{id:_,added:k}=c!=null?await this.#$(m,c,l):{id:m.layer??this.#h(m.room,null),added:null},q=w.session.drawArt(y,n,{layer:_,at:h,x:g.x,y:g.y,center:u?!1:i,clear:$,addToPalette:d!=="keep"}),{committed:L}=await this.submit(m.job,p||"draw_art",q),Z=(this.client(m.room).state.doc.palette??[]).map(b=>String(b).toLowerCase()).filter(b=>!S.has(b)),E=w.state.doc,M=Math.max(...y.map(b=>b.length),0),B=i&&!u?{x:Math.round((E.w-M)/2),y:Math.round((E.h-y.length)/2)}:g,z=0,I=0;for(let b=0;b<y.length;b++){let T=B.y+b;for(let v=0;v<y[b].length;v++){let ce=y[b][v];if(ce==="."||ce===" ")continue;let Se=B.x+v;Se<0||T<0||Se>=E.w||T>=E.h?I++:z++}}let C=z;if(L&&x)try{let b=w.session.composite(h),T=0;for(let v=0;v<b.length;v+=4)(b[v]!==x[v]||b[v+1]!==x[v+1]||b[v+2]!==x[v+2]||b[v+3]!==x[v+3])&&T++;C=T}catch{}let G=[],H=w.state.doc.layers.length-1,R=w.state.doc.frames.length;if(d==="replace"&&L){let b=D=>typeof D=="string"&&/^#[0-9a-f]{6}$/i.test(D)?D.toLowerCase():null,T=u?null:new Set(y.flatMap(D=>[...D])),v=Object.entries(n).filter(([D,kn])=>b(kn)&&(T===null||T.has(D))),ce=[...new Set(v.map(([,D])=>b(D)))],Se=new Set(ce);G=[...new Set(Object.values(n).map(b).filter(D=>D&&!Se.has(D)))];let xn=H>0||R>1?this.#l(this.client(m.room),{layer:_,frames:[h]}):[],it=[...new Set([...ce,...xn])];it.length&&await this.submit(m.job,"set_palette",[{t:"setPalette",colors:Q(it,"hue")}])}let A=(this.client(m.room).state.doc.palette??[]).length,ae=this.#y(this.client(m.room))??A,Le=(u?`the file's \u2014 the whole picture's, not the ${y[0]?.length??0}\xD7${y.length} that was drawn; nothing outside the rect moved`:"this drawing's")+(d==="replace"&&(H>0||R>1)?`, and what ${Hr(H,R)} drawn in \u2014 a replace is of the document, so it keeps them`:""),xe=G.length?`; ${G.length} in \`colors\` landed on no pixel and ${G.length===1?"was":"were"} left out: ${G.join(" ")}`:"",X=d==="replace"&&A>48?Vr(this.client(m.room).state.doc.palette??[]):{pairs:[],total:0,crowded:0},gn=X.crowded*3>A,bn=X.pairs.length&&!gn?` \u2014 ${A} colors, and ${X.total} pair${X.total===1?"":"s"} of them within ${mn} RGB of each other (${X.pairs.map(b=>`${b.a}\u2248${b.b}`).join(", ")}${X.total>X.pairs.length?", \u2026":""}): ramps that read alike, and a merge if they are not separate materials`:"",De=I?`${I} pixel${I===1?"":"s"} of the ${M}\xD7${y.length} grid at (${B.x},${B.y}) fell outside the ${E.w}\xD7${E.h} canvas and were not drawn \u2014 resize_canvas first, or generate at the canvas's size`:"",ke=d==="replace"&&L?{note:`palette replaced: ${A} color${A===1?"":"s"}, ${Le}${xe}${bn}`}:d==="keep"&&L?{note:`palette kept at ${A}: this drawing's colors are on the canvas and not in the palette`}:Z.length?{added:Q(Z,"hue"),note:`${Z.length} color${Z.length===1?"":"s"} joined the palette${A>48||A>ae*2?` \u2014 ${A} entries now and ${ae} with a pixel on them`:""} \u2014 for a generated piece redrawn from a script, pass \`palette: "replace"\` on this call and the palette is exactly what you drew; set_palette({ prune: true }) is the user's document and is theirs to ask for`}:{},$n=De?{...ke,note:ke.note?`${ke.note}; ${De}`:De}:ke;return{...this.#t(m.room),drew:u?`${y[0]?.length??0}\xD7${y.length} at (${g.x},${g.y})`:`${t[0]?.length??0}\xD7${t.length}`,changed:C,...k?{layerAdded:k}:{},...R>1?{frame:`${h} of ${R}, counted from 0`}:{},...C!==z?{inked:z}:{},committed:L,...$n}}async resizeCanvas({job:e,w:t,h:n,anchor:o="center",clear:s=!1}){let i=this.resolve(e),a=this.client(i.room).state,c=Math.max(1,Math.min(1024,Math.round(Number(t)||a.doc.w))),l=Math.max(1,Math.min(1024,Math.round(Number(n)||a.doc.h)));if(c===a.doc.w&&l===a.doc.h)return s&&await this.#x(i),{...this.#t(i.room),resized:s?"already that size; cleared":"already that size"};let h=String(o??"center").toLowerCase(),p=h.includes("left")?0:h.includes("right")?2:1,d=h.includes("top")?0:h.includes("bottom")?2:1,u=Math.round((c-a.doc.w)*p/2),m=Math.round((l-a.doc.h)*d/2);await this.submit(i.job,"resize_canvas",[{t:"resize",w:c,h:l,dx:u,dy:m}]),s&&await this.#x(i);let w=this.#c(i.room,Te(c,l)??Ze(c,l));return{...this.#t(i.room),resized:`${a.doc.w}\xD7${a.doc.h} \u2192 ${c}\xD7${l}, ${s?"cleared":`the art kept at ${u},${m}`}`,...w?{shape:w}:{}}}async closeDocument({document:e}){let t=await this.#s(e,null),n=this.rooms.get(t);if(!n)return{closed:t,open:!1};for(let[o,s]of this.jobs)s.room===t&&this.finish(o);return n.closing=!0,clearTimeout(n.retry),n.client.close(),this.rooms.delete(t),{closed:t,open:!1}}async drawAnimation({job:e,frames:t,colors:n,fps:o,layer:s,label:i,palette:a="add"}){let c=this.resolve(e),l=this.client(c.room),{id:h}=s!=null?await this.#$(c,s):{id:c.layer??this.#h(c.room,null)},p=l.state.doc.frames.length,d=t.length-p;if(d>0){let f=[],y=l.state.doc.frames[p-1]?.index??"V";for(let g=0;g<d;g++)y=y+"V",f.push({t:"addFrame",id:U(),index:y,durMs:Qe(o)});await this.submit(c.job,"add frames",f)}if(o){let f=Qe(o);await this.submit(c.job,"set fps",l.state.doc.frames.map(y=>({t:"setFrameDur",id:y.id,durMs:f})))}let u=l.session.drawAnimation(t,n,{layer:h,addToPalette:a!=="keep"}),{committed:m}=await this.submit(c.job,i||"draw_animation",u),w=null;if(a==="replace"&&m){let f=_=>typeof _=="string"&&/^#[0-9a-f]{6}$/i.test(_)?_.toLowerCase():null,y=new Set(t.flatMap(_=>_.rows.flatMap(k=>[...k]))),g=Object.entries(n).filter(([_,k])=>f(k)&&y.has(_)).map(([,_])=>f(_)),$=t.map((_,k)=>k),S=this.#l(this.client(c.room),{layer:h,frames:$}),x=[...new Set([...g,...S])];x.length&&await this.submit(c.job,"set_palette",[{t:"setPalette",colors:Q(x,"hue")}]),w=`palette replaced: ${x.length} color${x.length===1?"":"s"}, the ${t.length} frames' and what the cels this call did not write are drawn in`}else a==="keep"&&m&&(w=`palette kept at ${(this.client(c.room).state.doc.palette??[]).length}: the frames' colors are on the canvas and not in the palette`);return{...this.#t(c.room),wrote:`${t.length} frames, 0 to ${t.length-1}`,committed:m,...w?{note:w}:{}}}async animationFrames({document:e,job:t}){let n=await this.#o(e,t),o=await this.peek(n),{w:s,h:i,frames:a}=o.state.doc;return{room:n,w:s,h:i,frames:a.map((c,l)=>({pixels:Uint8Array.from(o.session.composite(l)),durMs:c.durMs??100}))}}async addLayer({job:e,name:t,below:n=null,above:o=null}){let s=this.resolve(e),i=this.client(s.room).state,a=i.doc.layers,l=(a[a.length-1]?.index??"V")+"V";if(n!=null||o!=null){if(n!=null&&o!=null)throw new Error("`below` or `above`, not both \u2014 name the one neighbour the new layer goes next to");let h=a.indexOf(a.find(u=>u.id===this.#a(i.doc,n??o,s.room))),p=n!=null?a[h-1]?.index:a[h].index,d=n!=null?a[h].index:a[h+1]?.index;l=this.core.call("frac.between",{...p?{lo:p}:{},...d?{hi:d}:{}}).index}return await this.submit(s.job,"add_layer",[{t:"addLayer",id:U(),index:l,name:t||`Layer ${i.doc.layers.length+1}`,visible:!0,locked:!1,opacity:1}]),this.#t(s.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=U(),c=[{t:"addFrame",id:a,index:(i?.index??"V")+"V",durMs:i?.durMs??83}];if(t&&i)for(let h of s.doc.layers){let p=o.session.cel(h.id,i.id);p.every(d=>d===0)||c.push(Ye(h.id,a,s.doc,p))}await this.submit(n.job,"add_frame",c);let l=this.client(n.room).state.doc.frames.findIndex(h=>h.id===a);return{...this.#t(n.room),added:`frame ${l}, counted from 0${t&&i?", a copy of the one before":""}`}}async renameLayer({job:e,layer:t,name:n}){let o=this.resolve(e),s=this.client(o.room).state.doc,i=this.#a(s,t,o.room),a=String(n??"").trim();if(!a)throw new Error("a layer needs a name \u2014 pass `name`");if(s.layers.find(l=>l.name===a&&l.id!==i))throw new Error(`another layer is already called ${JSON.stringify(a)} \u2014 a layer is found by its name, so two of one name is a draw_art landing on the wrong one`);return await this.submit(o.job,"rename_layer",[{t:"setLayerProp",id:i,prop:"name",value:a}]),this.#t(o.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,c=new Set;for(let h of a.doc.layers)for(let p of a.doc.frames){let d=i.session.cel(h.id,p.id);for(let u=0;u<d.length;u+=4)d[u+3]!==0&&c.add(`#${[d[u],d[u+1],d[u+2]].map(m=>m.toString(16).padStart(2,"0")).join("")}`)}let l=a.doc.palette??[];s=l.filter(h=>c.has(String(h).toLowerCase()));for(let h of c)s.some(p=>String(p).toLowerCase()===h)||s.push(h);return s=Q(s,"hue"),await this.submit(o.job,"set_palette",[{t:"setPalette",colors:s}]),{...this.#t(o.room),palette:s.length,pruned:l.length-s.length}}return s=Q(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=Qe(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#x(e){let t=this.client(e.room).state,n=new Uint8Array(t.doc.w*t.doc.h*4),o=[];for(let s of t.doc.layers)for(let i of t.doc.frames)o.push(Ye(s.id,i.id,t.doc,n));o.length&&await this.submit(e.job,"clear",o)}async clearCel({job:e,layer:t,frame:n=0}){let o=this.resolve(e),i=this.client(o.room).state,a=this.#h(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",[Ye(a,c.id,i.doc,l)]),this.#t(o.room)}async trim({job:e,margin:t=0,center:n=!1}){let o=this.resolve(e),s=this.client(o.room),i=s.state,a=this.#r(s),c=i.doc.layers.length>1?` every one of the ${i.doc.layers.length} layers`:"";if(n){if(!a)return{...this.#t(o.room),centered:"nothing drawn"};let p=Math.floor((i.doc.w-a.w)/2)-a.x,d=Math.floor((i.doc.h-a.h)/2)-a.y;if(!p&&!d)return{...this.#t(o.room),centered:"already centered"};await this.submit(o.job,"center_art",[{t:"resize",w:i.doc.w,h:i.doc.h,dx:p,dy:d}]);let u=[(i.doc.w-a.w)%2?"width":null,(i.doc.h-a.h)%2?"height":null].filter(Boolean);return{...this.#t(o.room),centered:`the ${a.w}\xD7${a.h} of art${c?` on${c}`:""} moved ${p},${d}${u.length?` \u2014 the ${u.join(" and ")} cannot be split evenly, so it sits half a pixel up-left`:""}`}}if(!a)return{...this.#t(o.room),trimmed:"nothing drawn"};let l=a.w+t*2,h=a.h+t*2;return l>=i.doc.w&&h>=i.doc.h?{...this.#t(o.room),trimmed:"already tight"}:(await this.submit(o.job,"trim_canvas",[{t:"resize",w:l,h,dx:-a.x+t,dy:-a.y+t}]),{...this.#t(o.room),trimmed:`${l}\xD7${h}${c?`, to the art on${c}`:""}`})}},Kr=new Set(["rename_layer","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","resize_canvas","close_document","get_info","list_documents","challenges","take_requests","create_folder","move_documents","push_source","pull_source"]);function Ye(r,e,t,n){return{t:"paintRegion",layer:r,frame:e,rect:{x:0,y:0,w:t.w,h:t.h},rle:Zr(n)}}function Zr(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 Xr(r){let e=process.platform==="darwin"?"open":process.platform==="win32"?"start":"xdg-open";return new Promise(t=>fn(e,[r],()=>t()))}function Qe(r){return!r||r<=0?83:Math.max(10,Math.round(1e3/r))}function Yr(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 un(r,e=8e3){return new Promise(t=>{let n=Date.now(),o=()=>{if(r.welcomed)return t(!0);if(Date.now()-n>e)return t(!1);setTimeout(o,20)};o()})}async function Qr(r,e,t,{ms:n}={}){await un(r,n);let o=()=>{let i=Yr(r,e,n);return r.lease("acquire",{job:e,...t}),i},s=await o();return s==="refused"&&!r.welcomed&&await un(r,n)&&(s=await o()),s}function eo(r,e=400){return new Promise(t=>{let n=Date.now(),o=()=>{if(r.state.pending===0&&Date.now()-n>40||Date.now()-n>e)return t();setTimeout(o,10)};o()})}function pn(){return(Date.now()^Math.floor(Math.random()*4294967295))>>>0}import{execFile as to}from"node:child_process";var no=2e4,nt="a past seq is a room thing \u2014 the history lives beside the document in a room, and a local editor keeps none. Save each pass with render_preview({ file }) and stack them with references/judge.py",ro=5e3,ye=class r{constructor({base:e,name:t="agent"}={}){this.base=String(e).replace(/\/$/,""),this.name=t,this.log=[],this.beat=setInterval(()=>void this.announce(),ro),this.beat.unref&&this.beat.unref()}get kind(){return"local"}get where(){return this.base}supports(e){return oo.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=no){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=F.w,h:n=F.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??[],...o.day?{day:o.day}:{},postedAt:new Date(o.at).toISOString(),post:o.url}:{theme:null,note:`no ${s} prompt is known right now`};return{daily:n(t?.dailies,"daily"),weekly:n(t?.weekly,"weekly"),inktober:n(t?.inktober,"Inktober")}}async challenge(e){let n=(await this.challenges())[e];if(!n?.theme)throw new Error(e==="inktober"?"no Inktober prompt is known right now \u2014 it runs the 31 days of October. Ask the user what to draw.":`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,layers:t=!1}={}){if(t)throw new Error("a per-layer bbox is a room thing \u2014 the frozen editor reports no such detail. read_art({ layer, png }) per layer and measure them with references/pngio.py");let n=await this.call("info",[]);return{editor:this.base,...e?{asked:e}:{},...n}}readArt({document:e,layer:t,frame:n,seq:o=null}){if(o!=null)throw new Error(nt);return this.call("readArt",[{document:e,layer:t,frame:n}])}async preview({document:e,frame:t,scale:n=null,rect:o=null,rects:s=null,reference:i=null,referenceRect:a=null,values:c=!1,background:l=null,file:h=null,overwrite:p=!1,seq:d=null}){if(o)throw new Error("the local editor cannot render a region \u2014 render the whole frame");if(s)throw new Error("the local editor cannot render regions \u2014 render the whole frame, or read_art({ png }) and tile the crops with references/compare.py");if(c)throw new Error("the local editor cannot render values only \u2014 read_art({ png }) and convert it, or run the squint test with references/compare.py");if(i||a)throw new Error("a reference beside the render is a room thing \u2014 in a local editor, read_art({ file }) and compare with references/compare.py");if(l)throw new Error("a background under the alpha is a room thing \u2014 in a local editor, read_art({ png }) and put it over one with references/render.py");if(d!=null)throw new Error(nt);let u=await this.call("toDataURL",[t??null,Number.isInteger(n)&&n>0?n:8,e??null]),m=rt(u.url),w=u.caption;if(h){let f=String(h),y=await ee(f,m,{overwrite:p});w+=` \u2014 written to ${f}${y?" (replaced; anything that showed this path before shows the old picture)":""}`}return{png:m,caption:w}}async readPng({document:e,frame:t=0,layer:n=null,seq:o=null}){if(o!=null)throw new Error(nt);if(n!=null)throw new Error("one layer as a PNG is a room thing \u2014 in a local editor, read_art({ layer, file }) and render it with references/render.py");let s=await this.call("toDataURL",[t??null,1,e??null]),i=/(\d+)×(\d+)/.exec(s.caption??"");return{document:e??null,png:rt(s.url),w:i?Number(i[1]):0,h:i?Number(i[2]):0}}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:rt(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,below:l=null,frame:h,label:p,rect:d=null}){if(l!=null)throw new Error("`below` is a room thing \u2014 the frozen editor makes new layers on top");if(d){let u=Math.max(0,d.x|0),m=Math.max(0,d.y|0);t=t.slice(m,m+(d.h|0)).map(w=>w.slice(u,u+(d.w|0))),o=u,s=m,i=!1}return this.call("drawArt",[t,n,{job:e,x:o,y:s,center:i,clear:a,layer:c,frame:h,label:p}])}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,below:t=null,above:n=null}){if(t!=null||n!=null)throw new Error("a layer under or over another is a room thing \u2014 the frozen editor adds on top; add it, then reorder it in the editor");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 so(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)}},oo=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 rt(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 so(r){let e=process.platform==="darwin"?"open":process.platform==="win32"?"start":"xdg-open";return new Promise(t=>to(e,[r],()=>t()))}function ot(r="room"){return`Pixel art editor with live collaboration. ${r==="local"?`Documents are tabs in a browser on this machine; the user watches as you
|
|
10
10
|
work and can take the canvas back at any time.`:`Documents are rooms; other people and other agents may be in one with
|
|
11
11
|
you, and they see what you do as you do it.`}
|
|
12
12
|
|
|
@@ -43,9 +43,9 @@ challenges: the daily, weekly and Inktober prompts.
|
|
|
43
43
|
|
|
44
44
|
A BIG OR GENERATED GRID DOES NOT PASS THROUGH YOU: write it to a file and
|
|
45
45
|
call draw_art with file. Do not ask abstract questions: commit to a good
|
|
46
|
-
default and show it.`}var
|
|
46
|
+
default and show it.`}var io=ot("room"),Ce=class{constructor({name:e="pixie",version:t=ue,tools:n=[],instructions:o=io}={}){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(`
|
|
47
47
|
`))!==-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)+`
|
|
48
|
-
`)}}),this}};var
|
|
48
|
+
`)}}),this}};var ge=(r,e)=>{let t=process.argv.indexOf(`--${r}`);return t>-1?process.argv[t+1]:e},st=process.env.SPRITE_EDITOR||"http://127.0.0.1:4242",Ue=process.env.SPRITE_SERVER||"ws://127.0.0.1:4243",co=new URL("./sprite_core.wasm",import.meta.url);function lo(r){let e=String(r??"").trim();return!e||!/^[a-z0-9]+([-_][a-z0-9]+)*$/.test(e)?e||"an agent":e.split(/[-_]/).map(t=>ho.has(t)?t.toUpperCase():t[0].toUpperCase()+t.slice(1)).join(" ")}var ho=new Set(["cli","ai","api","ide","mcp","sdk","gpt","llm"]);function uo(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 yn(r,{timeoutMs:e=700}={}){if(await lt(r,{timeoutMs:e}))return!0;try{let t=await fetch(`${j(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 po({editor:r=st,server:e=Ue,allowHost:t=!0,publicServer:n=le}={}){if(n)return{kind:"room",url:n,why:`the Pixie this agent belongs to, at ${j(n)}`};let o=await ye.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 yn(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 fo({kind:r=null,editor:e=st,server:t=Ue,wasm:n=co,name:o="agent",allowHost:s=!0}={}){let i=r?{kind:r,url:r==="local"?e:t,why:"you said so"}:await po({editor:e,server:t,allowHost:s}),a,c=null,l=null;if(i.kind==="local")a=new ye({base:i.url,name:o}),a.announce();else{i.start;let d=ht(i.url),u=n instanceof Uint8Array?n:await ao(n);c=await Ee.load(u);let m=await d;a=new Me(c,{author:U(),name:o,connect:(w,f={})=>{let y=new URLSearchParams;f.w&&y.set("w",String(f.w)),f.h&&y.set("h",String(f.h)),f.name&&y.set("name",f.name);let g=y.toString();return uo(`${m}/rooms/${w}${g?`?${g}`:""}`)},probe:()=>yn(i.url)}),a.serverUrl=m,a.getToken=async w=>{let f=await yt(i.url,w);if(f!==void 0)return f;if(await be(i.url))throw new Error($e(i.url))},a.createDocument=async w=>{let f=await gt(i.url,w);if(f!==void 0)return f;if(await be(i.url))throw new Error($e(i.url))},a.control=async(w,f)=>{let y=await bt(i.url,w,f);if(y!==void 0)return y;throw new Error(await be(i.url)?$e(i.url):"no library here: this Sprite has no accounts, so there are no folders")},a.openControl=async(w,f={})=>{if(await be(i.url))throw new Error($e(i.url));let y=await fetch(`${j(i.url)}${w}`,{method:f.method??"GET",headers:f.body!=null?{"Content-Type":"application/json"}:{},body:f.body!=null?JSON.stringify(f.body):void 0}),g=await y.json().catch(()=>({}));if(!y.ok)throw new Error(g?.error?String(g.error):`the server said ${y.status}`);return g},a.editorUrl=j(i.url),a.newsUrl=`${j(i.url)}/news.json`,a.hosted=l}let h=new Ce({tools:sn(a),instructions:ot(a.kind)}),p=h.handle.bind(h);return h.handle=async d=>{let u=await p(d);return d?.method==="initialize"&&h.client?.name&&(a.name=lo(h.client.name),a.announce()),u},{core:c,backend:a,mcp:h,chosen:i}}var wn=null;function be(r){return wn??=wt(r).catch(()=>!1),wn}function mo(r=process.argv){let e=r[1]??"";return/[\\/]pixie-mcp$/.test(e)?"pixie-mcp":/[\\/]sprite-mcp$/.test(e)?"sprite-mcp":"npx pixie-mcp"}function $e(r,e=mo()){return`this machine has not been given a credential for ${j(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(Ot(import.meta.url)){let r=ge("server",null)?"room":ge("editor",null)?"local":null;if(process.argv.includes("login")){let e=ge("server",le||Ue);try{let t=await At(e,{onUrl:n=>process.stderr.write(`
|
|
49
49
|
${n}
|
|
50
50
|
`),label:"the Pixie MCP server"});process.stderr.write(`
|
|
51
51
|
This machine can now draw as ${t.email??t.name??"you"}.
|
|
@@ -53,6 +53,6 @@ default and show it.`}var Vr=nt("room"),Me=class{constructor({name:e="pixie",ver
|
|
|
53
53
|
`)}catch(t){process.stderr.write(`
|
|
54
54
|
${t.message}
|
|
55
55
|
|
|
56
|
-
`),process.exit(1)}}else{let{mcp:e,chosen:t}=await
|
|
57
|
-
`),e.serve(),t.kind==="room"&&(async()=>(await
|
|
58
|
-
`))()}}export{
|
|
56
|
+
`),process.exit(1)}}else{let{mcp:e,chosen:t}=await fo({kind:r,editor:ge("editor",st),server:ge("server",Ue),allowHost:!process.argv.includes("--no-host")});process.stderr.write(`sprite-mcp \u2192 ${t.kind} (${t.why})
|
|
57
|
+
`),e.serve(),t.kind==="room"&&(async()=>(await he(t.url))?.token||await be(t.url)&&process.stderr.write(`sprite-mcp \u2192 ${$e(t.url)}
|
|
58
|
+
`))()}}export{st as DEFAULT_EDITOR,Ue as DEFAULT_SERVER,mo as commandName,fo as createServer,po as detect,lo as presentable,yn as probeRooms,$e as signInFirst,uo as websocketTransport};
|