@awesomate/hosting-mcp 0.17.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
@@ -39950,11 +40006,11 @@ function scrubKeyMaterial(text) {
39950
40006
  var KNOWLEDGE_GATE_COPY = {
39951
40007
  displayName: "Knowledge Base",
39952
40008
  requiredPlan: "Pro",
39953
- description: "Turn your own content \u2014 videos, blog posts, books, socials \u2014 into a knowledge base your AI agents answer from, with a citation for every claim. If it isn't in your content, the bot says so instead of making something up.",
40009
+ description: "Turn your own content (videos, blog posts, books, socials) into a knowledge base your AI agents answer from, with a citation for every claim. If it isn't in your content, the bot says so instead of making something up.",
39954
40010
  bullets: [
39955
40011
  "Answers come only from your published content, cited back to the exact source",
39956
- `No made-up answers in front of a customer \u2014 no evidence means an honest "I don't know"`,
39957
- "Feed it videos, podcasts, blog posts and books \u2014 we transcribe and index the lot",
40012
+ `No made-up answers in front of a customer: no evidence means an honest "I don't know"`,
40013
+ "Feed it videos, podcasts, blog posts and books. We transcribe and index the lot",
39958
40014
  "Plugs straight into your n8n chat agents, so the 10pm enquiry gets a real answer",
39959
40015
  "Hosted in Sydney, isolated to your account, and gone the moment you delete it"
39960
40016
  ]
@@ -39965,7 +40021,7 @@ function upsellPayload(apiBase, plan) {
39965
40021
  ...typeof plan === "string" ? { plan } : {},
39966
40022
  feature: KNOWLEDGE_GATE_COPY,
39967
40023
  billingUrl: `${apiBase}/billing`,
39968
- note: "Knowledge Base needs the Pro plan. Do NOT retry \u2014 relay this honestly, share the billing link, and let the user decide. Preview + explicit confirmation before any upgrade."
40024
+ note: "Knowledge Base needs the Pro plan. Do NOT retry. Relay this honestly, share the billing link, and let the user decide. Preview + explicit confirmation before any upgrade."
39969
40025
  };
39970
40026
  }
39971
40027
  function consentPayload(message) {
@@ -39973,14 +40029,14 @@ function consentPayload(message) {
39973
40029
  consent_required: true,
39974
40030
  message,
39975
40031
  settingsPath: 'Settings \u2192 Privacy \u2192 "Knowledge Platform"',
39976
- note: "The user must flip the consent themselves in the hub \u2014 do NOT retry until they confirm it is on."
40032
+ note: "The user must flip the consent themselves in the hub. Do NOT retry until they confirm it is on."
39977
40033
  };
39978
40034
  }
39979
40035
  function notAvailablePayload(apiBase) {
39980
40036
  return {
39981
40037
  available: false,
39982
40038
  reason: "not_available_on_this_hub",
39983
- note: "Knowledge Base is not enabled on this hub (feature switch off, or the hub predates it). The connection itself is fine \u2014 do not retry. The user can ask Awesomate support when it will be available for their account.",
40039
+ note: "Knowledge Base is not enabled on this hub (feature switch off, or the hub predates it). The connection itself is fine; do not retry. The user can ask Awesomate support when it will be available for their account.",
39984
40040
  hubUrl: apiBase
39985
40041
  };
39986
40042
  }
@@ -39990,7 +40046,7 @@ function packConfirmationPayload(body) {
39990
40046
  purchased: false,
39991
40047
  dimension: body.dimension ?? null,
39992
40048
  pack: body.pack ?? null,
39993
- note: "This ingest would exceed the included allowance. NOTHING was purchased and the source was NOT added. An Ingestion Pack costs 1 credit ($100) \u2014 state that plainly, get an explicit yes, then send the user to the hub Knowledge \u2192 Usage page to buy it (this tool never spends credits)."
40049
+ note: "This ingest would exceed the included allowance. NOTHING was purchased and the source was NOT added. An Ingestion Pack costs 1 credit ($100). State that plainly, get an explicit yes, then send the user to the hub Knowledge \u2192 Usage page to buy it (this tool never spends credits)."
39994
40050
  };
39995
40051
  }
