@pingroom/cli 0.8.1 → 0.9.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/README.md CHANGED
@@ -257,7 +257,12 @@ it **silently**, `end` closes it with one completion alert.
257
257
  The management nouns cover the rest of the agent surface (agent token required;
258
258
  `--room` where noted):
259
259
 
260
+ Room `--icon` takes a v3 catalog id (`bell`, `globe`, `terminal`, …), never an
261
+ emoji — the server rejects anything off-catalog. Browse ids with
262
+ `pingroom rooms icons`. Quick-action `--icon` is the opposite: it takes an emoji.
263
+
260
264
  ```bash
265
+ pingroom rooms icons # browse the v3 room-icon catalog
261
266
  pingroom rooms list # rooms this account belongs to
262
267
  pingroom rooms get GZNFB6BZGJIH
263
268
  pingroom rooms create -n "Deploys" --icon bell --color "#e33122"
@@ -38,9 +38,21 @@ function printJsonOr(args, text, lines) {
38
38
 
39
39
  export async function rooms(args) {
40
40
  if (args.help) { process.stdout.write(`${commandHelp('rooms')}\n`); return EXIT.OK; }
41
- const action = sub(args, ['list', 'get', 'create', 'join'], 'rooms');
41
+ const action = sub(args, ['list', 'get', 'create', 'join', 'icons'], 'rooms');
42
42
  const { token, apiBase } = agentContext(args);
43
43
 
44
+ if (action === 'icons') {
45
+ const { text, json } = await requireOk(
46
+ httpJson('GET', `${apiBase}/api/agent/room-icons`, { headers: auth(token) }),
47
+ 'rooms icons',
48
+ );
49
+ if (args.json) { process.stdout.write(`${text}\n`); return EXIT.OK; }
50
+ const categories = Array.isArray(json.categories) ? json.categories : [];
51
+ const lines = categories.map((c) => `${c.label ?? c.id}: ${(c.icons ?? []).join(' ')}`);
52
+ process.stdout.write(`${lines.join('\n') || '(no icons)'}\n`);
53
+ return EXIT.OK;
54
+ }
55
+
44
56
  if (action === 'list') {
45
57
  const { text, json } = await requireOk(
46
58
  httpJson('GET', `${apiBase}/api/agent/rooms`, { headers: auth(token) }),
@@ -69,17 +81,18 @@ export async function rooms(args) {
69
81
  httpJson('POST', `${apiBase}/api/agent/rooms/join`, { headers: auth(token), body: { invite_code: code } }),
70
82
  'rooms join',
71
83
  );
72
- printJsonOr(args, text, `joined ${json.room?.name ?? code}`);
84
+ // The server returns the room flat, so `json.room` is never set and the
85
+ // name always fell back to the invite code.
86
+ printJsonOr(args, text, `joined ${json.room?.name ?? json.name ?? code}`);
73
87
  return EXIT.OK;
74
88
  }
75
89
 
76
90
  // create — private by default; --public requires --handle and uses the
77
91
  // dedicated consent scope on the server.
78
92
  if (!args.name) fail('rooms create needs --name', EXIT.USAGE);
79
- if (!args.icon || !args.color) fail('rooms create needs --icon and --color (see "pingroom rooms list-icons" via the app, e.g. --icon bell --color "#e33122")', EXIT.USAGE);
93
+ if (!args.icon || !args.color) fail('rooms create needs --icon and --color. --icon is a v3 catalog id, not an emoji — run "pingroom rooms icons" to browse, e.g. --icon bell --color "#e33122"', EXIT.USAGE);
80
94
 
81
95
  const body = { name: args.name, icon: args.icon, color: args.color };
82
- if (args.description) body.description = args.description;
83
96
 
84
97
  let url = `${apiBase}/api/agent/rooms`;
85
98
  if (args.public) {
@@ -88,6 +101,17 @@ export async function rooms(args) {
88
101
  url = `${apiBase}/api/agent/rooms/public`;
89
102
  }
90
103
 
104
+ // The ordinary rooms:write scope creates a MINIMAL private room: the server
105
+ // marks description (with is_public, handle, category, actions and friends)
106
+ // `prohibited` there and answers 422. Only the public create carries them.
107
+ // Say so here rather than posting a request that cannot succeed.
108
+ if (args.description !== undefined) {
109
+ if (!args.public) {
110
+ fail('--description is only accepted on a public room (add --public --handle <handle>). A private agent room is created minimal; set its description from the app.', EXIT.USAGE);
111
+ }
112
+ body.description = args.description;
113
+ }
114
+
91
115
  const { text, json } = await requireOk(httpJson('POST', url, { headers: auth(token), body }), 'rooms create');
92
116
  printJsonOr(args, text, `created ${json.room?.invite_code ?? json.invite_code ?? ''} ${args.name}`.trim());
93
117
  return EXIT.OK;
@@ -128,7 +152,13 @@ export async function webhooks(args) {
128
152
  if (!body.name) fail('webhooks create needs --name', EXIT.USAGE);
129
153
  const { text, json } = await requireOk(httpJson('POST', base, { headers: auth(token), body }), 'webhooks create');
130
154
  // The trigger URL carries the webhook secret — print it once, like the app.
131
- printJsonOr(args, text, `created ${json.webhook?.id ?? ''}\n${json.webhook?.url ?? json.url ?? ''}`.trim());
155
+ // The server answers with the webhook FLAT plus `webhook_url`; the nested
156
+ // shapes are only kept as fallbacks. Reading `json.webhook.*` alone printed
157
+ // a bare "created" and swallowed the one credential this command exists to
158
+ // hand over.
159
+ const created = json.webhook ?? json;
160
+ const url = created.webhook_url ?? created.url ?? json.webhook_url ?? json.url ?? '';
161
+ printJsonOr(args, text, `created ${created.id ?? ''}\n${url}`.trim());
132
162
  return EXIT.OK;
133
163
  }
134
164
 
@@ -169,7 +199,12 @@ export async function actions(args) {
169
199
  return EXIT.OK;
170
200
  }
171
201
 
172
- if (!args.label || !args.icon) fail('actions set needs --label and --icon', EXIT.USAGE);
202
+ // A Ping's title is optional its emoji can be the whole name — so
203
+ // `--label ""` is a deliberate value, not a missing flag. The emoji is the
204
+ // half that must be there.
205
+ if (args.label === undefined || !args.icon) {
206
+ fail('actions set needs --label (may be empty) and --icon', EXIT.USAGE);
207
+ }
173
208
  const body = { label: args.label, icon: args.icon };
174
209
  if (args.sound !== undefined) body.sound = args.sound;
175
210
  if (args.require_ack) body.requires_ack = true;
package/lib/help.js CHANGED
@@ -32,7 +32,8 @@ Commands:
32
32
  Claude Desktop
33
33
  skills List the PingRoom agent skills, or install them for Claude Code
34
34
  activate Retry Agent Inbox activation with the saved QR-paired credential
35
- rooms List, inspect, create, or join rooms (rooms list|get|create|join)
35
+ rooms List, inspect, create, or join rooms; browse the icon catalog
36
+ (rooms list|get|create|join|icons)
36
37
  webhooks Manage a room's incoming webhooks (webhooks list|create|update|delete)
37
38
  actions List, configure, or trigger a room's quick actions (actions list|set|trigger)
38
39
  approval Send an approve/deny request; with --wait, block on the decision
@@ -313,10 +314,14 @@ export const COMMAND_HELP_SECTIONS = {
313
314
  rooms: `rooms (agent token required):
