@agenticmail/core 0.9.23 → 0.9.24
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.cjs +318 -23
- package/dist/index.d.cts +281 -1
- package/dist/index.d.ts +281 -1
- package/dist/index.js +309 -23
- package/dist/skills/built-in/airline-change-or-refund.json +111 -0
- package/dist/skills/built-in/book-medical-appointment.json +99 -0
- package/dist/skills/built-in/book-restaurant-reservation.json +93 -0
- package/dist/skills/built-in/cancel-subscription-graceful.json +101 -0
- package/dist/skills/built-in/court-administrative-checkin.json +99 -0
- package/dist/skills/built-in/dispute-credit-card-charge.json +105 -0
- package/dist/skills/built-in/handle-debt-collector.json +109 -0
- package/dist/skills/built-in/negotiate-bill-reduction.json +116 -0
- package/dist/skills/built-in/schedule-home-service.json +100 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1109,14 +1109,14 @@ var StalwartAdmin = class {
|
|
|
1109
1109
|
if (!isValidDomain(domain)) {
|
|
1110
1110
|
throw new Error(`Invalid domain format: "${domain}"`);
|
|
1111
1111
|
}
|
|
1112
|
-
const { readFileSync:
|
|
1113
|
-
const { homedir:
|
|
1114
|
-
const { join:
|
|
1115
|
-
const configPath =
|
|
1112
|
+
const { readFileSync: readFileSync11, writeFileSync: writeFileSync12 } = await import("fs");
|
|
1113
|
+
const { homedir: homedir14 } = await import("os");
|
|
1114
|
+
const { join: join17 } = await import("path");
|
|
1115
|
+
const configPath = join17(homedir14(), ".agenticmail", "stalwart.toml");
|
|
1116
1116
|
try {
|
|
1117
|
-
let config =
|
|
1117
|
+
let config = readFileSync11(configPath, "utf-8");
|
|
1118
1118
|
config = config.replace(/^hostname\s*=\s*"[^"]*"/m, `hostname = "${escapeTomlString(domain)}"`);
|
|
1119
|
-
|
|
1119
|
+
writeFileSync12(configPath, config);
|
|
1120
1120
|
console.log(`[Stalwart] Updated hostname to "${domain}" in stalwart.toml`);
|
|
1121
1121
|
} catch (err) {
|
|
1122
1122
|
throw new Error(`Failed to set config server.hostname=${domain}`);
|
|
@@ -1125,15 +1125,15 @@ var StalwartAdmin = class {
|
|
|
1125
1125
|
// --- DKIM ---
|
|
1126
1126
|
/** Path to the host-side stalwart.toml (mounted read-only into container) */
|
|
1127
1127
|
get configPath() {
|
|
1128
|
-
const { homedir:
|
|
1129
|
-
const { join:
|
|
1130
|
-
return
|
|
1128
|
+
const { homedir: homedir14 } = __require("os");
|
|
1129
|
+
const { join: join17 } = __require("path");
|
|
1130
|
+
return join17(homedir14(), ".agenticmail", "stalwart.toml");
|
|
1131
1131
|
}
|
|
1132
1132
|
/** Path to host-side DKIM key directory */
|
|
1133
1133
|
get dkimDir() {
|
|
1134
|
-
const { homedir:
|
|
1135
|
-
const { join:
|
|
1136
|
-
return
|
|
1134
|
+
const { homedir: homedir14 } = __require("os");
|
|
1135
|
+
const { join: join17 } = __require("path");
|
|
1136
|
+
return join17(homedir14(), ".agenticmail");
|
|
1137
1137
|
}
|
|
1138
1138
|
/**
|
|
1139
1139
|
* Create/reuse a DKIM signing key for a domain.
|
|
@@ -1234,12 +1234,12 @@ var StalwartAdmin = class {
|
|
|
1234
1234
|
* This bypasses the need for a PTR record on the sending IP.
|
|
1235
1235
|
*/
|
|
1236
1236
|
async configureOutboundRelay(config) {
|
|
1237
|
-
const { readFileSync:
|
|
1238
|
-
const { homedir:
|
|
1239
|
-
const { join:
|
|
1237
|
+
const { readFileSync: readFileSync11, writeFileSync: writeFileSync12 } = await import("fs");
|
|
1238
|
+
const { homedir: homedir14 } = await import("os");
|
|
1239
|
+
const { join: join17 } = await import("path");
|
|
1240
1240
|
const routeName = config.routeName ?? "gmail";
|
|
1241
|
-
const tomlPath =
|
|
1242
|
-
let toml =
|
|
1241
|
+
const tomlPath = join17(homedir14(), ".agenticmail", "stalwart.toml");
|
|
1242
|
+
let toml = readFileSync11(tomlPath, "utf-8");
|
|
1243
1243
|
toml = toml.replace(/\n\[queue\.route\.gmail\][\s\S]*?(?=\n\[|$)/, "");
|
|
1244
1244
|
toml = toml.replace(/\n\[queue\.strategy\][\s\S]*?(?=\n\[|$)/, "");
|
|
1245
1245
|
const safeRouteName = routeName.replace(/[^a-zA-Z0-9_-]/g, "");
|
|
@@ -1259,7 +1259,7 @@ auth.secret = "${escapeTomlString(config.password)}"
|
|
|
1259
1259
|
route = [ { if = "is_local_domain('', rcpt_domain)", then = "'local'" },
|
|
1260
1260
|
{ else = "'${safeRouteName}'" } ]
|
|
1261
1261
|
`;
|
|
1262
|
-
|
|
1262
|
+
writeFileSync12(tomlPath, toml, "utf-8");
|
|
1263
1263
|
await this.restartContainer();
|
|
1264
1264
|
}
|
|
1265
1265
|
};
|
|
@@ -7460,12 +7460,12 @@ var GatewayManager = class {
|
|
|
7460
7460
|
zone = await this.cfClient.createZone(domain);
|
|
7461
7461
|
}
|
|
7462
7462
|
const existingRecords = await this.cfClient.listDnsRecords(zone.id);
|
|
7463
|
-
const { homedir:
|
|
7464
|
-
const backupDir = join4(
|
|
7463
|
+
const { homedir: homedir14 } = await import("os");
|
|
7464
|
+
const backupDir = join4(homedir14(), ".agenticmail");
|
|
7465
7465
|
const backupPath = join4(backupDir, `dns-backup-${domain}-${Date.now()}.json`);
|
|
7466
|
-
const { writeFileSync:
|
|
7467
|
-
|
|
7468
|
-
|
|
7466
|
+
const { writeFileSync: writeFileSync12, mkdirSync: mkdirSync13 } = await import("fs");
|
|
7467
|
+
mkdirSync13(backupDir, { recursive: true });
|
|
7468
|
+
writeFileSync12(backupPath, JSON.stringify({
|
|
7469
7469
|
domain,
|
|
7470
7470
|
zoneId: zone.id,
|
|
7471
7471
|
backedUpAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -13874,6 +13874,284 @@ var AgentMemoryManager = class {
|
|
|
13874
13874
|
};
|
|
13875
13875
|
}
|
|
13876
13876
|
};
|
|
13877
|
+
|
|
13878
|
+
// src/skills/registry.ts
|
|
13879
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync12, readFileSync as readFileSync10, readdirSync as readdirSync3, statSync as statSync3, writeFileSync as writeFileSync11 } from "fs";
|
|
13880
|
+
import { join as join16, dirname as dirname3, basename as basename2 } from "path";
|
|
13881
|
+
import { fileURLToPath } from "url";
|
|
13882
|
+
import { homedir as homedir13 } from "os";
|
|
13883
|
+
function builtInDir() {
|
|
13884
|
+
const here = dirname3(fileURLToPath(import.meta.url));
|
|
13885
|
+
return join16(here, "built-in");
|
|
13886
|
+
}
|
|
13887
|
+
function userDir() {
|
|
13888
|
+
const dir2 = join16(homedir13(), ".agenticmail", "skills");
|
|
13889
|
+
try {
|
|
13890
|
+
if (!existsSync14(dir2)) mkdirSync12(dir2, { recursive: true });
|
|
13891
|
+
} catch {
|
|
13892
|
+
}
|
|
13893
|
+
return dir2;
|
|
13894
|
+
}
|
|
13895
|
+
var cache = { ts: 0, byId: null };
|
|
13896
|
+
var CACHE_TTL_MS = 5e3;
|
|
13897
|
+
function loadAllSkillsFromDisk() {
|
|
13898
|
+
const all = /* @__PURE__ */ new Map();
|
|
13899
|
+
const dirs = [builtInDir(), userDir()];
|
|
13900
|
+
for (const dir2 of dirs) {
|
|
13901
|
+
if (!existsSync14(dir2)) continue;
|
|
13902
|
+
let entries;
|
|
13903
|
+
try {
|
|
13904
|
+
entries = readdirSync3(dir2);
|
|
13905
|
+
} catch {
|
|
13906
|
+
continue;
|
|
13907
|
+
}
|
|
13908
|
+
for (const entry of entries) {
|
|
13909
|
+
if (!entry.endsWith(".json")) continue;
|
|
13910
|
+
const fullPath = join16(dir2, entry);
|
|
13911
|
+
try {
|
|
13912
|
+
const st = statSync3(fullPath);
|
|
13913
|
+
if (!st.isFile()) continue;
|
|
13914
|
+
const raw = readFileSync10(fullPath, "utf-8");
|
|
13915
|
+
const parsed = JSON.parse(raw);
|
|
13916
|
+
const errors = validateSkill(parsed);
|
|
13917
|
+
if (errors.length > 0) {
|
|
13918
|
+
console.warn(`[skills] ${entry} invalid, skipping: ${errors.map((e) => `${e.path}: ${e.message}`).join("; ")}`);
|
|
13919
|
+
continue;
|
|
13920
|
+
}
|
|
13921
|
+
all.set(parsed.id, parsed);
|
|
13922
|
+
} catch (err) {
|
|
13923
|
+
console.warn(`[skills] could not load ${fullPath}: ${err.message}`);
|
|
13924
|
+
}
|
|
13925
|
+
}
|
|
13926
|
+
}
|
|
13927
|
+
return all;
|
|
13928
|
+
}
|
|
13929
|
+
function ensureLoaded() {
|
|
13930
|
+
const now = Date.now();
|
|
13931
|
+
if (cache.byId && now - cache.ts < CACHE_TTL_MS) return cache.byId;
|
|
13932
|
+
const fresh = loadAllSkillsFromDisk();
|
|
13933
|
+
cache.byId = fresh;
|
|
13934
|
+
cache.ts = now;
|
|
13935
|
+
return fresh;
|
|
13936
|
+
}
|
|
13937
|
+
function invalidateSkillCache() {
|
|
13938
|
+
cache.byId = null;
|
|
13939
|
+
cache.ts = 0;
|
|
13940
|
+
}
|
|
13941
|
+
function validateSkill(s) {
|
|
13942
|
+
const errs = [];
|
|
13943
|
+
if (!s || typeof s !== "object" || Array.isArray(s)) {
|
|
13944
|
+
return [{ path: "$", message: "skill must be a JSON object" }];
|
|
13945
|
+
}
|
|
13946
|
+
const sk = s;
|
|
13947
|
+
const requireString = (key, minLen = 1) => {
|
|
13948
|
+
const v = sk[key];
|
|
13949
|
+
if (typeof v !== "string" || v.length < minLen) {
|
|
13950
|
+
errs.push({ path: key, message: `must be a non-empty string` });
|
|
13951
|
+
}
|
|
13952
|
+
};
|
|
13953
|
+
const requireArray = (key, minLen = 1) => {
|
|
13954
|
+
const v = sk[key];
|
|
13955
|
+
if (!Array.isArray(v) || v.length < minLen) {
|
|
13956
|
+
errs.push({ path: key, message: `must be a non-empty array` });
|
|
13957
|
+
}
|
|
13958
|
+
};
|
|
13959
|
+
requireString("id");
|
|
13960
|
+
if (typeof sk.id === "string" && !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(sk.id)) {
|
|
13961
|
+
errs.push({ path: "id", message: "must be lowercase-hyphenated slug (a-z, 0-9, -)" });
|
|
13962
|
+
}
|
|
13963
|
+
requireString("name");
|
|
13964
|
+
requireString("version");
|
|
13965
|
+
requireString("description");
|
|
13966
|
+
requireString("category");
|
|
13967
|
+
const validCategories = [
|
|
13968
|
+
"negotiation",
|
|
13969
|
+
"customer-service",
|
|
13970
|
+
"reservations",
|
|
13971
|
+
"medical-admin",
|
|
13972
|
+
"legal-admin",
|
|
13973
|
+
"finance-admin",
|
|
13974
|
+
"real-estate",
|
|
13975
|
+
"travel",
|
|
13976
|
+
"subscription",
|
|
13977
|
+
"home-services",
|
|
13978
|
+
"social",
|
|
13979
|
+
"civic",
|
|
13980
|
+
"employment",
|
|
13981
|
+
"debt-collection",
|
|
13982
|
+
"other"
|
|
13983
|
+
];
|
|
13984
|
+
if (typeof sk.category === "string" && !validCategories.includes(sk.category)) {
|
|
13985
|
+
errs.push({ path: "category", message: `unknown category "${sk.category}"; must be one of: ${validCategories.join(", ")}` });
|
|
13986
|
+
}
|
|
13987
|
+
if (!Array.isArray(sk.tags)) errs.push({ path: "tags", message: "must be an array of strings" });
|
|
13988
|
+
if (sk.disclaimer !== null && typeof sk.disclaimer !== "string") {
|
|
13989
|
+
errs.push({ path: "disclaimer", message: "must be a string or null" });
|
|
13990
|
+
}
|
|
13991
|
+
if (!sk.context || typeof sk.context !== "object") {
|
|
13992
|
+
errs.push({ path: "context", message: "must be an object" });
|
|
13993
|
+
} else {
|
|
13994
|
+
const ctx = sk.context;
|
|
13995
|
+
if (typeof ctx.when_to_use !== "string") errs.push({ path: "context.when_to_use", message: "must be a string" });
|
|
13996
|
+
if (!Array.isArray(ctx.preconditions)) errs.push({ path: "context.preconditions", message: "must be an array" });
|
|
13997
|
+
if (typeof ctx.estimated_call_duration_minutes !== "number") errs.push({ path: "context.estimated_call_duration_minutes", message: "must be a number" });
|
|
13998
|
+
}
|
|
13999
|
+
requireArray("principles", 2);
|
|
14000
|
+
if (!sk.phrases || typeof sk.phrases !== "object") errs.push({ path: "phrases", message: "must be an object of {key: phrase}" });
|
|
14001
|
+
if (!Array.isArray(sk.tactics) || sk.tactics.length === 0) {
|
|
14002
|
+
errs.push({ path: "tactics", message: "must be a non-empty array" });
|
|
14003
|
+
} else {
|
|
14004
|
+
sk.tactics.forEach((t, i) => {
|
|
14005
|
+
if (!t || typeof t !== "object") {
|
|
14006
|
+
errs.push({ path: `tactics[${i}]`, message: "must be an object" });
|
|
14007
|
+
return;
|
|
14008
|
+
}
|
|
14009
|
+
const tactic = t;
|
|
14010
|
+
if (typeof tactic.name !== "string") errs.push({ path: `tactics[${i}].name`, message: "must be a string" });
|
|
14011
|
+
if (typeof tactic.when !== "string") errs.push({ path: `tactics[${i}].when`, message: "must be a string" });
|
|
14012
|
+
if (typeof tactic.script !== "string" || tactic.script.length < 5) {
|
|
14013
|
+
errs.push({ path: `tactics[${i}].script`, message: "must be a non-empty string (>= 5 chars)" });
|
|
14014
|
+
}
|
|
14015
|
+
});
|
|
14016
|
+
}
|
|
14017
|
+
requireArray("boundaries", 1);
|
|
14018
|
+
if (!Array.isArray(sk.success_signals)) errs.push({ path: "success_signals", message: "must be an array" });
|
|
14019
|
+
if (!Array.isArray(sk.failure_signals)) errs.push({ path: "failure_signals", message: "must be an array" });
|
|
14020
|
+
if (!sk.exit_strategy || typeof sk.exit_strategy !== "object") {
|
|
14021
|
+
errs.push({ path: "exit_strategy", message: "must be an object" });
|
|
14022
|
+
} else {
|
|
14023
|
+
const xs = sk.exit_strategy;
|
|
14024
|
+
if (typeof xs.on_success !== "string") errs.push({ path: "exit_strategy.on_success", message: "must be a string" });
|
|
14025
|
+
if (typeof xs.on_failure !== "string") errs.push({ path: "exit_strategy.on_failure", message: "must be a string" });
|
|
14026
|
+
}
|
|
14027
|
+
if (!Array.isArray(sk.required_user_info)) errs.push({ path: "required_user_info", message: "must be an array" });
|
|
14028
|
+
if (typeof sk.contributed_by !== "string") errs.push({ path: "contributed_by", message: "must be a string" });
|
|
14029
|
+
return errs;
|
|
14030
|
+
}
|
|
14031
|
+
function summarize(s) {
|
|
14032
|
+
return {
|
|
14033
|
+
id: s.id,
|
|
14034
|
+
name: s.name,
|
|
14035
|
+
category: s.category,
|
|
14036
|
+
tags: s.tags,
|
|
14037
|
+
description: s.description,
|
|
14038
|
+
version: s.version,
|
|
14039
|
+
disclaimer_required: !!s.disclaimer,
|
|
14040
|
+
estimated_call_duration_minutes: s.context.estimated_call_duration_minutes
|
|
14041
|
+
};
|
|
14042
|
+
}
|
|
14043
|
+
function listSkills(opts = {}) {
|
|
14044
|
+
const all = Array.from(ensureLoaded().values());
|
|
14045
|
+
const filtered = all.filter((s) => {
|
|
14046
|
+
if (opts.category && s.category !== opts.category) return false;
|
|
14047
|
+
if (opts.tag && !s.tags.includes(opts.tag.toLowerCase())) return false;
|
|
14048
|
+
return true;
|
|
14049
|
+
});
|
|
14050
|
+
return filtered.sort((a, b) => a.name.localeCompare(b.name)).map(summarize);
|
|
14051
|
+
}
|
|
14052
|
+
function searchSkills(query, limit = 20) {
|
|
14053
|
+
const q = query.toLowerCase().trim();
|
|
14054
|
+
if (!q) return listSkills();
|
|
14055
|
+
const all = Array.from(ensureLoaded().values());
|
|
14056
|
+
const scored = [];
|
|
14057
|
+
for (const s of all) {
|
|
14058
|
+
let score = 0;
|
|
14059
|
+
if (s.id.toLowerCase().includes(q)) score += 10;
|
|
14060
|
+
if (s.name.toLowerCase().includes(q)) score += 8;
|
|
14061
|
+
if (s.tags.some((t) => t.toLowerCase().includes(q))) score += 5;
|
|
14062
|
+
if (s.category.toLowerCase().includes(q)) score += 4;
|
|
14063
|
+
if (s.description.toLowerCase().includes(q)) score += 3;
|
|
14064
|
+
if (s.principles.some((p) => p.toLowerCase().includes(q))) score += 2;
|
|
14065
|
+
if (Object.values(s.phrases).some((p) => p.toLowerCase().includes(q))) score += 2;
|
|
14066
|
+
if (s.tactics.some((t) => t.script.toLowerCase().includes(q) || t.name.toLowerCase().includes(q))) score += 2;
|
|
14067
|
+
if (score > 0) scored.push({ skill: s, score });
|
|
14068
|
+
}
|
|
14069
|
+
scored.sort((a, b) => b.score - a.score || a.skill.name.localeCompare(b.skill.name));
|
|
14070
|
+
return scored.slice(0, limit).map((x) => summarize(x.skill));
|
|
14071
|
+
}
|
|
14072
|
+
function loadSkill(id) {
|
|
14073
|
+
return ensureLoaded().get(id) ?? null;
|
|
14074
|
+
}
|
|
14075
|
+
function saveUserSkill(skill) {
|
|
14076
|
+
const errors = validateSkill(skill);
|
|
14077
|
+
if (errors.length > 0) {
|
|
14078
|
+
throw new Error(`skill validation failed: ${errors.map((e) => `${e.path}: ${e.message}`).join("; ")}`);
|
|
14079
|
+
}
|
|
14080
|
+
const dir2 = userDir();
|
|
14081
|
+
const path2 = join16(dir2, `${skill.id}.json`);
|
|
14082
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
14083
|
+
const out = {
|
|
14084
|
+
...skill,
|
|
14085
|
+
created_at: skill.created_at ?? now,
|
|
14086
|
+
updated_at: now
|
|
14087
|
+
};
|
|
14088
|
+
writeFileSync11(path2, JSON.stringify(out, null, 2), "utf-8");
|
|
14089
|
+
invalidateSkillCache();
|
|
14090
|
+
return { path: path2 };
|
|
14091
|
+
}
|
|
14092
|
+
function userSkillsDir() {
|
|
14093
|
+
return userDir();
|
|
14094
|
+
}
|
|
14095
|
+
|
|
14096
|
+
// src/skills/index.ts
|
|
14097
|
+
function renderSkillAsPrompt(skill) {
|
|
14098
|
+
const lines = [];
|
|
14099
|
+
lines.push(`=== SKILL LOADED: ${skill.name} (v${skill.version}) ===`);
|
|
14100
|
+
lines.push(`Category: ${skill.category} Tags: ${skill.tags.join(", ")}`);
|
|
14101
|
+
lines.push("");
|
|
14102
|
+
lines.push(skill.description);
|
|
14103
|
+
lines.push("");
|
|
14104
|
+
if (skill.disclaimer) {
|
|
14105
|
+
lines.push("REQUIRED DISCLAIMER (recite at start of the substantive turn):");
|
|
14106
|
+
lines.push(` "${skill.disclaimer}"`);
|
|
14107
|
+
lines.push("");
|
|
14108
|
+
}
|
|
14109
|
+
lines.push("WHEN TO USE THIS:");
|
|
14110
|
+
lines.push(` ${skill.context.when_to_use}`);
|
|
14111
|
+
if (skill.context.preconditions.length > 0) {
|
|
14112
|
+
lines.push("Preconditions:");
|
|
14113
|
+
for (const p of skill.context.preconditions) lines.push(` - ${p}`);
|
|
14114
|
+
}
|
|
14115
|
+
lines.push("");
|
|
14116
|
+
lines.push("PRINCIPLES:");
|
|
14117
|
+
for (const p of skill.principles) lines.push(` - ${p}`);
|
|
14118
|
+
lines.push("");
|
|
14119
|
+
if (Object.keys(skill.phrases).length > 0) {
|
|
14120
|
+
lines.push("PHRASES (paraphrase to match your voice; keep the structural move):");
|
|
14121
|
+
for (const [k, v] of Object.entries(skill.phrases)) lines.push(` [${k}] "${v}"`);
|
|
14122
|
+
lines.push("");
|
|
14123
|
+
}
|
|
14124
|
+
if (skill.tactics.length > 0) {
|
|
14125
|
+
lines.push("TACTICS (try in order; fall back to next on failure):");
|
|
14126
|
+
skill.tactics.forEach((t, i) => {
|
|
14127
|
+
lines.push(` ${i + 1}. ${t.name}`);
|
|
14128
|
+
lines.push(` When: ${t.when}`);
|
|
14129
|
+
lines.push(` Script: "${t.script}"`);
|
|
14130
|
+
});
|
|
14131
|
+
lines.push("");
|
|
14132
|
+
}
|
|
14133
|
+
if (skill.boundaries.length > 0) {
|
|
14134
|
+
lines.push("HARD BOUNDARIES \u2014 do not cross:");
|
|
14135
|
+
for (const b of skill.boundaries) lines.push(` - ${b}`);
|
|
14136
|
+
lines.push("");
|
|
14137
|
+
}
|
|
14138
|
+
lines.push("SUCCESS SIGNALS:");
|
|
14139
|
+
for (const s of skill.success_signals) lines.push(` - ${s}`);
|
|
14140
|
+
lines.push("");
|
|
14141
|
+
lines.push("FAILURE SIGNALS \u2014 when these appear, escalate or exit:");
|
|
14142
|
+
for (const s of skill.failure_signals) lines.push(` - ${s}`);
|
|
14143
|
+
lines.push("");
|
|
14144
|
+
lines.push("EXIT:");
|
|
14145
|
+
lines.push(` On success: ${skill.exit_strategy.on_success}`);
|
|
14146
|
+
lines.push(` On failure: ${skill.exit_strategy.on_failure}`);
|
|
14147
|
+
if (skill.exit_strategy.follow_ups && skill.exit_strategy.follow_ups.length > 0) {
|
|
14148
|
+
lines.push(" Follow-ups (after the call):");
|
|
14149
|
+
for (const f of skill.exit_strategy.follow_ups) lines.push(` - ${f}`);
|
|
14150
|
+
}
|
|
14151
|
+
lines.push("");
|
|
14152
|
+
lines.push("=== END SKILL ===");
|
|
14153
|
+
return lines.join("\n");
|
|
14154
|
+
}
|
|
13877
14155
|
export {
|
|
13878
14156
|
AGENT_ROLES,
|
|
13879
14157
|
ASK_OPERATOR_TOOL,
|
|
@@ -14018,6 +14296,7 @@ export {
|
|
|
14018
14296
|
getTelegramWebhookInfo,
|
|
14019
14297
|
hostSessionStoragePath,
|
|
14020
14298
|
inferPhoneRegion,
|
|
14299
|
+
invalidateSkillCache,
|
|
14021
14300
|
isInternalEmail,
|
|
14022
14301
|
isLoopbackMailHost,
|
|
14023
14302
|
isOperatorReplySender,
|
|
@@ -14026,7 +14305,9 @@ export {
|
|
|
14026
14305
|
isTelegramChatAllowed,
|
|
14027
14306
|
isTelegramStopCommand,
|
|
14028
14307
|
isValidPhoneNumber,
|
|
14308
|
+
listSkills,
|
|
14029
14309
|
loadHostSession,
|
|
14310
|
+
loadSkill,
|
|
14030
14311
|
mapProviderSmsStatus,
|
|
14031
14312
|
nextTelegramOffset,
|
|
14032
14313
|
normalizeAddress,
|
|
@@ -14051,6 +14332,7 @@ export {
|
|
|
14051
14332
|
redactSecret,
|
|
14052
14333
|
redactSmsConfig,
|
|
14053
14334
|
redactTelegramConfig,
|
|
14335
|
+
renderSkillAsPrompt,
|
|
14054
14336
|
requireBinary,
|
|
14055
14337
|
requireWhisperModel,
|
|
14056
14338
|
resolveConfig,
|
|
@@ -14059,8 +14341,10 @@ export {
|
|
|
14059
14341
|
sanitizeEmail,
|
|
14060
14342
|
saveConfig,
|
|
14061
14343
|
saveHostSession,
|
|
14344
|
+
saveUserSkill,
|
|
14062
14345
|
scanOutboundEmail,
|
|
14063
14346
|
scoreEmail,
|
|
14347
|
+
searchSkills,
|
|
14064
14348
|
sendTelegramMessage,
|
|
14065
14349
|
setOperatorEmail,
|
|
14066
14350
|
setTelegramWebhook,
|
|
@@ -14073,10 +14357,12 @@ export {
|
|
|
14073
14357
|
threadIdFor,
|
|
14074
14358
|
tokenize,
|
|
14075
14359
|
tryJoin,
|
|
14360
|
+
userSkillsDir,
|
|
14076
14361
|
validateApiUrl,
|
|
14077
14362
|
validatePhoneMissionPolicy,
|
|
14078
14363
|
validatePhoneMissionStart,
|
|
14079
14364
|
validatePhoneTransportProfile,
|
|
14365
|
+
validateSkill,
|
|
14080
14366
|
validateTwilioSignature,
|
|
14081
14367
|
webSearch
|
|
14082
14368
|
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "airline-change-or-refund",
|
|
3
|
+
"name": "Change or Refund an Airline Reservation",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"category": "travel",
|
|
6
|
+
"tags": ["airline", "flight", "refund", "rebook", "travel-disruption"],
|
|
7
|
+
"description": "Call an airline to change a booking, request a refund, or rebook around a cancelled / delayed flight. Surface fare rules, fee waivers, and travel credits that don't show in the app.",
|
|
8
|
+
"disclaimer": null,
|
|
9
|
+
"context": {
|
|
10
|
+
"when_to_use": "User needs to change a flight, get refunded for a cancelled flight, or rebook after a disruption. Use this when the airline's app / website doesn't expose the option the user needs, or when a fee waiver is plausible (weather, schedule change > 1 hour, illness with documentation, bereavement).",
|
|
11
|
+
"preconditions": [
|
|
12
|
+
"Confirmation number / record locator (6 letters and numbers).",
|
|
13
|
+
"Passenger's full name as shown on the ticket (matches photo ID).",
|
|
14
|
+
"Loyalty number, if any.",
|
|
15
|
+
"Original itinerary + the desired new itinerary (date / time / cities).",
|
|
16
|
+
"Reason for the change (schedule change, weather, personal — affects which waivers apply).",
|
|
17
|
+
"Whether the user wants a refund, travel credit, or a rebooked itinerary."
|
|
18
|
+
],
|
|
19
|
+
"estimated_call_duration_minutes": 30
|
|
20
|
+
},
|
|
21
|
+
"principles": [
|
|
22
|
+
"Loyalty status matters. Lead with the elite number (Platinum, 1K, etc) if the user has one — different reps, different rules.",
|
|
23
|
+
"If the AIRLINE caused the disruption (cancellation, schedule change > 1 hour, equipment swap), the rules are MUCH more in the user's favour. Lead with the disruption fact.",
|
|
24
|
+
"Travel credits and refunds are different. A 'refund' goes back to the original payment; a 'travel credit' is airline-only money with an expiry. Be explicit about which you're asking for.",
|
|
25
|
+
"Fee waivers are sometimes discretionary — a calm, polite request from a long-tenured customer gets them more often than a demanding one.",
|
|
26
|
+
"Schedule change protections (DOT rules for US flights with significant schedule changes) often entitle a refund EVEN ON A NON-REFUNDABLE FARE. Don't accept 'non-refundable' as final.",
|
|
27
|
+
"If the first rep can't help, hold for a supervisor or call back later (different rep, different mood, sometimes different answer)."
|
|
28
|
+
],
|
|
29
|
+
"phrases": {
|
|
30
|
+
"opener_with_status": "Hi, I'm calling about confirmation [PNR] for [passenger name]. I'm a [status] member — number [loyalty]. I need to [change / refund / rebook] this trip.",
|
|
31
|
+
"opener_without_status": "Hi, I'm calling about confirmation [PNR] for [passenger name]. I need to [change / refund / rebook] this trip.",
|
|
32
|
+
"lead_with_disruption": "Before we look at fees — this is because of [airline's schedule change / cancellation / equipment swap / weather waiver]. Can we look at change-fee or refund options under that policy first?",
|
|
33
|
+
"request_specific_rebook": "I'd like to move to [airline] flight [number] on [date] departing [time] — same routing. What's the difference in fare?",
|
|
34
|
+
"request_refund": "Given [reason], I'd like to refund the unused portion to the original form of payment. What's the refund amount and the processing time?",
|
|
35
|
+
"ask_credit_alternative": "If a refund isn't possible, what's the travel-credit option — value, expiry date, transferable?",
|
|
36
|
+
"ask_fee_waiver": "Is there any way to waive the change fee? I'm a long-time customer and this trip is for [brief reason].",
|
|
37
|
+
"stall_thinking": "Let me think about that for one second.",
|
|
38
|
+
"stall_research": "Hold on, I want to confirm something with the passenger before I commit.",
|
|
39
|
+
"graceful_close": "Thank you. To confirm: [new itinerary], confirmation [number], total [amount] charged or refund of [amount] to [method] arriving in [days]. I'll watch for the email."
|
|
40
|
+
},
|
|
41
|
+
"tactics": [
|
|
42
|
+
{
|
|
43
|
+
"name": "Lead with PNR + loyalty status",
|
|
44
|
+
"when": "First substantive turn.",
|
|
45
|
+
"script": "Use `opener_with_status` if the user has any elite status (even bronze). Status changes the rep's screen colour — they treat the call differently.",
|
|
46
|
+
"priority": 1
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"name": "Frame the call as a disruption claim, not a customer change",
|
|
50
|
+
"when": "The airline caused (or contributed to) the change.",
|
|
51
|
+
"script": "Use `lead_with_disruption`. Cancellations, schedule changes > 60 min (for US carriers), equipment swaps that affect seat assignments, all unlock different waivers."
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"name": "Ask for refund first, accept credit as fallback",
|
|
55
|
+
"when": "User has a clear preference for refund.",
|
|
56
|
+
"script": "Use `request_refund` first. If denied, use `ask_credit_alternative`. Credits should be accepted only if the user pre-authorised credits-OK."
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"name": "Politely ask for a fee waiver",
|
|
60
|
+
"when": "A change fee is being quoted on a personal change (not a disruption).",
|
|
61
|
+
"script": "Use `ask_fee_waiver`. Don't make it a demand — make it a question with a tone of 'help me out if you can'."
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"name": "Escalate to a supervisor",
|
|
65
|
+
"when": "Front-line rep says no on something a supervisor might say yes to (fee waiver, fare-difference reduction, specific seat reassignment).",
|
|
66
|
+
"script": "Polite: 'I appreciate you checking. Could we get a supervisor's read on this? Not because you've done anything wrong — I just want to make sure we've explored the policies before I commit.'"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"name": "Hang up and call back",
|
|
70
|
+
"when": "Call has gone in circles and the supervisor also said no.",
|
|
71
|
+
"script": "End politely, wait an hour, call back. Different rep, sometimes different answer. This is a known industry quirk; don't rage about it, just try again."
|
|
72
|
+
}
|
|
73
|
+
],
|
|
74
|
+
"boundaries": [
|
|
75
|
+
"Do NOT commit to a fare difference > $X without explicit user authorisation. Use `ask_operator` for big numbers.",
|
|
76
|
+
"Do NOT accept a travel credit instead of a refund unless the user pre-authorised credit-OK.",
|
|
77
|
+
"Do NOT agree to non-refundable rules being final on a disruption-caused change — push back politely.",
|
|
78
|
+
"Do NOT share the user's full credit card on a recorded line if avoidable; the airline usually has the card on file.",
|
|
79
|
+
"Do NOT make up reasons to claim a waiver (bereavement, illness) — if the user doesn't have actual documentation, don't use those framings."
|
|
80
|
+
],
|
|
81
|
+
"success_signals": [
|
|
82
|
+
"Rep waives a change fee.",
|
|
83
|
+
"Rep offers a refund to the original payment method.",
|
|
84
|
+
"Rep finds a flight in the user's preferred window.",
|
|
85
|
+
"Rep notes elite-status protections that exceed the base fare's rules."
|
|
86
|
+
],
|
|
87
|
+
"failure_signals": [
|
|
88
|
+
"Rep + supervisor both decline waiver requests on a disruption-caused change (try calling back).",
|
|
89
|
+
"Rep can find no flight in the user's window for any price the user pre-authorised.",
|
|
90
|
+
"Refund processing time quoted at > 60 days (probably wrong — escalate)."
|
|
91
|
+
],
|
|
92
|
+
"exit_strategy": {
|
|
93
|
+
"on_success": "Read back: new itinerary (flight numbers, dates, times, routing), confirmation number, total charged or refund amount + method + processing time. Thank the rep. Confirm email is coming.",
|
|
94
|
+
"on_failure": "Note exactly what was offered and what was refused. Thank the rep. Report back so the user can decide to call back, file a DOT complaint (US), or accept the loss.",
|
|
95
|
+
"follow_ups": [
|
|
96
|
+
"Email the user: new itinerary, confirmation number, refund / credit details, processing time.",
|
|
97
|
+
"Add the new flights to the user's calendar.",
|
|
98
|
+
"If a credit was issued, calendar the expiry date.",
|
|
99
|
+
"If a refund was promised, calendar a 14-day check that it actually arrived."
|
|
100
|
+
]
|
|
101
|
+
},
|
|
102
|
+
"required_user_info": [
|
|
103
|
+
"Confirmation number (PNR) + passenger name as on the ticket",
|
|
104
|
+
"Loyalty number + status, if any",
|
|
105
|
+
"Original itinerary + desired new itinerary",
|
|
106
|
+
"Reason for the change (disruption, personal, etc)",
|
|
107
|
+
"Outcome preference: refund / credit / rebook (with max fare-difference if rebook)",
|
|
108
|
+
"Max fare difference the user will authorise without callback"
|
|
109
|
+
],
|
|
110
|
+
"contributed_by": "AgenticMail core team"
|
|
111
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "book-medical-appointment",
|
|
3
|
+
"name": "Book a Medical / Dental Appointment",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"category": "medical-admin",
|
|
6
|
+
"tags": ["doctor", "dentist", "appointment", "scheduling", "insurance"],
|
|
7
|
+
"description": "Call a medical or dental office to book an appointment, verify they take the operator's insurance, capture the prep instructions, and confirm the time + location.",
|
|
8
|
+
"disclaimer": "I'm an AI assistant calling on behalf of [patient name]. I'm not a medical professional and cannot answer clinical questions — those will be addressed by the patient directly at the appointment.",
|
|
9
|
+
"context": {
|
|
10
|
+
"when_to_use": "User needs to book a routine appointment (cleaning, annual physical, follow-up visit, vaccination, etc). Not for symptom triage or emergencies — for those, the user should call directly or use the office's nurse line.",
|
|
11
|
+
"preconditions": [
|
|
12
|
+
"Patient's full name, date of birth, and insurance information (carrier + member ID + group number).",
|
|
13
|
+
"Reason for visit in plain language ('annual cleaning', 'follow-up on lab results', 'new-patient visit').",
|
|
14
|
+
"Date / time preferences with a flex window.",
|
|
15
|
+
"Existing patient or new patient? (Affects intake time required.)"
|
|
16
|
+
],
|
|
17
|
+
"estimated_call_duration_minutes": 10
|
|
18
|
+
},
|
|
19
|
+
"principles": [
|
|
20
|
+
"Lead with the reason for visit and the patient's status (existing vs new). Front-desk staff route the call entirely based on those two facts.",
|
|
21
|
+
"Verify insurance BEFORE confirming the appointment. 'Do you take [carrier]?' is a free question; finding out at the appointment that they don't is expensive.",
|
|
22
|
+
"Get every detail in writing if possible — confirmation email, text message, or patient portal entry. Verbal-only confirmations are forgotten by reception.",
|
|
23
|
+
"Ask for prep instructions (fasting, paperwork, ID, insurance card) — these are easy to forget and embarrassing to discover at check-in.",
|
|
24
|
+
"Do NOT discuss symptoms or clinical details. You're an AI; the patient will discuss medical specifics at the appointment."
|
|
25
|
+
],
|
|
26
|
+
"phrases": {
|
|
27
|
+
"open_disclaimer": "I'm an AI assistant calling on behalf of [patient name]. I'm not a medical professional, so I won't be able to discuss symptoms or clinical details — but I can handle scheduling.",
|
|
28
|
+
"opener_existing": "I'd like to book an appointment for [patient name] for [reason]. The date of birth on file is [DOB]. Existing patient.",
|
|
29
|
+
"opener_new": "I'd like to book a new-patient appointment for [patient name] for [reason]. They've never been seen at this practice.",
|
|
30
|
+
"verify_insurance": "Before we lock anything in — can you confirm you accept [insurance carrier], plan name [plan]? Member ID is [ID].",
|
|
31
|
+
"ask_prep": "What should the patient bring or do to prepare? Anything to bring (ID, insurance card, prior records)? Any fasting or medication restrictions?",
|
|
32
|
+
"ask_confirmation_method": "Will you send a text or email confirmation? What's the best way to confirm we're on the schedule?",
|
|
33
|
+
"request_callback_number": "And the best number to reach the patient if anything changes is [phone]. Could you note that on the file?",
|
|
34
|
+
"graceful_close_success": "Thank you so much. So we're set for [day] at [time] at [location] — and [prep summary]. Have a good day.",
|
|
35
|
+
"graceful_close_no_availability": "I understand. I'll talk to [patient name] and call back to look at later dates. Thanks for checking."
|
|
36
|
+
},
|
|
37
|
+
"tactics": [
|
|
38
|
+
{
|
|
39
|
+
"name": "Disclaimer + reason for visit + patient status",
|
|
40
|
+
"when": "First substantive turn.",
|
|
41
|
+
"script": "Lead with the disclaimer (`open_disclaimer`), then immediately give reason + existing/new status (`opener_existing` or `opener_new`). Saves the receptionist from extracting it.",
|
|
42
|
+
"priority": 1
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"name": "Verify insurance before booking",
|
|
46
|
+
"when": "Receptionist offers a time slot.",
|
|
47
|
+
"script": "Use `verify_insurance` BEFORE accepting the slot. If they don't take the insurance, ask if they're in-network with any other carriers the patient might have, then exit politely if not."
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"name": "Get prep instructions before hanging up",
|
|
51
|
+
"when": "Appointment is confirmed.",
|
|
52
|
+
"script": "Use `ask_prep`. Common items: photo ID, insurance card, list of medications, fasting (lab work), arrive 15 min early for paperwork (new patient)."
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"name": "Get the confirmation method in writing",
|
|
56
|
+
"when": "Receptionist is wrapping up.",
|
|
57
|
+
"script": "Use `ask_confirmation_method`. Some offices use a portal, some text, some don't confirm at all (in which case set a reminder yourself)."
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"name": "Read back the full appointment details",
|
|
61
|
+
"when": "Before ending the call.",
|
|
62
|
+
"script": "Patient's name + date + time + location + provider name + prep summary. Receptionists appreciate this — it catches their typos, not yours."
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
"boundaries": [
|
|
66
|
+
"Do NOT discuss the patient's symptoms, medical history, or any clinical information. You're a scheduler, not a clinician.",
|
|
67
|
+
"Do NOT share the patient's full SSN, even if asked — DOB + insurance ID is enough for routine scheduling.",
|
|
68
|
+
"Do NOT commit to an appointment without verifying insurance (unless the patient is paying cash and the office is informed).",
|
|
69
|
+
"Do NOT book without explicit slot acceptance — if multiple slots are offered, pick the user-pre-stated preferred one; if none match, ask the user via `ask_operator`."
|
|
70
|
+
],
|
|
71
|
+
"success_signals": [
|
|
72
|
+
"Office confirms insurance acceptance.",
|
|
73
|
+
"Office confirms appointment with a date, time, location, and provider name.",
|
|
74
|
+
"Office confirms a method for written confirmation."
|
|
75
|
+
],
|
|
76
|
+
"failure_signals": [
|
|
77
|
+
"Office doesn't take the insurance.",
|
|
78
|
+
"Office has no availability in the user's window.",
|
|
79
|
+
"Office requires a referral the patient doesn't have."
|
|
80
|
+
],
|
|
81
|
+
"exit_strategy": {
|
|
82
|
+
"on_success": "Read back date / time / location / provider / prep. Thank the receptionist. Set a calendar event for the patient.",
|
|
83
|
+
"on_failure": "Note the obstacle (no in-network, no availability, referral required). Thank the receptionist. Report back so the operator can pick a different provider or get the referral first.",
|
|
84
|
+
"follow_ups": [
|
|
85
|
+
"Email or message the operator: date, time, location, provider, prep list, confirmation number / portal entry.",
|
|
86
|
+
"Calendar the appointment.",
|
|
87
|
+
"Calendar a 24-hour reminder."
|
|
88
|
+
]
|
|
89
|
+
},
|
|
90
|
+
"required_user_info": [
|
|
91
|
+
"Patient's full name + date of birth",
|
|
92
|
+
"Insurance carrier, member ID, group number, plan name",
|
|
93
|
+
"Reason for visit (plain language)",
|
|
94
|
+
"Existing patient or new patient",
|
|
95
|
+
"Date / time preferences + flex window",
|
|
96
|
+
"Best callback phone number for the patient"
|
|
97
|
+
],
|
|
98
|
+
"contributed_by": "AgenticMail core team"
|
|
99
|
+
}
|