@awesomate/hosting-mcp 0.18.0 → 0.19.1

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,16 +39892,35 @@ 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 (code === "n8n_credential_invalid") {
39910
+ const settingsUrl = body && typeof body === "object" && typeof body.settingsUrl === "string" ? body.settingsUrl : "https://hub.awesomate.ai/n8n/settings";
39911
+ const instanceUrl = body && typeof body === "object" && typeof body.instanceUrl === "string" ? body.instanceUrl : null;
39912
+ hint = ` THIS IS NOT A HUB OUTAGE and not your workflow \u2014 the user's own n8n rejected the API key Awesomate has stored. Tell the user to fix it in two steps: (1) in their n8n${instanceUrl ? ` at ${instanceUrl}` : ""}, open Settings \u2192 n8n API and create a new API key; (2) paste it into their Awesomate account at ${settingsUrl} under "New n8n API Key" and save. Then retry. Do not retry before they confirm, and do not investigate hub/Cloudflare status \u2014 reads and validation keep working because those never touch their instance.`;
39913
+ } else if (res.status === 409) {
39914
+ hint = " Conflict \u2014 the payload carries the specifics (a limit reached, or a prerequisite missing). Read its code field; do not blind-retry.";
39915
+ } else if (res.status === 422) {
39916
+ 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.";
39917
+ } else if (res.status === 429) {
39918
+ hint = " Rate/quota limit hit. Wait before retrying; if it is a daily plan quota, relay that honestly instead of looping.";
39901
39919
  } else if (res.status === 503) {
39902
- hint = " The hub is temporarily unable to verify access tokens \u2014 retry shortly.";
39920
+ hint = " The hub is temporarily unable to serve this \u2014 retry shortly.";
39903
39921
  }
39904
- throw new HubApiError(`${serverMsg}.${hint}`, res.status, body);
39922
+ const stem = /[.!?]$/.test(serverMsg) ? serverMsg : `${serverMsg}.`;
39923
+ throw new HubApiError(`${stem}${hint}`, res.status, body);
39905
39924
  }
39906
39925
  return body;
39907
39926
  }
@@ -39919,28 +39938,70 @@ function hubDelete(config3, path, jsonBody) {
39919
39938
  }
39920
39939
 
39921
39940
  // src/skills.ts
39922
- import { cpSync, existsSync as existsSync2, readdirSync, writeFileSync } from "node:fs";
39941
+ import { cpSync, existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, rmSync, writeFileSync } from "node:fs";
39923
39942
  import { homedir as homedir2 } from "node:os";
39924
39943
  import { join as join2, dirname as dirname2 } from "node:path";
39925
39944
  import { fileURLToPath } from "node:url";
39926
39945
  var PACKAGE_ROOT = join2(dirname2(fileURLToPath(import.meta.url)), "..");
39927
- function installSkills(version2, sourceRoot = join2(PACKAGE_ROOT, "skill")) {
39946
+ var DEFAULT_SOURCE_ROOT = join2(PACKAGE_ROOT, "skill");
39947
+ function bundledSkillNames(sourceRoot = DEFAULT_SOURCE_ROOT) {
39948
+ try {
39949
+ return readdirSync(sourceRoot, { withFileTypes: true }).filter((e) => e.isDirectory() && existsSync2(join2(sourceRoot, e.name, "SKILL.md"))).map((e) => e.name);
39950
+ } catch {
39951
+ return [];
39952
+ }
39953
+ }
39954
+ function installedSkillVersion(name) {
39955
+ try {
39956
+ return readFileSync2(join2(homedir2(), ".claude", "skills", name, ".installed-version"), "utf8").trim() || null;
39957
+ } catch {
39958
+ return null;
39959
+ }
39960
+ }
39961
+ function skillVersions(sourceRoot = DEFAULT_SOURCE_ROOT) {
39962
+ return bundledSkillNames(sourceRoot).map((name) => ({ name, installed: installedSkillVersion(name) }));
39963
+ }
39964
+ function walkFiles(dir, prefix = "") {
39965
+ const out = [];
39966
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
39967
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
39968
+ if (entry.isDirectory()) out.push(...walkFiles(join2(dir, entry.name), rel));
39969
+ else out.push(rel);
39970
+ }
39971
+ return out;
39972
+ }
39973
+ function installSkills(version2, sourceRoot = DEFAULT_SOURCE_ROOT) {
39928
39974
  const destRoot = join2(homedir2(), ".claude", "skills");
39929
39975
  const installed = [];
39976
+ const updates = [];
39977
+ const removedOrphans = [];
39930
39978
  for (const entry of readdirSync(sourceRoot, { withFileTypes: true })) {
39931
39979
  if (!entry.isDirectory()) continue;
39932
39980
  const src = join2(sourceRoot, entry.name);
39933
39981
  if (!existsSync2(join2(src, "SKILL.md"))) continue;
39934
39982
  const dest = join2(destRoot, entry.name);
39983
+ const from = installedSkillVersion(entry.name);
39935
39984
  cpSync(src, dest, { recursive: true });
39985
+ try {
39986
+ const shipped = new Set(walkFiles(src));
39987
+ for (const rel of walkFiles(dest)) {
39988
+ if (rel === ".installed-version") continue;
39989
+ if (!shipped.has(rel)) {
39990
+ rmSync(join2(dest, rel), { force: true });
39991
+ removedOrphans.push(`${entry.name}/${rel}`);
39992
+ }
39993
+ }
39994
+ } catch {
39995
+ }
39936
39996
  try {
39937
39997
  writeFileSync(join2(dest, ".installed-version"), `${version2}
39938
39998
  `);
39939
39999
  } catch {
39940
40000
  }
39941
40001
  installed.push(entry.name);
40002
+ updates.push({ name: entry.name, from, to: version2 });
39942
40003
  }
39943
- return { installed, version: version2, skillsDir: destRoot };
40004
+ return { installed, updates, removedOrphans, version: version2, skillsDir: destRoot };
39944
40005
  }
