@awesomate/hosting-mcp 0.19.1 → 0.20.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 +234 -24
- package/package.json +2 -2
- package/skill/CHANGELOG.json +16 -2
- package/skill/awesomate-app-builder/SKILL.md +53 -3
- package/skill/awesomate-app-builder/references/stack-decision.md +36 -9
- package/skill/awesomate-app-builder/references/wp-migrate.md +4 -2
- package/skill/awesomate-credentials/SKILL.md +41 -4
- package/skill/awesomate-database/SKILL.md +60 -15
- package/skill/awesomate-github/SKILL.md +49 -8
- package/skill/awesomate-github/scripts/github-setup.mjs +4 -1
- package/skill/awesomate-hosting/SKILL.md +131 -20
- package/skill/awesomate-hosting/references/voice.md +1 -1
- package/skill/awesomate-knowledge/SKILL.md +37 -4
- package/skill/awesomate-knowledge/references/from-your-files.md +121 -0
- package/skill/awesomate-knowledge/references/showing-media.md +59 -0
- package/skill/awesomate-n8n/SKILL.md +37 -11
- package/skill/awesomate-n8n/references/error-handling.md +3 -1
- package/skill/awesomate-n8n/references/platform-notes.md +3 -1
- package/skill/awesomate-n8n/references/recurring-reports.md +13 -6
- package/skill/awesomate-n8n/references/rest-fallback.md +7 -2
- package/skill/awesomate-n8n/references/troubleshooting.md +3 -1
- package/skill/awesomate-seo/SKILL.md +109 -20
- package/skill/awesomate-support/SKILL.md +59 -12
package/dist/index.js
CHANGED
|
@@ -11593,7 +11593,7 @@ var require_formdata = __commonJS({
|
|
|
11593
11593
|
var { File: NativeFile } = __require("node:buffer");
|
|
11594
11594
|
var nodeUtil = __require("node:util");
|
|
11595
11595
|
var File2 = globalThis.File ?? NativeFile;
|
|
11596
|
-
var
|
|
11596
|
+
var FormData2 = class _FormData {
|
|
11597
11597
|
constructor(form) {
|
|
11598
11598
|
webidl.util.markAsUncloneable(this);
|
|
11599
11599
|
if (form !== void 0) {
|
|
@@ -11695,8 +11695,8 @@ var require_formdata = __commonJS({
|
|
|
11695
11695
|
return `FormData ${output.slice(output.indexOf("]") + 2)}`;
|
|
11696
11696
|
}
|
|
11697
11697
|
};
|
|
11698
|
-
iteratorMixin("FormData",
|
|
11699
|
-
Object.defineProperties(
|
|
11698
|
+
iteratorMixin("FormData", FormData2, kState, "name", "value");
|
|
11699
|
+
Object.defineProperties(FormData2.prototype, {
|
|
11700
11700
|
append: kEnumerableProperty,
|
|
11701
11701
|
delete: kEnumerableProperty,
|
|
11702
11702
|
get: kEnumerableProperty,
|
|
@@ -11724,7 +11724,7 @@ var require_formdata = __commonJS({
|
|
|
11724
11724
|
}
|
|
11725
11725
|
return { name, value };
|
|
11726
11726
|
}
|
|
11727
|
-
module.exports = { FormData, makeEntry };
|
|
11727
|
+
module.exports = { FormData: FormData2, makeEntry };
|
|
11728
11728
|
}
|
|
11729
11729
|
});
|
|
11730
11730
|
|
|
@@ -11994,7 +11994,7 @@ var require_body = __commonJS({
|
|
|
11994
11994
|
extractMimeType,
|
|
11995
11995
|
utf8DecodeBytes
|
|
11996
11996
|
} = require_util3();
|
|
11997
|
-
var { FormData } = require_formdata();
|
|
11997
|
+
var { FormData: FormData2 } = require_formdata();
|
|
11998
11998
|
var { kState } = require_symbols2();
|
|
11999
11999
|
var { webidl } = require_webidl();
|
|
12000
12000
|
var { Blob: Blob2 } = __require("node:buffer");
|
|
@@ -12214,13 +12214,13 @@ Content-Type: ${value.type || "application/octet-stream"}\r
|
|
|
12214
12214
|
if (parsed === "failure") {
|
|
12215
12215
|
throw new TypeError("Failed to parse body as FormData.");
|
|
12216
12216
|
}
|
|
12217
|
-
const fd = new
|
|
12217
|
+
const fd = new FormData2();
|
|
12218
12218
|
fd[kState] = parsed;
|
|
12219
12219
|
return fd;
|
|
12220
12220
|
}
|
|
12221
12221
|
case "application/x-www-form-urlencoded": {
|
|
12222
12222
|
const entries = new URLSearchParams(value.toString());
|
|
12223
|
-
const fd = new
|
|
12223
|
+
const fd = new FormData2();
|
|
12224
12224
|
for (const [name, value2] of entries) {
|
|
12225
12225
|
fd.append(name, value2);
|
|
12226
12226
|
}
|
|
@@ -18891,7 +18891,7 @@ var require_response = __commonJS({
|
|
|
18891
18891
|
} = require_constants3();
|
|
18892
18892
|
var { kState, kHeaders } = require_symbols2();
|
|
18893
18893
|
var { webidl } = require_webidl();
|
|
18894
|
-
var { FormData } = require_formdata();
|
|
18894
|
+
var { FormData: FormData2 } = require_formdata();
|
|
18895
18895
|
var { URLSerializer } = require_data_url();
|
|
18896
18896
|
var { kConstruct } = require_symbols();
|
|
18897
18897
|
var assert2 = __require("node:assert");
|
|
@@ -19204,7 +19204,7 @@ var require_response = __commonJS({
|
|
|
19204
19204
|
ReadableStream
|
|
19205
19205
|
);
|
|
19206
19206
|
webidl.converters.FormData = webidl.interfaceConverter(
|
|
19207
|
-
|
|
19207
|
+
FormData2
|
|
19208
19208
|
);
|
|
19209
19209
|
webidl.converters.URLSearchParams = webidl.interfaceConverter(
|
|
19210
19210
|
URLSearchParams
|
|
@@ -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
|
|
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)})` : "";
|
|
@@ -39924,6 +39924,56 @@ async function hubRequest(config3, method, path, jsonBody, opts = {}) {
|
|
|
39924
39924
|
}
|
|
39925
39925
|
return body;
|
|
39926
39926
|
}
|
|
39927
|
+
async function hubUploadFile(config3, path, file) {
|
|
39928
|
+
let blob;
|
|
39929
|
+
try {
|
|
39930
|
+
const { openAsBlob } = await import("node:fs");
|
|
39931
|
+
if (typeof openAsBlob === "function") {
|
|
39932
|
+
blob = await openAsBlob(file.localPath, { type: file.contentType });
|
|
39933
|
+
} else {
|
|
39934
|
+
throw new Error("openAsBlob unavailable");
|
|
39935
|
+
}
|
|
39936
|
+
} catch {
|
|
39937
|
+
const { readFile } = await import("node:fs/promises");
|
|
39938
|
+
blob = new Blob([await readFile(file.localPath)], { type: file.contentType });
|
|
39939
|
+
}
|
|
39940
|
+
const form = new FormData();
|
|
39941
|
+
form.append("file", blob, file.filename);
|
|
39942
|
+
const controller = new AbortController();
|
|
39943
|
+
const timer = setTimeout(() => controller.abort(), 6e5);
|
|
39944
|
+
let res;
|
|
39945
|
+
try {
|
|
39946
|
+
res = await (0, import_undici.fetch)(`${config3.apiBase}${path}`, {
|
|
39947
|
+
method: "POST",
|
|
39948
|
+
headers: { Authorization: `Bearer ${config3.pat}` },
|
|
39949
|
+
body: form,
|
|
39950
|
+
signal: controller.signal,
|
|
39951
|
+
dispatcher: proxyDispatcher
|
|
39952
|
+
});
|
|
39953
|
+
} catch (err) {
|
|
39954
|
+
const aborted2 = err instanceof Error && err.name === "AbortError";
|
|
39955
|
+
throw new HubApiError(
|
|
39956
|
+
aborted2 ? "The upload timed out after 10 minutes. Large media can exceed this \u2014 try a smaller file, or upload it in the hub at Knowledge \u2192 Sources." : `Could not reach the Awesomate hub at ${config3.apiBase} to upload. This is a connectivity issue, not an auth problem.`,
|
|
39957
|
+
0,
|
|
39958
|
+
null
|
|
39959
|
+
);
|
|
39960
|
+
} finally {
|
|
39961
|
+
clearTimeout(timer);
|
|
39962
|
+
}
|
|
39963
|
+
const text = await res.text();
|
|
39964
|
+
let body;
|
|
39965
|
+
try {
|
|
39966
|
+
body = text ? JSON.parse(text) : null;
|
|
39967
|
+
} catch {
|
|
39968
|
+
body = text;
|
|
39969
|
+
}
|
|
39970
|
+
if (!res.ok) {
|
|
39971
|
+
const serverMsg = body && typeof body === "object" && "message" in body && typeof body.message === "string" ? body.message : body && typeof body === "object" && "error" in body && typeof body.error === "string" ? body.error : `Upload failed (${res.status})`;
|
|
39972
|
+
const hint = res.status === 413 ? " The file exceeds the 100 MB limit. Split it, compress it, or upload it in the hub at Knowledge \u2192 Sources." : res.status === 402 ? " NOTHING was ingested and nothing was purchased \u2014 the ingestion allowance is exhausted. State the pack price (1 credit = $100) and let the user decide in the hub." : "";
|
|
39973
|
+
throw new HubApiError(`${/[.!?]$/.test(serverMsg) ? serverMsg : `${serverMsg}.`}${hint}`, res.status, body);
|
|
39974
|
+
}
|
|
39975
|
+
return body;
|
|
39976
|
+
}
|
|
39927
39977
|
function hubGet(config3, path) {
|
|
39928
39978
|
return hubRequest(config3, "GET", path);
|
|
39929
39979
|
}
|
|
@@ -40165,7 +40215,7 @@ async function knowledgeSources(config3, args) {
|
|
|
40165
40215
|
if (!args.url && !args.sitemap) {
|
|
40166
40216
|
return {
|
|
40167
40217
|
error: "invalid_request",
|
|
40168
|
-
note: "add needs url or sitemap. For a local FILE
|
|
40218
|
+
note: "add needs url or sitemap. For a local FILE on this machine, use awesomate_knowledge_upload \u2014 it reads the file off disk and streams it, so the bytes never pass through the conversation."
|
|
40169
40219
|
};
|
|
40170
40220
|
}
|
|
40171
40221
|
const body = args.url ? { url: args.url } : { sitemap: args.sitemap, ...args.since ? { since: args.since } : {} };
|
|
@@ -40221,9 +40271,24 @@ function renderAnswer(meta, streamedAnswer, streamError = null) {
|
|
|
40221
40271
|
locator: s.locator ?? s.section_path ?? null,
|
|
40222
40272
|
url: s.url ?? null
|
|
40223
40273
|
}));
|
|
40274
|
+
const answer = typeof meta.answer_plain === "string" && meta.answer_plain ? meta.answer_plain : streamedAnswer;
|
|
40275
|
+
if (sources.length === 0) {
|
|
40276
|
+
return {
|
|
40277
|
+
status: "ok",
|
|
40278
|
+
grounded: false,
|
|
40279
|
+
answer,
|
|
40280
|
+
sources: [],
|
|
40281
|
+
not_in_verified_content: true,
|
|
40282
|
+
configured_fallback: fallback,
|
|
40283
|
+
...typeof meta.score === "number" ? { score: meta.score } : {},
|
|
40284
|
+
...session,
|
|
40285
|
+
note: "UNGROUNDED: the platform returned ok but ZERO sources, which means this answer came from the model, NOT from the verified content. Do NOT present it as an answer from their knowledge base, do not quote it as fact, and do not build on it. Tell the user their content does not cover this. If they need it to refuse instead of improvising, the agent's grounding setting is not strict \u2014 a purpose-built agent (awesomate_knowledge_agents) can be scoped and set strict, and that is what should be wired to anything customer-facing."
|
|
40286
|
+
};
|
|
40287
|
+
}
|
|
40224
40288
|
return {
|
|
40225
40289
|
status: "ok",
|
|
40226
|
-
|
|
40290
|
+
grounded: true,
|
|
40291
|
+
answer,
|
|
40227
40292
|
sources,
|
|
40228
40293
|
...typeof meta.score === "number" ? { score: meta.score } : {},
|
|
40229
40294
|
...session,
|
|
@@ -40574,6 +40639,36 @@ var SKILL_DRIFT = (() => {
|
|
|
40574
40639
|
if (!info.updateAvailable) return null;
|
|
40575
40640
|
return `${info.installedSkillVersion ?? "unversioned"} \u2192 ${SERVER_VERSION}`;
|
|
40576
40641
|
})();
|
|
40642
|
+
function restartGuidance() {
|
|
40643
|
+
const entry = (process.env.CLAUDE_CODE_ENTRYPOINT ?? "").toLowerCase();
|
|
40644
|
+
const inVsCode = entry.includes("vscode") || Boolean(process.env.VSCODE_IPC_HOOK || process.env.VSCODE_PID);
|
|
40645
|
+
if (inVsCode) {
|
|
40646
|
+
return {
|
|
40647
|
+
surface: "vscode",
|
|
40648
|
+
instruction: 'Open the Command Palette (Cmd+Shift+P on macOS, Ctrl+Shift+P on Windows/Linux) and run "Developer: Reload Window".',
|
|
40649
|
+
certain: true
|
|
40650
|
+
};
|
|
40651
|
+
}
|
|
40652
|
+
if (entry.includes("desktop") || entry.includes("app")) {
|
|
40653
|
+
return {
|
|
40654
|
+
surface: "desktop-app",
|
|
40655
|
+
instruction: "Quit the Claude app completely and open it again, then reopen this project.",
|
|
40656
|
+
certain: true
|
|
40657
|
+
};
|
|
40658
|
+
}
|
|
40659
|
+
if (entry === "cli" || entry.includes("cli")) {
|
|
40660
|
+
return {
|
|
40661
|
+
surface: "terminal",
|
|
40662
|
+
instruction: "Type `exit` (or press Ctrl+C twice) to leave Claude Code, then run `claude` again.",
|
|
40663
|
+
certain: true
|
|
40664
|
+
};
|
|
40665
|
+
}
|
|
40666
|
+
return {
|
|
40667
|
+
surface: entry || "unknown",
|
|
40668
|
+
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.",
|
|
40669
|
+
certain: false
|
|
40670
|
+
};
|
|
40671
|
+
}
|
|
40577
40672
|
var SERVER_INSTRUCTIONS = [
|
|
40578
40673
|
"Awesomate connects this account's WordPress hosting, n8n automations, apps and Knowledge Base to Claude.",
|
|
40579
40674
|
"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 +41012,18 @@ server.registerTool(
|
|
|
40917
41012
|
...result,
|
|
40918
41013
|
changed: changed.map((u) => `${u.name}: ${u.from ?? "unversioned"} \u2192 ${u.to}`),
|
|
40919
41014
|
whatsNew: changelogSince(changed[0]?.from ?? null).flatMap((e) => e.highlights),
|
|
40920
|
-
restart: {
|
|
40921
|
-
|
|
40922
|
-
|
|
40923
|
-
|
|
40924
|
-
|
|
40925
|
-
|
|
41015
|
+
restart: (() => {
|
|
41016
|
+
const g = restartGuidance();
|
|
41017
|
+
return {
|
|
41018
|
+
note: "Updated skill files load in the next Claude Code session \u2014 one restart lands both the skills and the (self-updating) server.",
|
|
41019
|
+
// Detected from this session's own environment. Give the user THIS
|
|
41020
|
+
// instruction only — do not read out a list of surfaces.
|
|
41021
|
+
detectedSurface: g.surface,
|
|
41022
|
+
instruction: g.instruction,
|
|
41023
|
+
...g.certain ? {} : { note2: "Surface could not be detected \u2014 ask the user how they run Claude Code instead of guessing steps." },
|
|
41024
|
+
verify: "After restarting, awesomate_get_context should report skill.updateAvailable: false."
|
|
41025
|
+
};
|
|
41026
|
+
})()
|
|
40926
41027
|
});
|
|
40927
41028
|
} catch (err) {
|
|
40928
41029
|
return errorResult(err);
|
|
@@ -41015,7 +41116,7 @@ server.registerTool(
|
|
|
41015
41116
|
server.registerTool(
|
|
41016
41117
|
"awesomate_run_wp_cli",
|
|
41017
41118
|
{
|
|
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.",
|
|
41119
|
+
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
41120
|
inputSchema: {
|
|
41020
41121
|
domain: external_exports.string().describe("The site domain"),
|
|
41021
41122
|
args: external_exports.array(external_exports.string()).describe("WP-CLI args, e.g. ['plugin','list']")
|
|
@@ -41302,7 +41403,6 @@ server.registerTool(
|
|
|
41302
41403
|
server.registerTool(
|
|
41303
41404
|
"awesomate_app_deploy_info",
|
|
41304
41405
|
{
|
|
41305
|
-
annotations: READ_ONLY,
|
|
41306
41406
|
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
41407
|
inputSchema: { appId: external_exports.number().int().positive().describe("The app id") }
|
|
41308
41408
|
},
|
|
@@ -41431,7 +41531,7 @@ server.registerTool(
|
|
|
41431
41531
|
server.registerTool(
|
|
41432
41532
|
"awesomate_knowledge_sources",
|
|
41433
41533
|
{
|
|
41434
|
-
description: "The knowledge base's content sources. action 'list' \u2014 ONE page of sources, most recently ingested first (default 50, max 200 via limit): read `page.has_more`/`next_cursor` and pass cursor to continue \u2014 a page is never the whole library. 'summary' \u2014 exact whole-library counts {total, by_kind, chunks, indexed_chunks, failed_sources}: use THIS to say what the knowledge base contains. 'jobs' \u2014 ingest job statuses (optional status filter: queued|running|succeeded|failed). 'add' \u2014 ingest a public page {url} or a whole site {sitemap, since?}; ALWAYS get explicit approval first (ingest costs money and counts against quota),
|
|
41534
|
+
description: "The knowledge base's content sources. action 'list' \u2014 ONE page of sources, most recently ingested first (default 50, max 200 via limit): read `page.has_more`/`next_cursor` and pass cursor to continue \u2014 a page is never the whole library. 'summary' \u2014 exact whole-library counts {total, by_kind, chunks, indexed_chunks, failed_sources}: use THIS to say what the knowledge base contains. 'jobs' \u2014 ingest job statuses (optional status filter: queued|running|succeeded|failed). 'add' \u2014 ingest a public page {url} or a whole site {sitemap, since?}; ALWAYS get explicit approval first (ingest costs money and counts against quota), for a local FILE on the user's machine use awesomate_knowledge_upload instead (it streams the file from disk; this tool takes URLs only). A pack_required response means the allowance is exhausted: NOTHING was purchased \u2014 present the pack price (1 credit = $100) and let the user buy from the hub if they want it. 'remove' {sourceId} \u2014 deletes the source AND its indexed content; explicit approval required.",
|
|
41435
41535
|
inputSchema: {
|
|
41436
41536
|
action: external_exports.enum(["list", "summary", "add", "remove", "jobs"]),
|
|
41437
41537
|
url: external_exports.string().url().optional().describe("add: one public page / blog post / YouTube link"),
|
|
@@ -41457,7 +41557,7 @@ server.registerTool(
|
|
|
41457
41557
|
"awesomate_knowledge_search",
|
|
41458
41558
|
{
|
|
41459
41559
|
annotations: READ_ONLY,
|
|
41460
|
-
description: "Instant search over the knowledge library with live facet counts: the fastest way to see WHAT is in there and to find the exact video moment, book page, dataset or web section. Returns hits (title, kind, locator like t=612-640 or p.42, snippet with **matched words**, score) plus facets {kind, year, category, author, people, places, topics} whose counts describe the current filters: repeat a facet value to OR within it, combine facets to AND. include_media adds presigned url/poster_url to hits: they expire in minutes, use immediately, never store. Keyword-only and free (no answer quota); for a verified ANSWER use awesomate_knowledge_ask, optionally with the same filters.",
|
|
41560
|
+
description: "Instant search over the knowledge library with live facet counts: the fastest way to see WHAT is in there and to find the exact video moment, book page, dataset or web section. Returns hits (title, kind, locator like t=612-640 or p.42, snippet with **matched words**, score) plus facets {kind, year, category, author, people, places, topics} whose counts describe the current filters: repeat a facet value to OR within it, combine facets to AND. include_media adds presigned url/poster_url to hits: they expire in minutes, use immediately, never store. Keyword-only and free (no answer quota); for a verified ANSWER use awesomate_knowledge_ask, optionally with the same filters. Citations from awesomate_knowledge_ask carry NO media URLs \u2014 when a cited source is an image/video and the user wants to SEE it, re-query here with include_media (ideally filtered by its doc id). The returned url/poster_url expire in minutes: fine to show in chat, never safe to embed in a page \u2014 see the awesomate-knowledge skill's showing-media.md.",
|
|
41461
41561
|
inputSchema: {
|
|
41462
41562
|
q: external_exports.string().max(2e3).optional().describe("Search words; empty lists the library filtered by the facets"),
|
|
41463
41563
|
kind: external_exports.enum(["book", "document", "web", "image", "video", "audio", "post", "dataset"]).optional(),
|
|
@@ -41656,7 +41756,7 @@ server.registerTool(
|
|
|
41656
41756
|
"awesomate_n8n_findings",
|
|
41657
41757
|
{
|
|
41658
41758
|
annotations: READ_ONLY,
|
|
41659
|
-
description: "Findings from Awesomate's automated error analyzer for THIS account (Pro/Embedded + the 'AI
|
|
41759
|
+
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
41760
|
inputSchema: { limit: external_exports.number().int().min(1).max(100).optional() }
|
|
41661
41761
|
},
|
|
41662
41762
|
async ({ limit }) => {
|
|
@@ -41690,7 +41790,7 @@ readTool(
|
|
|
41690
41790
|
);
|
|
41691
41791
|
readTool(
|
|
41692
41792
|
"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
|
|
41793
|
+
"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
41794
|
"/api/client-settings/privacy"
|
|
41695
41795
|
);
|
|
41696
41796
|
server.registerTool(
|
|
@@ -41782,6 +41882,116 @@ server.registerTool(
|
|
|
41782
41882
|
}
|
|
41783
41883
|
}
|
|
41784
41884
|
);
|
|
41885
|
+
server.registerTool(
|
|
41886
|
+
"awesomate_wp_settings",
|
|
41887
|
+
{
|
|
41888
|
+
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.`,
|
|
41889
|
+
inputSchema: {
|
|
41890
|
+
domain: external_exports.string().min(3).max(253).describe("The site domain, e.g. mybusiness.awesomate.site"),
|
|
41891
|
+
title: external_exports.string().min(1).max(200).optional().describe("Site title (blogname)"),
|
|
41892
|
+
tagline: external_exports.string().max(300).optional().describe("Tagline (blogdescription)"),
|
|
41893
|
+
timezone: external_exports.string().max(64).optional(),
|
|
41894
|
+
dateFormat: external_exports.string().max(40).optional(),
|
|
41895
|
+
timeFormat: external_exports.string().max(40).optional(),
|
|
41896
|
+
postsPerPage: external_exports.number().int().min(1).max(100).optional(),
|
|
41897
|
+
searchEngineVisible: external_exports.boolean().optional()
|
|
41898
|
+
}
|
|
41899
|
+
},
|
|
41900
|
+
async ({ domain, ...fields }) => {
|
|
41901
|
+
try {
|
|
41902
|
+
const dom = encodeURIComponent(domain.toLowerCase());
|
|
41903
|
+
const payload = Object.fromEntries(Object.entries(fields).filter(([, v]) => v !== void 0));
|
|
41904
|
+
return textResult(await hubPatch(requireConfig(), `/api/client-hosting/sites/${dom}/settings`, payload));
|
|
41905
|
+
} catch (err) {
|
|
41906
|
+
return errorResult(err);
|
|
41907
|
+
}
|
|
41908
|
+
}
|
|
41909
|
+
);
|
|
41910
|
+
var KB_UPLOAD_TYPES = {
|
|
41911
|
+
".pdf": "application/pdf",
|
|
41912
|
+
".doc": "application/msword",
|
|
41913
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
41914
|
+
".ppt": "application/vnd.ms-powerpoint",
|
|
41915
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
41916
|
+
".xls": "application/vnd.ms-excel",
|
|
41917
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
41918
|
+
".csv": "text/csv",
|
|
41919
|
+
".txt": "text/plain",
|
|
41920
|
+
".md": "text/markdown",
|
|
41921
|
+
".rtf": "application/rtf",
|
|
41922
|
+
".html": "text/html",
|
|
41923
|
+
".htm": "text/html",
|
|
41924
|
+
".json": "application/json",
|
|
41925
|
+
".epub": "application/epub+zip",
|
|
41926
|
+
".mp3": "audio/mpeg",
|
|
41927
|
+
".m4a": "audio/mp4",
|
|
41928
|
+
".wav": "audio/wav",
|
|
41929
|
+
".mp4": "video/mp4",
|
|
41930
|
+
".mov": "video/quicktime",
|
|
41931
|
+
".m4v": "video/x-m4v",
|
|
41932
|
+
".png": "image/png",
|
|
41933
|
+
".jpg": "image/jpeg",
|
|
41934
|
+
".jpeg": "image/jpeg",
|
|
41935
|
+
".webp": "image/webp"
|
|
41936
|
+
};
|
|
41937
|
+
var KB_MAX_UPLOAD_BYTES = 100 * 1024 * 1024;
|
|
41938
|
+
server.registerTool(
|
|
41939
|
+
"awesomate_knowledge_upload",
|
|
41940
|
+
{
|
|
41941
|
+
description: "Ingest ONE file from the user's own computer into their Knowledge Base (Pro+). Pass a LOCAL PATH \u2014 this server runs on their machine and streams the file to the hub itself, so the file contents never pass through the conversation. Handles documents (pdf, docx, pptx, xlsx, csv, txt, md, epub), audio and video (transcribed), and images. Max 100 MB per file; bigger media goes through the hub's Knowledge \u2192 Sources page. INGESTING COSTS MONEY and counts against the monthly allowance, so ALWAYS get explicit approval for the specific file(s) first and say what it will consume. For several files, call once per file and report progress \u2014 do not loop silently. Returns a job; poll awesomate_knowledge_sources {action:'jobs'} until it succeeds, then probe the content with awesomate_knowledge_ask before building anything on it. A pack_required response means the allowance is exhausted: nothing was ingested and nothing was purchased.",
|
|
41942
|
+
inputSchema: {
|
|
41943
|
+
path: external_exports.string().min(1).max(4096).describe("Path to the file on the user's machine (~ is expanded)"),
|
|
41944
|
+
title: external_exports.string().max(300).optional().describe("Display title; defaults to the filename")
|
|
41945
|
+
}
|
|
41946
|
+
},
|
|
41947
|
+
async ({ path: rawPath, title }) => {
|
|
41948
|
+
try {
|
|
41949
|
+
const { statSync, existsSync: existsSync3 } = await import("node:fs");
|
|
41950
|
+
const { resolve: resolve2, basename, extname } = await import("node:path");
|
|
41951
|
+
const expanded = rawPath.startsWith("~") ? join3(homedir3(), rawPath.slice(1).replace(/^[/\\]/, "")) : rawPath;
|
|
41952
|
+
const abs = resolve2(expanded);
|
|
41953
|
+
if (!existsSync3(abs)) {
|
|
41954
|
+
return errorResult(
|
|
41955
|
+
new Error(
|
|
41956
|
+
`No file at ${abs}. Check the path with the user \u2014 a path that works in their shell may differ from this server's working directory, so prefer an absolute path.`
|
|
41957
|
+
)
|
|
41958
|
+
);
|
|
41959
|
+
}
|
|
41960
|
+
const st = statSync(abs);
|
|
41961
|
+
if (st.isDirectory()) {
|
|
41962
|
+
return errorResult(
|
|
41963
|
+
new Error(
|
|
41964
|
+
`${abs} is a folder, not a file. This tool takes one file at a time \u2014 list the folder, agree with the user which files to ingest, then call once per file.`
|
|
41965
|
+
)
|
|
41966
|
+
);
|
|
41967
|
+
}
|
|
41968
|
+
if (!st.isFile()) return errorResult(new Error(`${abs} is not a regular file.`));
|
|
41969
|
+
if (st.size === 0) return errorResult(new Error(`${abs} is empty \u2014 nothing to ingest.`));
|
|
41970
|
+
if (st.size > KB_MAX_UPLOAD_BYTES) {
|
|
41971
|
+
return errorResult(
|
|
41972
|
+
new Error(
|
|
41973
|
+
`${basename(abs)} is ${(st.size / 1024 / 1024).toFixed(1)} MB, over the 100 MB limit. Split or compress it, or have the user upload it in the hub at Knowledge \u2192 Sources.`
|
|
41974
|
+
)
|
|
41975
|
+
);
|
|
41976
|
+
}
|
|
41977
|
+
const ext = extname(abs).toLowerCase();
|
|
41978
|
+
const contentType = KB_UPLOAD_TYPES[ext] ?? "application/octet-stream";
|
|
41979
|
+
const filename = title?.trim() ? `${title.trim()}${ext}` : basename(abs);
|
|
41980
|
+
const result = await hubUploadFile(requireConfig(), "/api/knowledge/sources", {
|
|
41981
|
+
localPath: abs,
|
|
41982
|
+
filename,
|
|
41983
|
+
contentType
|
|
41984
|
+
});
|
|
41985
|
+
return knowledgeResult({
|
|
41986
|
+
...result,
|
|
41987
|
+
uploaded: { path: abs, filename, sizeBytes: st.size, contentType },
|
|
41988
|
+
note: `Queued \u2014 NOT yet ingested. Poll awesomate_knowledge_sources {action:"jobs"} and read the job's status AND error: a job that fails during processing creates no source row, so summary/failed_sources still look clean and the failure is invisible there. Big media is transcribed and can take a while. Only after the job reports succeeded, verify with awesomate_knowledge_ask before building on it.`
|
|
41989
|
+
});
|
|
41990
|
+
} catch (err) {
|
|
41991
|
+
return knowledgeError(err);
|
|
41992
|
+
}
|
|
41993
|
+
}
|
|
41994
|
+
);
|
|
41785
41995
|
server.registerPrompt(
|
|
41786
41996
|
"awesomate-status",
|
|
41787
41997
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@awesomate/hosting-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
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",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"scripts": {
|
|
20
20
|
"build": "esbuild src/index.ts --bundle --platform=node --target=node18 --format=esm --outfile=dist/index.js --banner:js='#!/usr/bin/env node\nimport { createRequire as __awmCreateRequire } from \"node:module\"; const require = __awmCreateRequire(import.meta.url);' --external:node:*",
|
|
21
21
|
"typecheck": "tsc --noEmit",
|
|
22
|
-
"test": "npm run build && esbuild src/config.ts --bundle --platform=node --target=node18 --format=esm --outfile=test/.build/config.mjs --external:node:* && esbuild src/knowledge.ts --bundle --platform=node --target=node18 --format=esm --outfile=test/.build/knowledge.mjs --banner:js='import { createRequire as __awmCreateRequire } from \"node:module\"; const require = __awmCreateRequire(import.meta.url);' --external:node:* && esbuild src/skills.ts --bundle --platform=node --target=node18 --format=esm --outfile=test/.build/skills.mjs --external:node:* && node --test test/*.test.mjs",
|
|
22
|
+
"test": "npm run build && esbuild src/config.ts --bundle --platform=node --target=node18 --format=esm --outfile=test/.build/config.mjs --external:node:* && esbuild src/knowledge.ts --bundle --platform=node --target=node18 --format=esm --outfile=test/.build/knowledge.mjs --banner:js='import { createRequire as __awmCreateRequire } from \"node:module\"; const require = __awmCreateRequire(import.meta.url);' --external:node:* && esbuild src/skills.ts --bundle --platform=node --target=node18 --format=esm --outfile=test/.build/skills.mjs --external:node:* && esbuild src/http.ts --bundle --platform=node --target=node18 --format=esm --outfile=test/.build/http.mjs --banner:js='import { createRequire as __awmCreateRequire } from \"node:module\"; const require = __awmCreateRequire(import.meta.url);' --external:node:* && node --test test/*.test.mjs",
|
|
23
23
|
"prepublishOnly": "npm run typecheck && npm run test"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
package/skill/CHANGELOG.json
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"versions": [
|
|
3
|
+
{
|
|
4
|
+
"version": "0.20.0",
|
|
5
|
+
"highlights": [
|
|
6
|
+
"Add your own documents, audio and video to your Knowledge Base straight from your computer - just point Claude at the files",
|
|
7
|
+
"Claude can take you all the way from your files to a tested chat agent live on your website"
|
|
8
|
+
]
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"version": "0.19.2",
|
|
12
|
+
"highlights": [
|
|
13
|
+
"Change your site title or tagline just by asking - multi-word names now work",
|
|
14
|
+
"Claude tells you how to restart in the way that matches how you actually run it, instead of listing every option"
|
|
15
|
+
]
|
|
16
|
+
},
|
|
3
17
|
{
|
|
4
18
|
"version": "0.19.1",
|
|
5
19
|
"highlights": [
|
|
@@ -20,13 +34,13 @@
|
|
|
20
34
|
"highlights": [
|
|
21
35
|
"Search your knowledge base with filters and ask questions within a slice of your content",
|
|
22
36
|
"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
|
|
37
|
+
"Build AI agents from your own content — drafted, tested privately, and published only when you approve"
|
|
24
38
|
]
|
|
25
39
|
},
|
|
26
40
|
{
|
|
27
41
|
"version": "0.14.0",
|
|
28
42
|
"highlights": [
|
|
29
|
-
"Tools built for business owners, not developers
|
|
43
|
+
"Tools built for business owners, not developers — plain-language answers about your account, plan and limits",
|
|
30
44
|
"New support skill: get help, understand credits, or request a done-for-you build without leaving Claude",
|
|
31
45
|
"New database skill: Claude picks the right place to keep your data and sets it up for you"
|
|
32
46
|
]
|
|
@@ -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?"*
|
|
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.
|
|
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
|
|
51
|
-
they already have: *"You've got Gmail +
|
|
52
|
-
form to email you and add the lead to a
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
64
|
-
|
|
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)
|
|
30
|
-
|
|
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
|