run402 4.33.2 → 4.35.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.
@@ -1,593 +1,74 @@
1
1
  /**
2
- * run402 notifications — Operator health notifications + Telegram channel/rules.
2
+ * `run402 notifications`RETIRED (legible-cli-surface).
3
3
  *
4
- * Wraps the v1.55 `add-operator-health-notifications` API surface:
4
+ * The group was two nouns in one coat: `list`/`get` read delivery RECORDS
5
+ * while `preferences`/`channels`/`rules` were the CONFIGURATION producing
6
+ * them. It has been split along that line:
5
7
  *
6
- * - run402 notifications list [--type ...] [--since ...] [--limit N] [--after <cursor>]
7
- * - run402 notifications get <id>
8
- * - run402 notifications preferences [get|set ...]
9
- * - run402 notifications test [--source app|platform] [--type <event_type>]
10
- * - run402 webhook-secret rotate
8
+ * notifications list|get -> deliveries list|get
9
+ * notifications channels -> contacts (merged with escalations contacts)
10
+ * notifications rules -> subscriptions
11
+ * notifications preferences -> contacts preferences
11
12
  *
12
- * ...and the `notification-channel-routing-telegram` cascade (self-serve
13
- * Telegram push + per-rule routing on top of the substrate above):
14
- *
15
- * - run402 notifications channels connect telegram [--label <name>]
16
- * - run402 notifications channels list
17
- * - run402 notifications channels revoke <binding_id>
18
- * - run402 notifications rules add --binding <id> [--project <id>] [--source app|platform] [--type a,b] [--class a,b]
19
- * - run402 notifications rules list
20
- * - run402 notifications rules rm <rule_id>
21
- *
22
- * All commands use the allowance-wallet auth path (SIWX). The assurance
23
- * ladder is enforced server-side; CLI surfaces 403/412/503 errors verbatim
24
- * (message + code + next_actions) via reportSdkError — no client-side
25
- * special-casing needed, e.g. a not-yet-provisioned Telegram bot surfaces as
26
- * 503 TELEGRAM_CHANNEL_NOT_CONFIGURED with its next_actions intact.
27
- *
28
- * `channels`/`rules` are nested groups dispatched via `if (sub === ...)`
29
- * (not `case`), mirroring cli/lib/org.mjs's `member`/`invite` groups — see
30
- * sync.test.ts's parseNotificationsGroupActions for why.
13
+ * Reserved, not aliased (design D3): each subcommand names its successor so
14
+ * one failed call teaches the new model, where an alias would teach the old
15
+ * one forever.
31
16
  */
17
+ import { fail } from "./sdk-errors.mjs";
18
+ import { hasHelp } from "./argparse.mjs";
32
19
 
