codex-imagegen-mcp 0.1.2

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.
Files changed (105) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +19 -0
  4. package/README.md +373 -0
  5. package/dist/src/auth/borrowed.js +77 -0
  6. package/dist/src/auth/borrowed.js.map +1 -0
  7. package/dist/src/auth/browser-login.js +179 -0
  8. package/dist/src/auth/browser-login.js.map +1 -0
  9. package/dist/src/auth/device-login.js +39 -0
  10. package/dist/src/auth/device-login.js.map +1 -0
  11. package/dist/src/auth/jwt.js +69 -0
  12. package/dist/src/auth/jwt.js.map +1 -0
  13. package/dist/src/auth/manager.js +289 -0
  14. package/dist/src/auth/manager.js.map +1 -0
  15. package/dist/src/auth/oauth.js +246 -0
  16. package/dist/src/auth/oauth.js.map +1 -0
  17. package/dist/src/auth/pages.js +39 -0
  18. package/dist/src/auth/pages.js.map +1 -0
  19. package/dist/src/auth/pkce.js +14 -0
  20. package/dist/src/auth/pkce.js.map +1 -0
  21. package/dist/src/auth/store.js +75 -0
  22. package/dist/src/auth/store.js.map +1 -0
  23. package/dist/src/backend/images-client.js +223 -0
  24. package/dist/src/backend/images-client.js.map +1 -0
  25. package/dist/src/backend/ratelimits.js +183 -0
  26. package/dist/src/backend/ratelimits.js.map +1 -0
  27. package/dist/src/cli.js +407 -0
  28. package/dist/src/cli.js.map +1 -0
  29. package/dist/src/config.js +87 -0
  30. package/dist/src/config.js.map +1 -0
  31. package/dist/src/constants.js +52 -0
  32. package/dist/src/constants.js.map +1 -0
  33. package/dist/src/doctor.js +122 -0
  34. package/dist/src/doctor.js.map +1 -0
  35. package/dist/src/errors.js +76 -0
  36. package/dist/src/errors.js.map +1 -0
  37. package/dist/src/generation.js +183 -0
  38. package/dist/src/generation.js.map +1 -0
  39. package/dist/src/history.js +47 -0
  40. package/dist/src/history.js.map +1 -0
  41. package/dist/src/images/chroma.js +200 -0
  42. package/dist/src/images/chroma.js.map +1 -0
  43. package/dist/src/images/codec.js +159 -0
  44. package/dist/src/images/codec.js.map +1 -0
  45. package/dist/src/images/inputs.js +83 -0
  46. package/dist/src/images/inputs.js.map +1 -0
  47. package/dist/src/images/output.js +100 -0
  48. package/dist/src/images/output.js.map +1 -0
  49. package/dist/src/images/preview.js +20 -0
  50. package/dist/src/images/preview.js.map +1 -0
  51. package/dist/src/images/resize.js +135 -0
  52. package/dist/src/images/resize.js.map +1 -0
  53. package/dist/src/install/opencode.js +138 -0
  54. package/dist/src/install/opencode.js.map +1 -0
  55. package/dist/src/install/skill.js +128 -0
  56. package/dist/src/install/skill.js.map +1 -0
  57. package/dist/src/install/snippets.js +127 -0
  58. package/dist/src/install/snippets.js.map +1 -0
  59. package/dist/src/log.js +58 -0
  60. package/dist/src/log.js.map +1 -0
  61. package/dist/src/remove-background.js +101 -0
  62. package/dist/src/remove-background.js.map +1 -0
  63. package/dist/src/server/context.js +2 -0
  64. package/dist/src/server/context.js.map +1 -0
  65. package/dist/src/server/index.js +94 -0
  66. package/dist/src/server/index.js.map +1 -0
  67. package/dist/src/server/instructions.js +20 -0
  68. package/dist/src/server/instructions.js.map +1 -0
  69. package/dist/src/server/login-coordinator.js +73 -0
  70. package/dist/src/server/login-coordinator.js.map +1 -0
  71. package/dist/src/server/progress.js +33 -0
  72. package/dist/src/server/progress.js.map +1 -0
  73. package/dist/src/server/prompts.js +45 -0
  74. package/dist/src/server/prompts.js.map +1 -0
  75. package/dist/src/server/resources.js +73 -0
  76. package/dist/src/server/resources.js.map +1 -0
  77. package/dist/src/server/tools.js +338 -0
  78. package/dist/src/server/tools.js.map +1 -0
  79. package/dist/src/server/workspace.js +45 -0
  80. package/dist/src/server/workspace.js.map +1 -0
  81. package/dist/src/status.js +86 -0
  82. package/dist/src/status.js.map +1 -0
  83. package/dist/src/util/format.js +36 -0
  84. package/dist/src/util/format.js.map +1 -0
  85. package/dist/src/util/fs.js +117 -0
  86. package/dist/src/util/fs.js.map +1 -0
  87. package/dist/src/util/http.js +65 -0
  88. package/dist/src/util/http.js.map +1 -0
  89. package/dist/src/util/invocation.js +43 -0
  90. package/dist/src/util/invocation.js.map +1 -0
  91. package/dist/src/util/open.js +41 -0
  92. package/dist/src/util/open.js.map +1 -0
  93. package/docs/ARCHITECTURE.md +123 -0
  94. package/docs/AUTH.md +242 -0
  95. package/docs/BACKEND.md +169 -0
  96. package/docs/CLIENTS.md +154 -0
  97. package/docs/DEVELOPMENT.md +171 -0
  98. package/docs/README.md +84 -0
  99. package/docs/TOOLS.md +221 -0
  100. package/package.json +67 -0
  101. package/skill/imagegen/LICENSE.txt +201 -0
  102. package/skill/imagegen/SKILL.md +189 -0
  103. package/skill/imagegen/references/prompting.md +112 -0
  104. package/skill/imagegen/references/sample-prompts.md +418 -0
  105. package/skill/imagegen/references/tools.md +90 -0
