@jurislm/coolify-plugin 4.0.26 → 4.0.28

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coolify-plugin",
3
- "version": "4.0.26",
3
+ "version": "4.0.28",
4
4
  "description": "Fallback Codex metadata for local stdio Coolify tools.",
5
5
  "author": {
6
6
  "name": "JurisLM",
package/.mcp.json CHANGED
@@ -2,8 +2,8 @@
2
2
  "mcpServers": {
3
3
  "coolify": {
4
4
  "type": "stdio",
5
- "command": "bunx",
6
- "args": ["-y", "@jurislm/coolify-plugin@latest"],
5
+ "command": "bun",
6
+ "args": ["x", "@jurislm/coolify-plugin@latest"],
7
7
  "env_vars": ["COOLIFY_CLOUD_BASE_URL", "COOLIFY_CLOUD_ACCESS_TOKEN", "CURSOR_COOLIFY_BASE_URL", "CURSOR_COOLIFY_ACCESS_TOKEN", "COOLIFY_BASE_URL", "COOLIFY_ACCESS_TOKEN"]
8
8
  }
9
9
  }
package/.mcp.json.example CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "mcpServers": {
3
3
  "coolify": {
4
- "command": "bunx",
5
- "args": ["-y", "@jurislm/coolify-plugin@latest"],
4
+ "command": "bun",
5
+ "args": ["x", "@jurislm/coolify-plugin@latest"],
6
6
  "env": {
7
7
  "COOLIFY_BASE_URL": "https://coolify.example",
8
8
  "COOLIFY_ACCESS_TOKEN": "<set-in-host-environment>"
package/README.md CHANGED
@@ -27,7 +27,7 @@ bun run check
27
27
  bun dist/index.js
28
28
  ```
29
29
 
30
- `mcp.json` and `.mcp.json` use the same published-package `bunx` stdio registration as the Woodpecker CI plugin. `.mcp.json.example` contains placeholder environment values only. NPM package release is allowed.
30
+ `mcp.json`, `.mcp.json` and `plugins/cursor/mcp.json` launch the published package with `bun x` (not the separate `bunx` executable, which some package-manager shims break). When a connection variable is missing, empty, or an unsubstituted `${...}` placeholder, the server fills it from `~/.config/jurislm/plugin.env` (`KEY=value` or `export KEY=value` lines; override the path with `JURISLM_PLUGIN_ENV`); host values always take precedence and values are never logged. `.mcp.json.example` contains placeholder environment values only. NPM package release is allowed.
31
31
 
32
32
  For Codex repository marketplace installation, use the repository root and leave the sparse path empty. The supported marketplace manifest is `.agents/plugins/marketplace.json`; do not enter `plugins/codex`.
33
33
  The native Codex registration forwards `COOLIFY_CLOUD_BASE_URL`, `COOLIFY_CLOUD_ACCESS_TOKEN`, `CURSOR_COOLIFY_BASE_URL`, `CURSOR_COOLIFY_ACCESS_TOKEN`, `COOLIFY_BASE_URL` and `COOLIFY_ACCESS_TOKEN` from its owning environment. Portable `mcp.json` contains no host-specific fields or credential defaults.
@@ -51,7 +51,7 @@ args = ["-f", "-c", 'source "$HOME/.codex/bin/coolify-mcp.zsh"']
51
51
  startup_timeout_sec = 30
52
52
  ```
53
53
 
54
- The launcher checks startup-file syntax before explicitly sourcing `${ZDOTDIR:-$HOME}/.zshenv`, suppresses stdout and preserves stderr. Source status 0 or 1 is accepted because a final optional guard can return 1; higher statuses stop startup. It retains only the six supported connection variables, `HOME`, `PATH`, `TMPDIR` and `LANG`, fixes PATH to `$HOME/.bun/bin:/usr/bin:/bin` and executes the absolute `$HOME/.bun/bin/bunx` path. The `--require-config` option reuses `loadConfig` to require the selected complete URL/token pair before starting stdio; trim, placeholder handling and pair precedence remain the same. Ordinary portable initialization and tool discovery can still start without credentials.
54
+ The launcher checks startup-file syntax before explicitly sourcing `${ZDOTDIR:-$HOME}/.zshenv`, suppresses stdout and preserves stderr. Source status 0 or 1 is accepted because a final optional guard can return 1; higher statuses stop startup. It retains only the six supported connection variables, `HOME`, `PATH`, `TMPDIR` and `LANG`, fixes PATH to `$HOME/.bun/bin:/usr/bin:/bin` and executes the absolute `$HOME/.bun/bin/bun x` path. The `--require-config` option reuses `loadConfig` to require the selected complete URL/token pair before starting stdio; trim, placeholder handling and pair precedence remain the same. Ordinary portable initialization and tool discovery can still start without credentials.
55
55
 
56
56
  Check `codex mcp get coolify`, restart Codex and open a fresh chat. Read `coolify_get_mcp_version` before connection checks and authenticated reads. Record the resolved package version, actual registration source, HTTP status and returned counts. Standalone stdio checks establish process behavior; fresh chat registration and provider reads require their own evidence.
57
57
 
package/dist/client.js CHANGED
@@ -1,6 +1,92 @@
1
+ import { isAlias, isScalar, parseDocument, stringify, visit } from "yaml";
1
2
  import { CoolifyApiError, redactErrorText } from "./errors.js";
2
3
  const sensitiveKey = /(^value$|real_?value|private_?key|api_?key|user_?key|webhook_?url|token|secret|password|authorization|cookie)/iu;
4
+ const composeKey = /^(docker_compose(_pr)?(_raw)?|configuration)$/u;
5
+ const composeSecretName = /(password|passwd|secret|token|private|credential|api_?key|access_?key|authorization|cookie)/iu;
6
+ const composeSecretSegment = /(^|[_.-])(key|pass|pwd|auth)($|[_.-])/iu;
7
+ const interpolation = /^\$\{?[A-Za-z_][A-Za-z0-9_]*\}?$/u;
8
+ const urlCredential = /([a-z][a-z0-9+.-]{0,31}:\/\/[^\s:/@]{0,256}):([^\s/?#]{1,256})@/giu;
9
+ const queryParameter = /([?&])([^=&#?\s"']{1,64})=([^&#\s"']+)/gu;
10
+ const querySecretSegments = new Set(["password", "passwd", "pass", "pwd", "secret", "token", "auth", "sig", "signature", "credential", "credentials", "apikey", "key"]);
11
+ const querySecretName = /(password|passwd|secret|token|api_?key|access_?key)/iu;
12
+ const flagValue = /^(true|false|yes|no|on|off|enabled|disabled|none)$/iu;
13
+ const assignment = /^([\w.-]+)=([\s\S]*)$/u;
14
+ const base64Text = /^[A-Za-z0-9+/]+={0,2}$/u;
15
+ function isComposeSecret(name, value) {
16
+ if (!composeSecretName.test(name) && !composeSecretSegment.test(name))
17
+ return false;
18
+ const text = typeof value === "string" ? value : typeof value === "number" ? String(value) : "";
19
+ return text.trim() !== "" && !interpolation.test(text) && !flagValue.test(text.trim());
20
+ }
21
+ function redactUrlCredentials(text) {
22
+ return text
23
+ .replace(urlCredential, (match, prefix, password) => interpolation.test(password) ? match : `${prefix}:[REDACTED]@`)
24
+ .replace(queryParameter, (match, separator, name, value) => value !== "[REDACTED]" && isQuerySecret(name) ? `${separator}${name}=[REDACTED]` : match);
25
+ }
26
+ function isQuerySecret(name) {
27
+ return querySecretName.test(name) || name.toLowerCase().split(/[_.-]/u).some((segment) => querySecretSegments.has(segment));
28
+ }
29
+ const composeAliasLimit = 100;
30
+ function redactComposeValue(value) {
31
+ if (Array.isArray(value))
32
+ return value.map(redactComposeValue);
33
+ if (value && typeof value === "object") {
34
+ return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, isComposeSecret(key, child) ? "[REDACTED]" : redactComposeValue(child)]));
35
+ }
36
+ if (typeof value !== "string")
37
+ return value;
38
+ const match = assignment.exec(value);
39
+ return match && isComposeSecret(match[1], match[2]) ? `${match[1]}=[REDACTED]` : redactUrlCredentials(value);
40
+ }
41
+ function redactComposeYaml(text) {
42
+ if (text.trim() === "" || text === "[REDACTED]")
43
+ return text;
44
+ const document = parseDocument(text, { schema: "failsafe", merge: true });
45
+ if (document.errors.length > 0)
46
+ return "[REDACTED]";
47
+ let parsed;
48
+ let rendered;
49
+ try {
50
+ const secrets = [];
51
+ visit(document, {
52
+ Pair(_key, pair) {
53
+ const key = isAlias(pair.key) ? pair.key.resolve(document) : pair.key;
54
+ const value = isAlias(pair.value) ? pair.value.resolve(document) : pair.value;
55
+ if (isScalar(key) && isScalar(value) && isComposeSecret(String(key.value), value.value))
56
+ secrets.push(value);
57
+ },
58
+ });
59
+ for (const node of secrets)
60
+ node.value = "[REDACTED]";
61
+ parsed = document.toJS({ maxAliasCount: composeAliasLimit });
62
+ rendered = stringify(redactComposeValue(parsed), { lineWidth: 0 });
63
+ }
64
+ catch {
65
+ return "[REDACTED]";
66
+ }
67
+ return text.endsWith("\n") ? rendered : rendered.replace(/\n$/u, "");
68
+ }
69
+ function decodeBase64Text(text) {
70
+ if (text.length % 4 !== 0 || !base64Text.test(text))
71
+ return undefined;
72
+ const bytes = Buffer.from(text, "base64");
73
+ if (bytes.toString("base64") !== text)
74
+ return undefined;
75
+ try {
76
+ const decoded = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
77
+ return /[\n:]/u.test(decoded) ? decoded : undefined;
78
+ }
79
+ catch {
80
+ return undefined;
81
+ }
82
+ }
83
+ function redactComposeText(text) {
84
+ const decoded = decodeBase64Text(text);
85
+ return decoded === undefined ? redactComposeYaml(text) : Buffer.from(redactComposeYaml(decoded), "utf8").toString("base64");
86
+ }
3
87
  export function redactSensitive(value) {
88
+ if (typeof value === "string")
89
+ return redactUrlCredentials(value);
4
90
  if (Array.isArray(value))
5
91
  return value.map(redactSensitive);
6
92
  if (!value || typeof value !== "object")
@@ -8,7 +94,7 @@ export function redactSensitive(value) {
8
94
  const binary = value.encoding === "base64" && typeof value.contentType === "string" && typeof value.value === "string";
9
95
  return Object.fromEntries(Object.entries(value).map(([key, child]) => [
10
96
  key,
11
- sensitiveKey.test(key) && !(binary && key === "value") ? "[REDACTED]" : redactSensitive(child),
97
+ sensitiveKey.test(key) && !(binary && key === "value") ? "[REDACTED]" : composeKey.test(key) && typeof child === "string" ? redactComposeText(child) : redactSensitive(child),
12
98
  ]));
13
99
  }
14
100
  function pathValue(value, name) {
package/dist/config.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export declare const CREDENTIAL_GROUPS: readonly [readonly ["COOLIFY_CLOUD_BASE_URL", "COOLIFY_CLOUD_ACCESS_TOKEN"], readonly ["CURSOR_COOLIFY_BASE_URL", "CURSOR_COOLIFY_ACCESS_TOKEN"], readonly ["COOLIFY_BASE_URL", "COOLIFY_ACCESS_TOKEN"]];
1
2
  export interface CoolifyConfig {
2
3
  baseUrl?: string;
3
4
  token?: string;
package/dist/config.js CHANGED
@@ -1,3 +1,8 @@
1
+ export const CREDENTIAL_GROUPS = [
2
+ ["COOLIFY_CLOUD_BASE_URL", "COOLIFY_CLOUD_ACCESS_TOKEN"],
3
+ ["CURSOR_COOLIFY_BASE_URL", "CURSOR_COOLIFY_ACCESS_TOKEN"],
4
+ ["COOLIFY_BASE_URL", "COOLIFY_ACCESS_TOKEN"],
5
+ ];
1
6
  export class ConfigError extends Error {
2
7
  constructor(message) {
3
8
  super(message);
package/dist/index.js CHANGED
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env bun
2
- import { ConfigError, loadConfig } from "./config.js";
2
+ import { CREDENTIAL_GROUPS, ConfigError, loadConfig } from "./config.js";
3
+ import { withPluginEnv } from "./plugin-env.js";
3
4
  import { createServer } from "./server.js";
4
5
  import { createStdioTransport } from "./transports/stdio.js";
5
6
  try {
6
- const config = loadConfig();
7
+ const config = loadConfig(withPluginEnv(process.env, CREDENTIAL_GROUPS));
7
8
  if (process.argv.includes("--require-config") && (!config.baseUrl || !config.token))
8
9
  throw new ConfigError("A complete URL/token pair is required for the selected Coolify configuration");
9
10
  await createServer(config).connect(createStdioTransport());
@@ -0,0 +1,6 @@
1
+ export type Environment = Record<string, string | undefined>;
2
+ export type CredentialGroups = readonly (readonly string[])[];
3
+ export declare const PLUGIN_ENV_PATH_VARIABLE = "JURISLM_PLUGIN_ENV";
4
+ export declare function parsePluginEnv(text: string): Record<string, string>;
5
+ export declare function pluginEnvPath(env: Environment): string;
6
+ export declare function withPluginEnv(env: Environment, groups: CredentialGroups, readText?: (path: string) => string, warn?: (message: string) => void): Environment;
@@ -0,0 +1,66 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join, resolve } from "node:path";
4
+ export const PLUGIN_ENV_PATH_VARIABLE = "JURISLM_PLUGIN_ENV";
5
+ const PLACEHOLDER = /^\$\{[^}]*\}$/u;
6
+ const ASSIGNMENT = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/u;
7
+ function isUnset(value) {
8
+ const trimmed = value?.trim();
9
+ return !trimmed || PLACEHOLDER.test(trimmed);
10
+ }
11
+ export function parsePluginEnv(text) {
12
+ const values = {};
13
+ for (const rawLine of text.split(/\r?\n/u)) {
14
+ const match = ASSIGNMENT.exec(rawLine.trim());
15
+ if (!match)
16
+ continue;
17
+ const [, name, rawValue] = match;
18
+ const quote = rawValue[0];
19
+ if (quote === '"' || quote === "'") {
20
+ const end = rawValue.indexOf(quote, 1);
21
+ if (end > 0)
22
+ values[name] = rawValue.slice(1, end);
23
+ }
24
+ else {
25
+ values[name] = rawValue.replace(/\s+#.*$/u, "").trim();
26
+ }
27
+ }
28
+ return values;
29
+ }
30
+ export function pluginEnvPath(env) {
31
+ const home = env.HOME?.trim() || homedir();
32
+ const override = env[PLUGIN_ENV_PATH_VARIABLE];
33
+ if (!override || isUnset(override))
34
+ return join(home, ".config", "jurislm", "plugin.env");
35
+ const path = override.trim();
36
+ return resolve(path === "~" || path.startsWith("~/") ? join(home, path.slice(1)) : path);
37
+ }
38
+ function readPluginEnv(path, readText, warn) {
39
+ try {
40
+ return parsePluginEnv(readText(path));
41
+ }
42
+ catch (error) {
43
+ const code = error.code;
44
+ if (code !== "ENOENT")
45
+ warn(`Could not read plugin env file ${path} (${code ?? "unknown error"})`);
46
+ return {};
47
+ }
48
+ }
49
+ export function withPluginEnv(env, groups, readText = (path) => readFileSync(path, "utf8"), warn = (message) => console.error(message)) {
50
+ if (groups.every((group) => group.every((name) => !isUnset(env[name]))))
51
+ return env;
52
+ const fileValues = groups.some((group) => group.every((name) => isUnset(env[name]))) ? readPluginEnv(pluginEnvPath(env), readText, warn) : {};
53
+ const resolved = { ...env };
54
+ for (const group of groups) {
55
+ const fillFromFile = group.every((name) => isUnset(env[name]));
56
+ for (const name of group) {
57
+ if (!isUnset(env[name]))
58
+ continue;
59
+ if (fillFromFile && !isUnset(fileValues[name]))
60
+ resolved[name] = fileValues[name];
61
+ else
62
+ delete resolved[name];
63
+ }
64
+ }
65
+ return resolved;
66
+ }
@@ -20,4 +20,4 @@ for name in ${(k)parameters}; do
20
20
  ;;
21
21
  esac
22
22
  done
23
- exec "$HOME/.bun/bin/bunx" -y @jurislm/coolify-plugin@latest --require-config
23
+ exec "$HOME/.bun/bin/bun" x @jurislm/coolify-plugin@latest --require-config
package/mcp.json CHANGED
@@ -3,8 +3,8 @@
3
3
  "mcpServers": {
4
4
  "coolify": {
5
5
  "type": "stdio",
6
- "command": "bunx",
7
- "args": ["-y", "@jurislm/coolify-plugin@latest"]
6
+ "command": "bun",
7
+ "args": ["x", "@jurislm/coolify-plugin@latest"]
8
8
  }
9
9
  }
10
10
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jurislm/coolify-plugin",
3
- "version": "4.0.26",
3
+ "version": "4.0.28",
4
4
  "description": "Portable local stdio MCP plugin for Coolify",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -47,6 +47,7 @@
47
47
  },
48
48
  "dependencies": {
49
49
  "@modelcontextprotocol/sdk": "1.30.0",
50
+ "yaml": "2.9.1",
50
51
  "zod": "4.6.5"
51
52
  },
52
53
  "devDependencies": {
@@ -57,8 +58,7 @@
57
58
  "json-schema-to-zod": "2.8.1",
58
59
  "openapi-typescript": "7.13.0",
59
60
  "typescript": "5.9.3",
60
- "typescript-eslint": "8.70.1",
61
- "yaml": "2.9.1"
61
+ "typescript-eslint": "8.70.1"
62
62
  },
63
63
  "packageManager": "bun@1.3.14"
64
64
  }
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "coolify-plugin",
4
- "version": "4.0.26",
4
+ "version": "4.0.28",
5
5
  "description": "Portable Coolify MCP plugin with local stdio tools generated from the official OpenAPI contract.",
6
6
  "author": {
7
7
  "name": "JurisLM",