@awesomate/hosting-mcp 0.91.1 → 0.93.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
@@ -40082,11 +40082,23 @@ ${JSON.stringify(details, null, 2)}`;
40082
40082
  }
40083
40083
  function keyHolder(ctx) {
40084
40084
  const c = ctx && typeof ctx === "object" ? ctx : {};
40085
- const email2 = typeof c.email === "string" ? c.email : null;
40085
+ const k = c.key && typeof c.key === "object" ? c.key : {};
40086
+ const email2 = typeof k.email === "string" ? k.email : typeof c.email === "string" ? c.email : null;
40087
+ if (k.holder === "person") {
40088
+ const name = typeof k.name === "string" && k.name ? k.name : email2 ?? "a team member";
40089
+ const level = k.level === "full" ? "full" : "view";
40090
+ return {
40091
+ holder: "person",
40092
+ email: email2,
40093
+ name,
40094
+ level,
40095
+ note: `This key belongs to ${name}${email2 && name !== email2 ? ` (${email2})` : ""}, a team member${level === "view" ? " with view-only access" : ""}: Claude does only what they can do in the hub, never more, and never with a shell on the hosting account. Do not offer anything their access does not include; if they ask, say the account owner can do it.`
40096
+ };
40097
+ }
40086
40098
  return {
40087
40099
  holder: "account_owner",
40088
40100
  email: email2,
40089
- note: `This key belongs to the account owner${email2 ? ` (${email2})` : ""}: everything Claude does with it is done as the owner. Team members cannot connect Claude Code with their own key yet.`
40101
+ note: `This key belongs to the account owner${email2 ? ` (${email2})` : ""}: everything Claude does with it is done as the owner. Team members connect their own Claude Code, at their own access, once the owner turns it on for them.`
40090
40102
  };
40091
40103
  }
40092
40104
 
@@ -40735,6 +40747,21 @@ function installedSkillVersion(name) {
40735
40747
  return null;
40736
40748
  }
40737
40749
  }
40750
+ function compareVersions(a, b) {
40751
+ const pa = a.split(/[.-]/).map((x) => Number.parseInt(x, 10) || 0);
40752
+ const pb = b.split(/[.-]/).map((x) => Number.parseInt(x, 10) || 0);
40753
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
40754
+ const d = (pa[i] ?? 0) - (pb[i] ?? 0);
40755
+ if (d !== 0) return d < 0 ? -1 : 1;
40756
+ }
40757
+ return 0;
40758
+ }
40759
+ function oldestInstalledVersion(perSkill) {
40760
+ return perSkill.reduce(
40761
+ (min, s) => s.installed !== null && (min === null || compareVersions(s.installed, min) < 0) ? s.installed : min,
40762
+ null
40763
+ );
40764
+ }
40738
40765
  function skillVersions(sourceRoot = DEFAULT_SOURCE_ROOT) {
40739
40766
  return bundledSkillNames(sourceRoot).map((name) => ({ name, installed: installedSkillVersion(name) }));
40740
40767
  }
@@ -40791,8 +40818,9 @@ function installSkills(version2, sourceRoot = DEFAULT_SOURCE_ROOT) {
40791
40818
  }
40792
40819
 
40793
40820
  // src/knowledge.ts
40794
- function scrubKeyMaterial(text) {
40795
- return text.replace(/amk_[a-z]_[A-Za-z0-9]{6,}/g, "[redacted:platform-key]").replace(/amt_[a-z]+_[A-Za-z0-9]{6,}/g, "[redacted:hub-token]");
40821
+ import { createHash } from "node:crypto";
40822
+ function scrubKeyMaterial(text, opts = {}) {
40823
+ return text.replace(/amk_([a-z])_[A-Za-z0-9]{6,}/g, (m, cls) => opts.keepWebsiteKeys && cls === "p" ? m : "[redacted:platform-key]").replace(/amt_[a-z]+_[A-Za-z0-9]{6,}/g, "[redacted:hub-token]");
40796
40824
  }
40797
40825
  var KNOWLEDGE_GATE_COPY = {
40798
40826
  displayName: "Knowledge Base",
@@ -40878,13 +40906,33 @@ function storageFullPayload(body) {
40878
40906
  note: "The Knowledge Base storage is full and the source was NOT added. Relay the message exactly as it is; offer no purchase, because none is open to this account or this person right now. Removing sources they no longer need frees space."
40879
40907
  };
40880
40908
  }
40909
+ async function deleteOrNotFound(config3, path) {
40910
+ try {
40911
+ return await hubDelete(config3, path) ?? { deleted: true };
40912
+ } catch (err) {
40913
+ if (err instanceof HubApiError && err.status === 404 && bodyOf(err)?.error === "not_found") {
40914
+ return {
40915
+ deleted: false,
40916
+ not_found: true,
40917
+ note: "Nothing with that id was there. It may already be deleted (an earlier attempt can go through even when its answer never arrives), or the id is wrong. List again to check before telling the user either way."
40918
+ };
40919
+ }
40920
+ throw err;
40921
+ }
40922
+ }
40881
40923
  function bodyOf(err) {
40882
40924
  return err.body && typeof err.body === "object" ? err.body : null;
40883
40925
  }
40884
40926
  function mapKnowledgeError(err, apiBase) {
40885
40927
  if (!(err instanceof HubApiError)) throw err;
40886
40928
  const body = bodyOf(err);
40887
- if (err.status === 404) return notAvailablePayload(apiBase);
40929
+ if (err.status === 404) {
40930
+ if (typeof body?.error !== "string" || !/^[a-z0-9_]+$/.test(body.error)) return notAvailablePayload(apiBase);
40931
+ if (body.error === "not_found") {
40932
+ return { error: "not_found", note: typeof body.message === "string" ? body.message : "There is no such item on this account. List again for the current ids." };
40933
+ }
40934
+ throw err;
40935
+ }
40888
40936
  if ((err.status === 403 || err.status === 402) && body?.upgrade_required) return upsellPayload(apiBase, body.plan, body);
40889
40937
  if (err.status === 403 && body?.consent_required) {
40890
40938
  return consentPayload(typeof body.error === "string" ? body.error : 'Knowledge is not using your content: "Use your content for Knowledge" is off', apiBase);
@@ -41135,10 +41183,7 @@ async function knowledgeSources(config3, args) {
41135
41183
  }
41136
41184
  if (args.action === "remove") {
41137
41185
  if (!args.sourceId) throw new Error("sourceId is required for remove");
41138
- return await hubDelete(
41139
- config3,
41140
- `/api/knowledge/sources/${encodeURIComponent(args.sourceId)}`
41141
- );
41186
+ return deleteOrNotFound(config3, `/api/knowledge/sources/${encodeURIComponent(args.sourceId)}`);
41142
41187
  }
41143
41188
  if (!args.url && !args.sitemap) {
41144
41189
  return {
@@ -41182,15 +41227,18 @@ function renderAnswer(meta, streamedAnswer, streamError = null) {
41182
41227
  const session = typeof meta.session === "string" && meta.session ? { session: meta.session } : {};
41183
41228
  const fallback = typeof meta.answer_plain === "string" && meta.answer_plain || typeof meta.no_answer_message === "string" && meta.no_answer_message || null;
41184
41229
  if (status === "error") {
41230
+ const errorCode = typeof meta.error_code === "string" ? meta.error_code : null;
41231
+ const ownKey = errorCode === "byok_key_invalid" || errorCode === "byok_no_credit";
41185
41232
  return {
41186
41233
  status: "error",
41187
41234
  answer: null,
41188
41235
  platform_error: true,
41189
41236
  not_in_verified_content: false,
41237
+ ...errorCode ? { error_code: errorCode } : {},
41190
41238
  detail: streamError ?? (streamedAnswer || null),
41191
41239
  configured_fallback: fallback,
41192
41240
  ...session,
41193
- 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."
41241
+ note: ownKey ? `The agent runs on this account's own OpenRouter key, and OpenRouter ${errorCode === "byok_no_credit" ? "says it is out of credit" : "refused it"}. This is NOT a content gap. Tell the user to top up or replace the key at Settings, Integrations (hub.awesomate.ai/settings?tab=integrations), or to clear the agent's model so it runs on Awesomate's.` : "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."
41194
41242
  };
