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 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 z7 } from "zod";
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([api.get("/workspaces"), api.get("/users")]);
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
- return { workspaces, users, stages, funnels, userName: (id2) => users.find((u) => u.id === id2)?.name ?? null };
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
- days_in_stage: Math.floor((Date.now() - Date.parse(l.stageEnteredAt)) / 864e5),
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/goals.ts
981
+ // src/tools/distribution.ts
867
982
  import { z as z3 } from "zod";
868
- var describePlan = (p) => ({
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(describePlan)))
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: z3.string().min(1).max(80),
891
- kind: z3.enum(["percent", "per_unit"]),
892
- tiers: z3.array(z3.object({ from_attainment_pct: z3.number().min(0).max(1e3), rate: z3.number().min(0) })).min(1).max(10).describe("Ex.: [{from_attainment_pct: 0, rate: 3}, {from_attainment_pct: 100, rate: 6}]")
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"]: describePlan(plan) });
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: z3.string() },
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: z3.string().optional(), workspace: z3.string().optional(), month: z3.string().regex(MONTH).optional().describe("YYYY-MM; padr\xE3o: m\xEAs atual") },
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: z3.array(z3.object({ seller: z3.string().describe("Nome ou email"), target: z3.number().min(0) })).min(1),
955
- funnel: z3.string().optional(),
956
- workspace: z3.string().optional(),
957
- month: z3.string().regex(MONTH).optional().describe("YYYY-MM; padr\xE3o: m\xEAs atual"),
958
- commission_plan: z3.string().optional()
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 z4 } from "zod";
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: z4.string().uuid() }
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: z4.string().uuid().describe("Lead ganho"),
1016
- funnel: z4.string().describe("Funil onde o cliente continua (nome ou id)"),
1017
- stage: z4.string().optional().describe("Etapa de entrada (padr\xE3o: a primeira)"),
1018
- workspace: z4.string().optional().describe("Workspace do funil, se for outro (s\xF3 admins)"),
1019
- owner: z4.string().optional().describe('Respons\xE1vel no novo funil (nome ou email; "none" = sem respons\xE1vel). Padr\xE3o: o mesmo do lead ganho'),
1020
- value: z4.number().min(0).optional().describe("Valor em reais no novo funil (padr\xE3o: 0)")
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: z4.array(z4.string().uuid()).min(1).max(1e3),
1038
- owner: z4.string().optional().describe("Nome ou email do novo respons\xE1vel"),
1039
- stage: z4.string().optional(),
1040
- funnel: z4.string().optional()
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: z4.string().uuid() },
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 z5 } from "zod";
1315
+ import { z as z7 } from "zod";
1080
1316
  var TEMPLATE_KEYS = Object.keys(FUNNEL_TEMPLATES);
