aicrm-mcp 0.3.0 → 0.4.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/cli.js +448 -143
- package/package.json +1 -1
- package/skill/aicrm/SKILL.md +15 -3
package/dist/cli.js
CHANGED
|
@@ -169,6 +169,9 @@ var FUNNEL_TEMPLATES = {
|
|
|
169
169
|
]
|
|
170
170
|
}
|
|
171
171
|
};
|
|
172
|
+
var DEFAULT_STAGE_LIMIT_DAYS = 14;
|
|
173
|
+
var DONE_EVENT_TYPES = ["note", "updated", "stage_changed", "status_changed"];
|
|
174
|
+
var ACTIVITY_EVENT_TYPES = [...DONE_EVENT_TYPES, "created", "handed_off"];
|
|
172
175
|
var CUSTOM_FIELD_TYPES = ["text", "number", "money", "date", "select", "boolean", "url"];
|
|
173
176
|
var CUSTOM_FIELD_TYPE_LABEL = {
|
|
174
177
|
text: "Texto",
|
|
@@ -338,7 +341,9 @@ var importTemplateUpdateSchema = z.object({ name: z.string().trim().min(1).max(8
|
|
|
338
341
|
var stageInputSchema = z.object({
|
|
339
342
|
id: id.optional(),
|
|
340
343
|
name: z.string().trim().min(1).max(60),
|
|
341
|
-
probability: z.number().int().min(0).max(100)
|
|
344
|
+
probability: z.number().int().min(0).max(100),
|
|
345
|
+
/** "Prazo da etapa" in business days; null = default. Omitted on update = keep the current one. */
|
|
346
|
+
limitDays: z.number().int().min(1).max(365).nullish()
|
|
342
347
|
});
|
|
343
348
|
var funnelSchema = z.object({
|
|
344
349
|
workspaceId: id,
|
|
@@ -347,7 +352,8 @@ var funnelSchema = z.object({
|
|
|
347
352
|
goalStageIndex: z.number().int().min(0).optional(),
|
|
348
353
|
template: z.enum(Object.keys(FUNNEL_TEMPLATES)).optional(),
|
|
349
354
|
stages: z.array(stageInputSchema).min(1).max(20).optional(),
|
|
350
|
-
nextFunnelId: id.nullish()
|
|
355
|
+
nextFunnelId: id.nullish(),
|
|
356
|
+
autoAssign: z.boolean().optional()
|
|
351
357
|
});
|
|
352
358
|
var funnelUpdateSchema = z.object({
|
|
353
359
|
name: z.string().trim().min(1).max(80).optional(),
|
|
@@ -355,6 +361,7 @@ var funnelUpdateSchema = z.object({
|
|
|
355
361
|
goalStageId: id.nullable().optional(),
|
|
356
362
|
stages: z.array(stageInputSchema).min(1).max(20).optional(),
|
|
357
363
|
nextFunnelId: id.nullable().optional(),
|
|
364
|
+
autoAssign: z.boolean().optional(),
|
|
358
365
|
archived: z.boolean().optional()
|
|
359
366
|
});
|
|
360
367
|
var optionalText = (max) => z.string().trim().max(max).nullish().transform((v) => v ? v : null);
|
|
@@ -411,7 +418,7 @@ var bulkLeadSchema = z.object({
|
|
|
411
418
|
var importSchema = z.object({
|
|
412
419
|
funnelId: id,
|
|
413
420
|
stageId: id,
|
|
414
|
-
ownerMode: z.enum(["fixed", "column", "round_robin"]),
|
|
421
|
+
ownerMode: z.enum(["fixed", "column", "round_robin", "auto"]),
|
|
415
422
|
ownerId: id.optional(),
|
|
416
423
|
duplicate: z.enum(["skip", "update"]).default("skip"),
|
|
417
424
|
rows: z.array(
|
|
@@ -464,6 +471,10 @@ var tvLinkSchema = z.object({
|
|
|
464
471
|
funnelId: id,
|
|
465
472
|
name: z.string().trim().min(1).max(80)
|
|
466
473
|
});
|
|
474
|
+
var distributionPauseSchema = z.object({
|
|
475
|
+
funnelId: id,
|
|
476
|
+
userId: id
|
|
477
|
+
});
|
|
467
478
|
|
|
468
479
|
// ../shared/src/format.ts
|
|
469
480
|
var brl = new Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL", maximumFractionDigits: 0 });
|
|
@@ -621,6 +632,73 @@ function guessFieldType(samples) {
|
|
|
621
632
|
return { type: "text", options: [] };
|
|
622
633
|
}
|
|
623
634
|
|
|
635
|
+
// ../shared/src/stats/calendar.ts
|
|
636
|
+
function toDateStr(d) {
|
|
637
|
+
return d.toISOString().slice(0, 10);
|
|
638
|
+
}
|
|
639
|
+
function parseDate(s) {
|
|
640
|
+
return /* @__PURE__ */ new Date(`${s}T00:00:00Z`);
|
|
641
|
+
}
|
|
642
|
+
function addDays(s, n) {
|
|
643
|
+
const d = parseDate(s);
|
|
644
|
+
d.setUTCDate(d.getUTCDate() + n);
|
|
645
|
+
return toDateStr(d);
|
|
646
|
+
}
|
|
647
|
+
function localDate(instant, timeZone) {
|
|
648
|
+
const d = instant instanceof Date ? instant : new Date(instant);
|
|
649
|
+
return new Intl.DateTimeFormat("en-CA", { timeZone, year: "numeric", month: "2-digit", day: "2-digit" }).format(d);
|
|
650
|
+
}
|
|
651
|
+
function easterSunday(year) {
|
|
652
|
+
const a = year % 19;
|
|
653
|
+
const b = Math.floor(year / 100);
|
|
654
|
+
const c = year % 100;
|
|
655
|
+
const d = Math.floor(b / 4);
|
|
656
|
+
const e = b % 4;
|
|
657
|
+
const f = Math.floor((b + 8) / 25);
|
|
658
|
+
const g = Math.floor((b - f + 1) / 3);
|
|
659
|
+
const h = (19 * a + b - d - g + 15) % 30;
|
|
660
|
+
const i = Math.floor(c / 4);
|
|
661
|
+
const k = c % 4;
|
|
662
|
+
const l = (32 + 2 * e + 2 * i - h - k) % 7;
|
|
663
|
+
const m = Math.floor((a + 11 * h + 22 * l) / 451);
|
|
664
|
+
const month = Math.floor((h + l - 7 * m + 114) / 31);
|
|
665
|
+
const day = (h + l - 7 * m + 114) % 31 + 1;
|
|
666
|
+
return `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
|
667
|
+
}
|
|
668
|
+
var holidayCache = /* @__PURE__ */ new Map();
|
|
669
|
+
function brazilianHolidays(year) {
|
|
670
|
+
const cached = holidayCache.get(year);
|
|
671
|
+
if (cached) return cached;
|
|
672
|
+
const easter = easterSunday(year);
|
|
673
|
+
const fixed = ["01-01", "04-21", "05-01", "09-07", "10-12", "11-02", "11-15", "11-20", "12-25"].map(
|
|
674
|
+
(md) => `${year}-${md}`
|
|
675
|
+
);
|
|
676
|
+
const set = /* @__PURE__ */ new Set([
|
|
677
|
+
...fixed,
|
|
678
|
+
addDays(easter, -48),
|
|
679
|
+
// Carnaval (segunda)
|
|
680
|
+
addDays(easter, -47),
|
|
681
|
+
// Carnaval (terça)
|
|
682
|
+
addDays(easter, -2),
|
|
683
|
+
// Sexta-feira Santa
|
|
684
|
+
addDays(easter, 60)
|
|
685
|
+
// Corpus Christi
|
|
686
|
+
]);
|
|
687
|
+
holidayCache.set(year, set);
|
|
688
|
+
return set;
|
|
689
|
+
}
|
|
690
|
+
function isBusinessDay(s) {
|
|
691
|
+
const dow = parseDate(s).getUTCDay();
|
|
692
|
+
if (dow === 0 || dow === 6) return false;
|
|
693
|
+
return !brazilianHolidays(Number(s.slice(0, 4))).has(s);
|
|
694
|
+
}
|
|
695
|
+
function businessDaysBetween(start, end) {
|
|
696
|
+
if (start > end) return 0;
|
|
697
|
+
let n = 0;
|
|
698
|
+
for (let d = start; d <= end; d = addDays(d, 1)) if (isBusinessDay(d)) n++;
|
|
699
|
+
return n;
|
|
700
|
+
}
|
|
701
|
+
|
|
624
702
|
// ../shared/src/stats/forecast.ts
|
|
625
703
|
var FORECAST_STATUS_LABEL = {
|
|
626
704
|
achieved: "Meta batida",
|
|
@@ -629,11 +707,36 @@ var FORECAST_STATUS_LABEL = {
|
|
|
629
707
|
off_track: "Improv\xE1vel"
|
|
630
708
|
};
|
|
631
709
|
|
|
710
|
+
// ../shared/src/stats/stage-age.ts
|
|
711
|
+
function stagesWithoutLimit(funnels) {
|
|
712
|
+
const out = /* @__PURE__ */ new Set();
|
|
713
|
+
for (const f of funnels) {
|
|
714
|
+
if (f.goalKpi !== "stage_reached" || !f.goalStageId) continue;
|
|
715
|
+
const goal = f.stages.find((s) => s.id === f.goalStageId);
|
|
716
|
+
if (!goal) continue;
|
|
717
|
+
for (const s of f.stages) if (s.position >= goal.position) out.add(s.id);
|
|
718
|
+
}
|
|
719
|
+
return out;
|
|
720
|
+
}
|
|
721
|
+
function businessDaysSince(enteredOn, today) {
|
|
722
|
+
return businessDaysBetween(addDays(enteredOn, 1), today);
|
|
723
|
+
}
|
|
724
|
+
function stageLevel(days, limit) {
|
|
725
|
+
if (days > 2 * limit) return "critical";
|
|
726
|
+
if (days > limit) return "late";
|
|
727
|
+
return "ok";
|
|
728
|
+
}
|
|
729
|
+
function stageAge(stageEnteredAt, limitDays, today, timeZone) {
|
|
730
|
+
const days = businessDaysSince(localDate(stageEnteredAt, timeZone), today);
|
|
731
|
+
const limit = limitDays ?? DEFAULT_STAGE_LIMIT_DAYS;
|
|
732
|
+
return { days, limit, level: stageLevel(days, limit) };
|
|
733
|
+
}
|
|
734
|
+
|
|
632
735
|
// src/server.ts
|
|
633
736
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
634
737
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
635
738
|
import Papa from "papaparse";
|
|
636
|
-
import { z as
|
|
739
|
+
import { z as z9 } from "zod";
|
|
637
740
|
|
|
638
741
|
// src/lib.ts
|
|
639
742
|
var norm = (s) => s.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase().trim();
|
|
@@ -646,14 +749,20 @@ function resolve(items, ref, what) {
|
|
|
646
749
|
}
|
|
647
750
|
var text = (data) => ({ content: [{ type: "text", text: typeof data === "string" ? data : JSON.stringify(data, null, 1) }] });
|
|
648
751
|
async function ctx(api) {
|
|
649
|
-
const [workspaces, users] = await Promise.all([
|
|
752
|
+
const [workspaces, users, me] = await Promise.all([
|
|
753
|
+
api.get("/workspaces"),
|
|
754
|
+
api.get("/users"),
|
|
755
|
+
api.get("/auth/me")
|
|
756
|
+
]);
|
|
650
757
|
const stages = /* @__PURE__ */ new Map();
|
|
651
758
|
const funnels = /* @__PURE__ */ new Map();
|
|
652
759
|
for (const w of workspaces) for (const f of w.funnels) {
|
|
653
760
|
funnels.set(f.id, f);
|
|
654
761
|
for (const s of f.stages) stages.set(s.id, s);
|
|
655
762
|
}
|
|
656
|
-
|
|
763
|
+
const timezone = me.org.timezone;
|
|
764
|
+
const noLimit = stagesWithoutLimit([...funnels.values()]);
|
|
765
|
+
return { workspaces, users, stages, funnels, noLimit, timezone, today: localDate(/* @__PURE__ */ new Date(), timezone), userName: (id2) => users.find((u) => u.id === id2)?.name ?? null };
|
|
657
766
|
}
|
|
658
767
|
function pickWorkspace(c, ref) {
|
|
659
768
|
const ws = resolve(c.workspaces, ref, "Workspace") ?? c.workspaces[0];
|
|
@@ -703,10 +812,16 @@ function leadSummary(c, l, fields) {
|
|
|
703
812
|
value: formatMoney(l.valueCents),
|
|
704
813
|
owner: c.userName(l.ownerId),
|
|
705
814
|
next_step: l.nextStep ? `${l.nextStep}${l.nextStepAt ? ` (${l.nextStepAt})` : ""}` : null,
|
|
706
|
-
|
|
815
|
+
...stageAgeSummary(c, l),
|
|
707
816
|
...Object.keys(custom).length ? { fields: custom } : {}
|
|
708
817
|
};
|
|
709
818
|
}
|
|
819
|
+
var RISK_LEVEL_LABEL = { ok: "no prazo", late: "passou do prazo", critical: "passou do dobro do prazo" };
|
|
820
|
+
function stageAgeSummary(c, l) {
|
|
821
|
+
if (l.status !== "open" || c.noLimit.has(l.stageId)) return {};
|
|
822
|
+
const age = stageAge(l.stageEnteredAt, c.stages.get(l.stageId)?.limitDays, c.today, c.timezone);
|
|
823
|
+
return { business_days_in_stage: age.days, stage_limit_days: age.limit, stage_deadline: RISK_LEVEL_LABEL[age.level] };
|
|
824
|
+
}
|
|
710
825
|
function describeForecast(kpi, f) {
|
|
711
826
|
return {
|
|
712
827
|
status: FORECAST_STATUS_LABEL[f.status],
|
|
@@ -863,9 +978,91 @@ function registerAdminTools(server) {
|
|
|
863
978
|
);
|
|
864
979
|
}
|
|
865
980
|
|
|
866
|
-
// src/tools/
|
|
981
|
+
// src/tools/distribution.ts
|
|
867
982
|
import { z as z3 } from "zod";
|
|
868
|
-
var
|
|
983
|
+
var br = (d) => `${d.slice(8, 10)}/${d.slice(5, 7)}`;
|
|
984
|
+
var why = (b) => b.reason === "manual" ? "pausado por um admin" : `sem nenhuma a\xE7\xE3o no funil em ${b.day ? br(b.day) : "um dia \xFAtil"}`;
|
|
985
|
+
var describePlan = (funnel, p) => ({
|
|
986
|
+
funnel,
|
|
987
|
+
automatic_distribution: p.enabled ? "ligada" : "desligada (ligue com update_funnel auto_assign=true)",
|
|
988
|
+
next_leads_go_to: p.sellers.map((s) => ({
|
|
989
|
+
seller: s.name,
|
|
990
|
+
share: formatPct(s.share),
|
|
991
|
+
why: s.why,
|
|
992
|
+
received_last_7_days: s.receivedRecently
|
|
993
|
+
})),
|
|
994
|
+
not_receiving: p.blocked.map((b) => ({ seller: b.name, why: why(b) })),
|
|
995
|
+
leads_without_owner: p.unowned
|
|
996
|
+
});
|
|
997
|
+
var funnelArgs = { funnel: z3.string().optional(), workspace: z3.string().optional() };
|
|
998
|
+
function registerDistributionTools(server) {
|
|
999
|
+
server.registerTool(
|
|
1000
|
+
"get_distribution",
|
|
1001
|
+
{
|
|
1002
|
+
title: "Distribui\xE7\xE3o de leads",
|
|
1003
|
+
description: "Mostra para quem v\xE3o os pr\xF3ximos leads sem vendedor do funil, a parte de cada um e o motivo (quem fecha mais recebe mais; todos recebem um m\xEDnimo), quem n\xE3o est\xE1 recebendo (pausado por admin ou um dia \xFAtil sem nenhuma a\xE7\xE3o no funil) e quantos leads esperam vendedor. Sem funil, lista os bloqueios de todos os funis. S\xF3 admins.",
|
|
1004
|
+
inputSchema: funnelArgs,
|
|
1005
|
+
annotations: { readOnlyHint: true }
|
|
1006
|
+
},
|
|
1007
|
+
withApi(async (api, a) => {
|
|
1008
|
+
if (!a.funnel && !a.workspace) {
|
|
1009
|
+
const alerts = await api.get("/distribution/alerts");
|
|
1010
|
+
return text(
|
|
1011
|
+
alerts.length ? { not_receiving: alerts.map((x) => ({ seller: x.name, funnel: x.funnelName, why: why(x) })) } : "Todos os vendedores est\xE3o recebendo leads em todos os funis."
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
const funnel = pickFunnel(await ctx(api), a.funnel, a.workspace);
|
|
1015
|
+
return text(describePlan(funnel.name, await api.get(`/distribution?funnelId=${funnel.id}`)));
|
|
1016
|
+
})
|
|
1017
|
+
);
|
|
1018
|
+
server.registerTool(
|
|
1019
|
+
"release_seller",
|
|
1020
|
+
{
|
|
1021
|
+
title: "Liberar vendedor",
|
|
1022
|
+
description: "Volta a mandar leads novos para um vendedor que foi bloqueado (dia sem a\xE7\xE3o) ou pausado no funil. S\xF3 admins.",
|
|
1023
|
+
inputSchema: { seller: z3.string().describe("Nome ou email"), ...funnelArgs }
|
|
1024
|
+
},
|
|
1025
|
+
withApi(async (api, a) => {
|
|
1026
|
+
const c = await ctx(api);
|
|
1027
|
+
const funnel = pickFunnel(c, a.funnel, a.workspace);
|
|
1028
|
+
const user = findUser(c, a.seller);
|
|
1029
|
+
const plan = await api.post("/distribution/release", { funnelId: funnel.id, userId: user.id });
|
|
1030
|
+
return text({ released: user.name, ...describePlan(funnel.name, plan) });
|
|
1031
|
+
})
|
|
1032
|
+
);
|
|
1033
|
+
server.registerTool(
|
|
1034
|
+
"pause_seller",
|
|
1035
|
+
{
|
|
1036
|
+
title: "Pausar vendedor",
|
|
1037
|
+
description: "Para de mandar leads novos para um vendedor no funil (ex.: f\xE9rias), at\xE9 release_seller. Os leads que ele j\xE1 tem continuam com ele. S\xF3 admins. Confirme com a pessoa antes.",
|
|
1038
|
+
inputSchema: { seller: z3.string().describe("Nome ou email"), ...funnelArgs }
|
|
1039
|
+
},
|
|
1040
|
+
withApi(async (api, a) => {
|
|
1041
|
+
const c = await ctx(api);
|
|
1042
|
+
const funnel = pickFunnel(c, a.funnel, a.workspace);
|
|
1043
|
+
const user = findUser(c, a.seller);
|
|
1044
|
+
const plan = await api.post("/distribution/pause", { funnelId: funnel.id, userId: user.id });
|
|
1045
|
+
return text({ paused: user.name, ...describePlan(funnel.name, plan) });
|
|
1046
|
+
})
|
|
1047
|
+
);
|
|
1048
|
+
server.registerTool(
|
|
1049
|
+
"assign_unowned_leads",
|
|
1050
|
+
{
|
|
1051
|
+
title: "Distribuir leads sem vendedor",
|
|
1052
|
+
description: "Entrega os leads abertos sem vendedor do funil para o time, na propor\xE7\xE3o da distribui\xE7\xE3o por desempenho (get_distribution). S\xF3 admins.",
|
|
1053
|
+
inputSchema: funnelArgs
|
|
1054
|
+
},
|
|
1055
|
+
withApi(async (api, a) => {
|
|
1056
|
+
const funnel = pickFunnel(await ctx(api), a.funnel, a.workspace);
|
|
1057
|
+
const r = await api.post("/distribution/assign-unowned", { funnelId: funnel.id });
|
|
1058
|
+
return text({ assigned: r.assigned, ...describePlan(funnel.name, r.plan) });
|
|
1059
|
+
})
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// src/tools/goals.ts
|
|
1064
|
+
import { z as z4 } from "zod";
|
|
1065
|
+
var describePlan2 = (p) => ({
|
|
869
1066
|
name: p.name,
|
|
870
1067
|
kind: p.kind === "percent" ? "% sobre a receita (metas de receita)" : "R$ por unidade (metas de quantidade)",
|
|
871
1068
|
tiers: p.tiers.map((t) => `a partir de ${t.minAttainmentPct}% da meta: ${p.kind === "percent" ? `${t.rate}%` : `${formatMoney(t.rate)} por unidade`}`)
|
|
@@ -879,7 +1076,7 @@ function registerGoalTools(server) {
|
|
|
879
1076
|
inputSchema: {},
|
|
880
1077
|
annotations: { readOnlyHint: true }
|
|
881
1078
|
},
|
|
882
|
-
withApi(async (api) => text((await api.get("/commission-plans")).map(
|
|
1079
|
+
withApi(async (api) => text((await api.get("/commission-plans")).map(describePlan2)))
|
|
883
1080
|
);
|
|
884
1081
|
server.registerTool(
|
|
885
1082
|
"save_commission_plan",
|
|
@@ -887,9 +1084,9 @@ function registerGoalTools(server) {
|
|
|
887
1084
|
title: "Criar ou alterar plano de comiss\xE3o",
|
|
888
1085
|
description: "Cria um plano de comiss\xE3o ou, se j\xE1 existir um com o mesmo nome, substitui as faixas (s\xF3 admins). Cada faixa vale a partir de um % de atingimento da meta e a taxa da faixa alcan\xE7ada vale para tudo (acelerador). kind percent: rate em % da receita (para funis com meta de receita). kind per_unit: rate em reais por unidade (para metas de quantidade).",
|
|
889
1086
|
inputSchema: {
|
|
890
|
-
name:
|
|
891
|
-
kind:
|
|
892
|
-
tiers:
|
|
1087
|
+
name: z4.string().min(1).max(80),
|
|
1088
|
+
kind: z4.enum(["percent", "per_unit"]),
|
|
1089
|
+
tiers: z4.array(z4.object({ from_attainment_pct: z4.number().min(0).max(1e3), rate: z4.number().min(0) })).min(1).max(10).describe("Ex.: [{from_attainment_pct: 0, rate: 3}, {from_attainment_pct: 100, rate: 6}]")
|
|
893
1090
|
}
|
|
894
1091
|
},
|
|
895
1092
|
withApi(async (api, a) => {
|
|
@@ -900,7 +1097,7 @@ function registerGoalTools(server) {
|
|
|
900
1097
|
};
|
|
901
1098
|
const existing = (await api.get("/commission-plans")).find((p) => norm(p.name) === norm(a.name));
|
|
902
1099
|
const plan = existing ? await api.patch(`/commission-plans/${existing.id}`, { ...body, name: existing.name }) : await api.post("/commission-plans", body);
|
|
903
|
-
return text({ [existing ? "updated" : "created"]:
|
|
1100
|
+
return text({ [existing ? "updated" : "created"]: describePlan2(plan) });
|
|
904
1101
|
})
|
|
905
1102
|
);
|
|
906
1103
|
server.registerTool(
|
|
@@ -908,7 +1105,7 @@ function registerGoalTools(server) {
|
|
|
908
1105
|
{
|
|
909
1106
|
title: "Apagar plano de comiss\xE3o",
|
|
910
1107
|
description: "Apaga um plano de comiss\xE3o (s\xF3 admins). As metas que o usavam ficam sem comiss\xE3o.",
|
|
911
|
-
inputSchema: { plan:
|
|
1108
|
+
inputSchema: { plan: z4.string() },
|
|
912
1109
|
annotations: { destructiveHint: true }
|
|
913
1110
|
},
|
|
914
1111
|
withApi(async (api, a) => {
|
|
@@ -922,7 +1119,7 @@ function registerGoalTools(server) {
|
|
|
922
1119
|
{
|
|
923
1120
|
title: "Metas do m\xEAs",
|
|
924
1121
|
description: "Mostra a meta de cada vendedor num funil e m\xEAs, e o plano de comiss\xE3o ligado. Para o andamento e a proje\xE7\xE3o, use get_dashboard.",
|
|
925
|
-
inputSchema: { funnel:
|
|
1122
|
+
inputSchema: { funnel: z4.string().optional(), workspace: z4.string().optional(), month: z4.string().regex(MONTH).optional().describe("YYYY-MM; padr\xE3o: m\xEAs atual") },
|
|
926
1123
|
annotations: { readOnlyHint: true }
|
|
927
1124
|
},
|
|
928
1125
|
withApi(async (api, a) => {
|
|
@@ -951,11 +1148,11 @@ function registerGoalTools(server) {
|
|
|
951
1148
|
title: "Definir metas",
|
|
952
1149
|
description: 'Define ou altera a meta de vendedores num funil e m\xEAs (s\xF3 admins). target em reais para funis de receita, ou em unidades (vendas/leads na etapa) nos demais. target 0 remove a meta. Sem commission_plan, mant\xE9m o plano que o m\xEAs j\xE1 usa; commission_plan="none" tira a comiss\xE3o.',
|
|
953
1150
|
inputSchema: {
|
|
954
|
-
targets:
|
|
955
|
-
funnel:
|
|
956
|
-
workspace:
|
|
957
|
-
month:
|
|
958
|
-
commission_plan:
|
|
1151
|
+
targets: z4.array(z4.object({ seller: z4.string().describe("Nome ou email"), target: z4.number().min(0) })).min(1),
|
|
1152
|
+
funnel: z4.string().optional(),
|
|
1153
|
+
workspace: z4.string().optional(),
|
|
1154
|
+
month: z4.string().regex(MONTH).optional().describe("YYYY-MM; padr\xE3o: m\xEAs atual"),
|
|
1155
|
+
commission_plan: z4.string().optional()
|
|
959
1156
|
}
|
|
960
1157
|
},
|
|
961
1158
|
withApi(
|
|
@@ -991,14 +1188,14 @@ function registerGoalTools(server) {
|
|
|
991
1188
|
}
|
|
992
1189
|
|
|
993
1190
|
// src/tools/leads.ts
|
|
994
|
-
import { z as
|
|
1191
|
+
import { z as z5 } from "zod";
|
|
995
1192
|
function registerLeadTools(server) {
|
|
996
1193
|
server.registerTool(
|
|
997
1194
|
"reopen_lead",
|
|
998
1195
|
{
|
|
999
1196
|
title: "Reabrir lead",
|
|
1000
1197
|
description: "Volta um lead ganho ou perdido para aberto, na mesma etapa.",
|
|
1001
|
-
inputSchema: { lead_id:
|
|
1198
|
+
inputSchema: { lead_id: z5.string().uuid() }
|
|
1002
1199
|
},
|
|
1003
1200
|
withApi(async (api, a) => {
|
|
1004
1201
|
const c = await ctx(api);
|
|
@@ -1012,12 +1209,12 @@ function registerLeadTools(server) {
|
|
|
1012
1209
|
title: "Enviar lead ganho para outro funil",
|
|
1013
1210
|
description: "Continua um lead GANHO em outro funil (ex.: de Aquisi\xE7\xE3o para Relacionamento/p\xF3s-venda): cria um lead novo, aberto e ligado ao original, na etapa escolhida (padr\xE3o: a primeira). O lead ganho fica onde est\xE1 e continua contando na meta e na comiss\xE3o. O novo lead copia contato e campos, come\xE7a com valor R$ 0 (informe value se j\xE1 houver nova venda prevista) e mant\xE9m o respons\xE1vel (ou use owner). S\xF3 funciona para leads com status won; cada lead s\xF3 pode continuar uma vez em cada funil.",
|
|
1014
1211
|
inputSchema: {
|
|
1015
|
-
lead_id:
|
|
1016
|
-
funnel:
|
|
1017
|
-
stage:
|
|
1018
|
-
workspace:
|
|
1019
|
-
owner:
|
|
1020
|
-
value:
|
|
1212
|
+
lead_id: z5.string().uuid().describe("Lead ganho"),
|
|
1213
|
+
funnel: z5.string().describe("Funil onde o cliente continua (nome ou id)"),
|
|
1214
|
+
stage: z5.string().optional().describe("Etapa de entrada (padr\xE3o: a primeira)"),
|
|
1215
|
+
workspace: z5.string().optional().describe("Workspace do funil, se for outro (s\xF3 admins)"),
|
|
1216
|
+
owner: z5.string().optional().describe('Respons\xE1vel no novo funil (nome ou email; "none" = sem respons\xE1vel). Padr\xE3o: o mesmo do lead ganho'),
|
|
1217
|
+
value: z5.number().min(0).optional().describe("Valor em reais no novo funil (padr\xE3o: 0)")
|
|
1021
1218
|
}
|
|
1022
1219
|
},
|
|
1023
1220
|
withApi(async (api, a) => {
|
|
@@ -1034,10 +1231,10 @@ function registerLeadTools(server) {
|
|
|
1034
1231
|
title: "Atualizar leads em massa",
|
|
1035
1232
|
description: "Reatribui (s\xF3 admins) e/ou move at\xE9 1000 leads de uma vez. Pegue os ids com search_leads. Para mover, informe stage (e funnel, se for outro funil; sem funnel, vale o funil do primeiro lead). Leads que voc\xEA n\xE3o pode editar s\xE3o pulados, assim como leads ganhos numa troca de funil (use handoff_lead para eles).",
|
|
1036
1233
|
inputSchema: {
|
|
1037
|
-
lead_ids:
|
|
1038
|
-
owner:
|
|
1039
|
-
stage:
|
|
1040
|
-
funnel:
|
|
1234
|
+
lead_ids: z5.array(z5.string().uuid()).min(1).max(1e3),
|
|
1235
|
+
owner: z5.string().optional().describe("Nome ou email do novo respons\xE1vel"),
|
|
1236
|
+
stage: z5.string().optional(),
|
|
1237
|
+
funnel: z5.string().optional()
|
|
1041
1238
|
}
|
|
1042
1239
|
},
|
|
1043
1240
|
withApi(async (api, a) => {
|
|
@@ -1064,7 +1261,7 @@ function registerLeadTools(server) {
|
|
|
1064
1261
|
{
|
|
1065
1262
|
title: "Excluir lead",
|
|
1066
1263
|
description: "Apaga um lead e todo o hist\xF3rico dele (s\xF3 admins). N\xE3o d\xE1 para desfazer \u2014 confirme com a pessoa antes. Para tirar do funil sem apagar, prefira mark_lost.",
|
|
1067
|
-
inputSchema: { lead_id:
|
|
1264
|
+
inputSchema: { lead_id: z5.string().uuid() },
|
|
1068
1265
|
annotations: { destructiveHint: true }
|
|
1069
1266
|
},
|
|
1070
1267
|
withApi(async (api, a) => {
|
|
@@ -1075,22 +1272,68 @@ function registerLeadTools(server) {
|
|
|
1075
1272
|
);
|
|
1076
1273
|
}
|
|
1077
1274
|
|
|
1275
|
+
// src/tools/risks.ts
|
|
1276
|
+
import { z as z6 } from "zod";
|
|
1277
|
+
function registerRiskTools(server) {
|
|
1278
|
+
server.registerTool(
|
|
1279
|
+
"get_lead_risks",
|
|
1280
|
+
{
|
|
1281
|
+
title: "Leads em risco",
|
|
1282
|
+
description: "Leads abertos parados al\xE9m do prazo da etapa (dias \xFAteis; s\xF3 mudar de etapa zera a contagem). Passou do prazo, o vendedor \xE9 avisado; passou do dobro, \xE9 cr\xEDtico e o admin \xE9 avisado. Mostra o valor em risco, onde trava (etapa), com quem (vendedor) e os mais atrasados. Para listar todos os fora do prazo use search_leads com filter=stale. S\xF3 admins.",
|
|
1283
|
+
inputSchema: { workspace: z6.string().optional() },
|
|
1284
|
+
annotations: { readOnlyHint: true }
|
|
1285
|
+
},
|
|
1286
|
+
withApi(async (api, a) => {
|
|
1287
|
+
const ws = pickWorkspace(await ctx(api), a.workspace);
|
|
1288
|
+
const r = await api.get(`/leads/risks?workspaceId=${ws.id}`);
|
|
1289
|
+
if (!r.critical.count && !r.late)
|
|
1290
|
+
return text(`Nenhum lead fora do prazo no workspace ${ws.name}.`);
|
|
1291
|
+
return text({
|
|
1292
|
+
workspace: ws.name,
|
|
1293
|
+
critical: {
|
|
1294
|
+
leads: r.critical.count,
|
|
1295
|
+
value: formatMoney(r.critical.valueCents),
|
|
1296
|
+
where_it_jams: r.critical.byStage.map((s) => ({ stage: `${s.funnelName} \xB7 ${s.stageName}`, leads: s.count, value: formatMoney(s.valueCents) })),
|
|
1297
|
+
with_whom: r.critical.byOwner.map((o) => ({ seller: o.ownerName ?? "Sem vendedor", leads: o.count, value: formatMoney(o.valueCents) })),
|
|
1298
|
+
worst: r.critical.top.map((l) => ({
|
|
1299
|
+
lead: l.company ? `${l.company} \u2014 ${l.name}` : l.name,
|
|
1300
|
+
stage: `${l.funnelName} \xB7 ${l.stageName}`,
|
|
1301
|
+
seller: l.ownerName ?? "Sem vendedor",
|
|
1302
|
+
business_days_in_stage: l.age.days,
|
|
1303
|
+
stage_limit_days: l.age.limit,
|
|
1304
|
+
stage_deadline: RISK_LEVEL_LABEL[l.age.level],
|
|
1305
|
+
value: formatMoney(l.valueCents)
|
|
1306
|
+
}))
|
|
1307
|
+
},
|
|
1308
|
+
late_only_seller_warned: r.late
|
|
1309
|
+
});
|
|
1310
|
+
})
|
|
1311
|
+
);
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1078
1314
|
// src/tools/structure.ts
|
|
1079
|
-
import { z as
|
|
1315
|
+
import { z as z7 } from "zod";
|
|
1080
1316
|
var TEMPLATE_KEYS = Object.keys(FUNNEL_TEMPLATES);
|
|
1081
|
-
var stageInput =
|
|
1082
|
-
name:
|
|
1083
|
-
probability:
|
|
1084
|
-
|
|
1317
|
+
var stageInput = z7.object({
|
|
1318
|
+
name: z7.string().min(1).max(60),
|
|
1319
|
+
probability: z7.number().int().min(0).max(100).describe("Chance (%) de um lead nesta etapa virar venda"),
|
|
1320
|
+
limit_days: z7.number().int().min(1).max(365).nullish().describe(
|
|
1321
|
+
`Prazo da etapa em dias \xFAteis: passou dele, o vendedor \xE9 avisado; passou do dobro, o admin tamb\xE9m. null volta ao padr\xE3o (${DEFAULT_STAGE_LIMIT_DAYS}); omitido mant\xE9m o atual.`
|
|
1322
|
+
),
|
|
1323
|
+
was: z7.string().optional().describe("Nome atual da etapa, se voc\xEA est\xE1 renomeando uma existente")
|
|
1085
1324
|
});
|
|
1086
1325
|
var describeFunnel = (f, ws) => ({
|
|
1087
1326
|
name: f.name,
|
|
1088
1327
|
goal_kpi: `${f.goalKpi} (${GOAL_KPI_LABEL[f.goalKpi]})`,
|
|
1089
1328
|
...f.goalKpi === "stage_reached" ? { goal_stage: f.stages.find((s) => s.id === f.goalStageId)?.name ?? null } : {},
|
|
1090
1329
|
...f.nextFunnelId ? { after_won_continue_in: ws?.funnels.find((x) => x.id === f.nextFunnelId)?.name ?? f.nextFunnelId } : {},
|
|
1091
|
-
|
|
1330
|
+
auto_assign: !!f.autoAssign,
|
|
1331
|
+
stages: f.stages.map((s) => `${s.name} (${s.probability}%, prazo ${s.limitDays ?? DEFAULT_STAGE_LIMIT_DAYS} dias \xFAteis${s.limitDays ? "" : " \u2014 padr\xE3o"})`)
|
|
1092
1332
|
});
|
|
1093
|
-
var
|
|
1333
|
+
var autoAssignArg = z7.boolean().optional().describe(
|
|
1334
|
+
"Distribui\xE7\xE3o autom\xE1tica: leads cadastrados ou importados por admins sem vendedor v\xE3o, em maior parte, para quem est\xE1 fechando mais (todos recebem). Quem passa um dia \xFAtil sem nenhuma a\xE7\xE3o no funil para de receber at\xE9 um admin liberar (release_seller)."
|
|
1335
|
+
);
|
|
1336
|
+
var nextFunnelArg = z7.string().nullable().optional().describe('Funil (do mesmo workspace) onde o cliente continua depois de ganho, ex.: "Relacionamento". Sugerido no app ao marcar ganho. null ou "none" remove.');
|
|
1094
1337
|
var nextFunnelId = (ws, ref) => ref === void 0 ? void 0 : ref === null || norm(ref) === "none" ? null : resolve(ws.funnels, ref, "Funil").id;
|
|
1095
1338
|
function registerStructureTools(server) {
|
|
1096
1339
|
server.registerTool(
|
|
@@ -1099,9 +1342,9 @@ function registerStructureTools(server) {
|
|
|
1099
1342
|
title: "Criar workspace",
|
|
1100
1343
|
description: "Cria um workspace (s\xF3 admins) \u2014 ex.: um time, regi\xE3o ou unidade. J\xE1 vem com um funil de vendas, a menos que create_funnel=false.",
|
|
1101
1344
|
inputSchema: {
|
|
1102
|
-
name:
|
|
1103
|
-
visibility:
|
|
1104
|
-
create_funnel:
|
|
1345
|
+
name: z7.string().min(1).max(80),
|
|
1346
|
+
visibility: z7.enum(["all", "own"]).default("all").describe("all = todos veem todos os leads; own = cada vendedor v\xEA s\xF3 os seus"),
|
|
1347
|
+
create_funnel: z7.boolean().default(true)
|
|
1105
1348
|
}
|
|
1106
1349
|
},
|
|
1107
1350
|
withApi(async (api, a) => {
|
|
@@ -1116,9 +1359,9 @@ function registerStructureTools(server) {
|
|
|
1116
1359
|
title: "Alterar workspace",
|
|
1117
1360
|
description: "Renomeia um workspace ou muda quem v\xEA quais leads (s\xF3 admins).",
|
|
1118
1361
|
inputSchema: {
|
|
1119
|
-
workspace:
|
|
1120
|
-
name:
|
|
1121
|
-
visibility:
|
|
1362
|
+
workspace: z7.string(),
|
|
1363
|
+
name: z7.string().min(1).max(80).optional(),
|
|
1364
|
+
visibility: z7.enum(["all", "own"]).optional().describe("all = todos veem todos os leads; own = cada vendedor v\xEA s\xF3 os seus")
|
|
1122
1365
|
}
|
|
1123
1366
|
},
|
|
1124
1367
|
withApi(async (api, a) => {
|
|
@@ -1132,7 +1375,7 @@ function registerStructureTools(server) {
|
|
|
1132
1375
|
{
|
|
1133
1376
|
title: "Excluir workspace",
|
|
1134
1377
|
description: "Apaga um workspace com TODOS os funis e leads dele (s\xF3 admins). N\xE3o d\xE1 para desfazer \u2014 confirme com a pessoa antes.",
|
|
1135
|
-
inputSchema: { workspace:
|
|
1378
|
+
inputSchema: { workspace: z7.string() },
|
|
1136
1379
|
annotations: { destructiveHint: true }
|
|
1137
1380
|
},
|
|
1138
1381
|
withApi(async (api, a) => {
|
|
@@ -1147,13 +1390,14 @@ function registerStructureTools(server) {
|
|
|
1147
1390
|
title: "Criar funil",
|
|
1148
1391
|
description: `Cria um funil num workspace (s\xF3 admins). Use um modelo (${TEMPLATE_KEYS.map((k) => `${k} = ${FUNNEL_TEMPLATES[k].label}`).join("; ")}) ou informe stages. goal_kpi define o que conta como meta: revenue (receita ganha), won_count (n\xBA de vendas) ou stage_reached (n\xBA de leads que chegaram a goal_stage).`,
|
|
1149
1392
|
inputSchema: {
|
|
1150
|
-
name:
|
|
1151
|
-
workspace:
|
|
1152
|
-
template:
|
|
1153
|
-
stages:
|
|
1154
|
-
goal_kpi:
|
|
1155
|
-
goal_stage:
|
|
1156
|
-
next_funnel: nextFunnelArg
|
|
1393
|
+
name: z7.string().min(1).max(80),
|
|
1394
|
+
workspace: z7.string().optional(),
|
|
1395
|
+
template: z7.enum(TEMPLATE_KEYS).optional(),
|
|
1396
|
+
stages: z7.array(stageInput.omit({ was: true })).min(1).max(20).optional(),
|
|
1397
|
+
goal_kpi: z7.enum(GOAL_KPIS).default("revenue"),
|
|
1398
|
+
goal_stage: z7.string().optional().describe("Etapa que conta como meta (s\xF3 para stage_reached; padr\xE3o: a \xFAltima)"),
|
|
1399
|
+
next_funnel: nextFunnelArg,
|
|
1400
|
+
auto_assign: autoAssignArg
|
|
1157
1401
|
}
|
|
1158
1402
|
},
|
|
1159
1403
|
withApi(
|
|
@@ -1166,9 +1410,10 @@ function registerStructureTools(server) {
|
|
|
1166
1410
|
workspaceId: ws.id,
|
|
1167
1411
|
name: a.name,
|
|
1168
1412
|
goalKpi: a.goal_kpi,
|
|
1169
|
-
...a.stages ? { stages: a.stages } : { template: a.template ?? "sales" },
|
|
1413
|
+
...a.stages ? { stages: a.stages.map((s) => ({ name: s.name, probability: s.probability, limitDays: s.limit_days ?? null })) } : { template: a.template ?? "sales" },
|
|
1170
1414
|
...goalIdx >= 0 ? { goalStageIndex: goalIdx } : {},
|
|
1171
|
-
...a.next_funnel ? { nextFunnelId: nextFunnelId(ws, a.next_funnel) } : {}
|
|
1415
|
+
...a.next_funnel ? { nextFunnelId: nextFunnelId(ws, a.next_funnel) } : {},
|
|
1416
|
+
...a.auto_assign !== void 0 ? { autoAssign: a.auto_assign } : {}
|
|
1172
1417
|
});
|
|
1173
1418
|
return text({ workspace: ws.name, created: describeFunnel(f, ws) });
|
|
1174
1419
|
}
|
|
@@ -1178,15 +1423,16 @@ function registerStructureTools(server) {
|
|
|
1178
1423
|
"update_funnel",
|
|
1179
1424
|
{
|
|
1180
1425
|
title: "Alterar funil",
|
|
1181
|
-
description: "Renomeia o funil, troca o KPI de meta ou redefine as etapas (s\xF3 admins). stages \xE9 a lista completa e na ordem: etapas com o mesmo nome (ou com was) s\xE3o mantidas, as novas s\xE3o criadas e as que ficarem de fora s\xE3o removidas \u2014 o que s\xF3 funciona se n\xE3o tiverem leads (mova antes com bulk_update_leads). next_funnel define para onde o cliente vai depois de ganho (ex.: Aquisi\xE7\xE3o \u2192 Relacionamento).",
|
|
1426
|
+
description: "Renomeia o funil, troca o KPI de meta ou redefine as etapas (s\xF3 admins). stages \xE9 a lista completa e na ordem: etapas com o mesmo nome (ou com was) s\xE3o mantidas, as novas s\xE3o criadas e as que ficarem de fora s\xE3o removidas \u2014 o que s\xF3 funciona se n\xE3o tiverem leads (mova antes com bulk_update_leads). next_funnel define para onde o cliente vai depois de ganho (ex.: Aquisi\xE7\xE3o \u2192 Relacionamento). auto_assign liga ou desliga a distribui\xE7\xE3o autom\xE1tica de leads. limit_days em cada etapa define o prazo (dias \xFAteis) antes de o lead ser considerado parado.",
|
|
1182
1427
|
inputSchema: {
|
|
1183
|
-
funnel:
|
|
1184
|
-
workspace:
|
|
1185
|
-
name:
|
|
1186
|
-
goal_kpi:
|
|
1187
|
-
goal_stage:
|
|
1188
|
-
stages:
|
|
1189
|
-
next_funnel: nextFunnelArg
|
|
1428
|
+
funnel: z7.string(),
|
|
1429
|
+
workspace: z7.string().optional(),
|
|
1430
|
+
name: z7.string().min(1).max(80).optional(),
|
|
1431
|
+
goal_kpi: z7.enum(GOAL_KPIS).optional(),
|
|
1432
|
+
goal_stage: z7.string().optional(),
|
|
1433
|
+
stages: z7.array(stageInput).min(1).max(20).optional(),
|
|
1434
|
+
next_funnel: nextFunnelArg,
|
|
1435
|
+
auto_assign: autoAssignArg
|
|
1190
1436
|
}
|
|
1191
1437
|
},
|
|
1192
1438
|
withApi(async (api, a) => {
|
|
@@ -1196,13 +1442,19 @@ function registerStructureTools(server) {
|
|
|
1196
1442
|
const next = nextFunnelId(ws, a.next_funnel);
|
|
1197
1443
|
const stages = a.stages?.map((s) => {
|
|
1198
1444
|
const existing = funnel.stages.find((x) => norm(x.name) === norm(s.was ?? s.name));
|
|
1199
|
-
return {
|
|
1445
|
+
return {
|
|
1446
|
+
...existing ? { id: existing.id } : {},
|
|
1447
|
+
name: s.name,
|
|
1448
|
+
probability: s.probability,
|
|
1449
|
+
...s.limit_days !== void 0 ? { limitDays: s.limit_days } : {}
|
|
1450
|
+
};
|
|
1200
1451
|
});
|
|
1201
1452
|
let f = await api.patch(`/funnels/${funnel.id}`, {
|
|
1202
1453
|
...a.name ? { name: a.name } : {},
|
|
1203
1454
|
...a.goal_kpi ? { goalKpi: a.goal_kpi } : {},
|
|
1204
1455
|
...stages ? { stages } : {},
|
|
1205
|
-
...next !== void 0 ? { nextFunnelId: next } : {}
|
|
1456
|
+
...next !== void 0 ? { nextFunnelId: next } : {},
|
|
1457
|
+
...a.auto_assign !== void 0 ? { autoAssign: a.auto_assign } : {}
|
|
1206
1458
|
});
|
|
1207
1459
|
if (a.goal_stage || a.goal_kpi === "stage_reached" && !f.goalStageId) {
|
|
1208
1460
|
const goal = a.goal_stage ? resolve(f.stages, a.goal_stage, "Etapa") : f.stages[f.stages.length - 1];
|
|
@@ -1216,7 +1468,7 @@ function registerStructureTools(server) {
|
|
|
1216
1468
|
{
|
|
1217
1469
|
title: "Excluir funil",
|
|
1218
1470
|
description: "Remove um funil (s\xF3 admins). Se ele tiver leads, \xE9 arquivado (some das telas, hist\xF3rico preservado); se estiver vazio, \xE9 apagado. Confirme com a pessoa antes.",
|
|
1219
|
-
inputSchema: { funnel:
|
|
1471
|
+
inputSchema: { funnel: z7.string(), workspace: z7.string().optional() },
|
|
1220
1472
|
annotations: { destructiveHint: true }
|
|
1221
1473
|
},
|
|
1222
1474
|
withApi(async (api, a) => {
|
|
@@ -1228,7 +1480,7 @@ function registerStructureTools(server) {
|
|
|
1228
1480
|
}
|
|
1229
1481
|
|
|
1230
1482
|
// src/tools/team.ts
|
|
1231
|
-
import { z as
|
|
1483
|
+
import { z as z8 } from "zod";
|
|
1232
1484
|
var wsNames = (c, ids = []) => ids.map((id2) => c.workspaces.find((w) => w.id === id2)?.name ?? id2);
|
|
1233
1485
|
function registerTeamTools(server) {
|
|
1234
1486
|
server.registerTool(
|
|
@@ -1236,7 +1488,7 @@ function registerTeamTools(server) {
|
|
|
1236
1488
|
{
|
|
1237
1489
|
title: "Listar usu\xE1rios",
|
|
1238
1490
|
description: "Lista as pessoas da organiza\xE7\xE3o: papel (admin/member), se est\xE1 ativa, convite pendente e workspaces de cada uma.",
|
|
1239
|
-
inputSchema: { include_inactive:
|
|
1491
|
+
inputSchema: { include_inactive: z8.boolean().default(false) },
|
|
1240
1492
|
annotations: { readOnlyHint: true }
|
|
1241
1493
|
},
|
|
1242
1494
|
withApi(async (api, a) => {
|
|
@@ -1259,10 +1511,10 @@ function registerTeamTools(server) {
|
|
|
1259
1511
|
title: "Convidar usu\xE1rio",
|
|
1260
1512
|
description: "Convida uma pessoa para a organiza\xE7\xE3o (s\xF3 admins) e devolve o link de convite (vale 7 dias) para voc\xEA enviar a ela. member = vendedor (v\xEA os workspaces em que est\xE1); admin = v\xEA e configura tudo.",
|
|
1261
1513
|
inputSchema: {
|
|
1262
|
-
name:
|
|
1263
|
-
email:
|
|
1264
|
-
role:
|
|
1265
|
-
workspaces:
|
|
1514
|
+
name: z8.string().min(2),
|
|
1515
|
+
email: z8.string().email(),
|
|
1516
|
+
role: z8.enum(["member", "admin"]).default("member"),
|
|
1517
|
+
workspaces: z8.array(z8.string()).optional().describe("Nomes dos workspaces (padr\xE3o: o primeiro)")
|
|
1266
1518
|
}
|
|
1267
1519
|
},
|
|
1268
1520
|
withApi(async (api, a) => {
|
|
@@ -1282,7 +1534,7 @@ function registerTeamTools(server) {
|
|
|
1282
1534
|
{
|
|
1283
1535
|
title: "Gerar novo link de convite",
|
|
1284
1536
|
description: "Gera um novo link de convite para algu\xE9m (s\xF3 admins) \u2014 \xFAtil se o anterior expirou. O link antigo deixa de valer.",
|
|
1285
|
-
inputSchema: { user:
|
|
1537
|
+
inputSchema: { user: z8.string().describe("Nome ou email") }
|
|
1286
1538
|
},
|
|
1287
1539
|
withApi(async (api, a) => {
|
|
1288
1540
|
const c = await ctx(api);
|
|
@@ -1297,11 +1549,11 @@ function registerTeamTools(server) {
|
|
|
1297
1549
|
title: "Alterar usu\xE1rio",
|
|
1298
1550
|
description: "Muda nome, papel, workspaces ou desativa/reativa algu\xE9m (s\xF3 admins). Desativar encerra as sess\xF5es e tokens da pessoa; os leads continuam com ela at\xE9 serem reatribu\xEDdos (bulk_update_leads).",
|
|
1299
1551
|
inputSchema: {
|
|
1300
|
-
user:
|
|
1301
|
-
name:
|
|
1302
|
-
role:
|
|
1303
|
-
active:
|
|
1304
|
-
workspaces:
|
|
1552
|
+
user: z8.string().describe("Nome ou email"),
|
|
1553
|
+
name: z8.string().min(2).optional(),
|
|
1554
|
+
role: z8.enum(["member", "admin"]).optional(),
|
|
1555
|
+
active: z8.boolean().optional(),
|
|
1556
|
+
workspaces: z8.array(z8.string()).optional().describe("Lista completa de workspaces da pessoa (substitui a atual)")
|
|
1305
1557
|
}
|
|
1306
1558
|
},
|
|
1307
1559
|
withApi(async (api, a) => {
|
|
@@ -1330,15 +1582,17 @@ function registerManageTools(server) {
|
|
|
1330
1582
|
registerTeamTools(server);
|
|
1331
1583
|
registerStructureTools(server);
|
|
1332
1584
|
registerGoalTools(server);
|
|
1585
|
+
registerDistributionTools(server);
|
|
1586
|
+
registerRiskTools(server);
|
|
1333
1587
|
registerAdminTools(server);
|
|
1334
1588
|
}
|
|
1335
1589
|
|
|
1336
1590
|
// src/server.ts
|
|
1337
1591
|
async function startServer() {
|
|
1338
1592
|
const server = new McpServer(
|
|
1339
|
-
{ name: "aicrm", version: "0.
|
|
1593
|
+
{ name: "aicrm", version: "0.4.0" },
|
|
1340
1594
|
{
|
|
1341
|
-
instructions:
|
|
1595
|
+
instructions: 'AICRM \xE9 um CRM de vendas. Workspaces cont\xEAm funis; funis cont\xEAm etapas e leads. Status do lead: open, won, lost. Workspaces, funis, etapas e vendedores podem ser referenciados por nome. Valores monet\xE1rios est\xE3o em reais (BRL). Use get_dashboard/get_forecast para metas e proje\xE7\xF5es estat\xEDsticas (Monte Carlo bayesiano). Para "o que eu fa\xE7o hoje?", use get_today_plan: lista os leads que mais ajudam a bater a meta, com motivo e a\xE7\xE3o sugerida. Cada workspace pode ter campos personalizados (ex.: Cidade, Porte) e esconder campos padr\xE3o: veja list_fields. Em create_lead/update_lead, preencha-os em custom_fields usando o nome do campo. Lead ganho n\xE3o troca de funil: para levar o cliente adiante (ex.: Aquisi\xE7\xE3o \u2192 Relacionamento), use handoff_lead ou next_funnel em mark_won, que criam um lead novo ligado ao ganho. Antes de importar um CSV desconhecido, rode import_leads_csv com dry_run=true para revisar o mapeamento de colunas. Admins tamb\xE9m gerenciam time (invite_user, update_user), estrutura (create_workspace, create_funnel, update_funnel), metas e comiss\xF5es (set_goals, save_commission_plan) e modo TV. Funis com auto_assign distribuem leads sem vendedor por desempenho; quem passa um dia \xFAtil sem nenhuma a\xE7\xE3o no funil para de receber at\xE9 um admin liberar: veja get_distribution, release_seller e pause_seller. O servidor aplica as permiss\xF5es do usu\xE1rio logado: se uma a\xE7\xE3o for negada, explique que precisa de um admin. A\xE7\xF5es destrutivas (delete_*, archive_field, revoke_*) exigem confirma\xE7\xE3o expl\xEDcita da pessoa antes.'
|
|
1342
1596
|
}
|
|
1343
1597
|
);
|
|
1344
1598
|
server.registerTool(
|
|
@@ -1364,7 +1618,8 @@ async function startServer() {
|
|
|
1364
1618
|
goal_kpi: f.goalKpi,
|
|
1365
1619
|
...f.goalKpi === "stage_reached" ? { goal_stage: f.stages.find((s) => s.id === f.goalStageId)?.name ?? null } : {},
|
|
1366
1620
|
...f.nextFunnelId ? { after_won_continue_in: c.funnels.get(f.nextFunnelId)?.name ?? null } : {},
|
|
1367
|
-
|
|
1621
|
+
...f.autoAssign ? { auto_assign: "leads sem vendedor s\xE3o distribu\xEDdos por desempenho" } : {},
|
|
1622
|
+
stages: f.stages.map((s) => `${s.name} (${s.probability}%${s.limitDays ? `, prazo ${s.limitDays} dias \xFAteis` : ""})`)
|
|
1368
1623
|
}))
|
|
1369
1624
|
})),
|
|
1370
1625
|
sellers: c.users.filter((u) => u.active).map((u) => `${u.name} <${u.email}>`)
|
|
@@ -1375,17 +1630,17 @@ async function startServer() {
|
|
|
1375
1630
|
"search_leads",
|
|
1376
1631
|
{
|
|
1377
1632
|
title: "Buscar leads",
|
|
1378
|
-
description: "Busca leads por texto e filtros. filter: mine (meus), stale (
|
|
1633
|
+
description: "Busca leads por texto e filtros. filter: mine (meus), stale (passaram do prazo da etapa, em dias \xFAteis), no_next_step (sem pr\xF3ximo passo).",
|
|
1379
1634
|
inputSchema: {
|
|
1380
|
-
query:
|
|
1381
|
-
workspace:
|
|
1382
|
-
funnel:
|
|
1383
|
-
stage:
|
|
1384
|
-
owner:
|
|
1385
|
-
status:
|
|
1386
|
-
filter:
|
|
1387
|
-
field_filters:
|
|
1388
|
-
limit:
|
|
1635
|
+
query: z9.string().optional().describe("Texto em nome, empresa ou email"),
|
|
1636
|
+
workspace: z9.string().optional(),
|
|
1637
|
+
funnel: z9.string().optional(),
|
|
1638
|
+
stage: z9.string().optional(),
|
|
1639
|
+
owner: z9.string().optional().describe("Nome ou email do respons\xE1vel"),
|
|
1640
|
+
status: z9.enum(["open", "won", "lost"]).optional(),
|
|
1641
|
+
filter: z9.enum(["mine", "stale", "no_next_step"]).optional(),
|
|
1642
|
+
field_filters: z9.record(z9.string(), z9.string()).optional().describe('Filtra por campos personalizados: { "Cidade": "Recife" }. Texto: cont\xE9m; demais tipos: igual (datas em dd/mm/aaaa, sim/n\xE3o).'),
|
|
1643
|
+
limit: z9.number().int().min(1).max(200).default(25)
|
|
1389
1644
|
},
|
|
1390
1645
|
annotations: { readOnlyHint: true }
|
|
1391
1646
|
},
|
|
@@ -1420,7 +1675,7 @@ async function startServer() {
|
|
|
1420
1675
|
{
|
|
1421
1676
|
title: "Detalhes do lead",
|
|
1422
1677
|
description: "Retorna todos os campos de um lead e o hist\xF3rico (movimenta\xE7\xF5es e notas).",
|
|
1423
|
-
inputSchema: { lead_id:
|
|
1678
|
+
inputSchema: { lead_id: z9.string().uuid() },
|
|
1424
1679
|
annotations: { readOnlyHint: true }
|
|
1425
1680
|
},
|
|
1426
1681
|
withApi(async (api, a) => {
|
|
@@ -1453,17 +1708,17 @@ async function startServer() {
|
|
|
1453
1708
|
})
|
|
1454
1709
|
);
|
|
1455
1710
|
const leadFields = {
|
|
1456
|
-
name:
|
|
1457
|
-
company:
|
|
1458
|
-
email:
|
|
1459
|
-
phone:
|
|
1460
|
-
title:
|
|
1461
|
-
value:
|
|
1462
|
-
source:
|
|
1463
|
-
next_step:
|
|
1464
|
-
next_step_date:
|
|
1465
|
-
expected_close:
|
|
1466
|
-
custom_fields:
|
|
1711
|
+
name: z9.string().optional(),
|
|
1712
|
+
company: z9.string().optional(),
|
|
1713
|
+
email: z9.string().optional(),
|
|
1714
|
+
phone: z9.string().optional(),
|
|
1715
|
+
title: z9.string().optional(),
|
|
1716
|
+
value: z9.number().min(0).optional().describe("Valor do neg\xF3cio em reais"),
|
|
1717
|
+
source: z9.enum(LEAD_SOURCES).optional(),
|
|
1718
|
+
next_step: z9.string().optional(),
|
|
1719
|
+
next_step_date: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
|
|
1720
|
+
expected_close: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
|
|
1721
|
+
custom_fields: z9.record(z9.string(), z9.union([z9.string(), z9.number(), z9.boolean(), z9.null()])).optional().describe('Campos personalizados do workspace pelo nome: { "Cidade": "Recife", "Faturamento": 50000 }. Dinheiro em reais; null limpa o campo. Veja list_fields.')
|
|
1467
1722
|
};
|
|
1468
1723
|
const toBody = (a) => Object.fromEntries(
|
|
1469
1724
|
Object.entries({
|
|
@@ -1483,8 +1738,8 @@ async function startServer() {
|
|
|
1483
1738
|
"create_lead",
|
|
1484
1739
|
{
|
|
1485
1740
|
title: "Criar lead",
|
|
1486
|
-
description: "Cria um lead. Sem funil/etapa, usa o primeiro funil do workspace e a primeira etapa. O respons\xE1vel padr\xE3o \xE9 voc\xEA.",
|
|
1487
|
-
inputSchema: { ...leadFields, name:
|
|
1741
|
+
description: "Cria um lead. Sem funil/etapa, usa o primeiro funil do workspace e a primeira etapa. O respons\xE1vel padr\xE3o \xE9 voc\xEA, exceto quando um admin cria sem owner num funil com distribui\xE7\xE3o autom\xE1tica (auto_assign): a\xED o AICRM escolhe o vendedor pelo desempenho (veja get_distribution).",
|
|
1742
|
+
inputSchema: { ...leadFields, name: z9.string(), workspace: z9.string().optional(), funnel: z9.string().optional(), stage: z9.string().optional(), owner: z9.string().optional() }
|
|
1488
1743
|
},
|
|
1489
1744
|
withApi(async (api, a) => {
|
|
1490
1745
|
const c = await ctx(api);
|
|
@@ -1507,7 +1762,7 @@ async function startServer() {
|
|
|
1507
1762
|
{
|
|
1508
1763
|
title: "Atualizar lead",
|
|
1509
1764
|
description: 'Atualiza campos de um lead (s\xF3 os informados; texto vazio "" limpa um campo padr\xE3o). Use owner para reatribuir, ou owner="none" para deixar sem respons\xE1vel.',
|
|
1510
|
-
inputSchema: { lead_id:
|
|
1765
|
+
inputSchema: { lead_id: z9.string().uuid(), ...leadFields, owner: z9.string().optional() }
|
|
1511
1766
|
},
|
|
1512
1767
|
withApi(async (api, a) => {
|
|
1513
1768
|
const c = await ctx(api);
|
|
@@ -1528,7 +1783,7 @@ async function startServer() {
|
|
|
1528
1783
|
{
|
|
1529
1784
|
title: "Mover lead",
|
|
1530
1785
|
description: "Move um lead para outra etapa \u2014 e opcionalmente para outro funil (entre workspaces: s\xF3 admins). Lead ganho n\xE3o muda de funil (a venda precisa continuar contando onde foi fechada): para lev\xE1-lo a outro funil, use handoff_lead.",
|
|
1531
|
-
inputSchema: { lead_id:
|
|
1786
|
+
inputSchema: { lead_id: z9.string().uuid(), stage: z9.string().describe("Nome ou id da etapa de destino"), funnel: z9.string().optional().describe("Funil de destino (padr\xE3o: o atual)") }
|
|
1532
1787
|
},
|
|
1533
1788
|
withApi(async (api, a) => {
|
|
1534
1789
|
const c = await ctx(api);
|
|
@@ -1545,12 +1800,12 @@ async function startServer() {
|
|
|
1545
1800
|
title: "Marcar como ganho",
|
|
1546
1801
|
description: "Marca o lead como ganho. Informe value (R$) se o valor fechado for diferente do cadastrado. Para j\xE1 continuar o cliente em outro funil (ex.: Relacionamento), informe next_funnel: \xE9 criado um lead novo ligado ao ganho, na mesma opera\xE7\xE3o. Se o funil tiver um funil seguinte configurado, a resposta avisa \u2014 pergunte \xE0 pessoa antes de usar handoff_lead.",
|
|
1547
1802
|
inputSchema: {
|
|
1548
|
-
lead_id:
|
|
1549
|
-
value:
|
|
1550
|
-
next_funnel:
|
|
1551
|
-
next_stage:
|
|
1552
|
-
next_owner:
|
|
1553
|
-
next_value:
|
|
1803
|
+
lead_id: z9.string().uuid(),
|
|
1804
|
+
value: z9.number().min(0).optional(),
|
|
1805
|
+
next_funnel: z9.string().optional().describe("Funil onde o cliente continua depois do ganho"),
|
|
1806
|
+
next_stage: z9.string().optional().describe("Etapa de entrada no pr\xF3ximo funil (padr\xE3o: a primeira)"),
|
|
1807
|
+
next_owner: z9.string().optional().describe("Respons\xE1vel no pr\xF3ximo funil (padr\xE3o: o mesmo)"),
|
|
1808
|
+
next_value: z9.number().min(0).optional().describe("Valor em reais no pr\xF3ximo funil (padr\xE3o: 0)")
|
|
1554
1809
|
}
|
|
1555
1810
|
},
|
|
1556
1811
|
withApi(async (api, a) => {
|
|
@@ -1578,7 +1833,7 @@ async function startServer() {
|
|
|
1578
1833
|
{
|
|
1579
1834
|
title: "Marcar como perdido",
|
|
1580
1835
|
description: "Marca o lead como perdido com um motivo: price, timing, competitor, no_response, no_fit, other.",
|
|
1581
|
-
inputSchema: { lead_id:
|
|
1836
|
+
inputSchema: { lead_id: z9.string().uuid(), reason: z9.enum(LOST_REASONS), note: z9.string().optional() }
|
|
1582
1837
|
},
|
|
1583
1838
|
withApi(async (api, a) => {
|
|
1584
1839
|
const c = await ctx(api);
|
|
@@ -1591,7 +1846,7 @@ async function startServer() {
|
|
|
1591
1846
|
{
|
|
1592
1847
|
title: "Adicionar nota",
|
|
1593
1848
|
description: "Adiciona uma nota \xE0 linha do tempo do lead.",
|
|
1594
|
-
inputSchema: { lead_id:
|
|
1849
|
+
inputSchema: { lead_id: z9.string().uuid(), note: z9.string().min(1) }
|
|
1595
1850
|
},
|
|
1596
1851
|
withApi(async (api, a) => {
|
|
1597
1852
|
await api.post(`/leads/${a.lead_id}/notes`, { body: a.note });
|
|
@@ -1603,7 +1858,7 @@ async function startServer() {
|
|
|
1603
1858
|
{
|
|
1604
1859
|
title: "Dashboard de metas",
|
|
1605
1860
|
description: "Metas do time e de cada vendedor no m\xEAs, com probabilidade de bater (Monte Carlo), proje\xE7\xE3o, ritmo, cobertura de pipeline, comiss\xE3o e KPIs descritivos.",
|
|
1606
|
-
inputSchema: { funnel:
|
|
1861
|
+
inputSchema: { funnel: z9.string().optional(), workspace: z9.string().optional(), month: z9.string().regex(/^\d{4}-\d{2}$/).optional().describe("YYYY-MM; padr\xE3o: m\xEAs atual") },
|
|
1607
1862
|
annotations: { readOnlyHint: true }
|
|
1608
1863
|
},
|
|
1609
1864
|
withApi(async (api, a) => {
|
|
@@ -1639,7 +1894,7 @@ async function startServer() {
|
|
|
1639
1894
|
{
|
|
1640
1895
|
title: "Vai bater a meta?",
|
|
1641
1896
|
description: "Proje\xE7\xE3o detalhada de um vendedor: probabilidade de bater a meta, intervalo de 80%, ritmo atual vs necess\xE1rio e comiss\xE3o projetada.",
|
|
1642
|
-
inputSchema: { seller:
|
|
1897
|
+
inputSchema: { seller: z9.string().describe("Nome ou email do vendedor"), funnel: z9.string().optional(), month: z9.string().regex(/^\d{4}-\d{2}$/).optional() },
|
|
1643
1898
|
annotations: { readOnlyHint: true }
|
|
1644
1899
|
},
|
|
1645
1900
|
withApi(async (api, a) => {
|
|
@@ -1658,12 +1913,62 @@ async function startServer() {
|
|
|
1658
1913
|
});
|
|
1659
1914
|
})
|
|
1660
1915
|
);
|
|
1916
|
+
server.registerTool(
|
|
1917
|
+
"get_today_plan",
|
|
1918
|
+
{
|
|
1919
|
+
title: "Plano do dia",
|
|
1920
|
+
description: 'Plano do dia: os leads abertos que mais ajudam o vendedor a bater a meta hoje, em ordem, com o motivo e a a\xE7\xE3o sugerida (WhatsApp, pr\xF3ximo passo ou mover de etapa). Use quando a pessoa perguntar "o que eu fa\xE7o hoje?", "por onde come\xE7o?" ou "quais leads priorizar?". Por padr\xE3o \xE9 o plano de quem est\xE1 logado; admins podem pedir o de outro vendedor.',
|
|
1921
|
+
inputSchema: {
|
|
1922
|
+
funnel: z9.string().optional(),
|
|
1923
|
+
workspace: z9.string().optional(),
|
|
1924
|
+
seller: z9.string().optional().describe("Nome ou email do vendedor (s\xF3 admins veem o plano de outra pessoa)"),
|
|
1925
|
+
limit: z9.number().int().min(5).max(10).optional().describe("Quantos leads sugerir (5 a 10; padr\xE3o 7)")
|
|
1926
|
+
},
|
|
1927
|
+
annotations: { readOnlyHint: true }
|
|
1928
|
+
},
|
|
1929
|
+
withApi(async (api, a) => {
|
|
1930
|
+
const c = await ctx(api);
|
|
1931
|
+
const funnel = pickFunnel(c, a.funnel, a.workspace);
|
|
1932
|
+
const userId = a.seller ? findUser(c, a.seller).id : void 0;
|
|
1933
|
+
const d = await api.get(`/today?funnelId=${funnel.id}${userId ? `&userId=${userId}` : ""}${a.limit ? `&limit=${a.limit}` : ""}`);
|
|
1934
|
+
const kpi = d.funnel.goalKpi;
|
|
1935
|
+
const suggest = (i) => i.action.kind === "whatsapp" && i.whatsappUrl ? `Chamar no WhatsApp: ${i.whatsappUrl} (entregue o link; depois registre o pr\xF3ximo passo com update_lead)` : i.action.kind === "move_stage" && i.nextStage ? `Se j\xE1 avan\xE7ou, mover para ${i.nextStage.name} (move_lead)` : "Combinar e registrar o pr\xF3ximo passo com data (update_lead com next_step e next_step_date)";
|
|
1936
|
+
return text({
|
|
1937
|
+
seller: d.seller.name,
|
|
1938
|
+
funnel: d.funnel.name,
|
|
1939
|
+
summary: d.summary,
|
|
1940
|
+
goal: d.goal ? {
|
|
1941
|
+
status: FORECAST_STATUS_LABEL[d.goal.status],
|
|
1942
|
+
probability_to_hit: formatPct(d.goal.probability),
|
|
1943
|
+
actual: formatKpi(kpi, d.goal.actual),
|
|
1944
|
+
target: formatKpi(kpi, d.goal.target),
|
|
1945
|
+
missing: formatKpi(kpi, d.goal.gap),
|
|
1946
|
+
low_confidence: d.goal.lowConfidence
|
|
1947
|
+
} : "sem meta neste m\xEAs (ordem por valor, chance e urg\xEAncia)",
|
|
1948
|
+
plan: d.items.map((i) => ({
|
|
1949
|
+
rank: i.rank,
|
|
1950
|
+
lead_id: i.lead.id,
|
|
1951
|
+
lead: i.lead.company ? `${i.lead.company} \u2014 ${i.lead.name}` : i.lead.name,
|
|
1952
|
+
stage: i.stageName,
|
|
1953
|
+
value: formatMoney(i.lead.valueCents),
|
|
1954
|
+
chance: formatPct(i.chance),
|
|
1955
|
+
...kpi === "revenue" ? { worth_for_goal: formatMoney(i.expected) } : {},
|
|
1956
|
+
...i.shareOfGap != null && kpi === "revenue" ? { share_of_missing: formatPct(Math.min(1, i.shareOfGap)) } : {},
|
|
1957
|
+
why: i.reasons.map((r) => r.text),
|
|
1958
|
+
suggested_action: suggest(i),
|
|
1959
|
+
phone: i.lead.phone
|
|
1960
|
+
})),
|
|
1961
|
+
done_today: d.doneToday.map((x) => `${x.name}: ${x.what}`),
|
|
1962
|
+
open_leads: d.counts.open
|
|
1963
|
+
});
|
|
1964
|
+
})
|
|
1965
|
+
);
|
|
1661
1966
|
server.registerTool(
|
|
1662
1967
|
"list_fields",
|
|
1663
1968
|
{
|
|
1664
1969
|
title: "Campos do workspace",
|
|
1665
1970
|
description: "Mostra os campos de lead de um workspace: campos padr\xE3o (e quais est\xE3o escondidos), campos personalizados com tipo e op\xE7\xF5es, e os modelos de importa\xE7\xE3o de CSV salvos.",
|
|
1666
|
-
inputSchema: { workspace:
|
|
1971
|
+
inputSchema: { workspace: z9.string().optional() },
|
|
1667
1972
|
annotations: { readOnlyHint: true }
|
|
1668
1973
|
},
|
|
1669
1974
|
withApi(async (api, a) => {
|
|
@@ -1695,10 +2000,10 @@ async function startServer() {
|
|
|
1695
2000
|
title: "Criar campo personalizado",
|
|
1696
2001
|
description: "Cria um campo de lead no workspace (s\xF3 admins). Tipos: text, number, money (R$), date, select (lista \u2014 exige options), boolean (sim/n\xE3o), url.",
|
|
1697
2002
|
inputSchema: {
|
|
1698
|
-
label:
|
|
1699
|
-
type:
|
|
1700
|
-
options:
|
|
1701
|
-
workspace:
|
|
2003
|
+
label: z9.string().min(1).max(60).describe("Nome do campo, ex.: Cidade"),
|
|
2004
|
+
type: z9.enum(CUSTOM_FIELD_TYPES),
|
|
2005
|
+
options: z9.array(z9.string()).optional().describe("Op\xE7\xF5es da lista (tipo select)"),
|
|
2006
|
+
workspace: z9.string().optional()
|
|
1702
2007
|
}
|
|
1703
2008
|
},
|
|
1704
2009
|
withApi(async (api, a) => {
|
|
@@ -1714,10 +2019,10 @@ async function startServer() {
|
|
|
1714
2019
|
title: "Editar campo personalizado",
|
|
1715
2020
|
description: "Renomeia um campo ou troca as op\xE7\xF5es de uma lista (s\xF3 admins). O tipo n\xE3o muda depois de criado. Renomear n\xE3o quebra modelos de importa\xE7\xE3o.",
|
|
1716
2021
|
inputSchema: {
|
|
1717
|
-
field:
|
|
1718
|
-
label:
|
|
1719
|
-
options:
|
|
1720
|
-
workspace:
|
|
2022
|
+
field: z9.string().describe("Nome ou id do campo"),
|
|
2023
|
+
label: z9.string().min(1).max(60).optional(),
|
|
2024
|
+
options: z9.array(z9.string()).optional().describe("Lista completa de op\xE7\xF5es (substitui a atual)"),
|
|
2025
|
+
workspace: z9.string().optional()
|
|
1721
2026
|
}
|
|
1722
2027
|
},
|
|
1723
2028
|
withApi(async (api, a) => {
|
|
@@ -1733,7 +2038,7 @@ async function startServer() {
|
|
|
1733
2038
|
{
|
|
1734
2039
|
title: "Remover campo personalizado",
|
|
1735
2040
|
description: "Remove um campo das telas e importa\xE7\xF5es (s\xF3 admins). Os valores j\xE1 preenchidos ficam guardados, mas deixam de aparecer.",
|
|
1736
|
-
inputSchema: { field:
|
|
2041
|
+
inputSchema: { field: z9.string().describe("Nome ou id do campo"), workspace: z9.string().optional() },
|
|
1737
2042
|
annotations: { destructiveHint: true }
|
|
1738
2043
|
},
|
|
1739
2044
|
withApi(async (api, a) => {
|
|
@@ -1750,9 +2055,9 @@ async function startServer() {
|
|
|
1750
2055
|
title: "Mostrar/esconder campos padr\xE3o",
|
|
1751
2056
|
description: `Esconde ou volta a mostrar campos padr\xE3o que o workspace n\xE3o usa (s\xF3 admins). Podem ser escondidos: ${HIDEABLE_FIELDS.join(", ")}. Nome e valor ficam sempre vis\xEDveis.`,
|
|
1752
2057
|
inputSchema: {
|
|
1753
|
-
hide:
|
|
1754
|
-
show:
|
|
1755
|
-
workspace:
|
|
2058
|
+
hide: z9.array(z9.enum(HIDEABLE_FIELDS)).optional(),
|
|
2059
|
+
show: z9.array(z9.enum(HIDEABLE_FIELDS)).optional(),
|
|
2060
|
+
workspace: z9.string().optional()
|
|
1756
2061
|
}
|
|
1757
2062
|
},
|
|
1758
2063
|
withApi(async (api, a) => {
|
|
@@ -1768,7 +2073,7 @@ async function startServer() {
|
|
|
1768
2073
|
{
|
|
1769
2074
|
title: "Renomear modelo de importa\xE7\xE3o",
|
|
1770
2075
|
description: "Renomeia um modelo de importa\xE7\xE3o de CSV (admins ou quem criou o modelo).",
|
|
1771
|
-
inputSchema: { template:
|
|
2076
|
+
inputSchema: { template: z9.string().describe("Nome ou id do modelo"), name: z9.string().min(1).max(80), workspace: z9.string().optional() }
|
|
1772
2077
|
},
|
|
1773
2078
|
withApi(async (api, a) => {
|
|
1774
2079
|
const c = await ctx(api);
|
|
@@ -1783,7 +2088,7 @@ async function startServer() {
|
|
|
1783
2088
|
{
|
|
1784
2089
|
title: "Apagar modelo de importa\xE7\xE3o",
|
|
1785
2090
|
description: "Apaga um modelo de importa\xE7\xE3o (admins ou quem criou). Leads j\xE1 importados n\xE3o mudam.",
|
|
1786
|
-
inputSchema: { template:
|
|
2091
|
+
inputSchema: { template: z9.string().describe("Nome ou id do modelo"), workspace: z9.string().optional() },
|
|
1787
2092
|
annotations: { destructiveHint: true }
|
|
1788
2093
|
},
|
|
1789
2094
|
withApi(async (api, a) => {
|
|
@@ -1800,15 +2105,15 @@ async function startServer() {
|
|
|
1800
2105
|
title: "Importar CSV",
|
|
1801
2106
|
description: 'Importa leads de um arquivo CSV local. As colunas s\xE3o ligadas aos campos do workspace (padr\xE3o e personalizados); se a planilha j\xE1 foi importada antes, o modelo salvo \xE9 reaproveitado. Use dry_run=true para ver o mapeamento, colunas ignoradas (com tipo sugerido para criar campo) e problemas antes de gravar. Ajuste com columns: { "Coluna do CSV": "Nome do campo" | "ignorar" }.',
|
|
1802
2107
|
inputSchema: {
|
|
1803
|
-
path:
|
|
1804
|
-
funnel:
|
|
1805
|
-
workspace:
|
|
1806
|
-
stage:
|
|
1807
|
-
owner_mode:
|
|
1808
|
-
duplicate:
|
|
1809
|
-
columns:
|
|
1810
|
-
dry_run:
|
|
1811
|
-
template_name:
|
|
2108
|
+
path: z9.string().describe("Caminho do arquivo CSV (aceita ~)"),
|
|
2109
|
+
funnel: z9.string().optional(),
|
|
2110
|
+
workspace: z9.string().optional(),
|
|
2111
|
+
stage: z9.string().optional(),
|
|
2112
|
+
owner_mode: z9.enum(["me", "auto", "round_robin", "column"]).default("me").describe("me = voc\xEA; auto = distribuir por desempenho (quem fecha mais recebe mais, s\xF3 admins); round_robin = distribuir igualmente; column = coluna 'respons\xE1vel' (email)"),
|
|
2113
|
+
duplicate: z9.enum(["skip", "update"]).default("skip"),
|
|
2114
|
+
columns: z9.record(z9.string(), z9.string()).optional().describe('For\xE7a o destino de colunas: { "Munic\xEDpio": "Cidade", "Obs": "ignorar" }. Aceita campo padr\xE3o (Nome, Empresa, Email\u2026) ou personalizado.'),
|
|
2115
|
+
dry_run: z9.boolean().default(false).describe("S\xF3 mostra o mapeamento e os problemas, sem importar"),
|
|
2116
|
+
template_name: z9.string().max(80).optional().describe("Salva o mapeamento com este nome para as pr\xF3ximas importa\xE7\xF5es")
|
|
1812
2117
|
}
|
|
1813
2118
|
},
|
|
1814
2119
|
withApi(
|
package/package.json
CHANGED
package/skill/aicrm/SKILL.md
CHANGED
|
@@ -112,7 +112,7 @@ Como ler as respostas de erro:
|
|
|
112
112
|
| Ferramenta | Para quê |
|
|
113
113
|
|---|---|
|
|
114
114
|
| `list_workspaces` | Quem sou, workspaces, funis, etapas, KPI e vendedores. Comece por aqui. |
|
|
115
|
-
| `search_leads` | Buscar por texto, funil, etapa, responsável, status; `filter`: `mine`, `stale` (
|
|
115
|
+
| `search_leads` | Buscar por texto, funil, etapa, responsável, status; `filter`: `mine`, `stale` (passaram do prazo da etapa), `no_next_step`; `field_filters` por campo personalizado. |
|
|
116
116
|
| `get_lead` | Todos os campos (inclusive personalizados) e o histórico. |
|
|
117
117
|
| `create_lead` | Criar (nome obrigatório; demais opcionais, inclusive `custom_fields`). |
|
|
118
118
|
| `update_lead` | Alterar só o que for informado; `owner: "none"` tira o responsável; `""` limpa um campo padrão. |
|
|
@@ -126,7 +126,7 @@ Como ler as respostas de erro:
|
|
|
126
126
|
### Importação de CSV
|
|
127
127
|
| Ferramenta | Para quê |
|
|
128
128
|
|---|---|
|
|
129
|
-
| `import_leads_csv` | Importar um arquivo local. `dry_run: true` só mostra o plano; `columns` força o destino de colunas; `template_name` salva o mapeamento; `duplicate`: `skip` ou `update` (pelo email); `owner_mode`: `me`, `round_robin
|
|
129
|
+
| `import_leads_csv` | Importar um arquivo local. `dry_run: true` só mostra o plano; `columns` força o destino de colunas; `template_name` salva o mapeamento; `duplicate`: `skip` ou `update` (pelo email); `owner_mode`: `me`, `auto` (por desempenho, admin), `round_robin` (igualmente), `column`. |
|
|
130
130
|
| `rename_import_template` / `delete_import_template` | Manter os modelos salvos. |
|
|
131
131
|
|
|
132
132
|
### Campos do workspace
|
|
@@ -153,6 +153,10 @@ Como ler as respostas de erro:
|
|
|
153
153
|
| `update_user` | Nome, papel, workspaces (lista completa), ativar/desativar. |
|
|
154
154
|
| `create_workspace` / `update_workspace` / `delete_workspace` | Workspaces e visibilidade. |
|
|
155
155
|
| `create_funnel` / `update_funnel` / `delete_funnel` | Funis, etapas (com `was` para renomear sem perder leads) e KPI de meta. |
|
|
156
|
+
| `get_distribution` | Distribuição automática de leads no funil: a parte de cada vendedor e o motivo, quem não está recebendo e quantos leads esperam vendedor. Sem `funnel`, lista os bloqueios de todos os funis. |
|
|
157
|
+
| `release_seller` / `pause_seller` | Voltar a mandar (ou parar de mandar) leads novos para um vendedor no funil. Os leads que ele já tem não mudam. |
|
|
158
|
+
| `get_lead_risks` | Leads em risco: abertos que passaram do **dobro** do prazo da etapa (crítico), com valor em risco, onde trava (etapa), com quem (vendedor) e os mais atrasados; também conta quantos só passaram do prazo. Só admins. |
|
|
159
|
+
| `assign_unowned_leads` | Entregar os leads abertos sem vendedor do funil, na proporção da distribuição. |
|
|
156
160
|
|
|
157
161
|
### Conta e operação
|
|
158
162
|
| Ferramenta | Para quê |
|
|
@@ -188,6 +192,14 @@ Como ler as respostas de erro:
|
|
|
188
192
|
2. Confirme com a pessoa quantos leads e para quem.
|
|
189
193
|
3. `bulk_update_leads` com os ids e o novo `owner`. Ao desligar alguém, faça isto **antes** de `update_user` com `active: false`.
|
|
190
194
|
|
|
195
|
+
### Distribuição automática de leads (admin)
|
|
196
|
+
- Ligar: `update_funnel` com `auto_assign: true`. A partir daí, leads que um admin cria (`create_lead` sem `owner`) ou importa (`owner_mode: "auto"`) sem vendedor vão para o time na proporção do desempenho: quem fecha mais (e, em funis de receita, negócios maiores) recebe mais; quem tem pouco histórico conta como a média; ninguém fica abaixo de uma parte mínima; carteira muito cheia recebe menos. Leads criados por um vendedor continuam dele.
|
|
197
|
+
- Disciplina: todo dia útil o AICRM confere se cada vendedor com leads no funil fez **alguma ação** nele (nota, etapa, próximo passo, ganho/perda, lead novo). Quem não fez nada para de receber leads novos naquele funil até um admin liberar.
|
|
198
|
+
- "Quais leads estão parados?" / "Onde o funil trava?" → `get_lead_risks`. Cada etapa tem um **prazo em dias úteis** (`limit_days` em `update_funnel`; padrão 14). Passou do prazo, o vendedor é avisado; passou do dobro, o admin também. Só mudar de etapa zera a contagem.
|
|
199
|
+
- "Quem está parado?" (vendedor sem ação) → `get_distribution` sem funil. Explique o motivo em uma frase e pergunte se deve liberar; só então `release_seller`.
|
|
200
|
+
- Férias ou folga: `pause_seller` (confirme antes) e, na volta, `release_seller`.
|
|
201
|
+
- Leads esperando vendedor (`leads_without_owner`): `assign_unowned_leads`.
|
|
202
|
+
|
|
191
203
|
### Metas do mês
|
|
192
204
|
1. `get_goals` para ver o que já existe; `list_commission_plans` se houver comissão.
|
|
193
205
|
2. `set_goals` com os alvos (em R$ se o KPI for receita; em unidades caso contrário).
|
|
@@ -213,7 +225,7 @@ Como ler as respostas de erro:
|
|
|
213
225
|
- **Plano do dia** (`get_today_plan`):
|
|
214
226
|
- **chance**: probabilidade de o lead contar na meta — a % da etapa; em funis de "leads que chegaram à etapa", a chance de chegar à etapa‑alvo (pelo histórico do funil). Leads que já passaram da etapa‑alvo não aparecem: já contaram.
|
|
215
227
|
- **worth_for_goal**: valor × chance ("R$ 10.000 com 40% ≈ R$ 4.000"); **share_of_missing**: quanto isso representa do que falta.
|
|
216
|
-
- **Por que esta ordem**: chance × valor para a meta (no máximo o que falta) × urgência (passo atrasado ou marcado para hoje,
|
|
228
|
+
- **Por que esta ordem**: chance × valor para a meta (no máximo o que falta) × urgência (passo atrasado ou marcado para hoje, passou do prazo da etapa, data de fechamento passada ou dentro do mês, sem próximo passo). Leads com passo já marcado para outro dia ou previsão de fechar depois do mês descem na lista.
|
|
217
229
|
- **done_today**: leads em que o vendedor registrou algo hoje (nota, próximo passo, etapa, fechamento).
|
|
218
230
|
|
|
219
231
|
## 8. Problemas comuns
|