storymode-cli 1.1.1 → 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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/src/api.mjs +2 -1
  3. package/src/mcp.mjs +76 -26
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "storymode-cli",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "Play AI-animated pixel art characters in your terminal",
5
5
  "type": "module",
6
6
  "bin": {
package/src/api.mjs CHANGED
@@ -28,11 +28,12 @@ export async function fetchGallery() {
28
28
  return JSON.parse(buffer.toString('utf-8'));
29
29
  }
30
30
 
31
- export async function fetchFrames(galleryId, { size, mode } = {}) {
31
+ export async function fetchFrames(galleryId, { size, mode, animId } = {}) {
32
32
  let url = `${BASE}/gallery/${galleryId}/frames`;
33
33
  const params = [];
34
34
  if (size && size !== 'full') params.push(`size=${size}`);
35
35
  if (mode) params.push(`mode=${mode}`);
36
+ if (animId) params.push(`anim_id=${animId}`);
36
37
  if (params.length) url += '?' + params.join('&');
37
38
  const { buffer, contentType } = await request(url);
38
39
  let json;
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 portrait (first frame as ANSI art)',
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: { gallery_id: { type: 'string', description: 'Gallery item ID' } },
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: 'Get animation data for a gallery character (returns first frame as ANSI art + metadata)',
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: { gallery_id: { type: 'string', description: 'Gallery item ID' } },
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
- const lines = items.map((item, i) =>
35
- `${i + 1}. ${item.name || 'untitled'}${item.prompt ? ' — ' + item.prompt : ''} (id: ${item.id})`
36
- );
37
- return lines.join('\n') || 'Gallery is empty.';
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 [character, framesData] = await Promise.all([
42
- fetchCharacter(id).catch(() => null),
43
- fetchFrames(id, { size: 'tiny' }),
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 += `Animations: ${character.animations.map(a => a.name || a.prompt).join(', ')}\n`;
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
- text += 'Portrait (ANSI):\n';
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
- const framesData = await fetchFrames(id, { size: 'tiny' });
64
- let text = '';
65
- if (framesData.frames?.length > 0) {
66
- text += framesData.frames[0].join('\n');
67
- text += `\n\n${framesData.frames.length} frames @ ${framesData.fps || 16} fps`;
68
- text += `\n\nTo see the full animation, run:\n npx storymode-cli play ${id}`;
69
- text += `\n\nTo play as a companion sprite alongside your session:\n npx storymode-cli companion ${id}`;
70
- } else {
71
- text = 'No frames available.';
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.0.0' },
145
+ serverInfo: { name: 'storymode', version: '1.1.2' },
96
146
  },
97
147
  });
98
148
  break;