@bitefoundry/retrieviewer 0.0.17 → 0.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/ASSET-PROVENANCE.md +11 -0
  2. package/ASSETS-LICENSE.md +6 -0
  3. package/ATTRIBUTION.md +8 -0
  4. package/DESKTOP-PLATFORMS.md +1 -1
  5. package/INSTALLING.md +2 -1
  6. package/LICENSE +16 -16
  7. package/NATIVE-RUNTIME.md +89 -0
  8. package/PREREQUISITES.md +7 -0
  9. package/PUPPY-MEMORY.md +11 -0
  10. package/README.ko.md +10 -1
  11. package/README.md +11 -1
  12. package/RELEASING.md +19 -0
  13. package/THIRD-PARTY-NOTICES.md +8 -0
  14. package/WINDOWS-COMPANION.md +15 -6
  15. package/braille.mjs +2 -2
  16. package/integrations/native-windows/CMakeLists.txt +15 -0
  17. package/integrations/native-windows/THIRD-PARTY-LICENSES.txt +77 -0
  18. package/integrations/native-windows/application.manifest +8 -0
  19. package/integrations/native-windows/bin/win32-x64/retrieviewer-native.exe +0 -0
  20. package/integrations/native-windows/desktop.mjs +45 -0
  21. package/integrations/native-windows/host.cpp +84 -0
  22. package/integrations/native-windows/manifest.json +14 -0
  23. package/integrations/native-windows/native-control.hpp +41 -0
  24. package/integrations/native-windows/native-window.hpp +128 -0
  25. package/integrations/native-windows/runtime.bundle.mjs +4694 -0
  26. package/integrations/native-windows/runtime.mjs +55 -0
  27. package/integrations/native-windows/sources.json +23 -0
  28. package/integrations/purrviewer/ART.md +289 -0
  29. package/integrations/purrviewer/ASSETS-LICENSE.md +21 -0
  30. package/integrations/purrviewer/LICENSE +24 -0
  31. package/integrations/purrviewer/art/ginger.bin.gz +0 -0
  32. package/integrations/purrviewer/art/ginger.json +1707 -0
  33. package/integrations/purrviewer/art/ginger.rvz +0 -0
  34. package/integrations/purrviewer/src/contact.mjs +63 -0
  35. package/integrations/purrviewer/src/habitat.mjs +41 -0
  36. package/integrations/purrviewer/src/life.mjs +280 -0
  37. package/integrations/purrviewer/src/purrviewer-core.mjs +130 -0
  38. package/integrations/purrviewer/src/purrviewer.mjs +37 -0
  39. package/integrations/purrviewer/src/walk-cycle.mjs +3 -0
  40. package/integrations/purrviewer/src/world.mjs +216 -0
  41. package/integrations/purrviewer/vendor/retrieviewer/LICENSE-CODE +21 -0
  42. package/integrations/purrviewer/vendor/retrieviewer/braille.mjs +73 -0
  43. package/integrations/purrviewer/vendor/retrieviewer/puppy-frame-file.mjs +15 -0
  44. package/integrations/purrviewer/vendor/retrieviewer/puppy-frame-indexed.mjs +78 -0
  45. package/integrations/purrviewer/vendor/retrieviewer/puppy-frame-predictive.mjs +170 -0
  46. package/integrations/purrviewer/vendor/retrieviewer/puppy-frame-store.mjs +114 -0
  47. package/integrations/windows-overlay/desktop.mjs +11 -3
  48. package/integrations/windows-overlay/worker.mjs +9 -2
  49. package/package.json +39 -4
  50. package/puppy-clean.mjs +2 -2
  51. package/puppy-companion-sprites.mjs +2 -2
  52. package/puppy-frame-indexed.mjs +2 -2
  53. package/puppy-paper-actions.mjs +2 -2
  54. package/puppy-paper-behaviors.mjs +1 -1
  55. package/puppy-paper-contact.mjs +2 -2
  56. package/puppy-paper-seat.mjs +2 -2
  57. package/puppy-paper.mjs +12 -12
  58. package/retrieviewer-cli.mjs +10 -6
  59. package/retrieviewer.mjs +13 -4
