@awesomate/hosting-mcp 0.67.0 → 0.69.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
|
@@ -39861,6 +39861,7 @@ var TOOL_FEATURE = {
|
|
|
39861
39861
|
awesomate_crm_queries: "contacts",
|
|
39862
39862
|
awesomate_crm_recipes: "contacts",
|
|
39863
39863
|
awesomate_crm_email: "email",
|
|
39864
|
+
awesomate_bookings: "bookings",
|
|
39864
39865
|
awesomate_knowledge_status: "knowledge",
|
|
39865
39866
|
awesomate_knowledge_provision: "knowledge",
|
|
39866
39867
|
awesomate_knowledge_sources: "knowledge",
|
|
@@ -40110,6 +40111,227 @@ function hubPatch(config3, path, jsonBody = {}) {
|
|
|
40110
40111
|
function hubDelete(config3, path, jsonBody) {
|
|
40111
40112
|
return hubRequest(config3, "DELETE", path, jsonBody);
|
|
40112
40113
|
}
|
|
40114
|
+
function serverMessage(body, status, fallback) {
|
|
40115
|
+
const msg = body && typeof body === "object" && "message" in body && typeof body.message === "string" ? body.message : body && typeof body === "object" && "error" in body && typeof body.error === "string" ? body.error : `${fallback} (${status})`;
|
|
40116
|
+
return /[.!?]$/.test(msg) ? msg : `${msg}.`;
|
|
40117
|
+
}
|
|
40118
|
+
async function readBody(res) {
|
|
40119
|
+
const text = await res.text();
|
|
40120
|
+
try {
|
|
40121
|
+
return text ? JSON.parse(text) : null;
|
|
40122
|
+
} catch {
|
|
40123
|
+
return text;
|
|
40124
|
+
}
|
|
40125
|
+
}
|
|
40126
|
+
async function hubUploadRaw(config3, path, file) {
|
|
40127
|
+
const { openAsBlob } = await import("node:fs");
|
|
40128
|
+
const blob = typeof openAsBlob === "function" ? await openAsBlob(file.localPath, { type: file.contentType ?? "application/octet-stream" }) : new Blob([await (await import("node:fs/promises")).readFile(file.localPath)], {
|
|
40129
|
+
type: file.contentType ?? "application/octet-stream"
|
|
40130
|
+
});
|
|
40131
|
+
const controller = new AbortController();
|
|
40132
|
+
const timer = setTimeout(() => controller.abort(), 6e5);
|
|
40133
|
+
let res;
|
|
40134
|
+
try {
|
|
40135
|
+
res = await (0, import_undici.fetch)(`${config3.apiBase}${path}`, {
|
|
40136
|
+
method: "POST",
|
|
40137
|
+
headers: {
|
|
40138
|
+
Authorization: `Bearer ${config3.pat}`,
|
|
40139
|
+
"Content-Type": blob.type || "application/octet-stream",
|
|
40140
|
+
"Content-Length": String(blob.size)
|
|
40141
|
+
},
|
|
40142
|
+
body: blob,
|
|
40143
|
+
signal: controller.signal,
|
|
40144
|
+
dispatcher: proxyDispatcher
|
|
40145
|
+
});
|
|
40146
|
+
} catch (err) {
|
|
40147
|
+
const aborted2 = err instanceof Error && err.name === "AbortError";
|
|
40148
|
+
throw new HubApiError(
|
|
40149
|
+
aborted2 ? "The upload timed out after 10 minutes. Try a smaller file, or upload it in the hub at Knowledge, Files." : `Could not reach the Awesomate hub at ${config3.apiBase} to upload. This is a connectivity issue, not an auth problem.`,
|
|
40150
|
+
0,
|
|
40151
|
+
null
|
|
40152
|
+
);
|
|
40153
|
+
} finally {
|
|
40154
|
+
clearTimeout(timer);
|
|
40155
|
+
}
|
|
40156
|
+
const body = await readBody(res);
|
|
40157
|
+
if (!res.ok) throw new HubApiError(serverMessage(body, res.status, "Upload failed"), res.status, body);
|
|
40158
|
+
return body;
|
|
40159
|
+
}
|
|
40160
|
+
async function hubDownloadToFile(config3, path, localPath) {
|
|
40161
|
+
const controller = new AbortController();
|
|
40162
|
+
const timer = setTimeout(() => controller.abort(), 6e5);
|
|
40163
|
+
try {
|
|
40164
|
+
let res;
|
|
40165
|
+
try {
|
|
40166
|
+
res = await (0, import_undici.fetch)(`${config3.apiBase}${path}`, {
|
|
40167
|
+
method: "GET",
|
|
40168
|
+
headers: { Authorization: `Bearer ${config3.pat}` },
|
|
40169
|
+
signal: controller.signal,
|
|
40170
|
+
dispatcher: proxyDispatcher
|
|
40171
|
+
});
|
|
40172
|
+
} catch (err) {
|
|
40173
|
+
const aborted2 = err instanceof Error && err.name === "AbortError";
|
|
40174
|
+
throw new HubApiError(
|
|
40175
|
+
aborted2 ? "The download timed out after 10 minutes." : `Could not reach the Awesomate hub at ${config3.apiBase} to download. This is a connectivity issue, not an auth problem.`,
|
|
40176
|
+
0,
|
|
40177
|
+
null
|
|
40178
|
+
);
|
|
40179
|
+
}
|
|
40180
|
+
if (!res.ok || !res.body) {
|
|
40181
|
+
const body = await readBody(res);
|
|
40182
|
+
throw new HubApiError(serverMessage(body, res.status, "Download failed"), res.status, body);
|
|
40183
|
+
}
|
|
40184
|
+
const { createWriteStream } = await import("node:fs");
|
|
40185
|
+
const { rename, rm } = await import("node:fs/promises");
|
|
40186
|
+
const { Readable } = await import("node:stream");
|
|
40187
|
+
const { pipeline } = await import("node:stream/promises");
|
|
40188
|
+
const tmp = `${localPath}.part-${process.pid}-${Date.now()}`;
|
|
40189
|
+
let bytes = 0;
|
|
40190
|
+
const counted = Readable.fromWeb(res.body);
|
|
40191
|
+
counted.on("data", (chunk) => {
|
|
40192
|
+
bytes += chunk.length;
|
|
40193
|
+
});
|
|
40194
|
+
try {
|
|
40195
|
+
await pipeline(counted, createWriteStream(tmp, { flags: "wx" }));
|
|
40196
|
+
await rename(tmp, localPath);
|
|
40197
|
+
} catch (err) {
|
|
40198
|
+
await rm(tmp, { force: true });
|
|
40199
|
+
throw err;
|
|
40200
|
+
}
|
|
40201
|
+
return bytes;
|
|
40202
|
+
} finally {
|
|
40203
|
+
clearTimeout(timer);
|
|
40204
|
+
}
|
|
40205
|
+
}
|
|
40206
|
+
|
|
40207
|
+
// src/files.ts
|
|
40208
|
+
var FILES_ACTIONS = [
|
|
40209
|
+
"status",
|
|
40210
|
+
"list",
|
|
40211
|
+
"search",
|
|
40212
|
+
"download",
|
|
40213
|
+
"upload",
|
|
40214
|
+
"mkdir",
|
|
40215
|
+
"move",
|
|
40216
|
+
"make_public",
|
|
40217
|
+
"make_private",
|
|
40218
|
+
"delete"
|
|
40219
|
+
];
|
|
40220
|
+
var FILES_MAX_UPLOAD_BYTES = 50 * 1024 * 1024;
|
|
40221
|
+
var TOP = ["public", "private", "temp"];
|
|
40222
|
+
function filesPath(raw, what = "path") {
|
|
40223
|
+
const p = String(raw ?? "").trim().replace(/^\/+/, "").replace(/^files\//, "");
|
|
40224
|
+
const top = p.split("/")[0];
|
|
40225
|
+
if (!p || !TOP.includes(top)) {
|
|
40226
|
+
throw new Error(`${what} must start with public/, private/ or temp/ (got "${raw ?? ""}"). Paths are relative to the account's files folder.`);
|
|
40227
|
+
}
|
|
40228
|
+
return p;
|
|
40229
|
+
}
|
|
40230
|
+
function swapTopFolder(path, to) {
|
|
40231
|
+
const rest = path.split("/").slice(1).join("/");
|
|
40232
|
+
if (!rest) throw new Error(`"${path}" is a top folder. Name the file or folder inside it.`);
|
|
40233
|
+
return `${to}/${rest}`;
|
|
40234
|
+
}
|
|
40235
|
+
function uploadTarget(dest, localName) {
|
|
40236
|
+
const d = filesPath(dest, "path");
|
|
40237
|
+
if (d.endsWith("/") || TOP.includes(d)) return `${d.replace(/\/$/, "")}/${localName}`;
|
|
40238
|
+
return d;
|
|
40239
|
+
}
|
|
40240
|
+
var q = (params) => Object.entries(params).filter(([, v]) => v !== void 0 && v !== "").map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
|
|
40241
|
+
var PUBLIC_NOTE = "A file in public/ can be read by anyone who has its public_url, with no login. Files in private/ and temp/ are never served publicly.";
|
|
40242
|
+
async function filesAction(config3, args) {
|
|
40243
|
+
switch (args.action) {
|
|
40244
|
+
case "status": {
|
|
40245
|
+
const caps = await hubGet(config3, "/api/files/capabilities");
|
|
40246
|
+
if (!caps.enabled) {
|
|
40247
|
+
const why = !caps.planEligible ? "Files comes with Support Plus, Pro and Embedded. Relay that honestly (hub.awesomate.ai/billing) and let the user decide." : !caps.variantEligible ? "Files needs a one-time switch of their automation account, which the owner starts in the hub at Knowledge, Files (a short restart). Never start it for them." : `The owner has not switched on "Open your automation account's file folders" in Settings, Privacy. Only they can switch it on; never do it for them.`;
|
|
40248
|
+
return { ...caps, note: why };
|
|
40249
|
+
}
|
|
40250
|
+
const usage = await hubGet(config3, "/api/files/usage");
|
|
40251
|
+
return { ...caps, usage, note: PUBLIC_NOTE };
|
|
40252
|
+
}
|
|
40253
|
+
case "list": {
|
|
40254
|
+
const path = args.path ? filesPath(args.path) : "";
|
|
40255
|
+
return { ...await hubGet(config3, `/api/files?${q({ path })}`), note: PUBLIC_NOTE };
|
|
40256
|
+
}
|
|
40257
|
+
case "search": {
|
|
40258
|
+
const out = await hubGet(
|
|
40259
|
+
config3,
|
|
40260
|
+
`/api/files/search?${q({ q: args.q ?? "", top: args.top, exts: args.exts?.join(",") })}`
|
|
40261
|
+
);
|
|
40262
|
+
return out.truncated ? { ...out, note: "Only the first 200 matches came back. Narrow the search (a longer name, a top folder or file types) before saying something is not there." } : out;
|
|
40263
|
+
}
|
|
40264
|
+
case "download": {
|
|
40265
|
+
const path = filesPath(args.path);
|
|
40266
|
+
const { resolve: resolve3, basename, join: join4 } = await import("node:path");
|
|
40267
|
+
const { existsSync: existsSync3, statSync } = await import("node:fs");
|
|
40268
|
+
const { homedir: homedir4 } = await import("node:os");
|
|
40269
|
+
const name = basename(path);
|
|
40270
|
+
const raw = args.to ? args.to.startsWith("~") ? join4(homedir4(), args.to.slice(1)) : args.to : name;
|
|
40271
|
+
let target = resolve3(raw);
|
|
40272
|
+
if (existsSync3(target) && statSync(target).isDirectory()) target = join4(target, name);
|
|
40273
|
+
if (existsSync3(target) && !args.overwrite) {
|
|
40274
|
+
throw new Error(`${target} already exists on this computer. Ask the user, then pass overwrite: true or a different "to".`);
|
|
40275
|
+
}
|
|
40276
|
+
const bytes = await hubDownloadToFile(config3, `/api/files/stream?${q({ path, disposition: "attachment" })}`, target);
|
|
40277
|
+
return { downloaded: path, savedTo: target, bytes };
|
|
40278
|
+
}
|
|
40279
|
+
case "upload": {
|
|
40280
|
+
if (!args.localPath) throw new Error('upload needs localPath (the file on this computer) and path (where it goes, e.g. "public/images/").');
|
|
40281
|
+
const { resolve: resolve3, basename, join: join4 } = await import("node:path");
|
|
40282
|
+
const { existsSync: existsSync3, statSync } = await import("node:fs");
|
|
40283
|
+
const { homedir: homedir4 } = await import("node:os");
|
|
40284
|
+
const abs = resolve3(args.localPath.startsWith("~") ? join4(homedir4(), args.localPath.slice(1)) : args.localPath);
|
|
40285
|
+
if (!existsSync3(abs)) throw new Error(`No file at ${abs}. Prefer an absolute path: this server's working directory may differ from the user's shell.`);
|
|
40286
|
+
const st = statSync(abs);
|
|
40287
|
+
if (!st.isFile()) throw new Error(`${abs} is not a file. Upload one file per call.`);
|
|
40288
|
+
if (st.size > FILES_MAX_UPLOAD_BYTES) {
|
|
40289
|
+
throw new Error(`${basename(abs)} is ${(st.size / 1024 / 1024).toFixed(1)} MB, over the 50 MB limit for one upload. Compress or split it.`);
|
|
40290
|
+
}
|
|
40291
|
+
const target = uploadTarget(args.path ?? "", basename(abs));
|
|
40292
|
+
const out = await hubUploadRaw(
|
|
40293
|
+
config3,
|
|
40294
|
+
`/api/files?${q({ path: target, overwrite: args.overwrite ? "1" : void 0 })}`,
|
|
40295
|
+
{ localPath: abs }
|
|
40296
|
+
);
|
|
40297
|
+
return target.startsWith("public/") ? { ...out, note: PUBLIC_NOTE } : out;
|
|
40298
|
+
}
|
|
40299
|
+
case "mkdir":
|
|
40300
|
+
return hubPost(config3, "/api/files/mkdir", { path: filesPath(args.path) });
|
|
40301
|
+
case "move": {
|
|
40302
|
+
const from = filesPath(args.path, "path");
|
|
40303
|
+
const to = filesPath(args.to, "to");
|
|
40304
|
+
if (to.startsWith("public/") && !from.startsWith("public/") && !args.confirm) {
|
|
40305
|
+
throw new Error(`Moving into public/ makes the file readable by anyone with its address. Ask the user first, then call again with confirm: true.`);
|
|
40306
|
+
}
|
|
40307
|
+
return hubPost(config3, "/api/files/move", { from, to });
|
|
40308
|
+
}
|
|
40309
|
+
case "make_public": {
|
|
40310
|
+
const from = filesPath(args.path);
|
|
40311
|
+
if (from.startsWith("public/")) throw new Error(`${from} is already public.`);
|
|
40312
|
+
if (!args.confirm) {
|
|
40313
|
+
throw new Error("Making a file public means anyone with its address can read it, with no login. Ask the user first, then call again with confirm: true.");
|
|
40314
|
+
}
|
|
40315
|
+
const out = await hubPost(config3, "/api/files/move", { from, to: swapTopFolder(from, "public") });
|
|
40316
|
+
return { ...out, note: `Moved from ${from}. Any workflow or page that used the old path needs the new one.` };
|
|
40317
|
+
}
|
|
40318
|
+
case "make_private": {
|
|
40319
|
+
const from = filesPath(args.path);
|
|
40320
|
+
if (!from.startsWith("public/")) throw new Error(`${from} is not in public/, so it is already private.`);
|
|
40321
|
+
const out = await hubPost(config3, "/api/files/move", { from, to: swapTopFolder(from, "private") });
|
|
40322
|
+
return {
|
|
40323
|
+
...out,
|
|
40324
|
+
note: `Moved from ${from}. Its public address no longer serves it, but Cloudflare can keep showing the last copy for up to 4 hours, and a copy someone already downloaded is not recalled. Any workflow, page or app that used it needs changing.`
|
|
40325
|
+
};
|
|
40326
|
+
}
|
|
40327
|
+
case "delete": {
|
|
40328
|
+
const path = filesPath(args.path);
|
|
40329
|
+
if (!args.confirm) throw new Error(`Delete moves ${path} to the trash. Ask the user first, then call again with confirm: true.`);
|
|
40330
|
+
const out = await hubDelete(config3, `/api/files?${q({ path })}`);
|
|
40331
|
+
return { ...out, note: "Moved to the trash folder (trashPath), not erased, and no longer counted toward the Files allowance." };
|
|
40332
|
+
}
|
|
40333
|
+
}
|
|
40334
|
+
}
|
|
40113
40335
|
|
|
40114
40336
|
// src/skills.ts
|
|
40115
40337
|
import { cpSync, existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, rmSync, writeFileSync } from "node:fs";
|
|
@@ -40346,10 +40568,10 @@ function describeSourcePage(page) {
|
|
|
40346
40568
|
async function knowledgeSources(config3, args) {
|
|
40347
40569
|
try {
|
|
40348
40570
|
if (args.action === "list") {
|
|
40349
|
-
const
|
|
40350
|
-
if (args.cursor)
|
|
40351
|
-
if (args.limit !== void 0)
|
|
40352
|
-
const qs =
|
|
40571
|
+
const q2 = new URLSearchParams();
|
|
40572
|
+
if (args.cursor) q2.set("cursor", args.cursor);
|
|
40573
|
+
if (args.limit !== void 0) q2.set("limit", String(args.limit));
|
|
40574
|
+
const qs = q2.toString();
|
|
40353
40575
|
const page = await hubGet(config3, `/api/knowledge/sources${qs ? `?${qs}` : ""}`);
|
|
40354
40576
|
return describeSourcePage(page);
|
|
40355
40577
|
}
|
|
@@ -40358,11 +40580,11 @@ async function knowledgeSources(config3, args) {
|
|
|
40358
40580
|
return { ...r, note: "Each rule keeps a File Manager folder synced into the knowledge base: files under path_prefix (matching include_globs, if any) are ingested with the rule visibility/tags/collection, re-ingested when they change, and handled per on_file_removed when they disappear. A worker sweeps every 15 min; uploads under an auto rule sync immediately; sync_run sweeps now." };
|
|
40359
40581
|
}
|
|
40360
40582
|
if (args.action === "sync_state") {
|
|
40361
|
-
const
|
|
40362
|
-
if (args.ruleId !== void 0)
|
|
40363
|
-
if (args.status)
|
|
40364
|
-
if (args.limit !== void 0)
|
|
40365
|
-
const qs =
|
|
40583
|
+
const q2 = new URLSearchParams();
|
|
40584
|
+
if (args.ruleId !== void 0) q2.set("rule_id", String(args.ruleId));
|
|
40585
|
+
if (args.status) q2.set("status", args.status);
|
|
40586
|
+
if (args.limit !== void 0) q2.set("limit", String(args.limit));
|
|
40587
|
+
const qs = q2.toString();
|
|
40366
40588
|
return await hubGet(config3, `/api/knowledge/files-sync/state${qs ? `?${qs}` : ""}`);
|
|
40367
40589
|
}
|
|
40368
40590
|
if (args.action === "sync_rule_create") {
|
|
@@ -40389,15 +40611,15 @@ async function knowledgeSources(config3, args) {
|
|
|
40389
40611
|
return { ...r, note: 'Files ingested this run are queued on the platform; verify with awesomate_knowledge_sources {action:"jobs"} and a search for a phrase from the file, never with the run summary alone.' };
|
|
40390
40612
|
}
|
|
40391
40613
|
if (args.action === "search") {
|
|
40392
|
-
const
|
|
40393
|
-
if (args.q)
|
|
40394
|
-
if (args.kind)
|
|
40395
|
-
if (args.visibility)
|
|
40396
|
-
for (const t of args.tags ?? [])
|
|
40397
|
-
for (const c of args.collectionIds ?? [])
|
|
40398
|
-
if (args.cursor)
|
|
40399
|
-
if (args.limit !== void 0)
|
|
40400
|
-
const qs =
|
|
40614
|
+
const q2 = new URLSearchParams();
|
|
40615
|
+
if (args.q) q2.set("q", args.q);
|
|
40616
|
+
if (args.kind) q2.set("kind", args.kind);
|
|
40617
|
+
if (args.visibility) q2.set("visibility", args.visibility);
|
|
40618
|
+
for (const t of args.tags ?? []) q2.append("tag", t);
|
|
40619
|
+
for (const c of args.collectionIds ?? []) q2.append("collection", c);
|
|
40620
|
+
if (args.cursor) q2.set("cursor", args.cursor);
|
|
40621
|
+
if (args.limit !== void 0) q2.set("limit", String(args.limit));
|
|
40622
|
+
const qs = q2.toString();
|
|
40401
40623
|
const page = await hubGet(config3, `/api/knowledge/library${qs ? `?${qs}` : ""}`);
|
|
40402
40624
|
return { ...describeSourcePage(page), note_visibility: "Each source carries visibility (private|internal|public \u2014 who may retrieve it; enforced) and collections. Nothing marked public = a public chatbot answers nothing." };
|
|
40403
40625
|
}
|
|
@@ -40457,8 +40679,8 @@ async function knowledgeSources(config3, args) {
|
|
|
40457
40679
|
}
|
|
40458
40680
|
}
|
|
40459
40681
|
if (args.action === "jobs") {
|
|
40460
|
-
const
|
|
40461
|
-
return await hubGet(config3, `/api/knowledge/jobs${
|
|
40682
|
+
const q2 = args.status ? `?status=${encodeURIComponent(args.status)}` : "";
|
|
40683
|
+
return await hubGet(config3, `/api/knowledge/jobs${q2}`);
|
|
40462
40684
|
}
|
|
40463
40685
|
if (args.action === "remove") {
|
|
40464
40686
|
if (!args.sourceId) throw new Error("sourceId is required for remove");
|
|
@@ -40622,10 +40844,10 @@ async function knowledgePeople(config3, args) {
|
|
|
40622
40844
|
const probe = await hubGet(config3, "/api/knowledge/entities");
|
|
40623
40845
|
if (!probe.available) return entityLayerUnavailablePayload();
|
|
40624
40846
|
if (args.action === "list") {
|
|
40625
|
-
const
|
|
40626
|
-
if (args.status)
|
|
40627
|
-
if (args.cursor)
|
|
40628
|
-
const qs =
|
|
40847
|
+
const q2 = new URLSearchParams();
|
|
40848
|
+
if (args.status) q2.set("status", args.status);
|
|
40849
|
+
if (args.cursor) q2.set("cursor", args.cursor);
|
|
40850
|
+
const qs = q2.toString();
|
|
40629
40851
|
const page = await hubGet(config3, `/api/knowledge/people${qs ? `?${qs}` : ""}`);
|
|
40630
40852
|
return {
|
|
40631
40853
|
...page,
|
|
@@ -40636,10 +40858,10 @@ async function knowledgePeople(config3, args) {
|
|
|
40636
40858
|
};
|
|
40637
40859
|
}
|
|
40638
40860
|
if (args.action === "aliases") {
|
|
40639
|
-
const
|
|
40640
|
-
if (args.kind)
|
|
40641
|
-
if (args.cursor)
|
|
40642
|
-
const page = await hubGet(config3, `/api/knowledge/entities/aliases?${
|
|
40861
|
+
const q2 = new URLSearchParams({ status: "pending" });
|
|
40862
|
+
if (args.kind) q2.set("kind", args.kind);
|
|
40863
|
+
if (args.cursor) q2.set("cursor", args.cursor);
|
|
40864
|
+
const page = await hubGet(config3, `/api/knowledge/entities/aliases?${q2.toString()}`);
|
|
40643
40865
|
return {
|
|
40644
40866
|
...page,
|
|
40645
40867
|
note: "Each suggestion is a text name the content uses that probably refers to a known entity. Show the user the alias, the suggested match and the confidence, then use decide with the (kind, alias_norm) pair: only after they explicitly say accept, reject, or create."
|
|
@@ -40763,9 +40985,9 @@ function plainSnippet(text) {
|
|
|
40763
40985
|
}
|
|
40764
40986
|
async function knowledgeSearch(config3, args) {
|
|
40765
40987
|
try {
|
|
40766
|
-
const
|
|
40767
|
-
if (args.q)
|
|
40768
|
-
if (args.kind)
|
|
40988
|
+
const q2 = new URLSearchParams();
|
|
40989
|
+
if (args.q) q2.set("q", args.q);
|
|
40990
|
+
if (args.kind) q2.set("kind", args.kind);
|
|
40769
40991
|
for (const [name, values] of [
|
|
40770
40992
|
["topic", args.topic],
|
|
40771
40993
|
["person", args.person],
|
|
@@ -40775,12 +40997,12 @@ async function knowledgeSearch(config3, args) {
|
|
|
40775
40997
|
["year", args.year],
|
|
40776
40998
|
["doc", args.doc]
|
|
40777
40999
|
]) {
|
|
40778
|
-
for (const v of values ?? [])
|
|
41000
|
+
for (const v of values ?? []) q2.append(name, v);
|
|
40779
41001
|
}
|
|
40780
|
-
if (args.limit !== void 0)
|
|
40781
|
-
if (args.offset !== void 0)
|
|
40782
|
-
if (args.include_media)
|
|
40783
|
-
const qs =
|
|
41002
|
+
if (args.limit !== void 0) q2.set("limit", String(args.limit));
|
|
41003
|
+
if (args.offset !== void 0) q2.set("offset", String(args.offset));
|
|
41004
|
+
if (args.include_media) q2.set("include", "media");
|
|
41005
|
+
const qs = q2.toString();
|
|
40784
41006
|
const result = await hubGet(config3, `/api/knowledge/explore${qs ? `?${qs}` : ""}`);
|
|
40785
41007
|
const hits = Array.isArray(result.hits) ? result.hits.map((h) => ({
|
|
40786
41008
|
...h,
|
|
@@ -41336,6 +41558,7 @@ var TITLES = {
|
|
|
41336
41558
|
awesomate_knowledge_ask: "Ask the Knowledge Base",
|
|
41337
41559
|
awesomate_knowledge_sources: "Manage knowledge sources",
|
|
41338
41560
|
awesomate_knowledge_upload: "Upload file to Knowledge Base",
|
|
41561
|
+
awesomate_files: "Manage Files (public and private file folders)",
|
|
41339
41562
|
awesomate_knowledge_agent: "Configure knowledge agent",
|
|
41340
41563
|
awesomate_knowledge_agents: "Manage knowledge agents",
|
|
41341
41564
|
awesomate_knowledge_collections: "Manage knowledge collections",
|
|
@@ -41356,6 +41579,7 @@ var TITLES = {
|
|
|
41356
41579
|
awesomate_crm_assistant: "Set up the AI in your app's conversations",
|
|
41357
41580
|
awesomate_crm_notifications: "Email people when someone replies in your app",
|
|
41358
41581
|
awesomate_crm_hooks: "Send record changes to your n8n",
|
|
41582
|
+
awesomate_bookings: "Your booking diary and booking box",
|
|
41359
41583
|
awesomate_crm_email: "Draft an email to a contact list"
|
|
41360
41584
|
};
|
|
41361
41585
|
function annotate(name, kind) {
|
|
@@ -41534,8 +41758,8 @@ server.registerTool(
|
|
|
41534
41758
|
return textResult(await hubGet(cfg, "/api/my-n8n/machine/agents/status"));
|
|
41535
41759
|
}
|
|
41536
41760
|
if (action === "describe") {
|
|
41537
|
-
const
|
|
41538
|
-
return textResult(await hubGet(cfg, `/api/my-n8n/machine/agents/tools${
|
|
41761
|
+
const q2 = tool ? `?name=${encodeURIComponent(tool)}` : "";
|
|
41762
|
+
return textResult(await hubGet(cfg, `/api/my-n8n/machine/agents/tools${q2}`));
|
|
41539
41763
|
}
|
|
41540
41764
|
if (action === "result") {
|
|
41541
41765
|
if (!call_id) return errorResult(new Error('call_id is required for action "result"'));
|
|
@@ -41610,10 +41834,10 @@ server.registerTool(
|
|
|
41610
41834
|
if (action === "mine") return textResult(await hubGet(cfg, "/api/bookings"));
|
|
41611
41835
|
if (!session_type) return errorResult(new Error(`session_type is required for ${action}`));
|
|
41612
41836
|
if (action === "slots") {
|
|
41613
|
-
const
|
|
41614
|
-
if (from)
|
|
41615
|
-
if (to)
|
|
41616
|
-
return textResult(await hubGet(cfg, `/api/bookings/slots?${
|
|
41837
|
+
const q2 = new URLSearchParams({ session_type });
|
|
41838
|
+
if (from) q2.set("from", String(Date.parse(from)));
|
|
41839
|
+
if (to) q2.set("to", String(Date.parse(to)));
|
|
41840
|
+
return textResult(await hubGet(cfg, `/api/bookings/slots?${q2.toString()}`));
|
|
41617
41841
|
}
|
|
41618
41842
|
if (!starts_at || !host_team_member_id) return errorResult(new Error("starts_at and host_team_member_id are required for book"));
|
|
41619
41843
|
return textResult(
|
|
@@ -41664,8 +41888,8 @@ server.registerTool(
|
|
|
41664
41888
|
if (active) qs.set("active", active);
|
|
41665
41889
|
if (limit) qs.set("limit", String(limit));
|
|
41666
41890
|
if (offset) qs.set("offset", String(offset));
|
|
41667
|
-
const
|
|
41668
|
-
return textResult(await hubGet(requireConfig(), `/api/my-n8n/machine/workflows${
|
|
41891
|
+
const q2 = qs.toString();
|
|
41892
|
+
return textResult(await hubGet(requireConfig(), `/api/my-n8n/machine/workflows${q2 ? `?${q2}` : ""}`));
|
|
41669
41893
|
}
|
|
41670
41894
|
const full = await hubGet(requireConfig(), `/api/my-n8n/workflows/${encodeURIComponent(id)}`);
|
|
41671
41895
|
if (detail === "structure" && full?.workflow?.nodes) {
|
|
@@ -41696,8 +41920,8 @@ server.registerTool(
|
|
|
41696
41920
|
const cfg = requireConfig();
|
|
41697
41921
|
if (what === "datatable_rows") {
|
|
41698
41922
|
if (!datatableId) return errorResult(new Error("datatableId is required for datatable_rows"));
|
|
41699
|
-
const
|
|
41700
|
-
return textResult(await hubGet(cfg, `/api/my-n8n/machine/datatables/${encodeURIComponent(datatableId)}/rows${
|
|
41923
|
+
const q2 = limit ? `?limit=${limit}` : "";
|
|
41924
|
+
return textResult(await hubGet(cfg, `/api/my-n8n/machine/datatables/${encodeURIComponent(datatableId)}/rows${q2}`));
|
|
41701
41925
|
}
|
|
41702
41926
|
const path = {
|
|
41703
41927
|
nodes: "/api/my-n8n/machine/nodes",
|
|
@@ -41850,19 +42074,19 @@ server.registerTool(
|
|
|
41850
42074
|
)
|
|
41851
42075
|
}
|
|
41852
42076
|
},
|
|
41853
|
-
async ({ action, q, subject, message, category }) => {
|
|
42077
|
+
async ({ action, q: q2, subject, message, category }) => {
|
|
41854
42078
|
try {
|
|
41855
42079
|
const cfg = requireConfig();
|
|
41856
42080
|
if (action === "ask") {
|
|
41857
|
-
if (!
|
|
41858
|
-
return textResult(await hubGet(cfg, `/api/help/ask?q=${encodeURIComponent(
|
|
42081
|
+
if (!q2) return errorResult(new Error("ask needs q"));
|
|
42082
|
+
return textResult(await hubGet(cfg, `/api/help/ask?q=${encodeURIComponent(q2)}`));
|
|
41859
42083
|
}
|
|
41860
42084
|
if (action === "faq") {
|
|
41861
|
-
return textResult(await hubGet(cfg, `/api/help/faq${
|
|
42085
|
+
return textResult(await hubGet(cfg, `/api/help/faq${q2 ? `?q=${encodeURIComponent(q2)}` : ""}`));
|
|
41862
42086
|
}
|
|
41863
42087
|
if (action === "help_docs") {
|
|
41864
|
-
if (!
|
|
41865
|
-
return textResult(await hubGet(cfg, `/api/help/docs?q=${encodeURIComponent(
|
|
42088
|
+
if (!q2) return errorResult(new Error("help_docs needs q"));
|
|
42089
|
+
return textResult(await hubGet(cfg, `/api/help/docs?q=${encodeURIComponent(q2)}`));
|
|
41866
42090
|
}
|
|
41867
42091
|
if (action === "list_tickets") {
|
|
41868
42092
|
return textResult(await hubGet(cfg, "/api/support/tickets"));
|
|
@@ -42564,10 +42788,10 @@ server.registerTool(
|
|
|
42564
42788
|
limit: external_exports.number().int().min(1).max(200).optional().describe("list only: page size (default 50, max 200)")
|
|
42565
42789
|
}
|
|
42566
42790
|
},
|
|
42567
|
-
async ({ action, url, sitemap, since, sourceId, status, cursor, limit, q, kind, visibility, tags, collectionIds, removeCollectionIds, sourceIds, confirm, pathPrefix, includeGlobs, onFileRemoved, ruleId }) => {
|
|
42791
|
+
async ({ action, url, sitemap, since, sourceId, status, cursor, limit, q: q2, kind, visibility, tags, collectionIds, removeCollectionIds, sourceIds, confirm, pathPrefix, includeGlobs, onFileRemoved, ruleId }) => {
|
|
42568
42792
|
try {
|
|
42569
42793
|
return knowledgeResult(
|
|
42570
|
-
await knowledgeSources(requireConfig(), { action, url, sitemap, since, sourceId, status, cursor, limit, q, kind, visibility, tags, collectionIds, removeCollectionIds, sourceIds, confirm, pathPrefix, includeGlobs, onFileRemoved, ruleId })
|
|
42794
|
+
await knowledgeSources(requireConfig(), { action, url, sitemap, since, sourceId, status, cursor, limit, q: q2, kind, visibility, tags, collectionIds, removeCollectionIds, sourceIds, confirm, pathPrefix, includeGlobs, onFileRemoved, ruleId })
|
|
42571
42795
|
);
|
|
42572
42796
|
} catch (err) {
|
|
42573
42797
|
return knowledgeError(err);
|
|
@@ -42762,6 +42986,31 @@ server.registerTool(
|
|
|
42762
42986
|
}
|
|
42763
42987
|
}
|
|
42764
42988
|
);
|
|
42989
|
+
server.registerTool(
|
|
42990
|
+
"awesomate_files",
|
|
42991
|
+
{
|
|
42992
|
+
annotations: annotate("awesomate_files", DESTRUCTIVE),
|
|
42993
|
+
description: `The account's Files: the folders on their automation account (n8n) that workflows read and write, and that the hub shows at Knowledge, Files. Three top folders: public/ (every file has a public_url anyone can open with no login: images for a site or email, downloads, media a workflow made), private/ (only the account, its workflows and the hub) and temp/ (scratch space). Support Plus, Pro and Embedded; the owner must also have switched on "Open your automation account's file folders" in Settings, Privacy. Paths are relative to the files folder and start with the top folder, e.g. public/images/logo.png. action 'status': whether Files is on, why not, and storage used against the plan's limit: call this first. 'list' {path?}: one folder (no path = the top folders); files in public/ carry public_url. 'search' {q?, top?, exts?}: find files by name across the folders (first 200 matches; truncated:true means narrow it). 'download' {path, to?, overwrite?}: save a file to THIS computer (to = a local file or folder, default the working directory); never overwrites without overwrite:true. 'upload' {localPath, path, overwrite?}: send a file from this computer (max 50 MB each); path is the target file, or a folder ending in / to keep the name. Upload into public/ only when the user wants it publicly reachable. 'mkdir' {path}. 'move' {path, to}: rename or move; moving into public/ needs confirm:true. 'make_public' {path, confirm:true}: moves private/ or temp/ to the same place under public/ and returns its public_url: ASK the user first, anyone with the address can read it. 'make_private' {path}: moves it out of public/, so its public address stops serving it; Cloudflare can keep showing the last copy for up to 4 hours (also after a delete or a replace of a public file), so never promise it is gone at once. 'delete' {path, confirm:true}: moves to the trash (not erased); ask first. Moving or renaming changes the path, so tell the user any workflow, page or app using the old path or address needs the new one. Contents stream between this computer and the hub: never paste a file's contents into the conversation to move it. A 507 files_storage_full means the plan's Files allowance is used: say how much, offer to find large or old files to remove, and leave any plan change to the user.`,
|
|
42994
|
+
inputSchema: {
|
|
42995
|
+
action: external_exports.enum(FILES_ACTIONS),
|
|
42996
|
+
path: external_exports.string().max(1024).optional().describe("A path in Files, starting public/, private/ or temp/. For upload, the target (a folder ending in / keeps the local name)."),
|
|
42997
|
+
to: external_exports.string().max(4096).optional().describe("move: the new Files path. download: where to save on this computer (file or folder; ~ expands)."),
|
|
42998
|
+
localPath: external_exports.string().max(4096).optional().describe("upload: the file on this computer (~ expands; prefer an absolute path)."),
|
|
42999
|
+
q: external_exports.string().max(200).optional().describe("search: part of a file name (* and ? work as wildcards)."),
|
|
43000
|
+
top: external_exports.enum(["public", "private", "temp"]).optional().describe("search: only this top folder."),
|
|
43001
|
+
exts: external_exports.array(external_exports.string().regex(/^\.[a-z0-9]+$/i)).max(20).optional().describe('search: only these extensions, e.g. [".png", ".jpg"].'),
|
|
43002
|
+
overwrite: external_exports.boolean().optional().describe("upload: replace a file already at path. download: replace a local file."),
|
|
43003
|
+
confirm: external_exports.boolean().optional().describe("true only after the user agreed: make_public, delete, and a move into public/.")
|
|
43004
|
+
}
|
|
43005
|
+
},
|
|
43006
|
+
async (args) => {
|
|
43007
|
+
try {
|
|
43008
|
+
return textResult(await filesAction(requireConfig(), args));
|
|
43009
|
+
} catch (err) {
|
|
43010
|
+
return errorResult(err);
|
|
43011
|
+
}
|
|
43012
|
+
}
|
|
43013
|
+
);
|
|
42765
43014
|
server.registerTool(
|
|
42766
43015
|
"awesomate_knowledge_people",
|
|
42767
43016
|
{
|
|
@@ -43035,7 +43284,7 @@ server.registerTool(
|
|
|
43035
43284
|
"awesomate_crm_apps",
|
|
43036
43285
|
{
|
|
43037
43286
|
annotations: annotate("awesomate_crm_apps", MUTATING),
|
|
43038
|
-
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 and stops its server keys; voice_agent_id names the account's agent that signed-in people talk to by voice (Talk to the agent: @awesomate/sdk voiceAgent() and voiceSession() with the hub's AwesomateAgent widget; the agent greets them by name and, when the app's assistant is set up, knows the 10 most recent conversations they can see, read as them; the app's origin must also be listed under the agent's 'Where your app runs'), null takes it away. 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. SDK docs, with a tutorial that builds a customer portal: https://hub.awesomate.ai/docs/sdk/ (as one text file for you: https://hub.awesomate.ai/docs/sdk/llms-full.txt). Confirm origins and sign-up mode with the user: open sign-up lets strangers in under the default role. action 'customer_lookup' {app_id, roles?}: which roles in that app may look customers up (name, email and phone, by search or id, with @awesomate/sdk lookupCustomers) and so put a record under any customer, not only themselves. Without roles it reads the setting; roles ['staff'] switches it on for staff; [] switches it off. Never the app's default role (the one customers sign in with): that would show every customer to every other customer, and the hub refuses it. Off until the owner asks for it. SERVER KEYS (ak_...), for the app's own server or an n8n workflow, used with @awesomate/sdk createClient({ token: <key> }) or as a Bearer token on https://hub.awesomate.ai/api/sdk/v1/server: action 'keys' {app_id} lists them (never the secrets); 'create_key' {app_id, name, access: 'read'|'write', kinds?} makes one (kinds leaves it to those kinds, 'contact' to read contacts; leave it out for every kind) and returns the key ONCE: put it straight into the app server's environment (awesomate_app_set_env for an app hosted with us) and never into browser code, a repository or a reply to the user. For an n8n workflow pass n8n_credential: true instead (prefer it): the hub puts the key into a Header Auth credential 'Awesomate app key: <name>' on their n8n (Authorization: Bearer, usable only towards the hub) and returns only its id and name, so the key never passes through you. 'revoke_key' {app_id, key_id} stops it at once, and removes a credential the hub made. A key reads every attribute of an app kind, contacts only as you read them, and writes need Support Plus and above. Prefer the narrowest key: read unless the server writes, and only the kinds it uses.",
|
|
43287
|
+
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 and stops its server keys; voice_agent_id names the account's agent that signed-in people talk to by voice (Talk to the agent: @awesomate/sdk voiceAgent() and voiceSession() with the hub's AwesomateAgent widget; the agent greets them by name and, when the app's assistant is set up, knows the 10 most recent conversations they can see, read as them; the app's origin must also be listed under the agent's 'Where your app runs'), null takes it away; file_uploads 'off' (default) | 'private' | 'public' lets signed-in people upload files from the app (@awesomate/sdk files.upload, up to 10 MB each) into the account's Files under private/apps/<app_id>/ or public/apps/<app_id>/ (public gives each a public address; page and script types are refused there), counted toward the plan's Files space: with open sign-up that means anyone, so confirm with the user. 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. SDK docs, with a tutorial that builds a customer portal: https://hub.awesomate.ai/docs/sdk/ (as one text file for you: https://hub.awesomate.ai/docs/sdk/llms-full.txt). Confirm origins and sign-up mode with the user: open sign-up lets strangers in under the default role. action 'customer_lookup' {app_id, roles?}: which roles in that app may look customers up (name, email and phone, by search or id, with @awesomate/sdk lookupCustomers) and so put a record under any customer, not only themselves. Without roles it reads the setting; roles ['staff'] switches it on for staff; [] switches it off. Never the app's default role (the one customers sign in with): that would show every customer to every other customer, and the hub refuses it. Off until the owner asks for it. SERVER KEYS (ak_...), for the app's own server or an n8n workflow, used with @awesomate/sdk createClient({ token: <key> }) or as a Bearer token on https://hub.awesomate.ai/api/sdk/v1/server: action 'keys' {app_id} lists them (never the secrets); 'create_key' {app_id, name, access: 'read'|'write', kinds?} makes one (kinds leaves it to those kinds, 'contact' to read contacts; leave it out for every kind) and returns the key ONCE: put it straight into the app server's environment (awesomate_app_set_env for an app hosted with us) and never into browser code, a repository or a reply to the user. For an n8n workflow pass n8n_credential: true instead (prefer it): the hub puts the key into a Header Auth credential 'Awesomate app key: <name>' on their n8n (Authorization: Bearer, usable only towards the hub) and returns only its id and name, so the key never passes through you. 'revoke_key' {app_id, key_id} stops it at once, and removes a credential the hub made. A key reads every attribute of an app kind, contacts only as you read them, and writes need Support Plus and above. Prefer the narrowest key: read unless the server writes, and only the kinds it uses.",
|
|
43039
43288
|
inputSchema: {
|
|
43040
43289
|
action: external_exports.enum(["list", "create", "update", "keys", "create_key", "revoke_key", "customer_lookup"]),
|
|
43041
43290
|
app_id: external_exports.string().max(48).optional().describe("update, keys, create_key, revoke_key, customer_lookup: the app"),
|
|
@@ -43049,10 +43298,11 @@ server.registerTool(
|
|
|
43049
43298
|
sign_up: external_exports.enum(["invite", "open"]).optional(),
|
|
43050
43299
|
default_role: external_exports.string().max(31).optional().describe("lower case; the role open sign-up gives"),
|
|
43051
43300
|
status: external_exports.enum(["active", "disabled"]).optional().describe("update only"),
|
|
43052
|
-
voice_agent_id: external_exports.string().uuid().nullable().optional().describe("update only: the account's own agent (awesomate_knowledge_agents list) that signed-in people talk to by voice in this app; null takes voice away")
|
|
43301
|
+
voice_agent_id: external_exports.string().uuid().nullable().optional().describe("update only: the account's own agent (awesomate_knowledge_agents list) that signed-in people talk to by voice in this app; null takes voice away"),
|
|
43302
|
+
file_uploads: external_exports.enum(["off", "private", "public"]).optional().describe("update only: whether signed-in people may upload files from the app, and whether each gets a public address")
|
|
43053
43303
|
}
|
|
43054
43304
|
},
|
|
43055
|
-
async ({ action, app_id, name, allowed_origins, sign_up, default_role, status, voice_agent_id, access, kinds, key_id, roles, n8n_credential }) => {
|
|
43305
|
+
async ({ action, app_id, name, allowed_origins, sign_up, default_role, status, voice_agent_id, file_uploads, access, kinds, key_id, roles, n8n_credential }) => {
|
|
43056
43306
|
try {
|
|
43057
43307
|
const cfg = requireConfig();
|
|
43058
43308
|
if (action === "list") return textResult(await hubGet(cfg, "/api/my-crm/v1/apps"));
|
|
@@ -43078,7 +43328,8 @@ server.registerTool(
|
|
|
43078
43328
|
return textResult(await hubPatch(cfg, `/api/my-crm/v1/apps/${encodeURIComponent(app_id)}`, {
|
|
43079
43329
|
...fields,
|
|
43080
43330
|
...status ? { status } : {},
|
|
43081
|
-
...voice_agent_id === void 0 ? {} : { voice_agent_id }
|
|
43331
|
+
...voice_agent_id === void 0 ? {} : { voice_agent_id },
|
|
43332
|
+
...file_uploads ? { file_uploads } : {}
|
|
43082
43333
|
}));
|
|
43083
43334
|
} catch (err) {
|
|
43084
43335
|
return errorResult(err);
|
|
@@ -43187,6 +43438,109 @@ server.registerTool(
|
|
|
43187
43438
|
}
|
|
43188
43439
|
}
|
|
43189
43440
|
);
|
|
43441
|
+
server.registerTool(
|
|
43442
|
+
"awesomate_bookings",
|
|
43443
|
+
{
|
|
43444
|
+
annotations: annotate("awesomate_bookings", MUTATING),
|
|
43445
|
+
description: `The business's own booking diary: customers book appointments or classes on the business's website, get an email with an invite and a link to change or cancel, and each booking lands in Contacts linked to the person. Read the awesomate-bookings skill first. Reading is every plan; every action that changes something needs Support Plus or above (on Essentials the hub answers upgrade_required: tell the owner they can do it in the hub under Contacts, Bookings, and do not retry). action 'setup': calendars, services, and this month's website bookings against the plan's limit. 'save_calendar' {key, name, timezone, hours, min_notice_minutes?, buffer_minutes?, max_per_day?, notify_email?, active?}: a person, room or resource with weekly hours ({"mon": [["09:00","17:00"]]}, 24-hour, in its own zone); a change names only what moves. 'save_service' {key, name, minutes, calendars, capacity?, price_text?, location?, cancel_cutoff_hours?, intake?, description?, active?}: capacity above 1 is a class several people join at one start; price is shown, never charged. 'open_times' {service, from?, to?, calendar?}. 'list' {from?, to?, status?}: bookings with each customer and their answers. 'book' {service, calendar, starts_at (from open_times), email, first_name?, last_name?, phone?, answers?, notify?, outside_hours?}. 'move' {booking_id, starts_at, notify?, outside_hours?}. 'cancel' {booking_id, reason?, notify?}. notify: false skips the customer's email; the calendar's notice email always hears. 'keys': the booking keys for the account's websites, each with the snippet to paste. 'create_key' {website}: the booking box for one site (https), returns the snippet. 'update_key' {key_id, active?, website?}. Nobody can double-book a time: the hub re-checks under a lock and answers 409 with why (not_open, slot_taken, session_full, day_full).`,
|
|
43446
|
+
inputSchema: {
|
|
43447
|
+
action: external_exports.enum(["setup", "save_calendar", "save_service", "open_times", "list", "book", "move", "cancel", "keys", "create_key", "update_key"]),
|
|
43448
|
+
key: external_exports.string().max(41).optional().describe("save_calendar, save_service: lower case, digits and underscores"),
|
|
43449
|
+
name: external_exports.string().max(80).optional(),
|
|
43450
|
+
timezone: external_exports.string().max(60).optional().describe("IANA zone, like Australia/Sydney"),
|
|
43451
|
+
hours: external_exports.record(external_exports.string(), external_exports.array(external_exports.tuple([external_exports.string(), external_exports.string()]))).optional().describe('save_calendar: {"mon": [["09:00","17:00"]], ...}'),
|
|
43452
|
+
min_notice_minutes: external_exports.number().int().min(0).optional(),
|
|
43453
|
+
buffer_minutes: external_exports.number().int().min(0).optional(),
|
|
43454
|
+
max_per_day: external_exports.number().int().min(1).nullable().optional(),
|
|
43455
|
+
notify_email: external_exports.string().max(254).nullable().optional(),
|
|
43456
|
+
active: external_exports.boolean().optional(),
|
|
43457
|
+
minutes: external_exports.number().int().optional().describe("save_service: length in minutes"),
|
|
43458
|
+
capacity: external_exports.number().int().min(1).optional(),
|
|
43459
|
+
price_text: external_exports.string().max(80).optional(),
|
|
43460
|
+
location: external_exports.string().max(500).optional(),
|
|
43461
|
+
description: external_exports.string().max(2e3).optional(),
|
|
43462
|
+
cancel_cutoff_hours: external_exports.number().int().min(0).optional(),
|
|
43463
|
+
intake: external_exports.array(external_exports.object({
|
|
43464
|
+
key: external_exports.string().max(41),
|
|
43465
|
+
label: external_exports.string().max(200),
|
|
43466
|
+
type: external_exports.enum(["text", "textarea", "select", "multiselect", "url"]),
|
|
43467
|
+
required: external_exports.boolean().optional(),
|
|
43468
|
+
options: external_exports.array(external_exports.string().max(100)).max(50).optional()
|
|
43469
|
+
})).max(20).optional().describe("save_service: the questions asked when booking"),
|
|
43470
|
+
calendars: external_exports.array(external_exports.string().max(41)).max(50).optional().describe("save_service: calendar keys that offer it"),
|
|
43471
|
+
service: external_exports.string().max(41).optional(),
|
|
43472
|
+
calendar: external_exports.string().max(41).optional(),
|
|
43473
|
+
from: external_exports.string().max(40).optional(),
|
|
43474
|
+
to: external_exports.string().max(40).optional(),
|
|
43475
|
+
status: external_exports.enum(["confirmed", "cancelled", "completed", "no_show"]).optional(),
|
|
43476
|
+
starts_at: external_exports.string().max(40).optional().describe("an ISO time from open_times"),
|
|
43477
|
+
email: external_exports.string().max(254).optional(),
|
|
43478
|
+
first_name: external_exports.string().max(80).optional(),
|
|
43479
|
+
last_name: external_exports.string().max(80).optional(),
|
|
43480
|
+
phone: external_exports.string().max(40).optional(),
|
|
43481
|
+
answers: external_exports.record(external_exports.string(), external_exports.union([external_exports.string(), external_exports.array(external_exports.string())])).optional(),
|
|
43482
|
+
notify: external_exports.boolean().optional(),
|
|
43483
|
+
outside_hours: external_exports.boolean().optional(),
|
|
43484
|
+
booking_id: external_exports.string().max(40).optional(),
|
|
43485
|
+
reason: external_exports.string().max(500).optional(),
|
|
43486
|
+
website: external_exports.string().max(300).optional().describe("create_key, update_key: the site address, like https://example.com.au"),
|
|
43487
|
+
key_id: external_exports.number().int().optional().describe("update_key: from keys")
|
|
43488
|
+
}
|
|
43489
|
+
},
|
|
43490
|
+
async (a) => {
|
|
43491
|
+
try {
|
|
43492
|
+
const cfg = requireConfig();
|
|
43493
|
+
const base = "/api/my-crm/v1/bookings";
|
|
43494
|
+
const pick2 = (...ks) => Object.fromEntries(ks.map((k) => [k, a[k]]).filter(([, v]) => v !== void 0));
|
|
43495
|
+
const qs = (o) => Object.entries(o).filter(([, v]) => v !== void 0).map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`).join("&");
|
|
43496
|
+
const origin = (w) => new URL(w).origin;
|
|
43497
|
+
switch (a.action) {
|
|
43498
|
+
case "setup":
|
|
43499
|
+
return textResult(await hubGet(cfg, `${base}/setup`));
|
|
43500
|
+
case "save_calendar":
|
|
43501
|
+
if (!a.key) return errorResult(new Error("save_calendar needs key."));
|
|
43502
|
+
return textResult(await hubPut(
|
|
43503
|
+
cfg,
|
|
43504
|
+
`${base}/calendars/${encodeURIComponent(a.key)}`,
|
|
43505
|
+
pick2("name", "timezone", "hours", "min_notice_minutes", "buffer_minutes", "max_per_day", "notify_email", "active")
|
|
43506
|
+
));
|
|
43507
|
+
case "save_service":
|
|
43508
|
+
if (!a.key) return errorResult(new Error("save_service needs key."));
|
|
43509
|
+
return textResult(await hubPut(
|
|
43510
|
+
cfg,
|
|
43511
|
+
`${base}/services/${encodeURIComponent(a.key)}`,
|
|
43512
|
+
pick2("name", "minutes", "capacity", "price_text", "location", "description", "cancel_cutoff_hours", "intake", "calendars", "active")
|
|
43513
|
+
));
|
|
43514
|
+
case "open_times":
|
|
43515
|
+
if (!a.service) return errorResult(new Error("open_times needs service."));
|
|
43516
|
+
return textResult(await hubGet(cfg, `${base}/open-times?${qs({ service: a.service, from: a.from, to: a.to, calendar: a.calendar })}`));
|
|
43517
|
+
case "list":
|
|
43518
|
+
return textResult(await hubGet(cfg, `${base}/?${qs({ from: a.from, to: a.to, status: a.status })}`));
|
|
43519
|
+
case "book":
|
|
43520
|
+
return textResult(await hubPost(cfg, base, pick2("service", "calendar", "starts_at", "email", "first_name", "last_name", "phone", "answers", "notify", "outside_hours")));
|
|
43521
|
+
case "move":
|
|
43522
|
+
if (!a.booking_id) return errorResult(new Error("move needs booking_id, from list."));
|
|
43523
|
+
return textResult(await hubPost(cfg, `${base}/${encodeURIComponent(a.booking_id)}/move`, pick2("starts_at", "notify", "outside_hours")));
|
|
43524
|
+
case "cancel":
|
|
43525
|
+
if (!a.booking_id) return errorResult(new Error("cancel needs booking_id, from list."));
|
|
43526
|
+
return textResult(await hubPost(cfg, `${base}/${encodeURIComponent(a.booking_id)}/cancel`, pick2("reason", "notify")));
|
|
43527
|
+
case "keys":
|
|
43528
|
+
return textResult(await hubGet(cfg, `${base}/keys`));
|
|
43529
|
+
case "create_key":
|
|
43530
|
+
if (!a.website) return errorResult(new Error("create_key needs website: the site's address, like https://example.com.au."));
|
|
43531
|
+
return textResult(await hubPost(cfg, `${base}/keys`, { name: new URL(a.website).hostname, allowed_origins: [origin(a.website)] }));
|
|
43532
|
+
case "update_key":
|
|
43533
|
+
if (a.key_id === void 0) return errorResult(new Error("update_key needs key_id, from keys."));
|
|
43534
|
+
return textResult(await hubPatch(cfg, `${base}/keys/${a.key_id}`, {
|
|
43535
|
+
...a.active === void 0 ? {} : { active: a.active },
|
|
43536
|
+
...a.website ? { allowed_origins: [origin(a.website)] } : {}
|
|
43537
|
+
}));
|
|
43538
|
+
}
|
|
43539
|
+
} catch (err) {
|
|
43540
|
+
return errorResult(err);
|
|
43541
|
+
}
|
|
43542
|
+
}
|
|
43543
|
+
);
|
|
43190
43544
|
server.registerTool(
|
|
43191
43545
|
"awesomate_crm_notifications",
|
|
43192
43546
|
{
|
|
@@ -43658,8 +44012,8 @@ server.registerTool(
|
|
|
43658
44012
|
async ({ domain, path }) => {
|
|
43659
44013
|
try {
|
|
43660
44014
|
const dom = encodeURIComponent(domain.toLowerCase());
|
|
43661
|
-
const
|
|
43662
|
-
return textResult(await hubGet(requireConfig(), `/api/client-hosting/sites/${dom}/audit${
|
|
44015
|
+
const q2 = path && path !== "/" ? `?path=${encodeURIComponent(path)}` : "";
|
|
44016
|
+
return textResult(await hubGet(requireConfig(), `/api/client-hosting/sites/${dom}/audit${q2}`));
|
|
43663
44017
|
} catch (err) {
|
|
43664
44018
|
return errorResult(err);
|
|
43665
44019
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@awesomate/hosting-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.69.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",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"scripts": {
|
|
20
20
|
"build": "esbuild src/index.ts --bundle --platform=node --target=node18 --format=esm --outfile=dist/index.js --banner:js='#!/usr/bin/env node\nimport { createRequire as __awmCreateRequire } from \"node:module\"; const require = __awmCreateRequire(import.meta.url);' --external:node:*",
|
|
21
21
|
"typecheck": "tsc --noEmit",
|
|
22
|
-
"test": "npm run build && esbuild src/config.ts --bundle --platform=node --target=node18 --format=esm --outfile=test/.build/config.mjs --external:node:* && esbuild src/knowledge.ts --bundle --platform=node --target=node18 --format=esm --outfile=test/.build/knowledge.mjs --banner:js='import { createRequire as __awmCreateRequire } from \"node:module\"; const require = __awmCreateRequire(import.meta.url);' --external:node:* && esbuild src/skills.ts --bundle --platform=node --target=node18 --format=esm --outfile=test/.build/skills.mjs --external:node:* && esbuild src/features.ts --bundle --platform=node --target=node18 --format=esm --outfile=test/.build/features.mjs --external:node:* && esbuild src/http.ts --bundle --platform=node --target=node18 --format=esm --outfile=test/.build/http.mjs --banner:js='import { createRequire as __awmCreateRequire } from \"node:module\"; const require = __awmCreateRequire(import.meta.url);' --external:node:* && node --test test/*.test.mjs",
|
|
22
|
+
"test": "npm run build && esbuild src/config.ts --bundle --platform=node --target=node18 --format=esm --outfile=test/.build/config.mjs --external:node:* && esbuild src/knowledge.ts --bundle --platform=node --target=node18 --format=esm --outfile=test/.build/knowledge.mjs --banner:js='import { createRequire as __awmCreateRequire } from \"node:module\"; const require = __awmCreateRequire(import.meta.url);' --external:node:* && esbuild src/skills.ts --bundle --platform=node --target=node18 --format=esm --outfile=test/.build/skills.mjs --external:node:* && esbuild src/features.ts --bundle --platform=node --target=node18 --format=esm --outfile=test/.build/features.mjs --external:node:* && esbuild src/http.ts --bundle --platform=node --target=node18 --format=esm --outfile=test/.build/http.mjs --banner:js='import { createRequire as __awmCreateRequire } from \"node:module\"; const require = __awmCreateRequire(import.meta.url);' --external:node:* && esbuild src/files.ts --bundle --platform=node --target=node18 --format=esm --outfile=test/.build/files.mjs --banner:js='import { createRequire as __awmCreateRequire } from \"node:module\"; const require = __awmCreateRequire(import.meta.url);' --external:node:* && node --test test/*.test.mjs",
|
|
23
23
|
"prepublishOnly": "npm run typecheck && npm run test"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
package/skill/CHANGELOG.json
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"versions": [
|
|
3
|
+
{
|
|
4
|
+
"version": "0.69.0",
|
|
5
|
+
"highlights": [
|
|
6
|
+
"Bookings: your customers book appointments or classes on your own website, get an email with an invite and a link to change or cancel, and each booking lands in Contacts. Claude sets up your calendars, services and the booking box (awesomate_bookings), and can book, move and cancel for you"
|
|
7
|
+
]
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"version": "0.68.0",
|
|
11
|
+
"highlights": [
|
|
12
|
+
"Claude can work with your Files: the folders your workflows use on your automation account. Put an image up and get its public address, fetch a report a workflow saved, make a file public or private, and tidy up, with storage counted against your plan's Files space",
|
|
13
|
+
"Apps you build can let signed-in people attach files, such as a photo of a job, when you switch uploads on for the app"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
3
16
|
{
|
|
4
17
|
"version": "0.67.0",
|
|
5
18
|
"highlights": [
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: awesomate-bookings
|
|
3
|
+
description: Let the business's own customers book appointments or classes online, and manage those bookings. Sets up calendars (a person, room or resource with weekly hours), services (what is booked, how long, how many places, the questions asked), and the booking box for their website, then books, moves and cancels for them. Use when the user says "booking", "bookings", "appointments", "book online", "booking calendar", "scheduling", "let customers book", "Calendly", "class bookings", "who's booked tomorrow", or wants a booking page on their site. Companion to awesomate-app-builder and awesomate-hosting (the page it goes on) and awesomate-n8n (follow-ups): same connection, same token.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Awesomate Bookings: customers book themselves, bookings land in Contacts
|
|
7
|
+
|
|
8
|
+
A business's customers pick a time on the business's own website, get an email with a calendar
|
|
9
|
+
invite and a link to change or cancel, and the business gets a notice for each booking. Every
|
|
10
|
+
booking is a record in the business's own Contacts, linked to the person who made it.
|
|
11
|
+
|
|
12
|
+
Everything goes through one tool, `awesomate_bookings`. Work in plain words with the owner: ask
|
|
13
|
+
what people book, who with, and when they are open, then set it up and show them the result in a
|
|
14
|
+
sentence or two.
|
|
15
|
+
|
|
16
|
+
## What it is made of
|
|
17
|
+
|
|
18
|
+
- **Calendars**: a person, room or resource people book. Each has weekly hours in its own time
|
|
19
|
+
zone, how much notice it needs, a gap between bookings, an optional daily limit, and the email
|
|
20
|
+
that hears about every booking (with an invite for their own calendar). Set the notice email:
|
|
21
|
+
without one, customers' replies to booking emails reach nobody.
|
|
22
|
+
- **Services**: what is booked. A length in minutes, places (1 for one person at a time, more for
|
|
23
|
+
a class several people join), a price shown as text (nothing is charged), where it happens,
|
|
24
|
+
questions to ask when booking, how many hours before the start changes close online, and which
|
|
25
|
+
calendars offer it.
|
|
26
|
+
- **The booking box**: one key per website, locked to that site's address. `create_key` returns the
|
|
27
|
+
snippet to paste.
|
|
28
|
+
|
|
29
|
+
## Setting up (in this order)
|
|
30
|
+
|
|
31
|
+
1. `awesomate_bookings` action `save_calendar` with `key` (lower case, like `nicole`), `name`,
|
|
32
|
+
`timezone` (like `Australia/Sydney`), `hours` (`{"mon": [["09:00","17:00"]], ...}`, 24-hour, no
|
|
33
|
+
overlaps), and `notify_email`.
|
|
34
|
+
2. `save_service` with `key`, `name`, `minutes`, `calendars` (keys), and optionally `capacity`,
|
|
35
|
+
`price_text`, `location`, `cancel_cutoff_hours`, `intake` (questions:
|
|
36
|
+
`[{"key":"reason","label":"What brings you in?","type":"textarea","required":true}]`; types are
|
|
37
|
+
text, textarea, select, multiselect, url; select and multiselect take `options`).
|
|
38
|
+
3. `open_times` with `service` to check the times look right before telling the owner it is ready.
|
|
39
|
+
4. `create_key` with `website` (the site's address, https). Put the returned `snippet` on the page
|
|
40
|
+
the owner wants, or give it to them to paste:
|
|
41
|
+
|
|
42
|
+
```html
|
|
43
|
+
<div data-awesomate-bookings></div>
|
|
44
|
+
<script src="https://hub.awesomate.ai/widget/v1/bookings.js" data-key="bk_..." async></script>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
On the div, `data-service="<key>"` offers one service only, and `data-accent="#0f766e"` colours the
|
|
48
|
+
buttons to match the site. On a WordPress site, a Custom HTML block holds both lines. On a site you
|
|
49
|
+
build with awesomate-app-builder, put them in the page where booking belongs (a "Book now" section,
|
|
50
|
+
or a /book page) and deploy as usual. The box only works on the website named on its key: to use it
|
|
51
|
+
on another domain, `update_key` with `allowed_origins`.
|
|
52
|
+
|
|
53
|
+
Changing a calendar or service later: call the same `save_` action with only what changes.
|
|
54
|
+
|
|
55
|
+
## Running the diary
|
|
56
|
+
|
|
57
|
+
- `list` (optionally `from`, `to`, `status`): upcoming bookings with each customer's name, email,
|
|
58
|
+
phone and answers. Use it for "who's booked tomorrow".
|
|
59
|
+
- `book`: book someone in (`service`, `calendar`, `starts_at` from `open_times`, the customer's
|
|
60
|
+
`email`, `first_name`, `last_name`, `phone`, `answers`). `notify: false` when the customer should
|
|
61
|
+
not get an email (a booking taken by phone that they already know about). Staff may book outside
|
|
62
|
+
the hours with `outside_hours: true`; nobody can double-book a time.
|
|
63
|
+
- `move` (`booking_id`, `starts_at`) and `cancel` (`booking_id`, `reason`): the customer is emailed
|
|
64
|
+
unless `notify: false`, and the calendar's notice email always is.
|
|
65
|
+
|
|
66
|
+
## Follow-ups through n8n (Pro)
|
|
67
|
+
|
|
68
|
+
Each booking is a record of the `booking` kind in Contacts. On Pro, an account with one of its own
|
|
69
|
+
apps (`awesomate_crm_apps`) can add a hook with `awesomate_crm_hooks` and `kinds: ["booking"]` to
|
|
70
|
+
send each booking's created, updated and archived events to its own n8n: an SMS reminder, copying it
|
|
71
|
+
into Google Calendar with the owner's own n8n credential, a thank-you after the visit. Bookings
|
|
72
|
+
themselves cannot be written through `awesomate_crm_write`; only these actions change them.
|
|
73
|
+
|
|
74
|
+
## Plans and limits
|
|
75
|
+
|
|
76
|
+
- Every plan can set up bookings and take them on its website. Essentials takes up to 50 website
|
|
77
|
+
bookings a month; bookings the business makes itself never count.
|
|
78
|
+
- Changing bookings from Claude Code (the write actions here) needs Support Plus or above. On
|
|
79
|
+
Essentials, the tool answers `upgrade_required`: say the owner can do the same in the hub under
|
|
80
|
+
Contacts, Bookings, and do not retry.
|
|
81
|
+
- Payments and deposits are not part of bookings yet. Do not promise them; the price is shown only.
|
|
82
|
+
- Two-way sync with the owner's own Google or Outlook calendar is not available yet: the notice
|
|
83
|
+
email's invite is how a booking reaches their calendar. Do not promise automatic sync.
|
|
@@ -224,6 +224,7 @@ MCP, so say that rather than stopping.
|
|
|
224
224
|
| `awesomate_dns_check` | Live answer to "where does this domain actually point" — before and after any DNS change |
|
|
225
225
|
| `awesomate_site_uptime` | 30-day availability, incidents, downtime per domain (cached, free) |
|
|
226
226
|
| `awesomate_site_audit` | SEO + AEO build gates for one page, fetched as GPTBot — content present without JS, retrieval bots reachable, canonical, schema, structure, speed. Not plan-gated. Run it after any public-page change, and see the awesomate-seo skill for the fixes |
|
|
227
|
+
| `awesomate_files` | Files on their automation account (public/, private/, temp/): put an image or download up and hand back its `public_url`, fetch what a workflow wrote, tidy a folder. Ask before make_public, a move into public/, or delete. Support Plus+, owner's file-folders switch on (`status` says what's missing). Guide: https://hub.awesomate.ai/docs/mcp/guides/files/ |
|
|
227
228
|
| `awesomate_snapshot_site` | Before ANY change to a live site — files + DB restore point |
|
|
228
229
|
| `awesomate_list_snapshots` | What restore points a site has |
|
|
229
230
|
| `awesomate_rollback_site` | Restore a snapshot after something went wrong (confirm first) |
|
|
@@ -178,6 +178,17 @@ https://hub.awesomate.ai/docs/sdk/guides/customer-actions/
|
|
|
178
178
|
`agentId: await app.voiceAgent()` and `session: () => app.voiceSession()`. Guide:
|
|
179
179
|
https://hub.awesomate.ai/docs/sdk/guides/voice/
|
|
180
180
|
|
|
181
|
+
## 6b2. Optional: file uploads
|
|
182
|
+
|
|
183
|
+
People signed in to the app can attach files (a photo of a job, a signed form) when the owner
|
|
184
|
+
switches it on: `awesomate_crm_apps {action:'update', app_id, file_uploads:'private'}` ('public'
|
|
185
|
+
also gives each file a public address; web pages and scripts are refused there). Off by default;
|
|
186
|
+
with open sign-up anyone could upload, so ask the owner. Give the kind a `file` attribute
|
|
187
|
+
(`awesomate_crm_kinds {action:'add_attribute'}`), then the page does
|
|
188
|
+
`const f = await app.files.upload(file)` (10 MB each) and stores `f.handle` in that attribute;
|
|
189
|
+
`app.files.download(handle)` gives the file back to anyone in the app who can read the record.
|
|
190
|
+
Uploads count toward the account's Files space. Guide: https://hub.awesomate.ai/docs/sdk/guides/files/
|
|
191
|
+
|
|
181
192
|
## 6c. Optional: reply emails
|
|
182
193
|
|
|
183
194
|
`awesomate_crm_notifications {action:'set', app_id, notifications: {enabled: true, thread_link: 'job'}}`
|