aicrm-mcp 0.2.0 → 0.2.1

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/README.md CHANGED
@@ -26,9 +26,9 @@ Outros clientes:
26
26
  | `aicrm-mcp skill [install] [--project \| --dir <pasta>]` | Imprime ou instala o SKILL.md para agentes |
27
27
  | `aicrm-mcp` | Inicia o servidor MCP (stdio) |
28
28
 
29
- ## Ferramentas (43)
29
+ ## Ferramentas (44)
30
30
 
31
- - **Leads**: `list_workspaces`, `search_leads`, `get_lead`, `create_lead`, `update_lead`, `move_lead`, `mark_won`, `mark_lost`, `reopen_lead`, `add_note`, `bulk_update_leads`, `delete_lead`
31
+ - **Leads**: `list_workspaces`, `search_leads`, `get_lead`, `create_lead`, `update_lead`, `move_lead`, `mark_won`, `mark_lost`, `reopen_lead`, `handoff_lead`, `add_note`, `bulk_update_leads`, `delete_lead`
32
32
  - **Importação**: `import_leads_csv` (`dry_run`, `columns`, `template_name`), `rename_import_template`, `delete_import_template`
33
33
  - **Campos**: `list_fields`, `create_field`, `update_field`, `archive_field`, `set_standard_fields`
34
34
  - **Metas**: `get_dashboard`, `get_forecast`, `get_goals`, `set_goals`, `list_commission_plans`, `save_commission_plan`, `delete_commission_plan`