39996
40052
  function bodyOf(err) {
@@ -40018,14 +40074,14 @@ async function knowledgeStatus(config3) {
40018
40074
  const annotated = {
40019
40075
  ...status,
40020
40076
  usage_glossary: {
40021
- pages: "month-to-date hub-submitted page quota used (vs usage.included.pages) \u2014 NOT the size of the library",
40077
+ pages: "month-to-date hub-submitted page quota used (vs usage.included.pages): NOT the size of the library",
40022
40078
  sources_total: 'sources currently in the library; kinds in usage.sources_by_kind or awesomate_knowledge_sources {action:"summary"}'
40023
40079
  }
40024
40080
  };
40025
40081
  if (usage.sources_total_source === "hourly_gauge") {
40026
- annotated.note = "The live source count failed for this call: usage.sources_total / sources_failed are the last hourly gauge \u2014 possibly stale, and 0 for a library the gauge could never count. Retry before quoting them; the tenant itself is active.";
40082
+ annotated.note = "The live source count failed for this call: usage.sources_total / sources_failed are the last hourly gauge: possibly stale, and 0 for a library the gauge could never count. Retry before quoting them; the tenant itself is active.";
40027
40083
  } else if (usage.sources_total_truncated === true) {
40028
- annotated.note = `usage.sources_total is a floor: the live count stopped at ${String(usage.sources_total)} sources (paged-count cap). The library holds at least that many \u2014 never quote it as the exact total.`;
40084
+ annotated.note = `usage.sources_total is a floor: the live count stopped at ${String(usage.sources_total)} sources (paged-count cap). The library holds at least that many: never quote it as the exact total.`;
40029
40085
  }
40030
40086
  return annotated;
40031
40087
  } catch (err) {
@@ -40038,7 +40094,7 @@ async function knowledgeProvision(config3) {
40038
40094
  if (result.pending) {
40039
40095
  return {
40040
40096
  ...result,
40041
- note: "Provisioning continues in the background \u2014 poll awesomate_knowledge_status until the tenant is active (a few minutes at most). Re-calling provision is safe (idempotent)."
40097
+ note: "Provisioning continues in the background. Poll awesomate_knowledge_status until the tenant is active (a few minutes at most). Re-calling provision is safe (idempotent)."
40042
40098
  };
40043
40099
  }
40044
40100
  return result;
@@ -40058,7 +40114,7 @@ function describeSourcePage(page) {
40058
40114
  ...page,
40059
40115
  page: { returned: page.sources.length, kinds_on_this_page: kinds, has_more: hasMore },
40060
40116
  ...hasMore ? {
40061
- note: 'This is ONE page of the most recently ingested sources, not the whole library \u2014 do not summarise the knowledge base from it. Pass cursor=next_cursor to continue (limit up to 200). For the total, use awesomate_knowledge_status \u2192 usage.sources_total (sources_total_truncated:true means "at least that many").'
40117
+ note: 'This is ONE page of the most recently ingested sources, not the whole library: do not summarise the knowledge base from it. Pass cursor=next_cursor to continue (limit up to 200). For the total, use awesomate_knowledge_status \u2192 usage.sources_total (sources_total_truncated:true means "at least that many").'
40062
40118
  } : {}
40063
40119
  };
40064
40120
  }
@@ -40104,7 +40160,7 @@ async function knowledgeSources(config3, args) {
40104
40160
  if (!args.url && !args.sitemap) {
40105
40161
  return {
40106
40162
  error: "invalid_request",
40107
- note: "add needs url or sitemap. For a local FILE, send the user to the hub: Knowledge \u2192 Sources \u2192 Upload \u2014 files stream there; this tool cannot carry file bytes in v1."
40163
+ note: "add needs url or sitemap. For a local FILE, send the user to the hub: Knowledge \u2192 Sources \u2192 Upload. Files stream there; this tool cannot carry file bytes in v1."
40108
40164
  };
40109
40165
  }
40110
40166
  const body = args.url ? { url: args.url } : { sitemap: args.sitemap, ...args.since ? { since: args.since } : {} };
@@ -40139,7 +40195,7 @@ function renderAnswer(meta, streamedAnswer, streamError = null) {
40139
40195
  detail: streamError ?? (streamedAnswer || null),
40140
40196
  configured_fallback: fallback,
40141
40197
  ...session,
40142
- note: "The knowledge platform failed to answer \u2014 a service problem, NOT a content gap. Do not tell the user their content lacks this. Say the service hit a problem, retry once, and if it persists raise it via awesomate_support. configured_fallback is only what an end customer would have seen meanwhile."
40198
+ note: "The knowledge platform failed to answer: a service problem, NOT a content gap. Do not tell the user their content lacks this. Say the service hit a problem, retry once, and if it persists raise it via awesomate_support. configured_fallback is only what an end customer would have seen meanwhile."
40143
40199
  };
40144
40200
  }
40145
40201
  if (status !== "ok") {
@@ -40149,7 +40205,7 @@ function renderAnswer(meta, streamedAnswer, streamError = null) {
40149
40205
  not_in_verified_content: true,
40150
40206
  configured_fallback: fallback,
40151
40207
  ...session,
40152
- note: "The knowledge base has no verified answer for this \u2014 relay that honestly (use the configured fallback wording if present). Never fill the gap from memory."
40208
+ note: "The knowledge base has no verified answer for this. Relay that honestly (use the configured fallback wording if present). Never fill the gap from memory."
40153
40209
  };
40154
40210
  }
40155
40211
  const rawSources = Array.isArray(meta.sources) ? meta.sources : [];
@@ -40166,7 +40222,7 @@ function renderAnswer(meta, streamedAnswer, streamError = null) {
40166
40222
  sources,
40167
40223
  ...typeof meta.score === "number" ? { score: meta.score } : {},
40168
40224
  ...session,
40169
- note: "Present the answer with its numbered sources \u2014 the citations are the product."
40225
+ note: "Present the answer with its numbered sources; the citations are the product."
40170
40226
  };
40171
40227
  }
40172
40228
  async function knowledgeAsk(config3, args) {
@@ -40174,7 +40230,7 @@ async function knowledgeAsk(config3, args) {
40174
40230
  const raw = await hubPost(
40175
40231
  config3,
40176
40232
  "/api/knowledge/chat",
40177
- { question: args.question, ...args.session ? { session: args.session } : {} },
40233
+ { question: args.question, ...args.session ? { session: args.session } : {}, ...args.filters ? { filters: askFilters(args.filters) } : {} },
40178
40234
  { accept: "application/json" }
40179
40235
  );
40180
40236
  if (raw && typeof raw === "object") {
@@ -40210,10 +40266,10 @@ function entityLayerUnavailablePayload() {
40210
40266
  return {
40211
40267
  available: false,
40212
40268
  reason: "entity_layer_not_available",
40213
- note: "People, places and connections are not switched on for this knowledge base yet (the platform has not deployed the entity layer for this tenant). Nothing to fix on the user's side \u2014 do not retry; the counts appear once the first resolution runs."
40269
+ note: "People, places and connections are not switched on for this knowledge base yet (the platform has not deployed the entity layer for this tenant). Nothing to fix on the user's side: do not retry; the counts appear once the first resolution runs."
40214
40270
  };
40215
40271
  }
40216
- var PHOTOS_NOTE = "Text only: names, mention counts and the sources behind them. Photos of unnamed people are only viewable on the hub Knowledge \u2192 People page \u2014 never guess who someone is from a handle.";
40272
+ var PHOTOS_NOTE = "Text only: names, mention counts and the sources behind them. Photos of unnamed people are only viewable on the hub Knowledge \u2192 People page: never guess who someone is from a handle.";
40217
40273
  function invalid(note) {
40218
40274
  return { error: "invalid_request", note };
40219
40275
  }
@@ -40224,7 +40280,7 @@ function mapPeopleError(err, apiBase) {
40224
40280
  return {
40225
40281
  error: "not_found",
40226
40282
  message: typeof body.message === "string" ? body.message : "No such person or entity on this knowledge base",
40227
- note: "Use the exact person_id / entity_id from a fresh list \u2014 ids are opaque and per-tenant."
40283
+ note: "Use the exact person_id / entity_id from a fresh list: ids are opaque and per-tenant."
40228
40284
  };
40229
40285
  }
40230
40286
  }
@@ -40256,13 +40312,13 @@ async function knowledgePeople(config3, args) {
40256
40312
  const page = await hubGet(config3, `/api/knowledge/entities/aliases?${q.toString()}`);
40257
40313
  return {
40258
40314
  ...page,
40259
- note: "Each suggestion is a text name the content uses that probably refers to a known entity. Show the user the alias, the suggested match and the confidence, then use decide with the (kind, alias_norm) pair \u2014 only after they explicitly say accept, reject, or create."
40315
+ note: "Each suggestion is a text name the content uses that probably refers to a known entity. Show the user the alias, the suggested match and the confidence, then use decide with the (kind, alias_norm) pair: only after they explicitly say accept, reject, or create."
40260
40316
  };
40261
40317
  }
40262
40318
  const result = await hubPost(config3, "/api/knowledge/entities/resolve", {});
40263
40319
  return {
40264
40320
  ...result,
40265
- note: result.status === "queued" ? "Resolution queued. It re-scans the content for people/places/topics and counts toward the ingestion allowance; the user's names and decisions are never undone. Check back with list for the refreshed counts." : "A resolution is already queued or running \u2014 nothing new was started. Check back with list for the refreshed counts."
40321
+ note: result.status === "queued" ? "Resolution queued. It re-scans the content for people/places/topics and counts toward the ingestion allowance; the user's names and decisions are never undone. Check back with list for the refreshed counts." : "A resolution is already queued or running: nothing new was started. Check back with list for the refreshed counts."
40266
40322
  };
40267
40323
  }
40268
40324
  if (args.action === "get") {
@@ -40278,7 +40334,7 @@ async function knowledgePeople(config3, args) {
40278
40334
  let body;
40279
40335
  if (args.action === "rename") {
40280
40336
  const name = args.displayName?.trim();
40281
- if (!name) return invalid("rename needs displayName (1\u201380 chars) \u2014 the name the USER gave, never a guess");
40337
+ if (!name) return invalid("rename needs displayName (1\u201380 chars): the name the USER gave, never a guess");
40282
40338
  body = { display_name: name };
40283
40339
  } else {
40284
40340
  body = { status: args.action === "hide" ? "hidden" : "unknown" };
@@ -40292,7 +40348,7 @@ async function knowledgePeople(config3, args) {
40292
40348
  changed: Object.keys(body),
40293
40349
  requested: body,
40294
40350
  ...result,
40295
- note: args.action === "rename" ? "The name is attached to every source this person appears in and the agent uses it from the next reindex. Read the new display_name back to the user to confirm." : args.action === "hide" ? "Hidden \u2014 this person no longer appears in answers. Reversible with unhide." : "Unhidden \u2014 back in answers from the next resolution."
40351
+ note: args.action === "rename" ? "The name is attached to every source this person appears in and the agent uses it from the next reindex. Read the new display_name back to the user to confirm." : args.action === "hide" ? "Hidden: this person no longer appears in answers. Reversible with unhide." : "Unhidden: back in answers from the next resolution."
40296
40352
  };
40297
40353
  }
40298
40354
  if (args.action === "merge") {
@@ -40325,7 +40381,7 @@ async function knowledgePeople(config3, args) {
40325
40381
  return {
40326
40382
  requested: body,
40327
40383
  ...result,
40328
- note: args.decision === "reject" ? "Rejected \u2014 the alias stays a plain text mention and will not be suggested again." : "Accepted \u2014 mentions re-link on the next resolution. Read entity_id back to the user."
40384
+ note: args.decision === "reject" ? "Rejected: the alias stays a plain text mention and will not be suggested again." : "Accepted: mentions re-link on the next resolution. Read entity_id back to the user."
40329
40385
  };
40330
40386
  }
40331
40387
  return invalid(`unknown action ${String(args.action)}`);
@@ -40357,6 +40413,101 @@ async function knowledgeAgent(config3, args) {
40357
40413
  return mapKnowledgeError(err, config3.apiBase);
40358
40414
  }
40359
40415
  }
40416
+ function askFilters(f) {
40417
+ const out = {};
40418
+ for (const name of ["kind", "topic", "person", "place", "category", "author"]) {
40419
+ if (f[name] !== void 0) out[name] = f[name];
40420
+ }
40421
+ const docs = f.doc ?? f.doc_ids;
40422
+ if (docs !== void 0) out.doc_ids = docs;
40423
+ if (f.year !== void 0) {
40424
+ const years = Array.isArray(f.year) ? f.year : [f.year];
40425
+ out.year = years.map((y) => Number(y)).filter((y) => Number.isInteger(y));
40426
+ }
40427
+ return out;
40428
+ }
40429
+ var SENTINEL_PRE = "\uE000";
40430
+ var SENTINEL_POST = "\uE001";
40431
+ function plainSnippet(text) {
40432
+ return text.split(SENTINEL_PRE).join("**").split(SENTINEL_POST).join("**");
40433
+ }
40434
+ async function knowledgeSearch(config3, args) {
40435
+ try {
40436
+ const q = new URLSearchParams();
40437
+ if (args.q) q.set("q", args.q);
40438
+ if (args.kind) q.set("kind", args.kind);
40439
+ for (const [name, values] of [
40440
+ ["topic", args.topic],
40441
+ ["person", args.person],
40442
+ ["place", args.place],
40443
+ ["category", args.category],
40444
+ ["author", args.author],
40445
+ ["year", args.year],
40446
+ ["doc", args.doc]
40447
+ ]) {
40448
+ for (const v of values ?? []) q.append(name, v);
40449
+ }
40450
+ if (args.limit !== void 0) q.set("limit", String(args.limit));
40451
+ if (args.offset !== void 0) q.set("offset", String(args.offset));
40452
+ if (args.include_media) q.set("include", "media");
40453
+ const qs = q.toString();
40454
+ const result = await hubGet(config3, `/api/knowledge/explore${qs ? `?${qs}` : ""}`);
40455
+ const hits = Array.isArray(result.hits) ? result.hits.map((h) => ({
40456
+ ...h,
40457
+ snippet: typeof h.snippet === "string" ? plainSnippet(h.snippet) : h.snippet
40458
+ })) : result.hits;
40459
+ const { highlight: _highlight, ...rest } = result;
40460
+ return {
40461
+ ...rest,
40462
+ hits,
40463
+ note: "Facet counts describe the CURRENT filters; pass repeated facet values to OR within a facet. media URLs (url/poster_url, present with include_media) are presigned and expire in minutes: use them immediately, never store them."
40464
+ };
40465
+ } catch (err) {
40466
+ return mapKnowledgeError(err, config3.apiBase);
40467
+ }
40468
+ }
40469
+ function mapAgentsError(err, apiBase) {
40470
+ if (err instanceof HubApiError && err.status === 404) {
40471
+ const body = bodyOf(err);
40472
+ if (body && body.error === "not_found") {
40473
+ return { error: "not_found", detail: typeof body.message === "string" ? body.message : "No such agent or resource for this account." };
40474
+ }
40475
+ }
40476
+ return mapKnowledgeError(err, apiBase);
40477
+ }
40478
+ async function knowledgeAgents(config3, args) {
40479
+ try {
40480
+ if (args.action === "list") {
40481
+ return await hubGet(config3, "/api/knowledge/agents");
40482
+ }
40483
+ if (args.action === "get") {
40484
+ if (!args.agentId) return { error: "agentId is required for get" };
40485
+ return await hubGet(config3, `/api/knowledge/agents/${encodeURIComponent(args.agentId)}`);
40486
+ }
40487
+ if (args.action === "create") {
40488
+ if (args.goal) {
40489
+ const draft = await hubPost(config3, "/api/knowledge/agents/draft", { goal: args.goal });
40490
+ if (!draft.proposal) return { error: "draft_failed", detail: draft };
40491
+ const created = await hubPost(config3, "/api/knowledge/agents", draft.proposal);
40492
+ return { ...created, test_questions: draft.test_questions ?? [], status_note: "Created as a private DRAFT: test it, then publish when the user approves." };
40493
+ }
40494
+ if (!args.name) return { error: "name (or goal) is required for create" };
40495
+ return await hubPost(config3, "/api/knowledge/agents", { name: args.name });
40496
+ }
40497
+ if (args.action === "publish") {
40498
+ if (!args.agentId) return { error: "agentId is required for publish" };
40499
+ const out = await hubPost(config3, `/api/knowledge/agents/${encodeURIComponent(args.agentId)}/publish`, {});
40500
+ return { ...out, status_note: "Live immediately for every key bound to this agent." };
40501
+ }
40502
+ if (!args.agentId || !args.message) return { error: "agentId and message are required for test" };
40503
+ return await hubPost(config3, `/api/knowledge/agents/${encodeURIComponent(args.agentId)}/chat`, {
40504
+ message: args.message,
40505
+ ...args.sessionId ? { session_id: args.sessionId } : {}
40506
+ });
40507
+ } catch (err) {
40508
+ return mapAgentsError(err, config3.apiBase);
40509
+ }
40510
+ }
40360
40511
 
40361
40512
  // src/index.ts
40362
40513
  var config2 = null;
@@ -40368,51 +40519,108 @@ function requireConfig() {
40368
40519
  return config2;
40369
40520
  }
40370
40521
  function accountStamp() {
40522
+ const drift = SKILL_DRIFT ? ` \xB7 skills ${SKILL_DRIFT} \u2014 update ready, run awesomate_skill_update` : "";
40371
40523
  if (!config2) return `account: NOT CONNECTED \u2014 ${configError ?? "unknown error"}`;
40372
- 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}`;
40373
40525
  }
40374
40526
  var SERVER_VERSION = (() => {
40375
40527
  try {
40376
40528
  const pkg = JSON.parse(
40377
- readFileSync2(join3(dirname3(fileURLToPath2(import.meta.url)), "..", "package.json"), "utf8")
40529
+ readFileSync3(join3(dirname3(fileURLToPath2(import.meta.url)), "..", "package.json"), "utf8")
40378
40530
  );
40379
40531
  return pkg.version ?? "0.0.0";
40380
40532
  } catch {
40381
40533
  return "0.0.0";
40382
40534
  }
40383
40535
  })();
40384
- function skillUpdateInfo() {
40385
- let installedSkillVersion = null;
40536
+ var CHANGELOG = (() => {
40386
40537
  try {
40387
- installedSkillVersion = readFileSync2(
40388
- join3(homedir3(), ".claude", "skills", "awesomate-hosting", ".installed-version"),
40389
- "utf8"
40390
- ).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 : [];
40391
40542
  } catch {
40543
+ return [];
40392
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;
40393
40559
  return {
40394
40560
  serverVersion: SERVER_VERSION,
40395
- installedSkillVersion,
40396
- updateAvailable: installedSkillVersion !== SERVER_VERSION
40561
+ installedSkillVersion: oldest,
40562
+ updateAvailable,
40563
+ staleSkills,
40564
+ whatsNew: updateAvailable ? changelogSince(oldest).flatMap((e) => e.highlights) : []
40397
40565
  };
40398
40566
  }
40399
- var server = new McpServer({
40400
- name: "awesomate-hosting",
40401
- version: SERVER_VERSION
40402
- });
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
+ );
40403
40587
  function textResult(data) {
40404
40588
  return {
40405
40589
  content: [{ type: "text", text: `${accountStamp()}
40406
40590
  ${JSON.stringify(data, null, 2)}` }]
40407
40591
  };
40408
40592
  }
40593
+ var ERROR_BODY_FIELDS = [
40594
+ "code",
40595
+ "recommendedPlan",
40596
+ "deepLink",
40597
+ "upgradeUrl",
40598
+ "settingsUrl",
40599
+ "missingScopes",
40600
+ "plan",
40601
+ "allowed"
40602
+ ];
40409
40603
  function errorResult(err) {
40410
40604
  const message = err instanceof Error ? err.message : String(err);
40411
- return { content: [{ type: "text", text: `${accountStamp()}
40412
- ${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 };
40413
40619
  }
40620
+ var READ_ONLY = { readOnlyHint: true };
40621
+ var DESTRUCTIVE = { destructiveHint: true };
40414
40622
  function readTool(name, description, path) {
40415
- server.registerTool(name, { description, inputSchema: {} }, async () => {
40623
+ server.registerTool(name, { description, inputSchema: {}, annotations: READ_ONLY }, async () => {
40416
40624
  try {
40417
40625
  return textResult(await hubGet(requireConfig(), path));
40418
40626
  } catch (err) {
@@ -40423,6 +40631,7 @@ function readTool(name, description, path) {
40423
40631
  server.registerTool(
40424
40632
  "awesomate_whoami",
40425
40633
  {
40634
+ annotations: READ_ONLY,
40426
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.",
40427
40636
  inputSchema: {}
40428
40637
  },
@@ -40446,7 +40655,8 @@ server.registerTool(
40446
40655
  server.registerTool(
40447
40656
  "awesomate_get_context",
40448
40657
  {
40449
- 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.",
40450
40660
  inputSchema: {}
40451
40661
  },
40452
40662
  async () => {
@@ -40466,6 +40676,7 @@ readTool(
40466
40676
  server.registerTool(
40467
40677
  "awesomate_n8n_deploy",
40468
40678
  {
40679
+ annotations: DESTRUCTIVE,
40469
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.",
40470
40681
  inputSchema: {
40471
40682
  action: external_exports.enum(["validate", "create_draft", "update_draft", "activate", "deactivate", "promote", "rollback", "delete_draft"]),
@@ -40540,6 +40751,7 @@ server.registerTool(
40540
40751
  server.registerTool(
40541
40752
  "awesomate_n8n_workflows",
40542
40753
  {
40754
+ annotations: READ_ONLY,
40543
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.",
40544
40756
  inputSchema: {
40545
40757
  id: external_exports.string().optional().describe("Workflow id for a single-workflow read; omit for the all-workflows summary"),
@@ -40575,6 +40787,7 @@ server.registerTool(
40575
40787
  server.registerTool(
40576
40788
  "awesomate_n8n_inspect",
40577
40789
  {
40790
+ annotations: READ_ONLY,
40578
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.`,
40579
40792
  inputSchema: {
40580
40793
  what: external_exports.enum(["nodes", "datatables", "datatable_rows", "possibilities", "credentials", "variables"]),
@@ -40606,7 +40819,8 @@ server.registerTool(
40606
40819
  server.registerTool(
40607
40820
  "awesomate_n8n_executions",
40608
40821
  {
40609
- 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.",
40610
40824
  inputSchema: {
40611
40825
  workflowId: external_exports.string().optional(),
40612
40826
  executionId: external_exports.string().optional(),
@@ -40634,6 +40848,7 @@ server.registerTool(
40634
40848
  server.registerTool(
40635
40849
  "awesomate_n8n_node_docs",
40636
40850
  {
40851
+ annotations: READ_ONLY,
40637
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.",
40638
40853
  inputSchema: {
40639
40854
  tool: external_exports.enum(["search_nodes", "get_node", "search_templates", "get_template", "validate_node", "tools_documentation"]),
@@ -40651,6 +40866,7 @@ server.registerTool(
40651
40866
  server.registerTool(
40652
40867
  "awesomate_n8n_datatable_write",
40653
40868
  {
40869
+ annotations: DESTRUCTIVE,
40654
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.",
40655
40871
  inputSchema: {
40656
40872
  action: external_exports.enum(["create", "add_column", "insert", "update", "delete_rows"]),
@@ -40685,13 +40901,24 @@ server.registerTool(
40685
40901
  server.registerTool(
40686
40902
  "awesomate_skill_update",
40687
40903
  {
40688
- 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.",
40689
40905
  inputSchema: {}
40690
40906
  },
40691
40907
  async () => {
40692
40908
  try {
40693
40909
  const result = installSkills(SERVER_VERSION);
40694
- 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
+ });
40695
40922
  } catch (err) {
40696
40923
  return errorResult(err);
40697
40924
  }
@@ -40731,6 +40958,7 @@ server.registerTool(
40731
40958
  server.registerTool(
40732
40959
  "awesomate_request_build",
40733
40960
  {
40961
+ annotations: DESTRUCTIVE,
40734
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).",
40735
40963
  inputSchema: {
40736
40964
  title: external_exports.string().describe("Short name for the automation"),
@@ -40799,6 +41027,7 @@ server.registerTool(
40799
41027
  server.registerTool(
40800
41028
  "awesomate_uninstall_site",
40801
41029
  {
41030
+ annotations: DESTRUCTIVE,
40802
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+.",
40803
41032
  inputSchema: {
40804
41033
  domain: external_exports.string().describe("The site domain to delete"),
@@ -40875,6 +41104,7 @@ var PLAN_LADDER_FALLBACK = {
40875
41104
  server.registerTool(
40876
41105
  "awesomate_get_plan_features",
40877
41106
  {
41107
+ annotations: READ_ONLY,
40878
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.",
40879
41109
  inputSchema: {}
40880
41110
  },
@@ -40915,6 +41145,7 @@ server.registerTool(
40915
41145
  server.registerTool(
40916
41146
  "awesomate_list_snapshots",
40917
41147
  {
41148
+ annotations: READ_ONLY,
40918
41149
  description: "List a site\u2019s available snapshots (newest first) with their ids, timestamps, and reasons. Requires shell access (Support Plus+).",
40919
41150
  inputSchema: { domain: external_exports.string().describe("The site domain") }
40920
41151
  },
@@ -40929,6 +41160,7 @@ server.registerTool(
40929
41160
  server.registerTool(
40930
41161
  "awesomate_rollback_site",
40931
41162
  {
41163
+ annotations: DESTRUCTIVE,
40932
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+).",
40933
41165
  inputSchema: {
40934
41166
  domain: external_exports.string().describe("The site domain"),
@@ -40966,6 +41198,7 @@ server.registerTool(
40966
41198
  server.registerTool(
40967
41199
  "awesomate_site_staging_promote",
40968
41200
  {
41201
+ annotations: DESTRUCTIVE,
40969
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+).",
40970
41203
  inputSchema: {
40971
41204
  domain: external_exports.string().describe("The LIVE site domain whose staging copy should go live")
@@ -40984,6 +41217,7 @@ server.registerTool(
40984
41217
  server.registerTool(
40985
41218
  "awesomate_site_staging_discard",
40986
41219
  {
41220
+ annotations: DESTRUCTIVE,
40987
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+).",
40988
41222
  inputSchema: {
40989
41223
  domain: external_exports.string().describe("The LIVE site domain whose staging copy should be discarded")
@@ -41012,6 +41246,7 @@ readTool(
41012
41246
  server.registerTool(
41013
41247
  "awesomate_app_get",
41014
41248
  {
41249
+ annotations: READ_ONLY,
41015
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).",
41016
41251
  inputSchema: { appId: external_exports.number().int().positive().describe("The app id from awesomate_app_list / _create") }
41017
41252
  },
@@ -41047,6 +41282,7 @@ server.registerTool(
41047
41282
  server.registerTool(
41048
41283
  "awesomate_app_scaffold",
41049
41284
  {
41285
+ annotations: READ_ONLY,
41050
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.",
41051
41287
  inputSchema: { appId: external_exports.number().int().positive().describe("The app id from awesomate_app_create / _list") }
41052
41288
  },
@@ -41059,9 +41295,10 @@ server.registerTool(
41059
41295
  }
41060
41296
  );
41061
41297
  server.registerTool(
41062
- "awesomate_app_deploy",
41298
+ "awesomate_app_deploy_info",
41063
41299
  {
41064
- 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.",
41065
41302
  inputSchema: { appId: external_exports.number().int().positive().describe("The app id") }
41066
41303
  },
41067
41304
  async ({ appId }) => {
@@ -41112,6 +41349,7 @@ server.registerTool(
41112
41349
  server.registerTool(
41113
41350
  "awesomate_app_health",
41114
41351
  {
41352
+ annotations: READ_ONLY,
41115
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.",
41116
41354
  inputSchema: { appId: external_exports.number().int().positive().describe("The app id") }
41117
41355
  },
@@ -41159,6 +41397,7 @@ ${scrubKeyMaterial(message)}` }],
41159
41397
  server.registerTool(
41160
41398
  "awesomate_knowledge_status",
41161
41399
  {
41400
+ annotations: READ_ONLY,
41162
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.",
41163
41402
  inputSchema: {}
41164
41403
  },
@@ -41209,18 +41448,56 @@ server.registerTool(
41209
41448
  }
41210
41449
  }
41211
41450
  );
41451
+ server.registerTool(
41452
+ "awesomate_knowledge_search",
41453
+ {
41454
+ annotations: READ_ONLY,
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.",
41456
+ inputSchema: {
41457
+ q: external_exports.string().max(2e3).optional().describe("Search words; empty lists the library filtered by the facets"),
41458
+ kind: external_exports.enum(["book", "document", "web", "image", "video", "audio", "post", "dataset"]).optional(),
41459
+ topic: external_exports.array(external_exports.string().max(200)).max(10).optional(),
41460
+ person: external_exports.array(external_exports.string().max(200)).max(10).optional(),
41461
+ place: external_exports.array(external_exports.string().max(200)).max(10).optional(),
41462
+ category: external_exports.array(external_exports.string().max(200)).max(10).optional(),
41463
+ author: external_exports.array(external_exports.string().max(200)).max(10).optional(),
41464
+ year: external_exports.array(external_exports.string().regex(/^\d{4}$/)).max(10).optional(),
41465
+ doc: external_exports.array(external_exports.string().max(200)).max(50).optional().describe("Restrict to these doc_ids (from earlier hits)"),
41466
+ limit: external_exports.number().int().min(1).max(50).optional(),
41467
+ offset: external_exports.number().int().min(0).max(2e3).optional(),
41468
+ include_media: external_exports.boolean().optional().describe("Add presigned url/poster_url to hits; they expire in minutes")
41469
+ }
41470
+ },
41471
+ async (args) => {
41472
+ try {
41473
+ return knowledgeResult(await knowledgeSearch(requireConfig(), args));
41474
+ } catch (err) {
41475
+ return knowledgeError(err);
41476
+ }
41477
+ }
41478
+ );
41212
41479
  server.registerTool(
41213
41480
  "awesomate_knowledge_ask",
41214
41481
  {
41215
41482
  description: `Ask the account's knowledge base a question and get the VERIFIED answer with numbered sources (title, locator, url) \u2014 the test surface for 'is my content in there and answering well'. Read status: ok \u2192 present answer + sources. no_results / failed_validation (not_in_verified_content:true) \u2192 the verified content has no answer: relay that honestly (use configured_fallback), never fill the gap from memory \u2014 an honest "it doesn't know" is the feature working. error (platform_error:true) \u2192 the platform itself failed (model/API/infra): NOT a content gap \u2014 never tell the user their content lacks the answer; retry once, then awesomate_support. Counts against the monthly answers quota.`,
41216
41483
  inputSchema: {
41217
41484
  question: external_exports.string().min(1).max(2e3),
41218
- session: external_exports.string().max(128).optional().describe("Stable id to keep follow-up questions in one conversation thread")
41485
+ session: external_exports.string().max(128).optional().describe("Stable id to keep follow-up questions in one conversation thread"),
41486
+ filters: external_exports.object({
41487
+ kind: external_exports.enum(["book", "document", "web", "image", "video", "audio", "post", "dataset"]).optional(),
41488
+ topic: external_exports.array(external_exports.string().max(200)).max(10).optional(),
41489
+ person: external_exports.array(external_exports.string().max(200)).max(10).optional(),
41490
+ place: external_exports.array(external_exports.string().max(200)).max(10).optional(),
41491
+ category: external_exports.array(external_exports.string().max(200)).max(10).optional(),
41492
+ author: external_exports.array(external_exports.string().max(200)).max(10).optional(),
41493
+ year: external_exports.array(external_exports.string().regex(/^\d{4}$/)).max(10).optional(),
41494
+ doc: external_exports.array(external_exports.string().max(200)).max(50).optional().describe("doc_ids from search hits")
41495
+ }).optional().describe("Ask within a slice of the library: EXACTLY the awesomate_knowledge_search input shapes, reusable verbatim; filters only ever narrow")
41219
41496
  }
41220
41497
  },
41221
- async ({ question, session }) => {
41498
+ async ({ question, session, filters }) => {
41222
41499
  try {
41223
- return knowledgeResult(await knowledgeAsk(requireConfig(), { question, session }));
41500
+ return knowledgeResult(await knowledgeAsk(requireConfig(), { question, session, filters }));
41224
41501
  } catch (err) {
41225
41502
  return knowledgeError(err);
41226
41503
  }
@@ -41239,7 +41516,7 @@ server.registerTool(
41239
41516
  tone: external_exports.string().max(500).optional()
41240
41517
  }).optional().describe("set only: persona fields to change"),
41241
41518
  no_answer_message: external_exports.string().max(1e3).optional().describe("set only: wording used when the content has no answer"),
41242
- model_tier: external_exports.enum(["sonnet", "opus"]).optional().describe("set only: opus needs the Embedded plan"),
41519
+ model_tier: external_exports.enum(["flash", "sonnet", "opus"]).optional().describe("set only: flash is the fastest (Gemini Flash, same citation checks), sonnet the thorough default, opus needs the Embedded plan"),
41243
41520
  datasets: external_exports.array(external_exports.string().max(64)).max(16).optional().describe("set only: datasets the agent may answer from")
41244
41521
  }
41245
41522
  },
@@ -41253,6 +41530,27 @@ server.registerTool(
41253
41530
  }
41254
41531
  }
41255
41532
  );
41533
+ server.registerTool(
41534
+ "awesomate_knowledge_agents",
41535
+ {
41536
+ description: "The agent builder (multi-agent; the older awesomate_knowledge_agent tool is the single workspace default). action 'list' \u2014 every agent with status (draft/published vN/suspended). 'get' {agentId} \u2014 full config incl. system message and scope. 'create' {goal} \u2014 AI drafts the whole setup (instructions, scope, tone, test questions) from the account's own content and saves it as a PRIVATE DRAFT (never live, nothing lost); or {name} for a blank draft. 'test' {agentId, message, sessionId?} \u2014 chat with the DRAFT config: free, unmetered, the right way to check behaviour before going live. 'publish' {agentId} \u2014 makes the draft LIVE immediately for every key bound to the agent: get the user's explicit approval first, and read the version back. Editing fields, policies, API keys and the request log live in the hub UI (Knowledge \u2192 Agents); keys are shown once there and never pass through this tool.",
41537
+ inputSchema: {
41538
+ action: external_exports.enum(["list", "get", "create", "publish", "test"]),
41539
+ agentId: external_exports.string().max(64).optional().describe("get/publish/test: agent_id from list"),
41540
+ name: external_exports.string().max(80).optional().describe("create: blank draft with this name"),
41541
+ goal: external_exports.string().max(2e3).optional().describe("create: describe the agent and AI drafts it from the account content"),
41542
+ message: external_exports.string().max(4e3).optional().describe("test: the question to ask the draft"),
41543
+ sessionId: external_exports.string().max(128).optional().describe("test: keep follow-ups in one thread")
41544
+ }
41545
+ },
41546
+ async (args) => {
41547
+ try {
41548
+ return knowledgeResult(await knowledgeAgents(requireConfig(), args));
41549
+ } catch (err) {
41550
+ return knowledgeError(err);
41551
+ }
41552
+ }
41553
+ );
41256
41554
  server.registerTool(
41257
41555
  "awesomate_knowledge_people",
41258
41556
  {
@@ -41279,6 +41577,274 @@ server.registerTool(
41279
41577
  }
41280
41578
  }
41281
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
+ );
41282
41848
  async function main() {
41283
41849
  try {
41284
41850
  config2 = loadConfig();
@@ -41286,10 +41852,34 @@ async function main() {
41286
41852
  configError = err instanceof Error ? err.message : String(err);
41287
41853
  console.error(`[awesomate-hosting-mcp] account not resolved: ${configError}`);
41288
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
+ }
41289
41868
  const transport = new StdioServerTransport();
41290
41869
  await server.connect(transport);
41291
41870
  console.error(`[awesomate-hosting-mcp] connected (stdio) \u2014 ${accountStamp()}`);
41292
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
+ }
41293
41883
  main().catch((err) => {
41294
41884
  console.error(
41295
41885
  "[awesomate-hosting-mcp] fatal:",