@awesomate/hosting-mcp 0.19.1 → 0.19.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
@@ -39897,7 +39897,7 @@ async function hubRequest(config3, method, path, jsonBody, opts = {}) {
39897
39897
  if (res.status === 401) {
39898
39898
  hint = " Your access token is invalid or expired \u2014 ask the user to open the hub Sites page (hub.awesomate.ai/sites), generate a fresh Connect prompt, and re-run the bootstrap.";
39899
39899
  } else if (res.status === 403 && code === "consent_required") {
39900
- const settingsUrl = body && typeof body === "object" && "settingsUrl" in body && typeof body.settingsUrl === "string" ? body.settingsUrl : "https://hub.awesomate.ai/settings?tab=privacy";
39900
+ const settingsUrl = body && typeof body === "object" && "settingsUrl" in body && typeof body.settingsUrl === "string" ? body.settingsUrl : "https://hub.awesomate.ai/n8n/settings";
39901
39901
  hint = ` This needs a privacy toggle the user must flip themselves \u2014 send them to ${settingsUrl}, wait for them to confirm, then retry. Never suggest a plan upgrade for a consent denial.`;
39902
39902
  } else if (res.status === 403) {
39903
39903
  const missing = body && typeof body === "object" && "missingScopes" in body ? ` (missing: ${JSON.stringify(body.missingScopes)})` : "";
@@ -40574,6 +40574,36 @@ var SKILL_DRIFT = (() => {
40574
40574
  if (!info.updateAvailable) return null;
40575
40575
  return `${info.installedSkillVersion ?? "unversioned"} \u2192 ${SERVER_VERSION}`;
40576
40576
  })();
40577
+ function restartGuidance() {
40578
+ const entry = (process.env.CLAUDE_CODE_ENTRYPOINT ?? "").toLowerCase();
40579
+ const inVsCode = entry.includes("vscode") || Boolean(process.env.VSCODE_IPC_HOOK || process.env.VSCODE_PID);
40580
+ if (inVsCode) {
40581
+ return {
40582
+ surface: "vscode",
40583
+ instruction: 'Open the Command Palette (Cmd+Shift+P on macOS, Ctrl+Shift+P on Windows/Linux) and run "Developer: Reload Window".',
40584
+ certain: true
40585
+ };
40586
+ }
40587
+ if (entry.includes("desktop") || entry.includes("app")) {
40588
+ return {
40589
+ surface: "desktop-app",
40590
+ instruction: "Quit the Claude app completely and open it again, then reopen this project.",
40591
+ certain: true
40592
+ };
40593
+ }
40594
+ if (entry === "cli" || entry.includes("cli")) {
40595
+ return {
40596
+ surface: "terminal",
40597
+ instruction: "Type `exit` (or press Ctrl+C twice) to leave Claude Code, then run `claude` again.",
40598
+ certain: true
40599
+ };
40600
+ }
40601
+ return {
40602
+ surface: entry || "unknown",
40603
+ instruction: "Start a fresh Claude Code session \u2014 however you normally start one (close this session and open a new one). Ask the user how they run Claude Code rather than guessing.",
40604
+ certain: false
40605
+ };
40606
+ }
40577
40607
  var SERVER_INSTRUCTIONS = [
40578
40608
  "Awesomate connects this account's WordPress hosting, n8n automations, apps and Knowledge Base to Claude.",
40579
40609
  "Entry point: call awesomate_get_context once per session FIRST \u2014 it returns the account, plan, limits, an `attention` digest (unread notifications, erroring workflows, token expiry) and skill freshness. The domain contexts (awesomate_n8n_context, awesomate_app_context, awesomate_knowledge_status) come after it, only for their domain.",
@@ -40917,12 +40947,18 @@ server.registerTool(
40917
40947
  ...result,
40918
40948
  changed: changed.map((u) => `${u.name}: ${u.from ?? "unversioned"} \u2192 ${u.to}`),
40919
40949
  whatsNew: changelogSince(changed[0]?.from ?? null).flatMap((e) => e.highlights),
40920
- restart: {
40921
- note: "Updated skill files load in the next Claude Code session \u2014 one restart lands both the skills and the (self-updating) server.",
40922
- terminal: "Type exit (or press Ctrl+C twice), then run `claude` again.",
40923
- vscode: 'Command Palette (Cmd/Ctrl+Shift+P) \u2192 "Developer: Reload Window".',
40924
- verify: "After restarting, awesomate_get_context should report skill.updateAvailable: false."
40925
- }
40950
+ restart: (() => {
40951
+ const g = restartGuidance();
40952
+ return {
40953
+ note: "Updated skill files load in the next Claude Code session \u2014 one restart lands both the skills and the (self-updating) server.",
40954
+ // Detected from this session's own environment. Give the user THIS
40955
+ // instruction only — do not read out a list of surfaces.
40956
+ detectedSurface: g.surface,
40957
+ instruction: g.instruction,
40958
+ ...g.certain ? {} : { note2: "Surface could not be detected \u2014 ask the user how they run Claude Code instead of guessing steps." },
40959
+ verify: "After restarting, awesomate_get_context should report skill.updateAvailable: false."
40960
+ };
40961
+ })()
40926
40962
  });
40927
40963
  } catch (err) {
40928
40964
  return errorResult(err);
@@ -41015,7 +41051,7 @@ server.registerTool(
41015
41051
  server.registerTool(
41016
41052
  "awesomate_run_wp_cli",
41017
41053
  {
41018
- description: "Run an allowlisted WP-CLI command on one of the user's WordPress sites (plugin/theme list+activate+update, cache flush, option get/update, post/media/menu/comment/user list). Installs accept wp.org SLUGS only \u2014 never URLs. args is the command as an array, e.g. ['plugin','list'] or ['plugin','install','wordpress-seo','--activate']. A 400 wp_cli_not_allowed means that command isn't permitted; a 502 wp_cli_unavailable is a temporary server-side issue, not your command.",
41054
+ description: "Run an allowlisted WP-CLI command on one of the user's WordPress sites (plugin/theme list+activate+update, cache flush, option get/update, post/media/menu/comment/user list). ARGUMENTS CANNOT CONTAIN SPACES (letters, digits and -_./=:@+, only) \u2014 so a site title, tagline or any multi-word value is impossible here: use awesomate_wp_settings for those, and awesomate_wp_post for post/page content. Installs accept wp.org SLUGS only \u2014 never URLs. args is the command as an array, e.g. ['plugin','list'] or ['plugin','install','wordpress-seo','--activate']. A 400 wp_cli_not_allowed means that command isn't permitted; a 502 wp_cli_unavailable is a temporary server-side issue, not your command.",
41019
41055
  inputSchema: {
41020
41056
  domain: external_exports.string().describe("The site domain"),
41021
41057
  args: external_exports.array(external_exports.string()).describe("WP-CLI args, e.g. ['plugin','list']")
@@ -41302,7 +41338,6 @@ server.registerTool(
41302
41338
  server.registerTool(
41303
41339
  "awesomate_app_deploy_info",
41304
41340
  {
41305
- annotations: READ_ONLY,
41306
41341
  description: "READ-ONLY deploy briefing \u2014 this never deploys anything (deploys happen via git push). Reports how to deploy an app, its per-env targets, last-deploy/health state, and how to promote (dev\u2192staging\u2192main) or roll back (git revert + push). Node apps deploy via git push (dev/staging/main \u2192 GitHub Actions \u2192 cPanel). Use the awesomate-github skill to wire push-to-deploy the first time.",
41307
41342
  inputSchema: { appId: external_exports.number().int().positive().describe("The app id") }
41308
41343
  },
@@ -41656,7 +41691,7 @@ server.registerTool(
41656
41691
  "awesomate_n8n_findings",
41657
41692
  {
41658
41693
  annotations: READ_ONLY,
41659
- description: "Findings from Awesomate's automated error analyzer for THIS account (Pro/Embedded + the 'AI error analysis' privacy toggle): severity, workflow, occurrences, a plain-language clientSummary, needsClientAction (something only the user can fix \u2014 expired logins, third-party quotas), and fixReady (a reviewed fix is prepared \u2014 raise it with awesomate_support to have it applied). Read-only. An empty list on a healthy instance is the good state, not an error.",
41694
+ description: "Findings from Awesomate's automated error analyzer for THIS account (Pro/Embedded + the 'Enable AI Error Diagnosis' privacy toggle (n8n \u2192 Settings \u2192 Privacy)): severity, workflow, occurrences, a plain-language clientSummary, needsClientAction (something only the user can fix \u2014 expired logins, third-party quotas), and fixReady (a reviewed fix is prepared \u2014 raise it with awesomate_support to have it applied). Read-only. An empty list on a healthy instance is the good state, not an error.",
41660
41695
  inputSchema: { limit: external_exports.number().int().min(1).max(100).optional() }
41661
41696
  },
41662
41697
  async ({ limit }) => {
@@ -41690,7 +41725,7 @@ readTool(
41690
41725
  );
41691
41726
  readTool(
41692
41727
  "awesomate_privacy_settings",
41693
- "READ which privacy/consent toggles are on or off for this account \u2014 call it when a tool returns 403 consent_required so you can name the exact toggle instead of guessing. Toggles are changed ONLY by the user in the hub (Settings \u2192 Privacy, hub.awesomate.ai/settings?tab=privacy); there is deliberately no write here.",
41728
+ "READ which privacy/consent toggles are on or off for this account \u2014 call it when a tool returns 403 consent_required so you can name the exact toggle instead of guessing. Toggles are changed ONLY by the user in the hub (n8n \u2192 Settings \u2192 Privacy, hub.awesomate.ai/n8n/settings); there is deliberately no write here.",
41694
41729
  "/api/client-settings/privacy"
41695
41730
  );
41696
41731
  server.registerTool(
@@ -41782,6 +41817,31 @@ server.registerTool(
41782
41817
  }
41783
41818
  }
41784
41819
  );
41820
+ server.registerTool(
41821
+ "awesomate_wp_settings",
41822
+ {
41823
+ description: `Change a WordPress site's core settings \u2014 THE tool for "change my site title" / tagline. Use this, never awesomate_run_wp_cli, for any value containing spaces: that tool's argument gate rejects spaces outright, so \`option update blogname "My Business Name"\` cannot work there. Fields (send only what you're changing): title, tagline, timezone (IANA, e.g. Australia/Sydney), dateFormat, timeFormat, postsPerPage (1-100), searchEngineVisible (false hides the site from search engines \u2014 confirm before setting it). Support Plus+ and audited; flushes the object cache so the change shows. Snapshot first with awesomate_snapshot_site if this is the session's first change to a live site.`,
41824
+ inputSchema: {
41825
+ domain: external_exports.string().min(3).max(253).describe("The site domain, e.g. mybusiness.awesomate.site"),
41826
+ title: external_exports.string().min(1).max(200).optional().describe("Site title (blogname)"),
41827
+ tagline: external_exports.string().max(300).optional().describe("Tagline (blogdescription)"),
41828
+ timezone: external_exports.string().max(64).optional(),
41829
+ dateFormat: external_exports.string().max(40).optional(),
41830
+ timeFormat: external_exports.string().max(40).optional(),
41831
+ postsPerPage: external_exports.number().int().min(1).max(100).optional(),
41832
+ searchEngineVisible: external_exports.boolean().optional()
41833
+ }
41834
+ },
41835
+ async ({ domain, ...fields }) => {
41836
+ try {
41837
+ const dom = encodeURIComponent(domain.toLowerCase());
41838
+ const payload = Object.fromEntries(Object.entries(fields).filter(([, v]) => v !== void 0));
41839
+ return textResult(await hubPatch(requireConfig(), `/api/client-hosting/sites/${dom}/settings`, payload));
41840
+ } catch (err) {
41841
+ return errorResult(err);
41842
+ }
41843
+ }
41844
+ );
41785
41845
  server.registerPrompt(
41786
41846
  "awesomate-status",
41787
41847
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awesomate/hosting-mcp",
3
- "version": "0.19.1",
3
+ "version": "0.19.2",
4
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",
@@ -1,5 +1,12 @@
1
1
  {
2
2
  "versions": [
3
+ {
4
+ "version": "0.19.2",
5
+ "highlights": [
6
+ "Change your site title or tagline just by asking - multi-word names now work",
7
+ "Claude tells you how to restart in the way that matches how you actually run it, instead of listing every option"
8
+ ]
9
+ },
3
10
  {
4
11
  "version": "0.19.1",
5
12
  "highlights": [
@@ -20,13 +27,13 @@
20
27
  "highlights": [
21
28
  "Search your knowledge base with filters and ask questions within a slice of your content",
22
29
  "People & entities: your knowledge base now recognises who appears in your content, and you can name and merge them",
23
- "Build AI agents from your own content \u2014 drafted, tested privately, and published only when you approve"
30
+ "Build AI agents from your own content drafted, tested privately, and published only when you approve"
24
31
  ]
25
32
  },
26
33
  {
27
34
  "version": "0.14.0",
28
35
  "highlights": [
29
- "Tools built for business owners, not developers \u2014 plain-language answers about your account, plan and limits",
36
+ "Tools built for business owners, not developers plain-language answers about your account, plan and limits",
30
37
  "New support skill: get help, understand credits, or request a done-for-you build without leaving Claude",
31
38
  "New database skill: Claude picks the right place to keep your data and sets it up for you"
32
39
  ]
@@ -45,7 +45,24 @@ decision brain. The short version, matched to what they say:
45
45
  - **Landing page / capture leads / one-pager / "just needs to look good and
46
46
  be found"** → **static site** (`kind: 'static'`). Fastest possible load,
47
47
  no database. This is the right default for marketing pages — don't reach for
48
- a full app.
48
+ a full app. Know what you give up, and check it against the job BEFORE
49
+ creating, because the stack can't be changed afterwards:
50
+ - **No server-side environment.** `awesomate_app_set_env` returns 400 for
51
+ a static app — there is nowhere to put a secret. Anything the page must
52
+ keep private (an API key, a webhook URL you don't want public) rules
53
+ static out.
54
+ - **`awesomate_n8n_attach_to_app` is Node-only.** So the "form posts to
55
+ their n8n" pattern needs a Node app, unless the webhook URL being public
56
+ is genuinely acceptable.
57
+ - **No separate dev site.** All three environment URLs point at the same
58
+ single subdomain — there's no dev copy to review on, so every change is
59
+ live the moment it's published.
60
+ - **No deploy workflow.** The static template is just the page files, so
61
+ pushing to GitHub does not publish it (see the awesomate-github skill's
62
+ §2 — deploy wiring needs Awesomate support).
63
+
64
+ If the page needs a webhook URL, a secret, or a place to test before it
65
+ goes live, it's a **Node app**. Say that in one line and move on.
49
66
  - **App with logins / custom logic / a dashboard / an API** → **Node app**
50
67
  (`kind: 'node'`). Pick **postgres** when the data is relational / needs
51
68
  concurrency / JSON / analytics; **mysql** for simple cases.
@@ -58,7 +75,14 @@ If the app needs to **send an email, look something up in their apps (Sheets,
58
75
  CRM, a database), or use AI** — their **own n8n** is often the fastest path,
59
76
  because their credentials are already connected there. Check the credential
60
77
  inventory (`awesomate_n8n_inspect {what:'credentials'}`) and say e.g. *"you already
61
- have Gmail connected in n8n — want the form to email you through that?"* Use
78
+ have Gmail connected in n8n — want the form to email you through that?"*
79
+ That read is **consent-gated**: it needs the **"Allow Claude Code to Build
80
+ n8n Workflows"** toggle (n8n → Settings → Privacy,
81
+ hub.awesomate.ai/n8n/settings; also on hub.awesomate.ai/n8n/settings),
82
+ which is **off by default** — so on a fresh account it comes back 403
83
+ `consent_required`. That's not a failure to work around: call
84
+ `awesomate_privacy_settings` to name the exact toggle, ask the user to flip
85
+ it, re-check, and meanwhile design as if you don't know what's connected. Use
62
86
  the **awesomate-n8n** skill to build the workflow, then store its webhook URL
63
87
  in the app (see the awesomate-credentials skill) and call it from the app.
64
88
  Don't reach for n8n for pure in-app logic with no external app/credential — a
@@ -108,6 +132,29 @@ target; never touch prod without an explicit ask.**
108
132
  by policy), then `awesomate_app_health` to confirm it came up (Node:
109
133
  `/api/ready`). Show the user the live dev URL and let them try it. Iterate
110
134
  on dev until they're happy.
135
+ **That dev address is Node-only.** A static app has ONE address —
136
+ `{app}.{slug}.awesomate.app` — and `awesomate_app_get` reports it for dev,
137
+ staging and prod alike. There is no separate copy to review on, so with a
138
+ static site "post it to dev first" isn't an option you can honestly offer:
139
+ say the change goes live, and get the yes before publishing. Read the real
140
+ addresses from `awesomate_app_get` rather than constructing them — an
141
+ account without the branded domains falls back to subdomains of its own
142
+ primary domain (`dev-{app}.{domain}`).
143
+ **Publishing to their WordPress site** (as opposed to an app) has its own
144
+ tools, and `awesomate_run_wp_cli` is the wrong one for content: its
145
+ argument gate rejects spaces outright, so a title or tagline can't go
146
+ through it.
147
+ - `awesomate_wp_post` — create/update/read a post or page, with real
148
+ titles and HTML. It lands as a **DRAFT** unless you explicitly pass
149
+ `status:'publish'`; never publish something the user hasn't read.
150
+ - `awesomate_wp_media_import` — pull one image in by https URL (it cannot
151
+ read local files) and get back an `attachmentId` to reference.
152
+ - `awesomate_wp_settings` — site title, tagline, timezone, date/time
153
+ format, posts per page, search-engine visibility.
154
+
155
+ Snapshot the site (`awesomate_snapshot_site`) before the session's first
156
+ change to a live WordPress site.
157
+
111
158
  7. **Wrap-up / promote** — only when the user approves, promote **dev → staging
112
159
  → main** (merge + push per branch). `awesomate_app_deploy_info` reports the
113
160
  branch→env map + last-deploy state (it never deploys — `git push` does). Prod is `main`. **Before merging to
@@ -141,7 +188,10 @@ app with a UI, an n8n data table (awesomate-n8n skill) beats building an app
141
188
  - A `409` with code `app_limit` = the plan's app allowance is full (Support
142
189
  Plus 5, Pro 20 — `awesomate_app_context.limits` has the live numbers).
143
190
  Don't retry; tell the user and surface the `recommendedPlan`/`deepLink`
144
- from the error. Deleting an unused app also frees a slot.
191
+ from the error. **There is no delete-app tool or route** — don't offer
192
+ "delete an old one to free a slot", because neither you nor the user can.
193
+ The honest options are an upgrade or asking support (awesomate-support
194
+ skill) to remove an app.
145
195
  - Never invent that a capability exists — read `awesomate_app_context` first.
146
196
  - Never print a secret or commit a `.env` (the credentials + github skills
147
197
  enforce this).
@@ -10,7 +10,22 @@ confirm before building. **Never default to WordPress.**
10
10
  |---|---|---|
11
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**. Existing WP elsewhere → migrate it, don't rebuild: [wp-migrate.md](wp-migrate.md) (also covers WP ↔ app coexistence on subdomains). |
12
12
  | Sell products / online shop | **WordPress + WooCommerce** | Provision via the awesomate-hosting skill, install WooCommerce over WP-CLI, then run **awesomate-seo** (product pages need `Product` JSON-LD + real product data). |
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. |
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 — but see the three limits under the table before you commit to it. |
14
+
15
+ **What a static site cannot do** (the stack can't be changed later, so check
16
+ this against the job first):
17
+
18
+ - **No server-side environment.** `awesomate_app_set_env` returns 400 for a
19
+ static app. Anything the page must keep secret has nowhere to live — it
20
+ would be in the page source, readable by anyone.
21
+ - **`awesomate_n8n_attach_to_app` is Node-only.** A static lead form can only
22
+ POST to an n8n webhook URL that is **hardcoded in the page and therefore
23
+ public**. Sometimes that's acceptable (a plain lead capture behind n8n-side
24
+ validation); when it isn't, it's a Node app.
25
+ - **No separate dev site.** All three environment URLs are the same single
26
+ subdomain, so there's nowhere to review a change before it's live.
27
+ - **No deploy workflow.** The static template is just the page files —
28
+ pushing to GitHub does not publish it (awesomate-github §2).
14
29
  | A tool/app with **logins, custom logic, a dashboard, or an API** | **Node app** (`kind: 'node'`) | A real backend + database. |
15
30
 
16
31
  ### Within a Node app: which starter template?
@@ -47,18 +62,30 @@ If the ask includes **email someone, look something up in their apps, save a
47
62
  lead somewhere, or use AI**, that's usually a job for their **own n8n** — their
48
63
  credentials are already connected there, so there's nothing new to set up.
49
64
 
50
- - Check `GET /api/my-n8n/machine/credentials` (names/types only) and offer what
51
- they already have: *"You've got Gmail + Google Sheets connected — want the
52
- form to email you and add the lead to a sheet?"*
65
+ - Check what's connected with `awesomate_n8n_inspect {what:'credentials'}`
66
+ (names/types only) and offer what they already have: *"You've got Gmail +
67
+ Google Sheets connected — want the form to email you and add the lead to a
68
+ sheet?"* That read needs the **"Allow Claude Code to Build n8n Workflows"**
69
+ privacy toggle, which is off by default — a 403 `consent_required` means
70
+ call `awesomate_privacy_settings`, name the toggle, and ask the user to flip
71
+ it at hub.awesomate.ai/n8n/settings.
53
72
  - Build the workflow with the **awesomate-n8n** skill (webhook trigger →
54
- action), take its **webhook URL**, and store it in the app as
55
- `N8N_WEBHOOK_URL` via the **awesomate-credentials** skill. The static
56
- template's lead form and the Node template's `src/lib/n8n.ts` both POST there.
73
+ action), take its **webhook URL**, and wire it into the app.
74
+ - **Node app:** `awesomate_n8n_attach_to_app` it stores
75
+ `N8N_WEBHOOK_URL` plus a generated `N8N_WEBHOOK_SECRET` (encrypted +
76
+ injected) and returns the secret so you can add the matching
77
+ `X-Awesomate-Webhook-Secret` check on the n8n side. `src/lib/n8n.ts`
78
+ POSTs there.
79
+ - **Static site:** there is no app environment, so the URL sits in the page
80
+ source, public. Tell the user that plainly, and defend the workflow on the
81
+ n8n side instead (validation, rate limiting) — or use a Node app.
57
82
  - Skip n8n for pure in-app logic with no external app/credential/AI — a webhook
58
83
  round-trip just adds latency and a failure point.
59
84
 
60
85
  ## Always, for any public site
61
86
  Offer **awesomate-seo** (meta/OG tags, `sitemap.xml`, `robots.txt`, JSON-LD, a
62
87
  general `llms.txt`) so it's findable by search engines *and* AI assistants — and
63
- **awesomate-github** so their work is version-controlled and deploys on push
64
- from day one.
88
+ **awesomate-github** so their work is version-controlled from day one.
89
+ Push-to-deploy is a second step and not one you can finish alone: the Actions
90
+ workflow needs SSH repo secrets no client-side tool returns, so set up the repo
91
+ and hand the deploy wiring to Awesomate support (awesomate-github §2).
@@ -26,8 +26,10 @@ until you're happy."*
26
26
  installed on the new site.
27
27
  4. **Verify before DNS.** Browse the copy on its Awesomate URL: pages,
28
28
  images, forms, admin login, HTTPS. Run **awesomate-seo** (sitemap,
29
- robots, meta) and offer the **wp-security-review** skill a migrated
30
- site imports its old plugins, and stale plugins are the top WP risk.
29
+ robots, meta). A migrated site brings its old plugins with it and stale
30
+ plugins are the top WP risk, so check for outdated ones
31
+ (`awesomate_run_wp_cli ['plugin','list']`), update what's safe to update,
32
+ and flag anything abandoned to the user.
31
33
  5. **Cut over DNS** — point the domain at Awesomate (add it as a custom
32
34
  domain first; plan limits apply). Old site stays live as the fallback
33
35
  until the user confirms; propagation can take up to a day and both
@@ -47,8 +47,13 @@ value goes browser → destination directly; you only ever see the key NAMES.
47
47
  node ~/.claude/skills/awesomate-credentials/scripts/secret-drop.mjs \
48
48
  --hub-app <appId> --env dev --keys STRIPE_SECRET_KEY --port <port> --token <hex>
49
49
  ```
50
- Get `<appId>` from `awesomate_app_list`. Keys are UPPER_SNAKE (lowercase is
51
- auto-uppercased). Node apps only static sites have no server env. The
50
+ Get `<appId>` from `awesomate_app_list`. Keys must be UPPER_SNAKE: the
51
+ drop script uppercases what you pass it in hub mode, but the
52
+ `awesomate_app_set_env` TOOL does not — it hard-rejects anything that
53
+ isn't `^[A-Z][A-Z0-9_]{0,63}$`, so a lowercase name or a leading
54
+ underscore is a validation error there, not a tidy-up. Pick the final
55
+ uppercase name once and use it in both places. Node apps only — static
56
+ sites have no server env (`set_env` returns 400 for them). The
52
57
  script posts each value to the hub with the user's own PAT
53
58
  (`~/.awesomate/credentials.json`) — the same route `awesomate_app_set_env`
54
59
  uses, so encryption, `.env` injection and restart all behave identically.
@@ -71,6 +76,26 @@ value goes browser → destination directly; you only ever see the key NAMES.
71
76
  6. Confirm with key names only. No rotation warning needed — the value never
72
77
  entered the conversation.
73
78
 
79
+ ### What the drop link can't do
80
+
81
+ - **The link is short-lived and single-use.** It expires after 15 minutes
82
+ (`--ttl <minutes>` to change it) and the server exits after the FIRST
83
+ successful save unless you passed `--stay`. If the user comes back later
84
+ and the page is dead, that's expected — start a fresh one on a new port
85
+ with a new token rather than telling them to retry the old link.
86
+ - **It cannot carry a MULTI-LINE value.** The bulk box is parsed line by
87
+ line, splitting on the first `=`, so a PEM private key or a Google
88
+ service-account JSON loses everything after line 1 — silently, and those
89
+ are two of the commonest secrets people have. The named field is a
90
+ single-line input, so it can't hold one either. For a multi-line secret,
91
+ either:
92
+ - **base64 it into one line** — `base64 -i key.pem | tr -d '\n'` — drop
93
+ that through the link as e.g. `GOOGLE_SA_JSON_B64`, and decode it in the
94
+ app at startup; or
95
+ - have the user **save it to a file** on their machine and tell you the
96
+ path, then read and store it — being honest that reading it puts the
97
+ value in the conversation record, so the rotation nudge applies.
98
+
74
99
  ## Detect and offer
75
100
 
76
101
  If the user's message contains something shaped like a secret — `sk-…`,
@@ -94,14 +119,26 @@ full map. Short version:
94
119
  the file-mode link; add a rotation note if it came through the chat.
95
120
  - **Never** a tracked file, a commit, or the chat.
96
121
 
122
+ A secret for an n8n workflow belongs on the n8n instance (awesomate-n8n
123
+ skill), never in an app `.env`. Those tools are consent-gated: if one comes
124
+ back 403 `consent_required`, call `awesomate_privacy_settings` — it names
125
+ the exact toggle that's off, so you can send the user to the right switch at
126
+ hub.awesomate.ai/n8n/settings instead of guessing. You can't flip it
127
+ for them.
128
+
97
129
  ## Fallback: the file-drop flow
98
130
 
99
131
  Only when the link flow can't work (e.g. Claude is running on a remote machine
100
132
  the user's browser can't reach as 127.0.0.1):
101
133
 
102
134
  1. Tell them: *"Create a file `.awesomate/secret-inbox.txt` in your project and
103
- paste the value there — I'll grab it and wipe the file."* (`.awesomate/` is
104
- gitignored by the github skill.)
135
+ paste the value there — I'll grab it and wipe the file."*
136
+ **Verify `.gitignore` yourself before any commit.** Don't assume the
137
+ folder is ignored: the github skill's setup script adds `.awesomate/` only
138
+ when it has actually been run in this project, and older installed copies
139
+ of that script didn't list it at all. `grep -q '^\.awesomate/' .gitignore`
140
+ — add the line if it's missing, before the file exists, and never commit
141
+ while it does.
105
142
  2. Read the file, store via `awesomate_app_set_env`, then **scrub the file**:
106
143
  `node ~/.claude/skills/awesomate-credentials/scripts/capture-secret.mjs .awesomate/secret-inbox.txt`
107
144
  3. Be honest with the user: reading the file put the value in the conversation
@@ -26,6 +26,14 @@ co-located with the workflow that uses it. No hosting slot consumed, and
26
26
  workflows query it directly — this is the default answer for anything
27
27
  automation-shaped. Inspect what exists with
28
28
  `awesomate_n8n_inspect {what:'datatables'}`.
29
+ **Both the write AND that read are gated:** Support Plus or above, plus the
30
+ privacy toggle **"Allow Claude Code to Build n8n Workflows"** — n8n →
31
+ Settings → Privacy (hub.awesomate.ai/n8n/settings, or the same toggles at
32
+ hub.awesomate.ai/n8n/settings, which is what the 403's
33
+ `settingsUrl` points at). It is **off by default**, so on a fresh account
34
+ both the read and the write come back 403 `consent_required`. Call
35
+ `awesomate_privacy_settings` to name the exact toggle that's off, send the
36
+ user to flip it, then re-check. You cannot flip it for them.
29
37
 
30
38
  **(b) Real APP data** — logins, dashboards, anything relational that an
31
39
  app serves to people. → the app's own Postgres.
@@ -38,7 +46,23 @@ app serves to people. → the app's own Postgres.
38
46
  **(c) A WORKFLOW that needs raw SQL against its own Postgres** — real
39
47
  joins, aggregates, or volumes a data table shouldn't carry. →
40
48
  `awesomate_n8n_provision_pg`. One step creates the database AND the n8n
41
- credential, so the workflow can query it immediately.
49
+ credential, so the workflow can query it immediately. It has the most gates
50
+ of anything here — check them BEFORE offering it, so the user doesn't hit a
51
+ wall halfway:
52
+ - Support Plus or above.
53
+ - The **"Allow Claude Code to Build n8n Workflows"** toggle (off by
54
+ default) — same one as branch (a).
55
+ - The **direct database writes** consent (`allow_pg_writes`). It's on by
56
+ default, so this is rare — but it has **no client-facing toggle in the
57
+ hub**, so if it's off, don't send the user hunting for a switch that
58
+ doesn't exist: route it to support via the awesomate-support skill.
59
+ - An Awesomate cPanel hosting account. Without one it returns 409
60
+ `hosting_required`; hosting comes first (awesomate-hosting skill).
61
+ - Capped at **2 a day**.
62
+
63
+ On any 403 here, call `awesomate_privacy_settings` first — it names the
64
+ exact toggle that's blocking, which decides whether this is a "flip this
65
+ switch" or a "raise it with support" answer.
42
66
 
43
67
  **(d) Analytics/BI or data shared with other systems** — reporting
44
68
  warehouses, a spreadsheet the whole team lives in, a CRM. → recommend
@@ -69,12 +93,19 @@ reach), ask "live or dev?" before provisioning against it.
69
93
 
70
94
  ## 2. Safety rules
71
95
 
72
- - **Passwords are shown ONCE by provisioning and land where they
73
- belong** the app's environment for `awesomate_app_provision_db`,
74
- the n8n credential for `awesomate_n8n_provision_pg`. Never echo a
75
- password into chat, and never put a `DATABASE_URL` in a message. Tell
76
- the user where it lives instead: "the app already has its connection —
77
- nothing for you to copy."
96
+ - **Passwords land where they belong, and the two branches differ.**
97
+ `awesomate_app_provision_db` NEVER returns a password: it's set
98
+ server-side, stored encrypted, and injected as the app's `DATABASE_URL`
99
+ there is nothing to show, so don't promise the user a password.
100
+ `awesomate_n8n_provision_pg` is the one that DOES return a password, once,
101
+ because it lives in the n8n credential and Awesomate never stores it. The
102
+ workflow doesn't need you to repeat it — the credential already holds it —
103
+ so say that, and only offer the value if the user actually needs it
104
+ elsewhere, warning that it has now passed through the transcript and
105
+ should be rotated. Never put a `DATABASE_URL` in a message. Tell the user where
106
+ it lives instead: "the app already has its connection — nothing for you
107
+ to copy." (After an app database is provisioned, the app needs a restart
108
+ or redeploy to pick it up.)
78
109
  - Secrets the user's own app code needs (API keys, tokens) go through
79
110
  the awesomate-credentials skill, never pasted into files or chat.
80
111
  - **Deleting data requires an explicit filter and an explicit yes.**
@@ -84,14 +115,28 @@ reach), ask "live or dev?" before provisioning against it.
84
115
 
85
116
  ## 3. Limits, honestly
86
117
 
87
- Provisioning quotas are per-day and plan-gated. App databases
88
- (`awesomate_app_provision_db`) and workflow Postgres
89
- (`awesomate_n8n_provision_pg`) need Support Plus or above; data tables
90
- work on any plan with n8n access. If the user's plan doesn't cover what
91
- branch (b) or (c) needs, say it once "app databases are on Support
92
- Plus" point them at hub.awesomate.ai/billing to upgrade, then help
93
- fully within what their plan allows. Usually that means branch (a): a
94
- data table covers more "keep track of X" asks than people expect. If a
118
+ Everything that provisions here needs **Support Plus or above** — app
119
+ databases (`awesomate_app_provision_db`), workflow Postgres
120
+ (`awesomate_n8n_provision_pg`) and data tables alike. Data tables are NOT
121
+ "any plan with n8n access": Essentials gets 0 creations a day, and both the
122
+ table read and the write also need the "Allow Claude Code to Build n8n
123
+ Workflows" toggle (§0(a)).
124
+
125
+ Only some of it is capped per day:
126
+ - `awesomate_n8n_provision_pg` — **2 a day**, flat; no plan raises it, so
127
+ don't pitch an upgrade for it.
128
+ - Data table creations — **10 a day** on Support Plus and Pro (20 on
129
+ Embedded), 0 on Essentials. An instance also tops out at 50 data tables.
130
+ - `awesomate_app_provision_db` — **no daily cap**. It's one database per
131
+ environment and refuses (409 `already_has_db`) if the app has one, so
132
+ there's nothing to ration.
133
+
134
+ If the user's plan doesn't cover what branch (b) or (c) needs, say it once
135
+ — "app databases are on Support Plus" — point them at
136
+ hub.awesomate.ai/billing to upgrade, then help fully within what their plan
137
+ allows. On Essentials that means none of (a), (b) or (c) is available —
138
+ don't dangle a data table as the consolation prize; say so once and look at
139
+ (d), keeping the data where it already lives and connecting to it. If a
95
140
  daily quota is hit, say when it resets and what can proceed meanwhile.
96
141
 
97
142
  Report every result as a business outcome: "your workflow now remembers
@@ -47,19 +47,48 @@ Ask only: **"What should we call this project?"** (repo name) and, if relevant,
47
47
  **"Keep it private?"** (default yes). Derive a sensible kebab-case repo name
48
48
  from what they tell you.
49
49
 
50
+ **The script's first push goes to `main`.** It runs `git init -b main` and
51
+ pushes the current branch, so on a fresh repo `main` — the PRODUCTION branch
52
+ for Node apps — is what gets created. That's fine for a repo that's just a
53
+ backup, but if the code isn't ready to be live, say so before running it, or
54
+ create and check out `dev` first. There is **no `dev` branch until you make
55
+ one**: `git push origin dev` on a fresh repo fails. Create it, then push:
56
+
57
+ ```
58
+ git checkout -b dev
59
+ git push -u origin dev
60
+ ```
61
+
50
62
  ## 2. Push-to-deploy (for apps built with awesomate-app-builder)
51
63
 
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.
64
+ Node apps from the app builder ship with a GitHub Actions workflow that
65
+ deploys on a push to `dev` / `staging` / `main` (dev dev, main → prod).
66
+ Once the branches exist, explain it as *"save your work = it goes live"*
67
+ don't lecture on branches.
68
+
69
+ **But you cannot finish the wiring yourself, and you should say so.** That
70
+ workflow needs `SSH_HOST`, `SSH_USER` and `SSH_PRIVATE_KEY` as repo secrets,
71
+ and **no client-reachable tool or route returns those values** — they're
72
+ admin-only. `github-setup.mjs --deploy-secrets` only copies them from your
73
+ environment, so with nothing there it prints `skip deploy secret …` and the
74
+ first deploy run goes red. So:
75
+
76
+ 1. Set up the repo, the branches and the commits (§1) — all of that works.
77
+ 2. Tell the user plainly: *"your code is backed up on GitHub; the automatic
78
+ deploy still needs Awesomate to connect it — I'll raise that for you."*
79
+ Raise it via the **awesomate-support** skill.
80
+ 3. **Do not loop on the failing pipeline.** Re-running it, rewriting the
81
+ workflow, or pushing again won't produce the missing secrets.
82
+
83
+ Static sites have no deploy workflow at all — the template is just the page
84
+ files — so pushing does not publish them either; same handoff.
58
85
 
59
86
  ## 3. Everyday use (plain language)
60
87
 
61
88
  - "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.
89
+ - "Put it live" → push to the right branch (dev/staging/main) and tell them the
90
+ URL. If the branch doesn't exist yet, create it (`git checkout -b <name>`)
91
+ before pushing — only `main` exists after first-time setup.
63
92
  - Always write clear commit messages **for** them; don't ask them to.
64
93
 
65
94
  ## 4. Be the version-control guardian (proactive, not reactive)
@@ -78,6 +107,13 @@ control — they should never have to think about it:
78
107
  aren't backed up anywhere — want me to set that up? Takes a minute."*
79
108
  `env_file_tracked` is urgent: untrack it, .gitignore it, and route the
80
109
  secrets through awesomate-credentials.
110
+ **`last_deploy_failed` that never clears is usually the unwired deploy
111
+ secrets (§2), not a bug you can fix.** Check whether `SSH_HOST` /
112
+ `SSH_USER` / `SSH_PRIVATE_KEY` are set on the repo (`gh secret list`)
113
+ before touching anything: if they're missing, stop, tell the user the
114
+ deploy connection is pending with Awesomate, and raise it via the
115
+ awesomate-support skill. Don't retry the run, and don't start editing the
116
+ workflow file.
81
117
  - **Auto-checkpoint.** After every meaningful working change (feature works,
82
118
  bug fixed, content updated), commit with a plain-English message — don't
83
119
  wait for "save my work", and never ask them to write a message. Push at
@@ -90,7 +126,12 @@ control — they should never have to think about it:
90
126
 
91
127
  ## Hard rules
92
128
  - Never commit a secret or a `.env`. The script's secret guard is a backstop,
93
- not a licence to skip checking. If you spot a secret, route it through the
129
+ not a licence to skip checking. The script's `.gitignore` list `.env`,
130
+ `.env.local`, `.awesomate/`, `*.log`, `.DS_Store`, `node_modules/` — is only
131
+ applied when the script has been run in that project, so on a repo somebody
132
+ else set up, read `.gitignore` before committing rather than assuming.
133
+ `.awesomate/` matters especially: the credentials skill's file-drop fallback
134
+ puts a live secret there. If you spot a secret, route it through the
94
135
  awesomate-credentials skill instead.
95
136
  - Never ask the user for a GitHub token or password; the device-flow is the
96
137
  only auth path.
@@ -50,7 +50,10 @@ if (run('gh', ['auth', 'status']).status !== 0) {
50
50
 
51
51
  // --- 1. .gitignore excludes .env ------------------------------------------
52
52
  const giPath = join(cwd, '.gitignore');
53
- const needed = ['.env', '.env.local', '*.log', '.DS_Store', 'node_modules/'];
53
+ // .awesomate/ is the secret-drop inbox the credentials skill tells users it is
54
+ // ignored, and the SECRET_RE guard only catches known key SHAPES, so a database
55
+ // URL or bare token in there would otherwise be committable.
56
+ const needed = ['.env', '.env.local', '.awesomate/', '*.log', '.DS_Store', 'node_modules/'];
54
57
  let gi = existsSync(giPath) ? readFileSync(giPath, 'utf8') : '';
55
58
  const have = new Set(gi.split(/\r?\n/).map((l) => l.trim()));
56
59
  const toAdd = needed.filter((n) => !have.has(n));