aicrm-mcp 0.3.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"];
@@ -169,6 +170,9 @@ var FUNNEL_TEMPLATES = {
169
170
  ]
170
171
  }
171
172
  };
173
+ var DEFAULT_STAGE_LIMIT_DAYS = 14;
174
+ var DONE_EVENT_TYPES = ["note", "updated", "stage_changed", "status_changed"];
175
+ var ACTIVITY_EVENT_TYPES = [...DONE_EVENT_TYPES, "created", "handed_off"];
172
176
  var CUSTOM_FIELD_TYPES = ["text", "number", "money", "date", "select", "boolean", "url"];
173
177
  var CUSTOM_FIELD_TYPE_LABEL = {
174
178
  text: "Texto",
@@ -307,6 +311,11 @@ var updateUserSchema = z.object({
307
311
  active: z.boolean().optional(),
308
312
  workspaceIds: z.array(id).optional()
309
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
+ });
310
319
  var workspaceSchema = z.object({
311
320
  name: z.string().trim().min(1, "Informe um nome").max(80),
312
321
  visibility: z.enum(VISIBILITIES).default("all")
@@ -315,19 +324,23 @@ var workspaceUpdateSchema = z.object({
315
324
  name: z.string().trim().min(1, "Informe um nome").max(80).optional(),
316
325
  visibility: z.enum(VISIBILITIES).optional(),
317
326
  hiddenFields: z.array(z.enum(HIDEABLE_FIELDS)).transform((a) => [...new Set(a)]).optional(),
318
- /** Raster data URL (PNG/JPEG/WebP), or null to remove. SVG is rejected: the browser rasterizes it first. */
319
- logo: z.string().regex(/^data:image\/(png|jpeg|webp);base64,[A-Za-z0-9+/=]+$/, "Envie uma imagem PNG, JPG ou WebP").max(4e5, "Imagem muito grande (m\xE1x. ~300 KB)").nullable().optional()
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()
320
331
  });
321
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)]);
322
333
  var leadFieldSchema = z.object({
323
334
  label: z.string().trim().min(1, "D\xEA um nome ao campo").max(60),
324
335
  type: z.enum(CUSTOM_FIELD_TYPES),
325
- options: fieldOptions.default([])
336
+ options: fieldOptions.default([]),
337
+ required: z.boolean().default(false)
326
338
  }).refine((f) => f.type !== "select" || f.options.length > 0, { message: "Adicione pelo menos uma op\xE7\xE3o", path: ["options"] });
327
339
  var leadFieldUpdateSchema = z.object({
328
340
  label: z.string().trim().min(1).max(60).optional(),
329
341
  options: fieldOptions.optional(),
330
- position: z.number().optional()
342
+ position: z.number().optional(),
343
+ required: z.boolean().optional()
331
344
  });
332
345
  var customInput = z.record(z.string().uuid(), z.union([z.string().max(2e3), z.number(), z.boolean(), z.null()]));
