backdraw 0.0.0-stage → 2.1.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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +606 -2
  3. package/README.tr.md +624 -0
  4. package/bin/backdraw.js +56 -0
  5. package/cli.js +105 -0
  6. package/doctor.js +174 -0
  7. package/extension/_locales/en/messages.json +3 -0
  8. package/extension/_locales/tr/messages.json +3 -0
  9. package/extension/aspect.js +25 -0
  10. package/extension/background.js +1015 -0
  11. package/extension/collection.html +45 -0
  12. package/extension/collection.js +62 -0
  13. package/extension/collections.js +409 -0
  14. package/extension/i18n.js +359 -0
  15. package/extension/icons/icon128.png +0 -0
  16. package/extension/icons/icon16.png +0 -0
  17. package/extension/icons/icon32.png +0 -0
  18. package/extension/icons/icon48.png +0 -0
  19. package/extension/icons/mascot-192.png +0 -0
  20. package/extension/icons/mascot-96.png +0 -0
  21. package/extension/inpage.js +624 -0
  22. package/extension/manifest.json +20 -0
  23. package/extension/menus.js +50 -0
  24. package/extension/popup.css +90 -0
  25. package/extension/popup.html +63 -0
  26. package/extension/popup.js +376 -0
  27. package/extension/settings.js +35 -0
  28. package/extension/setup.js +57 -0
  29. package/extension/sidepanel.css +113 -0
  30. package/extension/sidepanel.html +31 -0
  31. package/extension/sidepanel.js +433 -0
  32. package/extension/theme.css +22 -0
  33. package/host.js +664 -0
  34. package/identity.js +21 -0
  35. package/install.js +427 -0
  36. package/lib/args.js +30 -0
  37. package/lib/aspect.js +85 -0
  38. package/lib/browsers.js +74 -0
  39. package/lib/collection-store.js +288 -0
  40. package/lib/collections.js +192 -0
  41. package/lib/doctor.js +143 -0
  42. package/lib/extension-calls.js +59 -0
  43. package/lib/gallery.js +63 -0
  44. package/lib/limits.js +18 -0
  45. package/lib/mcp-clients.js +178 -0
  46. package/lib/naming.js +25 -0
  47. package/lib/png.js +24 -0
  48. package/lib/projects.js +56 -0
  49. package/lib/prompt.js +25 -0
  50. package/lib/protocol.js +50 -0
  51. package/lib/results.js +170 -0
  52. package/lib/term.js +22 -0
  53. package/lib/toml.js +248 -0
  54. package/mcp-server.js +437 -0
  55. package/package.json +54 -4
  56. package/platform/index.js +14 -0
  57. package/platform/win32.js +268 -0
  58. package/setup.js +212 -0
  59. package/shared.js +118 -0
