@awesomate/hosting-mcp 0.92.2 → 0.93.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -40747,6 +40747,21 @@ function installedSkillVersion(name) {
40747
40747
  return null;
40748
40748
  }
40749
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
+ }
40750
40765
  function skillVersions(sourceRoot = DEFAULT_SOURCE_ROOT) {
40751
40766
  return bundledSkillNames(sourceRoot).map((name) => ({ name, installed: installedSkillVersion(name) }));
40752
40767
  }
@@ -40803,8 +40818,9 @@ function installSkills(version2, sourceRoot = DEFAULT_SOURCE_ROOT) {
40803
40818
  }
40804
40819
 
40805
40820
  // src/knowledge.ts
40806
- function scrubKeyMaterial(text) {
40807
- 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]");
40808
40824
  }
40809
40825
  var KNOWLEDGE_GATE_COPY = {
40810
40826
  displayName: "Knowledge Base",
@@ -40890,13 +40906,33 @@ function storageFullPayload(body) {
40890
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."
40891
40907
  };
40892
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
+ }
40893
40923
  function bodyOf(err) {
40894
40924
  return err.body && typeof err.body === "object" ? err.body : null;
40895
40925
  }
40896
40926
  function mapKnowledgeError(err, apiBase) {
40897
40927
  if (!(err instanceof HubApiError)) throw err;
40898
40928
  const body = bodyOf(err);
40899
- 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
+ }
40900
40936
  if ((err.status === 403 || err.status === 402) && body?.upgrade_required) return upsellPayload(apiBase, body.plan, body);
