@awesomate/hosting-mcp 0.7.3 → 0.8.2

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/dist/index.js CHANGED
@@ -40179,6 +40179,130 @@ server.registerTool(
40179
40179
  }
40180
40180
  }
40181
40181
  );
40182
+ readTool(
40183
+ "awesomate_app_context",
40184
+ "Call FIRST before any app-builder work. Returns the connected account's plan, whether the app builder is unlocked (capability.appBuilder \u2014 Support Plus+), whether they have a cPanel hosting account yet, their existing apps, firstRun, and capability-aware next-step suggestions to offer the user. If appBuilder is false, relay the upgrade suggestion rather than attempting to build.",
40185
+ "/api/my-apps/context"
40186
+ );
40187
+ readTool(
40188
+ "awesomate_app_list",
40189
+ "List the client\u2019s apps (id, slug, kind, status, primary_domain, github repo). Use to find an appId before get/deploy.",
40190
+ "/api/my-apps/apps"
40191
+ );
40192
+ server.registerTool(
40193
+ "awesomate_app_get",
40194
+ {
40195
+ description: "Get one app plus its environments (subdomains, ports, db engine/name, deploy + health state). Poll this after awesomate_app_create \u2014 status goes provisioning \u2192 active | failed (read provision_error on failure).",
40196
+ inputSchema: { appId: external_exports.number().int().positive().describe("The app id from awesomate_app_list / _create") }
40197
+ },
40198
+ async ({ appId }) => {
40199
+ try {
40200
+ return textResult(await hubGet(requireConfig(), `/api/my-apps/apps/${appId}`));
40201
+ } catch (err) {
40202
+ return errorResult(err);
40203
+ }
40204
+ }
40205
+ );
40206
+ server.registerTool(
40207
+ "awesomate_app_create",
40208
+ {
40209
+ description: "Provision a new app on the client's cPanel account. Choose the stack deliberately (see the awesomate-app-builder skill): kind 'static' = a single fast landing/lead page served straight from the docroot (no DB, no server process \u2014 pick this for brochure/landing/lead-capture); kind 'node' = a dynamic app with a backend + database (logins, custom logic, an API). For node, dbEngine 'postgres' suits relational/JSON/AI-heavy data, 'mysql' is the simple default. Returns 202 immediately with an appId + subdomain(s); poll awesomate_app_get until status=active. Requires apps:write (Support Plus+) \u2014 a 403 means offer an upgrade. Get the user's confirmation on the stack + name before calling.",
40210
+ inputSchema: {
40211
+ appSlug: external_exports.string().regex(/^[a-z][a-z0-9]{1,15}$/).describe("2-16 chars, starts with a letter, lowercase alphanumeric \u2014 used for subdomains + db names"),
40212
+ kind: external_exports.enum(["node", "static"]).default("node").describe("'static' for a landing/lead page (no DB); 'node' for a dynamic app"),
40213
+ dbEngine: external_exports.enum(["mysql", "postgres"]).default("mysql").describe("node only \u2014 the database engine to provision"),
40214
+ template: external_exports.string().optional().describe("Starter template (defaults: node \u2192 'node-auth-sync', static \u2192 'static-landing')")
40215
+ }
40216
+ },
40217
+ async ({ appSlug, kind, dbEngine, template }) => {
40218
+ try {
40219
+ return textResult(
40220
+ await hubPost(requireConfig(), "/api/my-apps/apps", { appSlug, kind, dbEngine, template })
40221
+ );
40222
+ } catch (err) {
40223
+ return errorResult(err);
40224
+ }
40225
+ }
40226
+ );
40227
+ server.registerTool(
40228
+ "awesomate_app_deploy",
40229
+ {
40230
+ description: "Report how to deploy an app and its per-env targets. Node apps deploy via git push (dev/staging/main \u2192 GitHub Actions \u2192 cPanel); this returns the branch\u2192env map and subdomains. Use the awesomate-github skill to wire push-to-deploy the first time.",
40231
+ inputSchema: { appId: external_exports.number().int().positive().describe("The app id") }
40232
+ },
40233
+ async ({ appId }) => {
40234
+ try {
40235
+ return textResult(await hubPost(requireConfig(), `/api/my-apps/apps/${appId}/deploy`, {}));
40236
+ } catch (err) {
40237
+ return errorResult(err);
40238
+ }
40239
+ }
40240
+ );
40241
+ server.registerTool(
40242
+ "awesomate_app_set_env",
40243
+ {
40244
+ description: "Store an API key / secret for a Node app environment: it's encrypted in the hub AND injected into the app's .env (0600) then the app restarts. ALWAYS use this instead of putting a secret in code, a committed file, or leaving it in the chat. NEVER echo the value back \u2014 confirm with the key name only. If the user pastes a key in chat, store it here and tell them (per the awesomate-credentials skill) it should be rotated since it passed through the transcript. Node apps only (static sites have no server env).",
40245
+ inputSchema: {
40246
+ appId: external_exports.number().int().positive().describe("The app id"),
40247
+ key: external_exports.string().regex(/^[A-Z][A-Z0-9_]{0,63}$/).describe("Env var name, e.g. OPENAI_API_KEY"),
40248
+ value: external_exports.string().min(1).describe("The secret value (stored encrypted; never echoed)"),
40249
+ env: external_exports.enum(["dev", "staging", "prod"]).default("dev").describe("Which environment to set it on")
40250
+ }
40251
+ },
40252
+ async ({ appId, key, value, env }) => {
40253
+ try {
40254
+ return textResult(await hubPost(requireConfig(), `/api/my-apps/apps/${appId}/env`, { key, value, env }));
40255
+ } catch (err) {
40256
+ return errorResult(err);
40257
+ }
40258
+ }
40259
+ );
40260
+ server.registerTool(
40261
+ "awesomate_n8n_attach_to_app",
40262
+ {
40263
+ description: "Wire an app to call the user's own n8n as a backend. Give it the app + a webhook URL you already created/tested/promoted on their n8n (via awesomate_n8n_deploy \u2014 create_draft returns the webhook URL, and promote preserves it). It stores N8N_WEBHOOK_URL + a generated N8N_WEBHOOK_SECRET on the app (encrypted + injected) and RETURNS the secret so you can add a matching `X-Awesomate-Webhook-Secret` header check to the n8n workflow (so the webhook isn't world-callable). The app then calls it via src/lib/n8n.ts callWorkflow(). Node apps only. Only reach for this when the job needs an external app/credential or AI the user already has in n8n \u2014 not for pure in-app logic.",
40264
+ inputSchema: {
40265
+ appId: external_exports.number().int().positive().describe("The app id"),
40266
+ webhookUrl: external_exports.string().url().describe("The n8n workflow's production webhook URL (from awesomate_n8n_deploy)"),
40267
+ env: external_exports.enum(["dev", "staging", "prod"]).default("dev").describe("Which app environment to wire")
40268
+ }
40269
+ },
40270
+ async ({ appId, webhookUrl, env }) => {
40271
+ try {
40272
+ return textResult(await hubPost(requireConfig(), `/api/my-apps/apps/${appId}/attach-n8n`, { webhookUrl, env }));
40273
+ } catch (err) {
40274
+ return errorResult(err);
40275
+ }
40276
+ }
40277
+ );
40278
+ server.registerTool(
40279
+ "awesomate_app_health",
40280
+ {
40281
+ description: "Probe an app's environments live and report health. Node envs hit /api/ready (200 = deployed + DB up + migrations applied); static hits the root. An env that isn't deployed yet reports unreachable \u2014 expected, not a failure. Use after a deploy to confirm it came up, or when the user says something's down.",
40282
+ inputSchema: { appId: external_exports.number().int().positive().describe("The app id") }
40283
+ },
40284
+ async ({ appId }) => {
40285
+ try {
40286
+ return textResult(await hubPost(requireConfig(), `/api/my-apps/apps/${appId}/health`, {}));
40287
+ } catch (err) {
40288
+ return errorResult(err);
40289
+ }
40290
+ }
40291
+ );
40292
+ server.registerTool(
40293
+ "awesomate_n8n_provision_pg",
40294
+ {
40295
+ description: "Provision a NEW Postgres database on the user's own Awesomate cPanel hosting and wire it into their n8n as a ready-to-use `postgres` credential their workflows can select. This is a WRITE that creates real infrastructure \u2014 get explicit user approval first. Requires: their n8n builder access (Support Plus+, allow_client_cli_builds) AND the allow_pg_writes consent (403 consent_required \u2192 send them to settingsUrl); AND an Awesomate cPanel hosting account (409 hosting_required otherwise). Limited to 2/day. Returns the new credential id + name, the database/user, and the password ONCE (it lives in the n8n credential, never stored by Awesomate \u2014 tell the user to note or rotate it). After this, build the workflow selecting the returned Postgres credential.",
40296
+ inputSchema: {}
40297
+ },
40298
+ async () => {
40299
+ try {
40300
+ return textResult(await hubPost(requireConfig(), "/api/my-n8n/machine/pg-credentials", {}));
40301
+ } catch (err) {
40302
+ return errorResult(err);
40303
+ }
40304
+ }
40305
+ );
40182
40306
  async function main() {
40183
40307
  try {
40184
40308
  config2 = loadConfig();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@awesomate/hosting-mcp",
3
- "version": "0.7.3",
4
- "description": "Awesomate MCP server — lets Claude manage your Awesomate WordPress hosting, plan, limits, and n8n automations",
3
+ "version": "0.8.2",
4
+ "description": "Awesomate MCP server — lets Claude manage your Awesomate WordPress hosting, plan, limits, n8n automations, and build Node/static apps + databases",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
7
7
  "publishConfig": {
@@ -19,7 +19,7 @@
19
19
  "scripts": {
20
20
  "build": "esbuild src/index.ts --bundle --platform=node --target=node18 --format=esm --outfile=dist/index.js --banner:js='#!/usr/bin/env node' --external:node:*",
21
21
  "typecheck": "tsc --noEmit",
22
- "test": "esbuild src/config.ts --bundle --platform=node --target=node18 --format=esm --outfile=test/.build/config.mjs --external:node:* && node --test test/",
22
+ "test": "esbuild src/config.ts --bundle --platform=node --target=node18 --format=esm --outfile=test/.build/config.mjs --external:node:* && node --test test/*.test.mjs",
23
23
  "prepublishOnly": "npm run typecheck && npm run test && npm run build"
24
24
  },
25
25
  "dependencies": {
@@ -0,0 +1,104 @@
1
+ ---
2
+ name: awesomate-app-builder
3
+ description: Build real things on the user's Awesomate hosting from plain English — a website, a landing page, or a web app with a database. Use when the user says "get started", "what can you do", "build me an app", "make a website / landing page", "I need a form / signup / dashboard", "help", or mentions their Awesomate hosting and wants something built. The first-run greeter for non-technical users. Companion to awesomate-hosting, awesomate-credentials, awesomate-github and awesomate-seo — same connection, same PAT.
4
+ ---
5
+
6
+ # Awesomate App Builder — build real things from plain English
7
+
8
+ The user is likely **non-technical**. Your job: understand what they actually
9
+ want in plain words, pick the simplest stack that fits, and build + ship it on
10
+ their Awesomate hosting — explaining just enough, never jargon-dumping. Ask
11
+ short questions one or two at a time. Provisioning is consent-free (it's their
12
+ own hosting) but you should always confirm the **stack + name** before creating.
13
+
14
+ ## 0. First run (every session)
15
+
16
+ 1. Run `awesomate_whoami` — everything acts on whichever ACCOUNT this folder
17
+ resolves to. Every tool response is stamped `account: <slug>`; if it isn't
18
+ the account the user means, fix the pin/connection first (see the
19
+ awesomate-hosting skill's multi-account section).
20
+ 2. Call `awesomate_app_context` once and cache it. It returns:
21
+ - `capability.appBuilder` — if **false**, building is Support Plus+. Relay
22
+ the upgrade suggestion (don't attempt to build); reads still work.
23
+ - `hasCpanelAccount` — if **false**, they need hosting set up first (hand
24
+ off to the awesomate-hosting skill).
25
+ - `firstRun` + `suggestions[]` — on first run, greet warmly and offer the
26
+ suggestions as a short menu ("Here's what I can do for you… what would
27
+ you like?"). Don't dump all tools.
28
+ - `apps[]` — their existing apps; offer to continue one instead of starting
29
+ over.
30
+
31
+ ## 1. Decide the stack (do this before building)
32
+
33
+ Read [references/stack-decision.md](references/stack-decision.md) — it's the
34
+ decision brain. The short version, matched to what they say:
35
+
36
+ - **"I have a WordPress site" / blog / regular content / needs to rank on
37
+ Google** → **WordPress** (hand to the awesomate-hosting skill). Then use
38
+ **awesomate-seo** so it's findable.
39
+ - **Shop / sell products** → **WordPress + WooCommerce**.
40
+ - **Landing page / capture leads / one-pager / "just needs to look good and
41
+ be found"** → **static site** (`kind: 'static'`). Fastest possible load,
42
+ no database. This is the right default for marketing pages — don't reach for
43
+ a full app.
44
+ - **App with logins / custom logic / a dashboard / an API** → **Node app**
45
+ (`kind: 'node'`). Pick **postgres** when the data is relational / needs
46
+ concurrency / JSON / analytics; **mysql** for simple cases.
47
+
48
+ **Never default to WordPress.** Match the tech to the job; prefer the least
49
+ machinery that does it.
50
+
51
+ ### When to use their n8n instead of coding a backend
52
+ If the app needs to **send an email, look something up in their apps (Sheets,
53
+ CRM, a database), or use AI** — their **own n8n** is often the fastest path,
54
+ because their credentials are already connected there. Check the credential
55
+ inventory (`GET /api/my-n8n/machine/credentials`) and say e.g. *"you already
56
+ have Gmail connected in n8n — want the form to email you through that?"* Use
57
+ the **awesomate-n8n** skill to build the workflow, then store its webhook URL
58
+ in the app (see the awesomate-credentials skill) and call it from the app.
59
+ Don't reach for n8n for pure in-app logic with no external app/credential — a
60
+ webhook round-trip adds latency and a failure point for nothing.
61
+
62
+ ## 2. The build loop
63
+
64
+ Work in short phases, keeping the user in plain language. **Dev is the default
65
+ target; never touch prod without an explicit ask.**
66
+
67
+ 1. **Discovery** — one or two plain questions until you know the job + the
68
+ stack (§1). Don't over-interview.
69
+ 2. **Design** — say back, in a sentence or two, what you'll build and the stack
70
+ you recommend. Get a yes.
71
+ 3. **Confirmation gate** — confirm the **name** (2–16 lowercase letters/digits,
72
+ starts with a letter — becomes the subdomain + db names) and the **stack**,
73
+ then call `awesomate_app_create` (`appSlug`, `kind`, and for node `dbEngine`).
74
+ It returns **202** with an `appId` + subdomain(s); poll `awesomate_app_get`
75
+ until `status` is `active` (or `failed` — read `provision_error`, explain
76
+ plainly).
77
+ 4. **Implementation** — build the actual code/content locally against the
78
+ template. Handle **secrets** only via the **awesomate-credentials** skill
79
+ (encrypted + injected, never committed/echoed). Wire **version control**
80
+ from the first commit via **awesomate-github**. If it needs email/lookup/AI,
81
+ attach n8n (§"when to use n8n").
82
+ 5. **Testing** — deploy to **dev** (`git push` to `dev`), then
83
+ `awesomate_app_health` to confirm it came up (Node: `/api/ready`). Show the
84
+ user the live dev URL and let them try it. Iterate on dev until they're happy.
85
+ 6. **Wrap-up / promote** — only when the user approves, promote **dev → staging
86
+ → main** (merge + push per branch). `awesomate_app_deploy` reports the
87
+ branch→env map. Prod is `main`.
88
+
89
+ **Promote / rollback (git-based):** promoting is merging the tested branch
90
+ forward (`dev`→`staging`→`main`); rolling back is `git revert` of the bad
91
+ commit + push (redeploys the previous good state), or redeploy an earlier good
92
+ SHA. Always `awesomate_app_health` after a promote. Static sites ship their
93
+ files directly (no build step).
94
+
95
+ ## 3. Hard rules
96
+
97
+ - Non-technical user: explain in plain words, one or two short questions at a
98
+ time, no unexplained jargon. Recommend; don't quiz them on architecture.
99
+ - Confirm the stack + name before `awesomate_app_create`. Default the target
100
+ to **dev**; never push to prod without an explicit ask.
101
+ - A `403` on a write = plan gate (Support Plus+). Offer an upgrade, don't retry.
102
+ - Never invent that a capability exists — read `awesomate_app_context` first.
103
+ - Never print a secret or commit a `.env` (the credentials + github skills
104
+ enforce this).
@@ -0,0 +1,52 @@
1
+ # Stack decision — match the tech to the job
2
+
3
+ Bias toward whatever **loads fastest and is the least machinery** for the need.
4
+ Ask at most 2–3 plain questions, then recommend a stack in one sentence and
5
+ confirm before building. **Never default to WordPress.**
6
+
7
+ ## The map
8
+
9
+ | What the user is really trying to do | Stack | Why / how |
10
+ |---|---|---|
11
+ | "I already have a WordPress site" / a blog / publishes content regularly / **wants to rank on Google & AI** | **WordPress** | Best CMS for indexable content, sitemaps, schema. Hand to the awesomate-hosting skill to provision, then run **awesomate-seo**. |
12
+ | Sell products / online shop | **WordPress + WooCommerce** | Reuse the `woo-agent-storefront` (AI-shopping feeds + `llms.txt`) and `woo-catalog-perfection` skills. |
13
+ | A landing page / capture leads / a one-pager / "just needs to look good and be found" | **static site** (`kind: 'static'`) | Served straight from the cPanel docroot — no database, no server process, fastest load. Add SEO + a lead form (below). The right default for marketing pages. |
14
+ | A tool/app with **logins, custom logic, a dashboard, or an API** | **Node app** (`kind: 'node'`) | A real backend + database. |
15
+
16
+ ### Within a Node app: which database?
17
+ - **postgres** — relational data with relationships, concurrency, JSON columns,
18
+ or analytics/AI features. The modern default for a real app.
19
+ - **mysql** — simple/legacy needs, or when they already know MySQL.
20
+ - Provisioned automatically by `awesomate_app_create`; the app's `.env` gets
21
+ `DB_ENGINE`, `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASS`, and a
22
+ ready-to-use `DATABASE_URL`.
23
+
24
+ ## Questions that resolve the choice fast
25
+ 1. "Is this mainly a **page people read** (marketing, blog, shop) or a **tool
26
+ people use** (log in, do things, see their own data)?" → read → WP/static;
27
+ tool → Node.
28
+ 2. If read: "Do you need to **publish/edit content often** (blog, many pages)?"
29
+ → yes → WordPress; no → static.
30
+ 3. If tool: "Will people **sign in** or does it store data per user?" → yes →
31
+ Node + postgres.
32
+
33
+ ## Does it need a backend job? Prefer their n8n.
34
+ If the ask includes **email someone, look something up in their apps, save a
35
+ lead somewhere, or use AI**, that's usually a job for their **own n8n** — their
36
+ credentials are already connected there, so there's nothing new to set up.
37
+
38
+ - Check `GET /api/my-n8n/machine/credentials` (names/types only) and offer what
39
+ they already have: *"You've got Gmail + Google Sheets connected — want the
40
+ form to email you and add the lead to a sheet?"*
41
+ - Build the workflow with the **awesomate-n8n** skill (webhook trigger →
42
+ action), take its **webhook URL**, and store it in the app as
43
+ `N8N_WEBHOOK_URL` via the **awesomate-credentials** skill. The static
44
+ template's lead form and the Node template's `src/lib/n8n.ts` both POST there.
45
+ - Skip n8n for pure in-app logic with no external app/credential/AI — a webhook
46
+ round-trip just adds latency and a failure point.
47
+
48
+ ## Always, for any public site
49
+ Offer **awesomate-seo** (meta/OG tags, `sitemap.xml`, `robots.txt`, JSON-LD, a
50
+ general `llms.txt`) so it's findable by search engines *and* AI assistants — and
51
+ **awesomate-github** so their work is version-controlled and deploys on push
52
+ from day one.
@@ -0,0 +1,62 @@
1
+ ---
2
+ name: awesomate-credentials
3
+ description: Safely capture and store API keys, tokens, database URLs and other secrets for the user's Awesomate apps — encrypted in the hub and injected into the app's .env, never committed to git, never echoed back. Use whenever the user pastes something that looks like a secret, says "here's my API key", "add this key", "store this secret / token / password", "save my credentials", or drops a key into a file. Companion to awesomate-app-builder.
4
+ ---
5
+
6
+ # Awesomate Credentials — handle secrets safely
7
+
8
+ Non-technical users will hand you secrets the easy way: pasted into chat or
9
+ dropped in a file. Your job is to get them into the right place **safely** and
10
+ never leak them.
11
+
12
+ ## The rules (non-negotiable)
13
+
14
+ 1. **Never echo a secret value back** — not in a summary, not in a code block,
15
+ not "just to confirm". Confirm with the **key name + a redacted preview
16
+ only** (e.g. `OPENAI_API_KEY = sk-…redacted`).
17
+ 2. **Never commit a secret.** App secrets live in `.env` (gitignored) or the
18
+ hub's encrypted store — never in tracked files. If you see a secret in code
19
+ about to be committed, stop and move it.
20
+ 3. **A secret pasted in chat is exposed.** Store it, then tell the user plainly:
21
+ *"Because that key was pasted into the chat, treat it as exposed — rotate it
22
+ when convenient, and next time drop it in the inbox file (below) instead."*
23
+ 4. **Prefer the file-drop path** for anything sensitive (below).
24
+
25
+ ## Detect and offer
26
+
27
+ If the user's message contains something shaped like a secret — `sk-…`,
28
+ `ghp_`/`gho_…`, `AKIA…`, `xox[bp]-…`, a bearer token, a `postgres://…` /
29
+ `mysql://…` URL, or they say "here's my key" — don't just carry on. Offer to
30
+ store it safely: *"I'll store that as an encrypted secret for your app and keep
31
+ it out of the code — what's it for?"* Then store it (below) and redact it from
32
+ your own responses.
33
+
34
+ ## Where a secret goes
35
+
36
+ Read [references/where-secrets-go.md](references/where-secrets-go.md) for the
37
+ full map. Short version:
38
+
39
+ - **A running app needs it** (API key the backend calls, a DB URL) → store it on
40
+ the app with **`awesomate_app_set_env`** (tool). It's encrypted in the hub AND
41
+ written to the app's `.env` (0600), then the app restarts. Ask which env
42
+ (default `dev`). Node apps only — static sites have no server env.
43
+ - **A personal key just to keep** (not an app runtime secret) → your local
44
+ `~/.config/api-keys.env` (chmod 600), with a note if it was pasted in chat.
45
+ - **Never** a tracked file, a commit, or the chat.
46
+
47
+ ## The safe file-drop flow (preferred)
48
+
49
+ For anything sensitive, steer the user away from pasting in chat:
50
+
51
+ 1. Tell them: *"Create a file `.awesomate/secret-inbox.txt` in your project and
52
+ paste the value there — I'll grab it and wipe the file."* (`.awesomate/` is
53
+ gitignored by the github skill.)
54
+ 2. Read the file to capture the value, call `awesomate_app_set_env` to store it,
55
+ then **scrub the file**:
56
+ `node ~/.claude/skills/awesomate-credentials/scripts/capture-secret.mjs .awesomate/secret-inbox.txt`
57
+ — this securely overwrites and removes it so the plaintext doesn't linger.
58
+ 3. Confirm with the key name only.
59
+
60
+ ## After storing
61
+ Tell the user what you set (key + env), that it's encrypted and injected, and —
62
+ if it came through the chat — the one-line rotation nudge. Never restate the value.
@@ -0,0 +1,26 @@
1
+ # Where a secret goes
2
+
3
+ | The secret is… | Put it… | How |
4
+ |---|---|---|
5
+ | An app runtime secret (API key the backend calls, a third-party token, a DB URL) | The app's `.env` **and** the hub's encrypted store | `awesomate_app_set_env` (tool) — encrypts hub-side + injects into `.env` (0600) + restarts the app |
6
+ | An n8n webhook URL the app calls (`N8N_WEBHOOK_URL`) | Same as above | `awesomate_app_set_env` — it's not secret-secret, but it belongs in `.env` with the rest |
7
+ | A personal key the user just wants kept (not used by a running app) | `~/.config/api-keys.env` (chmod 600) | Append `KEY=value`; add a `# pasted in chat <date> — rotate` note if it came through chat |
8
+ | Anything, ever | **NOT** in code, a tracked file, a commit, or the chat transcript | — |
9
+
10
+ ## Why two places for app secrets
11
+ The hub's encrypted `hosted_app_env_secrets` row is the **durable source of
12
+ truth** (survives redeploys, is auditable, uses the same AES-256-GCM as the DB
13
+ password). The host `.env` is what the **running process actually reads**
14
+ (`dotenv/config`). `awesomate_app_set_env` writes both in one step.
15
+
16
+ ## Redaction pattern
17
+ When you confirm, show at most a short prefix:
18
+ - `sk-proj-abc…` → `OPENAI_API_KEY = sk-proj-…redacted`
19
+ - a DB URL → `DATABASE_URL = postgresql://…redacted`
20
+ Never the full value.
21
+
22
+ ## Rotation note (chat-pasted keys)
23
+ A value pasted into the conversation is in the transcript — treat it as exposed.
24
+ After storing it, tell the user once: *"rotate this when convenient, and next
25
+ time use the `.awesomate/secret-inbox.txt` drop so it stays out of the chat."*
26
+ High-value keys (payment, production DB, cloud root) — recommend rotating now.
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Securely scrub a secret-inbox file after its value has been captured and
4
+ * stored (via awesomate_app_set_env). Overwrites the file contents with random
5
+ * bytes, then removes it, so the plaintext secret doesn't linger on disk.
6
+ *
7
+ * Usage: node capture-secret.mjs <path-to-inbox-file>
8
+ *
9
+ * Prints only a non-sensitive confirmation — never the file contents. The
10
+ * capturing step (reading the value + calling awesomate_app_set_env) is done by
11
+ * Claude with the MCP tool; this script's single job is the safe wipe.
12
+ */
13
+
14
+ import { statSync, writeFileSync, rmSync } from 'node:fs';
15
+ import { randomBytes } from 'node:crypto';
16
+
17
+ const target = process.argv[2];
18
+ if (!target) {
19
+ console.error('usage: node capture-secret.mjs <path-to-inbox-file>');
20
+ process.exit(2);
21
+ }
22
+
23
+ let size = 0;
24
+ try {
25
+ size = statSync(target).size;
26
+ } catch {
27
+ console.log(`nothing to scrub: ${target} does not exist`);
28
+ process.exit(0);
29
+ }
30
+
31
+ try {
32
+ // Overwrite with random bytes of the same length, then delete. Best-effort
33
+ // (a single pass is enough on modern storage for this threat model — the
34
+ // point is not leaving the plaintext readable at the known path).
35
+ if (size > 0) writeFileSync(target, randomBytes(size), { flag: 'w' });
36
+ rmSync(target, { force: true });
37
+ console.log(`scrubbed ${target} (${size} bytes overwritten + removed)`);
38
+ } catch (err) {
39
+ console.error(`failed to scrub ${target}: ${err instanceof Error ? err.message : String(err)}`);
40
+ process.exit(1);
41
+ }
@@ -0,0 +1,71 @@
1
+ ---
2
+ name: awesomate-github
3
+ description: Set up GitHub version control for the user's app with zero Git knowledge required — connect their GitHub once, then create the repo, .gitignore, first commit and push-to-deploy for them. Use when the user says "connect GitHub", "back up my app", "save my work", "set up version control", "put this on GitHub", or after building an app when it should be version-controlled. Companion to awesomate-app-builder.
4
+ ---
5
+
6
+ # Awesomate GitHub — version control for people who don't know Git
7
+
8
+ Assume the user knows **nothing** about Git or GitHub. Never make them run raw
9
+ git commands or understand branches/remotes. You drive everything through the
10
+ `gh` CLI and the bundled script; you ask only simple, plain questions.
11
+
12
+ ## 0. First run
13
+
14
+ 1. **Is `gh` installed?** Run `gh --version`. If missing, give the one-line
15
+ install for their OS and stop until it's there:
16
+ - macOS: `brew install gh`
17
+ - Debian/Ubuntu: `sudo apt install gh` (or https://cli.github.com)
18
+ - Windows: `winget install --id GitHub.cli`
19
+ 2. **Are they connected to GitHub?** Run `gh auth status`. If not:
20
+ - Say plainly: *"I'll connect you to GitHub — I'll show a link and a short
21
+ code. Open the link, paste the code, and click approve. No password, no
22
+ tokens to copy."*
23
+ - Have them run `gh auth login` (choose GitHub.com → HTTPS → "Login with a
24
+ web browser"). It prints a one-time code + opens the device-flow page.
25
+ Wait for them to finish, then re-check `gh auth status`.
26
+ - Never ask them for a token or paste one yourself.
27
+
28
+ ## 1. Set up a project (the whole thing, via one script)
29
+
30
+ Once `gh auth status` is good, run the scaffolder from the project folder:
31
+
32
+ ```
33
+ node ~/.claude/skills/awesomate-github/scripts/github-setup.mjs <repo-name> [--public]
34
+ ```
35
+
36
+ It does, in order (all idempotent — safe to re-run):
37
+ 1. **Secret guard** — refuses if a `.env` or an obvious secret (key/token
38
+ shapes) is about to be tracked. Fix that first (use the
39
+ awesomate-credentials skill) — never commit secrets.
40
+ 2. Ensures `.gitignore` exists and excludes `.env` (creates/appends if needed).
41
+ 3. `git init` (if needed), stages everything, makes the first commit.
42
+ 4. `gh repo create <name> --private --source=. --push` (private by default;
43
+ `--public` to override). Personal accounts work — no org needed.
44
+ 5. Prints the repo URL.
45
+
46
+ Ask only: **"What should we call this project?"** (repo name) and, if relevant,
47
+ **"Keep it private?"** (default yes). Derive a sensible kebab-case repo name
48
+ from what they tell you.
49
+
50
+ ## 2. Push-to-deploy (for apps built with awesomate-app-builder)
51
+
52
+ If this is a Node app from the app builder, wire the deploy secrets so a push
53
+ ships it (GitHub Actions → cPanel). The app builder / hub provides the SSH
54
+ deploy values; set them as repo secrets with `gh secret set` (the script does
55
+ this when run with `--deploy-secrets` and the values are available in the
56
+ environment). Then: **push to `dev` → deploys dev; `main` → deploys prod.**
57
+ Explain it as *"save your work = it goes live"* — don't lecture on branches.
58
+
59
+ ## 3. Everyday use (plain language)
60
+
61
+ - "Save my work" → stage, commit with a short message you write for them, push.
62
+ - "Put it live" → push to the right branch (dev/staging/main) and tell them the URL.
63
+ - Always write clear commit messages **for** them; don't ask them to.
64
+
65
+ ## Hard rules
66
+ - Never commit a secret or a `.env`. The script's secret guard is a backstop,
67
+ not a licence to skip checking. If you spot a secret, route it through the
68
+ awesomate-credentials skill instead.
69
+ - Never ask the user for a GitHub token or password; the device-flow is the
70
+ only auth path.
71
+ - Default repos to **private**.
@@ -0,0 +1,12 @@
1
+ # GitHub, in four sentences (only surface if the user asks)
2
+
3
+ - **A repository ("repo")** is just a folder of your project that GitHub keeps a
4
+ safe, versioned copy of — every save is remembered, so you can always go back.
5
+ - **A commit** is one saved snapshot with a short note about what changed.
6
+ - **Pushing** sends your latest commits up to GitHub (the backup + the thing
7
+ that triggers deploys).
8
+ - **Branches** are parallel copies; here they map to environments — `dev` is
9
+ your workspace, `main` is what's live — so "push to main" means "make it live".
10
+
11
+ You never need to run Git commands yourself — just tell me "save my work" or
12
+ "put it live" and I'll do the right thing.
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Zero-knowledge GitHub scaffolder. Idempotent — safe to re-run.
4
+ *
5
+ * node github-setup.mjs <repo-name> [--public] [--path <dir>] [--deploy-secrets]
6
+ *
7
+ * Steps: verify gh auth → ensure .gitignore excludes .env → SECRET GUARD
8
+ * (refuse to commit a .env or an obvious key/token) → git init + first commit →
9
+ * `gh repo create --source=. --push` (private by default; personal accounts OK)
10
+ * → optionally seed deploy Actions secrets from the environment.
11
+ *
12
+ * Deploy secrets are read from env vars (SSH_HOST/SSH_USER/SSH_PRIVATE_KEY), not
13
+ * argv, so they never appear in `ps`. Prints only non-sensitive output.
14
+ */
15
+
16
+ import { spawnSync } from 'node:child_process';
17
+ import { existsSync, readFileSync, writeFileSync, statSync } from 'node:fs';
18
+ import { join } from 'node:path';
19
+
20
+ const args = process.argv.slice(2);
21
+ const flags = new Set(args.filter((a) => a.startsWith('--')));
22
+ const positional = args.filter((a) => !a.startsWith('--'));
23
+ const repoName = positional[0];
24
+ const pathIdx = args.indexOf('--path');
25
+ const cwd = pathIdx >= 0 && args[pathIdx + 1] ? args[pathIdx + 1] : process.cwd();
26
+ const isPublic = flags.has('--public');
27
+ const wantDeploySecrets = flags.has('--deploy-secrets');
28
+
29
+ if (!repoName || !/^[a-z0-9][a-z0-9._-]{0,99}$/i.test(repoName)) {
30
+ console.error('usage: node github-setup.mjs <repo-name> [--public] [--path <dir>] [--deploy-secrets]');
31
+ console.error('repo-name: letters/digits/._- , starts alphanumeric');
32
+ process.exit(2);
33
+ }
34
+
35
+ function run(cmd, cmdArgs, opts = {}) {
36
+ return spawnSync(cmd, cmdArgs, { cwd, encoding: 'utf8', ...opts });
37
+ }
38
+ function die(msg) {
39
+ console.error(`\n✗ ${msg}`);
40
+ process.exit(1);
41
+ }
42
+
43
+ // --- 0. gh present + authenticated ----------------------------------------
44
+ if (run('gh', ['--version']).status !== 0) {
45
+ die("GitHub CLI 'gh' isn't installed. Install it (macOS: brew install gh) then re-run.");
46
+ }
47
+ if (run('gh', ['auth', 'status']).status !== 0) {
48
+ die("You're not connected to GitHub yet. Run: gh auth login (GitHub.com → HTTPS → Login with a web browser), then re-run this.");
49
+ }
50
+
51
+ // --- 1. .gitignore excludes .env ------------------------------------------
52
+ const giPath = join(cwd, '.gitignore');
53
+ const needed = ['.env', '.env.local', '*.log', '.DS_Store', 'node_modules/'];
54
+ let gi = existsSync(giPath) ? readFileSync(giPath, 'utf8') : '';
55
+ const have = new Set(gi.split(/\r?\n/).map((l) => l.trim()));
56
+ const toAdd = needed.filter((n) => !have.has(n));
57
+ if (toAdd.length) {
58
+ gi = (gi.endsWith('\n') || gi === '' ? gi : gi + '\n') + toAdd.join('\n') + '\n';
59
+ writeFileSync(giPath, gi);
60
+ console.log(`• .gitignore: added ${toAdd.join(', ')}`);
61
+ }
62
+
63
+ // --- init early so the secret guard can inspect the staged set ------------
64
+ if (!existsSync(join(cwd, '.git'))) {
65
+ if (run('git', ['init', '-b', 'main']).status !== 0) die('git init failed.');
66
+ console.log('• initialised a git repo (branch: main)');
67
+ }
68
+ if (run('git', ['add', '-A']).status !== 0) die('git add failed.');
69
+
70
+ // --- 2. SECRET GUARD -------------------------------------------------------
71
+ const staged = run('git', ['diff', '--cached', '--name-only']).stdout.split(/\r?\n/).filter(Boolean);
72
+ const envLeak = staged.find((f) => /(^|\/)\.env(\.|$)/.test(f) && !/\.env\.example$/.test(f));
73
+ if (envLeak) {
74
+ die(`Refusing to commit '${envLeak}' — that's a secrets file. It's now gitignored; run \`git rm --cached ${envLeak}\` and re-run. Store secrets via the awesomate-credentials skill, never in the repo.`);
75
+ }
76
+ const SECRET_RE = /(sk-[A-Za-z0-9]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|AKIA[0-9A-Z]{16}|xox[bp]-[A-Za-z0-9-]{10,}|-----BEGIN [A-Z ]*PRIVATE KEY-----)/;
77
+ for (const f of staged) {
78
+ const abs = join(cwd, f);
79
+ try {
80
+ if (statSync(abs).size > 512 * 1024) continue; // skip large/binary
81
+ if (SECRET_RE.test(readFileSync(abs, 'utf8'))) {
82
+ die(`Refusing to commit '${f}' — it contains something shaped like a secret (API key / token / private key). Move it out (use the awesomate-credentials skill) and re-run.`);
83
+ }
84
+ } catch {
85
+ /* unreadable/binary — skip */
86
+ }
87
+ }
88
+
89
+ // --- 3. first commit (only if there's anything to commit) -----------------
90
+ if (run('git', ['diff', '--cached', '--quiet']).status !== 0) {
91
+ if (run('git', ['commit', '-m', 'Initial commit']).status !== 0) die('git commit failed.');
92
+ console.log('• made the first commit');
93
+ } else {
94
+ console.log('• nothing new to commit');
95
+ }
96
+
97
+ // --- 4. create the GitHub repo + push -------------------------------------
98
+ const already = run('git', ['remote', 'get-url', 'origin']);
99
+ if (already.status === 0) {
100
+ console.log(`• remote already set: ${already.stdout.trim()}`);
101
+ run('git', ['push', '-u', 'origin', 'HEAD']);
102
+ } else {
103
+ const vis = isPublic ? '--public' : '--private';
104
+ const create = run('gh', ['repo', 'create', repoName, vis, '--source=.', '--remote=origin', '--push']);
105
+ if (create.status !== 0) {
106
+ die(`gh repo create failed: ${(create.stderr || create.stdout || '').trim()}`);
107
+ }
108
+ console.log(`• created ${isPublic ? 'public' : 'private'} repo and pushed`);
109
+ }
110
+
111
+ // --- 5. deploy secrets (values from env, never argv) ----------------------
112
+ if (wantDeploySecrets) {
113
+ for (const name of ['SSH_HOST', 'SSH_USER', 'SSH_PRIVATE_KEY']) {
114
+ const val = process.env[name];
115
+ if (!val) {
116
+ console.log(`• skip deploy secret ${name} (not in environment)`);
117
+ continue;
118
+ }
119
+ const set = run('gh', ['secret', 'set', name], { input: val });
120
+ console.log(set.status === 0 ? `• set deploy secret ${name}` : `• FAILED to set ${name}: ${(set.stderr || '').trim()}`);
121
+ }
122
+ }
123
+
124
+ // --- done ------------------------------------------------------------------
125
+ const url = run('gh', ['repo', 'view', '--json', 'url', '-q', '.url']).stdout.trim();
126
+ console.log(`\n✓ Done. Your project is on GitHub${url ? `: ${url}` : ''}.`);
@@ -0,0 +1,71 @@
1
+ ---
2
+ name: awesomate-seo
3
+ description: Make the user's site findable — by Google AND by AI assistants. Adds title/meta/Open Graph tags, sitemap.xml, robots.txt, canonical tags, JSON-LD structured data, and a general llms.txt. Works for static sites, Node apps, and WordPress. Use when the user says "SEO", "get found on Google", "rank higher", "make my site discoverable", "sitemap", "meta tags", "schema", or after building/changing a public site. Companion to awesomate-app-builder.
4
+ ---
5
+
6
+ # Awesomate SEO — get found on Google and AI
7
+
8
+ Search engines and AI assistants both need machine-readable signals. This skill
9
+ adds them to any site the user has. Treat SEO as **ongoing**, not one-shot:
10
+ bake it in when a page is built, and re-run the relevant parts whenever content
11
+ or structure changes.
12
+
13
+ Be honest about limits: these are the signals that *let* you rank and be cited
14
+ — they don't *guarantee* placement. Never promise rankings.
15
+
16
+ ## 0. What kind of site is this?
17
+
18
+ - **Static site or Node app** (from awesomate-app-builder) → you write the files
19
+ directly into the project (below), commit, and deploy.
20
+ - **WordPress** → drive it through WP-CLI via the awesomate-hosting skill
21
+ (`wp` passthrough). Install a free SEO plugin + let it generate the sitemap
22
+ rather than hand-writing files (see §WordPress).
23
+ - **A shop (WooCommerce)** → also use the `woo-agent-storefront` skill for
24
+ AI-shopping feeds + a store `llms.txt`, and `woo-catalog-perfection` for
25
+ product-data quality. This skill covers the site-level SEO around it.
26
+
27
+ ## 1. Static / Node — write these
28
+
29
+ Follow [references/seo-checklist.md](references/seo-checklist.md) for exact
30
+ formats + length limits. In short, per site:
31
+
32
+ 1. **`<head>` tags** on every page: unique `<title>` (≤60 chars), meta
33
+ description (≤155), canonical `<link>`, Open Graph (`og:title/description/
34
+ image/url/type`) and `twitter:card`. The static-landing template ships
35
+ `{{...}}` placeholders — fill them with real values.
36
+ 2. **`robots.txt`** at the site root — allow crawling, point to the sitemap.
37
+ 3. **`sitemap.xml`** at the root — list every real URL with `<loc>` (+ `<lastmod>`).
38
+ 4. **JSON-LD** (`<script type="application/ld+json">`) — the right schema.org
39
+ type (`Organization` / `WebSite` / `Product` / `Article` / `LocalBusiness`).
40
+ 5. **`llms.txt`** at the root — a short, plain-markdown summary of what the
41
+ site/business is and its key pages, so AI assistants can understand and cite
42
+ it. (This is the general, non-shop counterpart to the Woo `llms.txt`.)
43
+
44
+ Then commit + deploy (awesomate-github). On content changes, update the
45
+ affected `<head>` tags, `sitemap.xml` `<lastmod>`, and `llms.txt`.
46
+
47
+ ## 2. WordPress
48
+
49
+ Through the awesomate-hosting skill's `wp` passthrough:
50
+ - Install + activate a free SEO plugin (e.g. `wp plugin install wordpress-seo
51
+ --activate` for Yoast, or the user's preference). It manages titles/meta,
52
+ Open Graph, and the XML sitemap.
53
+ - Confirm the sitemap is live (`/sitemap_index.xml` or `/sitemap.xml`) and
54
+ `robots.txt` references it.
55
+ - Add an `llms.txt` at the site root for AI discoverability (the plugin won't).
56
+ - WordPress is the recommended stack precisely when SEO/organic reach matters —
57
+ lean into it here.
58
+
59
+ ## 3. Verify
60
+ - `robots.txt` and `sitemap.xml` (or the WP sitemap) return 200 at the root.
61
+ - Each key page has a unique title + description and valid JSON-LD (no syntax
62
+ errors — a broken `application/ld+json` block is worse than none).
63
+ - `llms.txt` exists and is accurate.
64
+ Tell the user the next step is submitting the sitemap in Google Search Console
65
+ (you can't do that for them) — offer a one-paragraph how-to if they want it.
66
+
67
+ ## Hard rules
68
+ - Never fabricate content, reviews, or business facts to "help SEO".
69
+ - Never claim these guarantee a ranking — they make the site *eligible* and
70
+ *legible*, to search engines and AI both.
71
+ - Invalid structured data is a bug — validate the JSON before shipping.
@@ -0,0 +1,72 @@
1
+ # SEO checklist — exact formats
2
+
3
+ ## `<head>` (every page, unique per page)
4
+ ```html
5
+ <title>Primary keyword — Brand</title> <!-- ≤60 chars -->
6
+ <meta name="description" content="…"> <!-- ≤155 chars, compelling -->
7
+ <link rel="canonical" href="https://DOMAIN/PATH">
8
+ <meta property="og:title" content="…">
9
+ <meta property="og:description" content="…">
10
+ <meta property="og:type" content="website"> <!-- or article/product -->
11
+ <meta property="og:url" content="https://DOMAIN/PATH">
12
+ <meta property="og:image" content="https://DOMAIN/og.png"> <!-- 1200×630 ideally -->
13
+ <meta name="twitter:card" content="summary_large_image">
14
+ ```
15
+
16
+ ## `robots.txt` (site root)
17
+ ```
18
+ User-agent: *
19
+ Allow: /
20
+ Sitemap: https://DOMAIN/sitemap.xml
21
+ ```
22
+ Only `Disallow:` paths that genuinely shouldn't be indexed (admin, cart,
23
+ thank-you). Don't block CSS/JS.
24
+
25
+ ## `sitemap.xml` (site root)
26
+ ```xml
27
+ <?xml version="1.0" encoding="UTF-8"?>
28
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
29
+ <url><loc>https://DOMAIN/</loc><lastmod>YYYY-MM-DD</lastmod></url>
30
+ <url><loc>https://DOMAIN/about</loc><lastmod>YYYY-MM-DD</lastmod></url>
31
+ </urlset>
32
+ ```
33
+ List only real, canonical, indexable URLs (no dupes, no query-string variants).
34
+
35
+ ## JSON-LD (in `<head>` or end of `<body>`)
36
+ Pick the type that fits; validate before shipping.
37
+ ```html
38
+ <script type="application/ld+json">
39
+ {
40
+ "@context": "https://schema.org",
41
+ "@type": "Organization",
42
+ "name": "Brand",
43
+ "url": "https://DOMAIN/",
44
+ "logo": "https://DOMAIN/logo.png"
45
+ }
46
+ </script>
47
+ ```
48
+ Common types: `WebSite` (+ `SearchAction`), `Organization` / `LocalBusiness`
49
+ (add `address`, `telephone`, `openingHours`), `Product` (`offers`, `brand`,
50
+ `gtin`), `Article` (`headline`, `datePublished`, `author`), `FAQPage`.
51
+
52
+ ## `llms.txt` (site root — AI discoverability)
53
+ Plain markdown so assistants can read + cite the site. Keep it current.
54
+ ```
55
+ # Brand — what we do
56
+
57
+ One-paragraph plain-English description of the business/product and who it's for.
58
+
59
+ ## Key pages
60
+ - [Home](https://DOMAIN/): …
61
+ - [Pricing](https://DOMAIN/pricing): …
62
+ - [Contact](https://DOMAIN/contact): …
63
+
64
+ ## Facts
65
+ - Location, hours, contact — only true, verifiable facts.
66
+ ```
67
+
68
+ ## Length + quality rules
69
+ - Titles ≤60, descriptions ≤155 chars, each unique across the site.
70
+ - One `<h1>` per page; descriptive; not keyword-stuffed.
71
+ - Alt text on meaningful images.
72
+ - Every claim in `llms.txt`/JSON-LD must be true — never invent facts or reviews.