33
- import { allowanceAuthHeaders } from "./config.mjs";
34
- import { getSdk } from "./sdk.mjs";
35
- import { reportSdkError, fail } from "./sdk-errors.mjs";
36
- import {
37
- assertAllowedValue,
38
- assertKnownFlags,
39
- flagValue,
40
- normalizeArgv,
41
- positionalArgs,
42
- requirePositionalCount,
43
- failUnknownSubcommand,
44
- } from "./argparse.mjs";
45
-
46
- const HELP = `run402 notifications — Operator health notifications + Telegram channel/rules
47
-
48
- Usage:
49
- run402 notifications list [--type <event_type>] [--since <iso>] [--limit N] [--after <cursor>]
50
- run402 notifications get <id>
51
- run402 notifications preferences
52
- run402 notifications preferences set <key>=<value> [<key>=<value> ...]
53
- run402 notifications test [--source app|platform] [--type <event_type>]
54
- run402 notifications channels connect telegram [--label <name>]
55
- run402 notifications channels list
56
- run402 notifications channels revoke <binding_id>
57
- run402 notifications rules add --binding <binding_id> [--project <id>] [--source app|platform] [--type a,b] [--class a,b]
58
- run402 notifications rules list
59
- run402 notifications rules rm <rule_id>
60
-
61
- Examples:
62
- run402 notifications list --limit 10
63
- run402 notifications list --type project_past_due --since 2026-05-01T00:00:00Z
64
- run402 notifications get 9b21fa0a-...
65
- run402 notifications preferences
66
- run402 notifications preferences set digest_cadence=weekly digest_day_of_week=1
67
- run402 notifications preferences set webhook_url=https://my-receiver.example.com/hook
68
- run402 notifications test
69
- run402 notifications test --source app --type signature_failed
70
- run402 notifications channels connect telegram --label "kychon alerts"
71
- run402 notifications channels list
72
- run402 notifications rules add --binding bnd_1 --project prj_abc --source app --type signature_failed
73
-
74
- Auth ladder (enforced server-side):
75
- - SIWX wallet: read own notifications + preferences
76
- - email_verified: cross-wallet rollup reads + multi-wallet preference updates
77
- - operator_passkey: webhook URL changes, webhook secret rotation, Telegram
78
- channel connect/revoke, routing rule add/rm (channel
79
- connect additionally requires a VERIFIED operator email)
80
-
81
- Telegram channels + routing rules teach their own rule model in detail via
82
- 'run402 notifications channels --help' / 'run402 notifications rules --help'
83
- (AND'd match dimensions, wildcards, one rule -> one chat, no rules -> no
84
- Telegram traffic).
85
- `;
86
-
87
- const CHANNELS_HELP = `run402 notifications channels — Telegram channel bindings
88
-
89
- Usage:
90
- run402 notifications channels connect telegram [--label <name>]
91
- run402 notifications channels list
92
- run402 notifications channels revoke <binding_id>
93
-
94
- connect telegram:
95
- Creates a PENDING binding and prints two single-use, 15-minute deep links:
96
- a private-chat link and a group-chat link. Open ONE of them in Telegram and
97
- tap Start — whichever is tapped first consumes the code. This command then
98
- POLLS 'channels list' (every few seconds, printing progress to stderr)
99
- until the binding flips to active, or the code expires, whichever happens
100
- first. Requires operator_passkey assurance AND a VERIFIED operator email
101
- (bindings are addressed to it — see 'run402 agent verify-email').
102
-
103
- --label <name> Human-readable name for the chat (e.g. "kychon alerts"),
104
- 1-64 characters.
20
+ const MOVED = {
21
+ list: "deliveries list",
22
+ get: "deliveries get",
23
+ channels: "contacts",
24
+ rules: "subscriptions",
25
+ preferences: "contacts preferences",
26
+ test: "contacts preferences",
27
+ };
105
28
 
106
- list:
107
- Shows every notification channel — email, webhook, and every live
108
- (non-revoked) Telegram binding — for the authenticated wallet.
109
-
110
- revoke <binding_id>:
111
- Revokes a Telegram binding. Requires operator_passkey assurance. A
112
- missing / already-revoked / another operator's binding id all return the
113
- same not-found error (no existence oracle).
114
-
115
- Until the platform's dedicated Telegram bot is provisioned on this
116
- deployment, 'connect telegram' returns 503 TELEGRAM_CHANNEL_NOT_CONFIGURED
117
- with a next_actions entry pointing at the operator to finish setup.
118
-
119
- Examples:
120
- run402 notifications channels connect telegram
121
- run402 notifications channels connect telegram --label "kychon alerts"
122
- run402 notifications channels list
123
- run402 notifications channels revoke bnd_1a2b3c
124
- `;
125
-
126
- const RULES_HELP = `run402 notifications rules — Telegram routing rules
29
+ const HELP = `run402 notifications — SPLIT into three nouns
127
30
 
128
31
  Usage:
