@trygocode/notify 0.1.6 → 0.3.1
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 +181 -2
- package/dist/src/cli.js +207 -1
- package/dist/src/creds.js +32 -3
- package/dist/src/dedup_lock.js +0 -0
- package/dist/src/doctor.js +262 -0
- package/dist/src/launch.js +218 -0
- package/dist/src/mcp.js +93 -10
- package/dist/src/on_stop.js +323 -7
- package/dist/src/rule-content.js +52 -1
- package/dist/src/send.js +14 -3
- package/dist/src/setup.js +20 -0
- package/dist/src/status.js +18 -0
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
- package/snippets/ralph-homer.sh +329 -9
package/README.md
CHANGED
|
@@ -13,10 +13,13 @@ waiting for you**, **errors out**, or when an overnight **loop completes / halts
|
|
|
13
13
|
- [Install — two equally-supported paths](#install--two-equally-supported-paths)
|
|
14
14
|
- [Pairing — step by step](#pairing--step-by-step)
|
|
15
15
|
- [The three triggers](#the-three-triggers)
|
|
16
|
+
- [Hand off to your server — `launch` / `autopilot`](#hand-off-to-your-server--launch--autopilot)
|
|
16
17
|
- [Auto-push to git (opt-in)](#auto-push-to-git-opt-in)
|
|
17
18
|
- [Settings sync — the `config` command](#settings-sync--the-config-command)
|
|
18
19
|
- [Ralph/Homer opt-in snippet (trigger C)](#ralphhomer-opt-in-snippet-trigger-c)
|
|
20
|
+
- [Notify from any custom script (the minimal one-liner)](#notify-from-any-custom-script-the-minimal-one-liner)
|
|
19
21
|
- [Troubleshooting](#troubleshooting)
|
|
22
|
+
- [Changelog](#changelog)
|
|
20
23
|
- [Develop](#develop)
|
|
21
24
|
- [Layout](#layout)
|
|
22
25
|
|
|
@@ -105,6 +108,12 @@ saved in `~/.gocode/credentials` → the built-in default
|
|
|
105
108
|
(`https://oh.jeltechsolutions.com`). You only need `--server` for a self-hosted
|
|
106
109
|
or staging GoCode server.
|
|
107
110
|
|
|
111
|
+
**Redistributing under your own server.** The built-in default is not baked in
|
|
112
|
+
as a fixed author box — set `GOCODE_DEFAULT_SERVER` to point fresh, unpaired
|
|
113
|
+
installs at your OWN GoCode server without patching the source or having every
|
|
114
|
+
user pass `--server`/`GOCODE_SERVER`. It only changes the *fallback* default;
|
|
115
|
+
already-paired machines keep the server in their `~/.gocode/credentials`.
|
|
116
|
+
|
|
108
117
|
## The three triggers
|
|
109
118
|
|
|
110
119
|
| Trigger | Mechanism | Fires when |
|
|
@@ -117,6 +126,21 @@ The installed rule/skill tells the agent **not** to call the MCP tool for
|
|
|
117
126
|
done/idle/error pings — those are owned by the deterministic hook (A), so you
|
|
118
127
|
never get double-pinged.
|
|
119
128
|
|
|
129
|
+
> **Cursor `stop.status` → kind mapping (since 0.3.0).** Cursor's `stop` hook
|
|
130
|
+
> carries a `status` field in its stdin JSON. The `on-stop` dispatcher maps it
|
|
131
|
+
> to the right notification kind automatically:
|
|
132
|
+
>
|
|
133
|
+
> | Cursor `stop.status` | Notification kind | Meaning |
|
|
134
|
+
> |---|---|---|
|
|
135
|
+
> | `completed` | `finished` | Agent turned cleanly (default — same as before 0.3.0) |
|
|
136
|
+
> | `aborted` | `awaiting_input` | Agent yielded back to you — may need your input |
|
|
137
|
+
> | `error` | `error` | Agent hit an error |
|
|
138
|
+
> | absent / unrecognised | `finished` | Back-compatible fallback |
|
|
139
|
+
>
|
|
140
|
+
> Before 0.3.0, Cursor only ever sent `finished` regardless of what the agent
|
|
141
|
+
> did. The status mapping requires no hook re-installation — run
|
|
142
|
+
> `gocode-notify setup --force` once to apply the updated hook command.
|
|
143
|
+
|
|
120
144
|
> **OpenCode's hook (A)** is a small `session.idle` plugin written to
|
|
121
145
|
> `~/.config/opencode/plugin/gocode-notify.js`; on each `session.idle` event it
|
|
122
146
|
> fire-and-forgets `gocode-notify on-stop --source opencode` (the same
|
|
@@ -133,6 +157,75 @@ never get double-pinged.
|
|
|
133
157
|
> `setup`/`setup --force`. `send` and `test` remain callable on their own for
|
|
134
158
|
> scripts and power users.
|
|
135
159
|
|
|
160
|
+
## Hand off to your server — `launch` / `autopilot`
|
|
161
|
+
|
|
162
|
+
> **New in 0.2.0.** Notify can now do more than ping your phone — it can **hand a
|
|
163
|
+
> big task off to your GoCode server** to run as an autonomous Autopilot loop.
|
|
164
|
+
> Close your laptop; the work keeps going server-side in your isolated sandbox,
|
|
165
|
+
> shows up in the GoCode phone app like any other Autopilot run, and your phone
|
|
166
|
+
> buzzes when it's done (or needs you).
|
|
167
|
+
|
|
168
|
+
This is the opposite direction of the three notify triggers above: instead of
|
|
169
|
+
*your local agent pinging your phone*, `launch` *starts a fresh server-side
|
|
170
|
+
build* from an explicit, self-contained task. It reuses the **same `gck_`
|
|
171
|
+
pairing** — no new login. It reads **no conversation content** (only the task
|
|
172
|
+
string you hand it), so it ships in this base package (see the trust note below).
|
|
173
|
+
|
|
174
|
+
### CLI
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
# Start a loop from a plain task (server synthesizes a PRD, then builds):
|
|
178
|
+
gocode-notify launch "Refactor the auth module to use sessions, add tests" \
|
|
179
|
+
--repo owner/repo
|
|
180
|
+
|
|
181
|
+
# alias:
|
|
182
|
+
gocode-notify autopilot "Add a CSV export endpoint with tests" --repo owner/repo
|
|
183
|
+
|
|
184
|
+
# Or hand it a pre-written checkbox PRD instead of a task string:
|
|
185
|
+
gocode-notify launch --prd-file ./my-prd.md --repo owner/repo
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
| Flag | Meaning |
|
|
189
|
+
|---|---|
|
|
190
|
+
| `<task>` (positional) | Plain-language task the server loop builds. Mutually exclusive with `--prd-file`. |
|
|
191
|
+
| `--prd-file <path>` | A pre-written checkbox PRD (base64-encoded and sent as `prd_markdown`). |
|
|
192
|
+
| `--repo owner/repo` | Optional repo context for synthesis / the loop's target. |
|
|
193
|
+
| `--branch <suffix>` | Optional branch suffix the loop pushes to. |
|
|
194
|
+
| `--model "Profile"` | Optional LLM-profile name override. |
|
|
195
|
+
| `--runner-kind <kind>` | Optional runner override (default `openhands-conversation`). |
|
|
196
|
+
| `--server <URL>` | Same server-resolution precedence as every other command. |
|
|
197
|
+
| `--agent-driven` | Emit one machine-readable JSON step line (for installers/agents). |
|
|
198
|
+
|
|
199
|
+
On success it prints the **loop id**, a **deep link** that opens the run in the
|
|
200
|
+
app, and a "running on your GoCode server — close your laptop; your phone will
|
|
201
|
+
buzz when it's done" line, then exits `0`. Unlike the fire-and-forget
|
|
202
|
+
`send`/`on-stop` hooks, `launch` is an **explicit interactive action**: it
|
|
203
|
+
reports real failures (not paired / unreachable / 4xx) with a clear message and a
|
|
204
|
+
**non-zero exit**, and it does **not** queue to the offline outbox (a stale
|
|
205
|
+
launch surfacing hours later could duplicate work).
|
|
206
|
+
|
|
207
|
+
### MCP tool
|
|
208
|
+
|
|
209
|
+
Your desktop agent (Cursor / Claude Code / OpenCode) can fire a remote loop
|
|
210
|
+
mid-session via the third MCP tool, **`gocode_launch_autopilot`**
|
|
211
|
+
(`{ task (required), repo?, branch? }`). Its description tells the agent to call
|
|
212
|
+
it **only when you explicitly ask to offload / run in the background / overnight**
|
|
213
|
+
— never for a normal task it can do right there. On success it relays the loop id
|
|
214
|
+
+ deep link; on failure it returns an actionable re-pair hint.
|
|
215
|
+
|
|
216
|
+
> **Heads-up the rule states:** the server loop is a **fresh agent with no access
|
|
217
|
+
> to your IDE's open/unsaved files** — so the task must be **self-contained** and
|
|
218
|
+
> point at a repo the server can build from a clean checkout. If the work depends
|
|
219
|
+
> on local uncommitted state, commit/push first, then hand it off.
|
|
220
|
+
|
|
221
|
+
### Trust boundary
|
|
222
|
+
|
|
223
|
+
`launch` reads **only the explicit task string** (and optional repo/branch) you
|
|
224
|
+
hand it — it does **not** read or upload your IDE conversation transcript, and it
|
|
225
|
+
touches only `~/.gocode/credentials` (the `gck_` key it already uses for notify).
|
|
226
|
+
It POSTs to **your own configurable GoCode server**. So the base package's "never
|
|
227
|
+
reads your conversation content" promise still holds.
|
|
228
|
+
|
|
136
229
|
## Auto-push to git (opt-in)
|
|
137
230
|
|
|
138
231
|
GoCode Notify can **auto-commit and push your work after every agent turn** —
|
|
@@ -239,7 +332,7 @@ Valid keys (mirror of the canonical schema):
|
|
|
239
332
|
|
|
240
333
|
| Key | Type | Meaning |
|
|
241
334
|
|---|---|---|
|
|
242
|
-
| `kinds.finished` / `kinds.error` / `kinds.awaiting_input` / `kinds.loop_completed` / `kinds.loop_halted` | bool | Per-kind notification toggles |
|
|
335
|
+
| `kinds.finished` / `kinds.error` / `kinds.awaiting_input` / `kinds.loop_completed` / `kinds.loop_halted` / `kinds.ralph_waiting` | bool | Per-kind notification toggles |
|
|
243
336
|
| `min_duration_seconds` | int ≥ 0 | Only notify if the turn ran ≥ N seconds (0 = always) |
|
|
244
337
|
| `quiet_hours.enabled` / `quiet_hours.start` / `quiet_hours.end` / `quiet_hours.tz` | bool / `HH:MM` / `HH:MM` / IANA tz | Do-not-disturb window |
|
|
245
338
|
| `auto_push.enabled` | bool | Master auto-push switch (OFF by default) |
|
|
@@ -281,6 +374,41 @@ The ready-to-copy version with comments lives at
|
|
|
281
374
|
loop scripts. Both lines are fire-and-forget (`|| true` + the CLI's 5s
|
|
282
375
|
self-timeout), so a failed or slow push can never block or fail your loop.
|
|
283
376
|
|
|
377
|
+
## Notify from any custom script (the minimal one-liner)
|
|
378
|
+
|
|
379
|
+
Any shell script — a cron job, a `Makefile` target, a post-build hook, or
|
|
380
|
+
someone else's automation — can send a push with a single line, no configuration
|
|
381
|
+
beyond a one-time `gocode-notify login` pairing:
|
|
382
|
+
|
|
383
|
+
```bash
|
|
384
|
+
gocode-notify send --kind finished --source <name> || true
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
Replace `<name>` with a short identifier for the script (e.g. `ci`, `build`,
|
|
388
|
+
`deploy`). The `|| true` guard ensures a failed or slow push **never** blocks the
|
|
389
|
+
calling script (the CLI also self-times-out in 5 seconds).
|
|
390
|
+
|
|
391
|
+
> **Discovery hint:** `gocode-notify status` always prints this line at the
|
|
392
|
+
> bottom of its report so you can copy-paste it even when you don't have the
|
|
393
|
+
> README handy.
|
|
394
|
+
|
|
395
|
+
**Other useful kinds for custom scripts:**
|
|
396
|
+
|
|
397
|
+
| Kind | When to use |
|
|
398
|
+
|---|---|
|
|
399
|
+
| `finished` | Script completed cleanly |
|
|
400
|
+
| `error` | Script hit an error |
|
|
401
|
+
| `awaiting_input` | Script paused; a human is needed |
|
|
402
|
+
| `loop_completed` | Long-running loop finished all work |
|
|
403
|
+
| `loop_halted` | Long-running loop stopped; human needed |
|
|
404
|
+
|
|
405
|
+
Pass `--title "My script"` and `--body "extra detail"` to customise the
|
|
406
|
+
notification text. Use `--project "$(basename "$PWD")"` to badge it with the
|
|
407
|
+
project name on your phone. All flags are optional — only `--kind` is required.
|
|
408
|
+
|
|
409
|
+
See `snippets/ralph-homer.sh` for the full Ralph/Homer lifecycle pattern
|
|
410
|
+
(completed / halted / stall-edge / resumed).
|
|
411
|
+
|
|
284
412
|
## Troubleshooting
|
|
285
413
|
|
|
286
414
|
**Start here:** `gocode-notify status` prints a one-screen report — whether
|
|
@@ -310,6 +438,56 @@ For the device/secret/publish/deploy steps that are **not** part of this package
|
|
|
310
438
|
npm publish, real-device E2E), see
|
|
311
439
|
[`docs/GOCODE_NOTIFY_MANUAL_STEPS.md`](../../docs/GOCODE_NOTIFY_MANUAL_STEPS.md).
|
|
312
440
|
|
|
441
|
+
## Changelog
|
|
442
|
+
|
|
443
|
+
### 0.3.0
|
|
444
|
+
|
|
445
|
+
- **New: Cursor `stop.status` → kind mapping (T-CUR1/T-CUR4).** The `on-stop`
|
|
446
|
+
dispatcher now reads the Cursor `stop` hook's stdin JSON and maps `status` to
|
|
447
|
+
the right notification kind: `completed→finished`, `aborted→awaiting_input`,
|
|
448
|
+
`error→error`. Back-compatible: absent/unrecognised status → `finished`.
|
|
449
|
+
Run `gocode-notify setup --force` to pick up the updated hook command.
|
|
450
|
+
- **New: `ralph_waiting` + `ralph_resumed` kinds (T-C1).** `NOTIFY_KINDS` now
|
|
451
|
+
includes the Ralph/Homer offline-stall lifecycle kinds. `ralph_waiting` fires
|
|
452
|
+
once on the stall edge (server drops repeats until a recovery event re-arms
|
|
453
|
+
it); `ralph_resumed` is a silent control event that resets the stall state
|
|
454
|
+
machine. See `snippets/ralph-homer.sh` for the edge-trigger pattern.
|
|
455
|
+
- **Kind taxonomy (T-S1).** The server now classifies every kind into push-worthy
|
|
456
|
+
vs. silent-info buckets: `ralph_question` / `ralph_advanced` are never pushed
|
|
457
|
+
(Oracle-answerable questions are silent); `ralph_halted` / `ralph_completed` /
|
|
458
|
+
`ralph_waiting` (edge) are the only loop kinds that reach FCM.
|
|
459
|
+
- **Foreground suppression (T-S2/T-S3).** The server suppresses a push when the
|
|
460
|
+
app is open on that exact chat (`POST /api/v1/notify/presence` heartbeat, 45s
|
|
461
|
+
TTL). Different-chat or closed app → push goes out as normal.
|
|
462
|
+
- **Offline/stall debounce (T-S4).** Server-side edge-triggered state machine:
|
|
463
|
+
a loop retrying every 60s on a quota outage pushes once on stall and stays
|
|
464
|
+
silent until it recovers and stalls again.
|
|
465
|
+
|
|
466
|
+
### 0.2.0
|
|
467
|
+
|
|
468
|
+
- **New: `launch` / `autopilot` command** — hand a large, multi-step task off to
|
|
469
|
+
your GoCode server to run as an autonomous Autopilot loop. Reuses the existing
|
|
470
|
+
`gck_` pairing; prints a loop id + app deep link; explicit failures exit
|
|
471
|
+
non-zero (no offline queueing). See
|
|
472
|
+
[Hand off to your server](#hand-off-to-your-server--launch--autopilot).
|
|
473
|
+
- **New: `gocode_launch_autopilot` MCP tool** — the third tool on the stdio MCP
|
|
474
|
+
server, so a desktop agent can fire a remote loop mid-session (explicit offload
|
|
475
|
+
only). The installed Cursor rule / Claude skill / OpenCode snippet teach the
|
|
476
|
+
agent *when* to offload and that the server loop is a fresh, self-contained
|
|
477
|
+
agent with no access to local files.
|
|
478
|
+
- **App monitoring** — IDE-launched loops are discovered by the phone app
|
|
479
|
+
(`GET /api/v1/ralph/active`), badged "Launched from <IDE>", and deep-link from
|
|
480
|
+
the completion/halt push into the run — full parity with app-launched loops.
|
|
481
|
+
- **Fix:** `--version` now reports the real package version. `src/version.ts` had
|
|
482
|
+
drifted (published `0.1.6` reported `0.1.3`); it is now kept in lockstep with
|
|
483
|
+
`package.json`, and the publish workflow refuses to ship a mismatch.
|
|
484
|
+
|
|
485
|
+
### 0.1.x
|
|
486
|
+
|
|
487
|
+
- Phone notifications for any coding agent via three triggers (runtime hook, MCP
|
|
488
|
+
tool, loop snippet), opt-in auto-push to git with an AI-written commit message,
|
|
489
|
+
and server-synced settings.
|
|
490
|
+
|
|
313
491
|
## Develop
|
|
314
492
|
|
|
315
493
|
```bash
|
|
@@ -331,7 +509,8 @@ Zero runtime dependencies beyond the MCP SDK (Node built-in `fetch`/`fs`/
|
|
|
331
509
|
| `src/cli.ts` | `gocode-notify` bin entrypoint + command dispatcher (incl. `on-stop`, `config`) |
|
|
332
510
|
| `src/setup.ts` | Installer orchestration (pair → detect → write configs) |
|
|
333
511
|
| `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 |
|
|
334
|
-
| `src/send.ts` / `src/login.ts` / `src/mcp.ts` | Core send, pairing, and MCP server |
|
|
512
|
+
| `src/send.ts` / `src/login.ts` / `src/mcp.ts` | Core send, pairing, and MCP server (incl. `gocode_launch_autopilot`) |
|
|
513
|
+
| `src/launch.ts` | Shared `launch()` core behind the `launch`/`autopilot` command + the MCP tool |
|
|
335
514
|
| `src/push.ts` | Auto-push flow behind the `on-stop` dispatcher — git add/commit/push, FF-only |
|
|
336
515
|
| `src/commit_message.ts` | AI commit-message resolution chain + deterministic fallback |
|
|
337
516
|
| `src/config.ts` | `config get\|set\|pull` — the dev-machine settings editor (server-synced) |
|
package/dist/src/cli.js
CHANGED
|
@@ -6,10 +6,12 @@
|
|
|
6
6
|
// later milestones. For now the entrypoint resolves help/version and reports
|
|
7
7
|
// not-yet-implemented for the known commands so the bin is wired and testable.
|
|
8
8
|
import { realpathSync } from "node:fs";
|
|
9
|
+
import { readFile } from "node:fs/promises";
|
|
9
10
|
import { fileURLToPath } from "node:url";
|
|
10
11
|
import { VERSION } from "./version.js";
|
|
11
12
|
import { login } from "./login.js";
|
|
12
13
|
import { resolveServerUrl } from "./creds.js";
|
|
14
|
+
import { launch } from "./launch.js";
|
|
13
15
|
import { send, isNotifyKind, NOTIFY_KINDS, } from "./send.js";
|
|
14
16
|
import { enqueue, flush } from "./outbox.js";
|
|
15
17
|
import { gatherStatus, formatStatus, formatStatusSteps } from "./status.js";
|
|
@@ -19,6 +21,7 @@ import { setup } from "./setup.js";
|
|
|
19
21
|
import { uninstall } from "./uninstall.js";
|
|
20
22
|
import { cmdConfig } from "./config.js";
|
|
21
23
|
import { onStop } from "./on_stop.js";
|
|
24
|
+
import { gatherDoctor, formatDoctor } from "./doctor.js";
|
|
22
25
|
/** Subcommands the finished CLI will expose (see PRD §4.1). */
|
|
23
26
|
export const COMMANDS = [
|
|
24
27
|
"login",
|
|
@@ -26,11 +29,54 @@ export const COMMANDS = [
|
|
|
26
29
|
"test",
|
|
27
30
|
"setup",
|
|
28
31
|
"status",
|
|
32
|
+
"doctor",
|
|
29
33
|
"mcp",
|
|
30
34
|
"uninstall",
|
|
31
35
|
"config",
|
|
32
36
|
"on-stop",
|
|
37
|
+
"launch",
|
|
33
38
|
];
|
|
39
|
+
/** Aliases that route to a canonical command in {@link runAsync}. */
|
|
40
|
+
export const COMMAND_ALIASES = {
|
|
41
|
+
autopilot: "launch",
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Read all available data from `process.stdin` when it is a pipe (not a TTY),
|
|
45
|
+
* returning the full content as a string. Best-effort: returns an empty string on
|
|
46
|
+
* any error or if stdin is a TTY (no data to consume). Used by {@link cmdOnStop}
|
|
47
|
+
* to consume the Cursor `stop` hook's JSON payload (T-CUR1 / PRD §8.5).
|
|
48
|
+
*
|
|
49
|
+
* The read is bounded to {@link MAX_STDIN_BYTES} to prevent a misbehaving hook
|
|
50
|
+
* from blocking with an enormous payload. The function resolves (never rejects)
|
|
51
|
+
* so it can never block the hook's turn.
|
|
52
|
+
*/
|
|
53
|
+
export const MAX_STDIN_BYTES = 65536; // 64 KiB — far more than any hook JSON
|
|
54
|
+
export async function readStdinIfPipe(stream) {
|
|
55
|
+
const src = stream ?? process.stdin;
|
|
56
|
+
// In a TTY / non-pipe context there is nothing to read — return early so we
|
|
57
|
+
// never block waiting for input on an interactive terminal.
|
|
58
|
+
if (src.isTTY)
|
|
59
|
+
return "";
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
const chunks = [];
|
|
62
|
+
let totalBytes = 0;
|
|
63
|
+
src.on("data", (chunk) => {
|
|
64
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
65
|
+
totalBytes += buf.byteLength;
|
|
66
|
+
if (totalBytes <= MAX_STDIN_BYTES)
|
|
67
|
+
chunks.push(buf);
|
|
68
|
+
});
|
|
69
|
+
src.on("end", () => {
|
|
70
|
+
try {
|
|
71
|
+
resolve(Buffer.concat(chunks).toString("utf8"));
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
resolve("");
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
src.on("error", () => resolve(""));
|
|
78
|
+
});
|
|
79
|
+
}
|
|
34
80
|
export function printHelp() {
|
|
35
81
|
console.log([
|
|
36
82
|
`gocode-notify v${VERSION}`,
|
|
@@ -45,10 +91,12 @@ export function printHelp() {
|
|
|
45
91
|
" test Send a canned test push",
|
|
46
92
|
" setup Pair + detect runtimes + write configs",
|
|
47
93
|
" status Report credentials / server / detected runtimes",
|
|
94
|
+
" doctor Self-diagnostic checklist with exact fix commands",
|
|
48
95
|
" mcp Run as an MCP server over stdio",
|
|
49
96
|
" uninstall Remove entries this tool added",
|
|
50
97
|
" config Get/set Notify settings (get | set <key> <value> | pull)",
|
|
51
98
|
" on-stop End-of-turn dispatcher (auto-push or finished ping)",
|
|
99
|
+
" launch Hand a big task off to your GoCode server (alias: autopilot)",
|
|
52
100
|
"",
|
|
53
101
|
" -h, --help Show this help",
|
|
54
102
|
" -v, --version Print the version",
|
|
@@ -89,6 +137,30 @@ function flagString(flags, name) {
|
|
|
89
137
|
const v = flags.get(name);
|
|
90
138
|
return typeof v === "string" ? v : undefined;
|
|
91
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Collect the bare positional arguments from an argv slice, mirroring the exact
|
|
142
|
+
* value-consumption logic of {@link parseFlags} so a positional is never confused
|
|
143
|
+
* with a flag's value. A `--flag value` pair consumes `value`; a `--flag=value`
|
|
144
|
+
* or a trailing/boolean `--flag` consumes nothing. Everything left over (an arg
|
|
145
|
+
* not starting with `--` and not eaten as a flag value) is a positional. Used by
|
|
146
|
+
* `launch`, whose task string is positional (PRD §4.2).
|
|
147
|
+
*/
|
|
148
|
+
export function collectPositionals(argv) {
|
|
149
|
+
const positionals = [];
|
|
150
|
+
for (let i = 0; i < argv.length; i++) {
|
|
151
|
+
const arg = argv[i];
|
|
152
|
+
if (arg.startsWith("--")) {
|
|
153
|
+
if (arg.indexOf("=") !== -1)
|
|
154
|
+
continue;
|
|
155
|
+
const next = argv[i + 1];
|
|
156
|
+
if (next !== undefined && !next.startsWith("--"))
|
|
157
|
+
i++;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
positionals.push(arg);
|
|
161
|
+
}
|
|
162
|
+
return positionals;
|
|
163
|
+
}
|
|
92
164
|
/** Handle `gocode-notify login [--code N] [--label "..."] [--server URL] [--agent-driven]`. */
|
|
93
165
|
export async function cmdLogin(args, deps = {}) {
|
|
94
166
|
const flags = parseFlags(args);
|
|
@@ -183,6 +255,10 @@ export async function cmdSend(args, deps = {}) {
|
|
|
183
255
|
const dedupeKey = flagString(flags, "dedupe-key");
|
|
184
256
|
if (dedupeKey)
|
|
185
257
|
payload.dedupe_key = dedupeKey;
|
|
258
|
+
// --autopilot forces the "Autopilot" tray badge on (loop kinds are badged
|
|
259
|
+
// automatically server-side; this is for an explicit non-loop Autopilot send).
|
|
260
|
+
if (flagBool(flags, "autopilot"))
|
|
261
|
+
payload.autopilot = true;
|
|
186
262
|
const server = await resolveServerUrl(flagString(flags, "server"), deps);
|
|
187
263
|
const sendOpts = {
|
|
188
264
|
home: deps.home,
|
|
@@ -288,6 +364,26 @@ export async function cmdStatus(args, deps = {}) {
|
|
|
288
364
|
}
|
|
289
365
|
return 0;
|
|
290
366
|
}
|
|
367
|
+
/**
|
|
368
|
+
* Handle `gocode-notify doctor [--server URL]` (PRD §8.6 R10 / T-COV4).
|
|
369
|
+
* Prints a checklist: (1) paired? (2) server reachable? (3) which runner hooks
|
|
370
|
+
* installed (Cursor stop / Claude Stop + Notification)? (4) gocode-notify on
|
|
371
|
+
* PATH for non-login shells? Each ✗ item prints the exact fix command.
|
|
372
|
+
* Always exits 0 — it is an informational report, not a gate.
|
|
373
|
+
*/
|
|
374
|
+
export async function cmdDoctor(args, deps = {}) {
|
|
375
|
+
const flags = parseFlags(args);
|
|
376
|
+
const report = await gatherDoctor({
|
|
377
|
+
home: deps.home,
|
|
378
|
+
serverFlag: flagString(flags, "server") ?? deps.serverFlag,
|
|
379
|
+
fetchImpl: deps.fetchImpl,
|
|
380
|
+
timeoutMs: deps.timeoutMs,
|
|
381
|
+
pathEnv: deps.pathEnv,
|
|
382
|
+
});
|
|
383
|
+
for (const line of formatDoctor(report))
|
|
384
|
+
console.log(line);
|
|
385
|
+
return 0;
|
|
386
|
+
}
|
|
291
387
|
/** True when a flag is present as a bare boolean or explicit `=true`. */
|
|
292
388
|
function flagBool(flags, name) {
|
|
293
389
|
const v = flags.get(name);
|
|
@@ -393,6 +489,17 @@ export async function cmdOnStop(args, deps = {}) {
|
|
|
393
489
|
const flags = parseFlags(args);
|
|
394
490
|
const agent = isAgentDriven(flags);
|
|
395
491
|
const sink = deps.sink ?? stdoutSink;
|
|
492
|
+
// Read the hook's stdin JSON (T-CUR1 / PRD §8.5). The Cursor `stop` hook pipes
|
|
493
|
+
// its event data to the process stdin; we consume it here so on-stop can map
|
|
494
|
+
// the `status` field to the correct notification kind. Best-effort: errors and
|
|
495
|
+
// TTY contexts return an empty string (falls back to `finished` in on_stop.ts).
|
|
496
|
+
//
|
|
497
|
+
// Injection priority: deps.hookStdin (test stub) > deps.readStdin (test reader)
|
|
498
|
+
// > real stdin read. An explicit empty string from deps.hookStdin means "no
|
|
499
|
+
// stdin provided" (do not read from the real process.stdin).
|
|
500
|
+
const hookStdin = deps.hookStdin !== undefined
|
|
501
|
+
? deps.hookStdin
|
|
502
|
+
: await (deps.readStdin ?? readStdinIfPipe)();
|
|
396
503
|
const server = await resolveServerUrl(flagString(flags, "server"), deps);
|
|
397
504
|
const run = deps.onStopImpl ?? onStop;
|
|
398
505
|
const result = await run({
|
|
@@ -405,6 +512,7 @@ export async function cmdOnStop(args, deps = {}) {
|
|
|
405
512
|
fetchImpl: deps.fetchImpl,
|
|
406
513
|
timeoutMs: deps.timeoutMs,
|
|
407
514
|
timestamp: deps.timestamp,
|
|
515
|
+
hookStdin,
|
|
408
516
|
});
|
|
409
517
|
const delivered = result.mode === "push"
|
|
410
518
|
? result.push?.notified === true
|
|
@@ -415,7 +523,11 @@ export async function cmdOnStop(args, deps = {}) {
|
|
|
415
523
|
? `auto-push: ${result.push?.outcome ?? "unknown"}${delivered ? " (notified)" : ""}`
|
|
416
524
|
: result.mode === "dry-run-send"
|
|
417
525
|
? "dry-run: would send finished (auto-push off)"
|
|
418
|
-
:
|
|
526
|
+
: result.mode === "deduped"
|
|
527
|
+
? "deduped: another source already notified for this run"
|
|
528
|
+
: result.mode === "autopilot-suppressed"
|
|
529
|
+
? "autopilot-suppressed: an Autopilot loop owns this turn's ping"
|
|
530
|
+
: `finished ${delivered ? "delivered" : "not delivered"}`;
|
|
419
531
|
if (agent) {
|
|
420
532
|
sink({ step: "on-stop", ok: true, detail });
|
|
421
533
|
}
|
|
@@ -425,6 +537,96 @@ export async function cmdOnStop(args, deps = {}) {
|
|
|
425
537
|
// PRD §0.5 / §4.4: a stop hook must NEVER block the turn — always exit 0.
|
|
426
538
|
return 0;
|
|
427
539
|
}
|
|
540
|
+
/**
|
|
541
|
+
* Handle `gocode-notify launch "<task>" [--repo owner/repo] [--branch suffix]
|
|
542
|
+
* [--prd-file path.md] [--model "Profile"] [--runner-kind K] [--server URL]
|
|
543
|
+
* [--agent-driven]` (alias `autopilot`). Hands a big, multi-step task off to the
|
|
544
|
+
* user's GoCode server to run as an autonomous Autopilot loop (PRD §4.2).
|
|
545
|
+
*
|
|
546
|
+
* The positional task string maps to `message`; `--prd-file` reads a file and
|
|
547
|
+
* sends it as `prd_markdown` instead — the two are mutually exclusive (a usage
|
|
548
|
+
* error → exit 2, before any network). Unlike the fire-and-forget `send`/`on-stop`
|
|
549
|
+
* hooks, `launch` is an explicit, interactive action: it reports real failures
|
|
550
|
+
* with a NON-zero exit (like `test`) and never queues to the offline outbox.
|
|
551
|
+
*/
|
|
552
|
+
export async function cmdLaunch(args, deps = {}) {
|
|
553
|
+
const flags = parseFlags(args);
|
|
554
|
+
const agent = isAgentDriven(flags);
|
|
555
|
+
const sink = deps.sink ?? stdoutSink;
|
|
556
|
+
const fail = (detail, code) => {
|
|
557
|
+
if (agent)
|
|
558
|
+
sink({ step: "launch", ok: false, detail });
|
|
559
|
+
else
|
|
560
|
+
console.error(`gocode-notify launch: ${detail}`);
|
|
561
|
+
return code;
|
|
562
|
+
};
|
|
563
|
+
// Drop the `--agent-driven` boolean from positionals so it is never mistaken
|
|
564
|
+
// for the task string; everything else not consumed as a flag value is a task.
|
|
565
|
+
const positionals = collectPositionals(args);
|
|
566
|
+
const task = positionals.length > 0 ? positionals.join(" ") : undefined;
|
|
567
|
+
const prdFile = flagString(flags, "prd-file");
|
|
568
|
+
// Exactly-one-of the two task sources — caught locally before any network so
|
|
569
|
+
// the user gets a clear usage error (the server enforces the same rule, 422).
|
|
570
|
+
if (task !== undefined && prdFile !== undefined) {
|
|
571
|
+
return fail("provide either a task string OR --prd-file, not both", 2);
|
|
572
|
+
}
|
|
573
|
+
if (task === undefined && prdFile === undefined) {
|
|
574
|
+
return fail('a task is required: gocode-notify launch "<task>" (or --prd-file <path>)', 2);
|
|
575
|
+
}
|
|
576
|
+
const input = {};
|
|
577
|
+
if (prdFile !== undefined) {
|
|
578
|
+
try {
|
|
579
|
+
input.prdMarkdown = await readFile(prdFile, "utf8");
|
|
580
|
+
}
|
|
581
|
+
catch (err) {
|
|
582
|
+
return fail(`could not read --prd-file ${prdFile}: ${err instanceof Error ? err.message : String(err)}`, 2);
|
|
583
|
+
}
|
|
584
|
+
input.prdFilename = prdFile.split("/").pop() || prdFile;
|
|
585
|
+
}
|
|
586
|
+
else {
|
|
587
|
+
input.message = task;
|
|
588
|
+
}
|
|
589
|
+
const repo = flagString(flags, "repo");
|
|
590
|
+
if (repo)
|
|
591
|
+
input.selectedRepository = repo;
|
|
592
|
+
const branch = flagString(flags, "branch");
|
|
593
|
+
if (branch)
|
|
594
|
+
input.branchSuffix = branch;
|
|
595
|
+
const model = flagString(flags, "model");
|
|
596
|
+
if (model)
|
|
597
|
+
input.modelOverride = model;
|
|
598
|
+
const runnerKind = flagString(flags, "runner-kind");
|
|
599
|
+
if (runnerKind)
|
|
600
|
+
input.runnerKind = runnerKind;
|
|
601
|
+
const launchFn = deps.launchImpl ?? launch;
|
|
602
|
+
const result = await launchFn(input, {
|
|
603
|
+
home: deps.home,
|
|
604
|
+
fetchImpl: deps.fetchImpl,
|
|
605
|
+
timeoutMs: deps.timeoutMs,
|
|
606
|
+
timestamp: deps.timestamp,
|
|
607
|
+
env: deps.env,
|
|
608
|
+
serverFlag: flagString(flags, "server"),
|
|
609
|
+
});
|
|
610
|
+
if (!result.ok) {
|
|
611
|
+
// launch()'s error strings are already actionable (re-pair hint, status, etc.).
|
|
612
|
+
return fail(result.error ?? "launch failed", 1);
|
|
613
|
+
}
|
|
614
|
+
const loop = result.loop_id ?? "(id pending)";
|
|
615
|
+
const link = result.deep_link;
|
|
616
|
+
const tail = "Running on your GoCode server — close your laptop; your phone will buzz when it's done.";
|
|
617
|
+
if (agent) {
|
|
618
|
+
const where = link ? ` (${link})` : "";
|
|
619
|
+
sink({ step: "launch", ok: true, detail: `launched loop ${loop}${where}` });
|
|
620
|
+
}
|
|
621
|
+
else {
|
|
622
|
+
console.log(`✓ Autopilot launched on your GoCode server.`);
|
|
623
|
+
console.log(` Loop: ${loop}`);
|
|
624
|
+
if (link)
|
|
625
|
+
console.log(` Open: ${link}`);
|
|
626
|
+
console.log(` ${tail}`);
|
|
627
|
+
}
|
|
628
|
+
return 0;
|
|
629
|
+
}
|
|
428
630
|
/** Synchronous dispatcher: help/version/unknown + not-yet-implemented commands. */
|
|
429
631
|
export function run(argv) {
|
|
430
632
|
const cmd = argv[0];
|
|
@@ -458,6 +660,8 @@ export async function runAsync(argv) {
|
|
|
458
660
|
return cmdTest(argv.slice(1));
|
|
459
661
|
if (cmd === "status")
|
|
460
662
|
return cmdStatus(argv.slice(1));
|
|
663
|
+
if (cmd === "doctor")
|
|
664
|
+
return cmdDoctor(argv.slice(1));
|
|
461
665
|
if (cmd === "setup")
|
|
462
666
|
return cmdSetup(argv.slice(1));
|
|
463
667
|
if (cmd === "mcp")
|
|
@@ -468,6 +672,8 @@ export async function runAsync(argv) {
|
|
|
468
672
|
return cmdConfig(argv.slice(1));
|
|
469
673
|
if (cmd === "on-stop")
|
|
470
674
|
return cmdOnStop(argv.slice(1));
|
|
675
|
+
if (cmd === "launch" || cmd === "autopilot")
|
|
676
|
+
return cmdLaunch(argv.slice(1));
|
|
471
677
|
return run(argv);
|
|
472
678
|
}
|
|
473
679
|
/**
|
package/dist/src/creds.js
CHANGED
|
@@ -7,12 +7,41 @@
|
|
|
7
7
|
// Server-URL precedence (PRD §4.2, the acceptance criterion for this task):
|
|
8
8
|
// --server flag > GOCODE_SERVER env > credentials file > built-in default
|
|
9
9
|
//
|
|
10
|
+
// The "built-in default" itself is configurable at distribution/build time via
|
|
11
|
+
// GOCODE_DEFAULT_SERVER (PRD §6/§7 — "never hardcode the author's box for the
|
|
12
|
+
// world's installs"): an open-source redistribution can point new, unpaired
|
|
13
|
+
// installs at its OWN GoCode server without patching this source literal.
|
|
14
|
+
//
|
|
10
15
|
// Zero runtime deps — only Node built-ins, to match the package's zero-dep rule.
|
|
11
16
|
import { promises as fs } from "node:fs";
|
|
12
17
|
import os from "node:os";
|
|
13
18
|
import path from "node:path";
|
|
14
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* Compiled-in fallback server, used ONLY when neither the caller, the env, the
|
|
21
|
+
* credentials file, nor the GOCODE_DEFAULT_SERVER distribution override resolves
|
|
22
|
+
* a URL. Open-source forks set GOCODE_DEFAULT_SERVER (see {@link builtinDefaultServer})
|
|
23
|
+
* rather than editing this literal, so no fork is forced to ship the author's box.
|
|
24
|
+
*/
|
|
15
25
|
export const DEFAULT_SERVER = "https://oh.jeltechsolutions.com";
|
|
26
|
+
/**
|
|
27
|
+
* Env var that overrides the built-in default server at distribution/build time.
|
|
28
|
+
* Distinct from `GOCODE_SERVER` (a per-run user override that outranks the
|
|
29
|
+
* credentials file): `GOCODE_DEFAULT_SERVER` only sets the *fallback* a fresh,
|
|
30
|
+
* unpaired install resolves to, so a redistribution can rebrand the default
|
|
31
|
+
* target without touching paired users' `~/.gocode/credentials`.
|
|
32
|
+
*/
|
|
33
|
+
export const DEFAULT_SERVER_ENV = "GOCODE_DEFAULT_SERVER";
|
|
34
|
+
/**
|
|
35
|
+
* The effective built-in default: the `GOCODE_DEFAULT_SERVER` distribution
|
|
36
|
+
* override when it is set to a non-empty value, else the compiled-in
|
|
37
|
+
* {@link DEFAULT_SERVER} literal. Read live (not cached at import) so a
|
|
38
|
+
* redistribution/build/CI can set it without re-importing the module, and so
|
|
39
|
+
* tests can exercise the override. The result is trimmed + trailing-slash-stripped.
|
|
40
|
+
*/
|
|
41
|
+
export function builtinDefaultServer() {
|
|
42
|
+
const override = firstNonEmpty(process.env[DEFAULT_SERVER_ENV]);
|
|
43
|
+
return normalizeServer(override ?? DEFAULT_SERVER);
|
|
44
|
+
}
|
|
16
45
|
/**
|
|
17
46
|
* Resolve the home directory. Prefers an explicit override, then `$HOME`
|
|
18
47
|
* (so tests can point at a temp dir via the env), then `os.homedir()`.
|
|
@@ -129,9 +158,9 @@ export async function writeConfig(config, opts) {
|
|
|
129
158
|
* The result is trimmed and has trailing slashes stripped.
|
|
130
159
|
*/
|
|
131
160
|
export function resolveServer(sources) {
|
|
132
|
-
const chosen = firstNonEmpty(sources.flag, sources.env, sources.creds?.server, sources.default ??
|
|
161
|
+
const chosen = firstNonEmpty(sources.flag, sources.env, sources.creds?.server, sources.default ?? builtinDefaultServer());
|
|
133
162
|
// The default is always non-empty, so `chosen` is defined here.
|
|
134
|
-
return normalizeServer(chosen ??
|
|
163
|
+
return normalizeServer(chosen ?? builtinDefaultServer());
|
|
135
164
|
}
|
|
136
165
|
/**
|
|
137
166
|
* Convenience wrapper: resolve the server URL using the `--server` flag, the
|
|
Binary file
|