@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.
- package/LICENSE +21 -0
- package/README.md +112 -0
- package/dist/agents/claude.js +152 -0
- package/dist/agents/codex.js +155 -0
- package/dist/agents/detect.js +15 -0
- package/dist/agents/handoff.js +22 -0
- package/dist/agents/hermes-skills.js +177 -0
- package/dist/agents/hermes.js +474 -0
- package/dist/agents/index.js +57 -0
- package/dist/agents/manual.js +22 -0
- package/dist/agents/other.js +39 -0
- package/dist/agents/shell.js +15 -0
- package/dist/agents/types.js +2 -0
- package/dist/config.js +48 -0
- package/dist/exec.js +279 -0
- package/dist/hostinger.js +75 -0
- package/dist/hub-command.js +144 -0
- package/dist/index.js +726 -0
- package/dist/licenses.js +93 -0
- package/dist/mcp.js +100 -0
- package/dist/oauth.js +578 -0
- package/dist/onboarding-marker.js +48 -0
- package/dist/preferences.js +40 -0
- package/dist/skills/agents-dev/SKILL.md +37 -0
- package/dist/skills/agents-dev/gotchas.md +6 -0
- package/dist/skills/agents-dev/guardrails.md +6 -0
- package/dist/skills/agents-dev/references/00-get-the-code.md +28 -0
- package/dist/skills/agents-dev/references/01-layout-and-bun-check.md +29 -0
- package/dist/skills/agents-dev/references/02-free-full-and-invariants.md +7 -0
- package/dist/skills/agents-dev/references/03-implement.md +9 -0
- package/dist/skills/agents-dev/references/04-own-image-and-deploy.md +13 -0
- package/dist/skills/agents-onboarding/SKILL.md +80 -0
- package/dist/skills/agents-onboarding/gotchas.md +157 -0
- package/dist/skills/agents-onboarding/guardrails.md +65 -0
- package/dist/skills/agents-onboarding/references/00-prereqs-and-access.md +37 -0
- package/dist/skills/agents-onboarding/references/01-vps-dns-ssh.md +67 -0
- package/dist/skills/agents-onboarding/references/01b-brownfield.md +70 -0
- package/dist/skills/agents-onboarding/references/01c-pick-tier.md +61 -0
- package/dist/skills/agents-onboarding/references/02-coolify.md +109 -0
- package/dist/skills/agents-onboarding/references/03-chatwoot-pro.md +61 -0
- package/dist/skills/agents-onboarding/references/04-agents-image.md +46 -0
- package/dist/skills/agents-onboarding/references/05-langfuse.md +45 -0
- package/dist/skills/agents-onboarding/references/06-setup-and-mcp.md +47 -0
- package/dist/skills/agents-onboarding/references/08-agent-import.md +55 -0
- package/dist/skills/agents-onboarding/references/09-chatwoot-bind.md +41 -0
- package/dist/skills/agents-onboarding/references/10-validate-e2e.md +34 -0
- package/dist/skills/agents-onboarding/references/agent-features.md +61 -0
- package/dist/skills/agents-onboarding/references/chatwoot-hub-register.md +69 -0
- package/dist/skills/agents-onboarding/references/deploy-b-portainer.md +138 -0
- package/dist/skills/agents-onboarding/references/deploy-c-compose.md +64 -0
- package/dist/skills/agents-onboarding/samples/agents/README.md +23 -0
- package/dist/skills/agents-onboarding/samples/agents/maria-clinica-moreira.json +313 -0
- package/dist/skills/agents-onboarding/scripts/chatwoot-admin.py +248 -0
- package/dist/skills/agents-onboarding/scripts/coolify.py +552 -0
- package/dist/skills/agents-onboarding/scripts/docker-status.py +129 -0
- package/dist/skills/agents-onboarding/scripts/gen-onboarding-env.ts +187 -0
- package/dist/skills/agents-onboarding/scripts/harbor-login.py +118 -0
- package/dist/skills/agents-onboarding/scripts/langfuse-verify.py +118 -0
- package/dist/skills/agents-onboarding/scripts/portainer-brownfield.py +115 -0
- package/dist/skills/agents-onboarding/scripts/remote.py +198 -0
- package/dist/skills/agents-onboarding/scripts/sshkey.py +140 -0
- package/dist/skills/agents-onboarding/templates/chatwoot/.env.example +30 -0
- package/dist/skills/agents-onboarding/templates/chatwoot/README.md +65 -0
- package/dist/skills/agents-onboarding/templates/chatwoot/docker-compose.coolify.yml +136 -0
- package/dist/skills/agents-onboarding/templates/chatwoot/docker-compose.yml +139 -0
- package/dist/skills/agents-onboarding/templates/docker-compose.coolify.yml +73 -0
- package/dist/skills/agents-onboarding/templates/docker-compose.portainer.yml +132 -0
- package/dist/skills/agents-onboarding/templates/docker-compose.prod.yml +85 -0
- package/dist/skills/agents-onboarding/templates/langfuse/.env.example +59 -0
- package/dist/skills/agents-onboarding/templates/langfuse/README.md +132 -0
- package/dist/skills/agents-onboarding/templates/langfuse/docker-compose.coolify.yml +189 -0
- package/dist/skills/agents-onboarding/templates/langfuse/docker-compose.yml +185 -0
- package/dist/skills/agents-operation/SKILL.md +42 -0
- package/dist/skills/agents-operation/gotchas.md +61 -0
- package/dist/skills/agents-operation/guardrails.md +26 -0
- package/dist/skills/agents-operation/references/00-production-safety.md +24 -0
- package/dist/skills/agents-operation/references/01-diagnose.md +34 -0
- package/dist/skills/agents-operation/references/02-reproduce.md +22 -0
- package/dist/skills/agents-operation/references/03-adjust.md +36 -0
- package/dist/skills/agents-operation/references/04-validate-and-apply.md +31 -0
- package/dist/ui-select.js +279 -0
- package/dist/ui.js +167 -0
- 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
|
+
}
|