storymode-cli 1.1.0 → 1.1.2
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/package.json +1 -1
- package/src/api.mjs +7 -5
- package/src/cli.mjs +16 -8
- package/src/mcp.mjs +76 -26
package/package.json
CHANGED
package/src/api.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import https from 'node:https';
|
|
|
2
2
|
import http from 'node:http';
|
|
3
3
|
import { gunzipSync } from 'node:zlib';
|
|
4
4
|
|
|
5
|
-
const BASE = 'https://
|
|
5
|
+
const BASE = process.env.STORYMODE_BASE_URL || 'https://pixterm-server.proudisland-84e92871.westus2.azurecontainerapps.io';
|
|
6
6
|
|
|
7
7
|
function request(url) {
|
|
8
8
|
return new Promise((resolve, reject) => {
|
|
@@ -28,11 +28,13 @@ export async function fetchGallery() {
|
|
|
28
28
|
return JSON.parse(buffer.toString('utf-8'));
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
export async function fetchFrames(galleryId, { size } = {}) {
|
|
31
|
+
export async function fetchFrames(galleryId, { size, mode, animId } = {}) {
|
|
32
32
|
let url = `${BASE}/gallery/${galleryId}/frames`;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
33
|
+
const params = [];
|
|
34
|
+
if (size && size !== 'full') params.push(`size=${size}`);
|
|
35
|
+
if (mode) params.push(`mode=${mode}`);
|
|
36
|
+
if (animId) params.push(`anim_id=${animId}`);
|
|
37
|
+
if (params.length) url += '?' + params.join('&');
|
|
36
38
|
const { buffer, contentType } = await request(url);
|
|
37
39
|
let json;
|
|
38
40
|
if (contentType.includes('gzip') || buffer[0] === 0x1f) {
|
package/src/cli.mjs
CHANGED
|
@@ -14,10 +14,11 @@ const HELP = `
|
|
|
14
14
|
storymode browse Browse the gallery interactively
|
|
15
15
|
storymode mcp Start MCP server (for Claude Code)
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
Options:
|
|
18
|
+
--sextant Use sextant rendering (2x3 sub-pixels, sharper but needs
|
|
19
|
+
a modern terminal). Works with play, companion, show.
|
|
20
|
+
--size=compact|tiny Size for companion mode (default: compact).
|
|
21
|
+
--detach Return control immediately (companion mode).
|
|
21
22
|
|
|
22
23
|
Controls (during playback):
|
|
23
24
|
space pause / resume
|
|
@@ -55,7 +56,9 @@ export async function run(args) {
|
|
|
55
56
|
}
|
|
56
57
|
try {
|
|
57
58
|
process.stderr.write(' Loading animation...\n');
|
|
58
|
-
const
|
|
59
|
+
const fetchOpts = {};
|
|
60
|
+
if (flags.sextant) { fetchOpts.size = 'compact'; fetchOpts.mode = 'sextant'; }
|
|
61
|
+
const framesData = await fetchFrames(id, fetchOpts);
|
|
59
62
|
await playAnimation(framesData);
|
|
60
63
|
} catch (err) {
|
|
61
64
|
console.error(`Error: ${err.message}`);
|
|
@@ -76,7 +79,9 @@ export async function run(args) {
|
|
|
76
79
|
// If we're already the companion pane (spawned by tmux split), play inline
|
|
77
80
|
if (process.env.STORYMODE_COMPANION === '1') {
|
|
78
81
|
try {
|
|
79
|
-
const
|
|
82
|
+
const fetchOpts = { size };
|
|
83
|
+
if (flags.sextant) fetchOpts.mode = 'sextant';
|
|
84
|
+
const framesData = await fetchFrames(id, fetchOpts);
|
|
80
85
|
await playCompanion(framesData);
|
|
81
86
|
} catch (err) {
|
|
82
87
|
console.error(`Error: ${err.message}`);
|
|
@@ -87,7 +92,8 @@ export async function run(args) {
|
|
|
87
92
|
|
|
88
93
|
// Otherwise, open a tmux side pane
|
|
89
94
|
const inTmux = !!process.env.TMUX;
|
|
90
|
-
const
|
|
95
|
+
const sextantFlag = flags.sextant ? ' --sextant' : '';
|
|
96
|
+
const companionCmd = `STORYMODE_COMPANION=1 npx storymode-cli companion ${id} --size=${size}${sextantFlag}`;
|
|
91
97
|
|
|
92
98
|
try {
|
|
93
99
|
if (inTmux) {
|
|
@@ -132,8 +138,10 @@ export async function run(args) {
|
|
|
132
138
|
}
|
|
133
139
|
try {
|
|
134
140
|
process.stderr.write(' Loading...\n');
|
|
141
|
+
const fetchOpts = {};
|
|
142
|
+
if (flags.sextant) { fetchOpts.size = 'compact'; fetchOpts.mode = 'sextant'; }
|
|
135
143
|
const [framesData, character] = await Promise.all([
|
|
136
|
-
fetchFrames(id),
|
|
144
|
+
fetchFrames(id, fetchOpts),
|
|
137
145
|
fetchCharacter(id).catch(() => null),
|
|
138
146
|
]);
|
|
139
147
|
const info = character
|
package/src/mcp.mjs
CHANGED
|
@@ -4,72 +4,122 @@ import { fetchGallery, fetchFrames, fetchCharacter } from './api.mjs';
|
|
|
4
4
|
const TOOLS = [
|
|
5
5
|
{
|
|
6
6
|
name: 'list_characters',
|
|
7
|
-
description: 'List all characters in the Storymode gallery',
|
|
7
|
+
description: 'List all characters in the Storymode gallery with their available animations',
|
|
8
8
|
inputSchema: { type: 'object', properties: {} },
|
|
9
9
|
},
|
|
10
10
|
{
|
|
11
11
|
name: 'show_character',
|
|
12
|
-
description: 'Show character info and
|
|
12
|
+
description: 'Show character info, portrait, and list of available animations. Use this to see what a character looks like and what animations they have.',
|
|
13
13
|
inputSchema: {
|
|
14
14
|
type: 'object',
|
|
15
|
-
properties: {
|
|
15
|
+
properties: {
|
|
16
|
+
gallery_id: { type: 'string', description: 'Gallery item ID' },
|
|
17
|
+
animation: { type: 'string', description: 'Animation name to show a frame from (e.g. "waving hello", "casting lightning spell"). If omitted, shows the default animation.' },
|
|
18
|
+
},
|
|
16
19
|
required: ['gallery_id'],
|
|
17
20
|
},
|
|
18
21
|
},
|
|
19
22
|
{
|
|
20
23
|
name: 'play_animation',
|
|
21
|
-
description: '
|
|
24
|
+
description: 'Show a frame from a specific animation and provide the CLI command to play it. Choose the animation contextually — "waving hello" for greetings, "thinking" for processing, "casting lightning spell" for impressive actions, "idle breathing" for ambient.',
|
|
22
25
|
inputSchema: {
|
|
23
26
|
type: 'object',
|
|
24
|
-
properties: {
|
|
27
|
+
properties: {
|
|
28
|
+
gallery_id: { type: 'string', description: 'Gallery item ID' },
|
|
29
|
+
animation: { type: 'string', description: 'Animation name (e.g. "waving hello", "idle breathing"). Use list_characters or show_character to see available animations.' },
|
|
30
|
+
},
|
|
25
31
|
required: ['gallery_id'],
|
|
26
32
|
},
|
|
27
33
|
},
|
|
28
34
|
];
|
|
29
35
|
|
|
36
|
+
function findAnimation(animations, query) {
|
|
37
|
+
if (!query || !animations?.length) return null;
|
|
38
|
+
const q = query.toLowerCase();
|
|
39
|
+
// Exact match first
|
|
40
|
+
const exact = animations.find(a => (a.name || a.prompt || '').toLowerCase() === q);
|
|
41
|
+
if (exact) return exact;
|
|
42
|
+
// Partial match
|
|
43
|
+
const partial = animations.find(a => (a.name || a.prompt || '').toLowerCase().includes(q));
|
|
44
|
+
return partial || null;
|
|
45
|
+
}
|
|
46
|
+
|
|
30
47
|
async function handleToolCall(name, args) {
|
|
31
48
|
switch (name) {
|
|
32
49
|
case 'list_characters': {
|
|
33
50
|
const items = await fetchGallery();
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
)
|
|
37
|
-
|
|
51
|
+
if (!items.length) return 'Gallery is empty.';
|
|
52
|
+
const parts = [];
|
|
53
|
+
for (const item of items) {
|
|
54
|
+
const char = await fetchCharacter(item.id).catch(() => null);
|
|
55
|
+
let line = `${item.name || 'untitled'} (id: ${item.id})`;
|
|
56
|
+
if (char?.backstory) line += `\n ${char.backstory}`;
|
|
57
|
+
if (char?.animations?.length) {
|
|
58
|
+
const names = char.animations.map(a => a.name || a.prompt).join(', ');
|
|
59
|
+
line += `\n Animations: ${names}`;
|
|
60
|
+
}
|
|
61
|
+
parts.push(line);
|
|
62
|
+
}
|
|
63
|
+
return parts.join('\n\n');
|
|
38
64
|
}
|
|
39
65
|
case 'show_character': {
|
|
40
66
|
const id = args.gallery_id;
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
67
|
+
const character = await fetchCharacter(id).catch(() => null);
|
|
68
|
+
|
|
69
|
+
// Determine which animation to show
|
|
70
|
+
const fetchOpts = { size: 'tiny' };
|
|
71
|
+
if (args.animation && character?.animations?.length) {
|
|
72
|
+
const match = findAnimation(character.animations, args.animation);
|
|
73
|
+
if (match) fetchOpts.animId = match.id;
|
|
74
|
+
}
|
|
75
|
+
const framesData = await fetchFrames(id, fetchOpts);
|
|
76
|
+
|
|
45
77
|
let text = '';
|
|
46
78
|
if (character) {
|
|
47
79
|
text += `Name: ${character.name || 'untitled'}\n`;
|
|
48
80
|
if (character.backstory) text += `Backstory: ${character.backstory}\n`;
|
|
49
81
|
if (character.animations?.length) {
|
|
50
|
-
text +=
|
|
82
|
+
text += `\nAnimations (${character.animations.length}):\n`;
|
|
83
|
+
for (const a of character.animations) {
|
|
84
|
+
text += ` • ${a.name || a.prompt}\n`;
|
|
85
|
+
}
|
|
51
86
|
}
|
|
52
87
|
text += '\n';
|
|
53
88
|
}
|
|
54
|
-
// First frame as ANSI
|
|
55
89
|
if (framesData.frames?.length > 0) {
|
|
56
|
-
|
|
90
|
+
const label = args.animation || 'portrait';
|
|
91
|
+
text += `${label} (ANSI):\n`;
|
|
57
92
|
text += framesData.frames[0].join('\n');
|
|
58
93
|
}
|
|
59
94
|
return text;
|
|
60
95
|
}
|
|
61
96
|
case 'play_animation': {
|
|
62
97
|
const id = args.gallery_id;
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
98
|
+
|
|
99
|
+
// Look up animation ID by name
|
|
100
|
+
const fetchOpts = { size: 'tiny' };
|
|
101
|
+
let animLabel = 'default';
|
|
102
|
+
if (args.animation) {
|
|
103
|
+
const character = await fetchCharacter(id).catch(() => null);
|
|
104
|
+
if (character?.animations?.length) {
|
|
105
|
+
const match = findAnimation(character.animations, args.animation);
|
|
106
|
+
if (match) {
|
|
107
|
+
fetchOpts.animId = match.id;
|
|
108
|
+
animLabel = match.name || match.prompt;
|
|
109
|
+
} else {
|
|
110
|
+
const names = character.animations.map(a => a.name || a.prompt).join(', ');
|
|
111
|
+
return `Animation "${args.animation}" not found. Available: ${names}`;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
72
114
|
}
|
|
115
|
+
|
|
116
|
+
const framesData = await fetchFrames(id, fetchOpts);
|
|
117
|
+
if (!framesData.frames?.length) return 'No frames available.';
|
|
118
|
+
|
|
119
|
+
let text = framesData.frames[0].join('\n');
|
|
120
|
+
text += `\n\n"${animLabel}" — ${framesData.frames.length} frames @ ${framesData.fps || 16} fps`;
|
|
121
|
+
text += `\n\nTo see the full animation, run:\n npx storymode-cli play ${id}`;
|
|
122
|
+
text += `\n\nTo play as a companion sprite alongside your session:\n npx storymode-cli companion ${id}`;
|
|
73
123
|
return text;
|
|
74
124
|
}
|
|
75
125
|
default:
|
|
@@ -92,7 +142,7 @@ function handleMessage(msg) {
|
|
|
92
142
|
result: {
|
|
93
143
|
protocolVersion: '2024-11-05',
|
|
94
144
|
capabilities: { tools: {} },
|
|
95
|
-
serverInfo: { name: 'storymode', version: '1.
|
|
145
|
+
serverInfo: { name: 'storymode', version: '1.1.2' },
|
|
96
146
|
},
|
|
97
147
|
});
|
|
98
148
|
break;
|