@fazer-ai/agents 1.0.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.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +112 -0
  3. package/dist/agents/claude.js +152 -0
  4. package/dist/agents/codex.js +155 -0
  5. package/dist/agents/detect.js +15 -0
  6. package/dist/agents/handoff.js +22 -0
  7. package/dist/agents/hermes-skills.js +177 -0
  8. package/dist/agents/hermes.js +474 -0
  9. package/dist/agents/index.js +57 -0
  10. package/dist/agents/manual.js +22 -0
  11. package/dist/agents/other.js +39 -0
  12. package/dist/agents/shell.js +15 -0
  13. package/dist/agents/types.js +2 -0
  14. package/dist/config.js +48 -0
  15. package/dist/exec.js +279 -0
  16. package/dist/hostinger.js +75 -0
  17. package/dist/hub-command.js +144 -0
  18. package/dist/index.js +726 -0
  19. package/dist/licenses.js +93 -0
  20. package/dist/mcp.js +100 -0
  21. package/dist/oauth.js +578 -0
  22. package/dist/onboarding-marker.js +48 -0
  23. package/dist/preferences.js +40 -0
  24. package/dist/skills/agents-dev/SKILL.md +37 -0
  25. package/dist/skills/agents-dev/gotchas.md +6 -0
  26. package/dist/skills/agents-dev/guardrails.md +6 -0
  27. package/dist/skills/agents-dev/references/00-get-the-code.md +28 -0
  28. package/dist/skills/agents-dev/references/01-layout-and-bun-check.md +29 -0
  29. package/dist/skills/agents-dev/references/02-free-full-and-invariants.md +7 -0
  30. package/dist/skills/agents-dev/references/03-implement.md +9 -0
  31. package/dist/skills/agents-dev/references/04-own-image-and-deploy.md +13 -0
  32. package/dist/skills/agents-onboarding/SKILL.md +80 -0
  33. package/dist/skills/agents-onboarding/gotchas.md +157 -0
  34. package/dist/skills/agents-onboarding/guardrails.md +65 -0
  35. package/dist/skills/agents-onboarding/references/00-prereqs-and-access.md +37 -0
  36. package/dist/skills/agents-onboarding/references/01-vps-dns-ssh.md +67 -0
  37. package/dist/skills/agents-onboarding/references/01b-brownfield.md +70 -0
  38. package/dist/skills/agents-onboarding/references/01c-pick-tier.md +61 -0
  39. package/dist/skills/agents-onboarding/references/02-coolify.md +109 -0
  40. package/dist/skills/agents-onboarding/references/03-chatwoot-pro.md +61 -0
  41. package/dist/skills/agents-onboarding/references/04-agents-image.md +46 -0
  42. package/dist/skills/agents-onboarding/references/05-langfuse.md +45 -0
  43. package/dist/skills/agents-onboarding/references/06-setup-and-mcp.md +47 -0
  44. package/dist/skills/agents-onboarding/references/08-agent-import.md +55 -0
  45. package/dist/skills/agents-onboarding/references/09-chatwoot-bind.md +41 -0
  46. package/dist/skills/agents-onboarding/references/10-validate-e2e.md +34 -0
  47. package/dist/skills/agents-onboarding/references/agent-features.md +61 -0
  48. package/dist/skills/agents-onboarding/references/chatwoot-hub-register.md +69 -0
  49. package/dist/skills/agents-onboarding/references/deploy-b-portainer.md +138 -0
  50. package/dist/skills/agents-onboarding/references/deploy-c-compose.md +64 -0
  51. package/dist/skills/agents-onboarding/samples/agents/README.md +23 -0
  52. package/dist/skills/agents-onboarding/samples/agents/maria-clinica-moreira.json +313 -0
  53. package/dist/skills/agents-onboarding/scripts/chatwoot-admin.py +248 -0
  54. package/dist/skills/agents-onboarding/scripts/coolify.py +552 -0
  55. package/dist/skills/agents-onboarding/scripts/docker-status.py +129 -0
  56. package/dist/skills/agents-onboarding/scripts/gen-onboarding-env.ts +187 -0
  57. package/dist/skills/agents-onboarding/scripts/harbor-login.py +118 -0
  58. package/dist/skills/agents-onboarding/scripts/langfuse-verify.py +118 -0
  59. package/dist/skills/agents-onboarding/scripts/portainer-brownfield.py +115 -0
  60. package/dist/skills/agents-onboarding/scripts/remote.py +198 -0
  61. package/dist/skills/agents-onboarding/scripts/sshkey.py +140 -0
  62. package/dist/skills/agents-onboarding/templates/chatwoot/.env.example +30 -0
  63. package/dist/skills/agents-onboarding/templates/chatwoot/README.md +65 -0
  64. package/dist/skills/agents-onboarding/templates/chatwoot/docker-compose.coolify.yml +136 -0
  65. package/dist/skills/agents-onboarding/templates/chatwoot/docker-compose.yml +139 -0
  66. package/dist/skills/agents-onboarding/templates/docker-compose.coolify.yml +73 -0
  67. package/dist/skills/agents-onboarding/templates/docker-compose.portainer.yml +132 -0
  68. package/dist/skills/agents-onboarding/templates/docker-compose.prod.yml +85 -0
  69. package/dist/skills/agents-onboarding/templates/langfuse/.env.example +59 -0
  70. package/dist/skills/agents-onboarding/templates/langfuse/README.md +132 -0
  71. package/dist/skills/agents-onboarding/templates/langfuse/docker-compose.coolify.yml +189 -0
  72. package/dist/skills/agents-onboarding/templates/langfuse/docker-compose.yml +185 -0
  73. package/dist/skills/agents-operation/SKILL.md +42 -0
  74. package/dist/skills/agents-operation/gotchas.md +61 -0
  75. package/dist/skills/agents-operation/guardrails.md +26 -0
  76. package/dist/skills/agents-operation/references/00-production-safety.md +24 -0
  77. package/dist/skills/agents-operation/references/01-diagnose.md +34 -0
  78. package/dist/skills/agents-operation/references/02-reproduce.md +22 -0
  79. package/dist/skills/agents-operation/references/03-adjust.md +36 -0
  80. package/dist/skills/agents-operation/references/04-validate-and-apply.md +31 -0
  81. package/dist/ui-select.js +279 -0
  82. package/dist/ui.js +167 -0
  83. package/package.json +53 -0