129
- run402 notifications rules add --binding <binding_id> [--project <id>] [--source app|platform] [--type a,b] [--class a,b]
130
- run402 notifications rules list
131
- run402 notifications rules rm <rule_id>
132
-
133
- The rule model:
134
- - One rule routes to exactly ONE Telegram binding (one chat) — "N
135
- destinations" is N rules, never a fan-out list on a single rule.
136
- - Every match dimension you set is ANDed: --project + --source + --type +
137
- --class must ALL match an event for the rule to fire.
138
- - An OMITTED dimension is a WILDCARD (matches anything for that
139
- dimension). 'rules add --binding bnd_1' with no other flags matches
140
- EVERY event routed to that operator.
141
- - --type and --class accept comma-separated lists and match if the event
142
- is ANY of the listed values, e.g.
143
- --type signature_failed,signature_expired.
144
- - --source is "app" (your deployed function's events.emit(...) calls from
145
- @run402/functions) or "platform" (deploys, lifecycle, verification,
146
- ...). Omit it to match both.
147
- - Multiple rules OR together across an event; if two of YOUR rules both
148
- resolve to the SAME binding for one event, only one message is sent
149
- (deduped per chat, not per rule).
150
- - NO RULES = NO TELEGRAM TRAFFIC for that operator. The Telegram channel
151
- is opt-in per event, per rule — there is no "send everything" default,
152
- unlike the always-on email channel.
153
- - Rules govern the Telegram channel ONLY (v1). The mandatory email floor
154
- (security / recovery / billing_critical / destructive_lifecycle /
155
- verification classes) is completely untouched by this command and can
156
- NEVER be silenced by adding or omitting a rule.
157
-
158
- Requires operator_passkey assurance for add/rm. 'rules add' rejects an
159
- unusable telegram_binding_id (revoked / not yours / nonexistent) with the
160
- same 404 either way (authorize-before-reveal) — run 'channels list' first to
161
- confirm the binding is 'active'.
162
-
163
- Examples:
164
- run402 notifications rules add --binding bnd_1
165
- run402 notifications rules add --binding bnd_1 --project prj_abc --source app --type signature_failed
166
- run402 notifications rules add --binding bnd_1 --class security,recovery
167
- run402 notifications rules list
168
- run402 notifications rules rm rule_9b21fa0a
32
+ run402 deliveries <list|get> delivery records did it land
33
+ run402 contacts <list|add|connect|rm|preferences|test>
34
+ where a human is reachable
35
+ run402 subscriptions <add|list|rm> which events go where
36
+
37
+ The group mixed delivery RECORDS with the CONFIGURATION that produces them.
38
+ Where everything went:
39
+
40
+ notifications list|get -> run402 deliveries list|get
41
+ notifications channels -> run402 contacts (merged with the
42
+ escalation paging ladder one question:
43
+ where is a human reachable)
44
+ notifications rules -> run402 subscriptions
45
+ notifications preferences -> run402 contacts preferences
46
+ notifications test -> run402 contacts test
47
+
48
+ Run \`run402 deliveries --help\`, \`run402 contacts --help\`, or
49
+ \`run402 subscriptions --help\`.
169
50
  `;
170
51
 
