@lovinka/deployik-mcp 0.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.
- package/README.md +59 -0
- package/dist/client/errors.js +51 -0
- package/dist/client/errors.js.map +1 -0
- package/dist/client/http.js +106 -0
- package/dist/client/http.js.map +1 -0
- package/dist/client/types.js +5 -0
- package/dist/client/types.js.map +1 -0
- package/dist/config/audit.js +43 -0
- package/dist/config/audit.js.map +1 -0
- package/dist/config/binding.js +44 -0
- package/dist/config/binding.js.map +1 -0
- package/dist/config/cache.js +44 -0
- package/dist/config/cache.js.map +1 -0
- package/dist/config/env.js +40 -0
- package/dist/config/env.js.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/knowledge/index.js +116 -0
- package/dist/knowledge/index.js.map +1 -0
- package/dist/knowledge/prompts.js +20 -0
- package/dist/knowledge/prompts.js.map +1 -0
- package/dist/knowledge/recipes.generated.js +17 -0
- package/dist/knowledge/recipes.generated.js.map +1 -0
- package/dist/lib/format.js +96 -0
- package/dist/lib/format.js.map +1 -0
- package/dist/lib/logs.js +46 -0
- package/dist/lib/logs.js.map +1 -0
- package/dist/lib/poll.js +20 -0
- package/dist/lib/poll.js.map +1 -0
- package/dist/lib/safety.js +38 -0
- package/dist/lib/safety.js.map +1 -0
- package/dist/resolve/project.js +130 -0
- package/dist/resolve/project.js.map +1 -0
- package/dist/server.js +32 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/_helpers.js +76 -0
- package/dist/tools/_helpers.js.map +1 -0
- package/dist/tools/analytics.js +46 -0
- package/dist/tools/analytics.js.map +1 -0
- package/dist/tools/auto_build.js +84 -0
- package/dist/tools/auto_build.js.map +1 -0
- package/dist/tools/deployments.js +120 -0
- package/dist/tools/deployments.js.map +1 -0
- package/dist/tools/domains.js +104 -0
- package/dist/tools/domains.js.map +1 -0
- package/dist/tools/email.js +90 -0
- package/dist/tools/email.js.map +1 -0
- package/dist/tools/env.js +116 -0
- package/dist/tools/env.js.map +1 -0
- package/dist/tools/github.js +59 -0
- package/dist/tools/github.js.map +1 -0
- package/dist/tools/help.js +34 -0
- package/dist/tools/help.js.map +1 -0
- package/dist/tools/index.js +33 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/projects.js +164 -0
- package/dist/tools/projects.js.map +1 -0
- package/dist/tools/protection.js +88 -0
- package/dist/tools/protection.js.map +1 -0
- package/dist/tools/secrets.js +115 -0
- package/dist/tools/secrets.js.map +1 -0
- package/dist/tools/tokens.js +60 -0
- package/dist/tools/tokens.js.map +1 -0
- package/dist/tools/volumes.js +77 -0
- package/dist/tools/volumes.js.map +1 -0
- package/dist/tools/workflows.js +350 -0
- package/dist/tools/workflows.js.map +1 -0
- package/dist/tools/workspaces.js +48 -0
- package/dist/tools/workspaces.js.map +1 -0
- package/package.json +50 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// AUTO-GENERATED by scripts/copy-recipes.mjs. Do not edit by hand.
|
|
2
|
+
// Source: ../../.claude/skills/deployik-howto/
|
|
3
|
+
export const RECIPE_FILES = [
|
|
4
|
+
{
|
|
5
|
+
file: "SKILL.md",
|
|
6
|
+
content: "---\nname: deployik-howto\ndescription: Use when a user asks how to do something in the Deployik dashboard — connecting a GitHub repo, custom domains, environment variables or secrets, auto-deploy on push, password protection, sending email from a contact form (Webglobe SMTP + reCAPTCHA v3), or rolling back a deployment — or asks Claude to perform one of those actions for them. Triggers include \"how do I…\", \"where do I click…\", \"I want my own domain\", \"I want my contact form to send emails\", \"I want to make X work\", \"deploy my repo\", and \"just do X for me\" phrased against Deployik. Do NOT activate for questions about Deployik's source code (Go handlers, React pages, migrations) — those are codebase tasks, not dashboard tasks.\n---\n\n# Deployik How-To\n\nHelps a user navigate or operate the Deployik dashboard. Two modes: **guide** (walk them through clicks) and **action** (do it for them via the Deployik API).\n\n## Tone\n\nThe primary user is non-technical. Speak warmly and in plain language. Quote Deployik button labels exactly (they're in English) but explain in everyday words around them. Never assume the user already knows what an \"MX record\" or a \"site key\" is — define it the first time it appears, in one short sentence.\n\nWhen you give a recipe, **always finish with the same friendly offer**:\n\n> Stuck on any of these steps? Tell me which one and I'll walk through it with you.\n\nIf the user gets stuck, ask one question at a time, and offer to do the part on their behalf if it's something action mode can do (e.g. \"Want me to add the secret for you, or do you want to do it in the dashboard?\").\n\nIf a sub-step happens outside Deployik (a registrar's DNS panel, the Google reCAPTCHA admin, a webmail customer portal), say so explicitly so they know they're leaving Deployik for a moment. Then guide them step-by-step in the external UI before bringing them back.\n\n## When to use\n\n- User goal phrased against the Deployik dashboard — e.g. \"I want my own domain\", \"where do I add an env var\", \"make the site password-protected\", \"redeploy when I push to GitHub\", \"make my contact form send emails\", \"roll back my last release\".\n- User asks Claude to *perform* one of those tasks instead of guiding them.\n\n## When NOT to use\n\n- Questions about Deployik's source code, build pipeline internals, or how a Go handler/React page is implemented. Those are not dashboard tasks.\n- Self-hosting / VPS / nginx / SSL provider questions — those belong to the lovinka-infra repo and a different audience.\n\n## How to choose the mode\n\nRead the user's phrasing:\n\n- *\"How do I…\"*, *\"Where do I click…\"*, *\"I want to learn how to…\"* → **guide mode** → see [click-paths.md](click-paths.md).\n- *\"Do X for me\"*, *\"Add this env var\"*, *\"Trigger a deploy\"*, imperative phrasing → **action mode** → see [api-actions.md](api-actions.md).\n\nIf ambiguous, ask warmly: \"Want me to walk you through the dashboard, or do it for you?\"\n\n## Action mode prerequisites\n\nAction mode requires a Personal Access Token. If `~/.config/deployik/config` doesn't exist or is missing `DEPLOYIK_BASE_URL` / `DEPLOYIK_TOKEN`, stop and explain in friendly tone:\n\n> Before I can do this for you, I need a Deployik access token — it's like a password just for tools. In the Deployik sidebar, click **Access tokens** → **Create token**, give it a name like \"skill\", and copy the value it shows you (it starts with `dpk_`). Then save it to a file at `~/.config/deployik/config` like this:\n>\n> ```\n> DEPLOYIK_BASE_URL=https://your-deployik-host\n> DEPLOYIK_TOKEN=dpk_...\n> ```\n>\n> Once that's saved, just ask me the same thing again and I'll do it.\n\n## Safety rules for action mode\n\n| Verb / target | Rule |\n|---------------|------|\n| `GET *` | Execute silently |\n| `POST/PUT/PATCH` to non-production | Print intended `<METHOD> <path>` and JSON body, ask \"Do this?\" — wait for an affirmative reply |\n| `POST/PUT/PATCH` to production env | Print payload, **flag PRODUCTION explicitly**, ask yes/no |\n| `DELETE *` and `POST .../regenerate` | Require typed string confirmation matching the target name (e.g. `yes delete example.com`) — anything else aborts |\n\nAlways invoke the API via `helpers/deployik` (bundled with this skill). Never hand-roll curl with the token inline — keeps the token out of shell history.\n",
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
file: "api-actions.md",
|
|
10
|
+
content: "# Deployik — API actions\n\nAction mode for the goals in [click-paths.md](click-paths.md). Each entry has the endpoint, payload shape, safety tier, and the exact `helpers/deployik` invocation.\n\n## Helper script\n\nAlways invoke the API via `./helpers/deployik` (relative to this skill's directory) — never paste tokens into raw curl. The wrapper:\n\n- Reads `~/.config/deployik/config` (`DEPLOYIK_BASE_URL`, `DEPLOYIK_TOKEN`)\n- Sends `Authorization: Bearer $DEPLOYIK_TOKEN`\n- Prints `HTTP <status>` to stderr, body to stdout\n- Exits non-zero on HTTP error\n- Pretty-prints JSON if `jq` is installed\n\nUsage:\n\n```\ndeployik api <METHOD> <path> [json-body]\n```\n\nIf `~/.config/deployik/config` is missing, stop and direct the user to create a token (Sidebar → **Access tokens** → **Create token**) before retrying.\n\n## Safety tiers\n\n| Verb / target | Rule |\n|---------------|------|\n| `GET *` | Execute silently — read-only |\n| `POST/PUT/PATCH` non-production | Print payload, ask \"Do this?\" — wait for affirmative reply |\n| `POST/PUT/PATCH` production env | Flag `**PRODUCTION**`, ask explicit yes/no |\n| `DELETE *` and `POST .../regenerate` | Require typed string confirmation matching the target name (e.g. `yes delete example.com`) |\n\n## Resolving project id\n\nMost endpoints need `{id}` (a ULID). When the user names a project, run:\n\n```\ndeployik api GET /api/projects\n```\n\n…and find the row whose `name` matches. Use its `id`.\n\n---\n\n## create-project\n\n**Goal:** [click-paths.md#create-project](click-paths.md#create-project)\n\n**Endpoint:** `POST /api/projects`\n**Tier:** Mutation — confirm before executing.\n\n**Body shape:**\n\n```json\n{\n \"name\": \"my-app\",\n \"github_repo\": \"my-repo\",\n \"github_owner\": \"my-org\",\n \"branch\": \"main\",\n \"framework\": \"nextjs\",\n \"package_manager\": \"auto\",\n \"root_directory\": \"\",\n \"output_directory\": \".next\",\n \"build_command\": \"bun run build\",\n \"install_command\": \"bun install\",\n \"node_version\": \"22\",\n \"organization_id\": \"(optional — defaults to personal workspace)\"\n}\n```\n\n**Invocation:**\n\n```\ndeployik api POST /api/projects '{\"name\":\"my-app\",\"github_repo\":\"my-repo\",\"github_owner\":\"my-org\",\"branch\":\"main\",\"framework\":\"nextjs\",\"package_manager\":\"auto\",\"output_directory\":\".next\",\"build_command\":\"bun run build\",\"install_command\":\"bun install\",\"node_version\":\"22\"}'\n```\n\n**Behavior:** creates the project, auto-domain, GitHub webhook (best-effort), and triggers an initial preview deployment.\n\n---\n\n## custom-domain\n\n**Goal:** [click-paths.md#custom-domain](click-paths.md#custom-domain)\n\n**Endpoints:**\n- `POST /api/projects/{id}/domains` — adds the row\n- `POST /api/projects/{id}/domains/{did}/verify` — runs DNS check + cert provisioning (returns 202; final result streams over WebSocket but the synchronous response confirms acceptance)\n- `PATCH /api/projects/{id}/domains/{did}` `{is_primary: true}` — optional, mark as primary\n\n**Tier:** Mutation — confirm before each step.\n\n**Body shape (POST):**\n\n```json\n{ \"domain\": \"example.com\", \"environment\": \"production\" }\n```\n\n**Invocation:**\n\n```\ndeployik api POST /api/projects/{id}/domains '{\"domain\":\"example.com\",\"environment\":\"production\"}'\ndeployik api POST /api/projects/{id}/domains/{did}/verify\n```\n\n**Critical reminder for the user:** the DNS A-record at the registrar must point to the VPS IP **before** Verify, otherwise it fails. Tell them the click-paths recipe explains where to add it.\n\n---\n\n## env-vars\n\n**Goal:** [click-paths.md#env-vars](click-paths.md#env-vars)\n\n**Endpoints:**\n- `POST /api/projects/{id}/env` — single upsert (env var)\n- `POST /api/projects/{id}/secrets` — single upsert (secret)\n- `DELETE /api/projects/{id}/env/{key}?environment=preview|production|shared` — destructive\n- `PUT /api/projects/{id}/env` — bulk replace (don't use unless the user says \"replace all\")\n\n**Tier:** Mutation — confirm. **Production environment writes get extra production-flag confirmation.**\n\n**Body shape (single upsert):**\n\n```json\n{ \"key\": \"STRIPE_PUBLIC\", \"value\": \"pk_test_xxx\", \"environment\": \"preview\" }\n```\n\n**Environment values:** `shared` (applies to both), `preview`, or `production`. If the user says \"preview\", use `preview`; if \"production\" or \"live\", use `production`. If unsure, ask.\n\n**Invocations:**\n\n```\ndeployik api POST /api/projects/{id}/env '{\"key\":\"STRIPE_PUBLIC\",\"value\":\"pk_test_xxx\",\"environment\":\"preview\"}'\ndeployik api POST /api/projects/{id}/secrets '{\"key\":\"DATABASE_URL\",\"value\":\"postgres://...\",\"environment\":\"production\"}'\ndeployik api DELETE /api/projects/{id}/env/STRIPE_PUBLIC?environment=preview\n```\n\n**Constraint:** secrets refuse `NEXT_PUBLIC_*` keys (they need to be in the variables store to bake into the build). Surface that error verbatim if the API returns it.\n\n**Heads-up:** changing a variable does NOT redeploy. After applying, ask the user if they want you to trigger a redeploy via the [#rollback](#rollback) endpoint with the latest commit sha.\n\n---\n\n## auto-deploy\n\n**Goal:** [click-paths.md#auto-deploy](click-paths.md#auto-deploy)\n\n**Endpoint:** `PUT /api/projects/{id}/auto-build`\n**Tier:** Mutation — confirm.\n\n**Body shape:**\n\n```json\n{\n \"enabled\": true,\n \"production_branch\": \"main\",\n \"preview_branches\": \"*\"\n}\n```\n\n**Invocation:**\n\n```\ndeployik api PUT /api/projects/{id}/auto-build '{\"enabled\":true,\"production_branch\":\"main\",\"preview_branches\":\"*\"}'\n```\n\n**Side effect:** Deployik creates a webhook on the GitHub repo. If the user hasn't granted the `admin:repo_hook` scope to the OAuth app, this errors — ask them to sign out and back in in the dashboard, then retry.\n\nTo disable: `deployik api DELETE /api/projects/{id}/auto-build` (deletes config + webhook). Treat as destructive — typed confirmation.\n\n---\n\n## password-protection\n\n**Goal:** [click-paths.md#password-protection](click-paths.md#password-protection)\n\n**Endpoints:**\n- `PUT /api/projects/{id}/protection` — enable/disable per environment\n- `POST /api/projects/{id}/protection/regenerate` — new password (destructive — require typed confirmation, the old password stops working at this point)\n\n**Tier:** PUT = mutation/confirm. Regenerate = destructive/typed-confirm.\n\n**Body shapes:**\n\n```json\n{ \"environment\": \"preview\", \"enabled\": true }\n{ \"environment\": \"production\", \"enabled\": false }\n```\n\n**Invocations:**\n\n```\ndeployik api PUT /api/projects/{id}/protection '{\"environment\":\"preview\",\"enabled\":true}'\ndeployik api POST /api/projects/{id}/protection/regenerate '{\"environment\":\"preview\"}'\n```\n\n**Response on enable / regenerate:** the JSON includes the plaintext password under `password`. Surface it to the user with a warning: this is the only time the API will return it.\n\n---\n\n## contact-form-email\n\n**Goal:** [click-paths.md#contact-form-email](click-paths.md#contact-form-email)\n\n**Endpoints:**\n- `GET /api/projects/{id}/email` — fetch current settings + the **AI install prompt** (auto-generated server-side from the project's framework, package manager, root directory, and reCAPTCHA site key)\n- `PUT /api/projects/{id}/email` — save SMTP host/port/security/user, From address + name, contact recipients, reCAPTCHA site key, score threshold; the SMTP password and reCAPTCHA secret are persisted as encrypted secrets\n- `POST /api/projects/{id}/email/test-smtp` — send a test email through the saved SMTP credentials\n\n**Tier:** PUT = mutation/confirm. test-smtp = mutation/confirm (it actually sends an email — usually fine, but flag it explicitly because the user might be configuring against a live mailbox). GET = read silent.\n\n**Body shape (PUT):**\n\n```json\n{\n \"provider\": \"webglobe\",\n \"smtp_host\": \"smtp.webglobe.cz\",\n \"smtp_port\": 587,\n \"smtp_security\": \"starttls\",\n \"smtp_user\": \"noreply@example.com\",\n \"smtp_password\": \"<paste, only sent on save>\",\n \"email_from\": \"noreply@example.com\",\n \"email_from_name\": \"My Site\",\n \"contact_email_to\": \"owner@example.com\",\n \"recaptcha_site_key\": \"6Lc...public\",\n \"recaptcha_secret\": \"6Lc...secret\",\n \"recaptcha_mode\": \"v3\",\n \"recaptcha_score_threshold\": 0.5\n}\n```\n\n**Provider values:** `webglobe` (defaults derived from the project's primary production domain) or `smtp` (custom — user supplies all fields). Use `webglobe` unless the user says otherwise.\n\n**Security values:** `starttls` (port 587), `tls` (port 465), `none` (only for explicit testing — never recommend it).\n\n**Invocations:**\n\n```\ndeployik api GET /api/projects/{id}/email\ndeployik api PUT /api/projects/{id}/email '{\"provider\":\"webglobe\",\"smtp_host\":\"smtp.webglobe.cz\",\"smtp_port\":587,\"smtp_security\":\"starttls\",\"smtp_user\":\"noreply@example.com\",\"smtp_password\":\"<...>\",\"email_from\":\"noreply@example.com\",\"email_from_name\":\"My Site\",\"contact_email_to\":\"owner@example.com\",\"recaptcha_site_key\":\"<site>\",\"recaptcha_secret\":\"<secret>\",\"recaptcha_mode\":\"v3\",\"recaptcha_score_threshold\":0.5}'\ndeployik api POST /api/projects/{id}/email/test-smtp\n```\n\n**Response from GET:** includes the field `ai_install_prompt` (or similar — confirm the exact key by reading the response). This is a fully-formed prompt the user can paste into any AI coding assistant to install the contact-form code into their site. **When the user asks for the AI prompt, surface this string verbatim** — do not paraphrase or shorten it.\n\n**Workflow when guiding action mode for this recipe:**\n1. Ask the user for the four SMTP values from the Webglobe portal and the two reCAPTCHA keys from Google. Offer to walk them through Parts 1 and 2 of the click-paths recipe if they don't have them yet.\n2. Print the PUT payload (with **password and secret masked** as `***` in the preview) and ask \"Do this?\"\n3. On confirm, send the PUT.\n4. Ask if they want to verify by sending a test email; if yes, run POST `/email/test-smtp`.\n5. If the test succeeds, fetch GET `/email` and surface the AI install prompt for Part 4 of the recipe.\n6. If the test fails, surface the error from `payload.settings.last_test_error` verbatim and offer to walk through troubleshooting.\n\n**Sensitive values:** `smtp_password` and `recaptcha_secret` are the only truly sensitive fields. Treat them like passwords. If the user pastes them in chat, complete the action then suggest rotating them.\n\n---\n\n## rollback\n\n**Goal:** [click-paths.md#rollback](click-paths.md#rollback)\n\n**Endpoints:**\n- `GET /api/projects/{id}/deployments?environment=production&status=live` — find the previous good deployment\n- `POST /api/projects/{id}/deployments` — trigger a redeploy of a specific commit\n\n**Tier:** Mutation — confirm. **Production redeploys flagged as production.**\n\n**Body shape (POST):**\n\n```json\n{ \"environment\": \"production\", \"branch\": \"main\", \"commit_sha\": \"abc1234\" }\n```\n\nIf the user wants to roll back to \"the previous good one\", list deployments first and pick the most recent `live` row before the regression. Show its commit sha and message in the confirmation prompt.\n\n**Invocations:**\n\n```\ndeployik api GET /api/projects/{id}/deployments?environment=production&status=live&limit=5\ndeployik api POST /api/projects/{id}/deployments '{\"environment\":\"production\",\"branch\":\"main\",\"commit_sha\":\"abc1234\"}'\n```\n\n---\n\n## Errors\n\nWhen the API returns a non-2xx status:\n\n- Print the HTTP status and the JSON body (the helper does this automatically — let the user see it).\n- **Don't retry silently.** Most errors are user-meaningful (e.g. `403` from auto-build = missing OAuth scope, `409` from domain add = duplicate, `400` from secret = `NEXT_PUBLIC_` not allowed).\n- Suggest the next step based on the error message rather than guessing or hand-rolling a fix.\n",
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
file: "click-paths.md",
|
|
14
|
+
content: "# Deployik — Where to click\n\nSeven recipes for the most common things a user wants to do in the Deployik dashboard. If the user's goal isn't in the table, ask them warmly to rephrase, or check whether their goal is actually outside Deployik's scope.\n\nEvery recipe ends with a friendly \"stuck on a step?\" line. If the user uses it, ask which step and walk through that single step in fine detail (one click, one screenshot description, or one clarifying question at a time) before continuing.\n\n## What do you want to do?\n\n| Goal | Recipe |\n|------|--------|\n| I want to put my GitHub repo online | [#create-project](#create-project) |\n| I want to use my own domain (example.com) | [#custom-domain](#custom-domain) |\n| I want to set environment variables / API keys | [#env-vars](#env-vars) |\n| I want it to redeploy when I push to GitHub | [#auto-deploy](#auto-deploy) |\n| I want a password before people see the site | [#password-protection](#password-protection) |\n| I want my contact form to actually send emails (with spam protection) | [#contact-form-email](#contact-form-email) |\n| I want to roll back to a previous version | [#rollback](#rollback) |\n\n---\n\n## create-project\n\n**Goal:** Connect a GitHub repository to Deployik and deploy it for the first time.\n\n**Route:** `/new`\n**Sidebar:** (top-right of the Projects page) → **+ New project**\n\n**Steps:**\n1. Click **+ New project** on the dashboard. The page lists every GitHub repo Deployik can see for your account.\n2. Search or scroll for the repo. Click **Import** on the row.\n3. *If Deployik detects a monorepo (multiple apps in subfolders):* a step appears asking which app to deploy. Click the one you want. Framework, root directory, and build command pre-fill.\n4. *If single-app:* this step is skipped automatically.\n5. On the configuration screen:\n - **Project name** — used as the preview subdomain. Lowercase letters, digits, and hyphens only.\n - **Branch** — usually `main` or `master`.\n - **Framework** — auto-detected. If wrong, change it; install/build/output reset to that framework's defaults.\n - **Workspace** — leave on Personal unless you've been added to a shared org.\n6. Click **Create project**. Deployik creates the auto-domain `{project-name}.preview.lovinka.com`, sets up the GitHub webhook for auto-deploy, and starts the first preview deployment.\n7. The page navigates to the project Overview. The **Preview** strip shows the deployment progressing through queued → building → live.\n\n**Gotcha:** the GitHub OAuth scope must include `admin:repo_hook` for the webhook step to work. If you signed in before that scope was added, sign out and back in.\n\n**Stuck on any of these steps? Tell me which one and I'll walk through it with you.**\n\n**API equivalent:** [api-actions.md#create-project](api-actions.md#create-project)\n\n---\n\n## custom-domain\n\n**Goal:** Point your own domain (e.g. `example.com`) at a Deployik project.\n\n**Route:** `/projects/$id/settings/domains`\n**Sidebar:** Project → **Settings** (expand) → **Domains**\n\n**Steps:**\n1. Click **Domains** under the project's Settings section.\n2. Find the **Add domain** form near the top. Enter the domain (apex like `example.com` or subdomain like `app.example.com`).\n3. Pick the environment — **Preview** for staging, **Production** for live.\n4. Click **Add**. The row appears with status `pending` and `dns_verified: no`.\n5. **Outside Deployik** — go to your domain's DNS provider:\n - **Cloudflare:** DNS → **Records** → **Add record** → Type `A`, Name (apex = `@`, subdomain = the prefix), IPv4 = the VPS IP shown in Deployik's expandable **DNS Setup Guide** on the same page, Proxy status: **DNS only** (gray cloud, not orange — Cloudflare's proxy interferes with Let's Encrypt). Save.\n - **Namecheap:** Domain List → **Manage** → **Advanced DNS** → **Add new record** → Type `A Record`, Host (`@` or subdomain), Value = VPS IP, TTL Automatic. Save.\n - **GoDaddy:** Domain → **DNS** → **Add** → Type `A`, Name (`@` or subdomain), Value = VPS IP. Save.\n - **Other registrar:** add an `A` record pointing at the VPS IP from the DNS Setup Guide. The exact UI varies — the record type and value matter, the UI doesn't.\n6. Wait 1–5 minutes for DNS to propagate. You can check with `dig +short A example.com` or `nslookup example.com` from your terminal — when it returns the VPS IP, you're ready.\n7. Back in Deployik, on the same Domains page, click the **Verify** button on the domain's row. A live log streams: DNS check → Let's Encrypt cert request → nginx reload. Wait for \"Provisioning complete\".\n8. Once `ssl_status: active`, the **Open** button on the row works. Production primary domain also drives the **Open** link on the project Overview.\n\n**Gotcha:** if Verify fails with \"DNS does not match\", DNS hasn't propagated yet — wait another minute and try again. Don't change anything in Deployik in the meantime.\n\n**Set as primary:** to make this domain the one Deployik treats as canonical for its environment, click the row's three-dot menu → **Set as primary**. A star badge appears.\n\n**Stuck on any of these steps? Tell me which one and I'll walk through it with you.** I can especially help with the registrar's DNS panel — just tell me which provider you use and I'll talk you through the exact clicks.\n\n**API equivalent:** [api-actions.md#custom-domain](api-actions.md#custom-domain)\n\n---\n\n## env-vars\n\n**Goal:** Set environment variables (for build-time `NEXT_PUBLIC_*`) or secrets (runtime-only) for the deployed app.\n\n**Route:** `/projects/$id/settings/env`\n**Sidebar:** Project → **Settings** (expand) → **Environments**\n\n**Steps:**\n1. Click **Environments** under the project's Settings section.\n2. Two tabs at the top: **Variables** (visible at build time, can include `NEXT_PUBLIC_*`) and **Secrets** (runtime only, never in build).\n3. Each row picks a **Scope** — *Shared* (applies to both preview and production), *Preview* only, or *Production* only.\n4. Click **Add variable** (or **Add secret** on that tab). Type the key (e.g. `STRIPE_PUBLIC` or `DATABASE_URL`), the value, pick the scope. Click **Save**.\n5. To bulk-paste from a `.env` file: click **Import .env**, paste the file's contents, pick the scope. Each non-empty line becomes one variable.\n6. Existing values are masked as `****abcd` (last 4 chars). To change a value, click the row's edit (pencil) icon, paste the new value, save.\n\n**Gotcha:** changing a variable does NOT redeploy automatically. You'll see a \"Variables changed since last deploy\" badge on the project Overview. Click **Redeploy** there to apply the new values.\n\n**Build-time vs runtime:** `NEXT_PUBLIC_*` keys are baked into the static bundle at build time, so changing one requires a rebuild. Secrets are always runtime-only — Deployik refuses to save a secret with a `NEXT_PUBLIC_` prefix.\n\n**Stuck on any of these steps? Tell me which one and I'll walk through it with you.** I can also add a variable for you via the API — just tell me the key, value, and which environment.\n\n**API equivalent:** [api-actions.md#env-vars](api-actions.md#env-vars)\n\n---\n\n## auto-deploy\n\n**Goal:** Make Deployik redeploy automatically when you push to GitHub.\n\n**Route:** `/projects/$id/settings`\n**Sidebar:** Project → **Settings** (expand) → **Build** (the default Settings page)\n\n**Steps:**\n1. Click **Settings** in the project sidebar (it lands on Build by default).\n2. Scroll to the **Auto-deploy** section.\n3. Toggle **Enable auto-deploy**. The first time you turn it on, Deployik creates a webhook on the GitHub repo using your OAuth token — this requires the `admin:repo_hook` scope. If the toggle errors, sign out and back in (GitHub re-prompts for the scope).\n4. **Production branch** — the branch whose pushes trigger production deployments. Defaults to the project's main branch (e.g. `main`).\n5. **Preview branches** — comma-separated list of branch names or branch patterns. `*` matches every other branch. Leave as `*` unless you want to restrict preview builds to specific branches.\n6. Click **Save**. The status indicator turns green and shows the GitHub webhook ID.\n7. Push a commit to the production branch. Within seconds, a new deployment with `trigger_source: webhook` appears in the Deployments list, and Deployik streams the build log.\n\n**Gotcha:** if a push doesn't trigger a build, check **GitHub repo → Settings → Webhooks**. The Deployik webhook should show recent deliveries with `200 OK` responses. Re-deliver a failed one from there to debug.\n\n**Stuck on any of these steps? Tell me which one and I'll walk through it with you.**\n\n**API equivalent:** [api-actions.md#auto-deploy](api-actions.md#auto-deploy)\n\n---\n\n## password-protection\n\n**Goal:** Hide the deployed site (preview, production, or both) behind a password before showing it to clients or doing QA.\n\n**Route:** `/projects/$id/settings/protection`\n**Sidebar:** Project → **Settings** (expand) → **Protection**\n\n**Steps:**\n1. Click **Protection** under the project's Settings section.\n2. Two cards: **Preview** and **Production**. Each has a toggle and (when enabled) a password reveal button.\n3. Toggle **Enable password protection** on the environment you want.\n4. Deployik generates a 16-character random password, encrypts and stores it, regenerates the nginx config to add `auth_request` blocks, and shows you the password once. Click the eye icon to reveal it. Copy it now.\n5. Click **Regenerate password** to mint a new one (invalidates the old). The new password is shown the same way.\n6. Visit the deployed site in an incognito window to confirm: the Czech-language auth page (`Heslo`) appears. Enter the password — you're in for 24 hours.\n\n**Gotcha:** changing the password does not log out anyone who's already on the site. The signed `deployik_site_auth` cookie they hold remains valid for its 24-hour lifetime. To force everyone off, change the cookie's `JWT_SECRET` on the Deployik server (operator action).\n\n**Stuck on any of these steps? Tell me which one and I'll walk through it with you.**\n\n**API equivalent:** [api-actions.md#password-protection](api-actions.md#password-protection)\n\n---\n\n## contact-form-email\n\n**Goal:** Make the contact form on your deployed site actually deliver emails to your inbox, with spam protection so bots don't flood you.\n\n**Route:** `/projects/$id/integrations/email`\n**Sidebar:** Project → **Integrations** (expand) → **Email**\n\n**What this gives you:** when someone submits the contact form on your site, a real email lands in your inbox. Bots are filtered out using Google's invisible spam check (reCAPTCHA v3 — your visitors never see a \"click the traffic lights\" puzzle).\n\nThis recipe has three parts. Two of them happen outside Deployik — I'll tell you when we're leaving and when we're coming back.\n\n### Part 1 — Get your SMTP credentials from Webglobe (outside Deployik)\n\nSMTP is just the technical name for \"email sending\" — your hosting provider gives you a username and password your site uses to send mail through their servers.\n\n1. Open a new browser tab, go to your **Webglobe customer portal** (the place where you manage your hosting). Sign in.\n2. Find the section called **Mail** or **E-mail accounts**. Pick the address you want emails to come *from* (e.g. `noreply@yourdomain.com`). Create it if it doesn't exist yet.\n3. On that mailbox, look for **SMTP** or **Outgoing mail** settings. Write down four things:\n - **SMTP host** (looks like `smtp.webglobe.cz` or similar — exact name varies)\n - **SMTP port** (usually `587` for STARTTLS or `465` for TLS)\n - **Security** (`STARTTLS` for port 587, `TLS` for port 465)\n - **Username** (often the full email address, e.g. `noreply@yourdomain.com`)\n - **Password** (the mailbox password — keep it safe, you'll paste it into Deployik in Part 3)\n\nIf you can't find any of these, ask me — I'll help you locate them based on what you see in the portal.\n\n### Part 2 — Get reCAPTCHA v3 keys from Google (outside Deployik)\n\nThis is the spam protection. Google gives you two keys: a **site key** that goes on the public form, and a **secret key** the server uses to verify the form was submitted by a human.\n\n1. In a new tab, open `https://www.google.com/recaptcha/admin/create`. Sign in with your Google account.\n2. Fill the form:\n - **Label:** anything memorable, e.g. `myproject contact form`\n - **reCAPTCHA type:** pick **reCAPTCHA v3** (very important — v2 won't work).\n - **Domains:** add your production hostname (e.g. `example.com`). If you have a `www.example.com` variant, add it too. You can add `localhost` for testing.\n - Accept the terms, click **Submit**.\n3. The next page shows two keys:\n - **Site key** — public, goes in Deployik. Copy it.\n - **Secret key** — private, goes in your secrets store. Copy it too (and treat it like a password).\n\n### Part 3 — Wire it up in Deployik\n\nWe're back in Deployik now.\n\n1. In the project sidebar, expand **Integrations** and click **Email**.\n2. Fill in the SMTP fields with what you wrote down in Part 1:\n - **SMTP host**, **Port**, **Security** (STARTTLS / TLS), **Username**.\n - For **Password**, click the field and paste the mailbox password.\n3. Fill in the email fields:\n - **From address** — the mailbox address (e.g. `noreply@yourdomain.com`).\n - **From name** — the human name shown to recipients (e.g. `My Site`).\n - **Contact recipients** — who receives the form submissions. Comma-separated if more than one.\n4. Paste the reCAPTCHA **site key** from Part 2 into the **reCAPTCHA site key** field.\n5. **Score threshold** — leave at the default (`0.5`) unless you know you want to change it. Higher = stricter (more bots blocked, but more humans rejected too).\n6. Click **Save**. Deployik writes the SMTP host/port/from-address as **shared variables** and the SMTP password and reCAPTCHA secret as **secrets** — so the deployed site can read them at runtime.\n7. Click **Send test email**. Check the inbox of one of your **Contact recipients**. If it arrives, you're done with Part 3.\n\n### Part 4 — Add the contact form code to your site\n\nThe Email page has an **AI install prompt** section. It generates a copy-pasteable prompt with your project's exact framework, environment variables, and reCAPTCHA site key already filled in.\n\n1. On the Email page, scroll to **AI install prompt** (or **Install with AI**).\n2. Click **Copy prompt**.\n3. Paste the prompt into your AI coding assistant (Cursor, Claude Code, etc.) inside your project's repo. The assistant will write the contact form route, the reCAPTCHA hook, and the server handler that calls Deployik's SMTP credentials.\n4. Commit, push. If auto-deploy is on, the new version is live in a couple of minutes.\n5. Visit your live site, fill out the form, submit. The email should land in the inbox you configured.\n\n**Gotcha:** if the test email succeeds but real form submissions don't arrive, the most common cause is that the contact form code isn't reading the reCAPTCHA secret from environment variables on the server. The AI install prompt sets this up correctly — if it doesn't work, paste the error your form shows you and I'll diagnose.\n\n**Gotcha:** the **SMTP password** and the **reCAPTCHA secret** are the only two truly sensitive values here. Both are stored as secrets (encrypted at rest) — never paste them into a chat with me unless you're willing to rotate them after.\n\n**Stuck on any of these steps? Tell me which one and I'll walk through it with you.** I can also do Part 3 for you via the API once you have the values from Parts 1 and 2 — just ask.\n\n**API equivalent:** [api-actions.md#contact-form-email](api-actions.md#contact-form-email)\n\n---\n\n## rollback\n\n**Goal:** Revert to a previous successful deployment when the latest one is broken.\n\n**Route:** `/projects/$id/deployments`\n**Sidebar:** Project → **Deployments**\n\n**Steps:**\n1. Click **Deployments** in the project sidebar.\n2. The list shows every deployment with status, branch, commit message, who triggered it, and a thumbnail screenshot.\n3. Find the most recent **live**-status row from before the regression. Click the row to open the deployment detail page.\n4. On the detail page, in the top-right, click **Redeploy this commit**. (If you don't see that button, scroll to the build log header — it's there.)\n5. A confirmation modal appears. Click **Redeploy**. A new deployment row appears in the list with the same `commit_sha` but a new `id`, new build, and `triggered_by_username: you`.\n6. When status reaches **live**, the swap completes — the previous broken container is stopped and the rolled-back container takes over. Visitors see the older code immediately.\n\n**Gotcha:** rollback re-runs the build from source. If the regression is a config/env-var issue (not a code change), changing the env var and triggering a new build is faster — see [#env-vars](#env-vars). If it's a runtime dependency that broke between commits, rollback is the right move.\n\n**Stuck on any of these steps? Tell me which one and I'll walk through it with you.** I can also list your recent deployments and pick the right \"last good\" one for you — just say the word.\n\n**API equivalent:** [api-actions.md#rollback](api-actions.md#rollback)\n",
|
|
15
|
+
},
|
|
16
|
+
];
|
|
17
|
+
//# sourceMappingURL=recipes.generated.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recipes.generated.js","sourceRoot":"","sources":["../../src/knowledge/recipes.generated.ts"],"names":[],"mappings":"AAAA,mEAAmE;AACnE,+CAA+C;AAO/C,MAAM,CAAC,MAAM,YAAY,GAAiB;IACxC;QACE,IAAI,EAAE,UAAU;QAChB,OAAO,EAAE,myIAAmyI;KAC7yI;IACD;QACE,IAAI,EAAE,gBAAgB;QACtB,OAAO,EAAE,izXAAizX;KAC3zX;IACD;QACE,IAAI,EAAE,gBAAgB;QACtB,OAAO,EAAE,mgiBAAmgiB;KAC7giB;CACF,CAAC"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
export function renderDryRun(dryRun) {
|
|
2
|
+
const lines = [
|
|
3
|
+
`DRY RUN — \`${dryRun.tool}\` (${dryRun.tier})`,
|
|
4
|
+
"",
|
|
5
|
+
"Impact:",
|
|
6
|
+
...Object.entries(dryRun.willDo).map(([k, v]) => ` • ${k}: ${formatValue(v)}`),
|
|
7
|
+
"",
|
|
8
|
+
"To proceed, call this tool again with:",
|
|
9
|
+
` ${JSON.stringify(dryRun.nextCall)}`,
|
|
10
|
+
];
|
|
11
|
+
return lines.join("\n");
|
|
12
|
+
}
|
|
13
|
+
export function renderProjectSummary(project) {
|
|
14
|
+
const lines = [
|
|
15
|
+
`Project: ${project.name} (id: ${project.id})`,
|
|
16
|
+
` Workspace: ${project.organization_name ?? project.organization_id}`,
|
|
17
|
+
` Repo: ${project.github_owner}/${project.github_repo} @ ${project.branch}`,
|
|
18
|
+
` Framework: ${project.framework} (${project.package_manager}, node ${project.node_version})`,
|
|
19
|
+
` Status: ${project.status}`,
|
|
20
|
+
` Resource: ${project.resource_tier}`,
|
|
21
|
+
` Created: ${project.created_at}`,
|
|
22
|
+
];
|
|
23
|
+
if (project.latest_deployment_id) {
|
|
24
|
+
lines.push(` Latest: ${project.latest_deployment_environment ?? "?"} · ${project.latest_deployment_status ?? "?"} (${project.latest_deployment_id})`);
|
|
25
|
+
}
|
|
26
|
+
return lines.join("\n");
|
|
27
|
+
}
|
|
28
|
+
export function renderDeploymentSummary(deployment) {
|
|
29
|
+
return [
|
|
30
|
+
`Deployment ${deployment.id}`,
|
|
31
|
+
` Project: ${deployment.project_id}`,
|
|
32
|
+
` Environment: ${deployment.environment}`,
|
|
33
|
+
` Branch: ${deployment.branch}`,
|
|
34
|
+
` Status: ${deployment.status}`,
|
|
35
|
+
` Commit: ${deployment.commit_sha.slice(0, 8)} — ${truncate(deployment.commit_message, 80)}`,
|
|
36
|
+
` Triggered: ${deployment.trigger_source} · ${deployment.triggered_by_username || deployment.triggered_by}`,
|
|
37
|
+
` Created: ${deployment.created_at}${deployment.finished_at ? ` → finished ${deployment.finished_at}` : ""}`,
|
|
38
|
+
deployment.error_message ? ` Error: ${deployment.error_message}` : "",
|
|
39
|
+
]
|
|
40
|
+
.filter(Boolean)
|
|
41
|
+
.join("\n");
|
|
42
|
+
}
|
|
43
|
+
export function renderDomainsList(domains) {
|
|
44
|
+
if (domains.length === 0)
|
|
45
|
+
return "(no domains)";
|
|
46
|
+
return domains
|
|
47
|
+
.map((d) => {
|
|
48
|
+
const flags = [
|
|
49
|
+
d.is_primary ? "primary" : null,
|
|
50
|
+
d.is_auto ? "auto" : null,
|
|
51
|
+
d.dns_verified ? "dns-ok" : "dns-pending",
|
|
52
|
+
`ssl:${d.ssl_status}`,
|
|
53
|
+
]
|
|
54
|
+
.filter(Boolean)
|
|
55
|
+
.join(" ");
|
|
56
|
+
return ` • ${d.domain} [${d.environment}] ${flags}`;
|
|
57
|
+
})
|
|
58
|
+
.join("\n");
|
|
59
|
+
}
|
|
60
|
+
export function renderVolumesList(volumes) {
|
|
61
|
+
if (volumes.length === 0)
|
|
62
|
+
return "(no volumes)";
|
|
63
|
+
return volumes
|
|
64
|
+
.map((v) => {
|
|
65
|
+
const size = v.exists ? formatBytes(v.size_bytes) : "(missing)";
|
|
66
|
+
const inUse = v.in_use ? "in-use" : "idle";
|
|
67
|
+
return ` • ${v.name} [${v.environment}] ${size} ${inUse} mount=${v.mount_path}`;
|
|
68
|
+
})
|
|
69
|
+
.join("\n");
|
|
70
|
+
}
|
|
71
|
+
export function renderProtection(status) {
|
|
72
|
+
return [
|
|
73
|
+
` Preview: ${status.preview_enabled ? "enabled" : "disabled"}`,
|
|
74
|
+
` Production: ${status.production_enabled ? "enabled" : "disabled"}`,
|
|
75
|
+
].join("\n");
|
|
76
|
+
}
|
|
77
|
+
function truncate(s, n) {
|
|
78
|
+
if (s.length <= n)
|
|
79
|
+
return s;
|
|
80
|
+
return `${s.slice(0, n - 1)}…`;
|
|
81
|
+
}
|
|
82
|
+
function formatValue(v) {
|
|
83
|
+
if (typeof v === "string")
|
|
84
|
+
return v;
|
|
85
|
+
return JSON.stringify(v);
|
|
86
|
+
}
|
|
87
|
+
function formatBytes(n) {
|
|
88
|
+
if (n < 1024)
|
|
89
|
+
return `${n} B`;
|
|
90
|
+
if (n < 1024 * 1024)
|
|
91
|
+
return `${(n / 1024).toFixed(1)} KiB`;
|
|
92
|
+
if (n < 1024 * 1024 * 1024)
|
|
93
|
+
return `${(n / 1024 / 1024).toFixed(1)} MiB`;
|
|
94
|
+
return `${(n / 1024 / 1024 / 1024).toFixed(2)} GiB`;
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=format.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format.js","sourceRoot":"","sources":["../../src/lib/format.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,YAAY,CAAC,MAA0G;IACrI,MAAM,KAAK,GAAG;QACZ,eAAe,MAAM,CAAC,IAAI,OAAO,MAAM,CAAC,IAAI,GAAG;QAC/C,EAAE;QACF,SAAS;QACT,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/E,EAAE;QACF,wCAAwC;QACxC,KAAK,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE;KACvC,CAAC;IACF,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,OAAgB;IACnD,MAAM,KAAK,GAAG;QACZ,YAAY,OAAO,CAAC,IAAI,SAAS,OAAO,CAAC,EAAE,GAAG;QAC9C,mBAAmB,OAAO,CAAC,iBAAiB,IAAI,OAAO,CAAC,eAAe,EAAE;QACzE,mBAAmB,OAAO,CAAC,YAAY,IAAI,OAAO,CAAC,WAAW,MAAM,OAAO,CAAC,MAAM,EAAE;QACpF,mBAAmB,OAAO,CAAC,SAAS,KAAK,OAAO,CAAC,eAAe,UAAU,OAAO,CAAC,YAAY,GAAG;QACjG,mBAAmB,OAAO,CAAC,MAAM,EAAE;QACnC,mBAAmB,OAAO,CAAC,aAAa,EAAE;QAC1C,mBAAmB,OAAO,CAAC,UAAU,EAAE;KACxC,CAAC;IACF,IAAI,OAAO,CAAC,oBAAoB,EAAE,CAAC;QACjC,KAAK,CAAC,IAAI,CACR,mBAAmB,OAAO,CAAC,6BAA6B,IAAI,GAAG,MAAM,OAAO,CAAC,wBAAwB,IAAI,GAAG,KAAK,OAAO,CAAC,oBAAoB,GAAG,CACjJ,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,UAAsB;IAC5D,OAAO;QACL,cAAc,UAAU,CAAC,EAAE,EAAE;QAC7B,kBAAkB,UAAU,CAAC,UAAU,EAAE;QACzC,kBAAkB,UAAU,CAAC,WAAW,EAAE;QAC1C,kBAAkB,UAAU,CAAC,MAAM,EAAE;QACrC,kBAAkB,UAAU,CAAC,MAAM,EAAE;QACrC,kBAAkB,UAAU,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,MAAM,QAAQ,CAAC,UAAU,CAAC,cAAc,EAAE,EAAE,CAAC,EAAE;QAClG,kBAAkB,UAAU,CAAC,cAAc,MAAM,UAAU,CAAC,qBAAqB,IAAI,UAAU,CAAC,YAAY,EAAE;QAC9G,kBAAkB,UAAU,CAAC,UAAU,GAAG,UAAU,CAAC,WAAW,CAAC,CAAC,CAAC,eAAe,UAAU,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE;QACjH,UAAU,CAAC,aAAa,CAAC,CAAC,CAAC,kBAAkB,UAAU,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE;KAC7E;SACE,MAAM,CAAC,OAAO,CAAC;SACf,IAAI,CAAC,IAAI,CAAC,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,OAAiB;IACjD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,cAAc,CAAC;IAChD,OAAO,OAAO;SACX,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QACT,MAAM,KAAK,GAAG;YACZ,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI;YAC/B,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI;YACzB,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,aAAa;YACzC,OAAO,CAAC,CAAC,UAAU,EAAE;SACtB;aACE,MAAM,CAAC,OAAO,CAAC;aACf,IAAI,CAAC,GAAG,CAAC,CAAC;QACb,OAAO,OAAO,CAAC,CAAC,MAAM,MAAM,CAAC,CAAC,WAAW,MAAM,KAAK,EAAE,CAAC;IACzD,CAAC,CAAC;SACD,IAAI,CAAC,IAAI,CAAC,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,OAAqB;IACrD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,cAAc,CAAC;IAChD,OAAO,OAAO;SACX,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QACT,MAAM,IAAI,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC;QAChE,MAAM,KAAK,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC;QAC3C,OAAO,OAAO,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC,WAAW,MAAM,IAAI,KAAK,KAAK,WAAW,CAAC,CAAC,UAAU,EAAE,CAAC;IACvF,CAAC,CAAC;SACD,IAAI,CAAC,IAAI,CAAC,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,MAAwB;IACvD,OAAO;QACL,iBAAiB,MAAM,CAAC,eAAe,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,EAAE;QAClE,iBAAiB,MAAM,CAAC,kBAAkB,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,EAAE;KACtE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED,SAAS,QAAQ,CAAC,CAAS,EAAE,CAAS;IACpC,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC;QAAE,OAAO,CAAC,CAAC;IAC5B,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC;AACjC,CAAC;AAED,SAAS,WAAW,CAAC,CAAU;IAC7B,IAAI,OAAO,CAAC,KAAK,QAAQ;QAAE,OAAO,CAAC,CAAC;IACpC,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;AAC3B,CAAC;AAED,SAAS,WAAW,CAAC,CAAS;IAC5B,IAAI,CAAC,GAAG,IAAI;QAAE,OAAO,GAAG,CAAC,IAAI,CAAC;IAC9B,IAAI,CAAC,GAAG,IAAI,GAAG,IAAI;QAAE,OAAO,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC;IAC3D,IAAI,CAAC,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI;QAAE,OAAO,GAAG,CAAC,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC;IACzE,OAAO,GAAG,CAAC,CAAC,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC;AACtD,CAAC"}
|
package/dist/lib/logs.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const ERROR_PATTERNS = [
|
|
2
|
+
/\berror\b/i,
|
|
3
|
+
/\bfailed\b/i,
|
|
4
|
+
/\bpanic\b/i,
|
|
5
|
+
/\bcannot\b/i,
|
|
6
|
+
/\bENOENT\b/,
|
|
7
|
+
/\bcommand not found\b/i,
|
|
8
|
+
/\bexit (status |code )?[1-9]/i,
|
|
9
|
+
];
|
|
10
|
+
const DEFAULTS = { maxLines: 200, maxBytes: 8 * 1024 };
|
|
11
|
+
export function formatLogs(logs, opts = {}) {
|
|
12
|
+
const maxLines = opts.maxLines ?? DEFAULTS.maxLines;
|
|
13
|
+
const maxBytes = opts.maxBytes ?? DEFAULTS.maxBytes;
|
|
14
|
+
const anchor = opts.anchorErrors !== false;
|
|
15
|
+
const total = logs.length;
|
|
16
|
+
if (total === 0) {
|
|
17
|
+
return { text: "(no log lines yet)", totalLines: 0, returnedLines: 0, truncated: false, errorAnchors: [] };
|
|
18
|
+
}
|
|
19
|
+
const slice = opts.tail !== false ? logs.slice(-maxLines) : logs.slice(0, maxLines);
|
|
20
|
+
const truncatedLines = slice.length < total;
|
|
21
|
+
const errorAnchors = [];
|
|
22
|
+
const rendered = [];
|
|
23
|
+
let byteCount = 0;
|
|
24
|
+
for (const log of slice) {
|
|
25
|
+
const prefix = log.stream === "stderr" ? "! " : " ";
|
|
26
|
+
const isError = anchor && ERROR_PATTERNS.some((rx) => rx.test(log.content));
|
|
27
|
+
const marker = isError ? "↑ " : " ";
|
|
28
|
+
const line = `${prefix}${marker}${log.line_number.toString().padStart(4, " ")} ${log.content}`;
|
|
29
|
+
const lineBytes = Buffer.byteLength(line, "utf8") + 1;
|
|
30
|
+
if (byteCount + lineBytes > maxBytes)
|
|
31
|
+
break;
|
|
32
|
+
rendered.push(line);
|
|
33
|
+
byteCount += lineBytes;
|
|
34
|
+
if (isError)
|
|
35
|
+
errorAnchors.push(log.line_number);
|
|
36
|
+
}
|
|
37
|
+
const returnedLines = rendered.length;
|
|
38
|
+
const truncated = truncatedLines || returnedLines < slice.length;
|
|
39
|
+
const text = rendered.join("\n");
|
|
40
|
+
const lastSeen = slice[slice.length - 1]?.line_number ?? 0;
|
|
41
|
+
const hint = truncated
|
|
42
|
+
? `Showing ${returnedLines} of ${total} lines. Call get_deploy_logs with { since_line: ${lastSeen} } for more.`
|
|
43
|
+
: undefined;
|
|
44
|
+
return { text, totalLines: total, returnedLines, truncated, errorAnchors, hint };
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=logs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logs.js","sourceRoot":"","sources":["../../src/lib/logs.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/poll.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export async function pollUntil(probe, predicate, opts) {
|
|
2
|
+
const start = Date.now();
|
|
3
|
+
if (opts.initialDelayMs && opts.initialDelayMs > 0) {
|
|
4
|
+
await sleep(opts.initialDelayMs);
|
|
5
|
+
}
|
|
6
|
+
let last = await probe();
|
|
7
|
+
if (predicate(last))
|
|
8
|
+
return { done: true, value: last, elapsedMs: Date.now() - start };
|
|
9
|
+
while (Date.now() - start < opts.timeoutMs) {
|
|
10
|
+
await sleep(opts.intervalMs);
|
|
11
|
+
last = await probe();
|
|
12
|
+
if (predicate(last))
|
|
13
|
+
return { done: true, value: last, elapsedMs: Date.now() - start };
|
|
14
|
+
}
|
|
15
|
+
return { done: false, value: last, elapsedMs: Date.now() - start };
|
|
16
|
+
}
|
|
17
|
+
export function sleep(ms) {
|
|
18
|
+
return new Promise((res) => setTimeout(res, ms));
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=poll.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"poll.js","sourceRoot":"","sources":["../../src/lib/poll.ts"],"names":[],"mappings":"AAOA,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,KAAuB,EACvB,SAAgC,EAChC,IAAsB;IAEtB,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACzB,IAAI,IAAI,CAAC,cAAc,IAAI,IAAI,CAAC,cAAc,GAAG,CAAC,EAAE,CAAC;QACnD,MAAM,KAAK,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;IACnC,CAAC;IACD,IAAI,IAAI,GAAM,MAAM,KAAK,EAAE,CAAC;IAC5B,IAAI,SAAS,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,EAAE,CAAC;IACvF,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QAC3C,MAAM,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC7B,IAAI,GAAG,MAAM,KAAK,EAAE,CAAC;QACrB,IAAI,SAAS,CAAC,IAAI,CAAC;YAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,EAAE,CAAC;IACzF,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,EAAE,CAAC;AACrE,CAAC;AAED,MAAM,UAAU,KAAK,CAAC,EAAU;IAC9B,OAAO,IAAI,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC;AACnD,CAAC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export function checkSafety(ctx, args) {
|
|
2
|
+
if (ctx.tier === "read" || ctx.tier === "mutating") {
|
|
3
|
+
return { proceed: true };
|
|
4
|
+
}
|
|
5
|
+
const required = { confirm: true };
|
|
6
|
+
if (ctx.tier === "destructive_production") {
|
|
7
|
+
if (!ctx.expectedName) {
|
|
8
|
+
throw new Error(`safety: destructive_production tier requires expectedName for tool ${ctx.toolName}`);
|
|
9
|
+
}
|
|
10
|
+
required.confirm_name = ctx.expectedName;
|
|
11
|
+
}
|
|
12
|
+
if (args.confirm !== true) {
|
|
13
|
+
return {
|
|
14
|
+
proceed: false,
|
|
15
|
+
dryRun: {
|
|
16
|
+
tool: ctx.toolName,
|
|
17
|
+
tier: ctx.tier,
|
|
18
|
+
willDo: ctx.impact,
|
|
19
|
+
nextCall: required,
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
if (ctx.tier === "destructive_production") {
|
|
24
|
+
if (args.confirm_name !== ctx.expectedName) {
|
|
25
|
+
return {
|
|
26
|
+
proceed: false,
|
|
27
|
+
dryRun: {
|
|
28
|
+
tool: ctx.toolName,
|
|
29
|
+
tier: ctx.tier,
|
|
30
|
+
willDo: ctx.impact,
|
|
31
|
+
nextCall: required,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return { proceed: true };
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=safety.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"safety.js","sourceRoot":"","sources":["../../src/lib/safety.ts"],"names":[],"mappings":"AAoBA,MAAM,UAAU,WAAW,CAAC,GAAkB,EAAE,IAAgB;IAC9D,IAAI,GAAG,CAAC,IAAI,KAAK,MAAM,IAAI,GAAG,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;QACnD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAC3B,CAAC;IAED,MAAM,QAAQ,GAA4B,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAC5D,IAAI,GAAG,CAAC,IAAI,KAAK,wBAAwB,EAAE,CAAC;QAC1C,IAAI,CAAC,GAAG,CAAC,YAAY,EAAE,CAAC;YACtB,MAAM,IAAI,KAAK,CAAC,sEAAsE,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC;QACxG,CAAC;QACD,QAAQ,CAAC,YAAY,GAAG,GAAG,CAAC,YAAY,CAAC;IAC3C,CAAC;IAED,IAAI,IAAI,CAAC,OAAO,KAAK,IAAI,EAAE,CAAC;QAC1B,OAAO;YACL,OAAO,EAAE,KAAK;YACd,MAAM,EAAE;gBACN,IAAI,EAAE,GAAG,CAAC,QAAQ;gBAClB,IAAI,EAAE,GAAG,CAAC,IAAI;gBACd,MAAM,EAAE,GAAG,CAAC,MAAM;gBAClB,QAAQ,EAAE,QAAQ;aACnB;SACF,CAAC;IACJ,CAAC;IAED,IAAI,GAAG,CAAC,IAAI,KAAK,wBAAwB,EAAE,CAAC;QAC1C,IAAI,IAAI,CAAC,YAAY,KAAK,GAAG,CAAC,YAAY,EAAE,CAAC;YAC3C,OAAO;gBACL,OAAO,EAAE,KAAK;gBACd,MAAM,EAAE;oBACN,IAAI,EAAE,GAAG,CAAC,QAAQ;oBAClB,IAAI,EAAE,GAAG,CAAC,IAAI;oBACd,MAAM,EAAE,GAAG,CAAC,MAAM;oBAClB,QAAQ,EAAE,QAAQ;iBACnB;aACF,CAAC;QACJ,CAAC;IACH,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AAC3B,CAAC"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { readBinding } from "../config/binding.js";
|
|
3
|
+
import { readCache, writeCache, isCacheFresh } from "../config/cache.js";
|
|
4
|
+
export class ResolveError extends Error {
|
|
5
|
+
hint;
|
|
6
|
+
constructor(message, hint) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.hint = hint;
|
|
9
|
+
this.name = "ResolveError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export async function resolveProject(ctx, input = {}) {
|
|
13
|
+
// 1. explicit id
|
|
14
|
+
if (input.project_id) {
|
|
15
|
+
const project = await ctx.client.request(`/projects/${encodeURIComponent(input.project_id)}`);
|
|
16
|
+
return { project, source: "id" };
|
|
17
|
+
}
|
|
18
|
+
// gather projects once
|
|
19
|
+
const projects = await fetchProjects(ctx);
|
|
20
|
+
// 2. explicit slug
|
|
21
|
+
if (input.project) {
|
|
22
|
+
const match = matchProjectByName(projects, input.project, input.workspace);
|
|
23
|
+
if (match)
|
|
24
|
+
return { project: match, source: "slug" };
|
|
25
|
+
throw new ResolveError(`No project named '${input.project}' is visible to this token${input.workspace ? ` in workspace '${input.workspace}'` : ""}.`, "Call `list_projects` to see what this token can reach.");
|
|
26
|
+
}
|
|
27
|
+
// 3. binding file
|
|
28
|
+
const binding = readBinding(ctx.stateDir);
|
|
29
|
+
if (binding?.project) {
|
|
30
|
+
const match = matchProjectByName(projects, binding.project, binding.workspace);
|
|
31
|
+
if (match)
|
|
32
|
+
return { project: match, source: "binding" };
|
|
33
|
+
}
|
|
34
|
+
// 4. git remote → cached project lookup
|
|
35
|
+
const remote = readGitRemote(ctx.cwd);
|
|
36
|
+
if (remote) {
|
|
37
|
+
const match = projects.find((p) => p.github_owner.toLowerCase() === remote.owner.toLowerCase() && p.github_repo.toLowerCase() === remote.repo.toLowerCase());
|
|
38
|
+
if (match)
|
|
39
|
+
return { project: match, source: "git_remote" };
|
|
40
|
+
}
|
|
41
|
+
// 5. single project visible
|
|
42
|
+
if (projects.length === 1) {
|
|
43
|
+
return { project: projects[0], source: "single_workspace" };
|
|
44
|
+
}
|
|
45
|
+
throw new ResolveError("Could not figure out which Deployik project you mean.", "Pass `project: <slug>` or `project_id: <id>`, or run `init_in_repo` to bind this folder to a project.");
|
|
46
|
+
}
|
|
47
|
+
export async function fetchProjects(ctx) {
|
|
48
|
+
const projects = await ctx.client.request(`/projects`);
|
|
49
|
+
await refreshCache(ctx, projects);
|
|
50
|
+
return projects;
|
|
51
|
+
}
|
|
52
|
+
export async function refreshCache(ctx, projects) {
|
|
53
|
+
// workspaces fetched lazily — only when cache is stale or missing
|
|
54
|
+
let workspaces = [];
|
|
55
|
+
const existing = readCache(ctx.stateDir);
|
|
56
|
+
if (!existing || !isCacheFresh(existing)) {
|
|
57
|
+
try {
|
|
58
|
+
workspaces = await ctx.client.request(`/organizations`);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
workspaces = existing?.workspaces.map((w) => ({
|
|
62
|
+
id: w.id,
|
|
63
|
+
slug: w.slug,
|
|
64
|
+
name: w.name,
|
|
65
|
+
is_personal: false,
|
|
66
|
+
membership_role: "member",
|
|
67
|
+
project_count: 0,
|
|
68
|
+
created_at: "",
|
|
69
|
+
updated_at: "",
|
|
70
|
+
})) ?? [];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
workspaces = existing.workspaces.map((w) => ({
|
|
75
|
+
id: w.id,
|
|
76
|
+
slug: w.slug,
|
|
77
|
+
name: w.name,
|
|
78
|
+
is_personal: false,
|
|
79
|
+
membership_role: "member",
|
|
80
|
+
project_count: 0,
|
|
81
|
+
created_at: "",
|
|
82
|
+
updated_at: "",
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
const cached = projects.map((p) => ({
|
|
86
|
+
id: p.id,
|
|
87
|
+
name: p.name,
|
|
88
|
+
workspace: p.organization_name ?? p.organization_id,
|
|
89
|
+
github_owner: p.github_owner,
|
|
90
|
+
github_repo: p.github_repo,
|
|
91
|
+
}));
|
|
92
|
+
writeCache(ctx.stateDir, {
|
|
93
|
+
projects: cached,
|
|
94
|
+
workspaces: workspaces.map((w) => ({ id: w.id, slug: w.slug, name: w.name })),
|
|
95
|
+
platform: existing?.platform,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
function matchProjectByName(projects, slug, workspace) {
|
|
99
|
+
const slugLower = slug.toLowerCase();
|
|
100
|
+
const matches = projects.filter((p) => p.name.toLowerCase() === slugLower);
|
|
101
|
+
if (matches.length === 0)
|
|
102
|
+
return undefined;
|
|
103
|
+
if (matches.length === 1)
|
|
104
|
+
return matches[0];
|
|
105
|
+
if (!workspace)
|
|
106
|
+
return matches[0];
|
|
107
|
+
const ws = workspace.toLowerCase();
|
|
108
|
+
const refined = matches.find((p) => (p.organization_name ?? "").toLowerCase() === ws || p.organization_id === workspace);
|
|
109
|
+
return refined ?? matches[0];
|
|
110
|
+
}
|
|
111
|
+
function readGitRemote(cwd) {
|
|
112
|
+
try {
|
|
113
|
+
const url = execSync("git remote get-url origin", { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
114
|
+
return parseGithubUrl(url);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function parseGithubUrl(url) {
|
|
121
|
+
// git@github.com:owner/repo.git | https://github.com/owner/repo(.git)?
|
|
122
|
+
const ssh = url.match(/^git@github\.com:([^\/]+)\/([^\/]+?)(?:\.git)?$/);
|
|
123
|
+
if (ssh)
|
|
124
|
+
return { owner: ssh[1], repo: ssh[2] };
|
|
125
|
+
const https = url.match(/^https?:\/\/github\.com\/([^\/]+)\/([^\/]+?)(?:\.git)?$/);
|
|
126
|
+
if (https)
|
|
127
|
+
return { owner: https[1], repo: https[2] };
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=project.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project.js","sourceRoot":"","sources":["../../src/resolve/project.ts"],"names":[],"mappings":"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"}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { DeployikClient } from "./client/http.js";
|
|
3
|
+
import { loadEnv } from "./config/env.js";
|
|
4
|
+
import { registerKnowledgePrompts } from "./knowledge/prompts.js";
|
|
5
|
+
import { registerAllTools } from "./tools/index.js";
|
|
6
|
+
export function buildServer(env = process.env, cwd = process.cwd()) {
|
|
7
|
+
const cfg = loadEnv(env, cwd);
|
|
8
|
+
const client = new DeployikClient({
|
|
9
|
+
baseUrl: cfg.baseUrl,
|
|
10
|
+
token: cfg.token,
|
|
11
|
+
timeoutMs: cfg.timeoutMs,
|
|
12
|
+
userAgent: "deployik-mcp/0.1.0",
|
|
13
|
+
});
|
|
14
|
+
const server = new McpServer({
|
|
15
|
+
name: "deployik",
|
|
16
|
+
version: "0.1.0",
|
|
17
|
+
}, {
|
|
18
|
+
capabilities: {
|
|
19
|
+
tools: {},
|
|
20
|
+
prompts: {},
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
const ctx = {
|
|
24
|
+
client,
|
|
25
|
+
stateDir: cfg.stateDir,
|
|
26
|
+
cwd: cfg.cwd,
|
|
27
|
+
};
|
|
28
|
+
registerKnowledgePrompts(server);
|
|
29
|
+
registerAllTools(server, ctx);
|
|
30
|
+
return { server, ctx };
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAC1C,OAAO,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAQpD,MAAM,UAAU,WAAW,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,MAAc,OAAO,CAAC,GAAG,EAAE;IACxE,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IAC9B,MAAM,MAAM,GAAG,IAAI,cAAc,CAAC;QAChC,OAAO,EAAE,GAAG,CAAC,OAAO;QACpB,KAAK,EAAE,GAAG,CAAC,KAAK;QAChB,SAAS,EAAE,GAAG,CAAC,SAAS;QACxB,SAAS,EAAE,oBAAoB;KAChC,CAAC,CAAC;IAEH,MAAM,MAAM,GAAG,IAAI,SAAS,CAC1B;QACE,IAAI,EAAE,UAAU;QAChB,OAAO,EAAE,OAAO;KACjB,EACD;QACE,YAAY,EAAE;YACZ,KAAK,EAAE,EAAE;YACT,OAAO,EAAE,EAAE;SACZ;KACF,CACF,CAAC;IAEF,MAAM,GAAG,GAAgB;QACvB,MAAM;QACN,QAAQ,EAAE,GAAG,CAAC,QAAQ;QACtB,GAAG,EAAE,GAAG,CAAC,GAAG;KACb,CAAC;IAEF,wBAAwB,CAAC,MAAM,CAAC,CAAC;IACjC,gBAAgB,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAE9B,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;AACzB,CAAC"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { ApiError, asString } from "../client/errors.js";
|
|
2
|
+
import { appendAudit, redact } from "../config/audit.js";
|
|
3
|
+
export function registerTool(server, ctx, def) {
|
|
4
|
+
const cb = async (rawArgs) => {
|
|
5
|
+
const args = (rawArgs ?? {});
|
|
6
|
+
const start = Date.now();
|
|
7
|
+
try {
|
|
8
|
+
const result = await def.handler(args, ctx);
|
|
9
|
+
if (def.audit) {
|
|
10
|
+
const entry = {
|
|
11
|
+
ts: new Date().toISOString(),
|
|
12
|
+
tool: def.name,
|
|
13
|
+
args: redact(args),
|
|
14
|
+
durationMs: Date.now() - start,
|
|
15
|
+
outcome: "ok",
|
|
16
|
+
};
|
|
17
|
+
appendAudit(ctx.stateDir, entry);
|
|
18
|
+
}
|
|
19
|
+
const out = {
|
|
20
|
+
content: [{ type: "text", text: result.text }],
|
|
21
|
+
};
|
|
22
|
+
if (result.data !== undefined) {
|
|
23
|
+
out.structuredContent = result.data;
|
|
24
|
+
}
|
|
25
|
+
if (result.isError) {
|
|
26
|
+
out.isError = true;
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
const message = formatError(err);
|
|
32
|
+
if (def.audit) {
|
|
33
|
+
appendAudit(ctx.stateDir, {
|
|
34
|
+
ts: new Date().toISOString(),
|
|
35
|
+
tool: def.name,
|
|
36
|
+
args: redact(args),
|
|
37
|
+
httpStatus: err instanceof ApiError ? err.status : undefined,
|
|
38
|
+
durationMs: Date.now() - start,
|
|
39
|
+
outcome: "error",
|
|
40
|
+
error: asString(err),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
content: [{ type: "text", text: message }],
|
|
45
|
+
isError: true,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const config = {
|
|
50
|
+
description: def.description,
|
|
51
|
+
};
|
|
52
|
+
if (def.annotations?.title)
|
|
53
|
+
config.title = def.annotations.title;
|
|
54
|
+
if (def.inputSchema)
|
|
55
|
+
config.inputSchema = def.inputSchema;
|
|
56
|
+
const annotations = { openWorldHint: true, ...(def.annotations ?? {}) };
|
|
57
|
+
delete annotations.title;
|
|
58
|
+
config.annotations = annotations;
|
|
59
|
+
// SDK overload variance: cast through unknown.
|
|
60
|
+
server.registerTool(def.name, config, cb);
|
|
61
|
+
}
|
|
62
|
+
export function formatError(err) {
|
|
63
|
+
if (err instanceof ApiError) {
|
|
64
|
+
const lines = [`Error ${err.status || ""} calling ${err.endpoint}: ${err.message}`.trim()];
|
|
65
|
+
if (err.hint)
|
|
66
|
+
lines.push(`Hint: ${err.hint}`);
|
|
67
|
+
return lines.join("\n");
|
|
68
|
+
}
|
|
69
|
+
if (err instanceof Error)
|
|
70
|
+
return err.message;
|
|
71
|
+
return String(err);
|
|
72
|
+
}
|
|
73
|
+
export function jsonBlock(label, value) {
|
|
74
|
+
return `${label}:\n${JSON.stringify(value, null, 2)}`;
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=_helpers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_helpers.js","sourceRoot":"","sources":["../../src/tools/_helpers.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { registerTool } from "./_helpers.js";
|
|
3
|
+
import { resolveProject } from "../resolve/project.js";
|
|
4
|
+
const ENV = z.enum(["all", "preview", "production"]);
|
|
5
|
+
const RANGE = z.enum(["1h", "24h", "7d", "30d"]);
|
|
6
|
+
export function registerAnalyticsTools(server, ctx) {
|
|
7
|
+
registerTool(server, ctx, {
|
|
8
|
+
name: "get_project_analytics",
|
|
9
|
+
description: "Get the combined Umami audience + Loki runtime analytics payload for a project.",
|
|
10
|
+
inputSchema: {
|
|
11
|
+
project_id: z.string().optional(),
|
|
12
|
+
project: z.string().optional(),
|
|
13
|
+
environment: ENV.default("all"),
|
|
14
|
+
range: RANGE.default("24h"),
|
|
15
|
+
timezone: z.string().default("UTC"),
|
|
16
|
+
},
|
|
17
|
+
annotations: { readOnlyHint: true },
|
|
18
|
+
handler: async (args) => {
|
|
19
|
+
const { project } = await resolveProject(ctx, args);
|
|
20
|
+
const payload = await ctx.client.request(`/projects/${project.id}/analytics`, {
|
|
21
|
+
query: { environment: args.environment, range: args.range, timezone: args.timezone },
|
|
22
|
+
});
|
|
23
|
+
return { text: JSON.stringify(payload, null, 2), data: payload };
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
registerTool(server, ctx, {
|
|
27
|
+
name: "verify_project_analytics",
|
|
28
|
+
description: "Force a refresh of analytics verification status (re-checks Umami install signal).",
|
|
29
|
+
inputSchema: {
|
|
30
|
+
project_id: z.string().optional(),
|
|
31
|
+
project: z.string().optional(),
|
|
32
|
+
environment: ENV.default("all"),
|
|
33
|
+
range: RANGE.default("24h"),
|
|
34
|
+
timezone: z.string().default("UTC"),
|
|
35
|
+
},
|
|
36
|
+
handler: async (args) => {
|
|
37
|
+
const { project } = await resolveProject(ctx, args);
|
|
38
|
+
const payload = await ctx.client.request(`/projects/${project.id}/analytics/verify`, {
|
|
39
|
+
method: "POST",
|
|
40
|
+
query: { environment: args.environment, range: args.range, timezone: args.timezone },
|
|
41
|
+
});
|
|
42
|
+
return { text: JSON.stringify(payload, null, 2), data: payload };
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=analytics.js.map
|