@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-2026 FAZER.AI LTDA
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,112 @@
1
+ # @fazer-ai/agents
2
+
3
+ CLI de bootstrap do onboarding da **fazer.ai agents**. Roda no PC do usuário,
4
+ agnóstico de SO (via `bunx`/`npx`), e prepara o seu agente de IA para conduzir a
5
+ jornada de onboarding ponta a ponta.
6
+
7
+ ## O que faz
8
+
9
+ `bunx @fazer-ai/agents@latest` (ou `npx @fazer-ai/agents@latest`):
10
+
11
+ 1. **Escolhe o agente**: Hermes, Claude Code, Codex ou "outro" (genérico). Sem
12
+ `--agent`, o CLI pergunta; a última escolha vira o default ("último usado").
13
+ 2. **Autentica na fazer.ai**: login OAuth no browser por padrão, com a sessão
14
+ reaproveitada nas próximas vezes (`~/.fazer-ai/oauth.json`); `--login` força um
15
+ login novo.
16
+ 3. **Instala o agente** se faltar (instalador oficial de cada um) e confere o login.
17
+ 4. **Define a edição do Chatwoot (licença do Kanban)**: lista as suas licenças do
18
+ hub: uma livre → **Pro** (com Kanban); uma já atribuída → alerta pra desvincular;
19
+ nenhuma disponível → adquirir (`app.fazer.ai?cart=1`) ou seguir em **OSS**; lista
20
+ vazia → a comunidade do Lucas Moreira (licença grátis) ou OSS. A escolha vira o
21
+ marcador `~/.fazer-ai/onboarding.json` que a skill lê no deploy.
22
+ 5. **Conecta as ferramentas (MCP)**: os conectores da Hostinger (DNS, VPS,
23
+ domínio) quando o provider é Hostinger. O MCP da fazer.ai agents é conectado
24
+ depois, pelo próprio agente, após o deploy.
25
+ 6. **Instala o guia de onboarding**: a skill `agents-onboarding` a partir do
26
+ marketplace fazer.ai.
27
+ 7. **Faz o handoff**: abre o agente já na jornada, com o prompt inicial semeado.
28
+
29
+ Os agentes automatizados (Hermes/Claude/Codex) executam tudo de ponta a ponta.
30
+ O "outro" não automatiza: imprime os passos e o prompt para você levar ao agente
31
+ que for usar.
32
+
33
+ ## Uso
34
+
35
+ ```sh
36
+ bunx @fazer-ai/agents@latest [opções]
37
+ npx @fazer-ai/agents@latest [opções]
38
+ ```
39
+
40
+ | Opção | Para quê |
41
+ | ----------------- | ------------------------------------------------------------------ |
42
+ | `--agent <id>` | `hermes`, `claude`, `codex` ou `other`. Sem isto, o CLI pergunta. |
43
+ | `--provider <id>` | Infra de VPS/DNS: `hostinger` (default) ou `other`. |
44
+ | `--login` | Força um login novo no browser (ignora a sessão salva). |
45
+ | `--dry-run` | Só mostra o que faria, sem executar. |
46
+ | `-y, --yes` | Executa sem confirmação interativa. |
47
+ | `--no-handoff` | Prepara tudo, mas não abre o agente ao final. |
48
+ | `-v, --verbose` | Mostra a saída dos comandos (normalmente silenciada) + diagnósticos. |
49
+ | `-V, --version` | Versão do CLI. |
50
+ | `-h, --help` | Ajuda. |
51
+
52
+ O **provider** `hostinger` injeta os MCPs de provisionamento; `other` não injeta
53
+ (o agente instrui DNS/VPS com base no provider que você indicar).
54
+
55
+ Defina `FAZER_AI_CLI_THEME=light` para o tema claro (terminais de fundo branco); o
56
+ padrão é o tema escuro. `NO_COLOR` desliga a cor por completo.
57
+
58
+ ## Plataformas
59
+
60
+ Roda em **Linux, macOS, Windows nativo (PowerShell/cmd) e WSL**. Pré-requisito:
61
+ Node ≥ 18 (ou Bun, via `bunx`).
62
+
63
+ No **Windows nativo** (sem WSL) o CLI resolve os shims dos CLIs instalados via npm
64
+ (`npm.ps1`, `claude.cmd`, …) e usa o instalador **PowerShell oficial** de cada
65
+ agente (`install.ps1`). Use um terminal moderno (Windows Terminal recomendado)
66
+ para a UI interativa de setas/Enter.
67
+
68
+ ## Estado em disco (`~/.fazer-ai/`, 0600)
69
+
70
+ O CLI guarda localmente, para reaproveitar entre execuções:
71
+
72
+ | Arquivo | Conteúdo |
73
+ | ------------------ | ------------------------------------------------------------- |
74
+ | `oauth.json` | Sessão OAuth da fazer.ai. |
75
+ | `hostinger.json` | Token da API Hostinger (opt-in; reaproveitado nas próximas). |
76
+ | `onboarding.json` | Marcador da jornada que a skill lê: `chatwootTier` (`pro`/`community`) + `chatwootLicenseId`. |
77
+ | `preferences.json` | Últimas escolhas (agente, provider, licença) pra virar default. |
78
+ | `mcp/` | Conector da Hostinger instalado localmente (`hostinger-api-mcp`). |
79
+
80
+ O token da Hostinger é **gravado no config de MCP do agente** (a partir do
81
+ `hostinger.json`), para o conector funcionar mesmo quando você reabrir o agente
82
+ fora do handoff. Consequência: **não versione nem compartilhe o config de MCP do
83
+ agente** (`~/.codex/…`, `~/.claude.json`, config do Hermes), pois ele passa a
84
+ conter o token. No preview (`--dry-run`) o token é sempre redigido.
85
+
86
+ ## Desenvolvimento
87
+
88
+ ```sh
89
+ npm install
90
+ npm run build # tsc -> dist/
91
+ node dist/index.js --help
92
+ npm test # vitest
93
+ ```
94
+
95
+ Uma única dependência de runtime (`cross-spawn`, para resolver os shims
96
+ `.cmd`/`.ps1` no Windows nativo); o resto é `node:*`. Excluído do Biome/tsconfig
97
+ do app principal; tem o próprio `tsconfig.json` e suíte `vitest`.
98
+
99
+ ## Configuração (env)
100
+
101
+ | Variável | Default | Para quê |
102
+ | ------------------------------- | ------------------------ | --------------------------------------------------- |
103
+ | `FAZER_AI_HUB_URL` | `https://app.fazer.ai` | Base do hub (MCP, OAuth/licenças). |
104
+ | `HOSTINGER_API_TOKEN` | (nenhum) | Token da API Hostinger (senão o CLI pede/usa cache).|
105
+ | `AGENTS_PROVIDER` | `hostinger` | Provider de infra (equivale a `--provider`). |
106
+ | `AGENTS_MARKETPLACE_TARGET` | `fazer-ai/agents-skills` | Repo do `plugin marketplace add` (Claude/Codex). |
107
+ | `AGENTS_MARKETPLACE_NAME` | `agents` | Nome sob o qual o marketplace é registrado. |
108
+ | `AGENTS_HERMES_TAP_REPO` | `fazer-ai/agents-skills` | Repo do `hermes skills tap add`. |
109
+ | `AGENTS_HERMES_SKILL_REF` | `fazer-ai/agents-skills/agents-onboarding` | Ref do `hermes skills install`. |
110
+ | `AGENTS_PLUGIN` | `agents` | Plugin/skill a instalar + handoff. |
111
+ | `AGENTS_OAUTH_CLIENT_ID` | (nenhum) | `client_id` OAuth pré-registrado (se o hub não usar DCR).|
112
+ </content>
@@ -0,0 +1,152 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.claudeAdapter = void 0;
4
+ exports.claudeMcpAddCommand = claudeMcpAddCommand;
5
+ exports.claudeMcpAddArgs = claudeMcpAddArgs;
6
+ exports.claudeMcpAdd = claudeMcpAdd;
7
+ const exec_1 = require("../exec");
8
+ const mcp_1 = require("../mcp");
9
+ const ui_1 = require("../ui");
10
+ const detect_1 = require("./detect");
11
+ const handoff_1 = require("./handoff");
12
+ const shell_1 = require("./shell");
13
+ // Total de macro-passos do setup do Claude (barra de progresso).
14
+ const CLAUDE_STEPS = 3;
15
+ function claudeMcpAddCommand(spec) {
16
+ if (spec.transport === "http") {
17
+ return `claude mcp add --transport http ${spec.name} ${spec.url}`;
18
+ }
19
+ const envFlags = Object.entries(spec.env ?? {})
20
+ .map(([key, value]) => {
21
+ const shown = spec.secretEnvKeys?.includes(key) ? shell_1.REDACTED : value;
22
+ return `-e ${key}=${(0, shell_1.shellQuote)(shown)}`;
23
+ })
24
+ .join(" ");
25
+ const cmd = [spec.command, ...(spec.args ?? [])]
26
+ .filter((part) => Boolean(part))
27
+ .map(shell_1.shellQuote)
28
+ .join(" ");
29
+ return `claude mcp add ${spec.name}${envFlags ? ` ${envFlags}` : ""} -- ${cmd}`;
30
+ }
31
+ // Argv para execução (spawn sem shell). O valor de env vai LITERAL: para o token
32
+ // é a referência ${HOSTINGER_API_TOKEN}, que o Claude expande no launch do MCP.
33
+ function claudeMcpAddArgs(spec) {
34
+ if (spec.transport === "http") {
35
+ return ["mcp", "add", "--transport", "http", spec.name, spec.url ?? ""];
36
+ }
37
+ const envFlags = Object.entries(spec.env ?? {}).flatMap(([key, value]) => [
38
+ "-e",
39
+ `${key}=${value}`,
40
+ ]);
41
+ const cmd = [spec.command, ...(spec.args ?? [])].filter((part) => Boolean(part));
42
+ return ["mcp", "add", spec.name, ...envFlags, "--", ...cmd];
43
+ }
44
+ // `claude mcp add` sai != 0 quando o servidor já existe ("... already exists in ... config"), o que
45
+ // derruba um re-run (run() rejeita e o passo de conectar as ferramentas aborta). A config que remontamos é idêntica entre runs (o
46
+ // token é a referência ${HOSTINGER_API_TOKEN}, o path é estável), então "already exists" é sucesso: o MCP
47
+ // já está do jeito que queremos. Qualquer outra falha propaga. Idempotência alinhada à resumabilidade do fluxo.
48
+ async function claudeMcpAdd(spec) {
49
+ try {
50
+ await (0, exec_1.run)("claude", claudeMcpAddArgs(spec), { quiet: true });
51
+ }
52
+ catch (err) {
53
+ const msg = err instanceof Error ? err.message : String(err);
54
+ if (/already exists/i.test(msg))
55
+ return;
56
+ throw err;
57
+ }
58
+ }
59
+ exports.claudeAdapter = {
60
+ id: "claude",
61
+ displayName: "Claude Code",
62
+ automated: true,
63
+ skillInvoker: "/",
64
+ detectCommand: "claude --version",
65
+ installCommand: {
66
+ posix: "curl -fsSL https://claude.ai/install.sh | bash",
67
+ win32: "irm https://claude.ai/install.ps1 | iex",
68
+ },
69
+ marketplaceSource: (config) => config.marketplaceAddTarget,
70
+ detect() {
71
+ return (0, detect_1.detectVersion)("claude", ["--version"]);
72
+ },
73
+ // Sem login/configured no CLI: o Claude Code autentica no próprio TUI ao abrir o
74
+ // handoff. Um pre-login aqui (claude auth login) duplicava o sign-in (o CLI logava e
75
+ // o TUI pedia de novo), então deixamos o Claude cuidar do próprio login.
76
+ plan(ctx) {
77
+ const { config } = ctx;
78
+ const specs = (0, mcp_1.bootstrapMcpSpecs)({
79
+ config,
80
+ provider: ctx.provider,
81
+ hostingerTokenProvided: ctx.hostingerTokenProvided,
82
+ });
83
+ return [
84
+ {
85
+ title: ctx.provider === "hostinger"
86
+ ? "Conectar as ferramentas (DNS, VPS, domínio)"
87
+ : "Conectar as ferramentas de infraestrutura",
88
+ commands: specs.map(claudeMcpAddCommand),
89
+ },
90
+ {
91
+ title: "Instalar/atualizar o guia de onboarding",
92
+ commands: [
93
+ `claude plugin marketplace add ${config.marketplaceAddTarget}`,
94
+ `claude plugin marketplace update ${config.marketplaceName}`,
95
+ `claude plugin install ${config.onboardingPlugin}@${config.marketplaceName}`,
96
+ `claude plugin update ${config.onboardingPlugin}@${config.marketplaceName}`,
97
+ ],
98
+ },
99
+ { title: "Abrir o Claude no onboarding" },
100
+ ];
101
+ },
102
+ async execute(ctx) {
103
+ const { config, log } = ctx;
104
+ const phase = (n, label) => log((0, ui_1.renderProgress)(n, CLAUDE_STEPS, label));
105
+ const specs = (0, mcp_1.bootstrapMcpSpecs)({
106
+ config,
107
+ provider: ctx.provider,
108
+ hostingerTokenProvided: ctx.hostingerTokenProvided,
109
+ hostingerToken: ctx.hostingerToken,
110
+ });
111
+ phase(1, specs.length === 0
112
+ ? "Sem ferramentas extras a conectar (provider externo)"
113
+ : "Conectando as ferramentas (DNS, VPS, domínio)");
114
+ if ((0, mcp_1.hasHostingerSpecs)(specs))
115
+ await (0, mcp_1.installHostingerMcp)(log);
116
+ for (const spec of specs) {
117
+ await claudeMcpAdd(spec);
118
+ }
119
+ phase(2, "Instalando/atualizando o guia de onboarding");
120
+ await (0, exec_1.run)("claude", ["plugin", "marketplace", "add", config.marketplaceAddTarget]);
121
+ // marketplace update refresca o snapshot do repo (vê a nova versão publicada);
122
+ // plugin update força a atualização quando o plugin já estava instalado (re-run).
123
+ // Sem isso, `install` é no-op num re-run e o agente fica preso na versão antiga.
124
+ await (0, exec_1.run)("claude", ["plugin", "marketplace", "update", config.marketplaceName]);
125
+ await (0, exec_1.run)("claude", [
126
+ "plugin",
127
+ "install",
128
+ `${config.onboardingPlugin}@${config.marketplaceName}`,
129
+ ]);
130
+ // update usa o id COMPLETO plugin@marketplace (o Claude chaveia o plugin instalado
131
+ // assim; `update agents` puro dá "not found"). No-op quando já está na latest.
132
+ await (0, exec_1.run)("claude", [
133
+ "plugin",
134
+ "update",
135
+ `${config.onboardingPlugin}@${config.marketplaceName}`,
136
+ ]);
137
+ if (ctx.handoff) {
138
+ phase(3, "Abrindo o Claude no onboarding");
139
+ const env = ctx.hostingerToken
140
+ ? { ...process.env, HOSTINGER_API_TOKEN: ctx.hostingerToken }
141
+ : process.env;
142
+ const launched = await (0, exec_1.runHandoff)("claude", [(0, handoff_1.handoffPrompt)("/", config.onboardingSkill, ctx.provider)], { env });
143
+ if (!launched) {
144
+ phase(3, "Tudo pronto.");
145
+ log(`${ui_1.sym.ok} Abra o Claude e invoque a skill: rode \`claude\` e digite \`/${config.onboardingSkill}\`.`);
146
+ }
147
+ }
148
+ else {
149
+ phase(3, "Tudo pronto. Para abrir depois: claude");
150
+ }
151
+ },
152
+ };
@@ -0,0 +1,155 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.codexAdapter = void 0;
4
+ exports.codexLoggedIn = codexLoggedIn;
5
+ exports.codexMcpAddCommand = codexMcpAddCommand;
6
+ exports.codexMcpAddArgs = codexMcpAddArgs;
7
+ const exec_1 = require("../exec");
8
+ const mcp_1 = require("../mcp");
9
+ const ui_1 = require("../ui");
10
+ const detect_1 = require("./detect");
11
+ const handoff_1 = require("./handoff");
12
+ const shell_1 = require("./shell");
13
+ // Total de macro-passos do setup do Codex (barra de progresso).
14
+ const CODEX_STEPS = 3;
15
+ // Lê o estado do `codex login status`. "Not logged in" ⇒ false; "Logged in …" ⇒
16
+ // true; nenhum dos dois ⇒ undefined (indeterminado). A ordem importa: "not logged
17
+ // in" contém "logged in", então testa o negativo primeiro.
18
+ function codexLoggedIn(output) {
19
+ if (/not logged in/i.test(output))
20
+ return false;
21
+ if (/logged in/i.test(output))
22
+ return true;
23
+ return undefined;
24
+ }
25
+ // Projeta um McpServerSpec no `codex mcp add` (validado no Codex 0.142). stdio:
26
+ // `--env KEY=VALUE` antes do `--`, depois o comando. http: bridge stdio via
27
+ // `mcp-remote` (formato que o hub documenta para o Codex).
28
+ function codexMcpAddCommand(spec) {
29
+ if (spec.transport === "http") {
30
+ return `codex mcp add ${spec.name} -- npx -y mcp-remote ${spec.url}`;
31
+ }
32
+ const envFlags = Object.entries(spec.env ?? {})
33
+ .map(([key, value]) => {
34
+ const shown = spec.secretEnvKeys?.includes(key) ? shell_1.REDACTED : value;
35
+ return `--env ${key}=${(0, shell_1.shellQuote)(shown)}`;
36
+ })
37
+ .join(" ");
38
+ const cmd = [spec.command, ...(spec.args ?? [])]
39
+ .filter((part) => Boolean(part))
40
+ .map(shell_1.shellQuote)
41
+ .join(" ");
42
+ return `codex mcp add ${spec.name}${envFlags ? ` ${envFlags}` : ""} -- ${cmd}`;
43
+ }
44
+ // Mesmo comando, como argv para spawn (sem shell). O valor de env vai LITERAL: a
45
+ // referência ${HOSTINGER_API_TOKEN} é expandida pelo Codex no launch do MCP.
46
+ function codexMcpAddArgs(spec) {
47
+ if (spec.transport === "http") {
48
+ return ["mcp", "add", spec.name, "--", "npx", "-y", "mcp-remote", spec.url ?? ""];
49
+ }
50
+ const envFlags = Object.entries(spec.env ?? {}).flatMap(([key, value]) => [
51
+ "--env",
52
+ `${key}=${value}`,
53
+ ]);
54
+ const cmd = [spec.command, ...(spec.args ?? [])].filter((part) => Boolean(part));
55
+ return ["mcp", "add", spec.name, ...envFlags, "--", ...cmd];
56
+ }
57
+ // NOTE: Comandos validados contra o Codex CLI real (0.142): `codex mcp add`,
58
+ // `codex plugin marketplace add owner/repo` (clona repo público direto, sem proxy nem
59
+ // credencial, `<SOURCE>` aceita owner/repo[@ref]/HTTPS), `codex plugin marketplace upgrade`
60
+ // (refresca o snapshot Git), `codex plugin remove`, `codex plugin add <plugin>@<mkt>`.
61
+ // Fonte: o repo dedicado da skill (config.marketplaceAddTarget), validado no plugin list.
62
+ exports.codexAdapter = {
63
+ id: "codex",
64
+ displayName: "Codex",
65
+ automated: true,
66
+ skillInvoker: "$",
67
+ detectCommand: "codex --version",
68
+ installCommand: {
69
+ posix: "curl -fsSL https://chatgpt.com/codex/install.sh | sh",
70
+ win32: "irm https://chatgpt.com/codex/install.ps1 | iex",
71
+ },
72
+ marketplaceSource: (config) => config.marketplaceAddTarget,
73
+ detect() {
74
+ return (0, detect_1.detectVersion)("codex", ["--version"]);
75
+ },
76
+ // `codex login status` imprime o estado no STDERR (não no stdout), então lê os
77
+ // dois fluxos. "Logged in using ChatGPT" ⇒ logado; "Not logged in" ⇒ não.
78
+ async configured() {
79
+ const { stdout, stderr } = await (0, exec_1.capture)("codex", ["login", "status"]);
80
+ return codexLoggedIn(`${stdout}\n${stderr}`);
81
+ },
82
+ // `codex login` abre o OAuth do ChatGPT no browser; creds em ~/.codex/config.toml.
83
+ login() {
84
+ return (0, exec_1.runInteractive)("codex", ["login"]);
85
+ },
86
+ plan(ctx) {
87
+ const { config } = ctx;
88
+ const specs = (0, mcp_1.bootstrapMcpSpecs)({
89
+ config,
90
+ provider: ctx.provider,
91
+ hostingerTokenProvided: ctx.hostingerTokenProvided,
92
+ });
93
+ return [
94
+ {
95
+ title: ctx.provider === "hostinger"
96
+ ? "Conectar as ferramentas (DNS, VPS, domínio)"
97
+ : "Conectar as ferramentas de infraestrutura",
98
+ commands: specs.map(codexMcpAddCommand),
99
+ },
100
+ {
101
+ title: "Instalar/atualizar o guia de onboarding",
102
+ commands: [
103
+ `codex plugin marketplace add ${config.marketplaceAddTarget}`,
104
+ `codex plugin marketplace upgrade ${config.marketplaceName}`,
105
+ `codex plugin add ${config.onboardingPlugin}@${config.marketplaceName}`,
106
+ ],
107
+ },
108
+ { title: "Abrir o Codex no onboarding" },
109
+ ];
110
+ },
111
+ async execute(ctx) {
112
+ const { config, log } = ctx;
113
+ const phase = (n, label) => log((0, ui_1.renderProgress)(n, CODEX_STEPS, label));
114
+ const specs = (0, mcp_1.bootstrapMcpSpecs)({
115
+ config,
116
+ provider: ctx.provider,
117
+ hostingerTokenProvided: ctx.hostingerTokenProvided,
118
+ hostingerToken: ctx.hostingerToken,
119
+ });
120
+ phase(1, specs.length === 0
121
+ ? "Sem ferramentas extras a conectar (provider externo)"
122
+ : "Conectando as ferramentas (DNS, VPS, domínio)");
123
+ if ((0, mcp_1.hasHostingerSpecs)(specs))
124
+ await (0, mcp_1.installHostingerMcp)(log);
125
+ for (const spec of specs) {
126
+ await (0, exec_1.run)("codex", codexMcpAddArgs(spec), { quiet: true });
127
+ }
128
+ phase(2, "Instalando/atualizando o guia de onboarding");
129
+ await (0, exec_1.run)("codex", ["plugin", "marketplace", "add", config.marketplaceAddTarget]);
130
+ // O Codex não tem `plugin update`: pra um re-run pegar a versão nova, refrescamos o
131
+ // snapshot do marketplace (upgrade) e reinstalamos. O remove é tolerante (capture, não
132
+ // run) porque na 1ª run o plugin ainda não existe; sem ele, `add` seria no-op no re-run.
133
+ await (0, exec_1.run)("codex", ["plugin", "marketplace", "upgrade", config.marketplaceName]);
134
+ await (0, exec_1.capture)("codex", ["plugin", "remove", config.onboardingPlugin]);
135
+ await (0, exec_1.run)("codex", [
136
+ "plugin",
137
+ "add",
138
+ `${config.onboardingPlugin}@${config.marketplaceName}`,
139
+ ]);
140
+ if (ctx.handoff) {
141
+ phase(3, "Abrindo o Codex no onboarding");
142
+ const env = ctx.hostingerToken
143
+ ? { ...process.env, HOSTINGER_API_TOKEN: ctx.hostingerToken }
144
+ : process.env;
145
+ const launched = await (0, exec_1.runHandoff)("codex", [(0, handoff_1.handoffPrompt)("$", config.onboardingSkill, ctx.provider)], { env });
146
+ if (!launched) {
147
+ phase(3, "Tudo pronto.");
148
+ log(`${ui_1.sym.ok} Abra o Codex e invoque a skill: rode \`codex\` e digite \`$${config.onboardingSkill}\`.`);
149
+ }
150
+ }
151
+ else {
152
+ phase(3, "Tudo pronto. Para abrir depois: codex");
153
+ }
154
+ },
155
+ };
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.detectVersion = detectVersion;
4
+ const exec_1 = require("../exec");
5
+ // Detecta um agente pelo seu comando de versão. Reusa `capture` (sobre
6
+ // cross-spawn) para resolver shims `.cmd`/`.ps1` no Windows. Código != 0
7
+ // (binário ausente, timeout, saída não-zero) vira { installed: false }, nunca lança.
8
+ async function detectVersion(command, args) {
9
+ const { code, stdout } = await (0, exec_1.capture)(command, args);
10
+ if (code !== 0)
11
+ return { installed: false };
12
+ // Só a 1ª linha: alguns agentes (Hermes) imprimem versão multi-linha.
13
+ const version = stdout.trim().split(/\r?\n/)[0]?.trim();
14
+ return { installed: true, version: version || undefined };
15
+ }
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handoffPrompt = handoffPrompt;
4
+ // Prompt do handoff: instrui o agente a usar a skill E entrega o contexto que o CLI já preparou
5
+ // (provider, MCPs conectados, marcador), pra ele NÃO re-perguntar o que já foi decidido. O
6
+ // invocador é específico do agente (Claude "/", Codex "$"; vazio no genérico).
7
+ function handoffPrompt(skillInvoker, plugin, provider) {
8
+ const base = `Use a skill ${skillInvoker}${plugin} para conduzir o onboarding da fazer.ai agents.`;
9
+ let msg = base;
10
+ if (provider === "hostinger") {
11
+ msg = `${base} Contexto já preparado pelo CLI: provider de infra = Hostinger (os MCPs hostinger-dns/vps/domains já estão conectados, use-os; não pergunte "se for Hostinger") e o marcador da jornada está em ~/.fazer-ai/onboarding.json (tier do Chatwoot + licença, edição da fazer.ai agents free/pro). LEIA o marcador e os MCPs conectados antes de perguntar; não re-pergunte provider/tier/licença/edição já definidos.`;
12
+ }
13
+ else if (provider === "other") {
14
+ msg = `${base} Contexto já preparado pelo CLI: provider de infra = outro (sem MCPs da Hostinger) e o marcador da jornada está em ~/.fazer-ai/onboarding.json (tier do Chatwoot + licença, edição da fazer.ai agents free/pro). LEIA o marcador antes de perguntar; só pergunte o provider de VPS/DNS se a skill precisar.`;
15
+ }
16
+ // No Windows o agente tende a tratar o PowerShell como bash (escape/BOM) e a contornar o MCP por REST.
17
+ // Semeia as duas regras antes do 1º comando: enterradas na skill, não pegam o começo da run.
18
+ if (process.platform === "win32") {
19
+ msg += ` IMPORTANTE (Windows/PowerShell): o shell só ORQUESTRA, NUNCA carrega código. Não rode script/SQL/JSON inline (sem here-string @'…'@ | …, sem ssh host '…código…' com aspas/{{…}}/(, sem \\ de continuação de linha, sem pipe de payload): escreva o payload num ARQUIVO (ferramenta de edição) e rode via "scripts/remote.py --script-file" (bash/psql/rails-runner remoto) ou "python x.py" (local). A config do fazer.ai agents é só por MCP: se as tools MCP não estiverem expostas na sessão, PARE e peça pra reiniciar o harness, NUNCA contorne por REST/console/leitura do código.`;
20
+ }
21
+ return msg;
22
+ }
@@ -0,0 +1,177 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.hermesSkillsDir = hermesSkillsDir;
4
+ exports.hermesLockPath = hermesLockPath;
5
+ exports.comparePosixPaths = comparePosixPaths;
6
+ exports.listSkillFiles = listSkillFiles;
7
+ exports.skillContentHash = skillContentHash;
8
+ exports.buildLockEntry = buildLockEntry;
9
+ exports.installBundledSkill = installBundledSkill;
10
+ exports.bundledSkillsRoot = bundledSkillsRoot;
11
+ exports.skillNameFromRef = skillNameFromRef;
12
+ // Instalação OFFLINE das skills do fazer.ai agents no profile do Hermes.
13
+ //
14
+ // Por que existe: instalar via `hermes skills tap add` + `skills install` bate no
15
+ // skills.sh / GitHub anônimo, cujo rate-limit (60/h) estourava no meio do onboarding
16
+ // ("skill not found", lock.json vazio). Em vez de puxar da rede no caminho quente,
17
+ // EMPACOTAMOS as skills já buildadas (`tooling/skills/dist`) dentro do CLI e as
18
+ // escrevemos direto no profile, reproduzindo o que o instalador nativo do Hermes faria:
19
+ // os arquivos em `profiles/<p>/skills/<skill>/` + a entrada em
20
+ // `profiles/<p>/skills/.hub/lock.json` com o `content_hash` correto.
21
+ //
22
+ // O `content_hash` tem que casar bit a bit com o do Hermes (skills_guard.content_hash
23
+ // == skills_hub.bundle_content_hash): sha256 sobre, para cada arquivo em `sorted(files)`
24
+ // (ordem de codepoint, NÃO case-insensitive), `relPosix + "\x00" + bytes`, e então
25
+ // `sha256:` + os 16 primeiros hex. Assim um `hermes skills update` futuro compara certo.
26
+ //
27
+ // NOTE: o proxy autenticado do app.fazer.ai (fallback quando não há bundle, p.ex. skill
28
+ // nova publicada só no hub) vive em OUTRO repo (o hub) e não é construído aqui. Este
29
+ // caminho empacotado é o primário; o `tap add`/`skills install` na rede fica como
30
+ // fallback no adapter, atrás de uma flag.
31
+ const node_crypto_1 = require("node:crypto");
32
+ const node_fs_1 = require("node:fs");
33
+ const node_path_1 = require("node:path");
34
+ // Caminhos-espelho da resolução do Hermes (skills_hub.py): SKILLS_DIR = <profile>/skills,
35
+ // HUB_DIR = SKILLS_DIR/.hub, LOCK = HUB_DIR/lock.json. Puros.
36
+ function hermesSkillsDir(profileDir) {
37
+ return (0, node_path_1.join)(profileDir, "skills");
38
+ }
39
+ function hermesLockPath(profileDir) {
40
+ return (0, node_path_1.join)(hermesSkillsDir(profileDir), ".hub", "lock.json");
41
+ }
42
+ // Ordena caminhos POSIX igual ao `sorted()` de PurePosixPath do Python (por COMPONENTES,
43
+ // não pela string juntada): ("a","b") < ("a.txt",) porque compara "a" < "a.txt", enquanto
44
+ // a string "a/b" > "a.txt" (porque "/" 0x2F > "." 0x2E). O content_hash do Hermes ordena
45
+ // os Path component-wise, então temos que casar isso pra o hash bater em árvores aninhadas.
46
+ function comparePosixPaths(a, b) {
47
+ const pa = a.split("/");
48
+ const pb = b.split("/");
49
+ const n = Math.min(pa.length, pb.length);
50
+ for (let i = 0; i < n; i++) {
51
+ if (pa[i] < pb[i])
52
+ return -1;
53
+ if (pa[i] > pb[i])
54
+ return 1;
55
+ }
56
+ return pa.length - pb.length;
57
+ }
58
+ // Lista os arquivos de um diretório de skill em POSIX relativo, ORDENADOS component-wise
59
+ // (o mesmo `sorted(rglob("*"))` do Hermes sobre PurePosixPath). Pula diretórios. Puro
60
+ // sobre o FS.
61
+ function listSkillFiles(skillDir) {
62
+ const out = [];
63
+ const walk = (dir) => {
64
+ for (const name of (0, node_fs_1.readdirSync)(dir)) {
65
+ const full = (0, node_path_1.join)(dir, name);
66
+ if ((0, node_fs_1.statSync)(full).isDirectory())
67
+ walk(full);
68
+ else
69
+ out.push((0, node_path_1.relative)(skillDir, full).split("\\").join("/"));
70
+ }
71
+ };
72
+ walk(skillDir);
73
+ return out.sort(comparePosixPaths);
74
+ }
75
+ // Reproduz skills_hub.bundle_content_hash / skills_guard.content_hash: para cada arquivo
76
+ // em ordem, `path + NUL + conteúdo`, sha256, prefixo `sha256:` + 16 hex. `read` injetável
77
+ // pra teste. `files` DEVE vir ordenado (listSkillFiles já ordena).
78
+ function skillContentHash(skillDir, files, read = (rel) => (0, node_fs_1.readFileSync)((0, node_path_1.join)(skillDir, rel))) {
79
+ const h = (0, node_crypto_1.createHash)("sha256");
80
+ for (const rel of files) {
81
+ h.update(Buffer.from(rel, "utf8"));
82
+ h.update(Buffer.from([0]));
83
+ h.update(read(rel));
84
+ }
85
+ return `sha256:${h.digest("hex").slice(0, 16)}`;
86
+ }
87
+ // Monta a entrada do lock idêntica ao HubLockFile.record_install do Hermes. `identifier`
88
+ // no formato skills.sh (`skills-sh/<owner>/<repo>/<skill>`) pra um `skills update` futuro
89
+ // resolver a mesma origem. `now` injetável pra teste. Puro.
90
+ function buildLockEntry(opts) {
91
+ const iso = opts.now ?? new Date().toISOString();
92
+ return {
93
+ source: "skills.sh",
94
+ identifier: `skills-sh/${opts.tapRepo}/${opts.skill}`,
95
+ trust_level: "community",
96
+ scan_verdict: "caution",
97
+ content_hash: opts.contentHash,
98
+ install_path: opts.skill,
99
+ files: opts.files,
100
+ metadata: {
101
+ detail_url: `https://skills.sh/${opts.tapRepo}/${opts.skill}`,
102
+ repo_url: `https://github.com/${opts.tapRepo}`,
103
+ },
104
+ installed_at: iso,
105
+ updated_at: iso,
106
+ };
107
+ }
108
+ function loadLock(lockPath) {
109
+ if ((0, node_fs_1.existsSync)(lockPath)) {
110
+ try {
111
+ const data = JSON.parse((0, node_fs_1.readFileSync)(lockPath, "utf8"));
112
+ if (data && typeof data === "object" && data.installed) {
113
+ return { version: data.version ?? 1, installed: data.installed };
114
+ }
115
+ }
116
+ catch {
117
+ // lock corrompido: recomeça limpo (o Hermes reconcilia no próximo update).
118
+ }
119
+ }
120
+ return { version: 1, installed: {} };
121
+ }
122
+ // Copia os arquivos da skill buildada pro profile (substitui a árvore inteira, como o
123
+ // install_from_quarantine do Hermes) e devolve o dir instalado.
124
+ function copySkillTree(srcSkillDir, destSkillDir) {
125
+ (0, node_fs_1.rmSync)(destSkillDir, { recursive: true, force: true });
126
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(destSkillDir), { recursive: true });
127
+ (0, node_fs_1.cpSync)(srcSkillDir, destSkillDir, { recursive: true });
128
+ }
129
+ // Instala UMA skill buildada offline no profile: copia os arquivos, calcula o
130
+ // content_hash a partir do que foi escrito no disco (fonte da verdade, igual ao Hermes) e
131
+ // grava a entrada no lock.json. Idempotente (re-run substitui a árvore e reescreve a
132
+ // entrada). Lança em erro de FS; quem chama trata por-skill pra uma não derrubar as outras.
133
+ function installBundledSkill(opts) {
134
+ const skillsDir = hermesSkillsDir(opts.profileDir);
135
+ const destSkillDir = (0, node_path_1.join)(skillsDir, opts.skill);
136
+ copySkillTree(opts.bundledSkillDir, destSkillDir);
137
+ const files = listSkillFiles(destSkillDir);
138
+ const contentHash = skillContentHash(destSkillDir, files);
139
+ const lockPath = hermesLockPath(opts.profileDir);
140
+ const lock = loadLock(lockPath);
141
+ lock.installed[opts.skill] = buildLockEntry({
142
+ skill: opts.skill,
143
+ tapRepo: opts.tapRepo,
144
+ contentHash,
145
+ files,
146
+ now: opts.now,
147
+ });
148
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(lockPath), { recursive: true });
149
+ (0, node_fs_1.writeFileSync)(lockPath, `${JSON.stringify(lock, null, 2)}\n`, "utf8");
150
+ return { skill: opts.skill, files: files.length, contentHash };
151
+ }
152
+ // Resolve a raiz das skills empacotadas no pacote do CLI. Em runtime buildado, `__dirname`
153
+ // é `dist/agents/` → as skills ficam em `dist/skills/` (copiadas no build por
154
+ // `scripts/bundle-skills.mjs`). Em dev/teste (`src/agents/`), cai no `tooling/skills/dist`
155
+ // do monorepo. Devolve undefined se nenhum existir (o adapter cai no fallback de rede).
156
+ function bundledSkillsRoot(fromDir = __dirname) {
157
+ const candidates = [
158
+ (0, node_path_1.join)(fromDir, "..", "skills"), // dist/agents → dist/skills (pacote publicado)
159
+ (0, node_path_1.join)(fromDir, "..", "..", "..", "skills", "dist"), // src/agents → tooling/skills/dist (dev)
160
+ ];
161
+ return candidates.find((p) => (0, node_fs_1.existsSync)((0, node_path_1.join)(p)) && hasAnySkill(p));
162
+ }
163
+ // Um diretório de skills válido tem ao menos um <skill>/SKILL.md.
164
+ function hasAnySkill(root) {
165
+ try {
166
+ return (0, node_fs_1.readdirSync)(root, { withFileTypes: true }).some((d) => d.isDirectory() && (0, node_fs_1.existsSync)((0, node_path_1.join)(root, d.name, "SKILL.md")));
167
+ }
168
+ catch {
169
+ return false;
170
+ }
171
+ }
172
+ // Nome da skill a partir do ref `owner/repo/skill` do config (hermesSkillRefs). O offline
173
+ // instala por nome de pasta (o último segmento). Puro.
174
+ function skillNameFromRef(ref) {
175
+ const parts = ref.split("/").filter(Boolean);
176
+ return parts[parts.length - 1] ?? ref;
177
+ }