shmakk 1.2.0 → 1.2.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.
@@ -0,0 +1,184 @@
1
+ ---
2
+ name: video-voice
3
+ description: Generate per-segment voice-over audio using the tts_generate tool. Takes a storyboard JSON from the script agent and produces a WAV audio file for each segment. Part of the video production pipeline.
4
+ category: media
5
+ ---
6
+
7
+ # Video Voice-Over
8
+
9
+ Generate spoken audio for each segment of a video storyboard. This agent receives the script agent's JSON output and produces individual WAV files — one per segment — that the compositor will synchronize with visuals.
10
+
11
+ ## When to use
12
+
13
+ - You receive a storyboard JSON with `segments` containing `narration` fields
14
+ - You are the voice agent in the video production pipeline, running in parallel with the visual agent
15
+ - The user explicitly asks to generate voice-over audio for a video script
16
+
17
+ ## When not to use
18
+
19
+ - The user wants a single TTS clip outside the video pipeline (use `tts_generate` directly)
20
+ - There is no storyboard — wait for the script agent to finish first
21
+ - All narration fields are empty strings (music-only video — skip voice generation)
22
+
23
+ ## Input format
24
+
25
+ You receive the script agent's handoff — a JSON object with a `segments` array:
26
+
27
+ ```json
28
+ {
29
+ "segments": [
30
+ {
31
+ "index": 0,
32
+ "durationSec": 3.5,
33
+ "startSec": 0.0,
34
+ "narration": "Your best ideas don't wait for the right moment.",
35
+ "visualDesc": "...",
36
+ "transition": null
37
+ }
38
+ ]
39
+ }
40
+ ```
41
+
42
+ ## Tool: `tts_generate`
43
+
44
+ The `tts_generate` tool wraps the local Kokoro TTS engine (`src/services/tts.js`). It takes text and produces a WAV file.
45
+
46
+ ### Parameters
47
+
48
+ | Parameter | Type | Required | Default | Description |
49
+ |-----------|------|----------|---------|-------------|
50
+ | `text` | string | Yes | — | The text to synthesize. Must be non-empty. |
51
+ | `voice` | string | No | `"af_heart"` | Voice ID. See available voices below. |
52
+ | `speed` | number | No | `1.5` | Speech rate multiplier. 1.0 = normal, 1.5 = slightly faster (good for video). Range: 0.5–3.0. |
53
+ | `outputPath` | string | No | auto-generated temp path | Where to save the WAV file. Always provide an explicit path in `$SHMAKK_OUTPUT_DIR/voice/` so the compositor can find the files. |
54
+
55
+ ### Returns
56
+
57
+ ```json
58
+ {
59
+ "audioPath": "/path/to/output.wav",
60
+ "voice": "af_heart",
61
+ "durationSec": 3.4
62
+ }
63
+ ```
64
+
65
+ ### Available voices
66
+
67
+ Kokoro provides multiple voices. Run `tts.listVoices()` to get the full list. Common voices:
68
+
69
+ | Voice ID | Language | Gender | Character |
70
+ |----------|----------|--------|-----------|
71
+ | `af_heart` | en-us | female | Warm, natural, good default for narration |
72
+ | `af_bella` | en-us | female | Energetic, younger sounding |
73
+ | `af_nicole` | en-us | female | Calm, professional |
74
+ | `af_sarah` | en-us | female | Bright, articulate |
75
+ | `af_sky` | en-us | female | Soft, gentle |
76
+ | `am_adam` | en-us | male | Deep, authoritative |
77
+ | `am_michael` | en-us | male | Neutral, clear |
78
+ | `am_eric` | en-us | male | Friendly, casual |
79
+ | `am_jesse` | en-us | male | Relaxed, conversational |
80
+
81
+ ### Voice selection strategy
82
+
83
+ - **Single narrator:** Pick one voice for all segments and use it consistently. `af_heart` (female) or `am_michael` (male) are solid defaults unless the user specifies a preference.
84
+ - **Multiple speakers:** If the storyboard narration fields contain speaker labels like `[Interviewer]: ...` and `[Speaker]: ...`, assign different voices to each role. Extract the speaker label, strip it from the text sent to TTS, and apply the assigned voice.
85
+ - **User preference:** If the user specifies a voice (e.g., "use a British female voice" or "deep male voice"), map that to the closest available Kokoro voice. If no match exists, pick the closest and note the choice in the output.
86
+
87
+ ## Workflow
88
+
89
+ ### Step 1: Receive the storyboard
90
+
91
+ Extract the `segments` array from the script agent's handoff. Validate that it is an array with at least one segment and that segments have non-empty `narration` fields (skip segments where narration is empty).
92
+
93
+ ### Step 2: Choose voice(s)
94
+
95
+ - If the storyboard uses speaker labels, identify all unique speakers
96
+ - Assign a distinct voice to each speaker role
97
+ - If no speaker labels, pick a single voice based on:
98
+ 1. User's explicit request (if any)
99
+ 2. Content tone: energetic → `af_bella`, professional → `af_nicole`, warm → `af_heart`, authoritative → `am_adam`
100
+ 3. Default: `af_heart`
101
+
102
+ ### Step 3: Create output directory
103
+
104
+ Use `make_dir` to create the output directory. The convention is:
105
+
106
+ ```
107
+ output/voice/
108
+ ```
109
+
110
+ All audio files go here so the compositor can reference them by path.
111
+
112
+ ### Step 4: Generate audio per segment
113
+
114
+ For each segment with non-empty narration:
115
+
116
+ 1. **Extract text:** If narration contains a speaker label like `"[Speaker]: text here"`, strip the label and only pass the text after the colon to TTS.
117
+ 2. **Call `tts_generate`:** Pass the text, voice, and explicit output path.
118
+ 3. **Name files predictably:** Use the pattern `segment-{index}.wav` (e.g., `segment-0.wav`, `segment-1.wav`). This makes it trivial for the compositor to match audio to segments.
119
+
120
+ For segments with empty narration, skip generation and mark the segment with `audioPath: null`.
121
+
122
+ ### Step 5: Collect results
123
+
124
+ After all TTS calls complete, assemble the output payload:
125
+
126
+ ```json
127
+ {
128
+ "voice": "af_heart",
129
+ "speed": 1.5,
130
+ "segments": [
131
+ {
132
+ "index": 0,
133
+ "audioPath": "output/voice/segment-0.wav",
134
+ "durationSec": 3.4,
135
+ "voice": "af_heart"
136
+ },
137
+ {
138
+ "index": 1,
139
+ "audioPath": "output/voice/segment-1.wav",
140
+ "durationSec": 5.1,
141
+ "voice": "af_heart"
142
+ }
143
+ ]
144
+ }
145
+ ```
146
+
147
+ ### Step 6: Hand off
148
+
149
+ Return this payload. The compositor will merge it with the visual agent's output to assemble the final video. Include the `durationSec` for each segment (from `tts_generate` return value) — the compositor uses this to verify timing alignment.
150
+
151
+ ## Budget awareness
152
+
153
+ `tts_generate` costs 1 budget point per call. However, TTS runs locally on Kokoro (no API cost). Be mindful of:
154
+ - Each non-empty narration segment = 1 `tts_generate` call
155
+ - A 12-segment video with all narration = 12 budget points
156
+ - If budget is tight, consider whether segments with very short narration (< 10 words) can be merged with adjacent segments (coordinate with script agent — but if you already have the storyboard, proceed as-is; script changes are the script agent's responsibility)
157
+
158
+ ## Edge cases
159
+
160
+ - **Empty narration (music-only segment):** Skip `tts_generate`. Set `audioPath: null` and `durationSec: null` in the output for that segment. The compositor will use the visual duration for timing.
161
+ - **Very long narration (> 75 words):** Kokoro handles it fine, but video pacing may suffer. Flag in a note but generate the audio anyway.
162
+ - **Speaker label with no colon:** Treat the entire string as narration text. If the label pattern is ambiguous, generate as-is.
163
+ - **TTS generation fails:** If a single segment fails, retry once with `speed: 1.0` (some voices handle slower speeds more reliably). If it still fails, log the error and set `audioPath: null` for that segment — the compositor can still assemble the video with silence for that segment.
164
+ - **Voice not found:** Run `tts.listVoices()` to get the available voice list. Pick the closest match by gender/language. If the user specified a voice that does not exist, explain and pick a fallback.
165
+
166
+ ## Example
167
+
168
+ ```bash
169
+ # After receiving storyboard, create output directory and generate audio:
170
+ make_dir output/voice/
171
+
172
+ # For each segment with narration:
173
+ tts_generate --text "Your best ideas don't wait for the right moment." \
174
+ --voice af_heart \
175
+ --speed 1.5 \
176
+ --outputPath output/voice/segment-0.wav
177
+
178
+ tts_generate --text "They arrive in the shower, on a walk, or right before you fall asleep." \
179
+ --voice af_heart \
180
+ --speed 1.5 \
181
+ --outputPath output/voice/segment-1.wav
182
+ ```
183
+
184
+ Note: The actual `tts_generate` tool is invoked via the LLM function call interface, not shell commands. The examples above illustrate the parameter values, not the invocation syntax.
@@ -0,0 +1,320 @@
1
+ // Agent overview — live tracking registry for multi-agent team execution.
2
+ //
3
+ // Maintains an in-memory registry of all agents (active and completed) during
4
+ // a team run. Provides query methods for the overview self-commands so users
5
+ // can see which agents are working, which skills they use, and drill into
6
+ // specific agents for detailed output.
7
+ //
8
+ // Architecture:
9
+ // team.js → agentOverview.register(id, meta) when an agent starts
10
+ // team.js → agentOverview.update(id, patch) when an agent finishes
11
+ // self-commands → agentOverview.getAll() for overview display
12
+ // self-commands → agentOverview.get(id) for detailed drill-down
13
+
14
+ const MAX_HISTORY = 50; // keep at most N completed entries after reset
15
+
16
+ // In-memory state — one registry per process lifetime.
17
+ // Keys: agent id (string). Values: entry object.
18
+ const registry = new Map();
19
+
20
+ // Stable order of registration (for overview listing).
21
+ const order = [];
22
+
23
+ let teamRunActive = false;
24
+ let teamRunId = null;
25
+
26
+ // ── Public API ────────────────────────────────────────────────────────────────
27
+
28
+ function startTeamRun(id) {
29
+ teamRunActive = true;
30
+ teamRunId = id || `team-${Date.now()}`;
31
+ }
32
+
33
+ function endTeamRun() {
34
+ teamRunActive = false;
35
+ teamRunId = null;
36
+ }
37
+
38
+ function isTeamRunActive() {
39
+ return teamRunActive;
40
+ }
41
+
42
+ function register(id, meta) {
43
+ if (!id) id = `agent-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
44
+
45
+ const entry = {
46
+ id,
47
+ role: meta.role || 'unknown',
48
+ skill: meta.skill || null,
49
+ skillSource: meta.skillSource || null,
50
+ task: meta.task || '',
51
+ fileScope: meta.fileScope || null,
52
+ topology: meta.topology || 'unknown',
53
+ status: meta.status || 'pending', // pending | running | done | error
54
+ startTime: meta.startTime || Date.now(),
55
+ endTime: null,
56
+ toolCount: 0,
57
+ error: null,
58
+ output: '', // truncated on store; full accessed via separate buffer
59
+ outputPreview: '', // first 2000 chars for quick display
60
+ };
61
+
62
+ registry.set(id, entry);
63
+ order.push(id);
64
+ return id;
65
+ }
66
+
67
+ function update(id, patch) {
68
+ const entry = registry.get(id);
69
+ if (!entry) return false;
70
+
71
+ if (patch.status) entry.status = patch.status;
72
+ if (patch.endTime) entry.endTime = patch.endTime;
73
+ if (patch.toolCount !== undefined) entry.toolCount = patch.toolCount;
74
+ if (patch.error !== undefined) entry.error = patch.error;
75
+ if (patch.skill !== undefined) entry.skill = patch.skill;
76
+ if (patch.skillSource !== undefined) entry.skillSource = patch.skillSource;
77
+
78
+ if (patch.output !== undefined) {
79
+ const stripped = stripAnsi(String(patch.output));
80
+ entry.outputPreview = stripped.slice(0, 2000);
81
+ entry.output = stripped.slice(0, 8000); // keep reasonable cap
82
+ }
83
+
84
+ return true;
85
+ }
86
+
87
+ // Mark an agent as running (transitions from pending → running).
88
+ function markRunning(id) {
89
+ return update(id, { status: 'running', startTime: Date.now() });
90
+ }
91
+
92
+ // Mark an agent as completed with result data.
93
+ function markDone(id, { toolCount, output, skill, skillSource } = {}) {
94
+ return update(id, {
95
+ status: 'done',
96
+ endTime: Date.now(),
97
+ toolCount: toolCount || 0,
98
+ output: output || '',
99
+ skill: skill || undefined,
100
+ skillSource: skillSource || undefined,
101
+ });
102
+ }
103
+
104
+ // Mark an agent as errored.
105
+ function markError(id, error) {
106
+ return update(id, { status: 'error', endTime: Date.now(), error: String(error || '') });
107
+ }
108
+
109
+ // ── Query API ─────────────────────────────────────────────────────────────────
110
+
111
+ function get(id) {
112
+ const entry = registry.get(id);
113
+ if (!entry) return null;
114
+ return { ...entry }; // defensive copy
115
+ }
116
+
117
+ function getAll() {
118
+ // Return entries in registration order, newest last.
119
+ return order.map(id => {
120
+ const e = registry.get(id);
121
+ return e ? { ...e } : null;
122
+ }).filter(Boolean);
123
+ }
124
+
125
+ function getActive() {
126
+ return getAll().filter(e => e.status === 'running' || e.status === 'pending');
127
+ }
128
+
129
+ function getCompleted() {
130
+ return getAll().filter(e => e.status === 'done' || e.status === 'error');
131
+ }
132
+
133
+ // Find by role name (case-insensitive partial match).
134
+ function findByRole(role) {
135
+ const lower = String(role).toLowerCase();
136
+ return getAll().filter(e => e.role.toLowerCase().includes(lower));
137
+ }
138
+
139
+ // Find by skill name.
140
+ function findBySkill(skill) {
141
+ const lower = String(skill).toLowerCase();
142
+ return getAll().filter(e => e.skill && e.skill.toLowerCase().includes(lower));
143
+ }
144
+
145
+ // ── Reset ─────────────────────────────────────────────────────────────────────
146
+
147
+ // Called at the end of a team run to clear state for the next run.
148
+ // Keeps a small history window for post-run inspection.
149
+ function reset() {
150
+ const completed = getCompleted();
151
+ // Trim to MAX_HISTORY, keeping most recent.
152
+ const toKeep = completed.slice(-MAX_HISTORY);
153
+
154
+ registry.clear();
155
+ order.length = 0;
156
+
157
+ for (const e of toKeep) {
158
+ registry.set(e.id, e);
159
+ order.push(e.id);
160
+ }
161
+
162
+ teamRunActive = false;
163
+ teamRunId = null;
164
+ }
165
+
166
+ // ── Formatting helpers ────────────────────────────────────────────────────────
167
+
168
+ function stripAnsi(s) {
169
+ return String(s || '').replace(/\x1b\[[0-9;]*m/g, '');
170
+ }
171
+
172
+ function formatDuration(ms) {
173
+ if (!ms || ms < 0) return '—';
174
+ const s = ms / 1000;
175
+ if (s < 1) return `${Math.round(ms)}ms`;
176
+ if (s < 60) return `${s.toFixed(1)}s`;
177
+ const m = Math.floor(s / 60);
178
+ const sec = Math.round(s % 60);
179
+ return `${m}m ${sec}s`;
180
+ }
181
+
182
+ function statusIcon(status) {
183
+ switch (status) {
184
+ case 'pending': return '\x1b[2m○\x1b[0m'; // dim circle
185
+ case 'running': return '\x1b[36m◉\x1b[0m'; // cyan filled circle
186
+ case 'done': return '\x1b[32m●\x1b[0m'; // green filled circle
187
+ case 'error': return '\x1b[31m●\x1b[0m'; // red filled circle
188
+ default: return '\x1b[2m?\x1b[0m';
189
+ }
190
+ }
191
+
192
+ // Build a compact overview table as an array of strings.
193
+ function formatOverview(agents) {
194
+ if (!agents || agents.length === 0) {
195
+ return ['\x1b[2mNo agents registered.\x1b[0m'];
196
+ }
197
+
198
+ const now = Date.now();
199
+ const lines = [];
200
+
201
+ // Header
202
+ const teamTag = teamRunId ? ` \x1b[2m(team: ${teamRunId.slice(-8)})\x1b[0m` : '';
203
+ lines.push(`\x1b[1mAgent Overview${teamTag}\x1b[0m`);
204
+ lines.push('');
205
+
206
+ // Column widths
207
+ const roleWidth = Math.max(8, ...agents.map(a => a.role.length));
208
+ const skillWidth = Math.max(5, ...agents.map(a => (a.skill || '—').length));
209
+ const taskWidth = Math.min(60, Math.max(4, ...agents.map(a => (a.task || '').length)));
210
+
211
+ for (const a of agents) {
212
+ const icon = statusIcon(a.status);
213
+ const role = a.role.padEnd(roleWidth);
214
+ const skill = (a.skill || '\x1b[2m—\x1b[0m').padEnd(skillWidth + (a.skill ? 0 : 9)); // +9 for ANSI codes
215
+ const task = (a.task || '').slice(0, taskWidth);
216
+ const elapsed = a.endTime
217
+ ? formatDuration(a.endTime - a.startTime)
218
+ : formatDuration(now - a.startTime);
219
+ const tools = a.toolCount > 0 ? `${a.toolCount} tools` : '';
220
+
221
+ lines.push(` ${icon} \x1b[36m${role}\x1b[0m \x1b[2m${skill.trim()}\x1b[0m ${task}`);
222
+ const infoParts = [];
223
+ if (elapsed) infoParts.push(elapsed);
224
+ if (tools) infoParts.push(tools);
225
+ if (a.error) infoParts.push(`\x1b[31m${a.error}\x1b[0m`);
226
+ lines.push(` \x1b[2m${' '.repeat(roleWidth)} ${infoParts.join(' · ')}\x1b[0m`);
227
+ }
228
+
229
+ // Summary line
230
+ const active = agents.filter(a => a.status === 'running' || a.status === 'pending').length;
231
+ const done = agents.filter(a => a.status === 'done').length;
232
+ const errors = agents.filter(a => a.status === 'error').length;
233
+ const summaryParts = [];
234
+ if (active) summaryParts.push(`\x1b[36m${active} active\x1b[0m`);
235
+ if (done) summaryParts.push(`\x1b[32m${done} done\x1b[0m`);
236
+ if (errors) summaryParts.push(`\x1b[31m${errors} errors\x1b[0m`);
237
+ lines.push('');
238
+ lines.push(`\x1b[2m${agents.length} total${summaryParts.length ? ' · ' + summaryParts.join(' · ') : ''}\x1b[0m`);
239
+
240
+ return lines;
241
+ }
242
+
243
+ // Build a detailed single-agent view.
244
+ function formatAgentDetail(agent) {
245
+ if (!agent) return ['\x1b[31mAgent not found.\x1b[0m'];
246
+
247
+ const now = Date.now();
248
+ const lines = [];
249
+
250
+ lines.push(`\x1b[1m${agent.role}\x1b[0m ${statusIcon(agent.status)} ${agent.status}`);
251
+ lines.push(`\x1b[2mid: ${agent.id}\x1b[0m`);
252
+ lines.push('');
253
+
254
+ if (agent.task) {
255
+ lines.push(`\x1b[1mTask\x1b[0m`);
256
+ lines.push(` ${agent.task}`);
257
+ lines.push('');
258
+ }
259
+
260
+ lines.push(`\x1b[1mDetails\x1b[0m`);
261
+ lines.push(` Role: ${agent.role}`);
262
+ lines.push(` Skill: ${agent.skill || '\x1b[2m(none — using roster hint)\x1b[0m'}`);
263
+ if (agent.skillSource) lines.push(` Source: \x1b[2m${agent.skillSource}\x1b[0m`);
264
+ lines.push(` Topology: ${agent.topology}`);
265
+ if (agent.fileScope) lines.push(` Scope: ${agent.fileScope}`);
266
+
267
+ const elapsed = agent.endTime
268
+ ? formatDuration(agent.endTime - agent.startTime)
269
+ : `\x1b[36m${formatDuration(now - agent.startTime)} (running)\x1b[0m`;
270
+ lines.push(` Duration: ${elapsed}`);
271
+ lines.push(` Tools: ${agent.toolCount}`);
272
+
273
+ if (agent.error) {
274
+ lines.push(` Error: \x1b[31m${agent.error}\x1b[0m`);
275
+ }
276
+ lines.push('');
277
+
278
+ if (agent.outputPreview) {
279
+ lines.push(`\x1b[1mOutput\x1b[0m (first 2000 chars)`);
280
+ lines.push('\x1b[2m──────────────────────────────────────────────────────\x1b[0m');
281
+ lines.push(agent.outputPreview);
282
+ lines.push('\x1b[2m──────────────────────────────────────────────────────\x1b[0m');
283
+ if (agent.output && agent.output.length >= 2000) {
284
+ lines.push(`\x1b[2m... output truncated (${agent.output.length} total)\x1b[0m`);
285
+ }
286
+ }
287
+
288
+ return lines;
289
+ }
290
+
291
+ // ── Exports ───────────────────────────────────────────────────────────────────
292
+
293
+ module.exports = {
294
+ // lifecycle
295
+ startTeamRun,
296
+ endTeamRun,
297
+ isTeamRunActive,
298
+ reset,
299
+
300
+ // mutation
301
+ register,
302
+ update,
303
+ markRunning,
304
+ markDone,
305
+ markError,
306
+
307
+ // query
308
+ get,
309
+ getAll,
310
+ getActive,
311
+ getCompleted,
312
+ findByRole,
313
+ findBySkill,
314
+
315
+ // formatting
316
+ formatOverview,
317
+ formatAgentDetail,
318
+ statusIcon,
319
+ formatDuration,
320
+ };
@@ -0,0 +1,53 @@
1
+ // Additional agent roster entries for media/video production roles.
2
+ //
3
+ // These extend the main AGENT_ROSTER in src/team.js with specialist roles
4
+ // for the video production pipeline: script writing and video compositing.
5
+ //
6
+ // The voice and visual roles are handled by the existing media-video-voice
7
+ // and media-imagegen skills respectively.
8
+ //
9
+ // Each entry maps to a skill file in the skills/ directory:
10
+ // script → skills/media-video-script.md
11
+ // compositor → skills/media-video-compose.md
12
+
13
+ const AGENT_ROSTER_EXTENSIONS = {
14
+ script: {
15
+ profile: 'deep',
16
+ hint: `Specialist: Video Script Writer
17
+ Focus: turning user prompts into structured timed storyboards for video production.
18
+ Guidelines:
19
+ - Output valid JSON: an array of segments, each with startTime, endTime, narration, visualDesc.
20
+ - startTime/endTime in seconds (floating-point). Total duration must match user request.
21
+ - narration: conversational text suitable for TTS. Keep each segment under 30 seconds of speech (~75 words max).
22
+ - visualDesc: detailed visual prompt for image generation. Describe scene, style, composition, color palette.
23
+ - Match the user's requested tone, pacing, and style. For explainer videos, prefer clear logical flow. For demos, prefer step-by-step walkthrough.
24
+ - If duration or segment count is unclear, ask before finalizing.`,
25
+ skill: 'media-video-script',
26
+ },
27
+
28
+ compositor: {
29
+ profile: 'builder',
30
+ hint: `Specialist: Video Compositor
31
+ Tools: video_compose (assemble clips/images/audio into a segment), video_concat (join rendered segments), video_probe (inspect metadata).
32
+ Focus: assembling audio, images, and transitions into a final video file.
33
+ Guidelines:
34
+ - Read the script agent's output first — it defines the timeline and assets per segment.
35
+ - For each segment: call video_compose with the image path, audio path, startTime, and endTime to render that segment.
36
+ - Use video_probe to verify audio duration and image dimensions before composing.
37
+ - After all segments are rendered, call video_concat to join them into the final output.
38
+ - Transitions: prefer crossfade (0.3–0.5s) between segments unless otherwise specified.
39
+ - Output format: H.264 video (libx264), AAC audio, .mp4 container. Match the first segment's resolution.
40
+ - If an asset is missing or has wrong duration, report the exact segment and path — do not silently skip.
41
+ - Verify the final output with video_probe: check total duration matches expected.`,
42
+ skill: 'media-video-compose',
43
+ },
44
+ };
45
+
46
+ // Role-to-skill mapping for these extensions. Used by src/team.js to look up
47
+ // skill files that provide the full agent instructions.
48
+ const ROLE_TO_SKILL_EXTENSIONS = {
49
+ script: 'media-video-script',
50
+ compositor: 'media-video-compose',
51
+ };
52
+
53
+ module.exports = { AGENT_ROSTER_EXTENSIONS, ROLE_TO_SKILL_EXTENSIONS };