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 +14 -12
- package/package.json +1 -1
- package/src/api.mjs +23 -0
- package/src/cli.mjs +28 -9
- package/src/player.mjs +107 -0
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
|
|
6
|
+
npx storymode-cli play
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
## Quick Start
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
|
|
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
|
-
|
|
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
|
|
25
|
-
npx storymode-cli play
|
|
26
|
-
npx storymode-cli play
|
|
27
|
-
npx storymode-cli play
|
|
28
|
-
npx storymode-cli play
|
|
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
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
|
|
82
|
-
storymode browse Browse the gallery and
|
|
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
|
+
}
|