For agentic workers: REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Port the 158 Replicate AI model nodes to the TS monorepo using a configuration-driven code generator, reusing the same architecture as fal-codegen/fal-nodes.
Architecture: A new replicate-nodes runtime package holds the base class (replicate-base.ts) and generated node files. A new replicate-codegen package reads node configs + Replicate’s OpenAPI API, then generates TypeScript node classes. The code generator follows the exact same types.ts/NodeSpec/NodeConfig/ModuleConfig pattern as fal-codegen, so a future unification is straightforward. The Replicate API is called with plain fetch() (create prediction → poll status → return output) — no SDK dependency.
Tech Stack: TypeScript, fetch(), @nodetool/node-sdk (BaseNode, @prop)
packages/replicate-codegen/ — Code generation toolpackages/replicate-codegen/
├── package.json
├── tsconfig.json
├── src/
│ ├── types.ts # Re-export from fal-codegen types (NodeSpec, FieldDef, etc.)
│ ├── schema-fetcher.ts # Fetch OpenAPI schema from Replicate API
│ ├── schema-parser.ts # Parse Replicate OpenAPI → NodeSpec
│ ├── node-generator.ts # Generate TS node classes (Replicate variant)
│ ├── generate.ts # CLI entry point
│ └── configs/
│ ├── index.ts # Export all module configs
│ ├── image-generate.ts # Image generation model configs
│ ├── image-upscale.ts # Image upscaling configs
│ ├── image-enhance.ts # Image enhancement configs
│ ├── image-analyze.ts # Image analysis configs
│ ├── image-face.ts # Face manipulation configs
│ ├── image-process.ts # Image processing configs
│ ├── image-ocr.ts # OCR configs
│ ├── video-generate.ts # Video generation configs
│ ├── video-enhance.ts # Video enhancement configs
│ ├── audio-generate.ts # Audio generation configs
│ ├── audio-enhance.ts # Audio enhancement configs
│ ├── audio-separate.ts # Audio separation configs
│ ├── audio-transcribe.ts # Audio transcription configs
│ └── text-generate.ts # Text/LLM generation configs
packages/replicate-nodes/ — Runtime packagepackages/replicate-nodes/
├── package.json
├── tsconfig.json
├── src/
│ ├── replicate-base.ts # API key, submit/poll, output conversion, cost calc
│ ├── index.ts # Register all nodes, export REPLICATE_NODES
│ └── generated/ # Auto-generated by replicate-codegen
│ ├── image-generate.ts
│ ├── image-upscale.ts
│ ├── image-enhance.ts
│ ├── image-analyze.ts
│ ├── image-face.ts
│ ├── image-process.ts
│ ├── image-ocr.ts
│ ├── video-generate.ts
│ ├── video-enhance.ts
│ ├── audio-generate.ts
│ ├── audio-enhance.ts
│ ├── audio-separate.ts
│ ├── audio-transcribe.ts
│ └── text-generate.ts
└── tests/
├── replicate-base.test.ts
└── generated-nodes.test.ts
replicate-nodesFiles:
packages/replicate-nodes/package.jsonCreate: packages/replicate-nodes/tsconfig.json
{
"name": "@nodetool/replicate-nodes",
"type": "module",
"version": "0.1.0",
"description": "Replicate AI model nodes for the TS workflow runner",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc",
"test": "vitest run",
"test:watch": "vitest",
"lint": "tsc --noEmit"
},
"dependencies": {
"@nodetool/node-sdk": "*"
},
"devDependencies": {
"@types/node": "^20.0.0",
"typescript": "^5.4.0",
"vitest": "^1.6.1"
}
}
Copy from packages/fal-nodes/tsconfig.json, adjust outDir and rootDir.
Run: cd packages/replicate-nodes && npm install && npm run build
Expected: Empty build succeeds (no source files yet)
git add packages/replicate-nodes/
git commit -m "feat: scaffold replicate-nodes package"
replicate-base.ts — API key, submit/poll, output conversionFiles:
packages/replicate-nodes/src/replicate-base.tspackages/replicate-nodes/tests/replicate-base.test.tsThis is the core runtime. All generated nodes call these helpers. The Replicate API is simple HTTP:
https://api.replicate.com/v1/predictions with {version, input}https://api.replicate.com/v1/predictions/{id} to poll statusStatus flow: starting → processing → succeeded/failed/canceled
// tests/replicate-base.test.ts
import { describe, it, expect } from "vitest";
import { getReplicateApiKey, removeNulls } from "../src/replicate-base.js";
describe("getReplicateApiKey", () => {
it("reads from _secrets", () => {
const key = getReplicateApiKey({
_secrets: { REPLICATE_API_TOKEN: "test-key" },
});
expect(key).toBe("test-key");
});
it("throws when no key configured", () => {
expect(() => getReplicateApiKey({})).toThrow("REPLICATE_API_TOKEN");
});
});
describe("removeNulls", () => {
it("removes null and undefined values", () => {
const obj = { a: 1, b: null, c: undefined, d: "ok" };
removeNulls(obj);
expect(obj).toEqual({ a: 1, d: "ok" });
});
});
Run: npx vitest run tests/replicate-base.test.ts
Expected: FAIL — module not found
// src/replicate-base.ts
/**
* Shared Replicate API utilities.
* Uses plain fetch() — no SDK dependency.
*/
const REPLICATE_API_BASE = "https://api.replicate.com/v1";
// ---------------------------------------------------------------------------
// API Key extraction
// ---------------------------------------------------------------------------
export function getReplicateApiKey(inputs: Record<string, unknown>): string {
const key =
(inputs._secrets as Record<string, string>)?.REPLICATE_API_TOKEN ||
process.env.REPLICATE_API_TOKEN ||
"";
if (!key) throw new Error("REPLICATE_API_TOKEN is not configured");
return key;
}
// ---------------------------------------------------------------------------
// Remove null/undefined from args before sending to API
// ---------------------------------------------------------------------------
export function removeNulls(obj: Record<string, unknown>): void {
for (const key of Object.keys(obj)) {
if (obj[key] === null || obj[key] === undefined) {
delete obj[key];
}
}
}
// ---------------------------------------------------------------------------
// Check if an asset ref has actual content
// ---------------------------------------------------------------------------
export function isRefSet(
ref: Record<string, unknown> | null | undefined
): boolean {
if (!ref || typeof ref !== "object") return false;
return Boolean(ref.uri || ref.data);
}
// ---------------------------------------------------------------------------
// Convert an asset ref to a URL string for the Replicate API
// ---------------------------------------------------------------------------
export async function assetToUrl(
ref: Record<string, unknown>
): Promise<string | null> {
if (!ref) return null;
if (typeof ref.uri === "string" && ref.uri) return ref.uri;
if (typeof ref.data === "string" && ref.data.startsWith("data:"))
return ref.data;
return null;
}
// ---------------------------------------------------------------------------
// Submit prediction and poll until complete
// ---------------------------------------------------------------------------
export interface ReplicatePrediction {
id: string;
status: string;
output: unknown;
error: string | null;
logs: string;
metrics?: Record<string, number>;
}
export async function replicateSubmit(
apiKey: string,
modelVersion: string,
input: Record<string, unknown>
): Promise<ReplicatePrediction> {
// Create prediction
const createRes = await fetch(`${REPLICATE_API_BASE}/predictions`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
Prefer: "wait",
},
body: JSON.stringify({ version: modelVersion, input }),
});
if (!createRes.ok) {
const text = await createRes.text();
throw new Error(
`Replicate API error (${createRes.status}): ${text}`
);
}
let prediction = (await createRes.json()) as ReplicatePrediction;
// If "Prefer: wait" returned a completed prediction, return immediately
if (
prediction.status === "succeeded" ||
prediction.status === "failed" ||
prediction.status === "canceled"
) {
if (prediction.status === "failed") {
throw new Error(prediction.error || "Prediction failed");
}
return prediction;
}
// Poll for completion (max 30 minutes, 2s interval)
for (let i = 0; i < 900; i++) {
await new Promise((r) => setTimeout(r, 2000));
const pollRes = await fetch(
`${REPLICATE_API_BASE}/predictions/${prediction.id}`,
{
headers: { Authorization: `Bearer ${apiKey}` },
}
);
if (!pollRes.ok) continue;
prediction = (await pollRes.json()) as ReplicatePrediction;
if (prediction.status === "failed" || prediction.status === "canceled") {
throw new Error(prediction.error || "Prediction failed");
}
if (prediction.status === "succeeded") {
return prediction;
}
}
throw new Error("Replicate prediction timed out after 30 minutes");
}
// ---------------------------------------------------------------------------
// Extract version hash from model ID "owner/name:version"
// ---------------------------------------------------------------------------
export function extractVersion(modelId: string): string {
const colonIdx = modelId.indexOf(":");
if (colonIdx < 0) {
throw new Error(
`Invalid Replicate model ID: ${modelId}. Expected format: owner/name:version`
);
}
return modelId.slice(colonIdx + 1);
}
// ---------------------------------------------------------------------------
// Convert Replicate output to asset ref
// ---------------------------------------------------------------------------
export function outputToImageRef(output: unknown): Record<string, unknown> {
if (typeof output === "string") return { type: "image", uri: output };
if (Array.isArray(output) && output.length > 0)
return { type: "image", uri: String(output[0]) };
return { type: "image", uri: "" };
}
export function outputToVideoRef(output: unknown): Record<string, unknown> {
if (typeof output === "string") return { type: "video", uri: output };
if (Array.isArray(output) && output.length > 0)
return { type: "video", uri: String(output[0]) };
return { type: "video", uri: "" };
}
export function outputToAudioRef(output: unknown): Record<string, unknown> {
if (typeof output === "string") return { type: "audio", uri: output };
if (Array.isArray(output) && output.length > 0)
return { type: "audio", uri: String(output[0]) };
return { type: "audio", uri: "" };
}
export function outputToString(output: unknown): string {
if (typeof output === "string") return output;
if (Array.isArray(output)) return output.join("");
return String(output ?? "");
}
Run: npx vitest run tests/replicate-base.test.ts
Expected: PASS
git add packages/replicate-nodes/
git commit -m "feat: add replicate-base.ts with API helpers"
index.ts and verify buildFiles:
Create: packages/replicate-nodes/src/index.ts
Step 1: Create index.ts stub
// src/index.ts
import type { NodeClass } from "@nodetool/node-sdk";
import { NodeRegistry } from "@nodetool/node-sdk";
// Generated node arrays will be imported here after codegen runs.
// For now, export an empty array.
export const REPLICATE_NODES: readonly NodeClass[] = [];
export function registerReplicateNodes(registry: NodeRegistry): void {
for (const nodeClass of REPLICATE_NODES) {
registry.register(nodeClass);
}
}
Run: cd packages/replicate-nodes && npm run build
Expected: Build succeeds
git add packages/replicate-nodes/src/index.ts
git commit -m "feat: add replicate-nodes index stub"
replicate-codegenFiles:
packages/replicate-codegen/package.jsonCreate: packages/replicate-codegen/tsconfig.json
{
"name": "@nodetool/replicate-codegen",
"type": "module",
"version": "0.1.0",
"private": true,
"description": "Code generator for Replicate AI nodes",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc",
"generate": "tsx src/generate.ts",
"test": "vitest run",
"lint": "tsc --noEmit"
},
"devDependencies": {
"@nodetool/node-sdk": "*",
"@types/node": "^20.0.0",
"tsx": "^4.0.0",
"typescript": "^5.4.0",
"vitest": "^1.6.1"
}
}
Copy from packages/fal-codegen/tsconfig.json.
git add packages/replicate-codegen/
git commit -m "feat: scaffold replicate-codegen package"
Files:
packages/replicate-codegen/src/types.tspackages/replicate-codegen/src/schema-fetcher.tsThe types reuse the exact same NodeSpec, FieldDef, EnumDef, NodeConfig, ModuleConfig interfaces from fal-codegen.
Copy packages/fal-codegen/src/types.ts verbatim. These types are provider-agnostic.
Fetches model metadata from Replicate’s API: GET https://api.replicate.com/v1/models/{owner}/{name}/versions/{version} returns OpenAPI-style schema with input/output property definitions.
// src/schema-fetcher.ts
import { readFile, writeFile, mkdir } from "node:fs/promises";
import { existsSync } from "node:fs";
import { join } from "node:path";
const REPLICATE_API = "https://api.replicate.com/v1";
export interface ReplicateSchema {
modelId: string; // "owner/name:version"
owner: string;
name: string;
version: string;
description: string;
inputSchema: Record<string, unknown>;
outputSchema: Record<string, unknown>;
hardware?: string;
}
export class SchemaFetcher {
private cacheDir = join(process.cwd(), ".schema-cache", "replicate");
async fetchSchema(
modelId: string,
useCache = true
): Promise<ReplicateSchema> {
const cacheKey = modelId.replace(/[/:]/g, "_");
const cachePath = join(this.cacheDir, `${cacheKey}.json`);
if (useCache && existsSync(cachePath)) {
const raw = await readFile(cachePath, "utf8");
return JSON.parse(raw) as ReplicateSchema;
}
const colonIdx = modelId.indexOf(":");
const ownerName = colonIdx > 0 ? modelId.slice(0, colonIdx) : modelId;
const [owner, name] = ownerName.split("/");
// Fetch latest version if no version specified
const token = process.env.REPLICATE_API_TOKEN ?? "";
const headers: Record<string, string> = {};
if (token) headers.Authorization = `Bearer ${token}`;
let version: string;
let description = "";
let inputSchema: Record<string, unknown> = {};
let outputSchema: Record<string, unknown> = {};
if (colonIdx > 0) {
version = modelId.slice(colonIdx + 1);
} else {
// Fetch model to get latest version
const modelRes = await fetch(
`${REPLICATE_API}/models/${owner}/${name}`,
{ headers }
);
if (!modelRes.ok) throw new Error(`Failed to fetch model ${modelId}`);
const modelData = (await modelRes.json()) as Record<string, unknown>;
const latestVersion = modelData.latest_version as Record<string, unknown>;
version = latestVersion?.id as string ?? "";
description = (modelData.description as string) ?? "";
}
// Fetch version details with schema
const versionRes = await fetch(
`${REPLICATE_API}/models/${owner}/${name}/versions/${version}`,
{ headers }
);
if (!versionRes.ok)
throw new Error(`Failed to fetch version for ${modelId}`);
const versionData = (await versionRes.json()) as Record<string, unknown>;
const openApiSchema = versionData.openapi_schema as Record<string, unknown>;
if (openApiSchema) {
const components = openApiSchema.components as Record<string, unknown>;
const schemas = components?.schemas as Record<string, unknown>;
if (schemas) {
inputSchema = (schemas.Input as Record<string, unknown>) ?? {};
outputSchema = (schemas.Output as Record<string, unknown>) ?? {};
}
}
const result: ReplicateSchema = {
modelId: `${owner}/${name}:${version}`,
owner,
name,
version,
description: description || (versionData.description as string) ?? "",
inputSchema,
outputSchema,
};
// Cache
if (useCache) {
await mkdir(this.cacheDir, { recursive: true });
await writeFile(cachePath, JSON.stringify(result, null, 2));
}
return result;
}
}
git add packages/replicate-codegen/src/
git commit -m "feat: add replicate-codegen types and schema fetcher"
Files:
packages/replicate-codegen/src/schema-parser.tsParses Replicate’s OpenAPI Input schema properties into FieldDef[] and EnumDef[].
Key mappings from Replicate OpenAPI types to TS prop types:
string → "str", with enum → "enum"integer → "int"number → "float"boolean → "bool"string with format: "uri" → check overrides for ImageRef/AudioRef/VideoRefThe parser should:
inputSchema.propertiesFieldDefEnumDef[]NodeSpec with endpointId = modelIdgit add packages/replicate-codegen/src/schema-parser.ts
git commit -m "feat: add Replicate schema parser"
Files:
packages/replicate-codegen/src/node-generator.tsThis follows the fal-codegen pattern but generates Replicate-specific process() methods:
getReplicateApiKey(inputs) + extractVersion(MODEL_ID) + replicateSubmit(apiKey, version, args)Output conversion based on return_type: outputToImageRef(res.output), outputToVideoRef(...), etc.
The generator produces classes like:
export class FluxSchnell extends ReplicateNode {
static readonly nodeType = "replicate.image_generate.FluxSchnell";
static readonly title = "Flux Schnell";
static readonly description = "...";
static readonly requiredSettings = ["REPLICATE_API_TOKEN"];
@prop({ type: "str", default: "", description: "..." })
declare prompt: any;
async process(inputs: Record<string, unknown>): Promise<Record<string, unknown>> {
const apiKey = getReplicateApiKey(inputs);
const prompt = String(inputs.prompt ?? this.prompt ?? "");
const args: Record<string, unknown> = { prompt };
// ... handle asset inputs ...
removeNulls(args);
const res = await replicateSubmit(apiKey, extractVersion("owner/name:version"), args);
return { output: outputToImageRef(res.output) };
}
}
Key differences from fal generator:
../replicate-base.js instead of ../fal-base.jsReplicateNode instead of FalNodereplicateSubmit(apiKey, extractVersion(MODEL_ID), args) instead of falSubmit(apiKey, endpoint, args)outputToImageRef(res.output) etc. based on return typeREPLICATE_API_TOKEN instead of FAL_API_KEYAsset upload: assetToUrl(ref) (simpler — Replicate accepts URLs directly)
git add packages/replicate-codegen/src/node-generator.ts
git commit -m "feat: add Replicate node generator"
Files:
packages/replicate-codegen/src/configs/index.tspackages/replicate-codegen/src/configs/image-generate.ts (and all other config files)Port the 158 node configurations from /Users/mg/workspace/nodetool-replicate/src/nodetool/nodes/replicate/gencode.py replicate_nodes list to TypeScript ModuleConfig format.
Each Python entry like:
{
"node_name": "FluxSchnell",
"namespace": "image.generate",
"model_id": "black-forest-labs/flux-schnell",
"return_type": ImageRef,
"overrides": {"image": ImageRef},
}
Becomes a TS config entry:
"black-forest-labs/flux-schnell": {
className: "FluxSchnell",
returnType: "image",
fieldOverrides: {
image: { propType: "image" },
},
}
Group by namespace into separate config files matching the Python directory structure.
Port all 158 entries from gencode.py, grouped by namespace:
image-generate.ts — ~60 nodes (namespace: image.generate)image-upscale.ts — ~9 nodesimage-enhance.ts — ~6 nodesimage-analyze.ts — ~9 nodesimage-face.ts — ~8 nodesimage-process.ts — ~7 nodesimage-ocr.ts — ~2 nodesvideo-generate.ts — ~24 nodesvideo-enhance.ts — ~2 nodesaudio-generate.ts — ~7 nodesaudio-enhance.ts — ~1 nodeaudio-separate.ts — ~1 nodeaudio-transcribe.ts — ~2 nodestext-generate.ts — ~19 nodesReference: /Users/mg/workspace/nodetool-replicate/src/nodetool/nodes/replicate/gencode.py lines 18-1100+
// src/configs/index.ts
import type { ModuleConfig } from "../types.js";
import { imageGenerateConfig } from "./image-generate.js";
import { imageUpscaleConfig } from "./image-upscale.js";
// ... all other configs
export const allConfigs: Record<string, ModuleConfig> = {
"image-generate": imageGenerateConfig,
"image-upscale": imageUpscaleConfig,
// ... all modules
};
git add packages/replicate-codegen/src/configs/
git commit -m "feat: port 158 Replicate node configs from Python"
Files:
packages/replicate-codegen/src/generate.tsSame pattern as fal-codegen/src/generate.ts: reads configs, fetches schemas, generates TS files.
#!/usr/bin/env node
/**
* CLI for generating Replicate node TypeScript classes.
*
* Usage:
* npx tsx src/generate.ts --all
* npx tsx src/generate.ts --module image-generate
* npx tsx src/generate.ts --all --no-cache
*/
import { parseArgs } from "node:util";
import { mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { SchemaFetcher } from "./schema-fetcher.js";
import { SchemaParser } from "./schema-parser.js";
import { NodeGenerator } from "./node-generator.js";
import { allConfigs } from "./configs/index.js";
import type { ModuleConfig } from "./types.js";
// ... same structure as fal-codegen generate.ts
// Output dir defaults to ../replicate-nodes/src/generated
git add packages/replicate-codegen/src/generate.ts
git commit -m "feat: add Replicate codegen CLI"
Run: cd packages/replicate-codegen && REPLICATE_API_TOKEN=$REPLICATE_API_TOKEN npx tsx src/generate.ts --all
This will:
packages/replicate-nodes/src/generated/Run: ls packages/replicate-nodes/src/generated/
Expected: 14 .ts files, one per namespace
git add packages/replicate-nodes/src/generated/
git commit -m "feat: generate 158 Replicate node classes"
Files:
Modify: packages/replicate-nodes/src/index.ts
Step 1: Update index.ts
Import all generated arrays and compose REPLICATE_NODES:
import type { NodeClass } from "@nodetool/node-sdk";
import { NodeRegistry } from "@nodetool/node-sdk";
import { REPLICATE_IMAGE_GENERATE_NODES } from "./generated/image-generate.js";
import { REPLICATE_IMAGE_UPSCALE_NODES } from "./generated/image-upscale.js";
// ... all generated modules
export const REPLICATE_NODES: readonly NodeClass[] = [
...REPLICATE_IMAGE_GENERATE_NODES,
...REPLICATE_IMAGE_UPSCALE_NODES,
// ... all
];
export function registerReplicateNodes(registry: NodeRegistry): void {
for (const nodeClass of REPLICATE_NODES) {
registry.register(nodeClass);
}
}
Run: cd packages/replicate-nodes && npm run build
Expected: Build succeeds with all 158 nodes
git add packages/replicate-nodes/src/index.ts
git commit -m "feat: wire up all generated Replicate nodes in index"
Files:
packages/websocket/src/server.tsModify: packages/websocket/package.json (add dependency)
Add "@nodetool/replicate-nodes": "*" to packages/websocket/package.json dependencies.
Add alongside existing registerFalNodes(registry):
import { registerReplicateNodes } from "@nodetool/replicate-nodes";
// ...
registerReplicateNodes(registry);
Run: cd packages/websocket && npm install && npm run build
Expected: Build succeeds
git add packages/websocket/
git commit -m "feat: register Replicate nodes in websocket server"
Files:
Create: packages/replicate-nodes/tests/generated-nodes.test.ts
Step 1: Write tests
import { describe, it, expect } from "vitest";
import { REPLICATE_NODES } from "../src/index.js";
describe("Generated Replicate nodes", () => {
it("exports at least 150 nodes", () => {
expect(REPLICATE_NODES.length).toBeGreaterThanOrEqual(150);
});
it("all nodes have valid nodeType", () => {
for (const node of REPLICATE_NODES) {
expect(node.nodeType).toMatch(/^replicate\./);
expect(node.title).toBeTruthy();
}
});
it("all nodes have requiredSettings with REPLICATE_API_TOKEN", () => {
for (const node of REPLICATE_NODES) {
expect(node.requiredSettings).toContain("REPLICATE_API_TOKEN");
}
});
it("all nodes can be instantiated", () => {
for (const NodeClass of REPLICATE_NODES) {
const instance = new NodeClass({});
expect(instance).toBeDefined();
expect(typeof instance.process).toBe("function");
}
});
});
Run: cd packages/replicate-nodes && npx vitest run
Expected: PASS
git add packages/replicate-nodes/tests/
git commit -m "test: add basic tests for generated Replicate nodes"