@bitefoundry/retrieviewer 0.0.4 → 0.0.5

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.
@@ -35,6 +35,11 @@ must be checked again for the final release bytes.
35
35
 
36
36
  ## Remaining limits
37
37
 
38
+ 0.0.5 extends that ninth licensed archive with four sleeping studies generated
39
+ from the same existing puppy identity/pose references, approved by the owner in
40
+ this conversation. COMFORT-ART.md and the comfort metadata record preparation,
41
+ source hashes and size changes. No new downloaded photo/video input is introduced.
42
+
38
43
  Later source candidate: belly/sleep reuse existing licensed roll/rest frames.
39
44
  Tug adds eight built-in imagegen keys referenced only from the existing generated
40
45
  puppy sheet, not new photographs. COMFORT-ART.md records the prompt, source hash
package/COMFORT-ART.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # Comfort extension art record
2
2
 
3
+ ## 0.0.5 sleeping studies
4
+
5
+ Four owner-approved built-in imagegen originals (side, belly, curl, chin), using
6
+ the existing `puppy-behaviors16-sheet.png` identity and `puppy-roll16-v4-sheet.png`
7
+ pose references, extend the same CC BY-SA4.0 comfort archive. No new external
8
+ photo or video input. Source PNGs remain local; metadata records SHA-256 hashes.
9
+ Preparation: `node tools/prepare-sleep-art.mjs` with the preserved local studies.
10
+ It keeps the nine existing tug keys pixel-identical and registers four new static
11
+ keys to ground y=300. There are no duplicated right-facing images: source sampling
12
+ reverses x. The pose is selected once per nap, not switched during a sleeping hold.
13
+ A short rest-to-sleep/rest recovery blend uses existing scratch; the sleeping body
14
+ itself is held, without scale pulsing. The z cue is silent.
15
+
16
+ The archive grows from103549 to129148 bytes (+25599). Packed resident pixels grow
17
+ from291337 to341139 (+49802 bytes). The renderer's mutually exclusive authored
18
+ samplers now share one action-sized RGBA scratch, with320px subviews for smaller
19
+ poses, replacing their separate buffers (2457600 bytes less before tug playback).
20
+ Sleep display is capped at5fps in the desktop pump, while simulation advances
21
+ normally and accepted interactions invalidate the next paint immediately.
22
+ Ink presets only change the native painter color; no RGB sprite families or
23
+ background capture are introduced. Actual RSS depends on host and allocator.
24
+
25
+ The four prompts share: one complete naturally proportioned pale fluffy retriever,
26
+ same bandana, closed eyes, grayscale fine fur on pure black, fixed slightly elevated
27
+ camera, no text/bed/toy/background. Pose clauses: relaxed side sleep; belly-up with
28
+ loosely bent paws and tiny tongue; loose curl with tail near muzzle; chin resting
29
+ on forepaws. Source prompts and PNGs are preserved locally with the approved studies.
30
+
31
+ The following section records the earlier0.0.4 preparation, not the new sleep keys.
32
+
3
33
  Source code MIT; generated puppy art and frame data CC BY-SA4.0, retaining the
4
34
  credits in ATTRIBUTION.md. This is a development-source record, not new legal
5
35
  clearance. No external photograph, YouTube frame or downloaded rig was introduced.
