@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.
- package/CHANGELOG.md +29 -0
- package/LICENSE +21 -0
- package/README.md +237 -0
- package/assets/README.md +16 -0
- package/assets/icon.svg +8 -0
- package/dist/src/agent.js +33 -0
- package/dist/src/claude.js +287 -0
- package/dist/src/cli.js +444 -0
- package/dist/src/commit_message.js +321 -0
- package/dist/src/config.js +348 -0
- package/dist/src/creds.js +158 -0
- package/dist/src/cursor.js +273 -0
- package/dist/src/detect.js +109 -0
- package/dist/src/login.js +152 -0
- package/dist/src/mcp.js +215 -0
- package/dist/src/outbox.js +150 -0
- package/dist/src/push.js +278 -0
- package/dist/src/repo_key.js +98 -0
- package/dist/src/rule-content.js +71 -0
- package/dist/src/send.js +141 -0
- package/dist/src/settings.js +213 -0
- package/dist/src/setup.js +148 -0
- package/dist/src/status.js +150 -0
- package/dist/src/uninstall.js +39 -0
- package/dist/src/version.js +2 -0
- package/package.json +61 -0
- package/snippets/ralph-homer.sh +21 -0
|
@@ -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
|
+
}
|
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
|