autodev-cli 1.4.149 → 1.4.151

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.
@@ -0,0 +1,57 @@
1
+ import { type DetectAppResult } from './binDetect';
2
+ export interface GimpInstallDeps {
3
+ platform: NodeJS.Platform;
4
+ env: NodeJS.ProcessEnv;
5
+ homedir: () => string;
6
+ fileExists: (p: string) => boolean;
7
+ listDir: (p: string) => string[];
8
+ mkdirp: (p: string) => void;
9
+ writeFile: (p: string, data: string) => void;
10
+ chmod: (p: string, mode: number) => void;
11
+ copyFile: (src: string, dst: string) => void;
12
+ /** Read an existing file (for idempotent-skip content comparison). '' if unreadable. */
13
+ readFile: (p: string) => string;
14
+ /**
15
+ * The auto-start plugin source. Defaults to the EMBEDDED string
16
+ * (GIMP_PLUGIN_SOURCE) so it works when esbuild-bundled into the VS Code
17
+ * extension where media/gimp/gimp_mcp_server.py is NOT on disk at runtime.
18
+ */
19
+ readPluginSource: () => string;
20
+ /** GIMP detection (to gate on major < 3). Defaults to detectApp('gimp'). */
21
+ detectGimp: () => DetectAppResult;
22
+ }
23
+ export interface GimpInstallResult {
24
+ installed: boolean;
25
+ /** Absolute path(s) of the written gimp_mcp_server.py file(s). */
26
+ paths: string[];
27
+ /** The GIMP minor-version dir chosen (e.g. "3.2"), or null when not installed. */
28
+ version: string | null;
29
+ warnings: string[];
30
+ }
31
+ export interface GimpInstallOpts {
32
+ /**
33
+ * Explicit versioned GIMP config dir to target (e.g. `~/.config/GIMP/3.2`),
34
+ * bypassing the auto-resolver. Same semantics as GIMP3_DIRECTORY.
35
+ */
36
+ targetDir?: string;
37
+ }
38
+ /** Absolute path to the vendored auto-start plugin shipped in the tarball. */
39
+ export declare function vendoredPluginSourcePath(): string;
40
+ /** Extract the GIMP MAJOR version from a launcher path (e.g. gimp-2.10 → 2). */
41
+ export declare function gimpMajorFromPath(p: string | null): number | null;
42
+ /**
43
+ * Install the auto-start GIMP MCP plugin. Resolves the target plug-ins dir(s)
44
+ * per-OS and writes the vendored plugin so GIMP opens the localhost:9876 socket
45
+ * on launch. Does NOT install when a clearly < 3 GIMP is the only one detected.
46
+ */
47
+ export declare function installGimpPlugin(opts?: GimpInstallOpts, deps?: Partial<GimpInstallDeps>): GimpInstallResult;
48
+ /**
49
+ * Sync-path variant: install the auto-start plugin ONLY when GIMP 3.x is actually
50
+ * DETECTED. Unlike installGimpPlugin (which will create a 3.0 config dir even with
51
+ * no GIMP present — appropriate for the explicit `mcp detect --enable gimp` /
52
+ * `gimp-install-plugin` flows), this refuses to touch the filesystem when GIMP is
53
+ * absent or is clearly < 3, so a routine ConfigManager.syncProjectMcpServers on a
54
+ * box without GIMP never spuriously creates a config dir. Returns a skip result
55
+ * (installed:false + warning) instead of throwing so callers can swallow it.
56
+ */
57
+ export declare function installGimpPluginIfDetected(opts?: GimpInstallOpts, deps?: Partial<GimpInstallDeps>): GimpInstallResult;
@@ -0,0 +1,273 @@
1
+ "use strict";
2
+ // ---------------------------------------------------------------------------
3
+ // gimpPluginInstall — drop the vendored AUTO-START GIMP MCP plugin into the
4
+ // correct GIMP 3.x user plug-ins folder so the socket bridge opens on GIMP
5
+ // launch (no manual copy, no menu click).
6
+ //
7
+ // GROUNDED RULES (see AutoDev research):
8
+ // - GIMP 3.x user config is MINOR-versioned: GIMP 3.2 → .../GIMP/3.2/ (NOT 3.0).
9
+ // Bases: linux $XDG_CONFIG_HOME/GIMP or ~/.config/GIMP; win32 %APPDATA%\GIMP;
10
+ // darwin ~/Library/Application Support/GIMP. Honor GIMP3_DIRECTORY (overrides
11
+ // the WHOLE versioned dir). Glob GIMP/3.* and pick the HIGHEST minor; if none
12
+ // exists, create 3.0 and warn (user should launch GIMP once).
13
+ // - GIMP 3.x layout: plug-ins/gimp_mcp_server/gimp_mcp_server.py — the .py MUST
14
+ // sit in a sub-folder whose name == the .py basename (a loose .py is ignored).
15
+ // On non-Windows the .py must be executable (0o755).
16
+ // - Windows Store (MSIX) GIMP reads the REAL %APPDATA%\GIMP\<ver>. As a safety
17
+ // net we ALSO write the virtualized path when a package dir exists:
18
+ // %LOCALAPPDATA%\Packages\GIMP.<...>_*\LocalCache\Roaming\GIMP\<ver>\plug-ins.
19
+ //
20
+ // Everything is dependency-injected (platform/env/homedir/fileExists/listDir/
21
+ // mkdirp/writeFile/chmod/copyFile/readPluginSource/detectGimp) so the resolver is
22
+ // unit-testable without a real GIMP or filesystem.
23
+ // ---------------------------------------------------------------------------
24
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
25
+ if (k2 === undefined) k2 = k;
26
+ var desc = Object.getOwnPropertyDescriptor(m, k);
27
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
28
+ desc = { enumerable: true, get: function() { return m[k]; } };
29
+ }
30
+ Object.defineProperty(o, k2, desc);
31
+ }) : (function(o, m, k, k2) {
32
+ if (k2 === undefined) k2 = k;
33
+ o[k2] = m[k];
34
+ }));
35
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
36
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
37
+ }) : function(o, v) {
38
+ o["default"] = v;
39
+ });
40
+ var __importStar = (this && this.__importStar) || (function () {
41
+ var ownKeys = function(o) {
42
+ ownKeys = Object.getOwnPropertyNames || function (o) {
43
+ var ar = [];
44
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
45
+ return ar;
46
+ };
47
+ return ownKeys(o);
48
+ };
49
+ return function (mod) {
50
+ if (mod && mod.__esModule) return mod;
51
+ var result = {};
52
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
53
+ __setModuleDefault(result, mod);
54
+ return result;
55
+ };
56
+ })();
57
+ Object.defineProperty(exports, "__esModule", { value: true });
58
+ exports.vendoredPluginSourcePath = vendoredPluginSourcePath;
59
+ exports.gimpMajorFromPath = gimpMajorFromPath;
60
+ exports.installGimpPlugin = installGimpPlugin;
61
+ exports.installGimpPluginIfDetected = installGimpPluginIfDetected;
62
+ const fs = __importStar(require("fs"));
63
+ const path = __importStar(require("path"));
64
+ const os = __importStar(require("os"));
65
+ const binDetect_1 = require("./binDetect");
66
+ const gimpPluginSource_1 = require("./gimpPluginSource");
67
+ /** Absolute path to the vendored auto-start plugin shipped in the tarball. */
68
+ function vendoredPluginSourcePath() {
69
+ // Compiled to out/core/gimpPluginInstall.js → package root is ../../ .
70
+ return path.join(__dirname, '..', '..', 'media', 'gimp', 'gimp_mcp_server.py');
71
+ }
72
+ function realDeps() {
73
+ return {
74
+ platform: process.platform,
75
+ env: process.env,
76
+ homedir: os.homedir,
77
+ fileExists: (p) => { try {
78
+ return fs.existsSync(p);
79
+ }
80
+ catch {
81
+ return false;
82
+ } },
83
+ listDir: (p) => { try {
84
+ return fs.readdirSync(p);
85
+ }
86
+ catch {
87
+ return [];
88
+ } },
89
+ mkdirp: (p) => { fs.mkdirSync(p, { recursive: true }); },
90
+ writeFile: (p, data) => { fs.writeFileSync(p, data); },
91
+ chmod: (p, mode) => { try {
92
+ fs.chmodSync(p, mode);
93
+ }
94
+ catch { /* non-POSIX fs */ } },
95
+ copyFile: (src, dst) => { fs.copyFileSync(src, dst); },
96
+ readFile: (p) => { try {
97
+ return fs.readFileSync(p, 'utf8');
98
+ }
99
+ catch {
100
+ return '';
101
+ } },
102
+ // Runtime uses the EMBEDDED source (bundle-safe). media/gimp/gimp_mcp_server.py
103
+ // stays the canonical source + GPL attribution, but is no longer read at runtime.
104
+ readPluginSource: () => gimpPluginSource_1.GIMP_PLUGIN_SOURCE,
105
+ detectGimp: () => (0, binDetect_1.detectApp)('gimp'),
106
+ };
107
+ }
108
+ /** GIMP user-config BASE dir (the folder that CONTAINS the 3.x version dirs). */
109
+ function gimpConfigBase(d) {
110
+ const home = d.homedir();
111
+ if (d.platform === 'win32') {
112
+ const appData = d.env.APPDATA || path.join(home, 'AppData', 'Roaming');
113
+ return path.join(appData, 'GIMP');
114
+ }
115
+ if (d.platform === 'darwin') {
116
+ return path.join(home, 'Library', 'Application Support', 'GIMP');
117
+ }
118
+ const xdg = d.env.XDG_CONFIG_HOME && d.env.XDG_CONFIG_HOME.trim();
119
+ const cfg = xdg ? xdg : path.join(home, '.config');
120
+ return path.join(cfg, 'GIMP');
121
+ }
122
+ /** Parse the numeric minor from a `3.<minor>` dir name; null if not `3.*`. */
123
+ function minorOf(name) {
124
+ const m = /^3\.(\d+)$/.exec(name);
125
+ return m ? parseInt(m[1], 10) : null;
126
+ }
127
+ /**
128
+ * Pick the HIGHEST-minor `3.*` version dir under base. Returns the chosen
129
+ * version string and whether it already existed. When none exist, defaults to
130
+ * "3.0" (created + warned by the caller).
131
+ */
132
+ function chooseVersion(base, d) {
133
+ const kids = d.listDir(base)
134
+ .map((n) => ({ name: n, minor: minorOf(n) }))
135
+ .filter((x) => x.minor !== null);
136
+ if (kids.length === 0) {
137
+ return { version: '3.0', existed: false };
138
+ }
139
+ kids.sort((a, b) => b.minor - a.minor);
140
+ return { version: kids[0].name, existed: true };
141
+ }
142
+ /**
143
+ * Windows Store virtualized fallback plug-ins dir(s) for a given version. Only
144
+ * returned when a `GIMP.*` package dir actually exists under %LOCALAPPDATA%\Packages.
145
+ */
146
+ function storeVirtualizedPluginDirs(version, d) {
147
+ if (d.platform !== 'win32') {
148
+ return [];
149
+ }
150
+ const home = d.homedir();
151
+ const localAppData = d.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
152
+ const packagesDir = path.join(localAppData, 'Packages');
153
+ const pkgs = d.listDir(packagesDir).filter((n) => /^GIMP\./i.test(n));
154
+ return pkgs.map((pkg) => path.join(packagesDir, pkg, 'LocalCache', 'Roaming', 'GIMP', version, 'plug-ins'));
155
+ }
156
+ /** Extract the GIMP MAJOR version from a launcher path (e.g. gimp-2.10 → 2). */
157
+ function gimpMajorFromPath(p) {
158
+ if (!p) {
159
+ return null;
160
+ }
161
+ const base = p.replace(/\\/g, '/').split('/').pop() || p;
162
+ const m = /gimp[-_]?(\d+)/i.exec(base);
163
+ return m ? parseInt(m[1], 10) : null;
164
+ }
165
+ /**
166
+ * Write the plugin into one plug-ins dir. IDEMPOTENT: if the target already
167
+ * holds byte-identical content, skip the write (and the .bak) — but still ensure
168
+ * the executable bit on non-Windows. A .bak backup is made ONLY when overwriting
169
+ * content that DIFFERS from the source.
170
+ */
171
+ function installInto(pluginsDir, source, d) {
172
+ const sub = path.join(pluginsDir, 'gimp_mcp_server');
173
+ const py = path.join(sub, 'gimp_mcp_server.py');
174
+ d.mkdirp(sub);
175
+ if (d.fileExists(py)) {
176
+ const existing = d.readFile(py);
177
+ if (existing === source) {
178
+ // Already up to date — no write, no backup. Still ensure it's executable.
179
+ if (d.platform !== 'win32') {
180
+ d.chmod(py, 0o755);
181
+ }
182
+ return py;
183
+ }
184
+ // Different content → back it up before overwriting.
185
+ d.copyFile(py, py + '.bak');
186
+ }
187
+ d.writeFile(py, source);
188
+ if (d.platform !== 'win32') {
189
+ d.chmod(py, 0o755);
190
+ }
191
+ return py;
192
+ }
193
+ /**
194
+ * Install the auto-start GIMP MCP plugin. Resolves the target plug-ins dir(s)
195
+ * per-OS and writes the vendored plugin so GIMP opens the localhost:9876 socket
196
+ * on launch. Does NOT install when a clearly < 3 GIMP is the only one detected.
197
+ */
198
+ function installGimpPlugin(opts, deps) {
199
+ const d = { ...realDeps(), ...deps };
200
+ const warnings = [];
201
+ // Gate: if a GIMP is detected and is clearly < 3, do not install.
202
+ const det = d.detectGimp();
203
+ const major = gimpMajorFromPath(det.path);
204
+ if (det.installed && major !== null && major < 3) {
205
+ warnings.push(`Detected GIMP ${major}.x at ${det.path}. GIMP MCP needs GIMP 3.x — ` +
206
+ 'the auto-start plugin uses the GIMP 3.0 Python API and was NOT installed. ' +
207
+ 'Install GIMP 3 and re-run.');
208
+ return { installed: false, paths: [], version: null, warnings };
209
+ }
210
+ const source = d.readPluginSource();
211
+ // Resolve the versioned config dir(s). GIMP3_DIRECTORY / opts.targetDir
212
+ // override the WHOLE versioned dir.
213
+ const override = (opts?.targetDir && opts.targetDir.trim()) || (d.env.GIMP3_DIRECTORY && d.env.GIMP3_DIRECTORY.trim());
214
+ const targets = [];
215
+ let version;
216
+ if (override) {
217
+ version = path.basename(override) || 'custom';
218
+ if (!d.fileExists(override)) {
219
+ warnings.push(`GIMP dir ${override} does not exist yet — creating it. Launch GIMP once so it initialises its config.`);
220
+ }
221
+ targets.push(path.join(override, 'plug-ins'));
222
+ }
223
+ else {
224
+ const base = gimpConfigBase(d);
225
+ const chosen = chooseVersion(base, d);
226
+ version = chosen.version;
227
+ if (!chosen.existed) {
228
+ warnings.push(`No GIMP 3.x config dir found under ${base} — creating ${version}. ` +
229
+ 'Launch GIMP 3 at least once (so it creates its real config dir), then re-run to be sure it lands in the right place.');
230
+ }
231
+ targets.push(path.join(base, version, 'plug-ins'));
232
+ // Windows Store virtualized safety net (only when a package dir exists).
233
+ for (const vdir of storeVirtualizedPluginDirs(version, d)) {
234
+ targets.push(vdir);
235
+ }
236
+ }
237
+ const paths = [];
238
+ for (const pluginsDir of targets) {
239
+ try {
240
+ paths.push(installInto(pluginsDir, source, d));
241
+ }
242
+ catch (e) {
243
+ warnings.push(`Failed to write plugin into ${pluginsDir}: ${e.message}`);
244
+ }
245
+ }
246
+ return { installed: paths.length > 0, paths, version, warnings };
247
+ }
248
+ /**
249
+ * Sync-path variant: install the auto-start plugin ONLY when GIMP 3.x is actually
250
+ * DETECTED. Unlike installGimpPlugin (which will create a 3.0 config dir even with
251
+ * no GIMP present — appropriate for the explicit `mcp detect --enable gimp` /
252
+ * `gimp-install-plugin` flows), this refuses to touch the filesystem when GIMP is
253
+ * absent or is clearly < 3, so a routine ConfigManager.syncProjectMcpServers on a
254
+ * box without GIMP never spuriously creates a config dir. Returns a skip result
255
+ * (installed:false + warning) instead of throwing so callers can swallow it.
256
+ */
257
+ function installGimpPluginIfDetected(opts, deps) {
258
+ const d = { ...realDeps(), ...deps };
259
+ const det = d.detectGimp();
260
+ const major = gimpMajorFromPath(det.path);
261
+ // Require a detected GIMP that is not clearly < 3 (major null = version not in
262
+ // the launcher path, e.g. a macOS app bundle — allowed).
263
+ if (!det.installed || (major !== null && major < 3)) {
264
+ return {
265
+ installed: false,
266
+ paths: [],
267
+ version: null,
268
+ warnings: ['GIMP 3.x not detected — auto-start plugin install skipped (sync path).'],
269
+ };
270
+ }
271
+ return installGimpPlugin(opts, deps);
272
+ }
273
+ //# sourceMappingURL=gimpPluginInstall.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gimpPluginInstall.js","sourceRoot":"","sources":["../../src/core/gimpPluginInstall.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ /** sha256 (first 12 hex) of the canonical media/gimp/gimp_mcp_server.py. */
2
+ export declare const GIMP_PLUGIN_SOURCE_HASH = "93b608412db2";
3
+ /** Full body of the auto-start GIMP 3.x MCP plugin (media/gimp/gimp_mcp_server.py). */
4
+ export declare const GIMP_PLUGIN_SOURCE = "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n#\n# gimp_mcp_server.py \u2014 AutoDev auto-start GIMP 3.x MCP bridge\n#\n# Derived from libreearth/gimp-mcp @49c46dd (GPL-3.0); modified by AutoDev to\n# auto-start as a GIMP 3.x PERSISTENT extension. The upstream plugin registered a\n# manual \"<Image>/Filters/Development/ \u2192 Start MCP Server\" ImageProcedure that the\n# user had to click; this variant registers a PERSISTENT extension so GIMP starts\n# the socket server automatically on launch \u2014 no menu click, no open image needed.\n#\n# The socket / command-marshalling logic (start_server, handle_client,\n# execute_command, GLib.idle_add onto GIMP's main thread) is kept from upstream.\n# ONLY the registration + run() model changed to PERSISTENT + defensive ordering.\n#\n# ---------------------------------------------------------------------------\n# GIMP 3.x plugin layout (REQUIRED): this file must live at\n# <gimp-config>/<ver>/plug-ins/gimp_mcp_server/gimp_mcp_server.py\n# i.e. inside a sub-folder whose name == this file's basename. A loose .py is\n# IGNORED by GIMP 3.x. On Linux/macOS it must ALSO be executable (chmod 0o755).\n# ---------------------------------------------------------------------------\n#\n# Copyright (C) 2023 libreearth (upstream, GPL-3.0)\n# Modifications Copyright (C) 2025 AutoDev\n#\n# This program is free software: you can redistribute it and/or modify it under\n# the terms of the GNU General Public License as published by the Free Software\n# Foundation, either version 3 of the License, or (at your option) any later\n# version.\n#\n# This program is distributed in the hope that it will be useful, but WITHOUT ANY\n# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A\n# PARTICULAR PURPOSE. See the GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License along with\n# this program. If not, see <https://www.gnu.org/licenses/>. A copy is vendored\n# next to this file as LICENSE.\n\nimport os\nimport sys\nimport json\nimport socket\nimport threading\nimport traceback\nimport datetime\n\nimport gi\ngi.require_version('Gimp', '3.0')\nfrom gi.repository import Gimp\nfrom gi.repository import GLib\n\n\n# ---------------------------------------------------------------------------\n# Logging \u2014 an auto-start extension has NO console, so every diagnostic goes to\n# a log file the user can read. We resolve it next to this plug-in first (always\n# writable if the plug-in loaded at all), falling back to the GIMP config dir.\n# Nothing here may raise: a logging failure must never stop GIMP from launching.\n# ---------------------------------------------------------------------------\ndef _log_path():\n try:\n here = os.path.dirname(os.path.abspath(__file__))\n return os.path.join(here, 'gimp-mcp-server.log')\n except Exception:\n try:\n return os.path.join(GLib.get_user_config_dir(), 'gimp-mcp-server.log')\n except Exception:\n return os.path.join(os.path.expanduser('~'), 'gimp-mcp-server.log')\n\n\n_LOG_FILE = _log_path()\n\n\ndef log(msg):\n \"\"\"Best-effort append to the log file AND stderr. Never raises.\"\"\"\n line = '[%s] %s' % (datetime.datetime.now().isoformat(timespec='seconds'), msg)\n try:\n with open(_LOG_FILE, 'a', encoding='utf-8') as fh:\n fh.write(line + '\\n')\n except Exception:\n pass\n try:\n sys.stderr.write(line + '\\n')\n except Exception:\n pass\n\n\nclass MCPPlugin(Gimp.PlugIn):\n # \u2500\u2500 Registration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n def do_query_procedures(self):\n return [\"plug-in-mcp-server-autostart\"]\n\n def do_create_procedure(self, name):\n # PERSISTENT + no args + no menu = GIMP auto-starts this extension at\n # launch. Adding ANY argument, a menu label, or a menu path would turn\n # it back into a user-invoked plug-in and disable auto-start.\n procedure = Gimp.Procedure.new(\n self, name, Gimp.PDBProcType.PERSISTENT, self.run, None,\n )\n try:\n procedure.set_documentation(\n \"AutoDev MCP server (auto-start)\",\n \"Auto-starts a localhost:9876 socket bridge so external MCP \"\n \"clients can drive GIMP. Derived from libreearth/gimp-mcp \"\n \"(GPL-3.0), modified by AutoDev to run as a PERSISTENT extension.\",\n name,\n )\n procedure.set_attribution(\"AutoDev\", \"AutoDev (GPL-3.0)\", \"2025\")\n except Exception as e:\n # Attribution/doc setters are cosmetic \u2014 never let a binding\n # mismatch here prevent the procedure from registering.\n log(\"do_create_procedure: non-fatal setter error: %s\" % e)\n return procedure\n\n # \u2500\u2500 Extension entry point \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n def run(self, procedure, config, run_data):\n \"\"\"PERSISTENT extension entry point.\n\n DEFENSIVE ORDER (the brick-risk part): call persistent_ready() FIRST.\n Its ABSENCE is what hangs GIMP at startup, so it must run before\n anything that could throw. Everything after it is wrapped so a bind\n failure logs + degrades instead of taking GIMP core down with it.\n \"\"\"\n # 1) Hand control back to GIMP core IMMEDIATELY so the UI keeps loading.\n try:\n procedure.persistent_ready()\n log(\"persistent_ready() OK \u2014 extension is live\")\n except Exception as e:\n # A binding mismatch on this exact call is the known lock-up cause;\n # log it (do NOT re-raise) so GIMP still finishes launching.\n log(\"persistent_ready() FAILED (non-fatal): %s\\n%s\"\n % (e, traceback.format_exc()))\n\n # 2) Start the socket server on a daemon thread. A bind failure (port in\n # use, sandbox denial) MUST NOT crash the process or block launch.\n try:\n t = threading.Thread(target=self.start_server, daemon=True)\n t.start()\n log(\"MCP server thread started (listening on localhost:9876)\")\n except Exception as e:\n log(\"failed to start MCP server thread (non-fatal): %s\\n%s\"\n % (e, traceback.format_exc()))\n\n # 3) Keep this extension process alive for the whole GIMP session so the\n # socket stays open and idle_add callbacks can marshal onto core.\n try:\n GLib.MainLoop().run()\n except Exception as e:\n log(\"main loop exited (non-fatal): %s\" % e)\n\n # 4) Standard PERSISTENT success return.\n return procedure.new_return_values(Gimp.PDBStatusType.SUCCESS, GLib.Error())\n\n # \u2500\u2500 Socket server (kept from upstream) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n def start_server(self):\n host = 'localhost'\n port = 9876\n sock = None\n try:\n sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n sock.bind((host, port))\n sock.listen(1)\n log(\"server listening on %s:%d\" % (host, port))\n except Exception as e:\n # Bind/listen failed (e.g. port already taken by Blender's MCP or a\n # second GIMP). Log + bail out of the thread; GIMP keeps running.\n log(\"bind/listen failed (non-fatal): %s\\n%s\" % (e, traceback.format_exc()))\n try:\n if sock is not None:\n sock.close()\n except Exception:\n pass\n return\n\n while True:\n try:\n client, addr = sock.accept()\n log(\"client connected: %s\" % (addr,))\n threading.Thread(\n target=self.handle_client, args=(client,), daemon=True,\n ).start()\n except Exception as e:\n log(\"accept error: %s\" % e)\n break\n try:\n sock.close()\n except Exception:\n pass\n\n def handle_client(self, client):\n buffer = b''\n while True:\n try:\n data = client.recv(1024)\n if not data:\n break\n buffer += data\n try:\n command = json.loads(buffer.decode('utf-8'))\n buffer = b''\n # Marshal onto GIMP's main thread \u2014 the PDB is not thread-safe.\n GLib.idle_add(self.execute_command, command, client)\n except json.JSONDecodeError:\n # Partial frame \u2014 keep buffering until it parses.\n continue\n except Exception as e:\n log(\"client error: %s\" % e)\n break\n try:\n client.close()\n except Exception:\n pass\n\n def execute_command(self, command, client):\n \"\"\"Runs on GIMP's main thread via idle_add. Guarded so a bad command or\n a wrong binding logs + returns an error frame rather than hanging.\"\"\"\n response = {}\n try:\n cmd_type = command.get(\"type\")\n params = command.get(\"params\", {})\n\n if cmd_type == \"call_api\":\n api_path = params[\"api_path\"]\n args = params.get(\"args\", [])\n kwargs = params.get(\"kwargs\", {})\n\n module = Gimp\n for part in api_path.split(\".\"):\n module = getattr(module, part)\n\n result = module(*args, **kwargs)\n\n if hasattr(result, \"ID\"):\n result = {\"id\": result.ID}\n elif isinstance(result, list):\n result = [self.serialize_gimp_object(item) for item in result]\n else:\n result = str(result)\n\n response = {\"status\": \"success\", \"result\": result}\n else:\n response = {\"status\": \"error\", \"message\": \"Unknown command: %s\" % cmd_type}\n except Exception as e:\n response = {\"status\": \"error\", \"message\": str(e),\n \"traceback\": traceback.format_exc()}\n\n try:\n client.sendall(json.dumps(response).encode('utf-8'))\n except Exception as e:\n log(\"failed to send response: %s\" % e)\n return False # one-shot idle callback\n\n def serialize_gimp_object(self, obj):\n try:\n if hasattr(obj, \"ID\"):\n return {\"id\": obj.ID, \"type\": obj.__class__.__name__}\n except Exception:\n pass\n return str(obj)\n\n\ntry:\n Gimp.main(MCPPlugin.__gtype__, sys.argv)\nexcept Exception as _e:\n # If GIMP's main dispatch itself fails, log it \u2014 never let the plug-in\n # process take the launch down with an unhandled traceback.\n log(\"Gimp.main failed: %s\\n%s\" % (_e, traceback.format_exc()))\n";
@@ -0,0 +1,284 @@
1
+ "use strict";
2
+ // ---------------------------------------------------------------------------
3
+ // gimpPluginSource.ts — GENERATED. Do not edit by hand.
4
+ //
5
+ // Source of truth: media/gimp/gimp_mcp_server.py (GPL-3.0; see media/gimp/LICENSE).
6
+ // Regenerate with: node scripts/gen-gimp-plugin-source.mjs (runs in `npm run build`).
7
+ //
8
+ // This embeds the auto-start GIMP MCP plugin body as a string so the installer
9
+ // works even when bundled (VS Code extension esbuild) with no media file on disk.
10
+ // ---------------------------------------------------------------------------
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.GIMP_PLUGIN_SOURCE = exports.GIMP_PLUGIN_SOURCE_HASH = void 0;
13
+ /** sha256 (first 12 hex) of the canonical media/gimp/gimp_mcp_server.py. */
14
+ exports.GIMP_PLUGIN_SOURCE_HASH = "93b608412db2";
15
+ /** Full body of the auto-start GIMP 3.x MCP plugin (media/gimp/gimp_mcp_server.py). */
16
+ exports.GIMP_PLUGIN_SOURCE = `#!/usr/bin/env python3
17
+ # -*- coding: utf-8 -*-
18
+ #
19
+ # gimp_mcp_server.py — AutoDev auto-start GIMP 3.x MCP bridge
20
+ #
21
+ # Derived from libreearth/gimp-mcp @49c46dd (GPL-3.0); modified by AutoDev to
22
+ # auto-start as a GIMP 3.x PERSISTENT extension. The upstream plugin registered a
23
+ # manual "<Image>/Filters/Development/ → Start MCP Server" ImageProcedure that the
24
+ # user had to click; this variant registers a PERSISTENT extension so GIMP starts
25
+ # the socket server automatically on launch — no menu click, no open image needed.
26
+ #
27
+ # The socket / command-marshalling logic (start_server, handle_client,
28
+ # execute_command, GLib.idle_add onto GIMP's main thread) is kept from upstream.
29
+ # ONLY the registration + run() model changed to PERSISTENT + defensive ordering.
30
+ #
31
+ # ---------------------------------------------------------------------------
32
+ # GIMP 3.x plugin layout (REQUIRED): this file must live at
33
+ # <gimp-config>/<ver>/plug-ins/gimp_mcp_server/gimp_mcp_server.py
34
+ # i.e. inside a sub-folder whose name == this file's basename. A loose .py is
35
+ # IGNORED by GIMP 3.x. On Linux/macOS it must ALSO be executable (chmod 0o755).
36
+ # ---------------------------------------------------------------------------
37
+ #
38
+ # Copyright (C) 2023 libreearth (upstream, GPL-3.0)
39
+ # Modifications Copyright (C) 2025 AutoDev
40
+ #
41
+ # This program is free software: you can redistribute it and/or modify it under
42
+ # the terms of the GNU General Public License as published by the Free Software
43
+ # Foundation, either version 3 of the License, or (at your option) any later
44
+ # version.
45
+ #
46
+ # This program is distributed in the hope that it will be useful, but WITHOUT ANY
47
+ # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
48
+ # PARTICULAR PURPOSE. See the GNU General Public License for more details.
49
+ #
50
+ # You should have received a copy of the GNU General Public License along with
51
+ # this program. If not, see <https://www.gnu.org/licenses/>. A copy is vendored
52
+ # next to this file as LICENSE.
53
+
54
+ import os
55
+ import sys
56
+ import json
57
+ import socket
58
+ import threading
59
+ import traceback
60
+ import datetime
61
+
62
+ import gi
63
+ gi.require_version('Gimp', '3.0')
64
+ from gi.repository import Gimp
65
+ from gi.repository import GLib
66
+
67
+
68
+ # ---------------------------------------------------------------------------
69
+ # Logging — an auto-start extension has NO console, so every diagnostic goes to
70
+ # a log file the user can read. We resolve it next to this plug-in first (always
71
+ # writable if the plug-in loaded at all), falling back to the GIMP config dir.
72
+ # Nothing here may raise: a logging failure must never stop GIMP from launching.
73
+ # ---------------------------------------------------------------------------
74
+ def _log_path():
75
+ try:
76
+ here = os.path.dirname(os.path.abspath(__file__))
77
+ return os.path.join(here, 'gimp-mcp-server.log')
78
+ except Exception:
79
+ try:
80
+ return os.path.join(GLib.get_user_config_dir(), 'gimp-mcp-server.log')
81
+ except Exception:
82
+ return os.path.join(os.path.expanduser('~'), 'gimp-mcp-server.log')
83
+
84
+
85
+ _LOG_FILE = _log_path()
86
+
87
+
88
+ def log(msg):
89
+ """Best-effort append to the log file AND stderr. Never raises."""
90
+ line = '[%s] %s' % (datetime.datetime.now().isoformat(timespec='seconds'), msg)
91
+ try:
92
+ with open(_LOG_FILE, 'a', encoding='utf-8') as fh:
93
+ fh.write(line + '\\n')
94
+ except Exception:
95
+ pass
96
+ try:
97
+ sys.stderr.write(line + '\\n')
98
+ except Exception:
99
+ pass
100
+
101
+
102
+ class MCPPlugin(Gimp.PlugIn):
103
+ # ── Registration ──────────────────────────────────────────────────────
104
+ def do_query_procedures(self):
105
+ return ["plug-in-mcp-server-autostart"]
106
+
107
+ def do_create_procedure(self, name):
108
+ # PERSISTENT + no args + no menu = GIMP auto-starts this extension at
109
+ # launch. Adding ANY argument, a menu label, or a menu path would turn
110
+ # it back into a user-invoked plug-in and disable auto-start.
111
+ procedure = Gimp.Procedure.new(
112
+ self, name, Gimp.PDBProcType.PERSISTENT, self.run, None,
113
+ )
114
+ try:
115
+ procedure.set_documentation(
116
+ "AutoDev MCP server (auto-start)",
117
+ "Auto-starts a localhost:9876 socket bridge so external MCP "
118
+ "clients can drive GIMP. Derived from libreearth/gimp-mcp "
119
+ "(GPL-3.0), modified by AutoDev to run as a PERSISTENT extension.",
120
+ name,
121
+ )
122
+ procedure.set_attribution("AutoDev", "AutoDev (GPL-3.0)", "2025")
123
+ except Exception as e:
124
+ # Attribution/doc setters are cosmetic — never let a binding
125
+ # mismatch here prevent the procedure from registering.
126
+ log("do_create_procedure: non-fatal setter error: %s" % e)
127
+ return procedure
128
+
129
+ # ── Extension entry point ─────────────────────────────────────────────
130
+ def run(self, procedure, config, run_data):
131
+ """PERSISTENT extension entry point.
132
+
133
+ DEFENSIVE ORDER (the brick-risk part): call persistent_ready() FIRST.
134
+ Its ABSENCE is what hangs GIMP at startup, so it must run before
135
+ anything that could throw. Everything after it is wrapped so a bind
136
+ failure logs + degrades instead of taking GIMP core down with it.
137
+ """
138
+ # 1) Hand control back to GIMP core IMMEDIATELY so the UI keeps loading.
139
+ try:
140
+ procedure.persistent_ready()
141
+ log("persistent_ready() OK — extension is live")
142
+ except Exception as e:
143
+ # A binding mismatch on this exact call is the known lock-up cause;
144
+ # log it (do NOT re-raise) so GIMP still finishes launching.
145
+ log("persistent_ready() FAILED (non-fatal): %s\\n%s"
146
+ % (e, traceback.format_exc()))
147
+
148
+ # 2) Start the socket server on a daemon thread. A bind failure (port in
149
+ # use, sandbox denial) MUST NOT crash the process or block launch.
150
+ try:
151
+ t = threading.Thread(target=self.start_server, daemon=True)
152
+ t.start()
153
+ log("MCP server thread started (listening on localhost:9876)")
154
+ except Exception as e:
155
+ log("failed to start MCP server thread (non-fatal): %s\\n%s"
156
+ % (e, traceback.format_exc()))
157
+
158
+ # 3) Keep this extension process alive for the whole GIMP session so the
159
+ # socket stays open and idle_add callbacks can marshal onto core.
160
+ try:
161
+ GLib.MainLoop().run()
162
+ except Exception as e:
163
+ log("main loop exited (non-fatal): %s" % e)
164
+
165
+ # 4) Standard PERSISTENT success return.
166
+ return procedure.new_return_values(Gimp.PDBStatusType.SUCCESS, GLib.Error())
167
+
168
+ # ── Socket server (kept from upstream) ────────────────────────────────
169
+ def start_server(self):
170
+ host = 'localhost'
171
+ port = 9876
172
+ sock = None
173
+ try:
174
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
175
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
176
+ sock.bind((host, port))
177
+ sock.listen(1)
178
+ log("server listening on %s:%d" % (host, port))
179
+ except Exception as e:
180
+ # Bind/listen failed (e.g. port already taken by Blender's MCP or a
181
+ # second GIMP). Log + bail out of the thread; GIMP keeps running.
182
+ log("bind/listen failed (non-fatal): %s\\n%s" % (e, traceback.format_exc()))
183
+ try:
184
+ if sock is not None:
185
+ sock.close()
186
+ except Exception:
187
+ pass
188
+ return
189
+
190
+ while True:
191
+ try:
192
+ client, addr = sock.accept()
193
+ log("client connected: %s" % (addr,))
194
+ threading.Thread(
195
+ target=self.handle_client, args=(client,), daemon=True,
196
+ ).start()
197
+ except Exception as e:
198
+ log("accept error: %s" % e)
199
+ break
200
+ try:
201
+ sock.close()
202
+ except Exception:
203
+ pass
204
+
205
+ def handle_client(self, client):
206
+ buffer = b''
207
+ while True:
208
+ try:
209
+ data = client.recv(1024)
210
+ if not data:
211
+ break
212
+ buffer += data
213
+ try:
214
+ command = json.loads(buffer.decode('utf-8'))
215
+ buffer = b''
216
+ # Marshal onto GIMP's main thread — the PDB is not thread-safe.
217
+ GLib.idle_add(self.execute_command, command, client)
218
+ except json.JSONDecodeError:
219
+ # Partial frame — keep buffering until it parses.
220
+ continue
221
+ except Exception as e:
222
+ log("client error: %s" % e)
223
+ break
224
+ try:
225
+ client.close()
226
+ except Exception:
227
+ pass
228
+
229
+ def execute_command(self, command, client):
230
+ """Runs on GIMP's main thread via idle_add. Guarded so a bad command or
231
+ a wrong binding logs + returns an error frame rather than hanging."""
232
+ response = {}
233
+ try:
234
+ cmd_type = command.get("type")
235
+ params = command.get("params", {})
236
+
237
+ if cmd_type == "call_api":
238
+ api_path = params["api_path"]
239
+ args = params.get("args", [])
240
+ kwargs = params.get("kwargs", {})
241
+
242
+ module = Gimp
243
+ for part in api_path.split("."):
244
+ module = getattr(module, part)
245
+
246
+ result = module(*args, **kwargs)
247
+
248
+ if hasattr(result, "ID"):
249
+ result = {"id": result.ID}
250
+ elif isinstance(result, list):
251
+ result = [self.serialize_gimp_object(item) for item in result]
252
+ else:
253
+ result = str(result)
254
+
255
+ response = {"status": "success", "result": result}
256
+ else:
257
+ response = {"status": "error", "message": "Unknown command: %s" % cmd_type}
258
+ except Exception as e:
259
+ response = {"status": "error", "message": str(e),
260
+ "traceback": traceback.format_exc()}
261
+
262
+ try:
263
+ client.sendall(json.dumps(response).encode('utf-8'))
264
+ except Exception as e:
265
+ log("failed to send response: %s" % e)
266
+ return False # one-shot idle callback
267
+
268
+ def serialize_gimp_object(self, obj):
269
+ try:
270
+ if hasattr(obj, "ID"):
271
+ return {"id": obj.ID, "type": obj.__class__.__name__}
272
+ except Exception:
273
+ pass
274
+ return str(obj)
275
+
276
+
277
+ try:
278
+ Gimp.main(MCPPlugin.__gtype__, sys.argv)
279
+ except Exception as _e:
280
+ # If GIMP's main dispatch itself fails, log it — never let the plug-in
281
+ # process take the launch down with an unhandled traceback.
282
+ log("Gimp.main failed: %s\\n%s" % (_e, traceback.format_exc()))
283
+ `;
284
+ //# sourceMappingURL=gimpPluginSource.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gimpPluginSource.js","sourceRoot":"","sources":["../../src/core/gimpPluginSource.ts"],"names":[],"mappings":";AAAA,8EAA8E;AAC9E,wDAAwD;AACxD,EAAE;AACF,oFAAoF;AACpF,wFAAwF;AACxF,EAAE;AACF,+EAA+E;AAC/E,kFAAkF;AAClF,8EAA8E;;;AAE9E,4EAA4E;AAC/D,QAAA,uBAAuB,GAAG,cAAc,CAAC;AAEtD,uFAAuF;AAC1E,QAAA,kBAAkB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2QjC,CAAC"}