@awesomate/hosting-mcp 0.45.0 → 0.47.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
@@ -39984,6 +39984,9 @@ function hubGet(config3, path) {
39984
39984
  function hubPost(config3, path, jsonBody = {}, opts = {}) {
39985
39985
  return hubRequest(config3, "POST", path, jsonBody, opts);
39986
39986
  }
39987
+ function hubPut(config3, path, jsonBody = {}) {
39988
+ return hubRequest(config3, "PUT", path, jsonBody);
39989
+ }
39987
39990
  function hubPatch(config3, path, jsonBody = {}) {
39988
39991
  return hubRequest(config3, "PATCH", path, jsonBody);
39989
39992
  }
@@ -41190,6 +41193,9 @@ var TITLES = {
41190
41193
  awesomate_crm_write: "Write app data records",
41191
41194
  awesomate_crm_queries: "Save and run named data queries",
41192
41195
  awesomate_crm_recipes: "Save named write recipes",
41196
+ awesomate_crm_apps: "Set up sign-in for your own apps",
41197
+ awesomate_crm_app_users: "Manage the people who sign in to your apps",
41198
+ awesomate_crm_assistant: "Set up the AI in your app's conversations",
41193
41199
  awesomate_crm_email: "Draft an email to a contact list"
41194
41200
  };
41195
41201
  function annotate(name, kind) {
@@ -41878,23 +41884,13 @@ readTool(
41878
41884
  "Usage vs plan limits across every dimension (sites, custom domains, apps, database provisions today, workflow credits, AI-editor credits) plus a nudges[] array. Call BEFORE any create action; when a nudge has severity 'approaching' or 'exceeded', surface it to the user with the recommended plan \u2014 never execute an upgrade without preview + explicit confirmation (upgrade tools ship in a later release; for now deep-link to the hub billing page). pgProvisionsToday is a flat daily cap that resets within 24h \u2014 no plan raises it, so don't pitch an upgrade for it.",
41879
41885
  "/api/client-hosting/limits"
41880
41886
  );
41881
- var KNOWLEDGE_NOT_INCLUDED = {
41882
- included: false,
41883
- onboardingMediaHours: 0,
41884
- onboardingPages: 0,
41885
- onboardingUrls: 0,
41886
- monthlyMediaHours: 0,
41887
- monthlyAnswers: 0,
41888
- maxStorageGb: 0,
41889
- agentModelTier: "sonnet"
41890
- };
41891
41887
  var PLAN_LADDER_FALLBACK = {
41892
- note: "Live per-account limits come from awesomate_get_limits \u2014 this is the plan ladder reference (static fallback).",
41888
+ note: "Live per-account limits come from awesomate_get_limits. This is the plan ladder reference (static fallback).",
41893
41889
  plans: [
41894
- { name: "Essentials", purchasable: false, monthlyPriceUsd: 75, maxSites: 1, maxCustomDomains: 1, maxApps: 0, cpanelAccess: true, shellAccess: false, appBuilder: false, knowledgeBase: KNOWLEDGE_NOT_INCLUDED },
41895
- { name: "Support Plus", purchasable: true, monthlyPriceUsd: 175, maxSites: 2, maxCustomDomains: 5, maxApps: 5, cpanelAccess: true, shellAccess: true, appBuilder: true, knowledgeBase: KNOWLEDGE_NOT_INCLUDED },
41896
- { name: "Pro", purchasable: true, monthlyPriceUsd: 375, maxSites: 10, maxCustomDomains: 20, maxApps: 20, cpanelAccess: true, shellAccess: true, appBuilder: true, knowledgeBase: { included: true, onboardingMediaHours: 10, onboardingPages: 2e3, onboardingUrls: 500, monthlyMediaHours: 5, monthlyAnswers: 1e3, maxStorageGb: 10, agentModelTier: "sonnet" } },
41897
- { name: "Embedded", purchasable: false, monthlyPriceUsd: null, maxSites: 100, maxCustomDomains: 100, maxApps: 100, cpanelAccess: true, shellAccess: true, appBuilder: true, knowledgeBase: { included: true, onboardingMediaHours: 50, onboardingPages: 1e4, onboardingUrls: 2500, monthlyMediaHours: 20, monthlyAnswers: 1e4, maxStorageGb: 50, agentModelTier: "opus" }, note: "Not generally available yet" }
41890
+ { name: "Essentials", purchasable: false, monthlyPriceUsd: 75, maxSites: 1, maxCustomDomains: 1, maxApps: 0, cpanelAccess: true, shellAccess: false, appBuilder: false, knowledgeBase: { included: true, onboardingMediaHours: 0, onboardingPages: 50, onboardingUrls: 10, monthlyMediaHours: 0, monthlyAnswers: 50, maxStorageGb: 5, agentModelTier: "sonnet" } },
41891
+ { name: "Support Plus", purchasable: true, monthlyPriceUsd: 175, maxSites: 2, maxCustomDomains: 5, maxApps: 5, cpanelAccess: true, shellAccess: true, appBuilder: true, knowledgeBase: { included: true, onboardingMediaHours: 5, onboardingPages: 1e3, onboardingUrls: 250, monthlyMediaHours: 2, monthlyAnswers: 500, maxStorageGb: 250, agentModelTier: "sonnet" } },
41892
+ { name: "Pro", purchasable: true, monthlyPriceUsd: 375, maxSites: 10, maxCustomDomains: 20, maxApps: 20, cpanelAccess: true, shellAccess: true, appBuilder: true, knowledgeBase: { included: true, onboardingMediaHours: 10, onboardingPages: 2e3, onboardingUrls: 500, monthlyMediaHours: 5, monthlyAnswers: 1e3, maxStorageGb: 1024, agentModelTier: "sonnet" } },
41893
+ { name: "Embedded", purchasable: false, monthlyPriceUsd: null, maxSites: 100, maxCustomDomains: 100, maxApps: 100, cpanelAccess: true, shellAccess: true, appBuilder: true, knowledgeBase: { included: true, onboardingMediaHours: 50, onboardingPages: 1e4, onboardingUrls: 2500, monthlyMediaHours: 20, monthlyAnswers: 1e4, maxStorageGb: 1024, agentModelTier: "opus" }, note: "Not generally available yet" }
41898
41894
  ],
41899
41895
  upgradePath: "Essentials \u2192 Support Plus \u2192 Pro \u2192 Embedded; upgrades at hub.awesomate.ai/billing (only Support Plus and Pro are purchasable)"
41900
41896
  };
@@ -42792,25 +42788,35 @@ server.registerTool(
42792
42788
  "awesomate_crm_kinds",
42793
42789
  {
42794
42790
  annotations: annotate("awesomate_crm_kinds", MUTATING),
42795
- description: "The kinds of app data in the client's own database (the tables an app keeps: jobs, memos, bookings, messages), created directly from here on Support Plus and above. action 'list': every kind with its attributes and links. 'define' {key, label?, label_plural?, attributes, links?}: a new kind; links: [{key, to, label?, required?}] where to is 'contact' or a kind already made, and each shows as <key>_id. 'add_attribute' {kind, attribute}. 'archive_attribute' {kind, key}: leaves the views, values kept. 'archive' {kind}: kept, restorable. No migration and no SQL: a kind is registry rows plus generated views. Business things whose values must be traced to a source (a property, an installed system) are 'tracked' kinds the owner accepts in the hub; they are refused here. Mark an attribute readable_by_ai only when the owner wants Claude and agents to see it, and never for anything sensitive. Confirm the design with the user before defining: a kind is part of their data, not scratch space.",
42791
+ description: "The kinds of app data in the client's own database (the tables an app keeps: jobs, memos, bookings, messages), created directly from here on Support Plus and above. action 'list': every kind with its attributes and links. 'define' {key, label?, label_plural?, attributes, links?}: a new kind; links: [{key, to, label?, required?}] where to is 'contact' or a kind already made, and each shows as <key>_id. 'add_attribute' {kind, attribute}. 'archive_attribute' {kind, key}: leaves the views, values kept. 'archive' {kind}: kept, restorable. No migration and no SQL: a kind is registry rows plus generated views. Business things whose values must be traced to a source (a property, an installed system) are 'tracked' kinds the owner accepts in the hub; they are refused here. Mark an attribute readable_by_ai only when the owner wants Claude and agents to see it, and never for anything sensitive. 'set_access' {kind, access} (or access on define): who among the people signed in to the account's apps (awesomate_crm_apps) may read and change records of this kind, by their role: {read: {<role>: <rule>}, write: {<role>: <rule>}}. A rule is all, none, own (records they created), or linked:<link path ending at a contact> (linked:customer: jobs whose customer is them; linked:job.customer: memos on their jobs), joined with |. A role with no rule gets nothing; the default lets owner and staff do everything and members nothing. A write must leave the record inside the person's rule. Confirm the design with the user before defining: a kind is part of their data, not scratch space.",
42796
42792
  inputSchema: {
42797
- action: external_exports.enum(["list", "define", "add_attribute", "archive_attribute", "archive"]),
42798
- kind: external_exports.string().max(41).optional().describe("the kind to change (add_attribute, archive_attribute, archive)"),
42793
+ action: external_exports.enum(["list", "define", "add_attribute", "archive_attribute", "archive", "set_access"]),
42794
+ kind: external_exports.string().max(41).optional().describe("the kind to change (add_attribute, archive_attribute, archive, set_access)"),
42799
42795
  key: external_exports.string().max(41).optional().describe("define: the new kind; archive_attribute: the attribute"),
42800
42796
  label: external_exports.string().max(80).optional(),
42801
42797
  label_plural: external_exports.string().max(80).optional(),
42802
42798
  attributes: external_exports.array(crmAttribute).max(60).optional(),
42803
42799
  links: external_exports.array(external_exports.object({ key: external_exports.string().max(38), to: external_exports.string().max(41), label: external_exports.string().max(80).optional(), to_label: external_exports.string().max(80).optional(), required: external_exports.boolean().optional() })).max(20).optional(),
42804
- attribute: crmAttribute.optional().describe("add_attribute")
42800
+ attribute: crmAttribute.optional().describe("add_attribute"),
42801
+ access: external_exports.object({
42802
+ read: external_exports.record(external_exports.string().max(200)).optional(),
42803
+ write: external_exports.record(external_exports.string().max(200)).optional()
42804
+ }).optional().describe("define / set_access: {read: {<role>: <rule>}, write: {<role>: <rule>}}")
42805
42805
  }
42806
42806
  },
42807
- async ({ action, kind, key, label, label_plural, attributes, links, attribute }) => {
42807
+ async ({ action, kind, key, label, label_plural, attributes, links, attribute, access }) => {
42808
42808
  try {
42809
42809
  const cfg = requireConfig();
42810
42810
  if (action === "list") return textResult(await hubGet(cfg, "/api/my-crm/v1/kinds"));
42811
- if (action === "define") return textResult(await hubPost(cfg, "/api/my-crm/v1/kinds", { key, label, label_plural, attributes: attributes ?? [], links: links ?? [] }));
42811
+ if (action === "define") {
42812
+ return textResult(await hubPost(cfg, "/api/my-crm/v1/kinds", { key, label, label_plural, attributes: attributes ?? [], links: links ?? [], ...access ? { access } : {} }));
42813
+ }
42812
42814
  if (!kind) return errorResult(new Error(`${action} needs the kind.`));
42813
42815
  const k = encodeURIComponent(kind);
42816
+ if (action === "set_access") {
42817
+ if (!access) return errorResult(new Error("set_access needs access: {read: {<role>: <rule>}, write: {<role>: <rule>}}."));
42818
+ return textResult(await hubPut(cfg, `/api/my-crm/v1/kinds/${k}/access`, { access }));
42819
+ }
42814
42820
  if (action === "add_attribute") return textResult(await hubPost(cfg, `/api/my-crm/v1/kinds/${k}/attributes`, { attribute }));
42815
42821
  if (action === "archive_attribute") {
42816
42822
  if (!key) return errorResult(new Error("archive_attribute needs key, the attribute."));
@@ -42855,6 +42861,102 @@ server.registerTool(
42855
42861
  }
42856
42862
  }
42857
42863
  );
42864
+ server.registerTool(
42865
+ "awesomate_crm_apps",
42866
+ {
42867
+ annotations: annotate("awesomate_crm_apps", MUTATING),
42868
+ description: "The account's own apps (a client portal, a members' app) whose users sign in by email link and then read and change only what each kind's access rules allow them (awesomate_crm_kinds set_access). Pro and above to create or change; reading is every plan. action 'list': each app with its publishable key, allowed origins, sign-up mode and default role. action 'create' {name, allowed_origins, sign_up?, default_role?}: name appears in the sign-in email; allowed_origins are the exact origins the app runs on (https://portal.example.com; http://localhost:5173 for development; capacitor://localhost for the mobile shell), with no path and no wildcard; sign_up 'invite' (default: only people added with awesomate_crm_app_users) or 'open' (anyone who uses the email link gets the default role). action 'update' {app_id, ...any of those, status?}: status 'disabled' signs everyone out of that app. The publishable key (pk_...) is not a secret: it goes in the app's browser code, with @awesomate/sdk. Never put the account's own token (amt_pat_...) in browser code. Confirm origins and sign-up mode with the user: open sign-up lets strangers in under the default role.",
42869
+ inputSchema: {
42870
+ action: external_exports.enum(["list", "create", "update"]),
42871
+ app_id: external_exports.string().max(48).optional().describe("update: the app"),
42872
+ name: external_exports.string().max(80).optional(),
42873
+ allowed_origins: external_exports.array(external_exports.string().max(200)).max(10).optional(),
42874
+ sign_up: external_exports.enum(["invite", "open"]).optional(),
42875
+ default_role: external_exports.string().max(31).optional().describe("lower case; the role open sign-up gives"),
42876
+ status: external_exports.enum(["active", "disabled"]).optional().describe("update only")
42877
+ }
42878
+ },
42879
+ async ({ action, app_id, name, allowed_origins, sign_up, default_role, status }) => {
42880
+ try {
42881
+ const cfg = requireConfig();
42882
+ if (action === "list") return textResult(await hubGet(cfg, "/api/my-crm/v1/apps"));
42883
+ const fields = Object.fromEntries(Object.entries({ name, allowed_origins, sign_up, default_role }).filter(([, v]) => v !== void 0));
42884
+ if (action === "create") return textResult(await hubPost(cfg, "/api/my-crm/v1/apps", fields));
42885
+ if (!app_id) return errorResult(new Error("update needs app_id, from list."));
42886
+ return textResult(await hubPatch(cfg, `/api/my-crm/v1/apps/${encodeURIComponent(app_id)}`, { ...fields, ...status ? { status } : {} }));
42887
+ } catch (err) {
42888
+ return errorResult(err);
42889
+ }
42890
+ }
42891
+ );
42892
+ server.registerTool(
42893
+ "awesomate_crm_app_users",
42894
+ {
42895
+ annotations: annotate("awesomate_crm_app_users", MUTATING),
42896
+ description: "The people who sign in to the account's apps (awesomate_crm_apps): their email, role, the contact they are (matched on email when they are added or first sign in), last sign-in and whether they are disabled. Pro and above to change; reading is every plan. action 'list'. action 'add' {email, role?}: lets them sign in to an invite-only app (no email is sent; they ask for a link in the app). action 'update' {id, role?, disabled?, contact_id?}: a role change applies to their very next request; disabled: true stops their next request and signs them out of every app at once; contact_id links them to a contact (null unlinks), which is what linked: rules follow. These are people's email addresses: show them only when the user asks, and never paste them anywhere else.",
42897
+ inputSchema: {
42898
+ action: external_exports.enum(["list", "add", "update"]),
42899
+ id: external_exports.string().max(64).optional().describe("update: the person's id from list"),
42900
+ email: external_exports.string().max(254).optional().describe("add"),
42901
+ role: external_exports.string().max(31).optional().describe("lower case: owner, staff, member, or one of your own"),
42902
+ disabled: external_exports.boolean().optional(),
42903
+ contact_id: external_exports.string().max(64).nullable().optional()
42904
+ }
42905
+ },
42906
+ async ({ action, id, email: email2, role, disabled, contact_id }) => {
42907
+ try {
42908
+ const cfg = requireConfig();
42909
+ if (action === "list") return textResult(await hubGet(cfg, "/api/my-crm/v1/app-users"));
42910
+ if (action === "add") {
42911
+ if (!email2) return errorResult(new Error("add needs the email."));
42912
+ return textResult(await hubPost(cfg, "/api/my-crm/v1/app-users", { email: email2, ...role ? { role } : {} }));
42913
+ }
42914
+ if (!id) return errorResult(new Error("update needs id, from list."));
42915
+ const patch = Object.fromEntries(Object.entries({ role, disabled, contact_id }).filter(([, v]) => v !== void 0));
42916
+ return textResult(await hubPatch(cfg, `/api/my-crm/v1/app-users/${encodeURIComponent(id)}`, patch));
42917
+ } catch (err) {
42918
+ return errorResult(err);
42919
+ }
42920
+ }
42921
+ );
42922
+ server.registerTool(
42923
+ "awesomate_crm_assistant",
42924
+ {
42925
+ annotations: annotate("awesomate_crm_assistant", MUTATING),
42926
+ description: "An AI participant in an app's conversations (a portal's per-job messages): when a signed-in customer writes, it answers from what that customer may see plus the account's own Knowledge Base, or drafts an answer for staff. Each conversation has a mode staff switch: off (people only, the default), draft (a suggested reply only staff see, sent by a person as themselves) or auto (it replies at once under its own name, marked as AI, when its gate allows; otherwise it drafts). It reads AS the customer, so the account's access rules bound it, and only attributes marked readable_by_ai reach it. The hub writes its replies, never the model. Pro and above to change; reading is every plan. action 'get' {app_id}. action 'set' {app_id, assistant}: assistant is {name, thread_link (the message kind's link to the job/booking), instructions?, knowledge_agent_id? (a PUBLISHED agent from awesomate_knowledge_agents, audience public), default_mode? (off), message_kind? (message), body_attribute? (body), settings_kind? (assistant_thread), drafts_kind? (assistant_draft), staff_roles? ([owner, staff]), daily_cap? (200), enabled?}. The tenant must already hold: the message kind with the body attribute marked readable_by_ai, a yes/no from_assistant attribute and the thread link; a settings kind {mode: choice off|draft|auto, needs_person?: yes_no} and a drafts kind {body, why?}, both linked to the same thread kind and readable by staff only (set_access). Saving an enabled assistant is refused with every problem listed; fix them with awesomate_crm_kinds and save again. action 'runs' {app_id, limit?}: what it decided per customer message (send, draft, handoff, skip, error) and why, by reference only. Confirm the name, the default mode and the knowledge agent with the user before 'set': switching it on lets an AI reply to their customers.",
42927
+ inputSchema: {
42928
+ action: external_exports.enum(["get", "set", "runs"]),
42929
+ app_id: external_exports.string().max(48).describe("the app, from awesomate_crm_apps list"),
42930
+ assistant: external_exports.object({
42931
+ name: external_exports.string().max(40),
42932
+ thread_link: external_exports.string().max(41),
42933
+ instructions: external_exports.string().max(4e3).optional(),
42934
+ knowledge_agent_id: external_exports.string().max(64).nullable().optional(),
42935
+ default_mode: external_exports.enum(["off", "draft", "auto"]).optional(),
42936
+ message_kind: external_exports.string().max(41).optional(),
42937
+ body_attribute: external_exports.string().max(41).optional(),
42938
+ settings_kind: external_exports.string().max(41).optional(),
42939
+ drafts_kind: external_exports.string().max(41).optional(),
42940
+ staff_roles: external_exports.array(external_exports.string().max(31)).max(10).optional(),
42941
+ daily_cap: external_exports.number().int().min(1).max(1e3).optional(),
42942
+ enabled: external_exports.boolean().optional()
42943
+ }).optional().describe("set: the whole config; fields left out take their defaults"),
42944
+ limit: external_exports.number().int().min(1).max(200).optional().describe("runs: how many, newest first (default 50)")
42945
+ }
42946
+ },
42947
+ async ({ action, app_id, assistant, limit }) => {
42948
+ try {
42949
+ const cfg = requireConfig();
42950
+ const path = `/api/my-crm/v1/apps/${encodeURIComponent(app_id)}/assistant`;
42951
+ if (action === "get") return textResult(await hubGet(cfg, path));
42952
+ if (action === "runs") return textResult(await hubGet(cfg, `${path}/runs${limit ? `?limit=${limit}` : ""}`));
42953
+ if (!assistant) return errorResult(new Error("set needs assistant: {name, thread_link, ...}."));
42954
+ return textResult(await hubPut(cfg, path, { assistant }));
42955
+ } catch (err) {
42956
+ return errorResult(err);
42957
+ }
42958
+ }
42959
+ );
42858
42960
  var crmParam = external_exports.object({
42859
42961
  name: external_exports.string().max(41).describe("lower_case"),
42860
42962
  type: external_exports.enum(["text", "number", "date", "datetime", "boolean", "uuid", "text_list", "number_list"]).optional().describe("default text"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awesomate/hosting-mcp",
3
- "version": "0.45.0",
3
+ "version": "0.47.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,21 @@
1
1
  {
2
2
  "versions": [
3
+ {
4
+ "version": "0.47.0",
5
+ "highlights": [
6
+ "Your app's conversations can have an AI in them (awesomate_crm_assistant). When a customer writes, it answers from their own records and your Knowledge Base, or drafts an answer for your team. Pro and above",
7
+ "Your team decides per conversation: off, draft (a suggested reply only staff see, sent by a person as themselves) or auto (it replies at once, marked as AI). New conversations start off unless you choose otherwise",
8
+ "It reads as the customer it is answering, so your access rules bound it, and it sees only the fields you marked readable by AI. When someone asks for a person, it hands the conversation over and flags it"
9
+ ]
10
+ },
11
+ {
12
+ "version": "0.46.0",
13
+ "highlights": [
14
+ "Your own apps can now sign people in. Claude can set up an app (awesomate_crm_apps): where it runs, whether anyone can sign up or only people you add, and the key that goes in the app. Each person gets an email link, no passwords. Pro and above",
15
+ "You decide who sees what, per kind of record: members see their own jobs and the memos on them, staff see everything (awesomate_crm_kinds, set_access). The rules are kept in your own database, so a mistake in an app can't show anyone more",
16
+ "Add, re-role or switch off the people who sign in (awesomate_crm_app_users). Switching someone off takes effect on their very next click and signs them out everywhere"
17
+ ]
18
+ },
3
19
  {
4
20
  "version": "0.45.0",
5
21
  "highlights": [
@@ -1,21 +1,27 @@
1
1
  # Quotas, allowances, and Ingestion Packs
2
2
 
3
3
  Live numbers ALWAYS come from `awesomate_knowledge_status` (`usage` vs
4
- `usage.included` + `usage.packs`) — this file explains the model.
4
+ `usage.included` + `usage.packs`). This file explains the model.
5
5
 
6
6
  ## Plan allowances
7
7
 
8
- | | Pro | Embedded |
9
- |---|---|---|
10
- | Included | yes | yes |
11
- | Onboarding (first month, one-off) | 10 media-hours, 2,000 pages / 500 URLs | 50 media-hours, 10,000 pages / 2,500 URLs |
12
- | Monthly media ingest | 5 hours | 20 hours |
13
- | Monthly verified answers | 1,000 | 10,000 |
14
- | Storage | 10 GB | 50 GB |
15
- | Agent model tier | sonnet | opus-eligible |
8
+ Every plan includes the Knowledge Base. The NUMBERS (pages, answers a month,
9
+ storage, business data) are not written here on purpose: they change as we learn
10
+ real usage. Read them from `awesomate_knowledge_status` (`usage.included`), or
11
+ from `awesomate_get_plan_features` for another plan.
16
12
 
17
- Essentials and Support Plus don't include Knowledge Base — status returns
18
- `upgrade_required` with the upsell copy; relay it once, honestly.
13
+ | | Essentials | Support Plus | Pro |
14
+ |---|---|---|---|
15
+ | What it is for | Trying it out: your FAQs and an agent that answers from them | Day-to-day use | Everything, at the highest usage |
16
+ | FAQs | Yes | Yes | Yes |
17
+ | Documents, spreadsheets, web pages, pasted text | A small allowance | More | The most |
18
+ | Website chat widget, unanswered-questions inbox | No | Yes | Yes |
19
+ | Audio and video, scanned documents and images | No | No | Yes |
20
+ | Ingestion Packs (below) | No: the answer is an upgrade | Yes | Yes |
21
+
22
+ FAQ entries don't use up the page allowance. When an Essentials client outgrows
23
+ it, say so plainly and point to Support Plus or Pro at hub.awesomate.ai/billing;
24
+ never offer a pack, since Essentials cannot buy one.
19
25
 
20
26
  ## Ingestion Packs (overage)
21
27
 
@@ -31,8 +37,18 @@ One pack = **1 credit ($100)**, the user picks the dimension at purchase:
31
37
  - Insufficient credits → 402 `insufficient_credits`: top up from Billing
32
38
  first. Never loop on it.
33
39
 
34
- ## Answer overage
35
-
36
- Answers are soft-gated: warnings at 100% of allowance, throttling well past
37
- it — chat never silently debits credits. If a client consistently outgrows
38
- the allowance, that's an upgrade/pack conversation, not a workaround.
40
+ ## Answer limit
41
+
42
+ Answers are a hard stop. The allowance is the plan's monthly answers plus
43
+ every answer pack held. When it is used up, every answer route (widget, agent
44
+ chat, n8n, `ask`) returns 429 `quota_exceeded` until the 1st of next month
45
+ (UTC), and a visitor sees "This assistant has reached its limit for this
46
+ month". Answers on the client's own OpenRouter key never count.
47
+
48
+ - The owner is emailed at 80% and again when the limit is reached.
49
+ - Nothing ever debits credits on its own. The way past the limit is an answer
50
+ pack (Support Plus and above, the pack flow above, applied straight away) or
51
+ an upgrade. Essentials cannot buy packs, so for them it's the upgrade.
52
+ - If a 429 `quota_exceeded` comes back from `ask` or a test chat, say so
53
+ plainly and offer those two routes. Don't retry: nothing changes until the
54
+ month turns or the allowance rises.