39945
40006
 
39946
40007
  // src/knowledge.ts
@@ -40463,51 +40524,108 @@ function requireConfig() {
40463
40524
  return config2;
40464
40525
  }
40465
40526
  function accountStamp() {
40527
+ const drift = SKILL_DRIFT ? ` \xB7 skills ${SKILL_DRIFT} \u2014 update ready, run awesomate_skill_update` : "";
40466
40528
  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})`;
40529
+ return `account: ${config2.account ?? "unknown (env PAT not matched to a profile)"} (source: ${config2.source})${drift}`;
40468
40530
  }
40469
40531
  var SERVER_VERSION = (() => {
40470
40532
  try {
40471
40533
  const pkg = JSON.parse(
40472
- readFileSync2(join3(dirname3(fileURLToPath2(import.meta.url)), "..", "package.json"), "utf8")
40534
+ readFileSync3(join3(dirname3(fileURLToPath2(import.meta.url)), "..", "package.json"), "utf8")
40473
40535
  );
40474
40536
  return pkg.version ?? "0.0.0";
40475
40537
  } catch {
40476
40538
  return "0.0.0";
40477
40539
  }
40478
40540
  })();
40479
- function skillUpdateInfo() {
40480
- let installedSkillVersion = null;
40541
+ var CHANGELOG = (() => {
40481
40542
  try {
40482
- installedSkillVersion = readFileSync2(
40483
- join3(homedir3(), ".claude", "skills", "awesomate-hosting", ".installed-version"),
40484
- "utf8"
40485
- ).trim() || null;
40543
+ const raw = JSON.parse(
40544
+ readFileSync3(join3(dirname3(fileURLToPath2(import.meta.url)), "..", "skill", "CHANGELOG.json"), "utf8")
40545
+ );
40546
+ return Array.isArray(raw.versions) ? raw.versions : [];
40486
40547
  } catch {
40548
+ return [];
40487
40549
  }
40550
+ })();
40551
+ function changelogSince(installed) {
40552
+ if (!installed) return CHANGELOG.slice(0, 3);
40553
+ const idx = CHANGELOG.findIndex((e) => e.version === installed);
40554
+ return idx === -1 ? CHANGELOG.slice(0, 3) : CHANGELOG.slice(0, idx);
40555
+ }
40556
+ function skillUpdateInfo() {
40557
+ const perSkill = skillVersions();
40558
+ const staleSkills = perSkill.filter((s) => s.installed !== SERVER_VERSION).map((s) => s.name);
40559
+ const oldest = perSkill.reduce(
40560
+ (min, s) => s.installed !== null && (min === null || s.installed < min) ? s.installed : min,
40561
+ null
40562
+ );
40563
+ const updateAvailable = perSkill.length === 0 || staleSkills.length > 0;
40488
40564
  return {
40489
40565
  serverVersion: SERVER_VERSION,
40490
- installedSkillVersion,
40491
- updateAvailable: installedSkillVersion !== SERVER_VERSION
40566
+ installedSkillVersion: oldest,
40567
+ updateAvailable,
40568
+ staleSkills,
40569
+ whatsNew: updateAvailable ? changelogSince(oldest).flatMap((e) => e.highlights) : []
40492
40570
  };
40493
40571
  }
40494
- var server = new McpServer({
40495
- name: "awesomate-hosting",
40496
- version: SERVER_VERSION
40497
- });
40572
+ var SKILL_DRIFT = (() => {
40573
+ const info = skillUpdateInfo();
40574
+ if (!info.updateAvailable) return null;
40575
+ return `${info.installedSkillVersion ?? "unversioned"} \u2192 ${SERVER_VERSION}`;
40576
+ })();
40577
+ var SERVER_INSTRUCTIONS = [
40578
+ "Awesomate connects this account's WordPress hosting, n8n automations, apps and Knowledge Base to Claude.",
40579
+ "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.",
40580
+ "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.",
40581
+ "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).",
40582
+ "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.",
40583
+ 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
40584
+ ].filter((line) => line !== null).join("\n");
40585
+ var server = new McpServer(
40586
+ {
40587
+ name: "awesomate-hosting",
40588
+ version: SERVER_VERSION
40589
+ },
40590
+ { instructions: SERVER_INSTRUCTIONS }
40591
+ );
40498
40592
  function textResult(data) {
40499
40593
  return {
40500
40594
  content: [{ type: "text", text: `${accountStamp()}
