@suknna/pixelforge 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -39,7 +39,27 @@ Secrets may be plain strings or environment references:
39
39
  { "apiKey": { "env": "OPENAI_API_KEY" } }
40
40
  ```
41
41
 
42
- See [`examples/`](./examples) for complete multi-provider configurations. Supported providers: `openai-images`, `fal`, `replicate`, `stability-ai`.
42
+ A minimal single-provider config looks like this:
43
+
44
+ ```json
45
+ {
46
+ "$schema": "https://raw.githubusercontent.com/Suknna/pixelforge/main/schema/pixelforge.schema.json",
47
+ "defaultProfile": "openai",
48
+ "profiles": {
49
+ "openai": {
50
+ "provider": "openai-images",
51
+ "apiKey": { "env": "OPENAI_API_KEY" },
52
+ "model": "gpt-image-1",
53
+ "baseUrl": "https://api.openai.com/v1",
54
+ "timeoutMs": 180000
55
+ }
56
+ }
57
+ }
58
+ ```
59
+
60
+ Add the `$schema` reference shown above so your editor validates the config as you type. The schema lives at [`schema/pixelforge.schema.json`](./schema/pixelforge.schema.json).
61
+
62
+ See [`examples/`](./examples) for the minimal config ([`pixelforge.minimal.json`](./examples/pixelforge.minimal.json)) and complete multi-provider configurations. Supported providers: `openai-images`, `fal`, `replicate`, `stability-ai`.
43
63
 
44
64
  ## Tools
45
65
 
@@ -51,6 +71,14 @@ PixelForge registers three tools. None of them expose the internal `profile` con
51
71
 
52
72
  Generation runs in the background. The generation tools return before the file exists; use `pixelforge_check_image_job` to confirm completion.
53
73
 
74
+ Both generation tools accept optional render parameters that the model fills per request, not the config:
75
+
76
+ - `size` — `"<width>x<height>"`, e.g. `"1024x1024"`. Mapped per provider (OpenAI `size`, fal/Stability dimensions). When omitted, the provider default is used.
77
+ - `outputFormat` — `png`, `jpeg`, or `webp`. When omitted, the provider default is used.
78
+ - `strength` (image-to-image only) — `0`–`1`, how far the result may diverge from the base image.
79
+
80
+ Provider-specific settings (endpoint, model/version, base URL, input-key mappings, API key, timeouts, OpenAI `quality`) stay in the config. Render parameters that vary per image live on the tools.
81
+
54
82
  ## Output Paths
55
83
 
56
84
  Output paths must resolve inside the current opencode workspace. Relative paths are resolved from the session directory. Files are written atomically (temp file plus rename) so a visible output file always means a completed job.
@@ -1,5 +1,5 @@
1
1
  import { loadLocalImage, toDataUri } from "../image-input.js";
2
- import { decodeDataUri, fetchBytes, fetchJson } from "./http.js";
2
+ import { decodeDataUri, fetchBytes, fetchJson, parseSize } from "./http.js";
3
3
  /**
4
4
  * fal adapter.
5
5
  *
@@ -13,8 +13,8 @@ export function createFalAdapter(profile) {
13
13
  async generateImage(request) {
14
14
  const response = await callFal(profile.endpoint, profile, {
15
15
  prompt: request.prompt,
16
- image_size: profile.imageSize,
17
- output_format: profile.outputFormat,
16
+ image_size: parseSize(request.size, "fal"),
17
+ output_format: request.outputFormat,
18
18
  }, request.signal);
19
19
  return imageFromFalResponse(response);
20
20
  },
@@ -24,9 +24,9 @@ export function createFalAdapter(profile) {
24
24
  const response = await callFal(profile.imageToImageEndpoint, profile, {
25
25
  prompt: request.prompt,
26
26
  image_url: imageUrl,
27
- strength: request.strength ?? profile.defaultStrength,
28
- image_size: profile.imageSize,
29
- output_format: profile.outputFormat,
27
+ strength: request.strength,
28
+ image_size: parseSize(request.size, "fal"),
29
+ output_format: request.outputFormat,
30
30
  }, request.signal);
31
31
  return imageFromFalResponse(response);
32
32
  },
@@ -15,6 +15,16 @@ export declare function decodeBase64Image(base64: string, mimeType?: string): {
15
15
  bytes: Uint8Array;
16
16
  mimeType: string;
17
17
  };
18
+ /**
19
+ * Parses a model-supplied `"<width>x<height>"` size string into numeric
20
+ * dimensions. Returns `undefined` when the input is absent. Throws a
21
+ * fail-closed bad-request error on a malformed value so a bad `size` never
22
+ * silently degrades to a provider default the model did not intend.
23
+ */
24
+ export declare function parseSize(size: string | undefined, provider: ProviderName): {
25
+ width: number;
26
+ height: number;
27
+ } | undefined;
18
28
  export declare function decodeDataUri(dataUri: string): {
19
29
  bytes: Uint8Array;
20
30
  mimeType: string;
@@ -35,6 +35,28 @@ export async function fetchBytes(url, provider) {
35
35
  export function decodeBase64Image(base64, mimeType = "image/png") {
36
36
  return { bytes: Buffer.from(base64, "base64"), mimeType };
37
37
  }
38
+ /**
39
+ * Parses a model-supplied `"<width>x<height>"` size string into numeric
40
+ * dimensions. Returns `undefined` when the input is absent. Throws a
41
+ * fail-closed bad-request error on a malformed value so a bad `size` never
42
+ * silently degrades to a provider default the model did not intend.
43
+ */
44
+ export function parseSize(size, provider) {
45
+ if (size === undefined)
46
+ return undefined;
47
+ const match = /^(\d+)\s*[x×]\s*(\d+)$/.exec(size.trim());
48
+ const width = match?.[1];
49
+ const height = match?.[2];
50
+ if (!width || !height) {
51
+ throw new PixelForgeError({
52
+ category: "bad-request",
53
+ message: `Invalid size '${size}'. Expected "<width>x<height>", e.g. 1024x1024.`,
54
+ retryable: false,
55
+ provider,
56
+ });
57
+ }
58
+ return { width: Number(width), height: Number(height) };
59
+ }
38
60
  export function decodeDataUri(dataUri) {
39
61
  const match = /^data:([^;]+);base64,(.+)$/.exec(dataUri);
40
62
  const mimeType = match?.[1];
@@ -21,31 +21,31 @@ export function createOpenAIImagesAdapter(profile) {
21
21
  body: JSON.stringify({
22
22
  model: profile.model,
23
23
  prompt: request.prompt,
24
- size: profile.size,
24
+ size: request.size,
25
25
  quality: profile.quality,
26
- output_format: profile.outputFormat,
26
+ output_format: request.outputFormat,
27
27
  }),
28
28
  }, "openai-images");
29
- return imageFromOpenAIResponse(response, profile.outputFormat);
29
+ return imageFromOpenAIResponse(response, request.outputFormat);
30
30
  },
31
31
  async generateImageFromImage(request) {
32
32
  const form = new FormData();
33
33
  form.set("model", profile.model);
34
34
  form.set("prompt", request.prompt);
35
35
  form.set("image[]", await fileFromPath(request.baseImagePath));
36
- if (profile.size)
37
- form.set("size", profile.size);
36
+ if (request.size)
37
+ form.set("size", request.size);
38
38
  if (profile.quality)
39
39
  form.set("quality", profile.quality);
40
- if (profile.outputFormat)
41
- form.set("output_format", profile.outputFormat);
40
+ if (request.outputFormat)
41
+ form.set("output_format", request.outputFormat);
42
42
  const response = await fetchJson(`${profile.baseUrl.replace(/\/$/, "")}/images/edits`, {
43
43
  method: "POST",
44
44
  signal: request.signal,
45
45
  headers: { Authorization: `Bearer ${profile.credential}` },
46
46
  body: form,
47
47
  }, "openai-images");
48
- return imageFromOpenAIResponse(response, profile.outputFormat);
48
+ return imageFromOpenAIResponse(response, request.outputFormat);
49
49
  },
50
50
  };
51
51
  }
@@ -13,10 +13,8 @@ export function createReplicateAdapter(profile) {
13
13
  [profile.promptInputKey]: request.prompt,
14
14
  [profile.imageInputKey]: toDataUri(image),
15
15
  };
16
- if (profile.strengthInputKey) {
17
- const strength = request.strength ?? profile.defaultStrength;
18
- if (strength !== undefined)
19
- input[profile.strengthInputKey] = strength;
16
+ if (profile.strengthInputKey && request.strength !== undefined) {
17
+ input[profile.strengthInputKey] = request.strength;
20
18
  }
21
19
  const prediction = await createPrediction(profile, input, request.signal);
22
20
  return pollPrediction(profile, prediction, request.signal);
@@ -1,5 +1,5 @@
1
1
  import { blobFromImage, loadLocalImage } from "../image-input.js";
2
- import { decodeBase64Image, fetchJson } from "./http.js";
2
+ import { decodeBase64Image, fetchJson, parseSize } from "./http.js";
3
3
  /**
4
4
  * Stability returns base64 image payloads. Text-to-image uses a JSON body;
5
5
  * image-to-image uploads the base image as multipart form data with an
@@ -9,6 +9,7 @@ export function createStabilityAIAdapter(profile) {
9
9
  return {
10
10
  provider: "stability-ai",
11
11
  async generateImage(request) {
12
+ const dimensions = parseSize(request.size, "stability-ai");
12
13
  const response = await fetchJson(`${profile.baseUrl.replace(/\/$/, "")}${profile.textToImagePath}`, {
13
14
  method: "POST",
14
15
  signal: request.signal,
@@ -19,21 +20,20 @@ export function createStabilityAIAdapter(profile) {
19
20
  },
20
21
  body: JSON.stringify({
21
22
  prompt: request.prompt,
22
- output_format: profile.outputFormat,
23
- width: profile.width,
24
- height: profile.height,
23
+ output_format: request.outputFormat,
24
+ width: dimensions?.width,
25
+ height: dimensions?.height,
25
26
  }),
26
27
  }, "stability-ai");
27
- return imageFromStabilityResponse(response, profile.outputFormat);
28
+ return imageFromStabilityResponse(response, request.outputFormat);
28
29
  },
29
30
  async generateImageFromImage(request) {
30
31
  const image = await loadLocalImage(request.baseImagePath);
31
32
  const form = new FormData();
32
33
  form.set("prompt", request.prompt);
33
- form.set("output_format", profile.outputFormat ?? "png");
34
- const strength = request.strength ?? profile.defaultStrength;
35
- if (strength !== undefined)
36
- form.set("strength", String(strength));
34
+ form.set("output_format", request.outputFormat ?? "png");
35
+ if (request.strength !== undefined)
36
+ form.set("strength", String(request.strength));
37
37
  form.set("image", blobFromImage(image), image.fileName);
38
38
  const response = await fetchJson(`${profile.baseUrl.replace(/\/$/, "")}${profile.imageToImagePath}`, {
39
39
  method: "POST",
@@ -44,7 +44,7 @@ export function createStabilityAIAdapter(profile) {
44
44
  },
45
45
  body: form,
46
46
  }, "stability-ai");
47
- return imageFromStabilityResponse(response, profile.outputFormat);
47
+ return imageFromStabilityResponse(response, request.outputFormat);
48
48
  },
49
49
  };
50
50
  }
package/dist/config.js CHANGED
@@ -49,12 +49,10 @@ export function resolveConfig(raw, env = process.env) {
49
49
  }
50
50
  function resolveProfile(profileName, profile, env) {
51
51
  validateTimeout(profileName, profile.timeoutMs);
52
- validateStrength(profileName, profile.defaultStrength);
53
52
  switch (profile.provider) {
54
53
  case "openai-images":
55
54
  requireString(profileName, "model", profile.model);
56
55
  requireString(profileName, "baseUrl", profile.baseUrl);
57
- requireString(profileName, "size", profile.size);
58
56
  return { ...profile, profileName, credential: resolveSecret(profile.apiKey, env, `${profileName}.apiKey`) };
59
57
  case "fal":
60
58
  requireString(profileName, "endpoint", profile.endpoint);
@@ -89,13 +87,6 @@ function validateTimeout(profileName, timeoutMs, field = "timeoutMs") {
89
87
  throw configurationError(`${profileName}.${field} must be a positive integer.`);
90
88
  }
91
89
  }
92
- function validateStrength(profileName, strength) {
93
- if (strength === undefined)
94
- return;
95
- if (typeof strength !== "number" || strength < 0 || strength > 1) {
96
- throw configurationError(`${profileName}.defaultStrength must be between 0 and 1.`);
97
- }
98
- }
99
90
  function requireString(profileName, field, value) {
100
91
  if (typeof value !== "string" || value.length === 0) {
101
92
  throw configurationError(`${profileName}.${field} must be a non-empty string.`);
package/dist/runner.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { JobRegistry } from "./jobs.js";
2
2
  import { type NotificationClient } from "./notifications.js";
3
- import type { ImageJob, PixelForgeConfig } from "./types.js";
3
+ import type { ImageJob, PixelForgeConfig, RenderOptions } from './types.js';
4
4
  /**
5
5
  * Everything a background job needs to execute: validated config, the shared
6
6
  * job registry, and the notification client. `sessionID` is optional because
@@ -12,5 +12,5 @@ export type RunnerContext = {
12
12
  client: NotificationClient;
13
13
  sessionID?: string;
14
14
  };
15
- export declare function startTextToImageJob(context: RunnerContext, job: ImageJob, overwrite: boolean): void;
16
- export declare function startImageToImageJob(context: RunnerContext, job: ImageJob, overwrite: boolean, strength?: number): void;
15
+ export declare function startTextToImageJob(context: RunnerContext, job: ImageJob, overwrite: boolean, render?: RenderOptions): void;
16
+ export declare function startImageToImageJob(context: RunnerContext, job: ImageJob, overwrite: boolean, strength?: number, render?: RenderOptions): void;
package/dist/runner.js CHANGED
@@ -2,10 +2,15 @@ import { createAdapter } from "./adapters/index.js";
2
2
  import { PixelForgeError, toPixelForgeError } from "./errors.js";
3
3
  import { notifyJobCompleted } from "./notifications.js";
4
4
  import { assertCanWriteOutput, writeFileAtomic } from "./path-safety.js";
5
- export function startTextToImageJob(context, job, overwrite) {
6
- void runJob(context, job, overwrite, async (adapter, signal) => adapter.generateImage({ prompt: job.prompt, signal }));
5
+ export function startTextToImageJob(context, job, overwrite, render = {}) {
6
+ void runJob(context, job, overwrite, async (adapter, signal) => adapter.generateImage({
7
+ prompt: job.prompt,
8
+ signal,
9
+ ...(render.size !== undefined ? { size: render.size } : {}),
10
+ ...(render.outputFormat !== undefined ? { outputFormat: render.outputFormat } : {}),
11
+ }));
7
12
  }
8
- export function startImageToImageJob(context, job, overwrite, strength) {
13
+ export function startImageToImageJob(context, job, overwrite, strength, render = {}) {
9
14
  void runJob(context, job, overwrite, async (adapter, signal) => {
10
15
  if (!job.baseImagePath)
11
16
  throw new Error("Image-to-image job is missing baseImagePath.");
@@ -13,6 +18,8 @@ export function startImageToImageJob(context, job, overwrite, strength) {
13
18
  baseImagePath: job.baseImagePath,
14
19
  prompt: job.prompt,
15
20
  ...(strength !== undefined ? { strength } : {}),
21
+ ...(render.size !== undefined ? { size: render.size } : {}),
22
+ ...(render.outputFormat !== undefined ? { outputFormat: render.outputFormat } : {}),
16
23
  signal,
17
24
  });
18
25
  });
package/dist/tools.d.ts CHANGED
@@ -12,12 +12,20 @@ export declare function createPixelForgeTools(context: RunnerContext): {
12
12
  args: {
13
13
  prompt: import("zod").ZodString;
14
14
  outputPath: import("zod").ZodString;
15
+ size: import("zod").ZodOptional<import("zod").ZodString>;
16
+ outputFormat: import("zod").ZodOptional<import("zod").ZodEnum<{
17
+ png: "png";
18
+ jpeg: "jpeg";
19
+ webp: "webp";
20
+ }>>;
15
21
  overwrite: import("zod").ZodBoolean;
16
22
  };
17
23
  execute(args: {
18
24
  prompt: string;
19
25
  outputPath: string;
20
26
  overwrite: boolean;
27
+ size?: string | undefined;
28
+ outputFormat?: "png" | "jpeg" | "webp" | undefined;
21
29
  }, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
22
30
  };
23
31
  pixelforge_generate_image_from_image: {
@@ -27,6 +35,12 @@ export declare function createPixelForgeTools(context: RunnerContext): {
27
35
  prompt: import("zod").ZodString;
28
36
  outputPath: import("zod").ZodString;
29
37
  strength: import("zod").ZodOptional<import("zod").ZodNumber>;
38
+ size: import("zod").ZodOptional<import("zod").ZodString>;
39
+ outputFormat: import("zod").ZodOptional<import("zod").ZodEnum<{
40
+ png: "png";
41
+ jpeg: "jpeg";
42
+ webp: "webp";
43
+ }>>;
30
44
  overwrite: import("zod").ZodBoolean;
31
45
  };
32
46
  execute(args: {
@@ -35,6 +49,8 @@ export declare function createPixelForgeTools(context: RunnerContext): {
35
49
  outputPath: string;
36
50
  overwrite: boolean;
37
51
  strength?: number | undefined;
52
+ size?: string | undefined;
53
+ outputFormat?: "png" | "jpeg" | "webp" | undefined;
38
54
  }, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
39
55
  };
40
56
  pixelforge_check_image_job: {
package/dist/tools.js CHANGED
@@ -23,12 +23,24 @@ export function createPixelForgeTools(context) {
23
23
  .string()
24
24
  .min(1)
25
25
  .describe("File path where the generated image should be written. Relative paths are resolved from the current session directory."),
26
+ size: tool.schema
27
+ .string()
28
+ .min(1)
29
+ .optional()
30
+ .describe('Optional image size as "<width>x<height>", e.g. "1024x1024". When omitted, the provider default is used.'),
31
+ outputFormat: tool.schema
32
+ .enum(["png", "jpeg", "webp"])
33
+ .optional()
34
+ .describe("Optional output image format. When omitted, the provider default is used."),
26
35
  overwrite: tool.schema.boolean().describe("Whether to overwrite the output file if it already exists."),
27
36
  },
28
37
  async execute(args, opencodeContext) {
29
38
  const outputPath = resolveWorkspacePath(args.outputPath, opencodeContext.directory, opencodeContext.worktree);
30
39
  const job = context.jobs.create({ kind: "text-to-image", prompt: args.prompt, outputPath });
31
- startTextToImageJob({ ...context, sessionID: opencodeContext.sessionID }, job, args.overwrite);
40
+ startTextToImageJob({ ...context, sessionID: opencodeContext.sessionID }, job, args.overwrite, {
41
+ ...(args.size !== undefined ? { size: args.size } : {}),
42
+ ...(args.outputFormat !== undefined ? { outputFormat: args.outputFormat } : {}),
43
+ });
32
44
  return JSON.stringify({
33
45
  jobId: job.id,
34
46
  status: "queued",
@@ -59,6 +71,15 @@ export function createPixelForgeTools(context) {
59
71
  .max(1)
60
72
  .optional()
61
73
  .describe("How strongly the generated result may diverge from the base image, from 0 to 1."),
74
+ size: tool.schema
75
+ .string()
76
+ .min(1)
77
+ .optional()
78
+ .describe('Optional image size as "<width>x<height>", e.g. "1024x1024". When omitted, the provider default is used.'),
79
+ outputFormat: tool.schema
80
+ .enum(["png", "jpeg", "webp"])
81
+ .optional()
82
+ .describe("Optional output image format. When omitted, the provider default is used."),
62
83
  overwrite: tool.schema.boolean().describe("Whether to overwrite the output file if it already exists."),
63
84
  },
64
85
  async execute(args, opencodeContext) {
@@ -70,7 +91,10 @@ export function createPixelForgeTools(context) {
70
91
  baseImagePath,
71
92
  outputPath,
72
93
  });
73
- startImageToImageJob({ ...context, sessionID: opencodeContext.sessionID }, job, args.overwrite, args.strength);
94
+ startImageToImageJob({ ...context, sessionID: opencodeContext.sessionID }, job, args.overwrite, args.strength, {
95
+ ...(args.size !== undefined ? { size: args.size } : {}),
96
+ ...(args.outputFormat !== undefined ? { outputFormat: args.outputFormat } : {}),
97
+ });
74
98
  return JSON.stringify({
75
99
  jobId: job.id,
76
100
  status: "queued",
package/dist/types.d.ts CHANGED
@@ -14,15 +14,23 @@ export type ImageResult = {
14
14
  mimeType: string;
15
15
  provider: ProviderName;
16
16
  };
17
+ export type RenderOptions = {
18
+ size?: string;
19
+ outputFormat?: "png" | "jpeg" | "webp";
20
+ };
17
21
  export type TextToImageRequest = {
18
22
  prompt: string;
19
23
  signal: AbortSignal;
24
+ size?: string;
25
+ outputFormat?: "png" | "jpeg" | "webp";
20
26
  };
21
27
  export type ImageToImageRequest = {
22
28
  baseImagePath: string;
23
29
  prompt: string;
24
30
  strength?: number;
25
31
  signal: AbortSignal;
32
+ size?: string;
33
+ outputFormat?: "png" | "jpeg" | "webp";
26
34
  };
27
35
  export type ImageAdapter = {
28
36
  provider: ProviderName;
@@ -40,15 +48,12 @@ export type NotificationConfig = {
40
48
  export type BaseProfile = {
41
49
  provider: ProviderName;
42
50
  timeoutMs: number;
43
- outputFormat?: "png" | "jpeg" | "webp";
44
- defaultStrength?: number;
45
51
  };
46
52
  export type OpenAIImagesProfile = BaseProfile & {
47
53
  provider: "openai-images";
48
54
  apiKey: SecretValue;
49
55
  model: string;
50
56
  baseUrl: string;
51
- size: string;
52
57
  quality?: "low" | "medium" | "high" | "auto";
53
58
  };
54
59
  export type FalProfile = BaseProfile & {
@@ -56,7 +61,6 @@ export type FalProfile = BaseProfile & {
56
61
  apiKey: SecretValue;
57
62
  endpoint: string;
58
63
  imageToImageEndpoint: string;
59
- imageSize?: string;
60
64
  };
61
65
  export type ReplicateProfile = BaseProfile & {
62
66
  provider: "replicate";
@@ -73,8 +77,6 @@ export type StabilityAIProfile = BaseProfile & {
73
77
  baseUrl: string;
74
78
  textToImagePath: string;
75
79
  imageToImagePath: string;
76
- width?: number;
77
- height?: number;
78
80
  };
79
81
  export type RawProfile = OpenAIImagesProfile | FalProfile | ReplicateProfile | StabilityAIProfile;
80
82
  export type RawPixelForgeConfig = {
@@ -0,0 +1,13 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/Suknna/pixelforge/main/schema/pixelforge.schema.json",
3
+ "defaultProfile": "openai",
4
+ "profiles": {
5
+ "openai": {
6
+ "provider": "openai-images",
7
+ "apiKey": { "env": "OPENAI_API_KEY" },
8
+ "model": "gpt-image-1",
9
+ "baseUrl": "https://api.openai.com/v1",
10
+ "timeoutMs": 180000
11
+ }
12
+ }
13
+ }
@@ -0,0 +1,36 @@
1
+ {
2
+ "defaultProfile": "primary-openai",
3
+ "fallbackProfiles": ["fal-backup", "replicate-backup"],
4
+ "notifications": {
5
+ "tui": true,
6
+ "sessionMessage": true,
7
+ "autoContinue": false
8
+ },
9
+ "profiles": {
10
+ "primary-openai": {
11
+ "provider": "openai-images",
12
+ "apiKey": { "env": "OPENAI_API_KEY" },
13
+ "model": "gpt-image-1",
14
+ "baseUrl": "https://api.openai.com/v1",
15
+ "quality": "medium",
16
+ "timeoutMs": 180000
17
+ },
18
+ "fal-backup": {
19
+ "provider": "fal",
20
+ "apiKey": { "env": "FAL_KEY" },
21
+ "endpoint": "fal-ai/flux/dev",
22
+ "imageToImageEndpoint": "fal-ai/flux/dev/image-to-image",
23
+ "timeoutMs": 180000
24
+ },
25
+ "replicate-backup": {
26
+ "provider": "replicate",
27
+ "apiToken": { "env": "REPLICATE_API_TOKEN" },
28
+ "version": "black-forest-labs/flux-schnell",
29
+ "promptInputKey": "prompt",
30
+ "imageInputKey": "image",
31
+ "strengthInputKey": "prompt_strength",
32
+ "pollIntervalMs": 2000,
33
+ "timeoutMs": 300000
34
+ }
35
+ }
36
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "defaultProfile": "stability",
3
+ "fallbackProfiles": [],
4
+ "notifications": {
5
+ "tui": true,
6
+ "sessionMessage": false,
7
+ "autoContinue": false
8
+ },
9
+ "profiles": {
10
+ "stability": {
11
+ "provider": "stability-ai",
12
+ "apiKey": { "env": "STABILITY_API_KEY" },
13
+ "baseUrl": "https://api.stability.ai",
14
+ "textToImagePath": "/v2beta/stable-image/generate/core",
15
+ "imageToImagePath": "/v2beta/stable-image/edit/search-and-replace",
16
+ "timeoutMs": 240000
17
+ }
18
+ }
19
+ }
package/package.json CHANGED
@@ -1,11 +1,17 @@
1
1
  {
2
2
  "name": "@suknna/pixelforge",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "OpenCode plugin that provides background image generation tools.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
8
- "files": ["dist", "README.md", "LICENSE"],
8
+ "files": [
9
+ "dist",
10
+ "schema",
11
+ "examples",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
9
15
  "exports": {
10
16
  ".": {
11
17
  "types": "./dist/index.d.ts",
@@ -41,6 +47,11 @@
41
47
  "publishConfig": {
42
48
  "access": "public"
43
49
  },
44
- "keywords": ["opencode", "plugin", "image-generation", "pixel-forge"],
50
+ "keywords": [
51
+ "opencode",
52
+ "plugin",
53
+ "image-generation",
54
+ "pixel-forge"
55
+ ],
45
56
  "license": "MIT"
46
57
  }
@@ -0,0 +1,177 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://raw.githubusercontent.com/Suknna/pixelforge/main/schema/pixelforge.schema.json",
4
+ "title": "PixelForge Config",
5
+ "description": "Configuration for the PixelForge opencode plugin. Lives at ~/.config/opencode/pixelforge.json. The plugin fails fast at startup if this file is missing or invalid. Render parameters (size, output format, strength) are supplied by the model through the tools, not configured here.",
6
+ "type": "object",
7
+ "required": ["defaultProfile", "profiles"],
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "$schema": {
11
+ "type": "string",
12
+ "description": "Optional reference to this JSON Schema for editor validation."
13
+ },
14
+ "defaultProfile": {
15
+ "type": "string",
16
+ "minLength": 1,
17
+ "description": "Name of the profile tried first for every job. Must be a key in profiles."
18
+ },
19
+ "fallbackProfiles": {
20
+ "type": "array",
21
+ "description": "Explicit, ordered list of profiles tried after the default. Only profiles listed here are ever attempted, and only on transient errors (network failure, timeout, HTTP 429, HTTP 5xx). Each entry must be a key in profiles.",
22
+ "items": { "type": "string", "minLength": 1 }
23
+ },
24
+ "notifications": { "$ref": "#/$defs/notifications" },
25
+ "profiles": {
26
+ "type": "object",
27
+ "description": "Map of profile name to provider configuration.",
28
+ "minProperties": 1,
29
+ "additionalProperties": { "$ref": "#/$defs/profile" }
30
+ }
31
+ },
32
+ "$defs": {
33
+ "secret": {
34
+ "description": "A secret value: either a plain string or an environment-variable reference.",
35
+ "oneOf": [
36
+ { "type": "string", "minLength": 1 },
37
+ {
38
+ "type": "object",
39
+ "required": ["env"],
40
+ "additionalProperties": false,
41
+ "properties": {
42
+ "env": {
43
+ "type": "string",
44
+ "minLength": 1,
45
+ "description": "Name of the environment variable holding the secret."
46
+ }
47
+ }
48
+ }
49
+ ]
50
+ },
51
+ "notifications": {
52
+ "type": "object",
53
+ "description": "Completion notification toggles.",
54
+ "additionalProperties": false,
55
+ "properties": {
56
+ "tui": {
57
+ "type": "boolean",
58
+ "description": "Show an opencode TUI Toast when a job completes, naming the provider that produced the image."
59
+ },
60
+ "sessionMessage": {
61
+ "type": "boolean",
62
+ "description": "Inject a completion message into the current session."
63
+ },
64
+ "autoContinue": {
65
+ "type": "boolean",
66
+ "description": "Allow a completed background job to trigger the model to continue. Off by default."
67
+ }
68
+ }
69
+ },
70
+ "timeoutMs": {
71
+ "type": "integer",
72
+ "minimum": 1,
73
+ "description": "Per-request timeout in milliseconds. Must be a positive integer."
74
+ },
75
+ "profile": {
76
+ "oneOf": [
77
+ { "$ref": "#/$defs/openaiImagesProfile" },
78
+ { "$ref": "#/$defs/falProfile" },
79
+ { "$ref": "#/$defs/replicateProfile" },
80
+ { "$ref": "#/$defs/stabilityAiProfile" }
81
+ ]
82
+ },
83
+ "openaiImagesProfile": {
84
+ "type": "object",
85
+ "description": "OpenAI Images provider profile.",
86
+ "required": ["provider", "apiKey", "model", "baseUrl", "timeoutMs"],
87
+ "additionalProperties": false,
88
+ "properties": {
89
+ "provider": { "const": "openai-images" },
90
+ "apiKey": { "$ref": "#/$defs/secret" },
91
+ "model": { "type": "string", "minLength": 1, "description": "Image model id, e.g. gpt-image-1." },
92
+ "baseUrl": { "type": "string", "minLength": 1, "description": "API base URL, e.g. https://api.openai.com/v1." },
93
+ "quality": {
94
+ "type": "string",
95
+ "enum": ["low", "medium", "high", "auto"],
96
+ "description": "Optional OpenAI-only image quality. Other providers ignore it."
97
+ },
98
+ "timeoutMs": { "$ref": "#/$defs/timeoutMs" }
99
+ }
100
+ },
101
+ "falProfile": {
102
+ "type": "object",
103
+ "description": "fal provider profile.",
104
+ "required": ["provider", "apiKey", "endpoint", "imageToImageEndpoint", "timeoutMs"],
105
+ "additionalProperties": false,
106
+ "properties": {
107
+ "provider": { "const": "fal" },
108
+ "apiKey": { "$ref": "#/$defs/secret" },
109
+ "endpoint": { "type": "string", "minLength": 1, "description": "Text-to-image endpoint, e.g. fal-ai/flux/dev." },
110
+ "imageToImageEndpoint": {
111
+ "type": "string",
112
+ "minLength": 1,
113
+ "description": "Image-to-image endpoint, e.g. fal-ai/flux/dev/image-to-image."
114
+ },
115
+ "timeoutMs": { "$ref": "#/$defs/timeoutMs" }
116
+ }
117
+ },
118
+ "replicateProfile": {
119
+ "type": "object",
120
+ "description": "Replicate provider profile.",
121
+ "required": [
122
+ "provider",
123
+ "apiToken",
124
+ "version",
125
+ "promptInputKey",
126
+ "imageInputKey",
127
+ "pollIntervalMs",
128
+ "timeoutMs"
129
+ ],
130
+ "additionalProperties": false,
131
+ "properties": {
132
+ "provider": { "const": "replicate" },
133
+ "apiToken": { "$ref": "#/$defs/secret" },
134
+ "version": {
135
+ "type": "string",
136
+ "minLength": 1,
137
+ "description": "Replicate model or version reference, e.g. black-forest-labs/flux-schnell."
138
+ },
139
+ "promptInputKey": {
140
+ "type": "string",
141
+ "minLength": 1,
142
+ "description": "Input key the model expects for the text prompt, e.g. prompt."
143
+ },
144
+ "imageInputKey": {
145
+ "type": "string",
146
+ "minLength": 1,
147
+ "description": "Input key the model expects for the base image, e.g. image."
148
+ },
149
+ "strengthInputKey": {
150
+ "type": "string",
151
+ "minLength": 1,
152
+ "description": "Optional input key the model expects for strength, e.g. prompt_strength."
153
+ },
154
+ "pollIntervalMs": {
155
+ "type": "integer",
156
+ "minimum": 1,
157
+ "description": "Prediction poll interval in milliseconds. Must be a positive integer."
158
+ },
159
+ "timeoutMs": { "$ref": "#/$defs/timeoutMs" }
160
+ }
161
+ },
162
+ "stabilityAiProfile": {
163
+ "type": "object",
164
+ "description": "Stability AI provider profile.",
165
+ "required": ["provider", "apiKey", "baseUrl", "textToImagePath", "imageToImagePath", "timeoutMs"],
166
+ "additionalProperties": false,
167
+ "properties": {
168
+ "provider": { "const": "stability-ai" },
169
+ "apiKey": { "$ref": "#/$defs/secret" },
170
+ "baseUrl": { "type": "string", "minLength": 1, "description": "Stability API base URL." },
171
+ "textToImagePath": { "type": "string", "minLength": 1, "description": "Text-to-image request path." },
172
+ "imageToImagePath": { "type": "string", "minLength": 1, "description": "Image-to-image request path." },
173
+ "timeoutMs": { "$ref": "#/$defs/timeoutMs" }
174
+ }
175
+ }
176
+ }
177
+ }