@pingroom/cli 0.7.0 → 0.7.2
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 +2 -2
- package/bin/pingroom.js +236 -196
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,9 +6,9 @@ from CI, scripts, and agents. Delivered as push straight to your phone.
|
|
|
6
6
|
One dependency (`qrcode-terminal`, used only to draw the pairing QR). Works
|
|
7
7
|
anywhere Node ≥ 20 runs.
|
|
8
8
|
|
|
9
|
-
> **Release status:** 0.7.
|
|
9
|
+
> **Release status:** 0.7.2 is prepared for publish — the room grant, the
|
|
10
10
|
> `attachments:write` scope `--attach` always needed, and actionable text on the
|
|
11
|
-
> refusals you can fix. The GitHub Action pins 0.7.
|
|
11
|
+
> refusals you can fix. The GitHub Action pins 0.7.2, so publish before relying
|
|
12
12
|
> on it.
|
|
13
13
|
|
|
14
14
|
## Install and first run
|
package/bin/pingroom.js
CHANGED
|
@@ -41,13 +41,17 @@ import { join } from 'node:path';
|
|
|
41
41
|
// Kept in lockstep with package.json / package-lock.json. The GitHub Action is
|
|
42
42
|
// pinned independently to the latest version already published on npm; a test
|
|
43
43
|
// makes that release gate explicit. `hook --print-config` emits this candidate.
|
|
44
|
-
const VERSION = '0.7.
|
|
44
|
+
const VERSION = '0.7.2';
|
|
45
45
|
|
|
46
46
|
const BUILTIN_API = 'https://api.pingroom.io';
|
|
47
47
|
const MCP_ENDPOINT = `${BUILTIN_API}/api/agent/mcp`;
|
|
48
48
|
const DEFAULT_API = process.env.PINGROOM_API_URL || BUILTIN_API;
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
// The help text lives as one section per command plus intro/shared/tail, so
|
|
51
|
+
// `pingroom <command> --help` can print a focused excerpt (see commandHelp).
|
|
52
|
+
// The full HELP below joins them in the historical order — `pingroom --help`
|
|
53
|
+
// output is byte-identical to the pre-split single blob.
|
|
54
|
+
const HELP_INTRO = `pingroom — send a ping, or ask a human a question, from CI/scripts/agents
|
|
51
55
|
|
|
52
56
|
Usage:
|
|
53
57
|
pingroom <command> [options]
|
|
@@ -69,9 +73,9 @@ Commands:
|
|
|
69
73
|
Claude Desktop
|
|
70
74
|
activate Retry Agent Inbox activation with the saved QR-paired credential
|
|
71
75
|
config Read/write local settings (config list | get <key> | set <key> <val>)
|
|
72
|
-
logout Forget the stored credential
|
|
76
|
+
logout Forget the stored credential`;
|
|
73
77
|
|
|
74
|
-
ping options:
|
|
78
|
+
const HELP_PING = `ping options:
|
|
75
79
|
-m, --message <text> Ping body text (required)
|
|
76
80
|
-t, --title <text> Ping title (<= 40 chars)
|
|
77
81
|
-a, --action <1-4> Quick-action slot to attribute the ping to
|
|
@@ -84,9 +88,9 @@ ping options:
|
|
|
84
88
|
repeat for up to 4. Requires --token and a Pro account
|
|
85
89
|
-w, --webhook <url> Room webhook URL (or env PINGROOM_WEBHOOK_URL)
|
|
86
90
|
--token <token> Agent access token (or env PINGROOM_TOKEN)
|
|
87
|
-
--room <code> Room invite code (used with --token)
|
|
91
|
+
--room <code> Room invite code (used with --token)`;
|
|
88
92
|
|
|
89
|
-
ask options (agent token required):
|
|
93
|
+
const HELP_ASK = `ask options (agent token required):
|
|
90
94
|
-p, --prompt <text> The question a human reads (required)
|
|
91
95
|
-o, --option <v:label[:style]>
|
|
92
96
|
An answer option (style: primary|danger|default);
|
|
@@ -102,12 +106,12 @@ ask options (agent token required):
|
|
|
102
106
|
-d, --data <json> Structured data object echoed back on the answer
|
|
103
107
|
--correlation-id <id> Opaque id echoed on every read of this question
|
|
104
108
|
--reply-to <id> Id of the ping this question replies to
|
|
105
|
-
--room <code> Room invite code (required for ask)
|
|
109
|
+
--room <code> Room invite code (required for ask)`;
|
|
106
110
|
|
|
107
|
-
list options:
|
|
108
|
-
--state <s> pending | answered | expired | cancelled | all
|
|
111
|
+
const HELP_LIST = `list options:
|
|
112
|
+
--state <s> pending | answered | expired | cancelled | all`;
|
|
109
113
|
|
|
110
|
-
handoff options (agent token required; consent scope pingroom:handoffs:create):
|
|
114
|
+
const HELP_HANDOFF = `handoff options (agent token required; consent scope pingroom:handoffs:create):
|
|
111
115
|
-m, --message <text> The prompt a human reads (required)
|
|
112
116
|
--question Make it a question (else a simple acknowledge). Also
|
|
113
117
|
implied whenever one or more --option is given.
|
|
@@ -121,19 +125,19 @@ handoff options (agent token required; consent scope pingroom:handoffs:create):
|
|
|
121
125
|
-d, --data <json> Structured data object echoed on the handoff
|
|
122
126
|
--wait Block until acked / answered / expired / cancelled
|
|
123
127
|
--timeout <sec> Per long-poll hold with --wait (0–20, server caps 25)
|
|
124
|
-
--github-output <path> Safely append handoff outputs for GitHub Actions
|
|
128
|
+
--github-output <path> Safely append handoff outputs for GitHub Actions`;
|
|
125
129
|
|
|
126
|
-
handoffs options (agent token required; consent scope pingroom:handoffs:create):
|
|
127
|
-
--state <s> open | all (default open)
|
|
130
|
+
const HELP_HANDOFFS = `handoffs options (agent token required; consent scope pingroom:handoffs:create):
|
|
131
|
+
--state <s> open | all (default open)`;
|
|
128
132
|
|
|
129
|
-
listen options (agent token required; consent scope pingroom:notifications:read):
|
|
133
|
+
const HELP_LISTEN = `listen options (agent token required; consent scope pingroom:notifications:read):
|
|
130
134
|
--timeout <sec> Per long-poll hold (0-30, default 25)
|
|
131
135
|
--limit <n> Max pings per batch (1-100, default 50)
|
|
132
136
|
--from <id> Start after this ping id instead of "now"
|
|
133
137
|
--once Print one batch and exit instead of blocking forever
|
|
134
|
-
--json One JSON object per line instead of a readable line
|
|
138
|
+
--json One JSON object per line instead of a readable line`;
|
|
135
139
|
|
|
136
|
-
live <start|update|end|get> options (agent token, or a room webhook):
|
|
140
|
+
const HELP_LIVE = `live <start|update|end|get> options (agent token, or a room webhook):
|
|
137
141
|
-c, --correlation-id <id> The stream key — reuse it for every ping (required)
|
|
138
142
|
--template <name> start only: status | steps | progress | metrics |
|
|
139
143
|
countdown | decision | matchup (fixed at creation;
|
|
@@ -163,38 +167,38 @@ live <start|update|end|get> options (agent token, or a room webhook):
|
|
|
163
167
|
--require-ack Add an Acknowledge button
|
|
164
168
|
--ack-timeout <s> Ack deadline in seconds
|
|
165
169
|
--room <code> Room invite code (used with --token)
|
|
166
|
-
-w, --webhook <url> Room webhook URL instead of a token
|
|
170
|
+
-w, --webhook <url> Room webhook URL instead of a token`;
|
|
167
171
|
|
|
168
|
-
hook options (reads a Claude Code event; defaults to stored credentials/config):
|
|
172
|
+
const HELP_HOOK = `hook options (reads a Claude Code event; defaults to stored credentials/config):
|
|
169
173
|
--room <code> Room invite code (or env/config/paired room)
|
|
170
174
|
--ttl <seconds> Approval-question expiry for PreToolUse (default 900)
|
|
171
175
|
--quiet Suppress the informational stderr lines
|
|
172
|
-
--print-config Print a ready-to-paste ~/.claude/settings.json block
|
|
176
|
+
--print-config Print a ready-to-paste ~/.claude/settings.json block`;
|
|
173
177
|
|
|
174
|
-
mcp:
|
|
178
|
+
const HELP_MCP = `mcp:
|
|
175
179
|
pingroom mcp Print the endpoint and client setup snippets
|
|
176
180
|
pingroom mcp add claude-code Print the Claude Code setup command
|
|
177
|
-
(output-only; does not change client config)
|
|
181
|
+
(output-only; does not change client config)`;
|
|
178
182
|
|
|
179
|
-
activate:
|
|
183
|
+
const HELP_ACTIVATE = `activate:
|
|
180
184
|
pingroom activate Send one test Question to your phone to prove the
|
|
181
185
|
saved QR-paired credential works (optional —
|
|
182
|
-
connecting no longer does this for you)
|
|
186
|
+
connecting no longer does this for you)`;
|
|
183
187
|
|
|
184
|
-
config options:
|
|
188
|
+
const HELP_CONFIG = `config options:
|
|
185
189
|
pingroom config list Print the stored settings
|
|
186
190
|
pingroom config get <key> Print one setting
|
|
187
191
|
pingroom config set <key> <val> Store a setting (an empty value clears it)
|
|
188
|
-
Keys: default_room, api_url
|
|
192
|
+
Keys: default_room, api_url`;
|
|
189
193
|
|
|
190
|
-
Shared:
|
|
194
|
+
const HELP_SHARED = `Shared:
|
|
191
195
|
--token <token> Agent access token (or env PINGROOM_TOKEN)
|
|
192
196
|
--api <url> API base URL (default ${DEFAULT_API}; env PINGROOM_API_URL)
|
|
193
197
|
--json Print the raw JSON response
|
|
194
198
|
-h, --help Show this help
|
|
195
|
-
-v, --version Show the CLI version
|
|
199
|
+
-v, --version Show the CLI version`;
|
|
196
200
|
|
|
197
|
-
Connecting:
|
|
201
|
+
const HELP_TAIL = `Connecting:
|
|
198
202
|
Install globally, then run with no arguments:
|
|
199
203
|
npm install --global @pingroom/cli
|
|
200
204
|
pingroom
|
|
@@ -292,6 +296,54 @@ or the recipient was not ready (409 recipient_not_ready). A question answered
|
|
|
292
296
|
with ANY value — including a negative one like 'hold' or 'deny' — exits 0: a
|
|
293
297
|
human decision is not an infrastructure failure.`;
|
|
294
298
|
|
|
299
|
+
const HELP = [
|
|
300
|
+
HELP_INTRO, HELP_PING, HELP_ASK, HELP_LIST, HELP_HANDOFF, HELP_HANDOFFS,
|
|
301
|
+
HELP_LISTEN, HELP_LIVE, HELP_HOOK, HELP_MCP, HELP_ACTIVATE, HELP_CONFIG,
|
|
302
|
+
HELP_SHARED, HELP_TAIL,
|
|
303
|
+
].join('\n\n');
|
|
304
|
+
|
|
305
|
+
// Sections for `pingroom <command> --help`. watch/cancel/logout have no block
|
|
306
|
+
// of their own in the full help, so they get a minimal one here.
|
|
307
|
+
const COMMAND_HELP_SECTIONS = {
|
|
308
|
+
ping: HELP_PING,
|
|
309
|
+
ask: HELP_ASK,
|
|
310
|
+
watch: `watch:
|
|
311
|
+
pingroom watch <question-id> Block until the question resolves and
|
|
312
|
+
print the outcome
|
|
313
|
+
--timeout <sec> Per long-poll hold (0–30, default 25)`,
|
|
314
|
+
cancel: `cancel:
|
|
315
|
+
pingroom cancel <question-id> Withdraw a pending question`,
|
|
316
|
+
list: HELP_LIST,
|
|
317
|
+
handoff: HELP_HANDOFF,
|
|
318
|
+
handoffs: HELP_HANDOFFS,
|
|
319
|
+
listen: HELP_LISTEN,
|
|
320
|
+
live: HELP_LIVE,
|
|
321
|
+
hook: HELP_HOOK,
|
|
322
|
+
activate: HELP_ACTIVATE,
|
|
323
|
+
config: HELP_CONFIG,
|
|
324
|
+
logout: `logout:
|
|
325
|
+
pingroom logout Forget the stored credential (PINGROOM_TOKEN
|
|
326
|
+
in the environment is unaffected)`,
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
// config and logout are local-only commands that reject --token/--api (and,
|
|
330
|
+
// for logout, --json), so their help gets a footer that only lists what they
|
|
331
|
+
// actually accept instead of the full shared block.
|
|
332
|
+
const COMMAND_HELP_FOOTERS = {
|
|
333
|
+
config: `Shared:
|
|
334
|
+
--json Print the raw JSON response
|
|
335
|
+
-h, --help Show this help`,
|
|
336
|
+
logout: `Shared:
|
|
337
|
+
-h, --help Show this help`,
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
// `<command> --help`: that command's section plus the shared flags, instead of
|
|
341
|
+
// the full reference `pingroom --help` / `pingroom help` still print.
|
|
342
|
+
function commandHelp(name) {
|
|
343
|
+
const section = COMMAND_HELP_SECTIONS[name];
|
|
344
|
+
return section ? `${section}\n\n${COMMAND_HELP_FOOTERS[name] ?? HELP_SHARED}` : HELP;
|
|
345
|
+
}
|
|
346
|
+
|
|
295
347
|
const EXIT = { OK: 0, ERROR: 1, USAGE: 2, EXPIRED: 3, CANCELLED: 4 };
|
|
296
348
|
|
|
297
349
|
function fail(message, code = EXIT.ERROR) {
|
|
@@ -319,8 +371,11 @@ const API_HINTS = {
|
|
|
319
371
|
* thing that would fix it when we know one.
|
|
320
372
|
*/
|
|
321
373
|
function apiDetail(res, json) {
|
|
322
|
-
|
|
323
|
-
|
|
374
|
+
// The server's wording is untrusted text headed for the terminal — strip
|
|
375
|
+
// escapes so a hostile API can't smuggle ANSI (same threat model as pair_url).
|
|
376
|
+
const base = stripControlChars(
|
|
377
|
+
(json && (json.message || json.error || json.code)) || `HTTP ${res ? res.status : 'error'}`,
|
|
378
|
+
);
|
|
324
379
|
const hint = json && typeof json.code === 'string' ? API_HINTS[json.code] : undefined;
|
|
325
380
|
return hint ? `${base}\n ${hint}` : base;
|
|
326
381
|
}
|
|
@@ -514,16 +569,72 @@ function requireMaxLength(value, max, flag) {
|
|
|
514
569
|
}
|
|
515
570
|
}
|
|
516
571
|
|
|
572
|
+
/**
|
|
573
|
+
* Validate --timeout and resolve the per-poll hold. Called by ask/handoff
|
|
574
|
+
* BEFORE the create POST: the old in-wait check ran only after the question or
|
|
575
|
+
* handoff already existed, so `--timeout -5` put a live question on someone's
|
|
576
|
+
* phone and then exited 2, orphaning it until its TTL.
|
|
577
|
+
*/
|
|
578
|
+
function resolveWaitHold(args, { def, cap }) {
|
|
579
|
+
if (args.timeout === undefined) return Math.min(def, cap);
|
|
580
|
+
const hold = Number(args.timeout);
|
|
581
|
+
if (!Number.isFinite(hold) || hold < 0) fail('--timeout must be a non-negative integer', EXIT.USAGE);
|
|
582
|
+
return Math.min(hold, cap);
|
|
583
|
+
}
|
|
584
|
+
|
|
517
585
|
function stripControlChars(value) {
|
|
518
586
|
// eslint-disable-next-line no-control-regex
|
|
519
587
|
return String(value).replace(/[\u0000-\u001F\u007F-\u009F]/g, '');
|
|
520
588
|
}
|
|
521
589
|
|
|
590
|
+
// --- argument parsing -------------------------------------------------------
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Build an argv parser from a flag table. Every command parser runs the same
|
|
594
|
+
* loop; only the tables differ:
|
|
595
|
+
* aliases flag or alias -> canonical args key
|
|
596
|
+
* booleans keys that take no value
|
|
597
|
+
* repeatable keys collected into an array (the flag may repeat)
|
|
598
|
+
* bareDashIsPositional whether a lone `-` collects into `_` (the question-
|
|
599
|
+
* style parsers) or fails as an unknown option (ping,
|
|
600
|
+
* live)
|
|
601
|
+
* Unknown flags always fail as a usage error; bare words collect into `_`.
|
|
602
|
+
*/
|
|
603
|
+
function makeParser({ aliases, booleans, repeatable = [], bareDashIsPositional = false }) {
|
|
604
|
+
const booleanKeys = new Set(booleans);
|
|
605
|
+
const repeatableKeys = new Set(repeatable);
|
|
606
|
+
return function parse(argv) {
|
|
607
|
+
const args = { _: [] };
|
|
608
|
+
for (let i = 0; i < argv.length; i++) {
|
|
609
|
+
const token = argv[i];
|
|
610
|
+
// Object.hasOwn, not aliases[token]: a bare lookup walks the prototype
|
|
611
|
+
// chain, so `constructor` / `toString` / `__proto__` in flag position
|
|
612
|
+
// resolve to a truthy inherited value, get treated as an option, and
|
|
613
|
+
// swallow the next argument instead of failing as an unknown flag.
|
|
614
|
+
const key = Object.hasOwn(aliases, token) ? aliases[token] : undefined;
|
|
615
|
+
if (key && booleanKeys.has(key)) {
|
|
616
|
+
args[key] = true;
|
|
617
|
+
} else if (key) {
|
|
618
|
+
const value = argv[++i];
|
|
619
|
+
if (value === undefined) {
|
|
620
|
+
fail(`option ${token} needs a value`, EXIT.USAGE);
|
|
621
|
+
}
|
|
622
|
+
if (repeatableKeys.has(key)) (args[key] ||= []).push(value);
|
|
623
|
+
else args[key] = value;
|
|
624
|
+
} else if (token.startsWith('-') && !(bareDashIsPositional && token === '-')) {
|
|
625
|
+
fail(`Unknown option: ${token}`, EXIT.USAGE);
|
|
626
|
+
} else {
|
|
627
|
+
args._.push(token);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return args;
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
|
|
522
634
|
// --- ping (unchanged wire behaviour) ---------------------------------------
|
|
523
635
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
const alias = {
|
|
636
|
+
const parseArgs = makeParser({
|
|
637
|
+
aliases: {
|
|
527
638
|
'-m': 'message', '--message': 'message',
|
|
528
639
|
'-t': 'title', '--title': 'title',
|
|
529
640
|
'-a': 'action', '--action': 'action',
|
|
@@ -539,40 +650,15 @@ function parseArgs(argv) {
|
|
|
539
650
|
'--api': 'api',
|
|
540
651
|
'--json': 'json',
|
|
541
652
|
'-h': 'help', '--help': 'help',
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
for (let i = 0; i < argv.length; i++) {
|
|
547
|
-
const token = argv[i];
|
|
548
|
-
// Object.hasOwn, not alias[token]: a bare lookup walks the prototype chain,
|
|
549
|
-
// so `constructor` / `toString` / `__proto__` in flag position resolve to a
|
|
550
|
-
// truthy inherited value, get treated as an option, and swallow the next
|
|
551
|
-
// argument instead of failing as an unknown flag.
|
|
552
|
-
const key = Object.hasOwn(alias, token) ? alias[token] : undefined;
|
|
553
|
-
if (key && booleans.has(key)) {
|
|
554
|
-
args[key] = true;
|
|
555
|
-
} else if (key) {
|
|
556
|
-
const value = argv[++i];
|
|
557
|
-
if (value === undefined) {
|
|
558
|
-
fail(`option ${token} needs a value`, EXIT.USAGE);
|
|
559
|
-
}
|
|
560
|
-
if (repeatable.has(key)) (args[key] ||= []).push(value);
|
|
561
|
-
else args[key] = value;
|
|
562
|
-
} else if (token.startsWith('-')) {
|
|
563
|
-
fail(`Unknown option: ${token}`, EXIT.USAGE);
|
|
564
|
-
} else {
|
|
565
|
-
args._.push(token);
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
return args;
|
|
569
|
-
}
|
|
653
|
+
},
|
|
654
|
+
booleans: ['require_ack', 'json', 'help'],
|
|
655
|
+
repeatable: ['attach'],
|
|
656
|
+
});
|
|
570
657
|
|
|
571
658
|
// Parser for the question commands: supports repeatable --option and a trailing
|
|
572
659
|
// positional (a question id). Unknown flags fail like the ping parser.
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
const alias = {
|
|
660
|
+
const parseQArgs = makeParser({
|
|
661
|
+
aliases: {
|
|
576
662
|
'-p': 'prompt', '--prompt': 'prompt',
|
|
577
663
|
'-o': 'option', '--option': 'option',
|
|
578
664
|
'-c': 'context', '--context': 'context',
|
|
@@ -595,40 +681,16 @@ function parseQArgs(argv) {
|
|
|
595
681
|
'--wait': 'wait',
|
|
596
682
|
'--json': 'json',
|
|
597
683
|
'-h': 'help', '--help': 'help',
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
const token = argv[i];
|
|
604
|
-
// hasOwn, not a bare lookup — see parseArgs: an inherited key would swallow args.
|
|
605
|
-
const key = Object.hasOwn(alias, token) ? alias[token] : undefined;
|
|
606
|
-
if (key && booleans.has(key)) {
|
|
607
|
-
args[key] = true;
|
|
608
|
-
} else if (key) {
|
|
609
|
-
const value = argv[++i];
|
|
610
|
-
if (value === undefined) {
|
|
611
|
-
fail(`option ${token} needs a value`, EXIT.USAGE);
|
|
612
|
-
}
|
|
613
|
-
if (multi.has(key)) {
|
|
614
|
-
(args[key] ||= []).push(value);
|
|
615
|
-
} else {
|
|
616
|
-
args[key] = value;
|
|
617
|
-
}
|
|
618
|
-
} else if (token.startsWith('-') && token !== '-') {
|
|
619
|
-
fail(`Unknown option: ${token}`, EXIT.USAGE);
|
|
620
|
-
} else {
|
|
621
|
-
args._.push(token);
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
return args;
|
|
625
|
-
}
|
|
684
|
+
},
|
|
685
|
+
booleans: ['wait', 'json', 'help', 'once'],
|
|
686
|
+
repeatable: ['option'],
|
|
687
|
+
bareDashIsPositional: true,
|
|
688
|
+
});
|
|
626
689
|
|
|
627
690
|
// Parser for `handoff`: --message plus repeatable --option, boolean --question,
|
|
628
691
|
// and the handoff-specific flags. Unknown flags fail like the other parsers.
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
const alias = {
|
|
692
|
+
const parseHandoffArgs = makeParser({
|
|
693
|
+
aliases: {
|
|
632
694
|
'-m': 'message', '--message': 'message',
|
|
633
695
|
'--question': 'question',
|
|
634
696
|
'-o': 'option', '--option': 'option',
|
|
@@ -646,34 +708,11 @@ function parseHandoffArgs(argv) {
|
|
|
646
708
|
'--wait': 'wait',
|
|
647
709
|
'--json': 'json',
|
|
648
710
|
'-h': 'help', '--help': 'help',
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
const token = argv[i];
|
|
655
|
-
// hasOwn, not a bare lookup — see parseArgs: an inherited key would swallow args.
|
|
656
|
-
const key = Object.hasOwn(alias, token) ? alias[token] : undefined;
|
|
657
|
-
if (key && booleans.has(key)) {
|
|
658
|
-
args[key] = true;
|
|
659
|
-
} else if (key) {
|
|
660
|
-
const value = argv[++i];
|
|
661
|
-
if (value === undefined) {
|
|
662
|
-
fail(`option ${token} needs a value`, EXIT.USAGE);
|
|
663
|
-
}
|
|
664
|
-
if (multi.has(key)) {
|
|
665
|
-
(args[key] ||= []).push(value);
|
|
666
|
-
} else {
|
|
667
|
-
args[key] = value;
|
|
668
|
-
}
|
|
669
|
-
} else if (token.startsWith('-') && token !== '-') {
|
|
670
|
-
fail(`Unknown option: ${token}`, EXIT.USAGE);
|
|
671
|
-
} else {
|
|
672
|
-
args._.push(token);
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
return args;
|
|
676
|
-
}
|
|
711
|
+
},
|
|
712
|
+
booleans: ['question', 'wait', 'json', 'help'],
|
|
713
|
+
repeatable: ['option'],
|
|
714
|
+
bareDashIsPositional: true,
|
|
715
|
+
});
|
|
677
716
|
|
|
678
717
|
// True when a URL is safe to attach a bearer token or webhook secret to: https,
|
|
679
718
|
// or http on loopback so local dev against http://localhost still works.
|
|
@@ -838,7 +877,7 @@ async function uploadAttachments(paths, apiBase, token) {
|
|
|
838
877
|
}
|
|
839
878
|
|
|
840
879
|
async function ping(args) {
|
|
841
|
-
if (args.help) { process.stdout.write(`${
|
|
880
|
+
if (args.help) { process.stdout.write(`${commandHelp('ping')}\n`); return EXIT.OK; }
|
|
842
881
|
|
|
843
882
|
const message = args.message;
|
|
844
883
|
if (!message) fail('a --message is required', EXIT.USAGE);
|
|
@@ -984,9 +1023,8 @@ const LIVE_TEMPLATE_NAMES = ['status', 'steps', 'progress', 'metrics', 'countdow
|
|
|
984
1023
|
|
|
985
1024
|
// Parser for `live`: a leading subcommand (start|update|end|get) plus the
|
|
986
1025
|
// live-status flags. Unknown flags fail like the other parsers.
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
const alias = {
|
|
1026
|
+
const parseLiveArgs = makeParser({
|
|
1027
|
+
aliases: {
|
|
990
1028
|
'-c': 'correlation_id', '--correlation-id': 'correlation_id',
|
|
991
1029
|
'-t': 'title', '--title': 'title',
|
|
992
1030
|
'-m': 'message', '--message': 'message',
|
|
@@ -1015,29 +1053,10 @@ function parseLiveArgs(argv) {
|
|
|
1015
1053
|
'--api': 'api',
|
|
1016
1054
|
'--json': 'json',
|
|
1017
1055
|
'-h': 'help', '--help': 'help',
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
for (let i = 0; i < argv.length; i++) {
|
|
1023
|
-
const token = argv[i];
|
|
1024
|
-
// hasOwn, not a bare lookup — see parseArgs: an inherited key would swallow args.
|
|
1025
|
-
const key = Object.hasOwn(alias, token) ? alias[token] : undefined;
|
|
1026
|
-
if (key && booleans.has(key)) {
|
|
1027
|
-
args[key] = true;
|
|
1028
|
-
} else if (key) {
|
|
1029
|
-
const value = argv[++i];
|
|
1030
|
-
if (value === undefined) fail(`option ${token} needs a value`, EXIT.USAGE);
|
|
1031
|
-
if (repeatable.has(key)) (args[key] ||= []).push(value);
|
|
1032
|
-
else args[key] = value;
|
|
1033
|
-
} else if (token.startsWith('-')) {
|
|
1034
|
-
fail(`Unknown option: ${token}`, EXIT.USAGE);
|
|
1035
|
-
} else {
|
|
1036
|
-
args._.push(token);
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
return args;
|
|
1040
|
-
}
|
|
1056
|
+
},
|
|
1057
|
+
booleans: ['require_ack', 'json', 'help', 'failed'],
|
|
1058
|
+
repeatable: ['metric', 'option'],
|
|
1059
|
+
});
|
|
1041
1060
|
|
|
1042
1061
|
// "label:value" -> {label, value}. Only the first colon splits.
|
|
1043
1062
|
function buildMetrics(list) {
|
|
@@ -1099,6 +1118,7 @@ function numberOption(raw, flag, { min, max, integer = false } = {}) {
|
|
|
1099
1118
|
* (--webhook), which speak the same `live_status` contract.
|
|
1100
1119
|
*/
|
|
1101
1120
|
async function live(args) {
|
|
1121
|
+
if (args.help) { process.stdout.write(`${commandHelp('live')}\n`); return EXIT.OK; }
|
|
1102
1122
|
const sub = args._[0];
|
|
1103
1123
|
const known = ['start', 'update', 'end', 'get'];
|
|
1104
1124
|
if (!sub || !known.includes(sub)) {
|
|
@@ -1317,11 +1337,10 @@ function printResolution(q) {
|
|
|
1317
1337
|
// and return the state's exit code. The server expires it at its ttl, so this
|
|
1318
1338
|
// always terminates.
|
|
1319
1339
|
async function waitForResolution(id, args, { token, apiBase }) {
|
|
1320
|
-
|
|
1321
|
-
if (!Number.isFinite(hold) || hold < 0) fail('--timeout must be a non-negative integer', EXIT.USAGE);
|
|
1322
|
-
hold = Math.min(hold, 30);
|
|
1340
|
+
const hold = resolveWaitHold(args, { def: 25, cap: 30 });
|
|
1323
1341
|
|
|
1324
1342
|
for (;;) {
|
|
1343
|
+
const started = Date.now();
|
|
1325
1344
|
const url = `${apiBase}/api/agent/questions/${encodeURIComponent(id)}/wait?timeout=${hold}`;
|
|
1326
1345
|
const { res, text, json } = await httpJson('GET', url, { headers: { Authorization: `Bearer ${token}` } });
|
|
1327
1346
|
if (!res.ok) {
|
|
@@ -1333,12 +1352,16 @@ async function waitForResolution(id, args, { token, apiBase }) {
|
|
|
1333
1352
|
else printResolution(json);
|
|
1334
1353
|
return exitForState(json.state);
|
|
1335
1354
|
}
|
|
1336
|
-
// Still pending at the hold timeout — poll again
|
|
1355
|
+
// Still pending at the hold timeout — poll again, but never hot-loop: a
|
|
1356
|
+
// misbehaving server that answers `pending` instantly (ignoring the hold)
|
|
1357
|
+
// would otherwise be hammered at full speed.
|
|
1358
|
+
const elapsed = Date.now() - started;
|
|
1359
|
+
if (elapsed < 1000) await sleep(1000 - elapsed);
|
|
1337
1360
|
}
|
|
1338
1361
|
}
|
|
1339
1362
|
|
|
1340
1363
|
async function ask(args) {
|
|
1341
|
-
if (args.help) { process.stdout.write(`${
|
|
1364
|
+
if (args.help) { process.stdout.write(`${commandHelp('ask')}\n`); return EXIT.OK; }
|
|
1342
1365
|
|
|
1343
1366
|
const prompt = args.prompt;
|
|
1344
1367
|
if (!prompt) fail('a --prompt is required', EXIT.USAGE);
|
|
@@ -1376,6 +1399,9 @@ async function ask(args) {
|
|
|
1376
1399
|
}
|
|
1377
1400
|
if (args.data !== undefined) body.data = parseDataObject(args.data);
|
|
1378
1401
|
|
|
1402
|
+
// Pre-flight: reject a bad --timeout before the question exists.
|
|
1403
|
+
if (args.wait) resolveWaitHold(args, { def: 25, cap: 30 });
|
|
1404
|
+
|
|
1379
1405
|
const url = `${apiBase}/api/agent/rooms/${encodeURIComponent(room)}/questions`;
|
|
1380
1406
|
const { res, text, json } = await httpJson('POST', url, { body, headers: { Authorization: `Bearer ${token}` } });
|
|
1381
1407
|
if (!res.ok) {
|
|
@@ -1393,7 +1419,7 @@ async function ask(args) {
|
|
|
1393
1419
|
}
|
|
1394
1420
|
|
|
1395
1421
|
async function watch(args) {
|
|
1396
|
-
if (args.help) { process.stdout.write(`${
|
|
1422
|
+
if (args.help) { process.stdout.write(`${commandHelp('watch')}\n`); return EXIT.OK; }
|
|
1397
1423
|
const id = args._[0];
|
|
1398
1424
|
if (!id) fail('a question id is required (pingroom watch <id>)', EXIT.USAGE);
|
|
1399
1425
|
const { token, apiBase } = agentContext(args);
|
|
@@ -1401,7 +1427,7 @@ async function watch(args) {
|
|
|
1401
1427
|
}
|
|
1402
1428
|
|
|
1403
1429
|
async function cancel(args) {
|
|
1404
|
-
if (args.help) { process.stdout.write(`${
|
|
1430
|
+
if (args.help) { process.stdout.write(`${commandHelp('cancel')}\n`); return EXIT.OK; }
|
|
1405
1431
|
const id = args._[0];
|
|
1406
1432
|
if (!id) fail('a question id is required (pingroom cancel <id>)', EXIT.USAGE);
|
|
1407
1433
|
const { token, apiBase } = agentContext(args);
|
|
@@ -1417,7 +1443,7 @@ async function cancel(args) {
|
|
|
1417
1443
|
}
|
|
1418
1444
|
|
|
1419
1445
|
async function list(args) {
|
|
1420
|
-
if (args.help) { process.stdout.write(`${
|
|
1446
|
+
if (args.help) { process.stdout.write(`${commandHelp('list')}\n`); return EXIT.OK; }
|
|
1421
1447
|
const { token, apiBase } = agentContext(args);
|
|
1422
1448
|
const qs = args.state ? `?state=${encodeURIComponent(args.state)}` : '';
|
|
1423
1449
|
const url = `${apiBase}/api/agent/questions${qs}`;
|
|
@@ -1449,7 +1475,7 @@ async function list(args) {
|
|
|
1449
1475
|
|
|
1450
1476
|
/** Cursor bookkeeping is the whole protocol: `after` in, `cursor` back. */
|
|
1451
1477
|
async function listen(args) {
|
|
1452
|
-
if (args.help) { process.stdout.write(`${
|
|
1478
|
+
if (args.help) { process.stdout.write(`${commandHelp('listen')}\n`); return EXIT.OK; }
|
|
1453
1479
|
|
|
1454
1480
|
const { token, apiBase } = agentContext(args);
|
|
1455
1481
|
const headers = { Authorization: `Bearer ${token}` };
|
|
@@ -1530,7 +1556,7 @@ function formatIncoming(item) {
|
|
|
1530
1556
|
}
|
|
1531
1557
|
|
|
1532
1558
|
async function listHandoffs(args) {
|
|
1533
|
-
if (args.help) { process.stdout.write(`${
|
|
1559
|
+
if (args.help) { process.stdout.write(`${commandHelp('handoffs')}\n`); return EXIT.OK; }
|
|
1534
1560
|
const { token, apiBase } = agentContext(args);
|
|
1535
1561
|
const state = args.state || 'open';
|
|
1536
1562
|
if (state !== 'open' && state !== 'all') {
|
|
@@ -1648,11 +1674,10 @@ function writeGitHubHandoffOutputs(path, h) {
|
|
|
1648
1674
|
// Long-poll GET /handoffs/{id}/wait until the handoff leaves open/pending, then
|
|
1649
1675
|
// print it and return the state's exit code. Reuses the shared bounded hold.
|
|
1650
1676
|
async function waitForHandoff(id, args, { token, apiBase }, initialDeliveryState) {
|
|
1651
|
-
|
|
1652
|
-
if (!Number.isFinite(hold) || hold < 0) fail('--timeout must be a non-negative integer', EXIT.USAGE);
|
|
1653
|
-
hold = Math.min(hold, 25);
|
|
1677
|
+
const hold = resolveWaitHold(args, { def: 20, cap: 25 });
|
|
1654
1678
|
|
|
1655
1679
|
for (;;) {
|
|
1680
|
+
const started = Date.now();
|
|
1656
1681
|
const url = `${apiBase}/api/agent/handoffs/${encodeURIComponent(id)}/wait?timeout=${hold}`;
|
|
1657
1682
|
const { res, text, json } = await httpJson('GET', url, { headers: { Authorization: `Bearer ${token}` } });
|
|
1658
1683
|
if (!res.ok) {
|
|
@@ -1671,12 +1696,15 @@ async function waitForHandoff(id, args, { token, apiBase }, initialDeliveryState
|
|
|
1671
1696
|
else printHandoff(resolved);
|
|
1672
1697
|
return exitForHandoffState(resolved.state);
|
|
1673
1698
|
}
|
|
1674
|
-
// Still open/pending at the hold timeout — poll again
|
|
1699
|
+
// Still open/pending at the hold timeout — poll again, with the same
|
|
1700
|
+
// hot-loop floor as waitForResolution.
|
|
1701
|
+
const elapsed = Date.now() - started;
|
|
1702
|
+
if (elapsed < 1000) await sleep(1000 - elapsed);
|
|
1675
1703
|
}
|
|
1676
1704
|
}
|
|
1677
1705
|
|
|
1678
1706
|
async function handoff(args) {
|
|
1679
|
-
if (args.help) { process.stdout.write(`${
|
|
1707
|
+
if (args.help) { process.stdout.write(`${commandHelp('handoff')}\n`); return EXIT.OK; }
|
|
1680
1708
|
|
|
1681
1709
|
const message = args.message;
|
|
1682
1710
|
if (!message) fail('a --message is required', EXIT.USAGE);
|
|
@@ -1726,6 +1754,9 @@ async function handoff(args) {
|
|
|
1726
1754
|
headers['Idempotency-Key'] = args.idempotency_key;
|
|
1727
1755
|
}
|
|
1728
1756
|
|
|
1757
|
+
// Pre-flight: reject a bad --timeout before the handoff exists.
|
|
1758
|
+
if (args.wait) resolveWaitHold(args, { def: 20, cap: 25 });
|
|
1759
|
+
|
|
1729
1760
|
const url = `${apiBase}/api/agent/handoffs`;
|
|
1730
1761
|
const { res, text, json } = await httpJson('POST', url, { body, headers });
|
|
1731
1762
|
if (!res.ok) {
|
|
@@ -1765,9 +1796,8 @@ async function handoff(args) {
|
|
|
1765
1796
|
// normal local prompt (PreToolUse -> permissionDecision "ask") and exits 0. It
|
|
1766
1797
|
// must not call fail() (a non-zero exit — 2 especially — would break the run).
|
|
1767
1798
|
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
const alias = {
|
|
1799
|
+
const parseHookArgs = makeParser({
|
|
1800
|
+
aliases: {
|
|
1771
1801
|
'--room': 'room',
|
|
1772
1802
|
'--ttl': 'ttl',
|
|
1773
1803
|
'--quiet': 'quiet',
|
|
@@ -1776,27 +1806,10 @@ function parseHookArgs(argv) {
|
|
|
1776
1806
|
'--api': 'api',
|
|
1777
1807
|
'--json': 'json',
|
|
1778
1808
|
'-h': 'help', '--help': 'help',
|
|
1779
|
-
}
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
const token = argv[i];
|
|
1784
|
-
// hasOwn, not a bare lookup — see parseArgs: an inherited key would swallow args.
|
|
1785
|
-
const key = Object.hasOwn(alias, token) ? alias[token] : undefined;
|
|
1786
|
-
if (key && booleans.has(key)) {
|
|
1787
|
-
args[key] = true;
|
|
1788
|
-
} else if (key) {
|
|
1789
|
-
const value = argv[++i];
|
|
1790
|
-
if (value === undefined) fail(`option ${token} needs a value`, EXIT.USAGE);
|
|
1791
|
-
args[key] = value;
|
|
1792
|
-
} else if (token.startsWith('-') && token !== '-') {
|
|
1793
|
-
fail(`Unknown option: ${token}`, EXIT.USAGE);
|
|
1794
|
-
} else {
|
|
1795
|
-
args._.push(token);
|
|
1796
|
-
}
|
|
1797
|
-
}
|
|
1798
|
-
return args;
|
|
1799
|
-
}
|
|
1809
|
+
},
|
|
1810
|
+
booleans: ['quiet', 'print_config', 'json', 'help'],
|
|
1811
|
+
bareDashIsPositional: true,
|
|
1812
|
+
});
|
|
1800
1813
|
|
|
1801
1814
|
// Read all of stdin as a string. Resolves '' when nothing is piped (TTY), so a
|
|
1802
1815
|
// stray `pingroom hook` in a terminal is a silent no-op rather than a hang.
|
|
@@ -2058,7 +2071,7 @@ ${JSON.stringify(config, null, 2)}
|
|
|
2058
2071
|
}
|
|
2059
2072
|
|
|
2060
2073
|
async function hook(args) {
|
|
2061
|
-
if (args.help) { process.stdout.write(`${
|
|
2074
|
+
if (args.help) { process.stdout.write(`${commandHelp('hook')}\n`); return EXIT.OK; }
|
|
2062
2075
|
if (args.print_config) { printHookConfig(); return EXIT.OK; }
|
|
2063
2076
|
|
|
2064
2077
|
let event = {};
|
|
@@ -2574,7 +2587,7 @@ async function activateInboxAfterPairing(cred) {
|
|
|
2574
2587
|
|
|
2575
2588
|
/** Retry activation only for the durable credential created by QR pairing. */
|
|
2576
2589
|
async function activateStoredInbox(args) {
|
|
2577
|
-
if (args.help) { process.stdout.write(`${
|
|
2590
|
+
if (args.help) { process.stdout.write(`${commandHelp('activate')}\n`); return EXIT.OK; }
|
|
2578
2591
|
if (args._.length > 0) fail('usage: pingroom activate', EXIT.USAGE);
|
|
2579
2592
|
if (args.token !== undefined) {
|
|
2580
2593
|
fail('pingroom activate uses the saved QR-paired credential; remove --token', EXIT.USAGE);
|
|
@@ -2927,7 +2940,7 @@ const CONFIG_KEYS = {
|
|
|
2927
2940
|
};
|
|
2928
2941
|
|
|
2929
2942
|
async function config(args) {
|
|
2930
|
-
if (args.help) { process.stdout.write(`${
|
|
2943
|
+
if (args.help) { process.stdout.write(`${commandHelp('config')}\n`); return EXIT.OK; }
|
|
2931
2944
|
|
|
2932
2945
|
const sub = args._[0];
|
|
2933
2946
|
const known = ['list', 'get', 'set'];
|
|
@@ -2985,7 +2998,7 @@ async function config(args) {
|
|
|
2985
2998
|
// --- logout ----------------------------------------------------------------
|
|
2986
2999
|
|
|
2987
3000
|
async function logout(args) {
|
|
2988
|
-
if (args.help) { process.stdout.write(`${
|
|
3001
|
+
if (args.help) { process.stdout.write(`${commandHelp('logout')}\n`); return EXIT.OK; }
|
|
2989
3002
|
|
|
2990
3003
|
const path = credentialsPath();
|
|
2991
3004
|
const stored = readStoredCredential();
|
|
@@ -3007,6 +3020,33 @@ async function logout(args) {
|
|
|
3007
3020
|
return EXIT.OK;
|
|
3008
3021
|
}
|
|
3009
3022
|
|
|
3023
|
+
// config/logout/handoffs used to share parseQArgs, which silently accepted and
|
|
3024
|
+
// ignored flags those commands never read (`logout --wait --prompt x`). Minimal
|
|
3025
|
+
// tables instead, so an irrelevant flag is a usage error like everywhere else.
|
|
3026
|
+
const parseConfigArgs = makeParser({
|
|
3027
|
+
aliases: { '--json': 'json', '-h': 'help', '--help': 'help' },
|
|
3028
|
+
booleans: ['json', 'help'],
|
|
3029
|
+
bareDashIsPositional: true,
|
|
3030
|
+
});
|
|
3031
|
+
|
|
3032
|
+
const parseLogoutArgs = makeParser({
|
|
3033
|
+
aliases: { '-h': 'help', '--help': 'help' },
|
|
3034
|
+
booleans: ['help'],
|
|
3035
|
+
bareDashIsPositional: true,
|
|
3036
|
+
});
|
|
3037
|
+
|
|
3038
|
+
const parseHandoffsArgs = makeParser({
|
|
3039
|
+
aliases: {
|
|
3040
|
+
'--state': 'state',
|
|
3041
|
+
'--token': 'token',
|
|
3042
|
+
'--api': 'api',
|
|
3043
|
+
'--json': 'json',
|
|
3044
|
+
'-h': 'help', '--help': 'help',
|
|
3045
|
+
},
|
|
3046
|
+
booleans: ['json', 'help'],
|
|
3047
|
+
bareDashIsPositional: true,
|
|
3048
|
+
});
|
|
3049
|
+
|
|
3010
3050
|
const COMMANDS = {
|
|
3011
3051
|
ping: (rest) => ping(parseArgs(rest)),
|
|
3012
3052
|
ask: (rest) => ask(parseQArgs(rest)),
|
|
@@ -3015,14 +3055,14 @@ const COMMANDS = {
|
|
|
3015
3055
|
cancel: (rest) => cancel(parseQArgs(rest)),
|
|
3016
3056
|
list: (rest) => list(parseQArgs(rest)),
|
|
3017
3057
|
handoff: (rest) => handoff(parseHandoffArgs(rest)),
|
|
3018
|
-
handoffs: (rest) => listHandoffs(
|
|
3058
|
+
handoffs: (rest) => listHandoffs(parseHandoffsArgs(rest)),
|
|
3019
3059
|
listen: (rest) => listen(parseQArgs(rest)),
|
|
3020
3060
|
hook: (rest) => hook(parseHookArgs(rest)),
|
|
3021
3061
|
mcp,
|
|
3022
3062
|
activate: (rest) => activateStoredInbox(parseQArgs(rest)),
|
|
3023
3063
|
live: (rest) => live(parseLiveArgs(rest)),
|
|
3024
|
-
config: (rest) => config(
|
|
3025
|
-
logout: (rest) => logout(
|
|
3064
|
+
config: (rest) => config(parseConfigArgs(rest)),
|
|
3065
|
+
logout: (rest) => logout(parseLogoutArgs(rest)),
|
|
3026
3066
|
};
|
|
3027
3067
|
|
|
3028
3068
|
function waitFrom(handler, rest) {
|