@@ -0,0 +1,114 @@
1
+ // Lossless packed grayscale frames (RVF1). Exact duplicate frames are stored
2
+ // once and each unique frame keeps only the bounding rectangle of its nonzero
3
+ // pixels; every other pixel is exactly zero. No quantization or resampling.
4
+ //
5
+ // Little-endian layout:
6
+ // 0 'RVF1' 4 u32 width 8 u32 height 12 u32 logicalFrameCount 16 u32 uniqueCount
7
+ // 20 u32[logicalFrameCount] logical -> unique index
8
+ // then uniqueCount records of u32 x,y,w,h,offset,length (offset into pixel data)
9
+ // then the cropped row-major pixels of every unique frame, contiguous in order.
10
+ import {isPredictiveFrames,readPredictiveFrames} from './puppy-frame-predictive.mjs';
11
+ const MAGIC=[0x52,0x56,0x46,0x31],HEADER=20,RECORD=24,MAX_SIDE=16384;
12
+ const stores=new WeakSet();
13
+
14
+ // Indexed local files obey the same borrowed two-frame contract as memory stores.
15
+ export function registerFrameStore(store){
16
+ if(!store||typeof store.getFrame!=='function'||typeof store.stats!=='function'||!Number.isInteger(store.width)||!Number.isInteger(store.height)||!Number.isInteger(store.frameCount))throw Error('Invalid frame store');
17
+ stores.add(store);return store;
18
+ }
19
+
20
+ export function isPackedFrames(value){return stores.has(value);}
21
+ export function isGrayFrameSource(value){return value instanceof Uint8Array||stores.has(value);}
22
+
23
+ const fnv=(bytes,start,end)=>{let h=0x811c9dc5;for(let i=start;i<end;i++)h=Math.imul(h^bytes[i],0x01000193);return h>>>0;};
24
+ const sameBytes=(bytes,a,b,n)=>{for(let i=0;i<n;i++)if(bytes[a+i]!==bytes[b+i])return false;return true;};
25
+
26
+ // Pack a legacy concatenated raster (frameCount full width*height frames).
27
+ export function packFrames(raw,width,height,frameCount){
28
+ checkShape(width,height,frameCount);
29
+ const n=width*height;
30
+ if(!(raw instanceof Uint8Array)||raw.length!==n*frameCount)throw Error('Invalid gray frames: byte length');
31
+ const map=new Uint32Array(frameCount),firsts=[],rects=[],buckets=new Map();let total=0;
32
+ for(let f=0;f<frameCount;f++){
33
+ const start=f*n,h=fnv(raw,start,start+n),bucket=buckets.get(h);
34
+ const hit=bucket?.find(u=>sameBytes(raw,firsts[u]*n,start,n));
35
+ if(hit!==undefined){map[f]=hit;continue;}
36
+ let x0=width,y0=height,x1=-1,y1=-1;
37
+ for(let y=0;y<height;y++){const row=start+y*width;for(let x=0;x<width;x++)if(raw[row+x]){if(x<x0)x0=x;if(x>x1)x1=x;if(y<y0)y0=y;y1=y;}}
38
+ const rect=x1<0?{x:0,y:0,w:0,h:0}:{x:x0,y:y0,w:x1-x0+1,h:y1-y0+1};
39
+ rect.offset=total;rect.length=rect.w*rect.h;total+=rect.length;
40
+ map[f]=firsts.length;if(bucket)bucket.push(firsts.length);else buckets.set(h,[firsts.length]);firsts.push(f);rects.push(rect);
41
+ }
42
+ const unique=firsts.length,data=HEADER+4*frameCount+RECORD*unique,out=new Uint8Array(data+total),view=new DataView(out.buffer);
43
+ out.set(MAGIC,0);[width,height,frameCount,unique].forEach((v,i)=>view.setUint32(4+4*i,v,true));
44
+ for(let f=0;f<frameCount;f++)view.setUint32(HEADER+4*f,map[f],true);
45
+ rects.forEach((r,u)=>{
46
+ const at=HEADER+4*frameCount+RECORD*u;[r.x,r.y,r.w,r.h,r.offset,r.length].forEach((v,i)=>view.setUint32(at+4*i,v,true));
47
+ const start=firsts[u]*n;for(let y=0;y<r.h;y++){const src=start+(r.y+y)*width+r.x;out.set(raw.subarray(src,src+r.w),data+r.offset+y*r.w);}
48
+ });
49
+ return out;
50
+ }
51
+
52
+ function checkShape(width,height,frameCount){
53
+ if(!Number.isInteger(width)||!Number.isInteger(height)||width<1||height<1||width>MAX_SIDE||height>MAX_SIDE)throw Error('Invalid gray frames: dimensions');
54
+ if(!Number.isInteger(frameCount)||frameCount<1||frameCount>0xffffffff||!Number.isSafeInteger(width*height*frameCount))throw Error('Invalid gray frames: frame count');
55
+ }
56
+
57
+ // Parse and fully validate an RVF1 store. The returned store keeps `bytes`
58
+ // resident (crops only) and reconstructs at most two full frames at a time.
59
+ // A returned frame is valid until two other unique frames have been requested.
60
+ // An 'RVQ5' store (lossy 32-level comparison codec) is delegated and branded.
61
+ export function readPackedFrames(bytes,options){
62
+ if(!(bytes instanceof Uint8Array))throw Error('Invalid packed frames: bytes');
63
+ if(isPredictiveFrames(bytes)){const store=readPredictiveFrames(bytes,options);stores.add(store);return store;}
64
+ if(bytes.length<HEADER||MAGIC.some((v,i)=>bytes[i]!==v))throw Error('Invalid packed frames: magic');
65
+ const view=new DataView(bytes.buffer,bytes.byteOffset,bytes.byteLength),u32=at=>view.getUint32(at,true);
66
+ const width=u32(4),height=u32(8),frameCount=u32(12),unique=u32(16);
67
+ try{checkShape(width,height,frameCount);}catch{throw Error('Invalid packed frames: header');}
68
+ if(unique<1||unique>frameCount)throw Error('Invalid packed frames: unique count');
69
+ const data=HEADER+4*frameCount+RECORD*unique,n=width*height;
70
+ if(bytes.length<data)throw Error('Invalid packed frames: truncated');
71
+ const map=new Uint32Array(frameCount),used=new Uint8Array(unique);
72
+ for(let f=0;f<frameCount;f++){const u=u32(HEADER+4*f);if(u>=unique)throw Error('Invalid packed frames: map');map[f]=u;used[u]=1;}
73
+ if(used.includes(0))throw Error('Invalid packed frames: unreferenced frame');
74
+ const rects=new Uint32Array(unique*6);let total=0;
75
+ for(let u=0;u<unique;u++){
76
+ const at=HEADER+4*frameCount+RECORD*u,x=u32(at),y=u32(at+4),w=u32(at+8),h=u32(at+12),offset=u32(at+16),length=u32(at+20);
77
+ if((w===0)!==(h===0)||(w===0&&(x||y))||x+w>width||y+h>height)throw Error('Invalid packed frames: rectangle');
78
+ if(length!==w*h||offset!==total)throw Error('Invalid packed frames: offset');
79
+ rects.set([x,y,w,h,offset,length],u*6);total+=length;
80
+ }
81
+ if(bytes.length!==data+total)throw Error('Invalid packed frames: pixel length');
82
+ const pixels=bytes.subarray(data),slots=[{unique:-1,frame:null},{unique:-1,frame:null}];let decodes=0,hits=0;
83
+ const store=Object.freeze({
84
+ width,height,frameCount,uniqueCount:unique,byteLength:n*frameCount,
85
+ get residentBytes(){return bytes.byteLength+map.byteLength+rects.byteLength+slots.reduce((s,e)=>s+(e.frame?.byteLength??0),0);},
86
+ stats(){return{decodes,hits,cachedFrames:slots.filter(e=>e.frame).length};},
87
+ getFrame(index){
88
+ if(!Number.isInteger(index)||index<0||index>=frameCount)throw Error('Invalid packed frames: frame index');
89
+ const u=map[index];
90
+ if(slots[0].unique===u){hits++;return slots[0].frame;}
91
+ if(slots[1].unique===u){hits++;slots.reverse();return slots[0].frame;}
92
+ const slot=slots.pop();slots.unshift(slot);decodes++;
93
+ const frame=slot.frame??=new Uint8Array(n),k=u*6,x=rects[k],y=rects[k+1],w=rects[k+2],h=rects[k+3],offset=rects[k+4];
94
+ frame.fill(0);slot.unique=u;
95
+ for(let r=0;r<h;r++)frame.set(pixels.subarray(offset+r*w,offset+(r+1)*w),(y+r)*width+x);
96
+ return frame;
97
+ },
98
+ });
99
+ stores.add(store);return store;
100
+ }
101
+
102
+ // Uniform frame access over a legacy concatenated Uint8Array or a packed store.
103
+ export function grayFrameReader(source,width,height,frameCount){
104
+ checkShape(width,height,frameCount);
105
+ const n=width*height,byteLength=n*frameCount;
106
+ const check=index=>{if(!Number.isInteger(index)||index<0||index>=frameCount)throw Error('Invalid gray frames: frame index');};
107
+ if(stores.has(source)){
108
+ if(source.width!==width||source.height!==height||source.frameCount!==frameCount)throw Error('Invalid gray frames: store shape');
109
+ return{width,height,frameCount,byteLength,getFrame(index){check(index);return source.getFrame(index);}};
110
+ }
111
+ if(!(source instanceof Uint8Array))throw Error('Invalid gray frames: source');
112
+ if(source.length!==byteLength)throw Error('Invalid gray frames: byte length');
113
+ return{width,height,frameCount,byteLength,getFrame(index){check(index);return source.subarray(index*n,(index+1)*n);}};
114
+ }
@@ -93,6 +93,8 @@ export async function desktopHelper(options={}){
93
93
  throw Error('Linux desktop needs system Python3.6+ (AlmaLinux8 platform-python is supported). No packages or system settings were installed.');
94
94
  }
