Vibecode FileChanger
track this build4 phases, 12 steps, beginner friendly0%The conversion is other people's software. Pandoc, ImageMagick and FFmpeg do the actual work, the format list looks enormous because those three projects are enormous, and a coding agent can wire all of them behind an upload form and a REST endpoint in one sitting. What a subscription buys is not the conversion, it is somebody else running a box that eats untrusted files from strangers all day without falling over, and keeping a few hundred format pairs working after every upstream release. Point it at your own files on your own machine and most of that risk simply is not yours, which is why this one is a yes.
You are building a lean indie version of FileChanger. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # FileChanger · indie build A personal conversion API whose hard problems are not the conversions, which other people's software does, but discovering what the installed engines can actually do, fencing each invocation so a bad file cannot take the machine with it, and reporting failures in a way that is useful to you and useless to an attacker. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python with a small HTTP framework, one process, no broker | a queue of one person's files does not need a message bus | | Engines | Pandoc for markup, an office suite in headless mode, ImageMagick for images, FFmpeg for media | never write a converter; the format list is enormous because these projects are | | State | SQLite for jobs and keys, files on disk in per-job directories | a job needs to survive a restart and a directory is the easiest thing to delete | | Discovery | The format matrix built at startup by asking each binary what it supports | a hard-coded matrix is wrong the day after an upgrade | | Isolation | A temporary directory, a wall-clock timeout, an output cap and no network per conversion | the input is a file whose author you did not choose | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Python 3.12 or newer** · free - Why: Runs the service, the worker and the MCP server. - Get it: python.org, or your version manager. - Verify: python3 --version prints 3.12 or higher - [ ] **Pandoc, FFmpeg, ImageMagick and a headless office suite** · free - Why: These four do all the work, and the format list you will advertise is exactly what they report. - Get it: Your package manager, or the container image this build ships. - Verify: Each binary prints a version and the startup probe lists its formats - [ ] **A PDF engine for document output** · free, but the install is large - Why: Pandoc does not produce PDF on its own; it hands off to a typesetting engine, and forgetting this is the most common first failure in a converter. - Get it: Install a supported typesetting engine and name it in the configuration. - Verify: A markup file converts to a PDF that opens - [ ] **An explicit image engine policy file** · free - Why: The distribution default may already disable the delegates you need, or may not disable ones you would rather not run, and either way it is not your decision until you commit one. - Get it: Read the image engine's own security policy documentation and write a policy covering resource limits and which coders are permitted. - Verify: The policy file is committed and the service starts with it - [ ] **A container runtime** · free - Why: Pinning every engine and its fonts in one image is what makes an upgrade a deliberate act rather than a surprise. - Get it: Any container runtime with compose support. - Verify: The image builds and the engine round trips pass inside it ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Any converter written here. - Uploads from people you do not know. - Metering, quotas and billing. - The long tail of awkward format pairs. - sandboxing, because your version runs files straight through the same toolchain as everything else on the machine - the long tail of format pairs, where the fix is usually a missing font, a filter flag or an upstream bug - queueing and timeouts for large video, which is where the compute bill actually lives - a hosted MCP server and API keys your other tools can reach from anywhere, not just from localhost - somebody else being on call when a LibreOffice or LaTeX upgrade quietly breaks PDF output If one of those is essential to you, that is the reason to keep paying for FileChanger, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · FileChanger The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local conversion service that shells out to the engines everyone already uses and never pretends a failed conversion worked to replace FileChanger. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: Python with a small HTTP framework, one process, no broker · a queue of one person's files does not need a message bus - Engines: Pandoc for markup, an office suite in headless mode, ImageMagick for images, FFmpeg for media · never write a converter; the format list is enormous because these projects are - State: SQLite for jobs and keys, files on disk in per-job directories · a job needs to survive a restart and a directory is the easiest thing to delete - Discovery: The format matrix built at startup by asking each binary what it supports · a hard-coded matrix is wrong the day after an upgrade - Isolation: A temporary directory, a wall-clock timeout, an output cap and no network per conversion · the input is a file whose author you did not choose ### Data model (create this before Phase 1) - engines(name, binary_path, version_string, probed_at) · the version is what explains a conversion that worked last month - formats(engine, direction, format_name, probed_at) · direction is input or output, discovered rather than typed - jobs(id, created_at, source_format, target_format, input_bytes, state, started_at, finished_at, exit_code, failure_kind, output_bytes, output_path) - job_logs(job_id, stream, content) · the command and its error output, stored and never returned over HTTP - api_keys(id, name, hash, created_at, last_used_at, revoked_at) · the plaintext is shown once and never stored - limits(name, value) · timeout seconds, maximum input bytes, maximum output bytes, per-key concurrency - The format matrix is a cache of what the installed binaries said, with the date they said it. An upgrade invalidates it, which is the point. ### Phase 1 · One engine, one round trip, one honest failure Build: One route accepting a file, a source format and a target format, invoking pandoc in a temporary directory and returning the bytes. Capture exit code, standard error and duration into the job log. Return a reason class such as unsupported pair, engine error or timeout, never the command or its error text. Delete the input whether the conversion worked or not. Trap: Returning the engine's error output to the caller is the natural debugging shortcut and it hands over absolute paths, installed versions and sometimes fragments of the document itself. Store the command and its error in the job log, return a reason class and a job id, and make the only path to the detail a local command you run yourself. Done when: A markup conversion returns correct bytes and a nonsense conversion returns a reason class with no path, version or command in the response, asserted by a test that scans the body. Input files are removed on both the success and the failure path, tested by forcing a failure. Exit code, duration and error output are stored in the job log and are reachable only locally. A conversion exceeding the timeout is reported as a timeout rather than as an engine error. Do not build yet: No format discovery, no other engines, no jobs, no keys, no MCP. ### Phase 2 · Ask the binaries what they can do Build: At startup, query each engine for its input and output formats and its version, and store both with the probe time. Serve the matrix on a route. Refuse a pair that is not in the matrix before touching a file. Re-probe on demand and log when a version string has changed since the last probe. Trap: Hard-coding a list of a few hundred format pairs produces a service that accepts a conversion the installed binary can no longer do, wastes the upload and returns an engine error that looks like a bug in your code. Ask the binaries, cache what they said with the date, and treat a version change as a reason to re-probe rather than a curiosity. Done when: The matrix is populated by probing the binaries and contains no hard-coded format list, verified by review and by removing an engine and seeing its formats vanish. A request for a pair outside the matrix is refused before the upload is read, tested. Engine version strings are stored and a change since the last probe is logged at startup. The matrix route reports the probe time so a stale matrix is visible. Do not build yet: No isolation policy work, no jobs, no keys, no MCP. ### Phase 3 · Fencing each conversion Build: Per-conversion limits on wall-clock time, input size and output size, enforced and tested. Give the image engine an explicit policy file rather than relying on the distribution's default, because that default may already disable the delegates you need and its documentation recommends distributions disable the ones that shell out to a page description interpreter. Give each office-suite worker its own user profile directory, because two instances sharing one profile is a documented way to get a silent failure; verify that behaviour on your own install before relying on it. Trap: Running two office-suite conversions at once with the default shared profile produces a job that exits without an output file and without a useful error, and because it only happens under concurrency it survives every single-file test you write. Give each worker its own profile directory derived from the worker index, and add a concurrency test that runs several conversions of the same type simultaneously. Done when: Two concurrent office-suite conversions both produce correct output, tested by running them in parallel. A conversion exceeding the output size cap is stopped and reported rather than filling the disk, tested with a deliberately expanding input. The image engine runs against a policy file committed in the repository, and a conversion requiring a disabled delegate fails with a clear reason class. A conversion attempting a network fetch fails, tested with an input referencing a remote resource. Do not build yet: No asynchronous jobs, no keys, no MCP, no operational documentation. ### Phase 4 · Big files become jobs Build: Below a configured threshold the route returns the bytes. Above it, the same route returns a job identifier, a status route reports state, and a result route streams the file. One background worker thread claims queued jobs. A job in a running state at startup is marked interrupted rather than resumed, because a half-written output is not a resumable thing. Trap: Marking an interrupted job as queued again on restart re-runs a conversion whose partial output is still on disk, and the second attempt writes over it while a client is streaming the first, which produces a corrupt download nobody can reproduce. Mark it interrupted, delete the partial output, and let the caller decide whether to ask again. Done when: A conversion under the threshold returns bytes inline and one over it returns a job identifier, tested at the boundary. Killing the process mid-job leaves that job in an interrupted state with its partial output deleted, tested. The result route streams rather than loading the file into memory, verified with a large fixture and a memory measurement. Job directories are removed after a configured retention whether or not the result was collected. Do not build yet: No keys, no MCP, no upgrade handling. ### Phase 5 · Keys, agents and the day an upgrade breaks a format Build: API keys stored hashed, shown in plaintext once, revocable. An MCP server over standard input and output exposing convert and formats as tools. A round-trip test per engine run in the suite and after every container build, so an upgrade that breaks a format pair fails the build rather than a user's conversion. One container image carrying every engine, with the versions recorded in the image. Trap: Upgrading the container image and deploying it because the tests passed is fine until an engine drops a filter or a font package changes, and then the failure arrives as a single user's broken document weeks later with no way to tell which upgrade did it. Run one real round trip per engine as part of the build, store the engine versions in the image, and compare them against the previous build in the release notes. Done when: A key is displayed once at creation and only its hash is stored, asserted by inspecting the database. A revoked key is refused immediately and the refusal is distinguishable from an unknown key only in the local log, not in the response. The MCP server exposes convert and formats and completes one conversion without going through HTTP, tested. The build fails when any engine round trip stops producing a valid output, tested by pinning a deliberately broken engine version. Do not build yet: Nothing after this. ### Out of scope (and why) - Writing any converter, since every format here belongs to a project with decades of work behind it and your version will be worse in ways you will not notice. - Accepting uploads from people you do not know, which is the actual product on the other side and needs sandboxing far beyond a temporary directory and a timeout. - Metering, quotas and billing, which exist to ration a shared machine and have no purpose on your own. - Chasing the long tail of format pairs, where the fix is usually a missing font or an upstream bug rather than anything in your code. ### README must contain - That no converter is written here and which engine handles which family, with the versions the image ships - That the format matrix is discovered from the installed binaries at startup and carries its probe time - The security assumptions: your own files, a temporary directory, a timeout, size caps and no network - That error detail is stored locally and never returned, and the local command for reading a job log - That an engine upgrade can break a format pair, and that the build runs a round trip per engine to catch it ===== AGENTS.md ===== # Agent instructions · FileChanger indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Python with a small HTTP framework, one process, no broker, Pandoc for markup, an office suite in headless mode, ImageMagick for images, FFmpeg for media, SQLite for jobs and keys, files on disk in per-job directories, The format matrix built at startup by asking each binary what it supports, A temporary directory, a wall-clock timeout, an output cap and no network per conversion. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Returning the engine's error output to the caller is the natural debugging shortcut and it hands over absolute paths, installed versions and sometimes fragments of the document itself. Store the command and its error in the job log, return a reason class and a job id, and make the only path to the detail a local command you run yourself. - Hard-coding a list of a few hundred format pairs produces a service that accepts a conversion the installed binary can no longer do, wastes the upload and returns an engine error that looks like a bug in your code. Ask the binaries, cache what they said with the date, and treat a version change as a reason to re-probe rather than a curiosity. - Running two office-suite conversions at once with the default shared profile produces a job that exits without an output file and without a useful error, and because it only happens under concurrency it survives every single-file test you write. Give each worker its own profile directory derived from the worker index, and add a concurrency test that runs several conversions of the same type simultaneously. - Marking an interrupted job as queued again on restart re-runs a conversion whose partial output is still on disk, and the second attempt writes over it while a client is streaming the first, which produces a corrupt download nobody can reproduce. Mark it interrupted, delete the partial output, and let the caller decide whether to ask again. ===== BUILD_PLAN.md ===== # Build plan · FileChanger A personal conversion API whose hard problems are not the conversions, which other people's software does, but discovering what the installed engines can actually do, fencing each invocation so a bad file cannot take the machine with it, and reporting failures in a way that is useful to you and useless to an attacker. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · One engine, one round trip, one honest failure A single conversion works end to end and a failed conversion is reported as failed with a reason class. ### Steps 1. Return a reason class, store the detail Error output is a description of your filesystem. Files: `src/convert/run.py`, `tests/test_no_leak.py` 2. Delete inputs in a finally block The failure path is where the leftovers come from. Files: `src/convert/workspace.py` 3. Distinguish timeout from engine error They have completely different fixes. Files: `src/convert/classify.py` ### Done when - [ ] Responses contain no path, version or command - [ ] Inputs are deleted on success and on failure - [ ] Exit code and error output are stored locally - [ ] A timeout is classified as a timeout ### Watch out - Returning the engine's error output to the caller is the natural debugging shortcut and it hands over absolute paths, installed versions and sometimes fragments of the document itself. Store the command and its error in the job log, return a reason class and a job id, and make the only path to the detail a local command you run yourself. ## Phase 2 · Ask the binaries what they can do The supported format matrix comes from the installed engines at startup, with their versions recorded. ### Steps 1. Probe each engine and store its version The version is the first thing you will want after a surprise. Files: `src/engines/probe.py`, `tests/test_probe.py` 2. Refuse unsupported pairs before reading the upload Failing after a large upload wastes everybody's time. Files: `src/api/convert.py` 3. Report the probe time on the matrix route A cached answer should say how old it is. Files: `src/api/formats.py` ### Done when - [ ] No hard-coded format list exists - [ ] Unsupported pairs are refused before upload - [ ] Engine versions are stored and changes logged - [ ] The matrix reports its probe time ### Watch out - Hard-coding a list of a few hundred format pairs produces a service that accepts a conversion the installed binary can no longer do, wastes the upload and returns an engine error that looks like a bug in your code. Ask the binaries, cache what they said with the date, and treat a version change as a reason to re-probe rather than a curiosity. ## Phase 3 · Fencing each conversion One conversion cannot exhaust the machine, reach the network, or be affected by another running at the same time. ### Steps 1. Give each office-suite worker its own profile A shared profile fails only under concurrency, which is where you use it. Files: `src/engines/office.py`, `tests/test_concurrent_office.py` 2. Commit an explicit image policy file The distribution default is a decision somebody else made. Files: `config/imagemagick-policy.xml` 3. Cap output size as well as input size A small input can produce an enormous output on purpose. Files: `src/convert/limits.py` ### Done when - [ ] Concurrent office conversions both succeed - [ ] The output cap stops a runaway conversion - [ ] A committed policy file governs the image engine - [ ] A remote reference in an input is refused ### Watch out - Running two office-suite conversions at once with the default shared profile produces a job that exits without an output file and without a useful error, and because it only happens under concurrency it survives every single-file test you write. Give each worker its own profile directory derived from the worker index, and add a concurrency test that runs several conversions of the same type simultaneously. ## Phase 4 · Big files become jobs A conversion too large to answer inline becomes a job that survives a restart. ### Steps 1. Return bytes below the threshold, a job above it One route, two shapes, is easier to use than two routes. Files: `src/api/convert.py`, `tests/test_threshold.py` 2. Mark interrupted, do not resume A partial output is not a checkpoint. Files: `src/jobs/recover.py` 3. Stream the result, never load it Video is the reason this matters. Files: `src/api/result.py` ### Done when - [ ] The size threshold selects inline or job - [ ] An interrupted job is marked and cleaned - [ ] Results stream without loading into memory - [ ] Job directories expire on a retention schedule ### Watch out - Marking an interrupted job as queued again on restart re-runs a conversion whose partial output is still on disk, and the second attempt writes over it while a client is streaming the first, which produces a corrupt download nobody can reproduce. Mark it interrupted, delete the partial output, and let the caller decide whether to ask again. ## Not in this build - Any converter written here. - Uploads from people you do not know. - Metering, quotas and billing. - The long tail of awkward format pairs. ## After v1, if you want it - A per-pair round trip fixture set, grown one entry at a time whenever something breaks, which becomes the most valuable file in the repository - A dry-run route reporting which engine and arguments a pair would use, which answers most questions without a conversion - Font package pinning in the image, since a font change is the most common cause of a document that converts and looks wrong ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Jobs, logs, keys and the format matrix cache. DATABASE_PATH=./data/jobs.db # Required. Per-job directories. Deleted on completion and on retention. WORK_DIR=./data/work # Required. Above this a conversion becomes a job instead of a response. INLINE_MAX_BYTES=8388608 # Required. Wall clock per conversion. A timeout is a distinct reason class. CONVERT_TIMEOUT_SECONDS=120 # Required. A small input can expand enormously, deliberately or otherwise. OUTPUT_MAX_BYTES=536870912 # Required. Job directories are removed after this whether collected or not. JOB_RETENTION_HOURS=24 # Required. The committed policy, used instead of whatever the distribution shipped. IMAGEMAGICK_POLICY_PATH=./config/imagemagick-policy.xml # Required. Each worker gets its own office-suite profile directory derived from its index. WORKER_COUNT=2
You are building a lean indie version of FileChanger. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # FileChanger · indie build A personal conversion API whose hard problems are not the conversions, which other people's software does, but discovering what the installed engines can actually do, fencing each invocation so a bad file cannot take the machine with it, and reporting failures in a way that is useful to you and useless to an attacker. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python with a small HTTP framework, one process, no broker | a queue of one person's files does not need a message bus | | Engines | Pandoc for markup, an office suite in headless mode, ImageMagick for images, FFmpeg for media | never write a converter; the format list is enormous because these projects are | | State | SQLite for jobs and keys, files on disk in per-job directories | a job needs to survive a restart and a directory is the easiest thing to delete | | Discovery | The format matrix built at startup by asking each binary what it supports | a hard-coded matrix is wrong the day after an upgrade | | Isolation | A temporary directory, a wall-clock timeout, an output cap and no network per conversion | the input is a file whose author you did not choose | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Python 3.12 or newer** · free - Why: Runs the service, the worker and the MCP server. - Get it: python.org, or your version manager. - Verify: python3 --version prints 3.12 or higher - [ ] **Pandoc, FFmpeg, ImageMagick and a headless office suite** · free - Why: These four do all the work, and the format list you will advertise is exactly what they report. - Get it: Your package manager, or the container image this build ships. - Verify: Each binary prints a version and the startup probe lists its formats - [ ] **A PDF engine for document output** · free, but the install is large - Why: Pandoc does not produce PDF on its own; it hands off to a typesetting engine, and forgetting this is the most common first failure in a converter. - Get it: Install a supported typesetting engine and name it in the configuration. - Verify: A markup file converts to a PDF that opens - [ ] **An explicit image engine policy file** · free - Why: The distribution default may already disable the delegates you need, or may not disable ones you would rather not run, and either way it is not your decision until you commit one. - Get it: Read the image engine's own security policy documentation and write a policy covering resource limits and which coders are permitted. - Verify: The policy file is committed and the service starts with it - [ ] **A container runtime** · free - Why: Pinning every engine and its fonts in one image is what makes an upgrade a deliberate act rather than a surprise. - Get it: Any container runtime with compose support. - Verify: The image builds and the engine round trips pass inside it ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Any converter written here. - Uploads from people you do not know. - Metering, quotas and billing. - The long tail of awkward format pairs. - sandboxing, because your version runs files straight through the same toolchain as everything else on the machine - the long tail of format pairs, where the fix is usually a missing font, a filter flag or an upstream bug - queueing and timeouts for large video, which is where the compute bill actually lives - a hosted MCP server and API keys your other tools can reach from anywhere, not just from localhost - somebody else being on call when a LibreOffice or LaTeX upgrade quietly breaks PDF output If one of those is essential to you, that is the reason to keep paying for FileChanger, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · FileChanger The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local conversion service that shells out to the engines everyone already uses and never pretends a failed conversion worked to replace FileChanger. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: Python with a small HTTP framework, one process, no broker · a queue of one person's files does not need a message bus - Engines: Pandoc for markup, an office suite in headless mode, ImageMagick for images, FFmpeg for media · never write a converter; the format list is enormous because these projects are - State: SQLite for jobs and keys, files on disk in per-job directories · a job needs to survive a restart and a directory is the easiest thing to delete - Discovery: The format matrix built at startup by asking each binary what it supports · a hard-coded matrix is wrong the day after an upgrade - Isolation: A temporary directory, a wall-clock timeout, an output cap and no network per conversion · the input is a file whose author you did not choose ### Data model (create this before Phase 1) - engines(name, binary_path, version_string, probed_at) · the version is what explains a conversion that worked last month - formats(engine, direction, format_name, probed_at) · direction is input or output, discovered rather than typed - jobs(id, created_at, source_format, target_format, input_bytes, state, started_at, finished_at, exit_code, failure_kind, output_bytes, output_path) - job_logs(job_id, stream, content) · the command and its error output, stored and never returned over HTTP - api_keys(id, name, hash, created_at, last_used_at, revoked_at) · the plaintext is shown once and never stored - limits(name, value) · timeout seconds, maximum input bytes, maximum output bytes, per-key concurrency - The format matrix is a cache of what the installed binaries said, with the date they said it. An upgrade invalidates it, which is the point. ### Phase 1 · One engine, one round trip, one honest failure Build: One route accepting a file, a source format and a target format, invoking pandoc in a temporary directory and returning the bytes. Capture exit code, standard error and duration into the job log. Return a reason class such as unsupported pair, engine error or timeout, never the command or its error text. Delete the input whether the conversion worked or not. Trap: Returning the engine's error output to the caller is the natural debugging shortcut and it hands over absolute paths, installed versions and sometimes fragments of the document itself. Store the command and its error in the job log, return a reason class and a job id, and make the only path to the detail a local command you run yourself. Done when: A markup conversion returns correct bytes and a nonsense conversion returns a reason class with no path, version or command in the response, asserted by a test that scans the body. Input files are removed on both the success and the failure path, tested by forcing a failure. Exit code, duration and error output are stored in the job log and are reachable only locally. A conversion exceeding the timeout is reported as a timeout rather than as an engine error. Do not build yet: No format discovery, no other engines, no jobs, no keys, no MCP. ### Phase 2 · Ask the binaries what they can do Build: At startup, query each engine for its input and output formats and its version, and store both with the probe time. Serve the matrix on a route. Refuse a pair that is not in the matrix before touching a file. Re-probe on demand and log when a version string has changed since the last probe. Trap: Hard-coding a list of a few hundred format pairs produces a service that accepts a conversion the installed binary can no longer do, wastes the upload and returns an engine error that looks like a bug in your code. Ask the binaries, cache what they said with the date, and treat a version change as a reason to re-probe rather than a curiosity. Done when: The matrix is populated by probing the binaries and contains no hard-coded format list, verified by review and by removing an engine and seeing its formats vanish. A request for a pair outside the matrix is refused before the upload is read, tested. Engine version strings are stored and a change since the last probe is logged at startup. The matrix route reports the probe time so a stale matrix is visible. Do not build yet: No isolation policy work, no jobs, no keys, no MCP. ### Phase 3 · Fencing each conversion Build: Per-conversion limits on wall-clock time, input size and output size, enforced and tested. Give the image engine an explicit policy file rather than relying on the distribution's default, because that default may already disable the delegates you need and its documentation recommends distributions disable the ones that shell out to a page description interpreter. Give each office-suite worker its own user profile directory, because two instances sharing one profile is a documented way to get a silent failure; verify that behaviour on your own install before relying on it. Trap: Running two office-suite conversions at once with the default shared profile produces a job that exits without an output file and without a useful error, and because it only happens under concurrency it survives every single-file test you write. Give each worker its own profile directory derived from the worker index, and add a concurrency test that runs several conversions of the same type simultaneously. Done when: Two concurrent office-suite conversions both produce correct output, tested by running them in parallel. A conversion exceeding the output size cap is stopped and reported rather than filling the disk, tested with a deliberately expanding input. The image engine runs against a policy file committed in the repository, and a conversion requiring a disabled delegate fails with a clear reason class. A conversion attempting a network fetch fails, tested with an input referencing a remote resource. Do not build yet: No asynchronous jobs, no keys, no MCP, no operational documentation. ### Phase 4 · Big files become jobs Build: Below a configured threshold the route returns the bytes. Above it, the same route returns a job identifier, a status route reports state, and a result route streams the file. One background worker thread claims queued jobs. A job in a running state at startup is marked interrupted rather than resumed, because a half-written output is not a resumable thing. Trap: Marking an interrupted job as queued again on restart re-runs a conversion whose partial output is still on disk, and the second attempt writes over it while a client is streaming the first, which produces a corrupt download nobody can reproduce. Mark it interrupted, delete the partial output, and let the caller decide whether to ask again. Done when: A conversion under the threshold returns bytes inline and one over it returns a job identifier, tested at the boundary. Killing the process mid-job leaves that job in an interrupted state with its partial output deleted, tested. The result route streams rather than loading the file into memory, verified with a large fixture and a memory measurement. Job directories are removed after a configured retention whether or not the result was collected. Do not build yet: No keys, no MCP, no upgrade handling. ### Phase 5 · Keys, agents and the day an upgrade breaks a format Build: API keys stored hashed, shown in plaintext once, revocable. An MCP server over standard input and output exposing convert and formats as tools. A round-trip test per engine run in the suite and after every container build, so an upgrade that breaks a format pair fails the build rather than a user's conversion. One container image carrying every engine, with the versions recorded in the image. Trap: Upgrading the container image and deploying it because the tests passed is fine until an engine drops a filter or a font package changes, and then the failure arrives as a single user's broken document weeks later with no way to tell which upgrade did it. Run one real round trip per engine as part of the build, store the engine versions in the image, and compare them against the previous build in the release notes. Done when: A key is displayed once at creation and only its hash is stored, asserted by inspecting the database. A revoked key is refused immediately and the refusal is distinguishable from an unknown key only in the local log, not in the response. The MCP server exposes convert and formats and completes one conversion without going through HTTP, tested. The build fails when any engine round trip stops producing a valid output, tested by pinning a deliberately broken engine version. Do not build yet: Nothing after this. ### Out of scope (and why) - Writing any converter, since every format here belongs to a project with decades of work behind it and your version will be worse in ways you will not notice. - Accepting uploads from people you do not know, which is the actual product on the other side and needs sandboxing far beyond a temporary directory and a timeout. - Metering, quotas and billing, which exist to ration a shared machine and have no purpose on your own. - Chasing the long tail of format pairs, where the fix is usually a missing font or an upstream bug rather than anything in your code. ### README must contain - That no converter is written here and which engine handles which family, with the versions the image ships - That the format matrix is discovered from the installed binaries at startup and carries its probe time - The security assumptions: your own files, a temporary directory, a timeout, size caps and no network - That error detail is stored locally and never returned, and the local command for reading a job log - That an engine upgrade can break a format pair, and that the build runs a round trip per engine to catch it ===== AGENTS.md ===== # Agent instructions · FileChanger indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Python with a small HTTP framework, one process, no broker, Pandoc for markup, an office suite in headless mode, ImageMagick for images, FFmpeg for media, SQLite for jobs and keys, files on disk in per-job directories, The format matrix built at startup by asking each binary what it supports, A temporary directory, a wall-clock timeout, an output cap and no network per conversion. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Returning the engine's error output to the caller is the natural debugging shortcut and it hands over absolute paths, installed versions and sometimes fragments of the document itself. Store the command and its error in the job log, return a reason class and a job id, and make the only path to the detail a local command you run yourself. - Hard-coding a list of a few hundred format pairs produces a service that accepts a conversion the installed binary can no longer do, wastes the upload and returns an engine error that looks like a bug in your code. Ask the binaries, cache what they said with the date, and treat a version change as a reason to re-probe rather than a curiosity. - Running two office-suite conversions at once with the default shared profile produces a job that exits without an output file and without a useful error, and because it only happens under concurrency it survives every single-file test you write. Give each worker its own profile directory derived from the worker index, and add a concurrency test that runs several conversions of the same type simultaneously. - Marking an interrupted job as queued again on restart re-runs a conversion whose partial output is still on disk, and the second attempt writes over it while a client is streaming the first, which produces a corrupt download nobody can reproduce. Mark it interrupted, delete the partial output, and let the caller decide whether to ask again. ===== BUILD_PLAN.md ===== # Build plan · FileChanger A personal conversion API whose hard problems are not the conversions, which other people's software does, but discovering what the installed engines can actually do, fencing each invocation so a bad file cannot take the machine with it, and reporting failures in a way that is useful to you and useless to an attacker. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · One engine, one round trip, one honest failure A single conversion works end to end and a failed conversion is reported as failed with a reason class. ### Steps 1. Return a reason class, store the detail Error output is a description of your filesystem. Files: `src/convert/run.py`, `tests/test_no_leak.py` 2. Delete inputs in a finally block The failure path is where the leftovers come from. Files: `src/convert/workspace.py` 3. Distinguish timeout from engine error They have completely different fixes. Files: `src/convert/classify.py` ### Done when - [ ] Responses contain no path, version or command - [ ] Inputs are deleted on success and on failure - [ ] Exit code and error output are stored locally - [ ] A timeout is classified as a timeout ### Watch out - Returning the engine's error output to the caller is the natural debugging shortcut and it hands over absolute paths, installed versions and sometimes fragments of the document itself. Store the command and its error in the job log, return a reason class and a job id, and make the only path to the detail a local command you run yourself. ## Phase 2 · Ask the binaries what they can do The supported format matrix comes from the installed engines at startup, with their versions recorded. ### Steps 1. Probe each engine and store its version The version is the first thing you will want after a surprise. Files: `src/engines/probe.py`, `tests/test_probe.py` 2. Refuse unsupported pairs before reading the upload Failing after a large upload wastes everybody's time. Files: `src/api/convert.py` 3. Report the probe time on the matrix route A cached answer should say how old it is. Files: `src/api/formats.py` ### Done when - [ ] No hard-coded format list exists - [ ] Unsupported pairs are refused before upload - [ ] Engine versions are stored and changes logged - [ ] The matrix reports its probe time ### Watch out - Hard-coding a list of a few hundred format pairs produces a service that accepts a conversion the installed binary can no longer do, wastes the upload and returns an engine error that looks like a bug in your code. Ask the binaries, cache what they said with the date, and treat a version change as a reason to re-probe rather than a curiosity. ## Phase 3 · Fencing each conversion One conversion cannot exhaust the machine, reach the network, or be affected by another running at the same time. ### Steps 1. Give each office-suite worker its own profile A shared profile fails only under concurrency, which is where you use it. Files: `src/engines/office.py`, `tests/test_concurrent_office.py` 2. Commit an explicit image policy file The distribution default is a decision somebody else made. Files: `config/imagemagick-policy.xml` 3. Cap output size as well as input size A small input can produce an enormous output on purpose. Files: `src/convert/limits.py` ### Done when - [ ] Concurrent office conversions both succeed - [ ] The output cap stops a runaway conversion - [ ] A committed policy file governs the image engine - [ ] A remote reference in an input is refused ### Watch out - Running two office-suite conversions at once with the default shared profile produces a job that exits without an output file and without a useful error, and because it only happens under concurrency it survives every single-file test you write. Give each worker its own profile directory derived from the worker index, and add a concurrency test that runs several conversions of the same type simultaneously. ## Phase 4 · Big files become jobs A conversion too large to answer inline becomes a job that survives a restart. ### Steps 1. Return bytes below the threshold, a job above it One route, two shapes, is easier to use than two routes. Files: `src/api/convert.py`, `tests/test_threshold.py` 2. Mark interrupted, do not resume A partial output is not a checkpoint. Files: `src/jobs/recover.py` 3. Stream the result, never load it Video is the reason this matters. Files: `src/api/result.py` ### Done when - [ ] The size threshold selects inline or job - [ ] An interrupted job is marked and cleaned - [ ] Results stream without loading into memory - [ ] Job directories expire on a retention schedule ### Watch out - Marking an interrupted job as queued again on restart re-runs a conversion whose partial output is still on disk, and the second attempt writes over it while a client is streaming the first, which produces a corrupt download nobody can reproduce. Mark it interrupted, delete the partial output, and let the caller decide whether to ask again. ## Not in this build - Any converter written here. - Uploads from people you do not know. - Metering, quotas and billing. - The long tail of awkward format pairs. ## After v1, if you want it - A per-pair round trip fixture set, grown one entry at a time whenever something breaks, which becomes the most valuable file in the repository - A dry-run route reporting which engine and arguments a pair would use, which answers most questions without a conversion - Font package pinning in the image, since a font change is the most common cause of a document that converts and looks wrong ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Jobs, logs, keys and the format matrix cache. DATABASE_PATH=./data/jobs.db # Required. Per-job directories. Deleted on completion and on retention. WORK_DIR=./data/work # Required. Above this a conversion becomes a job instead of a response. INLINE_MAX_BYTES=8388608 # Required. Wall clock per conversion. A timeout is a distinct reason class. CONVERT_TIMEOUT_SECONDS=120 # Required. A small input can expand enormously, deliberately or otherwise. OUTPUT_MAX_BYTES=536870912 # Required. Job directories are removed after this whether collected or not. JOB_RETENTION_HOURS=24 # Required. The committed policy, used instead of whatever the distribution shipped. IMAGEMAGICK_POLICY_PATH=./config/imagemagick-policy.xml # Required. Each worker gets its own office-suite profile directory derived from its index. WORKER_COUNT=2
You are building a production product version of FileChanger. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # FileChanger · product brief ## Problem The conversion is other people's software. Pandoc, ImageMagick and FFmpeg do the actual work, the format list looks enormous because those three projects are enormous, and a coding agent can wire all of them behind an upload form and a REST endpoint in one sitting. What a subscription buys is not the conversion, it is somebody else running a box that eats untrusted files from strangers all day without falling over, and keeping a few hundred format pairs working after every upstream release. Point it at your own files on your own machine and most of that risk simply is not yours, which is why this one is a yes. ## Product outcome A conversion service that knows what it can do because it asked, fences every invocation, tells you what failed without telling anyone else, and fails its own build when an engine upgrade breaks a format pair. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - pandoc - FFmpeg - ImageMagick - LibreOffice headless for Office formats - Typst or a LaTeX install if you want PDF out ## Explicit non-goals for v1 - Any converter written here. - Uploads from people you do not know. - Metering, quotas and billing. - The long tail of awkward format pairs. - sandboxing, because your version runs files straight through the same toolchain as everything else on the machine - the long tail of format pairs, where the fix is usually a missing font, a filter flag or an upstream bug - queueing and timeouts for large video, which is where the compute bill actually lives - a hosted MCP server and API keys your other tools can reach from anywhere, not just from localhost - somebody else being on call when a LibreOffice or LaTeX upgrade quietly breaks PDF output ## Success criteria - Responses are confirmed to contain no path, version or command - The format matrix is confirmed discovered rather than declared - Concurrent office conversions are confirmed to both succeed - An interrupted job is confirmed marked and cleaned rather than resumed - A broken engine round trip is confirmed to fail the build ===== BRIEF.md ===== # Build brief · FileChanger The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local conversion service that shells out to the engines everyone already uses and never pretends a failed conversion worked to replace FileChanger. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: Python with a small HTTP framework, one process, no broker · a queue of one person's files does not need a message bus - Engines: Pandoc for markup, an office suite in headless mode, ImageMagick for images, FFmpeg for media · never write a converter; the format list is enormous because these projects are - State: SQLite for jobs and keys, files on disk in per-job directories · a job needs to survive a restart and a directory is the easiest thing to delete - Discovery: The format matrix built at startup by asking each binary what it supports · a hard-coded matrix is wrong the day after an upgrade - Isolation: A temporary directory, a wall-clock timeout, an output cap and no network per conversion · the input is a file whose author you did not choose ### Data model (create this before Phase 1) - engines(name, binary_path, version_string, probed_at) · the version is what explains a conversion that worked last month - formats(engine, direction, format_name, probed_at) · direction is input or output, discovered rather than typed - jobs(id, created_at, source_format, target_format, input_bytes, state, started_at, finished_at, exit_code, failure_kind, output_bytes, output_path) - job_logs(job_id, stream, content) · the command and its error output, stored and never returned over HTTP - api_keys(id, name, hash, created_at, last_used_at, revoked_at) · the plaintext is shown once and never stored - limits(name, value) · timeout seconds, maximum input bytes, maximum output bytes, per-key concurrency - The format matrix is a cache of what the installed binaries said, with the date they said it. An upgrade invalidates it, which is the point. ### Phase 1 · One engine, one round trip, one honest failure Build: One route accepting a file, a source format and a target format, invoking pandoc in a temporary directory and returning the bytes. Capture exit code, standard error and duration into the job log. Return a reason class such as unsupported pair, engine error or timeout, never the command or its error text. Delete the input whether the conversion worked or not. Trap: Returning the engine's error output to the caller is the natural debugging shortcut and it hands over absolute paths, installed versions and sometimes fragments of the document itself. Store the command and its error in the job log, return a reason class and a job id, and make the only path to the detail a local command you run yourself. Done when: A markup conversion returns correct bytes and a nonsense conversion returns a reason class with no path, version or command in the response, asserted by a test that scans the body. Input files are removed on both the success and the failure path, tested by forcing a failure. Exit code, duration and error output are stored in the job log and are reachable only locally. A conversion exceeding the timeout is reported as a timeout rather than as an engine error. Do not build yet: No format discovery, no other engines, no jobs, no keys, no MCP. ### Phase 2 · Ask the binaries what they can do Build: At startup, query each engine for its input and output formats and its version, and store both with the probe time. Serve the matrix on a route. Refuse a pair that is not in the matrix before touching a file. Re-probe on demand and log when a version string has changed since the last probe. Trap: Hard-coding a list of a few hundred format pairs produces a service that accepts a conversion the installed binary can no longer do, wastes the upload and returns an engine error that looks like a bug in your code. Ask the binaries, cache what they said with the date, and treat a version change as a reason to re-probe rather than a curiosity. Done when: The matrix is populated by probing the binaries and contains no hard-coded format list, verified by review and by removing an engine and seeing its formats vanish. A request for a pair outside the matrix is refused before the upload is read, tested. Engine version strings are stored and a change since the last probe is logged at startup. The matrix route reports the probe time so a stale matrix is visible. Do not build yet: No isolation policy work, no jobs, no keys, no MCP. ### Phase 3 · Fencing each conversion Build: Per-conversion limits on wall-clock time, input size and output size, enforced and tested. Give the image engine an explicit policy file rather than relying on the distribution's default, because that default may already disable the delegates you need and its documentation recommends distributions disable the ones that shell out to a page description interpreter. Give each office-suite worker its own user profile directory, because two instances sharing one profile is a documented way to get a silent failure; verify that behaviour on your own install before relying on it. Trap: Running two office-suite conversions at once with the default shared profile produces a job that exits without an output file and without a useful error, and because it only happens under concurrency it survives every single-file test you write. Give each worker its own profile directory derived from the worker index, and add a concurrency test that runs several conversions of the same type simultaneously. Done when: Two concurrent office-suite conversions both produce correct output, tested by running them in parallel. A conversion exceeding the output size cap is stopped and reported rather than filling the disk, tested with a deliberately expanding input. The image engine runs against a policy file committed in the repository, and a conversion requiring a disabled delegate fails with a clear reason class. A conversion attempting a network fetch fails, tested with an input referencing a remote resource. Do not build yet: No asynchronous jobs, no keys, no MCP, no operational documentation. ### Phase 4 · Big files become jobs Build: Below a configured threshold the route returns the bytes. Above it, the same route returns a job identifier, a status route reports state, and a result route streams the file. One background worker thread claims queued jobs. A job in a running state at startup is marked interrupted rather than resumed, because a half-written output is not a resumable thing. Trap: Marking an interrupted job as queued again on restart re-runs a conversion whose partial output is still on disk, and the second attempt writes over it while a client is streaming the first, which produces a corrupt download nobody can reproduce. Mark it interrupted, delete the partial output, and let the caller decide whether to ask again. Done when: A conversion under the threshold returns bytes inline and one over it returns a job identifier, tested at the boundary. Killing the process mid-job leaves that job in an interrupted state with its partial output deleted, tested. The result route streams rather than loading the file into memory, verified with a large fixture and a memory measurement. Job directories are removed after a configured retention whether or not the result was collected. Do not build yet: No keys, no MCP, no upgrade handling. ### Phase 5 · Keys, agents and the day an upgrade breaks a format Build: API keys stored hashed, shown in plaintext once, revocable. An MCP server over standard input and output exposing convert and formats as tools. A round-trip test per engine run in the suite and after every container build, so an upgrade that breaks a format pair fails the build rather than a user's conversion. One container image carrying every engine, with the versions recorded in the image. Trap: Upgrading the container image and deploying it because the tests passed is fine until an engine drops a filter or a font package changes, and then the failure arrives as a single user's broken document weeks later with no way to tell which upgrade did it. Run one real round trip per engine as part of the build, store the engine versions in the image, and compare them against the previous build in the release notes. Done when: A key is displayed once at creation and only its hash is stored, asserted by inspecting the database. A revoked key is refused immediately and the refusal is distinguishable from an unknown key only in the local log, not in the response. The MCP server exposes convert and formats and completes one conversion without going through HTTP, tested. The build fails when any engine round trip stops producing a valid output, tested by pinning a deliberately broken engine version. Do not build yet: Nothing after this. ### Out of scope (and why) - Writing any converter, since every format here belongs to a project with decades of work behind it and your version will be worse in ways you will not notice. - Accepting uploads from people you do not know, which is the actual product on the other side and needs sandboxing far beyond a temporary directory and a timeout. - Metering, quotas and billing, which exist to ration a shared machine and have no purpose on your own. - Chasing the long tail of format pairs, where the fix is usually a missing font or an upstream bug rather than anything in your code. ### README must contain - That no converter is written here and which engine handles which family, with the versions the image ships - That the format matrix is discovered from the installed binaries at startup and carries its probe time - The security assumptions: your own files, a temporary directory, a timeout, size caps and no network - That error detail is stored locally and never returned, and the local command for reading a job log - That an engine upgrade can break a format pair, and that the build runs a round trip per engine to catch it ===== ARCHITECTURE.md ===== # Architecture · FileChanger ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python with a small HTTP framework, one process, no broker | a queue of one person's files does not need a message bus | | Engines | Pandoc for markup, an office suite in headless mode, ImageMagick for images, FFmpeg for media | never write a converter; the format list is enormous because these projects are | | State | SQLite for jobs and keys, files on disk in per-job directories | a job needs to survive a restart and a directory is the easiest thing to delete | | Discovery | The format matrix built at startup by asking each binary what it supports | a hard-coded matrix is wrong the day after an upgrade | | Isolation | A temporary directory, a wall-clock timeout, an output cap and no network per conversion | the input is a file whose author you did not choose | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | API | The convert, formats, job and result routes. | Any framework, provided error detail never reaches the response. | | Engines | Probing, versions and the format matrix. | Any engine set, provided the matrix stays discovered rather than declared. | | Isolation | Temporary directories, limits, policy and worker profiles. | Any implementation, provided each worker has its own office profile. | | Jobs | Threshold, worker, interruption and retention. | Any queue, provided an interrupted job is never silently resumed. | | Access | Hashed keys, revocation and the MCP surface. | Any auth, provided the plaintext key is shown exactly once. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_PATH` · required · Jobs, logs, keys and the format matrix cache. - `WORK_DIR` · required · Per-job directories. Deleted on completion and on retention. - `INLINE_MAX_BYTES` · required · Above this a conversion becomes a job instead of a response. - `CONVERT_TIMEOUT_SECONDS` · required · Wall clock per conversion. A timeout is a distinct reason class. - `OUTPUT_MAX_BYTES` · required · A small input can expand enormously, deliberately or otherwise. - `JOB_RETENTION_HOURS` · required · Job directories are removed after this whether collected or not. - `IMAGEMAGICK_POLICY_PATH` · required · The committed policy, used instead of whatever the distribution shipped. - `WORKER_COUNT` · required · Each worker gets its own office-suite profile directory derived from its index. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · FileChanger product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Python with a small HTTP framework, one process, no broker, Pandoc for markup, an office suite in headless mode, ImageMagick for images, FFmpeg for media, SQLite for jobs and keys, files on disk in per-job directories, The format matrix built at startup by asking each binary what it supports, A temporary directory, a wall-clock timeout, an output cap and no network per conversion. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Returning the engine's error output to the caller is the natural debugging shortcut and it hands over absolute paths, installed versions and sometimes fragments of the document itself. Store the command and its error in the job log, return a reason class and a job id, and make the only path to the detail a local command you run yourself. - Hard-coding a list of a few hundred format pairs produces a service that accepts a conversion the installed binary can no longer do, wastes the upload and returns an engine error that looks like a bug in your code. Ask the binaries, cache what they said with the date, and treat a version change as a reason to re-probe rather than a curiosity. - Running two office-suite conversions at once with the default shared profile produces a job that exits without an output file and without a useful error, and because it only happens under concurrency it survives every single-file test you write. Give each worker its own profile directory derived from the worker index, and add a concurrency test that runs several conversions of the same type simultaneously. - Marking an interrupted job as queued again on restart re-runs a conversion whose partial output is still on disk, and the second attempt writes over it while a client is streaming the first, which produces a corrupt download nobody can reproduce. Mark it interrupted, delete the partial output, and let the caller decide whether to ask again. - Upgrading the container image and deploying it because the tests passed is fine until an engine drops a filter or a font package changes, and then the failure arrives as a single user's broken document weeks later with no way to tell which upgrade did it. Run one real round trip per engine as part of the build, store the engine versions in the image, and compare them against the previous build in the release notes. ===== MILESTONES.md ===== # Delivery milestones · FileChanger Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · One engine, one round trip, one honest failure A single conversion works end to end and a failed conversion is reported as failed with a reason class. ### Steps 1. Return a reason class, store the detail Error output is a description of your filesystem. Files: `src/convert/run.py`, `tests/test_no_leak.py` 2. Delete inputs in a finally block The failure path is where the leftovers come from. Files: `src/convert/workspace.py` 3. Distinguish timeout from engine error They have completely different fixes. Files: `src/convert/classify.py` ### Done when - [ ] Responses contain no path, version or command - [ ] Inputs are deleted on success and on failure - [ ] Exit code and error output are stored locally - [ ] A timeout is classified as a timeout ### Watch out - Returning the engine's error output to the caller is the natural debugging shortcut and it hands over absolute paths, installed versions and sometimes fragments of the document itself. Store the command and its error in the job log, return a reason class and a job id, and make the only path to the detail a local command you run yourself. ## M2 · Ask the binaries what they can do The supported format matrix comes from the installed engines at startup, with their versions recorded. ### Steps 1. Probe each engine and store its version The version is the first thing you will want after a surprise. Files: `src/engines/probe.py`, `tests/test_probe.py` 2. Refuse unsupported pairs before reading the upload Failing after a large upload wastes everybody's time. Files: `src/api/convert.py` 3. Report the probe time on the matrix route A cached answer should say how old it is. Files: `src/api/formats.py` ### Done when - [ ] No hard-coded format list exists - [ ] Unsupported pairs are refused before upload - [ ] Engine versions are stored and changes logged - [ ] The matrix reports its probe time ### Watch out - Hard-coding a list of a few hundred format pairs produces a service that accepts a conversion the installed binary can no longer do, wastes the upload and returns an engine error that looks like a bug in your code. Ask the binaries, cache what they said with the date, and treat a version change as a reason to re-probe rather than a curiosity. ## M3 · Fencing each conversion One conversion cannot exhaust the machine, reach the network, or be affected by another running at the same time. ### Steps 1. Give each office-suite worker its own profile A shared profile fails only under concurrency, which is where you use it. Files: `src/engines/office.py`, `tests/test_concurrent_office.py` 2. Commit an explicit image policy file The distribution default is a decision somebody else made. Files: `config/imagemagick-policy.xml` 3. Cap output size as well as input size A small input can produce an enormous output on purpose. Files: `src/convert/limits.py` ### Done when - [ ] Concurrent office conversions both succeed - [ ] The output cap stops a runaway conversion - [ ] A committed policy file governs the image engine - [ ] A remote reference in an input is refused ### Watch out - Running two office-suite conversions at once with the default shared profile produces a job that exits without an output file and without a useful error, and because it only happens under concurrency it survives every single-file test you write. Give each worker its own profile directory derived from the worker index, and add a concurrency test that runs several conversions of the same type simultaneously. ## M4 · Big files become jobs A conversion too large to answer inline becomes a job that survives a restart. ### Steps 1. Return bytes below the threshold, a job above it One route, two shapes, is easier to use than two routes. Files: `src/api/convert.py`, `tests/test_threshold.py` 2. Mark interrupted, do not resume A partial output is not a checkpoint. Files: `src/jobs/recover.py` 3. Stream the result, never load it Video is the reason this matters. Files: `src/api/result.py` ### Done when - [ ] The size threshold selects inline or job - [ ] An interrupted job is marked and cleaned - [ ] Results stream without loading into memory - [ ] Job directories expire on a retention schedule ### Watch out - Marking an interrupted job as queued again on restart re-runs a conversion whose partial output is still on disk, and the second attempt writes over it while a client is streaming the first, which produces a corrupt download nobody can reproduce. Mark it interrupted, delete the partial output, and let the caller decide whether to ask again. ## M5 · Keys, agents and the day an upgrade breaks a format (production only) Access is controlled, an agent can call it without HTTP, and an engine upgrade cannot silently change what works. ### Steps 1. Hash keys, show the plaintext once The one-time display is the whole design. Files: `src/auth/keys.py`, `tests/test_key_storage.py` 2. Expose convert and formats over MCP An agent should not have to speak HTTP to use this. Files: `src/mcp/server.py` 3. Run a round trip per engine in the build An upgrade that breaks a format should break the build. Files: `.github/workflows/build.yml`, `tests/test_round_trips.py` ### Done when - [ ] Keys are stored hashed and shown once - [ ] Revocation is immediate and uninformative in the response - [ ] MCP completes a conversion without HTTP - [ ] A broken engine round trip fails the build ### Watch out - Upgrading the container image and deploying it because the tests passed is fine until an engine drops a filter or a font package changes, and then the failure arrives as a single user's broken document weeks later with no way to tell which upgrade did it. Run one real round trip per engine as part of the build, store the engine versions in the image, and compare them against the previous build in the release notes. ===== OPERATIONS.md ===== # Operations · FileChanger ## Backup Almost nothing here is worth backing up: jobs are transient and outputs are regenerable. The two exceptions are the key table and the committed policy and configuration files, which live in the repository. ## Restore Rebuild the image, start the service, and confirm the startup probe reports the same engine versions as the image it replaced. A silently different version is how a format pair disappears. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Failure counts by reason class, the age of the format matrix probe, job directory disk usage, and the engine versions at startup. A jump in engine errors after a rebuild points straight at the upgrade. ## Incident checklist The incident is a conversion that produced a plausible but wrong file, usually from a missing font or a changed default. The stored job log with the exact command and the recorded engine versions are what let you reproduce it rather than guess. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Responses are confirmed to contain no path, version or command - [ ] The format matrix is confirmed discovered rather than declared - [ ] Concurrent office conversions are confirmed to both succeed - [ ] An interrupted job is confirmed marked and cleaned rather than resumed - [ ] A broken engine round trip is confirmed to fail the build ## Launch constraint Do not market omitted FileChanger capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Jobs, logs, keys and the format matrix cache. DATABASE_PATH=./data/jobs.db # Required. Per-job directories. Deleted on completion and on retention. WORK_DIR=./data/work # Required. Above this a conversion becomes a job instead of a response. INLINE_MAX_BYTES=8388608 # Required. Wall clock per conversion. A timeout is a distinct reason class. CONVERT_TIMEOUT_SECONDS=120 # Required. A small input can expand enormously, deliberately or otherwise. OUTPUT_MAX_BYTES=536870912 # Required. Job directories are removed after this whether collected or not. JOB_RETENTION_HOURS=24 # Required. The committed policy, used instead of whatever the distribution shipped. IMAGEMAGICK_POLICY_PATH=./config/imagemagick-policy.xml # Required. Each worker gets its own office-suite profile directory derived from its index. WORKER_COUNT=2
# FileChanger · indie build A personal conversion API whose hard problems are not the conversions, which other people's software does, but discovering what the installed engines can actually do, fencing each invocation so a bad file cannot take the machine with it, and reporting failures in a way that is useful to you and useless to an attacker. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python with a small HTTP framework, one process, no broker | a queue of one person's files does not need a message bus | | Engines | Pandoc for markup, an office suite in headless mode, ImageMagick for images, FFmpeg for media | never write a converter; the format list is enormous because these projects are | | State | SQLite for jobs and keys, files on disk in per-job directories | a job needs to survive a restart and a directory is the easiest thing to delete | | Discovery | The format matrix built at startup by asking each binary what it supports | a hard-coded matrix is wrong the day after an upgrade | | Isolation | A temporary directory, a wall-clock timeout, an output cap and no network per conversion | the input is a file whose author you did not choose | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Python 3.12 or newer** · free - Why: Runs the service, the worker and the MCP server. - Get it: python.org, or your version manager. - Verify: python3 --version prints 3.12 or higher - [ ] **Pandoc, FFmpeg, ImageMagick and a headless office suite** · free - Why: These four do all the work, and the format list you will advertise is exactly what they report. - Get it: Your package manager, or the container image this build ships. - Verify: Each binary prints a version and the startup probe lists its formats - [ ] **A PDF engine for document output** · free, but the install is large - Why: Pandoc does not produce PDF on its own; it hands off to a typesetting engine, and forgetting this is the most common first failure in a converter. - Get it: Install a supported typesetting engine and name it in the configuration. - Verify: A markup file converts to a PDF that opens - [ ] **An explicit image engine policy file** · free - Why: The distribution default may already disable the delegates you need, or may not disable ones you would rather not run, and either way it is not your decision until you commit one. - Get it: Read the image engine's own security policy documentation and write a policy covering resource limits and which coders are permitted. - Verify: The policy file is committed and the service starts with it - [ ] **A container runtime** · free - Why: Pinning every engine and its fonts in one image is what makes an upgrade a deliberate act rather than a surprise. - Get it: Any container runtime with compose support. - Verify: The image builds and the engine round trips pass inside it ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Any converter written here. - Uploads from people you do not know. - Metering, quotas and billing. - The long tail of awkward format pairs. - sandboxing, because your version runs files straight through the same toolchain as everything else on the machine - the long tail of format pairs, where the fix is usually a missing font, a filter flag or an upstream bug - queueing and timeouts for large video, which is where the compute bill actually lives - a hosted MCP server and API keys your other tools can reach from anywhere, not just from localhost - somebody else being on call when a LibreOffice or LaTeX upgrade quietly breaks PDF output If one of those is essential to you, that is the reason to keep paying for FileChanger, and the README should say so rather than pretend.
# Build brief · FileChanger The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local conversion service that shells out to the engines everyone already uses and never pretends a failed conversion worked to replace FileChanger. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: Python with a small HTTP framework, one process, no broker · a queue of one person's files does not need a message bus - Engines: Pandoc for markup, an office suite in headless mode, ImageMagick for images, FFmpeg for media · never write a converter; the format list is enormous because these projects are - State: SQLite for jobs and keys, files on disk in per-job directories · a job needs to survive a restart and a directory is the easiest thing to delete - Discovery: The format matrix built at startup by asking each binary what it supports · a hard-coded matrix is wrong the day after an upgrade - Isolation: A temporary directory, a wall-clock timeout, an output cap and no network per conversion · the input is a file whose author you did not choose ### Data model (create this before Phase 1) - engines(name, binary_path, version_string, probed_at) · the version is what explains a conversion that worked last month - formats(engine, direction, format_name, probed_at) · direction is input or output, discovered rather than typed - jobs(id, created_at, source_format, target_format, input_bytes, state, started_at, finished_at, exit_code, failure_kind, output_bytes, output_path) - job_logs(job_id, stream, content) · the command and its error output, stored and never returned over HTTP - api_keys(id, name, hash, created_at, last_used_at, revoked_at) · the plaintext is shown once and never stored - limits(name, value) · timeout seconds, maximum input bytes, maximum output bytes, per-key concurrency - The format matrix is a cache of what the installed binaries said, with the date they said it. An upgrade invalidates it, which is the point. ### Phase 1 · One engine, one round trip, one honest failure Build: One route accepting a file, a source format and a target format, invoking pandoc in a temporary directory and returning the bytes. Capture exit code, standard error and duration into the job log. Return a reason class such as unsupported pair, engine error or timeout, never the command or its error text. Delete the input whether the conversion worked or not. Trap: Returning the engine's error output to the caller is the natural debugging shortcut and it hands over absolute paths, installed versions and sometimes fragments of the document itself. Store the command and its error in the job log, return a reason class and a job id, and make the only path to the detail a local command you run yourself. Done when: A markup conversion returns correct bytes and a nonsense conversion returns a reason class with no path, version or command in the response, asserted by a test that scans the body. Input files are removed on both the success and the failure path, tested by forcing a failure. Exit code, duration and error output are stored in the job log and are reachable only locally. A conversion exceeding the timeout is reported as a timeout rather than as an engine error. Do not build yet: No format discovery, no other engines, no jobs, no keys, no MCP. ### Phase 2 · Ask the binaries what they can do Build: At startup, query each engine for its input and output formats and its version, and store both with the probe time. Serve the matrix on a route. Refuse a pair that is not in the matrix before touching a file. Re-probe on demand and log when a version string has changed since the last probe. Trap: Hard-coding a list of a few hundred format pairs produces a service that accepts a conversion the installed binary can no longer do, wastes the upload and returns an engine error that looks like a bug in your code. Ask the binaries, cache what they said with the date, and treat a version change as a reason to re-probe rather than a curiosity. Done when: The matrix is populated by probing the binaries and contains no hard-coded format list, verified by review and by removing an engine and seeing its formats vanish. A request for a pair outside the matrix is refused before the upload is read, tested. Engine version strings are stored and a change since the last probe is logged at startup. The matrix route reports the probe time so a stale matrix is visible. Do not build yet: No isolation policy work, no jobs, no keys, no MCP. ### Phase 3 · Fencing each conversion Build: Per-conversion limits on wall-clock time, input size and output size, enforced and tested. Give the image engine an explicit policy file rather than relying on the distribution's default, because that default may already disable the delegates you need and its documentation recommends distributions disable the ones that shell out to a page description interpreter. Give each office-suite worker its own user profile directory, because two instances sharing one profile is a documented way to get a silent failure; verify that behaviour on your own install before relying on it. Trap: Running two office-suite conversions at once with the default shared profile produces a job that exits without an output file and without a useful error, and because it only happens under concurrency it survives every single-file test you write. Give each worker its own profile directory derived from the worker index, and add a concurrency test that runs several conversions of the same type simultaneously. Done when: Two concurrent office-suite conversions both produce correct output, tested by running them in parallel. A conversion exceeding the output size cap is stopped and reported rather than filling the disk, tested with a deliberately expanding input. The image engine runs against a policy file committed in the repository, and a conversion requiring a disabled delegate fails with a clear reason class. A conversion attempting a network fetch fails, tested with an input referencing a remote resource. Do not build yet: No asynchronous jobs, no keys, no MCP, no operational documentation. ### Phase 4 · Big files become jobs Build: Below a configured threshold the route returns the bytes. Above it, the same route returns a job identifier, a status route reports state, and a result route streams the file. One background worker thread claims queued jobs. A job in a running state at startup is marked interrupted rather than resumed, because a half-written output is not a resumable thing. Trap: Marking an interrupted job as queued again on restart re-runs a conversion whose partial output is still on disk, and the second attempt writes over it while a client is streaming the first, which produces a corrupt download nobody can reproduce. Mark it interrupted, delete the partial output, and let the caller decide whether to ask again. Done when: A conversion under the threshold returns bytes inline and one over it returns a job identifier, tested at the boundary. Killing the process mid-job leaves that job in an interrupted state with its partial output deleted, tested. The result route streams rather than loading the file into memory, verified with a large fixture and a memory measurement. Job directories are removed after a configured retention whether or not the result was collected. Do not build yet: No keys, no MCP, no upgrade handling. ### Phase 5 · Keys, agents and the day an upgrade breaks a format Build: API keys stored hashed, shown in plaintext once, revocable. An MCP server over standard input and output exposing convert and formats as tools. A round-trip test per engine run in the suite and after every container build, so an upgrade that breaks a format pair fails the build rather than a user's conversion. One container image carrying every engine, with the versions recorded in the image. Trap: Upgrading the container image and deploying it because the tests passed is fine until an engine drops a filter or a font package changes, and then the failure arrives as a single user's broken document weeks later with no way to tell which upgrade did it. Run one real round trip per engine as part of the build, store the engine versions in the image, and compare them against the previous build in the release notes. Done when: A key is displayed once at creation and only its hash is stored, asserted by inspecting the database. A revoked key is refused immediately and the refusal is distinguishable from an unknown key only in the local log, not in the response. The MCP server exposes convert and formats and completes one conversion without going through HTTP, tested. The build fails when any engine round trip stops producing a valid output, tested by pinning a deliberately broken engine version. Do not build yet: Nothing after this. ### Out of scope (and why) - Writing any converter, since every format here belongs to a project with decades of work behind it and your version will be worse in ways you will not notice. - Accepting uploads from people you do not know, which is the actual product on the other side and needs sandboxing far beyond a temporary directory and a timeout. - Metering, quotas and billing, which exist to ration a shared machine and have no purpose on your own. - Chasing the long tail of format pairs, where the fix is usually a missing font or an upstream bug rather than anything in your code. ### README must contain - That no converter is written here and which engine handles which family, with the versions the image ships - That the format matrix is discovered from the installed binaries at startup and carries its probe time - The security assumptions: your own files, a temporary directory, a timeout, size caps and no network - That error detail is stored locally and never returned, and the local command for reading a job log - That an engine upgrade can break a format pair, and that the build runs a round trip per engine to catch it
# Agent instructions · FileChanger indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Python with a small HTTP framework, one process, no broker, Pandoc for markup, an office suite in headless mode, ImageMagick for images, FFmpeg for media, SQLite for jobs and keys, files on disk in per-job directories, The format matrix built at startup by asking each binary what it supports, A temporary directory, a wall-clock timeout, an output cap and no network per conversion. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Returning the engine's error output to the caller is the natural debugging shortcut and it hands over absolute paths, installed versions and sometimes fragments of the document itself. Store the command and its error in the job log, return a reason class and a job id, and make the only path to the detail a local command you run yourself. - Hard-coding a list of a few hundred format pairs produces a service that accepts a conversion the installed binary can no longer do, wastes the upload and returns an engine error that looks like a bug in your code. Ask the binaries, cache what they said with the date, and treat a version change as a reason to re-probe rather than a curiosity. - Running two office-suite conversions at once with the default shared profile produces a job that exits without an output file and without a useful error, and because it only happens under concurrency it survives every single-file test you write. Give each worker its own profile directory derived from the worker index, and add a concurrency test that runs several conversions of the same type simultaneously. - Marking an interrupted job as queued again on restart re-runs a conversion whose partial output is still on disk, and the second attempt writes over it while a client is streaming the first, which produces a corrupt download nobody can reproduce. Mark it interrupted, delete the partial output, and let the caller decide whether to ask again.
# Build plan · FileChanger A personal conversion API whose hard problems are not the conversions, which other people's software does, but discovering what the installed engines can actually do, fencing each invocation so a bad file cannot take the machine with it, and reporting failures in a way that is useful to you and useless to an attacker. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · One engine, one round trip, one honest failure A single conversion works end to end and a failed conversion is reported as failed with a reason class. ### Steps 1. Return a reason class, store the detail Error output is a description of your filesystem. Files: `src/convert/run.py`, `tests/test_no_leak.py` 2. Delete inputs in a finally block The failure path is where the leftovers come from. Files: `src/convert/workspace.py` 3. Distinguish timeout from engine error They have completely different fixes. Files: `src/convert/classify.py` ### Done when - [ ] Responses contain no path, version or command - [ ] Inputs are deleted on success and on failure - [ ] Exit code and error output are stored locally - [ ] A timeout is classified as a timeout ### Watch out - Returning the engine's error output to the caller is the natural debugging shortcut and it hands over absolute paths, installed versions and sometimes fragments of the document itself. Store the command and its error in the job log, return a reason class and a job id, and make the only path to the detail a local command you run yourself. ## Phase 2 · Ask the binaries what they can do The supported format matrix comes from the installed engines at startup, with their versions recorded. ### Steps 1. Probe each engine and store its version The version is the first thing you will want after a surprise. Files: `src/engines/probe.py`, `tests/test_probe.py` 2. Refuse unsupported pairs before reading the upload Failing after a large upload wastes everybody's time. Files: `src/api/convert.py` 3. Report the probe time on the matrix route A cached answer should say how old it is. Files: `src/api/formats.py` ### Done when - [ ] No hard-coded format list exists - [ ] Unsupported pairs are refused before upload - [ ] Engine versions are stored and changes logged - [ ] The matrix reports its probe time ### Watch out - Hard-coding a list of a few hundred format pairs produces a service that accepts a conversion the installed binary can no longer do, wastes the upload and returns an engine error that looks like a bug in your code. Ask the binaries, cache what they said with the date, and treat a version change as a reason to re-probe rather than a curiosity. ## Phase 3 · Fencing each conversion One conversion cannot exhaust the machine, reach the network, or be affected by another running at the same time. ### Steps 1. Give each office-suite worker its own profile A shared profile fails only under concurrency, which is where you use it. Files: `src/engines/office.py`, `tests/test_concurrent_office.py` 2. Commit an explicit image policy file The distribution default is a decision somebody else made. Files: `config/imagemagick-policy.xml` 3. Cap output size as well as input size A small input can produce an enormous output on purpose. Files: `src/convert/limits.py` ### Done when - [ ] Concurrent office conversions both succeed - [ ] The output cap stops a runaway conversion - [ ] A committed policy file governs the image engine - [ ] A remote reference in an input is refused ### Watch out - Running two office-suite conversions at once with the default shared profile produces a job that exits without an output file and without a useful error, and because it only happens under concurrency it survives every single-file test you write. Give each worker its own profile directory derived from the worker index, and add a concurrency test that runs several conversions of the same type simultaneously. ## Phase 4 · Big files become jobs A conversion too large to answer inline becomes a job that survives a restart. ### Steps 1. Return bytes below the threshold, a job above it One route, two shapes, is easier to use than two routes. Files: `src/api/convert.py`, `tests/test_threshold.py` 2. Mark interrupted, do not resume A partial output is not a checkpoint. Files: `src/jobs/recover.py` 3. Stream the result, never load it Video is the reason this matters. Files: `src/api/result.py` ### Done when - [ ] The size threshold selects inline or job - [ ] An interrupted job is marked and cleaned - [ ] Results stream without loading into memory - [ ] Job directories expire on a retention schedule ### Watch out - Marking an interrupted job as queued again on restart re-runs a conversion whose partial output is still on disk, and the second attempt writes over it while a client is streaming the first, which produces a corrupt download nobody can reproduce. Mark it interrupted, delete the partial output, and let the caller decide whether to ask again. ## Not in this build - Any converter written here. - Uploads from people you do not know. - Metering, quotas and billing. - The long tail of awkward format pairs. ## After v1, if you want it - A per-pair round trip fixture set, grown one entry at a time whenever something breaks, which becomes the most valuable file in the repository - A dry-run route reporting which engine and arguments a pair would use, which answers most questions without a conversion - Font package pinning in the image, since a font change is the most common cause of a document that converts and looks wrong
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Jobs, logs, keys and the format matrix cache. DATABASE_PATH=./data/jobs.db # Required. Per-job directories. Deleted on completion and on retention. WORK_DIR=./data/work # Required. Above this a conversion becomes a job instead of a response. INLINE_MAX_BYTES=8388608 # Required. Wall clock per conversion. A timeout is a distinct reason class. CONVERT_TIMEOUT_SECONDS=120 # Required. A small input can expand enormously, deliberately or otherwise. OUTPUT_MAX_BYTES=536870912 # Required. Job directories are removed after this whether collected or not. JOB_RETENTION_HOURS=24 # Required. The committed policy, used instead of whatever the distribution shipped. IMAGEMAGICK_POLICY_PATH=./config/imagemagick-policy.xml # Required. Each worker gets its own office-suite profile directory derived from its index. WORKER_COUNT=2
# FileChanger · product brief ## Problem The conversion is other people's software. Pandoc, ImageMagick and FFmpeg do the actual work, the format list looks enormous because those three projects are enormous, and a coding agent can wire all of them behind an upload form and a REST endpoint in one sitting. What a subscription buys is not the conversion, it is somebody else running a box that eats untrusted files from strangers all day without falling over, and keeping a few hundred format pairs working after every upstream release. Point it at your own files on your own machine and most of that risk simply is not yours, which is why this one is a yes. ## Product outcome A conversion service that knows what it can do because it asked, fences every invocation, tells you what failed without telling anyone else, and fails its own build when an engine upgrade breaks a format pair. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - pandoc - FFmpeg - ImageMagick - LibreOffice headless for Office formats - Typst or a LaTeX install if you want PDF out ## Explicit non-goals for v1 - Any converter written here. - Uploads from people you do not know. - Metering, quotas and billing. - The long tail of awkward format pairs. - sandboxing, because your version runs files straight through the same toolchain as everything else on the machine - the long tail of format pairs, where the fix is usually a missing font, a filter flag or an upstream bug - queueing and timeouts for large video, which is where the compute bill actually lives - a hosted MCP server and API keys your other tools can reach from anywhere, not just from localhost - somebody else being on call when a LibreOffice or LaTeX upgrade quietly breaks PDF output ## Success criteria - Responses are confirmed to contain no path, version or command - The format matrix is confirmed discovered rather than declared - Concurrent office conversions are confirmed to both succeed - An interrupted job is confirmed marked and cleaned rather than resumed - A broken engine round trip is confirmed to fail the build
# Build brief · FileChanger The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local conversion service that shells out to the engines everyone already uses and never pretends a failed conversion worked to replace FileChanger. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: Python with a small HTTP framework, one process, no broker · a queue of one person's files does not need a message bus - Engines: Pandoc for markup, an office suite in headless mode, ImageMagick for images, FFmpeg for media · never write a converter; the format list is enormous because these projects are - State: SQLite for jobs and keys, files on disk in per-job directories · a job needs to survive a restart and a directory is the easiest thing to delete - Discovery: The format matrix built at startup by asking each binary what it supports · a hard-coded matrix is wrong the day after an upgrade - Isolation: A temporary directory, a wall-clock timeout, an output cap and no network per conversion · the input is a file whose author you did not choose ### Data model (create this before Phase 1) - engines(name, binary_path, version_string, probed_at) · the version is what explains a conversion that worked last month - formats(engine, direction, format_name, probed_at) · direction is input or output, discovered rather than typed - jobs(id, created_at, source_format, target_format, input_bytes, state, started_at, finished_at, exit_code, failure_kind, output_bytes, output_path) - job_logs(job_id, stream, content) · the command and its error output, stored and never returned over HTTP - api_keys(id, name, hash, created_at, last_used_at, revoked_at) · the plaintext is shown once and never stored - limits(name, value) · timeout seconds, maximum input bytes, maximum output bytes, per-key concurrency - The format matrix is a cache of what the installed binaries said, with the date they said it. An upgrade invalidates it, which is the point. ### Phase 1 · One engine, one round trip, one honest failure Build: One route accepting a file, a source format and a target format, invoking pandoc in a temporary directory and returning the bytes. Capture exit code, standard error and duration into the job log. Return a reason class such as unsupported pair, engine error or timeout, never the command or its error text. Delete the input whether the conversion worked or not. Trap: Returning the engine's error output to the caller is the natural debugging shortcut and it hands over absolute paths, installed versions and sometimes fragments of the document itself. Store the command and its error in the job log, return a reason class and a job id, and make the only path to the detail a local command you run yourself. Done when: A markup conversion returns correct bytes and a nonsense conversion returns a reason class with no path, version or command in the response, asserted by a test that scans the body. Input files are removed on both the success and the failure path, tested by forcing a failure. Exit code, duration and error output are stored in the job log and are reachable only locally. A conversion exceeding the timeout is reported as a timeout rather than as an engine error. Do not build yet: No format discovery, no other engines, no jobs, no keys, no MCP. ### Phase 2 · Ask the binaries what they can do Build: At startup, query each engine for its input and output formats and its version, and store both with the probe time. Serve the matrix on a route. Refuse a pair that is not in the matrix before touching a file. Re-probe on demand and log when a version string has changed since the last probe. Trap: Hard-coding a list of a few hundred format pairs produces a service that accepts a conversion the installed binary can no longer do, wastes the upload and returns an engine error that looks like a bug in your code. Ask the binaries, cache what they said with the date, and treat a version change as a reason to re-probe rather than a curiosity. Done when: The matrix is populated by probing the binaries and contains no hard-coded format list, verified by review and by removing an engine and seeing its formats vanish. A request for a pair outside the matrix is refused before the upload is read, tested. Engine version strings are stored and a change since the last probe is logged at startup. The matrix route reports the probe time so a stale matrix is visible. Do not build yet: No isolation policy work, no jobs, no keys, no MCP. ### Phase 3 · Fencing each conversion Build: Per-conversion limits on wall-clock time, input size and output size, enforced and tested. Give the image engine an explicit policy file rather than relying on the distribution's default, because that default may already disable the delegates you need and its documentation recommends distributions disable the ones that shell out to a page description interpreter. Give each office-suite worker its own user profile directory, because two instances sharing one profile is a documented way to get a silent failure; verify that behaviour on your own install before relying on it. Trap: Running two office-suite conversions at once with the default shared profile produces a job that exits without an output file and without a useful error, and because it only happens under concurrency it survives every single-file test you write. Give each worker its own profile directory derived from the worker index, and add a concurrency test that runs several conversions of the same type simultaneously. Done when: Two concurrent office-suite conversions both produce correct output, tested by running them in parallel. A conversion exceeding the output size cap is stopped and reported rather than filling the disk, tested with a deliberately expanding input. The image engine runs against a policy file committed in the repository, and a conversion requiring a disabled delegate fails with a clear reason class. A conversion attempting a network fetch fails, tested with an input referencing a remote resource. Do not build yet: No asynchronous jobs, no keys, no MCP, no operational documentation. ### Phase 4 · Big files become jobs Build: Below a configured threshold the route returns the bytes. Above it, the same route returns a job identifier, a status route reports state, and a result route streams the file. One background worker thread claims queued jobs. A job in a running state at startup is marked interrupted rather than resumed, because a half-written output is not a resumable thing. Trap: Marking an interrupted job as queued again on restart re-runs a conversion whose partial output is still on disk, and the second attempt writes over it while a client is streaming the first, which produces a corrupt download nobody can reproduce. Mark it interrupted, delete the partial output, and let the caller decide whether to ask again. Done when: A conversion under the threshold returns bytes inline and one over it returns a job identifier, tested at the boundary. Killing the process mid-job leaves that job in an interrupted state with its partial output deleted, tested. The result route streams rather than loading the file into memory, verified with a large fixture and a memory measurement. Job directories are removed after a configured retention whether or not the result was collected. Do not build yet: No keys, no MCP, no upgrade handling. ### Phase 5 · Keys, agents and the day an upgrade breaks a format Build: API keys stored hashed, shown in plaintext once, revocable. An MCP server over standard input and output exposing convert and formats as tools. A round-trip test per engine run in the suite and after every container build, so an upgrade that breaks a format pair fails the build rather than a user's conversion. One container image carrying every engine, with the versions recorded in the image. Trap: Upgrading the container image and deploying it because the tests passed is fine until an engine drops a filter or a font package changes, and then the failure arrives as a single user's broken document weeks later with no way to tell which upgrade did it. Run one real round trip per engine as part of the build, store the engine versions in the image, and compare them against the previous build in the release notes. Done when: A key is displayed once at creation and only its hash is stored, asserted by inspecting the database. A revoked key is refused immediately and the refusal is distinguishable from an unknown key only in the local log, not in the response. The MCP server exposes convert and formats and completes one conversion without going through HTTP, tested. The build fails when any engine round trip stops producing a valid output, tested by pinning a deliberately broken engine version. Do not build yet: Nothing after this. ### Out of scope (and why) - Writing any converter, since every format here belongs to a project with decades of work behind it and your version will be worse in ways you will not notice. - Accepting uploads from people you do not know, which is the actual product on the other side and needs sandboxing far beyond a temporary directory and a timeout. - Metering, quotas and billing, which exist to ration a shared machine and have no purpose on your own. - Chasing the long tail of format pairs, where the fix is usually a missing font or an upstream bug rather than anything in your code. ### README must contain - That no converter is written here and which engine handles which family, with the versions the image ships - That the format matrix is discovered from the installed binaries at startup and carries its probe time - The security assumptions: your own files, a temporary directory, a timeout, size caps and no network - That error detail is stored locally and never returned, and the local command for reading a job log - That an engine upgrade can break a format pair, and that the build runs a round trip per engine to catch it
# Architecture · FileChanger ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python with a small HTTP framework, one process, no broker | a queue of one person's files does not need a message bus | | Engines | Pandoc for markup, an office suite in headless mode, ImageMagick for images, FFmpeg for media | never write a converter; the format list is enormous because these projects are | | State | SQLite for jobs and keys, files on disk in per-job directories | a job needs to survive a restart and a directory is the easiest thing to delete | | Discovery | The format matrix built at startup by asking each binary what it supports | a hard-coded matrix is wrong the day after an upgrade | | Isolation | A temporary directory, a wall-clock timeout, an output cap and no network per conversion | the input is a file whose author you did not choose | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | API | The convert, formats, job and result routes. | Any framework, provided error detail never reaches the response. | | Engines | Probing, versions and the format matrix. | Any engine set, provided the matrix stays discovered rather than declared. | | Isolation | Temporary directories, limits, policy and worker profiles. | Any implementation, provided each worker has its own office profile. | | Jobs | Threshold, worker, interruption and retention. | Any queue, provided an interrupted job is never silently resumed. | | Access | Hashed keys, revocation and the MCP surface. | Any auth, provided the plaintext key is shown exactly once. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_PATH` · required · Jobs, logs, keys and the format matrix cache. - `WORK_DIR` · required · Per-job directories. Deleted on completion and on retention. - `INLINE_MAX_BYTES` · required · Above this a conversion becomes a job instead of a response. - `CONVERT_TIMEOUT_SECONDS` · required · Wall clock per conversion. A timeout is a distinct reason class. - `OUTPUT_MAX_BYTES` · required · A small input can expand enormously, deliberately or otherwise. - `JOB_RETENTION_HOURS` · required · Job directories are removed after this whether collected or not. - `IMAGEMAGICK_POLICY_PATH` · required · The committed policy, used instead of whatever the distribution shipped. - `WORKER_COUNT` · required · Each worker gets its own office-suite profile directory derived from its index. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · FileChanger product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Python with a small HTTP framework, one process, no broker, Pandoc for markup, an office suite in headless mode, ImageMagick for images, FFmpeg for media, SQLite for jobs and keys, files on disk in per-job directories, The format matrix built at startup by asking each binary what it supports, A temporary directory, a wall-clock timeout, an output cap and no network per conversion. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Returning the engine's error output to the caller is the natural debugging shortcut and it hands over absolute paths, installed versions and sometimes fragments of the document itself. Store the command and its error in the job log, return a reason class and a job id, and make the only path to the detail a local command you run yourself. - Hard-coding a list of a few hundred format pairs produces a service that accepts a conversion the installed binary can no longer do, wastes the upload and returns an engine error that looks like a bug in your code. Ask the binaries, cache what they said with the date, and treat a version change as a reason to re-probe rather than a curiosity. - Running two office-suite conversions at once with the default shared profile produces a job that exits without an output file and without a useful error, and because it only happens under concurrency it survives every single-file test you write. Give each worker its own profile directory derived from the worker index, and add a concurrency test that runs several conversions of the same type simultaneously. - Marking an interrupted job as queued again on restart re-runs a conversion whose partial output is still on disk, and the second attempt writes over it while a client is streaming the first, which produces a corrupt download nobody can reproduce. Mark it interrupted, delete the partial output, and let the caller decide whether to ask again. - Upgrading the container image and deploying it because the tests passed is fine until an engine drops a filter or a font package changes, and then the failure arrives as a single user's broken document weeks later with no way to tell which upgrade did it. Run one real round trip per engine as part of the build, store the engine versions in the image, and compare them against the previous build in the release notes.
# Delivery milestones · FileChanger Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · One engine, one round trip, one honest failure A single conversion works end to end and a failed conversion is reported as failed with a reason class. ### Steps 1. Return a reason class, store the detail Error output is a description of your filesystem. Files: `src/convert/run.py`, `tests/test_no_leak.py` 2. Delete inputs in a finally block The failure path is where the leftovers come from. Files: `src/convert/workspace.py` 3. Distinguish timeout from engine error They have completely different fixes. Files: `src/convert/classify.py` ### Done when - [ ] Responses contain no path, version or command - [ ] Inputs are deleted on success and on failure - [ ] Exit code and error output are stored locally - [ ] A timeout is classified as a timeout ### Watch out - Returning the engine's error output to the caller is the natural debugging shortcut and it hands over absolute paths, installed versions and sometimes fragments of the document itself. Store the command and its error in the job log, return a reason class and a job id, and make the only path to the detail a local command you run yourself. ## M2 · Ask the binaries what they can do The supported format matrix comes from the installed engines at startup, with their versions recorded. ### Steps 1. Probe each engine and store its version The version is the first thing you will want after a surprise. Files: `src/engines/probe.py`, `tests/test_probe.py` 2. Refuse unsupported pairs before reading the upload Failing after a large upload wastes everybody's time. Files: `src/api/convert.py` 3. Report the probe time on the matrix route A cached answer should say how old it is. Files: `src/api/formats.py` ### Done when - [ ] No hard-coded format list exists - [ ] Unsupported pairs are refused before upload - [ ] Engine versions are stored and changes logged - [ ] The matrix reports its probe time ### Watch out - Hard-coding a list of a few hundred format pairs produces a service that accepts a conversion the installed binary can no longer do, wastes the upload and returns an engine error that looks like a bug in your code. Ask the binaries, cache what they said with the date, and treat a version change as a reason to re-probe rather than a curiosity. ## M3 · Fencing each conversion One conversion cannot exhaust the machine, reach the network, or be affected by another running at the same time. ### Steps 1. Give each office-suite worker its own profile A shared profile fails only under concurrency, which is where you use it. Files: `src/engines/office.py`, `tests/test_concurrent_office.py` 2. Commit an explicit image policy file The distribution default is a decision somebody else made. Files: `config/imagemagick-policy.xml` 3. Cap output size as well as input size A small input can produce an enormous output on purpose. Files: `src/convert/limits.py` ### Done when - [ ] Concurrent office conversions both succeed - [ ] The output cap stops a runaway conversion - [ ] A committed policy file governs the image engine - [ ] A remote reference in an input is refused ### Watch out - Running two office-suite conversions at once with the default shared profile produces a job that exits without an output file and without a useful error, and because it only happens under concurrency it survives every single-file test you write. Give each worker its own profile directory derived from the worker index, and add a concurrency test that runs several conversions of the same type simultaneously. ## M4 · Big files become jobs A conversion too large to answer inline becomes a job that survives a restart. ### Steps 1. Return bytes below the threshold, a job above it One route, two shapes, is easier to use than two routes. Files: `src/api/convert.py`, `tests/test_threshold.py` 2. Mark interrupted, do not resume A partial output is not a checkpoint. Files: `src/jobs/recover.py` 3. Stream the result, never load it Video is the reason this matters. Files: `src/api/result.py` ### Done when - [ ] The size threshold selects inline or job - [ ] An interrupted job is marked and cleaned - [ ] Results stream without loading into memory - [ ] Job directories expire on a retention schedule ### Watch out - Marking an interrupted job as queued again on restart re-runs a conversion whose partial output is still on disk, and the second attempt writes over it while a client is streaming the first, which produces a corrupt download nobody can reproduce. Mark it interrupted, delete the partial output, and let the caller decide whether to ask again. ## M5 · Keys, agents and the day an upgrade breaks a format (production only) Access is controlled, an agent can call it without HTTP, and an engine upgrade cannot silently change what works. ### Steps 1. Hash keys, show the plaintext once The one-time display is the whole design. Files: `src/auth/keys.py`, `tests/test_key_storage.py` 2. Expose convert and formats over MCP An agent should not have to speak HTTP to use this. Files: `src/mcp/server.py` 3. Run a round trip per engine in the build An upgrade that breaks a format should break the build. Files: `.github/workflows/build.yml`, `tests/test_round_trips.py` ### Done when - [ ] Keys are stored hashed and shown once - [ ] Revocation is immediate and uninformative in the response - [ ] MCP completes a conversion without HTTP - [ ] A broken engine round trip fails the build ### Watch out - Upgrading the container image and deploying it because the tests passed is fine until an engine drops a filter or a font package changes, and then the failure arrives as a single user's broken document weeks later with no way to tell which upgrade did it. Run one real round trip per engine as part of the build, store the engine versions in the image, and compare them against the previous build in the release notes.
# Operations · FileChanger ## Backup Almost nothing here is worth backing up: jobs are transient and outputs are regenerable. The two exceptions are the key table and the committed policy and configuration files, which live in the repository. ## Restore Rebuild the image, start the service, and confirm the startup probe reports the same engine versions as the image it replaced. A silently different version is how a format pair disappears. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Failure counts by reason class, the age of the format matrix probe, job directory disk usage, and the engine versions at startup. A jump in engine errors after a rebuild points straight at the upgrade. ## Incident checklist The incident is a conversion that produced a plausible but wrong file, usually from a missing font or a changed default. The stored job log with the exact command and the recorded engine versions are what let you reproduce it rather than guess. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Responses are confirmed to contain no path, version or command - [ ] The format matrix is confirmed discovered rather than declared - [ ] Concurrent office conversions are confirmed to both succeed - [ ] An interrupted job is confirmed marked and cleaned rather than resumed - [ ] A broken engine round trip is confirmed to fail the build ## Launch constraint Do not market omitted FileChanger capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Jobs, logs, keys and the format matrix cache. DATABASE_PATH=./data/jobs.db # Required. Per-job directories. Deleted on completion and on retention. WORK_DIR=./data/work # Required. Above this a conversion becomes a job instead of a response. INLINE_MAX_BYTES=8388608 # Required. Wall clock per conversion. A timeout is a distinct reason class. CONVERT_TIMEOUT_SECONDS=120 # Required. A small input can expand enormously, deliberately or otherwise. OUTPUT_MAX_BYTES=536870912 # Required. Job directories are removed after this whether collected or not. JOB_RETENTION_HOURS=24 # Required. The committed policy, used instead of whatever the distribution shipped. IMAGEMAGICK_POLICY_PATH=./config/imagemagick-policy.xml # Required. Each worker gets its own office-suite profile directory derived from its index. WORKER_COUNT=2
$ choose a build depth, inspect the files, then open the complete pack in your agent
Nobody pays for the conversions that work. They pay for the awkward ones: the DOCX with tracked changes, the HEIC nobody can open, the MXF from a camera, the 4 GB video that has to finish while a browser tab is still open. Doing those reliably means keeping a toolchain installed, patched and fenced off from the files it is fed, and doing it for other people means doing it while strangers upload whatever they like. That upkeep is the product. The convert button is a weekend.
xsandboxing, because your version runs files straight through the same toolchain as everything else on the machine
xthe long tail of format pairs, where the fix is usually a missing font, a filter flag or an upstream bug
xqueueing and timeouts for large video, which is where the compute bill actually lives
xa hosted MCP server and API keys your other tools can reach from anywhere, not just from localhost
xsomebody else being on call when a LibreOffice or LaTeX upgrade quietly breaks PDF output
Don't feel like building it? These folks already made it free.
all 3 free alternatives to FileChanger →· no votes, no pay-to-list · just what's real
FileChanger pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free | $0 | $0 | 200 MB documents + 200 MB images + 200 MB audio + 50 MB video per calendar month; about 200 document conversions; API keys and MCP access |
| starter | $39 | — | Per month: 100 GB documents, 100 GB images, 50 GB audio, 10 GB video; about 100,000 document conversions; API + MCP |
| pro | $99 | — | Per month: 350 GB documents, 350 GB images, 200 GB audio, 50 GB video; about 350,000 document conversions; API + MCP |
free tier200 MB documents, 200 MB images, 200 MB audio and 50 MB video per calendar month; about 200 document conversions; API and MCP included
billingmonthly only; change or cancel any time through Polar
hidden costsAllowances are separate by format group and meter input bytes, not output size; no annual plan is published
verified 2026-08-13 · source ↗
Is FileChanger free?
The free tier is real and permanent: 200 MB each of documents, images and audio per month, 50 MB of video, and API keys plus MCP access included. Paid is Starter at $39/mo (checked 2026-08-07).
Vibecode FileChanger
Yes. A competent AI coding agent (Claude Code, Codex, Cursor) can build a usable personal FileChanger replacement in one session with the prompt on this page. It runs on your own machine or server with no subscription.
How much does FileChanger cost?
FileChanger costs about $39/month (Starter, checked 2026-08-07), which is $468 per year. That's what you save by replacing it with one prompt.
What do I lose by replacing FileChanger?
Honestly: sandboxing, because your version runs files straight through the same toolchain as everything else on the machine; the long tail of format pairs, where the fix is usually a missing font, a filter flag or an upstream bug; queueing and timeouts for large video, which is where the compute bill actually lives; a hosted MCP server and API keys your other tools can reach from anywhere, not just from localhost; somebody else being on call when a LibreOffice or LaTeX upgrade quietly breaks PDF output. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to FileChanger?
Yes: Pandoc (If the job is only documents, the paid service is a web form on top of this. Installer, twenty years of releases, and no upload step.) ConvertX (A self-hosted web converter that wraps the same engines, with accounts and a job list already built. One container and you own the whole pipeline.) VERT (Converts images and documents in the browser tab itself, so the file never leaves the machine. Video is the exception and needs their server or yours.) All 3 curated free alternatives are at vibecodeit.com/filechanger/alternatives. The prompt is for when you want it exactly your way.