@trygocode/notify 0.1.2 → 0.1.3
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 +178 -104
- package/dist/src/claude.js +27 -14
- package/dist/src/cli.js +52 -0
- package/dist/src/cursor.js +25 -12
- package/dist/src/mcp.js +1 -38
- package/dist/src/on_stop.js +132 -0
- package/dist/src/opencode.js +292 -0
- package/dist/src/push.js +69 -1
- package/dist/src/setup.js +12 -7
- package/dist/src/uninstall.js +2 -0
- package/dist/src/version.js +1 -1
- package/package.json +3 -21
- package/snippets/ralph-homer.sh +3 -3
- package/CHANGELOG.md +0 -59
- package/LICENSE +0 -21
- package/assets/README.md +0 -13
- package/assets/banner.png +0 -0
- package/assets/icon-128.png +0 -0
- package/assets/icon-512.png +0 -0
- package/assets/icon.svg +0 -8
package/README.md
CHANGED
|
@@ -1,76 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
<img src="assets/icon-512.png" alt="GoCode Notify" width="104" height="104" />
|
|
3
|
-
</p>
|
|
1
|
+
# @trygocode/notify
|
|
4
2
|
|
|
5
|
-
|
|
3
|
+
Free phone notifications for any coding agent — **Cursor, Claude Code, OpenCode,
|
|
4
|
+
or any Ralph/Homer loop** — delivered to your phone via the
|
|
5
|
+
[GoCode](https://oh.jeltechsolutions.com) app + FCM.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
</p>
|
|
11
|
-
|
|
12
|
-
<p align="center">
|
|
13
|
-
<a href="https://www.npmjs.com/package/@trygocode/notify"><img alt="npm" src="https://img.shields.io/npm/v/@trygocode/notify?color=5EE6A8&label=npm"></a>
|
|
14
|
-
<a href="LICENSE"><img alt="license" src="https://img.shields.io/badge/license-MIT-blue.svg"></a>
|
|
15
|
-
<img alt="price" src="https://img.shields.io/badge/price-free-5EE6A8">
|
|
16
|
-
<img alt="node" src="https://img.shields.io/badge/node-%3E%3D18-339933?logo=node.js&logoColor=white">
|
|
17
|
-
<img alt="works with" src="https://img.shields.io/badge/works%20with-Cursor%20%C2%B7%20Claude%20Code%20%C2%B7%20OpenCode-111">
|
|
18
|
-
</p>
|
|
19
|
-
|
|
20
|
-
```bash
|
|
21
|
-
npx @trygocode/notify@latest setup
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
<p align="center">
|
|
25
|
-
<img src="assets/banner.png" alt="GoCode Notify" width="640" />
|
|
26
|
-
</p>
|
|
27
|
-
|
|
28
|
-
> **Free.** `@trygocode/notify` and the **[GoCode](https://oh.jeltechsolutions.com)** phone app
|
|
29
|
-
> that receives the pushes are both free to use.
|
|
30
|
-
>
|
|
31
|
-
> **You need the GoCode app.** Notifications are delivered to your phone through the free GoCode
|
|
32
|
-
> app — it's the pairing target and the thing your phone buzzes with. Install it, pair once
|
|
33
|
-
> (a 6-digit code), done. Without it there's nowhere for the pushes to land.
|
|
34
|
-
|
|
35
|
-
---
|
|
36
|
-
|
|
37
|
-
## Why?
|
|
38
|
-
|
|
39
|
-
You kick off a long agent run, then walk away to make coffee, take a call, or
|
|
40
|
-
context-switch to something else. Now you're stuck in the loop of *checking back
|
|
41
|
-
every 30 seconds* to see if it's done — or worse, it finished 20 minutes ago and
|
|
42
|
-
you didn't notice.
|
|
43
|
-
|
|
44
|
-
**`@trygocode/notify` pings your phone the instant your agent finishes a turn, goes
|
|
45
|
-
idle waiting for you, errors out, or an overnight loop completes/halts.** Walk
|
|
46
|
-
away. Your phone tells you when it needs you.
|
|
47
|
-
|
|
48
|
-
- 🆓 **Free.** The package and the GoCode phone app are free. No card, no trial.
|
|
49
|
-
- ⚡ **One command.** `npx @trygocode/notify@latest setup` — no server to host, no config files to hand-edit.
|
|
50
|
-
- 🔒 **Push-only & private.** The paired key can *only* send notifications to your phone. It can't read your chats, code, or settings. Revoke it any time.
|
|
51
|
-
- 🧩 **Auto-detects your tools.** Wires up Cursor, Claude Code, and OpenCode in one go — never clobbering your existing hooks/MCP config.
|
|
52
|
-
- 🪶 **Never blocks your agent.** Every send is fire-and-forget with a hard timeout + an offline queue. A slow push can't slow your work.
|
|
53
|
-
|
|
54
|
-
## Works with
|
|
55
|
-
|
|
56
|
-
| Tool | How it hooks in |
|
|
57
|
-
|---|---|
|
|
58
|
-
| **Cursor** | `stop` hook |
|
|
59
|
-
| **Claude Code** | `Stop` + `Notification` + `SubagentStop` hooks |
|
|
60
|
-
| **OpenCode** | runtime hook |
|
|
61
|
-
| **Ralph Wiggum / Autopilot script loops** | opt-in completion/halt snippet |
|
|
62
|
-
|
|
63
|
-
> **Requires the free [GoCode](https://oh.jeltechsolutions.com) phone app.** It's the one-time
|
|
64
|
-
> pairing target and where every push lands. Install GoCode, pair once, done — both the app and
|
|
65
|
-
> this package are free.
|
|
7
|
+
You get an automatic push when your agent **finishes a turn**, **goes idle
|
|
8
|
+
waiting for you**, **errors out**, or when an overnight **loop completes / halts**
|
|
9
|
+
— so you can walk away and let your phone tell you when it needs you.
|
|
66
10
|
|
|
67
11
|
## Contents
|
|
68
12
|
|
|
69
13
|
- [Install — two equally-supported paths](#install--two-equally-supported-paths)
|
|
70
14
|
- [Pairing — step by step](#pairing--step-by-step)
|
|
71
15
|
- [The three triggers](#the-three-triggers)
|
|
72
|
-
- [
|
|
73
|
-
- [
|
|
16
|
+
- [Auto-push to git (opt-in)](#auto-push-to-git-opt-in)
|
|
17
|
+
- [Settings sync — the `config` command](#settings-sync--the-config-command)
|
|
18
|
+
- [Ralph/Homer opt-in snippet (trigger C)](#ralphhomer-opt-in-snippet-trigger-c)
|
|
74
19
|
- [Troubleshooting](#troubleshooting)
|
|
75
20
|
- [Develop](#develop)
|
|
76
21
|
- [Layout](#layout)
|
|
@@ -93,9 +38,10 @@ This runs the interactive installer: it prompts for the 6-digit pairing code
|
|
|
93
38
|
Claude Code / Cursor / OpenCode. Re-run any time — it's idempotent; pass
|
|
94
39
|
`--force` to re-pair.
|
|
95
40
|
|
|
96
|
-
>
|
|
97
|
-
>
|
|
98
|
-
>
|
|
41
|
+
> Until the package is published to npm, point `npx` at the local tarball
|
|
42
|
+
> (`npm pack` in this directory, then `npx ./gocode-notify-*.tgz setup`) or a
|
|
43
|
+
> path install. The npm publish is a user-owned step — see
|
|
44
|
+
> [`docs/GOCODE_NOTIFY_MANUAL_STEPS.md`](../../docs/GOCODE_NOTIFY_MANUAL_STEPS.md).
|
|
99
45
|
|
|
100
46
|
### Path 2 — paste a prompt into your AI agent and let it install
|
|
101
47
|
|
|
@@ -163,26 +109,168 @@ or staging GoCode server.
|
|
|
163
109
|
|
|
164
110
|
| Trigger | Mechanism | Fires when |
|
|
165
111
|
|---|---|---|
|
|
166
|
-
| **(A) Runtime hook** | Cursor `stop` / Claude Code `Stop`+`Notification`+`SubagentStop` | Agent finishes a turn, goes idle, or errors — **automatic, the killer feature** |
|
|
112
|
+
| **(A) Runtime hook** | Cursor `stop` / Claude Code `Stop`+`Notification`+`SubagentStop` / OpenCode `session.idle` plugin | Agent finishes a turn, goes idle, or errors — **automatic, the killer feature** |
|
|
167
113
|
| **(B) MCP tool** | `gocode_notify` tool the agent calls | You *explicitly* ask "ping me when X is done" mid-task |
|
|
168
|
-
| **(C) Loop shell hook** | one line in your loop's completion/halt path | A Ralph
|
|
114
|
+
| **(C) Loop shell hook** | one line in your loop's completion/halt path | A Ralph/Homer loop reaches `completed` / `halted` |
|
|
169
115
|
|
|
170
116
|
The installed rule/skill tells the agent **not** to call the MCP tool for
|
|
171
117
|
done/idle/error pings — those are owned by the deterministic hook (A), so you
|
|
172
118
|
never get double-pinged.
|
|
173
119
|
|
|
174
|
-
|
|
120
|
+
> **OpenCode's hook (A)** is a small `session.idle` plugin written to
|
|
121
|
+
> `~/.config/opencode/plugin/gocode-notify.js`; on each `session.idle` event it
|
|
122
|
+
> fire-and-forgets `gocode-notify on-stop --source opencode` (the same
|
|
123
|
+
> dispatcher as Cursor/Claude), so OpenCode is a fully-supported runtime with
|
|
124
|
+
> identical behaviour.
|
|
125
|
+
|
|
126
|
+
> **Under the hood (since auto-push landed):** the runtime hook (A) no longer
|
|
127
|
+
> shells straight to `send`. It now calls a single end-of-turn **dispatcher**,
|
|
128
|
+
> `gocode-notify on-stop --source <runtime> || true`, which decides per repo:
|
|
129
|
+
> if auto-push is enabled for this repo it runs the push flow (and sends ONE
|
|
130
|
+
> notification carrying the commit summary); otherwise it sends the ordinary
|
|
131
|
+
> `finished` ping exactly as before. One hook entry, one notification per turn.
|
|
132
|
+
> Existing installs upgrade their hook automatically on the next
|
|
133
|
+
> `setup`/`setup --force`. `send` and `test` remain callable on their own for
|
|
134
|
+
> scripts and power users.
|
|
135
|
+
|
|
136
|
+
## Auto-push to git (opt-in)
|
|
137
|
+
|
|
138
|
+
GoCode Notify can **auto-commit and push your work after every agent turn** —
|
|
139
|
+
with an **AI-written commit message** describing what just changed — so an
|
|
140
|
+
overnight Cursor/Claude/Ralph session lands as a tidy series of pushed commits
|
|
141
|
+
and your phone tells you each time, commit summary included.
|
|
142
|
+
|
|
143
|
+
It is **OFF by default** and deliberately conservative (see the safety posture
|
|
144
|
+
below). The full design lives in
|
|
145
|
+
[`docs/GOCODE_NOTIFY_AUTO_PUSH_AND_SYNC.md`](../../docs/GOCODE_NOTIFY_AUTO_PUSH_AND_SYNC.md).
|
|
146
|
+
|
|
147
|
+
### Enable it
|
|
148
|
+
|
|
149
|
+
From the **GoCode app** (Settings → **GoCode Notify** → *Auto-push after every
|
|
150
|
+
prompt*; a first-enable dialog explains the behaviour), or from the terminal:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
# Turn auto-push on for ALL repos (global default):
|
|
154
|
+
gocode-notify config set auto_push.enabled true
|
|
155
|
+
|
|
156
|
+
# Or scope it to just the current repo:
|
|
157
|
+
gocode-notify config set auto_push.enabled true --repo
|
|
158
|
+
```
|
|
175
159
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
160
|
+
Either editor writes to the server (the single source of truth); the change is
|
|
161
|
+
picked up by every linked machine on its next turn (within the 60s settings
|
|
162
|
+
cache, or immediately after `gocode-notify config pull`).
|
|
163
|
+
|
|
164
|
+
### What happens on each turn (when enabled)
|
|
165
|
+
|
|
166
|
+
The `on-stop` dispatcher runs this best-effort push flow (it **always exits 0**
|
|
167
|
+
so a git hiccup can never block or fail your agent's turn):
|
|
168
|
+
|
|
169
|
+
1. Resolve the merged Notify settings for this repo (cached ~60s).
|
|
170
|
+
2. If `auto_push.enabled` is not true for the repo → no-op (plain ping).
|
|
171
|
+
3. Verify it's a git repo; resolve the target branch (per-repo override →
|
|
172
|
+
global default → current checked-out branch).
|
|
173
|
+
4. **Branch guard:** never push to `main`/`master` unless you have separately
|
|
174
|
+
set `auto_push.allow_protected true`. Protected + not allowed → skip, no push.
|
|
175
|
+
5. `git add -A`; if the tree is clean → exit 0 (no spurious notification).
|
|
176
|
+
6. Compose the commit message (AI, with deterministic fallback — see below).
|
|
177
|
+
7. `git commit` then `git push <remote> <branch>` — **fast-forward only, never
|
|
178
|
+
`--force`.** A non-FF rejection sends an `error` notification ("auto-push
|
|
179
|
+
rejected — pull + retry") and exits 0.
|
|
180
|
+
8. Send ONE `finished` push whose body carries the commit summary, branch, and
|
|
181
|
+
short SHA.
|
|
182
|
+
|
|
183
|
+
### The AI commit message (free) + deterministic fallback
|
|
184
|
+
|
|
185
|
+
The commit subject/body is summarised **using credentials you already have on
|
|
186
|
+
the machine** — so it costs GoCode nothing. Resolution chain (first that works
|
|
187
|
+
wins, hard **8s** cap):
|
|
188
|
+
|
|
189
|
+
1. `commit_message.mode = "deterministic"` → skip AI, use the template below.
|
|
190
|
+
2. Otherwise an explicit `commit_message.command` (e.g. `"claude -p"`,
|
|
191
|
+
`"cursor-agent --print"`, `"ollama run llama3.2"`) — the staged diff is
|
|
192
|
+
piped in on stdin with a fixed prompt; output is sanitised (code fences /
|
|
193
|
+
"Here is…" preambles stripped, subject clamped to ≤72 chars).
|
|
194
|
+
3. Auto-detect from `--source`: `claude` on PATH for Claude Code, `cursor-agent`
|
|
195
|
+
for Cursor, else `ollama` if present.
|
|
196
|
+
4. **Deterministic fallback** (always available — used if the above fail or time
|
|
197
|
+
out): `<type>: <N> file(s) changed on <branch>` + a `git diff --stat` block,
|
|
198
|
+
where `<type>` is inferred from the changed paths (`docs:`/`test:`/`chore:`/
|
|
199
|
+
`feat:`). The push **never** fails because the AI summary was unavailable.
|
|
200
|
+
|
|
201
|
+
### Try it without writing anything
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
# Print exactly what the end-of-turn dispatcher WOULD do (no git writes, no push):
|
|
205
|
+
gocode-notify on-stop --dry-run
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Safety posture (conservative defaults)
|
|
209
|
+
|
|
210
|
+
- **OFF by default**, opt-in, with a first-time confirmation in the app.
|
|
211
|
+
- **Branch guard ON:** `main`/`master` are never auto-pushed unless
|
|
212
|
+
`auto_push.allow_protected` is explicitly true.
|
|
213
|
+
- **No force-push, ever** — plain fast-forward `git push`; non-FF → error
|
|
214
|
+
notification, no rewrite.
|
|
215
|
+
- **Never blocks the agent** — every step is best-effort and exits 0, the same
|
|
216
|
+
contract as `send`.
|
|
217
|
+
|
|
218
|
+
## Settings sync — the `config` command
|
|
219
|
+
|
|
220
|
+
Notify behaviour used to be hard-wired; now it's a small set of **per-user
|
|
221
|
+
settings stored on the server** and editable from either the phone app or the
|
|
222
|
+
terminal. Whatever you change in one place propagates to the other (and to your
|
|
223
|
+
other linked machines).
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
gocode-notify config get [--repo] # print the merged settings (JSON/pretty)
|
|
227
|
+
gocode-notify config set <key> <value> [--repo] # validate, write to server, refresh cache
|
|
228
|
+
gocode-notify config pull # force-refresh the local cache from server
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
- `--repo` scopes a read or write to the **current repo's** per-project
|
|
232
|
+
override (keyed by a stable `repo_key` derived from the `origin` remote)
|
|
233
|
+
instead of your global defaults. Per-repo overrides deep-merge over the global
|
|
234
|
+
blob.
|
|
235
|
+
- Writes go to the server **and** update the local cache so the next stop-hook
|
|
236
|
+
is immediate. Unknown keys are rejected with the list of valid keys.
|
|
237
|
+
|
|
238
|
+
Valid keys (mirror of the canonical schema):
|
|
239
|
+
|
|
240
|
+
| Key | Type | Meaning |
|
|
241
|
+
|---|---|---|
|
|
242
|
+
| `kinds.finished` / `kinds.error` / `kinds.awaiting_input` / `kinds.loop_completed` / `kinds.loop_halted` | bool | Per-kind notification toggles |
|
|
243
|
+
| `min_duration_seconds` | int ≥ 0 | Only notify if the turn ran ≥ N seconds (0 = always) |
|
|
244
|
+
| `quiet_hours.enabled` / `quiet_hours.start` / `quiet_hours.end` / `quiet_hours.tz` | bool / `HH:MM` / `HH:MM` / IANA tz | Do-not-disturb window |
|
|
245
|
+
| `auto_push.enabled` | bool | Master auto-push switch (OFF by default) |
|
|
246
|
+
| `auto_push.default_branch` | string \| null | Global default push branch (null = current branch) |
|
|
247
|
+
| `auto_push.branch` | string \| null | **`--repo` only** — per-repo push branch override |
|
|
248
|
+
| `auto_push.allow_protected` | bool | Permit auto-push to `main`/`master` (default false) |
|
|
249
|
+
| `auto_push.remote` | string | Git remote to push to (default `origin`) |
|
|
250
|
+
| `auto_push.skip_git_hooks` | bool | Pass `--no-verify` to the auto-commit (default false) |
|
|
251
|
+
| `commit_message.mode` | `auto` \| `ai` \| `deterministic` | Commit-message strategy |
|
|
252
|
+
| `commit_message.command` | string \| null | Explicit local summariser command |
|
|
253
|
+
| `commit_message.max_diff_bytes` | int > 0 | Diff byte cap fed to the summariser (default 61440) |
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
# Examples
|
|
257
|
+
gocode-notify config set quiet_hours "23:00-07:00" # shorthand: enable + set window
|
|
258
|
+
gocode-notify config set min_duration_seconds 30
|
|
259
|
+
gocode-notify config set kinds.error false
|
|
260
|
+
gocode-notify config set auto_push.branch dev --repo # push this repo to "dev"
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Ralph/Homer opt-in snippet (trigger C)
|
|
264
|
+
|
|
265
|
+
For power users running a loop **they control** (this repo's `ralph`/`homer`
|
|
266
|
+
skills, a `while :; do … done` one-liner, or any custom driver), drop these two
|
|
267
|
+
lines into the loop's completion/halt path:
|
|
180
268
|
|
|
181
269
|
```bash
|
|
182
270
|
# At loop completion:
|
|
183
|
-
|
|
271
|
+
gocode-notify send --kind loop_completed --source ralph --project "$(basename "$PWD")" || true
|
|
184
272
|
# At loop halt (paused_max_failures / awaiting_human):
|
|
185
|
-
|
|
273
|
+
gocode-notify send --kind loop_halted --source ralph --project "$(basename "$PWD")" \
|
|
186
274
|
--title "Ralph halted — needs you" || true
|
|
187
275
|
```
|
|
188
276
|
|
|
@@ -193,24 +281,6 @@ The ready-to-copy version with comments lives at
|
|
|
193
281
|
loop scripts. Both lines are fire-and-forget (`|| true` + the CLI's 5s
|
|
194
282
|
self-timeout), so a failed or slow push can never block or fail your loop.
|
|
195
283
|
|
|
196
|
-
## GoCode platform settings
|
|
197
|
-
|
|
198
|
-
This package is the *notifier*. The richer automation lives in the free **GoCode**
|
|
199
|
-
phone app (Settings → per-project + global), which uses these notifications as its
|
|
200
|
-
signalling layer. Once paired, the app gives you:
|
|
201
|
-
|
|
202
|
-
| Setting | What it does |
|
|
203
|
-
|---|---|
|
|
204
|
-
| **Notification preferences** | Choose which events ping your phone — done / idle / error, loop completed, loop halted, agent questions, merge-ready — globally or per project. |
|
|
205
|
-
| **Auto-push to git** | Opt-in: after a turn, the agent commits + pushes to a chosen branch so your work is never stranded on one machine and you can pick up anywhere. Off by default. |
|
|
206
|
-
| **Pull-before-push (safe)** | When auto-push is on, a `git pull --rebase --autostash` runs first so remote changes merge cleanly. On a real conflict it aborts safely (nothing lost) and pings your phone. |
|
|
207
|
-
| **AI-Solve conflicts** | If a push hits a conflict, the notification deep-links into the chat with two actions: **Acknowledge** or **AI-Solve** — your agent diagnoses local vs remote and resolves it with no code loss, asking you only when it's unsure. |
|
|
208
|
-
| **Review & merge / Create PR** | When an Autopilot loop finishes on its own branch, you get a merge-ready ping; choose **Merge & push** (solo repos) or **Create PR** (teams), with a remembered default branch. |
|
|
209
|
-
| **Default LLM & loop model** | Per-project defaults for the agent model and the Autopilot loop model. |
|
|
210
|
-
|
|
211
|
-
> These toggles are configured **in the GoCode app**, not in this npm package — the
|
|
212
|
-
> package just delivers the pings that drive them. All of it is free.
|
|
213
|
-
|
|
214
284
|
## Troubleshooting
|
|
215
285
|
|
|
216
286
|
**Start here:** `gocode-notify status` prints a one-screen report — whether
|
|
@@ -225,9 +295,9 @@ been written. Most issues below are diagnosable from that output.
|
|
|
225
295
|
| `status` shows **Server: not reachable** | Network/DNS/firewall, or a wrong server URL. Confirm you can reach `https://oh.jeltechsolutions.com`; check the `--server` flag / `GOCODE_SERVER` env / the `server` field in `~/.gocode/credentials`. |
|
|
226
296
|
| **No push arrives** even though `test` exits 0 | The send is fire-and-forget and exits 0 even on failure — check `~/.gocode/notify.log` for the real error. Also confirm push permissions are granted in the GoCode app and the device token is registered (re-open the app once after signing in). |
|
|
227
297
|
| **Double pings** (two notifications per event) | The agent is calling the `gocode_notify` MCP tool *and* the runtime hook is firing. Re-run `setup` so the anti-double-ping rule/skill is installed; it tells the agent not to notify for automatic done/idle/error events. |
|
|
228
|
-
| **Hook doesn't fire** in Cursor / Claude Code | Re-run `setup` and check `status` shows "config written" for that runtime. Restart the agent app so it reloads `~/.cursor/hooks.json` / `~/.claude/settings.json`. The hooks are merged, never clobbered — your existing hooks are preserved. |
|
|
298
|
+
| **Hook doesn't fire** in Cursor / Claude Code / OpenCode | Re-run `setup` and check `status` shows "config written" for that runtime. Restart the agent app so it reloads `~/.cursor/hooks.json` / `~/.claude/settings.json` / `~/.config/opencode/plugin/gocode-notify.js`. The hooks are merged, never clobbered — your existing hooks are preserved. |
|
|
229
299
|
| **Pushes queue up while offline** then arrive later | Expected. Sends made while the server is unreachable are enqueued to `~/.gocode/outbox/` (size-capped, drop-oldest) and flushed best-effort on the next `send`. A missed "done" ping is acceptable; a blocked agent is not. |
|
|
230
|
-
| **`npx @trygocode/notify` can't find the package** |
|
|
300
|
+
| **`npx @trygocode/notify` can't find the package** | Until it's published to npm, run from a local tarball: `npm pack` in `tools/gocode-notify/`, then `npx ./gocode-notify-*.tgz <command>`. See [`docs/GOCODE_NOTIFY_MANUAL_STEPS.md`](../../docs/GOCODE_NOTIFY_MANUAL_STEPS.md). |
|
|
231
301
|
| **Want it gone** | `gocode-notify uninstall` removes exactly the hook/MCP/rule entries this tool added (nothing else). Delete `~/.gocode/` to also drop the stored credentials, and revoke the key from the app's **"Connected agents"** screen. |
|
|
232
302
|
|
|
233
303
|
**Logs & files.** Failures are appended to `~/.gocode/notify.log` (size-capped,
|
|
@@ -235,15 +305,15 @@ rotated to `notify.log.1`). Credentials live in `~/.gocode/credentials` (chmod
|
|
|
235
305
|
`600`); non-secret prefs in `~/.gocode/config.json`; the offline queue in
|
|
236
306
|
`~/.gocode/outbox/`.
|
|
237
307
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
308
|
+
For the device/secret/publish/deploy steps that are **not** part of this package
|
|
309
|
+
(Android `google-services.json`, iOS APNs, server service-account, WSL rebuild,
|
|
310
|
+
npm publish, real-device E2E), see
|
|
311
|
+
[`docs/GOCODE_NOTIFY_MANUAL_STEPS.md`](../../docs/GOCODE_NOTIFY_MANUAL_STEPS.md).
|
|
241
312
|
|
|
242
313
|
## Develop
|
|
243
314
|
|
|
244
315
|
```bash
|
|
245
|
-
|
|
246
|
-
cd gocode-notify
|
|
316
|
+
cd tools/gocode-notify
|
|
247
317
|
npm install
|
|
248
318
|
npm run build # compile TypeScript -> dist/
|
|
249
319
|
npm test # builds, then runs node --test on dist/test/
|
|
@@ -258,9 +328,13 @@ Zero runtime dependencies beyond the MCP SDK (Node built-in `fetch`/`fs`/
|
|
|
258
328
|
|
|
259
329
|
| Path | Purpose |
|
|
260
330
|
|---|---|
|
|
261
|
-
| `src/cli.ts` | `gocode-notify` bin entrypoint + command dispatcher |
|
|
331
|
+
| `src/cli.ts` | `gocode-notify` bin entrypoint + command dispatcher (incl. `on-stop`, `config`) |
|
|
262
332
|
| `src/setup.ts` | Installer orchestration (pair → detect → write configs) |
|
|
263
|
-
| `src/claude.ts` / `src/cursor.ts` | Per-client config writers (hooks + MCP + rule/skill) |
|
|
333
|
+
| `src/claude.ts` / `src/cursor.ts` / `src/opencode.ts` | Per-client config writers (hooks + MCP + rule/skill); hooks call the `on-stop` dispatcher. OpenCode uses a `session.idle` plugin instead of a hooks file |
|
|
264
334
|
| `src/send.ts` / `src/login.ts` / `src/mcp.ts` | Core send, pairing, and MCP server |
|
|
335
|
+
| `src/push.ts` | Auto-push flow behind the `on-stop` dispatcher — git add/commit/push, FF-only |
|
|
336
|
+
| `src/commit_message.ts` | AI commit-message resolution chain + deterministic fallback |
|
|
337
|
+
| `src/config.ts` | `config get\|set\|pull` — the dev-machine settings editor (server-synced) |
|
|
338
|
+
| `src/settings.ts` | Canonical Notify settings schema + valid-key specs (mirrors the server + app) |
|
|
265
339
|
| `snippets/ralph-homer.sh` | Opt-in loop completion/halt snippet (trigger C) |
|
|
266
340
|
| `test/` | `node:test` smoke + unit tests |
|
package/dist/src/claude.js
CHANGED
|
@@ -36,26 +36,38 @@ export const MCP_SERVER_ENTRY = {
|
|
|
36
36
|
args: ["-y", "@trygocode/notify", "mcp"],
|
|
37
37
|
};
|
|
38
38
|
/**
|
|
39
|
-
* The hook command for each Claude Code event (PRD §5.3
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
39
|
+
* The hook command for each Claude Code event (PRD §2.2, §5.3). Every command
|
|
40
|
+
* ends in `|| true` so a notification failure can never block the agent's turn,
|
|
41
|
+
* and carries a per-session `--dedupe-key` so overlapping triggers (e.g. Cursor
|
|
42
|
+
* `stop` + Claude `Stop`) coalesce server-side.
|
|
43
|
+
*
|
|
44
|
+
* `Stop` calls the `on-stop` DISPATCHER (PRD §2.2): it resolves settings and
|
|
45
|
+
* either runs the auto-push flow (which sends its own notification) or fires the
|
|
46
|
+
* plain `finished` ping — exactly one notification per turn. The pre-T-C6 form
|
|
47
|
+
* was `send --kind finished --source claude_code …`; both old and new commands
|
|
48
|
+
* contain the {@link HOOK_MARKERS} tokens, so old installs upgrade idempotently
|
|
49
|
+
* and uninstall cleanly. `Notification`/`SubagentStop` stay on the plain `send`
|
|
50
|
+
* (they are not turn-completion events and never auto-push).
|
|
43
51
|
*/
|
|
44
52
|
export const CLAUDE_HOOK_COMMANDS = {
|
|
45
|
-
Stop: 'npx -y @trygocode/notify
|
|
53
|
+
Stop: 'npx -y @trygocode/notify on-stop --source claude_code --dedupe-key "$CLAUDE_SESSION_ID-stop" || true',
|
|
46
54
|
Notification: 'npx -y @trygocode/notify send --kind awaiting_input --source claude_code --title "Agent needs you" --dedupe-key "$CLAUDE_SESSION_ID-notify" || true',
|
|
47
55
|
SubagentStop: 'npx -y @trygocode/notify send --kind finished --source claude_code --title "Subagent done" --dedupe-key "$CLAUDE_SESSION_ID-subagent" || true',
|
|
48
56
|
};
|
|
49
57
|
/**
|
|
50
58
|
* Substrings that together identify a hook command as OURS. Used for idempotent
|
|
51
59
|
* merge (replace, don't duplicate) and for surgical uninstall (remove exactly
|
|
52
|
-
* ours). A command must contain BOTH to be considered ours.
|
|
53
|
-
*
|
|
54
|
-
* `
|
|
55
|
-
*
|
|
56
|
-
* older install is detected and cleanly replaced rather than duplicated.
|
|
60
|
+
* ours). A command must contain BOTH to be considered ours. These tokens are
|
|
61
|
+
* present in BOTH the legacy `send --source claude_code` commands and the new
|
|
62
|
+
* `on-stop --source claude_code` Stop command, so they match old AND new
|
|
63
|
+
* installs (PRD §2.2).
|
|
57
64
|
*/
|
|
58
|
-
|
|
65
|
+
// Match OUR claude hook commands across ALL historical forms (current
|
|
66
|
+
// `npx -y @trygocode/notify …`, published-0.1.2 `npx … notify send …`, and legacy
|
|
67
|
+
// bare `gocode-notify …`). `--source claude_code` is the stable discriminator;
|
|
68
|
+
// the name token matches whichever invocation style produced the entry.
|
|
69
|
+
const HOOK_NAME_TOKENS = ["@trygocode/notify", "gocode-notify", "notify send"];
|
|
70
|
+
const HOOK_SOURCE_TOKEN = "--source claude_code";
|
|
59
71
|
/**
|
|
60
72
|
* The on-demand skill written to `~/.claude/skills/gocode-notify/SKILL.md`
|
|
61
73
|
* (PRD §5.5). The crucial content is the anti-double-ping rule: the automatic
|
|
@@ -123,9 +135,10 @@ async function writeJsonFile(file, value) {
|
|
|
123
135
|
}
|
|
124
136
|
/** True when a single hook ENTRY (`{ type, command }`) is one we wrote. */
|
|
125
137
|
function isOurHookCommand(h) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
138
|
+
if (!isRecord(h) || typeof h.command !== "string")
|
|
139
|
+
return false;
|
|
140
|
+
const cmd = h.command;
|
|
141
|
+
return cmd.includes(HOOK_SOURCE_TOKEN) && HOOK_NAME_TOKENS.some((t) => cmd.includes(t));
|
|
129
142
|
}
|
|
130
143
|
/**
|
|
131
144
|
* Strip OUR hook entries out of an event's group array, operating at the
|
package/dist/src/cli.js
CHANGED
|
@@ -18,6 +18,7 @@ import { serveStdio } from "./mcp.js";
|
|
|
18
18
|
import { setup } from "./setup.js";
|
|
19
19
|
import { uninstall } from "./uninstall.js";
|
|
20
20
|
import { cmdConfig } from "./config.js";
|
|
21
|
+
import { onStop } from "./on_stop.js";
|
|
21
22
|
/** Subcommands the finished CLI will expose (see PRD §4.1). */
|
|
22
23
|
export const COMMANDS = [
|
|
23
24
|
"login",
|
|
@@ -28,6 +29,7 @@ export const COMMANDS = [
|
|
|
28
29
|
"mcp",
|
|
29
30
|
"uninstall",
|
|
30
31
|
"config",
|
|
32
|
+
"on-stop",
|
|
31
33
|
];
|
|
32
34
|
export function printHelp() {
|
|
33
35
|
console.log([
|
|
@@ -46,6 +48,7 @@ export function printHelp() {
|
|
|
46
48
|
" mcp Run as an MCP server over stdio",
|
|
47
49
|
" uninstall Remove entries this tool added",
|
|
48
50
|
" config Get/set Notify settings (get | set <key> <value> | pull)",
|
|
51
|
+
" on-stop End-of-turn dispatcher (auto-push or finished ping)",
|
|
49
52
|
"",
|
|
50
53
|
" -h, --help Show this help",
|
|
51
54
|
" -v, --version Print the version",
|
|
@@ -375,6 +378,53 @@ export async function cmdMcp(args, deps = {}) {
|
|
|
375
378
|
await serveStdio({ ...deps, serverFlag: flagString(flags, "server") });
|
|
376
379
|
return 0;
|
|
377
380
|
}
|
|
381
|
+
/**
|
|
382
|
+
* Handle `gocode-notify on-stop --source S [--dedupe-key D] [--cwd DIR]
|
|
383
|
+
* [--server URL] [--dry-run] [--agent-driven]` (PRD §2.2). The single command
|
|
384
|
+
* the runtime stop hooks call: it resolves settings and either runs the
|
|
385
|
+
* auto-push flow (which sends its own notification) OR fires the plain
|
|
386
|
+
* `finished` ping — never both, so a turn yields exactly one notification.
|
|
387
|
+
*
|
|
388
|
+
* Like every hook entry it follows the PRD §0.5 non-blocking contract: it
|
|
389
|
+
* ALWAYS exits 0, even on a failed push/send, so a slow or failed notification
|
|
390
|
+
* can never block the agent's turn.
|
|
391
|
+
*/
|
|
392
|
+
export async function cmdOnStop(args, deps = {}) {
|
|
393
|
+
const flags = parseFlags(args);
|
|
394
|
+
const agent = isAgentDriven(flags);
|
|
395
|
+
const sink = deps.sink ?? stdoutSink;
|
|
396
|
+
const server = await resolveServerUrl(flagString(flags, "server"), deps);
|
|
397
|
+
const run = deps.onStopImpl ?? onStop;
|
|
398
|
+
const result = await run({
|
|
399
|
+
home: deps.home,
|
|
400
|
+
source: flagString(flags, "source"),
|
|
401
|
+
cwd: flagString(flags, "cwd"),
|
|
402
|
+
dedupeKey: flagString(flags, "dedupe-key"),
|
|
403
|
+
server,
|
|
404
|
+
dryRun: flagBool(flags, "dry-run"),
|
|
405
|
+
fetchImpl: deps.fetchImpl,
|
|
406
|
+
timeoutMs: deps.timeoutMs,
|
|
407
|
+
timestamp: deps.timestamp,
|
|
408
|
+
});
|
|
409
|
+
const delivered = result.mode === "push"
|
|
410
|
+
? result.push?.notified === true
|
|
411
|
+
: result.mode === "send"
|
|
412
|
+
? result.send?.ok === true
|
|
413
|
+
: undefined;
|
|
414
|
+
const detail = result.mode === "push"
|
|
415
|
+
? `auto-push: ${result.push?.outcome ?? "unknown"}${delivered ? " (notified)" : ""}`
|
|
416
|
+
: result.mode === "dry-run-send"
|
|
417
|
+
? "dry-run: would send finished (auto-push off)"
|
|
418
|
+
: `finished ${delivered ? "delivered" : "not delivered"}`;
|
|
419
|
+
if (agent) {
|
|
420
|
+
sink({ step: "on-stop", ok: true, detail });
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
console.log(`✓ on-stop (${result.mode}): ${detail}`);
|
|
424
|
+
}
|
|
425
|
+
// PRD §0.5 / §4.4: a stop hook must NEVER block the turn — always exit 0.
|
|
426
|
+
return 0;
|
|
427
|
+
}
|
|
378
428
|
/** Synchronous dispatcher: help/version/unknown + not-yet-implemented commands. */
|
|
379
429
|
export function run(argv) {
|
|
380
430
|
const cmd = argv[0];
|
|
@@ -416,6 +466,8 @@ export async function runAsync(argv) {
|
|
|
416
466
|
return cmdUninstall(argv.slice(1));
|
|
417
467
|
if (cmd === "config")
|
|
418
468
|
return cmdConfig(argv.slice(1));
|
|
469
|
+
if (cmd === "on-stop")
|
|
470
|
+
return cmdOnStop(argv.slice(1));
|
|
419
471
|
return run(argv);
|
|
420
472
|
}
|
|
421
473
|
/**
|
package/dist/src/cursor.js
CHANGED
|
@@ -36,22 +36,34 @@ export const MCP_SERVER_ENTRY = {
|
|
|
36
36
|
args: ["-y", "@trygocode/notify", "mcp"],
|
|
37
37
|
};
|
|
38
38
|
/**
|
|
39
|
-
* The Cursor `stop` hook command (PRD §5.4
|
|
40
|
-
*
|
|
39
|
+
* The Cursor `stop` hook command (PRD §2.2, §5.4). Calls the `on-stop`
|
|
40
|
+
* DISPATCHER (not `send` directly): on-stop resolves settings and either runs
|
|
41
|
+
* the auto-push flow (which sends its own notification) or fires the plain
|
|
42
|
+
* `finished` ping — exactly one notification per turn. Ends in `|| true` so a
|
|
43
|
+
* notification/push failure can never block the agent's turn, and carries a
|
|
41
44
|
* `--dedupe-key` so overlapping triggers (e.g. Cursor `stop` + Claude `Stop`)
|
|
42
45
|
* coalesce server-side.
|
|
46
|
+
*
|
|
47
|
+
* The pre-T-C6 form was `gocode-notify send --kind finished --source cursor …`;
|
|
48
|
+
* both old and new commands contain the {@link HOOK_MARKERS} tokens, so an old
|
|
49
|
+
* install still upgrades idempotently and uninstalls cleanly (PRD §2.2).
|
|
43
50
|
*/
|
|
44
|
-
export const CURSOR_STOP_COMMAND = "npx -y @trygocode/notify
|
|
51
|
+
export const CURSOR_STOP_COMMAND = "npx -y @trygocode/notify on-stop --source cursor --dedupe-key cursor-stop || true";
|
|
45
52
|
/**
|
|
46
53
|
* Substrings that together identify a `stop` hook entry as OURS. Used for
|
|
47
54
|
* idempotent merge (replace, don't duplicate) and for surgical uninstall (remove
|
|
48
|
-
* exactly ours). A command must contain BOTH to be considered ours.
|
|
49
|
-
*
|
|
50
|
-
* `
|
|
51
|
-
* form AND the legacy bare `gocode-notify send …` form, so an upgrade from an
|
|
52
|
-
* older install is detected and cleanly replaced rather than duplicated.
|
|
55
|
+
* exactly ours). A command must contain BOTH to be considered ours. These tokens
|
|
56
|
+
* are present in BOTH the legacy `send --source cursor` command and the new
|
|
57
|
+
* `on-stop --source cursor` command, so they match old AND new installs (§2.2).
|
|
53
58
|
*/
|
|
54
|
-
|
|
59
|
+
// Match OUR cursor stop hook across ALL historical command forms so upgrades
|
|
60
|
+
// replace (never duplicate) and uninstall stays surgical. The `--source cursor`
|
|
61
|
+
// token is the stable discriminator; the first token matches whichever invocation
|
|
62
|
+
// style produced the entry: the current `npx -y @trygocode/notify …`, the
|
|
63
|
+
// published-0.1.2 `npx … notify send …`, or the legacy bare `gocode-notify …`.
|
|
64
|
+
// A command is ours if it has `--source cursor` AND any of these name tokens.
|
|
65
|
+
const HOOK_NAME_TOKENS = ["@trygocode/notify", "gocode-notify", "notify send"];
|
|
66
|
+
const HOOK_SOURCE_TOKEN = "--source cursor";
|
|
55
67
|
/**
|
|
56
68
|
* The on-demand rule written to `~/.cursor/rules/gocode-notify.md` (PRD §5.5).
|
|
57
69
|
* The crucial content is the anti-double-ping rule: the automatic pings are
|
|
@@ -125,9 +137,10 @@ async function writeJsonFile(file, value) {
|
|
|
125
137
|
}
|
|
126
138
|
/** True when a single `stop` hook entry (`{ command }`) is one we wrote. */
|
|
127
139
|
function isOurStopHook(h) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
140
|
+
if (!isRecord(h) || typeof h.command !== "string")
|
|
141
|
+
return false;
|
|
142
|
+
const cmd = h.command;
|
|
143
|
+
return cmd.includes(HOOK_SOURCE_TOKEN) && HOOK_NAME_TOKENS.some((t) => cmd.includes(t));
|
|
131
144
|
}
|
|
132
145
|
/**
|
|
133
146
|
* Strip OUR entries out of the `stop` array. Returns the cleaned array plus
|
package/dist/src/mcp.js
CHANGED
|
@@ -19,35 +19,6 @@ import { send, isNotifyKind, NOTIFY_KINDS, } from "./send.js";
|
|
|
19
19
|
import { gatherStatus } from "./status.js";
|
|
20
20
|
/** Server identity advertised in the MCP `initialize` handshake. */
|
|
21
21
|
export const SERVER_NAME = "gocode-notify";
|
|
22
|
-
/**
|
|
23
|
-
* Public website for this MCP server, surfaced in the MCP `initialize`
|
|
24
|
-
* handshake (`Implementation.websiteUrl`, SEP-973). Clients that support it
|
|
25
|
-
* (MCP Inspector today; more clients over time) link the server to its docs.
|
|
26
|
-
*/
|
|
27
|
-
export const SERVER_WEBSITE_URL = "https://github.com/joseph-lewis/gocode-notify";
|
|
28
|
-
/**
|
|
29
|
-
* Icons advertised in the MCP handshake (`Implementation.icons`, SEP-973) so
|
|
30
|
-
* clients can show the GoCode mark next to this server in their UI. Served from
|
|
31
|
-
* the public repo's `assets/` over GitHub's raw CDN so there's nothing to host.
|
|
32
|
-
*
|
|
33
|
-
* NOTE: as of 2026, Cursor does not yet RENDER custom MCP server icons (it uses
|
|
34
|
-
* hardcoded marks for a few popular servers), so this stays invisible there for
|
|
35
|
-
* now — but it already shows in MCP Inspector and other spec-compliant clients,
|
|
36
|
-
* and will light up in Cursor automatically once it adds rendering. Cheap +
|
|
37
|
-
* future-proof; no behaviour depends on it.
|
|
38
|
-
*/
|
|
39
|
-
export const SERVER_ICONS = [
|
|
40
|
-
{
|
|
41
|
-
src: "https://raw.githubusercontent.com/joseph-lewis/gocode-notify/main/assets/icon-128.png",
|
|
42
|
-
mimeType: "image/png",
|
|
43
|
-
sizes: ["128x128"],
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
src: "https://raw.githubusercontent.com/joseph-lewis/gocode-notify/main/assets/icon-512.png",
|
|
47
|
-
mimeType: "image/png",
|
|
48
|
-
sizes: ["512x512"],
|
|
49
|
-
},
|
|
50
|
-
];
|
|
51
22
|
/** Tool name an agent calls to send an on-demand push. */
|
|
52
23
|
export const NOTIFY_TOOL = "gocode_notify";
|
|
53
24
|
/** Tool name an agent calls to self-diagnose pairing/reachability. */
|
|
@@ -177,15 +148,7 @@ export async function handleStatus(deps = {}) {
|
|
|
177
148
|
* drive it over an in-memory transport.
|
|
178
149
|
*/
|
|
179
150
|
export function createMcpServer(deps = {}) {
|
|
180
|
-
const server = new Server({
|
|
181
|
-
name: SERVER_NAME,
|
|
182
|
-
version: VERSION,
|
|
183
|
-
// SEP-973 server metadata: icons + website for client UIs that support it
|
|
184
|
-
// (MCP Inspector now; more clients over time). Ignored by clients that
|
|
185
|
-
// don't read them.
|
|
186
|
-
icons: [...SERVER_ICONS],
|
|
187
|
-
websiteUrl: SERVER_WEBSITE_URL,
|
|
188
|
-
}, { capabilities: { tools: {} } });
|
|
151
|
+
const server = new Server({ name: SERVER_NAME, version: VERSION }, { capabilities: { tools: {} } });
|
|
189
152
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [...TOOLS] }));
|
|
190
153
|
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
191
154
|
const { name, arguments: args } = req.params;
|