333
346
  var importTargetSchema = z.custom(
@@ -338,7 +351,9 @@ var importTemplateUpdateSchema = z.object({ name: z.string().trim().min(1).max(8
338
351
  var stageInputSchema = z.object({
339
352
  id: id.optional(),
340
353
  name: z.string().trim().min(1).max(60),
341
- probability: z.number().int().min(0).max(100)
354
+ probability: z.number().int().min(0).max(100),
355
+ /** "Prazo da etapa" in business days; null = default. Omitted on update = keep the current one. */
356
+ limitDays: z.number().int().min(1).max(365).nullish()
342
357
  });
343
358
  var funnelSchema = z.object({
344
359
  workspaceId: id,
@@ -347,7 +362,8 @@ var funnelSchema = z.object({
347
362
  goalStageIndex: z.number().int().min(0).optional(),
348
363
  template: z.enum(Object.keys(FUNNEL_TEMPLATES)).optional(),
349
364
  stages: z.array(stageInputSchema).min(1).max(20).optional(),
350
- nextFunnelId: id.nullish()
365
+ nextFunnelId: id.nullish(),
366
+ autoAssign: z.boolean().optional()
351
367
  });
352
368
  var funnelUpdateSchema = z.object({
353
369
  name: z.string().trim().min(1).max(80).optional(),
@@ -355,6 +371,7 @@ var funnelUpdateSchema = z.object({
355
371
  goalStageId: id.nullable().optional(),
356
372
  stages: z.array(stageInputSchema).min(1).max(20).optional(),
357
373
  nextFunnelId: id.nullable().optional(),
374
+ autoAssign: z.boolean().optional(),
358
375
  archived: z.boolean().optional()
359
376
  });
360
377
  var optionalText = (max) => z.string().trim().max(max).nullish().transform((v) => v ? v : null);
@@ -374,7 +391,7 @@ var leadInputSchema = z.object({
374
391
  expectedCloseAt: dateStr.nullish(),
375
392
  custom: customInput.optional()
376
393
  });
377
- 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();
378
395
  var moveLeadSchema = z.object({
379
396
  funnelId: id.optional(),
380
397
  stageId: id
@@ -400,6 +417,8 @@ var leadQuerySchema = z.object({
400
417
  status: z.enum(LEAD_STATUSES).optional(),
401
418
  q: z.string().max(120).optional(),
402
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(),
403
422
  limit: z.coerce.number().int().min(1).max(1e3).default(500)
404
423
  });
405
424
  var bulkLeadSchema = z.object({
@@ -408,10 +427,13 @@ var bulkLeadSchema = z.object({
408
427
  stageId: id.optional(),
409
428
  funnelId: id.optional()
410
429
  });
430
+ var bulkDeleteSchema = z.object({
431
+ leadIds: z.array(id).min(1).max(1e3)
432
+ });
411
433
  var importSchema = z.object({
412
434
  funnelId: id,
413
435
  stageId: id,
414
- ownerMode: z.enum(["fixed", "column", "round_robin"]),
436
+ ownerMode: z.enum(["fixed", "column", "round_robin", "auto"]),
415
437
  ownerId: id.optional(),
416
438
  duplicate: z.enum(["skip", "update"]).default("skip"),
417
439
  rows: z.array(
@@ -433,6 +455,11 @@ var importSchema = z.object({
433
455
  id: id.optional(),
434
456
  name: z.string().trim().min(1).max(80),
435
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()
436
463
  }).optional()
437
464
  });
438
465
  var commissionTierSchema = z.object({
@@ -464,6 +491,68 @@ var tvLinkSchema = z.object({
464
491
  funnelId: id,
465
492
  name: z.string().trim().min(1).max(80)
466
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"] });
552
+ var distributionPauseSchema = z.object({
553
+ funnelId: id,
554
+ userId: id
555
+ });
467
556
 
468
557
  // ../shared/src/format.ts
469
558
  var brl = new Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL", maximumFractionDigits: 0 });
@@ -621,6 +710,98 @@ function guessFieldType(samples) {
621
710
  return { type: "text", options: [] };
622
711
  }
623
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
+
738
+ // ../shared/src/stats/calendar.ts
739
+ function toDateStr(d) {
740
+ return d.toISOString().slice(0, 10);
741
+ }
742
+ function parseDate(s) {
743
+ return /* @__PURE__ */ new Date(`${s}T00:00:00Z`);
744
+ }
745
+ function addDays(s, n) {
746
+ const d = parseDate(s);
747
+ d.setUTCDate(d.getUTCDate() + n);
748
+ return toDateStr(d);
749
+ }
750
+ function localDate(instant, timeZone) {
751
+ const d = instant instanceof Date ? instant : new Date(instant);
752
+ return new Intl.DateTimeFormat("en-CA", { timeZone, year: "numeric", month: "2-digit", day: "2-digit" }).format(d);
753
+ }
754
+ function easterSunday(year) {
755
+ const a = year % 19;
756
+ const b = Math.floor(year / 100);
757
+ const c = year % 100;
758
+ const d = Math.floor(b / 4);
759
+ const e = b % 4;
760
+ const f = Math.floor((b + 8) / 25);
761
+ const g = Math.floor((b - f + 1) / 3);
762
+ const h = (19 * a + b - d - g + 15) % 30;
763
+ const i = Math.floor(c / 4);
764
+ const k = c % 4;
765
+ const l = (32 + 2 * e + 2 * i - h - k) % 7;
766
+ const m = Math.floor((a + 11 * h + 22 * l) / 451);
767
+ const month = Math.floor((h + l - 7 * m + 114) / 31);
768
+ const day = (h + l - 7 * m + 114) % 31 + 1;
769
+ return `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
770
+ }
771
+ var holidayCache = /* @__PURE__ */ new Map();
772
+ function brazilianHolidays(year) {
773
+ const cached = holidayCache.get(year);
774
+ if (cached) return cached;
775
+ const easter = easterSunday(year);
776
+ const fixed = ["01-01", "04-21", "05-01", "09-07", "10-12", "11-02", "11-15", "11-20", "12-25"].map(
777
+ (md) => `${year}-${md}`
778
+ );
779
+ const set = /* @__PURE__ */ new Set([
780
+ ...fixed,
781
+ addDays(easter, -48),
782
+ // Carnaval (segunda)
783
+ addDays(easter, -47),
784
+ // Carnaval (terça)
785
+ addDays(easter, -2),
786
+ // Sexta-feira Santa
787
+ addDays(easter, 60)
788
+ // Corpus Christi
789
+ ]);
790
+ holidayCache.set(year, set);
791
+ return set;
792
+ }
793
+ function isBusinessDay(s) {
794
+ const dow = parseDate(s).getUTCDay();
795
+ if (dow === 0 || dow === 6) return false;
796
+ return !brazilianHolidays(Number(s.slice(0, 4))).has(s);
797
+ }
798
+ function businessDaysBetween(start, end) {
799
+ if (start > end) return 0;
800
+ let n = 0;
801
+ for (let d = start; d <= end; d = addDays(d, 1)) if (isBusinessDay(d)) n++;
802
+ return n;
803
+ }
804
+
624
805
  // ../shared/src/stats/forecast.ts
625
806
  var FORECAST_STATUS_LABEL = {
626
807
  achieved: "Meta batida",
@@ -629,11 +810,36 @@ var FORECAST_STATUS_LABEL = {
629
810
  off_track: "Improv\xE1vel"
630
811
  };
631
812
 
813
+ // ../shared/src/stats/stage-age.ts
814
+ function stagesWithoutLimit(funnels) {
815
+ const out = /* @__PURE__ */ new Set();
816
+ for (const f of funnels) {
817
+ if (f.goalKpi !== "stage_reached" || !f.goalStageId) continue;
818
+ const goal = f.stages.find((s) => s.id === f.goalStageId);
819
+ if (!goal) continue;
820
+ for (const s of f.stages) if (s.position >= goal.position) out.add(s.id);
821
+ }
822
+ return out;
823
+ }
824
+ function businessDaysSince(enteredOn, today) {
825
+ return businessDaysBetween(addDays(enteredOn, 1), today);
826
+ }
827
+ function stageLevel(days, limit) {
828
+ if (days > 2 * limit) return "critical";
829
+ if (days > limit) return "late";
830
+ return "ok";
831
+ }
832
+ function stageAge(stageEnteredAt, limitDays, today, timeZone) {
833
+ const days = businessDaysSince(localDate(stageEnteredAt, timeZone), today);
834
+ const limit = limitDays ?? DEFAULT_STAGE_LIMIT_DAYS;
835
+ return { days, limit, level: stageLevel(days, limit) };
836
+ }
837
+
632
838
  // src/server.ts
633
839
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
634
840
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
635
841
  import Papa from "papaparse";
636
- import { z as z7 } from "zod";
842
+ import { z as z10 } from "zod";
637
843
 
638
844
  // src/lib.ts
639
845
  var norm = (s) => s.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase().trim();
@@ -646,14 +852,20 @@ function resolve(items, ref, what) {
646
852
  }
647
853
  var text = (data) => ({ content: [{ type: "text", text: typeof data === "string" ? data : JSON.stringify(data, null, 1) }] });
648
854
  async function ctx(api) {
649
- const [workspaces, users] = await Promise.all([api.get("/workspaces"), api.get("/users")]);
855
+ const [workspaces, users, me] = await Promise.all([
856
+ api.get("/workspaces"),
857
+ api.get("/users"),
858
+ api.get("/auth/me")
859
+ ]);
650
860
  const stages = /* @__PURE__ */ new Map();
651
861
  const funnels = /* @__PURE__ */ new Map();
652
862
  for (const w of workspaces) for (const f of w.funnels) {
653
863
  funnels.set(f.id, f);
654
864
  for (const s of f.stages) stages.set(s.id, s);
655
865
  }
656
- return { workspaces, users, stages, funnels, userName: (id2) => users.find((u) => u.id === id2)?.name ?? null };
866
+ const timezone = me.org.timezone;
867
+ const noLimit = stagesWithoutLimit([...funnels.values()]);
868
+ return { workspaces, users, stages, funnels, noLimit, timezone, today: localDate(/* @__PURE__ */ new Date(), timezone), userName: (id2) => users.find((u) => u.id === id2)?.name ?? null };
657
869
  }
658
870
  function pickWorkspace(c, ref) {
659
871
  const ws = resolve(c.workspaces, ref, "Workspace") ?? c.workspaces[0];
@@ -703,10 +915,16 @@ function leadSummary(c, l, fields) {
703
915
  value: formatMoney(l.valueCents),
704
916
  owner: c.userName(l.ownerId),
705
917
  next_step: l.nextStep ? `${l.nextStep}${l.nextStepAt ? ` (${l.nextStepAt})` : ""}` : null,
706
- days_in_stage: Math.floor((Date.now() - Date.parse(l.stageEnteredAt)) / 864e5),
918
+ ...stageAgeSummary(c, l),
707
919
  ...Object.keys(custom).length ? { fields: custom } : {}
708
920
  };
709
921
  }
922
+ var RISK_LEVEL_LABEL = { ok: "no prazo", late: "passou do prazo", critical: "passou do dobro do prazo" };
923
+ function stageAgeSummary(c, l) {
924
+ if (l.status !== "open" || c.noLimit.has(l.stageId)) return {};
925
+ const age = stageAge(l.stageEnteredAt, c.stages.get(l.stageId)?.limitDays, c.today, c.timezone);
926
+ return { business_days_in_stage: age.days, stage_limit_days: age.limit, stage_deadline: RISK_LEVEL_LABEL[age.level] };
927
+ }
710
928
  function describeForecast(kpi, f) {
711
929
  return {
712
930
  status: FORECAST_STATUS_LABEL[f.status],
@@ -728,7 +946,7 @@ var withApi = (fn) => async (args) => {
728
946
  try {
729
947
  return await fn(apiFor(creds), args);
730
948
  } catch (e) {
731
- 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;
732
950
  return { ...text(`Erro: ${msg}`), isError: true };
733
951
  }
734
952
  };
@@ -767,8 +985,301 @@ function handoffBody(c, source, a) {
767
985
  }
768
986
  var MONTH = /^\d{4}-\d{2}$/;
769
987
 
770
- // src/tools/admin.ts
988
+ // src/tools/integrate.ts
771
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";
772
1283
  function registerAdminTools(server) {
773
1284
  server.registerTool(
774
1285
  "get_setup_status",
@@ -794,7 +1305,7 @@ function registerAdminTools(server) {
794
1305
  "list_tv_links",
795
1306
  {
796
1307
  title: "Links do modo TV",
797
- 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).",
798
1309
  inputSchema: {},
799
1310
  annotations: { readOnlyHint: true }
800
1311
  },
@@ -806,8 +1317,8 @@ function registerAdminTools(server) {
806
1317
  "create_tv_link",
807
1318
  {
808
1319
  title: "Criar link do modo TV",
809
- 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.",
810
- 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() }
811
1322
  },
812
1323
  withApi(async (api, a) => {
813
1324
  const funnel = pickFunnel(await ctx(api), a.funnel, a.workspace);
@@ -819,8 +1330,8 @@ function registerAdminTools(server) {
819
1330
  "revoke_tv_link",
820
1331
  {
821
1332
  title: "Revogar link do modo TV",
822
- description: "Desativa um link do modo TV (s\xF3 admins); a TV que usava o link para de mostrar o dashboard.",
823
- 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") },
824
1335
  annotations: { destructiveHint: true }
825
1336
  },
826
1337
  withApi(async (api, a) => {
@@ -853,7 +1364,7 @@ function registerAdminTools(server) {
853
1364
  {
854
1365
  title: "Revogar token de API",
855
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.",
856
- inputSchema: { token_id: z2.string().uuid() },
1367
+ inputSchema: { token_id: z3.string().uuid() },
857
1368
  annotations: { destructiveHint: true }
858
1369
  },
859
1370
  withApi(async (api, a) => {
@@ -863,9 +1374,91 @@ function registerAdminTools(server) {
863
1374
  );
864
1375
  }
865
1376
 
1377
+ // src/tools/distribution.ts
1378
+ import { z as z4 } from "zod";
1379
+ var br = (d) => `${d.slice(8, 10)}/${d.slice(5, 7)}`;
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"}`;
1381
+ var describePlan = (funnel, p) => ({
1382
+ funnel,
1383
+ automatic_distribution: p.enabled ? "ligada" : "desligada (ligue com update_funnel auto_assign=true)",
1384
+ next_leads_go_to: p.sellers.map((s) => ({
1385
+ seller: s.name,
1386
+ share: formatPct(s.share),
1387
+ why: s.why,
1388
+ received_last_7_days: s.receivedRecently
1389
+ })),
1390
+ not_receiving: p.blocked.map((b) => ({ seller: b.name, why: why(b) })),
1391
+ leads_without_owner: p.unowned
1392
+ });
1393
+ var funnelArgs = { funnel: z4.string().optional(), workspace: z4.string().optional() };
1394
+ function registerDistributionTools(server) {
1395
+ server.registerTool(
1396
+ "get_distribution",
1397
+ {
1398
+ title: "Distribui\xE7\xE3o de leads",
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.",
1400
+ inputSchema: funnelArgs,
1401
+ annotations: { readOnlyHint: true }
1402
+ },
1403
+ withApi(async (api, a) => {
1404
+ if (!a.funnel && !a.workspace) {
1405
+ const alerts = await api.get("/distribution/alerts");
1406
+ return text(
1407
+ alerts.length ? { not_receiving: alerts.map((x) => ({ seller: x.name, funnel: x.funnelName, why: why(x) })) } : "Todos os vendedores est\xE3o recebendo leads em todos os funis."
1408
+ );
1409
+ }
1410
+ const funnel = pickFunnel(await ctx(api), a.funnel, a.workspace);
1411
+ return text(describePlan(funnel.name, await api.get(`/distribution?funnelId=${funnel.id}`)));
1412
+ })
1413
+ );
1414
+ server.registerTool(
1415
+ "release_seller",
1416
+ {
1417
+ title: "Liberar vendedor",
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 }
1420
+ },
1421
+ withApi(async (api, a) => {
1422
+ const c = await ctx(api);
1423
+ const funnel = pickFunnel(c, a.funnel, a.workspace);
1424
+ const user = findUser(c, a.seller);
1425
+ const plan = await api.post("/distribution/release", { funnelId: funnel.id, userId: user.id });
1426
+ return text({ released: user.name, ...describePlan(funnel.name, plan) });
1427
+ })
1428
+ );
1429
+ server.registerTool(
1430
+ "pause_seller",
1431
+ {
1432
+ title: "Pausar vendedor",
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 }
1435
+ },
1436
+ withApi(async (api, a) => {
1437
+ const c = await ctx(api);
1438
+ const funnel = pickFunnel(c, a.funnel, a.workspace);
1439
+ const user = findUser(c, a.seller);
1440
+ const plan = await api.post("/distribution/pause", { funnelId: funnel.id, userId: user.id });
1441
+ return text({ paused: user.name, ...describePlan(funnel.name, plan) });
1442
+ })
1443
+ );
1444
+ server.registerTool(
1445
+ "assign_unowned_leads",
1446
+ {
1447
+ title: "Distribuir leads sem vendedor",
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.",
1449
+ inputSchema: funnelArgs
1450
+ },
1451
+ withApi(async (api, a) => {
1452
+ const funnel = pickFunnel(await ctx(api), a.funnel, a.workspace);
1453
+ const r = await api.post("/distribution/assign-unowned", { funnelId: funnel.id });
1454
+ return text({ assigned: r.assigned, ...describePlan(funnel.name, r.plan) });
1455
+ })
1456
+ );
1457
+ }
1458
+
866
1459
  // src/tools/goals.ts
867
- import { z as z3 } from "zod";
868
- var describePlan = (p) => ({
1460
+ import { z as z5 } from "zod";
1461
+ var describePlan2 = (p) => ({
869
1462
  name: p.name,
870
1463
  kind: p.kind === "percent" ? "% sobre a receita (metas de receita)" : "R$ por unidade (metas de quantidade)",
871
1464
  tiers: p.tiers.map((t) => `a partir de ${t.minAttainmentPct}% da meta: ${p.kind === "percent" ? `${t.rate}%` : `${formatMoney(t.rate)} por unidade`}`)
@@ -879,7 +1472,7 @@ function registerGoalTools(server) {
879
1472
  inputSchema: {},
880
1473
  annotations: { readOnlyHint: true }
881
1474
  },
882
- withApi(async (api) => text((await api.get("/commission-plans")).map(describePlan)))
1475
+ withApi(async (api) => text((await api.get("/commission-plans")).map(describePlan2)))
883
1476
  );
884
1477
  server.registerTool(
885
1478
  "save_commission_plan",
@@ -887,9 +1480,9 @@ function registerGoalTools(server) {
887
1480
  title: "Criar ou alterar plano de comiss\xE3o",
888
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).",
889
1482
  inputSchema: {
890
- name: z3.string().min(1).max(80),
891
- kind: z3.enum(["percent", "per_unit"]),
892
- tiers: z3.array(z3.object({ from_attainment_pct: z3.number().min(0).max(1e3), rate: z3.number().min(0) })).min(1).max(10).describe("Ex.: [{from_attainment_pct: 0, rate: 3}, {from_attainment_pct: 100, rate: 6}]")
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}]")
893
1486
  }
894
1487
  },
895
1488
  withApi(async (api, a) => {
@@ -900,7 +1493,7 @@ function registerGoalTools(server) {
900
1493
  };
901
1494
  const existing = (await api.get("/commission-plans")).find((p) => norm(p.name) === norm(a.name));
902
1495
  const plan = existing ? await api.patch(`/commission-plans/${existing.id}`, { ...body, name: existing.name }) : await api.post("/commission-plans", body);
903
- return text({ [existing ? "updated" : "created"]: describePlan(plan) });
1496
+ return text({ [existing ? "updated" : "created"]: describePlan2(plan) });
904
1497
  })
905
1498
  );
906
1499
  server.registerTool(
@@ -908,7 +1501,7 @@ function registerGoalTools(server) {
908
1501
  {
909
1502
  title: "Apagar plano de comiss\xE3o",
910
1503
  description: "Apaga um plano de comiss\xE3o (s\xF3 admins). As metas que o usavam ficam sem comiss\xE3o.",
911
- inputSchema: { plan: z3.string() },
1504
+ inputSchema: { plan: z5.string() },
912
1505
  annotations: { destructiveHint: true }
913
1506
  },
914
1507
  withApi(async (api, a) => {
@@ -922,7 +1515,7 @@ function registerGoalTools(server) {
922
1515
  {
923
1516
  title: "Metas do m\xEAs",
924
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.",
925
- inputSchema: { funnel: z3.string().optional(), workspace: z3.string().optional(), month: z3.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") },
926
1519
  annotations: { readOnlyHint: true }
927
1520
  },
928
1521
  withApi(async (api, a) => {
@@ -949,13 +1542,13 @@ function registerGoalTools(server) {
949
1542
  "set_goals",
950
1543
  {
951
1544
  title: "Definir metas",
952
- 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.',
953
1546
  inputSchema: {
954
- targets: z3.array(z3.object({ seller: z3.string().describe("Nome ou email"), target: z3.number().min(0) })).min(1),
955
- funnel: z3.string().optional(),
956
- workspace: z3.string().optional(),
957
- month: z3.string().regex(MONTH).optional().describe("YYYY-MM; padr\xE3o: m\xEAs atual"),
958
- commission_plan: z3.string().optional()
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()
959
1552
  }
960
1553
  },
961
1554
  withApi(
@@ -991,14 +1584,14 @@ function registerGoalTools(server) {
991
1584
  }
992
1585
 
993
1586
  // src/tools/leads.ts
994
- import { z as z4 } from "zod";
1587
+ import { z as z6 } from "zod";
995
1588
  function registerLeadTools(server) {
996
1589
  server.registerTool(
997
1590
  "reopen_lead",
998
1591
  {
999
1592
  title: "Reabrir lead",
1000
1593
  description: "Volta um lead ganho ou perdido para aberto, na mesma etapa.",
1001
- inputSchema: { lead_id: z4.string().uuid() }
1594
+ inputSchema: { lead_id: z6.string().uuid() }
1002
1595
  },
1003
1596
  withApi(async (api, a) => {
1004
1597
  const c = await ctx(api);
@@ -1012,12 +1605,12 @@ function registerLeadTools(server) {
1012
1605
  title: "Enviar lead ganho para outro funil",
1013
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.",
1014
1607
  inputSchema: {
1015
- lead_id: z4.string().uuid().describe("Lead ganho"),
1016
- funnel: z4.string().describe("Funil onde o cliente continua (nome ou id)"),
1017
- stage: z4.string().optional().describe("Etapa de entrada (padr\xE3o: a primeira)"),
1018
- workspace: z4.string().optional().describe("Workspace do funil, se for outro (s\xF3 admins)"),
1019
- owner: z4.string().optional().describe('Respons\xE1vel no novo funil (nome ou email; "none" = sem respons\xE1vel). Padr\xE3o: o mesmo do lead ganho'),
1020
- value: z4.number().min(0).optional().describe("Valor em reais no novo funil (padr\xE3o: 0)")
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)")
1021
1614
  }
1022
1615
  },
1023
1616
  withApi(async (api, a) => {
@@ -1032,12 +1625,12 @@ function registerLeadTools(server) {
1032
1625
  "bulk_update_leads",
1033
1626
  {
1034
1627
  title: "Atualizar leads em massa",
1035
- description: "Reatribui (s\xF3 admins) e/ou move at\xE9 1000 leads de uma vez. Pegue os ids com search_leads. Para mover, informe stage (e funnel, se for outro funil; sem funnel, vale o funil do primeiro lead). Leads que voc\xEA n\xE3o pode editar s\xE3o pulados, assim como leads ganhos numa troca de funil (use handoff_lead para eles).",
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).",
1036
1629
  inputSchema: {
1037
- lead_ids: z4.array(z4.string().uuid()).min(1).max(1e3),
1038
- owner: z4.string().optional().describe("Nome ou email do novo respons\xE1vel"),
1039
- stage: z4.string().optional(),
1040
- funnel: z4.string().optional()
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()
1041
1634
  }
1042
1635
  },
1043
1636
  withApi(async (api, a) => {
@@ -1063,8 +1656,8 @@ function registerLeadTools(server) {
1063
1656
  "delete_lead",
1064
1657
  {
1065
1658
  title: "Excluir lead",
1066
- description: "Apaga um lead e todo o hist\xF3rico dele (s\xF3 admins). N\xE3o d\xE1 para desfazer \u2014 confirme com a pessoa antes. Para tirar do funil sem apagar, prefira mark_lost.",
1067
- inputSchema: { lead_id: z4.string().uuid() },
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() },
1068
1661
  annotations: { destructiveHint: true }
1069
1662
  },
1070
1663
  withApi(async (api, a) => {
@@ -1075,22 +1668,68 @@ function registerLeadTools(server) {
1075
1668
  );
1076
1669
  }
1077
1670
 
1671
+ // src/tools/risks.ts
1672
+ import { z as z7 } from "zod";
1673
+ function registerRiskTools(server) {
1674
+ server.registerTool(
1675
+ "get_lead_risks",
1676
+ {
1677
+ title: "Leads em risco",
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() },
1680
+ annotations: { readOnlyHint: true }
1681
+ },
1682
+ withApi(async (api, a) => {
1683
+ const ws = pickWorkspace(await ctx(api), a.workspace);
1684
+ const r = await api.get(`/leads/risks?workspaceId=${ws.id}`);
1685
+ if (!r.critical.count && !r.late)
1686
+ return text(`Nenhum lead fora do prazo no workspace ${ws.name}.`);
1687
+ return text({
1688
+ workspace: ws.name,
1689
+ critical: {
1690
+ leads: r.critical.count,
1691
+ value: formatMoney(r.critical.valueCents),
1692
+ where_it_jams: r.critical.byStage.map((s) => ({ stage: `${s.funnelName} \xB7 ${s.stageName}`, leads: s.count, value: formatMoney(s.valueCents) })),
1693
+ with_whom: r.critical.byOwner.map((o) => ({ seller: o.ownerName ?? "Sem vendedor", leads: o.count, value: formatMoney(o.valueCents) })),
1694
+ worst: r.critical.top.map((l) => ({
1695
+ lead: l.company ? `${l.company} \u2014 ${l.name}` : l.name,
1696
+ stage: `${l.funnelName} \xB7 ${l.stageName}`,
1697
+ seller: l.ownerName ?? "Sem vendedor",
1698
+ business_days_in_stage: l.age.days,
1699
+ stage_limit_days: l.age.limit,
1700
+ stage_deadline: RISK_LEVEL_LABEL[l.age.level],
1701
+ value: formatMoney(l.valueCents)
1702
+ }))
1703
+ },
1704
+ late_only_seller_warned: r.late
1705
+ });
1706
+ })
1707
+ );
1708
+ }
1709
+
1078
1710
  // src/tools/structure.ts
1079
- import { z as z5 } from "zod";
1711
+ import { z as z8 } from "zod";
1080
1712
  var TEMPLATE_KEYS = Object.keys(FUNNEL_TEMPLATES);
1081
- var stageInput = z5.object({
1082
- name: z5.string().min(1).max(60),
1083
- probability: z5.number().int().min(0).max(100).describe("Chance (%) de um lead nesta etapa virar venda"),
1084
- was: z5.string().optional().describe("Nome atual da etapa, se voc\xEA est\xE1 renomeando uma existente")
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.`
1718
+ ),
1719
+ was: z8.string().optional().describe("Nome atual da etapa, se voc\xEA est\xE1 renomeando uma existente")
1085
1720
  });
1086
1721
  var describeFunnel = (f, ws) => ({
1087
1722
  name: f.name,
1088
1723
  goal_kpi: `${f.goalKpi} (${GOAL_KPI_LABEL[f.goalKpi]})`,
1089
1724
  ...f.goalKpi === "stage_reached" ? { goal_stage: f.stages.find((s) => s.id === f.goalStageId)?.name ?? null } : {},
1090
1725
  ...f.nextFunnelId ? { after_won_continue_in: ws?.funnels.find((x) => x.id === f.nextFunnelId)?.name ?? f.nextFunnelId } : {},
1091
- stages: f.stages.map((s) => `${s.name} (${s.probability}%)`)
1726
+ auto_assign: !!f.autoAssign,
1727
+ stages: f.stages.map((s) => `${s.name} (${s.probability}%, prazo ${s.limitDays ?? DEFAULT_STAGE_LIMIT_DAYS} dias \xFAteis${s.limitDays ? "" : " \u2014 padr\xE3o"})`)
1092
1728
  });
1093
- var nextFunnelArg = z5.string().nullable().optional().describe('Funil (do mesmo workspace) onde o cliente continua depois de ganho, ex.: "Relacionamento". Sugerido no app ao marcar ganho. null ou "none" remove.');
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)."
1731
+ );
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.');
1094
1733
  var nextFunnelId = (ws, ref) => ref === void 0 ? void 0 : ref === null || norm(ref) === "none" ? null : resolve(ws.funnels, ref, "Funil").id;
1095
1734
  function registerStructureTools(server) {
1096
1735
  server.registerTool(
@@ -1099,9 +1738,9 @@ function registerStructureTools(server) {
1099
1738
  title: "Criar workspace",
1100
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.",
1101
1740
  inputSchema: {
1102
- name: z5.string().min(1).max(80),
1103
- visibility: z5.enum(["all", "own"]).default("all").describe("all = todos veem todos os leads; own = cada vendedor v\xEA s\xF3 os seus"),
1104
- create_funnel: z5.boolean().default(true)
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)
1105
1744
  }
1106
1745
  },
1107
1746
  withApi(async (api, a) => {
@@ -1116,9 +1755,9 @@ function registerStructureTools(server) {
1116
1755
  title: "Alterar workspace",
1117
1756
  description: "Renomeia um workspace ou muda quem v\xEA quais leads (s\xF3 admins).",
1118
1757
  inputSchema: {
1119
- workspace: z5.string(),
1120
- name: z5.string().min(1).max(80).optional(),
1121
- visibility: z5.enum(["all", "own"]).optional().describe("all = todos veem todos os leads; own = cada vendedor v\xEA s\xF3 os seus")
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")
1122
1761
  }
1123
1762
  },
1124
1763
  withApi(async (api, a) => {
@@ -1132,7 +1771,7 @@ function registerStructureTools(server) {
1132
1771
  {
1133
1772
  title: "Excluir workspace",
1134
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.",
1135
- inputSchema: { workspace: z5.string() },
1774
+ inputSchema: { workspace: z8.string() },
1136
1775
  annotations: { destructiveHint: true }
1137
1776
  },
1138
1777
  withApi(async (api, a) => {
@@ -1147,13 +1786,14 @@ function registerStructureTools(server) {
1147
1786
  title: "Criar funil",
1148
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).`,
1149
1788
  inputSchema: {
1150
- name: z5.string().min(1).max(80),
1151
- workspace: z5.string().optional(),
1152
- template: z5.enum(TEMPLATE_KEYS).optional(),
1153
- stages: z5.array(stageInput.omit({ was: true })).min(1).max(20).optional(),
1154
- goal_kpi: z5.enum(GOAL_KPIS).default("revenue"),
1155
- goal_stage: z5.string().optional().describe("Etapa que conta como meta (s\xF3 para stage_reached; padr\xE3o: a \xFAltima)"),
1156
- next_funnel: nextFunnelArg
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)"),
1795
+ next_funnel: nextFunnelArg,
1796
+ auto_assign: autoAssignArg
1157
1797
  }
1158
1798
  },
1159
1799
  withApi(
@@ -1166,9 +1806,10 @@ function registerStructureTools(server) {
1166
1806
  workspaceId: ws.id,
1167
1807
  name: a.name,
1168
1808
  goalKpi: a.goal_kpi,
1169
- ...a.stages ? { stages: a.stages } : { template: a.template ?? "sales" },
1809
+ ...a.stages ? { stages: a.stages.map((s) => ({ name: s.name, probability: s.probability, limitDays: s.limit_days ?? null })) } : { template: a.template ?? "sales" },
1170
1810
  ...goalIdx >= 0 ? { goalStageIndex: goalIdx } : {},
1171
- ...a.next_funnel ? { nextFunnelId: nextFunnelId(ws, a.next_funnel) } : {}
1811
+ ...a.next_funnel ? { nextFunnelId: nextFunnelId(ws, a.next_funnel) } : {},
1812
+ ...a.auto_assign !== void 0 ? { autoAssign: a.auto_assign } : {}
1172
1813
  });
1173
1814
  return text({ workspace: ws.name, created: describeFunnel(f, ws) });
1174
1815
  }
@@ -1178,15 +1819,16 @@ function registerStructureTools(server) {
1178
1819
  "update_funnel",
1179
1820
  {
1180
1821
  title: "Alterar funil",
1181
- description: "Renomeia o funil, troca o KPI de meta ou redefine as etapas (s\xF3 admins). stages \xE9 a lista completa e na ordem: etapas com o mesmo nome (ou com was) s\xE3o mantidas, as novas s\xE3o criadas e as que ficarem de fora s\xE3o removidas \u2014 o que s\xF3 funciona se n\xE3o tiverem leads (mova antes com bulk_update_leads). next_funnel define para onde o cliente vai depois de ganho (ex.: Aquisi\xE7\xE3o \u2192 Relacionamento).",
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.",
1182
1823
  inputSchema: {
1183
- funnel: z5.string(),
1184
- workspace: z5.string().optional(),
1185
- name: z5.string().min(1).max(80).optional(),
1186
- goal_kpi: z5.enum(GOAL_KPIS).optional(),
1187
- goal_stage: z5.string().optional(),
1188
- stages: z5.array(stageInput).min(1).max(20).optional(),
1189
- next_funnel: nextFunnelArg
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(),
1830
+ next_funnel: nextFunnelArg,
1831
+ auto_assign: autoAssignArg
1190
1832
  }
1191
1833
  },
1192
1834
  withApi(async (api, a) => {
@@ -1196,13 +1838,19 @@ function registerStructureTools(server) {
1196
1838
  const next = nextFunnelId(ws, a.next_funnel);
1197
1839
  const stages = a.stages?.map((s) => {
1198
1840
  const existing = funnel.stages.find((x) => norm(x.name) === norm(s.was ?? s.name));
1199
- return { ...existing ? { id: existing.id } : {}, name: s.name, probability: s.probability };
1841
+ return {
1842
+ ...existing ? { id: existing.id } : {},
1843
+ name: s.name,
1844
+ probability: s.probability,
1845
+ ...s.limit_days !== void 0 ? { limitDays: s.limit_days } : {}
1846
+ };
1200
1847
  });
1201
1848
  let f = await api.patch(`/funnels/${funnel.id}`, {
1202
1849
  ...a.name ? { name: a.name } : {},
1203
1850
  ...a.goal_kpi ? { goalKpi: a.goal_kpi } : {},
1204
1851
  ...stages ? { stages } : {},
1205
- ...next !== void 0 ? { nextFunnelId: next } : {}
1852
+ ...next !== void 0 ? { nextFunnelId: next } : {},
1853
+ ...a.auto_assign !== void 0 ? { autoAssign: a.auto_assign } : {}
1206
1854
  });
1207
1855
  if (a.goal_stage || a.goal_kpi === "stage_reached" && !f.goalStageId) {
1208
1856
  const goal = a.goal_stage ? resolve(f.stages, a.goal_stage, "Etapa") : f.stages[f.stages.length - 1];
@@ -1216,7 +1864,7 @@ function registerStructureTools(server) {
1216
1864
  {
1217
1865
  title: "Excluir funil",
1218
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.",
1219
- inputSchema: { funnel: z5.string(), workspace: z5.string().optional() },
1867
+ inputSchema: { funnel: z8.string(), workspace: z8.string().optional() },
1220
1868
  annotations: { destructiveHint: true }
1221
1869
  },
1222
1870
  withApi(async (api, a) => {
@@ -1228,15 +1876,15 @@ function registerStructureTools(server) {
1228
1876
  }
1229
1877
 
1230
1878
  // src/tools/team.ts
1231
- import { z as z6 } from "zod";
1879
+ import { z as z9 } from "zod";
1232
1880
  var wsNames = (c, ids = []) => ids.map((id2) => c.workspaces.find((w) => w.id === id2)?.name ?? id2);
1233
1881
  function registerTeamTools(server) {
1234
1882
  server.registerTool(
1235
1883
  "list_users",
1236
1884
  {
1237
1885
  title: "Listar usu\xE1rios",
1238
- description: "Lista as pessoas da organiza\xE7\xE3o: papel (admin/member), se est\xE1 ativa, convite pendente e workspaces de cada uma.",
1239
- inputSchema: { include_inactive: z6.boolean().default(false) },
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) },
1240
1888
  annotations: { readOnlyHint: true }
1241
1889
  },
1242
1890
  withApi(async (api, a) => {
@@ -1257,12 +1905,12 @@ function registerTeamTools(server) {
1257
1905
  "invite_user",
1258
1906
  {
1259
1907
  title: "Convidar usu\xE1rio",
1260
- 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.",
1261
1909
  inputSchema: {
1262
- name: z6.string().min(2),
1263
- email: z6.string().email(),
1264
- role: z6.enum(["member", "admin"]).default("member"),
1265
- workspaces: z6.array(z6.string()).optional().describe("Nomes dos workspaces (padr\xE3o: o primeiro)")
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)")
1266
1914
  }
1267
1915
  },
1268
1916
  withApi(async (api, a) => {
@@ -1281,8 +1929,8 @@ function registerTeamTools(server) {
1281
1929
  "resend_invite",
1282
1930
  {
1283
1931
  title: "Gerar novo link de convite",
1284
- description: "Gera um novo link de convite para algu\xE9m (s\xF3 admins) \u2014 \xFAtil se o anterior expirou. O link antigo deixa de valer.",
1285
- inputSchema: { user: z6.string().describe("Nome ou email") }
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") }
1286
1934
  },
1287
1935
  withApi(async (api, a) => {
1288
1936
  const c = await ctx(api);
@@ -1295,13 +1943,13 @@ function registerTeamTools(server) {
1295
1943
  "update_user",
1296
1944
  {
1297
1945
  title: "Alterar usu\xE1rio",
1298
- 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).",
1299
1947
  inputSchema: {
1300
- user: z6.string().describe("Nome ou email"),
1301
- name: z6.string().min(2).optional(),
1302
- role: z6.enum(["member", "admin"]).optional(),
1303
- active: z6.boolean().optional(),
1304
- workspaces: z6.array(z6.string()).optional().describe("Lista completa de workspaces da pessoa (substitui a atual)")
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)")
1305
1953
  }
1306
1954
  },
1307
1955
  withApi(async (api, a) => {
@@ -1330,15 +1978,31 @@ function registerManageTools(server) {
1330
1978
  registerTeamTools(server);
1331
1979
  registerStructureTools(server);
1332
1980
  registerGoalTools(server);
1981
+ registerDistributionTools(server);
1982
+ registerRiskTools(server);
1333
1983
  registerAdminTools(server);
1334
1984
  }
1335
1985
 
1336
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
+ }
1337
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
+ }
1338
2002
  const server = new McpServer(
1339
- { name: "aicrm", version: "0.2.0" },
2003
+ { name: "aicrm", version: "0.6.0" },
1340
2004
  {
1341
- instructions: "AICRM \xE9 um CRM de vendas. Workspaces cont\xEAm funis; funis cont\xEAm etapas e leads. Status do lead: open, won, lost. Workspaces, funis, etapas e vendedores podem ser referenciados por nome. Valores monet\xE1rios est\xE3o em reais (BRL). Use get_dashboard/get_forecast para metas e proje\xE7\xF5es estat\xEDsticas (Monte Carlo bayesiano). Cada workspace pode ter campos personalizados (ex.: Cidade, Porte) e esconder campos padr\xE3o: veja list_fields. Em create_lead/update_lead, preencha-os em custom_fields usando o nome do campo. Lead ganho n\xE3o troca de funil: para levar o cliente adiante (ex.: Aquisi\xE7\xE3o \u2192 Relacionamento), use handoff_lead ou next_funnel em mark_won, que criam um lead novo ligado ao ganho. Antes de importar um CSV desconhecido, rode import_leads_csv com dry_run=true para revisar o mapeamento de colunas. Admins tamb\xE9m gerenciam time (invite_user, update_user), estrutura (create_workspace, create_funnel, update_funnel), metas e comiss\xF5es (set_goals, save_commission_plan) e modo TV. O servidor aplica as permiss\xF5es do usu\xE1rio logado: se uma a\xE7\xE3o for negada, explique que precisa de um admin. A\xE7\xF5es destrutivas (delete_*, archive_field, revoke_*) exigem confirma\xE7\xE3o expl\xEDcita da pessoa antes."
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.'
1342
2006
  }
1343
2007
  );
1344
2008
  server.registerTool(
@@ -1364,7 +2028,8 @@ async function startServer() {
1364
2028
  goal_kpi: f.goalKpi,
1365
2029
  ...f.goalKpi === "stage_reached" ? { goal_stage: f.stages.find((s) => s.id === f.goalStageId)?.name ?? null } : {},
1366
2030
  ...f.nextFunnelId ? { after_won_continue_in: c.funnels.get(f.nextFunnelId)?.name ?? null } : {},
1367
- stages: f.stages.map((s) => `${s.name} (${s.probability}%)`)
2031
+ ...f.autoAssign ? { auto_assign: "leads sem vendedor s\xE3o distribu\xEDdos por desempenho" } : {},
2032
+ stages: f.stages.map((s) => `${s.name} (${s.probability}%${s.limitDays ? `, prazo ${s.limitDays} dias \xFAteis` : ""})`)
1368
2033
  }))
1369
2034
  })),
1370
2035
  sellers: c.users.filter((u) => u.active).map((u) => `${u.name} <${u.email}>`)
@@ -1375,17 +2040,17 @@ async function startServer() {
1375
2040
  "search_leads",
1376
2041
  {
1377
2042
  title: "Buscar leads",
1378
- description: "Busca leads por texto e filtros. filter: mine (meus), stale (parados h\xE1 14+ dias), no_next_step (sem pr\xF3ximo passo).",
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).",
1379
2044
  inputSchema: {
1380
- query: z7.string().optional().describe("Texto em nome, empresa ou email"),
1381
- workspace: z7.string().optional(),
1382
- funnel: z7.string().optional(),
1383
- stage: z7.string().optional(),
1384
- owner: z7.string().optional().describe("Nome ou email do respons\xE1vel"),
1385
- status: z7.enum(["open", "won", "lost"]).optional(),
1386
- filter: z7.enum(["mine", "stale", "no_next_step"]).optional(),
1387
- field_filters: z7.record(z7.string(), z7.string()).optional().describe('Filtra por campos personalizados: { "Cidade": "Recife" }. Texto: cont\xE9m; demais tipos: igual (datas em dd/mm/aaaa, sim/n\xE3o).'),
1388
- limit: z7.number().int().min(1).max(200).default(25)
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)
1389
2054
  },
1390
2055
  annotations: { readOnlyHint: true }
1391
2056
  },
@@ -1420,7 +2085,7 @@ async function startServer() {
1420
2085
  {
1421
2086
  title: "Detalhes do lead",
1422
2087
  description: "Retorna todos os campos de um lead e o hist\xF3rico (movimenta\xE7\xF5es e notas).",
1423
- inputSchema: { lead_id: z7.string().uuid() },
2088
+ inputSchema: { lead_id: z10.string().uuid() },
1424
2089
  annotations: { readOnlyHint: true }
1425
2090
  },
1426
2091
  withApi(async (api, a) => {
@@ -1453,17 +2118,17 @@ async function startServer() {
1453
2118
  })
1454
2119
  );
1455
2120
  const leadFields = {
1456
- name: z7.string().optional(),
1457
- company: z7.string().optional(),
1458
- email: z7.string().optional(),
1459
- phone: z7.string().optional(),
1460
- title: z7.string().optional(),
1461
- value: z7.number().min(0).optional().describe("Valor do neg\xF3cio em reais"),
1462
- source: z7.enum(LEAD_SOURCES).optional(),
1463
- next_step: z7.string().optional(),
1464
- next_step_date: z7.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
1465
- expected_close: z7.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
1466
- custom_fields: z7.record(z7.string(), z7.union([z7.string(), z7.number(), z7.boolean(), z7.null()])).optional().describe('Campos personalizados do workspace pelo nome: { "Cidade": "Recife", "Faturamento": 50000 }. Dinheiro em reais; null limpa o campo. Veja list_fields.')
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.')
1467
2132
  };
1468
2133
  const toBody = (a) => Object.fromEntries(
1469
2134
  Object.entries({
@@ -1483,8 +2148,8 @@ async function startServer() {
1483
2148
  "create_lead",
1484
2149
  {
1485
2150
  title: "Criar lead",
1486
- description: "Cria um lead. Sem funil/etapa, usa o primeiro funil do workspace e a primeira etapa. O respons\xE1vel padr\xE3o \xE9 voc\xEA.",
1487
- inputSchema: { ...leadFields, name: z7.string(), workspace: z7.string().optional(), funnel: z7.string().optional(), stage: z7.string().optional(), owner: z7.string().optional() }
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() }
1488
2153
  },
1489
2154
  withApi(async (api, a) => {
1490
2155
  const c = await ctx(api);
@@ -1507,7 +2172,7 @@ async function startServer() {
1507
2172
  {
1508
2173
  title: "Atualizar lead",
1509
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.',
1510
- inputSchema: { lead_id: z7.string().uuid(), ...leadFields, owner: z7.string().optional() }
2175
+ inputSchema: { lead_id: z10.string().uuid(), ...leadFields, owner: z10.string().optional() }
1511
2176
  },
1512
2177
  withApi(async (api, a) => {
1513
2178
  const c = await ctx(api);
@@ -1527,8 +2192,8 @@ async function startServer() {
1527
2192
  "move_lead",
1528
2193
  {
1529
2194
  title: "Mover lead",
1530
- description: "Move um lead para outra etapa \u2014 e opcionalmente para outro funil (entre workspaces: s\xF3 admins). Lead ganho n\xE3o muda de funil (a venda precisa continuar contando onde foi fechada): para lev\xE1-lo a outro funil, use handoff_lead.",
1531
- inputSchema: { lead_id: z7.string().uuid(), stage: z7.string().describe("Nome ou id da etapa de destino"), funnel: z7.string().optional().describe("Funil de destino (padr\xE3o: o atual)") }
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)") }
1532
2197
  },
1533
2198
  withApi(async (api, a) => {
1534
2199
  const c = await ctx(api);
@@ -1545,12 +2210,12 @@ async function startServer() {
1545
2210
  title: "Marcar como ganho",
1546
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.",
1547
2212
  inputSchema: {
1548
- lead_id: z7.string().uuid(),
1549
- value: z7.number().min(0).optional(),
1550
- next_funnel: z7.string().optional().describe("Funil onde o cliente continua depois do ganho"),
1551
- next_stage: z7.string().optional().describe("Etapa de entrada no pr\xF3ximo funil (padr\xE3o: a primeira)"),
1552
- next_owner: z7.string().optional().describe("Respons\xE1vel no pr\xF3ximo funil (padr\xE3o: o mesmo)"),
1553
- next_value: z7.number().min(0).optional().describe("Valor em reais no pr\xF3ximo funil (padr\xE3o: 0)")
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)")
1554
2219
  }
1555
2220
  },
1556
2221
  withApi(async (api, a) => {
@@ -1578,7 +2243,7 @@ async function startServer() {
1578
2243
  {
1579
2244
  title: "Marcar como perdido",
1580
2245
  description: "Marca o lead como perdido com um motivo: price, timing, competitor, no_response, no_fit, other.",
1581
- inputSchema: { lead_id: z7.string().uuid(), reason: z7.enum(LOST_REASONS), note: z7.string().optional() }
2246
+ inputSchema: { lead_id: z10.string().uuid(), reason: z10.enum(LOST_REASONS), note: z10.string().optional() }
1582
2247
  },
1583
2248
  withApi(async (api, a) => {
1584
2249
  const c = await ctx(api);
@@ -1591,7 +2256,7 @@ async function startServer() {
1591
2256
  {
1592
2257
  title: "Adicionar nota",
1593
2258
  description: "Adiciona uma nota \xE0 linha do tempo do lead.",
1594
- inputSchema: { lead_id: z7.string().uuid(), note: z7.string().min(1) }
2259
+ inputSchema: { lead_id: z10.string().uuid(), note: z10.string().min(1) }
1595
2260
  },
1596
2261
  withApi(async (api, a) => {
1597
2262
  await api.post(`/leads/${a.lead_id}/notes`, { body: a.note });
@@ -1603,7 +2268,7 @@ async function startServer() {
1603
2268
  {
1604
2269
  title: "Dashboard de metas",
1605
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.",
1606
- inputSchema: { funnel: z7.string().optional(), workspace: z7.string().optional(), month: z7.string().regex(/^\d{4}-\d{2}$/).optional().describe("YYYY-MM; padr\xE3o: m\xEAs atual") },
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") },
1607
2272
  annotations: { readOnlyHint: true }
1608
2273
  },
1609
2274
  withApi(async (api, a) => {
@@ -1639,7 +2304,7 @@ async function startServer() {
1639
2304
  {
1640
2305
  title: "Vai bater a meta?",
1641
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.",
1642
- inputSchema: { seller: z7.string().describe("Nome ou email do vendedor"), funnel: z7.string().optional(), month: z7.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() },
1643
2308
  annotations: { readOnlyHint: true }
1644
2309
  },
1645
2310
  withApi(async (api, a) => {
@@ -1658,12 +2323,62 @@ async function startServer() {
1658
2323
  });
1659
2324
  })
1660
2325
  );
2326
+ server.registerTool(
2327
+ "get_today_plan",
2328
+ {
2329
+ title: "Plano do dia",
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.',
2331
+ inputSchema: {
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)")
2336
+ },
2337
+ annotations: { readOnlyHint: true }
2338
+ },
2339
+ withApi(async (api, a) => {
2340
+ const c = await ctx(api);
2341
+ const funnel = pickFunnel(c, a.funnel, a.workspace);
2342
+ const userId = a.seller ? findUser(c, a.seller).id : void 0;
2343
+ const d = await api.get(`/today?funnelId=${funnel.id}${userId ? `&userId=${userId}` : ""}${a.limit ? `&limit=${a.limit}` : ""}`);
2344
+ const kpi = d.funnel.goalKpi;
2345
+ const suggest = (i) => i.action.kind === "whatsapp" && i.whatsappUrl ? `Chamar no WhatsApp: ${i.whatsappUrl} (entregue o link; depois registre o pr\xF3ximo passo com update_lead)` : i.action.kind === "move_stage" && i.nextStage ? `Se j\xE1 avan\xE7ou, mover para ${i.nextStage.name} (move_lead)` : "Combinar e registrar o pr\xF3ximo passo com data (update_lead com next_step e next_step_date)";
2346
+ return text({
2347
+ seller: d.seller.name,
2348
+ funnel: d.funnel.name,
2349
+ summary: d.summary,
2350
+ goal: d.goal ? {
2351
+ status: FORECAST_STATUS_LABEL[d.goal.status],
2352
+ probability_to_hit: formatPct(d.goal.probability),
2353
+ actual: formatKpi(kpi, d.goal.actual),
2354
+ target: formatKpi(kpi, d.goal.target),
2355
+ missing: formatKpi(kpi, d.goal.gap),
2356
+ low_confidence: d.goal.lowConfidence
2357
+ } : "sem meta neste m\xEAs (ordem por valor, chance e urg\xEAncia)",
2358
+ plan: d.items.map((i) => ({
2359
+ rank: i.rank,
2360
+ lead_id: i.lead.id,
2361
+ lead: i.lead.company ? `${i.lead.company} \u2014 ${i.lead.name}` : i.lead.name,
2362
+ stage: i.stageName,
2363
+ value: formatMoney(i.lead.valueCents),
2364
+ chance: formatPct(i.chance),
2365
+ ...kpi === "revenue" ? { worth_for_goal: formatMoney(i.expected) } : {},
2366
+ ...i.shareOfGap != null && kpi === "revenue" ? { share_of_missing: formatPct(Math.min(1, i.shareOfGap)) } : {},
2367
+ why: i.reasons.map((r) => r.text),
2368
+ suggested_action: suggest(i),
2369
+ phone: i.lead.phone
2370
+ })),
2371
+ done_today: d.doneToday.map((x) => `${x.name}: ${x.what}`),
2372
+ open_leads: d.counts.open
2373
+ });
2374
+ })
2375
+ );
1661
2376
  server.registerTool(
1662
2377
  "list_fields",
1663
2378
  {
1664
2379
  title: "Campos do workspace",
1665
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.",
1666
- inputSchema: { workspace: z7.string().optional() },
2381
+ inputSchema: { workspace: z10.string().optional() },
1667
2382
  annotations: { readOnlyHint: true }
1668
2383
  },
1669
2384
  withApi(async (api, a) => {
@@ -1695,10 +2410,10 @@ async function startServer() {
1695
2410
  title: "Criar campo personalizado",
1696
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.",
1697
2412
  inputSchema: {
1698
- label: z7.string().min(1).max(60).describe("Nome do campo, ex.: Cidade"),
1699
- type: z7.enum(CUSTOM_FIELD_TYPES),
1700
- options: z7.array(z7.string()).optional().describe("Op\xE7\xF5es da lista (tipo select)"),
1701
- workspace: z7.string().optional()
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()
1702
2417
  }
1703
2418
  },
1704
2419
  withApi(async (api, a) => {
@@ -1714,10 +2429,10 @@ async function startServer() {
1714
2429
  title: "Editar campo personalizado",
1715
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.",
1716
2431
  inputSchema: {
1717
- field: z7.string().describe("Nome ou id do campo"),
1718
- label: z7.string().min(1).max(60).optional(),
1719
- options: z7.array(z7.string()).optional().describe("Lista completa de op\xE7\xF5es (substitui a atual)"),
1720
- workspace: z7.string().optional()
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()
1721
2436
  }
1722
2437
  },
1723
2438
  withApi(async (api, a) => {
@@ -1733,7 +2448,7 @@ async function startServer() {
1733
2448
  {
1734
2449
  title: "Remover campo personalizado",
1735
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.",
1736
- inputSchema: { field: z7.string().describe("Nome ou id do campo"), workspace: z7.string().optional() },
2451
+ inputSchema: { field: z10.string().describe("Nome ou id do campo"), workspace: z10.string().optional() },
1737
2452
  annotations: { destructiveHint: true }
1738
2453
  },
1739
2454
  withApi(async (api, a) => {
@@ -1750,9 +2465,9 @@ async function startServer() {
1750
2465
  title: "Mostrar/esconder campos padr\xE3o",
1751
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.`,
1752
2467
  inputSchema: {
1753
- hide: z7.array(z7.enum(HIDEABLE_FIELDS)).optional(),
1754
- show: z7.array(z7.enum(HIDEABLE_FIELDS)).optional(),
1755
- workspace: z7.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()
1756
2471
  }
1757
2472
  },
1758
2473
  withApi(async (api, a) => {
@@ -1768,7 +2483,7 @@ async function startServer() {
1768
2483
  {
1769
2484
  title: "Renomear modelo de importa\xE7\xE3o",
1770
2485
  description: "Renomeia um modelo de importa\xE7\xE3o de CSV (admins ou quem criou o modelo).",
1771
- inputSchema: { template: z7.string().describe("Nome ou id do modelo"), name: z7.string().min(1).max(80), workspace: z7.string().optional() }
2486
+ inputSchema: { template: z10.string().describe("Nome ou id do modelo"), name: z10.string().min(1).max(80), workspace: z10.string().optional() }
1772
2487
  },
1773
2488
  withApi(async (api, a) => {
1774
2489
  const c = await ctx(api);
@@ -1783,7 +2498,7 @@ async function startServer() {
1783
2498
  {
1784
2499
  title: "Apagar modelo de importa\xE7\xE3o",
1785
2500
  description: "Apaga um modelo de importa\xE7\xE3o (admins ou quem criou). Leads j\xE1 importados n\xE3o mudam.",
1786
- inputSchema: { template: z7.string().describe("Nome ou id do modelo"), workspace: z7.string().optional() },
2501
+ inputSchema: { template: z10.string().describe("Nome ou id do modelo"), workspace: z10.string().optional() },
1787
2502
  annotations: { destructiveHint: true }
1788
2503
  },
1789
2504
  withApi(async (api, a) => {
@@ -1800,15 +2515,17 @@ async function startServer() {
1800
2515
  title: "Importar CSV",
1801
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" }.',
1802
2517
  inputSchema: {
1803
- path: z7.string().describe("Caminho do arquivo CSV (aceita ~)"),
1804
- funnel: z7.string().optional(),
1805
- workspace: z7.string().optional(),
1806
- stage: z7.string().optional(),
1807
- owner_mode: z7.enum(["me", "round_robin", "column"]).default("me").describe("me = voc\xEA; round_robin = distribuir; column = coluna 'respons\xE1vel' (email)"),
1808
- duplicate: z7.enum(["skip", "update"]).default("skip"),
1809
- columns: z7.record(z7.string(), z7.string()).optional().describe('For\xE7a o destino de colunas: { "Munic\xEDpio": "Cidade", "Obs": "ignorar" }. Aceita campo padr\xE3o (Nome, Empresa, Email\u2026) ou personalizado.'),
1810
- dry_run: z7.boolean().default(false).describe("S\xF3 mostra o mapeamento e os problemas, sem importar"),
1811
- template_name: z7.string().max(80).optional().describe("Salva o mapeamento com este nome para as pr\xF3ximas importa\xE7\xF5es")
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")
1812
2529
  }
1813
2530
  },
1814
2531
  withApi(
@@ -1818,8 +2535,8 @@ async function startServer() {
1818
2535
  const stage = resolve(funnel.stages, a.stage, "Etapa") ?? funnel.stages[0];
1819
2536
  const ws = c.workspaces.find((w) => w.id === funnel.workspaceId);
1820
2537
  const hidden = ws?.hiddenFields ?? [];
1821
- const file = path2.resolve(a.path.replace(/^~(?=$|\/)/, homedir2()));
1822
- 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() });
1823
2540
  const headers = (parsed.meta.fields ?? []).filter(Boolean);
1824
2541
  const [fields, templates] = await Promise.all([listFields(api, funnel.workspaceId), listTemplates(api, funnel.workspaceId)]);
1825
2542
  const match = matchTemplate(headers, templates);
@@ -1890,16 +2607,18 @@ async function startServer() {
1890
2607
  }
1891
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;
1892
2609
  const me = await api.get("/auth/me");
2610
+ const ownerMode = a.owner_mode ?? (me.user.role === "member" ? "me" : "auto");
1893
2611
  const res = await api.post(
1894
2612
  "/imports",
1895
2613
  {
1896
2614
  funnelId: funnel.id,
1897
2615
  stageId: stage.id,
1898
- ownerMode: a.owner_mode === "me" ? "fixed" : a.owner_mode,
1899
- 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,
1900
2618
  duplicate: a.duplicate,
1901
2619
  rows,
1902
- template
2620
+ template,
2621
+ origin: { fileName: path3.basename(a.path).slice(0, 200), valueColumn: headers.find((h) => mapping[h] === "std:value") }
1903
2622
  }
1904
2623
  );
1905
2624
  return text({
@@ -1915,9 +2634,19 @@ async function startServer() {
1915
2634
  )
1916
2635
  );
1917
2636
  registerManageTools(server);
2637
+ if (role === "admin") registerIntegrationTools(server);
1918
2638
  await server.connect(new StdioServerTransport());
1919
2639
  }
1920
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
+
1921
2650
  // src/cli.ts
1922
2651
  var HELP = `AICRM MCP
1923
2652
 
@@ -1925,9 +2654,10 @@ Uso:
1925
2654
  aicrm-mcp login [--url https://seu-aicrm] [--email voce@empresa.com]
1926
2655
  aicrm-mcp logout
1927
2656
  aicrm-mcp whoami
1928
- aicrm-mcp skill imprime o SKILL.md (guia para agentes)
1929
- aicrm-mcp skill install [--project | --dir <pasta>]
1930
- 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)
1931
2661
  aicrm-mcp inicia o servidor MCP (stdio) \u2014 use no seu cliente MCP
1932
2662
 
1933
2663
  Configurar:
@@ -1940,7 +2670,7 @@ function flag(name) {
1940
2670
  }
1941
2671
  async function login() {
1942
2672
  const existing = await loadCredentials();
1943
- 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"));
1944
2674
  const email2 = flag("email") ?? await ask("Email", existing?.user.email || void 0);
1945
2675
  const password2 = await askHidden("Senha");
1946
2676
  try {
@@ -1956,6 +2686,10 @@ async function login() {
1956
2686
  console.log(`
1957
2687
  Agora adicione ao seu cliente:
1958
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`);
1959
2693
  } catch (e) {
1960
2694
  console.error(`
1961
2695
  \u2717 ${e.message}`);
@@ -1983,14 +2717,18 @@ async function whoami() {
1983
2717
  process.exit(1);
1984
2718
  }
1985
2719
  }
1986
- var SKILL_PATH = new URL("../skill/aicrm/SKILL.md", import.meta.url);
1987
2720
  async function skill() {
1988
- const content = await readFile3(SKILL_PATH, "utf8");
1989
- if (process.argv[3] !== "install") return void process.stdout.write(content);
1990
- const base = flag("dir") ?? (process.argv.includes("--project") ? path3.join(process.cwd(), ".claude", "skills") : path3.join(homedir3(), ".claude", "skills"));
1991
- const target = path3.join(base, "aicrm", "SKILL.md");
1992
- await mkdir2(path3.dirname(target), { recursive: true });
1993
- 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);
1994
2732
  console.log(`\u2713 Skill instalado em ${target}`);
1995
2733
  }
1996
2734
  var cmd = process.argv[2];