41195
41243
  }
41196
41244
  if (status !== "ok") {
@@ -41624,6 +41672,17 @@ function mapAgentsError(err, apiBase) {
41624
41672
  }
41625
41673
  if (err instanceof HubApiError && err.status === 403) {
41626
41674
  const body = bodyOf(err);
41675
+ if (body && body.error === "upgrade_required" && body.capability === "public_widget") {
41676
+ const requiredPlan = typeof body.requiredPlan === "string" ? body.requiredPlan : "Support Plus";
41677
+ return {
41678
+ upgrade_required: true,
41679
+ capability: "public_widget",
41680
+ requiredPlan,
41681
+ ...typeof body.message === "string" ? { message: body.message } : {},
41682
+ billingUrl: `${apiBase}/billing?upgrade=${encodeURIComponent(requiredPlan)}`,
41683
+ note: "Nothing was made. Relay the message as it is and let the user decide; never retry."
41684
+ };
41685
+ }
41627
41686
  if (body && body.error === "upgrade_required" && body.capability === "knowledge_questions") {
41628
41687
  const requiredPlan = typeof body.requiredPlan === "string" ? body.requiredPlan : "Support Plus";
41629
41688
  return {
@@ -41638,6 +41697,107 @@ function mapAgentsError(err, apiBase) {
41638
41697
  }
41639
41698
  return mapKnowledgeError(err, apiBase);
41640
41699
  }
41700
+ var WEBSITE_KEY_MAX_ORIGINS = 16;
41701
+ function websiteOrigins(input) {
41702
+ const raw = (input ?? []).map((s) => s.trim()).filter(Boolean);
41703
+ if (!raw.length) return { ok: false, error: "website_key needs origins: the website addresses the chat will run on, like https://example.com.au" };
41704
+ const out = [];
41705
+ for (const r of raw) {
41706
+ if (r.includes("*")) return { ok: false, error: `"${r}": a wildcard is not allowed, because the key would then answer on any website.` };
41707
+ let u;
41708
+ try {
41709
+ u = new URL(r.includes("://") ? r : `https://${r}`);
41710
+ } catch {
41711
+ return { ok: false, error: `"${r}" is not a website address.` };
41712
+ }
41713
+ if (u.protocol !== "https:") return { ok: false, error: `"${r}" must be an https:// address.` };
41714
+ if (u.username || u.password) return { ok: false, error: `"${r}" must not include a username or password.` };
41715
+ if (u.pathname !== "/" || u.search || u.hash) return { ok: false, error: `"${r}" should be just the site address, without a path, for example ${u.origin}` };
41716
+ if (!out.includes(u.origin)) out.push(u.origin);
41717
+ }
41718
+ if (out.length > WEBSITE_KEY_MAX_ORIGINS) return { ok: false, error: `At most ${WEBSITE_KEY_MAX_ORIGINS} websites per key.` };
41719
+ return { ok: true, origins: out };
41720
+ }
41721
+ var htmlAttr = (v) => v.replace(/&/g, "&amp;").replace(/"/g, "&quot;");
41722
+ function websiteSnippet(widgetUrl, key, agentId, title) {
41723
+ const attrs = [["data-key", key], ["data-agent", agentId]];
41724
+ if (title?.trim()) attrs.push(["data-title", title.trim()]);
41725
+ const lines = attrs.map(([k, v], i) => ` ${k}="${htmlAttr(v)}"${i === attrs.length - 1 ? "></script>" : ""}`);
41726
+ return [`<script src="${htmlAttr(widgetUrl)}"`, ...lines].join("\n");
41727
+ }
41728
+ function websiteKeyRequestId(agentId, origins, label, cap, liveKeyIds) {
41729
+ const h = createHash("sha256").update(`${agentId}|${[...origins].sort().join(",")}|${label}|${cap ?? "default"}|${[...liveKeyIds].sort().join(",")}`).digest("hex");
41730
+ return `${h.slice(0, 8)}-${h.slice(8, 12)}-4${h.slice(13, 16)}-8${h.slice(17, 20)}-${h.slice(20, 32)}`;
41731
+ }
41732
+ function liveWebsiteKeys(keys, agentId) {
41733
+ const now = Date.now();
41734
+ return keys.filter(
41735
+ (k) => k.class === "p" && (!agentId || k.agent_id === agentId) && !k.revoked_at && !(typeof k.expires_at === "string" && new Date(k.expires_at).getTime() <= now)
41736
+ );
41737
+ }
41738
+ function websiteAgentRefusal(agent) {
41739
+ const name = typeof agent.name === "string" && agent.name ? `"${agent.name}"` : "This agent";
41740
+ if (agent.status !== "published") return `${name} is not published yet, so a website chat would have nothing to answer with. Test it, then publish it (with the user's yes) first.`;
41741
+ if (agent.audience !== "public") return `${name} serves a ${typeof agent.audience === "string" ? agent.audience : "private"} audience, and a website chat answers the public. Change its audience to public and publish again (with the user's yes) first.`;
41742
+ if (agent.suspended_at) return `${name} is paused, so a website chat would not answer. Resume it (with the user's yes) first.`;
41743
+ return null;
41744
+ }
41745
+ var WEBSITE_KEY_NOTE = "A website key is meant to sit in page source: it only answers from the websites listed, only as this agent, and only from sources marked public. It carries its own monthly answer cap (by default half the account's answers), so one busy site cannot use up every answer.";
41746
+ async function websiteKeyAction(config3, args) {
41747
+ const base = "/api/knowledge/agents";
41748
+ if (args.action === "website_keys") {
41749
+ const r = await hubGet(config3, `${base}/keys`);
41750
+ return { keys: liveWebsiteKeys(r.keys ?? [], args.agentId), note: `${WEBSITE_KEY_NOTE} The key itself is shown only once, when it is made; it cannot be read back.` };
41751
+ }
41752
+ if (args.action === "revoke_website_key") {
41753
+ if (!args.keyId) return { error: "invalid_request", note: "revoke_website_key needs keyId (from website_keys)." };
41754
+ const r = await hubGet(config3, `${base}/keys`);
41755
+ if (!liveWebsiteKeys(r.keys ?? []).some((k) => k.key_id === args.keyId)) {
41756
+ return { error: "not_found", note: "There is no working website key with that id. Read website_keys for the ids; other kinds of key are managed in the hub only." };
41757
+ }
41758
+ if (args.confirm !== true) {
41759
+ return { error: "confirmation_required", note: "Revoking this key stops the chat on every website using it at once. Tell the owner, and call again with confirm:true only after they say yes." };
41760
+ }
41761
+ return deleteOrNotFound(config3, `${base}/keys/${encodeURIComponent(args.keyId)}`);
41762
+ }
41763
+ if (!args.agentId) return { error: "invalid_request", note: "website_key needs agentId (from list) and origins." };
41764
+ const origins = websiteOrigins(args.origins);
41765
+ if (!origins.ok) return { error: "invalid_request", note: `${origins.error} Nothing was made.` };
41766
+ const agent = agentOf(await hubGet(config3, `${base}/${encodeURIComponent(args.agentId)}`));
41767
+ const notReady = websiteAgentRefusal(agent);
41768
+ if (notReady) return { error: "agent_not_ready", note: `${notReady} Nothing was made.` };
41769
+ if (args.confirm !== true) {
41770
+ return {
41771
+ error: "confirmation_required",
41772
+ note: `Making a website key puts this agent on ${origins.origins.join(", ")}, where anyone visiting can ask it questions, and those answers count against the account's monthly answers. Tell the owner that, and call again with confirm:true only after they say yes.`
41773
+ };
41774
+ }
41775
+ const status = await hubGet(config3, "/api/knowledge/status");
41776
+ const widgetUrl = typeof status.widget_url === "string" && status.widget_url ? status.widget_url : null;
41777
+ if (!widgetUrl) return { error: "widget_unavailable", note: "This hub has no website chat loader configured, so a key would have nowhere to run. Nothing was made; tell the user to contact Awesomate support." };
41778
+ const listed = await hubGet(config3, `${base}/keys`);
41779
+ const liveIds = liveWebsiteKeys(listed.keys ?? [], args.agentId).map((k) => String(k.key_id));
41780
+ const label = (args.label?.trim() || `website: ${origins.origins.map((o) => o.replace(/^https:\/\//, "")).join(", ")}`).slice(0, 120);
41781
+ const minted = await hubPost(config3, `${base}/keys`, {
41782
+ class: "p",
41783
+ agent_id: args.agentId,
41784
+ allowed_origins: origins.origins,
41785
+ label,
41786
+ request_id: websiteKeyRequestId(args.agentId, origins.origins, label, args.monthlyAnswerCap, liveIds),
41787
+ ...args.monthlyAnswerCap !== void 0 ? { monthly_answer_cap: args.monthlyAnswerCap } : {}
41788
+ });
41789
+ const key = typeof minted.key === "string" ? minted.key : "";
41790
+ const title = args.title?.trim() || (typeof agent.name === "string" ? agent.name : "");
41791
+ const cap = typeof minted.monthly_answer_cap === "number" ? minted.monthly_answer_cap : null;
41792
+ return {
41793
+ key_id: minted.key_id ?? null,
41794
+ key,
41795
+ allowed_origins: origins.origins,
41796
+ monthly_answer_cap: cap,
41797
+ snippet: key ? websiteSnippet(widgetUrl, key, args.agentId, title) : null,
41798
+ note: `${WEBSITE_KEY_NOTE} The key is shown ONCE: hand the snippet straight to the user to paste before </body> on those sites (or add it to the site for them), and do not repeat the key anywhere else.` + (cap !== null ? ` Tell the user this key has its own limit of ${cap} answers a month. A key's limit cannot be changed once it is made: to change it, make a new key with monthlyAnswerCap, swap the snippet on the site, then revoke this one.` : "")
41799
+ };
41800
+ }
41641
41801
  var VISITOR_TEST_MAX_CHARS = 2e3;
41642
41802
  var POLICY_TAGS_MAX = 50;
41643
41803
  function policyScope(args) {
@@ -41656,6 +41816,9 @@ var POLICY_NOTE = "An access policy narrows what a key or a signed-in person may
41656
41816
  async function knowledgeAgents(config3, args) {
41657
41817
  try {
41658
41818
  const base = "/api/knowledge/agents";
41819
+ if (args.action === "website_key" || args.action === "website_keys" || args.action === "revoke_website_key") {
41820
+ return await websiteKeyAction(config3, args);
41821
+ }
41659
41822
  if (args.action === "logs") {
41660
41823
  const q2 = new URLSearchParams();
41661
41824
  if (args.agentId) q2.set("agent_id", args.agentId);
@@ -41733,19 +41896,20 @@ async function knowledgeAgents(config3, args) {
41733
41896
  const current = scope && typeof scope === "object" && !Array.isArray(scope) ? scope : {};
41734
41897
  return touchesScope(scopeArgs) ? { scope: agentPatchBody(scopeArgs, current).scope } : scope !== void 0 ? { scope } : {};
41735
41898
  };
41899
+ const callerFields = agentPatchBody({ ...args, collectionIds: void 0, kinds: void 0, sourceIds: void 0 });
41736
41900
  let created;
41737
41901
  let sentScope;
41738
41902
  let extra = {};
41739
41903
  if (args.goal) {
41740
41904
  const draft = await hubPost(config3, "/api/knowledge/agents/draft", { goal: args.goal });
41741
41905
  if (!draft.proposal) return { error: "draft_failed", detail: draft };
41742
- const body = { ...draft.proposal, ...withScope(draft.proposal.scope), ...args.audience ? { audience: args.audience } : {} };
41906
+ const body = { ...draft.proposal, ...callerFields, ...withScope(draft.proposal.scope) };
41743
41907
  sentScope = body.scope;
41744
41908
  created = await hubPost(config3, "/api/knowledge/agents", body);
41745
41909
  extra = { test_questions: draft.test_questions ?? [], status_note: `Created as a private DRAFT serving audience '${args.audience ?? "private"}': test it, then publish when the user approves.` };
41746
41910
  } else {
41747
41911
  if (!args.name) return { error: "name (or goal) is required for create" };
41748
- const body = { name: args.name, ...args.audience ? { audience: args.audience } : {}, ...withScope(void 0) };
41912
+ const body = { ...callerFields, name: args.name, ...withScope(void 0) };
41749
41913
  sentScope = body.scope;
41750
41914
  created = await hubPost(config3, "/api/knowledge/agents", body);
41751
41915
  }
@@ -41844,7 +42008,7 @@ async function knowledgeCollections(config3, args) {
41844
42008
  return await hubPatch(config3, `/api/knowledge/collections/${encodeURIComponent(args.collectionId)}`, body);
41845
42009
  }
41846
42010
  if (args.action === "delete") {
41847
- return await hubDelete(config3, `/api/knowledge/collections/${encodeURIComponent(args.collectionId)}`);
42011
+ return deleteOrNotFound(config3, `/api/knowledge/collections/${encodeURIComponent(args.collectionId)}`);
41848
42012
  }
41849
42013
  if (!args.sourceIds?.length) return { error: "invalid_request", note: `${args.action} needs sourceIds` };
41850
42014
  return await hubPost(config3, `/api/knowledge/collections/${encodeURIComponent(args.collectionId)}/sources`, {
@@ -42123,10 +42287,7 @@ function changelogSince(installed) {
42123
42287
  function skillUpdateInfo() {
42124
42288
  const perSkill = skillVersions();
42125
42289
  const staleSkills = perSkill.filter((s) => s.installed !== SERVER_VERSION).map((s) => s.name);
42126
- const oldest = perSkill.reduce(
42127
- (min, s) => s.installed !== null && (min === null || s.installed < min) ? s.installed : min,
42128
- null
42129
- );
42290
+ const oldest = oldestInstalledVersion(perSkill);
42130
42291
  const updateAvailable = perSkill.length === 0 || staleSkills.length > 0;
42131
42292
  return {
42132
42293
  serverVersion: SERVER_VERSION,
@@ -42388,7 +42549,7 @@ server.registerTool(
42388
42549
  "awesomate_get_context",
42389
42550
  {
42390
42551
  annotations: annotate("awesomate_get_context", READ_ONLY),
42391
- description: "THE session entry point: call it FIRST, before any domain context. Returns the connected account: plan, capabilities, limits, scopes, token expiry, cPanel routing, PLUS `key` (whose key this is: always the account owner's today, so everything done with it is done as the owner; say so if the user is not the owner; team members cannot connect Claude Code with their own key yet; a tool the key's scopes do not cover answers at once with the hub's own refusal), PLUS `attention` ({unreadNotifications, erroringWorkflows7d, patExpiresInDays}: null means unknown, never zero; when something is non-zero, mention it to the user in one line before starting the asked task), `features` ({key: {available, reason, message}}: what this account has; never offer one whose available is false, say its message instead), `latestMcpVersion` (if serverVersion still lags it after a restart, the npx cache is stale; remedy: rm -rf ~/.npm/_npx, then restart), and `skill` ({updateAvailable, staleSkills, whatsNew}: 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/claude refreshes skills AND renews the token in one go.",
42552
+ description: "THE session entry point: call it FIRST, before any domain context. Returns the connected account: plan, capabilities, limits, scopes, token expiry, cPanel routing, PLUS `key` (whose key this is: the account owner's, so everything is done as the owner, or a team member's own (`holder: 'person'`), which does only what that person can do in the hub and never has a shell: say whose it is, and never offer work outside a person's access; a tool the key's scopes do not cover answers at once with the hub's own refusal), PLUS `attention` ({unreadNotifications, erroringWorkflows7d, patExpiresInDays}: null means unknown, never zero; when something is non-zero, mention it to the user in one line before starting the asked task), `features` ({key: {available, reason, message}}: what this account has; never offer one whose available is false, say its message instead), `latestMcpVersion` (if serverVersion still lags it after a restart, the npx cache is stale; remedy: rm -rf ~/.npm/_npx, then restart), and `skill` ({updateAvailable, staleSkills, whatsNew}: 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/claude refreshes skills AND renews the token in one go.",
42392
42553
  inputSchema: {}
42393
42554
  },
42394
42555
  async () => {
@@ -43079,7 +43240,7 @@ server.registerTool(
43079
43240
  "awesomate_site_staging_promote",
43080
43241
  {
43081
43242
  annotations: annotate("awesomate_site_staging_promote", DESTRUCTIVE),
43082
- 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+).",
43243
+ description: "Publish the staging copy to the LIVE site (overwrites live files + database with staging). The live site is auto-snapshotted first: the result includes preSnapshotId, which awesomate_rollback_site can restore if anything looks wrong. The live site keeps its own search engine visibility (staging is always hidden from search engines; that setting is not copied to live); if searchEngineVisibility.warning is present, live may now be hidden from search engines, so relay it to the user. **Confirm with the user before promoting: it replaces the live site.** Requires shell access (Support Plus+).",
43083
43244
  inputSchema: {
43084
43245
  domain: external_exports.string().describe("The LIVE site domain whose staging copy should go live")
43085
43246
  }
@@ -43414,13 +43575,13 @@ server.registerTool(
43414
43575
  }
43415
43576
  }
43416
43577
  );
43417
- function knowledgeResult(data) {
43578
+ function knowledgeResult(data, opts = {}) {
43418
43579
  return {
43419
43580
  content: [
43420
43581
  {
43421
43582
  type: "text",
43422
43583
  text: `${accountStamp()}
43423
- ${scrubKeyMaterial(JSON.stringify(data, null, 2))}`
43584
+ ${scrubKeyMaterial(JSON.stringify(data, null, 2), opts)}`
43424
43585
  }
43425
43586
  ]
43426
43587
  };
@@ -43452,7 +43613,7 @@ server.registerTool(
43452
43613
  "awesomate_knowledge_provision",
43453
43614
  {
43454
43615
  annotations: annotate("awesomate_knowledge_provision", MUTATING),
43455
- description: `Enable the Knowledge Base for this account (creates their isolated tenant on the Awesomate knowledge platform and wires the n8n credential). Idempotent \u2014 safe to re-call; a pending:true response means provisioning continues in the background: poll awesomate_knowledge_status. consent_required \u2192 send the user to Settings \u2192 Features (hub.awesomate.ai/settings?tab=features): under the Knowledge row, the second line "Use your content for Knowledge". Switching on Knowledge itself is NOT the consent. They must switch it themselves; re-check, then retry. upgrade_required \u2192 relay the upsell, don't retry. Get the user's explicit go-ahead before enabling.`,
43616
+ description: `Enable the Knowledge Base for this account (creates their isolated tenant on the Awesomate knowledge platform and wires the n8n credential). Idempotent: safe to re-call, and the account owner re-calling on an active account puts back a missing "Awesomate Knowledge Base" n8n credential. The response's credential is {status, verified?, reason?}: created / exists are fine; adopted with verified:false means an existing credential was kept and its key may be stale (tell the user to rotate the Knowledge key); skipped or failed carries the reason (for example no n8n API key stored, so the user connects n8n first); a pending:true response means provisioning continues in the background: poll awesomate_knowledge_status. consent_required \u2192 send the user to Settings \u2192 Features (hub.awesomate.ai/settings?tab=features): under the Knowledge row, the second line "Use your content for Knowledge". Switching on Knowledge itself is NOT the consent. They must switch it themselves; re-check, then retry. upgrade_required \u2192 relay the upsell, don't retry. Get the user's explicit go-ahead before enabling.`,
43456
43617
  inputSchema: {}
43457
43618
  },
43458
43619
  async () => {
@@ -43552,7 +43713,7 @@ server.registerTool(
43552
43713
  author: external_exports.array(external_exports.string().max(200)).max(10).optional(),
43553
43714
  year: external_exports.array(external_exports.string().regex(/^\d{4}$/)).max(10).optional(),
43554
43715
  doc: external_exports.array(external_exports.string().max(200)).max(50).optional().describe("doc_ids from search hits")
43555
- }).optional().describe("Ask within a slice of the library: EXACTLY the awesomate_knowledge_search input shapes, reusable verbatim; filters only ever narrow")
43716
+ }).optional().describe("Ask within a slice of the library: EXACTLY the awesomate_knowledge_search input shapes, reusable verbatim; filters only ever narrow. A filtered question is answered by the workspace's 'knowledge' agent (the default agent takes no filters), so its tone can differ from an unfiltered one")
43556
43717
  }
43557
43718
  },
43558
43719
  async ({ question, session, filters }) => {
@@ -43595,7 +43756,7 @@ server.registerTool(
43595
43756
  "awesomate_knowledge_agents",
43596
43757
  {
43597
43758
  annotations: annotate("awesomate_knowledge_agents", MUTATING),
43598
- description: "The agent builder (multi-agent; the older awesomate_knowledge_agent tool is the single workspace default). action 'list': every agent with status (draft/published vN/suspended). 'get' {agentId}: full config incl. system message and scope. 'create' {goal}: 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. The drafted scope is often too wide: it can come back limited to no collection (so it answers from every business's and every audience's content) or limited to kinds that leave out PDFs (stored as 'book'), FAQs ('faq') or images. Pass collectionIds (and kinds if needed) WITH create to scope it in one step; when the account has collections and the new agent reads none of them, or its kinds skip content the library holds, the result carries a warning listing the collections: fix it with update before testing or publishing. 'update' {agentId, ...fields}: edit the DRAFT config: name, description, systemMessage, noAnswerMessage, grounding, audience, collectionIds (scope to collections from awesomate_knowledge_collections), kinds, sourceIds, maxSources, temperature. Nothing changes for callers until 'publish'. 'test' {agentId, message, sessionId?}: chat with the DRAFT config: free, unmetered, the right way to check wording and scope before going live. WARNING: the draft test ignores the agent's audience, so it is NOT what the public sees for anything touching internal or private sources (seen live: a public-audience draft quoted an internal staff handbook's door code and Wi-Fi password, which the published agent correctly refused a visitor). 'visitor_test' {agentId, message (max 2000 chars), sessionId?}: ask the PUBLISHED agent through the real public visitor path, the only way to check what a website visitor actually gets; run it after publishing whenever the agent is public-facing or its scope includes internal/private sources. It needs the plan the public widget ships with (upgrade_required below it: relay, never retry), and a question it cannot answer lands in the owner's Questions inbox like a real visitor's, so say so before using it. 'publish' {agentId}: makes the draft LIVE immediately for every key bound to the agent: get the user's explicit approval first, and read the version back. 'suspend' / 'resume' {agentId}: stop the agent answering on every key bound to it, or start it again (its last published version); both change what live callers get at once, so get the owner's explicit approval first. 'delete' {agentId, confirm:true}: removes the agent for good and every key bound to it stops answering; tell the owner that and pass confirm:true only after they say yes. 'logs' {agentId?, since?, until?, endpoint?, status?, limit?, cursor?}: the request log (each call to the agents: when, which endpoint, its status), newest first. 'answer_rates' {days? 1-92, default 30}: how each agent is answering, in bands, counts only. 'where_used': each agent with its audience, scope and status, to see which agent a website or automation is using. 'ai_key_status': whether the account has its own AI key saved (names and dates, never a key); saving or removing one is the owner's, in the hub at Settings, Integrations. 'models': the models the account's own AI key can use. Access policies (customer groups): 'policies' lists them; 'policy_get' {policyId}; 'policy_create' {name, description?, collectionIds?|kinds?|sourceIds?|tags?}: a policy named segment:all or segment:<group> is what an agent's customer groups use, so a person in that group answers only from what it allows; 'policy_update' {policyId, name?|description?|scope fields} (a scope REPLACES the stored one); 'policy_delete', 'policy_suspend', 'policy_resume' {policyId}. Every policy change applies to live callers at once: show the owner what changes and get a yes first. API keys are shown once in the hub (Knowledge, Agents) and never pass through this tool.",
43759
+ description: "The agent builder (multi-agent; the older awesomate_knowledge_agent tool is the single workspace default). action 'list': every agent with status (draft/published vN/suspended). 'get' {agentId}: full config incl. system message and scope. 'create' {goal}: 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. The drafted scope is often too wide: it can come back limited to no collection (so it answers from every business's and every audience's content) or limited to kinds that leave out PDFs (stored as 'book'), FAQs ('faq') or images. Pass collectionIds (and kinds if needed) WITH create to scope it in one step; when the account has collections and the new agent reads none of them, or its kinds skip content the library holds, the result carries a warning listing the collections: fix it with update before testing or publishing. 'update' {agentId, ...fields}: edit the DRAFT config: name, description, systemMessage, noAnswerMessage, grounding, audience, collectionIds (scope to collections from awesomate_knowledge_collections), kinds, sourceIds, maxSources, temperature. Nothing changes for callers until 'publish'. 'test' {agentId, message, sessionId?}: chat with the DRAFT config: free, unmetered, the right way to check wording and scope before going live. WARNING: the draft test ignores the agent's audience, so it is NOT what the public sees for anything touching internal or private sources (seen live: a public-audience draft quoted an internal staff handbook's door code and Wi-Fi password, which the published agent correctly refused a visitor). 'visitor_test' {agentId, message (max 2000 chars), sessionId?}: ask the PUBLISHED agent through the real public visitor path, the only way to check what a website visitor actually gets; run it after publishing whenever the agent is public-facing or its scope includes internal/private sources. It needs the plan the public widget ships with (upgrade_required below it: relay, never retry), and a question it cannot answer lands in the owner's Questions inbox like a real visitor's, so say so before using it. 'publish' {agentId}: makes the draft LIVE immediately for every key bound to the agent: get the user's explicit approval first, and read the version back. 'suspend' / 'resume' {agentId}: stop the agent answering on every key bound to it, or start it again (its last published version); both change what live callers get at once, so get the owner's explicit approval first. 'delete' {agentId, confirm:true}: removes the agent for good and every key bound to it stops answering; tell the owner that and pass confirm:true only after they say yes. 'logs' {agentId?, since?, until?, endpoint?, status?, limit?, cursor?}: the request log (each call to the agents: when, which endpoint, its status), newest first. 'answer_rates' {days? 1-92, default 30}: how each agent is answering, in bands, counts only. 'where_used': each agent with its audience, scope and status, to see which agent a website or automation is using. 'ai_key_status': whether the account has its own AI key saved (names and dates, never a key); saving or removing one is the owner's, in the hub at Settings, Integrations. 'models': the models the account's own AI key can use. Access policies (customer groups): 'policies' lists them; 'policy_get' {policyId}; 'policy_create' {name, description?, collectionIds?|kinds?|sourceIds?|tags?}: a policy named segment:all or segment:<group> is what an agent's customer groups use, so a person in that group answers only from what it allows; 'policy_update' {policyId, name?|description?|scope fields} (a scope REPLACES the stored one); 'policy_delete', 'policy_suspend', 'policy_resume' {policyId}. Every policy change applies to live callers at once: show the owner what changes and get a yes first. Website chat (owner only; Support Plus and above, upgrade_required below it: relay it, never retry): 'website_keys' {agentId?}: the working website keys (no key text: a key is shown once, when made). 'website_key' {agentId, origins, title?, label?, monthlyAnswerCap?, confirm:true}: makes a website key for a PUBLISHED, public-audience agent (refused otherwise, with what to do first) and returns it ONCE with the ready <script> snippet to paste before </body>; anyone on those sites can then ask the agent, and answers count against the account's monthly answers. Each key has its own monthly answer limit (by default half the account's at the time it is made), returned as monthly_answer_cap: tell the user the number. A key's limit cannot be changed later; a different limit is a new key, then revoke the old one. Ask the owner which sites, say what it means, and pass confirm:true only after they say yes. 'revoke_website_key' {keyId, confirm:true}: stops the chat on every site using that key at once; ask first. Other keys (for automations and servers) are made in the hub (Knowledge, Agents) and never pass through this tool.",
43599
43760
  inputSchema: {
43600
43761
  action: external_exports.enum([
43601
43762
  "list",
@@ -43619,9 +43780,17 @@ server.registerTool(
43619
43780
  "policy_update",
43620
43781
  "policy_delete",
43621
43782
  "policy_suspend",
43622
- "policy_resume"
43783
+ "policy_resume",
43784
+ "website_key",
43785
+ "website_keys",
43786
+ "revoke_website_key"
43623
43787
  ]),
43624
- confirm: external_exports.boolean().optional().describe("delete: true only after the owner said yes to deleting this agent"),
43788
+ confirm: external_exports.boolean().optional().describe("delete, website_key, revoke_website_key: true only after the owner said yes"),
43789
+ origins: external_exports.array(external_exports.string().max(200)).max(16).optional().describe("website_key: the websites the chat runs on, https://host only (no path, no wildcard), e.g. https://example.com.au and https://www.example.com.au"),
43790
+ title: external_exports.string().max(60).optional().describe("website_key: the chat window's title in the snippet (data-title); defaults to the agent's name"),
43791
+ label: external_exports.string().max(120).optional().describe("website_key: a name for the key in key lists"),
43792
+ monthlyAnswerCap: external_exports.number().int().min(1).optional().describe("website_key: this key's own monthly answer cap; leave it out for half the account's monthly answers"),
43793
+ keyId: external_exports.string().max(128).optional().describe("revoke_website_key: key_id from website_keys"),
43625
43794
  since: external_exports.string().max(40).optional().describe("logs: ISO time to read from"),
43626
43795
  until: external_exports.string().max(40).optional().describe("logs: ISO time to read to"),
43627
43796
  endpoint: external_exports.string().max(200).optional().describe("logs: only calls to this endpoint, e.g. /v1/answer"),
@@ -43635,22 +43804,22 @@ server.registerTool(
43635
43804
  name: external_exports.string().max(120).optional().describe("create: blank draft with this name (max 80); update: rename; policy_create/update: the policy name, e.g. segment:members"),
43636
43805
  goal: external_exports.string().max(2e3).optional().describe("create: describe the agent and AI drafts it from the account content"),
43637
43806
  audience: external_exports.enum(["private", "internal", "public"]).optional().describe("create/update: who the agent serves: 'public' for anything customers or a website will talk to (it then answers from sources marked public ONLY), 'internal' for the team, 'private' (default) for the owner's own tools. Widening later is a publish, so choose now."),
43638
- description: external_exports.string().max(300).optional().describe("update: one-line description shown in the hub; policy_create/update: what the policy is for"),
43639
- systemMessage: external_exports.string().max(8e3).optional().describe("update: the agent instructions (system message)"),
43640
- noAnswerMessage: external_exports.string().max(500).optional().describe("update: what the agent says when the grounding gate declines to answer"),
43641
- grounding: external_exports.enum(["strict", "grounded_chat"]).optional().describe("update: 'strict' refuses anything not in the retrieved content (use it for anything customer-facing); 'grounded_chat' may add general knowledge around the cited content"),
43807
+ description: external_exports.string().max(300).optional().describe("create/update: one-line description shown in the hub; policy_create/update: what the policy is for"),
43808
+ systemMessage: external_exports.string().max(8e3).optional().describe("create/update: the agent instructions (system message); with goal, it replaces the drafted ones"),
43809
+ noAnswerMessage: external_exports.string().max(500).optional().describe("create/update: what the agent says when the grounding gate declines to answer"),
43810
+ grounding: external_exports.enum(["strict", "grounded_chat"]).optional().describe("create/update: 'strict' refuses anything not in the retrieved content (use it for anything customer-facing); 'grounded_chat' may add general knowledge around the cited content"),
43642
43811
  collectionIds: external_exports.array(external_exports.string().max(64)).max(16).optional().describe("create/update: scope the agent to these collections (collection_id from awesomate_knowledge_collections list); replaces the collection scope only, other scope fields and free-form tags are kept. policy_create/update: the collections the policy allows"),
43643
43812
  kinds: external_exports.array(external_exports.string().max(40)).max(16).optional().describe("create/update: restrict retrieval to source kinds (web, document, video, audio, image, book, faq, dataset; PDFs are stored as 'book'); omit for all kinds. policy_create/update: the kinds the policy allows"),
43644
43813
  sourceIds: external_exports.array(external_exports.string().max(200)).max(500).optional().describe("create/update, policy_create/update: pin the scope to specific sources"),
43645
- maxSources: external_exports.number().int().min(1).max(10).optional().describe("update: how many sources one answer may cite (1-10)"),
43646
- temperature: external_exports.number().min(0).max(1).optional().describe("update: 0 is the most literal"),
43814
+ maxSources: external_exports.number().int().min(1).max(10).optional().describe("create/update: how many sources one answer may cite (1-10)"),
43815
+ temperature: external_exports.number().min(0).max(1).optional().describe("create/update: 0 is the most literal"),
43647
43816
  message: external_exports.string().max(4e3).optional().describe("test: the question to ask the draft; visitor_test: the question a visitor asks the published agent (max 2000 characters)"),
43648
43817
  sessionId: external_exports.string().max(128).optional().describe("test/visitor_test: keep follow-ups in one thread")
43649
43818
  }
43650
43819
  },
43651
43820
  async (args) => {
43652
43821
  try {
43653
- return knowledgeResult(await knowledgeAgents(requireConfig(), args));
43822
+ return knowledgeResult(await knowledgeAgents(requireConfig(), args), { keepWebsiteKeys: args.action === "website_key" });
43654
43823
  } catch (err) {
43655
43824
  return knowledgeError(err);
43656
43825
  }
@@ -43793,7 +43962,7 @@ server.registerTool(
43793
43962
  "awesomate_knowledge_data",
43794
43963
  {
43795
43964
  annotations: annotate("awesomate_knowledge_data", MUTATING),
43796
- description: "The knowledge platform's business-data warehouse (every plan, within the plan's row and dataset limits). action 'metrics': headline numbers for the data tab. 'datasets': the datasets imported and their columns: read this FIRST, measures/dimensions must name real columns. 'imports': import job statuses. 'query' {dataset, measures:[{column, agg}], plus optional dimensions/filters passed through}: 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. 'dataset' {datasetId}: one dataset with its columns and how each is read. 'dataset_update' {datasetId, name?, description?, columns?:[{column_id, name?, semantic_type?, role?, unit?, currency?, canonical_metric_key?, pii?}]}: rename it or settle how its columns are read (show the owner the change first). 'import' {importId}: one import, with any questions it is waiting on. 'import_action' {importId, importAction: approve|reject|withdraw|reimport|answer, answers?}: approve lets a waiting import in, reject turns one away, withdraw takes an import that is already in back out again (confirm:true, after the owner says yes), reimport runs it again, answer {answers: {question key: answer}} replies to its questions. Every import_action changes the owner's numbers: say what it does and get a yes first. New data is imported in the hub UI, not here.",
43965
+ description: "The knowledge platform's business-data warehouse (every plan, within the plan's row and dataset limits). action 'metrics': headline numbers for the data tab. 'datasets': the datasets imported (names and ids, no columns): read this FIRST, then 'dataset' {datasetId} for a dataset's columns, because measures/dimensions must name real columns. 'imports': import job statuses. 'query' {dataset, measures:[{column, agg}], plus optional dimensions/filters passed through}: 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. 'dataset' {datasetId}: one dataset with its columns and how each is read. 'dataset_update' {datasetId, name?, description?, columns?:[{column_id, name?, semantic_type?, role?, unit?, currency?, canonical_metric_key?, pii?}]}: rename it or settle how its columns are read (show the owner the change first). 'import' {importId}: one import, with any questions it is waiting on. 'import_action' {importId, importAction: approve|reject|withdraw|reimport|answer, answers?}: approve lets a waiting import in, reject turns one away, withdraw takes an import that is already in back out again (confirm:true, after the owner says yes), reimport runs it again, answer {answers: {question key: answer}} replies to its questions. Every import_action changes the owner's numbers: say what it does and get a yes first. New data is imported in the hub UI, not here.",
43797
43966
  inputSchema: {
43798
43967
  action: external_exports.enum(["metrics", "datasets", "imports", "query", "dataset", "dataset_update", "import", "import_action"]),
43799
43968
  datasetId: external_exports.string().max(200).optional().describe("dataset, dataset_update: the dataset id from datasets"),
@@ -45286,19 +45455,25 @@ server.registerTool(
45286
45455
  "awesomate_knowledge_upload",
45287
45456
  {
45288
45457
  annotations: annotate("awesomate_knowledge_upload", MUTATING),
45289
- description: "Ingest ONE file from the user's own computer into their Knowledge Base. Pass a LOCAL PATH \u2014 this server runs on their machine and streams the file to the hub itself, so the file contents never pass through the conversation. Handles documents (pdf, md, txt), spreadsheets (csv, tsv, xls, xlsx, xlsm, json; these NEED kind, see below), audio and video (transcribed, Pro and above), and images (OCR, Pro and above). Word/PowerPoint/RTF/EPUB files are NOT parseable yet \u2014 the tool refuses them with the workaround (export to PDF, or save as .md/.txt). Max 100 MB per file; bigger media goes through the hub's Knowledge \u2192 Sources page. SPREADSHEETS: pass kind, or the tool refuses before uploading. kind 'sheet' makes each row a searchable, citable record: price lists, fee tables, timetables, FAQs, policies, product lists, anything a customer asks about. kind 'data-import' builds a reportable dataset for totals and trends: job or invoice history, sales by month, read with awesomate_knowledge_data. If it is not obvious which the user wants, ask them; never guess. The same goes for a spreadsheet in the File Manager (fromFilesPath): pass kind with it. The source's title comes from the filename, slugified (spaces, punctuation and non-ASCII letters become _), even when title is passed; fix it afterwards with awesomate_knowledge_sources {action:'rename'}. INGESTING COSTS MONEY and counts against the monthly allowance, so ALWAYS get explicit approval for the specific file(s) first and say what it will consume. For several files, call once per file and report progress \u2014 do not loop silently. Returns a job with its live status; poll awesomate_knowledge_sources {action:'jobs'} until it succeeds (a duplicate upload replays the earlier job \u2014 a response already reading succeeded needs no polling), then probe the content with awesomate_knowledge_ask before building anything on it. A pack_required response means the allowance is exhausted: nothing was ingested and nothing was purchased.",
45458
+ description: "Ingest ONE file from the user's own computer into their Knowledge Base. Pass a LOCAL PATH: this server runs on their machine and streams the file to the hub itself, so the file contents never pass through the conversation. Handles documents (pdf, md, txt), spreadsheets (csv, tsv, xls, xlsx, xlsm, json; these NEED kind, see below), audio and video (transcribed, Pro and above), and images (OCR, Pro and above). Word/PowerPoint/RTF/EPUB files are NOT parseable yet: the tool refuses them with the workaround (export to PDF, or save as .md/.txt). Max 100 MB per file; bigger media goes through the hub's Knowledge \u2192 Sources page. SPREADSHEETS: pass kind, or the tool refuses before uploading. kind 'sheet' makes each row a searchable, citable record: price lists, fee tables, timetables, FAQs, policies, product lists, anything a customer asks about. kind 'data-import' builds a reportable dataset for totals and trends: job or invoice history, sales by month, read with awesomate_knowledge_data. If it is not obvious which the user wants, ask them; never guess. The same goes for a spreadsheet in the File Manager (fromFilesPath): pass kind with it. The source's title comes from the filename, slugified (spaces, punctuation and non-ASCII letters become _), even when title is passed; fix it afterwards with awesomate_knowledge_sources {action:'rename'}. visibility 'public' publishes the file, so it needs confirm: the account slug, passed only after the user agreed in so many words (the same rule as awesomate_knowledge_sources add). INGESTING COSTS MONEY and counts against the monthly allowance, so ALWAYS get explicit approval for the specific file(s) first and say what it will consume. For several files, call once per file and report progress; do not loop silently. Returns a job with its live status; poll awesomate_knowledge_sources {action:'jobs'} until it succeeds (a duplicate upload replays the earlier job, and a response already reading succeeded needs no polling), then probe the content with awesomate_knowledge_ask before building anything on it. A pack_required response means the allowance is exhausted: nothing was ingested and nothing was purchased.",
45290
45459
  inputSchema: {
45291
45460
  path: external_exports.string().min(1).max(4096).optional().describe("Path to the file on the user's machine (~ is expanded). Omit when using fromFilesPath."),
45292
45461
  fromFilesPath: external_exports.string().max(1024).optional().describe("Instead of a local file: a path in the account's File Manager (their n8n file system), relative to files/, e.g. 'public/reports/weekly.md'. The hub streams it from their storage \u2014 nothing leaves this machine."),
45293
45462
  title: external_exports.string().max(300).optional().describe("Used as the uploaded file's name (local uploads only). The platform slugifies it (spaces, punctuation and non-ASCII letters become _), so for a readable title rename the source after the job succeeds"),
45294
45463
  kind: external_exports.enum(["sheet", "data-import"]).optional().describe("Spreadsheets only (csv, tsv, xls, xlsx, xlsm, json), and required for them: 'sheet' = each row a searchable, citable record (price lists, timetables, fee tables, FAQs); 'data-import' = a reportable dataset for totals and trends (job or invoice history). Ask the user if unclear; never guess"),
45295
- visibility: external_exports.enum(["private", "internal", "public"]).optional().describe("Who may retrieve this source (default private). public = customers and public chatbots; irreversible once fetched \u2014 ask first."),
45464
+ visibility: external_exports.enum(["private", "internal", "public"]).optional().describe("Who may retrieve this source (default private). public = customers and public chatbots; irreversible once fetched, so it also needs confirm."),
45465
+ confirm: external_exports.string().max(64).optional().describe("visibility 'public' only: the account slug, after the user agreed in so many words"),
45296
45466
  tags: external_exports.array(external_exports.string().max(64)).max(64).optional().describe("Free-form tags for the new source"),
45297
45467
  collectionIds: external_exports.array(external_exports.string().max(64)).max(16).optional().describe("Put the new source in these collections (awesomate_knowledge_collections list)")
45298
45468
  }
45299
45469
  },
45300
- async ({ path: rawPath, fromFilesPath, title, kind, visibility, tags, collectionIds }) => {
45470
+ async ({ path: rawPath, fromFilesPath, title, kind, visibility, confirm, tags, collectionIds }) => {
45301
45471
  try {
45472
+ if (visibility === "public" && !confirm) {
45473
+ return errorResult(
45474
+ new Error("Adding a file as public cannot be undone once it has been fetched or crawled. Ask the user in plain words, then call again with confirm: the account slug. Nothing was uploaded.")
45475
+ );
45476
+ }
45302
45477
  if (fromFilesPath) {
45303
45478
  const dot = fromFilesPath.lastIndexOf(".");
45304
45479
  const filesExt = dot > fromFilesPath.lastIndexOf("/") ? fromFilesPath.slice(dot).toLowerCase() : "";
@@ -45313,6 +45488,7 @@ server.registerTool(
45313
45488
  path: fromFilesPath,
45314
45489
  ...kind ? { kind } : {},
45315
45490
  ...visibility ? { visibility } : {},
45491
+ ...visibility === "public" && confirm ? { confirm } : {},
45316
45492
  ...tags?.length ? { tags } : {},
45317
45493
  ...collectionIds?.length ? { collection_ids: collectionIds } : {}
45318
45494
  });
@@ -45367,6 +45543,7 @@ server.registerTool(
45367
45543
  const filename = title?.trim() ? `${title.trim()}${ext}` : basename(abs);
45368
45544
  const uploadQuery = new URLSearchParams();
45369
45545
  if (visibility) uploadQuery.set("visibility", visibility);
45546
+ if (visibility === "public" && confirm) uploadQuery.set("confirm", confirm);
45370
45547
  if (kind) uploadQuery.set("kind", kind);
45371
45548
  for (const t of tags ?? []) uploadQuery.append("tags", t);
45372
45549
  for (const c of collectionIds ?? []) uploadQuery.append("collection_ids", c);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awesomate/hosting-mcp",
3
- "version": "0.91.1",
3
+ "version": "0.93.0",
4
4
  "description": "Awesomate MCP server — lets Claude manage your Awesomate WordPress hosting, plan, limits, n8n automations, and build Node/static apps + databases",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -1,5 +1,35 @@
1
1
  {
2
2
  "versions": [
3
+ {
4
+ "version": "0.93.0",
5
+ "highlights": [
6
+ "Claude can put a published agent on your website: it makes a website key for the sites you name, after your yes, and hands you the code to paste. Each website key has its own monthly answer limit, half your account's by default, and Claude tells you the number",
7
+ "Uploading a file as public now needs the same typed confirmation as adding a public page",
8
+ "A delete that finds nothing says the item may already be gone or the id may be wrong, and a delete that timed out says it may have gone through instead of saying nothing changed",
9
+ "Asking within filters works with the workspace's default agent, and a new agent drafted from a goal keeps the name you gave it",
10
+ "Skill update checks compare version numbers properly (0.10 is newer than 0.9)",
11
+ "When a Knowledge agent fails because the account's own OpenRouter key is refused or out of credit, Claude says so and where to fix it, instead of a generic service problem"
12
+ ]
13
+ },
14
+ {
15
+ "version": "0.92.2",
16
+ "highlights": [
17
+ "Publishing a staging copy keeps the live site's search engine setting, instead of hiding the live site from Google like the staging copy, and says so if it could not"
18
+ ]
19
+ },
20
+ {
21
+ "version": "0.92.1",
22
+ "highlights": [
23
+ "Turning on Knowledge again now puts back a missing n8n credential for it, and says why when it cannot (for example, n8n is not connected yet)"
24
+ ]
25
+ },
26
+ {
27
+ "version": "0.92.0",
28
+ "highlights": [
29
+ "Claude says whose key it is: the account owner's, or a team member's own, which does only what that person can do",
30
+ "Connecting on a computer already connected to the same account as someone else stops and asks, instead of replacing their key"
31
+ ]
32
+ },
3
33
  {
4
34
  "version": "0.91.1",
5
35
  "highlights": [
@@ -40,6 +40,7 @@ before any work** — every tool response is stamped `account: <slug> (source:
40
40
  once (plus `awesomate_get_limits` before any create/change) and cache:
41
41
 
42
42
  - **`plan`** and **`capabilities.shell`** — decides which branch below you're on.
43
+ - **`key`**: whose key this is. When `key.holder` is `person`, say so first ("This is Mia's own Claude Code, with view-only access") and never offer work their access does not include; if they ask for it, say the account owner can do it. A person's key never has a shell.
43
44
  - **`attention`** — `{unreadNotifications, erroringWorkflows7d, patExpiresInDays}`.
44
45
  If it shows erroring workflows or unread notifications, say so in ONE line
45
46
  ("two of your automations failed this week — want me to look?") before the
@@ -230,7 +231,7 @@ MCP, so say that rather than stopping.
230
231
  | Tool | Reach for it when |
231
232
  |---|---|
232
233
  | `awesomate_whoami` | First call of every session — which account this folder acts on (zero network) |
233
- | `awesomate_get_context` | Session start: plan, capabilities, PAT expiry, `attention`, skill updates, and `key`: whose key this is (always the account owner's today, so everything is done as the owner; team members cannot connect their own yet). A tool the key's scopes do not cover is refused at once in the hub's words: relay it, never retry |
234
+ | `awesomate_get_context` | Session start: plan, capabilities, PAT expiry, `attention`, skill updates, and `key`: whose key this is (the account owner's, so everything is done as the owner, or a team member's own, `holder: 'person'`, which does only what they can do in the hub). A tool the key's scopes do not cover is refused at once in the hub's words: relay it, never retry |
234
235
  | `awesomate_get_limits` | Before any create/change — live plan limits + workflow/AI-editor credit balances |
235
236
  | `awesomate_get_plan_features` | The live plan ladder, so upgrade talk is accurate |
236
237
  | `awesomate_business` | Before writing anything in the business's name: its own name, services, voice, colours, contact details. `read:'documents'` / `read:'document'` for the brand guide, voice guide and business summary; `read:'suggestions'` for details awaiting the owner's yes |
@@ -462,7 +463,10 @@ AI crawlers — anyone with the link can view, perfect for client review).
462
463
  `--domain <staging domain>`), send the user the staging URL to review.
463
464
  - `awesomate_site_staging_promote <live domain>` — publishes staging over live.
464
465
  Live is auto-snapshotted first; report the returned `preSnapshotId` as the
465
- undo handle (`awesomate_rollback_site`). **Confirm before promoting.**
466
+ undo handle (`awesomate_rollback_site`). Live keeps its own search engine
467
+ setting; if the result carries `searchEngineVisibility.warning`, tell the
468
+ user live may now be hidden from search engines and how to check.
469
+ **Confirm before promoting.**
466
470
  - `awesomate_site_staging_discard <live domain>` — deletes the staging copy;
467
471
  live untouched. **Confirm — unpublished staging changes are lost.**
468
472
 
@@ -25,6 +25,14 @@ The bootstrap prints a final `AWESOMATE CONNECT: SUCCESS account=<slug>` (or
25
25
  silence. `PARTIAL` means the token/pin are stored but a step (listed in
26
26
  `issues=`) needs the printed manual fix.
27
27
 
28
+ `FAILED reason=profile-belongs-to-someone-else` means this computer is already
29
+ connected to the same account as a different person (an owner and a team member
30
+ sharing a computer). Nothing was saved. Ask the person at the computer which key
31
+ should stay; re-run with `--replace` only on their clear yes, never on your own.
32
+ Otherwise they connect from their own user account on the computer. The key made
33
+ by the refused run is unused: they can remove it in the hub at Settings,
34
+ Account, Your Claude Code.
35
+
28
36
  ## Sandboxed / ephemeral environments (cloud agent containers)
29
37
 
30
38
  - **Egress proxy**: if `HTTPS_PROXY`/`HTTP_PROXY` is set, the environment
@@ -11,7 +11,7 @@
11
11
  * access token itself is never printed.
12
12
  *
13
13
  * Usage (Claude runs this — the user doesn't touch a terminal):
14
- * node bootstrap.mjs --code amt_bs_XXXX [--account <slug>] [--pin-dir <dir>]
14
+ * node bootstrap.mjs --code amt_bs_XXXX [--account <slug>] [--pin-dir <dir>] [--replace]
15
15
  * node bootstrap.mjs --pat amt_pat_XXXX [--account <slug>] # resume: skip redeem
16
16
  *
17
17
  * --account asserts WHICH account this connect is for — mismatch aborts before
@@ -472,6 +472,7 @@ async function main() {
472
472
  shellAccess: Boolean(ctx.capabilities?.shell),
473
473
  cpanel: ctx.cpanel ?? null,
474
474
  expiresAt: ctx.patExpiresAt ?? null,
475
+ ...(ctx.key?.holder === 'person' ? { keyKind: 'person_device' } : {}),
475
476
  };
476
477
  } else {
477
478
  console.log(`Redeeming setup code against ${apiBase} …`);
@@ -514,6 +515,21 @@ async function main() {
514
515
  redeemed.email = redeemed.email ?? context?.email ?? null;
515
516
  }
516
517
 
518
+ // One computer, two people (per-person keys design §6.1): never silently swap whose key this
519
+ // account uses here. An owner and a team member on a shared computer each keep their own.
520
+ const saved = readJsonFile(credPath) ?? {};
521
+ const before = (saved.profiles ?? {})[slug] ?? (saved.slug === slug ? saved : null);
522
+ const beforeEmail = typeof before?.email === 'string' ? before.email.trim().toLowerCase() : null;
523
+ const newEmail = typeof redeemed.email === 'string' ? redeemed.email.trim().toLowerCase() : null;
524
+ const replacing = Boolean(beforeEmail && newEmail && beforeEmail !== newEmail);
525
+ if (replacing && !process.argv.includes('--replace')) {
526
+ console.error(`This computer is already connected to "${slug}" as ${beforeEmail}. Connecting as ${newEmail} would replace that key here.`);
527
+ console.error('Ask the person at this computer which key should stay. Only on their clear yes, run the same command again with --replace; otherwise connect from your own user account on this computer.');
528
+ console.error(`The new key for ${newEmail} was made but not saved here. If it will not be used, remove it in the hub: Settings, Account, Your Claude Code.`);
529
+ console.error('AWESOMATE CONNECT: FAILED reason=profile-belongs-to-someone-else');
530
+ process.exit(1);
531
+ }
532
+
517
533
  const profile = {
518
534
  apiBase,
519
535
  pat: redeemed.pat,
@@ -523,6 +539,8 @@ async function main() {
523
539
  plan: redeemed.plan,
524
540
  cpanel: redeemed.cpanel ?? null,
525
541
  expiresAt: redeemed.expiresAt ?? null,
542
+ // Someone else's SSH key never carries over to this person's profile.
543
+ ...(replacing ? { ssh: undefined } : {}),
526
544
  };
527
545
  mergeProfile(profile);
528
546
  console.log(`✓ Access token stored as profile "${slug}" in ${credPath} (chmod 600). Plan: ${redeemed.plan}.`);
@@ -571,6 +589,8 @@ async function main() {
571
589
  console.log(' Everything else is set up. This usually means hosting isn\'t fully provisioned — contact Awesomate support, then ask me to "retry SSH setup".');
572
590
  }
573
591
  }
592
+ } else if (redeemed.keyKind === 'person_device') {
593
+ console.log('• This is your own key: it does what you can do in the hub. SSH and deploys stay with the account owner.');
574
594
  } else {
575
595
  console.log('• Plan has no shell access — read/status mode. Upgrade to Support Plus for SSH + deploy.');
576
596
  }
@@ -164,6 +164,7 @@ confirm:true}`, both after the owner's yes.
164
164
  | Agent builder (what `awesomate_knowledge_agents` calls) | `GET\|POST /api/knowledge/agents` · `GET\|PATCH\|DELETE /api/knowledge/agents/:agent_id` · `POST /api/knowledge/agents/:agent_id/{publish,suspend,resume}` · `GET /api/knowledge/agents/{logs,answer-rates,where-used}` · `GET\|POST /api/knowledge/agents/policies` · `GET\|PATCH\|DELETE /api/knowledge/agents/policies/:policy_id` · `POST /api/knowledge/agents/policies/:policy_id/{suspend,resume}` |
165
165
  | **Test a DRAFT agent** (`action:'test'`) | `POST /api/knowledge/agents/:agent_id/chat` `{message, session_id?}`. There is **no** `/test` endpoint — guessing one returns 404. It ignores the agent's audience. |
166
166
  | **Ask the PUBLISHED agent as a visitor** (`action:'visitor_test'`) | `POST /api/knowledge/agents/:agent_id/visitor-chat` `{message (max 2000), session_id?}`. 403 `upgrade_required` below the public-widget plan; an unanswered question lands in the Questions inbox. |
167
+ | **Website chat key** (`action:'website_key'`, `website_keys`, `revoke_website_key`; owner only, Support Plus and above) | `POST /api/knowledge/agents/keys` `{class:'p', agent_id, allowed_origins (https only, no path or wildcard), label?, request_id?, monthly_answer_cap?}` returns the key ONCE with its `monthly_answer_cap`; with no cap the hub sets half the account's monthly answers at that moment, and a key's cap cannot be changed later (a new limit is a new key). The tool refuses an agent that is not published, not public-audience, or paused. List with `GET /api/knowledge/agents/keys` (class `p`), revoke with `DELETE /api/knowledge/agents/keys/:key_id`. Every one only after the owner's yes. |
167
168
  | Entity layer probe (`available` false = not enabled yet, stop) | `GET /api/knowledge/entities` |
168
169
  | People list / detail | `GET /api/knowledge/people?status=named\|unknown\|hidden\|all` · `GET /api/knowledge/people/:id` — returns `{people, next_cursor}` and **no `counts` key**. |
169
170
  | Entity COUNTS + availability | `GET /api/knowledge/entities` → `{available, counts: {...}}` — counts are nested under `counts`. Reading `people.counts?.x ?? 0` prints a confident row of zeros next to data that plainly exists. |
@@ -260,8 +260,8 @@ while 1Brain is on.
260
260
  person's Task goes too), or move one on an older course to the role's
261
261
  current course (progress is read again from the new course). Each only on
262
262
  the owner's yes.
263
- - **`my_onboarding`**: the plans of the person this key belongs to, which
264
- today is always the account owner.
263
+ - **`my_onboarding`**: the plans of the person this key belongs to: the
264
+ account owner, or a team member on their own key.
265
265
  - Writing quizzes and acknowledgements, and sharing a course with a learner,
266
266
  are 1Brain's own connector's work (2d), owners and admins only.
267
267