95
95
  export async function startDesktop(options={}){
96
+ const pet=options.pet??'dog';if(!['dog','cat'].includes(pet))throw Error('Invalid desktop pet');
97
+ const engine=options.engine??'auto';if(!['auto','native','node'].includes(engine))throw Error('Invalid desktop engine');
96
98
  const watch=options.watch??'off';if(!['off','stackbite'].includes(watch))throw Error('Invalid desktop watch source');
97
99
  const brain=options.brain??'off';if(!['off','openrouter'].includes(brain))throw Error('Invalid desktop brain');
98
100
  if(brain==='openrouter'){
@@ -100,14 +102,20 @@ export async function startDesktop(options={}){
100
102
  const {createOpenRouterBrain}=await import('../openrouter/brain.mjs');createOpenRouterBrain().dispose();
101
103
  }
102
104
  if(watch==='stackbite'){const {stackBiteBinding}=await import('../stackbite/attention.mjs');stackBiteBinding();}
103
- const ink=options.ink??'cream';if(!['cream','gold','dark'].includes(ink))throw Error('Invalid desktop ink');
105
+ const ink=options.ink??(pet==='cat'?'gold':'cream');if(!['cream','gold','dark'].includes(ink))throw Error('Invalid desktop ink');
104
106
  const returnStyle=options.returnStyle??'auto';if(!['auto','brake','trot','run-trot'].includes(returnStyle))throw Error('Invalid return style');
105
107
  // Fail before creating any cache/session on unsupported desktops (e.g. Wayland).
106
108
  desktopPlatform(options);
107
- const status=await desktopStatus(options);if(status.running)return {...status,alreadyRunning:true,...status.returnStyle!==returnStyle?{requestedReturnStyle:returnStyle,restartRequired:true}:{},...(status.watch?.source??'off')!==watch?{requestedWatch:watch,restartRequired:true}:{},...(status.brain?.provider??'off')!==brain?{requestedBrain:brain,restartRequired:true}:{}};
109
+ const native=process.platform==='win32'&&process.arch==='x64'&&engine!=='node'&&watch==='off'&&brain==='off';
110
+ if(engine==='native'&&!native)throw Error('Native mode requires Windows x64 with watch/brain off; use --engine node for compatibility features');
111
+ const status=await desktopStatus(options);if(status.running){
112
+ if(native&&status.engine==='quickjs-native'&&status.pet!==pet){const {switchNativePet}=await import('../native-windows/desktop.mjs');return switchNativePet(options,await readSession(options),pet);}
113
+ return {...status,alreadyRunning:true,...(status.pet??'dog')!==pet?{requestedPet:pet,restartRequired:true}:{},...status.returnStyle!==returnStyle?{requestedReturnStyle:returnStyle,restartRequired:true}:{},...(status.watch?.source??'off')!==watch?{requestedWatch:watch,restartRequired:true}:{},...(status.brain?.provider??'off')!==brain?{requestedBrain:brain,restartRequired:true}:{}};
114
+ }
108
115
  if(status.stale)await stopDesktop(options);
116
+ if(native){const {startNativeDesktop}=await import('../native-windows/desktop.mjs');return startNativeDesktop(options);}
109
117
  const helper=await desktopHelper(options);
110
- const workerEnv={...process.env,RETRIEVIEWER_HOME:getConfigPaths(options).root,RETRIEVIEWER_INK:ink,RETRIEVIEWER_RETURN_STYLE:returnStyle,RETRIEVIEWER_WATCH:watch,RETRIEVIEWER_BRAIN:brain};
118
+ const workerEnv={...process.env,RETRIEVIEWER_HOME:getConfigPaths(options).root,RETRIEVIEWER_PET:pet,RETRIEVIEWER_INK:ink,RETRIEVIEWER_RETURN_STYLE:returnStyle,RETRIEVIEWER_WATCH:watch,RETRIEVIEWER_BRAIN:brain};
111
119
  if(brain==='off')delete workerEnv.OPENROUTER_API_KEY;
112
120
  return new Promise((resolve,reject)=>{
113
121
  const child=spawn(process.execPath,[fileURLToPath(new URL('./worker.mjs',import.meta.url)),'--helper-spec',JSON.stringify(helper)],{detached:true,windowsHide:true,stdio:['ignore','ignore','ignore','ipc'],env:workerEnv});
@@ -14,6 +14,7 @@ let attention=null,brain=null,attentionLabel=null;
14
14
  const returnStyle=process.env.RETRIEVIEWER_RETURN_STYLE??'auto';
15
15
  const watchSource=process.env.RETRIEVIEWER_WATCH??'off';
16
16
  const brainSource=process.env.RETRIEVIEWER_BRAIN??'off';
17
+ const petKind=process.env.RETRIEVIEWER_PET??'dog';
17
18
  let nativeStderr='';
18
19
  const sendParent=message=>{if(process.connected)try{process.send(message,error=>{if(error&&!detached)stop();});}catch{if(!detached)stop();}};
19
20
  const starting=()=>{if(stopping||(!process.connected&&!detached))throw Error('Desktop launcher closed during startup');};
@@ -44,7 +45,7 @@ try{
44
45
  body+=bytes.toString('utf8');if(body.length>2048)return socket.destroy();if(!body.includes('\n'))return;
45
46
  let command;try{command=JSON.parse(body.split('\n')[0]);}catch{return socket.destroy();}
46
47
  if(command.token!==session.token||!['status','stop'].includes(command.command))return socket.destroy();
47
- socket.end(JSON.stringify(command.command==='stop'?{stopped:true}:{running:true,reachable:true,selected,visible,pickerVisible,frames,paintedFrames,lastInteraction,returnStyle,attentionLabel,watch:{source:watchSource,...attention?.status??{state:'off',pendingCount:0}},brain:brain?.status??{provider:'off',state:'off'},pid:process.pid,painterPid:helper?.pid,rssBytes:process.memoryUsage().rss})+'\n');
48
+ socket.end(JSON.stringify(command.command==='stop'?{stopped:true}:{running:true,reachable:true,pet:petKind,engine:"node-compat",selected,visible,pickerVisible,frames,paintedFrames,lastInteraction,returnStyle,attentionLabel,watch:{source:watchSource,...attention?.status??{state:'off',pendingCount:0}},brain:brain?.status??{provider:'off',state:'off'},pid:process.pid,painterPid:helper?.pid,rssBytes:process.memoryUsage().rss})+'\n');
48
49
  if(command.command==='stop')stop();
49
50
  });
50
51
  });
@@ -72,7 +73,13 @@ try{
72
73
  else if(m.type==='quit')stop();
73
74
  }catch(error){write({type:'error',message:error.message});stop();}
74
75
  });
