@awesomate/hosting-mcp 0.18.0 → 0.19.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 CHANGED
@@ -39675,7 +39675,7 @@ var StdioServerTransport = class {
39675
39675
  };
39676
39676
 
39677
39677
  // src/index.ts
39678
- import { readFileSync as readFileSync2 } from "node:fs";
39678
+ import { readFileSync as readFileSync3 } from "node:fs";
39679
39679
  import { homedir as homedir3 } from "node:os";
39680
39680
  import { join as join3, dirname as dirname3 } from "node:path";
39681
39681
  import { fileURLToPath as fileURLToPath2 } from "node:url";
@@ -39892,14 +39892,28 @@ async function hubRequest(config3, method, path, jsonBody, opts = {}) {
39892
39892
  }
39893
39893
  if (!res.ok) {
39894
39894
  const serverMsg = body && typeof body === "object" && "error" in body && typeof body.error === "string" ? body.error : `Request failed (${res.status})`;
39895
+ const code = body && typeof body === "object" && "code" in body && typeof body.code === "string" ? body.code : null;
39895
39896
  let hint = "";
39896
39897
  if (res.status === 401) {
39897
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
+ } else if (res.status === 403 && code === "consent_required") {
39900
+ const settingsUrl = body && typeof body === "object" && "settingsUrl" in body && typeof body.settingsUrl === "string" ? body.settingsUrl : "https://hub.awesomate.ai/settings?tab=privacy";
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.`;
39898
39902
  } else if (res.status === 403) {
39899
39903
  const missing = body && typeof body === "object" && "missingScopes" in body ? ` (missing: ${JSON.stringify(body.missingScopes)})` : "";
39900
- hint = ` This plan/token does not allow that action${missing}. Consider suggesting a plan upgrade \u2014 preview it first and get explicit confirmation.`;
39904
+ hint = ` This plan/token does not allow that action${missing}. Relay any upgradeUrl/recommendedPlan in the payload honestly and let the user decide \u2014 never retry.`;
39905
+ } else if (res.status === 402) {
39906
+ hint = " Payment/allowance required \u2014 NOTHING was purchased. State the cost plainly and let the user decide in the hub; this tool never spends money.";
39907
+ } else if (res.status === 404) {
39908
+ hint = " Not found \u2014 usually the resource does not exist for THIS account. Re-check the id/domain before retrying; do not invent one.";
39909
+ } else if (res.status === 409) {
39910
+ hint = " Conflict \u2014 the payload carries the specifics (a limit reached, or a prerequisite missing). Read its code field; do not blind-retry.";
39911
+ } else if (res.status === 422) {
39912
+ hint = " The request was understood and refused \u2014 YOUR arguments were wrong or the remote command failed. The payload says why; fix and retry once.";
39913
+ } else if (res.status === 429) {
39914
+ hint = " Rate/quota limit hit. Wait before retrying; if it is a daily plan quota, relay that honestly instead of looping.";
39901
39915
  } else if (res.status === 503) {
39902
- hint = " The hub is temporarily unable to verify access tokens \u2014 retry shortly.";
39916
+ hint = " The hub is temporarily unable to serve this \u2014 retry shortly.";
39903
39917
  }
39904
39918
  throw new HubApiError(`${serverMsg}.${hint}`, res.status, body);
39905
39919
  }
@@ -39919,28 +39933,70 @@ function hubDelete(config3, path, jsonBody) {
39919
39933
  }
39920
39934
 
39921
39935
  // src/skills.ts
39922
- import { cpSync, existsSync as existsSync2, readdirSync, writeFileSync } from "node:fs";
39936
+ import { cpSync, existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, rmSync, writeFileSync } from "node:fs";
39923
39937
  import { homedir as homedir2 } from "node:os";
39924
39938
  import { join as join2, dirname as dirname2 } from "node:path";
39925
39939
  import { fileURLToPath } from "node:url";
39926
39940
  var PACKAGE_ROOT = join2(dirname2(fileURLToPath(import.meta.url)), "..");
39927
- function installSkills(version2, sourceRoot = join2(PACKAGE_ROOT, "skill")) {
39941
+ var DEFAULT_SOURCE_ROOT = join2(PACKAGE_ROOT, "skill");
39942
+ function bundledSkillNames(sourceRoot = DEFAULT_SOURCE_ROOT) {
39943
+ try {
39944
+ return readdirSync(sourceRoot, { withFileTypes: true }).filter((e) => e.isDirectory() && existsSync2(join2(sourceRoot, e.name, "SKILL.md"))).map((e) => e.name);
39945
+ } catch {
39946
+ return [];
39947
+ }
39948
+ }
39949
+ function installedSkillVersion(name) {
39950
+ try {
39951
+ return readFileSync2(join2(homedir2(), ".claude", "skills", name, ".installed-version"), "utf8").trim() || null;
39952
+ } catch {
39953
+ return null;
39954
+ }
39955
+ }
39956
+ function skillVersions(sourceRoot = DEFAULT_SOURCE_ROOT) {
39957
+ return bundledSkillNames(sourceRoot).map((name) => ({ name, installed: installedSkillVersion(name) }));
39958
+ }
39959
+ function walkFiles(dir, prefix = "") {
39960
+ const out = [];
39961
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
39962
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
39963
+ if (entry.isDirectory()) out.push(...walkFiles(join2(dir, entry.name), rel));
39964
+ else out.push(rel);
39965
+ }
39966
+ return out;
39967
+ }
39968
+ function installSkills(version2, sourceRoot = DEFAULT_SOURCE_ROOT) {
39928
39969
  const destRoot = join2(homedir2(), ".claude", "skills");
39929
39970
  const installed = [];
39971
+ const updates = [];
39972
+ const removedOrphans = [];
39930
39973
  for (const entry of readdirSync(sourceRoot, { withFileTypes: true })) {
39931
39974
  if (!entry.isDirectory()) continue;
39932
39975
  const src = join2(sourceRoot, entry.name);
39933
39976
  if (!existsSync2(join2(src, "SKILL.md"))) continue;
39934
39977
  const dest = join2(destRoot, entry.name);
39978
+ const from = installedSkillVersion(entry.name);
39935
39979
  cpSync(src, dest, { recursive: true });
39980
+ try {
39981
+ const shipped = new Set(walkFiles(src));
39982
+ for (const rel of walkFiles(dest)) {
39983
+ if (rel === ".installed-version") continue;
39984
+ if (!shipped.has(rel)) {
39985
+ rmSync(join2(dest, rel), { force: true });
39986
+ removedOrphans.push(`${entry.name}/${rel}`);
39987
+ }
39988
+ }
39989
+ } catch {
39990
+ }
39936
39991
  try {
39937
39992
  writeFileSync(join2(dest, ".installed-version"), `${version2}
39938
39993
  `);
39939
39994
  } catch {
39940
39995
  }
39941
39996
  installed.push(entry.name);
39997
+ updates.push({ name: entry.name, from, to: version2 });
39942
39998
  }
39943
- return { installed, version: version2, skillsDir: destRoot };
39999
+ return { installed, updates, removedOrphans, version: version2, skillsDir: destRoot };
39944
40000
  }
39945
40001
 
39946
40002
  // src/knowledge.ts
@@ -40463,51 +40519,108 @@ function requireConfig() {
40463
40519
  return config2;
40464
40520
  }
40465
40521
  function accountStamp() {
40522
+ const drift = SKILL_DRIFT ? ` \xB7 skills ${SKILL_DRIFT} \u2014 update ready, run awesomate_skill_update` : "";
40466
40523
  if (!config2) return `account: NOT CONNECTED \u2014 ${configError ?? "unknown error"}`;
40467
- return `account: ${config2.account ?? "unknown (env PAT not matched to a profile)"} (source: ${config2.source})`;
40524
+ return `account: ${config2.account ?? "unknown (env PAT not matched to a profile)"} (source: ${config2.source})${drift}`;
40468
40525
  }
40469
40526
  var SERVER_VERSION = (() => {
40470
40527
  try {
40471
40528
  const pkg = JSON.parse(
40472
- readFileSync2(join3(dirname3(fileURLToPath2(import.meta.url)), "..", "package.json"), "utf8")
40529
+ readFileSync3(join3(dirname3(fileURLToPath2(import.meta.url)), "..", "package.json"), "utf8")
40473
40530
  );
40474
40531
  return pkg.version ?? "0.0.0";
40475
40532
  } catch {
40476
40533
  return "0.0.0";
40477
40534
  }
40478
40535
  })();
40479
- function skillUpdateInfo() {
40480
- let installedSkillVersion = null;
40536
+ var CHANGELOG = (() => {
40481
40537
  try {
40482
- installedSkillVersion = readFileSync2(
40483
- join3(homedir3(), ".claude", "skills", "awesomate-hosting", ".installed-version"),
40484
- "utf8"
40485
- ).trim() || null;
40538
+ const raw = JSON.parse(
40539
+ readFileSync3(join3(dirname3(fileURLToPath2(import.meta.url)), "..", "skill", "CHANGELOG.json"), "utf8")
40540
+ );
40541
+ return Array.isArray(raw.versions) ? raw.versions : [];
40486
40542
  } catch {
40543
+ return [];
40487
40544
  }
40545
+ })();
40546
+ function changelogSince(installed) {
40547
+ if (!installed) return CHANGELOG.slice(0, 3);
40548
+ const idx = CHANGELOG.findIndex((e) => e.version === installed);
40549
+ return idx === -1 ? CHANGELOG.slice(0, 3) : CHANGELOG.slice(0, idx);
40550
+ }
40551
+ function skillUpdateInfo() {
40552
+ const perSkill = skillVersions();
40553
+ const staleSkills = perSkill.filter((s) => s.installed !== SERVER_VERSION).map((s) => s.name);
40554
+ const oldest = perSkill.reduce(
40555
+ (min, s) => s.installed !== null && (min === null || s.installed < min) ? s.installed : min,
40556
+ null
40557
+ );
40558
+ const updateAvailable = perSkill.length === 0 || staleSkills.length > 0;
40488
40559
  return {
40489
40560
  serverVersion: SERVER_VERSION,
40490
- installedSkillVersion,
40491
- updateAvailable: installedSkillVersion !== SERVER_VERSION
40561
+ installedSkillVersion: oldest,
40562
+ updateAvailable,
40563
+ staleSkills,
40564
+ whatsNew: updateAvailable ? changelogSince(oldest).flatMap((e) => e.highlights) : []
40492
40565
  };
40493
40566
  }
40494
- var server = new McpServer({
40495
- name: "awesomate-hosting",
40496
- version: SERVER_VERSION
40497
- });
40567
+ var SKILL_DRIFT = (() => {
40568
+ const info = skillUpdateInfo();
40569
+ if (!info.updateAvailable) return null;
40570
+ return `${info.installedSkillVersion ?? "unversioned"} \u2192 ${SERVER_VERSION}`;
40571
+ })();
40572
+ var SERVER_INSTRUCTIONS = [
40573
+ "Awesomate connects this account's WordPress hosting, n8n automations, apps and Knowledge Base to Claude.",
40574
+ "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.",
40575
+ "The awesomate-* skills are installed on this machine \u2014 load the one matching the task (awesomate-hosting, awesomate-n8n, awesomate-app-builder, awesomate-knowledge, awesomate-seo, awesomate-credentials, awesomate-github, awesomate-database, awesomate-support) before working in that area.",
40576
+ "Reads work on every plan; building/writing generally needs Support Plus or above, and the Knowledge Base needs Pro. Never retry a 403: it carries either an upgrade path (relay it honestly, let the user decide) or a privacy toggle only the user can flip (send them the settings link).",
40577
+ "Every response is stamped `account: <slug>` \u2014 if that is not the account the user expects, stop and fix the connection before doing any work.",
40578
+ SKILL_DRIFT ? `Skill files on this machine are ${SKILL_DRIFT}. Mention it ONCE this session (never mid-task): offer awesomate_skill_update, then a Claude Code restart.` : null
40579
+ ].filter((line) => line !== null).join("\n");
40580
+ var server = new McpServer(
40581
+ {
40582
+ name: "awesomate-hosting",
40583
+ version: SERVER_VERSION
40584
+ },
40585
+ { instructions: SERVER_INSTRUCTIONS }
40586
+ );
40498
40587
  function textResult(data) {
40499
40588
  return {
40500
40589
  content: [{ type: "text", text: `${accountStamp()}
40501
40590
  ${JSON.stringify(data, null, 2)}` }]
40502
40591
  };
40503
40592
  }
40593
+ var ERROR_BODY_FIELDS = [
40594
+ "code",
40595
+ "recommendedPlan",
40596
+ "deepLink",
40597
+ "upgradeUrl",
40598
+ "settingsUrl",
40599
+ "missingScopes",
40600
+ "plan",
40601
+ "allowed"
40602
+ ];
40504
40603
  function errorResult(err) {
40505
40604
  const message = err instanceof Error ? err.message : String(err);
40506
- return { content: [{ type: "text", text: `${accountStamp()}
40507
- ${message}` }], isError: true };
40605
+ let text = `${accountStamp()}
40606
+ ${message}`;
40607
+ if (err instanceof HubApiError && err.body && typeof err.body === "object") {
40608
+ const body = err.body;
40609
+ const details = {};
40610
+ for (const field of ERROR_BODY_FIELDS) {
40611
+ if (body[field] !== void 0 && body[field] !== null) details[field] = body[field];
40612
+ }
40613
+ if (Object.keys(details).length > 0) {
40614
+ text += `
40615
+ ${JSON.stringify(details, null, 2)}`;
40616
+ }
40617
+ }
40618
+ return { content: [{ type: "text", text }], isError: true };
40508
40619
  }
40620
+ var READ_ONLY = { readOnlyHint: true };
40621
+ var DESTRUCTIVE = { destructiveHint: true };
40509
40622
  function readTool(name, description, path) {
40510
- server.registerTool(name, { description, inputSchema: {} }, async () => {
40623
+ server.registerTool(name, { description, inputSchema: {}, annotations: READ_ONLY }, async () => {
40511
40624
  try {
40512
40625
  return textResult(await hubGet(requireConfig(), path));
40513
40626
  } catch (err) {
@@ -40518,6 +40631,7 @@ function readTool(name, description, path) {
40518
40631
  server.registerTool(
40519
40632
  "awesomate_whoami",
40520
40633
  {
40634
+ annotations: READ_ONLY,
40521
40635
  description: "Zero-network identity check: which Awesomate account this session is connected to (slug, plan, token expiry) and WHY (env var, project pin file, sole profile, or default). Call it after connecting, after switching folders, and any time the account in play matters \u2014 if the slug is not the account the user expects, STOP and fix the pin/connection before doing any work.",
40522
40636
  inputSchema: {}
40523
40637
  },
@@ -40541,7 +40655,8 @@ server.registerTool(
40541
40655
  server.registerTool(
40542
40656
  "awesomate_get_context",
40543
40657
  {
40544
- description: "Call this FIRST each session. Returns the connected Awesomate account: plan, capabilities (shell access), plan limits, scopes this token can exercise, token expiry (warn the user if within 7 days), cPanel routing when provisioned, and skill.updateAvailable \u2014 if true, the local Awesomate skill files are older than this server; run awesomate_skill_update to refresh them in place (new content loads next session \u2014 mention once, finish the current task first). If the token is also near expiry, re-running Connect Claude Code from hub.awesomate.ai/sites refreshes skills AND renews the token in one go.",
40658
+ annotations: READ_ONLY,
40659
+ description: "THE session entry point \u2014 call it FIRST, before any domain context. Returns the connected account: plan, capabilities, limits, scopes, token expiry, cPanel routing, PLUS `attention` ({unreadNotifications, erroringWorkflows7d, patExpiresInDays} \u2014 null means unknown, never zero; when something is non-zero, mention it to the user in one line before starting the asked task), `latestMcpVersion` (if serverVersion still lags it after a restart, the npx cache is stale \u2014 remedy: rm -rf ~/.npm/_npx, then restart), and `skill` ({updateAvailable, staleSkills, whatsNew} \u2014 if updateAvailable, mention ONCE with a whatsNew line, offer awesomate_skill_update, never mid-task). If the token is near expiry (patExpiresInDays < 7), re-running Connect Claude Code from hub.awesomate.ai/sites refreshes skills AND renews the token in one go.",
40545
40660
  inputSchema: {}
40546
40661
  },
40547
40662
  async () => {
@@ -40561,6 +40676,7 @@ readTool(
40561
40676
  server.registerTool(
40562
40677
  "awesomate_n8n_deploy",
40563
40678
  {
40679
+ annotations: DESTRUCTIVE,
40564
40680
  description: "Workflow lifecycle writes on the client's n8n, all consent-gated and audited server-side. Actions: 'validate' (structural + n8n-mcp check of workflowJson \u2014 ALWAYS validate before create_draft), 'create_draft' (creates an INACTIVE '[CLI] ' workflow tagged awm:client-cli; returns its webhook URLs), 'activate'/'deactivate' (activation strips the [CLI] prefix; production webhooks respond only while active; agency-managed workflows are refused), 'promote' (swaps a TESTED draft into the live workflow IN PLACE \u2014 live id + webhookIds preserved so external callers keep working; pass workflowId=the LIVE id and draftId=the tested draft; the draft is archived '[promoted <date>]'; response includes operationId for rollback), 'rollback' (restore a promote's pre-swap state \u2014 pass operationId), 'update_draft' (replace an INACTIVE self-built draft's full JSON in place \u2014 pass workflowId + workflowJson; live workflows are refused, use the promote path), 'delete_draft' (inactive self-built drafts only). Get explicit user approval before activate, promote, rollback, and delete_draft. 429 quota_exceeded = daily plan limit; 403 consent_required \u2192 send user to settingsUrl.",
40565
40681
  inputSchema: {
40566
40682
  action: external_exports.enum(["validate", "create_draft", "update_draft", "activate", "deactivate", "promote", "rollback", "delete_draft"]),
@@ -40635,6 +40751,7 @@ server.registerTool(
40635
40751
  server.registerTool(
40636
40752
  "awesomate_n8n_workflows",
40637
40753
  {
40754
+ annotations: READ_ONLY,
40638
40755
  description: "The client's workflows. No id \u2192 EVERY workflow as a node-level summary in one call (nodeCount, nodeTypes, triggers, usesAi, communityNodes, dates; ?active filter + pagination) \u2014 use this for the session's world picture instead of fetching workflows one by one. With id \u2192 that workflow's JSON: detail 'full' (default \u2014 nodes with parameters, connections, settings) or 'structure' (nodes WITHOUT parameters + connections \u2014 cheap shape check for big workflows). 403 consent_required \u2192 send the user to settingsUrl and re-check.",
40639
40756
  inputSchema: {
40640
40757
  id: external_exports.string().optional().describe("Workflow id for a single-workflow read; omit for the all-workflows summary"),
@@ -40670,6 +40787,7 @@ server.registerTool(
40670
40787
  server.registerTool(
40671
40788
  "awesomate_n8n_inspect",
40672
40789
  {
40790
+ annotations: READ_ONLY,
40673
40791
  description: `Instance inventory reads, one tool: 'nodes' (distinct node types in use, counts, versions, community/AI flags), 'datatables' (tables + columns + row counts), 'datatable_rows' (pass datatableId; limit \u2264 100), 'possibilities' (facts for "what could I automate": connected services with live-usage cross-check, unused connections, top nodes, AI tools [null = unknown, not none], community packages, counts \u2014 YOU turn these into suggestions, grounded only in what's actually there), 'credentials' (names/types/inferred service \u2014 never secrets), 'variables' ($vars keys). All consent-gated server-side; 403 consent_required \u2192 settingsUrl.`,
40674
40792
  inputSchema: {
40675
40793
  what: external_exports.enum(["nodes", "datatables", "datatable_rows", "possibilities", "credentials", "variables"]),
@@ -40701,7 +40819,8 @@ server.registerTool(
40701
40819
  server.registerTool(
40702
40820
  "awesomate_n8n_executions",
40703
40821
  {
40704
- description: "Execution reads. workflowId \u2192 recent executions of that workflow. executionId \u2192 full detail with error summary. executionId + debug:true \u2192 node-by-node decode (statuses, timings, errors, 2 example items per node) \u2014 the best failure-diagnosis view; needs the 'error content analysis' privacy toggle (403 consent_required \u2192 settingsUrl), and responses with tooLarge:true mean the payload exceeded 15MB \u2014 fall back to the non-debug detail.",
40822
+ annotations: READ_ONLY,
40823
+ description: "Execution reads. workflowId \u2192 recent executions of that workflow. executionId \u2192 detail with errorSummary \u2014 trust errorSummary.failingNode only when confident:true (structural decode; also carries errorType/code/lastNodeExecuted); when confident:false it came from a heuristic and may name a node that does not exist \u2014 verify against the workflow before editing anything. executionId + debug:true \u2192 node-by-node decode (statuses, timings, errors, 2 example items per node) \u2014 the best failure-diagnosis view; needs the 'error content analysis' privacy toggle (403 consent_required \u2192 settingsUrl), and responses with tooLarge:true mean the payload exceeded 15MB \u2014 fall back to the non-debug detail. For 'what is failing across ALL my workflows', use awesomate_n8n_errors instead.",
40705
40824
  inputSchema: {
40706
40825
  workflowId: external_exports.string().optional(),
40707
40826
  executionId: external_exports.string().optional(),
@@ -40729,6 +40848,7 @@ server.registerTool(
40729
40848
  server.registerTool(
40730
40849
  "awesomate_n8n_node_docs",
40731
40850
  {
40851
+ annotations: READ_ONLY,
40732
40852
  description: "Live n8n documentation, 500+ nodes + 2,500+ community templates \u2014 ALWAYS prefer this over memory for node schemas and typeVersions. tools: search_nodes {query}, get_node {nodeType \u2014 full form like 'n8n-nodes-base.gmail' works, add detail:'full' for everything}, search_templates {query} / get_template {templateId} (real importable community workflows \u2014 great starting points), validate_node {nodeType, config}, tools_documentation {}. Works on every plan, no consent needed. 503 node_catalog_unavailable \u2192 use the skill's references/vendor/ files instead; 422 catalog_tool_error \u2192 YOUR args were wrong (message says why), the catalog is fine.",
40733
40853
  inputSchema: {
40734
40854
  tool: external_exports.enum(["search_nodes", "get_node", "search_templates", "get_template", "validate_node", "tools_documentation"]),
@@ -40746,6 +40866,7 @@ server.registerTool(
40746
40866
  server.registerTool(
40747
40867
  "awesomate_n8n_datatable_write",
40748
40868
  {
40869
+ annotations: DESTRUCTIVE,
40749
40870
  description: "Datatable writes (Support Plus+, consent-gated, audited). 'create' {name, columns:[{name,type?}], workflowId?} \u2014 ALWAYS pass workflowId when the table serves a specific workflow (datatables resolve PER PROJECT at runtime; workflowId threads that workflow's project; a projectWarning in the response means pass it). 'add_column' {tableId, name, type?} \u2014 needs the 'direct database writes' privacy toggle; names: letters/digits/underscores only. 'insert' {tableId, rows:[...]} (\u2264100), 'update' {tableId, filter, data}, 'delete_rows' {tableId, filter \u2014 REQUIRED, there is no delete-all}. Writes only work on tables created through Claude Code (403 not_self_created otherwise \u2014 agency tables are off limits). Get explicit user approval before delete_rows.",
40750
40871
  inputSchema: {
40751
40872
  action: external_exports.enum(["create", "add_column", "insert", "update", "delete_rows"]),
@@ -40780,13 +40901,24 @@ server.registerTool(
40780
40901
  server.registerTool(
40781
40902
  "awesomate_skill_update",
40782
40903
  {
40783
- description: "Refresh the locally installed Awesomate skills from this (always-latest) server package \u2014 run when awesomate_get_context reports skill.updateAvailable. Copies every bundled skill into ~/.claude/skills and re-stamps versions. New skill content applies from the NEXT Claude Code session; finish the current task first, then suggest a restart.",
40904
+ description: "The one-step updater for the locally installed Awesomate skills \u2014 run when any response stamp or awesomate_get_context reports an update ready. Refreshes ALL bundled skills in ~/.claude/skills from this (always-latest) server package, removes files newer bundles no longer ship, re-stamps versions, and returns per-skill from\u2192to plus what's-new lines and the exact restart step. New skill content applies from the NEXT Claude Code session; finish the current task first, then hand the user the restart instruction verbatim.",
40784
40905
  inputSchema: {}
40785
40906
  },
40786
40907
  async () => {
40787
40908
  try {
40788
40909
  const result = installSkills(SERVER_VERSION);
40789
- return textResult({ ...result, note: "Updated skill files load in the next Claude Code session." });
40910
+ const changed = result.updates.filter((u) => u.from !== u.to);
40911
+ return textResult({
40912
+ ...result,
40913
+ changed: changed.map((u) => `${u.name}: ${u.from ?? "unversioned"} \u2192 ${u.to}`),
40914
+ whatsNew: changelogSince(changed[0]?.from ?? null).flatMap((e) => e.highlights),
40915
+ restart: {
40916
+ note: "Updated skill files load in the next Claude Code session \u2014 one restart lands both the skills and the (self-updating) server.",
40917
+ terminal: "Type exit (or press Ctrl+C twice), then run `claude` again.",
40918
+ vscode: 'Command Palette (Cmd/Ctrl+Shift+P) \u2192 "Developer: Reload Window".',
40919
+ verify: "After restarting, awesomate_get_context should report skill.updateAvailable: false."
40920
+ }
40921
+ });
40790
40922
  } catch (err) {
40791
40923
  return errorResult(err);
40792
40924
  }
@@ -40826,6 +40958,7 @@ server.registerTool(
40826
40958
  server.registerTool(
40827
40959
  "awesomate_request_build",
40828
40960
  {
40961
+ annotations: DESTRUCTIVE,
40829
40962
  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).",
40830
40963
  inputSchema: {
40831
40964
  title: external_exports.string().describe("Short name for the automation"),
@@ -40894,6 +41027,7 @@ server.registerTool(
40894
41027
  server.registerTool(
40895
41028
  "awesomate_uninstall_site",
40896
41029
  {
41030
+ annotations: DESTRUCTIVE,
40897
41031
  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+.",
40898
41032
  inputSchema: {
40899
41033
  domain: external_exports.string().describe("The site domain to delete"),
@@ -40970,6 +41104,7 @@ var PLAN_LADDER_FALLBACK = {
40970
41104
  server.registerTool(
40971
41105
  "awesomate_get_plan_features",
40972
41106
  {
41107
+ annotations: READ_ONLY,
40973
41108
  description: "The Awesomate plan ladder: what each plan includes for hosting (WordPress sites, custom domains, hosted apps, cPanel, shell/Claude Code access) and which plans are purchasable. Fetched live from the hub (single source of truth) with a static fallback. Use to explain what an upgrade unlocks; live per-account usage comes from awesomate_get_limits.",
40974
41109
  inputSchema: {}
40975
41110
  },
@@ -41010,6 +41145,7 @@ server.registerTool(
41010
41145
  server.registerTool(
41011
41146
  "awesomate_list_snapshots",
41012
41147
  {
41148
+ annotations: READ_ONLY,
41013
41149
  description: "List a site\u2019s available snapshots (newest first) with their ids, timestamps, and reasons. Requires shell access (Support Plus+).",
41014
41150
  inputSchema: { domain: external_exports.string().describe("The site domain") }
41015
41151
  },
@@ -41024,6 +41160,7 @@ server.registerTool(
41024
41160
  server.registerTool(
41025
41161
  "awesomate_rollback_site",
41026
41162
  {
41163
+ annotations: DESTRUCTIVE,
41027
41164
  description: "Restore a site to a previous snapshot (files + database). The current state is auto-snapshotted first, so a rollback is itself reversible (see preRollbackSnapshotId in the result). Confirm with the user before rolling back \u2014 it overwrites the live site. Requires shell access (Support Plus+).",
41028
41165
  inputSchema: {
41029
41166
  domain: external_exports.string().describe("The site domain"),
@@ -41061,6 +41198,7 @@ server.registerTool(
41061
41198
  server.registerTool(
41062
41199
  "awesomate_site_staging_promote",
41063
41200
  {
41201
+ annotations: DESTRUCTIVE,
41064
41202
  description: "Publish the staging copy to the LIVE site (overwrites live files + database with staging). The live site is auto-snapshotted first \u2014 the result includes preSnapshotId, which awesomate_rollback_site can restore if anything looks wrong. **Confirm with the user before promoting \u2014 it replaces the live site.** Requires shell access (Support Plus+).",
41065
41203
  inputSchema: {
41066
41204
  domain: external_exports.string().describe("The LIVE site domain whose staging copy should go live")
@@ -41079,6 +41217,7 @@ server.registerTool(
41079
41217
  server.registerTool(
41080
41218
  "awesomate_site_staging_discard",
41081
41219
  {
41220
+ annotations: DESTRUCTIVE,
41082
41221
  description: "Delete the staging copy of a site (staging WP install + its awesomate.dev address; the live site is untouched). **Confirm with the user before discarding \u2014 unpromoted staging changes are lost.** Requires shell access (Support Plus+).",
41083
41222
  inputSchema: {
41084
41223
  domain: external_exports.string().describe("The LIVE site domain whose staging copy should be discarded")
@@ -41107,6 +41246,7 @@ readTool(
41107
41246
  server.registerTool(
41108
41247
  "awesomate_app_get",
41109
41248
  {
41249
+ annotations: READ_ONLY,
41110
41250
  description: "Get one app plus its environments (subdomains, ports, db engine/name, deploy + health state). Poll this after awesomate_app_create \u2014 status goes provisioning \u2192 active | failed (read provision_error on failure).",
41111
41251
  inputSchema: { appId: external_exports.number().int().positive().describe("The app id from awesomate_app_list / _create") }
41112
41252
  },
@@ -41142,6 +41282,7 @@ server.registerTool(
41142
41282
  server.registerTool(
41143
41283
  "awesomate_app_scaffold",
41144
41284
  {
41285
+ annotations: READ_ONLY,
41145
41286
  description: "Fetch the app's starter files (its template rendered against the live app metadata \u2014 subdomains, control-plane URL, repo \u2014 with placeholders already substituted). Call after awesomate_app_create reaches status=active: write each returned file into a fresh local project folder at its relative path, then run npm install (Node apps) and follow the bundled CLAUDE.md. This is how the starter code gets onto the user's machine \u2014 don't reconstruct templates by hand.",
41146
41287
  inputSchema: { appId: external_exports.number().int().positive().describe("The app id from awesomate_app_create / _list") }
41147
41288
  },
@@ -41154,9 +41295,10 @@ server.registerTool(
41154
41295
  }
41155
41296
  );
41156
41297
  server.registerTool(
41157
- "awesomate_app_deploy",
41298
+ "awesomate_app_deploy_info",
41158
41299
  {
41159
- description: "Report 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.",
41300
+ annotations: READ_ONLY,
41301
+ 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.",
41160
41302
  inputSchema: { appId: external_exports.number().int().positive().describe("The app id") }
41161
41303
  },
41162
41304
  async ({ appId }) => {
@@ -41207,6 +41349,7 @@ server.registerTool(
41207
41349
  server.registerTool(
41208
41350
  "awesomate_app_health",
41209
41351
  {
41352
+ annotations: READ_ONLY,
41210
41353
  description: "Probe an app's environments live and report health. Node envs hit /api/ready (200 = deployed + DB up + migrations applied); static hits the root. An env that isn't deployed yet reports unreachable \u2014 expected, not a failure. Use after a deploy to confirm it came up, or when the user says something's down.",
41211
41354
  inputSchema: { appId: external_exports.number().int().positive().describe("The app id") }
41212
41355
  },
@@ -41254,6 +41397,7 @@ ${scrubKeyMaterial(message)}` }],
41254
41397
  server.registerTool(
41255
41398
  "awesomate_knowledge_status",
41256
41399
  {
41400
+ annotations: READ_ONLY,
41257
41401
  description: "Call FIRST for any Knowledge Base work. The account's knowledge tenant state (provisioning/active/suspended), plan entitlement, consent flag, month-to-date usage vs included quota, and purchased packs. upgrade_required:true \u2192 relay the included upsell copy + billing link honestly, do NOT retry. available:false \u2192 this hub doesn't serve Knowledge Base yet (kill switch / old hub) \u2014 also not retryable. Reads work on every plan.",
41258
41402
  inputSchema: {}
41259
41403
  },
@@ -41307,6 +41451,7 @@ server.registerTool(
41307
41451
  server.registerTool(
41308
41452
  "awesomate_knowledge_search",
41309
41453
  {
41454
+ annotations: READ_ONLY,
41310
41455
  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.",
41311
41456
  inputSchema: {
41312
41457
  q: external_exports.string().max(2e3).optional().describe("Search words; empty lists the library filtered by the facets"),
@@ -41432,6 +41577,274 @@ server.registerTool(
41432
41577
  }
41433
41578
  }
41434
41579
  );
41580
+ server.registerTool(
41581
+ "awesomate_knowledge_data",
41582
+ {
41583
+ description: "The knowledge platform's business-data warehouse (Pro+). action 'metrics' \u2014 headline numbers for the data tab. 'datasets' \u2014 the datasets imported and their columns: read this FIRST, measures/dimensions must name real columns. 'imports' \u2014 import job statuses. 'query' {dataset, measures:[{column, agg}], plus optional dimensions/filters passed through} \u2014 answer QUANTITATIVE questions from the user's own imported business data (revenue by month, top customers); read-only, results come back as rows to present honestly. New data is imported in the hub UI, not here.",
41584
+ inputSchema: {
41585
+ action: external_exports.enum(["metrics", "datasets", "imports", "query"]),
41586
+ dataset: external_exports.string().max(80).optional().describe("query: dataset name from datasets"),
41587
+ measures: external_exports.array(external_exports.object({ column: external_exports.string().max(80), agg: external_exports.string().max(10) })).max(8).optional().describe("query: e.g. [{column:'amount', agg:'sum'}]"),
41588
+ dimensions: external_exports.array(external_exports.string().max(80)).max(6).optional().describe("query: group-by columns"),
41589
+ filters: external_exports.array(external_exports.record(external_exports.unknown())).max(10).optional().describe("query: filter objects, passed through"),
41590
+ limit: external_exports.number().int().min(1).max(500).optional()
41591
+ }
41592
+ },
41593
+ async ({ action, dataset, measures, dimensions, filters, limit }) => {
41594
+ try {
41595
+ const cfg = requireConfig();
41596
+ if (action === "query") {
41597
+ return knowledgeResult(
41598
+ await hubPost(cfg, "/api/knowledge/data/query", { dataset, measures, dimensions, filters, limit })
41599
+ );
41600
+ }
41601
+ return knowledgeResult(await hubGet(cfg, `/api/knowledge/data/${action}`));
41602
+ } catch (err) {
41603
+ return knowledgeError(err);
41604
+ }
41605
+ }
41606
+ );
41607
+ server.registerTool(
41608
+ "awesomate_n8n_kpis",
41609
+ {
41610
+ annotations: READ_ONLY,
41611
+ description: "Automation KPIs from the hub's monitoring: executions, error rate, avg + p95 duration, time saved, 7-day-vs-prior deltas. No workflowId \u2192 the whole instance (the headline numbers for any report). With workflowId \u2192 that workflow, plus last_error_at and clean_days. Needs the 'aggregate monitoring' privacy toggle (on by default). Pair with awesomate_n8n_errors for what is failing and awesomate_site_uptime for the hosting side.",
41612
+ inputSchema: { workflowId: external_exports.string().max(64).optional() }
41613
+ },
41614
+ async ({ workflowId }) => {
41615
+ try {
41616
+ const cfg = requireConfig();
41617
+ const path = workflowId ? `/api/my-n8n/workflows/${encodeURIComponent(workflowId)}/kpis` : "/api/my-n8n/kpis";
41618
+ return textResult(await hubGet(cfg, path));
41619
+ } catch (err) {
41620
+ return errorResult(err);
41621
+ }
41622
+ }
41623
+ );
41624
+ server.registerTool(
41625
+ "awesomate_n8n_errors",
41626
+ {
41627
+ annotations: READ_ONLY,
41628
+ description: "What is broken across the WHOLE instance \u2014 error events grouped by fingerprint (category, workflow, node, occurrences, first/last seen), newest first. THE first call when the user says 'something is failing' or at the start of an n8n session (a quick 7-day sweep; stay silent when it's clean). Counts and categories only \u2014 drill into a specific failure with awesomate_n8n_executions. days defaults to 30 (max 90).",
41629
+ inputSchema: {
41630
+ days: external_exports.number().int().min(1).max(90).optional(),
41631
+ limit: external_exports.number().int().min(1).max(100).optional()
41632
+ }
41633
+ },
41634
+ async ({ days, limit }) => {
41635
+ try {
41636
+ const qs = new URLSearchParams();
41637
+ if (days) qs.set("days", String(days));
41638
+ if (limit) qs.set("limit", String(limit));
41639
+ return textResult(await hubGet(requireConfig(), `/api/my-n8n/errors${qs.size ? `?${qs}` : ""}`));
41640
+ } catch (err) {
41641
+ return errorResult(err);
41642
+ }
41643
+ }
41644
+ );
41645
+ readTool(
41646
+ "awesomate_n8n_storage",
41647
+ "What the n8n instance's data actually weighs: execution-table bytes, whole-database size, a per-table breakdown, and the last-24h per-workflow write offenders. Use it when executions feel slow, before suggesting cleanup, and as an OPPORTUNITY signal \u2014 heavy binary/media data is a cue to suggest a purpose-built app (awesomate-app-builder) for browsing it. Integers only; content never leaves the instance.",
41648
+ "/api/my-n8n/machine/storage"
41649
+ );
41650
+ server.registerTool(
41651
+ "awesomate_n8n_findings",
41652
+ {
41653
+ annotations: READ_ONLY,
41654
+ description: "Findings from Awesomate's automated error analyzer for THIS account (Pro/Embedded + the 'AI error analysis' privacy toggle): severity, workflow, occurrences, a plain-language clientSummary, needsClientAction (something only the user can fix \u2014 expired logins, third-party quotas), and fixReady (a reviewed fix is prepared \u2014 raise it with awesomate_support to have it applied). Read-only. An empty list on a healthy instance is the good state, not an error.",
41655
+ inputSchema: { limit: external_exports.number().int().min(1).max(100).optional() }
41656
+ },
41657
+ async ({ limit }) => {
41658
+ try {
41659
+ const qs = limit ? `?limit=${limit}` : "";
41660
+ return textResult(await hubGet(requireConfig(), `/api/my-n8n/machine/findings${qs}`));
41661
+ } catch (err) {
41662
+ return errorResult(err);
41663
+ }
41664
+ }
41665
+ );
41666
+ readTool(
41667
+ "awesomate_site_uptime",
41668
+ "Uptime for every monitored hosted site: current up/down state, 30-day availability %, incident count, downtime seconds. Served from the hub's cache \u2014 free and instant, safe to include in any report or health sweep. A site missing from the list simply is not monitored yet, not down.",
41669
+ "/api/client-hosting/uptime"
41670
+ );
41671
+ readTool(
41672
+ "awesomate_dns_check",
41673
+ "Live DNS check for the account's primary domain \u2014 where it ACTUALLY resolves right now versus where Awesomate hosting expects it. The first call when 'my domain isn't working': it separates a DNS problem (user must change records at their registrar) from a hosting problem (ours).",
41674
+ "/api/client-hosting/dns-check"
41675
+ );
41676
+ readTool(
41677
+ "awesomate_dashboard_metrics",
41678
+ "The account-wide dashboard aggregate in one call: executions by status, error rate, time saved, workflows total/active, chat sessions + unique users (30d), active users (7d), a 7-day daily exec/error trend, and the most recent live-workflow failure. The single best data source for a status update or weekly report; combine with awesomate_site_uptime and awesomate_knowledge_status for the full picture.",
41679
+ "/api/client-dashboard/metrics"
41680
+ );
41681
+ readTool(
41682
+ "awesomate_account_report",
41683
+ "A monthly-report-shaped read of the account: what's working (workflows, executions, success rate, chat, hosted sites), what's not (error clusters, open alerts), engagement (logins, credits, webinars) and commercials. Sections are independently fault-tolerant \u2014 null means 'could not be read right now', never zero. Assemble the user-facing story from these sections in THEIR language; don't paste the raw JSON at them.",
41684
+ "/api/client-report"
41685
+ );
41686
+ readTool(
41687
+ "awesomate_privacy_settings",
41688
+ "READ which privacy/consent toggles are on or off for this account \u2014 call it when a tool returns 403 consent_required so you can name the exact toggle instead of guessing. Toggles are changed ONLY by the user in the hub (Settings \u2192 Privacy, hub.awesomate.ai/settings?tab=privacy); there is deliberately no write here.",
41689
+ "/api/client-settings/privacy"
41690
+ );
41691
+ server.registerTool(
41692
+ "awesomate_notifications",
41693
+ {
41694
+ description: "The account's hub notification bell \u2014 the only channel where Awesomate pushes to the client: knowledge quota warnings (80%/100%), 'your quote is ready', support-access events. action 'list' {limit?} \u2192 notifications + unread count (surface unread ones once per session, in one line). 'read' {id} / 'read_all' \u2192 mark seen after you've relayed them. Not for sending anything.",
41695
+ inputSchema: {
41696
+ action: external_exports.enum(["list", "read", "read_all"]),
41697
+ id: external_exports.number().int().positive().optional().describe("read: the notification id"),
41698
+ limit: external_exports.number().int().min(1).max(100).optional().describe("list: default 30")
41699
+ }
41700
+ },
41701
+ async ({ action, id, limit }) => {
41702
+ try {
41703
+ const cfg = requireConfig();
41704
+ if (action === "list") {
41705
+ const qs = limit ? `?limit=${limit}` : "";
41706
+ return textResult(await hubGet(cfg, `/api/notifications${qs}`));
41707
+ }
41708
+ if (action === "read") {
41709
+ if (!id) return errorResult(new Error("action 'read' needs id"));
41710
+ return textResult(await hubPost(cfg, `/api/notifications/${id}/read`));
41711
+ }
41712
+ return textResult(await hubPost(cfg, "/api/notifications/read-all"));
41713
+ } catch (err) {
41714
+ return errorResult(err);
41715
+ }
41716
+ }
41717
+ );
41718
+ server.registerTool(
41719
+ "awesomate_wp_post",
41720
+ {
41721
+ description: "Create, update or read a WordPress post/page on the user's own Awesomate-hosted site \u2014 titles and content with spaces/HTML are fine (unlike awesomate_run_wp_cli). 'create' {domain, title, content?, status?, postType?: post|page, excerpt?, slug?} \u2014 lands as a DRAFT unless status:'publish' is explicit; never publish content the user hasn't seen or approved. 'update' {domain, postId, any of title/content/excerpt/slug/status}. 'get' {domain, postId, includeContent?}. Writes need Support Plus+ and are audited; snapshot the site first (awesomate_snapshot_site) before the session's first content change on a live site. Find post ids via awesomate_run_wp_cli ['post','list'].",
41722
+ inputSchema: {
41723
+ action: external_exports.enum(["create", "update", "get"]),
41724
+ domain: external_exports.string().min(3).max(253).describe("The site domain, e.g. mybusiness.awesomate.site"),
41725
+ postId: external_exports.number().int().positive().optional().describe("update/get"),
41726
+ postType: external_exports.enum(["post", "page"]).optional().describe("create only; default post"),
41727
+ title: external_exports.string().max(300).optional(),
41728
+ content: external_exports.string().max(15e4).optional().describe("HTML or plain text"),
41729
+ excerpt: external_exports.string().max(1e3).optional(),
41730
+ slug: external_exports.string().max(190).optional().describe("URL slug, [a-z0-9-]"),
41731
+ status: external_exports.enum(["draft", "publish", "pending", "private"]).optional(),
41732
+ includeContent: external_exports.boolean().optional().describe("get only")
41733
+ }
41734
+ },
41735
+ async ({ action, domain, postId, postType, title, content, excerpt, slug, status, includeContent }) => {
41736
+ try {
41737
+ const cfg = requireConfig();
41738
+ const dom = encodeURIComponent(domain.toLowerCase());
41739
+ if (action === "get") {
41740
+ if (!postId) return errorResult(new Error("action 'get' needs postId"));
41741
+ const qs = includeContent ? "?includeContent=1" : "";
41742
+ return textResult(await hubGet(cfg, `/api/client-hosting/sites/${dom}/posts/${postId}${qs}`));
41743
+ }
41744
+ return textResult(
41745
+ await hubPost(cfg, `/api/client-hosting/sites/${dom}/posts`, {
41746
+ action,
41747
+ postId,
41748
+ postType,
41749
+ title,
41750
+ content,
41751
+ excerpt,
41752
+ slug,
41753
+ status
41754
+ })
41755
+ );
41756
+ } catch (err) {
41757
+ return errorResult(err);
41758
+ }
41759
+ }
41760
+ );
41761
+ server.registerTool(
41762
+ "awesomate_wp_media_import",
41763
+ {
41764
+ description: "Import ONE media item into the user's WordPress media library by https URL (Support Plus+, audited). Returns the attachmentId to reference from posts. URLs only \u2014 this cannot read local files; for a local file, upload it somewhere reachable first or use wp-admin. Ask before importing anything the user didn't explicitly provide.",
41765
+ inputSchema: {
41766
+ domain: external_exports.string().min(3).max(253),
41767
+ url: external_exports.string().url().max(2048).describe("https URL of the image/file"),
41768
+ title: external_exports.string().max(300).optional()
41769
+ }
41770
+ },
41771
+ async ({ domain, url, title }) => {
41772
+ try {
41773
+ const dom = encodeURIComponent(domain.toLowerCase());
41774
+ return textResult(await hubPost(requireConfig(), `/api/client-hosting/sites/${dom}/media`, { url, title }));
41775
+ } catch (err) {
41776
+ return errorResult(err);
41777
+ }
41778
+ }
41779
+ );
41780
+ server.registerPrompt(
41781
+ "awesomate-status",
41782
+ {
41783
+ description: "One-screen health digest: account, automations, sites, knowledge, anything needing attention."
41784
+ },
41785
+ () => ({
41786
+ messages: [
41787
+ {
41788
+ role: "user",
41789
+ content: {
41790
+ type: "text",
41791
+ text: "Give me a one-screen Awesomate status. Call awesomate_get_context (attention block), awesomate_dashboard_metrics, awesomate_n8n_errors (7 days) and awesomate_site_uptime, then summarize in plain language: what ran, what failed (if anything), site uptime, and anything waiting on me \u2014 unread notifications, expiring token, available update. Lead with whatever needs attention; keep it short if everything is green."
41792
+ }
41793
+ }
41794
+ ]
41795
+ })
41796
+ );
41797
+ server.registerPrompt(
41798
+ "awesomate-checkup",
41799
+ {
41800
+ description: "Run the full health sweep: automations, error findings, uptime, DNS, storage, SEO basics."
41801
+ },
41802
+ () => ({
41803
+ messages: [
41804
+ {
41805
+ role: "user",
41806
+ content: {
41807
+ type: "text",
41808
+ text: "Run a full Awesomate checkup and fix what you safely can. Sweep: awesomate_n8n_errors (30 days) + awesomate_n8n_kpis; awesomate_n8n_findings if my plan supports it; awesomate_site_uptime + awesomate_dns_check; awesomate_n8n_storage for anything unusually heavy; and the awesomate-seo skill's upkeep checks on my main site. Report only exceptions in my language, propose fixes before applying anything, and never publish or delete without asking."
41809
+ }
41810
+ }
41811
+ ]
41812
+ })
41813
+ );
41814
+ server.registerPrompt(
41815
+ "awesomate-report",
41816
+ {
41817
+ description: "Assemble this account's business report: what worked, what broke, engagement, value delivered."
41818
+ },
41819
+ () => ({
41820
+ messages: [
41821
+ {
41822
+ role: "user",
41823
+ content: {
41824
+ type: "text",
41825
+ text: "Build my Awesomate report. Pull awesomate_account_report, awesomate_dashboard_metrics, awesomate_site_uptime and awesomate_knowledge_status (skip sections that are not available on my plan), then write it for a business owner: what my automations did for me, what needs attention, and one concrete suggestion for next month. Offer to set this up as a recurring weekly email via an n8n workflow (awesomate-n8n skill, recurring-reports recipe)."
41826
+ }
41827
+ }
41828
+ ]
41829
+ })
41830
+ );
41831
+ server.registerPrompt(
41832
+ "awesomate-ideas",
41833
+ {
41834
+ description: "Grounded automation ideas from what this account actually has \u2014 with what each would save."
41835
+ },
41836
+ () => ({
41837
+ messages: [
41838
+ {
41839
+ role: "user",
41840
+ content: {
41841
+ type: "text",
41842
+ text: "Suggest what I should automate next \u2014 grounded ONLY in what I actually have. Inventory first: awesomate_n8n_inspect (possibilities + credentials), awesomate_n8n_storage, awesomate_knowledge_status, and my site's content shape (awesomate_run_wp_cli post/media list counts). Then give me at most three ideas, each with: what it does, what it replaces or saves (hours per week or a subscription), and whether I can build it with you now or it's a done-for-you build (1 credit = $100). No generic ideas \u2014 every suggestion must cite something from my inventory."
41843
+ }
41844
+ }
41845
+ ]
41846
+ })
41847
+ );
41435
41848
  async function main() {
41436
41849
  try {
41437
41850
  config2 = loadConfig();
@@ -41439,10 +41852,34 @@ async function main() {
41439
41852
  configError = err instanceof Error ? err.message : String(err);
41440
41853
  console.error(`[awesomate-hosting-mcp] account not resolved: ${configError}`);
41441
41854
  }
41855
+ if (SKILL_DRIFT && autoUpdateEnabled()) {
41856
+ try {
41857
+ const result = installSkills(SERVER_VERSION);
41858
+ console.error(
41859
+ `[awesomate-hosting-mcp] skills auto-updated to ${SERVER_VERSION} (${result.installed.length} skills${result.removedOrphans.length ? `, ${result.removedOrphans.length} orphaned files removed` : ""}) \u2014 applies next session`
41860
+ );
41861
+ } catch (err) {
41862
+ console.error(
41863
+ "[awesomate-hosting-mcp] skills auto-update failed (non-fatal):",
41864
+ err instanceof Error ? err.message : err
41865
+ );
41866
+ }
41867
+ }
41442
41868
  const transport = new StdioServerTransport();
41443
41869
  await server.connect(transport);
41444
41870
  console.error(`[awesomate-hosting-mcp] connected (stdio) \u2014 ${accountStamp()}`);
41445
41871
  }
41872
+ function autoUpdateEnabled() {
41873
+ const env = process.env.AWESOMATE_SKILLS_AUTOUPDATE;
41874
+ if (env === "1" || env === "true") return true;
41875
+ if (env === "0" || env === "false") return false;
41876
+ try {
41877
+ const raw = JSON.parse(readFileSync3(join3(homedir3(), ".awesomate", "credentials.json"), "utf8"));
41878
+ return raw.autoUpdateSkills === true;
41879
+ } catch {
41880
+ return false;
41881
+ }
41882
+ }
41446
41883
  main().catch((err) => {
41447
41884
  console.error(
41448
41885
  "[awesomate-hosting-mcp] fatal:",