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/README.md +18 -4
- package/dist/cli.js +925 -187
- package/package.json +11 -9
- package/skill/aicrm/SKILL.md +45 -27
- package/skill/aicrm-integrate/SKILL.md +130 -0
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
|
|
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
|
|
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,
|
|
20
|
-
const res = await fetch(`${normalizeUrl(base)}/api${
|
|
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
|
|
128
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
129
129
|
import { homedir as homedir2 } from "os";
|
|
130
|
-
import
|
|
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
|
-
/**
|
|
319
|
-
|
|
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
|
|
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([
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
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 (
|
|
810
|
-
inputSchema: { name:
|
|
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 (
|
|
823
|
-
inputSchema: { 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:
|
|
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
|
|
868
|
-
var
|
|
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(
|
|
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:
|
|
891
|
-
kind:
|
|
892
|
-
tiers:
|
|
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"]:
|
|
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:
|
|
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:
|
|
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:
|
|
955
|
-
funnel:
|
|
956
|
-
workspace:
|
|
957
|
-
month:
|
|
958
|
-
commission_plan:
|
|
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
|
|
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:
|
|
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:
|
|
1016
|
-
funnel:
|
|
1017
|
-
stage:
|
|
1018
|
-
workspace:
|
|
1019
|
-
owner:
|
|
1020
|
-
value:
|
|
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:
|
|
1038
|
-
owner:
|
|
1039
|
-
stage:
|
|
1040
|
-
funnel:
|
|
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:
|
|
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
|
|
1711
|
+
import { z as z8 } from "zod";
|
|
1080
1712
|
var TEMPLATE_KEYS = Object.keys(FUNNEL_TEMPLATES);
|
|
1081
|
-
var stageInput =
|
|
1082
|
-
name:
|
|
1083
|
-
probability:
|
|
1084
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
1103
|
-
visibility:
|
|
1104
|
-
create_funnel:
|
|
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:
|
|
1120
|
-
name:
|
|
1121
|
-
visibility:
|
|
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:
|
|
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:
|
|
1151
|
-
workspace:
|
|
1152
|
-
template:
|
|
1153
|
-
stages:
|
|
1154
|
-
goal_kpi:
|
|
1155
|
-
goal_stage:
|
|
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:
|
|
1184
|
-
workspace:
|
|
1185
|
-
name:
|
|
1186
|
-
goal_kpi:
|
|
1187
|
-
goal_stage:
|
|
1188
|
-
stages:
|
|
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 {
|
|
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:
|
|
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
|
|
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
|
|
1239
|
-
inputSchema: { include_inactive:
|
|
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
|
|
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:
|
|
1263
|
-
email:
|
|
1264
|
-
role:
|
|
1265
|
-
workspaces:
|
|
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
|
|
1285
|
-
inputSchema: { user:
|
|
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
|
|
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:
|
|
1301
|
-
name:
|
|
1302
|
-
role:
|
|
1303
|
-
active:
|
|
1304
|
-
workspaces:
|
|
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.
|
|
2003
|
+
{ name: "aicrm", version: "0.6.0" },
|
|
1340
2004
|
{
|
|
1341
|
-
instructions:
|
|
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
|
-
|
|
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 (
|
|
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:
|
|
1381
|
-
workspace:
|
|
1382
|
-
funnel:
|
|
1383
|
-
stage:
|
|
1384
|
-
owner:
|
|
1385
|
-
status:
|
|
1386
|
-
filter:
|
|
1387
|
-
field_filters:
|
|
1388
|
-
limit:
|
|
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:
|
|
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:
|
|
1457
|
-
company:
|
|
1458
|
-
email:
|
|
1459
|
-
phone:
|
|
1460
|
-
title:
|
|
1461
|
-
value:
|
|
1462
|
-
source:
|
|
1463
|
-
next_step:
|
|
1464
|
-
next_step_date:
|
|
1465
|
-
expected_close:
|
|
1466
|
-
custom_fields:
|
|
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.
|
|
1487
|
-
inputSchema: { ...leadFields, name:
|
|
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:
|
|
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:
|
|
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:
|
|
1549
|
-
value:
|
|
1550
|
-
next_funnel:
|
|
1551
|
-
next_stage:
|
|
1552
|
-
next_owner:
|
|
1553
|
-
next_value:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
1699
|
-
type:
|
|
1700
|
-
options:
|
|
1701
|
-
workspace:
|
|
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:
|
|
1718
|
-
label:
|
|
1719
|
-
options:
|
|
1720
|
-
workspace:
|
|
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:
|
|
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:
|
|
1754
|
-
show:
|
|
1755
|
-
workspace:
|
|
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:
|
|
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:
|
|
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:
|
|
1804
|
-
funnel:
|
|
1805
|
-
workspace:
|
|
1806
|
-
stage:
|
|
1807
|
-
owner_mode:
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
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 =
|
|
1822
|
-
const parsed = Papa.parse(await
|
|
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:
|
|
1899
|
-
ownerId:
|
|
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
|
|
1929
|
-
aicrm-mcp skill install [--project | --dir <pasta>]
|
|
1930
|
-
instala em ~/.claude/skills
|
|
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 ?? "
|
|
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
|
|
1989
|
-
if (
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
await
|
|
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];
|