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