75
- starting();const {createPet}=await import('../../retrieviewer.mjs');starting();const pet=await createPet({returnStyle});
76
+ starting();let pet;
77
+ if(petKind==='cat'){
78
+ const {createCat}=await import('../purrviewer/src/purrviewer.mjs');starting();const cat=await createCat();
79
+ pet={step:dt=>cat.step(dt),frame:o=>cat.frame(o),dispose:()=>cat.dispose(),
80
+ interact:kind=>cat.interact(({ball:'toy',tug:'company'})[kind]??kind),
81
+ notify:()=>cat.interact('greet'),clearNotice:()=>{}};
82
+ }else {const {createPet}=await import('../../retrieviewer.mjs');starting();pet=await createPet({returnStyle});}
76
83
  if(stopping)pet.dispose();else{
77
84
  pump=createFramePump({pet,send:write});if(pendingViewport)pump.viewport(pendingViewport);
78
85
  if(!['off','stackbite'].includes(watchSource))throw Error('Invalid desktop watch source');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitefoundry/retrieviewer",
3
- "version": "0.0.17",
3
+ "version": "0.0.18",
4
4
  "description": "A fluffy Braille desktop pet for Windows and Linux X11, with experimental macOS support and an embeddable TUI engine.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://bitefoundry.github.io/retrieviewer/",
@@ -57,7 +57,8 @@
57
57
  },
58
58
  "./tui": "./integrations/opencode/tui.tsx",
59
59
  "./opencode/tui": "./integrations/opencode/tui.tsx",
60
- "./package.json": "./package.json"
60
+ "./package.json": "./package.json",
61
+ "./cat": "./integrations/purrviewer/src/purrviewer.mjs"
61
62
  },
62
63
  "files": [
63
64
  "LICENSE",
@@ -164,7 +165,39 @@
164
165
  "vendor/procedural-animals/LICENSE",
165
166
  "puppy-frame-indexed.mjs",
166
167
  "puppy-frame-file.mjs",
167
- "pet-indexed-manifest.json"
168
+ "pet-indexed-manifest.json",
169
+ "integrations/native-windows/desktop.mjs",
170
+ "integrations/native-windows/runtime.mjs",
171
+ "integrations/native-windows/runtime.bundle.mjs",
172
+ "integrations/native-windows/manifest.json",
173
+ "integrations/native-windows/bin/win32-x64/retrieviewer-native.exe",
174
+ "integrations/native-windows/host.cpp",
175
+ "integrations/native-windows/native-window.hpp",
176
+ "integrations/native-windows/native-control.hpp",
177
+ "integrations/native-windows/CMakeLists.txt",
178
+ "integrations/native-windows/sources.json",
179
+ "integrations/native-windows/THIRD-PARTY-LICENSES.txt",
180
+ "NATIVE-RUNTIME.md",
181
+ "integrations/purrviewer/ART.md",
182
+ "integrations/purrviewer/ASSETS-LICENSE.md",
183
+ "integrations/purrviewer/LICENSE",
184
+ "integrations/purrviewer/art/ginger.bin.gz",
185
+ "integrations/purrviewer/art/ginger.json",
186
+ "integrations/purrviewer/art/ginger.rvz",
187
+ "integrations/purrviewer/src/contact.mjs",
188
+ "integrations/purrviewer/src/habitat.mjs",
189
+ "integrations/purrviewer/src/life.mjs",
190
+ "integrations/purrviewer/src/purrviewer-core.mjs",
191
+ "integrations/purrviewer/src/purrviewer.mjs",
192
+ "integrations/purrviewer/src/walk-cycle.mjs",
193
+ "integrations/purrviewer/src/world.mjs",
194
+ "integrations/purrviewer/vendor/retrieviewer/braille.mjs",
195
+ "integrations/purrviewer/vendor/retrieviewer/LICENSE-CODE",
196
+ "integrations/purrviewer/vendor/retrieviewer/puppy-frame-predictive.mjs",
197
+ "integrations/purrviewer/vendor/retrieviewer/puppy-frame-store.mjs",
198
+ "integrations/purrviewer/vendor/retrieviewer/puppy-frame-file.mjs",
199
+ "integrations/purrviewer/vendor/retrieviewer/puppy-frame-indexed.mjs",
200
+ "integrations/native-windows/application.manifest"
168
201
  ],
169
202
  "scripts": {
170
203
  "bake:motion": "node tools/bake-pet-motion.mjs",
@@ -178,7 +211,9 @@
178
211
  "release:check": "npm run check:motion && node tools/check-release.mjs",
179
212
  "prepublishOnly": "npm run release:check",
180
213
  "bake:art-index": "node tools/index-pet-art.mjs",
181
- "check:art-index": "node tools/index-pet-art.mjs --check"
214
+ "check:art-index": "node tools/index-pet-art.mjs --check",
215
+ "build:native": "node tools/build-native-windows.mjs",
216
+ "check:native-pixels": "node tools/check-native-pixels.mjs"
182
217
  },
