@pingroom/cli 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.
Files changed (3) hide show
  1. package/README.md +93 -0
  2. package/bin/pingroom.js +175 -0
  3. package/package.json +31 -0
package/README.md ADDED
@@ -0,0 +1,93 @@
1
+ # @pingroom/cli
2
+
3
+ Send PingRoom pings from CI, scripts, and agents — deploy notifications in one line,
4
+ delivered as push straight to your phone.
5
+
6
+ Zero dependencies. Works anywhere Node ≥ 20 runs.
7
+
8
+ ```bash
9
+ npx @pingroom/cli ping -w "$PINGROOM_WEBHOOK_URL" -m "Deploy succeeded ✅"
10
+ ```
11
+
12
+ ## Getting a webhook URL
13
+
14
+ In the PingRoom app, open a room → **Connections → Incoming webhooks → Add**. Copy the
15
+ URL (it embeds its own secret — treat it like a password and store it as a CI secret).
16
+
17
+ ## Usage
18
+
19
+ ```
20
+ pingroom ping [options]
21
+
22
+ -m, --message <text> Ping body text (required)
23
+ -t, --title <text> Ping title (<= 40 chars)
24
+ -a, --action <1-4> Quick-action slot to attribute the ping to
25
+ -d, --data <json> Extra JSON data, e.g. '{"commit":"abc123"}'
26
+ -w, --webhook <url> Room webhook URL (or env PINGROOM_WEBHOOK_URL)
27
+ --token <token> Agent access token (or env PINGROOM_TOKEN)
28
+ --room <code> Room invite code (used with --token)
29
+ --api <url> API base URL (env PINGROOM_API_URL)
30
+ --json Print the raw JSON response
31
+ ```
32
+
33
+ Exit codes: `0` success · `1` delivery failed · `2` bad usage. So CI fails loudly if a
34
+ ping doesn't land.
35
+
36
+ ## GitHub Actions
37
+
38
+ ```yaml
39
+ # Notify on deploy
40
+ - uses: pingroom/cli@v0
41
+ with:
42
+ webhook-url: ${{ secrets.PINGROOM_WEBHOOK_URL }}
43
+ title: 'Deploy'
44
+ message: '🚀 ${{ github.repository }} deployed (${{ github.sha }})'
45
+ data: '{"ref":"${{ github.ref_name }}","run":"${{ github.run_id }}"}'
46
+
47
+ # Notify only on failure
48
+ - if: failure()
49
+ uses: pingroom/cli@v0
50
+ with:
51
+ webhook-url: ${{ secrets.PINGROOM_WEBHOOK_URL }}
52
+ title: 'CI failed'
53
+ message: '❌ ${{ github.workflow }} failed on ${{ github.ref_name }}'
54
+ action: '2'
55
+ ```
56
+
57
+ ## GitLab CI
58
+
59
+ ```yaml
60
+ notify:
61
+ stage: .post
62
+ image: node:20-alpine
63
+ script:
64
+ - npx --yes @pingroom/cli ping -t "Deploy" -m "🚀 $CI_PROJECT_NAME @ $CI_COMMIT_SHORT_SHA"
65
+ variables:
66
+ PINGROOM_WEBHOOK_URL: $PINGROOM_WEBHOOK_URL # set as a masked CI/CD variable
67
+ ```
68
+
69
+ ## Plain shell / curl
70
+
71
+ The webhook is just an HTTP POST, so you don't even need this CLI:
72
+
73
+ ```bash
74
+ curl -fsS -X POST "$PINGROOM_WEBHOOK_URL" \
75
+ -H 'Content-Type: application/json' \
76
+ -d '{"title":"Deploy","message":"🚀 shipped"}'
77
+ ```
78
+
79
+ ## Agent token mode
80
+
81
+ For an agent acting as a user (e.g. an OAuth/auth.md credential), send to a room the
82
+ agent belongs to instead of a webhook:
83
+
84
+ ```bash
85
+ pingroom ping --token "$PINGROOM_TOKEN" --room ab12cd -m "Release shipped" \
86
+ -d '{"version":"1.4.0"}'
87
+ ```
88
+
89
+ See <https://pingroom.io/connect-mcp.md> to connect Cursor, Claude Desktop, or Claude Code.
90
+
91
+ ## License
92
+
93
+ MIT
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env node
2
+ // @pingroom/cli — send a PingRoom ping from CI, a script, or an agent.
3
+ // Zero dependencies: uses Node's built-in fetch (Node >= 20).
4
+ //
5
+ // Two delivery modes:
6
+ // 1. Webhook (best for CI) — a room webhook URL carries its own secret, so no
7
+ // browser auth is needed. `pingroom ping -w <url> -m "deploy ok"`.
8
+ // 2. Agent token — a Bearer credential + room invite code hits the agent API.
9
+ // `pingroom ping --token <t> --room <code> -m "deploy ok"`.
10
+
11
+ const DEFAULT_API = process.env.PINGROOM_API_URL || 'https://api.pingroom.io';
12
+
13
+ const HELP = `pingroom — send a ping to a PingRoom room
14
+
15
+ Usage:
16
+ pingroom ping [options]
17
+
18
+ Options:
19
+ -m, --message <text> Ping body text (required)
20
+ -t, --title <text> Ping title (<= 40 chars)
21
+ -a, --action <1-4> Quick-action slot to attribute the ping to
22
+ -d, --data <json> Extra JSON data object, e.g. '{"commit":"abc123"}'
23
+ -w, --webhook <url> Room webhook URL (or env PINGROOM_WEBHOOK_URL)
24
+ --token <token> Agent access token (or env PINGROOM_TOKEN)
25
+ --room <code> Room invite code (used with --token)
26
+ --api <url> API base URL (default ${DEFAULT_API}; env PINGROOM_API_URL)
27
+ --json Print the raw JSON response
28
+ -h, --help Show this help
29
+
30
+ Examples:
31
+ pingroom ping -w "$PINGROOM_WEBHOOK_URL" -m "Deploy succeeded ✅"
32
+ pingroom ping -w "$PINGROOM_WEBHOOK_URL" -t "CI" -m "Build #42 failed" -a 2
33
+ pingroom ping --token "$PINGROOM_TOKEN" --room ab12cd -m "Release shipped" \\
34
+ -d '{"version":"1.4.0","env":"prod"}'
35
+
36
+ Exit codes: 0 on success, 1 on delivery failure, 2 on bad usage.`;
37
+
38
+ function parseArgs(argv) {
39
+ const args = { _: [] };
40
+ const alias = {
41
+ '-m': 'message', '--message': 'message',
42
+ '-t': 'title', '--title': 'title',
43
+ '-a': 'action', '--action': 'action',
44
+ '-d': 'data', '--data': 'data',
45
+ '-w': 'webhook', '--webhook': 'webhook',
46
+ '--token': 'token',
47
+ '--room': 'room',
48
+ '--api': 'api',
49
+ '--json': 'json',
50
+ '-h': 'help', '--help': 'help',
51
+ };
52
+
53
+ for (let i = 0; i < argv.length; i++) {
54
+ const token = argv[i];
55
+ if (token === '--json' || token === '-h' || token === '--help') {
56
+ args[alias[token]] = true;
57
+ continue;
58
+ }
59
+ const key = alias[token];
60
+ if (key) {
61
+ args[key] = argv[++i];
62
+ } else if (token.startsWith('-')) {
63
+ fail(`Unknown option: ${token}`, 2);
64
+ } else {
65
+ args._.push(token);
66
+ }
67
+ }
68
+ return args;
69
+ }
70
+
71
+ function fail(message, code = 1) {
72
+ process.stderr.write(`pingroom: ${message}\n`);
73
+ process.exit(code);
74
+ }
75
+
76
+ async function postJson(url, body, headers = {}) {
77
+ let res;
78
+ try {
79
+ res = await fetch(url, {
80
+ method: 'POST',
81
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json', ...headers },
82
+ body: JSON.stringify(body),
83
+ });
84
+ } catch (err) {
85
+ fail(`network error: ${err.message}`);
86
+ }
87
+
88
+ const text = await res.text();
89
+ let json = null;
90
+ try { json = text ? JSON.parse(text) : null; } catch { /* non-JSON response */ }
91
+
92
+ return { res, text, json };
93
+ }
94
+
95
+ async function ping(args) {
96
+ if (args.help) { process.stdout.write(`${HELP}\n`); return 0; }
97
+
98
+ const message = args.message;
99
+ if (!message) fail('a --message is required', 2);
100
+
101
+ if (args.action !== undefined && !/^[1-4]$/.test(String(args.action))) {
102
+ fail('--action must be an integer 1–4', 2);
103
+ }
104
+
105
+ let data;
106
+ if (args.data !== undefined) {
107
+ try {
108
+ data = JSON.parse(args.data);
109
+ } catch {
110
+ fail('--data must be valid JSON', 2);
111
+ }
112
+ if (typeof data !== 'object' || Array.isArray(data) || data === null) {
113
+ fail('--data must be a JSON object', 2);
114
+ }
115
+ }
116
+
117
+ const webhook = args.webhook || process.env.PINGROOM_WEBHOOK_URL;
118
+ const token = args.token || process.env.PINGROOM_TOKEN;
119
+ const apiBase = (args.api || DEFAULT_API).replace(/\/$/, '');
120
+
121
+ let result;
122
+
123
+ if (webhook) {
124
+ const body = { message };
125
+ if (args.title) body.title = args.title;
126
+ if (args.action !== undefined) body.action = Number(args.action);
127
+ if (data) body.data = data;
128
+ result = await postJson(webhook, body);
129
+ } else if (token) {
130
+ if (!args.room) fail('--room is required when using --token', 2);
131
+ const url = `${apiBase}/api/agent/rooms/${encodeURIComponent(args.room)}/notifications`;
132
+ const body = { message };
133
+ if (args.title) body.title = args.title;
134
+ if (args.action !== undefined) body.action_number = Number(args.action);
135
+ if (data) body.data = data;
136
+ result = await postJson(url, body, { Authorization: `Bearer ${token}` });
137
+ } else {
138
+ fail('provide a webhook (--webhook / PINGROOM_WEBHOOK_URL) or an agent token (--token / PINGROOM_TOKEN)', 2);
139
+ }
140
+
141
+ const { res, text, json } = result;
142
+
143
+ if (args.json) {
144
+ process.stdout.write(`${text || '{}'}\n`);
145
+ }
146
+
147
+ const ok = res.ok && !(json && json.success === false);
148
+
149
+ if (!ok) {
150
+ const detail = (json && (json.message || json.error)) || `HTTP ${res.status}`;
151
+ fail(`delivery failed: ${detail}`);
152
+ }
153
+
154
+ if (!args.json) process.stdout.write('ping sent ✅\n');
155
+ return 0;
156
+ }
157
+
158
+ async function main() {
159
+ const argv = process.argv.slice(2);
160
+ const command = argv[0];
161
+
162
+ if (!command || command === '-h' || command === '--help' || command === 'help') {
163
+ process.stdout.write(`${HELP}\n`);
164
+ process.exit(0);
165
+ }
166
+
167
+ if (command === 'ping') {
168
+ const code = await ping(parseArgs(argv.slice(1)));
169
+ process.exit(code);
170
+ }
171
+
172
+ fail(`unknown command: ${command}\nRun "pingroom --help".`, 2);
173
+ }
174
+
175
+ main();
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@pingroom/cli",
3
+ "version": "0.1.0",
4
+ "description": "Send PingRoom pings from CI, scripts, and agents — deploy notifications in one line.",
5
+ "type": "module",
6
+ "bin": {
7
+ "pingroom": "bin/pingroom.js"
8
+ },
9
+ "files": [
10
+ "bin"
11
+ ],
12
+ "engines": {
13
+ "node": ">=20"
14
+ },
15
+ "keywords": [
16
+ "pingroom",
17
+ "notifications",
18
+ "push",
19
+ "ci",
20
+ "deploy",
21
+ "webhook",
22
+ "agent",
23
+ "mcp"
24
+ ],
25
+ "license": "MIT",
26
+ "homepage": "https://pingroom.io",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://gitlab.com/pingroom/cli.git"
30
+ }
31
+ }