@fazer-ai/agents 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +112 -0
  3. package/dist/agents/claude.js +152 -0
  4. package/dist/agents/codex.js +155 -0
  5. package/dist/agents/detect.js +15 -0
  6. package/dist/agents/handoff.js +22 -0
  7. package/dist/agents/hermes-skills.js +177 -0
  8. package/dist/agents/hermes.js +474 -0
  9. package/dist/agents/index.js +57 -0
  10. package/dist/agents/manual.js +22 -0
  11. package/dist/agents/other.js +39 -0
  12. package/dist/agents/shell.js +15 -0
  13. package/dist/agents/types.js +2 -0
  14. package/dist/config.js +48 -0
  15. package/dist/exec.js +279 -0
  16. package/dist/hostinger.js +75 -0
  17. package/dist/hub-command.js +144 -0
  18. package/dist/index.js +726 -0
  19. package/dist/licenses.js +93 -0
  20. package/dist/mcp.js +100 -0
  21. package/dist/oauth.js +578 -0
  22. package/dist/onboarding-marker.js +48 -0
  23. package/dist/preferences.js +40 -0
  24. package/dist/skills/agents-dev/SKILL.md +37 -0
  25. package/dist/skills/agents-dev/gotchas.md +6 -0
  26. package/dist/skills/agents-dev/guardrails.md +6 -0
  27. package/dist/skills/agents-dev/references/00-get-the-code.md +28 -0
  28. package/dist/skills/agents-dev/references/01-layout-and-bun-check.md +29 -0
  29. package/dist/skills/agents-dev/references/02-free-full-and-invariants.md +7 -0
  30. package/dist/skills/agents-dev/references/03-implement.md +9 -0
  31. package/dist/skills/agents-dev/references/04-own-image-and-deploy.md +13 -0
  32. package/dist/skills/agents-onboarding/SKILL.md +80 -0
  33. package/dist/skills/agents-onboarding/gotchas.md +157 -0
  34. package/dist/skills/agents-onboarding/guardrails.md +65 -0
  35. package/dist/skills/agents-onboarding/references/00-prereqs-and-access.md +37 -0
  36. package/dist/skills/agents-onboarding/references/01-vps-dns-ssh.md +67 -0
  37. package/dist/skills/agents-onboarding/references/01b-brownfield.md +70 -0
  38. package/dist/skills/agents-onboarding/references/01c-pick-tier.md +61 -0
  39. package/dist/skills/agents-onboarding/references/02-coolify.md +109 -0
  40. package/dist/skills/agents-onboarding/references/03-chatwoot-pro.md +61 -0
  41. package/dist/skills/agents-onboarding/references/04-agents-image.md +46 -0
  42. package/dist/skills/agents-onboarding/references/05-langfuse.md +45 -0
  43. package/dist/skills/agents-onboarding/references/06-setup-and-mcp.md +47 -0
  44. package/dist/skills/agents-onboarding/references/08-agent-import.md +55 -0
  45. package/dist/skills/agents-onboarding/references/09-chatwoot-bind.md +41 -0
  46. package/dist/skills/agents-onboarding/references/10-validate-e2e.md +34 -0
  47. package/dist/skills/agents-onboarding/references/agent-features.md +61 -0
  48. package/dist/skills/agents-onboarding/references/chatwoot-hub-register.md +69 -0
  49. package/dist/skills/agents-onboarding/references/deploy-b-portainer.md +138 -0
  50. package/dist/skills/agents-onboarding/references/deploy-c-compose.md +64 -0
  51. package/dist/skills/agents-onboarding/samples/agents/README.md +23 -0
  52. package/dist/skills/agents-onboarding/samples/agents/maria-clinica-moreira.json +313 -0
  53. package/dist/skills/agents-onboarding/scripts/chatwoot-admin.py +248 -0
  54. package/dist/skills/agents-onboarding/scripts/coolify.py +552 -0
  55. package/dist/skills/agents-onboarding/scripts/docker-status.py +129 -0
  56. package/dist/skills/agents-onboarding/scripts/gen-onboarding-env.ts +187 -0
  57. package/dist/skills/agents-onboarding/scripts/harbor-login.py +118 -0
  58. package/dist/skills/agents-onboarding/scripts/langfuse-verify.py +118 -0
  59. package/dist/skills/agents-onboarding/scripts/portainer-brownfield.py +115 -0
  60. package/dist/skills/agents-onboarding/scripts/remote.py +198 -0
  61. package/dist/skills/agents-onboarding/scripts/sshkey.py +140 -0
  62. package/dist/skills/agents-onboarding/templates/chatwoot/.env.example +30 -0
  63. package/dist/skills/agents-onboarding/templates/chatwoot/README.md +65 -0
  64. package/dist/skills/agents-onboarding/templates/chatwoot/docker-compose.coolify.yml +136 -0
  65. package/dist/skills/agents-onboarding/templates/chatwoot/docker-compose.yml +139 -0
  66. package/dist/skills/agents-onboarding/templates/docker-compose.coolify.yml +73 -0
  67. package/dist/skills/agents-onboarding/templates/docker-compose.portainer.yml +132 -0
  68. package/dist/skills/agents-onboarding/templates/docker-compose.prod.yml +85 -0
  69. package/dist/skills/agents-onboarding/templates/langfuse/.env.example +59 -0
  70. package/dist/skills/agents-onboarding/templates/langfuse/README.md +132 -0
  71. package/dist/skills/agents-onboarding/templates/langfuse/docker-compose.coolify.yml +189 -0
  72. package/dist/skills/agents-onboarding/templates/langfuse/docker-compose.yml +185 -0
  73. package/dist/skills/agents-operation/SKILL.md +42 -0
  74. package/dist/skills/agents-operation/gotchas.md +61 -0
  75. package/dist/skills/agents-operation/guardrails.md +26 -0
  76. package/dist/skills/agents-operation/references/00-production-safety.md +24 -0
  77. package/dist/skills/agents-operation/references/01-diagnose.md +34 -0
  78. package/dist/skills/agents-operation/references/02-reproduce.md +22 -0
  79. package/dist/skills/agents-operation/references/03-adjust.md +36 -0
  80. package/dist/skills/agents-operation/references/04-validate-and-apply.md +31 -0
  81. package/dist/ui-select.js +279 -0
  82. package/dist/ui.js +167 -0
  83. package/package.json +53 -0