@@ -0,0 +1,31 @@
1
+ # 04: Validar e aplicar
2
+
3
+ Ordem: re-validar no playground → (quando fizer sentido) conversa de teste controlada → aplicar em produção com aprovação.
4
+
5
+ ## 1. Re-validar no playground
6
+
7
+ Com o ajuste **proposto** (ou já aplicado num agente de teste/clone), repita no playground o mesmo turno do `02-reproduce.md` e confirme que o comportamento corrigiu. Use `trace` + `sources` para checar a tool/grounding certos. Itere aqui (barato, isolado) antes de tocar tráfego real.
8
+
9
+ ## 2. Conversa de teste controlada (Inbox API)
10
+
11
+ Quando o problema depende da ponta real (webhook → debounce → turn → reply) e não só do agente, prove headless com um inbox `Channel::Api`, sem aparelho:
12
+
13
+ - Crie/use um inbox `Channel::Api` no Chatwoot bound ao agente (auto-provisiona Agent Bot + webhook).
14
+ - Injete uma mensagem **incoming** pela API do Chatwoot (criar conversa + `POST .../messages` com `message_type: incoming`).
15
+ - Cadeia esperada: incoming → `/api/v1/chatwoot/webhook/:routeToken` → debounce → turn → modelo real → resposta **outgoing**. Confirme a resposta + o novo `ExecutionLog`/trace.
16
+
17
+ Isso exercita o caminho de produção sem expor um cliente real. Faça nesse inbox de teste, não numa conversa de cliente.
18
+
19
+ ## 3. Aplicar
20
+
21
+ - Só com **aprovação explícita** do usuário para aquela mudança específica.
22
+ - Console (editor do agente) ou MCP com `dry_run:false` (mostre o diff do dry-run **antes**).
23
+ - O write é registrado: editor → via API normal; MCP → grava `AuditLog` (`actorType: "mcp"`, com before/after projetado e limitado, nunca segredo). Para auditar depois: página de audit ou `audit_list` (MCP read).
24
+
25
+ ## 4. Confirmar em produção
26
+
27
+ Após aplicar, acompanhe os próximos turnos reais no `/logs` + Langfuse e confirme que o estágio que divergia agora passa. Se uma conversa real ficou com erro pendente, há um badge de erro + botão de **re-engage** na conversa (re-responde o tail não respondido), use só com OK, é envio real ao cliente.
28
+
29
+ ## Tocar a conversa real diretamente (último recurso, efeito externo)
30
+
31
+ As tools `conversation_*` de escrita (`reply`/`handoff`/`return`/`status`/`reengage`, MCP `mcp:write`) ou a tela de Conversations postam/mudam estado numa conversa **viva** do cliente. O dry-run do `conversation_reply` mostra o **texto exato** que seria enviado; aplicar **não é reversível**. Só com OK explícito, e confirme que o alvo é a conversa certa.
@@ -0,0 +1,279 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.keyAction = keyAction;
4
+ exports.nextSelectable = nextSelectable;
5
+ exports.clampStart = clampStart;
6
+ exports.summaryTitle = summaryTitle;
7
+ exports.renderFrame = renderFrame;
8
+ exports.select = select;
9
+ exports.acknowledge = acknowledge;
10
+ exports.confirmKey = confirmKey;
11
+ exports.confirm = confirm;
12
+ // Seletor interativo de UMA opção: navegação por ↑/↓ (+ k/j) e Enter, zero-dep
13
+ // (raw mode no mesmo padrão do askSecret). Dígitos 1-9 saltam o destaque. Sem
14
+ // TTY, resolve o default sem prompt. A UI vai pro stderr (como o resto); ao
15
+ // confirmar, colapsa o menu numa linha "✔ título: escolha".
16
+ const ui_1 = require("./ui");
17
+ const ESC = "\x1b[";
18
+ // Mapeia os bytes do raw mode numa ação. Setas = CSI (\x1b[A/B); aceita k/j
19
+ // (vim), Enter (\r/\n), Ctrl-C/Ctrl-D (cancela) e dígitos 1-9 (salto).
20
+ function keyAction(data) {
21
+ if (data === "\x1b[A" || data === "k")
22
+ return "up";
23
+ if (data === "\x1b[B" || data === "j")
24
+ return "down";
25
+ if (data === "\r" || data === "\n")
26
+ return "enter";
27
+ if (data === "\x03" || data === "\x04")
28
+ return "cancel";
29
+ if (/^[1-9]$/.test(data))
30
+ return { digit: Number.parseInt(data, 10) };
31
+ return null;
32
+ }
33
+ // Próximo índice selecionável a partir de `from`, andando `step` (±1), pulando
34
+ // desabilitados, com wrap. Devolve `from` se nada for selecionável.
35
+ function nextSelectable(from, total, step, isDisabled) {
36
+ if (total === 0)
37
+ return from;
38
+ let i = from;
39
+ for (let n = 0; n < total; n++) {
40
+ i = (i + step + total) % total;
41
+ if (!isDisabled(i))
42
+ return i;
43
+ }
44
+ return from;
45
+ }
46
+ // Primeiro índice selecionável em/depois de `wanted` (senão o primeiro de todos).
47
+ function clampStart(wanted, options, isDisabled) {
48
+ const within = Math.min(Math.max(wanted, 0), options.length - 1);
49
+ if (!isDisabled(within))
50
+ return within;
51
+ return nextSelectable(within, options.length, 1, isDisabled);
52
+ }
53
+ // Condensa o título (que pode ser uma pergunta longa com recomendação) num ASSUNTO
54
+ // curto pro resumo pós-escolha: corta na 1ª quebra forte (?, :, .) e tira o "Qual/Onde"
55
+ // inicial pra virar substantivo. Ex.: "Qual agente você prefere usar? Recomendamos…" →
56
+ // "Agente". Fallback: o título inteiro. Puro (testável).
57
+ function summaryTitle(title) {
58
+ const head = title.split(/[?:.]/)[0]?.trim() ?? title;
59
+ const stripped = head
60
+ .replace(/^qual\s+(o\s+|a\s+)?/i, "")
61
+ .replace(/^onde\s+(está\s+)?(o\s+|a\s+)?(sua\s+|seu\s+)?/i, "")
62
+ .replace(/\s+você\s+(quer|prefere)\s+usar$/i, "")
63
+ .trim();
64
+ const subject = stripped || head || title;
65
+ return subject.charAt(0).toUpperCase() + subject.slice(1);
66
+ }
67
+ function lineFor(opt, index, selected) {
68
+ const pointer = selected ? ui_1.c.cyan("❯ ") : " ";
69
+ const num = ui_1.c.cyan(`${index + 1})`);
70
+ const label = opt.disabled
71
+ ? ui_1.c.gray(opt.label)
72
+ : selected
73
+ ? ui_1.c.bold(opt.label)
74
+ : opt.label;
75
+ const hint = opt.hint ? ` ${opt.hint}` : "";
76
+ return `${pointer}${num} ${label}${hint}`;
77
+ }
78
+ // O quadro completo (título + opções), como string. Puro: usado nos testes e
79
+ // reimpresso a cada tecla na versão interativa.
80
+ function renderFrame(title, options, selected) {
81
+ const lines = options.map((o, i) => lineFor(o, i, i === selected));
82
+ return `${(0, ui_1.headline)(title)}\n${lines.join("\n")}\n`;
83
+ }
84
+ function select(config) {
85
+ const { title, options } = config;
86
+ if (options.length === 0)
87
+ throw new Error("select: sem opções");
88
+ const out = config.out ?? process.stderr;
89
+ const isDisabled = (i) => Boolean(options[i]?.disabled);
90
+ const start = clampStart(config.defaultIndex ?? 0, options, isDisabled);
91
+ const startOpt = options[start];
92
+ // Sem TTY (CI/pipe): não há como navegar; resolve o default e segue.
93
+ if (!process.stdin.isTTY) {
94
+ out.write(`${ui_1.sym.ok} ${title}: ${ui_1.c.bold(startOpt.label)} ${ui_1.c.gray("(padrão, sem terminal interativo)")}\n`);
95
+ return Promise.resolve(startOpt.value);
96
+ }
97
+ const frameLines = options.length + 1; // título + opções
98
+ let selected = start;
99
+ const stdin = process.stdin;
100
+ out.write("\n");
101
+ out.write(`${ESC}?25l`); // esconde o cursor
102
+ out.write(renderFrame(title, options, selected));
103
+ return new Promise((resolve) => {
104
+ // Repinta o quadro NO LUGAR: sobe até o título e reescreve cada linha com `\r` +
105
+ // conteúdo + apagar-até-o-fim-da-linha (`\x1b[K`), em vez de limpar tudo (`\x1b[0J`)
106
+ // e repintar. Limpar-tudo deixa o quadro em branco por um instante entre o erase e o
107
+ // paint, o que pisca em terminal lento/SSH. Repintar linha a linha nunca some. Só
108
+ // redesenha quando o destaque MUDA (no-op de tecla repetida não repinta). (item 13)
109
+ const redrawInPlace = () => {
110
+ out.write(`${ESC}${frameLines}A`); // sobe até o título
111
+ // renderFrame termina com \n (split deixa um "" no fim); repinta só as frameLines
112
+ // linhas de conteúdo pra não empurrar o quadro pra baixo a cada tecla.
113
+ const lines = renderFrame(title, options, selected)
114
+ .split("\n")
115
+ .slice(0, frameLines);
116
+ for (const line of lines) {
117
+ // \r → coluna 0; linha; \x1b[K → apaga o resto (caso a nova seja mais curta).
118
+ out.write(`\r${line}${ESC}K\n`);
119
+ }
120
+ };
121
+ // Reposiciona o destaque e repinta só se mudou de fato.
122
+ const moveTo = (next) => {
123
+ if (next === selected)
124
+ return;
125
+ selected = next;
126
+ redrawInPlace();
127
+ };
128
+ const teardown = () => {
129
+ stdin.setRawMode(false);
130
+ stdin.pause();
131
+ stdin.removeListener("data", onData);
132
+ out.write(`${ESC}?25h`); // mostra o cursor
133
+ };
134
+ const onData = (chunk) => {
135
+ const action = keyAction(chunk);
136
+ if (action === "up") {
137
+ moveTo(nextSelectable(selected, options.length, -1, isDisabled));
138
+ }
139
+ else if (action === "down") {
140
+ moveTo(nextSelectable(selected, options.length, 1, isDisabled));
141
+ }
142
+ else if (action && typeof action === "object") {
143
+ const idx = action.digit - 1;
144
+ if (idx < options.length && !isDisabled(idx))
145
+ moveTo(idx);
146
+ }
147
+ else if (action === "enter") {
148
+ if (isDisabled(selected))
149
+ return;
150
+ const chosen = options[selected];
151
+ // Colapsa o quadro (aqui o 0J é legítimo: some com as N linhas de opção) numa
152
+ // linha de resumo clara: ✔ <assunto> · escolhido: <label>. (item 7)
153
+ out.write(`${ESC}${frameLines}A`);
154
+ out.write(`${ESC}0J`);
155
+ out.write(`\r${ui_1.sym.ok} ${summaryTitle(title)} ${ui_1.c.gray("· escolhido:")} ${ui_1.c.bold(chosen.label)}${ESC}K\n`);
156
+ teardown();
157
+ resolve(chosen.value);
158
+ }
159
+ else if (action === "cancel") {
160
+ teardown();
161
+ out.write("\n");
162
+ process.exit(130);
163
+ }
164
+ };
165
+ stdin.setRawMode(true);
166
+ stdin.resume();
167
+ stdin.setEncoding("utf8");
168
+ stdin.on("data", onData);
169
+ });
170
+ }
171
+ // "OK" de ação única: mostra a mensagem e espera UMA tecla (Enter/qualquer) pra
172
+ // seguir. Sem segunda opção, quem chama sempre prossegue (ex.: recarregar a
173
+ // lista). Ctrl-C cancela. Sem TTY, resolve na hora.
174
+ function acknowledge(message, out = process.stderr) {
175
+ if (!process.stdin.isTTY)
176
+ return Promise.resolve();
177
+ const stdin = process.stdin;
178
+ out.write(`\n${ui_1.sym.info} ${message} ${ui_1.c.gray("[Enter]")}`);
179
+ return new Promise((resolve) => {
180
+ const teardown = () => {
181
+ stdin.setRawMode(false);
182
+ stdin.pause();
183
+ stdin.removeListener("data", onData);
184
+ };
185
+ const onData = (chunk) => {
186
+ if (chunk === "\x03") {
187
+ teardown();
188
+ out.write("\n");
189
+ process.exit(130);
190
+ }
191
+ teardown();
192
+ out.write("\n");
193
+ resolve();
194
+ };
195
+ stdin.setRawMode(true);
196
+ stdin.resume();
197
+ stdin.setEncoding("utf8");
198
+ stdin.on("data", onData);
199
+ });
200
+ }
201
+ // Mapeia a tecla num confirm S/N. Atalho de tecla única (s/y → sim, n → não)
202
+ // confirma na hora; setas/hjkl movem o destaque (Sim à esquerda, Não à direita);
203
+ // Enter confirma o atual; Ctrl-C/Ctrl-D cancela.
204
+ function confirmKey(data) {
205
+ if (data === "s" || data === "S" || data === "y" || data === "Y")
206
+ return "yes";
207
+ if (data === "n" || data === "N")
208
+ return "no";
209
+ if (data === "\x1b[D" || data === "\x1b[A" || data === "h" || data === "k") {
210
+ return "left";
211
+ }
212
+ if (data === "\x1b[C" || data === "\x1b[B" || data === "l" || data === "j") {
213
+ return "right";
214
+ }
215
+ if (data === "\r" || data === "\n")
216
+ return "confirm";
217
+ if (data === "\x03" || data === "\x04")
218
+ return "cancel";
219
+ return null;
220
+ }
221
+ // Confirmação S/N interativa, no mesmo estilo do select(): uma linha que se
222
+ // redesenha, tecla única ou setas+Enter, colapsa em "✔ pergunta Sim/Não". Sem
223
+ // TTY devolve `false` (caminho seguro; automações usam --yes).
224
+ function confirm(question, defaultYes = false, out = process.stderr) {
225
+ if (!process.stdin.isTTY)
226
+ return Promise.resolve(false);
227
+ const q = question.replace(/\s+/g, " ").trim();
228
+ let yes = defaultYes;
229
+ const stdin = process.stdin;
230
+ const render = () => {
231
+ const pill = (isYes) => {
232
+ const label = isYes ? "Sim" : "Não";
233
+ return isYes === yes ? ui_1.c.cyan(`❯ ${label}`) : ui_1.c.gray(` ${label}`);
234
+ };
235
+ out.write(`\r${ESC}K${ui_1.sym.info} ${q} ${pill(true)} ${pill(false)} ${ui_1.c.gray("(s/n)")}`);
236
+ };
237
+ out.write("\n");
238
+ out.write(`${ESC}?25l`);
239
+ render();
240
+ return new Promise((resolve) => {
241
+ const teardown = () => {
242
+ stdin.setRawMode(false);
243
+ stdin.pause();
244
+ stdin.removeListener("data", onData);
245
+ out.write(`${ESC}?25h`);
246
+ };
247
+ const finish = (val) => {
248
+ out.write(`\r${ESC}K${ui_1.sym.ok} ${q} ${ui_1.c.bold(val ? "Sim" : "Não")}\n`);
249
+ teardown();
250
+ resolve(val);
251
+ };
252
+ const onData = (chunk) => {
253
+ const action = confirmKey(chunk);
254
+ if (action === "yes")
255
+ finish(true);
256
+ else if (action === "no")
257
+ finish(false);
258
+ else if (action === "left") {
259
+ yes = true;
260
+ render();
261
+ }
262
+ else if (action === "right") {
263
+ yes = false;
264
+ render();
265
+ }
266
+ else if (action === "confirm")
267
+ finish(yes);
268
+ else if (action === "cancel") {
269
+ teardown();
270
+ out.write("\n");
271
+ process.exit(130);
272
+ }
273
+ };
274
+ stdin.setRawMode(true);
275
+ stdin.resume();
276
+ stdin.setEncoding("utf8");
277
+ stdin.on("data", onData);
278
+ });
279
+ }
package/dist/ui.js ADDED
@@ -0,0 +1,167 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.cmd = exports.detail = exports.step = exports.sym = exports.c = exports.THEME = void 0;
4
+ exports.headline = headline;
5
+ exports.clearScreen = clearScreen;
6
+ exports.banner = banner;
7
+ exports.targetLine = targetLine;
8
+ exports.phaseHeader = phaseHeader;
9
+ exports.renderProgress = renderProgress;
10
+ // Estilo de terminal zero-dep, no padrão da casa fazer.ai (ANSI puro; espelha o
11
+ // install-script.sh do hub). Cor desliga sem TTY ou com NO_COLOR; FORCE_COLOR força.
12
+ const COLOR = !process.env.NO_COLOR &&
13
+ (process.env.FORCE_COLOR ? true : Boolean(process.stderr.isTTY));
14
+ // Tema claro opcional (FAZER_AI_CLI_THEME=light), para terminais de fundo branco em
15
+ // que o cinza-escuro/ciano-claro do tema padrão some. O default segue o tema escuro.
16
+ // NOTE: ANSI cru não conhece a cor real do terminal; o toggle é explícito por env.
17
+ exports.THEME = process.env.FAZER_AI_CLI_THEME?.trim().toLowerCase() === "light"
18
+ ? "light"
19
+ : "dark";
20
+ const ESC = "\x1b[";
21
+ function wrap(code, s) {
22
+ return COLOR ? `${ESC}${code}m${s}${ESC}0m` : s;
23
+ }
24
+ const PALETTES = {
25
+ dark: {
26
+ red: "0;31",
27
+ green: "0;32",
28
+ yellow: "1;33",
29
+ blue: "0;34",
30
+ cyan: "0;36",
31
+ magenta: "0;35",
32
+ gray: "0;90",
33
+ },
34
+ light: {
35
+ red: "0;31",
36
+ green: "0;32",
37
+ yellow: "0;33",
38
+ blue: "0;34",
39
+ cyan: "0;36",
40
+ magenta: "0;35",
41
+ gray: "2",
42
+ },
43
+ };
44
+ const P = PALETTES[exports.THEME];
45
+ exports.c = {
46
+ red: (s) => wrap(P.red, s),
47
+ green: (s) => wrap(P.green, s),
48
+ yellow: (s) => wrap(P.yellow, s),
49
+ blue: (s) => wrap(P.blue, s),
50
+ cyan: (s) => wrap(P.cyan, s),
51
+ magenta: (s) => wrap(P.magenta, s),
52
+ gray: (s) => wrap(P.gray, s),
53
+ bold: (s) => wrap("1", s),
54
+ dim: (s) => wrap("2", s),
55
+ };
56
+ exports.sym = {
57
+ ok: exports.c.green("✔"),
58
+ err: exports.c.red("✗"),
59
+ info: exports.c.cyan("→"),
60
+ warn: exports.c.yellow("!"),
61
+ bar: exports.c.yellow("│"),
62
+ };
63
+ // Bold + a cor da paleta num código SGR só (c.bold(c.cyan()) não serve porque o "0;"
64
+ // do fg reseta o bold). Troca o atributo de brilho por "1" (bold), preservando o
65
+ // número da cor (ex.: "0;36" → "1;36", "0;37" → "1;37").
66
+ function boldColor(code) {
67
+ return `1;${code.split(";").pop()}`;
68
+ }
69
+ // Cabeçalho de seção: bold + ciano (o acento da casa).
70
+ function headline(text) {
71
+ return COLOR ? `${ESC}${boldColor(P.cyan)}m${text}${ESC}0m` : text;
72
+ }
73
+ // Logo "fazer.ai" em ANSI Shadow (a mesma fonte do Hermes), na cor da casa (ciano + sombra cinza).
74
+ const LOGO = [
75
+ "███████╗ █████╗ ███████╗███████╗██████╗ █████╗ ██╗",
76
+ "██╔════╝██╔══██╗╚══███╔╝██╔════╝██╔══██╗ ██╔══██╗██║",
77
+ "█████╗ ███████║ ███╔╝ █████╗ ██████╔╝ ███████║██║",
78
+ "██╔══╝ ██╔══██║ ███╔╝ ██╔══╝ ██╔══██╗ ██╔══██║██║",
79
+ "██║ ██║ ██║███████╗███████╗██║ ██║██╗██║ ██║██║",
80
+ "╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝╚═╝ ╚═╝╚═╝",
81
+ ];
82
+ // Pinta os blocos (█) no acento ciano (bold) e a sombra (box-drawing ╗║╔═╝╚) em cinza, no padrão
83
+ // da casa. Sem cor (NO_COLOR/sem TTY), devolve cru.
84
+ const SHADOW_CHARS = new Set(["╗", "║", "╔", "═", "╝", "╚"]);
85
+ function paintLogo(line) {
86
+ if (!COLOR)
87
+ return line;
88
+ const block = boldColor(P.cyan);
89
+ let out = "";
90
+ let i = 0;
91
+ while (i < line.length) {
92
+ const ch = line[i];
93
+ if (ch === "█") {
94
+ let j = i;
95
+ while (line[j] === "█")
96
+ j++;
97
+ out += `${ESC}${block}m${line.slice(i, j)}${ESC}0m`;
98
+ i = j;
99
+ }
100
+ else if (ch !== undefined && SHADOW_CHARS.has(ch)) {
101
+ let j = i;
102
+ while (j < line.length && SHADOW_CHARS.has(line[j]))
103
+ j++;
104
+ out += `${ESC}${P.gray}m${line.slice(i, j)}${ESC}0m`;
105
+ i = j;
106
+ }
107
+ else {
108
+ out += ch ?? "";
109
+ i++;
110
+ }
111
+ }
112
+ return out;
113
+ }
114
+ // Limpa a tela antes do banner, só num terminal de verdade (stderr é TTY) e com cor
115
+ // habilitada (respeita NO_COLOR: quem desliga cor tende a estar em pipe/CI/log, onde um
116
+ // clear ANSI vira lixo). No-op fora disso. ANSI: 2J limpa a tela, H manda o cursor pro topo.
117
+ function clearScreen(out = process.stderr) {
118
+ if (process.env.NO_COLOR || !process.stderr.isTTY)
119
+ return;
120
+ out.write(`${ESC}2J${ESC}H`);
121
+ }
122
+ function banner() {
123
+ const logo = LOGO.map(paintLogo).join("\n");
124
+ return `\n${logo}\n${exports.c.bold(" Bem-vindo ao instalador do fazer.ai agents")}\n${exports.c.gray(" do zero ao seu agente de atendimento")}\n`;
125
+ }
126
+ // Linha de status do agente alvo.
127
+ function targetLine(displayName, state) {
128
+ const name = exports.c.bold(displayName);
129
+ if (!state.hasDetect)
130
+ return `${exports.sym.info} Agente alvo: ${name}`;
131
+ if (state.installed) {
132
+ const v = state.version ? exports.c.gray(` ${state.version}`) : "";
133
+ return `${exports.sym.ok} Agente alvo: ${name}${exports.c.gray(" detectado")}${v}`;
134
+ }
135
+ if (state.willInstall) {
136
+ return `${exports.sym.info} Agente alvo: ${name} ${exports.c.gray("(será instalado)")}`;
137
+ }
138
+ return `${exports.sym.err} Agente alvo: ${name}${exports.c.gray(" não instalado")}`;
139
+ }
140
+ // Cabeçalho de uma macro-fase da jornada ("Fase 2/4 · provedor"). Enquadra o onboarding
141
+ // nas 4 fases (agente → provedor → chatwoot → configurar agente) sem reordenar nada.
142
+ function phaseHeader(n, total, title) {
143
+ return `\n${headline(`Fase ${n}/${total}`)} ${exports.c.gray("·")} ${exports.c.bold(title)}`;
144
+ }
145
+ const step = (n, title) => `\n ${exports.c.cyan(`${n}.`)} ${exports.c.bold(title)}`;
146
+ exports.step = step;
147
+ const detail = (text) => ` ${exports.sym.bar} ${exports.c.gray(text)}`;
148
+ exports.detail = detail;
149
+ const cmd = (text) => ` ${exports.c.dim("$")} ${exports.c.cyan(text)}`;
150
+ exports.cmd = cmd;
151
+ // Barra de progresso de uma linha (substitui os contadores "[n/total]"). Puro e
152
+ // testável: monta a string; quem chama decide onde escrever (stderr). Clampa step em
153
+ // [0, total], usa uma largura fixa de blocos e anexa "n/total" + o rótulo do passo.
154
+ // Sem cor (NO_COLOR/sem TTY), sai cru, legível em log/pipe.
155
+ const PROGRESS_WIDTH = 14;
156
+ function renderProgress(step, total, label) {
157
+ const safeTotal = Math.max(1, Math.floor(total));
158
+ const done = Math.min(Math.max(0, Math.floor(step)), safeTotal);
159
+ const filled = Math.round((done / safeTotal) * PROGRESS_WIDTH);
160
+ const bar = "█".repeat(filled) + "░".repeat(PROGRESS_WIDTH - filled);
161
+ const painted = COLOR
162
+ ? `${exports.c.cyan("█".repeat(filled))}${exports.c.gray("░".repeat(PROGRESS_WIDTH - filled))}`
163
+ : bar;
164
+ const count = exports.c.gray(`${done}/${safeTotal}`);
165
+ const text = label.trim();
166
+ return ` ${painted} ${count}${text ? ` ${exports.c.bold(text)}` : ""}`;
167
+ }
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@fazer-ai/agents",
3
+ "version": "1.0.0",
4
+ "description": "Bootstrap do onboarding do fazer.ai agents: prepara o seu agente de IA (Claude Code, Codex, Hermes) e entrega a jornada de onboarding.",
5
+ "license": "MIT",
6
+ "author": {
7
+ "name": "fazer.ai",
8
+ "url": "https://fazer.ai"
9
+ },
10
+ "homepage": "https://app.fazer.ai",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/fazer-ai/agents.git",
14
+ "directory": "tooling/cli"
15
+ },
16
+ "bin": {
17
+ "agents": "./dist/index.js"
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "README.md",
22
+ "LICENSE"
23
+ ],
24
+ "scripts": {
25
+ "build": "tsc && node scripts/bundle-skills.mjs",
26
+ "test": "vitest run",
27
+ "test:watch": "vitest",
28
+ "prepublishOnly": "npm run build"
29
+ },
30
+ "keywords": [
31
+ "fazer-ai",
32
+ "agents",
33
+ "onboarding",
34
+ "cli",
35
+ "agent"
36
+ ],
37
+ "devDependencies": {
38
+ "@types/cross-spawn": "^6.0.6",
39
+ "@types/node": "^22.0.0",
40
+ "typescript": "^5.6.0",
41
+ "vitest": "^2.1.9"
42
+ },
43
+ "engines": {
44
+ "node": ">=18"
45
+ },
46
+ "publishConfig": {
47
+ "access": "public",
48
+ "registry": "https://registry.npmjs.org/"
49
+ },
50
+ "dependencies": {
51
+ "cross-spawn": "^7.0.6"
52
+ }
53
+ }