@pingroom/cli 0.7.3 → 0.7.4

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
@@ -153,7 +153,7 @@ URL (it embeds its own secret — treat it like a password and store it as a CI
153
153
  ```
154
154
  pingroom ping [options]
155
155
 
156
- -m, --message <text> Ping body text (required)
156
+ -m, --message <text> Ping body (required; <= 120 private / <= 160 public)
157
157
  -t, --title <text> Ping title (<= 40 chars)
158
158
  -a, --action <1-4> Quick-action slot to attribute the ping to
159
159
  -d, --data <json> Extra JSON data, e.g. '{"commit":"abc123"}'
@@ -169,6 +169,11 @@ pingroom ping [options]
169
169
  --json Print the raw JSON response
170
170
  ```
171
171
 
172
+ Ping titles are limited to 40 characters. Bodies are limited to 120 characters
173
+ in private rooms and 160 in public rooms. A room code or webhook URL does not
174
+ reveal room visibility, so the CLI rejects only bodies over 160 locally; the
175
+ server applies the tighter 120-character private-room limit.
176
+
172
177
  To make the ping actionable, add `--require-ack`. The first eligible recipient to
173
178
  acknowledge it wins; `--ack-timeout` optionally expires it if nobody responds:
174
179
 
@@ -392,6 +397,13 @@ Full protocol: <https://pingroom.io/liveactivities.md>
392
397
  including `@v0` before it is moved to v0.7.3 — GitHub treats them as unexpected
393
398
  inputs, warns, and drops them; the step then falls through to the plain `ping`
394
399
  path, `outputs.answer` is never set, and any job gated on it silently proceeds.
400
+
401
+ `urgent` was added in **v0.7.4** and has the same failure mode on an older pin:
402
+ GitHub drops the unknown input and the Ping goes out at normal priority, which
403
+ looks like it worked. `require-ack` is not a substitute — as of the same release
404
+ it opens the acknowledgement lifecycle without raising the interruption level,
405
+ so a workflow that used it to break through Focus needs `urgent: true`.
406
+
395
407
  Everything else on this page (ping, `require-ack`, and the whole `handoff`
396
408
  family) works on `@v0`.
397
409
 
@@ -1,6 +1,6 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
 
3
- import { EXIT } from '../constants.js';
3
+ import { EXIT, PUBLIC_PING_MESSAGE_MAX_LENGTH } from '../constants.js';
4
4
  import { truncate } from '../util.js';
5
5
  import { commandHelp } from '../help.js';
6
6
  import { hookFetch, isSafeUrl } from '../http.js';
@@ -62,7 +62,7 @@ function summarizeTranscript(path) {
62
62
  const msg = entry && entry.message;
63
63
  if (!msg || msg.role !== 'assistant') continue;
64
64
  const text = extractAssistantText(msg.content).replace(/\s+/g, ' ').trim();
65
- if (text) return truncate(text, 500);
65
+ if (text) return truncate(text, PUBLIC_PING_MESSAGE_MAX_LENGTH);
66
66
  }
67
67
  return '';
68
68
  }