171
- // ---------------------------------------------------------------------------
172
- // Operator health notifications (v1.55) — list / get / preferences / test.
173
- // ---------------------------------------------------------------------------
174
-
175
- async function list(args) {
176
- const parsedArgs = normalizeArgv(args);
177
- const valueFlags = ["--type", "--since", "--limit", "--after"];
178
- assertKnownFlags(parsedArgs, [...valueFlags, "--help", "-h"], valueFlags);
179
- const extra = positionalArgs(parsedArgs, valueFlags);
180
- if (extra.length > 0) {
181
- fail({ code: "BAD_USAGE", message: `Unexpected argument for notifications list: ${extra[0]}` });
182
- }
183
- allowanceAuthHeaders("/agent/v1/notifications");
184
- const opts = {};
185
- const type = flagValue(parsedArgs, "--type");
186
- const since = flagValue(parsedArgs, "--since");
187
- const limit = flagValue(parsedArgs, "--limit");
188
- const after = flagValue(parsedArgs, "--after");
189
- if (type) opts.type = type;
190
- if (since) opts.since = since;
191
- if (limit != null) opts.limit = Number(limit);
192
- if (after != null) opts.after = after;
193
- try {
194
- const data = await getSdk().admin.listNotifications(opts);
195
- console.log(JSON.stringify(data, null, 2));
196
- } catch (err) {
197
- reportSdkError(err);
198
- }
199
- }
200
-
201
- async function get(args) {
202
- const parsedArgs = normalizeArgv(args);
203
- assertKnownFlags(parsedArgs, ["--help", "-h"]);
204
- const positionals = positionalArgs(parsedArgs);
205
- if (positionals.length !== 1) {
206
- fail({ code: "BAD_USAGE", message: "Usage: run402 notifications get <id>" });
207
- }
208
- allowanceAuthHeaders("/agent/v1/notifications");
209
- try {
210
- const data = await getSdk().admin.getNotification(positionals[0]);
211
- console.log(JSON.stringify(data, null, 2));
212
- } catch (err) {
213
- reportSdkError(err);
214
- }
215
- }
216
-
217
- async function preferences(args) {
218
- const parsedArgs = normalizeArgv(args);
219
- assertKnownFlags(parsedArgs, ["--help", "-h"]);
220
- const positionals = positionalArgs(parsedArgs);
221
- allowanceAuthHeaders("/agent/v1/notifications/preferences");
222
-
223
- if (positionals.length === 0) {
224
- // GET preferences
225
- try {
226
- const data = await getSdk().admin.getNotificationPreferences();
227
- console.log(JSON.stringify(data, null, 2));
228
- } catch (err) {
229
- reportSdkError(err);
230
- }
231
- return;
232
- }
233
-
234
- if (positionals[0] !== "set") {
235
- fail({ code: "BAD_USAGE", message: "Usage: run402 notifications preferences [set <key>=<value> ...]" });
236
- }
237
-
238
- // SET — parse remaining positional args as key=value
239
- const patch = {};
240
- for (const kv of positionals.slice(1)) {
241
- const eq = kv.indexOf("=");
242
- if (eq <= 0) {
243
- fail({ code: "BAD_USAGE", message: `Expected key=value, got: ${kv}` });
244
- }
245
- const key = kv.slice(0, eq);
246
- const rawValue = kv.slice(eq + 1);
247
- if (key === "digest_day_of_week" || key === "digest_hour_utc") {
248
- patch[key] = Number(rawValue);
249
- } else if (key === "webhook_url" && (rawValue === "null" || rawValue === "")) {
250
- patch[key] = null;
251
- } else {
252
- patch[key] = rawValue;
253
- }
254
- }
255
- try {
256
- const data = await getSdk().admin.setNotificationPreferences(patch);
257
- console.log(JSON.stringify(data, null, 2));
258
- } catch (err) {
259
- reportSdkError(err);
260
- }
261
- }
262
-
263
- async function test(args) {
264
- const parsedArgs = normalizeArgv(args);
265
- const valueFlags = ["--source", "--type"];
266
- assertKnownFlags(parsedArgs, [...valueFlags, "--help", "-h"], valueFlags);
267
- const extra = positionalArgs(parsedArgs, valueFlags);
268
- if (extra.length > 0) {
269
- fail({ code: "BAD_USAGE", message: `Unexpected argument for notifications test: ${extra[0]}` });
270
- }
271
- const source = flagValue(parsedArgs, "--source");
272
- if (source !== null) assertAllowedValue(source, ["app", "platform"], "--source");
273
- const eventType = flagValue(parsedArgs, "--type");
274
- allowanceAuthHeaders("/agent/v1/notifications/test");
275
- const opts = {};
276
- if (source) opts.source = source;
277
- if (eventType) opts.eventType = eventType;
278
- try {
279
- const data = await getSdk().admin.testNotification(opts);
280
- console.log(JSON.stringify(data, null, 2));
281
- } catch (err) {
282
- reportSdkError(err);
283
- }
284
- }
285
-
286
- // ---------------------------------------------------------------------------
287
- // Telegram channel bindings — notification-channel-routing-telegram.
288
- // ---------------------------------------------------------------------------
289
-
290
- const TELEGRAM_CONNECT_POLL_INTERVAL_MS = 3000;
291
- /** Fallback wait bound if the server's code_expires_at is somehow
292
- * unparseable — the connect code's real TTL is 15 minutes server-side. */
293
- const TELEGRAM_CONNECT_FALLBACK_TIMEOUT_MS = 20 * 60 * 1000;
294
- /** Print a progress line every Nth poll tick (~15s at the interval above) —
295
- * frequent enough to reassure, not so frequent it floods stderr. */
296
- const TELEGRAM_CONNECT_PROGRESS_EVERY_N_TICKS = 5;
297
-
298
- function sleep(ms) {
299
- return new Promise((resolve) => setTimeout(resolve, ms));
300
- }
301
-
302
- /**
303
- * Poll `GET /agent/v1/notifications/channels` until `bindingId` shows
304
- * `status: "active"`, or the connect code's `codeExpiresAt` passes.
305
- * Transient poll failures (network blips) are logged and retried rather
306
- * than aborting the whole connect flow — bounded by the same expiry.
307
- */
308
- async function pollTelegramBindingActive(bindingId, codeExpiresAt) {
309
- const parsedExpiry = Date.parse(codeExpiresAt);
310
- const deadlineMs = Number.isFinite(parsedExpiry) ? parsedExpiry : Date.now() + TELEGRAM_CONNECT_FALLBACK_TIMEOUT_MS;
311
- let tick = 0;
312
- for (;;) {
313
- await sleep(TELEGRAM_CONNECT_POLL_INTERVAL_MS);
314
- tick += 1;
315
- let channels;
316
- try {
317
- channels = await getSdk().admin.channels.list();
318
- } catch (err) {
319
- console.error(` (poll failed, retrying: ${err?.message || err})`);
320
- if (Date.now() >= deadlineMs) return { active: false, timedOut: true, binding: null };
321
- continue;
322
- }
323
- const binding = (channels.telegram || []).find((b) => b.id === bindingId) || null;
324
- if (binding && binding.status === "active") {
325
- return { active: true, binding };
326
- }
327
- if (Date.now() >= deadlineMs) {
328
- return { active: false, timedOut: true, binding };
329
- }
330
- if (tick % TELEGRAM_CONNECT_PROGRESS_EVERY_N_TICKS === 1) {
331
- const remainingSec = Math.max(0, Math.round((deadlineMs - Date.now()) / 1000));
332
- const status = binding ? binding.status : "pending";
333
- console.error(` waiting for you to tap the link in Telegram... (status: ${status}, ${remainingSec}s left)`);
334
- }
335
- }
336
- }
337
-
338
- async function channelsConnect(args) {
339
- const a = normalizeArgv(args);
340
- assertKnownFlags(a, ["--label", "--help", "-h"], ["--label"]);
341
- const label = flagValue(a, "--label");
342
- const positionals = positionalArgs(a, ["--label"]);
343
- if (positionals.length === 0) {
344
- fail({
345
- code: "BAD_USAGE",
346
- message: "Usage: run402 notifications channels connect telegram [--label <name>]",
347
- });
348
- }
349
- if (positionals[0] !== "telegram") {
350
- fail({
351
- code: "BAD_USAGE",
352
- message: `Unknown channel type: ${positionals[0]}. Only 'telegram' is supported.`,
353
- details: { channel_type: positionals[0] },
354
- });
355
- }
356
- if (positionals.length > 1) {
357
- fail({ code: "BAD_USAGE", message: `Unexpected argument for notifications channels connect: ${positionals[1]}` });
358
- }
359
-
360
- allowanceAuthHeaders("/agent/v1/notifications/channels/telegram");
361
- let pending;
362
- try {
363
- pending = await getSdk().admin.channels.connectTelegram(label ? { label } : {});
364
- } catch (err) {
365
- reportSdkError(err);
366
- return;
367
- }
368
-
369
- console.error("");
370
- console.error("Open ONE of these links in Telegram and tap Start to connect this channel:");
371
- console.error(` Private chat: ${pending.connect_url}`);
372
- console.error(` Group chat: ${pending.connect_group_url}`);
373
- console.error(` Expires: ${pending.code_expires_at}`);
374
- console.error("");
375
- console.error("Waiting for you to tap the link...");
376
-
377
- const outcome = await pollTelegramBindingActive(pending.binding_id, pending.code_expires_at);
378
- if (outcome.active) {
379
- console.error("Connected.");
380
- console.log(JSON.stringify({ ...pending, connected: true, binding: outcome.binding }, null, 2));
381
- return;
382
- }
383
- console.error("Timed out waiting for you to tap the link — the connect code has expired.");
384
- console.error("Run `run402 notifications channels connect telegram` again for a fresh link.");
385
- console.log(JSON.stringify({ ...pending, connected: false, timed_out: true }, null, 2));
386
- process.exit(1);
387
- }
388
-
389
- async function channelsList() {
390
- allowanceAuthHeaders("/agent/v1/notifications/channels");
391
- try {
392
- console.log(JSON.stringify(await getSdk().admin.channels.list(), null, 2));
393
- } catch (err) {
394
- reportSdkError(err);
395
- }
396
- }
397
-
398
- async function channelsRevoke(args) {
399
- const a = normalizeArgv(args);
400
- assertKnownFlags(a, ["--help", "-h"]);
401
- const [bindingId] = requirePositionalCount(a, [], {
402
- min: 1,
403
- max: 1,
404
- command: "run402 notifications channels revoke <binding_id>",
405
- missing: "Missing <binding_id>.",
406
- });
407
- allowanceAuthHeaders("/agent/v1/notifications/channels/telegram");
408
- try {
409
- console.log(JSON.stringify(await getSdk().admin.channels.revokeTelegram(bindingId), null, 2));
410
- } catch (err) {
411
- reportSdkError(err);
412
- }
413
- }
414
-
415
- async function runChannels(args) {
416
- const channelsAction = args[0];
417
- const rest = args.slice(1);
418
- if (!channelsAction || channelsAction === "--help" || channelsAction === "-h") {
419
- console.log(CHANNELS_HELP);
420
- process.exit(channelsAction ? 0 : 1);
421
- }
422
- if (rest.includes("--help") || rest.includes("-h")) {
423
- console.log(CHANNELS_HELP);
424
- process.exit(0);
425
- }
426
-
427
- if (channelsAction === "connect") {
428
- await channelsConnect(rest);
429
- return;
430
- }
431
- if (channelsAction === "list") {
432
- await channelsList();
433
- return;
434
- }
435
- if (channelsAction === "revoke") {
436
- await channelsRevoke(rest);
437
- return;
438
- }
439
- failUnknownSubcommand("notifications channels", channelsAction, {
440
- hint: "Run `run402 notifications channels --help` for usage.",
441
- });
442
- }
443
-
444
- // ---------------------------------------------------------------------------
445
- // Telegram routing rules — notification-channel-routing-telegram.
446
- // ---------------------------------------------------------------------------
447
-
448
- function splitCsv(value) {
449
- return value
450
- .split(",")
451
- .map((s) => s.trim())
452
- .filter((s) => s.length > 0);
453
- }
454
-
455
- async function rulesAdd(args) {
456
- const a = normalizeArgv(args);
457
- const valueFlags = ["--binding", "--project", "--source", "--type", "--class"];
458
- assertKnownFlags(a, [...valueFlags, "--help", "-h"], valueFlags);
459
- const positionals = positionalArgs(a, valueFlags);
460
- if (positionals.length > 0) {
461
- fail({ code: "BAD_USAGE", message: `Unexpected argument for notifications rules add: ${positionals[0]}` });
462
- }
463
-
464
- const bindingId = flagValue(a, "--binding");
465
- if (!bindingId) {
466
- fail({
467
- code: "BAD_USAGE",
468
- message:
469
- "Usage: run402 notifications rules add --binding <binding_id> [--project <id>] [--source app|platform] [--type a,b] [--class a,b]",
470
- });
471
- }
472
- const projectId = flagValue(a, "--project");
473
- const source = flagValue(a, "--source");
474
- if (source !== null) assertAllowedValue(source, ["app", "platform"], "--source");
475
- const typeRaw = flagValue(a, "--type");
476
- const classRaw = flagValue(a, "--class");
477
-
478
- const input = { telegramBindingId: bindingId };
479
- if (projectId) input.projectId = projectId;
480
- if (source) input.source = source;
481
- if (typeRaw !== null) input.eventTypes = splitCsv(typeRaw);
482
- if (classRaw !== null) input.classes = splitCsv(classRaw);
483
-
484
- allowanceAuthHeaders("/agent/v1/notifications/rules");
485
- try {
486
- console.log(JSON.stringify(await getSdk().admin.rules.create(input), null, 2));
487
- } catch (err) {
488
- reportSdkError(err);
489
- }
490
- }
491
-
492
- async function rulesList() {
493
- allowanceAuthHeaders("/agent/v1/notifications/rules");
494
- try {
495
- console.log(JSON.stringify(await getSdk().admin.rules.list(), null, 2));
496
- } catch (err) {
497
- reportSdkError(err);
498
- }
499
- }
500
-
501
- async function rulesRm(args) {
502
- const a = normalizeArgv(args);
503
- assertKnownFlags(a, ["--help", "-h"]);
504
- const [ruleId] = requirePositionalCount(a, [], {
505
- min: 1,
506
- max: 1,
507
- command: "run402 notifications rules rm <rule_id>",
508
- missing: "Missing <rule_id>.",
509
- });
510
- allowanceAuthHeaders("/agent/v1/notifications/rules");
511
- try {
512
- console.log(JSON.stringify(await getSdk().admin.rules.delete(ruleId), null, 2));
513
- } catch (err) {
514
- reportSdkError(err);
515
- }
516
- }
517
-
518
- async function runRules(args) {
519
- const rulesAction = args[0];
520
- const rest = args.slice(1);
521
- if (!rulesAction || rulesAction === "--help" || rulesAction === "-h") {
522
- console.log(RULES_HELP);
523
- process.exit(rulesAction ? 0 : 1);
524
- }
525
- if (rest.includes("--help") || rest.includes("-h")) {
526
- console.log(RULES_HELP);
527
- process.exit(0);
528
- }
529
-
530
- if (rulesAction === "add") {
531
- await rulesAdd(rest);
532
- return;
533
- }
534
- if (rulesAction === "list") {
535
- await rulesList();
536
- return;
537
- }
538
- if (rulesAction === "rm") {
539
- await rulesRm(rest);
540
- return;
541
- }
542
- failUnknownSubcommand("notifications rules", rulesAction, {
543
- hint: "Run `run402 notifications rules --help` for usage.",
544
- });
545
- }
546
-
547
- // ---------------------------------------------------------------------------
548
- // Dispatch.
549
- // ---------------------------------------------------------------------------
550
-
551
52
  export async function run(sub, args) {
552
- if (!sub || sub === "--help" || sub === "-h") {
553
- console.log(HELP);
554
- process.exit(0);
555
- }
556
- // Nested groups use `if (sub === ...)` (not `case`) so the sync test
557
- // extracts their leaf actions via the dedicated channelsAction/rulesAction
558
- // parsers, mirroring cli/lib/org.mjs's member/invite groups.
559
- if (sub === "channels") {
560
- await runChannels(args ?? []);
561
- return;
562
- }
563
- if (sub === "rules") {
564
- await runRules(args ?? []);
565
- return;
566
- }
567
- // Flat subcommands share the module-level HELP: --help/-h anywhere in the
568
- // argv must short-circuit BEFORE the handlers touch auth/config (the
569
- // cli-help.test.mjs contract) — same pre-switch check as cli/lib/org.mjs.
570
- if (Array.isArray(args) && (args.includes("--help") || args.includes("-h"))) {
53
+ // `--help` is a QUESTION, and the most useful answer to "help me with
54
+ // notifications" is where everything moved — not a failure. Actually
55
+ // invoking a moved subcommand still fails, naming its successor.
56
+ if (!sub || hasHelp([sub, ...(Array.isArray(args) ? args : [])])) {
571
57
  console.log(HELP);
572
58
  process.exit(0);
573
59
  }
574
- switch (sub) {
575
- case "list":
576
- await list(args);
577
- return;
578
- case "get":
579
- await get(args);
580
- return;
581
- case "preferences":
582
- await preferences(args);
583
- return;
584
- case "test":
585
- await test(args);
586
- return;
587
- default:
588
- failUnknownSubcommand("notifications", sub);
589
- }
60
+ const now = MOVED[sub];
61
+ fail({
62
+ code: "COMMAND_REMOVED",
63
+ message: now
64
+ ? `\`run402 notifications ${sub}\` moved to \`run402 ${now}\`.`
65
+ : "`run402 notifications` was split into `deliveries`, `contacts` and `subscriptions`.",
66
+ hint: now ? `run402 ${now}` : "run402 deliveries --help | run402 contacts --help | run402 subscriptions --help",
67
+ details: {
68
+ was: `notifications${sub ? ` ${sub}` : ""}`,
69
+ now: now ?? null,
70
+ why: "the group mixed delivery RECORDS with the CONFIGURATION that produces them",
71
+ moved: MOVED,
72
+ },
73
+ });
590
74
  }
591
-
592
- // `run402 webhook-secret rotate` lives in cli/lib/webhook-secret.mjs as
593
- // its own top-level command, separate from this notifications module.