183
218
  "dependencies": {
184
219
  "jsonc-parser": "3.3.1"
package/puppy-clean.mjs CHANGED
@@ -31,7 +31,7 @@ export function cleanGroundOffset(state,meta){
31
31
  const a=v[Math.max(30,i-1)],b=v[i],c=v[i+1],d=v[Math.min(41,i+2)];
32
32
  return .5*(2*b+(-a+c)*p+(2*a-5*b+4*c-d)*p*p+(-a+3*b-3*c+d)*p*p*p);
33
33
  }
34
- export function createCachedTween(cache,info){
34
+ export function createCachedTween(cache,info,kernel=null){
35
35
  if(!isGrayFrameSource(cache)||cache.byteLength!==CLEAN_BYTES||info.pairs!==47||info.samples!==17||info.width!==320||info.height!==320||info.channels!==1||info.bytes!==CLEAN_BYTES)throw Error('Invalid baked tween cache');
36
36
  const frames=grayFrameReader(cache,320,320,47*17),output=new Uint8ClampedArray(320*320*4),stride=320*320;
37
37
  for(let i=3;i<output.length;i+=4)output[i]=255;
@@ -39,7 +39,7 @@ export function createCachedTween(cache,info){
39
39
  if(!Number.isInteger(state.pair)||state.pair<0||state.pair>=47||!Number.isFinite(state.blend))throw Error('Invalid baked tween state');
40
40
  const phase=Math.max(0,Math.min(1,state.blend))*16,index=Math.min(15,Math.floor(phase)),alpha=phase-index;
41
41
  const a=frames.getFrame(state.pair*17+index),b=frames.getFrame(state.pair*17+index+1);
42
- for(let i=0,j=0;i<stride;i++,j+=4){const value=a[i]*(1-alpha)+b[i]*alpha;output[j]=output[j+1]=output[j+2]=value;}
42
+ if(kernel?.blendGray)kernel.blendGray(output,a,b,alpha);else for(let i=0,j=0;i<stride;i++,j+=4){const value=a[i]*(1-alpha)+b[i]*alpha;output[j]=output[j+1]=output[j+2]=value;}
43
43
  return output;
44
44
  };
45
45
  }
@@ -1,7 +1,7 @@
1
1
  import {grayFrameReader,isGrayFrameSource} from './puppy-frame-store.mjs';
2
2
  // Prebaked interaction samples; the original seat is the exact shared endpoint.
3
3
  // Pure sampling, no live image morph, root motion, camera or pose-size fitting.
4
- export function createCompanionSpriteSampler(bytes,info,output){
4
+ export function createCompanionSpriteSampler(bytes,info,output,kernel=null){
5
5
  const n=320*320;
6
6
  if(!isGrayFrameSource(bytes)||info?.width!==320||info.height!==320||info.samples!==9||!Number.isInteger(info.pairs)||info.pairs<1||info.pairs>128||bytes.byteLength!==info.pairs*9*n)throw Error('Invalid companion sprite cache');
7
7
  for(const key of ['treat-eat','ball-pickup','ball-drop'])if(!Array.isArray(info.clips?.[key])||!info.clips[key].length||!info.clips[key].every(p=>Number.isInteger(p)&&p>=0&&p<info.pairs))throw Error('Invalid companion sprite clip');
@@ -11,6 +11,6 @@ export function createCompanionSpriteSampler(bytes,info,output){
11
11
  return({clip,progress})=>{
12
12
  const list=info.clips[clip];if(!list||!Number.isFinite(progress)||progress<0||progress>1)throw Error('Invalid companion sprite pose');
13
13
  const q=progress*list.length,index=Math.min(list.length-1,Math.floor(q)),s=Math.round((q-index)*8),frame=frames.getFrame(list[index]*9+s);
14
- for(let i=0,j=0;i<n;i++,j+=4)rgba[j]=rgba[j+1]=rgba[j+2]=frame[i];return rgba;
14
+ if(kernel?.copyGray)kernel.copyGray(rgba,frame);else for(let i=0,j=0;i<n;i++,j+=4)rgba[j]=rgba[j+1]=rgba[j+2]=frame[i];return rgba;
15
15
  };
16
16
  }
@@ -31,7 +31,7 @@ const display=Uint8Array.from({length:32},(_,q)=>Math.round(q*255/31));
31
31
 
32
32
  // The host provides bounded positional reads and a synchronous gzip decoder.
33
33
  // Only two reconstructed frames survive; compressed art stays on disk.
34
- export function createIndexedFrameStore(index,{read,expand,close=()=>{},frameIndices}={}){
34
+ export function createIndexedFrameStore(index,{read,expand,close=()=>{},frameIndices,kernel=null}={}){
35
35
  if(typeof read!=='function'||typeof expand!=='function')fail('reader');
36
36
  const {width,height,frameCount,uniqueCount,map,rects,indexBytes}=index,n=width*height;
37
37
  const retained=frameIndices===undefined?null:new Uint8Array(frameCount);
@@ -50,7 +50,7 @@ export function createIndexedFrameStore(index,{read,expand,close=()=>{},frameInd
50
50
  const rows=expand(compressed,expected);if(!(rows instanceof Uint8Array)||rows.length!==expected)fail('expanded length');
51
51
  // Mark the evicted slot invalid before parsing: a failed decode is never a hit.
52
52
  const slot=slots.pop();slots.unshift(slot);slot.unique=-1;const frame=slot.frame??=new Uint8Array(n);frame.fill(0);prev.fill(0);
53
- for(let r=0,at=0;r<h;r++){
53
+ if(kernel?.decodeRows)kernel.decodeRows(frame,rows,x,y,w,h,width);else for(let r=0,at=0;r<h;r++){
54
54
  const type=rows[at++];if(type>4)fail('filter');
55
55
  unfilterRow(type,rows,at,cur,prev,w);at+=w;
56
56
  for(let c=0;c<w;c++){
@@ -38,7 +38,7 @@ export function paperActionPose(name,progress,info,compiledClips){
38
38
  if(!Number.isFinite(blend))throw Error('Invalid paper action: blend');
39
39
  return{pair:list[index],blend};
40
40
  }
41
- export function createPaperActionSampler(bytes,info,output,compiledClips){
41
+ export function createPaperActionSampler(bytes,info,output,compiledClips,kernel=null){
42
42
  if(!isGrayFrameSource(bytes))throw Error('Invalid action cache: bytes');
43
43
  validatePaperActionInfo(info,bytes.byteLength);
44
44
  const stride=info.width*info.height,pairs=info.pairs,frames=grayFrameReader(bytes,info.width,info.height,pairs*9);
@@ -47,5 +47,5 @@ export function createPaperActionSampler(bytes,info,output,compiledClips){
47
47
  for(let i=3;i<out.length;i+=4)out[i]=255;
48
48
  return state=>{const p=paperActionPose(state.actionName,state.actionProgress,info,compiledClips),sample=Math.round(p.blend*8);
49
49
  if(p.pair>=pairs||sample<0||sample>8)throw Error('Invalid paper action: sample out of range');
50
- const frame=frames.getFrame(p.pair*9+sample);for(let i=0,j=0;i<stride;i++,j+=4)out[j]=out[j+1]=out[j+2]=frame[i];return out;};
50
+ const frame=frames.getFrame(p.pair*9+sample);if(kernel?.copyGray)kernel.copyGray(out,frame);else for(let i=0,j=0;i<stride;i++,j+=4)out[j]=out[j+1]=out[j+2]=frame[i];return out;};
51
51
  }
@@ -5,4 +5,4 @@ export function paperBehaviorPose(state,info){
5
5
  const q=Math.min(list.length-1e-9,progress*list.length),i=Math.floor(q),blend=Math.round((q-i)*8)/8;
6
6
  return{pair:list[i],blend};
7
7
  }
8
- export function createPaperBehaviorSampler(bytes,info,output){const sample=createRearSampler(bytes,info,output);return state=>{const p=paperBehaviorPose(state,info);return sample(p.pair,p.blend);};}
8
+ export function createPaperBehaviorSampler(bytes,info,output,kernel=null){const sample=createRearSampler(bytes,info,output,kernel);return state=>{const p=paperBehaviorPose(state,info);return sample(p.pair,p.blend);};}
@@ -101,7 +101,7 @@ export function footSourcePoint(x,y,pins){
101
101
  return chosen?{x:x-chosen.dx*best,y:y-chosen.dy*best}:{x,y};
102
102
  }
103
103
 
104
- export function createRearSampler(bytes,info,output){
104
+ export function createRearSampler(bytes,info,output,kernel=null){
105
105
  if(!isGrayFrameSource(bytes)||info.pairs!==18||info.samples!==9||bytes.byteLength!==18*9*320*320)throw Error('Invalid rear sprite cache');
106
106
  const frames=grayFrameReader(bytes,320,320,18*9),out=output??new Uint8ClampedArray(320*320*4);
107
107
  if(!(out instanceof Uint8ClampedArray)||out.length!==320*320*4)throw Error('Invalid rear output');
@@ -109,7 +109,7 @@ export function createRearSampler(bytes,info,output){
109
109
  return(pair,blend)=>{
110
110
  if(!Number.isInteger(pair)||pair<0||pair>=18||!Number.isFinite(blend)||blend<0||blend>1)throw Error('Invalid rear pose');
111
111
  const frame=frames.getFrame(pair*9+Math.round(blend*8));
112
- for(let j=0,i=0;i<320*320;i++,j+=4)out[j]=out[j+1]=out[j+2]=frame[i];
112
+ if(kernel?.copyGray)kernel.copyGray(out,frame);else for(let j=0,i=0;i<320*320;i++,j+=4)out[j]=out[j+1]=out[j+2]=frame[i];
113
113
  return out;
114
114
  };
115
115
  }
@@ -1,6 +1,6 @@
1
1
  import {grayFrameReader,isGrayFrameSource} from './puppy-frame-store.mjs';
2
2
  // Exact cached samples; no live crossfade. Reverse the same path to stand up.
3
- export function createPaperSeatSampler(bytes,info,output){
3
+ export function createPaperSeatSampler(bytes,info,output,kernel=null){
4
4
  const stride=320*320;
5
5
  if(!isGrayFrameSource(bytes)||info.width!==320||info.height!==320||info.pairs!==3||info.samples!==17||bytes.byteLength!==3*17*stride)throw Error('Invalid paper sitting cache');
6
6
  const frames=grayFrameReader(bytes,320,320,3*17),out=output??new Uint8ClampedArray(stride*4);
@@ -9,7 +9,7 @@ export function createPaperSeatSampler(bytes,info,output){
9
9
  return progress=>{
10
10
  if(!Number.isFinite(progress)||progress<0||progress>1)throw Error('Invalid paper sitting progress');
11
11
  const q=progress*3,pair=Math.min(2,Math.floor(q)),sample=Math.round((q-pair)*16),frame=frames.getFrame(pair*17+sample);
12
- for(let i=0,j=0;i<stride;i++,j+=4)out[j]=out[j+1]=out[j+2]=frame[i];
12
+ if(kernel?.copyGray)kernel.copyGray(out,frame);else for(let i=0,j=0;i<stride;i++,j+=4)out[j]=out[j+1]=out[j+2]=frame[i];
13
13
  return out;
14
14
  };
15
15
  }
package/puppy-paper.mjs CHANGED
@@ -354,9 +354,9 @@ export function createPaperActor(animal,info,{manualTricks=false,trickTimings=nu
354
354
  };
355
355
  }
356
356
 
357
- export function createPaperRenderer(cache,info,meta,width,height,{rear,approach,seat,arrivalSeat,behaviors,actions,interactions,comfort,contacts=true,pawTransport=false,motionTables=null}={}){
357
+ export function createPaperRenderer(cache,info,meta,width,height,{rear,approach,seat,arrivalSeat,behaviors,actions,interactions,comfort,contacts=true,pawTransport=false,motionTables=null,kernel=null}={}){
358
358
  if(![width,height].every(Number.isInteger)||width<2||height<2||width*height>1_000_000)throw Error('Invalid paper grid');
359
- const tween=createCachedTween(cache,info),renderMeta={...meta,pairs:info.pair_map,run_sequence:info.run_sequence};
359
+ const tween=createCachedTween(cache,info,kernel),renderMeta={...meta,pairs:info.pair_map,run_sequence:info.run_sequence};
360
360
  const out=new Uint8ClampedArray(width*height*4),area=createPaperAreaSampler();
361
361
  // Only one authored source is drawn at a time. Small samplers borrow a view of
362
362
  // the action-sized scratch. A sleep blend reads each base pixel before writing
@@ -364,14 +364,14 @@ export function createPaperRenderer(cache,info,meta,width,height,{rear,approach,
364
364
  const actionScratch=new Uint8ClampedArray(Math.max(320*320,(actions?.info.width??320)*(actions?.info.height??320))*4);
365
365
  const spriteScratch=actionScratch.subarray(0,320*320*4);
366
366
  for(let i=3;i<actionScratch.length;i+=4)actionScratch[i]=255;
367
- const rearSample=rear?createRearSampler(rear.bytes,rear.info,spriteScratch):null,lock=createPaperFootLock({maxImageStretch:pawTransport?1.45:Infinity});
368
- const approachSample=approach?createRearSampler(approach.bytes,approach.info,spriteScratch):null;
369
- const seatSample=seat?createPaperSeatSampler(seat.bytes,seat.info,spriteScratch):null;
370
- const arrivalSeatSample=arrivalSeat?createPaperSeatSampler(arrivalSeat.bytes,arrivalSeat.info,spriteScratch):null;
371
- const behaviorSample=behaviors?createPaperBehaviorSampler(behaviors.bytes,behaviors.info,spriteScratch):null;
372
- const interactionSample=interactions?createCompanionSpriteSampler(interactions.bytes,interactions.info,spriteScratch):null;
367
+ const rearSample=rear?createRearSampler(rear.bytes,rear.info,spriteScratch,kernel):null,lock=createPaperFootLock({maxImageStretch:pawTransport?1.45:Infinity});
368
+ const approachSample=approach?createRearSampler(approach.bytes,approach.info,spriteScratch,kernel):null;
369
+ const seatSample=seat?createPaperSeatSampler(seat.bytes,seat.info,spriteScratch,kernel):null;
370
+ const arrivalSeatSample=arrivalSeat?createPaperSeatSampler(arrivalSeat.bytes,arrivalSeat.info,spriteScratch,kernel):null;
371
+ const behaviorSample=behaviors?createPaperBehaviorSampler(behaviors.bytes,behaviors.info,spriteScratch,kernel):null;
372
+ const interactionSample=interactions?createCompanionSpriteSampler(interactions.bytes,interactions.info,spriteScratch,kernel):null;
373
373
  const comfortSample=comfort?createComfortSpriteSampler(comfort.bytes,comfort.info,spriteScratch):null;
374
- const actionSample=actions?createPaperActionSampler(actions.bytes,actions.info,actionScratch.subarray(0,actions.info.width*actions.info.height*4),motionTables?.actionClips):null,actionArea=actions?createPaperAreaSampler(actions.info.width):null;
374
+ const actionSample=actions?createPaperActionSampler(actions.bytes,actions.info,actionScratch.subarray(0,actions.info.width*actions.info.height*4),motionTables?.actionClips,kernel):null,actionArea=actions?createPaperAreaSampler(actions.info.width):null;
375
375
  const episodes=(key,labels,indices,samples,options)=>motionTables?.support[key]?createBakedSupportEpisodes(motionTables.support[key]):createPaperSupportEpisodes(labels,indices,samples,options);
376
376
  const walkEpisodes=actions?episodes('walk',actions.info.feet,actions.info.clips.walk,9):null;
377
377
  const sourceEpisodes=rear?{
@@ -415,8 +415,8 @@ export function createPaperRenderer(cache,info,meta,width,height,{rear,approach,
415
415
  let sx=Math.floor(rawX);if(state.flip)sx=sourceN-1-sx;
416
416
  return image[(Math.floor(rawY)*sourceN+sx)*4]>16;
417
417
  }}):null;render.lastTransport=transport;
418
- const minify=side<320||sideX<320,filter=isAction?actionArea:area;if(minify)filter.prepare(image);
419
- out.fill(0);for(let i=3;i<out.length;i+=4)out[i]=255;
418
+ const minify=side<320||sideX<320,filter=isAction?actionArea:area;if(minify&&!(kernel?.blit&&transport))filter.prepare(image);
419
+ if(kernel?.clear)kernel.clear(out);else{out.fill(0);for(let i=3;i<out.length;i+=4)out[i]=255;}
420
420
  render.lastPlacement={left,top,side,sideX};
421
421
  // Measured drawn mouth of the EXACT sample above (same priority, quantization,
422
422
  // placement and mirror). Side/rear/action/interaction stay null: unsupported.
@@ -430,7 +430,7 @@ export function createPaperRenderer(cache,info,meta,width,height,{rear,approach,
430
430
  }
431
431
  const drawTop=top-originY/320*side,bottom=top+Math.max(sourceN-originY,...pins.map(p=>p.target.y+20))/320*side;
432
432
  const drawLeft=left+Math.min(-originX,...pins.map(p=>p.target.x-20))/320*sideX,drawRight=left+Math.max(sourceN-originX,...pins.map(p=>p.target.x+20))/320*sideX;
433
- for(let dy=Math.max(0,minify?Math.floor(drawTop):Math.ceil(drawTop-.5));dy<Math.min(height,minify?Math.ceil(bottom):Math.ceil(bottom-.5));dy++)for(let dx=Math.max(0,minify?Math.floor(drawLeft):Math.ceil(drawLeft-.5));dx<Math.min(width,minify?Math.ceil(drawRight):Math.ceil(drawRight-.5));dx++){
433
+ if(kernel?.blit&&transport)kernel.blit(out,image,{width,height,sourceN,left,top,side,sideX,originX,originY,drawTop,bottom,drawLeft,drawRight,flip:state.flip,layers:transport.layers});else for(let dy=Math.max(0,minify?Math.floor(drawTop):Math.ceil(drawTop-.5));dy<Math.min(height,minify?Math.ceil(bottom):Math.ceil(bottom-.5));dy++)for(let dx=Math.max(0,minify?Math.floor(drawLeft):Math.ceil(drawLeft-.5));dx<Math.min(width,minify?Math.ceil(drawRight):Math.ceil(drawRight-.5));dx++){
434
434
  const px=(dx+.5-left)/sideX*320,py=(dy+.5-top)/side*320;
435
435
  const warped=transport?transport(px,py):footSourcePoint(px,py,pins);if(!warped)continue;
436
436
  const rawX=warped.x+originX,rawY=warped.y+originY;
@@ -13,7 +13,7 @@ Usage:
13
13
  retrieviewer doctor [--agent ID] [--json]
14
14
  retrieviewer agents [--json]
15
15
  retrieviewer preview [--original]
16
- retrieviewer desktop [--ink cream|gold|dark] [--return-style auto|brake|trot|run-trot] [--watch stackbite|off] [--brain openrouter|off]
16
+ retrieviewer desktop [--pet dog|cat] [--engine auto|native|node] [--ink cream|gold|dark] [--return-style auto|brake|trot|run-trot] [--watch stackbite|off] [--brain openrouter|off]
17
17
  retrieviewer brain-check [--json]
18
18
  retrieviewer attention [--json]
19
19
  retrieviewer desktop-status [--json]
@@ -26,8 +26,10 @@ Non-interactive changes require --agent and --yes. Unsupported native renderers
26
26
  are refused: MCP connectivity alone is never treated as in-screen rendering.
27
27
  No command changes credentials, shell profiles or agent executables.
28
28
  Desktop is a separate native overlay, NOT a Codex plugin. Choose a window once;
29
- use Codex normally. Exit from its tray icon or desktop-stop. No auto-start is added.
30
- Windows is verified; macOS and Linux X11 adapters are experimental. Native Wayland
29
+ use Codex normally. Exit from its controls or desktop-stop. No auto-start is added.
30
+ Windows x64 basic mode uses one native process; Node exits after launch.
31
+ --pet cat selects the ginger cat. Watch/brain use the Node compatibility engine.
32
+ Windows and AlmaLinux8 X11 are verified; macOS is experimental. Native Wayland
31
33
  app tracking is not supported. Linux targets AlmaLinux8/GLIBC2.28, system Python3.6+.
32
34
  Return style defaults to auto (mix all three). Restart the companion to change it.
33
35
  Watch is OFF by default. Inside a live StackBite Seat, --watch stackbite reads only
@@ -42,7 +44,7 @@ export function parseArguments(args){
42
44
  const [command='help',...rest]=args,options={};
43
45
  for(let i=0;i<rest.length;i++){
44
46
  const token=rest[i];
45
- if(token==='--agent'||token==='--ink'||token==='--return-style'||token==='--watch'||token==='--brain'){
47
+ if(token==='--agent'||token==='--ink'||token==='--return-style'||token==='--watch'||token==='--brain'||token==='--pet'||token==='--engine'){
46
48
  const key=token.slice(2);
47
49
  if(options[key]||!rest[i+1]||rest[i+1].startsWith('-'))throw Error(token+' requires one value');
48
50
  options[key]=rest[++i];
@@ -50,13 +52,15 @@ export function parseArguments(args){
50
52
  else throw Error('Unknown argument: '+token);
51
53
  }
52
54
  if(options.agent&&!AGENTS.some(a=>a.id===options.agent))throw Error('Unknown agent: '+options.agent);
55
+ if(options.pet&&!['dog','cat'].includes(options.pet))throw Error('--pet must be dog or cat');
56
+ if(options.engine&&!['auto','native','node'].includes(options.engine))throw Error('--engine must be auto, native or node');
53
57
  if(options.ink&&!['cream','gold','dark'].includes(options.ink))throw Error('--ink must be cream, gold or dark');
54
58
  if(options['return-style']&&!['auto','brake','trot','run-trot'].includes(options['return-style']))throw Error('--return-style must be auto, brake, trot or run-trot');
55
59
  if(options.watch&&!['off','stackbite'].includes(options.watch))throw Error('--watch must be stackbite or off');
56
60
  if(options.brain&&!['off','openrouter'].includes(options.brain))throw Error('--brain must be openrouter or off');
57
61
  if(options.brain==='openrouter'&&options.watch!=='stackbite')throw Error('--brain openrouter requires --watch stackbite');
58
62
  if(!['help','--help','-h','enable','disable','disconnect','status','doctor','agents','preview','attention','brain-check','desktop','desktop-status','desktop-stop','desktop-clean','desktop-remove'].includes(command))throw Error('Unknown command: '+command);
59
- const allowed={desktop:['ink','return-style','watch','brain'],attention:['json'],'brain-check':['json'],enable:['agent','yes','json','dry-run'],disconnect:['agent','yes','json','dry-run'],disable:['agent','json'],status:['agent','json'],doctor:['agent','json'],agents:['json'],preview:['original'],'desktop-status':['json'],'desktop-clean':['yes'],'desktop-remove':['yes']};
63
+ const allowed={desktop:['ink','return-style','watch','brain','pet','engine'],attention:['json'],'brain-check':['json'],enable:['agent','yes','json','dry-run'],disconnect:['agent','yes','json','dry-run'],disable:['agent','json'],status:['agent','json'],doctor:['agent','json'],agents:['json'],preview:['original'],'desktop-status':['json'],'desktop-clean':['yes'],'desktop-remove':['yes']};
60
64
  for(const key of Object.keys(options))if(!(allowed[command]??[]).includes(key))throw Error(`--${key} is not supported by ${command}`);
61
65
  return {command,options};
62
66
  }
@@ -82,7 +86,7 @@ export async function runCli(args,{input=process.stdin,output=process.stdout,err
82
86
  if(['desktop-clean','desktop-remove'].includes(command)&&!options.yes)throw Error('Use '+command+' --yes to remove only the companion cache.');
83
87
  const api=desktop??await import('./integrations/windows-overlay/desktop.mjs');
84
88
  const action={'desktop':'startDesktop','desktop-status':'desktopStatus','desktop-stop':'stopDesktop','desktop-clean':'cleanDesktop','desktop-remove':'removeDesktop'}[command];
85
- emit(await api[action]({...options.ink?{ink:options.ink}:{},...options['return-style']?{returnStyle:options['return-style']}:{},...options.watch?{watch:options.watch}:{},...options.brain?{brain:options.brain}:{} }));return 0;
89
+ emit(await api[action]({...options.pet?{pet:options.pet}:{},...options.engine?{engine:options.engine}:{},...options.ink?{ink:options.ink}:{},...options['return-style']?{returnStyle:options['return-style']}:{},...options.watch?{watch:options.watch}:{},...options.brain?{brain:options.brain}:{} }));return 0;
86
90
  }
87
91
  if(command==='preview'){await import('./examples/live.mjs');return 0;}
88
92
  if(['status','doctor','agents'].includes(command)){
package/retrieviewer.mjs CHANGED
@@ -123,7 +123,7 @@ async function acquireAssets(loadAsset,artProfile){
123
123
  try{const data=await entry.promise;entry.data=data;return{data,release};}catch(error){release();throw error;}
124
124
  }
125
125
 
126
- export async function createPet({loadAsset=defaultLoadAsset,seed,energy,pawTransport=true,artProfile=DEFAULT_ART_PROFILE,returnStyle='auto'}={}){
126
+ function petOptions({loadAsset=defaultLoadAsset,seed,energy,pawTransport=true,artProfile=DEFAULT_ART_PROFILE,returnStyle='auto'}={}){
127
127
  if(typeof loadAsset!=='function')throw TypeError('loadAsset must be a function');
128
128
  if(seed===undefined)seed=globalThis.crypto?.getRandomValues?globalThis.crypto.getRandomValues(new Uint32Array(1))[0]:731;
129
129
  if(!Number.isInteger(seed)||seed<0||seed>0xffffffff)throw RangeError('seed must be a uint32');
@@ -131,7 +131,16 @@ export async function createPet({loadAsset=defaultLoadAsset,seed,energy,pawTrans
131
131
  if(typeof pawTransport!=='boolean')throw TypeError('pawTransport must be boolean');
132
132
  if(!ART_PROFILES.includes(artProfile))throw RangeError('artProfile must be original or q5');
133
133
  if(!FETCH_RETURN_STYLES.includes(returnStyle))throw RangeError('returnStyle must be auto, brake, trot or run-trot');
134
- let lease=await acquireAssets(loadAsset,artProfile),data=lease.data;
134
+ return {loadAsset,seed,energy,pawTransport,artProfile,returnStyle};
135
+ }
136
+ export async function createPet(options={}){
137
+ const normalized=petOptions(options),lease=await acquireAssets(normalized.loadAsset,normalized.artProfile);
138
+ return createPetFromAssets(normalized,lease);
139
+ }
140
+ // Shared simulation/render core for local native hosts. The provider owns I/O.
141
+ export function createPetFromAssets(options,lease,kernel=null){
142
+ const {loadAsset,seed,energy,pawTransport,artProfile,returnStyle}=petOptions(options);
143
+ let data=lease.data;
135
144
  const release=lease.release;lease=null;
136
145
  let {cache,info,meta}=data;const assets={...data.assets};data=null;
137
146
  // Baked tables describe the bundled Q5 source only. External/optional artwork
@@ -166,14 +175,14 @@ export async function createPet({loadAsset=defaultLoadAsset,seed,energy,pawTrans
166
175
  if(typeof cellAspect!=='number'||!Number.isFinite(cellAspect)||cellAspect<=0)throw RangeError('cellAspect must be a positive number');
167
176
  if(![12,24,30].includes(poseFps))throw RangeError('poseFps must be 12, 24 or 30');
168
177
  const width=columns*2,height=rows*4,key=`${width}x${height}`;
169
- if(paintKey!==key){paint=createPaperRenderer(cache,info,meta,width,height,{...assets,pawTransport,motionTables});paintKey=key;}
178
+ if(paintKey!==key){paint=createPaperRenderer(cache,info,meta,width,height,{...assets,pawTransport,motionTables,kernel});paintKey=key;}
170
179
  const viewportWidth=camera?.viewportWidth??columns*cellAspect,viewportHeight=camera?.viewportHeight??rows;
171
180
  const projection=createPaperProjection({viewportWidth,viewportHeight,rect:camera?.rect??{left:0,top:0,width:viewportWidth,height:viewportHeight},width,height});
172
181
  const state=actor.sample(time,{poseFps}),report=life.report;
173
182
  if(report.gesture)state.interactionPose=report.gesture;
174
183
  const placement=projection.placement(state.x,state.z,state.bodyLift??0),pixels=paint(state,{...placement,contactEnabled:!!contactEnabled});
175
184
  paintCompanionObjects(pixels,width,height,projection,report,state,paint.lastMouth);
176
- const {text}=rgbaToBraille(pixels,width,height,{method:'stable',brightness:1.18,contrast:1.1});
185
+ const {text}=rgbaToBraille(pixels,width,height,{method:'stable',brightness:1.18,contrast:1.1,kernel});
177
186
  lastHeldFrameKey=heldFrameKey(state,report,{columns,rows,cellAspect,camera,poseFps,contactEnabled});
178
187
  return {text,columns,rows,pixels,state,report,placement,projection,artProfile,mouth:paint.lastMouth,contacts:paint.report()};
179
188
  },