@trygocode/notify 0.1.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.
@@ -0,0 +1,150 @@
1
+ // `gocode-notify status` — a coherent at-a-glance report (PRD §4.1, §8):
2
+ // - credentials present? (and the bound user/label)
3
+ // - server reachable? (a short, non-blocking probe)
4
+ // - which agent runtimes are detected on this machine?
5
+ // - have their config files been written?
6
+ //
7
+ // This module gathers the report into a plain {@link StatusReport} object and
8
+ // renders it human-readably. Keeping the gather/format split lets the later
9
+ // `--agent-driven` task emit the SAME report as JSON without re-collecting it.
10
+ //
11
+ // Runtime detection is delegated to the canonical {@link detectRuntimes} in
12
+ // `detect.ts` (fixture-HOME tested there, PRD §5.2). `status` re-exports the
13
+ // runtime types/table for backward-compatible imports and only consumes the
14
+ // detector to build its summary.
15
+ //
16
+ // Zero runtime deps — Node built-ins only, matching the package's zero-dep rule.
17
+ import { credentialsPath, readCredentials, resolveServerUrl, } from "./creds.js";
18
+ import { detectRuntimes } from "./detect.js";
19
+ // Re-exported so existing `from "./status.js"` imports keep working after the
20
+ // detector moved to its own module.
21
+ export { detectRuntimes, RUNTIMES } from "./detect.js";
22
+ /** Default reachability-probe timeout. Short so `status` never blocks a shell. */
23
+ export const DEFAULT_PROBE_TIMEOUT_MS = 3000;
24
+ /** Health endpoint hit by the reachability probe (any HTTP response = up). */
25
+ export const HEALTH_PATH = "/api/health";
26
+ function errMessage(err) {
27
+ return err instanceof Error ? err.message : String(err);
28
+ }
29
+ /**
30
+ * Probe `${url}${HEALTH_PATH}` to decide reachability. ANY HTTP response (even a
31
+ * 404) counts as reachable — we only care that the host answered. A network
32
+ * error or timeout is unreachable. Never throws.
33
+ */
34
+ export async function probeServer(url, opts = {}) {
35
+ const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
36
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS;
37
+ const controller = new AbortController();
38
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
39
+ try {
40
+ const res = await fetchImpl(`${url}${HEALTH_PATH}`, {
41
+ method: "GET",
42
+ signal: controller.signal,
43
+ });
44
+ return { reachable: true, detail: `HTTP ${res.status}` };
45
+ }
46
+ catch (err) {
47
+ const detail = controller.signal.aborted ? `timeout after ${timeoutMs}ms` : errMessage(err);
48
+ return { reachable: false, detail };
49
+ }
50
+ finally {
51
+ clearTimeout(timer);
52
+ }
53
+ }
54
+ /** Gather the full {@link StatusReport}. Never throws (collects errors inline). */
55
+ export async function gatherStatus(opts = {}) {
56
+ const pathOpts = { home: opts.home };
57
+ // Credentials: distinguish absent (present:false, no error) from malformed
58
+ // (present:false, error set) so the report can tell the user which to fix.
59
+ const credsStatus = { present: false, path: credentialsPath(pathOpts) };
60
+ let creds = null;
61
+ try {
62
+ creds = await readCredentials(pathOpts);
63
+ }
64
+ catch (err) {
65
+ credsStatus.error = errMessage(err);
66
+ }
67
+ if (creds) {
68
+ credsStatus.present = true;
69
+ credsStatus.user_id = creds.user_id;
70
+ credsStatus.label = creds.label;
71
+ credsStatus.server = creds.server;
72
+ }
73
+ // Server URL via the standard precedence chain (flag > env > creds > default),
74
+ // then probe it.
75
+ const url = await resolveServerUrl(opts.serverFlag, pathOpts);
76
+ const probe = await probeServer(url, { fetchImpl: opts.fetchImpl, timeoutMs: opts.timeoutMs });
77
+ const runtimes = await detectRuntimes(pathOpts);
78
+ return {
79
+ credentials: credsStatus,
80
+ server: { url, reachable: probe.reachable, detail: probe.detail },
81
+ runtimes,
82
+ };
83
+ }
84
+ function mark(ok) {
85
+ return ok ? "✓" : "✗";
86
+ }
87
+ /** Render a {@link StatusReport} as human-readable lines (one per element). */
88
+ export function formatStatus(report) {
89
+ const lines = ["gocode-notify status", ""];
90
+ const c = report.credentials;
91
+ if (c.present) {
92
+ lines.push(`${mark(true)} Credentials: paired as ${c.user_id} (${c.label})`);
93
+ }
94
+ else if (c.error) {
95
+ lines.push(`${mark(false)} Credentials: unreadable — ${c.error}`);
96
+ }
97
+ else {
98
+ lines.push(`${mark(false)} Credentials: not paired — run \`gocode-notify login\``);
99
+ }
100
+ lines.push(` path: ${c.path}`);
101
+ lines.push(`${mark(report.server.reachable)} Server: ${report.server.url} (${report.server.detail})`);
102
+ lines.push("", "Runtimes:");
103
+ for (const r of report.runtimes) {
104
+ if (!r.detected) {
105
+ lines.push(` ${mark(false)} ${r.name}: not detected`);
106
+ continue;
107
+ }
108
+ const cfg = r.configWritten ? "config written" : "no config yet";
109
+ lines.push(` ${mark(true)} ${r.name}: detected (${cfg})`);
110
+ lines.push(` config: ${r.configPath}`);
111
+ }
112
+ return lines;
113
+ }
114
+ /**
115
+ * Render a {@link StatusReport} as `--agent-driven` step lines (PRD §4.4): one
116
+ * line per report element — `credentials`, `server`, then `runtime:<name>` for
117
+ * each runtime — so an agent can parse the same report `formatStatus` renders
118
+ * for humans. `ok` reflects whether that element is in the desired state
119
+ * (paired / reachable / detected).
120
+ */
121
+ export function formatStatusSteps(report) {
122
+ const steps = [];
123
+ const c = report.credentials;
124
+ steps.push({
125
+ step: "credentials",
126
+ ok: c.present,
127
+ detail: c.present
128
+ ? `paired as ${c.user_id} (${c.label})`
129
+ : c.error
130
+ ? `unreadable — ${c.error}`
131
+ : "not paired",
132
+ });
133
+ steps.push({
134
+ step: "server",
135
+ ok: report.server.reachable,
136
+ detail: `${report.server.url} (${report.server.detail})`,
137
+ });
138
+ for (const r of report.runtimes) {
139
+ steps.push({
140
+ step: `runtime:${r.name}`,
141
+ ok: r.detected,
142
+ detail: r.detected
143
+ ? r.configWritten
144
+ ? "detected (config written)"
145
+ : "detected (no config yet)"
146
+ : "not detected",
147
+ });
148
+ }
149
+ return steps;
150
+ }
@@ -0,0 +1,39 @@
1
+ import { uninstallClaudeConfig, CLAUDE_RUNTIME_NAME, } from "./claude.js";
2
+ import { uninstallCursorConfig, CURSOR_RUNTIME_NAME } from "./cursor.js";
3
+ /** The production runtime uninstallers, in run order. */
4
+ export const defaultUninstallers = [
5
+ { runtime: CLAUDE_RUNTIME_NAME, run: uninstallClaudeConfig },
6
+ { runtime: CURSOR_RUNTIME_NAME, run: uninstallCursorConfig },
7
+ ];
8
+ /**
9
+ * Run the uninstall orchestration (PRD §4.1, §11). Resolves (never rejects) to a
10
+ * {@link UninstallSummary}. Removes exactly our entries from every known runtime,
11
+ * preserving the user's own config. Idempotent.
12
+ */
13
+ export async function uninstall(opts = {}) {
14
+ const pathOpts = { home: opts.home };
15
+ const runners = opts.uninstallers ?? defaultUninstallers;
16
+ const runtimes = [];
17
+ const removed = [];
18
+ const steps = [];
19
+ for (const { runtime, run } of runners) {
20
+ const result = await run(pathOpts);
21
+ const outcome = { runtime, ...result };
22
+ runtimes.push(outcome);
23
+ removed.push(...result.removed);
24
+ steps.push({
25
+ step: `uninstall:${runtime}`,
26
+ ok: !result.failed,
27
+ detail: result.detail,
28
+ });
29
+ }
30
+ const ok = runtimes.every((r) => !r.failed);
31
+ steps.push({
32
+ step: "summary",
33
+ ok,
34
+ detail: removed.length > 0
35
+ ? `removed ${removed.length} gocode-notify entr${removed.length === 1 ? "y" : "ies"}`
36
+ : "no gocode-notify entries found",
37
+ });
38
+ return { ok, runtimes, removed, steps };
39
+ }
@@ -0,0 +1,2 @@
1
+ // Single source of truth for the CLI version. Keep in sync with package.json.
2
+ export const VERSION = "0.1.0";
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@trygocode/notify",
3
+ "version": "0.1.0",
4
+ "description": "Free phone notifications for any coding agent (Cursor, Claude Code, OpenCode, Ralph/Homer) via the GoCode app. One npx command installs the hooks + MCP server.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "homepage": "https://github.com/joseph-lewis/gocode-notify#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/joseph-lewis/gocode-notify.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/joseph-lewis/gocode-notify/issues"
14
+ },
15
+ "author": "GoCode",
16
+ "bin": {
17
+ "gocode-notify": "dist/src/cli.js"
18
+ },
19
+ "files": [
20
+ "dist/src",
21
+ "snippets",
22
+ "assets",
23
+ "README.md",
24
+ "LICENSE",
25
+ "CHANGELOG.md"
26
+ ],
27
+ "engines": {
28
+ "node": ">=18"
29
+ },
30
+ "scripts": {
31
+ "build": "tsc -p tsconfig.json",
32
+ "typecheck": "tsc -p tsconfig.json --noEmit",
33
+ "pretest": "tsc -p tsconfig.json",
34
+ "test": "node --test \"dist/test/**/*.test.js\"",
35
+ "clean": "rm -rf dist"
36
+ },
37
+ "keywords": [
38
+ "notifications",
39
+ "push",
40
+ "push-notifications",
41
+ "fcm",
42
+ "mcp",
43
+ "model-context-protocol",
44
+ "cli",
45
+ "claude-code",
46
+ "cursor",
47
+ "cursor-ide",
48
+ "hooks",
49
+ "ai-coding",
50
+ "opencode",
51
+ "ralph",
52
+ "gocode"
53
+ ],
54
+ "dependencies": {
55
+ "@modelcontextprotocol/sdk": "^1.29.0"
56
+ },
57
+ "devDependencies": {
58
+ "@types/node": "^20.11.0",
59
+ "typescript": "^5.4.0"
60
+ }
61
+ }
@@ -0,0 +1,21 @@
1
+ # gocode-notify — Ralph/Homer loop opt-in snippet (PRD §5.6, trigger C)
2
+ #
3
+ # OPT-IN. This snippet is NOT auto-injected by `gocode-notify setup`; the
4
+ # installer never edits your loop scripts without consent. Paste these two
5
+ # lines into the completion/halt path of any loop you control (this repo's
6
+ # `ralph`/`homer` skills, Geoffrey Huntley's `while :; do … done` one-liner,
7
+ # or your own driver) to get a phone push when the loop finishes or halts.
8
+ #
9
+ # Prereq: you've already paired this machine once with
10
+ # npx @trygocode/notify@latest login --code <CODE>
11
+ # (get <CODE> from the GoCode app → "Connect a coding agent").
12
+ #
13
+ # Both lines are fire-and-forget: the `|| true` guarantees a failed/slow push
14
+ # can never block or fail your loop (the CLI also self-times-out in 5s).
15
+
16
+ # --- At loop completion (the loop finished all work cleanly) -----------------
17
+ gocode-notify send --kind loop_completed --source ralph --project "$(basename "$PWD")" || true
18
+
19
+ # --- At loop halt (paused_max_failures / awaiting_human / question raised) ---
20
+ gocode-notify send --kind loop_halted --source ralph --project "$(basename "$PWD")" \
21
+ --title "Ralph halted — needs you" || true