aicrm-mcp 0.4.0 → 0.6.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
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
4
+ import { mkdir as mkdir2, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
5
5
  import { homedir as homedir3, hostname } from "os";
6
- import path3 from "path";
6
+ import path4 from "path";
7
7
 
8
8
  // src/client.ts
9
9
  var ApiError = class extends Error {
@@ -16,8 +16,8 @@ var ApiError = class extends Error {
16
16
  function normalizeUrl(url) {
17
17
  return url.replace(/\/+$/, "").replace(/\/api$/, "");
18
18
  }
19
- async function call(base, method, path4, body, token) {
20
- const res = await fetch(`${normalizeUrl(base)}/api${path4}`, {
19
+ async function call(base, method, path5, body, token) {
20
+ const res = await fetch(`${normalizeUrl(base)}/api${path5}`, {
21
21
  method,
22
22
  headers: {
23
23
  ...body !== void 0 ? { "content-type": "application/json" } : {},
@@ -125,12 +125,13 @@ function askHidden(question) {
125
125
  }
126
126
 
127
127
  // src/server.ts
128
- import { readFile as readFile2 } from "fs/promises";
128
+ import { readFile as readFile3 } from "fs/promises";
129
129
  import { homedir as homedir2 } from "os";
130
- import path2 from "path";
130
+ import path3 from "path";
131
131
 
132
132
  // ../shared/src/constants.ts
133
- var ROLES = ["admin", "member"];
133
+ var ROLES = ["admin", "manager", "member", "developer"];
134
+ var LEAD_CHANNELS = ["web", "import", "api"];
134
135
  var LEAD_STATUSES = ["open", "won", "lost"];
135
136
  var LOST_REASONS = ["price", "timing", "competitor", "no_response", "no_fit", "other"];
136
137
  var LEAD_SOURCES = ["inbound", "outbound", "referral", "event", "partner", "other"];
@@ -310,6 +311,11 @@ var updateUserSchema = z.object({
310
311
  active: z.boolean().optional(),
311
312
  workspaceIds: z.array(id).optional()
312
313
  });
314
+ var imageDataUrl = 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)");
315
+ var profileUpdateSchema = z.object({
316
+ /** Photo as a data URL, or null to go back to the initials. */
317
+ avatar: imageDataUrl.nullable()
318
+ });
313
319
  var workspaceSchema = z.object({
314
320
  name: z.string().trim().min(1, "Informe um nome").max(80),
315
321
  visibility: z.enum(VISIBILITIES).default("all")
@@ -318,19 +324,23 @@ var workspaceUpdateSchema = z.object({
318
324
  name: z.string().trim().min(1, "Informe um nome").max(80).optional(),
319
325
  visibility: z.enum(VISIBILITIES).optional(),
320
326
  hiddenFields: z.array(z.enum(HIDEABLE_FIELDS)).transform((a) => [...new Set(a)]).optional(),
321
- /** Raster data URL (PNG/JPEG/WebP), or null to remove. SVG is rejected: the browser rasterizes it first. */
322
- 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()
327
+ /** Standard fields every new lead must fill. */
328
+ requiredFields: z.array(z.enum(HIDEABLE_FIELDS)).transform((a) => [...new Set(a)]).optional(),
329
+ /** Raster data URL, or null to remove. */
330
+ logo: imageDataUrl.nullable().optional()
323
331
  });
324
332
  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)]);
325
333
  var leadFieldSchema = z.object({
326
334
  label: z.string().trim().min(1, "D\xEA um nome ao campo").max(60),
327
335
  type: z.enum(CUSTOM_FIELD_TYPES),
328
- options: fieldOptions.default([])
336
+ options: fieldOptions.default([]),
337
+ required: z.boolean().default(false)
329
338
  }).refine((f) => f.type !== "select" || f.options.length > 0, { message: "Adicione pelo menos uma op\xE7\xE3o", path: ["options"] });
330
339
  var leadFieldUpdateSchema = z.object({
331
340
  label: z.string().trim().min(1).max(60).optional(),
332
341
  options: fieldOptions.optional(),
333
- position: z.number().optional()
342
+ position: z.number().optional(),
343
+ required: z.boolean().optional()
334
344
  });
335
345
  var customInput = z.record(z.string().uuid(), z.union([z.string().max(2e3), z.number(), z.boolean(), z.null()]));
336
346
  var importTargetSchema = z.custom(
@@ -381,7 +391,7 @@ var leadInputSchema = z.object({
381
391
  expectedCloseAt: dateStr.nullish(),
382
392
  custom: customInput.optional()
383
393
  });
384
- var leadUpdateSchema = leadInputSchema.omit({ funnelId: true, stageId: true }).partial();
394
+ var leadUpdateSchema = leadInputSchema.omit({ funnelId: true, stageId: true }).extend({ valueCents: z.number().int().min(0).max(1e13) }).partial();
385
395
  var moveLeadSchema = z.object({
386
396
  funnelId: id.optional(),
387
397
  stageId: id
@@ -407,6 +417,8 @@ var leadQuerySchema = z.object({
407
417
  status: z.enum(LEAD_STATUSES).optional(),
408
418
  q: z.string().max(120).optional(),
409
419
  filter: z.enum(["mine", "stale", "no_next_step"]).optional(),
420
+ /** How leads got in: typed, imported or through the API. */
421
+ createdVia: z.enum(LEAD_CHANNELS).optional(),
410
422
  limit: z.coerce.number().int().min(1).max(1e3).default(500)
411
423
  });
412
424
  var bulkLeadSchema = z.object({
@@ -415,6 +427,9 @@ var bulkLeadSchema = z.object({
415
427
  stageId: id.optional(),
416
428
  funnelId: id.optional()
417
429
  });
430
+ var bulkDeleteSchema = z.object({
431
+ leadIds: z.array(id).min(1).max(1e3)
432
+ });
418
433
  var importSchema = z.object({
419
434
  funnelId: id,
420
435
  stageId: id,
@@ -440,6 +455,11 @@ var importSchema = z.object({
440
455
  id: id.optional(),
441
456
  name: z.string().trim().min(1).max(80),
442
457
  columns: z.record(z.string().max(200), importTargetSchema)
458
+ }).optional(),
459
+ /** Where the data came from, kept in the lead's history so people can see e.g. which column filled "Valor". */
460
+ origin: z.object({
461
+ fileName: z.string().max(200).optional(),
462
+ valueColumn: z.string().max(200).optional()
443
463
  }).optional()
444
464
  });
445
465
  var commissionTierSchema = z.object({
@@ -471,6 +491,64 @@ var tvLinkSchema = z.object({
471
491
  funnelId: id,
472
492
  name: z.string().trim().min(1).max(80)
473
493
  });
494
+ var API_KEY_KINDS = ["secret", "publishable"];
495
+ var API_KEY_MODES = ["live", "test"];
496
+ var origin = z.string().trim().min(1).max(200).transform((v, ctx2) => {
497
+ try {
498
+ return new URL(/^[a-z]+:\/\//i.test(v) ? v : `https://${v}`).origin;
499
+ } catch {
500
+ ctx2.addIssue({ code: "custom", message: `Dom\xEDnio inv\xE1lido: ${v}` });
501
+ return z.NEVER;
502
+ }
503
+ });
504
+ var apiKeyCreateSchema = z.object({
505
+ name: z.string().trim().min(1, "D\xEA um nome para a chave").max(60),
506
+ kind: z.enum(API_KEY_KINDS),
507
+ mode: z.enum(API_KEY_MODES),
508
+ /** Workspaces the key reaches (secret keys). Empty = every workspace the creator reaches. */
509
+ workspaceIds: z.array(id).default([]),
510
+ /** The only funnel a publishable key creates leads in. */
511
+ funnelId: id.optional(),
512
+ /** Sites allowed to use a publishable key. Empty = any site. */
513
+ allowedOrigins: z.array(origin).max(20).default([])
514
+ }).refine((k) => k.kind !== "publishable" || k.funnelId, { message: "Escolha o funil onde a chave do site cria leads", path: ["funnelId"] });
515
+ var apiScalar = z.union([z.string().max(2e3), z.number(), z.boolean(), z.null()]);
516
+ var apiText = (max) => z.string().trim().max(max).nullish();
517
+ var apiLeadFields = {
518
+ name: z.string().trim().min(1, "Informe o nome").max(160),
519
+ email: z.string().trim().toLowerCase().email("Email inv\xE1lido").max(200).nullish().or(z.literal("").transform(() => null)),
520
+ phone: apiText(40),
521
+ company: apiText(160),
522
+ title: apiText(120),
523
+ value: z.union([z.number().min(0).max(1e11), z.string().max(40)]).nullish(),
524
+ source: z.enum(LEAD_SOURCES).nullish(),
525
+ owner: z.string().trim().toLowerCase().email("Use o email de quem vai cuidar do lead").nullish(),
526
+ nextStep: apiText(200),
527
+ nextStepAt: dateStr.nullish(),
528
+ expectedCloseAt: dateStr.nullish(),
529
+ /** Custom fields by label ("Cidade") or id. */
530
+ fields: z.record(z.string().max(100), apiScalar).optional(),
531
+ /** Free extra data (UTMs, landing page…), at most ~8 KB. */
532
+ metadata: z.record(z.string().max(100), z.unknown()).refine((m) => JSON.stringify(m).length <= 8e3, "metadata muito grande (m\xE1x. 8 KB)").optional()
533
+ };
534
+ var apiLeadCreateSchema = z.object({
535
+ /** Funnel by name or id. Publishable keys may leave it out: they always use their own funnel. */
536
+ funnel: z.string().trim().min(1).max(120).optional(),
537
+ /** Stage by name or id; defaults to the first stage. */
538
+ stage: z.string().trim().max(120).optional(),
539
+ /** Only needed when two workspaces have a funnel with the same name. */
540
+ workspace: z.string().trim().max(120).optional(),
541
+ /** Id of the lead in your system: sending it again updates the same lead. */
542
+ externalId: z.string().trim().min(1).max(200).optional(),
543
+ ...apiLeadFields
544
+ });
545
+ var apiLeadUpdateSchema = z.object({
546
+ ...apiLeadFields,
547
+ name: apiLeadFields.name.optional(),
548
+ stage: z.string().trim().max(120).optional(),
549
+ status: z.enum(LEAD_STATUSES).optional(),
550
+ lostReason: z.enum(LOST_REASONS).optional()
551
+ }).refine((u) => u.status !== "lost" || u.lostReason, { message: "Informe lostReason ao marcar como perdido", path: ["lostReason"] });
474
552
  var distributionPauseSchema = z.object({
475
553
  funnelId: id,
476
554
  userId: id
@@ -632,6 +710,31 @@ function guessFieldType(samples) {
632
710
  return { type: "text", options: [] };
633
711
  }
634
712
 
713
+ // ../shared/src/api-errors.ts
714
+ var API_ERROR_HELP = {
715
+ missing_api_key: 'Mande a chave no cabe\xE7alho: Authorization: Bearer aicrm_sk_\u2026. Com o SDK, basta new AICRM("sua-chave").',
716
+ invalid_api_key: "A chave est\xE1 errada ou foi revogada. Copie de novo ou gere uma nova em Chaves de API.",
717
+ secret_key_in_browser: "Chaves secretas n\xE3o podem ir para o site, porque qualquer visitante veria a chave. Crie uma chave do tipo \u201CNo site\u201D (aicrm_pk_) e use ela no navegador.",
718
+ origin_not_allowed: "Inclua este dom\xEDnio na chave do site, ou crie uma chave nova com o dom\xEDnio certo (ex.: https://seusite.com.br).",
719
+ publishable_key_not_allowed: "Chaves do site s\xF3 criam leads. Para ler ou alterar, use uma chave secreta no seu servidor.",
720
+ funnel_required: 'Diga em qual funil o lead entra: { funnel: "Vendas" }. Os nomes est\xE3o em Funis e campos.',
721
+ funnel_not_found: "Confira o nome do funil em Funis e campos. Mai\xFAsculas e acentos n\xE3o importam. Se o funil \xE9 de outro workspace, a chave precisa ter acesso a ele.",
722
+ funnel_not_allowed: "A chave do site s\xF3 cria leads no funil dela. Tire o campo funnel ou use a chave certa.",
723
+ ambiguous_funnel: 'H\xE1 dois funis com esse nome em workspaces diferentes. Mande tamb\xE9m workspace: "Nome do workspace", ou use o id do funil.',
724
+ workspace_not_found: "Confira o nome do workspace em Funis e campos.",
725
+ stage_not_found: "Use um dos nomes de etapa listados na mensagem, ou tire o campo stage para cair na primeira etapa.",
726
+ field_not_found: "Use o nome do campo exatamente como aparece em Funis e campos (mai\xFAsculas e acentos n\xE3o importam).",
727
+ required_field_missing: "O admin marcou este campo como obrigat\xF3rio. Mande o valor dele no lead.",
728
+ invalid_field: "O valor n\xE3o combina com o tipo do campo. Veja o exemplo na mensagem.",
729
+ invalid_value: 'Mande o valor em reais, como n\xFAmero (1500) ou texto ("R$ 1.500,00").',
730
+ invalid_request: "Algum dado veio no formato errado. A mensagem diz qual.",
731
+ owner_not_found: "Use o email de algu\xE9m do time que trabalha nesse workspace, ou tire o owner para a distribui\xE7\xE3o decidir.",
732
+ lead_not_found: "A API s\xF3 enxerga leads criados pela API, e com uma chave do mesmo modo (teste ou produ\xE7\xE3o).",
733
+ rate_limited: "Muitas requisi\xE7\xF5es em pouco tempo. Espere um minuto. Para cargas grandes, importe uma planilha.",
734
+ not_found: "Essa rota n\xE3o existe. Veja as rotas em Documenta\xE7\xE3o.",
735
+ internal_error: "Erro do nosso lado. Tente de novo em instantes."
736
+ };
737
+
635
738
  // ../shared/src/stats/calendar.ts
636
739
  function toDateStr(d) {
637
740
  return d.toISOString().slice(0, 10);
@@ -736,7 +839,7 @@ function stageAge(stageEnteredAt, limitDays, today, timeZone) {
736
839
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
737
840
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
738
841
  import Papa from "papaparse";
739
- import { z as z9 } from "zod";
842
+ import { z as z10 } from "zod";
740
843
 
741
844
  // src/lib.ts
742
845
  var norm = (s) => s.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase().trim();
@@ -843,7 +946,7 @@ var withApi = (fn) => async (args) => {
843
946
  try {
844
947
  return await fn(apiFor(creds), args);
845
948
  } catch (e) {
846
- const msg = e instanceof ApiError && e.status === 401 ? "Sess\xE3o expirada ou revogada. Rode: npx -y aicrm-mcp login" : e instanceof ApiError && e.status === 403 ? `${e.message} (seu usu\xE1rio n\xE3o tem permiss\xE3o para isso \u2014 pe\xE7a a um admin)` : e.message;
949
+ const msg = e instanceof ApiError && e.status === 401 ? "Sess\xE3o expirada ou revogada. Rode: npx -y aicrm-mcp login" : e instanceof ApiError && e.status === 403 ? `${e.message} (seu usu\xE1rio n\xE3o tem permiss\xE3o para isso \u2014 pe\xE7a a um admin ou Master)` : e.message;
847
950
  return { ...text(`Erro: ${msg}`), isError: true };
848
951
  }
849
952
  };
@@ -882,8 +985,301 @@ function handoffBody(c, source, a) {
882
985
  }
883
986
  var MONTH = /^\d{4}-\d{2}$/;
884
987
 
885
- // src/tools/admin.ts
988
+ // src/tools/integrate.ts
886
989
  import { z as z2 } from "zod";
990
+
991
+ // src/env-file.ts
992
+ import { appendFile, readFile as readFile2, stat, writeFile as writeFile2 } from "fs/promises";
993
+ import path2 from "path";
994
+ function isEnvFileName(file) {
995
+ const base = path2.basename(file);
996
+ return /^\.env(\.[\w.-]+)?$/.test(base) || /\.env$/.test(base);
997
+ }
998
+ var escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
999
+ function upsertEnv(content, vars) {
1000
+ let out = content;
1001
+ const replaced = [];
1002
+ for (const [key, value] of Object.entries(vars)) {
1003
+ const line = new RegExp(`^[ \\t]*(export[ \\t]+)?${escapeRe(key)}[ \\t]*=.*$`, "m");
1004
+ if (line.test(out)) {
1005
+ out = out.replace(line, `${key}=${value}`);
1006
+ replaced.push(key);
1007
+ } else {
1008
+ out = `${out}${out && !out.endsWith("\n") ? "\n" : ""}${key}=${value}
1009
+ `;
1010
+ }
1011
+ }
1012
+ return { content: out, replaced };
1013
+ }
1014
+ function gitignoreCovers(gitignore, fileName) {
1015
+ return gitignore.split(/\r?\n/).map((l) => l.trim()).filter((l) => l && !l.startsWith("#") && !l.startsWith("!")).some((l) => {
1016
+ const p = l.replace(/^\//, "").replace(/\/$/, "");
1017
+ if (p === fileName) return true;
1018
+ if (!p.includes("*")) return false;
1019
+ return new RegExp(`^${p.split("*").map(escapeRe).join(".*")}$`).test(fileName);
1020
+ });
1021
+ }
1022
+ var exists = (p) => stat(p).then(() => true, () => false);
1023
+ async function gitRoot(dir2) {
1024
+ let d = dir2;
1025
+ for (; ; ) {
1026
+ if (await exists(path2.join(d, ".git"))) return d;
1027
+ const up = path2.dirname(d);
1028
+ if (up === d) return null;
1029
+ d = up;
1030
+ }
1031
+ }
1032
+ async function writeEnvVars(file, vars, cwd = process.cwd()) {
1033
+ const target = path2.resolve(cwd, file);
1034
+ if (!isEnvFileName(target)) throw new Error(`"${file}" n\xE3o parece um arquivo .env. Use algo como .env ou .env.local.`);
1035
+ if (!await exists(path2.dirname(target))) throw new Error(`A pasta ${path2.dirname(target)} n\xE3o existe.`);
1036
+ const created = !await exists(target);
1037
+ const current = created ? "" : await readFile2(target, "utf8");
1038
+ const { content, replaced } = upsertEnv(current, vars);
1039
+ await writeFile2(target, content, created ? { mode: 384 } : void 0);
1040
+ const name = path2.basename(target);
1041
+ const root = await gitRoot(path2.dirname(target));
1042
+ let gitignore = "Fora de um reposit\xF3rio git: nada a ignorar.";
1043
+ if (root) {
1044
+ const candidates = [path2.join(path2.dirname(target), ".gitignore"), path2.join(root, ".gitignore")];
1045
+ let covered = false;
1046
+ for (const g of candidates) if (await exists(g)) covered ||= gitignoreCovers(await readFile2(g, "utf8"), name);
1047
+ if (covered) gitignore = `${name} j\xE1 est\xE1 no .gitignore.`;
1048
+ else {
1049
+ const g = path2.join(path2.dirname(target), ".gitignore");
1050
+ const before = await exists(g) ? await readFile2(g, "utf8") : "";
1051
+ await appendFile(g, `${before && !before.endsWith("\n") ? "\n" : ""}${name}
1052
+ `);
1053
+ gitignore = `Adicionei ${name} ao ${path2.relative(cwd, g) || ".gitignore"} para a chave n\xE3o ir para o git.`;
1054
+ }
1055
+ }
1056
+ return { path: target, created, replaced, gitignore };
1057
+ }
1058
+
1059
+ // src/tools/integrate.ts
1060
+ var SDK_DEFAULT_URL = "https://api.aicrm.dev";
1061
+ var schema = (api) => api.get("/integrations/schema");
1062
+ function pickFunnel2(workspaces, ref, workspaceRef) {
1063
+ const wss = workspaceRef ? [resolve(workspaces, workspaceRef, "Workspace")] : workspaces;
1064
+ const funnels = wss.flatMap((w) => w.funnels);
1065
+ if (!funnels.length) throw new Error("Voc\xEA n\xE3o tem acesso a nenhum funil. Pe\xE7a a um admin para incluir voc\xEA em um workspace com funil.");
1066
+ return ref ? resolve(funnels, ref, "Funil") : funnels[0];
1067
+ }
1068
+ var describe = (f) => ({
1069
+ funnel: f.name,
1070
+ funnel_id: f.id,
1071
+ workspace: f.workspace.name,
1072
+ stages: f.stages.map((s) => s.name),
1073
+ default_stage: f.stages[0]?.name ?? null,
1074
+ standard_fields: f.fields.standard.map((x) => ({ key: x.key, label: x.label, type: x.type, required: x.required, ...x.options?.length ? { options: x.options } : {} })),
1075
+ custom_fields: f.fields.custom.map((x) => ({ key: `fields.${x.key}`, label: x.label, type: x.type, required: x.required, ...x.options?.length ? { options: x.options } : {} })),
1076
+ example_payload: { ...f.example, funnel: f.name }
1077
+ });
1078
+ var help = (code) => code ? API_ERROR_HELP[code] ?? null : null;
1079
+ var logOut = (l) => ({
1080
+ id: l.id,
1081
+ when: l.createdAt,
1082
+ request: `${l.method} ${l.path}`,
1083
+ status: l.status,
1084
+ key: l.keyName ? `${l.keyName} (${l.keyMode === "test" ? "teste" : "produ\xE7\xE3o"})` : null,
1085
+ ms: l.durationMs,
1086
+ ...l.errorCode ? { error_code: l.errorCode, how_to_fix: help(l.errorCode) } : {}
1087
+ });
1088
+ async function createKeyIntoEnv(api, a, cwd = process.cwd()) {
1089
+ const kind = a.kind ?? "secret";
1090
+ const mode = a.mode ?? "test";
1091
+ const envFile = a.env_file ?? ".env";
1092
+ if (mode === "live" && !a.confirm_live)
1093
+ throw new Error("Chave de produ\xE7\xE3o cria leads de verdade para o time. Confirme com a pessoa e chame de novo com confirm_live: true.");
1094
+ if (!isEnvFileName(envFile)) throw new Error(`"${envFile}" n\xE3o parece um arquivo .env. Use algo como .env ou .env.local.`);
1095
+ const envVar = a.env_var ?? (kind === "secret" ? "AICRM_API_KEY" : "AICRM_PUBLISHABLE_KEY");
1096
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(envVar)) throw new Error(`Nome de vari\xE1vel inv\xE1lido: ${envVar}`);
1097
+ const [workspaces, config] = await Promise.all([schema(api), api.get("/integrations/config")]);
1098
+ const body = { name: a.name ?? (kind === "secret" ? "Servidor" : "Site"), kind, mode };
1099
+ if (kind === "publishable") {
1100
+ body.funnelId = pickFunnel2(workspaces, a.funnel).id;
1101
+ body.allowedOrigins = a.allowed_origins ?? [];
1102
+ } else if (a.workspaces?.length) body.workspaceIds = a.workspaces.map((w) => resolve(workspaces, w, "Workspace").id);
1103
+ const key = await api.post("/integrations/keys", body);
1104
+ const vars = { [envVar]: key.token };
1105
+ const url = config.apiUrl !== SDK_DEFAULT_URL ? config.apiUrl : null;
1106
+ if (url && kind === "secret") vars.AICRM_API_URL = url;
1107
+ try {
1108
+ const written = await writeEnvVars(envFile, vars, cwd);
1109
+ return {
1110
+ key: { name: key.name, kind, mode, prefix: `${key.prefix}\u2026` },
1111
+ env_file: written.path,
1112
+ variables: Object.keys(vars),
1113
+ ...written.replaced.length ? { replaced: written.replaced } : {},
1114
+ gitignore: written.gitignore,
1115
+ ...url && kind === "publishable" ? { api_url: url } : {},
1116
+ how_to_use: kind === "secret" ? `No servidor: new AICRM(${envVar === "AICRM_API_KEY" ? "" : `process.env.${envVar}`}) \u2014 o SDK l\xEA ${envVar === "AICRM_API_KEY" ? "AICRM_API_KEY" : "a chave do argumento"}${url ? " e AICRM_API_URL" : ""} sozinho. Nunca leve esta chave para o c\xF3digo do navegador.` : `No site: new AICRM(<${envVar}>${url ? `, { baseUrl: "${url}" }` : ""}). Em Vite use o prefixo VITE_, em Next.js NEXT_PUBLIC_ (passe env_var). A chave p\xFAblica s\xF3 cria leads no funil dela.`,
1117
+ note: "A chave completa foi gravada s\xF3 no arquivo; ela n\xE3o aparece nesta conversa."
1118
+ };
1119
+ } catch (e) {
1120
+ await api.del(`/integrations/keys/${key.id}`).catch(() => {
1121
+ });
1122
+ throw e;
1123
+ }
1124
+ }
1125
+ function registerIntegrationTools(server) {
1126
+ server.registerTool(
1127
+ "get_integration_schema",
1128
+ {
1129
+ title: "Funis, etapas e campos para integrar",
1130
+ description: "Mostra para onde os leads podem ir: workspaces, funis, etapas e os campos de cada lead (padr\xE3o e personalizados), com quais s\xE3o obrigat\xF3rios e um payload de exemplo pronto para leads.create(). Use antes de escrever o c\xF3digo da integra\xE7\xE3o.",
1131
+ inputSchema: { funnel: z2.string().optional().describe("Nome ou id do funil (sem ele, lista todos)"), workspace: z2.string().optional() },
1132
+ annotations: { readOnlyHint: true }
1133
+ },
1134
+ withApi(async (api, a) => {
1135
+ const wss = await schema(api);
1136
+ if (a.funnel) return text(describe(pickFunnel2(wss, a.funnel, a.workspace)));
1137
+ return text({
1138
+ workspaces: wss.map((w) => ({ workspace: w.name, funnels: w.funnels.map(describe) })),
1139
+ tips: [
1140
+ "Funil, etapa e campos aceitam nome ou id; mai\xFAsculas e acentos n\xE3o importam.",
1141
+ 'Campos personalizados v\xE3o em fields, pelo nome: fields: { Cidade: "Recife" }. Dinheiro em reais.',
1142
+ "Use externalId com o id do lead no sistema da pessoa: reenvios atualizam em vez de duplicar."
1143
+ ]
1144
+ });
1145
+ })
1146
+ );
1147
+ server.registerTool(
1148
+ "create_api_key",
1149
+ {
1150
+ title: "Criar chave de API no .env",
1151
+ description: "Cria uma chave da API p\xFAblica e grava direto no arquivo .env do projeto (e garante que o .env est\xE1 no .gitignore). A chave completa nunca volta para voc\xEA: s\xF3 o prefixo. secret (aicrm_sk_) \xE9 para o servidor; publishable (aicrm_pk_) \xE9 para o navegador e s\xF3 cria leads num funil. Comece com mode test. Chave de produ\xE7\xE3o (mode live) s\xF3 com o ok expl\xEDcito da pessoa e confirm_live: true.",
1152
+ inputSchema: {
1153
+ name: z2.string().max(60).optional().describe("Ex.: Checkout, Site institucional"),
1154
+ kind: z2.enum(["secret", "publishable"]).optional().describe("secret (padr\xE3o) no servidor; publishable no navegador"),
1155
+ mode: z2.enum(["test", "live"]).optional().describe("test (padr\xE3o): leads com selo Teste, fora das metas"),
1156
+ funnel: z2.string().optional().describe("Obrigat\xF3rio para publishable: o \xFAnico funil onde a chave cria leads"),
1157
+ workspaces: z2.array(z2.string()).optional().describe("Workspaces que a chave secreta alcan\xE7a (padr\xE3o: todos os seus)"),
1158
+ allowed_origins: z2.array(z2.string()).optional().describe('Dom\xEDnios que podem usar a chave publishable, ex.: ["seusite.com.br"]'),
1159
+ env_file: z2.string().optional().describe("Arquivo .env relativo \xE0 pasta do projeto (padr\xE3o .env)"),
1160
+ env_var: z2.string().optional().describe("Nome da vari\xE1vel (padr\xE3o AICRM_API_KEY ou AICRM_PUBLISHABLE_KEY)"),
1161
+ confirm_live: z2.boolean().optional().describe("true s\xF3 depois que a pessoa confirmou que quer uma chave de produ\xE7\xE3o")
1162
+ }
1163
+ },
1164
+ withApi(async (api, a) => text(await createKeyIntoEnv(api, a)))
1165
+ );
1166
+ server.registerTool(
1167
+ "send_test_lead",
1168
+ {
1169
+ title: "Mandar um lead de teste",
1170
+ description: "Manda o payload de exemplo do funil com a sua chave de teste mais recente, como o c\xF3digo faria, e mostra em que etapa o lead caiu (ou o erro e como corrigir). \xDAtil para conferir a estrutura antes e depois de escrever o c\xF3digo.",
1171
+ inputSchema: { funnel: z2.string().optional(), workspace: z2.string().optional() }
1172
+ },
1173
+ withApi(async (api, a) => {
1174
+ const funnel = pickFunnel2(await schema(api), a.funnel, a.workspace);
1175
+ const r = await api.post(
1176
+ "/integrations/test-event",
1177
+ { funnelId: funnel.id }
1178
+ );
1179
+ if (r.status >= 300) return text({ ok: false, status: r.status, error: r.response.error, how_to_fix: help(r.response.error?.code ?? null) });
1180
+ return text({ ok: true, landed_in: `${r.response.funnel?.name} \u203A ${r.response.stage?.name}`, sent: r.request, lead: r.response });
1181
+ })
1182
+ );
1183
+ server.registerTool(
1184
+ "list_api_logs",
1185
+ {
1186
+ title: "Logs da API",
1187
+ description: "\xDAltimas chamadas \xE0 API feitas com as suas chaves: status, rota e, nos erros, o c\xF3digo e como corrigir. Rode depois de testar o c\xF3digo para ver se o lead entrou ou por que falhou.",
1188
+ inputSchema: { errors_only: z2.boolean().optional(), limit: z2.number().int().min(1).max(100).optional() },
1189
+ annotations: { readOnlyHint: true }
1190
+ },
1191
+ withApi(async (api, a) => {
1192
+ const logs = await api.get(`/integrations/logs?limit=${a.limit ?? 20}${a.errors_only ? "&errors=1" : ""}`);
1193
+ return text(logs.length ? logs.map(logOut) : "Nenhuma chamada ainda. Rode o c\xF3digo (ou send_test_lead) e veja de novo.");
1194
+ })
1195
+ );
1196
+ server.registerTool(
1197
+ "get_api_log",
1198
+ {
1199
+ title: "Detalhe de uma chamada",
1200
+ description: "O que foi enviado e o que a API respondeu numa chamada (id vem de list_api_logs).",
1201
+ inputSchema: { id: z2.string() },
1202
+ annotations: { readOnlyHint: true }
1203
+ },
1204
+ withApi(async (api, a) => {
1205
+ const l = await api.get(`/integrations/logs/${a.id}`);
1206
+ return text({ ...logOut(l), sent: l.requestBody, response: l.responseBody });
1207
+ })
1208
+ );
1209
+ server.registerTool(
1210
+ "get_integration_status",
1211
+ {
1212
+ title: "Onde est\xE1 a integra\xE7\xE3o",
1213
+ description: "Mostra o que j\xE1 foi feito (chave de teste, primeiro lead, chave de produ\xE7\xE3o) e o pr\xF3ximo passo.",
1214
+ inputSchema: {},
1215
+ annotations: { readOnlyHint: true }
1216
+ },
1217
+ withApi(async (api) => {
1218
+ const s = await api.get("/integrations/status");
1219
+ const next = !s.hasTestKey ? "Crie uma chave de teste com create_api_key." : !s.firstLead ? "Escreva o c\xF3digo com o SDK @aicrm/sdk e rode; confira em list_api_logs." : !s.hasLiveKey ? "Quando a pessoa aprovar, crie a chave de produ\xE7\xE3o (create_api_key mode live) e troque no ambiente de produ\xE7\xE3o." : "Integra\xE7\xE3o completa. Se quiser, limpe os leads de teste com clear_test_leads.";
1220
+ return text({
1221
+ test_key: s.hasTestKey,
1222
+ first_lead: s.firstLead ? `${s.firstLead.name} em ${s.firstLead.funnelName} \u203A ${s.firstLead.stageName}${s.firstLead.isTest ? " (teste)" : ""}` : null,
1223
+ live_key: s.hasLiveKey,
1224
+ last_request: s.lastRequest ? logOut(s.lastRequest) : null,
1225
+ next_step: next
1226
+ });
1227
+ })
1228
+ );
1229
+ server.registerTool(
1230
+ "list_api_keys",
1231
+ {
1232
+ title: "Chaves de API",
1233
+ description: "Suas chaves ativas (o admin v\xEA as da empresa), com tipo, modo e \xFAltimo uso. S\xF3 o prefixo: a chave completa s\xF3 aparece ao criar.",
1234
+ inputSchema: {},
1235
+ annotations: { readOnlyHint: true }
1236
+ },
1237
+ withApi(
1238
+ async (api) => text(
1239
+ (await api.get("/integrations/keys")).map((k) => ({
1240
+ name: k.name,
1241
+ kind: k.kind,
1242
+ mode: k.mode,
1243
+ prefix: `${k.prefix}\u2026`,
1244
+ last_used: k.lastUsedAt,
1245
+ ...k.kind === "publishable" ? { allowed_origins: k.allowedOrigins.length ? k.allowedOrigins : "qualquer site" } : {},
1246
+ ...k.createdByName ? { created_by: k.createdByName } : {}
1247
+ }))
1248
+ )
1249
+ )
1250
+ );
1251
+ server.registerTool(
1252
+ "revoke_api_key",
1253
+ {
1254
+ title: "Revogar chave de API",
1255
+ description: "Desativa uma chave na hora: o sistema que a usa para de mandar leads. Pe\xE7a confirma\xE7\xE3o antes.",
1256
+ inputSchema: { key: z2.string().describe("Nome ou prefixo da chave") },
1257
+ annotations: { destructiveHint: true }
1258
+ },
1259
+ withApi(async (api, a) => {
1260
+ const keys = await api.get("/integrations/keys");
1261
+ const k = keys.find((x) => norm(x.prefix).startsWith(norm(a.key.replace(/…$/, "")))) ?? resolve(keys, a.key, "Chave");
1262
+ await api.del(`/integrations/keys/${k.id}`);
1263
+ return text(`Chave ${k.name} (${k.prefix}\u2026) revogada.`);
1264
+ })
1265
+ );
1266
+ server.registerTool(
1267
+ "clear_test_leads",
1268
+ {
1269
+ title: "Apagar leads de teste",
1270
+ description: "Tira do funil os leads criados com as suas chaves de teste. Leads reais n\xE3o s\xE3o afetados. Pe\xE7a confirma\xE7\xE3o antes.",
1271
+ inputSchema: {},
1272
+ annotations: { destructiveHint: true }
1273
+ },
1274
+ withApi(async (api) => {
1275
+ const r = await api.del("/integrations/test-leads");
1276
+ return text(r.deleted ? `${r.deleted} lead(s) de teste apagados.` : "N\xE3o havia leads de teste.");
1277
+ })
1278
+ );
1279
+ }
1280
+
1281
+ // src/tools/admin.ts
1282
+ import { z as z3 } from "zod";
887
1283
  function registerAdminTools(server) {
888
1284
  server.registerTool(
889
1285
  "get_setup_status",
@@ -909,7 +1305,7 @@ function registerAdminTools(server) {
909
1305
  "list_tv_links",
910
1306
  {
911
1307
  title: "Links do modo TV",
912
- description: "Lista os links ativos do modo TV (dashboard ao vivo sem login), s\xF3 admins.",
1308
+ description: "Lista os links ativos do modo TV (dashboard ao vivo sem login), s\xF3 admins e Masters (dos funis deles).",
913
1309
  inputSchema: {},
914
1310
  annotations: { readOnlyHint: true }
915
1311
  },
@@ -921,8 +1317,8 @@ function registerAdminTools(server) {
921
1317
  "create_tv_link",
922
1318
  {
923
1319
  title: "Criar link do modo TV",
924
- description: "Cria um link do dashboard ao vivo de um funil para abrir numa TV, sem login (s\xF3 admins). Quem tiver o link v\xEA o dashboard \u2014 trate como senha.",
925
- inputSchema: { name: z2.string().min(1).max(80).describe("Ex.: TV da sala de vendas"), funnel: z2.string().optional(), workspace: z2.string().optional() }
1320
+ description: "Cria um link do dashboard ao vivo de um funil para abrir numa TV, sem login (admins e Masters). Quem tiver o link v\xEA o dashboard \u2014 trate como senha.",
1321
+ inputSchema: { name: z3.string().min(1).max(80).describe("Ex.: TV da sala de vendas"), funnel: z3.string().optional(), workspace: z3.string().optional() }
926
1322
  },
927
1323
  withApi(async (api, a) => {
928
1324
  const funnel = pickFunnel(await ctx(api), a.funnel, a.workspace);
@@ -934,8 +1330,8 @@ function registerAdminTools(server) {
934
1330
  "revoke_tv_link",
935
1331
  {
936
1332
  title: "Revogar link do modo TV",
937
- description: "Desativa um link do modo TV (s\xF3 admins); a TV que usava o link para de mostrar o dashboard.",
938
- inputSchema: { link: z2.string().describe("Nome do link") },
1333
+ description: "Desativa um link do modo TV (admins e Masters); a TV que usava o link para de mostrar o dashboard.",
1334
+ inputSchema: { link: z3.string().describe("Nome do link") },
939
1335
  annotations: { destructiveHint: true }
940
1336
  },
941
1337
  withApi(async (api, a) => {
@@ -968,7 +1364,7 @@ function registerAdminTools(server) {
968
1364
  {
969
1365
  title: "Revogar token de API",
970
1366
  description: "Revoga um token de API seu. Se for o token que este MCP est\xE1 usando, a conex\xE3o para de funcionar at\xE9 um novo login.",
971
- inputSchema: { token_id: z2.string().uuid() },
1367
+ inputSchema: { token_id: z3.string().uuid() },
972
1368
  annotations: { destructiveHint: true }
973
1369
  },
974
1370
  withApi(async (api, a) => {
@@ -979,9 +1375,9 @@ function registerAdminTools(server) {
979
1375
  }
980
1376
 
981
1377
  // src/tools/distribution.ts
982
- import { z as z3 } from "zod";
1378
+ import { z as z4 } from "zod";
983
1379
  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"}`;
1380
+ var why = (b) => b.reason === "manual" ? "pausado pelo Master ou admin" : `sem nenhuma a\xE7\xE3o no funil em ${b.day ? br(b.day) : "um dia \xFAtil"}`;
985
1381
  var describePlan = (funnel, p) => ({
986
1382
  funnel,
987
1383
  automatic_distribution: p.enabled ? "ligada" : "desligada (ligue com update_funnel auto_assign=true)",
@@ -994,13 +1390,13 @@ var describePlan = (funnel, p) => ({
994
1390
  not_receiving: p.blocked.map((b) => ({ seller: b.name, why: why(b) })),
995
1391
  leads_without_owner: p.unowned
996
1392
  });
997
- var funnelArgs = { funnel: z3.string().optional(), workspace: z3.string().optional() };
1393
+ var funnelArgs = { funnel: z4.string().optional(), workspace: z4.string().optional() };
998
1394
  function registerDistributionTools(server) {
999
1395
  server.registerTool(
1000
1396
  "get_distribution",
1001
1397
  {
1002
1398
  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.",
1399
+ 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 Master/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 e Masters.",
1004
1400
  inputSchema: funnelArgs,
1005
1401
  annotations: { readOnlyHint: true }
1006
1402
  },
@@ -1019,8 +1415,8 @@ function registerDistributionTools(server) {
1019
1415
  "release_seller",
1020
1416
  {
1021
1417
  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 }
1418
+ description: "Volta a mandar leads novos para um vendedor que foi bloqueado (dia sem a\xE7\xE3o) ou pausado no funil. S\xF3 admins e Masters.",
1419
+ inputSchema: { seller: z4.string().describe("Nome ou email"), ...funnelArgs }
1024
1420
  },
1025
1421
  withApi(async (api, a) => {
1026
1422
  const c = await ctx(api);
@@ -1034,8 +1430,8 @@ function registerDistributionTools(server) {
1034
1430
  "pause_seller",
1035
1431
  {
1036
1432
  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 }
1433
+ 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 e Masters. Confirme com a pessoa antes.",
1434
+ inputSchema: { seller: z4.string().describe("Nome ou email"), ...funnelArgs }
1039
1435
  },
1040
1436
  withApi(async (api, a) => {
1041
1437
  const c = await ctx(api);
@@ -1049,7 +1445,7 @@ function registerDistributionTools(server) {
1049
1445
  "assign_unowned_leads",
1050
1446
  {
1051
1447
  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.",
1448
+ 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 e Masters. Admins e Masters nunca recebem.",
1053
1449
  inputSchema: funnelArgs
1054
1450
  },
1055
1451
  withApi(async (api, a) => {
@@ -1061,7 +1457,7 @@ function registerDistributionTools(server) {
1061
1457
  }
1062
1458
 
1063
1459
  // src/tools/goals.ts
1064
- import { z as z4 } from "zod";
1460
+ import { z as z5 } from "zod";
1065
1461
  var describePlan2 = (p) => ({
1066
1462
  name: p.name,
1067
1463
  kind: p.kind === "percent" ? "% sobre a receita (metas de receita)" : "R$ por unidade (metas de quantidade)",
@@ -1084,9 +1480,9 @@ function registerGoalTools(server) {
1084
1480
  title: "Criar ou alterar plano de comiss\xE3o",
1085
1481
  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).",
1086
1482
  inputSchema: {
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}]")
1483
+ name: z5.string().min(1).max(80),
1484
+ kind: z5.enum(["percent", "per_unit"]),
1485
+ tiers: z5.array(z5.object({ from_attainment_pct: z5.number().min(0).max(1e3), rate: z5.number().min(0) })).min(1).max(10).describe("Ex.: [{from_attainment_pct: 0, rate: 3}, {from_attainment_pct: 100, rate: 6}]")
1090
1486
  }
1091
1487
  },
1092
1488
  withApi(async (api, a) => {
@@ -1105,7 +1501,7 @@ function registerGoalTools(server) {
1105
1501
  {
1106
1502
  title: "Apagar plano de comiss\xE3o",
1107
1503
  description: "Apaga um plano de comiss\xE3o (s\xF3 admins). As metas que o usavam ficam sem comiss\xE3o.",
1108
- inputSchema: { plan: z4.string() },
1504
+ inputSchema: { plan: z5.string() },
1109
1505
  annotations: { destructiveHint: true }
1110
1506
  },
1111
1507
  withApi(async (api, a) => {
@@ -1119,7 +1515,7 @@ function registerGoalTools(server) {
1119
1515
  {
1120
1516
  title: "Metas do m\xEAs",
1121
1517
  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.",
1122
- inputSchema: { funnel: z4.string().optional(), workspace: z4.string().optional(), month: z4.string().regex(MONTH).optional().describe("YYYY-MM; padr\xE3o: m\xEAs atual") },
1518
+ inputSchema: { funnel: z5.string().optional(), workspace: z5.string().optional(), month: z5.string().regex(MONTH).optional().describe("YYYY-MM; padr\xE3o: m\xEAs atual") },
1123
1519
  annotations: { readOnlyHint: true }
1124
1520
  },
1125
1521
  withApi(async (api, a) => {
@@ -1146,13 +1542,13 @@ function registerGoalTools(server) {
1146
1542
  "set_goals",
1147
1543
  {
1148
1544
  title: "Definir metas",
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.',
1545
+ description: 'Define ou altera a meta de vendedores num funil e m\xEAs (admins e Masters; s\xF3 admins escolhem o plano de comiss\xE3o). 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.',
1150
1546
  inputSchema: {
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()
1547
+ targets: z5.array(z5.object({ seller: z5.string().describe("Nome ou email"), target: z5.number().min(0) })).min(1),
1548
+ funnel: z5.string().optional(),
1549
+ workspace: z5.string().optional(),
1550
+ month: z5.string().regex(MONTH).optional().describe("YYYY-MM; padr\xE3o: m\xEAs atual"),
1551
+ commission_plan: z5.string().optional()
1156
1552
  }
1157
1553
  },
1158
1554
  withApi(
@@ -1188,14 +1584,14 @@ function registerGoalTools(server) {
1188
1584
  }
1189
1585
 
1190
1586
  // src/tools/leads.ts
1191
- import { z as z5 } from "zod";
1587
+ import { z as z6 } from "zod";
1192
1588
  function registerLeadTools(server) {
1193
1589
  server.registerTool(
1194
1590
  "reopen_lead",
1195
1591
  {
1196
1592
  title: "Reabrir lead",
1197
1593
  description: "Volta um lead ganho ou perdido para aberto, na mesma etapa.",
1198
- inputSchema: { lead_id: z5.string().uuid() }
1594
+ inputSchema: { lead_id: z6.string().uuid() }
1199
1595
  },
1200
1596
  withApi(async (api, a) => {
1201
1597
  const c = await ctx(api);
@@ -1209,12 +1605,12 @@ function registerLeadTools(server) {
1209
1605
  title: "Enviar lead ganho para outro funil",
1210
1606
  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.",
1211
1607
  inputSchema: {
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)")
1608
+ lead_id: z6.string().uuid().describe("Lead ganho"),
1609
+ funnel: z6.string().describe("Funil onde o cliente continua (nome ou id)"),
1610
+ stage: z6.string().optional().describe("Etapa de entrada (padr\xE3o: a primeira)"),
1611
+ workspace: z6.string().optional().describe("Workspace do funil, se for outro (s\xF3 admins e Masters)"),
1612
+ owner: z6.string().optional().describe('Respons\xE1vel no novo funil (nome ou email; "none" = sem respons\xE1vel). Padr\xE3o: o mesmo do lead ganho'),
1613
+ value: z6.number().min(0).optional().describe("Valor em reais no novo funil (padr\xE3o: 0)")
1218
1614
  }
1219
1615
  },
1220
1616
  withApi(async (api, a) => {
@@ -1229,12 +1625,12 @@ function registerLeadTools(server) {
1229
1625
  "bulk_update_leads",
1230
1626
  {
1231
1627
  title: "Atualizar leads em massa",
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).",
1628
+ description: "Reatribui (s\xF3 admins e Masters) 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).",
1233
1629
  inputSchema: {
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()
1630
+ lead_ids: z6.array(z6.string().uuid()).min(1).max(1e3),
1631
+ owner: z6.string().optional().describe("Nome ou email do novo respons\xE1vel"),
1632
+ stage: z6.string().optional(),
1633
+ funnel: z6.string().optional()
1238
1634
  }
1239
1635
  },
1240
1636
  withApi(async (api, a) => {
@@ -1260,8 +1656,8 @@ function registerLeadTools(server) {
1260
1656
  "delete_lead",
1261
1657
  {
1262
1658
  title: "Excluir lead",
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.",
1264
- inputSchema: { lead_id: z5.string().uuid() },
1659
+ description: "Apaga um lead e todo o hist\xF3rico dele (s\xF3 admins e Masters). N\xE3o d\xE1 para desfazer \u2014 confirme com a pessoa antes. Para tirar do funil sem apagar, prefira mark_lost.",
1660
+ inputSchema: { lead_id: z6.string().uuid() },
1265
1661
  annotations: { destructiveHint: true }
1266
1662
  },
1267
1663
  withApi(async (api, a) => {
@@ -1273,14 +1669,14 @@ function registerLeadTools(server) {
1273
1669
  }
1274
1670
 
1275
1671
  // src/tools/risks.ts
1276
- import { z as z6 } from "zod";
1672
+ import { z as z7 } from "zod";
1277
1673
  function registerRiskTools(server) {
1278
1674
  server.registerTool(
1279
1675
  "get_lead_risks",
1280
1676
  {
1281
1677
  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() },
1678
+ 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 Master e o admin s\xE3o avisados. 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 e Masters.",
1679
+ inputSchema: { workspace: z7.string().optional() },
1284
1680
  annotations: { readOnlyHint: true }
1285
1681
  },
1286
1682
  withApi(async (api, a) => {
@@ -1312,15 +1708,15 @@ function registerRiskTools(server) {
1312
1708
  }
1313
1709
 
1314
1710
  // src/tools/structure.ts
1315
- import { z as z7 } from "zod";
1711
+ import { z as z8 } from "zod";
1316
1712
  var TEMPLATE_KEYS = Object.keys(FUNNEL_TEMPLATES);
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.`
1713
+ var stageInput = z8.object({
1714
+ name: z8.string().min(1).max(60),
1715
+ probability: z8.number().int().min(0).max(100).describe("Chance (%) de um lead nesta etapa virar venda"),
1716
+ limit_days: z8.number().int().min(1).max(365).nullish().describe(
1717
+ `Prazo da etapa em dias \xFAteis: passou dele, o vendedor \xE9 avisado; passou do dobro, o Master e o admin tamb\xE9m. null volta ao padr\xE3o (${DEFAULT_STAGE_LIMIT_DAYS}); omitido mant\xE9m o atual.`
1322
1718
  ),
1323
- was: z7.string().optional().describe("Nome atual da etapa, se voc\xEA est\xE1 renomeando uma existente")
1719
+ was: z8.string().optional().describe("Nome atual da etapa, se voc\xEA est\xE1 renomeando uma existente")
1324
1720
  });
1325
1721
  var describeFunnel = (f, ws) => ({
1326
1722
  name: f.name,
@@ -1330,10 +1726,10 @@ var describeFunnel = (f, ws) => ({
1330
1726
  auto_assign: !!f.autoAssign,
1331
1727
  stages: f.stages.map((s) => `${s.name} (${s.probability}%, prazo ${s.limitDays ?? DEFAULT_STAGE_LIMIT_DAYS} dias \xFAteis${s.limitDays ? "" : " \u2014 padr\xE3o"})`)
1332
1728
  });
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)."
1729
+ var autoAssignArg = z8.boolean().optional().describe(
1730
+ "Distribui\xE7\xE3o autom\xE1tica: leads cadastrados ou importados por admins/Masters sem vendedor v\xE3o, em maior parte, para quem est\xE1 fechando mais (todo vendedor recebe; admins e Masters nunca). Quem passa um dia \xFAtil sem nenhuma a\xE7\xE3o no funil para de receber at\xE9 um Master ou admin liberar (release_seller)."
1335
1731
  );
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.');
1732
+ var nextFunnelArg = z8.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.');
1337
1733
  var nextFunnelId = (ws, ref) => ref === void 0 ? void 0 : ref === null || norm(ref) === "none" ? null : resolve(ws.funnels, ref, "Funil").id;
1338
1734
  function registerStructureTools(server) {
1339
1735
  server.registerTool(
@@ -1342,9 +1738,9 @@ function registerStructureTools(server) {
1342
1738
  title: "Criar workspace",
1343
1739
  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.",
1344
1740
  inputSchema: {
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)
1741
+ name: z8.string().min(1).max(80),
1742
+ visibility: z8.enum(["all", "own"]).default("all").describe("all = todos veem todos os leads; own = cada vendedor v\xEA s\xF3 os seus"),
1743
+ create_funnel: z8.boolean().default(true)
1348
1744
  }
1349
1745
  },
1350
1746
  withApi(async (api, a) => {
@@ -1359,9 +1755,9 @@ function registerStructureTools(server) {
1359
1755
  title: "Alterar workspace",
1360
1756
  description: "Renomeia um workspace ou muda quem v\xEA quais leads (s\xF3 admins).",
1361
1757
  inputSchema: {
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")
1758
+ workspace: z8.string(),
1759
+ name: z8.string().min(1).max(80).optional(),
1760
+ visibility: z8.enum(["all", "own"]).optional().describe("all = todos veem todos os leads; own = cada vendedor v\xEA s\xF3 os seus")
1365
1761
  }
1366
1762
  },
1367
1763
  withApi(async (api, a) => {
@@ -1375,7 +1771,7 @@ function registerStructureTools(server) {
1375
1771
  {
1376
1772
  title: "Excluir workspace",
1377
1773
  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.",
1378
- inputSchema: { workspace: z7.string() },
1774
+ inputSchema: { workspace: z8.string() },
1379
1775
  annotations: { destructiveHint: true }
1380
1776
  },
1381
1777
  withApi(async (api, a) => {
@@ -1390,12 +1786,12 @@ function registerStructureTools(server) {
1390
1786
  title: "Criar funil",
1391
1787
  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).`,
1392
1788
  inputSchema: {
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)"),
1789
+ name: z8.string().min(1).max(80),
1790
+ workspace: z8.string().optional(),
1791
+ template: z8.enum(TEMPLATE_KEYS).optional(),
1792
+ stages: z8.array(stageInput.omit({ was: true })).min(1).max(20).optional(),
1793
+ goal_kpi: z8.enum(GOAL_KPIS).default("revenue"),
1794
+ goal_stage: z8.string().optional().describe("Etapa que conta como meta (s\xF3 para stage_reached; padr\xE3o: a \xFAltima)"),
1399
1795
  next_funnel: nextFunnelArg,
1400
1796
  auto_assign: autoAssignArg
1401
1797
  }
@@ -1425,12 +1821,12 @@ function registerStructureTools(server) {
1425
1821
  title: "Alterar funil",
1426
1822
  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.",
1427
1823
  inputSchema: {
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(),
1824
+ funnel: z8.string(),
1825
+ workspace: z8.string().optional(),
1826
+ name: z8.string().min(1).max(80).optional(),
1827
+ goal_kpi: z8.enum(GOAL_KPIS).optional(),
1828
+ goal_stage: z8.string().optional(),
1829
+ stages: z8.array(stageInput).min(1).max(20).optional(),
1434
1830
  next_funnel: nextFunnelArg,
1435
1831
  auto_assign: autoAssignArg
1436
1832
  }
@@ -1468,7 +1864,7 @@ function registerStructureTools(server) {
1468
1864
  {
1469
1865
  title: "Excluir funil",
1470
1866
  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.",
1471
- inputSchema: { funnel: z7.string(), workspace: z7.string().optional() },
1867
+ inputSchema: { funnel: z8.string(), workspace: z8.string().optional() },
1472
1868
  annotations: { destructiveHint: true }
1473
1869
  },
1474
1870
  withApi(async (api, a) => {
@@ -1480,15 +1876,15 @@ function registerStructureTools(server) {
1480
1876
  }
1481
1877
 
1482
1878
  // src/tools/team.ts
1483
- import { z as z8 } from "zod";
1879
+ import { z as z9 } from "zod";
1484
1880
  var wsNames = (c, ids = []) => ids.map((id2) => c.workspaces.find((w) => w.id === id2)?.name ?? id2);
1485
1881
  function registerTeamTools(server) {
1486
1882
  server.registerTool(
1487
1883
  "list_users",
1488
1884
  {
1489
1885
  title: "Listar usu\xE1rios",
1490
- description: "Lista as pessoas da organiza\xE7\xE3o: papel (admin/member), se est\xE1 ativa, convite pendente e workspaces de cada uma.",
1491
- inputSchema: { include_inactive: z8.boolean().default(false) },
1886
+ description: "Lista as pessoas da organiza\xE7\xE3o: papel (admin, manager = Master, member = vendedor), se est\xE1 ativa, convite pendente e workspaces de cada uma.",
1887
+ inputSchema: { include_inactive: z9.boolean().default(false) },
1492
1888
  annotations: { readOnlyHint: true }
1493
1889
  },
1494
1890
  withApi(async (api, a) => {
@@ -1509,12 +1905,12 @@ function registerTeamTools(server) {
1509
1905
  "invite_user",
1510
1906
  {
1511
1907
  title: "Convidar usu\xE1rio",
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.",
1908
+ description: "Convida uma pessoa para a organiza\xE7\xE3o (admins; Masters s\xF3 convidam vendedores para os workspaces deles) 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 e recebe leads distribu\xEDdos); manager = Master (lidera o time nos workspaces dele: v\xEA todos os leads, importa e distribui, define metas, convida vendedores; n\xE3o recebe leads automaticamente); admin = v\xEA e configura tudo.",
1513
1909
  inputSchema: {
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)")
1910
+ name: z9.string().min(2),
1911
+ email: z9.string().email(),
1912
+ role: z9.enum(["member", "manager", "admin"]).default("member"),
1913
+ workspaces: z9.array(z9.string()).optional().describe("Nomes dos workspaces (padr\xE3o: o primeiro)")
1518
1914
  }
1519
1915
  },
1520
1916
  withApi(async (api, a) => {
@@ -1533,8 +1929,8 @@ function registerTeamTools(server) {
1533
1929
  "resend_invite",
1534
1930
  {
1535
1931
  title: "Gerar novo link de convite",
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.",
1537
- inputSchema: { user: z8.string().describe("Nome ou email") }
1932
+ description: "Gera um novo link de convite para algu\xE9m (admins; Masters s\xF3 para vendedores do time) \u2014 \xFAtil se o anterior expirou. O link antigo deixa de valer.",
1933
+ inputSchema: { user: z9.string().describe("Nome ou email") }
1538
1934
  },
1539
1935
  withApi(async (api, a) => {
1540
1936
  const c = await ctx(api);
@@ -1547,13 +1943,13 @@ function registerTeamTools(server) {
1547
1943
  "update_user",
1548
1944
  {
1549
1945
  title: "Alterar usu\xE1rio",
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).",
1946
+ description: "Muda nome, papel, workspaces ou desativa/reativa algu\xE9m (admins; Masters s\xF3 ajustam workspaces e ativa\xE7\xE3o de vendedores do time, sem mudar papel). Desativar encerra as sess\xF5es e tokens da pessoa; os leads continuam com ela at\xE9 serem reatribu\xEDdos (bulk_update_leads).",
1551
1947
  inputSchema: {
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)")
1948
+ user: z9.string().describe("Nome ou email"),
1949
+ name: z9.string().min(2).optional(),
1950
+ role: z9.enum(["member", "manager", "admin"]).optional(),
1951
+ active: z9.boolean().optional(),
1952
+ workspaces: z9.array(z9.string()).optional().describe("Lista completa de workspaces da pessoa (substitui a atual)")
1557
1953
  }
1558
1954
  },
1559
1955
  withApi(async (api, a) => {
@@ -1588,11 +1984,25 @@ function registerManageTools(server) {
1588
1984
  }
1589
1985
 
1590
1986
  // src/server.ts
1987
+ var DEVELOPER_INSTRUCTIONS = "Voc\xEA est\xE1 conectado ao AICRM como Desenvolvedor: seu trabalho \xE9 integrar o sistema desta pessoa ao CRM pela API p\xFAblica, com o SDK @aicrm/sdk. Voc\xEA n\xE3o v\xEA leads nem o time, s\xF3 a estrutura (get_integration_schema), suas chaves e os logs. Roteiro: 1) get_integration_schema; 2) ache no c\xF3digo de onde v\xEAm os leads e mostre \xE0 pessoa o mapeamento campo \u2192 campo do CRM (inclusive os obrigat\xF3rios) antes de escrever; 3) create_api_key em modo test, que grava a chave no .env sem mostr\xE1-la; 4) npm i @aicrm/sdk e escreva o c\xF3digo com externalId; 5) rode e confira em list_api_logs, corrigindo pelos how_to_fix; 6) s\xF3 com o ok da pessoa, create_api_key mode live (confirm_live) e clear_test_leads. Chave secreta (aicrm_sk_) nunca vai para o navegador: no site use publishable. Se a skill aicrm-integrate estiver instalada, siga-a.";
1988
+ async function currentRole() {
1989
+ const creds = await loadCredentials();
1990
+ if (!creds) return "";
1991
+ if (creds.user.role) return creds.user.role;
1992
+ return apiFor(creds).get("/auth/me").then((m) => m.user.role, () => "");
1993
+ }
1591
1994
  async function startServer() {
1995
+ const role = await currentRole();
1996
+ if (role === "developer") {
1997
+ const dev = new McpServer({ name: "aicrm", version: "0.6.0" }, { instructions: DEVELOPER_INSTRUCTIONS });
1998
+ registerIntegrationTools(dev);
1999
+ await dev.connect(new StdioServerTransport());
2000
+ return;
2001
+ }
1592
2002
  const server = new McpServer(
1593
- { name: "aicrm", version: "0.4.0" },
2003
+ { name: "aicrm", version: "0.6.0" },
1594
2004
  {
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.'
2005
+ 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. Pap\xE9is: admin configura tudo (estrutura, campos, comiss\xF5es, pap\xE9is); manager (Master) lidera o time s\xF3 nos workspaces dele: v\xEA todos os leads, importa e distribui, reatribui e exclui leads, define metas (set_goals), pausa/libera vendedores, usa modo TV e convida vendedores; member \xE9 o vendedor. Admins e Masters n\xE3o recebem leads automaticamente (distribui\xE7\xE3o e rod\xEDzio v\xE3o s\xF3 para vendedores), mas podem assumir um lead de prop\xF3sito. 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 Master ou 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 ou Master. A\xE7\xF5es destrutivas (delete_*, archive_field, revoke_*) exigem confirma\xE7\xE3o expl\xEDcita da pessoa antes.'
1596
2006
  }
1597
2007
  );
1598
2008
  server.registerTool(
@@ -1632,15 +2042,15 @@ async function startServer() {
1632
2042
  title: "Buscar leads",
1633
2043
  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).",
1634
2044
  inputSchema: {
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)
2045
+ query: z10.string().optional().describe("Texto em nome, empresa ou email"),
2046
+ workspace: z10.string().optional(),
2047
+ funnel: z10.string().optional(),
2048
+ stage: z10.string().optional(),
2049
+ owner: z10.string().optional().describe("Nome ou email do respons\xE1vel"),
2050
+ status: z10.enum(["open", "won", "lost"]).optional(),
2051
+ filter: z10.enum(["mine", "stale", "no_next_step"]).optional(),
2052
+ field_filters: z10.record(z10.string(), z10.string()).optional().describe('Filtra por campos personalizados: { "Cidade": "Recife" }. Texto: cont\xE9m; demais tipos: igual (datas em dd/mm/aaaa, sim/n\xE3o).'),
2053
+ limit: z10.number().int().min(1).max(200).default(25)
1644
2054
  },
1645
2055
  annotations: { readOnlyHint: true }
1646
2056
  },
@@ -1675,7 +2085,7 @@ async function startServer() {
1675
2085
  {
1676
2086
  title: "Detalhes do lead",
1677
2087
  description: "Retorna todos os campos de um lead e o hist\xF3rico (movimenta\xE7\xF5es e notas).",
1678
- inputSchema: { lead_id: z9.string().uuid() },
2088
+ inputSchema: { lead_id: z10.string().uuid() },
1679
2089
  annotations: { readOnlyHint: true }
1680
2090
  },
1681
2091
  withApi(async (api, a) => {
@@ -1708,17 +2118,17 @@ async function startServer() {
1708
2118
  })
1709
2119
  );
1710
2120
  const leadFields = {
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.')
2121
+ name: z10.string().optional(),
2122
+ company: z10.string().optional(),
2123
+ email: z10.string().optional(),
2124
+ phone: z10.string().optional(),
2125
+ title: z10.string().optional(),
2126
+ value: z10.number().min(0).optional().describe("Valor do neg\xF3cio em reais"),
2127
+ source: z10.enum(LEAD_SOURCES).optional(),
2128
+ next_step: z10.string().optional(),
2129
+ next_step_date: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
2130
+ expected_close: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
2131
+ custom_fields: z10.record(z10.string(), z10.union([z10.string(), z10.number(), z10.boolean(), z10.null()])).optional().describe('Campos personalizados do workspace pelo nome: { "Cidade": "Recife", "Faturamento": 50000 }. Dinheiro em reais; null limpa o campo. Veja list_fields.')
1722
2132
  };
1723
2133
  const toBody = (a) => Object.fromEntries(
1724
2134
  Object.entries({
@@ -1738,8 +2148,8 @@ async function startServer() {
1738
2148
  "create_lead",
1739
2149
  {
1740
2150
  title: "Criar lead",
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() }
2151
+ description: "Cria um lead. Sem funil/etapa, usa o primeiro funil do workspace e a primeira etapa. Vendedor: o respons\xE1vel padr\xE3o \xE9 voc\xEA. Admin ou Master sem owner: o AICRM escolhe um vendedor pelo desempenho (veja get_distribution); se ningu\xE9m puder receber, o lead fica sem respons\xE1vel.",
2152
+ inputSchema: { ...leadFields, name: z10.string(), workspace: z10.string().optional(), funnel: z10.string().optional(), stage: z10.string().optional(), owner: z10.string().optional() }
1743
2153
  },
1744
2154
  withApi(async (api, a) => {
1745
2155
  const c = await ctx(api);
@@ -1762,7 +2172,7 @@ async function startServer() {
1762
2172
  {
1763
2173
  title: "Atualizar lead",
1764
2174
  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.',
1765
- inputSchema: { lead_id: z9.string().uuid(), ...leadFields, owner: z9.string().optional() }
2175
+ inputSchema: { lead_id: z10.string().uuid(), ...leadFields, owner: z10.string().optional() }
1766
2176
  },
1767
2177
  withApi(async (api, a) => {
1768
2178
  const c = await ctx(api);
@@ -1782,8 +2192,8 @@ async function startServer() {
1782
2192
  "move_lead",
1783
2193
  {
1784
2194
  title: "Mover lead",
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.",
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)") }
2195
+ description: "Move um lead para outra etapa \u2014 e opcionalmente para outro funil (entre workspaces: s\xF3 admins e Masters). 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.",
2196
+ inputSchema: { lead_id: z10.string().uuid(), stage: z10.string().describe("Nome ou id da etapa de destino"), funnel: z10.string().optional().describe("Funil de destino (padr\xE3o: o atual)") }
1787
2197
  },
1788
2198
  withApi(async (api, a) => {
1789
2199
  const c = await ctx(api);
@@ -1800,12 +2210,12 @@ async function startServer() {
1800
2210
  title: "Marcar como ganho",
1801
2211
  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.",
1802
2212
  inputSchema: {
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)")
2213
+ lead_id: z10.string().uuid(),
2214
+ value: z10.number().min(0).optional(),
2215
+ next_funnel: z10.string().optional().describe("Funil onde o cliente continua depois do ganho"),
2216
+ next_stage: z10.string().optional().describe("Etapa de entrada no pr\xF3ximo funil (padr\xE3o: a primeira)"),
2217
+ next_owner: z10.string().optional().describe("Respons\xE1vel no pr\xF3ximo funil (padr\xE3o: o mesmo)"),
2218
+ next_value: z10.number().min(0).optional().describe("Valor em reais no pr\xF3ximo funil (padr\xE3o: 0)")
1809
2219
  }
1810
2220
  },
1811
2221
  withApi(async (api, a) => {
@@ -1833,7 +2243,7 @@ async function startServer() {
1833
2243
  {
1834
2244
  title: "Marcar como perdido",
1835
2245
  description: "Marca o lead como perdido com um motivo: price, timing, competitor, no_response, no_fit, other.",
1836
- inputSchema: { lead_id: z9.string().uuid(), reason: z9.enum(LOST_REASONS), note: z9.string().optional() }
2246
+ inputSchema: { lead_id: z10.string().uuid(), reason: z10.enum(LOST_REASONS), note: z10.string().optional() }
1837
2247
  },
1838
2248
  withApi(async (api, a) => {
1839
2249
  const c = await ctx(api);
@@ -1846,7 +2256,7 @@ async function startServer() {
1846
2256
  {
1847
2257
  title: "Adicionar nota",
1848
2258
  description: "Adiciona uma nota \xE0 linha do tempo do lead.",
1849
- inputSchema: { lead_id: z9.string().uuid(), note: z9.string().min(1) }
2259
+ inputSchema: { lead_id: z10.string().uuid(), note: z10.string().min(1) }
1850
2260
  },
1851
2261
  withApi(async (api, a) => {
1852
2262
  await api.post(`/leads/${a.lead_id}/notes`, { body: a.note });
@@ -1858,7 +2268,7 @@ async function startServer() {
1858
2268
  {
1859
2269
  title: "Dashboard de metas",
1860
2270
  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.",
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") },
2271
+ inputSchema: { funnel: z10.string().optional(), workspace: z10.string().optional(), month: z10.string().regex(/^\d{4}-\d{2}$/).optional().describe("YYYY-MM; padr\xE3o: m\xEAs atual") },
1862
2272
  annotations: { readOnlyHint: true }
1863
2273
  },
1864
2274
  withApi(async (api, a) => {
@@ -1894,7 +2304,7 @@ async function startServer() {
1894
2304
  {
1895
2305
  title: "Vai bater a meta?",
1896
2306
  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.",
1897
- inputSchema: { seller: z9.string().describe("Nome ou email do vendedor"), funnel: z9.string().optional(), month: z9.string().regex(/^\d{4}-\d{2}$/).optional() },
2307
+ inputSchema: { seller: z10.string().describe("Nome ou email do vendedor"), funnel: z10.string().optional(), month: z10.string().regex(/^\d{4}-\d{2}$/).optional() },
1898
2308
  annotations: { readOnlyHint: true }
1899
2309
  },
1900
2310
  withApi(async (api, a) => {
@@ -1917,12 +2327,12 @@ async function startServer() {
1917
2327
  "get_today_plan",
1918
2328
  {
1919
2329
  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.',
2330
+ 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 e Masters podem pedir o de outro vendedor.',
1921
2331
  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)")
2332
+ funnel: z10.string().optional(),
2333
+ workspace: z10.string().optional(),
2334
+ seller: z10.string().optional().describe("Nome ou email do vendedor (s\xF3 admins e Masters veem o plano de outra pessoa)"),
2335
+ limit: z10.number().int().min(5).max(10).optional().describe("Quantos leads sugerir (5 a 10; padr\xE3o 7)")
1926
2336
  },
1927
2337
  annotations: { readOnlyHint: true }
1928
2338
  },
@@ -1968,7 +2378,7 @@ async function startServer() {
1968
2378
  {
1969
2379
  title: "Campos do workspace",
1970
2380
  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.",
1971
- inputSchema: { workspace: z9.string().optional() },
2381
+ inputSchema: { workspace: z10.string().optional() },
1972
2382
  annotations: { readOnlyHint: true }
1973
2383
  },
1974
2384
  withApi(async (api, a) => {
@@ -2000,10 +2410,10 @@ async function startServer() {
2000
2410
  title: "Criar campo personalizado",
2001
2411
  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.",
2002
2412
  inputSchema: {
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()
2413
+ label: z10.string().min(1).max(60).describe("Nome do campo, ex.: Cidade"),
2414
+ type: z10.enum(CUSTOM_FIELD_TYPES),
2415
+ options: z10.array(z10.string()).optional().describe("Op\xE7\xF5es da lista (tipo select)"),
2416
+ workspace: z10.string().optional()
2007
2417
  }
2008
2418
  },
2009
2419
  withApi(async (api, a) => {
@@ -2019,10 +2429,10 @@ async function startServer() {
2019
2429
  title: "Editar campo personalizado",
2020
2430
  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.",
2021
2431
  inputSchema: {
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()
2432
+ field: z10.string().describe("Nome ou id do campo"),
2433
+ label: z10.string().min(1).max(60).optional(),
2434
+ options: z10.array(z10.string()).optional().describe("Lista completa de op\xE7\xF5es (substitui a atual)"),
2435
+ workspace: z10.string().optional()
2026
2436
  }
2027
2437
  },
2028
2438
  withApi(async (api, a) => {
@@ -2038,7 +2448,7 @@ async function startServer() {
2038
2448
  {
2039
2449
  title: "Remover campo personalizado",
2040
2450
  description: "Remove um campo das telas e importa\xE7\xF5es (s\xF3 admins). Os valores j\xE1 preenchidos ficam guardados, mas deixam de aparecer.",
2041
- inputSchema: { field: z9.string().describe("Nome ou id do campo"), workspace: z9.string().optional() },
2451
+ inputSchema: { field: z10.string().describe("Nome ou id do campo"), workspace: z10.string().optional() },
2042
2452
  annotations: { destructiveHint: true }
2043
2453
  },
2044
2454
  withApi(async (api, a) => {
@@ -2055,9 +2465,9 @@ async function startServer() {
2055
2465
  title: "Mostrar/esconder campos padr\xE3o",
2056
2466
  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.`,
2057
2467
  inputSchema: {
2058
- hide: z9.array(z9.enum(HIDEABLE_FIELDS)).optional(),
2059
- show: z9.array(z9.enum(HIDEABLE_FIELDS)).optional(),
2060
- workspace: z9.string().optional()
2468
+ hide: z10.array(z10.enum(HIDEABLE_FIELDS)).optional(),
2469
+ show: z10.array(z10.enum(HIDEABLE_FIELDS)).optional(),
2470
+ workspace: z10.string().optional()
2061
2471
  }
2062
2472
  },
2063
2473
  withApi(async (api, a) => {
@@ -2073,7 +2483,7 @@ async function startServer() {
2073
2483
  {
2074
2484
  title: "Renomear modelo de importa\xE7\xE3o",
2075
2485
  description: "Renomeia um modelo de importa\xE7\xE3o de CSV (admins ou quem criou o modelo).",
2076
- inputSchema: { template: z9.string().describe("Nome ou id do modelo"), name: z9.string().min(1).max(80), workspace: z9.string().optional() }
2486
+ inputSchema: { template: z10.string().describe("Nome ou id do modelo"), name: z10.string().min(1).max(80), workspace: z10.string().optional() }
2077
2487
  },
2078
2488
  withApi(async (api, a) => {
2079
2489
  const c = await ctx(api);
@@ -2088,7 +2498,7 @@ async function startServer() {
2088
2498
  {
2089
2499
  title: "Apagar modelo de importa\xE7\xE3o",
2090
2500
  description: "Apaga um modelo de importa\xE7\xE3o (admins ou quem criou). Leads j\xE1 importados n\xE3o mudam.",
2091
- inputSchema: { template: z9.string().describe("Nome ou id do modelo"), workspace: z9.string().optional() },
2501
+ inputSchema: { template: z10.string().describe("Nome ou id do modelo"), workspace: z10.string().optional() },
2092
2502
  annotations: { destructiveHint: true }
2093
2503
  },
2094
2504
  withApi(async (api, a) => {
@@ -2105,15 +2515,17 @@ async function startServer() {
2105
2515
  title: "Importar CSV",
2106
2516
  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" }.',
2107
2517
  inputSchema: {
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")
2518
+ path: z10.string().describe("Caminho do arquivo CSV (aceita ~)"),
2519
+ funnel: z10.string().optional(),
2520
+ workspace: z10.string().optional(),
2521
+ stage: z10.string().optional(),
2522
+ owner_mode: z10.enum(["me", "auto", "round_robin", "column"]).optional().describe(
2523
+ "Padr\xE3o: auto para admins e Masters (eles n\xE3o ficam com a base), me para vendedores. me = voc\xEA; auto = distribuir por desempenho entre os vendedores (quem fecha mais recebe mais); round_robin = distribuir igualmente entre os vendedores; column = coluna 'respons\xE1vel' (email). auto, round_robin e column s\xF3 para admins e Masters."
2524
+ ),
2525
+ duplicate: z10.enum(["skip", "update"]).default("skip"),
2526
+ columns: z10.record(z10.string(), z10.string()).optional().describe('For\xE7a o destino de colunas: { "Munic\xEDpio": "Cidade", "Obs": "ignorar" }. Aceita campo padr\xE3o (Nome, Empresa, Email\u2026) ou personalizado.'),
2527
+ dry_run: z10.boolean().default(false).describe("S\xF3 mostra o mapeamento e os problemas, sem importar"),
2528
+ template_name: z10.string().max(80).optional().describe("Salva o mapeamento com este nome para as pr\xF3ximas importa\xE7\xF5es")
2117
2529
  }
2118
2530
  },
2119
2531
  withApi(
@@ -2123,8 +2535,8 @@ async function startServer() {
2123
2535
  const stage = resolve(funnel.stages, a.stage, "Etapa") ?? funnel.stages[0];
2124
2536
  const ws = c.workspaces.find((w) => w.id === funnel.workspaceId);
2125
2537
  const hidden = ws?.hiddenFields ?? [];
2126
- const file = path2.resolve(a.path.replace(/^~(?=$|\/)/, homedir2()));
2127
- const parsed = Papa.parse(await readFile2(file, "utf8"), { header: true, skipEmptyLines: "greedy", transformHeader: (h) => h.trim() });
2538
+ const file = path3.resolve(a.path.replace(/^~(?=$|\/)/, homedir2()));
2539
+ const parsed = Papa.parse(await readFile3(file, "utf8"), { header: true, skipEmptyLines: "greedy", transformHeader: (h) => h.trim() });
2128
2540
  const headers = (parsed.meta.fields ?? []).filter(Boolean);
2129
2541
  const [fields, templates] = await Promise.all([listFields(api, funnel.workspaceId), listTemplates(api, funnel.workspaceId)]);
2130
2542
  const match = matchTemplate(headers, templates);
@@ -2195,16 +2607,18 @@ async function startServer() {
2195
2607
  }
2196
2608
  const template = a.template_name ? { id: match?.template.name === a.template_name ? match.template.id : void 0, name: a.template_name, columns: mapping } : match && !changedFromTemplate ? { id: match.template.id, name: match.template.name, columns: mapping } : void 0;
2197
2609
  const me = await api.get("/auth/me");
2610
+ const ownerMode = a.owner_mode ?? (me.user.role === "member" ? "me" : "auto");
2198
2611
  const res = await api.post(
2199
2612
  "/imports",
2200
2613
  {
2201
2614
  funnelId: funnel.id,
2202
2615
  stageId: stage.id,
2203
- ownerMode: a.owner_mode === "me" ? "fixed" : a.owner_mode,
2204
- ownerId: a.owner_mode === "me" ? me.user.id : void 0,
2616
+ ownerMode: ownerMode === "me" ? "fixed" : ownerMode,
2617
+ ownerId: ownerMode === "me" ? me.user.id : void 0,
2205
2618
  duplicate: a.duplicate,
2206
2619
  rows,
2207
- template
2620
+ template,
2621
+ origin: { fileName: path3.basename(a.path).slice(0, 200), valueColumn: headers.find((h) => mapping[h] === "std:value") }
2208
2622
  }
2209
2623
  );
2210
2624
  return text({
@@ -2220,9 +2634,19 @@ async function startServer() {
2220
2634
  )
2221
2635
  );
2222
2636
  registerManageTools(server);
2637
+ if (role === "admin") registerIntegrationTools(server);
2223
2638
  await server.connect(new StdioServerTransport());
2224
2639
  }
2225
2640
 
2641
+ // src/skills.ts
2642
+ var SKILLS = ["aicrm", "aicrm-integrate"];
2643
+ function parseSkillArgs(args) {
2644
+ const positional = args.filter((a, i) => !a.startsWith("--") && args[i - 1] !== "--dir");
2645
+ const install = positional.includes("install");
2646
+ const name = positional.find((a) => a !== "install") ?? "aicrm";
2647
+ return { install, name };
2648
+ }
2649
+
2226
2650
  // src/cli.ts
2227
2651
  var HELP = `AICRM MCP
2228
2652
 
@@ -2230,9 +2654,10 @@ Uso:
2230
2654
  aicrm-mcp login [--url https://seu-aicrm] [--email voce@empresa.com]
2231
2655
  aicrm-mcp logout
2232
2656
  aicrm-mcp whoami
2233
- aicrm-mcp skill imprime o SKILL.md (guia para agentes)
2234
- aicrm-mcp skill install [--project | --dir <pasta>]
2235
- instala em ~/.claude/skills/aicrm (ou no projeto atual)
2657
+ aicrm-mcp skill [nome] imprime o SKILL.md (guia para agentes)
2658
+ aicrm-mcp skill install [nome] [--project | --dir <pasta>]
2659
+ instala em ~/.claude/skills/<nome> (ou no projeto atual)
2660
+ skills: aicrm (operar o CRM) \xB7 aicrm-integrate (integrar um sistema pela API)
2236
2661
  aicrm-mcp inicia o servidor MCP (stdio) \u2014 use no seu cliente MCP
2237
2662
 
2238
2663
  Configurar:
@@ -2245,7 +2670,7 @@ function flag(name) {
2245
2670
  }
2246
2671
  async function login() {
2247
2672
  const existing = await loadCredentials();
2248
- const url = normalizeUrl(flag("url") ?? process.env.AICRM_URL ?? await ask("URL do AICRM", existing?.url ?? "http://localhost:5173"));
2673
+ const url = normalizeUrl(flag("url") ?? process.env.AICRM_URL ?? await ask("URL do AICRM", existing?.url ?? "https://aicrm.dev"));
2249
2674
  const email2 = flag("email") ?? await ask("Email", existing?.user.email || void 0);
2250
2675
  const password2 = await askHidden("Senha");
2251
2676
  try {
@@ -2261,6 +2686,10 @@ async function login() {
2261
2686
  console.log(`
2262
2687
  Agora adicione ao seu cliente:
2263
2688
  claude mcp add aicrm -- npx -y aicrm-mcp`);
2689
+ if (res.user.role === "developer")
2690
+ console.log(`
2691
+ E instale o guia de integra\xE7\xE3o no projeto:
2692
+ npx -y aicrm-mcp skill install aicrm-integrate --project`);
2264
2693
  } catch (e) {
2265
2694
  console.error(`
2266
2695
  \u2717 ${e.message}`);
@@ -2288,14 +2717,18 @@ async function whoami() {
2288
2717
  process.exit(1);
2289
2718
  }
2290
2719
  }
2291
- var SKILL_PATH = new URL("../skill/aicrm/SKILL.md", import.meta.url);
2292
2720
  async function skill() {
2293
- const content = await readFile3(SKILL_PATH, "utf8");
2294
- if (process.argv[3] !== "install") return void process.stdout.write(content);
2295
- const base = flag("dir") ?? (process.argv.includes("--project") ? path3.join(process.cwd(), ".claude", "skills") : path3.join(homedir3(), ".claude", "skills"));
2296
- const target = path3.join(base, "aicrm", "SKILL.md");
2297
- await mkdir2(path3.dirname(target), { recursive: true });
2298
- await writeFile2(target, content);
2721
+ const { install, name } = parseSkillArgs(process.argv.slice(3));
2722
+ if (!SKILLS.includes(name)) {
2723
+ console.error(`Skill desconhecido: ${name}. Dispon\xEDveis: ${SKILLS.join(", ")}`);
2724
+ process.exit(1);
2725
+ }
2726
+ const content = await readFile4(new URL(`../skill/${name}/SKILL.md`, import.meta.url), "utf8");
2727
+ if (!install) return void process.stdout.write(content);
2728
+ const base = flag("dir") ?? (process.argv.includes("--project") ? path4.join(process.cwd(), ".claude", "skills") : path4.join(homedir3(), ".claude", "skills"));
2729
+ const target = path4.join(base, name, "SKILL.md");
2730
+ await mkdir2(path4.dirname(target), { recursive: true });
2731
+ await writeFile3(target, content);
2299
2732
  console.log(`\u2713 Skill instalado em ${target}`);
2300
2733
  }
2301
2734
  var cmd = process.argv[2];