@awesomate/hosting-mcp 0.13.1 → 0.15.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/dist/index.js +128 -2
- package/package.json +1 -1
- package/skill/awesomate-app-builder/SKILL.md +3 -1
- package/skill/awesomate-app-builder/references/solutions.md +125 -0
- package/skill/awesomate-credentials/SKILL.md +79 -26
- package/skill/awesomate-credentials/references/where-secrets-go.md +11 -8
- package/skill/awesomate-credentials/scripts/secret-drop.mjs +279 -0
- package/skill/awesomate-database/SKILL.md +99 -0
- package/skill/awesomate-hosting/references/voice.md +106 -0
- package/skill/awesomate-hosting/scripts/bootstrap.mjs +8 -0
- package/skill/awesomate-support/SKILL.md +101 -0
package/dist/index.js
CHANGED
|
@@ -39913,8 +39913,8 @@ function hubPost(config3, path, jsonBody = {}) {
|
|
|
39913
39913
|
function hubPatch(config3, path, jsonBody = {}) {
|
|
39914
39914
|
return hubRequest(config3, "PATCH", path, jsonBody);
|
|
39915
39915
|
}
|
|
39916
|
-
function hubDelete(config3, path) {
|
|
39917
|
-
return hubRequest(config3, "DELETE", path);
|
|
39916
|
+
function hubDelete(config3, path, jsonBody) {
|
|
39917
|
+
return hubRequest(config3, "DELETE", path, jsonBody);
|
|
39918
39918
|
}
|
|
39919
39919
|
|
|
39920
39920
|
// src/skills.ts
|
|
@@ -40282,6 +40282,132 @@ server.registerTool(
|
|
|
40282
40282
|
}
|
|
40283
40283
|
}
|
|
40284
40284
|
);
|
|
40285
|
+
server.registerTool(
|
|
40286
|
+
"awesomate_support",
|
|
40287
|
+
{
|
|
40288
|
+
description: "Help the user with the service itself \u2014 questions, being stuck, or reaching a human. action 'faq' {q} searches the published help library (answer FROM the results; never invent policy or pricing). action 'create_ticket' {subject, message} opens a support ticket \u2014 draft it in the user's words, SHOW it, and get an explicit yes before sending; it emails the Awesomate team and returns a portal URL. action 'list_tickets' shows their tickets and status. Use the awesomate-support skill for the full flow. Not for building \u2014 route n8n work to awesomate-n8n and hosting to awesomate-hosting.",
|
|
40289
|
+
inputSchema: {
|
|
40290
|
+
action: external_exports.enum(["faq", "create_ticket", "list_tickets"]),
|
|
40291
|
+
q: external_exports.string().optional().describe("faq only: the question"),
|
|
40292
|
+
subject: external_exports.string().optional().describe("create_ticket only"),
|
|
40293
|
+
message: external_exports.string().optional().describe("create_ticket only: the body, in the user's words")
|
|
40294
|
+
}
|
|
40295
|
+
},
|
|
40296
|
+
async ({ action, q, subject, message }) => {
|
|
40297
|
+
try {
|
|
40298
|
+
const cfg = requireConfig();
|
|
40299
|
+
if (action === "faq") {
|
|
40300
|
+
return textResult(await hubGet(cfg, `/api/help/faq${q ? `?q=${encodeURIComponent(q)}` : ""}`));
|
|
40301
|
+
}
|
|
40302
|
+
if (action === "list_tickets") {
|
|
40303
|
+
return textResult(await hubGet(cfg, "/api/support/tickets"));
|
|
40304
|
+
}
|
|
40305
|
+
if (!subject || !message) return errorResult(new Error("create_ticket needs subject and message"));
|
|
40306
|
+
return textResult(await hubPost(cfg, "/api/support/tickets", { subject, body_text: message }));
|
|
40307
|
+
} catch (err) {
|
|
40308
|
+
return errorResult(err);
|
|
40309
|
+
}
|
|
40310
|
+
}
|
|
40311
|
+
);
|
|
40312
|
+
server.registerTool(
|
|
40313
|
+
"awesomate_request_build",
|
|
40314
|
+
{
|
|
40315
|
+
description: "Submit a DONE-FOR-YOU automation request \u2014 the Awesomate team builds it, for clients who'd rather not build it themselves or whose request is beyond what you can build here. This SPENDS 1 CREDIT ($100). Two steps, always: call WITHOUT confirmCredit first \u2014 it returns the cost and the user's available balance and spends nothing; state both to the user in plain words, get an explicit yes, THEN call again with confirmCredit:true. Needs a wizard-enabled plan (Pro/Embedded) \u2014 a Support Plus user gets upgrade_required, relay it honestly. On success returns a tracking URL (progress shows on My Automations).",
|
|
40316
|
+
inputSchema: {
|
|
40317
|
+
title: external_exports.string().describe("Short name for the automation"),
|
|
40318
|
+
description: external_exports.string().describe("What it should do, triggered by what, with what outcome (min 20 chars)"),
|
|
40319
|
+
details: external_exports.record(external_exports.unknown()).optional().describe("Optional structured extras (apps, volumes)"),
|
|
40320
|
+
confirmCredit: external_exports.boolean().optional().describe("Omit for the free preview; true ONLY after the user approves the 1-credit cost")
|
|
40321
|
+
}
|
|
40322
|
+
},
|
|
40323
|
+
async ({ title, description, details, confirmCredit }) => {
|
|
40324
|
+
try {
|
|
40325
|
+
return textResult(await hubPost(requireConfig(), "/api/workflows/machine-request", { title, description, details, confirmCredit }));
|
|
40326
|
+
} catch (err) {
|
|
40327
|
+
return errorResult(err);
|
|
40328
|
+
}
|
|
40329
|
+
}
|
|
40330
|
+
);
|
|
40331
|
+
server.registerTool(
|
|
40332
|
+
"awesomate_site_create",
|
|
40333
|
+
{
|
|
40334
|
+
description: "Create a WordPress site on the user's hosting. Plan limits are enforced server-side (429/403 with an upgrade hint if they're at their cap \u2014 relay it). Ask 'live or dev?' first per the awesomate-hosting skill. Returns the new site's admin details.",
|
|
40335
|
+
inputSchema: {
|
|
40336
|
+
domain: external_exports.string().optional().describe("Custom domain if they have one; omit for a default *.awesomate.site subdomain"),
|
|
40337
|
+
siteTitle: external_exports.string().optional(),
|
|
40338
|
+
adminEmail: external_exports.string().optional().describe("Defaults to the account email")
|
|
40339
|
+
}
|
|
40340
|
+
},
|
|
40341
|
+
async ({ domain, siteTitle, adminEmail }) => {
|
|
40342
|
+
try {
|
|
40343
|
+
return textResult(await hubPost(requireConfig(), "/api/client-hosting/sites", { domain, siteTitle, adminEmail }));
|
|
40344
|
+
} catch (err) {
|
|
40345
|
+
return errorResult(err);
|
|
40346
|
+
}
|
|
40347
|
+
}
|
|
40348
|
+
);
|
|
40349
|
+
server.registerTool(
|
|
40350
|
+
"awesomate_domain_add",
|
|
40351
|
+
{
|
|
40352
|
+
description: "Add a custom domain to the user's hosting account. Returns the DNS steps they need to complete at their registrar. Plan-gated (some plans allow zero custom domains \u2014 relay the upgrade hint on 403).",
|
|
40353
|
+
inputSchema: { domain: external_exports.string().describe("The domain to add, e.g. example.com") }
|
|
40354
|
+
},
|
|
40355
|
+
async ({ domain }) => {
|
|
40356
|
+
try {
|
|
40357
|
+
return textResult(await hubPost(requireConfig(), "/api/client-hosting/domains", { domain }));
|
|
40358
|
+
} catch (err) {
|
|
40359
|
+
return errorResult(err);
|
|
40360
|
+
}
|
|
40361
|
+
}
|
|
40362
|
+
);
|
|
40363
|
+
server.registerTool(
|
|
40364
|
+
"awesomate_run_wp_cli",
|
|
40365
|
+
{
|
|
40366
|
+
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.",
|
|
40367
|
+
inputSchema: {
|
|
40368
|
+
domain: external_exports.string().describe("The site domain"),
|
|
40369
|
+
args: external_exports.array(external_exports.string()).describe("WP-CLI args, e.g. ['plugin','list']")
|
|
40370
|
+
}
|
|
40371
|
+
},
|
|
40372
|
+
async ({ domain, args }) => {
|
|
40373
|
+
try {
|
|
40374
|
+
return textResult(await hubPost(requireConfig(), `/api/client-hosting/sites/${encodeURIComponent(domain)}/wp-cli`, { args }));
|
|
40375
|
+
} catch (err) {
|
|
40376
|
+
return errorResult(err);
|
|
40377
|
+
}
|
|
40378
|
+
}
|
|
40379
|
+
);
|
|
40380
|
+
server.registerTool(
|
|
40381
|
+
"awesomate_uninstall_site",
|
|
40382
|
+
{
|
|
40383
|
+
description: "Permanently delete a WordPress site (files + database). IRREVERSIBLE \u2014 snapshot first if the user might want it back, and always get explicit confirmation. You MUST pass confirm equal to the exact domain, or the hub refuses. Support Plus+.",
|
|
40384
|
+
inputSchema: {
|
|
40385
|
+
domain: external_exports.string().describe("The site domain to delete"),
|
|
40386
|
+
confirm: external_exports.string().describe("Must equal domain exactly \u2014 proof of intent")
|
|
40387
|
+
}
|
|
40388
|
+
},
|
|
40389
|
+
async ({ domain, confirm }) => {
|
|
40390
|
+
try {
|
|
40391
|
+
return textResult(await hubDelete(requireConfig(), `/api/client-hosting/sites/${encodeURIComponent(domain)}`, { confirm }));
|
|
40392
|
+
} catch (err) {
|
|
40393
|
+
return errorResult(err);
|
|
40394
|
+
}
|
|
40395
|
+
}
|
|
40396
|
+
);
|
|
40397
|
+
server.registerTool(
|
|
40398
|
+
"awesomate_app_provision_db",
|
|
40399
|
+
{
|
|
40400
|
+
description: "Add a Postgres database to an EXISTING Node app that doesn't have one \u2014 one DB per environment, with DATABASE_URL injected into each app environment (restart/redeploy to pick it up). The password is set server-side and never returned. Support Plus+; static apps and apps that already have a DB are refused. Use the awesomate-database skill's decision tree first \u2014 a simple list an automation reads/writes is often better as an n8n data table.",
|
|
40401
|
+
inputSchema: { appId: external_exports.number().int().positive().describe("The app id from awesomate_app_list / _get") }
|
|
40402
|
+
},
|
|
40403
|
+
async ({ appId }) => {
|
|
40404
|
+
try {
|
|
40405
|
+
return textResult(await hubPost(requireConfig(), `/api/my-apps/apps/${appId}/provision-db`, {}));
|
|
40406
|
+
} catch (err) {
|
|
40407
|
+
return errorResult(err);
|
|
40408
|
+
}
|
|
40409
|
+
}
|
|
40410
|
+
);
|
|
40285
40411
|
readTool(
|
|
40286
40412
|
"awesomate_get_hosting_status",
|
|
40287
40413
|
"The hosting account's provisioning state: eligible/provisioned flags, in-progress provisioning step, primary domain, cPanel server, DNS targets. Use before suggesting any site action.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@awesomate/hosting-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Awesomate MCP server \u2014 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",
|
|
@@ -24,7 +24,9 @@ own hosting) but you should always confirm the **stack + name** before creating.
|
|
|
24
24
|
off to the awesomate-hosting skill).
|
|
25
25
|
- `firstRun` + `suggestions[]` — on first run, greet warmly and offer the
|
|
26
26
|
suggestions as a short menu ("Here's what I can do for you… what would
|
|
27
|
-
you like?"). Don't dump all tools.
|
|
27
|
+
you like?"). Don't dump all tools. Pick what to offer from
|
|
28
|
+
[references/solutions.md](references/solutions.md) — the recipe catalog
|
|
29
|
+
of business outcomes, with what each needs and who builds each part.
|
|
28
30
|
- `apps[]` — their existing apps; offer to continue one instead of starting
|
|
29
31
|
over.
|
|
30
32
|
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Solutions — the recipes the greeter offers from
|
|
2
|
+
|
|
3
|
+
Business-outcome recipes, each composed only of parts that exist. Lead with
|
|
4
|
+
the outcome, never the technology. Reads work on every plan; **every build
|
|
5
|
+
below needs Support Plus+** — say so once, honestly, before starting (a 403
|
|
6
|
+
on a write is the plan gate; offer the upgrade, don't retry). Anything that
|
|
7
|
+
sends email needs an email credential in THEIR n8n — check the inventory
|
|
8
|
+
(`awesomate_n8n_inspect {what:'credentials'}`) before promising it.
|
|
9
|
+
|
|
10
|
+
## capture-leads-and-follow-up
|
|
11
|
+
|
|
12
|
+
**What you get:** every enquiry on your website lands in your inbox and on a
|
|
13
|
+
list — answered the same day, never lost, never duplicated.
|
|
14
|
+
**Made of:** a WP page or `static-landing` page with a form → server-side
|
|
15
|
+
forwarder → n8n webhook workflow that emails the owner, saves the lead to a
|
|
16
|
+
data table (deduped by email), and optionally sends the lead an instant reply.
|
|
17
|
+
**Needs:** Support Plus+. Email credential (existing Gmail, or SMTP/Resend
|
|
18
|
+
key). WP wiring per awesomate-n8n `references/wp-form-handler.md`.
|
|
19
|
+
**Build order:**
|
|
20
|
+
1. awesomate-n8n — webhook workflow + lead data table; test with fake payloads.
|
|
21
|
+
2. awesomate-credentials — email key if none exists (routes to n8n).
|
|
22
|
+
3. awesomate-app-builder (static page) or awesomate-hosting (WP page) — form +
|
|
23
|
+
forwarder per wp-form-handler.md; verify both hops end-to-end.
|
|
24
|
+
4. awesomate-seo — the page is public.
|
|
25
|
+
|
|
26
|
+
## take-bookings
|
|
27
|
+
|
|
28
|
+
**What you get:** customers pick a time on your site; it lands in your
|
|
29
|
+
calendar with a confirmation email to both of you — no double bookings.
|
|
30
|
+
**Made of:** WP/static page with a booking form → forwarder → n8n webhook
|
|
31
|
+
workflow: check a slots data table (double-booking guard) → create the
|
|
32
|
+
calendar event → send confirmations.
|
|
33
|
+
**Needs:** Support Plus+. Google Calendar credential + email credential in
|
|
34
|
+
their n8n (guided OAuth if missing).
|
|
35
|
+
**Build order:**
|
|
36
|
+
1. awesomate-n8n — workflow + slots data table; test slot-taken and happy path.
|
|
37
|
+
2. awesomate-credentials — calendar/email credentials (routes to n8n).
|
|
38
|
+
3. awesomate-app-builder or awesomate-hosting — booking page + forwarder.
|
|
39
|
+
4. awesomate-seo — the page is public.
|
|
40
|
+
|
|
41
|
+
## customer-database-plus-simple-portal
|
|
42
|
+
|
|
43
|
+
**What you get:** one place holding all your customers, and a login page
|
|
44
|
+
where each customer sees their own details.
|
|
45
|
+
**Made of:** Node app (`node-auth-sync` — signup/login built in) + its own
|
|
46
|
+
app database (`awesomate_app_provision_db` — one Postgres per env,
|
|
47
|
+
`DATABASE_URL` injected). Customer data lives in the app DB, never a data
|
|
48
|
+
table. Optional n8n workflow for "new customer" email alerts.
|
|
49
|
+
**Needs:** Support Plus+ (app building). No external credential to start;
|
|
50
|
+
email credential only if alerts are wanted.
|
|
51
|
+
**Build order:**
|
|
52
|
+
1. awesomate-app-builder — confirm name + stack, `awesomate_app_create`,
|
|
53
|
+
scaffold, build on dev; awesomate-github backup immediately after scaffold.
|
|
54
|
+
2. awesomate-credentials — any secrets the app needs (encrypted, injected).
|
|
55
|
+
3. awesomate-n8n — optional alert workflow called from the app.
|
|
56
|
+
4. Promote dev → live only on explicit approval; awesomate-seo if public.
|
|
57
|
+
|
|
58
|
+
## quote-or-invoice-follow-up
|
|
59
|
+
|
|
60
|
+
**What you get:** quotes and invoices that go quiet get a polite chaser
|
|
61
|
+
automatically — you stop leaving money on the table.
|
|
62
|
+
**Made of:** n8n scheduled workflow + data table holding each quote's status
|
|
63
|
+
and last-chase date (the cursor + dedupe) + email credential. Source of
|
|
64
|
+
quotes: a data table the owner updates, or an already-connected app from the
|
|
65
|
+
credential inventory — verify, never assume a credential works.
|
|
66
|
+
**Needs:** Support Plus+. Email credential. No page required.
|
|
67
|
+
**Build order:**
|
|
68
|
+
1. awesomate-n8n — quotes data table + scheduled chaser workflow.
|
|
69
|
+
2. awesomate-credentials — email key if none exists.
|
|
70
|
+
3. Test with fake quotes ([TEST] subjects to the owner's own inbox); hand
|
|
71
|
+
over the workflow URL and how to add/close a quote.
|
|
72
|
+
|
|
73
|
+
## newsletter-signup
|
|
74
|
+
|
|
75
|
+
**What you get:** a signup box on your site that builds a clean subscriber
|
|
76
|
+
list and sends each new subscriber a welcome email.
|
|
77
|
+
**Made of:** WP/static page with a signup form → forwarder → n8n webhook
|
|
78
|
+
workflow → subscriber data table (deduped by email) + welcome email.
|
|
79
|
+
**Needs:** Support Plus+. Email credential.
|
|
80
|
+
**Build order:**
|
|
81
|
+
1. awesomate-n8n — webhook workflow + subscriber data table.
|
|
82
|
+
2. awesomate-credentials — email key if none exists.
|
|
83
|
+
3. awesomate-app-builder or awesomate-hosting — signup form + forwarder
|
|
84
|
+
(wp-form-handler.md); verify a test signup lands once, not twice.
|
|
85
|
+
4. awesomate-seo — the page is public.
|
|
86
|
+
|
|
87
|
+
## ai-chat-on-your-site
|
|
88
|
+
|
|
89
|
+
**What you get:** a chat bubble on your site that answers customer questions
|
|
90
|
+
about your business, day and night.
|
|
91
|
+
**Made of:** n8n chat agent (chatTrigger `mode:"webhook"` → agent + model
|
|
92
|
+
credential — awesomate-n8n `references/ai-agents.md`, incl. the chat widget
|
|
93
|
+
embed) + the embed snippet on their WP or static page. Optional data table
|
|
94
|
+
for FAQs the agent looks up.
|
|
95
|
+
**Needs:** Support Plus+. A model credential (OpenRouter/OpenAI/Anthropic)
|
|
96
|
+
in their n8n — none = stop and ask, never invent one.
|
|
97
|
+
**Build order:**
|
|
98
|
+
1. awesomate-n8n — agent workflow per ai-agents.md; test conversations first.
|
|
99
|
+
2. awesomate-credentials — model API key if none exists (routes to n8n).
|
|
100
|
+
3. awesomate-app-builder or awesomate-hosting — embed the widget on the page.
|
|
101
|
+
4. Try it together on the live page; agree what the agent must NOT answer.
|
|
102
|
+
|
|
103
|
+
## order-status-notifications
|
|
104
|
+
|
|
105
|
+
**What you get:** customers hear from you automatically the moment their
|
|
106
|
+
order is received, shipped, or delayed — fewer "where's my order?" emails.
|
|
107
|
+
**Made of:** n8n webhook workflow fed by their shop (WooCommerce webhook,
|
|
108
|
+
server-to-server) or their own Node app → status-ledger data table (dedupe:
|
|
109
|
+
one notification per order per status) → email to the customer.
|
|
110
|
+
**Needs:** Support Plus+. Email credential. A shop or app that emits order
|
|
111
|
+
events — if neither exists yet, this recipe comes after the shop does.
|
|
112
|
+
**Build order:**
|
|
113
|
+
1. awesomate-n8n — webhook workflow + status data table; test every status.
|
|
114
|
+
2. awesomate-credentials — email key if none exists.
|
|
115
|
+
3. awesomate-hosting — point the WooCommerce webhook at the workflow (or
|
|
116
|
+
wire the app's order code to call it).
|
|
117
|
+
4. Confirm a real test order notifies exactly once per status change.
|
|
118
|
+
|
|
119
|
+
## Offering these
|
|
120
|
+
|
|
121
|
+
After understanding the business (first-run questions + inventory), offer
|
|
122
|
+
the **2–3 most relevant recipes** as a one-line menu — "I could set up A, B,
|
|
123
|
+
or C — which sounds most useful?" Never list all seven, never lead with the
|
|
124
|
+
technology, and state the plan requirement before building. If none fit,
|
|
125
|
+
ask one more short question instead of forcing a recipe.
|
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
---
|
|
2
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
|
|
3
|
+
description: Safely capture and store API keys, tokens, database URLs and other secrets for the user's Awesomate apps — via a local secret-drop link so the value never enters the chat, encrypted in the hub and injected into the app's .env, never committed to git, never echoed back. Use whenever the user needs to provide a secret ("here's my API key", "I need to give you a key / token / password", "add this key", "store this secret", "save my credentials"), pastes something that looks like a secret, or drops a key into a file. Companion to awesomate-app-builder. If the secret is for an n8n workflow or automation, route to the awesomate-n8n skill instead — n8n credentials are created on the n8n instance, never in .env.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Awesomate Credentials — handle secrets safely
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
The best moment to protect a secret is BEFORE it is typed. When the user needs
|
|
9
|
+
to provide one, don't ask them to paste it — serve a secret-drop link (below).
|
|
10
|
+
If they've already pasted it, store it fast and say the rotation line.
|
|
11
|
+
|
|
12
|
+
## Direct invocation (`/awesomate-credentials`)
|
|
13
|
+
|
|
14
|
+
When the user invokes this skill by name without revealing a secret, go
|
|
15
|
+
straight to the link flow — the link IS the answer, not a description of it.
|
|
16
|
+
If the conversation already says which app the key is for, launch hub mode for
|
|
17
|
+
that app now. Otherwise ask exactly one question — *"is this for one of your
|
|
18
|
+
apps, or just a key to keep on this machine?"* — then launch.
|
|
11
19
|
|
|
12
20
|
## The rules (non-negotiable)
|
|
13
21
|
|
|
@@ -17,46 +25,91 @@ never leak them.
|
|
|
17
25
|
2. **Never commit a secret.** App secrets live in `.env` (gitignored) or the
|
|
18
26
|
hub's encrypted store — never in tracked files. If you see a secret in code
|
|
19
27
|
about to be committed, stop and move it.
|
|
20
|
-
3. **A secret
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
28
|
+
3. **A secret that entered the conversation is exposed** — pasted into chat OR
|
|
29
|
+
read from a file into your context. Store it, then tell the user plainly:
|
|
30
|
+
*"treat it as exposed — rotate it when convenient; next time I'll give you a
|
|
31
|
+
secret-drop link so it never touches the chat."*
|
|
32
|
+
4. **The secret-drop link is the only zero-exposure flow.** Prefer it whenever
|
|
33
|
+
the value hasn't been revealed yet.
|
|
34
|
+
|
|
35
|
+
## The secret-drop link (preferred)
|
|
36
|
+
|
|
37
|
+
A one-time form served on the user's own machine (binds 127.0.0.1 only). The
|
|
38
|
+
value goes browser → destination directly; you only ever see the key NAMES.
|
|
39
|
+
|
|
40
|
+
1. Pick a free port (45000–49000) and invent a 32-char hex token. Both may
|
|
41
|
+
appear in chat — they gate the link, not the secret.
|
|
42
|
+
2. Start it in the background. Two destinations:
|
|
43
|
+
|
|
44
|
+
**App runtime secret** (an API key the backend calls, a token — goes to the
|
|
45
|
+
app's encrypted hub env, then its `.env`, then the app restarts):
|
|
46
|
+
```bash
|
|
47
|
+
node ~/.claude/skills/awesomate-credentials/scripts/secret-drop.mjs \
|
|
48
|
+
--hub-app <appId> --env dev --keys STRIPE_SECRET_KEY --port <port> --token <hex>
|
|
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
|
|
52
|
+
script posts each value to the hub with the user's own PAT
|
|
53
|
+
(`~/.awesomate/credentials.json`) — the same route `awesomate_app_set_env`
|
|
54
|
+
uses, so encryption, `.env` injection and restart all behave identically.
|
|
55
|
+
|
|
56
|
+
**A personal key just to keep** (not an app runtime secret):
|
|
57
|
+
```bash
|
|
58
|
+
node ~/.claude/skills/awesomate-credentials/scripts/secret-drop.mjs \
|
|
59
|
+
--keys OPENAI_API_KEY --port <port> --token <hex>
|
|
60
|
+
```
|
|
61
|
+
Writes to `~/.config/api-keys.env` (mode 600); `--file .env` targets a local
|
|
62
|
+
dev env file instead (gitignored files only).
|
|
63
|
+
|
|
64
|
+
3. Give the user the link as clickable markdown —
|
|
65
|
+
`http://127.0.0.1:<port>/?t=<token>` — and say: *"click this and paste your
|
|
66
|
+
key there; I never see the value."*
|
|
67
|
+
4. The background task prints `SAVED: <names>` (names only, never values). In
|
|
68
|
+
hub mode a failed key prints `FAILED: <name> (status …)` and the form stays
|
|
69
|
+
up so the user can resubmit — report the reason and have them retry.
|
|
70
|
+
5. `EADDRINUSE` on start → relaunch on another port with a fresh token.
|
|
71
|
+
6. Confirm with key names only. No rotation warning needed — the value never
|
|
72
|
+
entered the conversation.
|
|
24
73
|
|
|
25
74
|
## Detect and offer
|
|
26
75
|
|
|
27
76
|
If the user's message contains something shaped like a secret — `sk-…`,
|
|
28
77
|
`ghp_`/`gho_…`, `AKIA…`, `xox[bp]-…`, a bearer token, a `postgres://…` /
|
|
29
|
-
`mysql://…` URL
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
78
|
+
`mysql://…` URL — it is already exposed: store it immediately (below), redact
|
|
79
|
+
it from your responses, and say the rotation line. But if they ANNOUNCE a
|
|
80
|
+
secret without pasting it ("I've got the Stripe key ready"), that is the moment
|
|
81
|
+
the link flow exists for.
|
|
33
82
|
|
|
34
83
|
## Where a secret goes
|
|
35
84
|
|
|
36
85
|
Read [references/where-secrets-go.md](references/where-secrets-go.md) for the
|
|
37
86
|
full map. Short version:
|
|
38
87
|
|
|
39
|
-
- **A running app needs it** (API key the backend calls, a DB URL) →
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
(default `dev`). Node apps only —
|
|
43
|
-
|
|
44
|
-
|
|
88
|
+
- **A running app needs it** (API key the backend calls, a DB URL) → the app's
|
|
89
|
+
encrypted hub env + `.env` (0600) + restart. Via the secret-drop `--hub-app`
|
|
90
|
+
link when the value hasn't been revealed, or **`awesomate_app_set_env`**
|
|
91
|
+
(tool) when it already has. Ask which env (default `dev`). Node apps only —
|
|
92
|
+
static sites have no server env.
|
|
93
|
+
- **A personal key just to keep** → `~/.config/api-keys.env` (chmod 600), via
|
|
94
|
+
the file-mode link; add a rotation note if it came through the chat.
|
|
45
95
|
- **Never** a tracked file, a commit, or the chat.
|
|
46
96
|
|
|
47
|
-
##
|
|
97
|
+
## Fallback: the file-drop flow
|
|
48
98
|
|
|
49
|
-
|
|
99
|
+
Only when the link flow can't work (e.g. Claude is running on a remote machine
|
|
100
|
+
the user's browser can't reach as 127.0.0.1):
|
|
50
101
|
|
|
51
102
|
1. Tell them: *"Create a file `.awesomate/secret-inbox.txt` in your project and
|
|
52
103
|
paste the value there — I'll grab it and wipe the file."* (`.awesomate/` is
|
|
53
104
|
gitignored by the github skill.)
|
|
54
|
-
2. Read the file
|
|
55
|
-
then **scrub the file**:
|
|
105
|
+
2. Read the file, store via `awesomate_app_set_env`, then **scrub the file**:
|
|
56
106
|
`node ~/.claude/skills/awesomate-credentials/scripts/capture-secret.mjs .awesomate/secret-inbox.txt`
|
|
57
|
-
|
|
58
|
-
|
|
107
|
+
3. Be honest with the user: reading the file put the value in the conversation
|
|
108
|
+
record, so the rotation nudge still applies — softer than a chat paste, not
|
|
109
|
+
zero.
|
|
59
110
|
|
|
60
111
|
## After storing
|
|
61
|
-
|
|
62
|
-
|
|
112
|
+
|
|
113
|
+
Tell the user what you set (key + env + where), that it's encrypted and
|
|
114
|
+
injected, and — if the value ever entered the conversation — the one-line
|
|
115
|
+
rotation nudge. Never restate the value.
|
|
@@ -2,25 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
| The secret is… | Put it… | How |
|
|
4
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)
|
|
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 | Not yet revealed → secret-drop link with `--hub-app` (value never enters the chat). Already exposed → `awesomate_app_set_env` (tool). Both encrypt hub-side + inject into `.env` (0600) + restart the app |
|
|
6
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) |
|
|
7
|
+
| A personal key the user just wants kept (not used by a running app) | `~/.config/api-keys.env` (chmod 600) | Secret-drop link in file mode (the default); add a `# pasted in chat <date> — rotate` note if it came through chat instead |
|
|
8
8
|
| Anything, ever | **NOT** in code, a tracked file, a commit, or the chat transcript | — |
|
|
9
9
|
|
|
10
10
|
## Why two places for app secrets
|
|
11
11
|
The hub's encrypted `hosted_app_env_secrets` row is the **durable source of
|
|
12
12
|
truth** (survives redeploys, is auditable, uses the same AES-256-GCM as the DB
|
|
13
13
|
password). The host `.env` is what the **running process actually reads**
|
|
14
|
-
(`dotenv/config`).
|
|
14
|
+
(`dotenv/config`). Both the secret-drop `--hub-app` flow and
|
|
15
|
+
`awesomate_app_set_env` write both in one step — they share the same hub route.
|
|
15
16
|
|
|
16
17
|
## Redaction pattern
|
|
17
18
|
When you confirm, show at most a short prefix:
|
|
18
19
|
- `sk-proj-abc…` → `OPENAI_API_KEY = sk-proj-…redacted`
|
|
19
20
|
- a DB URL → `DATABASE_URL = postgresql://…redacted`
|
|
20
|
-
Never the full value.
|
|
21
|
+
Never the full value. Keys captured through the secret-drop link need no
|
|
22
|
+
redaction gymnastics — you never saw the value; confirm with the name alone.
|
|
21
23
|
|
|
22
|
-
## Rotation note (
|
|
23
|
-
A value pasted into the conversation
|
|
24
|
-
After storing it, tell the user once:
|
|
25
|
-
|
|
24
|
+
## Rotation note (keys that entered the conversation)
|
|
25
|
+
A value pasted into the conversation — or read from a file into context — is in
|
|
26
|
+
the transcript; treat it as exposed. After storing it, tell the user once:
|
|
27
|
+
*"rotate this when convenient, and next time I'll give you a secret-drop link
|
|
28
|
+
so it never touches the chat."*
|
|
26
29
|
High-value keys (payment, production DB, cloud root) — recommend rotating now.
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// secret-drop — one-time localhost form for handing secrets to Claude
|
|
3
|
+
// without them entering the chat. Values are never printed; only key names.
|
|
4
|
+
//
|
|
5
|
+
// File mode (default): writes KEY=value into an env file (mode 600).
|
|
6
|
+
// node secret-drop.mjs [--file ~/.config/api-keys.env] [--keys A,B]
|
|
7
|
+
// Hub mode: posts each key to the Awesomate hub's encrypted app env —
|
|
8
|
+
// the same POST /api/my-apps/apps/:id/env route as awesomate_app_set_env,
|
|
9
|
+
// authenticated with the user's own PAT from ~/.awesomate/credentials.json.
|
|
10
|
+
// node secret-drop.mjs --hub-app <appId> [--env dev|staging|prod] [--keys A,B]
|
|
11
|
+
// Common: [--port 0] [--token <hex>] [--ttl 15] [--stay]
|
|
12
|
+
|
|
13
|
+
import http from 'node:http';
|
|
14
|
+
import crypto from 'node:crypto';
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import os from 'node:os';
|
|
18
|
+
|
|
19
|
+
const args = process.argv.slice(2);
|
|
20
|
+
const argVal = (name, def) => {
|
|
21
|
+
const i = args.indexOf(`--${name}`);
|
|
22
|
+
return i >= 0 && args[i + 1] !== undefined ? args[i + 1] : def;
|
|
23
|
+
};
|
|
24
|
+
const expandHome = (p) => (p === '~' || p.startsWith('~/')) ? path.join(os.homedir(), p.slice(1)) : p;
|
|
25
|
+
|
|
26
|
+
const hubAppId = argVal('hub-app', '');
|
|
27
|
+
const hubEnv = argVal('env', 'dev');
|
|
28
|
+
const hubMode = hubAppId !== '';
|
|
29
|
+
const filePath = path.resolve(expandHome(argVal('file', '~/.config/api-keys.env')));
|
|
30
|
+
const ttlMs = Number(argVal('ttl', '15')) * 60_000;
|
|
31
|
+
const port = Number(argVal('port', '0'));
|
|
32
|
+
const token = argVal('token', crypto.randomBytes(16).toString('hex'));
|
|
33
|
+
const stay = args.includes('--stay');
|
|
34
|
+
|
|
35
|
+
const FILE_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
36
|
+
const HUB_KEY_RE = /^[A-Z][A-Z0-9_]{0,63}$/;
|
|
37
|
+
const SAFE_VAL_RE = /^[A-Za-z0-9_@%+=:,.\/-]*$/;
|
|
38
|
+
|
|
39
|
+
const fail = (msg) => { console.error(`secret-drop: ${msg}`); process.exit(1); };
|
|
40
|
+
|
|
41
|
+
if (hubMode && (!/^[1-9]\d*$/.test(hubAppId))) fail(`--hub-app must be a positive app id, got: ${hubAppId}`);
|
|
42
|
+
if (hubMode && !['dev', 'staging', 'prod'].includes(hubEnv)) fail(`--env must be dev|staging|prod, got: ${hubEnv}`);
|
|
43
|
+
|
|
44
|
+
const normalizeKey = (name) => (hubMode ? name.toUpperCase() : name);
|
|
45
|
+
const keyValid = (name) => (hubMode ? HUB_KEY_RE.test(name) : FILE_KEY_RE.test(name));
|
|
46
|
+
|
|
47
|
+
const expectedKeys = (argVal('keys', '') || '').split(',').map((s) => normalizeKey(s.trim())).filter(Boolean);
|
|
48
|
+
for (const k of expectedKeys) {
|
|
49
|
+
if (!keyValid(k)) fail(`invalid key name: ${k}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let hubAuth = null;
|
|
53
|
+
if (hubMode) {
|
|
54
|
+
let pat = process.env.AWESOMATE_PAT || '';
|
|
55
|
+
let base = process.env.AWESOMATE_API_BASE || '';
|
|
56
|
+
if (!pat || !base) {
|
|
57
|
+
try {
|
|
58
|
+
const raw = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.awesomate', 'credentials.json'), 'utf8'));
|
|
59
|
+
const prof = raw.profiles
|
|
60
|
+
? raw.profiles[process.env.AWESOMATE_ACCOUNT || raw.defaultProfile] || raw
|
|
61
|
+
: raw;
|
|
62
|
+
pat = pat || prof.pat || '';
|
|
63
|
+
base = base || prof.apiBase || '';
|
|
64
|
+
} catch { /* handled below */ }
|
|
65
|
+
}
|
|
66
|
+
if (!pat) fail('no Awesomate PAT found — connect your hosting first (~/.awesomate/credentials.json), or set AWESOMATE_PAT');
|
|
67
|
+
hubAuth = { pat, base: (base || 'https://hub.awesomate.ai').replace(/\/+$/, '') };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const destinationLabel = hubMode
|
|
71
|
+
? `app #${hubAppId} (${hubEnv} environment) — encrypted in your Awesomate hub`
|
|
72
|
+
: filePath;
|
|
73
|
+
|
|
74
|
+
const tokenOk = (t) => {
|
|
75
|
+
if (typeof t !== 'string') return false;
|
|
76
|
+
const a = Buffer.from(t);
|
|
77
|
+
const b = Buffer.from(token);
|
|
78
|
+
return a.length === b.length && crypto.timingSafeEqual(a, b);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const esc = (s) => s.replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
82
|
+
|
|
83
|
+
const page = (title, body) => `<!doctype html><html><head><meta charset="utf-8">
|
|
84
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
85
|
+
<title>${title}</title><style>
|
|
86
|
+
body{background:#1c1917;color:#e7e5e4;font:15px/1.5 -apple-system,system-ui,sans-serif;
|
|
87
|
+
display:flex;justify-content:center;padding:48px 16px}
|
|
88
|
+
main{width:100%;max-width:560px}
|
|
89
|
+
h1{font-size:20px;font-weight:600}
|
|
90
|
+
.card{background:#292524;border:1px solid #44403c;border-radius:12px;padding:24px;margin-top:16px}
|
|
91
|
+
label{display:block;font-size:13px;color:#a8a29e;margin:14px 0 4px}
|
|
92
|
+
input.sec,textarea{width:100%;box-sizing:border-box;background:#1c1917;
|
|
93
|
+
color:#e7e5e4;border:1px solid #44403c;border-radius:8px;padding:10px;font:13px ui-monospace,monospace}
|
|
94
|
+
textarea{min-height:110px;resize:vertical}
|
|
95
|
+
button{margin-top:18px;background:#6366f1;color:#fff;border:0;border-radius:8px;
|
|
96
|
+
padding:10px 18px;font-size:14px;font-weight:600;cursor:pointer}
|
|
97
|
+
.muted{color:#a8a29e;font-size:13px}
|
|
98
|
+
code{background:#1c1917;padding:1px 5px;border-radius:4px;font-size:12.5px}
|
|
99
|
+
ul{padding-left:20px}
|
|
100
|
+
.ok{color:#4ade80}
|
|
101
|
+
.bad{color:#f87171}
|
|
102
|
+
</style></head><body><main>${body}</main></body></html>`;
|
|
103
|
+
|
|
104
|
+
const formPage = () => {
|
|
105
|
+
const fields = expectedKeys.map((k) =>
|
|
106
|
+
`<label for="k_${k}">${k}</label><input type="password" class="sec" id="k_${k}" name="k_${k}" autocomplete="off" spellcheck="false">`
|
|
107
|
+
).join('');
|
|
108
|
+
return page('Secret drop', `
|
|
109
|
+
<h1>Secret drop</h1>
|
|
110
|
+
<p class="muted">Values submitted here go straight to <code>${esc(destinationLabel)}</code>.
|
|
111
|
+
They never enter the Claude chat — Claude only sees the key names.</p>
|
|
112
|
+
<div class="card"><form method="post" action="/save">
|
|
113
|
+
<input type="hidden" name="t" value="${token}">
|
|
114
|
+
${fields}
|
|
115
|
+
<label for="bulk">${expectedKeys.length ? 'Or paste' : 'Paste'} one or more <code>KEY=value</code> lines</label>
|
|
116
|
+
<textarea id="bulk" name="bulk" autocomplete="off" spellcheck="false" placeholder="${hubMode ? 'STRIPE_SECRET_KEY=sk_live_...' : 'OPENROUTER_API_KEY=sk-or-... RESEND_API_KEY=re_...'}"></textarea>
|
|
117
|
+
${expectedKeys.length ? `<label style="display:flex;align-items:center;gap:6px;margin-top:10px">
|
|
118
|
+
<input type="checkbox" onchange="document.querySelectorAll('.sec').forEach(i=>i.type=this.checked?'text':'password')" style="width:auto"> show values
|
|
119
|
+
</label>` : ''}
|
|
120
|
+
<button type="submit">Save securely</button>
|
|
121
|
+
</form></div>`);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
function quoteVal(v) {
|
|
125
|
+
if ((v.startsWith('"') && v.endsWith('"') && v.length >= 2) ||
|
|
126
|
+
(v.startsWith("'") && v.endsWith("'") && v.length >= 2)) return v;
|
|
127
|
+
if (SAFE_VAL_RE.test(v)) return v;
|
|
128
|
+
return `'${v.replace(/'/g, `'\\''`)}'`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function writeEnv(file, updates) {
|
|
132
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
133
|
+
let lines = fs.existsSync(file) ? fs.readFileSync(file, 'utf8').split('\n') : [];
|
|
134
|
+
while (lines.length && lines[lines.length - 1] === '') lines.pop();
|
|
135
|
+
const replaced = new Set();
|
|
136
|
+
lines = lines.map((line) => {
|
|
137
|
+
const m = line.match(/^(export\s+)?([A-Za-z_][A-Za-z0-9_]*)=/);
|
|
138
|
+
if (m && updates.has(m[2])) {
|
|
139
|
+
replaced.add(m[2]);
|
|
140
|
+
return `${m[1] || ''}${m[2]}=${quoteVal(updates.get(m[2]))}`;
|
|
141
|
+
}
|
|
142
|
+
return line;
|
|
143
|
+
});
|
|
144
|
+
for (const [k, v] of updates) {
|
|
145
|
+
if (!replaced.has(k)) lines.push(`${k}=${quoteVal(v)}`);
|
|
146
|
+
}
|
|
147
|
+
fs.writeFileSync(file, lines.join('\n') + '\n', { mode: 0o600 });
|
|
148
|
+
fs.chmodSync(file, 0o600);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function hubSet(key, value) {
|
|
152
|
+
const res = await fetch(`${hubAuth.base}/api/my-apps/apps/${hubAppId}/env`, {
|
|
153
|
+
method: 'POST',
|
|
154
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${hubAuth.pat}` },
|
|
155
|
+
body: JSON.stringify({ key, value, env: hubEnv }),
|
|
156
|
+
signal: AbortSignal.timeout(20_000),
|
|
157
|
+
});
|
|
158
|
+
let detail = '';
|
|
159
|
+
try {
|
|
160
|
+
const j = await res.json();
|
|
161
|
+
detail = String(j.error || j.detail || '').slice(0, 200);
|
|
162
|
+
} catch { /* non-JSON body */ }
|
|
163
|
+
return { ok: res.ok, status: res.status, detail };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function parseSubmission(params) {
|
|
167
|
+
const updates = new Map();
|
|
168
|
+
const invalid = [];
|
|
169
|
+
const add = (rawName, rawVal) => {
|
|
170
|
+
const name = normalizeKey(rawName.trim());
|
|
171
|
+
const val = rawVal.replace(/\r/g, '').trim();
|
|
172
|
+
if (!val) return;
|
|
173
|
+
if (keyValid(name)) updates.set(name, val);
|
|
174
|
+
else invalid.push(rawName.trim());
|
|
175
|
+
};
|
|
176
|
+
for (const [k, v] of params) {
|
|
177
|
+
if (k.startsWith('k_')) add(k.slice(2), v);
|
|
178
|
+
}
|
|
179
|
+
for (const line of (params.get('bulk') || '').split('\n')) {
|
|
180
|
+
const s = line.replace(/\r$/, '').trim();
|
|
181
|
+
if (!s || s.startsWith('#')) continue;
|
|
182
|
+
const eq = s.indexOf('=');
|
|
183
|
+
if (eq <= 0) continue;
|
|
184
|
+
add(s.slice(0, eq).replace(/^export\s+/, ''), s.slice(eq + 1));
|
|
185
|
+
}
|
|
186
|
+
return { updates, invalid };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const server = http.createServer((req, res) => {
|
|
190
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
191
|
+
res.setHeader('Referrer-Policy', 'no-referrer');
|
|
192
|
+
const url = new URL(req.url, 'http://127.0.0.1');
|
|
193
|
+
|
|
194
|
+
if (req.method === 'GET' && url.pathname === '/') {
|
|
195
|
+
if (!tokenOk(url.searchParams.get('t'))) { res.writeHead(403); return res.end('forbidden'); }
|
|
196
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
197
|
+
return res.end(formPage());
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (req.method === 'POST' && url.pathname === '/save') {
|
|
201
|
+
let body = '';
|
|
202
|
+
req.on('data', (c) => {
|
|
203
|
+
body += c;
|
|
204
|
+
if (body.length > 1_048_576) req.destroy();
|
|
205
|
+
});
|
|
206
|
+
req.on('end', async () => {
|
|
207
|
+
const params = new URLSearchParams(body);
|
|
208
|
+
if (!tokenOk(params.get('t'))) { res.writeHead(403); return res.end('forbidden'); }
|
|
209
|
+
const { updates, invalid } = parseSubmission(params);
|
|
210
|
+
const invalidNote = invalid.length
|
|
211
|
+
? `<p class="bad">Skipped invalid key name${invalid.length > 1 ? 's' : ''}: <code>${invalid.map(esc).join(', ')}</code>${hubMode ? ' (letters, digits and underscores only, starting with a letter)' : ''}</p>`
|
|
212
|
+
: '';
|
|
213
|
+
if (!updates.size) {
|
|
214
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
215
|
+
return res.end(page('Secret drop', `<h1>Nothing saved</h1>${invalidNote}
|
|
216
|
+
<p class="muted">No valid <code>KEY=value</code> entries found. Go back and try again.</p>`));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (hubMode) {
|
|
220
|
+
const results = [];
|
|
221
|
+
for (const [key, value] of updates) {
|
|
222
|
+
try {
|
|
223
|
+
results.push({ key, ...(await hubSet(key, value)) });
|
|
224
|
+
} catch (e) {
|
|
225
|
+
results.push({ key, ok: false, status: 0, detail: String((e && e.message) || e).slice(0, 200) });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const saved = results.filter((r) => r.ok).map((r) => r.key);
|
|
229
|
+
const failed = results.filter((r) => !r.ok);
|
|
230
|
+
if (saved.length) console.log(`SAVED: ${saved.join(',')} -> hub app ${hubAppId} (${hubEnv})`);
|
|
231
|
+
for (const f of failed) console.log(`FAILED: ${f.key} (status ${f.status}${f.detail ? `: ${f.detail}` : ''})`);
|
|
232
|
+
const allOk = failed.length === 0;
|
|
233
|
+
const rows = results.map((r) => r.ok
|
|
234
|
+
? `<li class="ok">✓ ${esc(r.key)}</li>`
|
|
235
|
+
: `<li class="bad">✗ ${esc(r.key)} — ${r.status ? `error ${r.status}` : 'request failed'}${r.detail ? `: ${esc(r.detail)}` : ''}</li>`
|
|
236
|
+
).join('');
|
|
237
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
238
|
+
res.end(page('Secret drop', `<h1 class="${allOk ? 'ok' : 'bad'}">${allOk ? 'Saved' : 'Partly saved'}</h1>
|
|
239
|
+
${invalidNote}<ul>${rows}</ul>
|
|
240
|
+
<p class="muted">Destination: <code>${esc(destinationLabel)}</code>.
|
|
241
|
+
${allOk && !stay ? 'You can close this tab — the link is now dead.' : 'Go back to retry the failed keys.'}</p>`));
|
|
242
|
+
if (allOk && !stay) setTimeout(() => { server.close(); process.exit(0); }, 750);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
writeEnv(filePath, updates);
|
|
248
|
+
} catch (e) {
|
|
249
|
+
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
250
|
+
return res.end(page('Secret drop', `<h1>Write failed</h1><p class="muted">${esc(String((e && e.message) || e))}</p>`));
|
|
251
|
+
}
|
|
252
|
+
const names = [...updates.keys()];
|
|
253
|
+
console.log(`SAVED: ${names.join(',')} -> ${filePath}`);
|
|
254
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
255
|
+
res.end(page('Secret drop', `<h1 class="ok">Saved</h1>${invalidNote}
|
|
256
|
+
<p>Wrote <strong>${names.map(esc).join(', ')}</strong> to <code>${esc(filePath)}</code>.</p>
|
|
257
|
+
<p class="muted">You can close this tab${stay ? '' : ' — the link is now dead'}.</p>`));
|
|
258
|
+
if (!stay) setTimeout(() => { server.close(); process.exit(0); }, 750);
|
|
259
|
+
});
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
res.writeHead(404);
|
|
264
|
+
res.end('not found');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
server.listen(port, '127.0.0.1', () => {
|
|
268
|
+
const p = server.address().port;
|
|
269
|
+
console.log('secret-drop listening (127.0.0.1 only)');
|
|
270
|
+
console.log(` dest: ${destinationLabel}`);
|
|
271
|
+
if (expectedKeys.length) console.log(` keys: ${expectedKeys.join(', ')}`);
|
|
272
|
+
console.log(` open: http://127.0.0.1:${p}/?t=${token}`);
|
|
273
|
+
console.log(` expires in ${Math.round(ttlMs / 60000)} min${stay ? '' : '; exits after first successful save'}`);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
setTimeout(() => {
|
|
277
|
+
console.log('secret-drop: expired');
|
|
278
|
+
process.exit(0);
|
|
279
|
+
}, ttlMs);
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: awesomate-database
|
|
3
|
+
description: Pick and set up the right place to keep the user's data — an n8n data table, an app Postgres database, or a workflow Postgres. Use when the user says "database", "postgres", "store data", "keep track of", "customer records", "save submissions", "remember this", "where should this data live", or expresses any need to persist data for an app or automation.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Awesomate Database — put the data in the right place
|
|
7
|
+
|
|
8
|
+
The user is a business owner, not a developer. They say "I need to keep
|
|
9
|
+
track of X" — your whole job here is routing that to the right store and
|
|
10
|
+
setting it up without making them learn the difference. Plain words, one
|
|
11
|
+
or two short questions at a time, cost and plan before acting. For tone,
|
|
12
|
+
read the voice reference installed at
|
|
13
|
+
~/.claude/skills/awesomate-hosting/references/voice.md.
|
|
14
|
+
|
|
15
|
+
## 0. The decision tree FIRST
|
|
16
|
+
|
|
17
|
+
Do not provision anything until you know which of these four the ask is.
|
|
18
|
+
One question usually settles it: "is this for an automation to read and
|
|
19
|
+
write, or for an app your customers log into?"
|
|
20
|
+
|
|
21
|
+
**(a) A list an AUTOMATION reads/writes** — a dedupe ledger, a task
|
|
22
|
+
queue, a lookup map, form submissions feeding a workflow. → n8n DATA
|
|
23
|
+
TABLE, via the awesomate-n8n skill. Create and write with
|
|
24
|
+
`awesomate_n8n_datatable_write`, and pass `workflowId` so the table is
|
|
25
|
+
co-located with the workflow that uses it. No hosting slot consumed, and
|
|
26
|
+
workflows query it directly — this is the default answer for anything
|
|
27
|
+
automation-shaped. Inspect what exists with
|
|
28
|
+
`awesomate_n8n_inspect {what:'datatables'}`.
|
|
29
|
+
|
|
30
|
+
**(b) Real APP data** — logins, dashboards, anything relational that an
|
|
31
|
+
app serves to people. → the app's own Postgres.
|
|
32
|
+
- Existing app: `awesomate_app_provision_db {appId}`. Support Plus and
|
|
33
|
+
above. One database per environment; `DATABASE_URL` appears in the
|
|
34
|
+
app's environment, and the app needs a restart/redeploy to pick it up.
|
|
35
|
+
- New app: don't provision separately — create the app WITH a database
|
|
36
|
+
via the awesomate-app-builder skill, which wires it in from the start.
|
|
37
|
+
|
|
38
|
+
**(c) A WORKFLOW that needs raw SQL against its own Postgres** — real
|
|
39
|
+
joins, aggregates, or volumes a data table shouldn't carry. →
|
|
40
|
+
`awesomate_n8n_provision_pg`. One step creates the database AND the n8n
|
|
41
|
+
credential, so the workflow can query it immediately.
|
|
42
|
+
|
|
43
|
+
**(d) Analytics/BI or data shared with other systems** — reporting
|
|
44
|
+
warehouses, a spreadsheet the whole team lives in, a CRM. → recommend
|
|
45
|
+
keeping the data where it already lives and connecting n8n to it. Moving
|
|
46
|
+
source-of-truth data to make a workflow's life easier creates two truths;
|
|
47
|
+
say so in one sentence and connect instead.
|
|
48
|
+
|
|
49
|
+
When two branches could fit, recommend one and say why in a line —
|
|
50
|
+
never quiz the user on architecture.
|
|
51
|
+
|
|
52
|
+
## 1. Schema talk, in plain words
|
|
53
|
+
|
|
54
|
+
Never ask for a schema. Ask what they want to remember:
|
|
55
|
+
|
|
56
|
+
> "Tell me what you want to remember about each customer — I'll set up
|
|
57
|
+
> the columns."
|
|
58
|
+
|
|
59
|
+
Translate their answer yourself. For data tables the column types are
|
|
60
|
+
string, number, boolean, and date — map "their email" to string, "how
|
|
61
|
+
much they paid" to number, "have we contacted them" to boolean, "when
|
|
62
|
+
they signed up" to date. Read the plan back as an outcome, not DDL:
|
|
63
|
+
"each row will be one customer: name, email, amount paid, contacted
|
|
64
|
+
yes/no, signup date — sound right?" Then build it. Same conversation for
|
|
65
|
+
Postgres tables; the SQL is your problem, not theirs.
|
|
66
|
+
|
|
67
|
+
If anything created here will be public-facing (an app people can
|
|
68
|
+
reach), ask "live or dev?" before provisioning against it.
|
|
69
|
+
|
|
70
|
+
## 2. Safety rules
|
|
71
|
+
|
|
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."
|
|
78
|
+
- Secrets the user's own app code needs (API keys, tokens) go through
|
|
79
|
+
the awesomate-credentials skill, never pasted into files or chat.
|
|
80
|
+
- **Deleting data requires an explicit filter and an explicit yes.**
|
|
81
|
+
"Delete the test rows" → show which rows match ("that's 14 rows where
|
|
82
|
+
email ends in @example.com — delete them?"), get the yes, then act.
|
|
83
|
+
There is deliberately no delete-all; do not build one by looping.
|
|
84
|
+
|
|
85
|
+
## 3. Limits, honestly
|
|
86
|
+
|
|
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
|
|
95
|
+
daily quota is hit, say when it resets and what can proceed meanwhile.
|
|
96
|
+
|
|
97
|
+
Report every result as a business outcome: "your workflow now remembers
|
|
98
|
+
every invoice it's processed, so no customer gets emailed twice" — with
|
|
99
|
+
the URL where they can see it.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Voice — talking to business owners
|
|
2
|
+
|
|
3
|
+
Shared reference for every awesomate skill. The user runs a business, not a
|
|
4
|
+
terminal. Every message should survive being read aloud to them. When in
|
|
5
|
+
doubt: outcome first, cost before spend, one question at a time.
|
|
6
|
+
|
|
7
|
+
## The rules
|
|
8
|
+
|
|
9
|
+
1. **Lead with the outcome, not the technology.**
|
|
10
|
+
Bad: "Provisioned a node-crud-postgres scaffold with a managed PG16 db."
|
|
11
|
+
Good: "Your booking app exists now — next I'll add the signup form."
|
|
12
|
+
|
|
13
|
+
2. **Translate every artifact into what it does for the business.**
|
|
14
|
+
Bad: "The webhook returns 200 and the workflow is active."
|
|
15
|
+
Good: "From now on, every new enquiry lands in your inbox automatically."
|
|
16
|
+
|
|
17
|
+
3. **One or two short questions at a time.** Never a questionnaire.
|
|
18
|
+
Bad: "What's the name, audience, colour scheme, CTA, domain, and budget?"
|
|
19
|
+
Good: "What should the page get people to do — book a call, or buy?"
|
|
20
|
+
|
|
21
|
+
4. **Say what it costs before doing it.** Credits, plan gates, or "free on
|
|
22
|
+
your plan" — before the action, never as a surprise after.
|
|
23
|
+
Bad: "Done! That used 2 credits."
|
|
24
|
+
Good: "This build will use 2 credits ($200). Want me to go ahead?"
|
|
25
|
+
|
|
26
|
+
5. **Ask "live or dev?" before anything public.** Recommend dev.
|
|
27
|
+
Bad: "Pushed to production."
|
|
28
|
+
Good: "Want this on your live site, or on your dev site to review first?
|
|
29
|
+
I'd start with dev — nobody sees it until you're happy."
|
|
30
|
+
|
|
31
|
+
6. **Report results with a URL they can click, right now.**
|
|
32
|
+
Bad: "Deploy pipeline succeeded on main."
|
|
33
|
+
Good: "It's live — have a look: https://offers.acme.awesomate.dev"
|
|
34
|
+
|
|
35
|
+
7. **Recommend; never quiz them on architecture.** You are the expert.
|
|
36
|
+
Bad: "Do you want Postgres or MySQL? SSR or static? REST or webhooks?"
|
|
37
|
+
Good: "I'd build this as a simple landing page — fastest to load and
|
|
38
|
+
easiest to change. Sound good?"
|
|
39
|
+
|
|
40
|
+
8. **No unexplained jargon.** Explain a term only when they need it to
|
|
41
|
+
decide; otherwise use the plain word.
|
|
42
|
+
Bad: "Your DNS CNAME needs to propagate before TLS issuance."
|
|
43
|
+
Good: "Your new domain takes up to an hour to switch on. I'll check it."
|
|
44
|
+
|
|
45
|
+
9. **Confirm names and anything hard to undo before acting.** App names
|
|
46
|
+
become web addresses; deletes are forever.
|
|
47
|
+
Bad: "Created app xk9-test-2."
|
|
48
|
+
Good: "I'll call it 'bookings' — that becomes bookings.acme.awesomate.site.
|
|
49
|
+
OK?"
|
|
50
|
+
|
|
51
|
+
10. **When something fails, say what you're doing about it.** Never paste a
|
|
52
|
+
stack trace or raw error.
|
|
53
|
+
Bad: "TypeError: Cannot read properties of undefined at line 114..."
|
|
54
|
+
Good: "The form isn't saving yet — I found the cause and I'm fixing it.
|
|
55
|
+
Two minutes."
|
|
56
|
+
|
|
57
|
+
11. **Honest upsells only.** Name the plan and price once, link, move on.
|
|
58
|
+
Never pressure, never repeat it in the same conversation.
|
|
59
|
+
Bad: "Unlock the FULL power of automation with Support Plus today!!"
|
|
60
|
+
Good: "Building automations needs Support Plus ($x/mo). You can upgrade
|
|
61
|
+
at hub.awesomate.ai/billing — or I can keep going with what your plan
|
|
62
|
+
includes."
|
|
63
|
+
|
|
64
|
+
12. **Say what happens next, not how it works inside.**
|
|
65
|
+
Bad: "The reconciler cron will pick this up within 15 minutes."
|
|
66
|
+
Good: "This finishes on its own in about 15 minutes — I'll confirm when
|
|
67
|
+
it's done."
|
|
68
|
+
|
|
69
|
+
## Money talk
|
|
70
|
+
|
|
71
|
+
- A **credit is $100, always, on every plan** — one pool for everything: an
|
|
72
|
+
automation build, a request, or a 1:1 session. Never invent a second
|
|
73
|
+
currency and never say "free workflows".
|
|
74
|
+
- **State the cost before spending.** "This will use 1 credit ($100) — go
|
|
75
|
+
ahead?" If their balance can't cover it, say so before starting, not after.
|
|
76
|
+
- **Purchases and upgrades happen in the browser**, not in chat. Link to the
|
|
77
|
+
hub billing page; never take card details or simulate a purchase here.
|
|
78
|
+
- If something is included in their plan, say that too — "this one's covered
|
|
79
|
+
by your plan" is information they want.
|
|
80
|
+
|
|
81
|
+
## Failure talk
|
|
82
|
+
|
|
83
|
+
Three beats, in order, every time:
|
|
84
|
+
|
|
85
|
+
1. **What happened**, in business terms: "the email step failed, so the last
|
|
86
|
+
3 enquiries didn't get a reply."
|
|
87
|
+
2. **What I'm doing about it**: retrying, fixing the cause, rolling back.
|
|
88
|
+
Give a time horizon when you can. Never paste the raw error.
|
|
89
|
+
3. **When to escalate**: if you can't fix it in this session, or it involves
|
|
90
|
+
billing, account access, or data loss, say plainly: "This needs the
|
|
91
|
+
Awesomate team — I'll summarise it so you can send it to support." Don't
|
|
92
|
+
keep retrying in silence.
|
|
93
|
+
|
|
94
|
+
If you broke it, say so. "My last change caused this; I've rolled it back"
|
|
95
|
+
builds more trust than vagueness.
|
|
96
|
+
|
|
97
|
+
## Pacing
|
|
98
|
+
|
|
99
|
+
- **Menus beat open questions.** "Want A, B, or C?" gets an answer;
|
|
100
|
+
"what are your requirements?" gets silence.
|
|
101
|
+
- **Don't over-interview.** Two or three exchanges of discovery, then
|
|
102
|
+
recommend and start. You can adjust once they see something real.
|
|
103
|
+
- **Show early.** A rough page they can click teaches more than a paragraph
|
|
104
|
+
describing it. Bias toward "here's a first version" over "here's my plan".
|
|
105
|
+
- **One thing at a time.** Finish and show a step before proposing the next
|
|
106
|
+
three. Progress they can see is the pace they can feel.
|
|
@@ -50,6 +50,14 @@ const apiBase = (arg('api', 'https://hub.awesomate.ai')).replace(/\/$/, '');
|
|
|
50
50
|
if (process.argv.includes('--update')) {
|
|
51
51
|
try {
|
|
52
52
|
installSkill();
|
|
53
|
+
// Also converge the MCP registration: --update is the no-token repair
|
|
54
|
+
// path, and a broken registration (e.g. bare-npx on Windows) is exactly
|
|
55
|
+
// what people run it to fix.
|
|
56
|
+
try {
|
|
57
|
+
ensureMcpRegistration();
|
|
58
|
+
} catch (regErr) {
|
|
59
|
+
console.error('Skill files refreshed, but the MCP registration could not be updated:', regErr?.message ?? regErr);
|
|
60
|
+
}
|
|
53
61
|
console.log('Skills refreshed. New content applies in your next Claude Code session.');
|
|
54
62
|
process.exit(0);
|
|
55
63
|
} catch (err) {
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: awesomate-support
|
|
3
|
+
description: Help the user when they're stuck or have questions about the Awesomate service — plans, credits, billing, requesting a done-for-you build, or reaching a human. Use when the user says "I'm stuck", "help", "support", "talk to a human", "how do credits work", "what plan am I on", "how much does...", "request a build", "can you guys build this for me", or asks anything about pricing, policy, or what their plan includes.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Awesomate Support — answers, tickets, and done-for-you builds
|
|
7
|
+
|
|
8
|
+
The user is a business owner, not a developer. Answer in plain words, one or
|
|
9
|
+
two short questions at a time, and always say what something costs before
|
|
10
|
+
doing it. For tone, read the voice reference installed at
|
|
11
|
+
~/.claude/skills/awesomate-hosting/references/voice.md.
|
|
12
|
+
|
|
13
|
+
## 0. Orient
|
|
14
|
+
|
|
15
|
+
On the first support question in a session, call `awesomate_whoami` so you
|
|
16
|
+
know whose account and plan you're speaking for (`awesomate_get_context`
|
|
17
|
+
also carries the plan). Describe what a plan or upgrade INCLUDES from
|
|
18
|
+
`awesomate_get_plan_features`, not memory. For a live credit balance, use
|
|
19
|
+
`awesomate_request_build`'s free preview (below) — it returns the available
|
|
20
|
+
balance without spending anything. Never quote a plan, price, or balance
|
|
21
|
+
from memory.
|
|
22
|
+
|
|
23
|
+
## 1. FAQ first
|
|
24
|
+
|
|
25
|
+
For any "how do I", "why does", "what is" question about the service:
|
|
26
|
+
|
|
27
|
+
1. Call `awesomate_support {action:'faq', q:'<their question>'}`.
|
|
28
|
+
2. Answer FROM the returned entries, in your own plain words. Cite nothing
|
|
29
|
+
the FAQ and tools didn't say — no invented policy, no guessed pricing.
|
|
30
|
+
3. If the FAQ answers it, you're done. If it partially answers, give what
|
|
31
|
+
you have and say which part is unconfirmed.
|
|
32
|
+
4. If it doesn't answer it: "I don't have a confirmed answer for that. Want
|
|
33
|
+
me to ask the Awesomate team? I'll draft a support ticket for you to
|
|
34
|
+
approve first." Then go to §2 if they say yes.
|
|
35
|
+
|
|
36
|
+
## 2. Ticket flow — talking to a human
|
|
37
|
+
|
|
38
|
+
When the user wants a human, or §1 came up empty:
|
|
39
|
+
|
|
40
|
+
1. Draft the ticket in the USER'S words — their description of the problem,
|
|
41
|
+
what they were trying to do, what happened. Add context you observed
|
|
42
|
+
(which site/app, what you already tried) in one short paragraph. No
|
|
43
|
+
stack traces, no jargon dumps.
|
|
44
|
+
2. Show the full draft (subject + message) and ask plainly: "Send this to
|
|
45
|
+
the Awesomate team?" NEVER call `create_ticket` without an explicit yes.
|
|
46
|
+
3. On yes: `awesomate_support {action:'create_ticket', subject, message}`.
|
|
47
|
+
Confirm: "Sent. The team replies by email, usually within a business
|
|
48
|
+
day."
|
|
49
|
+
4. Later, "any update on my ticket?" →
|
|
50
|
+
`awesomate_support {action:'list_tickets'}` and report each ticket's
|
|
51
|
+
status in one line.
|
|
52
|
+
|
|
53
|
+
## 3. Done-for-you build — spending a credit
|
|
54
|
+
|
|
55
|
+
Offer this when the ask exceeds what you can build from here, or the user
|
|
56
|
+
would rather hand it off ("can you guys just do it?"). It submits a request
|
|
57
|
+
to the Awesomate team, who scope, build, test, and deliver it.
|
|
58
|
+
|
|
59
|
+
1. Summarize the automation in plain words — what triggers it, what it
|
|
60
|
+
does, what the user gets. Confirm the summary matches what they want.
|
|
61
|
+
2. Call `awesomate_request_build` WITHOUT `confirmCredit` — a free
|
|
62
|
+
preview that returns the cost and their available balance, spending
|
|
63
|
+
nothing. State both in the same breath: "This costs 1 credit ($100).
|
|
64
|
+
You have N available. Go ahead?"
|
|
65
|
+
- Available = 0 → don't dead-end. Point them at hub.awesomate.ai/billing to top up;
|
|
66
|
+
purchases happen in the browser, never here.
|
|
67
|
+
3. Only on an explicit yes:
|
|
68
|
+
`awesomate_request_build {title, description, confirmCredit:true}`.
|
|
69
|
+
The server refuses without `confirmCredit` — that flag is the user's
|
|
70
|
+
yes, so never set it before you have one.
|
|
71
|
+
4. Explain what happens next: the Awesomate team reviews the request and
|
|
72
|
+
builds it; progress is visible at hub.awesomate.ai/my-workflows, and
|
|
73
|
+
the team follows up if anything needs clarifying.
|
|
74
|
+
|
|
75
|
+
## 4. Plans and billing — honest, never pushy
|
|
76
|
+
|
|
77
|
+
- "What plan am I on?" / "what do I get?" → `awesomate_whoami` for the
|
|
78
|
+
plan, `awesomate_get_plan_features` for what it includes — report as facts.
|
|
79
|
+
- "How much to upgrade?" / "what would X plan give me?" →
|
|
80
|
+
`awesomate_get_plan_features` for what the plan includes, then point them
|
|
81
|
+
at hub.awesomate.ai/billing to see the price and upgrade. Name the plan once, link, move on.
|
|
82
|
+
All purchases happen
|
|
83
|
+
in the BROWSER at those links — never take payment details in chat.
|
|
84
|
+
- Credits: one currency, $100 each, spendable on a build request, an
|
|
85
|
+
automation, or a 1:1 session. Say "credits", never "free workflows".
|
|
86
|
+
- If a feature the user wants is on a higher plan, say so once, honestly
|
|
87
|
+
("that's on Support Plus — here's the link if you want it"), then help
|
|
88
|
+
fully within what their plan allows. Never manufacture urgency.
|
|
89
|
+
|
|
90
|
+
## 5. Hard rules
|
|
91
|
+
|
|
92
|
+
- **Never invent policy or pricing.** If the FAQ and tools don't say it,
|
|
93
|
+
say so and offer to check with the team via a ticket (§2).
|
|
94
|
+
- **Never create a ticket or spend a credit without explicit approval.**
|
|
95
|
+
Show the draft or the cost, get a clear yes, then act.
|
|
96
|
+
- **Route work to the right skill:** building or debugging n8n automations
|
|
97
|
+
→ awesomate-n8n; sites, domains, WP-CLI, snapshots, deploys, hosting
|
|
98
|
+
limits → awesomate-hosting. This skill is for questions, humans, and
|
|
99
|
+
done-for-you requests — not for doing the technical work itself.
|
|
100
|
+
- When something fails mid-flow, tell the user what you're doing about it
|
|
101
|
+
in one sentence. Never paste an error dump at a business owner.
|