@jurislm/coolify-plugin 3.7.1

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,4 @@
1
+ {
2
+ "name": "coolify-plugin",
3
+ "description": "No app surface: this plugin is local stdio MCP only."
4
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "coolify-plugin",
3
+ "version": "3.7.1",
4
+ "description": "Fallback Codex metadata for local stdio Coolify tools.",
5
+ "author": {
6
+ "name": "JurisLM"
7
+ },
8
+ "license": "MIT",
9
+ "skills": "./skills/",
10
+ "mcpServers": "./.mcp.json",
11
+ "interface": {
12
+ "displayName": "Coolify",
13
+ "shortDescription": "Local Coolify API tools",
14
+ "longDescription": "Generated local stdio tools for a configured Coolify instance.",
15
+ "developerName": "JurisLM",
16
+ "category": "Developer Tools",
17
+ "capabilities": [
18
+ "MCP",
19
+ "Local"
20
+ ],
21
+ "defaultPrompt": [
22
+ "List my Coolify resources."
23
+ ]
24
+ }
25
+ }
package/.mcp.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "mcpServers": {
3
+ "coolify": { "type": "stdio", "command": "bun", "args": ["dist/index.js"], "cwd": "./" }
4
+ }
5
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "mcpServers": {
3
+ "coolify": {
4
+ "type": "stdio",
5
+ "command": "bun",
6
+ "args": ["/absolute/path/to/coolify-plugin/dist/index.js"]
7
+ }
8
+ }
9
+ }
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 terry90918
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # @jurislm/coolify-plugin
2
+
3
+ Portable Coolify MCP plugin. It exposes focused `coolify_*` stdio tools generated from the committed official Coolify OpenAPI snapshot.
4
+
5
+ ## Configure
6
+
7
+ Only these environment variables are accepted:
8
+
9
+ ```sh
10
+ export COOLIFY_URL=https://coolify.example
11
+ export COOLIFY_TOKEN=your-api-token
12
+ ```
13
+
14
+ Build and run locally:
15
+
16
+ ```sh
17
+ bun install --frozen-lockfile
18
+ bun run check
19
+ bun dist/index.js
20
+ ```
21
+
22
+ `mcp.json` is a portable stdio registration; `.mcp.json.example` is the secret-free absolute-path example. NPM package release is allowed. Remote MCP transport, OAuth, vault, hosting, and OpenAI Plugin Directory submission are out of scope.
23
+
24
+ ## OpenAPI contract
25
+
26
+ `openapi/coolify-openapi.json` is fetched from the official Coolify repository. `api/manifest.json` records its source, fetch time, persisted-snapshot SHA-256, document/info versions, path count, and operation count. `bun run api:check` verifies the snapshot offline before checking generated parity.
27
+
28
+ ```sh
29
+ bun run api:fetch
30
+ bun run api:generate
31
+ bun run api:check
32
+ ```
33
+
34
+ ## Checks
35
+
36
+ ```sh
37
+ bun run api:check
38
+ bun run typecheck
39
+ bun test
40
+ bun run build
41
+ bun run package:check
42
+ ```
43
+
44
+ ## License
45
+
46
+ MIT
@@ -0,0 +1,9 @@
1
+ {
2
+ "sourceUrl": "https://raw.githubusercontent.com/coollabsio/coolify/main/openapi.json",
3
+ "fetchedAt": "2026-09-16T10:31:42.036Z",
4
+ "sha256": "4dbb392aac5e0a46186c4e6e5237f86d9534bb99bf6b8b82351a513598af3900",
5
+ "openapiVersion": "3.1.0",
6
+ "infoVersion": "0.1",
7
+ "pathCount": 192,
8
+ "operationCount": 275
9
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { CoolifyClient } from "./client.js";
3
+ export declare function registerV36Capabilities(server: McpServer, client: CoolifyClient): void;
@@ -0,0 +1,201 @@
1
+ import { z } from "zod";
2
+ import { redactSensitive } from "./client.js";
3
+ import { operations } from "./generated/operations.js";
4
+ import { pluginVersion } from "./version.js";
5
+ const outputSchema = z.object({ data: z.unknown(), status: z.number(), request: z.object({ method: z.string(), path: z.string() }) });
6
+ const databaseTypes = ["dragonfly", "keydb", "clickhouse"];
7
+ const truncationPrefix = "...[truncated]...\n";
8
+ function rows(value) {
9
+ if (Array.isArray(value))
10
+ return value.filter((item) => !!item && typeof item === "object");
11
+ if (!value || typeof value !== "object")
12
+ return [];
13
+ for (const key of ["data", "items", "results", "deployments", "applications", "databases", "resources", "services"]) {
14
+ const nested = value[key];
15
+ if (Array.isArray(nested))
16
+ return rows(nested);
17
+ }
18
+ return [];
19
+ }
20
+ function status(value) { return typeof value.status === "string" ? value.status : ""; }
21
+ function unhealthy(value) { return /exited|unhealthy|error|stopped/iu.test(status(value)); }
22
+ function errorMessage(value) { return value instanceof Error ? value.message : String(value); }
23
+ function truncateLogs(logs, lineLimit = 200, charLimit = 50_000) {
24
+ if (logs.split("\n").length <= lineLimit && logs.length <= charLimit)
25
+ return logs;
26
+ let result = logs.split("\n").slice(-lineLimit).join("\n");
27
+ if (result.length > charLimit)
28
+ result = truncationPrefix + result.slice(-(charLimit - truncationPrefix.length));
29
+ else
30
+ result = truncationPrefix + result;
31
+ return result;
32
+ }
33
+ function environmentSummary(value) {
34
+ return rows(value).map((item) => ({ key: item.key, is_build_time: item.is_build_time ?? item.is_buildtime ?? false }));
35
+ }
36
+ export function registerV36Capabilities(server, client) {
37
+ const call = async (name, input = {}) => {
38
+ const operation = operations.find((item) => item.name === name);
39
+ if (!operation)
40
+ throw new Error(`Missing generated operation: ${name}`);
41
+ return (await client.request(operation, operation.inputSchema.parse(input))).data;
42
+ };
43
+ const result = (name, data) => {
44
+ const structuredContent = redactSensitive({ data, status: 200, request: { method: "COMPOSITE", path: `/capabilities/${name}` } });
45
+ return { structuredContent, content: [{ type: "text", text: JSON.stringify(structuredContent) }] };
46
+ };
47
+ const register = (name, description, inputSchema, annotations, handler) => {
48
+ server.registerTool(name, { title: name, description, inputSchema, outputSchema, annotations }, async (input) => result(name, await handler(input)));
49
+ };
50
+ const settledRows = async (names) => {
51
+ const settled = await Promise.allSettled(names.map((name) => call(`coolify_list_${name}`)));
52
+ const values = [];
53
+ const errors = [];
54
+ settled.forEach((entry, index) => {
55
+ if (entry.status === "fulfilled")
56
+ values[index] = rows(entry.value);
57
+ else {
58
+ values[index] = [];
59
+ errors.push(`${names[index]}: ${errorMessage(entry.reason)}`);
60
+ }
61
+ });
62
+ return { values, errors };
63
+ };
64
+ const overview = async () => {
65
+ const names = ["servers", "projects", "applications", "databases", "services"];
66
+ const { values, errors } = await settledRows(names);
67
+ const data = Object.fromEntries(names.map((name, index) => [name, values[index]]));
68
+ return { summary: Object.fromEntries(names.map((name, index) => [name, values[index].length])), ...data, ...(errors.length ? { errors } : {}) };
69
+ };
70
+ const batch = async (resources, action) => {
71
+ const settled = await Promise.allSettled(resources.map(action));
72
+ const succeeded = [];
73
+ const failed = [];
74
+ settled.forEach((entry, index) => {
75
+ const resource = resources[index];
76
+ const item = { uuid: resource.uuid, name: resource.name ?? resource.uuid };
77
+ if (entry.status === "fulfilled")
78
+ succeeded.push(item);
79
+ else
80
+ failed.push({ ...item, error: errorMessage(entry.reason) });
81
+ });
82
+ return { summary: { total: resources.length, succeeded: succeeded.length, failed: failed.length }, succeeded, failed };
83
+ };
84
+ const application = async (query) => {
85
+ const apps = rows(await call("coolify_list_applications"));
86
+ return apps.find((app) => [app.uuid, app.name, app.fqdn].some((value) => typeof value === "string" && value.toLowerCase().includes(query.toLowerCase())));
87
+ };
88
+ const serverByQuery = async (query) => {
89
+ const servers = rows(await call("coolify_list_servers"));
90
+ return servers.find((item) => [item.uuid, item.name, item.ip].some((value) => typeof value === "string" && value.toLowerCase().includes(query.toLowerCase())));
91
+ };
92
+ register("coolify_get_mcp_version", "Get the local Coolify plugin version.", z.object({}), { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, async () => ({ name: "@jurislm/coolify-plugin", version: pluginVersion }));
93
+ register("coolify_get_infrastructure_overview", "Summarize Coolify infrastructure.", z.object({}), { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, overview);
94
+ register("coolify_diagnose_application", "Diagnose an application by UUID, name, or domain.", z.object({ query: z.string() }), { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, async ({ query }) => {
95
+ let app;
96
+ try {
97
+ app = await application(String(query));
98
+ }
99
+ catch (error) {
100
+ return { application: null, health: { status: "unknown", issues: [] }, logs: null, environment_variables: { count: 0, variables: [] }, recent_deployments: [], errors: [errorMessage(error)] };
101
+ }
102
+ if (!app?.uuid)
103
+ return { application: null, health: { status: "unknown", issues: [] }, logs: null, environment_variables: { count: 0, variables: [] }, recent_deployments: [], errors: [`No application found matching ${query}`] };
104
+ const names = ["application", "logs", "environment_variables", "deployments"];
105
+ const settled = await Promise.allSettled([
106
+ call("coolify_get_application_by_uuid", { uuid: app.uuid }), call("coolify_get_application_logs_by_uuid", { uuid: app.uuid, lines: 50 }), call("coolify_list_envs_by_application_uuid", { uuid: app.uuid }), call("coolify_list_deployments_by_app_uuid", { uuid: app.uuid }),
107
+ ]);
108
+ const errors = [];
109
+ const value = (index) => { const entry = settled[index]; if (entry.status === "fulfilled")
110
+ return entry.value; errors.push(`${names[index]}: ${errorMessage(entry.reason)}`); return null; };
111
+ const details = value(0);
112
+ const rawLogs = value(1);
113
+ const envVars = environmentSummary(value(2));
114
+ const deployments = rows(value(3));
115
+ const appInfo = details ?? app;
116
+ const appStatus = status(appInfo);
117
+ const issues = [];
118
+ let health = "unknown";
119
+ if (/exited|unhealthy|error/iu.test(appStatus)) {
120
+ health = "unhealthy";
121
+ issues.push(`Status: ${appStatus}`);
122
+ }
123
+ else if (/running/iu.test(appStatus))
124
+ health = "healthy";
125
+ const failed = deployments.slice(0, 5).filter((item) => item.status === "failed").length;
126
+ if (failed) {
127
+ health = "unhealthy";
128
+ issues.push(`${failed} failed deployment(s) in last 5`);
129
+ }
130
+ const logs = typeof rawLogs === "string" ? rawLogs : rawLogs && typeof rawLogs === "object" && typeof rawLogs.logs === "string" ? rawLogs.logs : null;
131
+ return { application: { uuid: appInfo.uuid, name: appInfo.name, status: appStatus || "unknown", fqdn: appInfo.fqdn ?? null, git_repository: appInfo.git_repository ?? null, git_branch: appInfo.git_branch ?? null }, health: { status: health, issues }, logs: logs === null ? null : truncateLogs(logs), environment_variables: { count: envVars.length, variables: envVars }, recent_deployments: deployments.slice(0, 5).map((item) => ({ uuid: item.uuid, status: item.status, created_at: item.created_at })), ...(errors.length ? { errors } : {}) };
132
+ });
133
+ register("coolify_diagnose_server", "Diagnose a server by UUID, name, or IP.", z.object({ query: z.string() }), { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, async ({ query }) => {
134
+ let found;
135
+ try {
136
+ found = await serverByQuery(String(query));
137
+ }
138
+ catch (error) {
139
+ return { server: null, health: { status: "unknown", issues: [] }, resources: [], domains: [], validation: null, errors: [errorMessage(error)] };
140
+ }
141
+ if (!found?.uuid)
142
+ return { server: null, health: { status: "unknown", issues: [] }, resources: [], domains: [], validation: null, errors: [`No server found matching ${query}`] };
143
+ const names = ["server", "resources", "domains", "validation"];
144
+ const settled = await Promise.allSettled([
145
+ call("coolify_get_server_by_uuid", { uuid: found.uuid }), call("coolify_get_resources_by_server_uuid", { uuid: found.uuid }), call("coolify_get_domains_by_server_uuid", { uuid: found.uuid }), call("coolify_validate_server_by_uuid", { uuid: found.uuid }),
146
+ ]);
147
+ const errors = [];
148
+ const value = (index) => { const entry = settled[index]; if (entry.status === "fulfilled")
149
+ return entry.value; errors.push(`${names[index]}: ${errorMessage(entry.reason)}`); return null; };
150
+ const details = value(0) ?? found;
151
+ const resources = rows(value(1));
152
+ const issues = [];
153
+ if (details.is_reachable === false)
154
+ issues.push("Server is not reachable");
155
+ if (details.is_usable === false)
156
+ issues.push("Server is not usable");
157
+ const unhealthyResources = resources.filter(unhealthy).length;
158
+ if (unhealthyResources)
159
+ issues.push(`${unhealthyResources} unhealthy resource(s)`);
160
+ return { server: { uuid: details.uuid, name: details.name, ip: details.ip, status: details.status ?? null, is_reachable: details.is_reachable ?? null }, health: { status: issues.length ? "unhealthy" : details.is_reachable === true ? "healthy" : "unknown", issues }, resources: resources.map((item) => ({ uuid: item.uuid, name: item.name, type: item.type, status: item.status })), domains: rows(value(2)), validation: value(3), ...(errors.length ? { errors } : {}) };
161
+ });
162
+ register("coolify_find_issues", "Find unhealthy Coolify infrastructure.", z.object({}), { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, async () => {
163
+ const names = ["servers", "applications", "databases", "services"];
164
+ const { values, errors } = await settledRows(names);
165
+ const issues = [];
166
+ values[0].forEach((item) => { if (item.is_reachable === false || item.is_usable === false)
167
+ issues.push({ type: "server", uuid: item.uuid, name: item.name, issue: item.is_reachable === false ? "Server is not reachable" : "Server is not usable", status: status(item) || "unreachable" }); });
168
+ values.slice(1).forEach((items, index) => items.filter(unhealthy).forEach((item) => issues.push({ type: names[index + 1].slice(0, -1), uuid: item.uuid, name: item.name, issue: `${names[index + 1].slice(0, -1)} status: ${status(item)}`, status: status(item) })));
169
+ return { summary: { total_issues: issues.length, unhealthy_applications: issues.filter((item) => item.type === "application").length, unhealthy_databases: issues.filter((item) => item.type === "database").length, unhealthy_services: issues.filter((item) => item.type === "service").length, unreachable_servers: issues.filter((item) => item.type === "server").length }, issues, ...(errors.length ? { errors } : {}) };
170
+ });
171
+ register("coolify_restart_project_applications", "Restart every application in a project.", z.object({ project_uuid: z.string() }), { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, async ({ project_uuid }) => batch(rows(await call("coolify_list_applications")).filter((app) => app.project_uuid === project_uuid), (app) => call("coolify_restart_application_by_uuid", { uuid: app.uuid })));
172
+ register("coolify_bulk_update_application_env", "Update an environment variable across applications.", z.object({ app_uuids: z.array(z.string()), key: z.string(), value: z.string() }).strict(), { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, async ({ app_uuids, key, value }) => {
173
+ if (!app_uuids.length)
174
+ return batch([], async () => undefined);
175
+ const names = rows(await call("coolify_list_applications"));
176
+ const labels = new Map(names.map((item) => [item.uuid, item.name ?? item.uuid]));
177
+ return batch(app_uuids.map((uuid) => ({ uuid, name: labels.get(uuid) ?? uuid })), (app) => call("coolify_update_env_by_application_uuid", { uuid: app.uuid, body: { key, value } }));
178
+ });
179
+ register("coolify_stop_all_applications", "Emergency stop every running application.", z.object({ confirm_stop_all_applications: z.literal(true) }), { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false }, async () => batch(rows(await call("coolify_list_applications")).filter((app) => /running|healthy/iu.test(status(app))), (app) => call("coolify_stop_application_by_uuid", { uuid: app.uuid })));
180
+ register("coolify_redeploy_project_applications", "Redeploy every application in a project.", z.object({ project_uuid: z.string(), force: z.boolean().optional() }), { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, async ({ project_uuid, force }) => batch(rows(await call("coolify_list_applications")).filter((app) => app.project_uuid === project_uuid), (app) => call("coolify_deploy_by_tag_or_uuid", { uuid: app.uuid, force: force ?? true })));
181
+ register("coolify_get_environment", "Get an environment and its relevant database types.", z.object({ project_uuid: z.string(), environment_name_or_uuid: z.string() }), { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, async ({ project_uuid, environment_name_or_uuid }) => {
182
+ const settled = await Promise.allSettled([call("coolify_get_environment_by_name_or_uuid", { uuid: project_uuid, environment_name_or_uuid }), call("coolify_list_databases")]);
183
+ const errors = [];
184
+ const environment = settled[0].status === "fulfilled" ? settled[0].value : (errors.push(`environment: ${errorMessage(settled[0].reason)}`), null);
185
+ const databases = settled[1].status === "fulfilled" ? rows(settled[1].value) : (errors.push(`databases: ${errorMessage(settled[1].reason)}`), []);
186
+ if (!environment)
187
+ return { environment: null, missing_database_types: [...databaseTypes], ...(errors.length ? { errors } : {}) };
188
+ const matching = databases.filter((database) => (environment.id !== undefined && database.environment_id === environment.id) || (environment.uuid !== undefined && database.environment_uuid === environment.uuid) || (environment.name !== undefined && database.environment_name === environment.name));
189
+ const byType = new Map();
190
+ matching.forEach((database) => { const type = String(database.database_type ?? database.type ?? "").toLowerCase(); for (const expected of databaseTypes)
191
+ if (type.includes(expected))
192
+ byType.set(expected, [...(byType.get(expected) ?? []), database]); });
193
+ const missing = databaseTypes.filter((type) => !byType.has(type));
194
+ return { ...environment, ...Object.fromEntries(databaseTypes.filter((type) => byType.has(type)).map((type) => [`${type}s`, byType.get(type)])), missing_database_types: missing, ...(errors.length ? { errors } : {}) };
195
+ });
196
+ register("coolify_docker_network_alias", "Generate Docker network alias remediation commands.", z.object({ server_uuid: z.string().regex(/^[a-zA-Z0-9-]{1,64}$/u), db_uuid: z.string().regex(/^[a-zA-Z0-9-]{1,64}$/u), name: z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,62}$/u), network: z.string().optional() }), { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, async ({ server_uuid, db_uuid, name, network = "coolify" }) => {
197
+ const server = await call("coolify_get_server_by_uuid", { uuid: server_uuid }).catch(() => null);
198
+ const quote = (value) => `'${value.replace(/'/gu, "'\\''")}'`;
199
+ return { bug: "Coolify database containers may require a friendly Docker network alias.", warning: "The alias is lost after a rebuild or redeploy.", ...(server ? { server: { ip: server.ip, user: server.user, port: server.port } } : { server_lookup_failed: "Could not retrieve server details" }), commands: { ssh_connect: server ? `ssh -p ${server.port} ${server.user}@${server.ip}` : "ssh -p <port> <user>@<server-ip>", add_alias: [`docker network disconnect ${quote(String(network))} ${quote(String(db_uuid))}`, `docker network connect ${quote(String(network))} ${quote(String(db_uuid))} --alias ${quote(String(name))} --alias ${quote(String(db_uuid))}`], verify: `docker exec <any-app-container> getent hosts ${quote(String(name))}` }, next_actions: [{ tool: "coolify_get_database_by_uuid", args: { uuid: db_uuid }, hint: "Check database status" }] };
200
+ });
201
+ }
@@ -0,0 +1,26 @@
1
+ import type { GeneratedOperation } from "./generated/operations.js";
2
+ import type { CoolifyConfig } from "./config.js";
3
+ import { CoolifyApiError } from "./errors.js";
4
+ export interface ToolEnvelope<T> {
5
+ data: T;
6
+ status: number;
7
+ request: {
8
+ method: string;
9
+ path: string;
10
+ };
11
+ }
12
+ export interface BinaryEnvelope {
13
+ encoding: "base64";
14
+ contentType: string;
15
+ value: string;
16
+ }
17
+ export type FetchLike = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
18
+ type Operation = Pick<GeneratedOperation, "method" | "path" | "parameters">;
19
+ export declare function redactSensitive<T>(value: T): T;
20
+ export declare class CoolifyClient {
21
+ private readonly config;
22
+ private readonly fetchImpl;
23
+ constructor(config: CoolifyConfig, fetchImpl?: FetchLike);
24
+ request<T>(operation: Operation, input: Record<string, unknown>): Promise<ToolEnvelope<T | null | string | BinaryEnvelope>>;
25
+ }
26
+ export { CoolifyApiError };
package/dist/client.js ADDED
@@ -0,0 +1,95 @@
1
+ import { CoolifyApiError } from "./errors.js";
2
+ const sensitiveKey = /(^value$|real_?value|private_?key|token|secret|password|authorization|cookie)/iu;
3
+ export function redactSensitive(value) {
4
+ if (Array.isArray(value))
5
+ return value.map(redactSensitive);
6
+ if (!value || typeof value !== "object")
7
+ return value;
8
+ return Object.fromEntries(Object.entries(value).map(([key, child]) => [
9
+ key,
10
+ sensitiveKey.test(key) ? "[REDACTED]" : redactSensitive(child),
11
+ ]));
12
+ }
13
+ function pathValue(value, name) {
14
+ if (value === undefined || value === null)
15
+ throw new Error(`Missing required path parameter: ${name}`);
16
+ return encodeURIComponent(String(value));
17
+ }
18
+ function appendQuery(url, name, value) {
19
+ if (value === undefined || value === null)
20
+ return;
21
+ if (Array.isArray(value))
22
+ for (const item of value)
23
+ url.searchParams.append(name, String(item));
24
+ else
25
+ url.searchParams.set(name, typeof value === "object" ? JSON.stringify(value) : String(value));
26
+ }
27
+ function base64(bytes) {
28
+ let text = "";
29
+ for (const byte of new Uint8Array(bytes))
30
+ text += String.fromCharCode(byte);
31
+ return btoa(text);
32
+ }
33
+ function normalizeKnownCollection(path, value) {
34
+ const collectionPaths = new Set(["/databases", "/resources", "/deployments", "/deployments/applications/{uuid}"]);
35
+ if (!collectionPaths.has(path))
36
+ return value;
37
+ if (Array.isArray(value))
38
+ return value;
39
+ if (!value || typeof value !== "object")
40
+ throw new Error(`Coolify collection response for ${path} must be an array or named array wrapper`);
41
+ for (const key of ["data", "items", "results", "databases", "resources", "deployments"]) {
42
+ const nested = value[key];
43
+ if (Array.isArray(nested))
44
+ return nested;
45
+ }
46
+ throw new Error(`Coolify collection response for ${path} must be an array or named array wrapper`);
47
+ }
48
+ export class CoolifyClient {
49
+ config;
50
+ fetchImpl;
51
+ constructor(config, fetchImpl = fetch) {
52
+ this.config = config;
53
+ this.fetchImpl = fetchImpl;
54
+ }
55
+ async request(operation, input) {
56
+ let path = operation.path;
57
+ for (const parameter of operation.parameters)
58
+ if (parameter.location === "path") {
59
+ path = path.replace(`{${parameter.name}}`, pathValue(input[parameter.name], parameter.name));
60
+ }
61
+ if (path.includes("{"))
62
+ throw new Error(`Unresolved path parameter in ${operation.path}`);
63
+ const url = new URL(this.config.baseUrl + path);
64
+ for (const parameter of operation.parameters)
65
+ if (parameter.location === "query")
66
+ appendQuery(url, parameter.name, input[parameter.name]);
67
+ const headers = new Headers({ accept: "application/json, text/plain, */*", authorization: `Bearer ${this.config.token}` });
68
+ const init = { method: operation.method, headers, signal: AbortSignal.timeout(this.config.timeoutMs) };
69
+ if (input.body !== undefined && !["GET", "HEAD"].includes(operation.method)) {
70
+ headers.set("content-type", "application/json");
71
+ init.body = JSON.stringify(input.body);
72
+ }
73
+ let response;
74
+ try {
75
+ response = await this.fetchImpl(url, init);
76
+ }
77
+ catch (error) {
78
+ throw new CoolifyApiError(0, operation.method, path, `Coolify request failed for ${operation.method} ${path}: ${error instanceof Error ? error.message.replaceAll(this.config.token, "[REDACTED]") : "request error"}`);
79
+ }
80
+ if (!response.ok)
81
+ throw new CoolifyApiError(response.status, operation.method, path, `Coolify API returned ${response.status} for ${operation.method} ${path}`);
82
+ let data = null;
83
+ if (response.status !== 204) {
84
+ const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
85
+ if (contentType.includes("json"))
86
+ data = normalizeKnownCollection(operation.path, await response.json());
87
+ else if (contentType.startsWith("text/") || contentType.includes("xml"))
88
+ data = await response.text();
89
+ else
90
+ data = { encoding: "base64", contentType: contentType || "application/octet-stream", value: base64(await response.arrayBuffer()) };
91
+ }
92
+ return redactSensitive({ data, status: response.status, request: { method: operation.method, path } });
93
+ }
94
+ }
95
+ export { CoolifyApiError };
@@ -0,0 +1,9 @@
1
+ export interface CoolifyConfig {
2
+ baseUrl: string;
3
+ token: string;
4
+ timeoutMs: number;
5
+ }
6
+ export declare class ConfigError extends Error {
7
+ constructor(message: string);
8
+ }
9
+ export declare function loadConfig(env?: Record<string, string | undefined>): CoolifyConfig;
package/dist/config.js ADDED
@@ -0,0 +1,30 @@
1
+ export class ConfigError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = "ConfigError";
5
+ }
6
+ }
7
+ export function loadConfig(env = process.env) {
8
+ const rawUrl = env.COOLIFY_URL?.trim();
9
+ const token = env.COOLIFY_TOKEN?.trim();
10
+ if (!rawUrl)
11
+ throw new ConfigError("COOLIFY_URL is required");
12
+ if (!token)
13
+ throw new ConfigError("COOLIFY_TOKEN is required");
14
+ let url;
15
+ try {
16
+ url = new URL(rawUrl);
17
+ }
18
+ catch {
19
+ throw new ConfigError("COOLIFY_URL must be a valid HTTP(S) URL");
20
+ }
21
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
22
+ throw new ConfigError("COOLIFY_URL must use http:// or https://");
23
+ }
24
+ if (url.search || url.hash)
25
+ throw new ConfigError("COOLIFY_URL must not include a query or fragment");
26
+ url.pathname = url.pathname.replace(/\/+$/u, "");
27
+ if (!url.pathname.endsWith("/api/v1"))
28
+ url.pathname = `${url.pathname}/api/v1`.replace(/^\/\//u, "/");
29
+ return { baseUrl: url.toString().replace(/\/$/u, ""), token, timeoutMs: 30_000 };
30
+ }
@@ -0,0 +1,6 @@
1
+ export declare class CoolifyApiError extends Error {
2
+ readonly status: number;
3
+ readonly method: string;
4
+ readonly path: string;
5
+ constructor(status: number, method: string, path: string, message: string);
6
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,12 @@
1
+ export class CoolifyApiError extends Error {
2
+ status;
3
+ method;
4
+ path;
5
+ constructor(status, method, path, message) {
6
+ super(message);
7
+ this.status = status;
8
+ this.method = method;
9
+ this.path = path;
10
+ this.name = "CoolifyApiError";
11
+ }
12
+ }