314
315
  pingroom rooms list List the rooms this account belongs to
315
316
  pingroom rooms get <code> Show one room
317
+ pingroom rooms icons Browse the v3 room-icon catalog
316
318
  pingroom rooms create -n <name> --icon <id> --color <hex>
317
- Create a private room (add --description);
319
+ Create a minimal private room.
320
+ --icon is a v3 catalog id from "rooms icons"
321
+ (e.g. bell), NOT an emoji;
318
322
  --public --handle <handle> creates a public
319
- room under its own consent scope
323
+ room under its own consent scope, and is
324
+ the only create that takes --description
320
325
  pingroom rooms join <code> Join a room by invite code`,
321
326
  webhooks: `webhooks (agent token + --room required):
322
327
  pingroom webhooks list --room <code>
@@ -329,6 +334,7 @@ export const COMMAND_HELP_SECTIONS = {
329
334
  actions: `actions (agent token + --room required):
330
335
  pingroom actions list --room <code>
331
336
  pingroom actions set <1-4> --room <code> --label <text> --icon <emoji>
337
+ (--label "" makes an emoji-only Ping)
332
338
  Optional: --sound, --require-ack
333
339
  pingroom actions trigger <1-4> --room <code>`,
334
340
  approval: `approval (agent token + --room required):
package/lib/http.js CHANGED
@@ -35,7 +35,13 @@ export function apiDetail(res, json) {
35
35
  const base = stripControlChars(
36
36
  (json && (json.message || json.error || json.code)) || `HTTP ${res ? res.status : 'error'}`,
37
37
  );
38
- const hint = json && typeof json.code === 'string' ? API_HINTS[json.code] : undefined;
38
+ let hint = json && typeof json.code === 'string' ? API_HINTS[json.code] : undefined;
39
+ // A 401 means the stored credential is dead (revoked in the app, or the
40
+ // pairing was cleaned up). Without this line the CLI prints a bare
41
+ // "Unauthenticated." and leaves the user to guess the remedy.
42
+ if (!hint && res && res.status === 401) {
43
+ hint = 'This credential is no longer valid — run "pingroom reconnect" to pair again.';
44
+ }
39
45
  return hint ? `${base}\n ${hint}` : base;
40
46
  }
41
47
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pingroom/cli",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "description": "Send PingRoom Pings and wait for human decisions from CI, scripts, and agents.",
5
5
  "type": "module",
6
6
  "bin": {