40901
40937
  if (err.status === 403 && body?.consent_required) {
40902
40938
  return consentPayload(typeof body.error === "string" ? body.error : 'Knowledge is not using your content: "Use your content for Knowledge" is off', apiBase);
@@ -41147,10 +41183,7 @@ async function knowledgeSources(config3, args) {
41147
41183
  }
41148
41184
  if (args.action === "remove") {
41149
41185
  if (!args.sourceId) throw new Error("sourceId is required for remove");
41150
- return await hubDelete(
41151
- config3,
41152
- `/api/knowledge/sources/${encodeURIComponent(args.sourceId)}`
41153
- );
41186
+ return deleteOrNotFound(config3, `/api/knowledge/sources/${encodeURIComponent(args.sourceId)}`);
41154
41187
  }
41155
41188
  if (!args.url && !args.sitemap) {
41156
41189
  return {
@@ -41194,15 +41227,18 @@ function renderAnswer(meta, streamedAnswer, streamError = null) {
41194
41227
  const session = typeof meta.session === "string" && meta.session ? { session: meta.session } : {};
41195
41228
  const fallback = typeof meta.answer_plain === "string" && meta.answer_plain || typeof meta.no_answer_message === "string" && meta.no_answer_message || null;
41196
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";
41197
41232
  return {
41198
41233
  status: "error",
41199
41234
  answer: null,
41200
41235
  platform_error: true,
41201
41236
  not_in_verified_content: false,
41237
+ ...errorCode ? { error_code: errorCode } : {},
41202
41238
  detail: streamError ?? (streamedAnswer || null),
41203
41239
  configured_fallback: fallback,
41204
41240
  ...session,
41205
- 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."
41206
41242
  };
41207
41243
  }
41208
41244
  if (status !== "ok") {
@@ -41239,14 +41275,41 @@ function renderAnswer(meta, streamedAnswer, streamError = null) {
41239
41275
  note: "UNGROUNDED: the platform returned ok but ZERO sources, which means this answer came from the model, NOT from the verified content. Do NOT present it as an answer from their knowledge base, do not quote it as fact, and do not build on it. Tell the user their content does not cover this. If they need it to refuse instead of improvising, the agent's grounding setting is not strict \u2014 a purpose-built agent (awesomate_knowledge_agents) can be scoped and set strict, and that is what should be wired to anything customer-facing."
41240
41276
  };
41241
41277
  }
41278
+ if (meta.reason === "hedged") {
41279
+ return {
41280
+ status: "ok",
41281
+ grounded: false,
41282
+ hedged: true,
41283
+ answer,
41284
+ sources,
41285
+ not_in_verified_content: true,
41286
+ configured_fallback: typeof meta.no_answer_message === "string" && meta.no_answer_message ? meta.no_answer_message : null,
41287
+ ...typeof meta.score === "number" ? { score: meta.score } : {},
41288
+ ...session,
41289
+ note: 'The assistant replied that it does not know (a hedge such as "not sure, please contact us"), so their content does not answer this. Tell the user so plainly and do not treat the cited sources as an answer.'
41290
+ };
41291
+ }
41292
+ const refFor = (docId, section) => {
41293
+ const i = rawSources.findIndex((s) => s.doc_id === docId && (section == null || s.section_path === section));
41294
+ const j = i >= 0 ? i : rawSources.findIndex((s) => s.doc_id === docId);
41295
+ return j >= 0 ? rawSources[j].ref ?? j + 1 : null;
41296
+ };
41297
+ const conflicts = Array.isArray(meta.conflicts) ? meta.conflicts.map((c) => ({
41298
+ item: typeof c.item === "string" ? c.item : null,
41299
+ values: (Array.isArray(c.values) ? c.values : []).map((v) => ({
41300
+ amount: v.amount ?? null,
41301
+ source_ref: refFor(v.doc_id, v.section_path)
41302
+ }))
41303
+ })) : null;
41242
41304
  return {
41243
41305
  status: "ok",
41244
41306
  grounded: true,
41245
41307
  answer,
41246
41308
  sources,
41309
+ ...conflicts && conflicts.length ? { conflicts } : {},
41247
41310
  ...typeof meta.score === "number" ? { score: meta.score } : {},
41248
41311
  ...session,
41249
- note: "Present the answer with its numbered sources; the citations are the product."
41312
+ note: conflicts && conflicts.length ? "Two of their documents disagree on this (see conflicts: each figure names the numbered source it came from). Present the answer with its sources, say which sources disagree, and suggest the user correct or remove the out-of-date one." : "Present the answer with its numbered sources; the citations are the product."
41250
41313
  };
41251
41314
  }
41252
41315
  async function knowledgeAsk(config3, args) {
@@ -41636,6 +41699,17 @@ function mapAgentsError(err, apiBase) {
41636
41699
  }
41637
41700
  if (err instanceof HubApiError && err.status === 403) {
41638
41701
  const body = bodyOf(err);
41702
+ if (body && body.error === "upgrade_required" && body.capability === "public_widget") {
41703
+ const requiredPlan = typeof body.requiredPlan === "string" ? body.requiredPlan : "Support Plus";
41704
+ return {
41705
+ upgrade_required: true,
41706
+ capability: "public_widget",
41707
+ requiredPlan,
41708
+ ...typeof body.message === "string" ? { message: body.message } : {},
41709
+ billingUrl: `${apiBase}/billing?upgrade=${encodeURIComponent(requiredPlan)}`,
41710
+ note: "Nothing was made. Relay the message as it is and let the user decide; never retry."
41711
+ };
41712
+ }
41639
41713
  if (body && body.error === "upgrade_required" && body.capability === "knowledge_questions") {
41640
41714
  const requiredPlan = typeof body.requiredPlan === "string" ? body.requiredPlan : "Support Plus";
41641
41715
  return {
@@ -41650,6 +41724,107 @@ function mapAgentsError(err, apiBase) {
41650
41724
  }
41651
41725
  return mapKnowledgeError(err, apiBase);
41652
41726
  }
41727
+ var WEBSITE_KEY_MAX_ORIGINS = 16;
41728
+ function websiteOrigins(input) {
41729
+ const raw = (input ?? []).map((s) => s.trim()).filter(Boolean);
41730
+ if (!raw.length) return { ok: false, error: "website_key needs origins: the website addresses the chat will run on, like https://example.com.au" };
41731
+ const out = [];
41732
+ for (const r of raw) {
41733
+ if (r.includes("*")) return { ok: false, error: `"${r}": a wildcard is not allowed, because the key would then answer on any website.` };
41734
+ let u;
41735
+ try {
41736
+ u = new URL(r.includes("://") ? r : `https://${r}`);
41737
+ } catch {
41738
+ return { ok: false, error: `"${r}" is not a website address.` };
41739
+ }
41740
+ if (u.protocol !== "https:") return { ok: false, error: `"${r}" must be an https:// address.` };
41741
+ if (u.username || u.password) return { ok: false, error: `"${r}" must not include a username or password.` };
41742
+ 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}` };
41743
+ if (!out.includes(u.origin)) out.push(u.origin);
41744
+ }
41745
+ if (out.length > WEBSITE_KEY_MAX_ORIGINS) return { ok: false, error: `At most ${WEBSITE_KEY_MAX_ORIGINS} websites per key.` };
41746
+ return { ok: true, origins: out };
41747
+ }
41748
+ var htmlAttr = (v) => v.replace(/&/g, "&amp;").replace(/"/g, "&quot;");
41749
+ function websiteSnippet(widgetUrl, key, agentId, title) {
41750
+ const attrs = [["data-key", key], ["data-agent", agentId]];
41751
+ if (title?.trim()) attrs.push(["data-title", title.trim()]);
41752
+ const lines = attrs.map(([k, v], i) => ` ${k}="${htmlAttr(v)}"${i === attrs.length - 1 ? "></script>" : ""}`);
41753
+ return [`<script src="${htmlAttr(widgetUrl)}"`, ...lines].join("\n");
41754
+ }
41755
+ function websiteKeyRequestId(agentId, origins, label, cap, liveKeyIds) {
41756
+ const h = createHash("sha256").update(`${agentId}|${[...origins].sort().join(",")}|${label}|${cap ?? "default"}|${[...liveKeyIds].sort().join(",")}`).digest("hex");
41757
+ return `${h.slice(0, 8)}-${h.slice(8, 12)}-4${h.slice(13, 16)}-8${h.slice(17, 20)}-${h.slice(20, 32)}`;
41758
+ }
41759
+ function liveWebsiteKeys(keys, agentId) {
41760
+ const now = Date.now();
41761
+ return keys.filter(
41762
+ (k) => k.class === "p" && (!agentId || k.agent_id === agentId) && !k.revoked_at && !(typeof k.expires_at === "string" && new Date(k.expires_at).getTime() <= now)
41763
+ );
41764
+ }
41765
+ function websiteAgentRefusal(agent) {
41766
+ const name = typeof agent.name === "string" && agent.name ? `"${agent.name}"` : "This agent";
41767
+ 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.`;
41768
+ 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.`;
41769
+ if (agent.suspended_at) return `${name} is paused, so a website chat would not answer. Resume it (with the user's yes) first.`;
41770
+ return null;
41771
+ }
41772
+ 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.";
41773
+ async function websiteKeyAction(config3, args) {
41774
+ const base = "/api/knowledge/agents";
41775
+ if (args.action === "website_keys") {
41776
+ const r = await hubGet(config3, `${base}/keys`);
41777
+ 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.` };
41778
+ }
41779
+ if (args.action === "revoke_website_key") {
41780
+ if (!args.keyId) return { error: "invalid_request", note: "revoke_website_key needs keyId (from website_keys)." };
41781
+ const r = await hubGet(config3, `${base}/keys`);
41782
+ if (!liveWebsiteKeys(r.keys ?? []).some((k) => k.key_id === args.keyId)) {
41783
+ 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." };
41784
+ }
41785
+ if (args.confirm !== true) {
41786
+ 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." };
41787
+ }
41788
+ return deleteOrNotFound(config3, `${base}/keys/${encodeURIComponent(args.keyId)}`);
41789
+ }
41790
+ if (!args.agentId) return { error: "invalid_request", note: "website_key needs agentId (from list) and origins." };
41791
+ const origins = websiteOrigins(args.origins);
41792
+ if (!origins.ok) return { error: "invalid_request", note: `${origins.error} Nothing was made.` };
41793
+ const agent = agentOf(await hubGet(config3, `${base}/${encodeURIComponent(args.agentId)}`));
41794
+ const notReady = websiteAgentRefusal(agent);
41795
+ if (notReady) return { error: "agent_not_ready", note: `${notReady} Nothing was made.` };
41796
+ if (args.confirm !== true) {
41797
+ return {
41798
+ error: "confirmation_required",
41799
+ 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.`
41800
+ };
41801
+ }
41802
+ const status = await hubGet(config3, "/api/knowledge/status");
41803
+ const widgetUrl = typeof status.widget_url === "string" && status.widget_url ? status.widget_url : null;
41804
+ 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." };
41805
+ const listed = await hubGet(config3, `${base}/keys`);
41806
+ const liveIds = liveWebsiteKeys(listed.keys ?? [], args.agentId).map((k) => String(k.key_id));
41807
+ const label = (args.label?.trim() || `website: ${origins.origins.map((o) => o.replace(/^https:\/\//, "")).join(", ")}`).slice(0, 120);
41808
+ const minted = await hubPost(config3, `${base}/keys`, {
41809
+ class: "p",
41810
+ agent_id: args.agentId,
41811
+ allowed_origins: origins.origins,
41812
+ label,
41813
+ request_id: websiteKeyRequestId(args.agentId, origins.origins, label, args.monthlyAnswerCap, liveIds),
41814
+ ...args.monthlyAnswerCap !== void 0 ? { monthly_answer_cap: args.monthlyAnswerCap } : {}
41815
+ });
41816
+ const key = typeof minted.key === "string" ? minted.key : "";
41817
+ const title = args.title?.trim() || (typeof agent.name === "string" ? agent.name : "");
41818
+ const cap = typeof minted.monthly_answer_cap === "number" ? minted.monthly_answer_cap : null;
41819
+ return {
41820
+ key_id: minted.key_id ?? null,
41821
+ key,
41822
+ allowed_origins: origins.origins,
41823
+ monthly_answer_cap: cap,
41824
+ snippet: key ? websiteSnippet(widgetUrl, key, args.agentId, title) : null,
41825
+ 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.` : "")
41826
+ };
41827
+ }
41653
41828
  var VISITOR_TEST_MAX_CHARS = 2e3;
41654
41829
  var POLICY_TAGS_MAX = 50;
41655
41830
  function policyScope(args) {
@@ -41668,6 +41843,9 @@ var POLICY_NOTE = "An access policy narrows what a key or a signed-in person may
41668
41843
  async function knowledgeAgents(config3, args) {
41669
41844
  try {
41670
41845
  const base = "/api/knowledge/agents";
41846
+ if (args.action === "website_key" || args.action === "website_keys" || args.action === "revoke_website_key") {
41847
+ return await websiteKeyAction(config3, args);
41848
+ }
41671
41849
  if (args.action === "logs") {
41672
41850
  const q2 = new URLSearchParams();
41673
41851
  if (args.agentId) q2.set("agent_id", args.agentId);
@@ -41745,19 +41923,20 @@ async function knowledgeAgents(config3, args) {
41745
41923
  const current = scope && typeof scope === "object" && !Array.isArray(scope) ? scope : {};
41746
41924
  return touchesScope(scopeArgs) ? { scope: agentPatchBody(scopeArgs, current).scope } : scope !== void 0 ? { scope } : {};
41747
41925
  };
41926
+ const callerFields = agentPatchBody({ ...args, collectionIds: void 0, kinds: void 0, sourceIds: void 0 });
41748
41927
  let created;
41749
41928
  let sentScope;
41750
41929
  let extra = {};
41751
41930
  if (args.goal) {
41752
41931
  const draft = await hubPost(config3, "/api/knowledge/agents/draft", { goal: args.goal });
41753
41932
  if (!draft.proposal) return { error: "draft_failed", detail: draft };
41754
- const body = { ...draft.proposal, ...withScope(draft.proposal.scope), ...args.audience ? { audience: args.audience } : {} };
41933
+ const body = { ...draft.proposal, ...callerFields, ...withScope(draft.proposal.scope) };
41755
41934
  sentScope = body.scope;
41756
41935
  created = await hubPost(config3, "/api/knowledge/agents", body);
41757
41936
  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.` };
41758
41937
  } else {
41759
41938
  if (!args.name) return { error: "name (or goal) is required for create" };
41760
- const body = { name: args.name, ...args.audience ? { audience: args.audience } : {}, ...withScope(void 0) };
41939
+ const body = { ...callerFields, name: args.name, ...withScope(void 0) };
41761
41940
  sentScope = body.scope;
41762
41941
  created = await hubPost(config3, "/api/knowledge/agents", body);
41763
41942
  }
@@ -41856,7 +42035,7 @@ async function knowledgeCollections(config3, args) {
41856
42035
  return await hubPatch(config3, `/api/knowledge/collections/${encodeURIComponent(args.collectionId)}`, body);
41857
42036
  }
41858
42037
  if (args.action === "delete") {
41859
- return await hubDelete(config3, `/api/knowledge/collections/${encodeURIComponent(args.collectionId)}`);
42038
+ return deleteOrNotFound(config3, `/api/knowledge/collections/${encodeURIComponent(args.collectionId)}`);
41860
42039
  }
41861
42040
  if (!args.sourceIds?.length) return { error: "invalid_request", note: `${args.action} needs sourceIds` };
41862
42041
  return await hubPost(config3, `/api/knowledge/collections/${encodeURIComponent(args.collectionId)}/sources`, {
@@ -42135,10 +42314,7 @@ function changelogSince(installed) {
42135
42314
  function skillUpdateInfo() {
42136
42315
  const perSkill = skillVersions();
42137
42316
  const staleSkills = perSkill.filter((s) => s.installed !== SERVER_VERSION).map((s) => s.name);
42138
- const oldest = perSkill.reduce(
42139
- (min, s) => s.installed !== null && (min === null || s.installed < min) ? s.installed : min,
42140
- null
42141
- );
42317
+ const oldest = oldestInstalledVersion(perSkill);
42142
42318
  const updateAvailable = perSkill.length === 0 || staleSkills.length > 0;
42143
42319
  return {
42144
42320
  serverVersion: SERVER_VERSION,
@@ -43426,13 +43602,13 @@ server.registerTool(
43426
43602
  }
43427
43603
  }
43428
43604
  );
43429
- function knowledgeResult(data) {
43605
+ function knowledgeResult(data, opts = {}) {
43430
43606
  return {
43431
43607
  content: [
43432
43608
  {
43433
43609
  type: "text",
43434
43610
  text: `${accountStamp()}
43435
- ${scrubKeyMaterial(JSON.stringify(data, null, 2))}`
43611
+ ${scrubKeyMaterial(JSON.stringify(data, null, 2), opts)}`
43436
43612
  }
43437
43613
  ]
43438
43614
  };
@@ -43564,7 +43740,7 @@ server.registerTool(
43564
43740
  author: external_exports.array(external_exports.string().max(200)).max(10).optional(),
43565
43741
  year: external_exports.array(external_exports.string().regex(/^\d{4}$/)).max(10).optional(),
43566
43742
  doc: external_exports.array(external_exports.string().max(200)).max(50).optional().describe("doc_ids from search hits")
43567
- }).optional().describe("Ask within a slice of the library: EXACTLY the awesomate_knowledge_search input shapes, reusable verbatim; filters only ever narrow")
43743
+ }).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")
43568
43744
  }
43569
43745
  },
43570
43746
  async ({ question, session, filters }) => {
@@ -43607,7 +43783,7 @@ server.registerTool(
43607
43783
  "awesomate_knowledge_agents",
43608
43784
  {
43609
43785
  annotations: annotate("awesomate_knowledge_agents", MUTATING),
43610
- 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.",
43786
+ 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.",
43611
43787
  inputSchema: {
43612
43788
  action: external_exports.enum([
43613
43789
  "list",
@@ -43631,9 +43807,17 @@ server.registerTool(
43631
43807
  "policy_update",
43632
43808
  "policy_delete",
43633
43809
  "policy_suspend",
43634
- "policy_resume"
43810
+ "policy_resume",
43811
+ "website_key",
43812
+ "website_keys",
43813
+ "revoke_website_key"
43635
43814
  ]),
43636
- confirm: external_exports.boolean().optional().describe("delete: true only after the owner said yes to deleting this agent"),
43815
+ confirm: external_exports.boolean().optional().describe("delete, website_key, revoke_website_key: true only after the owner said yes"),
43816
+ 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"),
43817
+ 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"),
43818
+ label: external_exports.string().max(120).optional().describe("website_key: a name for the key in key lists"),
43819
+ 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"),
43820
+ keyId: external_exports.string().max(128).optional().describe("revoke_website_key: key_id from website_keys"),
43637
43821
  since: external_exports.string().max(40).optional().describe("logs: ISO time to read from"),
43638
43822
  until: external_exports.string().max(40).optional().describe("logs: ISO time to read to"),
43639
43823
  endpoint: external_exports.string().max(200).optional().describe("logs: only calls to this endpoint, e.g. /v1/answer"),
@@ -43647,22 +43831,22 @@ server.registerTool(
43647
43831
  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"),
43648
43832
  goal: external_exports.string().max(2e3).optional().describe("create: describe the agent and AI drafts it from the account content"),
43649
43833
  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."),
43650
- description: external_exports.string().max(300).optional().describe("update: one-line description shown in the hub; policy_create/update: what the policy is for"),
43651
- systemMessage: external_exports.string().max(8e3).optional().describe("update: the agent instructions (system message)"),
43652
- noAnswerMessage: external_exports.string().max(500).optional().describe("update: what the agent says when the grounding gate declines to answer"),
43653
- 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"),
43834
+ 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"),
43835
+ systemMessage: external_exports.string().max(8e3).optional().describe("create/update: the agent instructions (system message); with goal, it replaces the drafted ones"),
43836
+ noAnswerMessage: external_exports.string().max(500).optional().describe("create/update: what the agent says when the grounding gate declines to answer"),
43837
+ 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"),
43654
43838
  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"),
43655
43839
  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"),
43656
43840
  sourceIds: external_exports.array(external_exports.string().max(200)).max(500).optional().describe("create/update, policy_create/update: pin the scope to specific sources"),
43657
- maxSources: external_exports.number().int().min(1).max(10).optional().describe("update: how many sources one answer may cite (1-10)"),
43658
- temperature: external_exports.number().min(0).max(1).optional().describe("update: 0 is the most literal"),
43841
+ maxSources: external_exports.number().int().min(1).max(10).optional().describe("create/update: how many sources one answer may cite (1-10)"),
43842
+ temperature: external_exports.number().min(0).max(1).optional().describe("create/update: 0 is the most literal"),
43659
43843
  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)"),
43660
43844
  sessionId: external_exports.string().max(128).optional().describe("test/visitor_test: keep follow-ups in one thread")
43661
43845
  }
43662
43846
  },
43663
43847
  async (args) => {
43664
43848
  try {
43665
- return knowledgeResult(await knowledgeAgents(requireConfig(), args));
43849
+ return knowledgeResult(await knowledgeAgents(requireConfig(), args), { keepWebsiteKeys: args.action === "website_key" });
43666
43850
  } catch (err) {
43667
43851
  return knowledgeError(err);
43668
43852
  }
@@ -43805,7 +43989,7 @@ server.registerTool(
43805
43989
  "awesomate_knowledge_data",
43806
43990
  {
43807
43991
  annotations: annotate("awesomate_knowledge_data", MUTATING),
43808
- 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.",
43992
+ 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.",
43809
43993
  inputSchema: {
43810
43994
  action: external_exports.enum(["metrics", "datasets", "imports", "query", "dataset", "dataset_update", "import", "import_action"]),
43811
43995
  datasetId: external_exports.string().max(200).optional().describe("dataset, dataset_update: the dataset id from datasets"),
@@ -45298,19 +45482,25 @@ server.registerTool(
45298
45482
  "awesomate_knowledge_upload",
45299
45483
  {
45300
45484
  annotations: annotate("awesomate_knowledge_upload", MUTATING),
45301
- 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.",
45485
+ 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.",
45302
45486
  inputSchema: {
45303
45487
  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."),
45304
45488
  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."),
45305
45489
  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"),
45306
45490
  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"),
45307
- 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."),
45491
+ 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."),
45492
+ confirm: external_exports.string().max(64).optional().describe("visibility 'public' only: the account slug, after the user agreed in so many words"),
45308
45493
  tags: external_exports.array(external_exports.string().max(64)).max(64).optional().describe("Free-form tags for the new source"),
45309
45494
  collectionIds: external_exports.array(external_exports.string().max(64)).max(16).optional().describe("Put the new source in these collections (awesomate_knowledge_collections list)")
45310
45495
  }
45311
45496
  },
45312
- async ({ path: rawPath, fromFilesPath, title, kind, visibility, tags, collectionIds }) => {
45497
+ async ({ path: rawPath, fromFilesPath, title, kind, visibility, confirm, tags, collectionIds }) => {
45313
45498
  try {
45499
+ if (visibility === "public" && !confirm) {
45500
+ return errorResult(
45501
+ 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.")
45502
+ );
45503
+ }
45314
45504
  if (fromFilesPath) {
45315
45505
  const dot = fromFilesPath.lastIndexOf(".");
45316
45506
  const filesExt = dot > fromFilesPath.lastIndexOf("/") ? fromFilesPath.slice(dot).toLowerCase() : "";
@@ -45325,6 +45515,7 @@ server.registerTool(
45325
45515
  path: fromFilesPath,
45326
45516
  ...kind ? { kind } : {},
45327
45517
  ...visibility ? { visibility } : {},
45518
+ ...visibility === "public" && confirm ? { confirm } : {},
45328
45519
  ...tags?.length ? { tags } : {},
45329
45520
  ...collectionIds?.length ? { collection_ids: collectionIds } : {}
45330
45521
  });
@@ -45379,6 +45570,7 @@ server.registerTool(
45379
45570
  const filename = title?.trim() ? `${title.trim()}${ext}` : basename(abs);
45380
45571
  const uploadQuery = new URLSearchParams();
45381
45572
  if (visibility) uploadQuery.set("visibility", visibility);
45573
+ if (visibility === "public" && confirm) uploadQuery.set("confirm", confirm);
45382
45574
  if (kind) uploadQuery.set("kind", kind);
45383
45575
  for (const t of tags ?? []) uploadQuery.append("tags", t);
45384
45576
  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.92.2",
3
+ "version": "0.93.1",
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,23 @@
1
1
  {
2
2
  "versions": [
3
+ {
4
+ "version": "0.93.1",
5
+ "highlights": [
6
+ "When a Knowledge agent replies that it does not know (\"not sure, please contact us\"), Claude says your content does not cover it instead of passing that on as an answer",
7
+ "When two of your documents disagree, Claude says so and suggests correcting the out-of-date one"
8
+ ]
9
+ },
10
+ {
11
+ "version": "0.93.0",
12
+ "highlights": [
13
+ "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",
14
+ "Uploading a file as public now needs the same typed confirmation as adding a public page",
15
+ "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",
16
+ "Asking within filters works with the workspace's default agent, and a new agent drafted from a goal keeps the name you gave it",
17
+ "Skill update checks compare version numbers properly (0.10 is newer than 0.9)",
18
+ "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"
19
+ ]
20
+ },
3
21
  {
4
22
  "version": "0.92.2",
5
23
  "highlights": [
@@ -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. |
@@ -10,7 +10,7 @@ estimating BEFORE asking approval.
10
10
  |---|---|---|
11
11
  | Web page / blog post | `add {url}` | One page per URL; extraction skips pages under ~200 chars of real text |
12
12
  | Whole site / blog | `add {sitemap, since?}` | sitemap.xml; default cap 500 pages per job — a bigger sitemap FAILS the job loudly rather than truncating silently. `since` skips entries with older `<lastmod>` |
13
- | YouTube / hosted video, podcast audio | `add {url}` | **Not transcribed today**: a link to a recording is read as a web page (title and description). Pro and above only; below Pro, or in a trial, it is refused (403 `media_ingest`). To transcribe a recording, upload the file |
13
+ | YouTube / hosted video, podcast audio | `add {url}` | **Not transcribed today.** A YouTube link is refused ("YouTube links are not supported yet"); another link to a recording is read as a web page (title and description). Pro and above only; below Pro, or in a trial, it is refused (403 `media_ingest`). To transcribe a recording, upload the file |
14
14
  | PDF / book / document / markdown | hub upload page | Text layer used; scanned pages OCR'd automatically (`auto` mode, capped at 300 OCR pages per job — over that the job fails BEFORE any vision spend) |
15
15
  | Video/audio FILES | hub upload page (≤ 100 MB) | Transcribed (Deepgram, language defaults to English). Pro and above, within the month's media hours. A longer recording: split or compress it and upload each part |
16
16
  | Images | hub upload page | Vision-described, then indexed |