1081
- var stageInput = z5.object({
1082
- name: z5.string().min(1).max(60),
1083
- probability: z5.number().int().min(0).max(100).describe("Chance (%) de um lead nesta etapa virar venda"),
1084
- was: z5.string().optional().describe("Nome atual da etapa, se voc\xEA est\xE1 renomeando uma existente")
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
- stages: f.stages.map((s) => `${s.name} (${s.probability}%)`)
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 nextFunnelArg = z5.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.');
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: z5.string().min(1).max(80),
1103
- visibility: z5.enum(["all", "own"]).default("all").describe("all = todos veem todos os leads; own = cada vendedor v\xEA s\xF3 os seus"),
1104
- create_funnel: z5.boolean().default(true)
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: z5.string(),
1120
- name: z5.string().min(1).max(80).optional(),
1121
- visibility: z5.enum(["all", "own"]).optional().describe("all = todos veem todos os leads; own = cada vendedor v\xEA s\xF3 os seus")
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: z5.string() },
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: z5.string().min(1).max(80),
1151
- workspace: z5.string().optional(),
1152
- template: z5.enum(TEMPLATE_KEYS).optional(),
1153
- stages: z5.array(stageInput.omit({ was: true })).min(1).max(20).optional(),
1154
- goal_kpi: z5.enum(GOAL_KPIS).default("revenue"),
1155
- goal_stage: z5.string().optional().describe("Etapa que conta como meta (s\xF3 para stage_reached; padr\xE3o: a \xFAltima)"),
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: z5.string(),
1184
- workspace: z5.string().optional(),
1185
- name: z5.string().min(1).max(80).optional(),
1186
- goal_kpi: z5.enum(GOAL_KPIS).optional(),
1187
- goal_stage: z5.string().optional(),
1188
- stages: z5.array(stageInput).min(1).max(20).optional(),
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 { ...existing ? { id: existing.id } : {}, name: s.name, probability: s.probability };
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: z5.string(), workspace: z5.string().optional() },
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 z6 } from "zod";
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: z6.boolean().default(false) },
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: z6.string().min(2),
1263
- email: z6.string().email(),
1264
- role: z6.enum(["member", "admin"]).default("member"),
1265
- workspaces: z6.array(z6.string()).optional().describe("Nomes dos workspaces (padr\xE3o: o primeiro)")
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: z6.string().describe("Nome ou email") }
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: z6.string().describe("Nome ou email"),
1301
- name: z6.string().min(2).optional(),
1302
- role: z6.enum(["member", "admin"]).optional(),
1303
- active: z6.boolean().optional(),
1304
- workspaces: z6.array(z6.string()).optional().describe("Lista completa de workspaces da pessoa (substitui a atual)")
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.2.0" },
1593
+ { name: "aicrm", version: "0.4.0" },
1340
1594
  {
1341
- 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). 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. 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."
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
- stages: f.stages.map((s) => `${s.name} (${s.probability}%)`)
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 (parados h\xE1 14+ dias), no_next_step (sem pr\xF3ximo passo).",
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: z7.string().optional().describe("Texto em nome, empresa ou email"),
1381
- workspace: z7.string().optional(),
1382
- funnel: z7.string().optional(),
1383
- stage: z7.string().optional(),
1384
- owner: z7.string().optional().describe("Nome ou email do respons\xE1vel"),
1385
- status: z7.enum(["open", "won", "lost"]).optional(),
1386
- filter: z7.enum(["mine", "stale", "no_next_step"]).optional(),
1387
- field_filters: z7.record(z7.string(), z7.string()).optional().describe('Filtra por campos personalizados: { "Cidade": "Recife" }. Texto: cont\xE9m; demais tipos: igual (datas em dd/mm/aaaa, sim/n\xE3o).'),
1388
- limit: z7.number().int().min(1).max(200).default(25)
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: z7.string().uuid() },
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: z7.string().optional(),
1457
- company: z7.string().optional(),
1458
- email: z7.string().optional(),
1459
- phone: z7.string().optional(),
1460
- title: z7.string().optional(),
1461
- value: z7.number().min(0).optional().describe("Valor do neg\xF3cio em reais"),
1462
- source: z7.enum(LEAD_SOURCES).optional(),
1463
- next_step: z7.string().optional(),
1464
- next_step_date: z7.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
1465
- expected_close: z7.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
1466
- custom_fields: z7.record(z7.string(), z7.union([z7.string(), z7.number(), z7.boolean(), z7.null()])).optional().describe('Campos personalizados do workspace pelo nome: { "Cidade": "Recife", "Faturamento": 50000 }. Dinheiro em reais; null limpa o campo. Veja list_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: z7.string(), workspace: z7.string().optional(), funnel: z7.string().optional(), stage: z7.string().optional(), owner: z7.string().optional() }
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: z7.string().uuid(), ...leadFields, owner: z7.string().optional() }
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: z7.string().uuid(), stage: z7.string().describe("Nome ou id da etapa de destino"), funnel: z7.string().optional().describe("Funil de destino (padr\xE3o: o atual)") }
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: z7.string().uuid(),
1549
- value: z7.number().min(0).optional(),
1550
- next_funnel: z7.string().optional().describe("Funil onde o cliente continua depois do ganho"),
1551
- next_stage: z7.string().optional().describe("Etapa de entrada no pr\xF3ximo funil (padr\xE3o: a primeira)"),
1552
- next_owner: z7.string().optional().describe("Respons\xE1vel no pr\xF3ximo funil (padr\xE3o: o mesmo)"),
1553
- next_value: z7.number().min(0).optional().describe("Valor em reais no pr\xF3ximo funil (padr\xE3o: 0)")
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: z7.string().uuid(), reason: z7.enum(LOST_REASONS), note: z7.string().optional() }
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: z7.string().uuid(), note: z7.string().min(1) }
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: z7.string().optional(), workspace: z7.string().optional(), month: z7.string().regex(/^\d{4}-\d{2}$/).optional().describe("YYYY-MM; padr\xE3o: m\xEAs atual") },
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: z7.string().describe("Nome ou email do vendedor"), funnel: z7.string().optional(), month: z7.string().regex(/^\d{4}-\d{2}$/).optional() },
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: z7.string().optional() },
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: z7.string().min(1).max(60).describe("Nome do campo, ex.: Cidade"),
1699
- type: z7.enum(CUSTOM_FIELD_TYPES),
1700
- options: z7.array(z7.string()).optional().describe("Op\xE7\xF5es da lista (tipo select)"),
1701
- workspace: z7.string().optional()
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: z7.string().describe("Nome ou id do campo"),
1718
- label: z7.string().min(1).max(60).optional(),
1719
- options: z7.array(z7.string()).optional().describe("Lista completa de op\xE7\xF5es (substitui a atual)"),
1720
- workspace: z7.string().optional()
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: z7.string().describe("Nome ou id do campo"), workspace: z7.string().optional() },
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: z7.array(z7.enum(HIDEABLE_FIELDS)).optional(),
1754
- show: z7.array(z7.enum(HIDEABLE_FIELDS)).optional(),
1755
- workspace: z7.string().optional()
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: z7.string().describe("Nome ou id do modelo"), name: z7.string().min(1).max(80), workspace: z7.string().optional() }
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: z7.string().describe("Nome ou id do modelo"), workspace: z7.string().optional() },
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: z7.string().describe("Caminho do arquivo CSV (aceita ~)"),
1804
- funnel: z7.string().optional(),
1805
- workspace: z7.string().optional(),
1806
- stage: z7.string().optional(),
1807
- owner_mode: z7.enum(["me", "round_robin", "column"]).default("me").describe("me = voc\xEA; round_robin = distribuir; column = coluna 'respons\xE1vel' (email)"),
1808
- duplicate: z7.enum(["skip", "update"]).default("skip"),
1809
- columns: z7.record(z7.string(), z7.string()).optional().describe('For\xE7a o destino de colunas: { "Munic\xEDpio": "Cidade", "Obs": "ignorar" }. Aceita campo padr\xE3o (Nome, Empresa, Email\u2026) ou personalizado.'),
1810
- dry_run: z7.boolean().default(false).describe("S\xF3 mostra o mapeamento e os problemas, sem importar"),
1811
- template_name: z7.string().max(80).optional().describe("Salva o mapeamento com este nome para as pr\xF3ximas importa\xE7\xF5es")
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aicrm-mcp",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "MCP server for AICRM — operate your CRM from Claude, Opencode and other MCP clients.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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` (parados 14+ dias), `no_next_step`; `field_filters` por campo personalizado. |
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`, `column`. |
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, parado há 14+ dias, 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.
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