package/mcp-server.js ADDED
@@ -0,0 +1,437 @@
1
+ 'use strict';
2
+ // MCP server (stdio, newline-delimited JSON-RPC) that Claude Code starts. It is a thin client:
3
+ // every tool call is forwarded to the native host that Chrome runs for the extension.
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const readline = require('readline');
7
+ const identity = require('./identity');
8
+ const bridge = require('./shared');
9
+ const protocol = require('./lib/protocol');
10
+ const { FINAL, DEFAULT_WAIT, waitSeconds, resultLines } = require('./lib/results');
11
+ const { MAX_REFS } = require('./lib/limits');
12
+ const { describeCollections } = require('./lib/collections');
13
+
14
+ const SERVER_INFO = { name: identity.MCP_NAME, title: identity.PRODUCT, version: bridge.VERSION };
15
+
16
+ const INSTRUCTIONS = `Generates images with the user's own ChatGPT account (already logged in in Google Chrome), with no OpenAI API key and no extra cost.
17
+ - When the user asks for an image "with ChatGPT" (in any language), call generate_image with a detailed, self-contained prompt.
18
+ - generate_image and fetch_images return within about ${DEFAULT_WAIT} seconds. Generating usually takes 1-3 minutes, so the first result is often STILL RUNNING or QUEUED with a job id: keep calling job_status(job_id, wait_seconds=${DEFAULT_WAIT}) until it reports a final status. Do not call generate_image again for the same request while its job is still running.
19
+ - To EDIT an image or keep a character consistent, pass the image file(s) in reference_images (e.g. a file saved by an earlier call). A fresh chat with reference_images is the most reliable way to edit; long revision chains via conversation_url make ChatGPT lose track of earlier images.
20
+ - For a specific shape (banner, avatar, phone wallpaper) pass aspect_ratio instead of describing the size in the prompt; for stickers/logos/icons that need a see-through background pass transparent_background=true and read the transparency line in the result.
21
+ - When the user mentions one of their Backdraw collections (reference images they saved from the web in Chrome), call list_collections if needed and pass reference_collection to generate_image.
22
+ - Read every result line: NEEDS REFERENCE IMAGE means ChatGPT could not reach the image to edit; CHATGPT REPLIED WITHOUT AN IMAGE shows ChatGPT's own text. Never resend the identical prompt in a loop; follow the next step given in the result.
23
+ - An EDIT CHECK warning means ChatGPT drew a new picture instead of editing; tell the user and retry with reference_images in a fresh chat if an exact edit is needed.
24
+ - The work happens in a background Chrome tab; never tell the user to switch tabs unless a result says NEEDS USER ACTION.
25
+ - NEEDS USER ACTION means ChatGPT shows a login or human-verification (CAPTCHA) page: ask the user to open the ChatGPT tab in Chrome and finish it themselves. Never try to get around it. The job continues automatically afterwards (check with job_status).
26
+ - If no project is selected, call list_projects, then select_project with the user's images project.
27
+ - If a job timed out but has a conversation link, fetch_images can still download the result later; pass the job's aspect_ratio again so it is cropped the same way.
28
+ - Report the saved file path(s) and the ChatGPT conversation link to the user.`;
29
+
30
+ // Shared by the tools that wait for a job.
31
+ const WAIT_SECONDS = {
32
+ type: 'integer', minimum: 0, maximum: 900, default: DEFAULT_WAIT,
33
+ description: `Return within this many seconds. The default ${DEFAULT_WAIT} keeps each call under the 60-second tool limit some clients have. ` +
34
+ 'If the job has not finished by then, the result says STILL RUNNING or QUEUED: call job_status with its job_id again.',
35
+ };
36
+
37
+ const TOOLS = [
38
+ {
39
+ name: 'generate_image',
40
+ title: 'Generate or edit an image with ChatGPT',
41
+ description:
42
+ "Generate or edit an image with ChatGPT through the user's own logged-in ChatGPT account in Google Chrome (no OpenAI API key, no extra cost). " +
43
+ "A new chat is started inside the user's selected ChatGPT Project, in a background tab: the user's active tab, window focus, mouse and keyboard are never touched. " +
44
+ `Waits up to wait_seconds (default ${DEFAULT_WAIT}) and returns: SUCCESS with the saved file path(s) (always kept in the images folder), a preview and the conversation link; ` +
45
+ "NEEDS REFERENCE IMAGE / CHATGPT REPLIED WITHOUT AN IMAGE together with ChatGPT's own reply text; " +
46
+ 'or, while ChatGPT is still working, STILL RUNNING / QUEUED with a job id: then keep calling job_status with that job_id. ' +
47
+ "Use it whenever the user asks for an image, visual, illustration or concept art with ChatGPT, in any language. " +
48
+ 'Write a detailed, self-contained prompt (subject, style, composition, colors, mood); give the shape in aspect_ratio rather than in the prompt. ' +
49
+ 'To edit an image or keep a character consistent, pass the image file(s) in reference_images: a fresh chat with reference_images is the most reliable way to edit. ' +
50
+ "To use one of the user's saved reference collections (see list_collections), pass reference_collection.",
51
+ inputSchema: {
52
+ type: 'object',
53
+ properties: {
54
+ prompt: { type: 'string', description: 'Full image description or edit instructions, in any language.' },
55
+ aspect_ratio: {
56
+ type: 'string',
57
+ description: 'Optional shape as W:H, e.g. 1:1, 3:2, 2:3, 4:3, 3:4, 16:9, 9:16, 4:5, 5:2 (Discord banner) or 3:1 (header). ' +
58
+ 'Any W:H with whole numbers between 1:4 and 4:1 works (not 1.5:1: use 3:2). ChatGPT draws 1:3 to 3:1 itself; wider or taller shapes ' +
59
+ 'are drawn at 3:1 / 1:3 and cropped. An image more than 1% off is cropped from the centre to this ratio (to the nearest pixel), and the full image is kept too.',
60
+ },
61
+ transparent_background: {
62
+ type: 'boolean', default: false,
63
+ description: 'Ask for a PNG with a transparent background (stickers, logos, icons, sprites). The result says whether the saved image really has transparent pixels.',
64
+ },
65
+ reference_images: {
66
+ type: 'array', items: { type: 'string' }, maxItems: MAX_REFS,
67
+ description: `Optional local image files (png/jpg/webp/gif, up to ${MAX_REFS}) attached to the message, e.g. the image to edit or a character sheet. Paths saved by earlier calls work directly.`,
68
+ },
69
+ conversation_url: {
70
+ type: 'string',
71
+ description: 'Optional ChatGPT conversation link from an earlier result: the prompt is sent in that chat instead of a new one. Fine for quick follow-ups; for exact edits prefer reference_images in a fresh chat.',
72
+ },
73
+ auto_reattach: {
74
+ type: 'boolean', default: false,
75
+ description: "If ChatGPT answers that it can't access the image and asks for it to be uploaded, attach the chat's latest image (or reference_images) automatically and ask once more.",
76
+ },
77
+ output_dir: {
78
+ type: 'string',
79
+ description: "Optional folder to also copy the image(s) into, e.g. the current project's assets folder (absolute path preferred). The central images folder always keeps a copy.",
80
+ },
81
+ wait_seconds: WAIT_SECONDS,
82
+ include_preview: { type: 'boolean', default: true, description: 'Attach a downscaled preview of the result so you can check it.' },
83
+ send_as_is: { type: 'boolean', default: false, description: 'Send the prompt verbatim, without the short instruction the bridge adds (useful to answer a question ChatGPT asked).' },
84
+ reference_collection: {
85
+ type: 'string',
86
+ description: "Optional name of one of the user's Backdraw reference collections (see list_collections; matched loosely). " +
87
+ `Its images are attached after any reference_images, up to ${MAX_REFS} references in total: over the limit the newest are used, unless reference_files picks others.`,
88
+ },
89
+ reference_files: {
90
+ type: 'array', items: { type: 'string' }, maxItems: MAX_REFS,
91
+ description: 'Optional file names from list_collections to attach only those images of reference_collection, instead of the newest.',
92
+ },
93
+ },
94
+ required: ['prompt'],
95
+ additionalProperties: false,
96
+ },
97
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
98
+ },
99
+ {
100
+ name: 'job_status',
101
+ title: 'ChatGPT image job status',
102
+ description:
103
+ `Check a ChatGPT image job by job_id, waiting up to wait_seconds (default ${DEFAULT_WAIT}) for it to finish; call it again while the result says STILL RUNNING or QUEUED. ` +
104
+ 'Without job_id it reports the bridge itself: whether Chrome and the extension are connected, the selected ChatGPT project, the images folder, the queue and recent jobs.',
105
+ inputSchema: {
106
+ type: 'object',
107
+ properties: {
108
+ job_id: { type: 'string', description: 'Job id returned by generate_image (e.g. img_1a2b3c4d).' },
109
+ wait_seconds: WAIT_SECONDS,
110
+ include_preview: { type: 'boolean', default: true, description: 'Attach the image preview when the job succeeded.' },
111
+ },
112
+ additionalProperties: false,
113
+ },
114
+ // Not read-only: a finished job's images are copied into the output_dir generate_image asked for
115
+ // here, and with the short default wait that is where it usually happens. It only adds files, once.
116
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
117
+ },
118
+ {
119
+ name: 'cancel_job',
120
+ title: 'Cancel a ChatGPT image job',
121
+ description: 'Cancel a queued or running ChatGPT image job. If ChatGPT is already generating, it presses Stop in the background tab and closes that tab.',
122
+ inputSchema: {
123
+ type: 'object',
124
+ properties: { job_id: { type: 'string', description: 'Job id to cancel.' } },
125
+ required: ['job_id'],
126
+ additionalProperties: false,
127
+ },
128
+ // Stopping ChatGPT mid-generation can't be undone; cancelling twice changes nothing more.
129
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
130
+ },
131
+ {
132
+ name: 'fetch_images',
133
+ title: 'Fetch images from a ChatGPT conversation',
134
+ description:
135
+ 'Download the generated images of the latest answer in an existing ChatGPT conversation (by its link) without sending anything. ' +
136
+ 'Use it when a job timed out or failed after ChatGPT had started, but the chat finished later, or to save images from an earlier chat. ' +
137
+ `Like generate_image it returns within wait_seconds (default ${DEFAULT_WAIT}); if it says STILL RUNNING or QUEUED, keep calling job_status with its job_id.`,
138
+ inputSchema: {
139
+ type: 'object',
140
+ properties: {
141
+ conversation_url: { type: 'string', description: 'ChatGPT conversation link (https://chatgpt.com/…/c/…).' },
142
+ aspect_ratio: { type: 'string', description: 'Optional W:H (between 1:4 and 4:1, e.g. 16:9) to crop the downloaded images to, from the centre; the full images are kept too. For the result of a generate_image job, pass its aspect_ratio again.' },
143
+ output_dir: { type: 'string', description: 'Optional folder to also copy the image(s) into (absolute path preferred).' },
144
+ wait_seconds: WAIT_SECONDS,
145
+ include_preview: { type: 'boolean', default: true, description: 'Attach a downscaled preview.' },
146
+ },
147
+ required: ['conversation_url'],
148
+ additionalProperties: false,
149
+ },
150
+ // Not read-only: it saves the downloaded images as new files.
151
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
152
+ },
153
+ {
154
+ name: 'list_projects',
155
+ title: 'List ChatGPT projects',
156
+ description: "List the Projects in the user's ChatGPT account (read from ChatGPT's sidebar in a background tab) and show which one is selected for images. Use it during setup.",
157
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false },
158
+ annotations: { readOnlyHint: true, openWorldHint: true },
159
+ },
160
+ {
161
+ name: 'select_project',
162
+ title: 'Select the ChatGPT project for images',
163
+ description: 'Select and remember the ChatGPT Project where new image chats are created. Pass the project name as shown in ChatGPT (matched loosely) or its https://chatgpt.com/g/g-p-…/project link.',
164
+ inputSchema: {
165
+ type: 'object',
166
+ properties: {
167
+ name: { type: 'string', description: 'Project name as shown in ChatGPT.' },
168
+ url: { type: 'string', description: 'Project link (alternative to name).' },
169
+ },
170
+ additionalProperties: false,
171
+ },
172
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
173
+ },
174
+ {
175
+ name: 'list_collections',
176
+ title: 'List Backdraw reference collections',
177
+ description:
178
+ 'List the reference collections the user saved in Chrome (right-click an image, "Add to Backdraw"): each with its image count, folder and files (newest first, with the site they came from). ' +
179
+ `Pass a collection's name as generate_image's reference_collection to attach its images (up to ${MAX_REFS} references; reference_files picks specific ones).`,
180
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false },
181
+ annotations: { readOnlyHint: true, openWorldHint: false },
182
+ },
183
+ ];
184
+
185
+ // ---------------------------------------------------------------- JSON-RPC over stdio
186
+
187
+ const frame = msg => ({ jsonrpc: '2.0', ...msg });
188
+ function send(msg) { process.stdout.write(JSON.stringify(frame(msg)) + '\n'); }
189
+ function logErr(...args) { process.stderr.write(`[${identity.MCP_NAME}] ${args.join(' ')}\n`); }
190
+
191
+ // An error answer leaves the id out when the message had none we can echo (MCP never allows a null id).
192
+ const errorReply = (id, error) => (id === undefined || id === null ? { error } : { id, error });
193
+ const INVALID_REQUEST = { code: -32600, message: 'Invalid Request' };
194
+
195
+ // Tool calls in progress, by request id. A client's notifications/cancelled marks one, and nothing
196
+ // more is sent for it (no progress, no result). The ChatGPT job keeps running: cancel_job stops that.
197
+ const inFlight = new Map();
198
+
199
+ // respond sends this message's response; a batch collects them into one array instead.
200
+ async function handle(msg, respond = send) {
201
+ if (!msg || typeof msg !== 'object' || Array.isArray(msg)) return respond(errorReply(null, INVALID_REQUEST));
202
+ if (typeof msg.method !== 'string') {
203
+ // A response (to a request this server never sends) needs no answer; anything else is malformed.
204
+ if (!('result' in msg) && !('error' in msg)) respond(errorReply(msg.id, INVALID_REQUEST));
205
+ return;
206
+ }
207
+ const isRequest = msg.id !== undefined && msg.id !== null;
208
+ const version = protocol.requestVersion(msg.params);
209
+ if (version !== null && !protocol.SUPPORTED_VERSIONS.includes(version)) {
210
+ if (isRequest) respond({ id: msg.id, error: protocol.unsupportedVersion(version) });
211
+ return;
212
+ }
213
+ // A notification never gets a response, whatever its method.
214
+ if (!isRequest) {
215
+ const call = msg.method === 'notifications/cancelled' && msg.params && inFlight.get(msg.params.requestId);
216
+ if (call) call.cancelled = true;
217
+ return;
218
+ }
219
+ // 2026-07-28 also requires _meta clientCapabilities on every request. A request without it is
220
+ // served as if it declared none: this server never reads client capabilities, and refusing would
221
+ // only break image generation for a client that leaves out a field nothing here uses.
222
+ const modern = protocol.MODERN_VERSIONS.includes(version);
223
+ const reply = result => respond({ id: msg.id, result: modern ? protocol.modernResult(msg.method, result, SERVER_INFO) : result });
224
+ try {
225
+ switch (msg.method) {
226
+ case 'initialize': {
227
+ const protocolVersion = protocol.negotiateLegacy(msg.params && msg.params.protocolVersion);
228
+ return respond({ id: msg.id, result: { protocolVersion, capabilities: { tools: { listChanged: false } }, serverInfo: SERVER_INFO, instructions: INSTRUCTIONS } });
229
+ }
230
+ case 'server/discover': return respond({ id: msg.id, result: protocol.discoverResult(SERVER_INFO, INSTRUCTIONS) });
231
+ case 'ping': return reply({}); // legacy only, but harmless to answer
232
+ case 'tools/list': return reply({ tools: TOOLS });
233
+ case 'tools/call': {
234
+ const p = msg.params || {};
235
+ const call = { progressToken: p._meta && p._meta.progressToken, cancelled: false };
236
+ inFlight.set(msg.id, call);
237
+ try {
238
+ const result = await callTool(p.name, p.arguments || {}, call);
239
+ if (!call.cancelled) reply(result);
240
+ } finally {
241
+ inFlight.delete(msg.id);
242
+ }
243
+ return;
244
+ }
245
+ case 'resources/list': return reply({ resources: [] });
246
+ case 'prompts/list': return reply({ prompts: [] });
247
+ default:
248
+ respond({ id: msg.id, error: { code: -32601, message: `Method not found: ${msg.method}` } });
249
+ }
250
+ } catch (e) {
251
+ logErr('request failed:', e.stack || e);
252
+ respond({ id: msg.id, error: { code: -32603, message: String(e.message || e) } });
253
+ }
254
+ }
255
+
256
+ const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
257
+ rl.on('line', async line => {
258
+ if (!line.trim()) return;
259
+ let msg;
260
+ try { msg = JSON.parse(line); } catch { return send(errorReply(null, { code: -32700, message: 'Parse error' })); }
261
+ if (!Array.isArray(msg) || !msg.length) return handle(msg);
262
+ // A JSON-RPC batch (2025-03-26 clients may send one): all its responses go back in one array.
263
+ const responses = [];
264
+ await Promise.all(msg.map(m => handle(m, r => responses.push(frame(r)))));
265
+ if (responses.length) process.stdout.write(JSON.stringify(responses) + '\n');
266
+ });
267
+ rl.on('close', () => process.exit(0));
268
+
269
+ // ---------------------------------------------------------------- tools
270
+
271
+ const sleep = ms => new Promise(r => setTimeout(r, ms));
272
+ const textResult = (text, isError = false) => ({ content: [{ type: 'text', text }], isError });
273
+ // The wait counts from the start of the tool call: starting a job with reference images can take
274
+ // a few seconds itself, and the whole call has to fit in the client's tool-call limit.
275
+ const deadlineFrom = args => Date.now() + waitSeconds(args) * 1000;
276
+ const secondsLeft = deadline => Math.max(0, (deadline - Date.now()) / 1000);
277
+
278
+ // call: { progressToken, cancelled } of the tools/call request.
279
+ async function callTool(name, args, call) {
280
+ try {
281
+ switch (name) {
282
+ case 'generate_image': return await generateImage(args, call);
283
+ case 'job_status': return await jobStatus(args, call);
284
+ case 'cancel_job': return await cancelJob(args, call);
285
+ case 'fetch_images': return await fetchImages(args, call);
286
+ case 'list_projects': return await listProjects();
287
+ case 'select_project': return await selectProject(args);
288
+ case 'list_collections': return await listCollections();
289
+ default: return textResult(`Unknown tool: ${name}`, true);
290
+ }
291
+ } catch (e) {
292
+ return textResult(e.code === 'NOT_RUNNING' ? `BRIDGE NOT RUNNING — ${e.message}` : `ERROR — ${e.message}`, true);
293
+ }
294
+ }
295
+
296
+ async function generateImage(args, call) {
297
+ const deadline = deadlineFrom(args);
298
+ const prompt = String(args.prompt || '').trim();
299
+ if (!prompt) return textResult('prompt is required.', true);
300
+ const references = bridge.readReferences(args.reference_images);
301
+ const job = await bridge.call('generate', {
302
+ prompt, references, autoReattach: !!args.auto_reattach, sendAsIs: !!args.send_as_is,
303
+ aspect: args.aspect_ratio, transparent: !!args.transparent_background,
304
+ copyTo: args.output_dir ? path.resolve(String(args.output_dir)) : undefined,
305
+ startUrl: args.conversation_url ? String(args.conversation_url) : undefined,
306
+ referenceCollection: args.reference_collection ? String(args.reference_collection) : undefined,
307
+ // A single file name as a string works too, like reference_images.
308
+ referenceFiles: args.reference_files ? [].concat(args.reference_files).map(String) : undefined,
309
+ }, 60000);
310
+ const done = await waitForJob(job.id, secondsLeft(deadline), call);
311
+ return jobResult(await finish(done), args.include_preview !== false);
312
+ }
313
+
314
+ async function fetchImages(args, call) {
315
+ const deadline = deadlineFrom(args);
316
+ const url = String(args.conversation_url || '').trim();
317
+ if (!url) return textResult('conversation_url is required.', true);
318
+ const copyTo = args.output_dir ? path.resolve(String(args.output_dir)) : undefined;
319
+ const job = await bridge.call('generate', { mode: 'collect', startUrl: url, copyTo, aspect: args.aspect_ratio });
320
+ const done = await waitForJob(job.id, secondsLeft(deadline), call);
321
+ return jobResult(await finish(done), args.include_preview !== false);
322
+ }
323
+
324
+ async function jobStatus(args, call) {
325
+ if (!args.job_id) return bridgeStatus();
326
+ const id = String(args.job_id).trim();
327
+ const job = await waitForJob(id, secondsLeft(deadlineFrom(args)), call, toldUser.has(id));
328
+ return jobResult(await finish(job), args.include_preview !== false);
329
+ }
330
+
331
+ async function cancelJob(args, call) {
332
+ const id = String(args.job_id || '').trim();
333
+ if (!id) return textResult('job_id is required.', true);
334
+ await bridge.call('cancel', { jobId: id });
335
+ // A job being cancelled needs nothing from the user any more: wait for it to stop instead.
336
+ return jobResult(await waitForJob(id, 25, call, true), false);
337
+ }
338
+
339
+ const finish = job => bridge.copyResults(job);
340
+
341
+ // Jobs whose current NEEDS USER ACTION the client has already been given.
342
+ const toldUser = new Set();
343
+
344
+ // A job that starts to need the user (login or CAPTCHA) returns at once so the user can be asked.
345
+ // humanKnown: the client already knows the job needs the user, so while it still does it is waited
346
+ // on like a running one (no instant answers to a polling loop); needing the user again after that
347
+ // is new and returns at once.
348
+ async function waitForJob(jobId, seconds, call, humanKnown = false) {
349
+ const deadline = Date.now() + seconds * 1000;
350
+ let job = await bridge.call('job', { jobId });
351
+ let tick = 0;
352
+ let lastNote = '';
353
+ let lastSent = 0;
354
+ let known = humanKnown && job.status === 'needs_human';
355
+ while (!call.cancelled && !FINAL.has(job.status) && (known || job.status !== 'needs_human') && Date.now() < deadline) {
356
+ await sleep(1500);
357
+ try {
358
+ job = await bridge.call('job', { jobId });
359
+ } catch (e) {
360
+ if (e.code === 'NOT_RUNNING' || e.code === 'CLOSED') {
361
+ throw new bridge.BridgeError(`Lost the connection to Chrome while job ${jobId} was ${job.status} (${job.message || job.step}). ${bridge.NOT_RUNNING}`, e.code);
362
+ }
363
+ throw e;
364
+ }
365
+ known = known && job.status === 'needs_human';
366
+ const note = `${job.status}: ${job.message || job.step}`;
367
+ const { progressToken } = call;
368
+ if (progressToken !== undefined && !call.cancelled && (note !== lastNote || Date.now() - lastSent > 10000)) {
369
+ send({ method: 'notifications/progress', params: { progressToken, progress: ++tick, message: note } });
370
+ lastNote = note;
371
+ lastSent = Date.now();
372
+ }
373
+ }
374
+ return job;
375
+ }
376
+
377
+ function jobResult(job, includePreview) {
378
+ if (job.status === 'needs_human') toldUser.add(job.id); else toldUser.delete(job.id);
379
+ const content = [{ type: 'text', text: resultLines(job).join('\n') }];
380
+ if (includePreview && job.status === 'succeeded') {
381
+ for (const im of job.images) {
382
+ try {
383
+ if (im.preview) content.push({ type: 'image', data: fs.readFileSync(im.preview).toString('base64'), mimeType: 'image/jpeg' });
384
+ } catch {}
385
+ }
386
+ }
387
+ return { content, isError: job.status === 'failed' };
388
+ }
389
+
390
+ async function bridgeStatus() {
391
+ let s;
392
+ try {
393
+ s = await bridge.call('status');
394
+ } catch (e) {
395
+ if (e.code === 'NOT_RUNNING') return textResult(`Bridge: NOT RUNNING. ${e.message}`);
396
+ throw e;
397
+ }
398
+ const lines = [
399
+ `Bridge: connected (Chrome extension v${s.extension ? s.extension.version : '?'}, host v${s.hostVersion}).`,
400
+ s.project
401
+ ? `ChatGPT project: ${s.project.name} — ${s.project.url}`
402
+ : 'ChatGPT project: NOT SELECTED — call list_projects, then select_project.',
403
+ `Images folder: ${s.outputDir}`,
404
+ s.running ? `Running: ${s.running.id} — ${s.running.status}: ${s.running.message || s.running.step}` : 'Running: nothing',
405
+ `Queued: ${s.queued.length ? s.queued.join(', ') : 'none'}`,
406
+ ];
407
+ if (s.recent.length) {
408
+ lines.push('Recent jobs:');
409
+ for (const j of s.recent) lines.push(`- ${j.id} ${j.status} (${j.createdAt}) "${j.prompt}"${j.conversationUrl ? ` ${j.conversationUrl}` : ''}`);
410
+ }
411
+ return textResult(lines.join('\n'));
412
+ }
413
+
414
+ async function listProjects() {
415
+ const r = await bridge.call('list_projects', {}, 130000);
416
+ if (!r.projects.length) return textResult('No projects were found in the ChatGPT sidebar.');
417
+ const lines = ['ChatGPT projects:'];
418
+ for (const p of r.projects) {
419
+ const selected = r.selected && (r.selected.id === p.id || r.selected.name === p.name);
420
+ lines.push(`- ${p.name}${p.url ? ` — ${p.url}` : ''}${selected ? ' (selected for images)' : ''}`);
421
+ }
422
+ if (!r.selected) lines.push('No project is selected yet; call select_project.');
423
+ return textResult(lines.join('\n'));
424
+ }
425
+
426
+ async function selectProject(args) {
427
+ if (!args.name && !args.url) return textResult('Give name or url.', true);
428
+ const r = await bridge.call('select_project', { name: args.name, url: args.url }, 130000);
429
+ return textResult(`Selected ChatGPT project "${r.project.name}" (${r.project.url}). New images will be created there and the choice is remembered.`);
430
+ }
431
+
432
+ async function listCollections() {
433
+ return textResult(describeCollections(await bridge.call('list_collections'), MAX_REFS));
434
+ }
435
+
436
+ process.on('uncaughtException', e => logErr('uncaught:', e.stack || e));
437
+ process.on('unhandledRejection', e => logErr('unhandled:', (e && e.stack) || e));
package/package.json CHANGED
@@ -1,6 +1,56 @@
1
1
  {
2
2
  "name": "backdraw",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "2.1.0",
4
+ "description": "Local MCP server, native messaging host and Chrome extension that let Claude generate images with your own signed-in ChatGPT account in a background tab.",
5
+ "license": "MIT",
6
+ "author": "devtalh",
7
+ "homepage": "https://github.com/devtalh/backdraw#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/devtalh/backdraw.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/devtalh/backdraw/issues"
14
+ },
15
+ "keywords": [
16
+ "mcp",
17
+ "mcp-server",
18
+ "model-context-protocol",
19
+ "claude",
20
+ "claude-code",
21
+ "chatgpt",
22
+ "image-generation",
23
+ "chrome-extension",
24
+ "native-messaging",
25
+ "windows"
26
+ ],
27
+ "bin": {
28
+ "backdraw": "bin/backdraw.js"
29
+ },
30
+ "files": [
31
+ "bin/",
32
+ "extension/",
33
+ "lib/",
34
+ "platform/",
35
+ "cli.js",
36
+ "doctor.js",
37
+ "host.js",
38
+ "identity.js",
39
+ "install.js",
40
+ "mcp-server.js",
41
+ "setup.js",
42
+ "shared.js",
43
+ "README.tr.md"
44
+ ],
45
+ "engines": {
46
+ "node": ">=20"
47
+ },
48
+ "scripts": {
49
+ "status": "node cli.js status",
50
+ "install-bridge": "node install.js",
51
+ "test": "node --test"
52
+ },
53
+ "os": [
54
+ "win32"
55
+ ]
56
+ }
@@ -0,0 +1,14 @@
1
+ 'use strict';
2
+ // OS-specific pieces behind one interface. Only Windows is implemented; on other platforms every
3
+ // function throws a clear "not supported yet" error when it is used.
4
+ const win32 = require('./win32');
5
+
6
+ const current = process.platform;
7
+
8
+ function unsupported() {
9
+ throw new Error(`This bridge only supports Windows so far; "${current}" is not supported yet.`);
10
+ }
11
+
12
+ module.exports = current === 'win32'
13
+ ? win32
14
+ : Object.fromEntries(Object.keys(win32).map(name => [name, unsupported]));