storymode-cli 1.2.1 → 1.2.3

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/README.md CHANGED
@@ -3,29 +3,31 @@
3
3
  AI-animated pixel art companions for your terminal. Reacts to Claude Code in real time.
4
4
 
5
5
  ```
6
- npx storymode-cli play 3
6
+ npx storymode-cli play
7
7
  ```
8
8
 
9
9
  ## Quick Start
10
10
 
11
11
  ```bash
12
- # Browse available characters
13
- npx storymode-cli browse
14
-
15
- # Run a companion alongside Claude Code
16
- npx storymode-cli play 3
12
+ npx storymode-cli play
17
13
  ```
18
14
 
19
- The companion opens in a tmux side pane and reacts to what Claude Code is doing — switching animations when it reads files, writes code, hits errors, etc. Speech bubbles show in-character lines.
15
+ That's it. A companion opens in a tmux side pane and reacts to what Claude Code is doing — switching animations when it reads files, writes code, hits errors, etc.
16
+
17
+ Use `browse` to switch characters or visit [storymode.fixmy.codes](https://storymode.fixmy.codes) to see the full gallery.
18
+
19
+ ```bash
20
+ npx storymode-cli browse
21
+ ```
20
22
 
21
23
  ## Options
22
24
 
23
25
  ```bash
24
- npx storymode-cli play 3 # full size (default)
25
- npx storymode-cli play 3 --compact # smaller pane
26
- npx storymode-cli play 3 --tiny # tiny pane
27
- npx storymode-cli play 3 --no-ai # disable AI personality
28
- npx storymode-cli play 3 --classic # full-screen animation viewer
26
+ npx storymode-cli play # full size (default)
27
+ npx storymode-cli play --compact # smaller pane
28
+ npx storymode-cli play --tiny # tiny pane
29
+ npx storymode-cli play --no-ai # disable AI personality
30
+ npx storymode-cli play --classic # full-screen animation viewer
29
31
  ```
30
32
 
31
33
  **Keyboard:** `a` add API key (enables AI personality), `q` quit.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "storymode-cli",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "description": "Play AI-animated pixel art characters in your terminal",
5
5
  "type": "module",
6
6
  "bin": {
package/src/api.mjs CHANGED
@@ -49,3 +49,26 @@ export async function fetchCharacter(galleryId) {
49
49
  const { buffer } = await request(`${BASE}/gallery/${galleryId}/character`);
50
50
  return JSON.parse(buffer.toString('utf-8'));
51
51
  }
52
+
53
+ /**
54
+ * Fetch raw PNG frames (base64) for a gallery animation.
55
+ * Returns { fps, frame_count, frames: [Buffer, ...] }
56
+ */
57
+ export async function fetchPngFrames(galleryId, { animId } = {}) {
58
+ let url = `${BASE}/gallery/${galleryId}/png-frames`;
59
+ if (animId) url += `?anim_id=${animId}`;
60
+ const { buffer, contentType } = await request(url);
61
+ let json;
62
+ if (contentType.includes('gzip') || buffer[0] === 0x1f) {
63
+ json = gunzipSync(buffer).toString('utf-8');
64
+ } else {
65
+ json = buffer.toString('utf-8');
66
+ }
67
+ const data = JSON.parse(json);
68
+ // Decode base64 frames to Buffers
69
+ return {
70
+ fps: data.fps,
71
+ frame_count: data.frame_count,
72
+ frames: data.frames.map(b64 => Buffer.from(b64, 'base64')),
73
+ };
74
+ }
package/src/cli.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { execSync } from 'node:child_process';
2
2
  import { createInterface } from 'node:readline';
3
- import { fetchFrames, fetchCharacter } from './api.mjs';
4
- import { playAnimation, playCompanion, playReactiveCompanion, showFrame } from './player.mjs';
3
+ import { fetchFrames, fetchCharacter, fetchPngFrames } from './api.mjs';
4
+ import { playAnimation, playCompanion, playReactiveCompanion, showFrame, playHdAnimation, detectGraphicsProtocol } from './player.mjs';
5
5
  import { browse } from './browse.mjs';
6
6
  import { startMcpServer } from './mcp.mjs';
7
7
  import { loadAnimations, clearCache } from './cache.mjs';
@@ -78,14 +78,15 @@ const HELP = `
78
78
  storymode-cli — AI-animated pixel art companions for your terminal
79
79
 
80
80
  Usage:
81
- storymode play <id> Run a reactive companion alongside Claude Code
82
- storymode browse Browse the gallery and pick a character
81
+ storymode play Run a reactive companion alongside Claude Code
82
+ storymode browse Browse the gallery and switch characters
83
83
 
84
84
  Options:
85
85
  --compact Smaller companion pane (default: full)
86
86
  --tiny Tiny companion pane
87
87
  --no-ai Disable AI personality (preset speech only)
88
88
  --model=ID Anthropic model for AI personality
89
+ --hd Pixel-perfect rendering (iTerm2/Kitty/Ghostty)
89
90
  --classic Full-screen animation viewer (non-reactive)
90
91
  --detach Return control immediately
91
92
 
@@ -110,19 +111,17 @@ function parseFlags(args) {
110
111
  return { flags, positional };
111
112
  }
112
113
 
114
+ const DEFAULT_CHARACTER = 3; // Zephyr
115
+
113
116
  export async function run(args) {
114
117
  const cmd = args[0];
115
118
  const { flags, positional } = parseFlags(args.slice(1));
116
- const id = positional[0];
119
+ const id = positional[0] || (cmd === 'play' || cmd === 'companion' ? DEFAULT_CHARACTER : undefined);
117
120
 
118
121
  switch (cmd) {
119
122
  // --- Main command: reactive companion ---
120
123
  case 'play':
121
124
  case 'companion': {
122
- if (!id) {
123
- console.error('Usage: storymode play <id>');
124
- process.exit(1);
125
- }
126
125
 
127
126
  // --classic: old full-screen animation viewer
128
127
  if (flags.classic) {
@@ -139,6 +138,26 @@ export async function run(args) {
139
138
  break;
140
139
  }
141
140
 
141
+ // --hd: pixel-perfect rendering via graphics protocol (iTerm2/Kitty/Ghostty)
142
+ if (flags.hd) {
143
+ const protocol = detectGraphicsProtocol();
144
+ if (!protocol) {
145
+ console.error(' --hd requires a terminal with graphics protocol support');
146
+ console.error(' (iTerm2, Kitty, Ghostty, or WezTerm)');
147
+ process.exit(1);
148
+ }
149
+ try {
150
+ process.stderr.write(' Loading HD frames...\n');
151
+ const pngData = await fetchPngFrames(id);
152
+ process.stderr.write(` ${pngData.frame_count} frames @ ${pngData.fps}fps (${protocol} protocol)\n`);
153
+ await playHdAnimation(pngData.frames, { fps: pngData.fps });
154
+ } catch (err) {
155
+ console.error(`Error: ${err.message}`);
156
+ process.exit(1);
157
+ }
158
+ break;
159
+ }
160
+
142
161
  // Determine size: default full, --compact, --tiny, or explicit --size=
143
162
  const size = flags.tiny ? 'tiny' : flags.compact ? 'compact' : (flags.size || 'full');
144
163
  const detach = !!flags.detach;
package/src/player.mjs CHANGED
@@ -661,3 +661,110 @@ export function showFrame(framesData, info) {
661
661
  }
662
662
  console.log('');
663
663
  }
664
+
665
+ // --- Graphics Protocol Rendering (HD mode) ---
666
+
667
+ const CHUNK_SIZE = 4096;
668
+
669
+ /** Detect terminal graphics protocol support */
670
+ export function detectGraphicsProtocol() {
671
+ const termProgram = process.env.TERM_PROGRAM || '';
672
+ const term = process.env.TERM || '';
673
+ if (process.env.KITTY_WINDOW_ID) return 'kitty';
674
+ if (term === 'xterm-ghostty' || process.env.GHOSTTY_RESOURCES_DIR) return 'kitty';
675
+ if (termProgram === 'WezTerm') return 'kitty';
676
+ if (termProgram === 'iTerm.app') return 'iterm2';
677
+ return null;
678
+ }
679
+
680
+ /** Display PNG via iTerm2 inline images protocol */
681
+ function writeIterm2(pngBuf, cols, rows) {
682
+ const b64 = pngBuf.toString('base64');
683
+ let params = `inline=1;size=${pngBuf.length}`;
684
+ if (cols) params += `;width=${cols}`;
685
+ if (rows) params += `;height=${rows}`;
686
+ params += `;preserveAspectRatio=1`;
687
+ stdout.write(`\x1b]1337;File=${params}:${b64}\x07`);
688
+ }
689
+
690
+ /** Display PNG via Kitty graphics protocol */
691
+ function writeKitty(pngBuf, cols, rows) {
692
+ const b64 = pngBuf.toString('base64');
693
+ if (b64.length <= CHUNK_SIZE) {
694
+ let params = `a=T,f=100`;
695
+ if (cols) params += `,c=${cols}`;
696
+ if (rows) params += `,r=${rows}`;
697
+ stdout.write(`\x1b_G${params};${b64}\x1b\\`);
698
+ return;
699
+ }
700
+ for (let i = 0; i < b64.length; i += CHUNK_SIZE) {
701
+ const chunk = b64.slice(i, i + CHUNK_SIZE);
702
+ const isFirst = i === 0;
703
+ const isLast = i + CHUNK_SIZE >= b64.length;
704
+ let params = `m=${isLast ? 0 : 1}`;
705
+ if (isFirst) {
706
+ params = `a=T,f=100`;
707
+ if (cols) params += `,c=${cols}`;
708
+ if (rows) params += `,r=${rows}`;
709
+ params += `,m=${isLast ? 0 : 1}`;
710
+ }
711
+ stdout.write(`\x1b_G${params};${chunk}\x1b\\`);
712
+ }
713
+ }
714
+
715
+ /**
716
+ * Play PNG frames using native graphics protocol.
717
+ * @param {Buffer[]} frames - Array of PNG buffers
718
+ * @param {object} opts - { fps, cols, rows }
719
+ */
720
+ export function playHdAnimation(frames, opts = {}) {
721
+ const { fps = 16, cols, rows } = opts;
722
+ const interval = 1000 / fps;
723
+ const protocol = detectGraphicsProtocol();
724
+
725
+ if (!protocol) {
726
+ console.error(' No graphics protocol supported. Use without --hd.');
727
+ process.exit(1);
728
+ }
729
+
730
+ let frameIdx = 0;
731
+ let quit = false;
732
+
733
+ // Alt screen, hide cursor, clear
734
+ stdout.write('\x1b[?1049h\x1b[?25l\x1b[2J');
735
+
736
+ function drawFrame() {
737
+ stdout.write('\x1b[H');
738
+ if (protocol === 'kitty') {
739
+ stdout.write(`\x1b_Ga=d,d=a\x1b\\`);
740
+ writeKitty(frames[frameIdx], cols, rows);
741
+ } else {
742
+ writeIterm2(frames[frameIdx], cols, rows);
743
+ }
744
+ }
745
+
746
+ if (stdin.isTTY) {
747
+ stdin.setRawMode(true);
748
+ stdin.resume();
749
+ stdin.on('data', (data) => {
750
+ const key = data.toString();
751
+ if (key === 'q' || key === '\x03') quit = true;
752
+ });
753
+ }
754
+
755
+ return new Promise((resolve) => {
756
+ function tick() {
757
+ if (quit) {
758
+ stdout.write('\x1b[?1049l\x1b[?25h');
759
+ if (stdin.isTTY) stdin.setRawMode(false);
760
+ stdin.pause();
761
+ resolve();
762
+ return;
763
+ }
764
+ drawFrame();
765
+ frameIdx = (frameIdx + 1) % frames.length;
766
+ setTimeout(tick, interval);
767
+ }
768
+ tick();
769
+ });
770
+ }