@@ -185,7 +185,7 @@ async function hookNotify(event, name, { token, room, apiBase, args }) {
185
185
  title = 'Claude finished';
186
186
  message = summarizeTranscript(event.transcript_path) || 'Session finished — waiting for you.';
187
187
  } else if (name === 'Notification') {
188
- message = truncate(event.message || 'Claude is waiting for your input.', 500);
188
+ message = truncate(event.message || 'Claude is waiting for your input.', PUBLIC_PING_MESSAGE_MAX_LENGTH);
189
189
  // A PreToolUse hook already turns permission prompts into a question; skip
190
190
  // the duplicate "needs your permission" Notification so you aren't paged twice.
191
191
  if (/permission/i.test(message)) return EXIT.OK;
@@ -51,7 +51,8 @@ export async function live(args) {
51
51
  state: sub === 'end' ? (args.failed ? 'failed' : 'done') : 'running',
52
52
  };
53
53
 
54
- // 256, not the 500 a ping body gets: this is the card's one live line.
54
+ // This live-status payload field has its own 256-character contract; ordinary
55
+ // Ping bodies are 120 in private rooms and 160 in public rooms.
55
56
  requireMaxLength(args.message, 256, '--message');
56
57
  requireMaxLength(args.title, 40, '--title');
57
58
  requireMaxLength(args.prompt, 256, '--prompt');
@@ -1,4 +1,4 @@
1
- import { EXIT } from '../constants.js';
1
+ import { EXIT, PING_TITLE_MAX_LENGTH, PUBLIC_PING_MESSAGE_MAX_LENGTH } from '../constants.js';
2
2
  import { fail, parseDataObject, requireMaxLength } from '../util.js';
3
3
  import { commandHelp } from '../help.js';
4
4
  import { apiDetail, httpJson, requireSafeUrl, uploadAttachments } from '../http.js';
@@ -9,8 +9,10 @@ export async function ping(args) {
9
9
 
10
10
  const message = args.message;
11
11
  if (!message) fail('a --message is required', EXIT.USAGE);
12
- requireMaxLength(message, 500, '--message');
13
- requireMaxLength(args.title, 40, '--title');
12
+ // Room visibility is not encoded in a room code or webhook URL. Validate the
13
+ // public ceiling here; the API applies 120 for private rooms and 160 for public.
14
+ requireMaxLength(message, PUBLIC_PING_MESSAGE_MAX_LENGTH, '--message');
15
+ requireMaxLength(args.title, PING_TITLE_MAX_LENGTH, '--title');
14
16
 
15
17
  if (args.action !== undefined && !/^[1-4]$/.test(String(args.action))) {
16
18
  fail('--action must be an integer 1–4', EXIT.USAGE);
@@ -81,6 +83,7 @@ export async function ping(args) {
81
83
  if (args.action !== undefined) body.action = Number(args.action);
82
84
  if (data) body.data = data;
83
85
  if (args.require_ack) body.requires_ack = true;
86
+ if (args.urgent) body.is_urgent = true;
84
87
  if (ackTimeout !== undefined) body.ack_timeout_seconds = ackTimeout;
85
88
  result = await httpJson('POST', webhook, { body });
86
89
  } else if (token) {
@@ -96,6 +99,7 @@ export async function ping(args) {
96
99
  if (args.action !== undefined) body.action_number = Number(args.action);
97
100
  if (data) body.data = data;
98
101
  if (args.require_ack) body.requires_ack = true;
102
+ if (args.urgent) body.is_urgent = true;
99
103
  if (ackTimeout !== undefined) body.ack_timeout_seconds = ackTimeout;
100
104
  if (attachPaths.length) {
101
105
  body.attachment_ids = await uploadAttachments(attachPaths, apiBase, token);
package/lib/constants.js CHANGED
@@ -5,3 +5,10 @@ export const MCP_ENDPOINT = `${BUILTIN_API}/api/agent/mcp`;
5
5
  export const DEFAULT_API = process.env.PINGROOM_API_URL || BUILTIN_API;
6
6
 
7
7
  export const EXIT = { OK: 0, ERROR: 1, USAGE: 2, EXPIRED: 3, CANCELLED: 4 };
8
+
9
+ // A caller holding only a room code or webhook URL cannot know the room's
10
+ // visibility without another request. Keep the CLI's local ceiling at the
11
+ // public-room limit; Laravel applies the tighter private-room limit.
12
+ export const PING_TITLE_MAX_LENGTH = 40;
13
+ export const PRIVATE_PING_MESSAGE_MAX_LENGTH = 120;
14
+ export const PUBLIC_PING_MESSAGE_MAX_LENGTH = 160;
package/lib/help.js CHANGED
@@ -35,13 +35,17 @@ Commands:
35
35
  logout Forget the stored credential`;
36
36
 
37
37
  export const HELP_PING = `ping options:
38
- -m, --message <text> Ping body text (required)
38
+ -m, --message <text> Ping body (required; <= 120 private / <= 160 public)
39
39
  -t, --title <text> Ping title (<= 40 chars)
40
40
  -a, --action <1-4> Quick-action slot to attribute the ping to
41
41
  -d, --data <json> Extra JSON data object, e.g. '{"commit":"abc123"}'
42
42
  --url <https-url> Make the ping a tappable link (absolute http(s) URL)
43
43
  --button-label <t> Link button text (<= 26 chars; requires --url)
44
- --require-ack Keep the ping open until an eligible recipient acknowledges it
44
+ --urgent Deliver time-sensitive so it breaks through Focus / Do Not
45
+ Disturb. Delivery only - asks nothing of the recipient
46
+ --require-ack Keep the ping open until an eligible recipient acknowledges it,
47
+ showing a lock-screen card with an Acknowledge button. Does
48
+ not raise the interruption level; combine with --urgent
45
49
  --ack-timeout <s> Ack deadline in seconds (requires --require-ack)
46
50
  --attach <path> Attach a file (md/pdf/html/txt/jpg/jpeg/png/zip, <= 5 MiB);
47
51
  repeat for up to 4. Requires --token and a Pro account
@@ -105,7 +109,7 @@ export const HELP_LIVE = `live <start|update|end|get> options (agent token, or a
105
109
  'question', which is still accepted)
106
110
  --category <name> start only: status | steps | alert. Legacy, but
107
111
  'alert' has no template equivalent and is the only
108
- way to start time-sensitive without --require-ack
112
+ way to start a stream time-sensitive
109
113
  --steps <a,b,c> start only: 2-8 comma-separated step labels
110
114
  -m, --message <text> The card's live message line
111
115
  --progress <0..1> Progress bar / Dynamic Island gauge
@@ -124,7 +128,8 @@ export const HELP_LIVE = `live <start|update|end|get> options (agent token, or a
124
128
  -d, --data <json> Structured data object carried on this frame
125
129
  -t, --title <text> Card title (<= 40 chars)
126
130
  -a, --action <1-4> Quick-action slot supplying the icon and sound
127
- --require-ack Add an Acknowledge button
131
+ --require-ack Add an Acknowledge button (does not raise the
132
+ interruption level; see --category alert)
128
133
  --ack-timeout <s> Ack deadline in seconds
129
134
  --room <code> Room invite code (used with --token)
130
135
  -w, --webhook <url> Room webhook URL instead of a token`;
package/lib/parser.js CHANGED
@@ -62,6 +62,7 @@ export const parseArgs = makeParser({
62
62
  '--url': 'url',
63
63
  '--button-label': 'button_label',
64
64
  '--require-ack': 'require_ack',
65
+ '--urgent': 'urgent',
65
66
  '--ack-timeout': 'ack_timeout',
66
67
  '--attach': 'attach',
67
68
  '--token': 'token',
@@ -70,7 +71,7 @@ export const parseArgs = makeParser({
70
71
  '--json': 'json',
71
72
  '-h': 'help', '--help': 'help',
72
73
  },
73
- booleans: ['require_ack', 'json', 'help'],
74
+ booleans: ['require_ack', 'urgent', 'json', 'help'],
74
75
  repeatable: ['attach'],
75
76
  });
76
77
 
@@ -159,6 +160,9 @@ export const parseLiveArgs = makeParser({
159
160
  '-a': 'action', '--action': 'action',
160
161
  '-d': 'data', '--data': 'data',
161
162
  '--require-ack': 'require_ack',
163
+ // No --urgent here on purpose. A STREAM starts time-sensitive via
164
+ // `--category alert` (fixed at creation); the live-status endpoint does not
165
+ // accept `is_urgent`, so the flag would parse and then be silently dropped.
162
166
  '--ack-timeout': 'ack_timeout',
163
167
  '-w': 'webhook', '--webhook': 'webhook',
164
168
  '--token': 'token',
package/lib/util.js CHANGED
@@ -39,7 +39,8 @@ export function stripControlChars(value) {
39
39
 
40
40
  export function truncate(value, max) {
41
41
  const str = String(value ?? '');
42
- return str.length <= max ? str : `${str.slice(0, max - 1)}…`;
42
+ const characters = Array.from(str);
43
+ return characters.length <= max ? str : `${characters.slice(0, max - 1).join('')}…`;
43
44
  }
44
45
 
45
46
  /**
@@ -51,8 +52,11 @@ export function truncate(value, max) {
51
52
  * it is, with the limit and the actual length named.
52
53
  */
53
54
  export function requireMaxLength(value, max, flag) {
54
- if (typeof value === 'string' && value.length > max) {
55
- fail(`${flag} must be at most ${max} characters (got ${value.length})`, EXIT.USAGE);
55
+ if (typeof value === 'string') {
56
+ const length = Array.from(value).length;
57
+ if (length > max) {
58
+ fail(`${flag} must be at most ${max} characters (got ${length})`, EXIT.USAGE);
59
+ }
56
60
  }
57
61
  }
58
62
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pingroom/cli",
3
- "version": "0.7.3",
3
+ "version": "0.7.4",
4
4
  "description": "Send PingRoom Pings and wait for human decisions from CI, scripts, and agents.",
5
5
  "type": "module",
6
6
  "bin": {