package/dist/cli.js CHANGED
@@ -314,7 +314,9 @@ var workspaceSchema = z.object({
314
314
  var workspaceUpdateSchema = z.object({
315
315
  name: z.string().trim().min(1, "Informe um nome").max(80).optional(),
316
316
  visibility: z.enum(VISIBILITIES).optional(),
317
- hiddenFields: z.array(z.enum(HIDEABLE_FIELDS)).transform((a) => [...new Set(a)]).optional()
317
+ hiddenFields: z.array(z.enum(HIDEABLE_FIELDS)).transform((a) => [...new Set(a)]).optional(),
318
+ /** Raster data URL (PNG/JPEG/WebP), or null to remove. SVG is rejected: the browser rasterizes it first. */
319
+ logo: z.string().regex(/^data:image\/(png|jpeg|webp);base64,[A-Za-z0-9+/=]+$/, "Envie uma imagem PNG, JPG ou WebP").max(4e5, "Imagem muito grande (m\xE1x. ~300 KB)").nullable().optional()
318
320
  });
319
321
  var fieldOptions = z.array(z.string().trim().min(1).max(60)).max(100, "No m\xE1ximo 100 op\xE7\xF5es").transform((o) => [...new Set(o)]);
320
322
  var leadFieldSchema = z.object({
@@ -344,13 +346,15 @@ var funnelSchema = z.object({
344
346
  goalKpi: z.enum(GOAL_KPIS).default("revenue"),
345
347
  goalStageIndex: z.number().int().min(0).optional(),
346
348
  template: z.enum(Object.keys(FUNNEL_TEMPLATES)).optional(),
347
- stages: z.array(stageInputSchema).min(1).max(20).optional()
349
+ stages: z.array(stageInputSchema).min(1).max(20).optional(),
350
+ nextFunnelId: id.nullish()
348
351
  });
349
352
  var funnelUpdateSchema = z.object({
350
353
  name: z.string().trim().min(1).max(80).optional(),
351
354
  goalKpi: z.enum(GOAL_KPIS).optional(),
352
355
  goalStageId: id.nullable().optional(),
353
356
  stages: z.array(stageInputSchema).min(1).max(20).optional(),
357
+ nextFunnelId: id.nullable().optional(),
354
358
  archived: z.boolean().optional()
355
359
  });
356
360
  var optionalText = (max) => z.string().trim().max(max).nullish().transform((v) => v ? v : null);
@@ -375,9 +379,16 @@ var moveLeadSchema = z.object({
375
379
  funnelId: id.optional(),
376
380
  stageId: id
377
381
  });
382
+ var handoffSchema = z.object({
383
+ funnelId: id,
384
+ stageId: id.optional(),
385
+ ownerId: id.nullish(),
386
+ valueCents: z.number().int().min(0).max(1e13).optional()
387
+ });
378
388
  var leadStatusSchema = z.discriminatedUnion("status", [
379
- z.object({ status: z.literal("open") }),
380
- z.object({ status: z.literal("won"), valueCents: z.number().int().min(0).optional() }),
389
+ /** undoHandoff also deletes continuations nobody has touched yet (used by the "Desfazer" of a win). */
390
+ z.object({ status: z.literal("open"), undoHandoff: z.boolean().optional() }),
391
+ z.object({ status: z.literal("won"), valueCents: z.number().int().min(0).optional(), handoff: handoffSchema.optional() }),
381
392
  z.object({ status: z.literal("lost"), lostReason: z.enum(LOST_REASONS), note: z.string().max(500).optional() })
382
393
  ]);
383
394
  var noteSchema = z.object({ body: z.string().trim().min(1).max(5e3) });
@@ -740,6 +751,20 @@ var targetLabel = (fields, t) => {
740
751
  return p.kind === "std" ? CSV_FIELD_LABEL[p.field] : fields.find((f) => f.id === p.fieldId)?.label ?? "campo removido";
741
752
  };
742
753
  var findUser = (c, ref) => c.users.find((u) => norm(u.email) === norm(ref)) ?? resolve(c.users, ref, "Usu\xE1rio");
754
+ var isNoOwner = (ref) => ["none", "ninguem", "sem responsavel"].includes(norm(ref));
755
+ function handoffBody(c, source, a) {
756
+ const funnel = pickFunnel(c, a.funnel, a.workspace ?? source.workspaceId);
757
+ const stage = resolve(funnel.stages, a.stage, "Etapa");
758
+ return {
759
+ funnel,
760
+ body: {
761
+ funnelId: funnel.id,
762
+ ...stage ? { stageId: stage.id } : {},
763
+ ...a.owner ? { ownerId: isNoOwner(a.owner) ? null : findUser(c, a.owner).id } : {},
764
+ ...a.value != null ? { valueCents: Math.round(a.value * 100) } : {}
765
+ }
766
+ };
767
+ }
743
768
  var MONTH = /^\d{4}-\d{2}$/;
744
769
 
745
770
  // src/tools/admin.ts
@@ -981,11 +1006,33 @@ function registerLeadTools(server) {
981
1006
  return text({ reopened: leadSummary(c, lead) });
982
1007
  })
983
1008
  );
1009
+ server.registerTool(
1010
+ "handoff_lead",
1011
+ {
1012
+ title: "Enviar lead ganho para outro funil",
1013
+ 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
+ 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)")
1021
+ }
1022
+ },
1023
+ withApi(async (api, a) => {
1024
+ const c = await ctx(api);
1025
+ const source = (await api.get(`/leads/${a.lead_id}`)).lead;
1026
+ const { body } = handoffBody(c, source, a);
1027
+ const r = await api.post(`/leads/${a.lead_id}/handoff`, body);
1028
+ return text({ won: leadSummary(c, r.source), continuation: leadSummary(c, r.lead) });
1029
+ })
1030
+ );
984
1031
  server.registerTool(
985
1032
  "bulk_update_leads",
986
1033
  {
987
1034
  title: "Atualizar leads em massa",
988
- 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.",
1035
+ 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).",
989
1036
  inputSchema: {
990
1037
  lead_ids: z4.array(z4.string().uuid()).min(1).max(1e3),
991
1038
  owner: z4.string().optional().describe("Nome ou email do novo respons\xE1vel"),
@@ -1036,12 +1083,15 @@ var stageInput = z5.object({
1036
1083
  probability: z5.number().int().min(0).max(100).describe("Chance (%) de um lead nesta etapa virar venda"),
1037
1084
  was: z5.string().optional().describe("Nome atual da etapa, se voc\xEA est\xE1 renomeando uma existente")
1038
1085
  });
1039
- var describeFunnel = (f) => ({
1086
+ var describeFunnel = (f, ws) => ({
1040
1087
  name: f.name,
1041
1088
  goal_kpi: `${f.goalKpi} (${GOAL_KPI_LABEL[f.goalKpi]})`,
1042
1089
  ...f.goalKpi === "stage_reached" ? { goal_stage: f.stages.find((s) => s.id === f.goalStageId)?.name ?? null } : {},
1090
+ ...f.nextFunnelId ? { after_won_continue_in: ws?.funnels.find((x) => x.id === f.nextFunnelId)?.name ?? f.nextFunnelId } : {},
1043
1091
  stages: f.stages.map((s) => `${s.name} (${s.probability}%)`)
1044
1092
  });
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.');
1094
+ var nextFunnelId = (ws, ref) => ref === void 0 ? void 0 : ref === null || norm(ref) === "none" ? null : resolve(ws.funnels, ref, "Funil").id;
1045
1095
  function registerStructureTools(server) {
1046
1096
  server.registerTool(
1047
1097
  "create_workspace",
@@ -1102,7 +1152,8 @@ function registerStructureTools(server) {
1102
1152
  template: z5.enum(TEMPLATE_KEYS).optional(),
1103
1153
  stages: z5.array(stageInput.omit({ was: true })).min(1).max(20).optional(),
1104
1154
  goal_kpi: z5.enum(GOAL_KPIS).default("revenue"),
1105
- goal_stage: z5.string().optional().describe("Etapa que conta como meta (s\xF3 para stage_reached; padr\xE3o: a \xFAltima)")
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
1106
1157
  }
1107
1158
  },
1108
1159
  withApi(
@@ -1116,9 +1167,10 @@ function registerStructureTools(server) {
1116
1167
  name: a.name,
1117
1168
  goalKpi: a.goal_kpi,
1118
1169
  ...a.stages ? { stages: a.stages } : { template: a.template ?? "sales" },
1119
- ...goalIdx >= 0 ? { goalStageIndex: goalIdx } : {}
1170
+ ...goalIdx >= 0 ? { goalStageIndex: goalIdx } : {},
1171
+ ...a.next_funnel ? { nextFunnelId: nextFunnelId(ws, a.next_funnel) } : {}
1120
1172
  });
1121
- return text({ workspace: ws.name, created: describeFunnel(f) });
1173
+ return text({ workspace: ws.name, created: describeFunnel(f, ws) });
1122
1174
  }
1123
1175
  )
1124
1176
  );
@@ -1126,18 +1178,22 @@ function registerStructureTools(server) {
1126
1178
  "update_funnel",
1127
1179
  {
1128
1180
  title: "Alterar funil",
1129
- 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).",
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).",
1130
1182
  inputSchema: {
1131
1183
  funnel: z5.string(),
1132
1184
  workspace: z5.string().optional(),
1133
1185
  name: z5.string().min(1).max(80).optional(),
1134
1186
  goal_kpi: z5.enum(GOAL_KPIS).optional(),
1135
1187
  goal_stage: z5.string().optional(),
1136
- stages: z5.array(stageInput).min(1).max(20).optional()
1188
+ stages: z5.array(stageInput).min(1).max(20).optional(),
1189
+ next_funnel: nextFunnelArg
1137
1190
  }
1138
1191
  },
1139
1192
  withApi(async (api, a) => {
1140
- const funnel = pickFunnel(await ctx(api), a.funnel, a.workspace);
1193
+ const c = await ctx(api);
1194
+ const funnel = pickFunnel(c, a.funnel, a.workspace);
1195
+ const ws = c.workspaces.find((w) => w.id === funnel.workspaceId);
1196
+ const next = nextFunnelId(ws, a.next_funnel);
1141
1197
  const stages = a.stages?.map((s) => {
1142
1198
  const existing = funnel.stages.find((x) => norm(x.name) === norm(s.was ?? s.name));
1143
1199
  return { ...existing ? { id: existing.id } : {}, name: s.name, probability: s.probability };
@@ -1145,13 +1201,14 @@ function registerStructureTools(server) {
1145
1201
  let f = await api.patch(`/funnels/${funnel.id}`, {
1146
1202
  ...a.name ? { name: a.name } : {},
1147
1203
  ...a.goal_kpi ? { goalKpi: a.goal_kpi } : {},
1148
- ...stages ? { stages } : {}
1204
+ ...stages ? { stages } : {},
1205
+ ...next !== void 0 ? { nextFunnelId: next } : {}
1149
1206
  });
1150
1207
  if (a.goal_stage || a.goal_kpi === "stage_reached" && !f.goalStageId) {
1151
1208
  const goal = a.goal_stage ? resolve(f.stages, a.goal_stage, "Etapa") : f.stages[f.stages.length - 1];
1152
1209
  f = await api.patch(`/funnels/${funnel.id}`, { goalStageId: goal.id });
1153
1210
  }
1154
- return text({ updated: describeFunnel(f) });
1211
+ return text({ updated: describeFunnel(f, ws) });
1155
1212
  })
1156
1213
  );
1157
1214
  server.registerTool(
@@ -1281,7 +1338,7 @@ async function startServer() {
1281
1338
  const server = new McpServer(
1282
1339
  { name: "aicrm", version: "0.2.0" },
1283
1340
  {
1284
- 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. 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."
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."
1285
1342
  }
1286
1343
  );
1287
1344
  server.registerTool(
@@ -1306,6 +1363,7 @@ async function startServer() {
1306
1363
  id: f.id,
1307
1364
  goal_kpi: f.goalKpi,
1308
1365
  ...f.goalKpi === "stage_reached" ? { goal_stage: f.stages.find((s) => s.id === f.goalStageId)?.name ?? null } : {},
1366
+ ...f.nextFunnelId ? { after_won_continue_in: c.funnels.get(f.nextFunnelId)?.name ?? null } : {},
1309
1367
  stages: f.stages.map((s) => `${s.name} (${s.probability}%)`)
1310
1368
  }))
1311
1369
  })),
@@ -1379,6 +1437,8 @@ async function startServer() {
1379
1437
  source: d.lead.source,
1380
1438
  lost_reason: d.lead.lostReason,
1381
1439
  expected_close: d.lead.expectedCloseAt,
1440
+ ...d.links.source ? { came_from: { lead_id: d.links.source.id, funnel: d.links.source.funnelName, won_at: d.links.source.wonAt, value: formatMoney(d.links.source.valueCents) } } : {},
1441
+ ...d.links.continuations.length ? { continues_in: d.links.continuations.map((x) => ({ lead_id: x.id, funnel: x.funnelName, stage: x.stageName, status: x.status })) } : {},
1382
1442
  history: d.events.slice(0, 30).map((e) => ({
1383
1443
  at: e.createdAt,
1384
1444
  by: e.actorName,
@@ -1386,6 +1446,7 @@ async function startServer() {
1386
1446
  ...e.type === "note" ? { note: e.payload?.body } : {},
1387
1447
  ...e.fromStageId || e.toStageId ? { from: d.names[e.fromStageId ?? ""], to: d.names[e.toStageId ?? ""] } : {},
1388
1448
  ...e.type === "status_changed" ? { status: e.payload } : {},
1449
+ ...e.type === "handed_off" ? { to_funnel: d.names[String(e.payload?.toFunnelId ?? "")] } : {},
1389
1450
  ...e.type === "updated" && Array.isArray(e.payload?.fields) ? { changed: e.payload.fields.map(fieldLabel) } : {}
1390
1451
  }))
1391
1452
  });
@@ -1450,7 +1511,7 @@ async function startServer() {
1450
1511
  },
1451
1512
  withApi(async (api, a) => {
1452
1513
  const c = await ctx(api);
1453
- const unassign = a.owner != null && ["none", "ninguem", "sem responsavel"].includes(norm(a.owner));
1514
+ const unassign = a.owner != null && isNoOwner(a.owner);
1454
1515
  const owner = a.owner && !unassign ? findUser(c, a.owner) : void 0;
1455
1516
  let fields;
1456
1517
  if (a.custom_fields) fields = await listFields(api, (await api.get(`/leads/${a.lead_id}`)).lead.workspaceId);
@@ -1466,7 +1527,7 @@ async function startServer() {
1466
1527
  "move_lead",
1467
1528
  {
1468
1529
  title: "Mover lead",
1469
- description: "Move um lead para outra etapa \u2014 e opcionalmente para outro funil (entre workspaces: s\xF3 admins).",
1530
+ 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.",
1470
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)") }
1471
1532
  },
1472
1533
  withApi(async (api, a) => {
@@ -1482,13 +1543,34 @@ async function startServer() {
1482
1543
  "mark_won",
1483
1544
  {
1484
1545
  title: "Marcar como ganho",
1485
- description: "Marca o lead como ganho. Informe value (R$) se o valor fechado for diferente do cadastrado.",
1486
- inputSchema: { lead_id: z7.string().uuid(), value: z7.number().min(0).optional() }
1546
+ 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
+ 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)")
1554
+ }
1487
1555
  },
1488
1556
  withApi(async (api, a) => {
1489
1557
  const c = await ctx(api);
1490
- const lead = await api.post(`/leads/${a.lead_id}/status`, { status: "won", ...a.value != null ? { valueCents: Math.round(a.value * 100) } : {} });
1491
- return text({ won: leadSummary(c, lead) });
1558
+ const current = (await api.get(`/leads/${a.lead_id}`)).lead;
1559
+ const next = a.next_funnel ? handoffBody(c, current, { funnel: a.next_funnel, stage: a.next_stage, owner: a.next_owner, value: a.next_value }) : null;
1560
+ const lead = await api.post(`/leads/${a.lead_id}/status`, {
1561
+ status: "won",
1562
+ ...a.value != null ? { valueCents: Math.round(a.value * 100) } : {},
1563
+ ...next ? { handoff: next.body } : {}
1564
+ });
1565
+ if (lead.handoffLeadId) {
1566
+ const continuation = (await api.get(`/leads/${lead.handoffLeadId}`)).lead;
1567
+ return text({ won: leadSummary(c, lead), continuation: leadSummary(c, continuation) });
1568
+ }
1569
+ const suggested = c.funnels.get(c.funnels.get(lead.funnelId)?.nextFunnelId ?? "");
1570
+ return text({
1571
+ won: leadSummary(c, lead),
1572
+ ...suggested ? { tip: `Este funil sugere continuar o cliente em "${suggested.name}". Se a pessoa quiser, use handoff_lead.` } : {}
1573
+ });
1492
1574
  })
1493
1575
  );
1494
1576
  server.registerTool(
package/package.json CHANGED
@@ -1,10 +1,15 @@
1
1
  {
2
2
  "name": "aicrm-mcp",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "MCP server for AICRM — operate your CRM from Claude, Opencode and other MCP clients.",
5
5
  "type": "module",
6
- "bin": { "aicrm-mcp": "dist/cli.js" },
7
- "files": ["dist", "skill"],
6
+ "bin": {
7
+ "aicrm-mcp": "dist/cli.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "skill"
12
+ ],
8
13
  "scripts": {
9
14
  "build": "tsup",
10
15
  "dev": "tsx src/cli.ts",
@@ -23,5 +28,7 @@
23
28
  "tsx": "^4.23.15",
24
29
  "typescript": "^5.9.3"
25
30
  },
26
- "engines": { "node": ">=20" }
31
+ "engines": {
32
+ "node": ">=20"
33
+ }
27
34
  }
@@ -102,8 +102,9 @@ Como ler as respostas de erro:
102
102
  5. **Campos personalizados** vão em `custom_fields` pelo nome: `{ "Cidade": "Recife", "Porte": "Média" }`. Em `select`, use uma das opções (veja `list_fields`); `null` limpa o campo.
103
103
  6. **Ações destrutivas exigem confirmação explícita da pessoa** antes: `delete_lead`, `delete_workspace`, `delete_funnel`, `archive_field`, `delete_import_template`, `delete_commission_plan`, `revoke_tv_link`, `revoke_api_token`, e `update_user` com `active: false`. Diga o que será afetado (ex.: quantos leads) e espere o "sim".
104
104
  7. **Para tirar um lead do funil sem apagar**, prefira `mark_lost` com o motivo certo.
105
- 8. **Responda em português simples.** A pessoa pode não ter experiência com CRM nem estatística: traduza números ("~70% de chance de bater a meta"), destaque o que fazer em seguida, evite jargão e ids.
106
- 9. **Volume:** `search_leads` devolve até 200 por chamada (padrão 25); `bulk_update_leads` aceita até 1000 ids; o CSV aceita até 10.000 linhas.
105
+ 8. **Lead ganho não muda de funil.** A venda precisa continuar contando na meta e na comissão de onde foi fechada. Para levar o cliente adiante (ex.: Aquisição → Relacionamento), use `handoff_lead` (ou `next_funnel` no `mark_won`): isso cria um lead novo, aberto e ligado ao ganho, no outro funil.
106
+ 9. **Responda em português simples.** A pessoa pode não ter experiência com CRM nem estatística: traduza números ("~70% de chance de bater a meta"), destaque o que fazer em seguida, evite jargão e ids.
107
+ 10. **Volume:** `search_leads` devolve até 200 por chamada (padrão 25); `bulk_update_leads` aceita até 1000 ids; o CSV aceita até 10.000 linhas.
107
108
 
108
109
  ## 5. Ferramentas
109
110
 
@@ -115,8 +116,9 @@ Como ler as respostas de erro:
115
116
  | `get_lead` | Todos os campos (inclusive personalizados) e o histórico. |
116
117
  | `create_lead` | Criar (nome obrigatório; demais opcionais, inclusive `custom_fields`). |
117
118
  | `update_lead` | Alterar só o que for informado; `owner: "none"` tira o responsável; `""` limpa um campo padrão. |
118
- | `move_lead` | Mudar de etapa (e de funil). |
119
- | `mark_won` / `mark_lost` / `reopen_lead` | Fechar como ganho (com valor final), perdido (motivo: `price`, `timing`, `competitor`, `no_response`, `no_fit`, `other`) ou reabrir. |
119
+ | `move_lead` | Mudar de etapa (e de funil, só para leads abertos ou perdidos). |
120
+ | `mark_won` / `mark_lost` / `reopen_lead` | Fechar como ganho (com valor final; `next_funnel` já continua o cliente em outro funil), perdido (motivo: `price`, `timing`, `competitor`, `no_response`, `no_fit`, `other`) ou reabrir. |
121
+ | `handoff_lead` | Continuar um lead **ganho** em outro funil (pós-venda, relacionamento): cria um lead novo ligado ao original, na 1ª etapa (ou `stage`), valor R$ 0 (ou `value`), mesmo responsável (ou `owner`). O `get_lead` mostra o vínculo em `came_from` / `continues_in`. |
120
122
  | `add_note` | Registrar conversa, ligação, contexto. |
121
123
  | `bulk_update_leads` | Mover e/ou reatribuir vários leads de uma vez (ids do `search_leads`). |
122
124
  | `delete_lead` | Apagar com histórico (admin, destrutivo). |
@@ -176,6 +178,8 @@ Como ler as respostas de erro:
176
178
 
177
179
  ### Fechar negócio
178
180
  - Ganho: `mark_won` (com `value` se o valor final mudou). Perdido: `mark_lost` com o motivo mais próximo e uma `note` curta. Errou? `reopen_lead`.
181
+ - Depois do ganho: se o funil tiver um funil seguinte (`after_won_continue_in` no `list_workspaces`, ou o `tip` na resposta do `mark_won`), pergunte se o cliente deve seguir para lá e use `handoff_lead`. Se a pessoa já pediu ("fechou, manda pro Relacionamento"), faça tudo de uma vez com `mark_won` + `next_funnel`.
182
+ - Para configurar o caminho padrão (admin): `update_funnel` com `next_funnel: "Relacionamento"`.
179
183
 
180
184
  ### Redistribuir carteira (férias, saída, rodízio)
181
185
  1. `search_leads` com `owner` e `status: "open"` (aumente `limit` se precisar).