wavebrain 0.0.0-stage → 0.2.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/README.md +48 -2
- package/assets/SKILL.md +27 -0
- package/assets/hermes/__init__.py +282 -0
- package/assets/hermes/plugin.yaml +8 -0
- package/assets/hermes/rules.json +31 -0
- package/assets/hooks/core.mjs +320 -0
- package/assets/hooks/hook.mjs +28 -0
- package/assets/hooks/rules.json +31 -0
- package/bin/wavebrain.mjs +10 -0
- package/package.json +35 -4
- package/src/capture.mjs +188 -0
- package/src/cli.mjs +353 -0
- package/src/files.mjs +292 -0
- package/src/state.mjs +36 -0
- package/src/targets.mjs +438 -0
package/README.md
CHANGED
|
@@ -1,3 +1,49 @@
|
|
|
1
|
-
#
|
|
1
|
+
# wavebrain
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Connect your AI agents to [WaveBrain](https://getwavebrain.com), the audit log of what your agents do.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npx wavebrain init
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
`init` finds the agents installed on this machine, lets you pick which ones to connect (nothing is
|
|
10
|
+
selected unless you choose it), asks for an API key (or opens
|
|
11
|
+
[getwavebrain.com/dashboard/setup](https://getwavebrain.com/dashboard/setup) to create one), and adds
|
|
12
|
+
the WaveBrain MCP server to each agent's own config:
|
|
13
|
+
|
|
14
|
+
| Agent | File | Key stored |
|
|
15
|
+
| --- | --- | --- |
|
|
16
|
+
| Claude Code | `~/.claude.json` (+ optional `mcp__wavebrain__*` in `~/.claude/settings.json`) | in the server's header |
|
|
17
|
+
| Codex CLI | `~/.codex/config.toml` | header, or `bearer_token_env_var` when you run with `WAVEBRAIN_API_KEY` set |
|
|
18
|
+
| Cursor | `~/.cursor/mcp.json` | header |
|
|
19
|
+
| Windsurf | `~/.codeium/windsurf/mcp_config.json` | header |
|
|
20
|
+
| Claude Desktop | `claude_desktop_config.json` (via `mcp-remote`) | `env` of the server entry |
|
|
21
|
+
| VS Code (Copilot) | user-profile `mcp.json` | header |
|
|
22
|
+
| Hermes Agent | `~/.hermes/config.yaml` and each profile's | `WAVEBRAIN_API_KEY` in the profile's `.env`; config.yaml only references it |
|
|
23
|
+
|
|
24
|
+
For Claude Code, Codex, Cursor and Hermes, `init` also installs **auto-capture**: hooks (a Hermes
|
|
25
|
+
plugin for Hermes) that record deploys, pushes, releases, payments, emails and database writes even
|
|
26
|
+
when the model forgets to call `log_action`, plus a short skill telling it when to log. Actions are
|
|
27
|
+
queued locally in `~/.wavebrain/queue`, sent at the end of each turn, and skipped if the agent logged
|
|
28
|
+
them itself. Secrets are redacted first. Use `--no-hooks` for the MCP server only, or set
|
|
29
|
+
`WAVEBRAIN_HOOKS=off` to pause capture.
|
|
30
|
+
|
|
31
|
+
Every file is copied to `<file>.bak-wavebrain` before it's changed, re-running is safe, and the full
|
|
32
|
+
key is never printed. Servers named `wavebrain` that you added yourself are left alone.
|
|
33
|
+
|
|
34
|
+
## Commands
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
npx wavebrain init # interactive
|
|
38
|
+
npx wavebrain init --agent cursor --agent hermes --key al_… # non-interactive
|
|
39
|
+
WAVEBRAIN_API_KEY=al_… npx wavebrain init --agent codex
|
|
40
|
+
npx wavebrain init --dry-run # show what would change
|
|
41
|
+
npx wavebrain status # which agents are configured
|
|
42
|
+
npx wavebrain remove # undo exactly what init added
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Agent names: `claude-code`, `codex`, `cursor`, `windsurf`, `claude-desktop`, `vscode`, `hermes`,
|
|
46
|
+
`hermes:<profile>`. Other options: `--no-hooks`, `--no-claude-permission`, `--yes`, `--url <mcp url>`.
|
|
47
|
+
|
|
48
|
+
What init added is recorded (file paths only, no keys) in `~/.wavebrain/installed.json`, so `remove`
|
|
49
|
+
can undo it without touching anything else.
|
package/assets/SKILL.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wavebrain
|
|
3
|
+
description: Use after any action that changes something outside this conversation (deploys, pushes, merges, releases, published posts, emails or messages sent, payments, database or config changes). Records it in WaveBrain with log_action.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Log your work to WaveBrain
|
|
7
|
+
|
|
8
|
+
WaveBrain is the user's audit log of what their agents did. Keep it complete and readable.
|
|
9
|
+
|
|
10
|
+
**After every action that changes something outside the conversation, call the WaveBrain `log_action` tool once.** That includes:
|
|
11
|
+
- deploying, releasing, publishing a package or app build
|
|
12
|
+
- git push, opening or merging a pull request
|
|
13
|
+
- sending an email, message or post; publishing content
|
|
14
|
+
- payments, refunds, subscriptions, invoices
|
|
15
|
+
- database writes or migrations; changing env vars, secrets or config in a live system
|
|
16
|
+
- creating, deleting or changing anything in a third-party service
|
|
17
|
+
|
|
18
|
+
Don't log reading, searching, planning, or local edits that weren't shipped anywhere.
|
|
19
|
+
|
|
20
|
+
How to write the entry:
|
|
21
|
+
- `title`: one short past-tense sentence a teammate would understand, e.g. "Deployed the new pricing page to production".
|
|
22
|
+
- `details`: what changed, why, and the result, including anything that failed or is unfinished. Don't repeat the title.
|
|
23
|
+
- `status`: `failure` if it didn't work, `pending` if it's waiting on someone.
|
|
24
|
+
- Never include secrets, passwords, one-time codes, API keys or full card/account numbers.
|
|
25
|
+
- For a multi-step task, call `start_session` first, pass its `session_id` to each `log_action`, and call `end_session` when done.
|
|
26
|
+
|
|
27
|
+
A WaveBrain hook also records high-stakes commands automatically as a safety net. When you log the action yourself, your entry replaces the automatic one, so a clear `log_action` is always better.
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"""WaveBrain for Hermes Agent.
|
|
2
|
+
|
|
3
|
+
Records the agent's high-stakes actions (deploys, pushes, publishes, payments, emails,
|
|
4
|
+
database writes) to WaveBrain, even when the model forgets to call log_action, and adds
|
|
5
|
+
a short standing instruction so it usually does.
|
|
6
|
+
|
|
7
|
+
Same rules (rules.json), queue format (~/.wavebrain/queue/*.jsonl) and REST endpoint as
|
|
8
|
+
the Claude Code, Codex and Cursor hooks.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import re
|
|
16
|
+
import threading
|
|
17
|
+
import time
|
|
18
|
+
import urllib.error
|
|
19
|
+
import urllib.request
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
HERE = Path(__file__).resolve().parent
|
|
23
|
+
DEFAULT_API_URL = "https://getwavebrain.com/api/v1/actions"
|
|
24
|
+
MAX_AGE = 7 * 24 * 3600
|
|
25
|
+
STALE = 10 * 60
|
|
26
|
+
MAX_PER_FLUSH = 25
|
|
27
|
+
SHELL_TOOLS = {"terminal", "bash", "shell", "execute_command", "run_terminal_cmd"}
|
|
28
|
+
|
|
29
|
+
INSTRUCTION = (
|
|
30
|
+
"WaveBrain is the user's audit log of what agents did. After every action that changes "
|
|
31
|
+
"something outside this conversation (deploys, git push, merges, releases, published posts, "
|
|
32
|
+
"emails or messages sent, payments, database or live config changes), call the WaveBrain "
|
|
33
|
+
"log_action tool once with a short past-tense title and details of what changed and the result. "
|
|
34
|
+
"Don't log reads, searches or plans. Never include secrets or codes. A hook records high-stakes "
|
|
35
|
+
"commands automatically as a safety net; your own log_action entry replaces it."
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _home() -> Path:
|
|
40
|
+
return Path(os.environ.get("WAVEBRAIN_HOME") or Path.home() / ".wavebrain")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _config() -> dict:
|
|
44
|
+
cfg = {}
|
|
45
|
+
try:
|
|
46
|
+
cfg = json.loads((_home() / "config.json").read_text())
|
|
47
|
+
except Exception:
|
|
48
|
+
pass
|
|
49
|
+
return {
|
|
50
|
+
"apiKey": os.environ.get("WAVEBRAIN_API_KEY") or cfg.get("apiKey"),
|
|
51
|
+
"apiUrl": os.environ.get("WAVEBRAIN_API_URL") or cfg.get("apiUrl") or DEFAULT_API_URL,
|
|
52
|
+
"enabled": os.environ.get("WAVEBRAIN_HOOKS") != "off" and cfg.get("autoCapture") is not False,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _js_regex(p: str) -> str:
|
|
57
|
+
return p # rules.json patterns use the common subset of JS and Python regex syntax
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
_RULES = json.loads((HERE / "rules.json").read_text())
|
|
61
|
+
_SHELL = [(r, re.compile(_js_regex(r["pattern"]), re.I)) for r in _RULES["shell"]]
|
|
62
|
+
_VERBS = set(_RULES["mcp_write_verbs"])
|
|
63
|
+
_SKIP = set(_RULES["mcp_skip_servers"])
|
|
64
|
+
|
|
65
|
+
_SECRETS = [
|
|
66
|
+
(re.compile(r"\b(sk|rk|pk)_(live|test)_[A-Za-z0-9]{8,}"), "[redacted]"),
|
|
67
|
+
(re.compile(r"\bwhsec_[A-Za-z0-9]{8,}"), "[redacted]"),
|
|
68
|
+
(re.compile(r"\b(ghp|gho|ghu|ghs|ghr|github_pat)_[A-Za-z0-9_]{16,}"), "[redacted]"),
|
|
69
|
+
(re.compile(r"\bnpm_[A-Za-z0-9]{20,}"), "[redacted]"),
|
|
70
|
+
(re.compile(r"\bal_[A-Za-z0-9_-]{16,}"), "[redacted]"),
|
|
71
|
+
(re.compile(r"\bxox[abprs]-[A-Za-z0-9-]{10,}"), "[redacted]"),
|
|
72
|
+
(re.compile(r"\bAKIA[0-9A-Z]{16}\b"), "[redacted]"),
|
|
73
|
+
(re.compile(r"\bAIza[0-9A-Za-z_-]{30,}"), "[redacted]"),
|
|
74
|
+
(re.compile(r"\b(sk-(proj-|ant-)?)[A-Za-z0-9_-]{16,}"), "[redacted]"),
|
|
75
|
+
(re.compile(r"\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}"), "[redacted]"),
|
|
76
|
+
(re.compile(r"(Bearer|Basic|token)\s+[A-Za-z0-9._~+/=-]{12,}", re.I), r"\1 [redacted]"),
|
|
77
|
+
(re.compile(r"\b([a-z][a-z0-9+.-]*://)[^\s:@/]+:[^\s@/]+@", re.I), r"\1[redacted]@"),
|
|
78
|
+
(re.compile(r"((?:password|passwd|secret|token|api[_-]?key|apikey|auth|access[_-]?key|private[_-]?key)\s*[=:]\s*)(\"[^\"]*\"|'[^']*'|\S+)", re.I), r"\1[redacted]"),
|
|
79
|
+
(re.compile(r"(--(?:password|token|api-key|secret|auth)[= ])(\"[^\"]*\"|'[^']*'|\S+)", re.I), r"\1[redacted]"),
|
|
80
|
+
(re.compile(r"\b[A-Za-z0-9+/_-]{40,}={0,2}"), "[redacted]"),
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def redact(text) -> str:
|
|
85
|
+
s = str(text or "")
|
|
86
|
+
for rx, rep in _SECRETS:
|
|
87
|
+
s = rx.sub(rep, s)
|
|
88
|
+
return s
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _short(s: str, n: int = 140) -> str:
|
|
92
|
+
one = re.sub(r"\s+", " ", str(s)).strip()
|
|
93
|
+
return one if len(one) <= n else one[: n - 1] + "…"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _mcp_parts(name: str):
|
|
97
|
+
m = re.match(r"^mcp__(.+?)__(.+)$", name) or re.match(r"^mcp_([a-z0-9-]+)_(.+)$", name, re.I)
|
|
98
|
+
return (m.group(1), m.group(2)) if m else None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def classify(tool_name: str, args: dict, cwd: str | None = None, failed: bool = False):
|
|
102
|
+
args = args or {}
|
|
103
|
+
where = f" in {os.path.basename(cwd)}" if cwd else ""
|
|
104
|
+
outcome = " It failed." if failed else ""
|
|
105
|
+
if tool_name.lower() in SHELL_TOOLS:
|
|
106
|
+
cmd = args.get("command") or args.get("cmd") or ""
|
|
107
|
+
if isinstance(cmd, list):
|
|
108
|
+
cmd = " ".join(cmd)
|
|
109
|
+
cmd = str(cmd).strip()
|
|
110
|
+
if not cmd:
|
|
111
|
+
return None
|
|
112
|
+
for r, rx in _SHELL:
|
|
113
|
+
m = rx.search(cmd)
|
|
114
|
+
if not m:
|
|
115
|
+
continue
|
|
116
|
+
frm = m.start() + len(m.group(1) or "")
|
|
117
|
+
start = max([0] + [cmd.rfind(d, 0, frm) + len(d) for d in ("&&", ";", "|") if cmd.rfind(d, 0, frm) >= 0])
|
|
118
|
+
ends = [i for i in (cmd.find(d, m.end()) for d in ("&&", ";", "|")) if i >= 0]
|
|
119
|
+
seg = cmd[start : min(ends) if ends else len(cmd)].strip()
|
|
120
|
+
return {
|
|
121
|
+
"type": r["type"],
|
|
122
|
+
"title": _short(r["title"].replace("{cmd}", _short(redact(seg), 120)), 200),
|
|
123
|
+
"details": f"Ran `{_short(redact(cmd), 1500)}`{where}.{outcome} (Recorded automatically by the WaveBrain hook.)",
|
|
124
|
+
"rule": r["id"],
|
|
125
|
+
}
|
|
126
|
+
return None
|
|
127
|
+
parts = _mcp_parts(tool_name)
|
|
128
|
+
if parts:
|
|
129
|
+
server, tool = parts
|
|
130
|
+
if server in _SKIP:
|
|
131
|
+
return None
|
|
132
|
+
if not set(re.split(r"[_\-.]+", tool.lower())) & _VERBS:
|
|
133
|
+
return None
|
|
134
|
+
return {
|
|
135
|
+
"type": "api_call",
|
|
136
|
+
"title": _short(f"Used {server}: {re.sub(r'[_-]+', ' ', tool)}", 200),
|
|
137
|
+
"details": f"Called the MCP tool {server}/{tool}{where} with {redact(json.dumps(args, default=str))[:1500]}.{outcome} (Recorded automatically by the WaveBrain hook.)",
|
|
138
|
+
"rule": "mcp-write",
|
|
139
|
+
}
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _queue(session: str) -> Path:
|
|
144
|
+
safe = re.sub(r"[^A-Za-z0-9._-]", "_", session or "default")[:80]
|
|
145
|
+
return _home() / "queue" / f"hermes-{safe}.jsonl"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
_lock = threading.Lock()
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _append(path: Path, item: dict):
|
|
152
|
+
with _lock:
|
|
153
|
+
path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
154
|
+
with open(path, "a") as f:
|
|
155
|
+
f.write(json.dumps(item) + "\n")
|
|
156
|
+
os.chmod(path, 0o600)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _read(path: Path) -> list:
|
|
160
|
+
try:
|
|
161
|
+
out = []
|
|
162
|
+
for line in path.read_text().splitlines():
|
|
163
|
+
try:
|
|
164
|
+
out.append(json.loads(line))
|
|
165
|
+
except Exception:
|
|
166
|
+
pass
|
|
167
|
+
return out
|
|
168
|
+
except Exception:
|
|
169
|
+
return []
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _rewrite(path: Path, items: list):
|
|
173
|
+
if not items:
|
|
174
|
+
path.unlink(missing_ok=True)
|
|
175
|
+
return
|
|
176
|
+
tmp = path.with_suffix(f".tmp-{os.getpid()}")
|
|
177
|
+
tmp.write_text("".join(json.dumps(i) + "\n" for i in items))
|
|
178
|
+
os.chmod(tmp, 0o600)
|
|
179
|
+
tmp.replace(path)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def flush(session: str) -> dict:
|
|
183
|
+
cfg = _config()
|
|
184
|
+
path = _queue(session)
|
|
185
|
+
with _lock:
|
|
186
|
+
items = _read(path)
|
|
187
|
+
if not items:
|
|
188
|
+
return {"sent": 0, "skipped": 0, "kept": 0}
|
|
189
|
+
now = time.time() * 1000
|
|
190
|
+
last_log = max([i["at"] for i in items if i.get("kind") == "agent_logged"], default=float("-inf"))
|
|
191
|
+
actions, seen = [], set()
|
|
192
|
+
for i in items:
|
|
193
|
+
if i.get("kind") != "action" or i["at"] <= last_log or now - i["at"] >= MAX_AGE * 1000:
|
|
194
|
+
continue
|
|
195
|
+
k = f"{i['payload'].get('title')}|{round(i['at'] / 5000)}"
|
|
196
|
+
if k not in seen:
|
|
197
|
+
seen.add(k)
|
|
198
|
+
actions.append(i)
|
|
199
|
+
skipped = sum(1 for i in items if i.get("kind") == "action") - len(actions)
|
|
200
|
+
if not (cfg["apiKey"] and cfg["enabled"]):
|
|
201
|
+
_rewrite(path, actions)
|
|
202
|
+
return {"sent": 0, "skipped": skipped, "kept": len(actions)}
|
|
203
|
+
keep, sent = [], 0
|
|
204
|
+
for n, item in enumerate(actions):
|
|
205
|
+
if n >= MAX_PER_FLUSH:
|
|
206
|
+
keep.append(item)
|
|
207
|
+
continue
|
|
208
|
+
req = urllib.request.Request(
|
|
209
|
+
cfg["apiUrl"],
|
|
210
|
+
data=json.dumps(item["payload"]).encode(),
|
|
211
|
+
headers={"content-type": "application/json", "authorization": f"Bearer {cfg['apiKey']}"},
|
|
212
|
+
method="POST",
|
|
213
|
+
)
|
|
214
|
+
try:
|
|
215
|
+
with urllib.request.urlopen(req, timeout=5) as r:
|
|
216
|
+
if 200 <= r.status < 300:
|
|
217
|
+
sent += 1
|
|
218
|
+
except urllib.error.HTTPError as e:
|
|
219
|
+
if e.code not in (400, 401, 402):
|
|
220
|
+
keep.append(item)
|
|
221
|
+
except Exception:
|
|
222
|
+
keep.append(item)
|
|
223
|
+
_rewrite(path, keep)
|
|
224
|
+
return {"sent": sent, "skipped": skipped, "kept": len(keep)}
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _flush_bg(session: str):
|
|
228
|
+
threading.Thread(target=flush, args=(session,), daemon=True).start()
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def on_post_tool_call(tool_name=None, args=None, result=None, session_id=None, status=None, error_message=None, **kwargs):
|
|
232
|
+
try:
|
|
233
|
+
cfg = _config()
|
|
234
|
+
if not cfg["enabled"]:
|
|
235
|
+
return
|
|
236
|
+
name = str(tool_name or "")
|
|
237
|
+
args = args if isinstance(args, dict) else {}
|
|
238
|
+
session = str(session_id or kwargs.get("task_id") or "default")
|
|
239
|
+
now = time.time() * 1000
|
|
240
|
+
if re.search(r"wavebrain.*log_action$", name, re.I):
|
|
241
|
+
_append(_queue(session), {"kind": "agent_logged", "at": now})
|
|
242
|
+
return
|
|
243
|
+
failed = status not in (None, "success", "ok") or bool(error_message)
|
|
244
|
+
if not failed and isinstance(result, str):
|
|
245
|
+
m = re.search(r'"exit_code"\s*:\s*(-?\d+)', result)
|
|
246
|
+
failed = bool(m and m.group(1) != "0")
|
|
247
|
+
action = classify(name, args, args.get("workdir") or os.getcwd(), failed)
|
|
248
|
+
if not action:
|
|
249
|
+
return
|
|
250
|
+
profile = os.environ.get("HERMES_PROFILE") or Path(os.environ.get("HERMES_HOME", "")).name or "hermes"
|
|
251
|
+
agent = "hermes" if profile in ("", ".hermes", "hermes") else f"hermes-{profile}"
|
|
252
|
+
cwd = args.get("workdir")
|
|
253
|
+
payload = {
|
|
254
|
+
"agent": agent[:64],
|
|
255
|
+
"type": action["type"],
|
|
256
|
+
"title": action["title"],
|
|
257
|
+
"details": action["details"],
|
|
258
|
+
"status": "failure" if failed else "success",
|
|
259
|
+
"metadata": {"source": "wavebrain-hook", "host": "hermes", "rule": action["rule"]},
|
|
260
|
+
}
|
|
261
|
+
if cwd:
|
|
262
|
+
payload["project"] = os.path.basename(str(cwd))[:200]
|
|
263
|
+
path = _queue(session)
|
|
264
|
+
_append(path, {"kind": "action", "at": now, "payload": payload})
|
|
265
|
+
if any(i.get("kind") == "action" and now - i["at"] > STALE * 1000 for i in _read(path)):
|
|
266
|
+
_flush_bg(session)
|
|
267
|
+
except Exception:
|
|
268
|
+
pass
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def on_session_end(session_id=None, **kwargs):
|
|
272
|
+
try:
|
|
273
|
+
_flush_bg(str(session_id or kwargs.get("task_id") or "default"))
|
|
274
|
+
except Exception:
|
|
275
|
+
pass
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def register(ctx):
|
|
279
|
+
ctx.register_hook("post_tool_call", on_post_tool_call)
|
|
280
|
+
ctx.register_hook("on_session_end", on_session_end)
|
|
281
|
+
if hasattr(ctx, "register_system_prompt_section"):
|
|
282
|
+
ctx.register_system_prompt_section("wavebrain.logging", INSTRUCTION, position="after_memory", max_chars=1500)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
name: wavebrain
|
|
2
|
+
version: 0.2.0
|
|
3
|
+
description: "WaveBrain: records your agent's deploys, pushes, releases, emails, payments and other real-world actions to its audit log, even when the model forgets to call log_action."
|
|
4
|
+
author: Tambo Labs
|
|
5
|
+
homepage: https://getwavebrain.com
|
|
6
|
+
hooks:
|
|
7
|
+
- post_tool_call
|
|
8
|
+
- on_session_end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "Shell commands that are recorded automatically. Each rule: regex (case-insensitive, matched against the whole command), WaveBrain action type, and a title template ({cmd} = first matching command segment). Shared by every plugin; edit here, then run `node scripts/sync.mjs`.",
|
|
3
|
+
"version": 1,
|
|
4
|
+
"shell": [
|
|
5
|
+
{ "id": "git-push", "pattern": "(^|[\\s;&|(])git\\s+push\\b", "type": "push", "title": "Pushed to git: {cmd}" },
|
|
6
|
+
{ "id": "git-commit", "pattern": "(^|[\\s;&|(])git\\s+commit\\b", "type": "commit", "title": "Made a git commit: {cmd}" },
|
|
7
|
+
{ "id": "gh-pr-create", "pattern": "(^|[\\s;&|(])gh\\s+pr\\s+create\\b", "type": "pull_request", "title": "Opened a pull request: {cmd}" },
|
|
8
|
+
{ "id": "gh-pr-merge", "pattern": "(^|[\\s;&|(])gh\\s+pr\\s+merge\\b", "type": "merge", "title": "Merged a pull request: {cmd}" },
|
|
9
|
+
{ "id": "gh-release", "pattern": "(^|[\\s;&|(])gh\\s+release\\s+create\\b", "type": "release", "title": "Created a GitHub release: {cmd}" },
|
|
10
|
+
{ "id": "vercel-deploy", "pattern": "(^|[\\s;&|(])(npx\\s+)?vercel\\b[^;&|]*(\\s--prod\\b|\\sdeploy\\b|\\sredeploy\\b|\\spromote\\b)", "type": "deploy", "title": "Deployed with Vercel: {cmd}" },
|
|
11
|
+
{ "id": "vercel-env", "pattern": "(^|[\\s;&|(])(npx\\s+)?vercel\\s+env\\s+(add|rm|remove|update)\\b", "type": "file_change", "title": "Changed Vercel environment variables: {cmd}" },
|
|
12
|
+
{ "id": "railway", "pattern": "(^|[\\s;&|(])railway\\s+(up|deploy|redeploy|variables\\s+--set|variable\\s+set)\\b", "type": "deploy", "title": "Deployed with Railway: {cmd}" },
|
|
13
|
+
{ "id": "fly", "pattern": "(^|[\\s;&|(])(fly|flyctl)\\s+(deploy|secrets\\s+set)\\b", "type": "deploy", "title": "Deployed with Fly.io: {cmd}" },
|
|
14
|
+
{ "id": "wrangler", "pattern": "(^|[\\s;&|(])(npx\\s+)?wrangler\\s+(deploy|publish|secret\\s+put)\\b", "type": "deploy", "title": "Deployed with Cloudflare Wrangler: {cmd}" },
|
|
15
|
+
{ "id": "netlify", "pattern": "(^|[\\s;&|(])netlify\\s+deploy\\b", "type": "deploy", "title": "Deployed with Netlify: {cmd}" },
|
|
16
|
+
{ "id": "kubectl", "pattern": "(^|[\\s;&|(])kubectl\\s+(apply|rollout|delete|scale|set)\\b", "type": "deploy", "title": "Changed Kubernetes: {cmd}" },
|
|
17
|
+
{ "id": "terraform", "pattern": "(^|[\\s;&|(])(terraform|tofu)\\s+(apply|destroy)\\b", "type": "deploy", "title": "Applied infrastructure changes: {cmd}" },
|
|
18
|
+
{ "id": "docker-push", "pattern": "(^|[\\s;&|(])docker\\s+(push|buildx\\s+build[^;&|]*--push)\\b", "type": "publish", "title": "Pushed a Docker image: {cmd}" },
|
|
19
|
+
{ "id": "npm-publish", "pattern": "(^|[\\s;&|(])(npm|pnpm|yarn|bun)\\s+publish\\b", "type": "publish", "title": "Published a package: {cmd}" },
|
|
20
|
+
{ "id": "eas", "pattern": "(^|[\\s;&|(])(npx\\s+)?eas\\s+(submit|update|build)\\b", "type": "release", "title": "Shipped a mobile build with EAS: {cmd}" },
|
|
21
|
+
{ "id": "xcrun-altool", "pattern": "(^|[\\s;&|(])xcrun\\s+altool\\b[^;&|]*--upload|(^|[\\s;&|(])fastlane\\s+(deliver|pilot|upload_to|release)", "type": "release", "title": "Uploaded an app build: {cmd}" },
|
|
22
|
+
{ "id": "stripe", "pattern": "(^|[\\s;&|(])stripe\\s+(products|prices|customers|subscriptions|refunds|payment_intents|webhook_endpoints)\\s+(create|update|delete|cancel)\\b", "type": "api_call", "title": "Changed Stripe: {cmd}" },
|
|
23
|
+
{ "id": "db-migrate", "pattern": "(^|[\\s;&|(])((npx\\s+)?(prisma\\s+migrate\\s+deploy|drizzle-kit\\s+(migrate|push))|(pnpm|npm|yarn)\\s+(run\\s+)?db:(migrate|push))\\b", "type": "database", "title": "Ran database migrations: {cmd}" },
|
|
24
|
+
{ "id": "psql-write", "pattern": "(^|[\\s;&|(])psql\\b[^;&|]*\\b(insert\\s+into|update\\s+\\w+\\s+set|delete\\s+from|drop\\s+table|alter\\s+table|truncate)\\b", "type": "database", "title": "Changed a database: {cmd}" },
|
|
25
|
+
{ "id": "curl-write", "pattern": "(^|[\\s;&|(])curl\\b[^;&|]*\\s(-X|--request)\\s*(POST|PUT|PATCH|DELETE)\\b", "type": "api_call", "title": "Called an API: {cmd}" },
|
|
26
|
+
{ "id": "send-mail", "pattern": "(^|[\\s;&|(])(sendmail|mail|mutt|himalaya\\s+(send|message\\s+send))\\b", "type": "email", "title": "Sent an email: {cmd}" }
|
|
27
|
+
],
|
|
28
|
+
"$comment_mcp": "MCP tool calls are recorded when the tool name matches one of these (after the mcp__<server>__ prefix). Read-only tools (list, get, search, read, fetch) are skipped.",
|
|
29
|
+
"mcp_write_verbs": ["create", "update", "delete", "remove", "send", "post", "publish", "deploy", "merge", "push", "submit", "cancel", "refund", "charge", "invite", "set", "add", "upload", "write", "reply", "comment", "approve", "close", "archive"],
|
|
30
|
+
"mcp_skip_servers": ["wavebrain", "context-mode", "plugin_context-mode_context-mode", "claude-in-chrome", "honcho"]
|
|
31
|
+
}
|