diff --git a/seed/tasks/convert-to-mp3/TASK.md b/seed/tasks/convert-to-mp3/TASK.md index 798e2dff..8f6a008d 100644 --- a/seed/tasks/convert-to-mp3/TASK.md +++ b/seed/tasks/convert-to-mp3/TASK.md @@ -1,7 +1,7 @@ --- name: Convert To MP3 description: Convert audio files to MP3 320kbps, preserving metadata. -version: 2 +version: 3 author: pastilhas tags: - audio @@ -38,10 +38,13 @@ inputs: Convert audio files to MP3 320kbps, preserving metadata. +## Important + +- Do NOT explore, list, or inspect the target path before converting. The tool handles everything internally — file discovery, format detection, and error reporting. +- Do NOT use bash, ls, or any other tool. Only use `convert_audio_to_mp3`. +- Call the tool exactly once, then report the result. Nothing else. + ## Steps -1. Determine whether `file_path` points to a single audio file or a directory. -2. Call `convert_audio_to_mp3` with the appropriate mode: - - Single file: `mode="single"`, `path=file_path` - - Directory: `mode="batch"`, `path=file_path` -3. Report the result to the user. +1. If `file_path` has an audio extension (flac, wav, ogg, etc.), call `convert_audio_to_mp3(mode="single", path=file_path)`. Otherwise call `convert_audio_to_mp3(mode="batch", path=file_path)`. +2. Print the tool's output as the final report. Do not add extra commentary. diff --git a/seed/tools/convert-audio-to-mp3/TOOL.md b/seed/tools/convert-audio-to-mp3/TOOL.md index 1ecfb84a..63db7ee8 100644 --- a/seed/tools/convert-audio-to-mp3/TOOL.md +++ b/seed/tools/convert-audio-to-mp3/TOOL.md @@ -2,6 +2,7 @@ name: convert_audio_to_mp3 label: Convert Audio to MP3 description: Convert audio files to MP3 320kbps using ffmpeg, preserving metadata. Supports single file conversion with real-time percentage progress, and batch conversion of an entire artist directory with per-file progress. Use when the user wants to convert FLAC, WAV, OGG, or other audio formats to MP3. +version: 2 language: typescript inputs: mode: diff --git a/seed/tools/convert-audio-to-mp3/index.ts b/seed/tools/convert-audio-to-mp3/index.ts index d89e3c8c..456fdd9f 100644 --- a/seed/tools/convert-audio-to-mp3/index.ts +++ b/seed/tools/convert-audio-to-mp3/index.ts @@ -1,5 +1,6 @@ import { readdirSync, existsSync, unlinkSync, statSync } from 'node:fs'; import { join, extname, basename } from 'node:path'; +import { spawn, execFile } from 'node:child_process'; const AUDIO_EXTS = new Set([ 'flac', 'wav', 'ogg', 'wma', 'aac', 'm4a', 'opus', @@ -13,14 +14,17 @@ function update(onUpdate: OnUpdate | undefined, text: string): void { } async function getDuration(filePath: string): Promise { - const proc = Bun.spawn( - ['ffprobe', '-v', 'quiet', '-show_entries', 'format=duration', '-of', 'csv=p=0', filePath], - { stdout: 'pipe', stderr: 'pipe' }, - ); - const text = await new Response(proc.stdout).text(); - await proc.exited; - const n = parseFloat(text.trim()); - return isNaN(n) ? null : n; + return new Promise((resolve) => { + execFile( + 'ffprobe', + ['-v', 'quiet', '-show_entries', 'format=duration', '-of', 'csv=p=0', filePath], + (err, stdout) => { + if (err) return resolve(null); + const n = parseFloat(stdout.trim()); + resolve(isNaN(n) ? null : n); + }, + ); + }); } function scanAudioFiles(dir: string): string[] { @@ -57,60 +61,55 @@ async function convertFile( return { outputFile, success: true, skipped: true }; } - const proc = Bun.spawn( - [ - 'ffmpeg', + return new Promise((resolve) => { + const proc = spawn('ffmpeg', [ '-i', inputFile, - '-progress', 'pipe:1', // progress data → stdout + '-progress', 'pipe:1', '-nostats', - '-loglevel', 'error', // only errors → stderr + '-loglevel', 'error', '-codec:a', 'libmp3lame', '-b:a', '320k', outputFile, '-y', - ], - { stdin: 'ignore', stdout: 'pipe', stderr: 'pipe' }, - ); + ], { stdio: ['ignore', 'pipe', 'pipe'] }); - // Parse -progress output from stdout for real-time percentage - const reader = (proc.stdout as ReadableStream).getReader(); - const decoder = new TextDecoder(); - let buf = ''; - let lastPercent = -1; + let buf = ''; + let lastPercent = -1; + let stderrText = ''; - (async () => { - try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - buf += decoder.decode(value, { stream: true }); - const lines = buf.split('\n'); - buf = lines.pop() ?? ''; - for (const line of lines) { - const m = line.match(/^out_time_us=(\d+)$/); - if (m && duration) { - const pct = Math.min(100, Math.round((parseInt(m[1]!, 10) / 1_000_000 / duration) * 100)); - if (pct >= lastPercent + 5) { - lastPercent = pct; - update(onUpdate, `${label} ${pct}%`); - } + proc.stdout.on('data', (chunk: Buffer) => { + buf += chunk.toString(); + const lines = buf.split('\n'); + buf = lines.pop() ?? ''; + for (const line of lines) { + const m = line.match(/^out_time_us=(\d+)$/); + if (m && duration) { + const pct = Math.min(100, Math.round((parseInt(m[1]!, 10) / 1_000_000 / duration) * 100)); + if (pct >= lastPercent + 5) { + lastPercent = pct; + update(onUpdate, `${label} ${pct}%`); } } } - } catch { - // stream closed - } - })(); + }); - const stderrText = await new Response(proc.stderr).text(); - await proc.exited; + proc.stderr.on('data', (chunk: Buffer) => { + stderrText += chunk.toString(); + }); - if (proc.exitCode !== 0) { - if (existsSync(outputFile)) unlinkSync(outputFile); - return { outputFile, success: false, error: stderrText.trim() || 'ffmpeg failed' }; - } + proc.on('close', (code) => { + if (code !== 0) { + if (existsSync(outputFile)) unlinkSync(outputFile); + resolve({ outputFile, success: false, error: stderrText.trim() || 'ffmpeg failed' }); + } else { + resolve({ outputFile, success: true }); + } + }); - return { outputFile, success: true }; + proc.on('error', (err) => { + resolve({ outputFile, success: false, error: err.message }); + }); + }); } export async function execute( diff --git a/src/servers/sync-processes.ts b/src/servers/sync-processes.ts index 4d242566..33a468bb 100644 --- a/src/servers/sync-processes.ts +++ b/src/servers/sync-processes.ts @@ -1,6 +1,7 @@ -import { readdirSync, existsSync, mkdirSync, cpSync } from 'node:fs'; +import { readdirSync, readFileSync, existsSync, mkdirSync, cpSync, rmSync } from 'node:fs'; import { join } from 'node:path'; import { SEED_PATH, DATA_PATH } from './data-path'; +import { parseSeedVersion } from './sync-version'; const SEED_PROCESSES_DIR = join(SEED_PATH, 'processes'); const GLOBAL_PROCESSES_DIR = join(DATA_PATH, 'processes'); @@ -16,14 +17,18 @@ export function syncSeedProcesses(): void { if (!entry.isDirectory()) continue; const seedProcessDir = join(SEED_PROCESSES_DIR, entry.name); - const processFile = join(seedProcessDir, 'PROCESS.md'); - if (!existsSync(processFile)) continue; + const seedFile = join(seedProcessDir, 'PROCESS.md'); + if (!existsSync(seedFile)) continue; const targetDir = join(GLOBAL_PROCESSES_DIR, entry.name); + const targetFile = join(targetDir, 'PROCESS.md'); if (existsSync(targetDir)) { - // Process already exists in DATA_PATH — skip to preserve user edits - continue; + const seedVersion = parseSeedVersion(readFileSync(seedFile, 'utf-8')); + const targetVersion = existsSync(targetFile) ? parseSeedVersion(readFileSync(targetFile, 'utf-8')) : 0; + if (seedVersion <= targetVersion) continue; + rmSync(targetDir, { recursive: true }); + console.log(`[processes] Updating seed process: ${entry.name} (v${targetVersion} → v${seedVersion})`); } cpSync(seedProcessDir, targetDir, { recursive: true }); diff --git a/src/servers/sync-skills.ts b/src/servers/sync-skills.ts index 273bcbd6..c92d78e7 100644 --- a/src/servers/sync-skills.ts +++ b/src/servers/sync-skills.ts @@ -1,6 +1,7 @@ -import { readdirSync, existsSync, mkdirSync, cpSync } from 'node:fs'; +import { readdirSync, readFileSync, existsSync, mkdirSync, cpSync, rmSync } from 'node:fs'; import { join } from 'node:path'; import { SEED_PATH, DATA_PATH } from './data-path'; +import { parseSeedVersion } from './sync-version'; const SEED_SKILLS_DIR = join(SEED_PATH, 'skills'); const GLOBAL_SKILLS_DIR = join(DATA_PATH, 'skills'); @@ -16,14 +17,18 @@ export function syncSeedSkills(): void { if (!entry.isDirectory()) continue; const seedSkillDir = join(SEED_SKILLS_DIR, entry.name); - const skillFile = join(seedSkillDir, 'SKILL.md'); - if (!existsSync(skillFile)) continue; + const seedFile = join(seedSkillDir, 'SKILL.md'); + if (!existsSync(seedFile)) continue; const targetDir = join(GLOBAL_SKILLS_DIR, entry.name); + const targetFile = join(targetDir, 'SKILL.md'); if (existsSync(targetDir)) { - // Skill already exists in DATA_PATH — skip to preserve user edits - continue; + const seedVersion = parseSeedVersion(readFileSync(seedFile, 'utf-8')); + const targetVersion = existsSync(targetFile) ? parseSeedVersion(readFileSync(targetFile, 'utf-8')) : 0; + if (seedVersion <= targetVersion) continue; + rmSync(targetDir, { recursive: true }); + console.log(`[skills] Updating seed skill: ${entry.name} (v${targetVersion} → v${seedVersion})`); } cpSync(seedSkillDir, targetDir, { recursive: true }); diff --git a/src/servers/sync-tasks.ts b/src/servers/sync-tasks.ts index 47d69492..0c243b05 100644 --- a/src/servers/sync-tasks.ts +++ b/src/servers/sync-tasks.ts @@ -1,6 +1,7 @@ -import { readdirSync, existsSync, mkdirSync, cpSync } from 'node:fs'; +import { readdirSync, readFileSync, existsSync, mkdirSync, cpSync, rmSync } from 'node:fs'; import { join } from 'node:path'; import { SEED_PATH, DATA_PATH } from './data-path'; +import { parseSeedVersion } from './sync-version'; const SEED_TASKS_DIR = join(SEED_PATH, 'tasks'); const GLOBAL_TASKS_DIR = join(DATA_PATH, 'tasks'); @@ -16,14 +17,18 @@ export function syncSeedTasks(): void { if (!entry.isDirectory()) continue; const seedTaskDir = join(SEED_TASKS_DIR, entry.name); - const taskFile = join(seedTaskDir, 'TASK.md'); - if (!existsSync(taskFile)) continue; + const seedFile = join(seedTaskDir, 'TASK.md'); + if (!existsSync(seedFile)) continue; const targetDir = join(GLOBAL_TASKS_DIR, entry.name); + const targetFile = join(targetDir, 'TASK.md'); if (existsSync(targetDir)) { - // Task already exists in DATA_PATH — skip to preserve user edits - continue; + const seedVersion = parseSeedVersion(readFileSync(seedFile, 'utf-8')); + const targetVersion = existsSync(targetFile) ? parseSeedVersion(readFileSync(targetFile, 'utf-8')) : 0; + if (seedVersion <= targetVersion) continue; + rmSync(targetDir, { recursive: true }); + console.log(`[tasks] Updating seed task: ${entry.name} (v${targetVersion} → v${seedVersion})`); } cpSync(seedTaskDir, targetDir, { recursive: true }); diff --git a/src/servers/sync-tools.ts b/src/servers/sync-tools.ts index e6b91ec2..d3a207b9 100644 --- a/src/servers/sync-tools.ts +++ b/src/servers/sync-tools.ts @@ -1,6 +1,7 @@ -import { readdirSync, existsSync, mkdirSync, cpSync } from 'node:fs'; +import { readdirSync, readFileSync, existsSync, mkdirSync, cpSync, rmSync } from 'node:fs'; import { join } from 'node:path'; import { SEED_PATH, DATA_PATH } from './data-path'; +import { parseSeedVersion } from './sync-version'; const SEED_TOOLS_DIR = join(SEED_PATH, 'tools'); const GLOBAL_TOOLS_DIR = join(DATA_PATH, 'tools'); @@ -16,14 +17,18 @@ export function syncSeedTools(): void { if (!entry.isDirectory()) continue; const seedToolDir = join(SEED_TOOLS_DIR, entry.name); - const toolFile = join(seedToolDir, 'TOOL.md'); - if (!existsSync(toolFile)) continue; + const seedFile = join(seedToolDir, 'TOOL.md'); + if (!existsSync(seedFile)) continue; const targetDir = join(GLOBAL_TOOLS_DIR, entry.name); + const targetFile = join(targetDir, 'TOOL.md'); if (existsSync(targetDir)) { - // Tool already exists in DATA_PATH — skip to preserve user edits - continue; + const seedVersion = parseSeedVersion(readFileSync(seedFile, 'utf-8')); + const targetVersion = existsSync(targetFile) ? parseSeedVersion(readFileSync(targetFile, 'utf-8')) : 0; + if (seedVersion <= targetVersion) continue; + rmSync(targetDir, { recursive: true }); + console.log(`[tools] Updating seed tool: ${entry.name} (v${targetVersion} → v${seedVersion})`); } cpSync(seedToolDir, targetDir, { recursive: true }); diff --git a/src/servers/sync-version.ts b/src/servers/sync-version.ts new file mode 100644 index 00000000..5b1f8b55 --- /dev/null +++ b/src/servers/sync-version.ts @@ -0,0 +1,10 @@ +/** + * Extract the `version` field from YAML frontmatter. + * Returns 0 if no version is found. + */ +export function parseSeedVersion(content: string): number { + const match = content.match(/^---\n([\s\S]*?)\n---/); + if (!match) return 0; + const versionMatch = match[1]!.match(/^version:\s*(\d+)/m); + return versionMatch ? parseInt(versionMatch[1]!, 10) : 0; +}