40501
40595
  ${JSON.stringify(data, null, 2)}` }]
40502
40596
  };
40503
40597
  }
40598
+ var ERROR_BODY_FIELDS = [
40599
+ "code",
40600
+ "recommendedPlan",
40601
+ "deepLink",
40602
+ "upgradeUrl",
40603
+ "settingsUrl",
40604
+ "missingScopes",
40605
+ "plan",
40606
+ "allowed"
40607
+ ];
40504
40608
  function errorResult(err) {
40505
40609
  const message = err instanceof Error ? err.message : String(err);
40506
- return { content: [{ type: "text", text: `${accountStamp()}
40507
- ${message}` }], isError: true };
40610
+ let text = `${accountStamp()}
40611
+ ${message}`;
40612
+ if (err instanceof HubApiError && err.body && typeof err.body === "object") {
40613
+ const body = err.body;
40614
+ const details = {};
40615
+ for (const field of ERROR_BODY_FIELDS) {
40616
+ if (body[field] !== void 0 && body[field] !== null) details[field] = body[field];
40617
+ }
40618
+ if (Object.keys(details).length > 0) {
40619
+ text += `
40620
+ ${JSON.stringify(details, null, 2)}`;
40621
+ }
40622
+ }
40623
+ return { content: [{ type: "text", text }], isError: true };
40508
40624
  }
40625
+ var READ_ONLY = { readOnlyHint: true };
40626
+ var DESTRUCTIVE = { destructiveHint: true };
40509
40627
  function readTool(name, description, path) {
40510
- server.registerTool(name, { description, inputSchema: {} }, async () => {
40628
+ server.registerTool(name, { description, inputSchema: {}, annotations: READ_ONLY }, async () => {
40511
40629
  try {
40512
40630
  return textResult(await hubGet(requireConfig(), path));
40513
40631
  } catch (err) {
@@ -40518,6 +40636,7 @@ function readTool(name, description, path) {
40518
40636
  server.registerTool(
40519
40637
  "awesomate_whoami",
40520
40638
  {
40639
+ annotations: READ_ONLY,
40521
40640
  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
40641
  inputSchema: {}
40523
40642
  },
@@ -40541,7 +40660,8 @@ server.registerTool(
40541
40660
  server.registerTool(
40542
40661
  "awesomate_get_context",
40543
40662
  {
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.",
40663
+ annotations: READ_ONLY,
40664
+ 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
40665
  inputSchema: {}
40546
40666
  },
40547
40667
  async () => {
@@ -40561,7 +40681,8 @@ readTool(
40561
40681
  server.registerTool(
40562
40682
  "awesomate_n8n_deploy",
40563
40683
  {
40564
- 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.",
40684
+ annotations: DESTRUCTIVE,
40685
+ 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; 409 n8n_credential_invalid = the user's OWN n8n rejected the stored API key (NOT a hub outage \u2014 reads and validate keep working because they never touch their instance): tell them to create a new API key in their n8n under Settings \u2192 n8n API, paste it into their Awesomate account at hub.awesomate.ai/n8n/settings, then retry.",
40565
40686
  inputSchema: {
40566
40687
  action: external_exports.enum(["validate", "create_draft", "update_draft", "activate", "deactivate", "promote", "rollback", "delete_draft"]),
40567
40688
  workflowJson: external_exports.record(external_exports.unknown()).optional().describe("For validate: the full workflow JSON. For create_draft: must contain name, nodes, connections (settings optional)."),
@@ -40635,6 +40756,7 @@ server.registerTool(
40635
40756
  server.registerTool(
40636
40757
  "awesomate_n8n_workflows",
40637
40758
  {
40759
+ annotations: READ_ONLY,
40638
40760
  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
40761
  inputSchema: {
40640
40762
  id: external_exports.string().optional().describe("Workflow id for a single-workflow read; omit for the all-workflows summary"),
@@ -40670,6 +40792,7 @@ server.registerTool(
40670
40792
  server.registerTool(
40671
40793
  "awesomate_n8n_inspect",
40672
40794
  {
40795
+ annotations: READ_ONLY,
40673
40796
  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
40797
  inputSchema: {
40675
40798
  what: external_exports.enum(["nodes", "datatables", "datatable_rows", "possibilities", "credentials", "variables"]),
@@ -40701,7 +40824,8 @@ server.registerTool(
40701
40824
  server.registerTool(
40702
40825
  "awesomate_n8n_executions",
40703
40826
  {
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.",
40827
+ annotations: READ_ONLY,
40828
+ 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
40829
  inputSchema: {
40706
40830
  workflowId: external_exports.string().optional(),
40707
40831
  executionId: external_exports.string().optional(),
@@ -40729,6 +40853,7 @@ server.registerTool(
40729
40853
  server.registerTool(
40730
40854
  "awesomate_n8n_node_docs",
40731
40855
  {
40856
+ annotations: READ_ONLY,
40732
40857
  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
40858
  inputSchema: {
40734
40859
  tool: external_exports.enum(["search_nodes", "get_node", "search_templates", "get_template", "validate_node", "tools_documentation"]),
@@ -40746,6 +40871,7 @@ server.registerTool(
40746
40871
  server.registerTool(
40747
40872
  "awesomate_n8n_datatable_write",
40748
40873
  {
40874
+ annotations: DESTRUCTIVE,
40749
40875
  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
40876
  inputSchema: {
40751
40877
  action: external_exports.enum(["create", "add_column", "insert", "update", "delete_rows"]),
@@ -40780,13 +40906,24 @@ server.registerTool(
40780
40906
  server.registerTool(
40781
40907
  "awesomate_skill_update",
40782
40908
  {
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.",
40909
+ 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
40910
  inputSchema: {}
40785
40911
  },
40786
40912
  async () => {
40787
40913
  try {
40788
40914
  const result = installSkills(SERVER_VERSION);
40789
- return textResult({ ...result, note: "Updated skill files load in the next Claude Code session." });
40915
+ const changed = result.updates.filter((u) => u.from !== u.to);
40916
+ return textResult({
40917
+ ...result,
40918
+ changed: changed.map((u) => `${u.name}: ${u.from ?? "unversioned"} \u2192 ${u.to}`),
40919
+ whatsNew: changelogSince(changed[0]?.from ?? null).flatMap((e) => e.highlights),
40920
+ restart: {
40921
+ note: "Updated skill files load in the next Claude Code session \u2014 one restart lands both the skills and the (self-updating) server.",
40922
+ terminal: "Type exit (or press Ctrl+C twice), then run `claude` again.",
40923
+ vscode: 'Command Palette (Cmd/Ctrl+Shift+P) \u2192 "Developer: Reload Window".',
40924
+ verify: "After restarting, awesomate_get_context should report skill.updateAvailable: false."
40925
+ }
40926
+ });
40790
40927
  } catch (err) {
40791
40928
  return errorResult(err);
40792
40929
  }
@@ -40826,6 +40963,7 @@ server.registerTool(
40826
40963
  server.registerTool(
40827
40964
  "awesomate_request_build",
40828
40965
  {
40966
+ annotations: DESTRUCTIVE,
40829
40967
  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
40968
  inputSchema: {
40831
40969
  title: external_exports.string().describe("Short name for the automation"),
@@ -40894,6 +41032,7 @@ server.registerTool(
40894
41032
  server.registerTool(
40895
41033
  "awesomate_uninstall_site",
40896
41034
  {
41035
+ annotations: DESTRUCTIVE,
40897
41036
  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
41037
  inputSchema: {
40899
41038
  domain: external_exports.string().describe("The site domain to delete"),
@@ -40970,6 +41109,7 @@ var PLAN_LADDER_FALLBACK = {
40970
41109
  server.registerTool(
40971
41110
  "awesomate_get_plan_features",
40972
41111
  {
41112
+ annotations: READ_ONLY,
40973
41113
  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
41114
  inputSchema: {}
40975
41115
  },
@@ -41010,6 +41150,7 @@ server.registerTool(
41010
41150
  server.registerTool(
41011
41151
  "awesomate_list_snapshots",
41012
41152
  {
41153
+ annotations: READ_ONLY,
41013
41154
  description: "List a site\u2019s available snapshots (newest first) with their ids, timestamps, and reasons. Requires shell access (Support Plus+).",
41014
41155
  inputSchema: { domain: external_exports.string().describe("The site domain") }
41015
41156
  },
@@ -41024,6 +41165,7 @@ server.registerTool(
41024
41165
  server.registerTool(
41025
41166
  "awesomate_rollback_site",
41026
41167
  {
41168
+ annotations: DESTRUCTIVE,
41027
41169
  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
41170
  inputSchema: {
41029
41171
  domain: external_exports.string().describe("The site domain"),
@@ -41061,6 +41203,7 @@ server.registerTool(
41061
41203
  server.registerTool(
41062
41204
  "awesomate_site_staging_promote",
41063
41205
  {
41206
+ annotations: DESTRUCTIVE,
41064
41207
  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
41208
  inputSchema: {
41066
41209
  domain: external_exports.string().describe("The LIVE site domain whose staging copy should go live")
@@ -41079,6 +41222,7 @@ server.registerTool(
41079
41222
  server.registerTool(
41080
41223
  "awesomate_site_staging_discard",
41081
41224
  {
41225
+ annotations: DESTRUCTIVE,
41082
41226
  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
41227
  inputSchema: {
41084
41228
  domain: external_exports.string().describe("The LIVE site domain whose staging copy should be discarded")
@@ -41107,6 +41251,7 @@ readTool(
41107
41251
  server.registerTool(
41108
41252
  "awesomate_app_get",
41109
41253
  {
41254
+ annotations: READ_ONLY,
41110
41255
  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
41256
  inputSchema: { appId: external_exports.number().int().positive().describe("The app id from awesomate_app_list / _create") }
41112
41257
  },
@@ -41142,6 +41287,7 @@ server.registerTool(
41142
41287
  server.registerTool(
41143
41288
  "awesomate_app_scaffold",
41144
41289
  {
41290
+ annotations: READ_ONLY,
41145
41291
  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
41292
  inputSchema: { appId: external_exports.number().int().positive().describe("The app id from awesomate_app_create / _list") }
41147
41293
  },
@@ -41154,9 +41300,10 @@ server.registerTool(
41154
41300
  }
41155
41301
  );
41156
41302
  server.registerTool(
41157
- "awesomate_app_deploy",
41303
+ "awesomate_app_deploy_info",
41158
41304
  {
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.",
41305
+ annotations: READ_ONLY,
41306
+ 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
41307
  inputSchema: { appId: external_exports.number().int().positive().describe("The app id") }
41161
41308
  },
41162
41309
  async ({ appId }) => {
@@ -41207,6 +41354,7 @@ server.registerTool(
41207
41354
  server.registerTool(
41208
41355
  "awesomate_app_health",
41209
41356
  {
41357
+ annotations: READ_ONLY,
41210
41358
  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
41359
  inputSchema: { appId: external_exports.number().int().positive().describe("The app id") }
41212
41360
  },
@@ -41254,6 +41402,7 @@ ${scrubKeyMaterial(message)}` }],
41254
41402
  server.registerTool(
41255
41403
  "awesomate_knowledge_status",
41256
41404
  {
41405
+ annotations: READ_ONLY,
41257
41406
  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
41407
  inputSchema: {}
41259
41408
  },
@@ -41307,6 +41456,7 @@ server.registerTool(
41307
41456
  server.registerTool(
41308
41457
  "awesomate_knowledge_search",
41309
41458
  {
41459
+ annotations: READ_ONLY,
41310
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.",
41311
41461
  inputSchema: {
41312
41462
  q: external_exports.string().max(2e3).optional().describe("Search words; empty lists the library filtered by the facets"),
@@ -41432,6 +41582,274 @@ server.registerTool(
41432
41582
  }
41433
41583
  }
41434
41584
  );
41585
+ server.registerTool(
41586
+ "awesomate_knowledge_data",
41587
+ {
41588
+ 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.",
41589
+ inputSchema: {
41590
+ action: external_exports.enum(["metrics", "datasets", "imports", "query"]),
41591
+ dataset: external_exports.string().max(80).optional().describe("query: dataset name from datasets"),
41592
+ 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'}]"),
41593
+ dimensions: external_exports.array(external_exports.string().max(80)).max(6).optional().describe("query: group-by columns"),
41594
+ filters: external_exports.array(external_exports.record(external_exports.unknown())).max(10).optional().describe("query: filter objects, passed through"),
41595
+ limit: external_exports.number().int().min(1).max(500).optional()
41596
+ }
41597
+ },
41598
+ async ({ action, dataset, measures, dimensions, filters, limit }) => {
41599
+ try {
41600
+ const cfg = requireConfig();
41601
+ if (action === "query") {
41602
+ return knowledgeResult(
41603
+ await hubPost(cfg, "/api/knowledge/data/query", { dataset, measures, dimensions, filters, limit })
41604
+ );
41605
+ }
41606
+ return knowledgeResult(await hubGet(cfg, `/api/knowledge/data/${action}`));
41607
+ } catch (err) {
41608
+ return knowledgeError(err);
41609
+ }
41610
+ }
41611
+ );
41612
+ server.registerTool(
41613
+ "awesomate_n8n_kpis",
41614
+ {
41615
+ annotations: READ_ONLY,
41616
+ 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.",
41617
+ inputSchema: { workflowId: external_exports.string().max(64).optional() }
41618
+ },
41619
+ async ({ workflowId }) => {
41620
+ try {
41621
+ const cfg = requireConfig();
41622
+ const path = workflowId ? `/api/my-n8n/workflows/${encodeURIComponent(workflowId)}/kpis` : "/api/my-n8n/kpis";
41623
+ return textResult(await hubGet(cfg, path));
41624
+ } catch (err) {
41625
+ return errorResult(err);
41626
+ }
41627
+ }
41628
+ );
41629
+ server.registerTool(
41630
+ "awesomate_n8n_errors",
41631
+ {
41632
+ annotations: READ_ONLY,
41633
+ 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).",
41634
+ inputSchema: {
41635
+ days: external_exports.number().int().min(1).max(90).optional(),
41636
+ limit: external_exports.number().int().min(1).max(100).optional()
41637
+ }
41638
+ },
41639
+ async ({ days, limit }) => {
41640
+ try {
41641
+ const qs = new URLSearchParams();
41642
+ if (days) qs.set("days", String(days));
41643
+ if (limit) qs.set("limit", String(limit));
41644
+ return textResult(await hubGet(requireConfig(), `/api/my-n8n/errors${qs.size ? `?${qs}` : ""}`));
41645
+ } catch (err) {
41646
+ return errorResult(err);
41647
+ }
41648
+ }
41649
+ );
41650
+ readTool(
41651
+ "awesomate_n8n_storage",
41652
+ "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.",
41653
+ "/api/my-n8n/machine/storage"
41654
+ );
41655
+ server.registerTool(
41656
+ "awesomate_n8n_findings",
41657
+ {
41658
+ annotations: READ_ONLY,
41659
+ description: "Findings from Awesomate's automated error analyzer for THIS account (Pro/Embedded + the 'AI error analysis' privacy toggle): severity, workflow, occurrences, a plain-language clientSummary, needsClientAction (something only the user can fix \u2014 expired logins, third-party quotas), and fixReady (a reviewed fix is prepared \u2014 raise it with awesomate_support to have it applied). Read-only. An empty list on a healthy instance is the good state, not an error.",
41660
+ inputSchema: { limit: external_exports.number().int().min(1).max(100).optional() }
41661
+ },
41662
+ async ({ limit }) => {
41663
+ try {
41664
+ const qs = limit ? `?limit=${limit}` : "";
41665
+ return textResult(await hubGet(requireConfig(), `/api/my-n8n/machine/findings${qs}`));
41666
+ } catch (err) {
41667
+ return errorResult(err);
41668
+ }
41669
+ }
41670
+ );
41671
+ readTool(
41672
+ "awesomate_site_uptime",
41673
+ "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.",
41674
+ "/api/client-hosting/uptime"
41675
+ );
41676
+ readTool(
41677
+ "awesomate_dns_check",
41678
+ "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).",
41679
+ "/api/client-hosting/dns-check"
41680
+ );
41681
+ readTool(
41682
+ "awesomate_dashboard_metrics",
41683
+ "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.",
41684
+ "/api/client-dashboard/metrics"
41685
+ );
41686
+ readTool(
41687
+ "awesomate_account_report",
41688
+ "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.",
41689
+ "/api/client-report"
41690
+ );
41691
+ readTool(
41692
+ "awesomate_privacy_settings",
41693
+ "READ which privacy/consent toggles are on or off for this account \u2014 call it when a tool returns 403 consent_required so you can name the exact toggle instead of guessing. Toggles are changed ONLY by the user in the hub (Settings \u2192 Privacy, hub.awesomate.ai/settings?tab=privacy); there is deliberately no write here.",
41694
+ "/api/client-settings/privacy"
41695
+ );
41696
+ server.registerTool(
41697
+ "awesomate_notifications",
41698
+ {
41699
+ 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.",
41700
+ inputSchema: {
41701
+ action: external_exports.enum(["list", "read", "read_all"]),
41702
+ id: external_exports.number().int().positive().optional().describe("read: the notification id"),
41703
+ limit: external_exports.number().int().min(1).max(100).optional().describe("list: default 30")
41704
+ }
41705
+ },
41706
+ async ({ action, id, limit }) => {
41707
+ try {
41708
+ const cfg = requireConfig();
41709
+ if (action === "list") {
41710
+ const qs = limit ? `?limit=${limit}` : "";
41711
+ return textResult(await hubGet(cfg, `/api/notifications${qs}`));
41712
+ }
41713
+ if (action === "read") {
41714
+ if (!id) return errorResult(new Error("action 'read' needs id"));
41715
+ return textResult(await hubPost(cfg, `/api/notifications/${id}/read`));
41716
+ }
41717
+ return textResult(await hubPost(cfg, "/api/notifications/read-all"));
41718
+ } catch (err) {
41719
+ return errorResult(err);
41720
+ }
41721
+ }
41722
+ );
41723
+ server.registerTool(
41724
+ "awesomate_wp_post",
41725
+ {
41726
+ 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'].",
41727
+ inputSchema: {
41728
+ action: external_exports.enum(["create", "update", "get"]),
41729
+ domain: external_exports.string().min(3).max(253).describe("The site domain, e.g. mybusiness.awesomate.site"),
41730
+ postId: external_exports.number().int().positive().optional().describe("update/get"),
41731
+ postType: external_exports.enum(["post", "page"]).optional().describe("create only; default post"),
41732
+ title: external_exports.string().max(300).optional(),
41733
+ content: external_exports.string().max(15e4).optional().describe("HTML or plain text"),
41734
+ excerpt: external_exports.string().max(1e3).optional(),
41735
+ slug: external_exports.string().max(190).optional().describe("URL slug, [a-z0-9-]"),
41736
+ status: external_exports.enum(["draft", "publish", "pending", "private"]).optional(),
41737
+ includeContent: external_exports.boolean().optional().describe("get only")
41738
+ }
41739
+ },
41740
+ async ({ action, domain, postId, postType, title, content, excerpt, slug, status, includeContent }) => {
41741
+ try {
41742
+ const cfg = requireConfig();
41743
+ const dom = encodeURIComponent(domain.toLowerCase());
41744
+ if (action === "get") {
41745
+ if (!postId) return errorResult(new Error("action 'get' needs postId"));
41746
+ const qs = includeContent ? "?includeContent=1" : "";
41747
+ return textResult(await hubGet(cfg, `/api/client-hosting/sites/${dom}/posts/${postId}${qs}`));
41748
+ }
41749
+ return textResult(
41750
+ await hubPost(cfg, `/api/client-hosting/sites/${dom}/posts`, {
41751
+ action,
41752
+ postId,
41753
+ postType,
41754
+ title,
41755
+ content,
41756
+ excerpt,
41757
+ slug,
41758
+ status
41759
+ })
41760
+ );
41761
+ } catch (err) {
41762
+ return errorResult(err);
41763
+ }
41764
+ }
41765
+ );
41766
+ server.registerTool(
41767
+ "awesomate_wp_media_import",
41768
+ {
41769
+ 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.",
41770
+ inputSchema: {
41771
+ domain: external_exports.string().min(3).max(253),
41772
+ url: external_exports.string().url().max(2048).describe("https URL of the image/file"),
41773
+ title: external_exports.string().max(300).optional()
41774
+ }
41775
+ },
41776
+ async ({ domain, url, title }) => {
41777
+ try {
41778
+ const dom = encodeURIComponent(domain.toLowerCase());
41779
+ return textResult(await hubPost(requireConfig(), `/api/client-hosting/sites/${dom}/media`, { url, title }));
41780
+ } catch (err) {
41781
+ return errorResult(err);
41782
+ }
41783
+ }
41784
+ );
41785
+ server.registerPrompt(
41786
+ "awesomate-status",
41787
+ {
41788
+ description: "One-screen health digest: account, automations, sites, knowledge, anything needing attention."
41789
+ },
41790
+ () => ({
41791
+ messages: [
41792
+ {
41793
+ role: "user",
41794
+ content: {
41795
+ type: "text",
41796
+ 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."
41797
+ }
41798
+ }
41799
+ ]
41800
+ })
41801
+ );
41802
+ server.registerPrompt(
41803
+ "awesomate-checkup",
41804
+ {
41805
+ description: "Run the full health sweep: automations, error findings, uptime, DNS, storage, SEO basics."
41806
+ },
41807
+ () => ({
41808
+ messages: [
41809
+ {
41810
+ role: "user",
41811
+ content: {
41812
+ type: "text",
41813
+ 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."
41814
+ }
41815
+ }
41816
+ ]
41817
+ })
41818
+ );
41819
+ server.registerPrompt(
41820
+ "awesomate-report",
41821
+ {
41822
+ description: "Assemble this account's business report: what worked, what broke, engagement, value delivered."
41823
+ },
41824
+ () => ({
41825
+ messages: [
41826
+ {
41827
+ role: "user",
41828
+ content: {
41829
+ type: "text",
41830
+ 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)."
41831
+ }
41832
+ }
41833
+ ]
41834
+ })
41835
+ );
41836
+ server.registerPrompt(
41837
+ "awesomate-ideas",
41838
+ {
41839
+ description: "Grounded automation ideas from what this account actually has \u2014 with what each would save."
41840
+ },
41841
+ () => ({
41842
+ messages: [
41843
+ {
41844
+ role: "user",
41845
+ content: {
41846
+ type: "text",
41847
+ 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."
41848
+ }
41849
+ }
41850
+ ]
41851
+ })
41852
+ );
41435
41853
  async function main() {
41436
41854
  try {
41437
41855
  config2 = loadConfig();
@@ -41439,10 +41857,34 @@ async function main() {
41439
41857
  configError = err instanceof Error ? err.message : String(err);
41440
41858
  console.error(`[awesomate-hosting-mcp] account not resolved: ${configError}`);
41441
41859
  }
41860
+ if (SKILL_DRIFT && autoUpdateEnabled()) {
41861
+ try {
41862
+ const result = installSkills(SERVER_VERSION);
41863
+ console.error(
41864
+ `[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`
41865
+ );
41866
+ } catch (err) {
41867
+ console.error(
41868
+ "[awesomate-hosting-mcp] skills auto-update failed (non-fatal):",
41869
+ err instanceof Error ? err.message : err
41870
+ );
41871
+ }
41872
+ }
41442
41873
  const transport = new StdioServerTransport();
41443
41874
  await server.connect(transport);
41444
41875
  console.error(`[awesomate-hosting-mcp] connected (stdio) \u2014 ${accountStamp()}`);
41445
41876
  }
41877
+ function autoUpdateEnabled() {
41878
+ const env = process.env.AWESOMATE_SKILLS_AUTOUPDATE;
41879
+ if (env === "1" || env === "true") return true;
41880
+ if (env === "0" || env === "false") return false;
41881
+ try {
41882
+ const raw = JSON.parse(readFileSync3(join3(homedir3(), ".awesomate", "credentials.json"), "utf8"));
41883
+ return raw.autoUpdateSkills === true;
41884
+ } catch {
41885
+ return false;
41886
+ }
41887
+ }
41446
41888
  main().catch((err) => {
41447
41889
  console.error(
41448
41890
  "[awesomate-hosting-mcp] fatal:",