@@ -0,0 +1,117 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { ImagegenError } from "../errors.js";
5
+ import { sleep } from "./http.js";
6
+ export async function ensureDir(dir, mode = 0o700) {
7
+ await fs.mkdir(dir, { recursive: true, mode });
8
+ }
9
+ export async function pathExists(p) {
10
+ try {
11
+ await fs.access(p);
12
+ return true;
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ }
18
+ /**
19
+ * Write a file atomically: write a temp file in the same directory, fsync, then rename over the
20
+ * target. Readers (including other processes) never observe a half-written credentials file.
21
+ */
22
+ export async function writeFileAtomic(file, data, mode = 0o600) {
23
+ const dir = path.dirname(file);
24
+ await ensureDir(dir);
25
+ const tmp = path.join(dir, `.${path.basename(file)}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`);
26
+ const handle = await fs.open(tmp, "w", mode);
27
+ try {
28
+ await handle.writeFile(data);
29
+ await handle.sync();
30
+ }
31
+ finally {
32
+ await handle.close();
33
+ }
34
+ try {
35
+ await fs.rename(tmp, file);
36
+ await fs.chmod(file, mode).catch(() => undefined);
37
+ }
38
+ catch (err) {
39
+ await fs.rm(tmp, { force: true }).catch(() => undefined);
40
+ throw err;
41
+ }
42
+ }
43
+ /** Parse a JSON file; `undefined` if it does not exist. Throws on malformed JSON. */
44
+ export async function readJsonFile(file) {
45
+ let text;
46
+ try {
47
+ text = await fs.readFile(file, "utf8");
48
+ }
49
+ catch (err) {
50
+ if (err.code === "ENOENT")
51
+ return undefined;
52
+ throw err;
53
+ }
54
+ return JSON.parse(text);
55
+ }
56
+ export function isPidAlive(pid) {
57
+ if (!Number.isInteger(pid) || pid <= 0)
58
+ return false;
59
+ try {
60
+ process.kill(pid, 0);
61
+ return true;
62
+ }
63
+ catch (err) {
64
+ return err.code === "EPERM";
65
+ }
66
+ }
67
+ /**
68
+ * Cross-process mutex based on an exclusively-created lock file. Used around token refresh so
69
+ * several server processes (e.g. one per opencode project) never spend the same single-use
70
+ * refresh token twice.
71
+ */
72
+ export async function withFileLock(lockPath, fn, options = {}) {
73
+ const timeoutMs = options.timeoutMs ?? 20_000;
74
+ const staleMs = options.staleMs ?? 60_000;
75
+ const deadline = Date.now() + timeoutMs;
76
+ await ensureDir(path.dirname(lockPath));
77
+ for (;;) {
78
+ try {
79
+ const handle = await fs.open(lockPath, "wx", 0o600);
80
+ try {
81
+ await handle.writeFile(JSON.stringify({ pid: process.pid, at: Date.now() }));
82
+ }
83
+ finally {
84
+ await handle.close();
85
+ }
86
+ break;
87
+ }
88
+ catch (err) {
89
+ if (err.code !== "EEXIST")
90
+ throw err;
91
+ const stat = await fs.stat(lockPath).catch(() => undefined);
92
+ if (!stat)
93
+ continue;
94
+ let stale = Date.now() - stat.mtimeMs > staleMs;
95
+ if (!stale) {
96
+ const owner = (await readJsonFile(lockPath).catch(() => undefined));
97
+ if (typeof owner?.pid === "number" && !isPidAlive(owner.pid))
98
+ stale = true;
99
+ }
100
+ if (stale) {
101
+ await fs.rm(lockPath, { force: true });
102
+ continue;
103
+ }
104
+ if (Date.now() > deadline) {
105
+ throw new ImagegenError("internal", `Timed out waiting for the credentials lock (${lockPath}). If no other process is signing in, delete that file.`);
106
+ }
107
+ await sleep(40 + Math.floor(Math.random() * 80));
108
+ }
109
+ }
110
+ try {
111
+ return await fn();
112
+ }
113
+ finally {
114
+ await fs.rm(lockPath, { force: true }).catch(() => undefined);
115
+ }
116
+ }
117
+ //# sourceMappingURL=fs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fs.js","sourceRoot":"","sources":["../../../src/util/fs.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,65 @@
1
+ /** Promise-based sleep that rejects with the signal's reason when aborted. */
2
+ export function sleep(ms, signal) {
3
+ return new Promise((resolve, reject) => {
4
+ if (signal?.aborted) {
5
+ reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
6
+ return;
7
+ }
8
+ const timer = setTimeout(() => {
9
+ signal?.removeEventListener("abort", onAbort);
10
+ resolve();
11
+ }, ms);
12
+ const onAbort = () => {
13
+ clearTimeout(timer);
14
+ reject(signal?.reason ?? new DOMException("Aborted", "AbortError"));
15
+ };
16
+ signal?.addEventListener("abort", onAbort, { once: true });
17
+ });
18
+ }
19
+ /** Combine optional abort signals (undefined entries are ignored). */
20
+ export function anySignal(...signals) {
21
+ const present = signals.filter((s) => s !== undefined);
22
+ if (present.length === 0)
23
+ return undefined;
24
+ if (present.length === 1)
25
+ return present[0];
26
+ return AbortSignal.any(present);
27
+ }
28
+ /** Read a response body once, parsing JSON when possible (never throws on bad JSON). */
29
+ export async function readBody(res) {
30
+ let text = "";
31
+ try {
32
+ text = await res.text();
33
+ }
34
+ catch {
35
+ return { text: "", json: undefined };
36
+ }
37
+ let json;
38
+ try {
39
+ json = text ? JSON.parse(text) : undefined;
40
+ }
41
+ catch {
42
+ json = undefined;
43
+ }
44
+ return { text, json };
45
+ }
46
+ export function snippet(text, max = 300) {
47
+ const flat = text.replace(/\s+/g, " ").trim();
48
+ return flat.length > max ? `${flat.slice(0, max)}…` : flat;
49
+ }
50
+ export function isRecord(value) {
51
+ return typeof value === "object" && value !== null && !Array.isArray(value);
52
+ }
53
+ export function str(value) {
54
+ return typeof value === "string" && value.length > 0 ? value : undefined;
55
+ }
56
+ export function num(value) {
57
+ if (typeof value === "number" && Number.isFinite(value))
58
+ return value;
59
+ if (typeof value === "string" && value.trim() !== "") {
60
+ const n = Number(value);
61
+ return Number.isFinite(n) ? n : undefined;
62
+ }
63
+ return undefined;
64
+ }
65
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.js","sourceRoot":"","sources":["../../../src/util/http.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { PACKAGE_NAME } from "../constants.js";
4
+ function realpathOrSelf(p) {
5
+ try {
6
+ return fs.realpathSync(p);
7
+ }
8
+ catch {
9
+ return p;
10
+ }
11
+ }
12
+ function quoteArg(arg) {
13
+ return /^[\w@%+=:,./-]+$/.test(arg) ? arg : JSON.stringify(arg);
14
+ }
15
+ /** Figure out how this CLI was launched so hints and installers can reproduce it. */
16
+ export function detectInvocation(argv = process.argv) {
17
+ const script = argv[1] ?? "";
18
+ const real = script ? realpathOrSelf(script) : "";
19
+ if (real.split(path.sep).includes("_npx"))
20
+ return { kind: "npx", script: real };
21
+ if (script && path.basename(script) === PACKAGE_NAME)
22
+ return { kind: "global", script: real };
23
+ return { kind: "local", script: real };
24
+ }
25
+ /** Command prefix for user-facing hints, e.g. `codex-imagegen-mcp` or `node "/abs/dist/src/cli.js"`. */
26
+ export function cliInvocation(argv = process.argv) {
27
+ const { kind, script } = detectInvocation(argv);
28
+ if (kind === "npx")
29
+ return `npx -y ${PACKAGE_NAME}`;
30
+ if (kind === "global" || !script.endsWith(".js"))
31
+ return PACKAGE_NAME;
32
+ return `node ${quoteArg(script)}`;
33
+ }
34
+ /** argv an MCP client should spawn to start this server over stdio. */
35
+ export function serverCommand(argv = process.argv) {
36
+ const { kind, script } = detectInvocation(argv);
37
+ if (kind === "npx")
38
+ return ["npx", "-y", `${PACKAGE_NAME}@latest`, "serve"];
39
+ if (kind === "global")
40
+ return [PACKAGE_NAME, "serve"];
41
+ return ["node", script, "serve"];
42
+ }
43
+ //# sourceMappingURL=invocation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invocation.js","sourceRoot":"","sources":["../../../src/util/invocation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAE/C,SAAS,cAAc,CAAC,CAAS;IAC/B,IAAI,CAAC;QACH,OAAO,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAC5B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,CAAC;IACX,CAAC;AACH,CAAC;AAED,SAAS,QAAQ,CAAC,GAAW;IAC3B,OAAO,kBAAkB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;AAClE,CAAC;AAID,qFAAqF;AACrF,MAAM,UAAU,gBAAgB,CAAC,IAAI,GAAsB,OAAO,CAAC,IAAI;IACrE,MAAM,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAC7B,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAClD,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IAChF,IAAI,MAAM,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,YAAY;QAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IAC9F,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;AACzC,CAAC;AAED,wGAAwG;AACxG,MAAM,UAAU,aAAa,CAAC,IAAI,GAAsB,OAAO,CAAC,IAAI;IAClE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC;IAChD,IAAI,IAAI,KAAK,KAAK;QAAE,OAAO,UAAU,YAAY,EAAE,CAAC;IACpD,IAAI,IAAI,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,YAAY,CAAC;IACtE,OAAO,QAAQ,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;AACpC,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,aAAa,CAAC,IAAI,GAAsB,OAAO,CAAC,IAAI;IAClE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC;IAChD,IAAI,IAAI,KAAK,KAAK;QAAE,OAAO,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,YAAY,SAAS,EAAE,OAAO,CAAC,CAAC;IAC5E,IAAI,IAAI,KAAK,QAAQ;QAAE,OAAO,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;IACtD,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;AACnC,CAAC"}
@@ -0,0 +1,41 @@
1
+ import { spawn } from "node:child_process";
2
+ /**
3
+ * Best-effort: open a URL in the user's default browser. Resolves `true` if a launcher process
4
+ * started. Honors $BROWSER. Never throws.
5
+ */
6
+ export function openInBrowser(url, env = process.env) {
7
+ let command;
8
+ let args;
9
+ const custom = env.BROWSER?.trim();
10
+ if (custom) {
11
+ command = custom;
12
+ args = [url];
13
+ }
14
+ else if (process.platform === "darwin") {
15
+ command = "open";
16
+ args = [url];
17
+ }
18
+ else if (process.platform === "win32") {
19
+ // rundll32 avoids cmd.exe re-parsing the `&` characters in OAuth URLs.
20
+ command = "rundll32";
21
+ args = ["url.dll,FileProtocolHandler", url];
22
+ }
23
+ else {
24
+ command = "xdg-open";
25
+ args = [url];
26
+ }
27
+ return new Promise((resolve) => {
28
+ try {
29
+ const child = spawn(command, args, { detached: true, stdio: "ignore" });
30
+ child.once("error", () => resolve(false));
31
+ child.once("spawn", () => {
32
+ child.unref();
33
+ resolve(true);
34
+ });
35
+ }
36
+ catch {
37
+ resolve(false);
38
+ }
39
+ });
40
+ }
41
+ //# sourceMappingURL=open.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"open.js","sourceRoot":"","sources":["../../../src/util/open.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAE3C;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,GAAW,EAAE,GAAG,GAAsB,OAAO,CAAC,GAAG;IAC7E,IAAI,OAAe,CAAC;IACpB,IAAI,IAAc,CAAC;IACnB,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC;IACnC,IAAI,MAAM,EAAE,CAAC;QACX,OAAO,GAAG,MAAM,CAAC;QACjB,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;IACf,CAAC;SAAM,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;QACzC,OAAO,GAAG,MAAM,CAAC;QACjB,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;IACf,CAAC;SAAM,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QACxC,uEAAuE;QACvE,OAAO,GAAG,UAAU,CAAC;QACrB,IAAI,GAAG,CAAC,6BAA6B,EAAE,GAAG,CAAC,CAAC;IAC9C,CAAC;SAAM,CAAC;QACN,OAAO,GAAG,UAAU,CAAC;QACrB,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;IACf,CAAC;IACD,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,IAAI,CAAC;YACH,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC;YACxE,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;YAC1C,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE;gBACvB,KAAK,CAAC,KAAK,EAAE,CAAC;gBACd,OAAO,CAAC,IAAI,CAAC,CAAC;YAChB,CAAC,CAAC,CAAC;QACL,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,CAAC,KAAK,CAAC,CAAC;QACjB,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,123 @@
1
+ <p align="center">
2
+ <img src="assets/banner-architecture.jpg" width="100%" alt="Architecture — a travel-poster illustration of a three-tier stone viaduct spanning a canyon, its central arch framing a sun rising behind a peak">
3
+ </p>
4
+
5
+ # Architecture
6
+
7
+ A small, layered codebase: two entry points, a shared engine, and three focused subsystems for auth, the backend and images. This page maps the modules, traces one request end to end, and explains the decisions behind the design.
8
+
9
+ **On this page:** [Module map](#module-map) · [Modules](#modules) · [Request flow](#request-flow-generate_image) · [Design decisions](#design-decisions)
10
+
11
+ ## Module map
12
+
13
+ ```mermaid
14
+ flowchart TB
15
+ client(["MCP client<br/>opencode · Claude Code · …"]):::ink
16
+ term(["terminal"]):::ink
17
+ subgraph entry ["entry points"]
18
+ server["src/server/<br/>tools · resources · prompts<br/>progress · roots · background sign-in"]:::rust
19
+ cli["src/cli.ts<br/>login · status · generate · install · doctor"]:::rust
20
+ end
21
+ subgraph engine ["shared engine"]
22
+ gen["generation.ts · remove-background.ts · status.ts"]:::ochre
23
+ end
24
+ subgraph subsystems ["subsystems"]
25
+ backend["src/backend/<br/>images-client · ratelimits"]:::teal
26
+ auth["src/auth/<br/>manager · oauth · browser/device login<br/>store (own) · borrowed (read-only)"]:::teal
27
+ images["src/images/<br/>codec · resize · preview · chroma<br/>inputs · output"]:::teal
28
+ end
29
+ chatgpt[("chatgpt.com/backend-api<br/>codex/images · wham/usage")]:::cream
30
+ openai[("auth.openai.com<br/>oauth · deviceauth")]:::cream
31
+ client -- "stdio JSON-RPC" --> server
32
+ term --> cli
33
+ server --> gen
34
+ cli --> gen
35
+ gen --> backend
36
+ gen --> images
37
+ backend --> auth
38
+ backend --> chatgpt
39
+ auth --> openai
40
+ classDef ink fill:#2A2523,stroke:#9A8C76,color:#E4D9C6
41
+ classDef rust fill:#A6553B,stroke:#7E3F2B,color:#FFFFFF
42
+ classDef ochre fill:#D9A05B,stroke:#B5813F,color:#2A2523
43
+ classDef teal fill:#4E6E63,stroke:#3A544B,color:#FFFFFF
44
+ classDef cream fill:#E4D9C6,stroke:#A89A80,color:#2A2523
45
+ style entry fill:#D9A05B1F,stroke:#B5813F,color:#B5813F
46
+ style engine fill:#D9A05B1F,stroke:#B5813F,color:#B5813F
47
+ style subsystems fill:#D9A05B1F,stroke:#B5813F,color:#B5813F
48
+ ```
49
+
50
+ ## Modules
51
+
52
+ | Path | Responsibility |
53
+ |---|---|
54
+ | `src/constants.ts` | Protocol constants (issuer, client id, ports, endpoints, limits), each with the reason it has that value |
55
+ | `src/config.ts` | Resolves the environment into a `RuntimeConfig`: paths, URLs, mode, timeouts |
56
+ | `src/auth/` | Sign-in flows, the token store, refresh, and credential source resolution |
57
+ | `src/backend/` | The ChatGPT image and usage HTTP client, error mapping, rate-limit parsing |
58
+ | `src/images/` | Pure-JS image handling: sniffing, codec, resampling, previews, chroma key, I/O |
59
+ | `src/generation.ts` | One generate/edit operation: load inputs → *n* concurrent requests → save → history → previews |
60
+ | `src/remove-background.ts` | One chroma-key operation |
61
+ | `src/status.ts` | Collects and formats sign-in, source and usage status |
62
+ | `src/history.ts` | The append-only `history.jsonl` |
63
+ | `src/server/` | MCP wiring: tools, resources, prompts, instructions, progress, roots, background sign-in |
64
+ | `src/install/` | The opencode installer (JSONC edits, skill copy) and snippets for other clients |
65
+ | `src/doctor.ts` | Environment diagnostics |
66
+ | `src/cli.ts` | The command-line entry point; `serve` is the MCP entry |
67
+ | `skill/imagegen/` | The Agent Skill shipped with the server |
68
+ | `upstream/` | Byte-exact copy of Codex's original skill, for provenance and diffing |
69
+ | `scripts/compose-doc-art.py` | Builds the documentation artwork from raw generations |
70
+
71
+ ## Request flow: `generate_image`
72
+
73
+ ```mermaid
74
+ sequenceDiagram
75
+ autonumber
76
+ participant T as tool handler
77
+ participant G as runGeneration
78
+ participant I as ImagesClient
79
+ participant A as AuthManager
80
+ participant B as ChatGPT backend
81
+ T->>T: start progress heartbeats, resolve workspace (MCP root → cwd)
82
+ T->>G: request
83
+ G->>G: validate, load inputs, add the aspect-ratio line, plan output paths
84
+ par one per variant (n = 1–4)
85
+ G->>I: createImage
86
+ I->>A: getCredentials (refresh under the lock if needed)
87
+ A-->>I: bearer token + account id
88
+ I->>B: POST Codex-identical body
89
+ alt 401
90
+ I->>A: recoverFromUnauthorized (refresh, re-read or next source)
91
+ I->>B: retry once
92
+ else 5xx or network error
93
+ I->>B: retry after 1 s, then 3 s
94
+ end
95
+ B-->>I: PNG
96
+ I-->>G: bytes + metadata
97
+ G->>G: convert if JPEG, write without overwriting, verify alpha, preview, append history
98
+ end
99
+ G-->>T: saved images + partial failures
100
+ T-->>T: text + preview images + structuredContent (or isError with the next step)
101
+ ```
102
+
103
+ Each variant is saved **as soon as it arrives**, so partial success is kept even if another variant fails.
104
+
105
+ ## Design decisions
106
+
107
+ - **ChatGPT sign-in only, no API key.** The goal is to use the subscription people already pay for, exactly like Codex. The Platform API needs separate billing and is deliberately out of scope.
108
+ - **The same request as Codex.** Endpoints, body and auth match `codex-rs` byte for byte, so the server behaves like Codex and inherits OpenAI's server-side model upgrades.
109
+ - **Borrowing is read-only.** Refresh tokens are single-use, so a second refresher would sign the owning app out. Borrowed tokens are used until they expire, and are never refreshed or written.
110
+ - **Refresh is lock-serialized.** Clients spawn several server processes (opencode runs one per project). A lock file plus a re-read under the lock means exactly one refresh per rotation.
111
+ - **Honest parameters.** The service ignores model, size, quality, count and format, so the tools don't pretend otherwise. `aspect_ratio` is a prompt line, which the service measurably honors; `n` is fan-out; JPEG is a local conversion.
112
+ - **Previews, not full images, in tool results.** A 1024 px JPEG goes to the model and the full-resolution file goes to disk. That keeps results far below the MCP SDK's 10 MB stdio limit and clients' attachment limits (opencode: 5 MiB, 2000 px), while still letting the model check its work.
113
+ - **No `resource_link` content.** Older clients reject unknown content types, and opencode drops them. Paths go in text and `structuredContent` instead.
114
+ - **Constraints repeated in descriptions.** opencode strips `min`, `max` and `default` from JSON Schemas for OpenAI models, so every limit is also written in the parameter description.
115
+ - **Progress heartbeats.** opencode resets its per-request timeout on progress, so long generations survive its 60 s default without special configuration.
116
+ - **Background sign-in inside the server.** `sign_in` can hand the user a link at once. MCP elicitation isn't widely supported (opencode lacks it), so returning the link as text is the portable choice.
117
+ - **Pure-JS image handling.** `pngjs` and `jpeg-js` mean no native dependencies, so it installs everywhere, which matters because MCP servers are launched from many environments.
118
+ - **Never overwrite.** This mirrors the Codex skill's save policy, and it's enforced in code (`wx` exclusive create with versioned siblings) rather than merely requested of the model.
119
+ - **stdout hygiene.** In `serve`, `console.log`, `console.info` and `console.debug` are redirected to stderr, and logs go to stderr plus `server.log`. The server exits when stdin closes, which is how clients stop stdio servers.
120
+
121
+ ---
122
+
123
+ <p align="center"><a href="BACKEND.md">← Backend</a> &nbsp;·&nbsp; <a href="README.md">Docs home</a> &nbsp;·&nbsp; <a href="DEVELOPMENT.md">Development →</a></p>