@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/dist/oauth.js ADDED
@@ -0,0 +1,578 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.pkce = pkce;
4
+ exports.randomState = randomState;
5
+ exports.browserCommand = browserCommand;
6
+ exports.openBrowser = openBrowser;
7
+ exports.callbackHtml = callbackHtml;
8
+ exports.startLoopback = startLoopback;
9
+ exports.parseMcpEnvelope = parseMcpEnvelope;
10
+ exports.readMcpResult = readMcpResult;
11
+ exports.oauthCachePath = oauthCachePath;
12
+ exports.loadOAuthCache = loadOAuthCache;
13
+ exports.saveOAuthCache = saveOAuthCache;
14
+ exports.classifyRefreshStatus = classifyRefreshStatus;
15
+ exports.resolveBrowserClientMode = resolveBrowserClientMode;
16
+ exports.listHubLicenses = listHubLicenses;
17
+ exports.hubCall = hubCall;
18
+ // Fluxo OAuth de CLI (authorization code + PKCE + loopback) contra o hub, e a
19
+ // auto-provisão da credencial de registro via MCP. Zero-dep (node:crypto/http).
20
+ const node_child_process_1 = require("node:child_process");
21
+ const node_crypto_1 = require("node:crypto");
22
+ const node_fs_1 = require("node:fs");
23
+ const node_http_1 = require("node:http");
24
+ const node_https_1 = require("node:https");
25
+ const node_os_1 = require("node:os");
26
+ const node_path_1 = require("node:path");
27
+ const exec_1 = require("./exec");
28
+ function base64url(buf) {
29
+ return buf
30
+ .toString("base64")
31
+ .replace(/\+/g, "-")
32
+ .replace(/\//g, "_")
33
+ .replace(/=+$/, "");
34
+ }
35
+ // PKCE S256: verifier aleatório + challenge = base64url(sha256(verifier)).
36
+ function pkce() {
37
+ const verifier = base64url((0, node_crypto_1.randomBytes)(32));
38
+ const challenge = base64url((0, node_crypto_1.createHash)("sha256").update(verifier).digest());
39
+ return { verifier, challenge };
40
+ }
41
+ function randomState() {
42
+ return base64url((0, node_crypto_1.randomBytes)(16));
43
+ }
44
+ // Monta o comando que abre uma URL no browser, por plataforma. Função pura
45
+ // (testável). No Windows usa `Start-Process` do PowerShell, NÃO `cmd /c start`: o
46
+ // `start` do cmd trata `&` como separador de comando e trunca a URL no primeiro
47
+ // query param (quebrando redirect_uri/state/PKCE do OAuth). `Start-Process` com a
48
+ // URL entre aspas simples preserva a URL inteira. macOS/Linux passam a URL como
49
+ // argv puro (sem shell), então o `&` já não é problema.
50
+ function browserCommand(url, platform = process.platform) {
51
+ if (platform === "win32") {
52
+ return {
53
+ cmd: (0, exec_1.windowsPowershell)(),
54
+ args: [
55
+ "-NoProfile",
56
+ "-Command",
57
+ `Start-Process '${url.replace(/'/g, "''")}'`,
58
+ ],
59
+ };
60
+ }
61
+ if (platform === "darwin")
62
+ return { cmd: "open", args: [url] };
63
+ return { cmd: "xdg-open", args: [url] };
64
+ }
65
+ // Abre a URL no browser do usuário (best-effort; não lança).
66
+ function openBrowser(url) {
67
+ const { cmd, args } = browserCommand(url);
68
+ try {
69
+ // No Windows NÃO usar `detached`: o PowerShell destacado roda sem a window
70
+ // station interativa e o `Start-Process` não abre o browser na sessão do
71
+ // usuário (o `cmd start` antigo tolerava por sair instantâneo). Sem detached
72
+ // ele herda a sessão e abre; `windowsHide` evita o flash de janela. No Unix,
73
+ // `detached` mantém o open/xdg-open vivo após o CLI sair.
74
+ const child = process.platform === "win32"
75
+ ? (0, node_child_process_1.spawn)(cmd, args, { stdio: "ignore", windowsHide: true })
76
+ : (0, node_child_process_1.spawn)(cmd, args, { stdio: "ignore", detached: true });
77
+ child.on("error", () => { });
78
+ child.unref();
79
+ }
80
+ catch {
81
+ // sem browser: o chamador imprime a URL como fallback.
82
+ }
83
+ }
84
+ function escapeHtml(s) {
85
+ return s
86
+ .replace(/&/g, "&")
87
+ .replace(/</g, "&lt;")
88
+ .replace(/>/g, "&gt;")
89
+ .replace(/"/g, "&quot;")
90
+ .replace(/'/g, "&#39;");
91
+ }
92
+ // Página self-contained mostrada no callback do loopback (modelada na
93
+ // buildOAuthCallbackHtml do projeto): card, dark/light por prefers-color-scheme,
94
+ // ícone SVG inline. Sem deps, sem fontes externas, sem script.
95
+ function callbackHtml(ok, detail) {
96
+ const icon = ok
97
+ ? '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>'
98
+ : '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18M6 6l12 12"/></svg>';
99
+ const heading = ok ? "Conexão concluída" : "Não foi possível conectar";
100
+ const body = ok
101
+ ? "Você já pode fechar esta aba e voltar ao terminal."
102
+ : (detail ?? "Tente de novo pelo terminal.");
103
+ return `<!doctype html>
104
+ <html lang="pt-BR">
105
+ <head>
106
+ <meta charset="utf-8">
107
+ <meta name="viewport" content="width=device-width, initial-scale=1">
108
+ <title>fazer.ai</title>
109
+ <style>
110
+ *{box-sizing:border-box}
111
+ html,body{height:100%;margin:0}
112
+ body{display:flex;align-items:center;justify-content:center;padding:24px;
113
+ font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
114
+ background:#f4f5f7;color:#1c1e21}
115
+ .card{width:100%;max-width:360px;background:#fff;border:1px solid #e4e6eb;border-radius:16px;
116
+ padding:32px 28px;text-align:center;box-shadow:0 10px 34px rgba(0,0,0,.08);animation:rise .25s ease-out}
117
+ @keyframes rise{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
118
+ .icon{width:56px;height:56px;border-radius:50%;margin:0 auto 18px;display:flex;align-items:center;justify-content:center}
119
+ .icon svg{width:30px;height:30px}
120
+ .ok .icon{background:#e7f7ee;color:#1a7f47}
121
+ .err .icon{background:#fdeaea;color:#c0392b}
122
+ h1{font-size:18px;font-weight:600;margin:0 0 8px}
123
+ .detail{font-size:14px;color:#4b4f56;margin:0;word-break:break-word}
124
+ .hint{font-size:12px;color:#8a8d91;margin:16px 0 0}
125
+ @media (prefers-color-scheme:dark){
126
+ body{background:#0f1013;color:#e9eaec}
127
+ .card{background:#1b1d21;border-color:#2a2d33;box-shadow:0 10px 34px rgba(0,0,0,.45)}
128
+ .detail{color:#b7bbc2}.hint{color:#7d8189}
129
+ .ok .icon{background:#13301f;color:#4ade80}
130
+ .err .icon{background:#3a1b1b;color:#f87171}
131
+ }
132
+ </style>
133
+ </head>
134
+ <body>
135
+ <main class="card ${ok ? "ok" : "err"}">
136
+ <div class="icon">${icon}</div>
137
+ <h1>${heading}</h1>
138
+ <p class="detail">${escapeHtml(body)}</p>
139
+ <p class="hint">fazer.ai · onboarding do fazer.ai agents</p>
140
+ </main>
141
+ </body>
142
+ </html>`;
143
+ }
144
+ // Sobe um servidor HTTP no loopback para capturar o redirect do OAuth.
145
+ function startLoopback(opts) {
146
+ const path = opts?.path ?? "/callback";
147
+ return new Promise((resolveServer, rejectServer) => {
148
+ let resolveCode;
149
+ let rejectCode;
150
+ const codePromise = new Promise((res, rej) => {
151
+ resolveCode = res;
152
+ rejectCode = rej;
153
+ });
154
+ const server = (0, node_http_1.createServer)((req, res) => {
155
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
156
+ if (url.pathname !== path) {
157
+ res.statusCode = 404;
158
+ res.end("not found");
159
+ return;
160
+ }
161
+ const error = url.searchParams.get("error");
162
+ const code = url.searchParams.get("code");
163
+ const state = url.searchParams.get("state") ?? "";
164
+ const ok = Boolean(code) && !error;
165
+ res.statusCode = ok ? 200 : 400;
166
+ res.setHeader("content-type", "text/html; charset=utf-8");
167
+ res.end(ok
168
+ ? callbackHtml(true)
169
+ : callbackHtml(false, error
170
+ ? `Autorização negada: ${error}`
171
+ : "Resposta sem código de autorização."));
172
+ if (error)
173
+ rejectCode(new Error(`autorização negada: ${error}`));
174
+ else if (code)
175
+ resolveCode({ code, state });
176
+ else
177
+ rejectCode(new Error("callback sem code"));
178
+ });
179
+ server.on("error", rejectServer);
180
+ server.listen(opts?.port ?? 0, "127.0.0.1", () => {
181
+ const addr = server.address();
182
+ const port = typeof addr === "object" && addr ? addr.port : (opts?.port ?? 0);
183
+ resolveServer({
184
+ redirectUri: `http://127.0.0.1:${port}${path}`,
185
+ waitForCode(timeoutMs = 300_000) {
186
+ const timer = setTimeout(() => rejectCode(new Error("tempo esgotado esperando o login")), timeoutMs);
187
+ return codePromise.finally(() => clearTimeout(timer));
188
+ },
189
+ close() {
190
+ server.close();
191
+ },
192
+ });
193
+ });
194
+ });
195
+ }
196
+ function httpJson(method, urlStr, opts) {
197
+ const url = new URL(urlStr);
198
+ const isHttps = url.protocol === "https:";
199
+ const request = isHttps ? node_https_1.request : node_http_1.request;
200
+ const payload = opts?.body !== undefined ? JSON.stringify(opts.body) : undefined;
201
+ return new Promise((resolve, reject) => {
202
+ const req = request({
203
+ method,
204
+ hostname: url.hostname,
205
+ port: url.port || (isHttps ? 443 : 80),
206
+ path: url.pathname + url.search,
207
+ headers: {
208
+ accept: "application/json",
209
+ "user-agent": "@fazer-ai/agents",
210
+ ...(payload
211
+ ? {
212
+ "content-type": "application/json",
213
+ "content-length": Buffer.byteLength(payload),
214
+ }
215
+ : {}),
216
+ ...opts?.headers,
217
+ },
218
+ }, (res) => {
219
+ let body = "";
220
+ res.setEncoding("utf8");
221
+ res.on("data", (chunk) => {
222
+ body += chunk;
223
+ });
224
+ res.on("end", () => resolve({
225
+ status: res.statusCode ?? 0,
226
+ body,
227
+ contentType: String(res.headers["content-type"] ?? ""),
228
+ }));
229
+ });
230
+ req.setTimeout(opts?.timeoutMs ?? 15_000, () => {
231
+ req.destroy(new Error("o hub não respondeu a tempo"));
232
+ });
233
+ req.on("error", reject);
234
+ if (payload)
235
+ req.write(payload);
236
+ req.end();
237
+ });
238
+ }
239
+ function parseJson(body) {
240
+ try {
241
+ return JSON.parse(body);
242
+ }
243
+ catch {
244
+ return {};
245
+ }
246
+ }
247
+ // O transporte Streamable HTTP do MCP responde JSON puro (enableJsonResponse) OU
248
+ // SSE (text/event-stream, o padrão do SDK): linhas `data: {json}`. Extrai o
249
+ // envelope JSON-RPC dos dois casos (no SSE, o último `data:` com result/error/id).
250
+ function parseMcpEnvelope(body, contentType) {
251
+ const isSse = contentType.includes("text/event-stream") ||
252
+ /^\s*(event|data):/m.test(body);
253
+ if (!isSse)
254
+ return parseJson(body);
255
+ const payloads = body
256
+ .split(/\r?\n/)
257
+ .filter((line) => line.startsWith("data:"))
258
+ .map((line) => line.slice("data:".length).trim())
259
+ .filter(Boolean);
260
+ for (let i = payloads.length - 1; i >= 0; i--) {
261
+ const obj = parseJson(payloads[i]);
262
+ if ("result" in obj || "error" in obj || "id" in obj)
263
+ return obj;
264
+ }
265
+ const last = payloads[payloads.length - 1];
266
+ return last ? parseJson(last) : {};
267
+ }
268
+ async function discover(hubBaseUrl) {
269
+ const r = await httpJson("GET", `${hubBaseUrl}/.well-known/oauth-authorization-server`);
270
+ if (r.status !== 200) {
271
+ throw new Error(`metadata OAuth indisponível no hub (HTTP ${r.status})`);
272
+ }
273
+ const m = parseJson(r.body);
274
+ const authorization = m.authorization_endpoint;
275
+ const token = m.token_endpoint;
276
+ if (!authorization || !token) {
277
+ throw new Error("metadata OAuth do hub sem authorization/token endpoint");
278
+ }
279
+ return {
280
+ authorization,
281
+ token,
282
+ registration: m.registration_endpoint,
283
+ };
284
+ }
285
+ async function dcrRegister(registrationUrl, redirectUri) {
286
+ const r = await httpJson("POST", registrationUrl, {
287
+ body: {
288
+ client_name: "@fazer-ai/agents CLI",
289
+ redirect_uris: [redirectUri],
290
+ grant_types: ["authorization_code", "refresh_token"],
291
+ response_types: ["code"],
292
+ token_endpoint_auth_method: "none",
293
+ scope: "mcp:read mcp:write",
294
+ },
295
+ });
296
+ if (r.status !== 201 && r.status !== 200) {
297
+ throw new Error(`registro do client OAuth falhou (HTTP ${r.status})`);
298
+ }
299
+ const clientId = parseJson(r.body).client_id;
300
+ if (!clientId)
301
+ throw new Error("o hub não devolveu client_id no registro");
302
+ return clientId;
303
+ }
304
+ // Interpreta o envelope JSON-RPC já parseado de um tools/call. Surfa tanto um
305
+ // erro de protocolo (`error`) quanto um resultado de tool com `isError:true`
306
+ // (cujo texto é a mensagem crua, não JSON), senão um erro de tool viraria
307
+ // silenciosamente um objeto vazio.
308
+ function readMcpResult(envelope) {
309
+ const rpcError = envelope.error;
310
+ if (rpcError)
311
+ return { ok: false, error: rpcError.message ?? "erro" };
312
+ const result = envelope.result;
313
+ const text = result?.content?.find((c) => c.type === "text")?.text;
314
+ if (result?.isError)
315
+ return { ok: false, error: text || "erro" };
316
+ return {
317
+ ok: true,
318
+ data: text ? parseJson(text) : (result ?? {}),
319
+ };
320
+ }
321
+ let rpcId = 0;
322
+ async function mcpCall(hubBaseUrl, accessToken, name, args) {
323
+ rpcId += 1;
324
+ const r = await httpJson("POST", `${hubBaseUrl}/api/mcp`, {
325
+ // O Streamable HTTP exige Accept com os DOIS tipos, senão devolve 406.
326
+ headers: {
327
+ authorization: `Bearer ${accessToken}`,
328
+ accept: "application/json, text/event-stream",
329
+ },
330
+ body: {
331
+ jsonrpc: "2.0",
332
+ id: rpcId,
333
+ method: "tools/call",
334
+ params: { name, arguments: args },
335
+ },
336
+ });
337
+ if (r.status !== 200) {
338
+ const snippet = r.body.trim().slice(0, 200);
339
+ throw new Error(`chamada MCP ${name} falhou (HTTP ${r.status})${snippet ? `: ${snippet}` : ""}`);
340
+ }
341
+ const parsed = readMcpResult(parseMcpEnvelope(r.body, r.contentType));
342
+ if (!parsed.ok)
343
+ throw new Error(`MCP ${name}: ${parsed.error}`);
344
+ return parsed.data;
345
+ }
346
+ function oauthCachePath(home = (0, node_os_1.homedir)()) {
347
+ return (0, node_path_1.join)(home, ".fazer-ai", "oauth.json");
348
+ }
349
+ // Lê o cache só se for do MESMO hub (cache de outro hub não serve).
350
+ function loadOAuthCache(hubBaseUrl, path = oauthCachePath()) {
351
+ try {
352
+ const obj = JSON.parse((0, node_fs_1.readFileSync)(path, "utf8"));
353
+ if (obj.hubBaseUrl !== hubBaseUrl)
354
+ return {};
355
+ return { clientId: obj.clientId, refreshToken: obj.refreshToken };
356
+ }
357
+ catch {
358
+ return {};
359
+ }
360
+ }
361
+ function saveOAuthCache(hubBaseUrl, data, path = oauthCachePath()) {
362
+ try {
363
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(path), { recursive: true, mode: 0o700 });
364
+ (0, node_fs_1.writeFileSync)(path, `${JSON.stringify({ hubBaseUrl, ...data }, null, 2)}\n`, {
365
+ mode: 0o600,
366
+ });
367
+ (0, node_fs_1.chmodSync)(path, 0o600);
368
+ }
369
+ catch {
370
+ // cache é best-effort; falhar aqui não quebra o login.
371
+ }
372
+ }
373
+ // Classifica o status do endpoint de token num veredito de refresh. Pura (testável). Um 4xx (≠429) é
374
+ // TERMINAL: invalid_grant/invalid_client/reuso detectado, a sessão morreu de fato. 5xx/429 (e erro de
375
+ // rede, mapeado fora daqui) são TRANSITÓRIOS: o hub está instável e o refresh token NÃO foi consumido,
376
+ // então re-tentar é seguro e quase sempre salva a sessão.
377
+ function classifyRefreshStatus(status) {
378
+ if (status === 200)
379
+ return "ok";
380
+ if (status >= 400 && status < 500 && status !== 429)
381
+ return "terminal";
382
+ return "transient";
383
+ }
384
+ // Re-tenta o refresh algumas vezes antes de desistir; só uma falha transitória (rede/5xx/429) é re-tentada.
385
+ const REFRESH_ATTEMPTS = 3;
386
+ const REFRESH_BACKOFF_MS = [300, 800];
387
+ function sleep(ms) {
388
+ return new Promise((resolve) => setTimeout(resolve, ms));
389
+ }
390
+ // Troca o refresh_token por um access_token novo, sem browser. Re-tenta em falha transitória (hub
391
+ // instável/rede): essas não consomem o refresh token, então re-tentar quase sempre salva a sessão. Só
392
+ // desiste como terminal num 4xx de auth (expirado/revogado/reuso), aí o chamador cai pro browser/re-login.
393
+ async function refreshAccessToken(tokenEndpoint, clientId, refreshToken, resource) {
394
+ let lastDetail = "o hub não respondeu ao refresh";
395
+ for (let attempt = 0; attempt < REFRESH_ATTEMPTS; attempt++) {
396
+ if (attempt > 0)
397
+ await sleep(REFRESH_BACKOFF_MS[attempt - 1] ?? 800);
398
+ let res;
399
+ try {
400
+ res = await httpJson("POST", tokenEndpoint, {
401
+ body: {
402
+ grant_type: "refresh_token",
403
+ refresh_token: refreshToken,
404
+ client_id: clientId,
405
+ resource,
406
+ },
407
+ });
408
+ }
409
+ catch (e) {
410
+ // Erro de transporte (DNS/conexão/timeout): sem veredito, o refresh token não foi consumido → re-tenta.
411
+ lastDetail = e instanceof Error ? e.message : "falha de rede no refresh";
412
+ continue;
413
+ }
414
+ const verdict = classifyRefreshStatus(res.status);
415
+ if (verdict === "ok") {
416
+ const tok = parseJson(res.body);
417
+ const accessToken = tok.access_token;
418
+ if (accessToken) {
419
+ return {
420
+ ok: true,
421
+ accessToken,
422
+ refreshToken: tok.refresh_token,
423
+ };
424
+ }
425
+ return { ok: false, transient: false, detail: "refresh sem access_token" };
426
+ }
427
+ if (verdict === "terminal") {
428
+ return {
429
+ ok: false,
430
+ transient: false,
431
+ detail: `refresh recusado (HTTP ${res.status})`,
432
+ };
433
+ }
434
+ lastDetail = `o hub respondeu HTTP ${res.status} ao refresh`;
435
+ }
436
+ return { ok: false, transient: true, detail: lastDetail };
437
+ }
438
+ // Decide como obter o client_id para o login NO BROWSER. O clientId em cache
439
+ // serve só pro refresh (sem browser) e pode ter expirado/sido removido no hub
440
+ // (→ `invalid_client` no /authorize), então no browser preferimos um client
441
+ // FRESCO via DCR. Precedência: client_id fixo (env) > registrar via DCR > cache
442
+ // (último recurso, hub sem DCR) > erro. Pura (testável).
443
+ function resolveBrowserClientMode(opts) {
444
+ if (opts.fixedClientId)
445
+ return "fixed";
446
+ if (opts.hasRegistration)
447
+ return "register";
448
+ if (opts.cachedClientId)
449
+ return "cached";
450
+ return "error";
451
+ }
452
+ // Obtém um access_token do hub: refresh do cache (sem browser) ou login no
453
+ // browser (loopback + PKCE). Salva clientId + refresh rotacionado pra próxima
454
+ // run dispensar o browser. Chamadas repetidas no mesmo run só refrescam (1 login).
455
+ async function acquireAccessToken(config, opts,
456
+ // Loga "sessão reaproveitada" ao usar o refresh em cache. Off no caminho que
457
+ // re-chama em loop (lista de licenças), pra não repetir a cada refresh.
458
+ logReuse = true) {
459
+ const endpoints = await discover(config.hubBaseUrl);
460
+ const resource = `${config.hubBaseUrl}/api/mcp`;
461
+ const cache = loadOAuthCache(config.hubBaseUrl);
462
+ let clientId = process.env.AGENTS_OAUTH_CLIENT_ID?.trim() || cache.clientId;
463
+ let accessToken;
464
+ let refreshToken;
465
+ // 1) Sessão salva: troca o refresh por um access novo, sem browser.
466
+ let refreshFailure;
467
+ if (!opts.forceLogin && clientId && cache.refreshToken) {
468
+ const refreshed = await refreshAccessToken(endpoints.token, clientId, cache.refreshToken, resource);
469
+ if (refreshed.ok) {
470
+ accessToken = refreshed.accessToken;
471
+ refreshToken = refreshed.refreshToken ?? cache.refreshToken;
472
+ if (logReuse) {
473
+ opts.log(`[${new URL(config.hubBaseUrl).host}] Sessão reaproveitada.`);
474
+ }
475
+ }
476
+ else {
477
+ refreshFailure = refreshed;
478
+ }
479
+ }
480
+ // Proxy do hub (headless): sem sessão válida, falha rápido em vez de abrir o browser e travar. Distingue
481
+ // um hub instável/inacessível (transitório → re-rodar o MESMO comando salva a sessão, que segue válida)
482
+ // de uma sessão de fato expirada/revogada (terminal → re-login no browser). Tratar um blip de rede como
483
+ // "sessão expirada" mandava o agente re-logar e tentar caminhos não documentados à toa.
484
+ if (!accessToken && opts.refreshOnly) {
485
+ if (refreshFailure?.transient) {
486
+ throw new Error(`o hub fazer.ai não respondeu ao refresh da sessão (${refreshFailure.detail}); a sessão provavelmente segue válida; rode o mesmo comando de novo em instantes.`);
487
+ }
488
+ throw new Error("sessão do hub expirada/ausente; re-rode o CLI de onboarding para logar (browser).");
489
+ }
490
+ // 2) Sem sessão válida: login no browser (loopback + PKCE).
491
+ if (!accessToken) {
492
+ const loop = await startLoopback();
493
+ try {
494
+ const fixedClientId = process.env.AGENTS_OAUTH_CLIENT_ID?.trim();
495
+ const mode = resolveBrowserClientMode({
496
+ fixedClientId,
497
+ hasRegistration: Boolean(endpoints.registration),
498
+ cachedClientId: cache.clientId,
499
+ });
500
+ if (mode === "fixed") {
501
+ clientId = fixedClientId;
502
+ }
503
+ else if (mode === "register") {
504
+ // Client fresco: o cache.clientId pode ter expirado no hub (invalid_client).
505
+ clientId = await dcrRegister(endpoints.registration, loop.redirectUri);
506
+ }
507
+ else if (mode === "cached") {
508
+ clientId = cache.clientId;
509
+ }
510
+ else {
511
+ throw new Error("o hub não tem DCR habilitado nem client pré-registrado. Habilite MCP_DCR_ENABLED ou defina AGENTS_OAUTH_CLIENT_ID.");
512
+ }
513
+ if (!clientId)
514
+ throw new Error("client_id do OAuth não resolvido.");
515
+ const { verifier, challenge } = pkce();
516
+ const state = randomState();
517
+ const authUrl = `${endpoints.authorization}?${new URLSearchParams({
518
+ client_id: clientId,
519
+ redirect_uri: loop.redirectUri,
520
+ response_type: "code",
521
+ code_challenge: challenge,
522
+ code_challenge_method: "S256",
523
+ scope: "mcp:read mcp:write",
524
+ state,
525
+ resource,
526
+ }).toString()}`;
527
+ opts.log("Abrindo o navegador para você entrar com a fazer.ai...");
528
+ opts.log(`Se não abrir, acesse: ${authUrl}`);
529
+ openBrowser(authUrl);
530
+ const cb = await loop.waitForCode();
531
+ if (cb.state !== state) {
532
+ throw new Error("resposta de login inconsistente (state não confere)");
533
+ }
534
+ const tokenRes = await httpJson("POST", endpoints.token, {
535
+ body: {
536
+ grant_type: "authorization_code",
537
+ client_id: clientId,
538
+ code: cb.code,
539
+ redirect_uri: loop.redirectUri,
540
+ code_verifier: verifier,
541
+ resource,
542
+ },
543
+ });
544
+ if (tokenRes.status !== 200) {
545
+ throw new Error(`troca de token falhou (HTTP ${tokenRes.status})`);
546
+ }
547
+ const tok = parseJson(tokenRes.body);
548
+ accessToken = tok.access_token;
549
+ refreshToken = tok.refresh_token;
550
+ }
551
+ finally {
552
+ loop.close();
553
+ }
554
+ }
555
+ if (!accessToken)
556
+ throw new Error("o hub não devolveu access_token");
557
+ // Guarda clientId + refresh (rotacionado) pra próxima run dispensar o browser.
558
+ saveOAuthCache(config.hubBaseUrl, { clientId, refreshToken });
559
+ return accessToken;
560
+ }
561
+ // Lista as licenças do usuário no hub (read-only, mcp:read). Reusa a aquisição de
562
+ // token; o `list_licenses` devolve `{ licenses: [...] }`. O filtro Kanban/Chatwoot
563
+ // fica no chamador (licenses.ts).
564
+ async function listHubLicenses(config, opts) {
565
+ // Silencia a tag de reuso: este caminho re-chama em loop (refresh da lista).
566
+ const accessToken = await acquireAccessToken(config, opts, false);
567
+ const data = await mcpCall(config.hubBaseUrl, accessToken, "list_licenses", {});
568
+ const licenses = data.licenses;
569
+ return Array.isArray(licenses) ? licenses : [];
570
+ }
571
+ // Operação genérica do hub, exposta pelo subcomando `agents hub …` (proxy) em vez de injetar o hub
572
+ // MCP no agente: o agente ganha só as ops que o onboarding precisa, e o refresh token nunca entra na
573
+ // sessão dele. Reusa o MESMO `acquireAccessToken` (refresh + rotação do oauth.json) e `mcpCall` que o
574
+ // `listHubLicenses`: zero duplicação de OAuth. `args.dry_run` controla preview vs apply.
575
+ async function hubCall(config, opts, name, args) {
576
+ const accessToken = await acquireAccessToken(config, opts, false);
577
+ return mcpCall(config.hubBaseUrl, accessToken, name, args);
578
+ }
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.onboardingMarkerPath = onboardingMarkerPath;
4
+ exports.loadOnboardingMarker = loadOnboardingMarker;
5
+ exports.saveOnboardingMarker = saveOnboardingMarker;
6
+ const node_fs_1 = require("node:fs");
7
+ const node_os_1 = require("node:os");
8
+ const node_path_1 = require("node:path");
9
+ function onboardingMarkerPath(home = (0, node_os_1.homedir)()) {
10
+ return (0, node_path_1.join)(home, ".fazer-ai", "onboarding.json");
11
+ }
12
+ function loadOnboardingMarker(path = onboardingMarkerPath()) {
13
+ try {
14
+ const obj = JSON.parse((0, node_fs_1.readFileSync)(path, "utf8"));
15
+ // Default conservador: markers antigos (sem o campo) e valores inválidos → "new".
16
+ const chatwootSource = obj.chatwootSource === "existing" ? "existing" : "new";
17
+ const edition = obj.edition === "pro" ? "pro" : "free";
18
+ // Chatwoot existente (BYO): sem tier/licença: o fazer.ai agents conecta ao que já está no ar.
19
+ if (chatwootSource === "existing") {
20
+ return { chatwootSource, edition };
21
+ }
22
+ // source = "new": exige um tier válido (pro/community), como o marker legado.
23
+ if (obj.chatwootTier !== "pro" && obj.chatwootTier !== "community") {
24
+ return undefined;
25
+ }
26
+ const out = {
27
+ chatwootSource,
28
+ chatwootTier: obj.chatwootTier,
29
+ edition,
30
+ };
31
+ if (typeof obj.chatwootLicenseId === "string" && obj.chatwootLicenseId) {
32
+ out.chatwootLicenseId = obj.chatwootLicenseId;
33
+ }
34
+ return out;
35
+ }
36
+ catch {
37
+ return undefined;
38
+ }
39
+ }
40
+ function saveOnboardingMarker(marker, path = onboardingMarkerPath()) {
41
+ try {
42
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(path), { recursive: true, mode: 0o700 });
43
+ (0, node_fs_1.writeFileSync)(path, `${JSON.stringify(marker, null, 2)}\n`);
44
+ }
45
+ catch {
46
+ // best-effort; falhar aqui não quebra o fluxo.
47
+ }
48
+ }
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.preferencesPath = preferencesPath;
4
+ exports.loadPreferences = loadPreferences;
5
+ exports.savePreferences = savePreferences;
6
+ const node_fs_1 = require("node:fs");
7
+ const node_os_1 = require("node:os");
8
+ const node_path_1 = require("node:path");
9
+ function preferencesPath(home = (0, node_os_1.homedir)()) {
10
+ return (0, node_path_1.join)(home, ".fazer-ai", "preferences.json");
11
+ }
12
+ function loadPreferences(path = preferencesPath()) {
13
+ try {
14
+ const obj = JSON.parse((0, node_fs_1.readFileSync)(path, "utf8"));
15
+ const out = {};
16
+ if (typeof obj.agent === "string" && obj.agent)
17
+ out.agent = obj.agent;
18
+ if (typeof obj.provider === "string" && obj.provider)
19
+ out.provider = obj.provider;
20
+ if (typeof obj.chatwootLicenseId === "string" && obj.chatwootLicenseId)
21
+ out.chatwootLicenseId = obj.chatwootLicenseId;
22
+ if (typeof obj.chatwootSource === "string" && obj.chatwootSource)
23
+ out.chatwootSource = obj.chatwootSource;
24
+ return out;
25
+ }
26
+ catch {
27
+ return {};
28
+ }
29
+ }
30
+ // Mescla com o que já está salvo, então gravar `agent` não apaga `provider`.
31
+ function savePreferences(partial, path = preferencesPath()) {
32
+ try {
33
+ const next = { ...loadPreferences(path), ...partial };
34
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(path), { recursive: true, mode: 0o700 });
35
+ (0, node_fs_1.writeFileSync)(path, `${JSON.stringify(next, null, 2)}\n`);
36
+ }
37
+ catch {
38
+ // best-effort; falhar aqui não quebra o fluxo.
39
+ }
40
+ }