package/INSTALLING.md CHANGED
@@ -6,6 +6,10 @@ Desktop setup (Node20+; [OS requirements and verification](DESKTOP-PLATFORMS.md)
6
6
  2. `retrieviewer desktop` — select the app window in the picker.
7
7
 
8
8
  No agent configuration is needed for this separate transparent companion.
9
+ Choose the dot ink at launch: `retrieviewer desktop --ink gold`, or
10
+ `retrieviewer desktop --ink dark` on light backgrounds. Default: `cream`.
11
+ Exit an existing companion before changing ink. Presets reuse the same Braille
12
+ mask and native painter, without new color sprites or screen capture.
9
13
  Windows and AlmaLinux8/GLIBC2.28 X11 have live test evidence. macOS support is
10
14
  experimental and not runtime verified; native Wayland is unsupported. Linux needs
11
15
  system Python3.6+, X11/Xrender/Xext and a compositor; Windows needs the .NET
package/README.ko.md CHANGED
@@ -4,7 +4,8 @@
4
4
  <p align="center"><a href="https://bitefoundry.github.io/retrieviewer/?lang=ko">직접 만나 보기</a> · <a href="README.md">English</a> · <a href="https://www.npmjs.com/package/@bitefoundry/retrieviewer">npm</a> · <a href="https://github.com/BiteFoundry/retrieviewer/issues">피드백</a></p>
5
5
 
6
6
  앱 창 위에서 혼자 걷고, 뛰고, 앉아서 쉬는 보송한 점자 도트 리트리버예요.
7
- 발라당 누워 쉬거나 눈을 감고 잠들기도 해요. 가끔 공·간식·터그로 놀아주세요.
7
+ 옆잠, 발라당, 몸 말기, 앞발에 턱 괴기. 조용하면 양쪽으로 누워 잠들어요.
8
+ 가끔 공·간식·터그로 놀아주세요.
8
9
  나머지 시간에는 각자의 일을 하면 돼요.
9
10
  [BiteFoundry](https://github.com/BiteFoundry)가 만듭니다.
10
11
  런타임에는 계정, 모델 API, 텔레메트리가 필요하지 않아요.
@@ -31,6 +32,11 @@ Node.js/npm이 없다면 [먼저 설치](PREREQUISITES.md)해 주세요.
31
32
  통과시킵니다. 앱 창 단위로 동작하고, 대상 창이 비활성화·최소화되면 숨습니다.
32
33
  자동 시작이나 셸 래퍼는 등록하지 않아요.
33
34
 
35
+ 도트 색은 실행할 때 고를 수 있어요. 기본은 크림색이고, 골드색은
36
+ `retrieviewer desktop --ink gold`, 밝은 배경에는
37
+ `retrieviewer desktop --ink dark`를 쓰세요. 실행 중이라면 먼저 종료하고 다시
38
+ 실행하세요. 배경을 캡처하지 않고, 같은 도트에 색만 바꿉니다.
39
+
34
40
  > **Cursor / VS Code Remote SSH 사용자:** 원격 서버의 터미널이 아닌,
35
41
  > Cursor 화면이 떠 있는 **로컬 PC의 PowerShell/터미널**에 설치하세요.
36
42
  > 서버의 `DISPLAY=:1`은 서버 쪽 X11 화면을 뜻합니다. VNC로 그 화면을 보고
package/README.md CHANGED
@@ -40,6 +40,10 @@ click-through native overlay—not a patched app, shell wrapper or injected MCP
40
40
  widget. It follows a whole app window, not a tab, and hides when that window is
41
41
  inactive or minimized. No automatic startup entry is installed.
42
42
 
43
+ Choose the dot color at launch: `retrieviewer desktop --ink gold`, or
44
+ `retrieviewer desktop --ink dark` for light backgrounds. Default: `cream`.
45
+ Quit a running companion first. Colors reuse the same artwork; no screen capture.
46
+
43
47
  > **Cursor / VS Code Remote SSH:** run those commands in your **local** computer's
44
48
  > terminal, not the remote server's terminal. The puppy belongs where the window
45
49
  > is displayed. A server's `DISPLAY=:1` refers to that server's X11 display, which
package/RELEASING.md CHANGED
@@ -1,4 +1,13 @@
1
- # Retrieviewer 0.0.4 release process
1
+ # Retrieviewer 0.0.5 release process
2
+
3
+ Current target: `@bitefoundry/retrieviewer@0.0.5`, public `latest`. The owner
4
+ explicitly requested npm publication and homepage update of the approved sleep
5
+ studies and mirrored side on2026-10-23. This also includes manual ink presets,
6
+ sleep pressure independent of energy, shared gesture scratch and5fps sleep paint.
7
+ Preserve0.0.4 registry bytes; never republish them. Run all checks below and publish
8
+ only the exact tested0.0.5 tarball. Four new approved keys stay under the existing
9
+ CC BY-SA4.0 artwork terms; see COMFORT-ART.md. The historical scope below describes
10
+ 0.0.4, not current verification results.
2
11
 
3
12
  Target: `@bitefoundry/retrieviewer@0.0.4`, public npm registry, `latest` tag.
4
13
  The npm organization `bitefoundry` uses the free public-packages plan; account
@@ -180,6 +180,12 @@ class Companion(object):
180
180
  if kind == 'quit':
181
181
  self.quit()
182
182
  elif kind == 'loaded':
183
+ palette = {'cream': (248, 242, 232), 'gold': (231, 184, 103), 'dark': (67, 48, 24)}
184
+ ink = message.get('ink', 'cream')
185
+ if ink not in palette:
186
+ raise OverlayError('Invalid ink')
187
+ self.tiles.ink = palette[ink]
188
+ self.tiles.cache.clear()
183
189
  self.loaded = True
184
190
  self.set_status('playing')
185
191
  if self.test_mode:
@@ -238,7 +238,7 @@ function createCore(io, options) {
238
238
  if (s.quitting) return;
239
239
  if (!m || typeof m !== 'object') throw new Error('Invalid overlay message');
240
240
  if (m.type === 'quit') { quit(); return; }
241
- if (m.type === 'loaded') { s.loaded = true; io.status(TEXT.playing); io.loaded(); return; }
241
+ if (m.type === 'loaded') { var palette = {cream:[248,242,232,245],gold:[231,184,103,245],dark:[67,48,24,245]}; var ink = m.ink || 'cream'; if (!Object.prototype.hasOwnProperty.call(palette,ink)) throw new Error('Invalid ink'); INK=palette[ink]; s.loaded = true; io.status(TEXT.playing); io.loaded(); return; }
242
242
  if (m.type === 'interaction') { io.status(m.accepted === true ? TEXT.accepted : TEXT.busy); return; }
243
243
  if (m.type === 'error') { io.status(TEXT.engineError); quit(); return; }
244
244
  if (m.type === 'frame') {
@@ -61,6 +61,7 @@ class QuietWindow : Form {
61
61
  sealed class PetWindow : QuietWindow {
62
62
  protected override CreateParams CreateParams {get{CreateParams p=base.CreateParams;p.ExStyle|=0x80000|0x20;return p;}} // LAYERED + TRANSPARENT
63
63
  internal int PaintedDots;
64
+ internal Color Ink=Color.FromArgb(245,248,242,232);
64
65
  internal Bitmap PaintFrame(Dictionary<string,object> m,int cell,Rectangle canvas){
65
66
  int cols=Program.Int(m,"columns"),rows=Program.Int(m,"rows"),cx=Program.Int(m,"x"),cy=Program.Int(m,"y");
66
67
  string text=Program.Str(m,"text");
@@ -69,7 +70,7 @@ sealed class PetWindow : QuietWindow {
69
70
  string[] lines=text.Split('\n');if(lines.Length!=rows)throw new Exception("Invalid overlay row count");
70
71
  Bitmap image=new Bitmap(cols*cell,rows*cell*2,PixelFormat.Format32bppPArgb);
71
72
  try{
72
- using(Graphics g=Graphics.FromImage(image))using(Brush ink=new SolidBrush(Color.FromArgb(245,248,242,232))){
73
+ using(Graphics g=Graphics.FromImage(image))using(Brush ink=new SolidBrush(Ink)){
73
74
  g.Clear(Color.Transparent);g.SmoothingMode=System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
74
75
  int[] dx={0,0,0,1,1,1,0,1},dy={0,1,2,0,1,2,3,3};float step=cell/2f,diameter=Math.Max(1.1f,step*.48f);PaintedDots=0;
75
76
  for(int y=0;y<rows;y++){
@@ -149,7 +150,7 @@ sealed class Companion : ApplicationContext {
149
150
  for(;;){Dictionary<string,object> m;lock(inputLock){if(messages.Count==0)break;m=messages.Dequeue();}
150
151
  string type=Program.Str(m,"type");
151
152
  if(type=="quit"){Quit();return;}
152
- if(type=="loaded"){loaded=true;status.Text="함께 노는 중";if(testMode)((Button)bar.Controls[0]).PerformClick();continue;}
153
+ if(type=="loaded"){string ink=m.ContainsKey("ink")?Program.Str(m,"ink"):"cream";if(ink!="cream"&&ink!="gold"&&ink!="dark")throw new Exception("Invalid ink");pet.Ink=ink=="gold"?Color.FromArgb(245,231,184,103):ink=="dark"?Color.FromArgb(245,67,48,24):Color.FromArgb(245,248,242,232);loaded=true;status.Text="함께 노는 중";if(testMode)((Button)bar.Controls[0]).PerformClick();continue;}
153
154
  if(type=="interaction"){status.Text=Program.Bool(m,"accepted")?"좋아!":"잠깐 기다려";continue;}
154
155
  if(type=="error"){status.Text="엔진 오류";Quit();return;}
155
156
  if(type=="frame"){
@@ -93,13 +93,14 @@ 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 ink=options.ink??'cream';if(!['cream','gold','dark'].includes(ink))throw Error('Invalid desktop ink');
96
97
  // Fail before creating any cache/session on unsupported desktops (e.g. Wayland).
97
98
  desktopPlatform(options);
98
99
  const status=await desktopStatus(options);if(status.running)return {...status,alreadyRunning:true};
99
100
  if(status.stale)await stopDesktop(options);
100
101
  const helper=await desktopHelper(options);
101
102
  return new Promise((resolve,reject)=>{
102
- 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:{...process.env,RETRIEVIEWER_HOME:getConfigPaths(options).root}});
103
+ 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:{...process.env,RETRIEVIEWER_HOME:getConfigPaths(options).root,RETRIEVIEWER_INK:ink}});
103
104
  let settled=false;const finish=(error,message)=>{if(settled)return;settled=true;clearTimeout(timer);if(child.connected)child.disconnect();child.unref();error?reject(error):resolve(message);};
104
105
  const timer=setTimeout(()=>{if(child.connected)child.send({command:'abort'});finish(Error('Desktop startup timed out; no agent settings changed'));},20000);
105
106
  child.on('error',error=>finish(error));child.on('message',message=>message.error?finish(Error(message.error)):finish(null,message));child.on('exit',code=>finish(Error('Desktop exited during startup ('+code+')')));
@@ -20,23 +20,25 @@ export function cropFrame(frame,revision){
20
20
 
21
21
  // Request/ack rendering: never queue obsolete frames behind a slow native painter.
22
22
  export function createFramePump({pet,send,now=()=>performance.now()}){
23
- let viewport=null,inFlight=false,last=null,lastFrame=null,revision=0,paused=false,disposed=false;
23
+ let viewport=null,inFlight=false,last=null,lastFrame=null,revision=0,paused=false,disposed=false,nextPaint=0;
24
24
  const alive=()=>{if(disposed)throw Error('Overlay pump is disposed');};
25
25
  return {
26
- viewport(message){alive();const next=viewportOf(message);revision=message.revision;viewport=next;last=null;lastFrame=null;},
27
- pause(value){alive();paused=!!value;last=null;lastFrame=null;},
26
+ viewport(message){alive();const next=viewportOf(message);revision=message.revision;viewport=next;last=null;lastFrame=null;nextPaint=0;},
27
+ pause(value){alive();paused=!!value;last=null;lastFrame=null;nextPaint=0;},
28
28
  ack(){alive();inFlight=false;},
29
29
  tick(){
30
30
  alive();if(inFlight||!viewport||paused)return false;
31
31
  const t=now(),dt=last===null?0:Math.max(0,(t-last)/1000);last=t;
32
- pet.step(dt);const frame=cropFrame(pet.frame(viewport),revision);
32
+ pet.step(dt);if(t<nextPaint)return false;
33
+ const source=pet.frame(viewport),frame=cropFrame(source,revision);
34
+ nextPaint=source.state?.state==='sleep'?t+200:0;
33
35
  // A held seat/down pose can be byte-identical for seconds. Advance the life
34
36
  // clock normally but avoid JSON, IPC, bitmap allocation and native repaint.
35
37
  if(lastFrame&&frame.x===lastFrame.x&&frame.y===lastFrame.y&&frame.columns===lastFrame.columns&&frame.rows===lastFrame.rows&&frame.text===lastFrame.text)return false;
36
38
  inFlight=true;
37
39
  try{send(frame);lastFrame=frame;}catch(error){inFlight=false;throw error;}return true;
38
40
  },
39
- interact(kind){alive();if(!['ball','treat','tug'].includes(kind))throw Error('Invalid overlay interaction');const accepted=pet.interact(kind);if(accepted)lastFrame=null;return accepted;},
41
+ interact(kind){alive();if(!['ball','treat','tug'].includes(kind))throw Error('Invalid overlay interaction');const accepted=pet.interact(kind);if(accepted){lastFrame=null;nextPaint=0;}return accepted;},
40
42
  get hidden(){return !viewport||paused;},
41
43
  dispose(){if(!disposed){disposed=true;viewport=lastFrame=null;pet.dispose();}},
42
44
  };
@@ -61,6 +61,6 @@ try{
61
61
  const {createPet}=await import('../../retrieviewer.mjs');const pet=await createPet();
62
62
  if(stopping)pet.dispose();else{
63
63
  pump=createFramePump({pet,send:write});if(pendingViewport)pump.viewport(pendingViewport);
64
- write({type:'loaded'});timer=setInterval(()=>{try{pump.tick();}catch(error){write({type:'error',message:error.message});stop();}},1000/spec.fps);
64
+ write({type:'loaded',ink:process.env.RETRIEVIEWER_INK??'cream'});timer=setInterval(()=>{try{pump.tick();}catch(error){write({type:'error',message:error.message});stop();}},1000/spec.fps);
65
65
  }
66
66
  }catch(error){sendParent({error:error.message});await stop();}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitefoundry/retrieviewer",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
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/",
@@ -12,14 +12,22 @@ export function comfortFrameIndex(progress,info){
12
12
  return loop[Math.floor((seconds-.65)/.28)%loop.length];
13
13
  }
14
14
 
15
- export function createComfortSpriteSampler(bytes,info){
16
- if(!isGrayFrameSource(bytes)||info?.version!==1||info.width!==320||info.height!==320||info.frames!==9)throw Error('Invalid comfort sprites');
17
- const frames=grayFrameReader(bytes,320,320,info.frames);let rgba=null;
15
+ export function createComfortSpriteSampler(bytes,info,output){
16
+ if(!isGrayFrameSource(bytes)||![1,2].includes(info?.version)||info.width!==320||info.height!==320||info.frames!==(info.version===2?13:9))throw Error('Invalid comfort sprites');
17
+ if(output!==undefined&&(!(output instanceof Uint8ClampedArray)||output.length!==320*320*4))throw Error('Invalid comfort output');
18
+ const frames=grayFrameReader(bytes,320,320,info.frames);let rgba=output??null;
19
+ if(rgba)for(let i=3;i<rgba.length;i+=4)rgba[i]=255;
18
20
  return pose=>{
19
- if(pose?.clip!=='tug')throw Error('Invalid comfort clip');
21
+ if(!['tug','sleep'].includes(pose?.clip))throw Error('Invalid comfort clip');
20
22
  if(!rgba){rgba=new Uint8ClampedArray(320*320*4);for(let i=3;i<rgba.length;i+=4)rgba[i]=255;}
21
- const index=comfortFrameIndex(pose.progress,info),frame=frames.getFrame(index);
22
- for(let i=0,j=0;i<frame.length;i++,j+=4)rgba[j]=rgba[j+1]=rgba[j+2]=frame[i];
23
+ const index=pose.clip==='sleep'?info.sleep?.[pose.variant]:comfortFrameIndex(pose.progress,info);
24
+ if(!Number.isInteger(index))throw Error('Invalid sleep variant');
25
+ const frame=frames.getFrame(index),base=pose.base,blend=pose.blend??1;
26
+ if(!Number.isFinite(blend)||blend<0||blend>1)throw Error('Invalid sleep blend');
27
+ for(let i=0,j=0;i<frame.length;i++,j+=4){
28
+ const v=frame[pose.right?Math.floor(i/320)*320+319-i%320:i];
29
+ rgba[j]=rgba[j+1]=rgba[j+2]=base&&blend<1?Math.round(base[j]+(v-base[j])*blend):v;
30
+ }
23
31
  return rgba;
24
32
  };
25
33
  }
Binary file
@@ -1,16 +1,17 @@
1
1
  {
2
- "version": 1,
2
+ "version": 2,
3
3
  "width": 320,
4
4
  "height": 320,
5
- "frames": 9,
5
+ "frames": 13,
6
6
  "clips": [
7
- "tug"
7
+ "tug",
8
+ "sleep"
8
9
  ],
9
- "gzipBytes": 103549,
10
- "residentPixelBytes": 291337,
10
+ "gzipBytes": 129148,
11
+ "residentPixelBytes": 341139,
11
12
  "sourceSheetSha256": "10fde9dacca5cb45acd68f387e7ed3bcf88cd72cf0f9c4220d31d819db91731e",
12
13
  "neutralSha256": "19f89a8bcb5a937e528593a8f36db846a0d45a5ee398f1b0873c056e5be2fa0a",
13
- "encodedSha256": "e046e214da585ffda65e8d2c1a86c589af589325b792634f128ab8b71f6dbf3a",
14
+ "encodedSha256": "35327cc103b1f7ac2ff1b09ed98658fd8ff1dcef322de9c9b18071a4358b7b87",
14
15
  "method": "Built-in imagegen authored tug keys. One fixed scale, ground/center translation only. Exact existing Q5 seat first/last. No expanded tween film or live body deformation.",
15
16
  "registration": {
16
17
  "sheetSize": [
@@ -73,5 +74,67 @@
73
74
  "dy": 74
74
75
  }
75
76
  ]
77
+ },
78
+ "sleep": {
79
+ "side": 9,
80
+ "belly": 10,
81
+ "curl": 11,
82
+ "chin": 12
83
+ },
84
+ "sleepRegistration": [
85
+ {
86
+ "key": "side",
87
+ "sourceBounds": [
88
+ 26,
89
+ 461,
90
+ 1234,
91
+ 873
92
+ ],
93
+ "width": 254,
94
+ "scale": 0.21026490066225165,
95
+ "ground": 300
96
+ },
97
+ {
98
+ "key": "belly",
99
+ "sourceBounds": [
100
+ 28,
101
+ 413,
102
+ 1239,
103
+ 881
104
+ ],
105
+ "width": 224,
106
+ "scale": 0.18497109826589594,
107
+ "ground": 300
108
+ },
109
+ {
110
+ "key": "curl",
111
+ "sourceBounds": [
112
+ 44,
113
+ 373,
114
+ 1213,
115
+ 992
116
+ ],
117
+ "width": 216,
118
+ "scale": 0.1847733105218135,
119
+ "ground": 300
120
+ },
121
+ {
122
+ "key": "chin",
123
+ "sourceBounds": [
124
+ 116,
125
+ 435,
126
+ 1218,
127
+ 953
128
+ ],
129
+ "width": 198,
130
+ "scale": 0.17967332123411978,
131
+ "ground": 300
132
+ }
133
+ ],
134
+ "sleepSourceHashes": {
135
+ "side": "85c567eb0958a213ee5b93d0a8592639489f5f29dded35e0d1d22e920ed1fe9f",
136
+ "belly": "e1967d9156a119d04e25fd6428fe5dcdbd2156c5e4eba0347217847874006109",
137
+ "curl": "10c9a85701af3587c48c1d8158972a07026e79a18a2375d86108538eda05aae7",
138
+ "chin": "e971eaa6861b597f0dba3a487403cee5f120cba0a9f9584d831b5634d096dee7"
76
139
  }
77
140
  }
@@ -39,10 +39,10 @@ export function createCompanionLife(actor,{seed=731,energy=.8}={}){
39
39
  if(!Number.isFinite(energy)||energy<0||energy>1)throw RangeError('energy must be within 0..1');
40
40
 
41
41
  const rand=mulberry32(seed),range=(a,b)=>a+(b-a)*rand(),side=()=>rand()<.5?'left':'right';
42
- const needs={energy,social:.5,curiosity:.5};
42
+ const needs={energy,social:.5,curiosity:.5,drowsiness:0};
43
43
  const cooldowns={ball:0,treat:0,tug:0};
44
44
  const episodeCooldowns={roll:0,play:0,belly:0,sleep:0};
45
- let clock=0,decisions=0,lastEpisode=null;
45
+ let clock=0,decisions=0,lastEpisode=null,quiet=0;
46
46
  let queue=[],hold=0,holdIntent=null,waiting=null,gesture=null;
47
47
  let intent='start',label=LABEL.start,pending=null,active=null,ball=null,treat=null;
48
48
 
@@ -72,7 +72,7 @@ export function createCompanionLife(actor,{seed=731,energy=.8}={}){
72
72
  checkin:()=>[home(),sit('sit'),cmd('look',[side()],'look'),wait(range(5,10),'checkin'),fx(()=>{needs.social-=.4;})],
73
73
  play:()=>[cmd('spin',[],'play'),wait(range(1,2),'pause'),sit('sit'),wait(range(2,4),'watch'),fx(()=>{needs.energy-=.12;needs.curiosity-=.2;})],
74
74
  belly:()=>[cmd('belly',[range(5,9)],'belly'),wait(range(2,4),'watch'),fx(()=>{needs.energy+=.08;needs.curiosity-=.15;})],
75
- sleep:()=>[cmd('sleep',[range(22,38)],'sleep'),wait(range(2,4),'recover'),fx(()=>{needs.energy+=.3;})],
75
+ sleep:()=>[cmd('sleep',[range(65,110),['side','curl','chin','belly'][Math.floor(rand()*4)],rand()<.5],'sleep'),wait(range(2,4),'recover'),fx(()=>{needs.energy+=.3;})],
76
76
  };
77
77
  const weights=()=>{
78
78
  const {energy:e,social:s,curiosity:c}=needs;
@@ -87,6 +87,9 @@ export function createCompanionLife(actor,{seed=731,energy=.8}={}){
87
87
  const w=weights();if(lastEpisode)w[lastEpisode]=0;
88
88
  const kinds=Object.keys(w),total=kinds.reduce((sum,k)=>sum+w[k],0);
89
89
  let pick=rand()*total,kind=kinds.find(k=>(pick-=w[k])<0)??'observe';
90
+ // Quiet-time sleep pressure is independent of energy restored by sitting.
91
+ // Wait for the current coherent episode to end; never yank a running puppy.
92
+ if(quiet>=120&&needs.drowsiness>=.35&&episodeCooldowns.sleep===0&&lastEpisode!=='sleep'&&typeof actor.sleep==='function')kind='sleep';
90
93
  lastEpisode=kind;decisions++;
91
94
  if(kind==='roll')episodeCooldowns.roll=90;
92
95
  if(kind==='play')episodeCooldowns.play=120;
@@ -123,6 +126,9 @@ export function createCompanionLife(actor,{seed=731,energy=.8}={}){
123
126
  function drift(dt){
124
127
  const resting=holdIntent==='rest'||holdIntent==='recover'||actor.state==='sleep'||intent==='belly';
125
128
  needs.energy+=resting?.04*dt:-.004*dt;
129
+ const asleep=actor.state==='sleep';
130
+ needs.drowsiness+=asleep?-dt/60:dt/240;
131
+ quiet=active||pending||asleep?0:quiet+dt;
126
132
  if(!active){needs.curiosity+=.01*dt;needs.social+=.008*dt;}
127
133
  for(const k in needs)needs[k]=clamp(needs[k],0,1);
128
134
  }
@@ -168,6 +174,7 @@ export function createCompanionLife(actor,{seed=731,energy=.8}={}){
168
174
  if(!['ball','treat','tug'].includes(kind))return false;
169
175
  if(kind==='tug'&&typeof actor.tug!=='function')return false;
170
176
  if(pending||active||cooldowns[kind]>0)return false;
177
+ quiet=0;
171
178
  if(kind==='treat'||kind==='tug'){
172
179
  pending=kind;
173
180
  // The pet wakes through its authored recovery, not after a long sleep timer.
@@ -206,7 +213,7 @@ export function createCompanionLife(actor,{seed=731,energy=.8}={}){
206
213
  return {
207
214
  intent,label:pending?`${label} · ${PENDING_LABEL[pending]}`:label,episode:lastEpisode,interaction,busy:interaction!==null,
208
215
  ball:ballReport(),treat:treatReport(),tug:actor.state==='tug'?{progress:actor.sample(clock).comfortPose?.progress??0,grip:1}:null,
209
- snore:actor.state==='sleep'?((clock%3.6)/3.6):null,energy:needs.energy,decisions,cooldowns:{...cooldowns},
216
+ snore:actor.state==='sleep'?((clock%3.6)/3.6):null,energy:needs.energy,drowsiness:needs.drowsiness,decisions,cooldowns:{...cooldowns},
210
217
  // Local hold clock only: progress freezes between updates; null outside the stationary hold.
211
218
  gesture:gesture&&SAFE.has(actor.state)?{clip:gesture.clip,progress:clamp(gesture.elapsed/gesture.duration,0,1)}:null
212
219
  };
@@ -1,11 +1,13 @@
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){
4
+ export function createCompanionSpriteSampler(bytes,info,output){
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');
8
- const frames=grayFrameReader(bytes,320,320,info.pairs*9),rgba=new Uint8ClampedArray(n*4);for(let i=3;i<rgba.length;i+=4)rgba[i]=255;
8
+ const frames=grayFrameReader(bytes,320,320,info.pairs*9),rgba=output??new Uint8ClampedArray(n*4);
9
+ if(!(rgba instanceof Uint8ClampedArray)||rgba.length!==n*4)throw Error('Invalid sprite output');
10
+ for(let i=3;i<rgba.length;i+=4)rgba[i]=255;
9
11
  return({clip,progress})=>{
10
12
  const list=info.clips[clip];if(!list||!Number.isFinite(progress)||progress<0||progress>1)throw Error('Invalid companion sprite pose');
11
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);
@@ -37,11 +37,13 @@ export function paperActionPose(name,progress,info){
37
37
  if(!Number.isFinite(blend))throw Error('Invalid paper action: blend');
38
38
  return{pair:list[index],blend};
39
39
  }
40
- export function createPaperActionSampler(bytes,info){
40
+ export function createPaperActionSampler(bytes,info,output){
41
41
  if(!isGrayFrameSource(bytes))throw Error('Invalid action cache: bytes');
42
42
  validatePaperActionInfo(info,bytes.byteLength);
43
43
  const stride=info.width*info.height,pairs=info.pairs,frames=grayFrameReader(bytes,info.width,info.height,pairs*9);
44
- const out=new Uint8ClampedArray(stride*4);for(let i=3;i<out.length;i+=4)out[i]=255;
44
+ const out=output??new Uint8ClampedArray(stride*4);
45
+ if(!(out instanceof Uint8ClampedArray)||out.length!==stride*4)throw Error('Invalid action output');
46
+ for(let i=3;i<out.length;i+=4)out[i]=255;
45
47
  return state=>{const p=paperActionPose(state.actionName,state.actionProgress,info),sample=Math.round(p.blend*8);
46
48
  if(p.pair>=pairs||sample<0||sample>8)throw Error('Invalid paper action: sample out of range');
47
49
  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;};
@@ -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){const sample=createRearSampler(bytes,info);return state=>{const p=paperBehaviorPose(state,info);return sample(p.pair,p.blend);};}
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);};}
@@ -91,9 +91,11 @@ export function footSourcePoint(x,y,pins){
91
91
  return chosen?{x:x-chosen.dx*best,y:y-chosen.dy*best}:{x,y};
92
92
  }
93
93
 
94
- export function createRearSampler(bytes,info){
94
+ export function createRearSampler(bytes,info,output){
95
95
  if(!isGrayFrameSource(bytes)||info.pairs!==18||info.samples!==9||bytes.byteLength!==18*9*320*320)throw Error('Invalid rear sprite cache');
96
- const frames=grayFrameReader(bytes,320,320,18*9),out=new Uint8ClampedArray(320*320*4);for(let i=3;i<out.length;i+=4)out[i]=255;
96
+ const frames=grayFrameReader(bytes,320,320,18*9),out=output??new Uint8ClampedArray(320*320*4);
97
+ if(!(out instanceof Uint8ClampedArray)||out.length!==320*320*4)throw Error('Invalid rear output');
98
+ for(let i=3;i<out.length;i+=4)out[i]=255;
97
99
  return(pair,blend)=>{
98
100
  if(!Number.isInteger(pair)||pair<0||pair>=18||!Number.isFinite(blend)||blend<0||blend>1)throw Error('Invalid rear pose');
99
101
  const frame=frames.getFrame(pair*9+Math.round(blend*8));
@@ -1,9 +1,11 @@
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){
3
+ export function createPaperSeatSampler(bytes,info,output){
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
- const frames=grayFrameReader(bytes,320,320,3*17),out=new Uint8ClampedArray(stride*4);for(let i=3;i<out.length;i+=4)out[i]=255;
6
+ const frames=grayFrameReader(bytes,320,320,3*17),out=output??new Uint8ClampedArray(stride*4);
7
+ if(!(out instanceof Uint8ClampedArray)||out.length!==stride*4)throw Error('Invalid seat output');
8
+ for(let i=3;i<out.length;i+=4)out[i]=255;
7
9
  return progress=>{
8
10
  if(!Number.isFinite(progress)||progress<0||progress>1)throw Error('Invalid paper sitting progress');
9
11
  const q=progress*3,pair=Math.min(2,Math.floor(q)),sample=Math.round((q-pair)*16),frame=frames.getFrame(pair*17+sample);
package/puppy-paper.mjs CHANGED
@@ -40,7 +40,7 @@ export function createPaperActor(animal,info,{manualTricks=false,trickTimings=nu
40
40
  let straight=false,seatProgress=0,seatVariant='default',travel=null,approachPlan=null,travelAge=0,travelDistance=0,motionSpeed=0,motionStage='rest',completed=false;
41
41
  let watchSide='left',restProgress=0,idleHold=2.5;
42
42
  let pendingAction=null,activeAction=null,actionDone=null,deferred=null,movementMode='run',stayDown=false,manualDown=false,spinReady=false;
43
- let comfort=null,comfortSeconds=0;
43
+ let comfort=null,comfortSeconds=0,sleepLook=null,nextSleepLook={variant:'side',right:false};
44
44
  const director=createPaperDirector();
45
45
  const points=[[.12,PAPER_GROUND.away]];
46
46
  const enter=s=>{state=s;age=0;};
@@ -99,7 +99,7 @@ export function createPaperActor(animal,info,{manualTricks=false,trickTimings=nu
99
99
  pendingAction=null;
100
100
  if(name==='sit'){enter('idle');return;}
101
101
  if(name==='belly'){const seconds=comfortSeconds;playAction('lie-down',()=>startBelly(seconds),.65);return;}
102
- if(name==='sleep'){stayDown=false;comfort={kind:'sleep',seconds:comfortSeconds};enter('recline');return;}
102
+ if(name==='sleep'){stayDown=false;sleepLook={...nextSleepLook};comfort={kind:'sleep',seconds:comfortSeconds};enter('recline');return;}
103
103
  if(name==='tug'){stop();comfort={kind:'tug',seconds:comfortSeconds};enter('tug');return;}
104
104
  if(name==='lie'){
105
105
  if(manualTricks){playAction('lie-down',()=>{manualDown=true;enter('down');drain();},.65);}
@@ -160,7 +160,7 @@ export function createPaperActor(animal,info,{manualTricks=false,trickTimings=nu
160
160
  lie(){const name=manualDown||activeAction?.name==='lie-down'||restProgress>0?'sit':'lie';return dispatch(()=>requestAction(name));},
161
161
  // Comfort poses: bounded finite seconds; same dispatch safety as tricks.
162
162
  belly(seconds=6){comfortDuration('belly',seconds);return dispatch(()=>{comfortSeconds=seconds;return requestAction('belly');},{keepDown:true});},
163
- sleep(seconds=25){comfortDuration('sleep',seconds);return dispatch(()=>{comfortSeconds=seconds;return requestAction('sleep');});},
163
+ sleep(seconds=25,variant='side',right=false){comfortDuration('sleep',seconds);if(!['side','belly','curl','chin'].includes(variant)||typeof right!=='boolean')throw Error('Invalid sleep pose');return dispatch(()=>{comfortSeconds=seconds;nextSleepLook={variant,right};return requestAction('sleep');});},
164
164
  tug(seconds=6){comfortDuration('tug',seconds);return dispatch(()=>{comfortSeconds=seconds;return requestAction('tug');});},
165
165
  rest(){return dispatch(()=>{route=false;target=null;waypoints=[];pendingAction=null;stayDown=false;comfort=null;stop();if(restProgress>0){enter('wake');}else if(state==='watch'||state==='arrival-exit'){/* finish shared return */}else if(seatProgress>0||state==='rise'||state==='sit-down')enter('sit-down');else if(!['idle','view-turn','turn'].includes(state))enter('finish');});},
166
166
  update(dt){
@@ -209,7 +209,8 @@ export function createPaperActor(animal,info,{manualTricks=false,trickTimings=nu
209
209
  if(![12,24,30].includes(poseFps))throw Error('Invalid pose cadence');
210
210
  let pair=20,blend=0,kind=state,rearPair=null,approachPair=null;
211
211
  let seat=state==='sit-down'||state==='rise'||seatProgress>0?Math.round(seatProgress*48)/48:null;
212
- let behavior=null,behaviorProgress=0,comfortPose=state==='tug'?{clip:'tug',progress:tugProgress()}:null;
212
+ if(restProgress===0&&state!=='recline')sleepLook=null;
213
+ let behavior=null,behaviorProgress=0,comfortPose=state==='tug'?{clip:'tug',progress:tugProgress()}:sleepLook&&restProgress>.72?{clip:'sleep',...sleepLook,blend:comfortEase((restProgress-.72)/.28)}:null;
213
214
  if(activeAction?.comfort)kind=activeAction.comfort;
214
215
  let actionName=activeAction?.name??(state==='run'&&movementMode==='walk'?'walk':null),actionProgress=activeAction?(activeAction.timing?activeAction.timing.progressAt(age):Math.min(1,age/activeAction.duration)):phase;
215
216
  if(manualDown&&state==='down'){actionName='lie-down';actionProgress=1;}
@@ -251,14 +252,20 @@ export function createPaperRenderer(cache,info,meta,width,height,{rear,approach,
251
252
  if(![width,height].every(Number.isInteger)||width<2||height<2||width*height>1_000_000)throw Error('Invalid paper grid');
252
253
  const tween=createCachedTween(cache,info),renderMeta={...meta,pairs:info.pair_map,run_sequence:info.run_sequence};
253
254
  const out=new Uint8ClampedArray(width*height*4),area=createPaperAreaSampler();
254
- const rearSample=rear?createRearSampler(rear.bytes,rear.info):null,lock=createPaperFootLock({maxImageStretch:pawTransport?1.45:Infinity});
255
- const approachSample=approach?createRearSampler(approach.bytes,approach.info):null;
256
- const seatSample=seat?createPaperSeatSampler(seat.bytes,seat.info):null;
257
- const arrivalSeatSample=arrivalSeat?createPaperSeatSampler(arrivalSeat.bytes,arrivalSeat.info):null;
258
- const behaviorSample=behaviors?createPaperBehaviorSampler(behaviors.bytes,behaviors.info):null;
259
- const interactionSample=interactions?createCompanionSpriteSampler(interactions.bytes,interactions.info):null;
260
- const comfortSample=comfort?createComfortSpriteSampler(comfort.bytes,comfort.info):null;
261
- const actionSample=actions?createPaperActionSampler(actions.bytes,actions.info):null,actionArea=actions?createPaperAreaSampler(actions.info.width):null;
255
+ // Only one authored source is drawn at a time. Small samplers borrow a view of
256
+ // the action-sized scratch. A sleep blend reads each base pixel before writing
257
+ // that same pixel, so it needs no second image or per-frame temporary buffer.
258
+ const actionScratch=new Uint8ClampedArray(Math.max(320*320,(actions?.info.width??320)*(actions?.info.height??320))*4);
259
+ const spriteScratch=actionScratch.subarray(0,320*320*4);
260
+ for(let i=3;i<actionScratch.length;i+=4)actionScratch[i]=255;
261
+ const rearSample=rear?createRearSampler(rear.bytes,rear.info,spriteScratch):null,lock=createPaperFootLock({maxImageStretch:pawTransport?1.45:Infinity});
262
+ const approachSample=approach?createRearSampler(approach.bytes,approach.info,spriteScratch):null;
263
+ const seatSample=seat?createPaperSeatSampler(seat.bytes,seat.info,spriteScratch):null;
264
+ const arrivalSeatSample=arrivalSeat?createPaperSeatSampler(arrivalSeat.bytes,arrivalSeat.info,spriteScratch):null;
265
+ const behaviorSample=behaviors?createPaperBehaviorSampler(behaviors.bytes,behaviors.info,spriteScratch):null;
266
+ const interactionSample=interactions?createCompanionSpriteSampler(interactions.bytes,interactions.info,spriteScratch):null;
267
+ const comfortSample=comfort?createComfortSpriteSampler(comfort.bytes,comfort.info,spriteScratch):null;
268
+ const actionSample=actions?createPaperActionSampler(actions.bytes,actions.info,actionScratch.subarray(0,actions.info.width*actions.info.height*4)):null,actionArea=actions?createPaperAreaSampler(actions.info.width):null;
262
269
  const walkEpisodes=actions?createPaperSupportEpisodes(actions.info.feet,actions.info.clips.walk,9):null;
263
270
  const sourceEpisodes=rear?{
264
271
  front:createPaperSupportEpisodes(rear.info.frontFeet,[0,1,2,3,4,5,6,7],17),
@@ -276,7 +283,7 @@ export function createPaperRenderer(cache,info,meta,width,height,{rear,approach,
276
283
  const isInteraction=isComfort||state.interactionPose&&interactionSample;
277
284
  const isBehavior=!isInteraction&&state.behavior&&behaviorSample;
278
285
  const isAction=!isInteraction&&state.actionName&&actionSample,sourceN=isAction?actions.info.width:320,originX=(sourceN-320)/2,originY=(sourceN/320-1)*300;
279
- const image=isComfort?comfortSample(state.comfortPose):isInteraction?interactionSample(state.interactionPose):isAction?actionSample(state):isBehavior?behaviorSample(state):isSeat?(state.seatVariant==='arrival'&&arrivalSeatSample?arrivalSeatSample:seatSample)(state.seatProgress):isApproach?approachSample(state.approachPair,state.blend):isRear?rearSample(state.rearPair,state.blend):tween(state),side=height*size,sideX=side/pixelAspect;
286
+ const image=isComfort?comfortSample({...state.comfortPose,...state.comfortPose.clip==='sleep'&&state.comfortPose.blend<1&&behaviorSample?{base:behaviorSample({behavior:'rest',behaviorProgress:state.behaviorProgress})}:{}}):isInteraction?interactionSample(state.interactionPose):isAction?actionSample(state):isBehavior?behaviorSample(state):isSeat?(state.seatVariant==='arrival'&&arrivalSeatSample?arrivalSeatSample:seatSample)(state.seatProgress):isApproach?approachSample(state.approachPair,state.blend):isRear?rearSample(state.rearPair,state.blend):tween(state),side=height*size,sideX=side/pixelAspect;
280
287
  // Quantize source lookup only, never the projected rectangle. Rounding side,
281
288
  // left and top first introduced whole-body scale/position jumps at thresholds.
282
289
  const left=width*x-sideX/2,floor=isInteraction||isAction||isBehavior||isSeat||isRear||isApproach?0:cleanGroundOffset(state,renderMeta),top=height*y-(300-floor)/320*side;
@@ -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
16
+ retrieviewer desktop [--ink cream|gold|dark]
17
17
  retrieviewer desktop-status [--json]
18
18
  retrieviewer desktop-stop
19
19
  retrieviewer desktop-clean --yes
@@ -32,15 +32,17 @@ export function parseArguments(args){
32
32
  const [command='help',...rest]=args,options={};
33
33
  for(let i=0;i<rest.length;i++){
34
34
  const token=rest[i];
35
- if(token==='--agent'){
36
- if(options.agent||!rest[i+1]||rest[i+1].startsWith('-'))throw Error('--agent requires one ID');
37
- options.agent=rest[++i];
35
+ if(token==='--agent'||token==='--ink'){
36
+ const key=token.slice(2);
37
+ if(options[key]||!rest[i+1]||rest[i+1].startsWith('-'))throw Error(token+' requires one value');
38
+ options[key]=rest[++i];
38
39
  }else if(['--yes','--json','--dry-run','--original'].includes(token))options[token.slice(2)]=true;
39
40
  else throw Error('Unknown argument: '+token);
40
41
  }
41
42
  if(options.agent&&!AGENTS.some(a=>a.id===options.agent))throw Error('Unknown agent: '+options.agent);
43
+ if(options.ink&&!['cream','gold','dark'].includes(options.ink))throw Error('--ink must be cream, gold or dark');
42
44
  if(!['help','--help','-h','enable','disable','disconnect','status','doctor','agents','preview','desktop','desktop-status','desktop-stop','desktop-clean','desktop-remove'].includes(command))throw Error('Unknown command: '+command);
43
- const allowed={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']};
45
+ const allowed={desktop:['ink'],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']};
44
46
  for(const key of Object.keys(options))if(!(allowed[command]??[]).includes(key))throw Error(`--${key} is not supported by ${command}`);
45
47
  return {command,options};
46
48
  }
@@ -51,7 +53,7 @@ export async function runCli(args,{input=process.stdin,output=process.stdout,err
51
53
  if(['desktop-clean','desktop-remove'].includes(command)&&!options.yes)throw Error('Use '+command+' --yes to remove only the companion cache.');
52
54
  const api=desktop??await import('./integrations/windows-overlay/desktop.mjs');
53
55
  const action={'desktop':'startDesktop','desktop-status':'desktopStatus','desktop-stop':'stopDesktop','desktop-clean':'cleanDesktop','desktop-remove':'removeDesktop'}[command];
54
- emit(await api[action]());return 0;
56
+ emit(await api[action](options.ink?{ink:options.ink}:{}));return 0;
55
57
  }
56
58
  if(command==='preview'){await import('./examples/live.mjs');return 0;}
57
59
  if(['status','doctor','agents'].includes(command)){
@@ -42,6 +42,8 @@ export interface LifeReport {
42
42
  interaction: 'ball' | 'treat' | 'tug' | null;
43
43
  busy: boolean;
44
44
  energy: number;
45
+ /** Awake-time sleep pressure; ordinary sitting does not erase it. */
46
+ drowsiness: number;
45
47
  decisions: number;
46
48
  cooldowns: {ball: number; treat: number; tug: number};
47
49
  gesture: {clip: string; progress: number} | null;