What the agent does
The agent clones the repository into its own sandbox and maps the JavaScript before it renames anything. In an Express API like hagopj13/node-express-boilerplate, package.json has "main": "src/index.js", Express ^4.17.1, Mongoose ^5.7.7, Passport with passport-jwt, Joi ^17.3.0 for validation, Winston for logging and pm2 in ecosystem.config.json for production, with "node": ">=12.0.0" in engines and a yarn.lock. The code is 48 .js files under src/ and tests/: src/app.js and src/index.js, seven files in src/config/, two controllers, four middlewares, two Mongoose models with two plugins, three route files under src/routes/v1/, four services, three utils and three validation modules.
Then it reads how the tests run, because they are what keeps the conversion honest. The test script is jest -i --colors --verbose --detectOpenHandles, jest.config.js sets testEnvironment to node with NODE_ENV=test, and tests/integration/ holds supertest suites for auth, users and docs that hit a real MongoDB through tests/utils/setupTestDB.js. src/config/config.js validates the environment with Joi and appends -test to MONGODB_URL under test. The agent starts MongoDB in the sandbox, runs the suite once in JavaScript, and keeps that result as the baseline.
The conversion goes one layer at a time, in dependency order: config first, then utils, models and their plugins, services, middlewares, validations, controllers, routes, and finally app and index. The agent adds typescript, ts-jest, @types/express, @types/jest, @types/supertest and the other @types packages at versions matching the lockfile, writes a tsconfig.json with strict: true, outDir: dist and esModuleInterop, and after each layer runs npx tsc --noEmit until it is clean and the Jest suite until it passes. Real interfaces replace guesses: the User document type from src/models/user.model.js with its isPasswordMatch method and isEmailTaken static, the ApiError class in src/utils/ApiError.js with its statusCode and isOperational, and typed request handlers for catchAsync.
It types code as it is rather than changing behavior. The toJSON and paginate plugins, the pick helper and the role map in src/config/roles.js keep their shapes; where a value is genuinely untyped, such as the Joi-validated environment, the agent writes the interface the schema implies. The start script, ecosystem.config.json and the Dockerfile move from src/index.js to the compiled dist/index.js, with a build script added. The ESLint setup, which is eslint:recommended through airbnb-base with the jest, security and prettier plugins in .eslintrc.json, gets the TypeScript parser and plugin so yarn lint still runs over the converted files.
Start it with one API call
POST https://api.prix.dev/o/{org}/sessions with this body:
{
"agent": "claude",
"input": "Convert this Express codebase from JavaScript to TypeScript with strict mode. First list every .js file by directory, the module style, the Node version in engines and CI, the package manager from the lockfile, and the test command and what it needs to run. Start the database the tests need in the sandbox and run the existing test suite once as the baseline. Add typescript, ts-jest and the @types packages for the dependencies at versions matching the lockfile, and a tsconfig.json with strict true, esModuleInterop and an outDir; add a build script and point the start script, the process manager config and the Dockerfile at the compiled output. Convert one directory at a time in dependency order, config before utils before models before services before middlewares before routes before the entry point, with real interfaces for documents, request handlers, errors and config instead of any, and run npx tsc --noEmit and the test suite after each directory until both are clean. Update the ESLint config so lint still runs over .ts files. Do not change behavior: type existing code as it is and list anything you think should change separately. Open a pull request with the final tsc output, the test results, and the count of any and @ts-expect-error left with the reason for each.",
"resources": [
{
"url": "https://github.com/your-org/your-app"
}
]
}- Get an API key. In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
- Send the request. POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
- Read what came back. Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.
What comes back
What comes back is a pull request on its own branch: tsconfig.json, the new dev dependencies in package.json and yarn.lock, every file under src/ and tests/ renamed from .js to .ts so GitHub shows each as a rename with edits, and the build changes in the start script, ecosystem.config.json, Dockerfile and .eslintrc.json. New type files sit beside the code they describe, such as a User document interface next to the model.
The description carries the last npx tsc --noEmit output, which should be empty, the Jest run with the same tests passing as the JavaScript baseline, and a table of every any and @ts-expect-error left with the file, line and reason. Anything the agent thinks should change in behavior, such as a handler that swallows an error, is listed separately rather than fixed, so the diff stays a conversion.
A reviewer checks that the test count matches the baseline, that the start path in ecosystem.config.json and the Dockerfile points at dist/, and reads the any table to decide what to tighten later. The session read returns the repository and the pull request, and the transcript holds every tsc and Jest run in the order the agent made them.
Run it on every directory still in JavaScript
A large codebase converts better in slices than in one pull request. Start one run per directory from your server, with a prompt that names the directory and stops there, and each run opens a pull request a reviewer can read in one sitting. Because every run makes its own branch in its own sandbox, the config and utils slices can run at once; routes waits for controllers to merge, and a follow-up message to a finished session continues in the same sandbox with the merged code.
With a webhook_url on the key, each run.completed event names the session to read the pull request from, so the next slice can be started when the previous one merges without anyone polling.
The use case this job follows, with its automation and trigger: Hand off from your terminal.
Why run it on Rush?
A conversion is only safe when the tests pass after every step, and that means a MongoDB instance, a clean install and dozens of tsc runs. The sandbox has the database, the compiler and the test runner, and the transcript records every run in order, so the reviewer reads the baseline and the final run instead of trusting a description.
The work is long and mechanical, which is exactly what a laptop is bad at holding for an afternoon. On Rush it runs in the cloud while the developer does something else; the sandbox parks when the turn ends, and a follow-up message wakes it with the same converted tree to tighten a type or convert the next directory.
Frequently asked questions
Will it use any to make the compiler pass?
Not as a shortcut. The prompt asks for real interfaces for documents, handlers, errors and config, and for every remaining any or @ts-expect-error to be listed with its reason in the pull request. A reviewer sees the count and decides whether it is acceptable.
Does it change how the app runs in production?
Only the entry path. The boilerplate starts with pm2 start ecosystem.config.json, whose script is src/index.js; after the conversion it is the compiled dist/index.js and a build step runs first. The Dockerfile gets the same change. Nothing in the request handling, the Mongoose models or the Joi validation changes.
My CI runs on an old Node version. Does that matter?
The agent reads engines and the CI config and picks a TypeScript target that compiles for the oldest version named. The boilerplate declares ">=12.0.0" and a Travis config on Node 12, so the pull request says which target it chose and that a newer Node would allow a higher one.
Can it convert the tests too?
Yes. The tests/ directory, including the supertest integration suites and the fixtures under tests/fixtures/, is converted with ts-jest in jest.config.js, and the suite is run after the conversion so the pull request shows the same tests passing in TypeScript.
Does it switch the project to ES modules?
No. The boilerplate uses require and module.exports throughout, and the conversion keeps CommonJS output with esModuleInterop on so the imports read as modern TypeScript without changing the module system. A switch to ESM is a separate change you can ask for in a follow-up.