workspaces to dashboards, imap email sync, ffmpeg tool, tts fix, file browser refresh, automation sidebar reorder

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-02 20:00:45 +00:00
co-authored by Claude Opus 4.6
parent bd362dc586
commit 927267e041
98 changed files with 1176 additions and 802 deletions
+1
View File
@@ -175,5 +175,6 @@ The `tool-loader` extension discovers tools from directories listed in the `PI_T
| `browser` | Control a Chrome browser via Browser Relay |
| `apify` | Run any Apify actor (web scraping, social media data) |
| `convert_audio_to_mp3` | Convert audio files to MP3 via ffmpeg |
| `ffmpeg` | Run ffmpeg/ffprobe commands for any audio/video processing |
| `ocr` | Optical character recognition on images |
| `email_db` | Query the synced email database |
+11 -1
View File
@@ -2,7 +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
version: 3
language: typescript
inputs:
mode:
@@ -18,6 +18,16 @@ inputs:
Converts audio to MP3 320kbps CBR via libmp3lame, preserving all metadata tags.
## Prerequisites
This tool requires `ffmpeg` and `ffprobe`. If they are not installed, install them before running:
```bash
sudo apt-get update && sudo apt-get install -y ffmpeg
```
All sudo commands run without a password prompt in this environment.
## Modes
### single
+94
View File
@@ -0,0 +1,94 @@
---
name: ffmpeg
label: FFmpeg
description: Run ffmpeg and ffprobe commands for audio/video processing. Converts formats, extracts audio/video streams, trims, merges, adjusts volume, changes resolution, extracts frames, and more. Auto-installs ffmpeg if not available. Use when the user needs any audio or video manipulation.
version: 1
language: typescript
inputs:
command:
type: enum
values: ffmpeg,ffprobe
description: "ffmpeg: process audio/video. ffprobe: inspect file metadata (duration, codecs, streams, etc.)"
args:
type: string
description: "Command-line arguments as a single string. Do NOT include the ffmpeg/ffprobe binary name — only the arguments. Example: '-i input.mp4 -vn -codec:a libmp3lame -b:a 320k output.mp3'"
---
# FFmpeg
General-purpose audio/video processing via ffmpeg and ffprobe.
## Prerequisites
This tool requires `ffmpeg` and `ffprobe`. It will automatically install them if not available:
```bash
sudo apt-get update && sudo apt-get install -y ffmpeg
```
All sudo commands run without a password prompt in this environment.
## Usage
### ffprobe — Inspect files
Get duration, codecs, streams, bitrate, and other metadata:
```
command: ffprobe
args: -v quiet -print_format json -show_format -show_streams input.mp4
```
### ffmpeg — Process files
Always include `-y` to overwrite output files without prompting.
**Convert video to MP4:**
```
command: ffmpeg
args: -i input.avi -codec:v libx264 -codec:a aac output.mp4 -y
```
**Extract audio from video:**
```
command: ffmpeg
args: -i video.mp4 -vn -codec:a libmp3lame -b:a 320k audio.mp3 -y
```
**Trim a clip:**
```
command: ffmpeg
args: -i input.mp4 -ss 00:01:30 -to 00:03:00 -codec copy clip.mp4 -y
```
**Change resolution:**
```
command: ffmpeg
args: -i input.mp4 -vf scale=1280:720 -codec:a copy output.mp4 -y
```
**Extract a frame as image:**
```
command: ffmpeg
args: -i video.mp4 -ss 00:00:10 -frames:v 1 frame.png -y
```
**Merge audio and video:**
```
command: ffmpeg
args: -i video.mp4 -i audio.mp3 -codec:v copy -codec:a aac -shortest merged.mp4 -y
```
**Convert audio format:**
```
command: ffmpeg
args: -i input.flac -codec:a libmp3lame -b:a 320k output.mp3 -y
```
## Notes
- Use absolute paths for input and output files.
- Always add `-y` to ffmpeg args to avoid interactive prompts.
- For long operations, progress is streamed in real time.
- stderr output from ffmpeg is captured and returned on failure.
- The tool has a 10-minute timeout.
+173
View File
@@ -0,0 +1,173 @@
import { execFileSync, spawn } from 'node:child_process';
type ToolResult = {
content: Array<{ type: string; text: string }>;
isError?: boolean;
};
type OnUpdate = (partial: { content: Array<{ type: string; text: string }> }) => void;
function update(onUpdate: OnUpdate | undefined, text: string): void {
onUpdate?.({ content: [{ type: 'text', text }] });
}
function ensureFfmpeg(): boolean {
try {
execFileSync('ffmpeg', ['-version'], { stdio: 'ignore' });
return true;
} catch {
try {
execFileSync('sudo', ['apt-get', 'update'], { stdio: 'ignore', timeout: 60_000 });
execFileSync('sudo', ['apt-get', 'install', '-y', 'ffmpeg'], { stdio: 'ignore', timeout: 120_000 });
execFileSync('ffmpeg', ['-version'], { stdio: 'ignore' });
return true;
} catch {
return false;
}
}
}
function parseArgs(argsString: string): string[] {
const args: string[] = [];
let current = '';
let inSingle = false;
let inDouble = false;
for (let i = 0; i < argsString.length; i++) {
const ch = argsString[i]!;
if (ch === "'" && !inDouble) {
inSingle = !inSingle;
} else if (ch === '"' && !inSingle) {
inDouble = !inDouble;
} else if (ch === ' ' && !inSingle && !inDouble) {
if (current.length > 0) {
args.push(current);
current = '';
}
} else {
current += ch;
}
}
if (current.length > 0) args.push(current);
return args;
}
export async function execute(
_toolCallId: string,
params: { command: 'ffmpeg' | 'ffprobe'; args: string },
_signal: AbortSignal | undefined,
onUpdate?: OnUpdate,
): Promise<ToolResult> {
const { command, args: argsString } = params;
if (!argsString || argsString.trim().length === 0) {
return {
content: [{ type: 'text', text: 'No arguments provided. See tool documentation for usage examples.' }],
isError: true,
};
}
update(onUpdate, 'Checking ffmpeg installation...');
if (!ensureFfmpeg()) {
return {
content: [{ type: 'text', text: 'Failed to install ffmpeg. Try manually: sudo apt-get update && sudo apt-get install -y ffmpeg' }],
isError: true,
};
}
const args = parseArgs(argsString);
// For ffprobe, run synchronously and return output
if (command === 'ffprobe') {
update(onUpdate, `Running ffprobe...`);
return new Promise((resolve) => {
const proc = spawn('ffprobe', args, { stdio: ['ignore', 'pipe', 'pipe'] });
let stdout = '';
let stderr = '';
proc.stdout.on('data', (chunk: Buffer) => { stdout += chunk.toString(); });
proc.stderr.on('data', (chunk: Buffer) => { stderr += chunk.toString(); });
proc.on('close', (code) => {
if (code !== 0) {
resolve({
content: [{ type: 'text', text: `ffprobe failed (exit ${code}):\n${stderr.trim()}` }],
isError: true,
});
} else {
// ffprobe prints info to stderr by default, stdout for -print_format
const output = stdout.trim() || stderr.trim();
resolve({
content: [{ type: 'text', text: output || 'No output' }],
});
}
});
proc.on('error', (err) => {
resolve({
content: [{ type: 'text', text: `ffprobe error: ${err.message}` }],
isError: true,
});
});
});
}
// For ffmpeg, stream progress
update(onUpdate, `Running ffmpeg...`);
return new Promise((resolve) => {
const proc = spawn('ffmpeg', args, { stdio: ['ignore', 'pipe', 'pipe'] });
let stdout = '';
let stderr = '';
let progressBuf = '';
proc.stdout.on('data', (chunk: Buffer) => {
stdout += chunk.toString();
});
proc.stderr.on('data', (chunk: Buffer) => {
const text = chunk.toString();
stderr += text;
// Parse ffmpeg progress from stderr (time= field)
progressBuf += text;
const lines = progressBuf.split('\r');
progressBuf = lines.pop() ?? '';
for (const line of lines) {
const timeMatch = line.match(/time=(\d{2}:\d{2}:\d{2}\.\d{2})/);
const speedMatch = line.match(/speed=\s*([\d.]+x)/);
if (timeMatch) {
const progress = `Time: ${timeMatch[1]}${speedMatch ? ` | Speed: ${speedMatch[1]}` : ''}`;
update(onUpdate, progress);
}
}
});
proc.on('close', (code) => {
if (code !== 0) {
// Extract the last meaningful error line from stderr
const errLines = stderr.trim().split('\n');
const lastLines = errLines.slice(-10).join('\n');
resolve({
content: [{ type: 'text', text: `ffmpeg failed (exit ${code}):\n${lastLines}` }],
isError: true,
});
} else {
const output = stdout.trim();
resolve({
content: [{ type: 'text', text: output ? `Done.\n\n${output}` : 'Done.' }],
});
}
});
proc.on('error', (err) => {
resolve({
content: [{ type: 'text', text: `ffmpeg error: ${err.message}` }],
isError: true,
});
});
});
}