@@ -0,0 +1,198 @@
1
+ #!/usr/bin/env python3
2
+ # Generic remote-bash runner for the fazer.ai agents onboarding. ONE job: run an arbitrary bash script on a
3
+ # host over SSH WITHOUT the agent hand-assembling it on the PowerShell command line. That assembly is the
4
+ # footgun that reappears every new context on Windows, in two distinct shapes (both seen in real runs):
5
+ #
6
+ # 1. Eaten quotes. `ssh <host> 'docker network ls --format "{{.Name}}" | grep -vE "^(a|b)$"'`, the inner
7
+ # double quotes are swallowed by Windows native-arg parsing on the way to ssh.exe, so the remote bash
8
+ # gets `{{.Name}}` / `^(a|b)$` UNQUOTED and dies with `syntax error near unexpected token '('`.
9
+ # 2. BOM on a here-string. `@'...set -euo pipefail...'@ | ssh ... 'bash -s'`, PowerShell prepends a UTF-8
10
+ # BOM to the piped text; the remote bash reads `set` as an unknown command on line 1, so the
11
+ # guard never arms and the rest of a (possibly destructive) script runs unguarded.
12
+ #
13
+ # Both vanish with the same move: write the script to a `.sh` file (with the editing tool, no shell in the
14
+ # loop) and feed it to the remote `bash -s` through THIS helper, which streams the bytes to ssh's stdin via
15
+ # a DIRECT argv list (no local shell). The script content never touches a command line, so quotes, `$()`,
16
+ # `{{...}}`, `(`, heredocs and newlines arrive byte-for-byte on every OS. Same payload-owning pattern as
17
+ # coolify.py (psql) and docker-status.py (`docker ps`), generalized to any script.
18
+ #
19
+ # remote.py --ssh root@HOST --ssh-opts "-i ~/.ssh/key" --script-file recon.sh # stream output
20
+ # remote.py --ssh root@HOST --ssh-opts "-i ~/.ssh/key" --script-file wipe.sh --sudo # via sudo -n
21
+ # remote.py --ssh ... --in-container coolify-db --exec "psql -U coolify -d coolify" --script-file q.sql # psql in a container
22
+ # remote.py --ssh ... --in-container <rails-c> --exec "bundle exec rails runner -" --script-file t.rb # rails runner
23
+ # remote.py --ssh root@HOST --ssh-opts "-i ~/.ssh/key" --script-file x.sh --capture # JSON result
24
+ # remote.py --ssh root@HOST --ssh-opts "-i ~/.ssh/key" --script-file x.sh --dry-run # argv+preview
25
+ #
26
+ # Same payload-on-stdin trick for a console INSIDE a container: psql and `rails runner -` read their program
27
+ # from stdin, so --in-container/--exec delivers a .sql/.rb file byte-exact too (the `\` of a PHP/Ruby
28
+ # namespace, accents, quotes, none of it touches a command line).
29
+ #
30
+ # Python 3 stdlib only (no pip). Runs ssh via Bash with dangerouslyDisableSandbox:true (it is network),
31
+ # same as sshkey.py/docker-status.py. Output streams live by default (long installs do not look hung and
32
+ # do not trip the harness timeout); exit code is propagated. --capture buffers and returns JSON instead.
33
+ import argparse
34
+ import json
35
+ import os
36
+ import re
37
+ import shlex
38
+ import subprocess
39
+ import sys
40
+
41
+ BOM = b"\xef\xbb\xbf"
42
+ # Container name is interpolated into the remote command, so keep it to a safe bare token.
43
+ SAFE_TOKEN = set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-")
44
+ # --exec is the in-container program that reads the script from stdin (psql / rails runner -). Allow only
45
+ # bare words, flags and the punctuation those need, no shell metacharacters that the remote shell would act on.
46
+ EXEC_OK = re.compile(r"^[A-Za-z0-9 _=:./-]+$")
47
+ # Git Bash/MSYS on Windows reports paths as "/c/Users/..." (or "/mnt/c/..." under WSL), but native Python
48
+ # on Windows cannot open those: it needs "C:\Users\...". Match a leading drive segment so we can retranslate.
49
+ MSYS_DRIVE_RE = re.compile(r"^/(?:mnt/)?([A-Za-z])/(.*)$")
50
+
51
+
52
+ def out(obj, code=0):
53
+ print(json.dumps(obj))
54
+ sys.exit(code)
55
+
56
+
57
+ def fail(msg, **extra):
58
+ out({"ok": False, "error": msg, **extra}, code=1)
59
+
60
+
61
+ def msys_to_native(path):
62
+ # Translate a Git Bash/MSYS/WSL path ("/c/Users/me/x.sh", "/mnt/c/Users/me/x.sh") to the native Windows
63
+ # form ("C:\Users\me\x.sh") so open() finds it. Returns None when the path is not drive-prefixed (nothing
64
+ # to translate). POSIX paths and already-native Windows paths ("C:\...", "C:/...") are left to open() as-is.
65
+ m = MSYS_DRIVE_RE.match(path)
66
+ if not m:
67
+ return None
68
+ drive, rest = m.group(1), m.group(2)
69
+ return f"{drive.upper()}:\\" + rest.replace("/", "\\")
70
+
71
+
72
+ def split_ssh_opts(opts, _nt=None):
73
+ # POSIX shlex eats backslashes, so a Windows key path ("-i C:\Users\me\.ssh\key") would arrive as
74
+ # "C:Usersme.sshkey". On Windows, tokenize WITHOUT escape processing and strip the surrounding quotes
75
+ # ourselves so backslashes survive. _nt is injectable for tests. (Kept in sync with docker-status.py.)
76
+ nt = (os.name == "nt") if _nt is None else _nt
77
+ if not opts:
78
+ return []
79
+ if nt:
80
+ toks = shlex.split(opts, posix=False)
81
+ return [t[1:-1] if len(t) >= 2 and t[0] == t[-1] and t[0] in "\"'" else t for t in toks]
82
+ return shlex.split(opts)
83
+
84
+
85
+ def _read_bytes(path):
86
+ with open(path, "rb") as f:
87
+ return f.read()
88
+
89
+
90
+ def read_script(path):
91
+ if path == "-":
92
+ data = sys.stdin.buffer.read()
93
+ else:
94
+ try:
95
+ data = _read_bytes(path)
96
+ except OSError as exc:
97
+ # A Git Bash/MSYS path ("/c/Users/me/x.sh") reaches native Windows Python verbatim and fails to
98
+ # open; retry the translated native form ("C:\Users\me\x.sh") before giving up.
99
+ native = msys_to_native(path)
100
+ if native is None:
101
+ fail(f"cannot read --script-file {path!r}: {exc}")
102
+ try:
103
+ data = _read_bytes(native)
104
+ except OSError as exc2:
105
+ fail(f"cannot read --script-file {path!r} (tried {native!r}): {exc2}")
106
+ # Strip a stray UTF-8 BOM defensively: a PowerShell here-string / `Out-File` adds one and it breaks the
107
+ # script's first line (the exact failure this helper exists to remove). We deliver the rest byte-exact.
108
+ if data[:3] == BOM:
109
+ data = data[3:]
110
+ if not data.strip():
111
+ fail("script is empty")
112
+ return data
113
+
114
+
115
+ def build_argv(args, remote):
116
+ return [
117
+ "ssh", "-o", "BatchMode=yes", "-o", f"ConnectTimeout={args.connect_timeout}",
118
+ "-o", "StrictHostKeyChecking=accept-new", *split_ssh_opts(args.ssh_opts), args.ssh, remote,
119
+ ]
120
+
121
+
122
+ def remote_command(args):
123
+ # The remote command carries NO payload; it just starts the interpreter; the script rides in on stdin,
124
+ # so it never hits a command line. `bash -s` and `docker exec -i <c> <prog>` (psql, `rails runner -`)
125
+ # all read their program from stdin. `sudo -n` fails fast instead of hanging on a password prompt.
126
+ if args.in_container:
127
+ if any(c not in SAFE_TOKEN for c in args.in_container):
128
+ fail(f"invalid --in-container {args.in_container!r} (expected a bare [A-Za-z0-9._-] token)")
129
+ inner = args.exec or "bash -s"
130
+ if not EXEC_OK.match(inner):
131
+ fail(f"invalid --exec {inner!r} (bare words/flags only: [A-Za-z0-9 _=:./-])")
132
+ base = f"docker exec -i {args.in_container} {inner}"
133
+ else:
134
+ base = "bash -s"
135
+ return f"sudo -n -- {base}" if args.sudo else base
136
+
137
+
138
+ def cmd_run(args):
139
+ script = read_script(args.script_file)
140
+ remote = remote_command(args)
141
+ argv = build_argv(args, remote)
142
+ if args.dry_run:
143
+ preview = script[:400].decode("utf-8", "replace")
144
+ out({
145
+ "ok": True, "dry_run": True, "remote_cmd": remote, "argv": argv,
146
+ "script_bytes": len(script), "script_preview": preview,
147
+ })
148
+ timeout = args.timeout or None
149
+ try:
150
+ if args.capture:
151
+ proc = subprocess.run(argv, input=script, capture_output=True, timeout=timeout)
152
+ ok = proc.returncode == 0
153
+ out({
154
+ "ok": ok, "exit_code": proc.returncode,
155
+ "stdout": proc.stdout.decode("utf-8", "replace"),
156
+ "stderr": proc.stderr.decode("utf-8", "replace"),
157
+ }, code=0 if ok else 1)
158
+ # Stream mode: inherit stdout/stderr so the agent sees output live; feed the script on stdin.
159
+ proc = subprocess.run(argv, input=script, timeout=timeout)
160
+ sys.exit(proc.returncode)
161
+ except FileNotFoundError:
162
+ fail("ssh not found on PATH")
163
+ except subprocess.TimeoutExpired:
164
+ fail("ssh timed out", remote_cmd=remote)
165
+
166
+
167
+ def build_parser():
168
+ parser = argparse.ArgumentParser(
169
+ prog="remote.py",
170
+ description="Run an arbitrary bash script on a host over SSH (payload-owning; survives "
171
+ "PowerShell->SSH). Write the script to a .sh file and point --script-file at it.",
172
+ )
173
+ parser.add_argument("--ssh", required=True, metavar="USER@HOST")
174
+ parser.add_argument("--ssh-opts", default="", help="extra ssh options, e.g. '-i ~/.ssh/key -p 2222'")
175
+ parser.add_argument("--script-file", required=True, metavar="PATH",
176
+ help="path to the bash script to run remotely ('-' reads stdin; avoid '-' on PowerShell)")
177
+ parser.add_argument("--in-container", default="", metavar="NAME",
178
+ help="pipe the script into `docker exec -i NAME ...` on the host (console in a container)")
179
+ parser.add_argument("--exec", default="", metavar="CMD",
180
+ help="program inside --in-container that reads the script on stdin, e.g. "
181
+ "'psql -U coolify -d coolify -v ON_ERROR_STOP=1' or 'bundle exec rails runner -' (default: bash -s)")
182
+ parser.add_argument("--sudo", action="store_true", help="run via `sudo -n -- …` (works with bash -s and --in-container)")
183
+ parser.add_argument("--capture", action="store_true",
184
+ help="buffer and print {ok, exit_code, stdout, stderr} as JSON instead of streaming")
185
+ parser.add_argument("--connect-timeout", type=int, default=12)
186
+ parser.add_argument("--timeout", type=int, default=0, help="ssh timeout in seconds (0 = no limit; installs run long)")
187
+ parser.add_argument("--dry-run", action="store_true", help="print the argv + script preview, do not connect")
188
+ parser.set_defaults(fn=cmd_run)
189
+ return parser
190
+
191
+
192
+ def main():
193
+ args = build_parser().parse_args()
194
+ args.fn(args)
195
+
196
+
197
+ if __name__ == "__main__":
198
+ main()
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env python3
2
+ # SSH key helper for the fazer.ai agents onboarding. Two jobs:
3
+ # generate create a dedicated ed25519 key and print the PUBLIC half for the operator to paste in the
4
+ # VPS panel. ssh-keygen is spawned with a DIRECT argv list (no shell) so the empty passphrase
5
+ # `-N ""` survives on every OS. Driven through a shell line, PowerShell DROPS the empty-string
6
+ # arg, ssh-keygen then falls into its interactive passphrase prompt and HANGS, exactly what
7
+ # cost a Windows run six failed attempts and a timeout. argv-direct sidesteps the whole class.
8
+ # wait-access poll SSH until the freshly pasted key logs in, so the agent detects "done" itself instead
9
+ # of asking the operator to come back and confirm. Timeout -> exit 1 (caller falls back to ask).
10
+ # Python 3 stdlib only (no pip). `generate` is local; `wait-access` runs ssh via Bash with
11
+ # dangerouslyDisableSandbox:true (it is network), same as the other helpers.
12
+ import argparse
13
+ import json
14
+ import os
15
+ import re
16
+ import shlex
17
+ import subprocess
18
+ import sys
19
+ import time
20
+ from pathlib import Path
21
+
22
+ NAME_RE = re.compile(r"^[A-Za-z0-9._-]+$")
23
+
24
+
25
+ def out(obj, code=0):
26
+ print(json.dumps(obj))
27
+ sys.exit(code)
28
+
29
+
30
+ def fail(msg, **extra):
31
+ out({"ok": False, "error": msg, **extra}, code=1)
32
+
33
+
34
+ def split_ssh_opts(opts, _nt=None):
35
+ # POSIX shlex eats backslashes, so a Windows key path ("-i C:\Users\me\.ssh\key") would arrive as
36
+ # "C:Usersme.sshkey" (a real onboarding failure). On Windows, tokenize WITHOUT escape processing and
37
+ # strip the surrounding quotes ourselves so backslashes are preserved. _nt is injectable for tests.
38
+ nt = (os.name == "nt") if _nt is None else _nt
39
+ if not opts:
40
+ return []
41
+ if nt:
42
+ toks = shlex.split(opts, posix=False)
43
+ return [t[1:-1] if len(t) >= 2 and t[0] == t[-1] and t[0] in "\"'" else t for t in toks]
44
+ return shlex.split(opts)
45
+
46
+
47
+ def cmd_generate(args):
48
+ if not NAME_RE.match(args.name):
49
+ fail(f"invalid --name {args.name!r} (expected a bare filename [A-Za-z0-9._-]+, no path)")
50
+ ssh_dir = Path(args.ssh_dir).expanduser() if args.ssh_dir else Path.home() / ".ssh"
51
+ try:
52
+ ssh_dir.mkdir(parents=True, exist_ok=True)
53
+ os.chmod(ssh_dir, 0o700)
54
+ except OSError as exc:
55
+ fail(f"cannot prepare ssh dir {ssh_dir}: {exc}")
56
+ key = ssh_dir / args.name
57
+ pub = Path(str(key) + ".pub")
58
+ existed = key.exists() and pub.exists()
59
+ if not existed:
60
+ # argv-direct: the empty passphrase "" is a real, separate argv element on every OS here.
61
+ argv = ["ssh-keygen", "-t", "ed25519", "-f", str(key), "-N", "", "-C", args.comment]
62
+ try:
63
+ proc = subprocess.run(argv, capture_output=True, text=True, timeout=args.timeout)
64
+ except FileNotFoundError:
65
+ fail("ssh-keygen not found on PATH (install OpenSSH client)")
66
+ except subprocess.TimeoutExpired:
67
+ fail("ssh-keygen timed out (interactive prompt? the empty passphrase did not pass through)")
68
+ if proc.returncode != 0 or not pub.exists():
69
+ fail("ssh-keygen failed", exit_code=proc.returncode, stderr=(proc.stderr or "")[-400:])
70
+ try:
71
+ os.chmod(key, 0o600)
72
+ except OSError:
73
+ pass
74
+ try:
75
+ public_key = pub.read_text(encoding="utf-8").strip()
76
+ except OSError as exc:
77
+ fail(f"key generated but cannot read public half: {exc}")
78
+ out({"ok": True, "key_path": str(key), "public_key": public_key, "existed": existed})
79
+
80
+
81
+ def cmd_wait_access(args):
82
+ argv = [
83
+ "ssh", "-o", "BatchMode=yes", "-o", f"ConnectTimeout={args.connect_timeout}",
84
+ "-o", "StrictHostKeyChecking=accept-new", *split_ssh_opts(args.ssh_opts), args.ssh, "echo OK",
85
+ ]
86
+ last = None
87
+ for i in range(args.attempts):
88
+ try:
89
+ proc = subprocess.run(argv, capture_output=True, text=True, timeout=args.timeout)
90
+ except FileNotFoundError:
91
+ fail("ssh not found on PATH")
92
+ except subprocess.TimeoutExpired:
93
+ proc = None
94
+ if proc is not None and proc.returncode == 0 and "OK" in (proc.stdout or ""):
95
+ out({"ok": True, "reachable": True, "attempt": i + 1})
96
+ if proc is not None:
97
+ last = (proc.stderr or proc.stdout or "").strip()[-200:]
98
+ if i < args.attempts - 1:
99
+ time.sleep(args.interval)
100
+ out(
101
+ {"ok": False, "reachable": False, "attempts": args.attempts, "last_error": last,
102
+ "note": "key not active yet: confirm it is pasted in the VPS panel, or ask the operator"},
103
+ code=1,
104
+ )
105
+
106
+
107
+ def build_parser():
108
+ parser = argparse.ArgumentParser(
109
+ prog="sshkey.py",
110
+ description="Generate a dedicated ed25519 key (argv-direct, empty passphrase safe on Windows) "
111
+ "and poll SSH access.",
112
+ )
113
+ sub = parser.add_subparsers(dest="cmd", required=True)
114
+
115
+ gen = sub.add_parser("generate", help="create ~/.ssh/<name> ed25519 (idempotent) and print the public key")
116
+ gen.add_argument("--name", required=True, help="bare key filename; use the fixed 'fazer-ai-agents' so re-runs reuse the same key (no per-run suffix)")
117
+ gen.add_argument("--comment", default="fazer-ai-onboarding", help="ssh-keygen -C comment")
118
+ gen.add_argument("--ssh-dir", default="", help="override the .ssh dir (default ~/.ssh; for tests)")
119
+ gen.add_argument("--timeout", type=int, default=30)
120
+ gen.set_defaults(fn=cmd_generate)
121
+
122
+ wait = sub.add_parser("wait-access", help="poll SSH until the pasted key logs in (else exit 1)")
123
+ wait.add_argument("--ssh", required=True, metavar="USER@HOST")
124
+ wait.add_argument("--ssh-opts", default="", help="extra ssh options, e.g. '-i ~/.ssh/key -p 2222'")
125
+ wait.add_argument("--attempts", type=int, default=30, help="poll attempts (default 30)")
126
+ wait.add_argument("--interval", type=int, default=5, help="seconds between attempts (default 5)")
127
+ wait.add_argument("--connect-timeout", type=int, default=12)
128
+ wait.add_argument("--timeout", type=int, default=20, help="per-attempt ssh timeout")
129
+ wait.set_defaults(fn=cmd_wait_access)
130
+
131
+ return parser
132
+
133
+
134
+ def main():
135
+ args = build_parser().parse_args()
136
+ args.fn(args)
137
+
138
+
139
+ if __name__ == "__main__":
140
+ main()
@@ -0,0 +1,30 @@
1
+ # Chatwoot (generic flavor): copy to .env and fill every CHANGE_ME. Do NOT commit the filled .env.
2
+ # Generate secrets with: openssl rand -hex 64 (SECRET_KEY_BASE) / openssl rand -hex 24 (passwords)
3
+
4
+ # Public URL (where your reverse proxy serves Chatwoot). Required.
5
+ CHATWOOT_URL=https://chatwoot.example.com
6
+ CHATWOOT_LOCALE=pt_BR
7
+
8
+ # Postgres (this stack's own DB; pgvector image).
9
+ POSTGRES_DB=chatwoot_production
10
+ POSTGRES_USER=chatwoot
11
+ POSTGRES_PASSWORD=CHANGE_ME_pg_password
12
+
13
+ # Rails secret (128 hex chars: openssl rand -hex 64).
14
+ SECRET_KEY_BASE=CHANGE_ME_secret_key_base
15
+
16
+ # Redis password (openssl rand -hex 24).
17
+ REDIS_PASSWORD=CHANGE_ME_redis_password
18
+
19
+ # Host bind for the web port (front it with your own TLS proxy). Default localhost-only.
20
+ CHATWOOT_PORT=127.0.0.1:3001
21
+
22
+ # ─────────────────────────────────────────────────────────────────────────────
23
+ # EDITION SELECTION
24
+ # OSS (default, no hub subscription): leave CHATWOOT_IMAGE unset (uses our ghcr.io/fazer-ai/chatwoot fork), do NOT set COMPOSE_PROFILES.
25
+ # Pro (hub subscription): uncomment BOTH lines below, then `docker login harbor.fazer.ai`
26
+ # (hub: generate_install_script reveals the credential) BEFORE deploying. Pro adds Baileys WhatsApp.
27
+ # ─────────────────────────────────────────────────────────────────────────────
28
+ # CHATWOOT_IMAGE=harbor.fazer.ai/chatwoot/fazer-ai/chatwoot-pro:latest
29
+ # COMPOSE_PROFILES=pro
30
+ # BAILEYS_DEFAULT_API_KEY=CHANGE_ME_baileys_key # required only for Pro (openssl rand -hex 24)
@@ -0,0 +1,65 @@
1
+ # Chatwoot (self-hosted) for fazer.ai
2
+
3
+ The inbox fazer.ai agents plugs into. The onboarding installs Chatwoot as part of the journey, in one of
4
+ **two editions**, chosen by whether the operator has a subscription on the fazer.ai hub:
5
+
6
+ | Edition | Image | When | Extra features |
7
+ | --- | --- | --- | --- |
8
+ | **Pro** | `harbor.fazer.ai/chatwoot/fazer-ai/chatwoot-pro` (private Harbor) | operator **has a hub license** | Baileys WhatsApp provider, Kanban |
9
+ | **OSS** | `ghcr.io/fazer-ai/chatwoot` (our public fork) | **no** hub subscription | standard Chatwoot (official WhatsApp Cloud API, etc.) |
10
+
11
+ **Both editions work with fazer.ai agents**: the integration is the standard Chatwoot Agent Bot + API
12
+ (see `docs/chatwoot.md`). OSS is **not** a downgrade of compatibility, only of
13
+ Pro-exclusive channels/features.
14
+
15
+ **Edition source (in order).** The onboarding CLI captures the operator's choice up front and writes it to
16
+ `~/.fazer-ai/onboarding.json` (`{ "chatwootTier": "pro" | "community", "chatwootLicenseId": "<id>" }`).
17
+ Read that marker **first**: `"pro"` → deploy the Pro image and license it at step 9b (use
18
+ `chatwootLicenseId`); `"community"` → deploy OSS (the operator deliberately chose to skip Pro). Only if the
19
+ marker is **absent** (token fallback or a non-CLI entry point) fall back to the hub (`list_licenses` /
20
+ `whoami`): a `CHATWOOT` license → Pro; otherwise OSS. The marker wins because "proceed without Pro" is an
21
+ intent the hub can't express: an operator may own a license yet still pick OSS for this box.
22
+
23
+ ## Files
24
+
25
+ | File | Use |
26
+ | --- | --- |
27
+ | `docker-compose.yml` | **Generic**: Portainer / EasyPanel / Dokploy / plain Docker. ONE file, both editions via env. |
28
+ | `docker-compose.coolify.yml` | **Coolify** (magic vars; secrets auto-generated). Pro image by default. |
29
+ | `.env.example` | Template for the generic flavor. `cp .env.example .env`, fill every `CHANGE_ME`. |
30
+
31
+ Topology (both): `chatwoot` (web) + `sidekiq` + `postgres` (pgvector) + `redis`, plus `baileys-api`
32
+ **only in Pro** (compose profile `pro`).
33
+
34
+ ## Deploy (generic: Portainer / plain Docker)
35
+
36
+ ```sh
37
+ cp .env.example .env # fill every CHANGE_ME (openssl rand -hex 64 / -hex 24)
38
+
39
+ # OSS (no subscription):
40
+ docker compose up -d
41
+
42
+ # Pro (hub subscription): authenticate to Harbor first, then enable the pro profile + image.
43
+ docker login harbor.fazer.ai # username + secret from the hub (generate_install_script)
44
+ CHATWOOT_IMAGE=harbor.fazer.ai/chatwoot/fazer-ai/chatwoot-pro:latest \
45
+ COMPOSE_PROFILES=pro docker compose up -d
46
+ ```
47
+
48
+ **Portainer**: paste `docker-compose.yml` as a Stack and provide the same variables in the Stack env
49
+ editor (for Pro, add `CHATWOOT_IMAGE` + `COMPOSE_PROFILES=pro` and register the Harbor credential under
50
+ Registries first, or `Registries:[<id>]` in the API). Front `chatwoot.<domain>` with a TLS proxy: the
51
+ fazer.ai agents [`docker-compose.portainer.yml`](../docker-compose.portainer.yml) bundled Caddy can do
52
+ this (point a Caddy site at the published `CHATWOOT_PORT`); see the `agents-onboarding` skill (`references/deploy-b-portainer.md`).
53
+
54
+ ## Wire into fazer.ai agents
55
+
56
+ After both are up (see `docs/chatwoot.md`): create a Chatwoot admin, mint an
57
+ admin token, then from the agents register the deployment (`POST /v1/chatwoot/deployment {baseUrl, adminToken}`),
58
+ sync the account inbox, and bind an inbox to an agent (`PATCH /v1/chatwoot/inboxes/:id {agentId}`), which
59
+ auto-provisions the Agent Bot. The bind is identical for Pro and OSS.
60
+
61
+ ## First admin
62
+
63
+ In the real flow the operator creates the first Chatwoot admin **in the browser** at `CHATWOOT_URL/app`.
64
+ The agent never creates the account or the admin; it waits for the operator, then reads the admin token
65
+ (`scripts/chatwoot-admin.py provision`, Rails runner read-only) to connect the deployment.
@@ -0,0 +1,136 @@
1
+ # Chatwoot for fazer.ai: COOLIFY flavor (magic vars; Coolify auto-generates the SERVICE_* secrets).
2
+ # Edition by CHATWOOT_IMAGE: default = Pro (private Harbor image); for OSS set
3
+ # CHATWOOT_IMAGE=ghcr.io/fazer-ai/chatwoot:latest (our public fork) AND drop the baileys-api service.
4
+ # Pro pull needs the Harbor registry credential configured in Coolify (hub: per-user credential); OSS needs none.
5
+ # Set the service Domain in Coolify to your Chatwoot FQDN (fills SERVICE_URL_CHATWOOT).
6
+ # This is the compose validated in the RUN 2 Coolify deploy; the generic/Portainer flavor is ./docker-compose.yml.
7
+
8
+ services:
9
+ chatwoot:
10
+ image: '${CHATWOOT_IMAGE:-harbor.fazer.ai/chatwoot/fazer-ai/chatwoot-pro:latest}'
11
+ pull_policy: always
12
+ volumes:
13
+ - 'storage:/app/storage'
14
+ depends_on:
15
+ - postgres
16
+ - redis
17
+ environment:
18
+ - SERVICE_URL_CHATWOOT
19
+ - NODE_ENV=production
20
+ - RAILS_ENV=production
21
+ - INSTALLATION_ENV=docker
22
+ - DEFAULT_LOCALE=pt_BR
23
+ - LOG_LEVEL=${RAILS_LOG_LEVEL:-info}
24
+ - 'FRONTEND_URL=${SERVICE_URL_CHATWOOT}'
25
+ - 'INTERNAL_HOST_URL=http://chatwoot:3000'
26
+ - POSTGRES_HOST=postgres
27
+ - POSTGRES_PORT=5432
28
+ - 'POSTGRES_USERNAME=${SERVICE_USER_POSTGRES}'
29
+ - 'POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES}'
30
+ - 'POSTGRES_DATABASE=${POSTGRES_DB:-chatwoot_production}'
31
+ - 'SECRET_KEY_BASE=${SERVICE_PASSWORD_64_SECRETKEYBASE}'
32
+ - 'REDIS_URL=redis://redis:6379'
33
+ - 'REDIS_PASSWORD=${SERVICE_PASSWORD_REDIS}'
34
+ - 'BAILEYS_PROVIDER_DEFAULT_CLIENT_NAME=${BAILEYS_PROVIDER_DEFAULT_CLIENT_NAME:-Chatwoot}'
35
+ - 'BAILEYS_PROVIDER_DEFAULT_URL=http://baileys-api:3025'
36
+ - 'BAILEYS_PROVIDER_DEFAULT_API_KEY=${SERVICE_PASSWORD_64_DEFAULTAPIKEY}'
37
+ - BAILEYS_PROVIDER_USE_INTERNAL_HOST_URL=true
38
+ - 'MAILER_SENDER_EMAIL=${MAILER_SENDER_EMAIL}'
39
+ - 'RESEND_API_KEY=${RESEND_API_KEY}'
40
+ entrypoint: docker/entrypoints/rails.sh
41
+ command:
42
+ - sh
43
+ - '-c'
44
+ - 'bundle exec rails db:chatwoot_prepare && bundle exec rails branding:update && exec bundle exec rails s -p 3000 -b 0.0.0.0'
45
+ restart: always
46
+ healthcheck:
47
+ test: ['CMD-SHELL', 'wget -qO- --header="Accept: text/html" http://127.0.0.1:3000/']
48
+ interval: 20s
49
+ timeout: 10s
50
+ retries: 5
51
+ sidekiq:
52
+ image: '${CHATWOOT_IMAGE:-harbor.fazer.ai/chatwoot/fazer-ai/chatwoot-pro:latest}'
53
+ pull_policy: always
54
+ volumes:
55
+ - 'storage:/app/storage'
56
+ depends_on:
57
+ - postgres
58
+ - redis
59
+ environment:
60
+ - NODE_ENV=production
61
+ - RAILS_ENV=production
62
+ - INSTALLATION_ENV=docker
63
+ - DEFAULT_LOCALE=pt_BR
64
+ - LOG_LEVEL=${RAILS_LOG_LEVEL:-info}
65
+ - 'FRONTEND_URL=${SERVICE_URL_CHATWOOT}'
66
+ - 'INTERNAL_HOST_URL=http://chatwoot:3000'
67
+ - POSTGRES_HOST=postgres
68
+ - POSTGRES_PORT=5432
69
+ - 'POSTGRES_USERNAME=${SERVICE_USER_POSTGRES}'
70
+ - 'POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES}'
71
+ - 'POSTGRES_DATABASE=${POSTGRES_DB:-chatwoot_production}'
72
+ - 'SECRET_KEY_BASE=${SERVICE_PASSWORD_64_SECRETKEYBASE}'
73
+ - 'REDIS_URL=redis://redis:6379'
74
+ - 'REDIS_PASSWORD=${SERVICE_PASSWORD_REDIS}'
75
+ - 'BAILEYS_PROVIDER_DEFAULT_URL=http://baileys-api:3025'
76
+ - 'BAILEYS_PROVIDER_DEFAULT_API_KEY=${SERVICE_PASSWORD_64_DEFAULTAPIKEY}'
77
+ - BAILEYS_PROVIDER_USE_INTERNAL_HOST_URL=true
78
+ - 'MAILER_SENDER_EMAIL=${MAILER_SENDER_EMAIL}'
79
+ - 'RESEND_API_KEY=${RESEND_API_KEY}'
80
+ command: ['bundle', 'exec', 'sidekiq', '-C', 'config/sidekiq.yml']
81
+ restart: always
82
+ healthcheck:
83
+ test: ['CMD-SHELL', 'ps aux | grep [s]idekiq']
84
+ interval: 20s
85
+ timeout: 10s
86
+ retries: 5
87
+ postgres:
88
+ image: 'ghcr.io/fazer-ai/pgvector:16'
89
+ restart: always
90
+ volumes:
91
+ - 'postgres:/var/lib/postgresql/data'
92
+ environment:
93
+ - 'POSTGRES_DB=${POSTGRES_DB:-chatwoot_production}'
94
+ - 'POSTGRES_USER=${SERVICE_USER_POSTGRES}'
95
+ - 'POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES}'
96
+ healthcheck:
97
+ test: ['CMD-SHELL', 'pg_isready -h localhost -p 5432 -U $${POSTGRES_USER} -d $${POSTGRES_DB}']
98
+ interval: 20s
99
+ timeout: 20s
100
+ retries: 10
101
+ redis:
102
+ image: 'redis:alpine'
103
+ restart: always
104
+ command: ['sh', '-c', 'redis-server --requirepass "${SERVICE_PASSWORD_REDIS}"']
105
+ volumes:
106
+ - 'redis:/data'
107
+ environment:
108
+ - 'REDIS_PASSWORD=${SERVICE_PASSWORD_REDIS}'
109
+ healthcheck:
110
+ test: ['CMD-SHELL', 'redis-cli -h localhost -p 6379 -a $${REDIS_PASSWORD} ping']
111
+ interval: 20s
112
+ timeout: 20s
113
+ retries: 10
114
+ baileys-api:
115
+ image: 'ghcr.io/fazer-ai/baileys-api:latest'
116
+ pull_policy: always
117
+ volumes:
118
+ - 'storage:/app/storage'
119
+ environment:
120
+ - NODE_ENV=production
121
+ - 'REDIS_URL=redis://redis:6379'
122
+ - 'REDIS_PASSWORD=${SERVICE_PASSWORD_REDIS}'
123
+ - 'LOG_LEVEL=${BAILEYS_API_LOG_LEVEL:-info}'
124
+ - 'BAILEYS_PROVIDER_DEFAULT_API_KEY=${SERVICE_PASSWORD_64_DEFAULTAPIKEY}'
125
+ command: ['sh', '-c', 'bun manage-api-keys create user ${SERVICE_PASSWORD_64_DEFAULTAPIKEY} && bun start']
126
+ restart: always
127
+ healthcheck:
128
+ test: ['CMD-SHELL', 'wget -qO- http://localhost:3025/status']
129
+ interval: 20s
130
+ timeout: 20s
131
+ retries: 10
132
+
133
+ volumes:
134
+ storage:
135
+ postgres:
136
+ redis: