wavebrain-cursor 0.0.0-stage → 0.3.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 +19 -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/cli.mjs +10 -0
- package/lib/capture.mjs +252 -0
- package/lib/cli.mjs +356 -0
- package/lib/files.mjs +292 -0
- package/lib/harness.mjs +149 -0
- package/lib/state.mjs +36 -0
- package/lib/targets.mjs +445 -0
- package/package.json +30 -4
package/README.md
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
-
#
|
|
1
|
+
# wavebrain-cursor
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Connect Cursor to [WaveBrain](https://getwavebrain.com), the audit log of what your AI agents do.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npx wavebrain-cursor install
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
It asks for an API key (or opens [getwavebrain.com/dashboard/setup](https://getwavebrain.com/dashboard/setup) to create one; `--key` or `WAVEBRAIN_API_KEY` skip the prompt).
|
|
10
|
+
|
|
11
|
+
Adds the `wavebrain` MCP server to `~/.cursor/mcp.json` and the hooks (`postToolUse`, `stop`) to `~/.cursor/hooks.json`.
|
|
12
|
+
|
|
13
|
+
**Auto-capture:** the hooks record deploys, pushes, releases, publishes, payments, emails and database writes even when the model forgets to call `log_action`. They queue locally in `~/.wavebrain/queue`, send at the end of each turn, and skip anything the agent logged itself. Secrets are redacted first. `--no-hooks` installs the MCP server only; `WAVEBRAIN_HOOKS=off` pauses capture.
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npx wavebrain-cursor status
|
|
17
|
+
npx wavebrain-cursor uninstall # undoes exactly what install added
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Every changed file is backed up to `<file>.bak-wavebrain`. `--dry-run` shows the changes without writing.
|
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.3.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
|
+
}
|