@howaboua/pi-pet 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{join as Ee}from"node:path";import{fileURLToPath as be}from"node:url";import{randomUUID as ce}from"node:crypto";import{copyFileSync as Ue,lstatSync as fe,mkdirSync as Le,readFileSync as Fe,statSync as Ve}from"node:fs";import{cp as ue,mkdir as de,readFile as pe,rename as we,rm as me,stat as ge,writeFile as Je}from"node:fs/promises";import{homedir as _}from"node:os";import{dirname as he,join as m}from"node:path";import{readFile as G,realpath as P,stat as v}from"node:fs/promises";import{join as y,resolve as W,sep as A}from"node:path";import{readFile as T}from"node:fs/promises";function D(e,t,n){if(n<10)return;return{format:"webp",width:1+e.readUIntLE(t+4,3),height:1+e.readUIntLE(t+7,3)}}function U(e,t,n){if(n<10||e[t+3]!==157||e[t+4]!==1||e[t+5]!==42)return;return{format:"webp",width:e.readUInt16LE(t+6)&16383,height:e.readUInt16LE(t+8)&16383}}function L(e,t,n){if(n<5||e[t]!==47)return;let r=e[t+1]??0,o=e[t+2]??0,a=e[t+3]??0,s=e[t+4]??0;return{format:"webp",width:1+((r|o<<8)&16383),height:1+((o>>6|a<<2|s<<10)&16383)}}function F(e,t,n,r){if(t==="VP8X")return D(e,n,r);if(t==="VP8 ")return U(e,n,r);if(t==="VP8L")return L(e,n,r);return}function V(e){if(e.length<30||e.toString("ascii",0,4)!=="RIFF"||e.toString("ascii",8,12)!=="WEBP")return;let t=12;while(t+8<=e.length){let n=e.toString("ascii",t,t+4),r=e.readUInt32LE(t+4),o=t+8;if(o+r>e.length)throw Error("Truncated WebP chunk.");let a=F(e,n,o,r);if(a)return a;t=o+r+r%2}throw Error("WebP dimensions were not found.")}function z(e){if(e.length>=24&&e.subarray(0,8).toString("hex")==="89504e470d0a1a0a")return{format:"png",width:e.readUInt32BE(16),height:e.readUInt32BE(20)};let n=V(e);if(n)return n;throw Error("Asset must be a PNG or WebP image.")}async function N(e){return z(await T(e))}var J=/^[a-z0-9][a-z0-9._-]{0,63}$/,B=new Set(["__proto__","constructor"]),u=Object.freeze({actionName:64,noteText:280,manifestBytes:131072,catalogBytes:262144,assetBytes:16777216,frameCount:64,decodedPixels:16000000});class i extends Error{name="ContractError"}function M(e,t,n){if(typeof e!=="string")throw new i(`${t} must be a string.`);let r=e.trim();if(!r)throw new i(`${t} cannot be empty.`);if(r.length>n)throw new i(`${t} exceeds ${n} characters.`);return r}function d(e,t="action"){let n=M(e,t,u.actionName);if(n==="list")throw new i(`${t} cannot use the reserved pet_show discovery name list.`);if(B.has(n))throw new i(`${t} cannot use a reserved object property name.`);if(!J.test(n))throw new i(`${t} must use lowercase letters, numbers, dots, underscores, or hyphens.`);return n}function b(e){if(!e||e.includes("\x00")||e.includes("\\")||e.startsWith("/"))return!1;return e.split("/").every((n)=>n!==""&&n!=="."&&n!=="..")}var p={width:192,height:208},H=[["idle",0,[280,110,110,140,140,320]],["running-right",1,[120,120,120,120,120,120,120,220]],["running-left",2,[120,120,120,120,120,120,120,220]],["waving",3,[140,140,140,280]],["jumping",4,[140,140,140,140,280]],["failed",5,[140,140,140,140,140,140,140,240]],["waiting",6,[150,150,150,150,150,260]],["running",7,[120,120,120,120,120,220]],["review",8,[150,150,150,150,150,280]]],X=Object.freeze({active:"running",error:"failed",hello:"waving",settled:"review",success:"jumping",thinking:"running",working:"running"});function w(e,t){if(!e||typeof e!=="object"||Array.isArray(e))throw new i(`${t} must be an object.`);return e}function h(e,t,n,r){if(!Number.isInteger(e)||e<n||e>r)throw new i(`${t} must be an integer from ${n} to ${r}.`);return e}async function O(e,t){let n=await v(e);if(!n.isFile()||n.size>t)throw new i(`${e} is not a bounded regular file.`);return JSON.parse(await G(e,"utf8"))}function Y(e,t){return t.map((n,r)=>({x:r*p.width,y:e*p.height,width:p.width,height:p.height,durationMs:n}))}function q(e){let t=w(e,"pet.json"),n=new Set(["id","displayName","description","spriteVersionNumber","spritesheetPath"]);for(let s of Object.keys(t))if(!n.has(s))throw new i(`pet.json has unknown field: ${s}.`);let r=d(t.id,"pet id");if(typeof t.displayName!=="string"||!t.displayName.trim()||t.displayName.length>80)throw new i("pet.json displayName must contain 1-80 characters.");if(typeof t.description!=="string"||!t.description.trim()||t.description.length>280)throw new i("pet.json description must contain 1-280 characters.");if(t.spriteVersionNumber!==2)throw new i("Only Codex spriteVersionNumber 2 is supported.");if(typeof t.spritesheetPath!=="string"||!b(t.spritesheetPath))throw new i("pet.json spritesheetPath must be a safe relative path.");let o={},a={};for(let[s,f,c]of H)o[s]={name:s,asset:t.spritesheetPath,frames:Y(f,c),loop:!0};for(let s=0;s<16;s+=1){let f=s*22.5,c=`look-${String(f).padStart(3,"0").replace(".5","_5")}`,E=s<8?9:10,l=s%8;a[c]={name:c,asset:t.spritesheetPath,frames:[{x:l*p.width,y:E*p.height,...p,durationMs:1000}],loop:!0}}return{schemaVersion:1,id:r,displayName:t.displayName.trim(),description:t.description.trim(),defaultAction:"idle",canvas:{...p},actions:o,aliases:{...X},directions:a}}function K(e,t){let n=w(t,`action ${e}`),r=new Set(["asset","frames","loop","next"]);for(let s of Object.keys(n))if(!r.has(s))throw new i(`action ${e} has unknown field: ${s}.`);if(typeof n.asset!=="string"||!b(n.asset))throw new i(`action ${e} asset must be a safe relative path.`);if(!Array.isArray(n.frames)||n.frames.length===0||n.frames.length>u.frameCount)throw new i(`action ${e} must have 1-${u.frameCount} frames.`);let o=n.frames.map((s,f)=>{let c=w(s,`action ${e} frame ${f}`),E=new Set(["x","y","width","height","durationMs"]);for(let k of Object.keys(c))if(!E.has(k))throw new i(`action ${e} frame ${f} has unknown field: ${k}.`);let l=h(c.width,"frame width",1,4096),x=h(c.height,"frame height",1,4096);if(l*x>u.decodedPixels)throw new i(`action ${e} frame ${f} is too large.`);return{x:h(c.x,"frame x",0,32768),y:h(c.y,"frame y",0,32768),width:l,height:x,durationMs:h(c.durationMs,"frame durationMs",16,60000)}});if(typeof n.loop!=="boolean")throw new i(`action ${e} loop must be a boolean.`);let a={name:e,asset:n.asset,frames:o,loop:n.loop};if(n.next!==void 0)a.next=d(n.next,`action ${e} next`);return a}async function Z(e){let t=y(e,"pet.pi.json");try{return w(await O(t,u.manifestBytes),"pet.pi.json")}catch(n){if(n.code==="ENOENT")return;throw n}}function Q(e){let t=new Set(["schemaVersion","defaultAction","actions","aliases"]);for(let n of Object.keys(e))if(!t.has(n))throw new i(`pet.pi.json has unknown field: ${n}.`);if(e.schemaVersion!==1)throw new i("pet.pi.json schemaVersion must be 1.")}function ee(e,t){if(e===void 0)return;for(let[n,r]of Object.entries(w(e,"pet.pi.json actions"))){let o=d(n,"action name");t.actions[o]=K(o,r),delete t.aliases[o]}}function te(e,t){if(e===void 0)return;for(let[n,r]of Object.entries(w(e,"pet.pi.json aliases"))){let o=d(n,"alias name");if(t.actions[o])throw new i(`Alias ${o} conflicts with an action of the same name.`);t.aliases[o]=d(r,`alias ${o} target`)}}function ne(e){if(!e.actions[e.defaultAction])throw new i(`Unknown defaultAction: ${e.defaultAction}.`);for(let t of Object.values(e.actions))if(t.next!==void 0&&!e.actions[t.next])throw new i(`Action ${t.name} has unknown next action: ${t.next}.`);for(let[t,n]of Object.entries(e.aliases))if(!e.actions[n])throw new i(`Alias ${t} has unknown target action: ${n}.`)}function re(e){if(Buffer.byteLength(JSON.stringify(e))>u.catalogBytes)throw new i(`Generated pet catalog exceeds ${u.catalogBytes} bytes.`)}async function oe(e,t){let n=await Z(e);if(!n)return;if(Q(n),ee(n.actions,t),te(n.aliases,t),n.defaultAction!==void 0)t.defaultAction=d(n.defaultAction,"defaultAction");ne(t)}async function ie(e,t){let n=await P(y(e,t));if(n!==e&&!n.startsWith(`${e}${A}`))throw new i(`Asset escapes pet directory: ${t}.`);let r=await v(n);if(!r.isFile()||r.size===0||r.size>u.assetBytes)throw new i(`Asset is not a bounded regular file: ${t}.`);let o=await N(n);if(o.width*o.height>u.decodedPixels)throw new i(`Asset has too many decoded pixels: ${t}.`);return o}function se(e,t,n,r){for(let o of t)for(let a of o.frames)if(a.x+a.width>n||a.y+a.height>r)throw new i(`Action ${o.name} has a frame outside ${e}.`)}async function ae(e,t,n){let r=await P(e),o=[...Object.values(t.actions),...Object.values(t.directions)],a=new Set(o.map((s)=>s.asset));for(let s of a){let f=await ie(r,s);if(s===n&&(f.width!==1536||f.height!==2288))throw new i(`Codex v2 spritesheet must be 1536x2288; got ${f.width}x${f.height}.`);se(s,o.filter((c)=>c.asset===s),f.width,f.height)}}async function S(e,t){let n=d(t,"active pet"),r=await P(W(e)),o=await P(y(r,n));if(o!==r&&!o.startsWith(`${r}${A}`))throw new i("Active pet escapes the pets directory.");let a=await O(y(o,"pet.json"),u.manifestBytes),s=q(a);if(s.id!==n)throw new i(`Active pet directory ${n} does not match manifest id ${s.id}.`);await oe(o,s),re(s);let f=w(a,"pet.json").spritesheetPath;return await ae(o,s,f),{directory:o,catalog:s}}var le=16384;function Pe(e=process.env,t=_()){return e.PI_CODING_AGENT_DIR?.trim()||m(t,".pi","agent")}function g(e=process.env,t=_()){return m(Pe(e,t),"pi-pet")}function ye(e){if(!e||typeof e!=="object"||Array.isArray(e))throw Error("Pi Pet config must be an object.");let t=e,n=Object.keys(t).find((r)=>!["schemaVersion","activePet"].includes(r));if(n)throw Error(`Pi Pet config has unknown field: ${n}.`);if(t.schemaVersion!==1)throw Error("Unsupported Pi Pet config schemaVersion.");return{schemaVersion:1,activePet:d(t.activePet,"active pet")}}async function C(e=g()){try{let t=m(e,"config.json"),n=await ge(t);if(!n.isFile()||n.size>le)throw Error(`Pi Pet config must be a bounded regular file: ${t}`);return ye(JSON.parse(await pe(t,"utf8")))}catch(t){if(t.code==="ENOENT")return;throw t}}async function R(e,t,n=g()){let r=d(t,"pet id"),o=m(e,"pets",r),a=m(n,"pets",r),s=!0;try{let c=fe(a);if(!c.isDirectory()||c.isSymbolicLink())throw Error(`User pet path is not a directory: ${a}`)}catch(c){if(c.code!=="ENOENT")throw c;s=!1}if(s)return await S(m(n,"pets"),r),a;await de(he(a),{recursive:!0,mode:448});let f=`${a}.${process.pid}.${ce()}.tmp`;try{return await ue(o,f,{recursive:!0,errorOnExist:!0,force:!1}),await we(f,a),a}catch(c){throw await me(f,{recursive:!0,force:!0}),c}}async function I(e=g()){return(await C(e))?.activePet||"clawa"}var Se=be(new URL("../../",import.meta.url)),j=process.argv[2]||await I(),$e=await R(Se,j);process.stdout.write(`Pet: ${$e}
|
|
1
|
+
import{join as Ee}from"node:path";import{fileURLToPath as be}from"node:url";import{randomUUID as ce}from"node:crypto";import{copyFileSync as Ue,lstatSync as fe,mkdirSync as Le,readFileSync as Fe,statSync as Ve}from"node:fs";import{cp as ue,mkdir as de,readFile as pe,rename as we,rm as me,stat as ge,writeFile as Je}from"node:fs/promises";import{homedir as _}from"node:os";import{dirname as he,join as m}from"node:path";import{readFile as G,realpath as P,stat as v}from"node:fs/promises";import{join as y,resolve as W,sep as A}from"node:path";import{readFile as T}from"node:fs/promises";function D(e,t,n){if(n<10)return;return{format:"webp",width:1+e.readUIntLE(t+4,3),height:1+e.readUIntLE(t+7,3)}}function U(e,t,n){if(n<10||e[t+3]!==157||e[t+4]!==1||e[t+5]!==42)return;return{format:"webp",width:e.readUInt16LE(t+6)&16383,height:e.readUInt16LE(t+8)&16383}}function L(e,t,n){if(n<5||e[t]!==47)return;let r=e[t+1]??0,o=e[t+2]??0,a=e[t+3]??0,s=e[t+4]??0;return{format:"webp",width:1+((r|o<<8)&16383),height:1+((o>>6|a<<2|s<<10)&16383)}}function F(e,t,n,r){if(t==="VP8X")return D(e,n,r);if(t==="VP8 ")return U(e,n,r);if(t==="VP8L")return L(e,n,r);return}function V(e){if(e.length<30||e.toString("ascii",0,4)!=="RIFF"||e.toString("ascii",8,12)!=="WEBP")return;let t=12;while(t+8<=e.length){let n=e.toString("ascii",t,t+4),r=e.readUInt32LE(t+4),o=t+8;if(o+r>e.length)throw Error("Truncated WebP chunk.");let a=F(e,n,o,r);if(a)return a;t=o+r+r%2}throw Error("WebP dimensions were not found.")}function z(e){if(e.length>=24&&e.subarray(0,8).toString("hex")==="89504e470d0a1a0a")return{format:"png",width:e.readUInt32BE(16),height:e.readUInt32BE(20)};let n=V(e);if(n)return n;throw Error("Asset must be a PNG or WebP image.")}async function N(e){return z(await T(e))}var J=/^[a-z0-9][a-z0-9._-]{0,63}$/,B=new Set(["__proto__","constructor"]),u=Object.freeze({actionName:64,noteText:280,manifestBytes:131072,catalogBytes:262144,assetBytes:16777216,frameCount:64,decodedPixels:16000000});class i extends Error{name="ContractError"}function M(e,t,n){if(typeof e!=="string")throw new i(`${t} must be a string.`);let r=e.trim();if(!r)throw new i(`${t} cannot be empty.`);if(r.length>n)throw new i(`${t} exceeds ${n} characters.`);return r}function d(e,t="action"){let n=M(e,t,u.actionName);if(n==="list")throw new i(`${t} cannot use the reserved pet_show discovery name list.`);if(B.has(n))throw new i(`${t} cannot use a reserved object property name.`);if(!J.test(n))throw new i(`${t} must use lowercase letters, numbers, dots, underscores, or hyphens.`);return n}function b(e){if(!e||e.includes("\x00")||e.includes("\\")||e.startsWith("/"))return!1;return e.split("/").every((n)=>n!==""&&n!=="."&&n!=="..")}var p={width:192,height:208},H=[["idle",0,[280,110,110,140,140,320]],["running-right",1,[120,120,120,120,120,120,120,220]],["running-left",2,[120,120,120,120,120,120,120,220]],["waving",3,[140,140,140,280]],["jumping",4,[140,140,140,140,280]],["failed",5,[140,140,140,140,140,140,140,240]],["waiting",6,[150,150,150,150,150,260]],["running",7,[120,120,120,120,120,220]],["review",8,[150,150,150,150,150,280]]],X=Object.freeze({active:"running",error:"failed",hello:"waving",settled:"review",success:"jumping",thinking:"running",working:"running"});function w(e,t){if(!e||typeof e!=="object"||Array.isArray(e))throw new i(`${t} must be an object.`);return e}function h(e,t,n,r){if(!Number.isInteger(e)||e<n||e>r)throw new i(`${t} must be an integer from ${n} to ${r}.`);return e}async function O(e,t){let n=await v(e);if(!n.isFile()||n.size>t)throw new i(`${e} is not a bounded regular file.`);return JSON.parse(await G(e,"utf8"))}function Y(e,t){return t.map((n,r)=>({x:r*p.width,y:e*p.height,width:p.width,height:p.height,durationMs:n}))}function q(e){let t=w(e,"pet.json"),n=new Set(["id","displayName","description","spriteVersionNumber","spritesheetPath"]);for(let s of Object.keys(t))if(!n.has(s))throw new i(`pet.json has unknown field: ${s}.`);let r=d(t.id,"pet id");if(typeof t.displayName!=="string"||!t.displayName.trim()||t.displayName.length>80)throw new i("pet.json displayName must contain 1-80 characters.");if(typeof t.description!=="string"||!t.description.trim()||t.description.length>280)throw new i("pet.json description must contain 1-280 characters.");if(t.spriteVersionNumber!==2)throw new i("Only Codex spriteVersionNumber 2 is supported.");if(typeof t.spritesheetPath!=="string"||!b(t.spritesheetPath))throw new i("pet.json spritesheetPath must be a safe relative path.");let o={},a={};for(let[s,f,c]of H)o[s]={name:s,asset:t.spritesheetPath,frames:Y(f,c),loop:!0};for(let s=0;s<16;s+=1){let f=s*22.5,c=`look-${String(f).padStart(3,"0").replace(".5","_5")}`,E=s<8?9:10,l=s%8;a[c]={name:c,asset:t.spritesheetPath,frames:[{x:l*p.width,y:E*p.height,...p,durationMs:1000}],loop:!0}}return{schemaVersion:1,id:r,displayName:t.displayName.trim(),description:t.description.trim(),defaultAction:"idle",canvas:{...p},actions:o,aliases:{...X},directions:a}}function K(e,t){let n=w(t,`action ${e}`),r=new Set(["asset","frames","loop","next"]);for(let s of Object.keys(n))if(!r.has(s))throw new i(`action ${e} has unknown field: ${s}.`);if(typeof n.asset!=="string"||!b(n.asset))throw new i(`action ${e} asset must be a safe relative path.`);if(!Array.isArray(n.frames)||n.frames.length===0||n.frames.length>u.frameCount)throw new i(`action ${e} must have 1-${u.frameCount} frames.`);let o=n.frames.map((s,f)=>{let c=w(s,`action ${e} frame ${f}`),E=new Set(["x","y","width","height","durationMs"]);for(let x of Object.keys(c))if(!E.has(x))throw new i(`action ${e} frame ${f} has unknown field: ${x}.`);let l=h(c.width,"frame width",1,4096),k=h(c.height,"frame height",1,4096);if(l*k>u.decodedPixels)throw new i(`action ${e} frame ${f} is too large.`);return{x:h(c.x,"frame x",0,32768),y:h(c.y,"frame y",0,32768),width:l,height:k,durationMs:h(c.durationMs,"frame durationMs",16,60000)}});if(typeof n.loop!=="boolean")throw new i(`action ${e} loop must be a boolean.`);let a={name:e,asset:n.asset,frames:o,loop:n.loop};if(n.next!==void 0)a.next=d(n.next,`action ${e} next`);return a}async function Z(e){let t=y(e,"pet.pi.json");try{return w(await O(t,u.manifestBytes),"pet.pi.json")}catch(n){if(n.code==="ENOENT")return;throw n}}function Q(e){let t=new Set(["schemaVersion","defaultAction","actions","aliases"]);for(let n of Object.keys(e))if(!t.has(n))throw new i(`pet.pi.json has unknown field: ${n}.`);if(e.schemaVersion!==1)throw new i("pet.pi.json schemaVersion must be 1.")}function ee(e,t){if(e===void 0)return;for(let[n,r]of Object.entries(w(e,"pet.pi.json actions"))){let o=d(n,"action name");t.actions[o]=K(o,r),delete t.aliases[o]}}function te(e,t){if(e===void 0)return;for(let[n,r]of Object.entries(w(e,"pet.pi.json aliases"))){let o=d(n,"alias name");if(t.actions[o])throw new i(`Alias ${o} conflicts with an action of the same name.`);t.aliases[o]=d(r,`alias ${o} target`)}}function ne(e){if(!e.actions[e.defaultAction])throw new i(`Unknown defaultAction: ${e.defaultAction}.`);for(let t of Object.values(e.actions))if(t.next!==void 0&&!e.actions[t.next])throw new i(`Action ${t.name} has unknown next action: ${t.next}.`);for(let[t,n]of Object.entries(e.aliases))if(!e.actions[n])throw new i(`Alias ${t} has unknown target action: ${n}.`)}function re(e){if(Buffer.byteLength(JSON.stringify(e))>u.catalogBytes)throw new i(`Generated pet catalog exceeds ${u.catalogBytes} bytes.`)}async function oe(e,t){let n=await Z(e);if(!n)return;if(Q(n),ee(n.actions,t),te(n.aliases,t),n.defaultAction!==void 0)t.defaultAction=d(n.defaultAction,"defaultAction");ne(t)}async function ie(e,t){let n=await P(y(e,t));if(n!==e&&!n.startsWith(`${e}${A}`))throw new i(`Asset escapes pet directory: ${t}.`);let r=await v(n);if(!r.isFile()||r.size===0||r.size>u.assetBytes)throw new i(`Asset is not a bounded regular file: ${t}.`);let o=await N(n);if(o.width*o.height>u.decodedPixels)throw new i(`Asset has too many decoded pixels: ${t}.`);return o}function se(e,t,n,r){for(let o of t)for(let a of o.frames)if(a.x+a.width>n||a.y+a.height>r)throw new i(`Action ${o.name} has a frame outside ${e}.`)}async function ae(e,t,n){let r=await P(e),o=[...Object.values(t.actions),...Object.values(t.directions)],a=new Set(o.map((s)=>s.asset));for(let s of a){let f=await ie(r,s);if(s===n&&(f.width!==1536||f.height!==2288))throw new i(`Codex v2 spritesheet must be 1536x2288; got ${f.width}x${f.height}.`);se(s,o.filter((c)=>c.asset===s),f.width,f.height)}}async function S(e,t){let n=d(t,"active pet"),r=await P(W(e)),o=await P(y(r,n));if(o!==r&&!o.startsWith(`${r}${A}`))throw new i("Active pet escapes the pets directory.");let a=await O(y(o,"pet.json"),u.manifestBytes),s=q(a);if(s.id!==n)throw new i(`Active pet directory ${n} does not match manifest id ${s.id}.`);await oe(o,s),re(s);let f=w(a,"pet.json").spritesheetPath;return await ae(o,s,f),{directory:o,catalog:s}}var le=16384;function Pe(e=process.env,t=_()){return e.PI_CODING_AGENT_DIR?.trim()||m(t,".pi","agent")}function g(e=process.env,t=_()){return m(Pe(e,t),"pi-pet")}function ye(e){if(!e||typeof e!=="object"||Array.isArray(e))throw Error("Pi Pet config must be an object.");let t=e,n=Object.keys(t).find((r)=>!["schemaVersion","activePet"].includes(r));if(n)throw Error(`Pi Pet config has unknown field: ${n}.`);if(t.schemaVersion!==1)throw Error("Unsupported Pi Pet config schemaVersion.");return{schemaVersion:1,activePet:d(t.activePet,"active pet")}}async function C(e=g()){try{let t=m(e,"config.json"),n=await ge(t);if(!n.isFile()||n.size>le)throw Error(`Pi Pet config must be a bounded regular file: ${t}`);return ye(JSON.parse(await pe(t,"utf8")))}catch(t){if(t.code==="ENOENT")return;throw t}}async function R(e,t,n=g()){let r=d(t,"pet id"),o=m(e,"pets",r),a=m(n,"pets",r),s=!0;try{let c=fe(a);if(!c.isDirectory()||c.isSymbolicLink())throw Error(`User pet path is not a directory: ${a}`)}catch(c){if(c.code!=="ENOENT")throw c;s=!1}if(s)return await S(m(n,"pets"),r),a;await de(he(a),{recursive:!0,mode:448});let f=`${a}.${process.pid}.${ce()}.tmp`;try{return await ue(o,f,{recursive:!0,errorOnExist:!0,force:!1}),await we(f,a),a}catch(c){throw await me(f,{recursive:!0,force:!0}),c}}async function I(e=g()){return(await C(e))?.activePet||"clawa"}var Se=be(new URL("../../",import.meta.url)),j=process.argv[2]||await I(),$e=await R(Se,j);process.stdout.write(`Pet: ${$e}
|
|
2
2
|
Runs: ${Ee(g(),"runs",j)}
|
|
3
3
|
`);
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{basename as we,dirname as me,join as ge,resolve as he}from"node:path";import{readFile as G,realpath as l,stat as v}from"node:fs/promises";import{join as P,resolve as W,sep as A}from"node:path";import{readFile as T}from"node:fs/promises";function D(e,t,n){if(n<10)return;return{format:"webp",width:1+e.readUIntLE(t+4,3),height:1+e.readUIntLE(t+7,3)}}function L(e,t,n){if(n<10||e[t+3]!==157||e[t+4]!==1||e[t+5]!==42)return;return{format:"webp",width:e.readUInt16LE(t+6)&16383,height:e.readUInt16LE(t+8)&16383}}function U(e,t,n){if(n<5||e[t]!==47)return;let r=e[t+1]??0,o=e[t+2]??0,a=e[t+3]??0,s=e[t+4]??0;return{format:"webp",width:1+((r|o<<8)&16383),height:1+((o>>6|a<<2|s<<10)&16383)}}function F(e,t,n,r){if(t==="VP8X")return D(e,n,r);if(t==="VP8 ")return L(e,n,r);if(t==="VP8L")return U(e,n,r);return}function V(e){if(e.length<30||e.toString("ascii",0,4)!=="RIFF"||e.toString("ascii",8,12)!=="WEBP")return;let t=12;while(t+8<=e.length){let n=e.toString("ascii",t,t+4),r=e.readUInt32LE(t+4),o=t+8;if(o+r>e.length)throw Error("Truncated WebP chunk.");let a=F(e,n,o,r);if(a)return a;t=o+r+r%2}throw Error("WebP dimensions were not found.")}function z(e){if(e.length>=24&&e.subarray(0,8).toString("hex")==="89504e470d0a1a0a")return{format:"png",width:e.readUInt32BE(16),height:e.readUInt32BE(20)};let n=V(e);if(n)return n;throw Error("Asset must be a PNG or WebP image.")}async function N(e){return z(await T(e))}var J=/^[a-z0-9][a-z0-9._-]{0,63}$/,B=new Set(["__proto__","constructor"]),d=Object.freeze({actionName:64,noteText:280,manifestBytes:131072,catalogBytes:262144,assetBytes:16777216,frameCount:64,decodedPixels:16000000});class i extends Error{name="ContractError"}function M(e,t,n){if(typeof e!=="string")throw new i(`${t} must be a string.`);let r=e.trim();if(!r)throw new i(`${t} cannot be empty.`);if(r.length>n)throw new i(`${t} exceeds ${n} characters.`);return r}function u(e,t="action"){let n=M(e,t,d.actionName);if(n==="list")throw new i(`${t} cannot use the reserved pet_show discovery name list.`);if(B.has(n))throw new i(`${t} cannot use a reserved object property name.`);if(!J.test(n))throw new i(`${t} must use lowercase letters, numbers, dots, underscores, or hyphens.`);return n}function b(e){if(!e||e.includes("\x00")||e.includes("\\")||e.startsWith("/"))return!1;return e.split("/").every((n)=>n!==""&&n!=="."&&n!=="..")}var p={width:192,height:208},H=[["idle",0,[280,110,110,140,140,320]],["running-right",1,[120,120,120,120,120,120,120,220]],["running-left",2,[120,120,120,120,120,120,120,220]],["waving",3,[140,140,140,280]],["jumping",4,[140,140,140,140,280]],["failed",5,[140,140,140,140,140,140,140,240]],["waiting",6,[150,150,150,150,150,260]],["running",7,[120,120,120,120,120,220]],["review",8,[150,150,150,150,150,280]]],X=Object.freeze({active:"running",error:"failed",hello:"waving",settled:"review",success:"jumping",thinking:"running",working:"running"});function w(e,t){if(!e||typeof e!=="object"||Array.isArray(e))throw new i(`${t} must be an object.`);return e}function m(e,t,n,r){if(!Number.isInteger(e)||e<n||e>r)throw new i(`${t} must be an integer from ${n} to ${r}.`);return e}async function O(e,t){let n=await v(e);if(!n.isFile()||n.size>t)throw new i(`${e} is not a bounded regular file.`);return JSON.parse(await G(e,"utf8"))}function Y(e,t){return t.map((n,r)=>({x:r*p.width,y:e*p.height,width:p.width,height:p.height,durationMs:n}))}function q(e){let t=w(e,"pet.json"),n=new Set(["id","displayName","description","spriteVersionNumber","spritesheetPath"]);for(let s of Object.keys(t))if(!n.has(s))throw new i(`pet.json has unknown field: ${s}.`);let r=u(t.id,"pet id");if(typeof t.displayName!=="string"||!t.displayName.trim()||t.displayName.length>80)throw new i("pet.json displayName must contain 1-80 characters.");if(typeof t.description!=="string"||!t.description.trim()||t.description.length>280)throw new i("pet.json description must contain 1-280 characters.");if(t.spriteVersionNumber!==2)throw new i("Only Codex spriteVersionNumber 2 is supported.");if(typeof t.spritesheetPath!=="string"||!b(t.spritesheetPath))throw new i("pet.json spritesheetPath must be a safe relative path.");let o={},a={};for(let[s,c,f]of H)o[s]={name:s,asset:t.spritesheetPath,frames:Y(c,f),loop:!0};for(let s=0;s<16;s+=1){let c=s*22.5,f=`look-${String(c).padStart(3,"0").replace(".5","_5")}`,E=s<8?9:10,h=s%8;a[f]={name:f,asset:t.spritesheetPath,frames:[{x:h*p.width,y:E*p.height,...p,durationMs:1000}],loop:!0}}return{schemaVersion:1,id:r,displayName:t.displayName.trim(),description:t.description.trim(),defaultAction:"idle",canvas:{...p},actions:o,aliases:{...X},directions:a}}function K(e,t){let n=w(t,`action ${e}`),r=new Set(["asset","frames","loop","next"]);for(let s of Object.keys(n))if(!r.has(s))throw new i(`action ${e} has unknown field: ${s}.`);if(typeof n.asset!=="string"||!b(n.asset))throw new i(`action ${e} asset must be a safe relative path.`);if(!Array.isArray(n.frames)||n.frames.length===0||n.frames.length>d.frameCount)throw new i(`action ${e} must have 1-${d.frameCount} frames.`);let o=n.frames.map((s,c)=>{let f=w(s,`action ${e} frame ${c}`),E=new Set(["x","y","width","height","durationMs"]);for(let
|
|
1
|
+
import{basename as we,dirname as me,join as ge,resolve as he}from"node:path";import{readFile as G,realpath as l,stat as v}from"node:fs/promises";import{join as P,resolve as W,sep as A}from"node:path";import{readFile as T}from"node:fs/promises";function D(e,t,n){if(n<10)return;return{format:"webp",width:1+e.readUIntLE(t+4,3),height:1+e.readUIntLE(t+7,3)}}function L(e,t,n){if(n<10||e[t+3]!==157||e[t+4]!==1||e[t+5]!==42)return;return{format:"webp",width:e.readUInt16LE(t+6)&16383,height:e.readUInt16LE(t+8)&16383}}function U(e,t,n){if(n<5||e[t]!==47)return;let r=e[t+1]??0,o=e[t+2]??0,a=e[t+3]??0,s=e[t+4]??0;return{format:"webp",width:1+((r|o<<8)&16383),height:1+((o>>6|a<<2|s<<10)&16383)}}function F(e,t,n,r){if(t==="VP8X")return D(e,n,r);if(t==="VP8 ")return L(e,n,r);if(t==="VP8L")return U(e,n,r);return}function V(e){if(e.length<30||e.toString("ascii",0,4)!=="RIFF"||e.toString("ascii",8,12)!=="WEBP")return;let t=12;while(t+8<=e.length){let n=e.toString("ascii",t,t+4),r=e.readUInt32LE(t+4),o=t+8;if(o+r>e.length)throw Error("Truncated WebP chunk.");let a=F(e,n,o,r);if(a)return a;t=o+r+r%2}throw Error("WebP dimensions were not found.")}function z(e){if(e.length>=24&&e.subarray(0,8).toString("hex")==="89504e470d0a1a0a")return{format:"png",width:e.readUInt32BE(16),height:e.readUInt32BE(20)};let n=V(e);if(n)return n;throw Error("Asset must be a PNG or WebP image.")}async function N(e){return z(await T(e))}var J=/^[a-z0-9][a-z0-9._-]{0,63}$/,B=new Set(["__proto__","constructor"]),d=Object.freeze({actionName:64,noteText:280,manifestBytes:131072,catalogBytes:262144,assetBytes:16777216,frameCount:64,decodedPixels:16000000});class i extends Error{name="ContractError"}function M(e,t,n){if(typeof e!=="string")throw new i(`${t} must be a string.`);let r=e.trim();if(!r)throw new i(`${t} cannot be empty.`);if(r.length>n)throw new i(`${t} exceeds ${n} characters.`);return r}function u(e,t="action"){let n=M(e,t,d.actionName);if(n==="list")throw new i(`${t} cannot use the reserved pet_show discovery name list.`);if(B.has(n))throw new i(`${t} cannot use a reserved object property name.`);if(!J.test(n))throw new i(`${t} must use lowercase letters, numbers, dots, underscores, or hyphens.`);return n}function b(e){if(!e||e.includes("\x00")||e.includes("\\")||e.startsWith("/"))return!1;return e.split("/").every((n)=>n!==""&&n!=="."&&n!=="..")}var p={width:192,height:208},H=[["idle",0,[280,110,110,140,140,320]],["running-right",1,[120,120,120,120,120,120,120,220]],["running-left",2,[120,120,120,120,120,120,120,220]],["waving",3,[140,140,140,280]],["jumping",4,[140,140,140,140,280]],["failed",5,[140,140,140,140,140,140,140,240]],["waiting",6,[150,150,150,150,150,260]],["running",7,[120,120,120,120,120,220]],["review",8,[150,150,150,150,150,280]]],X=Object.freeze({active:"running",error:"failed",hello:"waving",settled:"review",success:"jumping",thinking:"running",working:"running"});function w(e,t){if(!e||typeof e!=="object"||Array.isArray(e))throw new i(`${t} must be an object.`);return e}function m(e,t,n,r){if(!Number.isInteger(e)||e<n||e>r)throw new i(`${t} must be an integer from ${n} to ${r}.`);return e}async function O(e,t){let n=await v(e);if(!n.isFile()||n.size>t)throw new i(`${e} is not a bounded regular file.`);return JSON.parse(await G(e,"utf8"))}function Y(e,t){return t.map((n,r)=>({x:r*p.width,y:e*p.height,width:p.width,height:p.height,durationMs:n}))}function q(e){let t=w(e,"pet.json"),n=new Set(["id","displayName","description","spriteVersionNumber","spritesheetPath"]);for(let s of Object.keys(t))if(!n.has(s))throw new i(`pet.json has unknown field: ${s}.`);let r=u(t.id,"pet id");if(typeof t.displayName!=="string"||!t.displayName.trim()||t.displayName.length>80)throw new i("pet.json displayName must contain 1-80 characters.");if(typeof t.description!=="string"||!t.description.trim()||t.description.length>280)throw new i("pet.json description must contain 1-280 characters.");if(t.spriteVersionNumber!==2)throw new i("Only Codex spriteVersionNumber 2 is supported.");if(typeof t.spritesheetPath!=="string"||!b(t.spritesheetPath))throw new i("pet.json spritesheetPath must be a safe relative path.");let o={},a={};for(let[s,c,f]of H)o[s]={name:s,asset:t.spritesheetPath,frames:Y(c,f),loop:!0};for(let s=0;s<16;s+=1){let c=s*22.5,f=`look-${String(c).padStart(3,"0").replace(".5","_5")}`,E=s<8?9:10,h=s%8;a[f]={name:f,asset:t.spritesheetPath,frames:[{x:h*p.width,y:E*p.height,...p,durationMs:1000}],loop:!0}}return{schemaVersion:1,id:r,displayName:t.displayName.trim(),description:t.description.trim(),defaultAction:"idle",canvas:{...p},actions:o,aliases:{...X},directions:a}}function K(e,t){let n=w(t,`action ${e}`),r=new Set(["asset","frames","loop","next"]);for(let s of Object.keys(n))if(!r.has(s))throw new i(`action ${e} has unknown field: ${s}.`);if(typeof n.asset!=="string"||!b(n.asset))throw new i(`action ${e} asset must be a safe relative path.`);if(!Array.isArray(n.frames)||n.frames.length===0||n.frames.length>d.frameCount)throw new i(`action ${e} must have 1-${d.frameCount} frames.`);let o=n.frames.map((s,c)=>{let f=w(s,`action ${e} frame ${c}`),E=new Set(["x","y","width","height","durationMs"]);for(let x of Object.keys(f))if(!E.has(x))throw new i(`action ${e} frame ${c} has unknown field: ${x}.`);let h=m(f.width,"frame width",1,4096),k=m(f.height,"frame height",1,4096);if(h*k>d.decodedPixels)throw new i(`action ${e} frame ${c} is too large.`);return{x:m(f.x,"frame x",0,32768),y:m(f.y,"frame y",0,32768),width:h,height:k,durationMs:m(f.durationMs,"frame durationMs",16,60000)}});if(typeof n.loop!=="boolean")throw new i(`action ${e} loop must be a boolean.`);let a={name:e,asset:n.asset,frames:o,loop:n.loop};if(n.next!==void 0)a.next=u(n.next,`action ${e} next`);return a}async function Z(e){let t=P(e,"pet.pi.json");try{return w(await O(t,d.manifestBytes),"pet.pi.json")}catch(n){if(n.code==="ENOENT")return;throw n}}function Q(e){let t=new Set(["schemaVersion","defaultAction","actions","aliases"]);for(let n of Object.keys(e))if(!t.has(n))throw new i(`pet.pi.json has unknown field: ${n}.`);if(e.schemaVersion!==1)throw new i("pet.pi.json schemaVersion must be 1.")}function ee(e,t){if(e===void 0)return;for(let[n,r]of Object.entries(w(e,"pet.pi.json actions"))){let o=u(n,"action name");t.actions[o]=K(o,r),delete t.aliases[o]}}function te(e,t){if(e===void 0)return;for(let[n,r]of Object.entries(w(e,"pet.pi.json aliases"))){let o=u(n,"alias name");if(t.actions[o])throw new i(`Alias ${o} conflicts with an action of the same name.`);t.aliases[o]=u(r,`alias ${o} target`)}}function ne(e){if(!e.actions[e.defaultAction])throw new i(`Unknown defaultAction: ${e.defaultAction}.`);for(let t of Object.values(e.actions))if(t.next!==void 0&&!e.actions[t.next])throw new i(`Action ${t.name} has unknown next action: ${t.next}.`);for(let[t,n]of Object.entries(e.aliases))if(!e.actions[n])throw new i(`Alias ${t} has unknown target action: ${n}.`)}function re(e){if(Buffer.byteLength(JSON.stringify(e))>d.catalogBytes)throw new i(`Generated pet catalog exceeds ${d.catalogBytes} bytes.`)}async function oe(e,t){let n=await Z(e);if(!n)return;if(Q(n),ee(n.actions,t),te(n.aliases,t),n.defaultAction!==void 0)t.defaultAction=u(n.defaultAction,"defaultAction");ne(t)}async function ie(e,t){let n=await l(P(e,t));if(n!==e&&!n.startsWith(`${e}${A}`))throw new i(`Asset escapes pet directory: ${t}.`);let r=await v(n);if(!r.isFile()||r.size===0||r.size>d.assetBytes)throw new i(`Asset is not a bounded regular file: ${t}.`);let o=await N(n);if(o.width*o.height>d.decodedPixels)throw new i(`Asset has too many decoded pixels: ${t}.`);return o}function se(e,t,n,r){for(let o of t)for(let a of o.frames)if(a.x+a.width>n||a.y+a.height>r)throw new i(`Action ${o.name} has a frame outside ${e}.`)}async function ae(e,t,n){let r=await l(e),o=[...Object.values(t.actions),...Object.values(t.directions)],a=new Set(o.map((s)=>s.asset));for(let s of a){let c=await ie(r,s);if(s===n&&(c.width!==1536||c.height!==2288))throw new i(`Codex v2 spritesheet must be 1536x2288; got ${c.width}x${c.height}.`);se(s,o.filter((f)=>f.asset===s),c.width,c.height)}}async function y(e,t){let n=u(t,"active pet"),r=await l(W(e)),o=await l(P(r,n));if(o!==r&&!o.startsWith(`${r}${A}`))throw new i("Active pet escapes the pets directory.");let a=await O(P(o,"pet.json"),d.manifestBytes),s=q(a);if(s.id!==n)throw new i(`Active pet directory ${n} does not match manifest id ${s.id}.`);await oe(o,s),re(s);let c=w(a,"pet.json").spritesheetPath;return await ae(o,s,c),{directory:o,catalog:s}}import{cp as _e,mkdir as Ce,readFile as ce,rename as je,rm as Ie,stat as fe,writeFile as Re}from"node:fs/promises";import{homedir as _}from"node:os";import{dirname as Le,join as S}from"node:path";var de=16384;function ue(e=process.env,t=_()){return e.PI_CODING_AGENT_DIR?.trim()||S(t,".pi","agent")}function g(e=process.env,t=_()){return S(ue(e,t),"pi-pet")}function pe(e){if(!e||typeof e!=="object"||Array.isArray(e))throw Error("Pi Pet config must be an object.");let t=e,n=Object.keys(t).find((r)=>!["schemaVersion","activePet"].includes(r));if(n)throw Error(`Pi Pet config has unknown field: ${n}.`);if(t.schemaVersion!==1)throw Error("Unsupported Pi Pet config schemaVersion.");return{schemaVersion:1,activePet:u(t.activePet,"active pet")}}async function C(e=g()){try{let t=S(e,"config.json"),n=await fe(t);if(!n.isFile()||n.size>de)throw Error(`Pi Pet config must be a bounded regular file: ${t}`);return pe(JSON.parse(await ce(t,"utf8")))}catch(t){if(t.code==="ENOENT")return;throw t}}async function j(e=g()){return(await C(e))?.activePet||"clawa"}var le=await j(),I=he(process.argv[2]||ge(g(),"pets",le)),R=await y(me(I),we(I));process.stdout.write(`${R.catalog.displayName}: ${Object.keys(R.catalog.actions).length} actions valid
|
|
2
2
|
`);
|
|
@@ -175,7 +175,7 @@ main()
|
|
|
175
175
|
.finally(() => { clearInterval(heartbeat); clearTimeout(stopTimer); });
|
|
176
176
|
`;
|
|
177
177
|
|
|
178
|
-
|
|
178
|
+
interface DesktopProcessSpec {
|
|
179
179
|
program: string;
|
|
180
180
|
args: string[];
|
|
181
181
|
source: string;
|
|
@@ -197,11 +197,7 @@ function packagedDesktopSource(): { files: Record<string, string>; packageVersio
|
|
|
197
197
|
return { files, packageVersion: manifest.version, sourceDigest: digest.digest("hex") };
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
-
|
|
201
|
-
target: string,
|
|
202
|
-
gippityUrl: string,
|
|
203
|
-
useSshSourceAddress = false,
|
|
204
|
-
): DesktopProcessSpec {
|
|
200
|
+
function remoteDesktopProcessSpec(target: string, gippityUrl: string, useSshSourceAddress = false): DesktopProcessSpec {
|
|
205
201
|
const origin = parseDesktopConfig({ schemaVersion: 1, gippityUrl }).gippityUrl;
|
|
206
202
|
const options = { ...packagedDesktopSource(), gippityUrl: origin, useSshSourceAddress };
|
|
207
203
|
return {
|
|
@@ -211,7 +207,7 @@ export function remoteDesktopProcessSpec(
|
|
|
211
207
|
};
|
|
212
208
|
}
|
|
213
209
|
|
|
214
|
-
|
|
210
|
+
function localDesktopProcessSpec(gippityUrl: string): DesktopProcessSpec {
|
|
215
211
|
const origin = parseDesktopConfig({ schemaVersion: 1, gippityUrl }).gippityUrl;
|
|
216
212
|
const agentDirectory = process.env["PI_CODING_AGENT_DIR"]?.trim() || join(homedir(), ".pi", "agent");
|
|
217
213
|
const options = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@howaboua/pi-pet",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Animated companions for GipPity Remote.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
},
|
|
77
77
|
"packageManager": "bun@1.4.0",
|
|
78
78
|
"peerDependencies": {
|
|
79
|
-
"@howaboua/pi-gippity-control": ">=0.0.
|
|
79
|
+
"@howaboua/pi-gippity-control": ">=0.0.20",
|
|
80
80
|
"@earendil-works/pi-coding-agent": ">=0.84.3",
|
|
81
81
|
"@earendil-works/pi-tui": ">=0.84.3",
|
|
82
82
|
"typebox": "*"
|
|
@@ -88,8 +88,8 @@
|
|
|
88
88
|
},
|
|
89
89
|
"devDependencies": {
|
|
90
90
|
"@biomejs/biome": "2.5.10",
|
|
91
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
92
|
-
"@earendil-works/pi-tui": "0.
|
|
91
|
+
"@earendil-works/pi-coding-agent": "0.86.1",
|
|
92
|
+
"@earendil-works/pi-tui": "0.86.1",
|
|
93
93
|
"@types/node": "^26.2.0",
|
|
94
94
|
"electron": "43.4.1",
|
|
95
95
|
"knip": "^6.32.2",
|
|
@@ -22,7 +22,7 @@ export function hyprlandCursorSocket(env: NodeJS.ProcessEnv): string | undefined
|
|
|
22
22
|
return join(runtime, "hypr", signature, ".socket.sock");
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
function parseHyprlandCursorResponse(value: string): DesktopCursorPosition {
|
|
26
26
|
const position = parseDesktopCursorPosition(JSON.parse(value) as unknown);
|
|
27
27
|
if (!position) throw new Error("Hyprland returned an empty cursor position.");
|
|
28
28
|
return position;
|
|
@@ -42,7 +42,7 @@ function parseCoordinatePair(value: unknown, label: string): [number, number] {
|
|
|
42
42
|
return [value[0], value[1]];
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
function parseHyprlandClientBounds(value: string, pid: number): DesktopWindowBounds {
|
|
46
46
|
const clients = JSON.parse(value) as unknown;
|
|
47
47
|
if (!Array.isArray(clients)) throw new Error("Hyprland clients response must be an array.");
|
|
48
48
|
const client = clients.find(
|
package/src/pet-image-size.ts
CHANGED
|
@@ -57,7 +57,7 @@ function webpSize(buffer: Buffer): ImageSize | undefined {
|
|
|
57
57
|
throw new Error("WebP dimensions were not found.");
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
function imageSize(buffer: Buffer): ImageSize {
|
|
61
61
|
const pngSignature = "89504e470d0a1a0a";
|
|
62
62
|
if (buffer.length >= 24 && buffer.subarray(0, 8).toString("hex") === pngSignature) {
|
|
63
63
|
return { format: "png", width: buffer.readUInt32BE(16), height: buffer.readUInt32BE(20) };
|