@audienti/cli 0.1.61 → 0.1.62
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/CHANGELOG.md +8 -0
- package/README.md +30 -10
- package/package.json +1 -1
- package/skills/audienti/SKILL.md +7 -0
- package/src/api-client.js +7 -0
- package/src/cli.js +475 -21
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,14 @@ All notable changes to the Audienti CLI are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.1.62] - 2026-09-10
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `inbox-ops queue` now follows every page, numbers each row, saves the numbered list locally per account, and offers `--group-by domain`.
|
|
12
|
+
- Add bulk `inbox-ops ignore`, `filter-sender`, `filter-domain`, `allow-sender`, and `allow-domain` verbs that select rows by number, range, row id, `--domain`, or `--sender`, print a manifest, validate with `--dry-run`, and apply with `--yes` through the new `POST inbox_ops/actions` API with per-row results.
|
|
13
|
+
- Add `inbox_ops.queue`, `inbox_ops.filters`, `inbox_ops.actions`, `inbox_ops.rules.set`, and `inbox_ops.rules.remove` MCP tools.
|
|
14
|
+
|
|
7
15
|
## [0.1.61] - 2026-09-08
|
|
8
16
|
|
|
9
17
|
### Added
|
package/README.md
CHANGED
|
@@ -245,22 +245,42 @@ the provider action was queued; it is not confirmation that LinkedIn completed
|
|
|
245
245
|
the action. The same queue can be read with
|
|
246
246
|
`audienti operator queue --opportunity-kind network`.
|
|
247
247
|
|
|
248
|
-
Inbox Ops has its own owner-scoped CLI surface
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
248
|
+
Inbox Ops has its own owner-scoped CLI surface built for clearing a backlog in
|
|
249
|
+
bulk. `inbox-ops queue` follows every page, numbers each row, and saves the
|
|
250
|
+
numbered list locally for the selected account. The bulk verbs select rows by
|
|
251
|
+
number, range, row id, `--domain`, or `--sender`, print a manifest, and apply
|
|
252
|
+
only with `--yes` (or an interactive confirmation). `--dry-run` validates every
|
|
253
|
+
row on the server and reports planned, skipped, and rejected rows without
|
|
254
|
+
changing anything:
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
audienti inbox-ops queue
|
|
258
|
+
audienti inbox-ops queue --group-by domain
|
|
259
|
+
audienti inbox-ops ignore 1-25 --dry-run
|
|
260
|
+
audienti inbox-ops ignore 1-25 --yes
|
|
261
|
+
audienti inbox-ops filter-domain 3,7 --yes
|
|
262
|
+
audienti inbox-ops filter-domain --domain alerts.example.com --yes
|
|
263
|
+
audienti inbox-ops filter-sender 12 --yes
|
|
264
|
+
audienti inbox-ops allow-sender 4 --yes
|
|
265
|
+
audienti inbox-ops allow-domain --domain customer.example --yes
|
|
254
266
|
audienti inbox-ops filters
|
|
255
267
|
audienti inbox-ops rule <row_id> --scope sender --disposition filter
|
|
256
|
-
audienti inbox-ops rule <row_id> --scope domain --disposition allow
|
|
257
268
|
audienti inbox-ops rule set --scope sender --key news@example.com --disposition filter
|
|
258
269
|
audienti inbox-ops rule remove --scope domain --key example.com
|
|
259
270
|
```
|
|
260
271
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
272
|
+
Numbers stay valid until `inbox-ops queue` runs again. `ignore` hides one
|
|
273
|
+
thread until a newer reply arrives; `filter-*` and `allow-*` write durable rules
|
|
274
|
+
that apply to every current and future matching message across the owner's
|
|
275
|
+
accounts. Rule verbs given only `--domain` or `--sender` write the keyed rule
|
|
276
|
+
directly, even when the matching rows are no longer listed. Every row is
|
|
277
|
+
authorized individually on the server: rows another owner controls, stale rows,
|
|
278
|
+
and malformed ids are rejected one by one while the rest still apply. Pass
|
|
279
|
+
`--page`, `--offset`, or `--cursor` to read exactly one page instead.
|
|
280
|
+
|
|
281
|
+
All Inbox Ops commands update only the authenticated owner's personal/global
|
|
282
|
+
Inbox Ops preferences or private ignore receipts. They do not archive mail, call
|
|
283
|
+
the provider, or add a DNC entry.
|
|
264
284
|
|
|
265
285
|
To manage simple reminders for yourself:
|
|
266
286
|
|
package/package.json
CHANGED
package/skills/audienti/SKILL.md
CHANGED
|
@@ -12,6 +12,8 @@ undocumented API endpoints.
|
|
|
12
12
|
|
|
13
13
|
Set `approach` in a Motion create/update payload, or use `motions update <id> --approach "..."`. A nonblank Approach automatically guides adaptive planning and writing; blank, nil, or whitespace keeps the existing sequence. The simple `--approach ""` flag explicitly clears the guidance and restores that sequence. There is no separate planning-mode selector, and existing sending controls still apply.
|
|
14
14
|
|
|
15
|
+
To clear an Inbox Ops backlog, run `inbox-ops queue` first: it follows every page, numbers each row, and saves the numbered list locally for the selected account. Then use `inbox-ops ignore`, `filter-domain`, `filter-sender`, `allow-domain`, or `allow-sender` with numbers, ranges (`1-25,30`), row ids, `--domain`, or `--sender`. Always run `--dry-run` first when acting for a person, read the per-row planned/skipped/rejected results back, and only then re-run with `--yes`. Numbers stay valid until `inbox-ops queue` runs again; ignore hides one thread until a new reply, while filter and allow rules are durable across all of the owner's accounts. Rule verbs given only `--domain` or `--sender` write the keyed rule directly. Through MCP use `inbox_ops.queue`, then `inbox_ops.actions` with `{operation, row_ids, dry_run}`.
|
|
16
|
+
|
|
15
17
|
When Operator exposes `answer_planner_question`, read its prompt and candidate options with `operator queue` or `operator next`. Use `operator answer <row_id> --choice <id>` or `--answer "..."`, exactly one input. This command refetches the exact row and submits the current row and decision fingerprints. Do not substitute a generic outcome, a reusable steer note, or a guessed send. A 409 means refresh the question; 202 means the answer was recorded and planning continues. Answers apply only to that decision.
|
|
16
18
|
|
|
17
19
|
## Setup
|
|
@@ -140,6 +142,11 @@ audienti network-ops queue --json
|
|
|
140
142
|
audienti network-ops accept <row_id> --account <acct_id> --json
|
|
141
143
|
audienti network-ops decline <row_id> --account <acct_id> --json
|
|
142
144
|
audienti inbox-ops queue --json
|
|
145
|
+
audienti inbox-ops queue --group-by domain
|
|
146
|
+
audienti inbox-ops ignore 1-25 --dry-run --json
|
|
147
|
+
audienti inbox-ops ignore 1-25 --yes --json
|
|
148
|
+
audienti inbox-ops filter-domain --domain alerts.example.com --yes --json
|
|
149
|
+
audienti inbox-ops filter-sender 3,7 --yes --json
|
|
143
150
|
audienti inbox-ops filters --json
|
|
144
151
|
audienti inbox-ops rule <row_id> --scope <sender|domain> --disposition <allow|filter> --json
|
|
145
152
|
audienti analytics motions --json
|
package/src/api-client.js
CHANGED
|
@@ -674,6 +674,13 @@ export class AudientiClient {
|
|
|
674
674
|
});
|
|
675
675
|
}
|
|
676
676
|
|
|
677
|
+
inboxOpsActions(accountId, body) {
|
|
678
|
+
return this.requestJson(accountPath(accountId, ["inbox_ops", "actions"]), {
|
|
679
|
+
method: "POST",
|
|
680
|
+
body
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
|
|
677
684
|
setInboxOpsRule(accountId, body) {
|
|
678
685
|
return this.requestJson(accountPath(accountId, ["inbox_ops", "rules"]), {
|
|
679
686
|
method: "PATCH",
|
package/src/cli.js
CHANGED
|
@@ -2,10 +2,11 @@ import { parseArgs } from "node:util";
|
|
|
2
2
|
import { createServer } from "node:http";
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
4
|
import { randomBytes } from "node:crypto";
|
|
5
|
-
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
6
6
|
import { basename, join } from "node:path";
|
|
7
|
+
import { createInterface } from "node:readline/promises";
|
|
7
8
|
import { ApiError, AudientiClient, DEFAULT_HOST, normalizeHost } from "./api-client.js";
|
|
8
|
-
import { configPath, deleteConfig, maskToken, readConfig, writeConfig } from "./config.js";
|
|
9
|
+
import { configDirectory, configPath, deleteConfig, maskToken, readConfig, writeConfig } from "./config.js";
|
|
9
10
|
|
|
10
11
|
class CommandError extends Error {
|
|
11
12
|
constructor(message, { exitCode = 1 } = {}) {
|
|
@@ -65,7 +66,24 @@ const PROSPECTS_CHECK_USAGE = "Usage: audienti prospects check [--json|--csv] [f
|
|
|
65
66
|
const PROSPECTS_IMPORT_BATCH_USAGE = "Usage: audienti prospects import-batch --file <csv|jsonl|json> [--list <list_id>] [--motion <motn_id>] [--assigned-user <id|me>] [--json] [--account <acct_id>]";
|
|
66
67
|
const OPERATOR_FAILED_DRAFTS_USAGE = "Usage: audienti operator failed-drafts [--json] [filters] [--account <acct_id>]";
|
|
67
68
|
const OPERATOR_FAILED_DRAFTS_REQUEUE_USAGE = "Usage: audienti operator failed-drafts requeue (--all | <row_id> [row_id...]) [--limit <n>] [--json] [filters] [--account <acct_id>]";
|
|
68
|
-
const INBOX_OPS_QUEUE_USAGE = "Usage: audienti inbox-ops queue [--page <n>] [--offset <n>|--cursor <token>] [--json] [--account <acct_id>]";
|
|
69
|
+
const INBOX_OPS_QUEUE_USAGE = "Usage: audienti inbox-ops queue [--page <n>] [--offset <n>|--cursor <token>] [--group-by domain] [--json] [--account <acct_id>]";
|
|
70
|
+
const INBOX_OPS_BULK_VERBS = {
|
|
71
|
+
ignore: "ignore",
|
|
72
|
+
"filter-sender": "filter_sender",
|
|
73
|
+
"filter-domain": "filter_domain",
|
|
74
|
+
"allow-sender": "allow_sender",
|
|
75
|
+
"allow-domain": "allow_domain"
|
|
76
|
+
};
|
|
77
|
+
const INBOX_OPS_BULK_LABELS = {
|
|
78
|
+
ignore: "Ignore",
|
|
79
|
+
"filter-sender": "Always filter the sender of",
|
|
80
|
+
"filter-domain": "Always filter the domain of",
|
|
81
|
+
"allow-sender": "Always show the sender of",
|
|
82
|
+
"allow-domain": "Always show the domain of"
|
|
83
|
+
};
|
|
84
|
+
const INBOX_OPS_SNAPSHOT_FILENAME = "inbox-ops-snapshot.json";
|
|
85
|
+
const INBOX_OPS_MAX_PAGES = 50;
|
|
86
|
+
const INBOX_OPS_BATCH_SIZE = 50;
|
|
69
87
|
const NETWORK_OPS_QUEUE_USAGE = "Usage: audienti network-ops queue [--page <n>] [--offset <n>|--cursor <token>] [--json] [--account <acct_id>]";
|
|
70
88
|
const NETWORK_OPS_ACCEPT_USAGE = "Usage: audienti network-ops accept <row_id> [--json] [--account <acct_id>]";
|
|
71
89
|
const NETWORK_OPS_DECLINE_USAGE = "Usage: audienti network-ops decline <row_id> [--json] [--account <acct_id>]";
|
|
@@ -204,7 +222,8 @@ export async function run(argv = process.argv.slice(2), deps = {}) {
|
|
|
204
222
|
now: deps.now || (() => new Date()),
|
|
205
223
|
sleep: deps.sleep || sleep,
|
|
206
224
|
stdout: deps.stdout || process.stdout,
|
|
207
|
-
stderr: deps.stderr || process.stderr
|
|
225
|
+
stderr: deps.stderr || process.stderr,
|
|
226
|
+
stdin: deps.stdin || process.stdin
|
|
208
227
|
};
|
|
209
228
|
|
|
210
229
|
try {
|
|
@@ -357,6 +376,7 @@ async function dispatch(argv, context) {
|
|
|
357
376
|
if (normalizedResource === "inbox-ops" && action === "queue") return inboxOpsQueue(rest, context, { accountOverride });
|
|
358
377
|
if (normalizedResource === "inbox-ops" && action === "filters") return inboxOpsFilters(rest, context, { accountOverride });
|
|
359
378
|
if (normalizedResource === "inbox-ops" && action === "rule") return inboxOpsRule(rest, context, { accountOverride });
|
|
379
|
+
if (normalizedResource === "inbox-ops" && Object.hasOwn(INBOX_OPS_BULK_VERBS, action)) return inboxOpsBulk(action, rest, context, { accountOverride });
|
|
360
380
|
if (normalizedResource === "analytics" && ["prospects", "prospect"].includes(action)) return analyticsProspects(rest, context, { accountOverride });
|
|
361
381
|
if (normalizedResource === "analytics" && ["users", "user"].includes(action)) return analyticsUsers(rest, context, { accountOverride });
|
|
362
382
|
if (normalizedResource === "analytics" && ["visibility", "visops"].includes(action)) return analyticsVisibility(rest, context, { accountOverride });
|
|
@@ -3271,16 +3291,343 @@ async function operatorQueue(args, context, { accountOverride } = {}) {
|
|
|
3271
3291
|
async function inboxOpsQueue(args, context, { accountOverride } = {}) {
|
|
3272
3292
|
const { values, positionals } = parseCommandArgs(args, {
|
|
3273
3293
|
...jsonOptions(),
|
|
3274
|
-
...operatorPaginationOptions()
|
|
3294
|
+
...operatorPaginationOptions(),
|
|
3295
|
+
"group-by": { type: "string" }
|
|
3275
3296
|
});
|
|
3276
3297
|
if (positionals.length > 0) throw new CommandError(INBOX_OPS_QUEUE_USAGE);
|
|
3298
|
+
const groupBy = values["group-by"];
|
|
3299
|
+
if (groupBy !== undefined && groupBy !== "domain") throw new CommandError("--group-by only supports domain.");
|
|
3277
3300
|
|
|
3278
3301
|
const query = { opportunity_kind: "inbox", ...operatorPaginationQuery(values) };
|
|
3302
|
+
const singlePage = [values.page, values.offset, values.cursor].some((value) => value !== undefined);
|
|
3279
3303
|
const { client, accountId } = await requireAccountContext(context, { accountOverride });
|
|
3280
|
-
|
|
3304
|
+
|
|
3305
|
+
if (singlePage) {
|
|
3306
|
+
const payload = await client.operatorQueue(accountId, query);
|
|
3307
|
+
const rows = numberInboxOpsRows(inboxOpsPayloadRows(payload));
|
|
3308
|
+
await writeInboxOpsSnapshot(context, { accountId, rows });
|
|
3309
|
+
if (values.json) return writeJson(context.stdout, payload);
|
|
3310
|
+
|
|
3311
|
+
renderOperatorRead(payload, context, { command: "inbox-ops queue", accountId, query }, () => renderInboxOpsQueue(rows, context, { groupBy }));
|
|
3312
|
+
return;
|
|
3313
|
+
}
|
|
3314
|
+
|
|
3315
|
+
const { rows, pages, truncated } = await fetchAllInboxOpsRows(client, accountId, query);
|
|
3316
|
+
await writeInboxOpsSnapshot(context, { accountId, rows });
|
|
3317
|
+
if (values.json) {
|
|
3318
|
+
return writeJson(context.stdout, {
|
|
3319
|
+
kind: "inbox_ops_queue",
|
|
3320
|
+
account_id: accountId,
|
|
3321
|
+
row_count: rows.length,
|
|
3322
|
+
pages_fetched: pages,
|
|
3323
|
+
truncated,
|
|
3324
|
+
decision_queue: rows
|
|
3325
|
+
});
|
|
3326
|
+
}
|
|
3327
|
+
|
|
3328
|
+
renderInboxOpsQueue(rows, context, { groupBy });
|
|
3329
|
+
if (truncated) {
|
|
3330
|
+
writeLine(context.stdout, `Stopped after ${pages} pages; clear some rows and re-run to list the rest.`);
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3333
|
+
|
|
3334
|
+
async function fetchAllInboxOpsRows(client, accountId, baseQuery) {
|
|
3335
|
+
const rows = [];
|
|
3336
|
+
const seen = new Set();
|
|
3337
|
+
let query = { ...baseQuery };
|
|
3338
|
+
let pages = 0;
|
|
3339
|
+
let truncated = false;
|
|
3340
|
+
|
|
3341
|
+
for (;;) {
|
|
3342
|
+
const payload = await client.operatorQueue(accountId, query);
|
|
3343
|
+
pages += 1;
|
|
3344
|
+
for (const row of inboxOpsPayloadRows(payload)) {
|
|
3345
|
+
if (seen.has(row.id)) continue;
|
|
3346
|
+
seen.add(row.id);
|
|
3347
|
+
rows.push(row);
|
|
3348
|
+
}
|
|
3349
|
+
if (payload?.has_more !== true) break;
|
|
3350
|
+
if (pages >= INBOX_OPS_MAX_PAGES) {
|
|
3351
|
+
truncated = true;
|
|
3352
|
+
break;
|
|
3353
|
+
}
|
|
3354
|
+
|
|
3355
|
+
const cursor = payload?.metrics?.next_cursor;
|
|
3356
|
+
const nextOffset = payload?.metrics?.next_offset;
|
|
3357
|
+
if (!cursor && !payload?.next_page) break;
|
|
3358
|
+
query = compactObject({
|
|
3359
|
+
...baseQuery,
|
|
3360
|
+
operator_page: payload?.next_page,
|
|
3361
|
+
operator_cursor: cursor || undefined,
|
|
3362
|
+
operator_offset: cursor ? undefined : (nextOffset ?? undefined)
|
|
3363
|
+
});
|
|
3364
|
+
}
|
|
3365
|
+
|
|
3366
|
+
return { rows: numberInboxOpsRows(rows), pages, truncated };
|
|
3367
|
+
}
|
|
3368
|
+
|
|
3369
|
+
function inboxOpsPayloadRows(payload) {
|
|
3370
|
+
const decisionQueue = Array.isArray(payload?.decision_queue) ? payload.decision_queue : [];
|
|
3371
|
+
const rows = decisionQueue.length > 0 ? decisionQueue : [payload?.next_move].filter(Boolean);
|
|
3372
|
+
return rows.filter((row) => row?.id).map((row) => ({
|
|
3373
|
+
id: String(row.id),
|
|
3374
|
+
display_name: row.display_name,
|
|
3375
|
+
sender: row?.inbox_ops?.sender,
|
|
3376
|
+
domain: row?.inbox_ops?.domain,
|
|
3377
|
+
subject: row?.inbox_ops?.subject,
|
|
3378
|
+
channel: row?.inbox_ops?.channel,
|
|
3379
|
+
connected_account: row?.inbox_ops?.connected_account,
|
|
3380
|
+
state: row?.inbox_ops?.state
|
|
3381
|
+
}));
|
|
3382
|
+
}
|
|
3383
|
+
|
|
3384
|
+
function numberInboxOpsRows(rows) {
|
|
3385
|
+
return rows.map((row, index) => ({ number: index + 1, ...row }));
|
|
3386
|
+
}
|
|
3387
|
+
|
|
3388
|
+
function inboxOpsSnapshotPath(context) {
|
|
3389
|
+
return join(configDirectory(context.env), INBOX_OPS_SNAPSHOT_FILENAME);
|
|
3390
|
+
}
|
|
3391
|
+
|
|
3392
|
+
async function writeInboxOpsSnapshot(context, { accountId, rows }) {
|
|
3393
|
+
await mkdir(configDirectory(context.env), { recursive: true });
|
|
3394
|
+
await writeFile(inboxOpsSnapshotPath(context), JSON.stringify({
|
|
3395
|
+
version: 1,
|
|
3396
|
+
account_id: accountId,
|
|
3397
|
+
listed_at: context.now().toISOString(),
|
|
3398
|
+
rows
|
|
3399
|
+
}, null, 2));
|
|
3400
|
+
}
|
|
3401
|
+
|
|
3402
|
+
async function readInboxOpsSnapshot(context, accountId) {
|
|
3403
|
+
let snapshot;
|
|
3404
|
+
try {
|
|
3405
|
+
snapshot = JSON.parse(await readFile(inboxOpsSnapshotPath(context), "utf8"));
|
|
3406
|
+
} catch (error) {
|
|
3407
|
+
if (error.code === "ENOENT" || error instanceof SyntaxError) return null;
|
|
3408
|
+
throw error;
|
|
3409
|
+
}
|
|
3410
|
+
if (!Array.isArray(snapshot?.rows)) return null;
|
|
3411
|
+
if (String(snapshot.account_id) !== String(accountId)) {
|
|
3412
|
+
throw new CommandError(`The current Inbox Ops list belongs to account ${snapshot.account_id}. Run \`audienti inbox-ops queue --account ${accountId}\` first.`);
|
|
3413
|
+
}
|
|
3414
|
+
return snapshot;
|
|
3415
|
+
}
|
|
3416
|
+
|
|
3417
|
+
function parseInboxOpsSelectors(positionals) {
|
|
3418
|
+
const numbers = new Set();
|
|
3419
|
+
const rowIds = [];
|
|
3420
|
+
const tokens = positionals.flatMap((value) => String(value).split(",")).map((token) => token.trim()).filter(Boolean);
|
|
3421
|
+
|
|
3422
|
+
for (const token of tokens) {
|
|
3423
|
+
if (/^\d+$/.test(token)) {
|
|
3424
|
+
numbers.add(Number(token));
|
|
3425
|
+
continue;
|
|
3426
|
+
}
|
|
3427
|
+
const range = /^(\d+)-(\d+)$/.exec(token);
|
|
3428
|
+
if (range) {
|
|
3429
|
+
const [start, end] = [Number(range[1]), Number(range[2])];
|
|
3430
|
+
if (start < 1 || end < start) throw new CommandError(`Invalid range "${token}". Use ascending ranges like 1-25.`);
|
|
3431
|
+
for (let number = start; number <= end; number += 1) numbers.add(number);
|
|
3432
|
+
continue;
|
|
3433
|
+
}
|
|
3434
|
+
if (INBOX_OPS_ROW_ID_PATTERN.test(token)) {
|
|
3435
|
+
rowIds.push(token);
|
|
3436
|
+
continue;
|
|
3437
|
+
}
|
|
3438
|
+
throw new CommandError(`Unrecognized selector "${token}". Use numbers, ranges like 1-25, or row ids like inbox_ops_message_123.`);
|
|
3439
|
+
}
|
|
3440
|
+
if (numbers.has(0)) throw new CommandError("Row numbers start at 1.");
|
|
3441
|
+
|
|
3442
|
+
return { numbers: [...numbers].sort((left, right) => left - right), rowIds };
|
|
3443
|
+
}
|
|
3444
|
+
|
|
3445
|
+
function inboxOpsDomainMatches(rowDomain, domain) {
|
|
3446
|
+
const candidate = String(rowDomain || "").trim().toLowerCase();
|
|
3447
|
+
const wanted = String(domain || "").trim().toLowerCase().replace(/^@/, "").replace(/\.$/, "");
|
|
3448
|
+
if (!candidate || !wanted) return false;
|
|
3449
|
+
return candidate === wanted || candidate.endsWith(`.${wanted}`);
|
|
3450
|
+
}
|
|
3451
|
+
|
|
3452
|
+
function inboxOpsSenderMatches(rowSender, sender) {
|
|
3453
|
+
const candidate = String(rowSender || "").trim().toLowerCase();
|
|
3454
|
+
const wanted = String(sender || "").trim().toLowerCase();
|
|
3455
|
+
return Boolean(candidate) && candidate === wanted;
|
|
3456
|
+
}
|
|
3457
|
+
|
|
3458
|
+
async function resolveInboxOpsSelection(context, accountId, { positionals, domain, sender, usage }) {
|
|
3459
|
+
const { numbers, rowIds } = parseInboxOpsSelectors(positionals);
|
|
3460
|
+
const needsSnapshot = numbers.length > 0 || domain !== undefined || sender !== undefined;
|
|
3461
|
+
const snapshot = needsSnapshot ? await readInboxOpsSnapshot(context, accountId) : null;
|
|
3462
|
+
if (needsSnapshot && !snapshot) {
|
|
3463
|
+
throw new CommandError("No Inbox Ops list for this account yet. Run `audienti inbox-ops queue` first, then select rows by number.");
|
|
3464
|
+
}
|
|
3465
|
+
|
|
3466
|
+
const selected = [];
|
|
3467
|
+
const seen = new Set();
|
|
3468
|
+
const add = (row) => {
|
|
3469
|
+
if (seen.has(row.id)) return;
|
|
3470
|
+
seen.add(row.id);
|
|
3471
|
+
selected.push(row);
|
|
3472
|
+
};
|
|
3473
|
+
|
|
3474
|
+
for (const number of numbers) {
|
|
3475
|
+
const row = snapshot.rows.find((candidate) => candidate.number === number);
|
|
3476
|
+
if (!row) {
|
|
3477
|
+
throw new CommandError(`Row ${number} is not in the current Inbox Ops list (1-${snapshot.rows.length}). Re-run \`audienti inbox-ops queue\` to refresh the numbers.`);
|
|
3478
|
+
}
|
|
3479
|
+
add(row);
|
|
3480
|
+
}
|
|
3481
|
+
if (domain !== undefined) {
|
|
3482
|
+
const matches = snapshot.rows.filter((row) => inboxOpsDomainMatches(row.domain, domain));
|
|
3483
|
+
if (matches.length === 0) throw new CommandError(`No listed rows match domain ${domain}.`);
|
|
3484
|
+
matches.forEach(add);
|
|
3485
|
+
}
|
|
3486
|
+
if (sender !== undefined) {
|
|
3487
|
+
const matches = snapshot.rows.filter((row) => inboxOpsSenderMatches(row.sender, sender));
|
|
3488
|
+
if (matches.length === 0) throw new CommandError(`No listed rows match sender ${sender}.`);
|
|
3489
|
+
matches.forEach(add);
|
|
3490
|
+
}
|
|
3491
|
+
for (const rowId of rowIds) add(snapshot?.rows.find((row) => row.id === rowId) || { id: rowId });
|
|
3492
|
+
if (selected.length === 0) throw new CommandError(usage);
|
|
3493
|
+
|
|
3494
|
+
return selected;
|
|
3495
|
+
}
|
|
3496
|
+
|
|
3497
|
+
function inboxOpsBulkUsage(verb) {
|
|
3498
|
+
return `Usage: audienti inbox-ops ${verb} <numbers|ranges|row_ids...> [--domain <domain>] [--sender <email>] [--dry-run] [--yes] [--json] [--account <acct_id>]`;
|
|
3499
|
+
}
|
|
3500
|
+
|
|
3501
|
+
async function inboxOpsBulk(verb, args, context, { accountOverride } = {}) {
|
|
3502
|
+
const operation = INBOX_OPS_BULK_VERBS[verb];
|
|
3503
|
+
const usage = inboxOpsBulkUsage(verb);
|
|
3504
|
+
const { values, positionals } = parseCommandArgs(args, {
|
|
3505
|
+
...jsonOptions(),
|
|
3506
|
+
domain: { type: "string" },
|
|
3507
|
+
sender: { type: "string" },
|
|
3508
|
+
"dry-run": { type: "boolean" },
|
|
3509
|
+
yes: { type: "boolean" }
|
|
3510
|
+
});
|
|
3511
|
+
if (positionals.length === 0 && values.domain === undefined && values.sender === undefined) throw new CommandError(usage);
|
|
3512
|
+
if (values["dry-run"] && values.yes) throw new CommandError("Choose either --dry-run or --yes, not both.");
|
|
3513
|
+
if (values.domain !== undefined && !String(values.domain).trim()) throw new CommandError("--domain must not be blank.");
|
|
3514
|
+
if (values.sender !== undefined && !String(values.sender).trim()) throw new CommandError("--sender must not be blank.");
|
|
3515
|
+
|
|
3516
|
+
const { client, accountId } = await requireAccountContext(context, { accountOverride });
|
|
3517
|
+
const keyedRule = inboxOpsKeyedRule(operation, values, positionals);
|
|
3518
|
+
if (keyedRule) return inboxOpsBulkKeyedRule(verb, keyedRule, values, context, { client, accountId });
|
|
3519
|
+
|
|
3520
|
+
const selected = await resolveInboxOpsSelection(context, accountId, {
|
|
3521
|
+
positionals, domain: values.domain, sender: values.sender, usage
|
|
3522
|
+
});
|
|
3523
|
+
if (!values.json) renderInboxOpsManifest(context, verb, selected);
|
|
3524
|
+
|
|
3525
|
+
if (values["dry-run"]) {
|
|
3526
|
+
const payload = await inboxOpsActionsInBatches(client, accountId, { operation, rows: selected, dryRun: true });
|
|
3527
|
+
if (values.json) return writeJson(context.stdout, payload);
|
|
3528
|
+
return renderInboxOpsBulkResults(context, { selected, payload });
|
|
3529
|
+
}
|
|
3530
|
+
|
|
3531
|
+
if (!values.yes) {
|
|
3532
|
+
const confirmed = await confirmInboxOpsAction(context);
|
|
3533
|
+
if (!confirmed) {
|
|
3534
|
+
if (values.json) return writeJson(context.stdout, { applied: false, operation, row_ids: selected.map((row) => row.id), message: "Re-run with --yes to apply." });
|
|
3535
|
+
writeLine(context.stdout, "");
|
|
3536
|
+
writeLine(context.stdout, "Nothing applied. Re-run with --yes to apply, or --dry-run to validate on the server.");
|
|
3537
|
+
return;
|
|
3538
|
+
}
|
|
3539
|
+
}
|
|
3540
|
+
|
|
3541
|
+
const payload = await inboxOpsActionsInBatches(client, accountId, { operation, rows: selected, dryRun: false });
|
|
3281
3542
|
if (values.json) return writeJson(context.stdout, payload);
|
|
3543
|
+
renderInboxOpsBulkResults(context, { selected, payload });
|
|
3544
|
+
}
|
|
3545
|
+
|
|
3546
|
+
function inboxOpsKeyedRule(operation, values, positionals) {
|
|
3547
|
+
if (operation === "ignore" || positionals.length > 0) return null;
|
|
3548
|
+
const [disposition, scope] = operation.split("_");
|
|
3549
|
+
const key = scope === "domain" ? values.domain : values.sender;
|
|
3550
|
+
const otherKey = scope === "domain" ? values.sender : values.domain;
|
|
3551
|
+
if (key === undefined || otherKey !== undefined) return null;
|
|
3552
|
+
return { scope, disposition, key: String(key).trim() };
|
|
3553
|
+
}
|
|
3554
|
+
|
|
3555
|
+
async function inboxOpsBulkKeyedRule(verb, { scope, disposition, key }, values, context, { client, accountId }) {
|
|
3556
|
+
const snapshot = await readInboxOpsSnapshot(context, accountId).catch(() => null);
|
|
3557
|
+
const matches = (snapshot?.rows || []).filter((row) => (scope === "domain" ? inboxOpsDomainMatches(row.domain, key) : inboxOpsSenderMatches(row.sender, key)));
|
|
3558
|
+
const action = disposition === "allow" ? "Always show" : "Always filter";
|
|
3559
|
+
if (!values.json) {
|
|
3560
|
+
const listed = matches.length > 0 ? ` Listed rows affected: ${compressNumbers(matches.map((row) => row.number))}.` : "";
|
|
3561
|
+
writeLine(context.stdout, `${action} ${scope} ${key}.${listed}`);
|
|
3562
|
+
writeLine(context.stdout, "This rule is durable and applies to every current and future matching message across your accounts.");
|
|
3563
|
+
}
|
|
3564
|
+
if (values["dry-run"]) {
|
|
3565
|
+
if (values.json) return writeJson(context.stdout, { dry_run: true, rule: { scope, disposition, key }, listed_row_ids: matches.map((row) => row.id) });
|
|
3566
|
+
return writeLine(context.stdout, "Dry run: no rule was written.");
|
|
3567
|
+
}
|
|
3568
|
+
if (!values.yes) {
|
|
3569
|
+
const confirmed = await confirmInboxOpsAction(context);
|
|
3570
|
+
if (!confirmed) {
|
|
3571
|
+
if (values.json) return writeJson(context.stdout, { applied: false, rule: { scope, disposition, key }, message: "Re-run with --yes to apply." });
|
|
3572
|
+
writeLine(context.stdout, "Nothing applied. Re-run with --yes to apply.");
|
|
3573
|
+
return;
|
|
3574
|
+
}
|
|
3575
|
+
}
|
|
3282
3576
|
|
|
3283
|
-
|
|
3577
|
+
const payload = await client.setInboxOpsRule(accountId, { scope, key, disposition });
|
|
3578
|
+
if (values.json) return writeJson(context.stdout, payload);
|
|
3579
|
+
renderInboxOpsRule(payload, context);
|
|
3580
|
+
}
|
|
3581
|
+
|
|
3582
|
+
async function inboxOpsActionsInBatches(client, accountId, { operation, rows, dryRun }) {
|
|
3583
|
+
const results = [];
|
|
3584
|
+
let last = null;
|
|
3585
|
+
for (let index = 0; index < rows.length; index += INBOX_OPS_BATCH_SIZE) {
|
|
3586
|
+
const batch = rows.slice(index, index + INBOX_OPS_BATCH_SIZE);
|
|
3587
|
+
last = await client.inboxOpsActions(accountId, compactObject({
|
|
3588
|
+
operation,
|
|
3589
|
+
row_ids: batch.map((row) => row.id),
|
|
3590
|
+
dry_run: dryRun || undefined
|
|
3591
|
+
}));
|
|
3592
|
+
results.push(...(Array.isArray(last?.results) ? last.results : []));
|
|
3593
|
+
}
|
|
3594
|
+
|
|
3595
|
+
const counts = {};
|
|
3596
|
+
for (const result of results) counts[result.status] = (counts[result.status] || 0) + 1;
|
|
3597
|
+
return { ...(last || {}), operation, dry_run: dryRun, counts, results };
|
|
3598
|
+
}
|
|
3599
|
+
|
|
3600
|
+
async function confirmInboxOpsAction(context) {
|
|
3601
|
+
const input = context.stdin;
|
|
3602
|
+
if (!input || !input.isTTY) return false;
|
|
3603
|
+
const prompt = createInterface({ input, output: context.stdout });
|
|
3604
|
+
try {
|
|
3605
|
+
const answer = await prompt.question("Apply? [y/N] ");
|
|
3606
|
+
return /^y(es)?$/i.test(String(answer).trim());
|
|
3607
|
+
} finally {
|
|
3608
|
+
prompt.close();
|
|
3609
|
+
}
|
|
3610
|
+
}
|
|
3611
|
+
|
|
3612
|
+
function compressNumbers(numbers) {
|
|
3613
|
+
const sorted = [...new Set(numbers.filter((value) => Number.isInteger(value)))].sort((left, right) => left - right);
|
|
3614
|
+
const parts = [];
|
|
3615
|
+
let start = null;
|
|
3616
|
+
let previous = null;
|
|
3617
|
+
for (const number of sorted) {
|
|
3618
|
+
if (start === null) {
|
|
3619
|
+
start = previous = number;
|
|
3620
|
+
continue;
|
|
3621
|
+
}
|
|
3622
|
+
if (number === previous + 1) {
|
|
3623
|
+
previous = number;
|
|
3624
|
+
continue;
|
|
3625
|
+
}
|
|
3626
|
+
parts.push(start === previous ? String(start) : `${start}-${previous}`);
|
|
3627
|
+
start = previous = number;
|
|
3628
|
+
}
|
|
3629
|
+
if (start !== null) parts.push(start === previous ? String(start) : `${start}-${previous}`);
|
|
3630
|
+
return parts.join(",");
|
|
3284
3631
|
}
|
|
3285
3632
|
|
|
3286
3633
|
async function networkOpsQueue(args, context, { accountOverride } = {}) {
|
|
@@ -6475,18 +6822,78 @@ function operatorCommandArgument(value) {
|
|
|
6475
6822
|
return /^[a-zA-Z0-9_./:@=-]+$/.test(text) ? text : `'${text.replaceAll("'", "'\\''")}'`;
|
|
6476
6823
|
}
|
|
6477
6824
|
|
|
6478
|
-
function renderInboxOpsQueue(
|
|
6479
|
-
const decisionQueue = Array.isArray(payload?.decision_queue) ? payload.decision_queue : [];
|
|
6480
|
-
const rows = decisionQueue.length > 0 ? decisionQueue : [payload?.next_move].filter(Boolean);
|
|
6825
|
+
function renderInboxOpsQueue(rows, context, { groupBy } = {}) {
|
|
6481
6826
|
if (rows.length === 0) return writeLine(context.stdout, "No Inbox Ops rows found.");
|
|
6482
6827
|
|
|
6483
|
-
|
|
6484
|
-
|
|
6485
|
-
|
|
6486
|
-
|
|
6487
|
-
|
|
6488
|
-
|
|
6828
|
+
if (groupBy === "domain") {
|
|
6829
|
+
const groups = new Map();
|
|
6830
|
+
for (const row of rows) {
|
|
6831
|
+
const domain = row.domain || "-";
|
|
6832
|
+
if (!groups.has(domain)) groups.set(domain, []);
|
|
6833
|
+
groups.get(domain).push(row.number);
|
|
6834
|
+
}
|
|
6835
|
+
const sorted = [...groups.entries()].sort(([leftDomain, left], [rightDomain, right]) => right.length - left.length || leftDomain.localeCompare(rightDomain));
|
|
6836
|
+
writeAlignedTable(context, ["DOMAIN", "COUNT", "ROWS"], sorted.map(([domain, numbers]) => [domain, numbers.length, compressNumbers(numbers)]));
|
|
6837
|
+
writeLine(context.stdout, "");
|
|
6838
|
+
writeLine(context.stdout, `${rows.length} rows across ${groups.size} domains.`);
|
|
6839
|
+
writeLine(context.stdout, "Filter a whole domain: audienti inbox-ops filter-domain --domain <domain> --yes");
|
|
6840
|
+
writeLine(context.stdout, "Ignore listed rows: audienti inbox-ops ignore 1-25 --yes");
|
|
6841
|
+
return;
|
|
6842
|
+
}
|
|
6843
|
+
|
|
6844
|
+
writeAlignedTable(context, ["#", "ROW ID", "SENDER", "DOMAIN", "SUBJECT", "CONNECTED INBOX"], rows.map((row) => [
|
|
6845
|
+
row.number,
|
|
6846
|
+
display(row.id),
|
|
6847
|
+
display(row.sender),
|
|
6848
|
+
display(row.domain),
|
|
6849
|
+
display(row.subject),
|
|
6850
|
+
display(row.connected_account)
|
|
6851
|
+
]));
|
|
6852
|
+
writeLine(context.stdout, "");
|
|
6853
|
+
writeLine(context.stdout, `${rows.length} rows. Numbers stay valid until you re-run this command.`);
|
|
6854
|
+
writeLine(context.stdout, "Act by number: audienti inbox-ops ignore 1-25 | filter-domain 3,7 | filter-sender 12 | allow-sender 4 (add --yes to apply, --dry-run to validate)");
|
|
6855
|
+
writeLine(context.stdout, "Group first: audienti inbox-ops queue --group-by domain");
|
|
6856
|
+
}
|
|
6857
|
+
|
|
6858
|
+
function renderInboxOpsManifest(context, verb, selected) {
|
|
6859
|
+
writeLine(context.stdout, `${INBOX_OPS_BULK_LABELS[verb]} ${selected.length} message${selected.length === 1 ? "" : "s"}:`);
|
|
6860
|
+
writeAlignedTable(context, ["#", "ROW ID", "SENDER", "DOMAIN", "SUBJECT"], selected.map((row) => [
|
|
6861
|
+
row.number ?? "-",
|
|
6862
|
+
display(row.id),
|
|
6863
|
+
display(row.sender),
|
|
6864
|
+
display(row.domain),
|
|
6865
|
+
display(row.subject)
|
|
6866
|
+
]));
|
|
6867
|
+
}
|
|
6868
|
+
|
|
6869
|
+
function renderInboxOpsBulkResults(context, { selected, payload }) {
|
|
6870
|
+
const numbers = new Map(selected.map((row) => [row.id, row.number]));
|
|
6871
|
+
const results = Array.isArray(payload?.results) ? payload.results : [];
|
|
6872
|
+
writeLine(context.stdout, "");
|
|
6873
|
+
writeAlignedTable(context, ["#", "ROW ID", "RESULT", "DETAIL"], results.map((result) => [
|
|
6874
|
+
numbers.get(result.row_id) ?? "-",
|
|
6875
|
+
display(result.row_id),
|
|
6876
|
+
humanize(result.status),
|
|
6877
|
+
inboxOpsResultDetail(result)
|
|
6489
6878
|
]));
|
|
6879
|
+
const counts = payload?.counts || {};
|
|
6880
|
+
const summary = ["applied", "planned", "skipped", "rejected"]
|
|
6881
|
+
.filter((status) => counts[status])
|
|
6882
|
+
.map((status) => `${humanize(status)} ${counts[status]}`)
|
|
6883
|
+
.join(", ");
|
|
6884
|
+
writeLine(context.stdout, "");
|
|
6885
|
+
writeLine(context.stdout, payload?.dry_run ? `Dry run: ${summary || "nothing to do"}. No changes were made.` : `${summary || "Nothing changed"}.`);
|
|
6886
|
+
if (!payload?.dry_run && counts.applied) {
|
|
6887
|
+
writeLine(context.stdout, "Row numbers stay valid until you re-run `audienti inbox-ops queue`.");
|
|
6888
|
+
}
|
|
6889
|
+
}
|
|
6890
|
+
|
|
6891
|
+
function inboxOpsResultDetail(result) {
|
|
6892
|
+
const parts = [];
|
|
6893
|
+
if (result.reason) parts.push(humanize(result.reason));
|
|
6894
|
+
if (result.scope && result.key) parts.push(`${result.disposition || ""} ${result.scope} ${result.key}`.trim());
|
|
6895
|
+
if (result.message) parts.push(result.message);
|
|
6896
|
+
return parts.join(" | ") || "-";
|
|
6490
6897
|
}
|
|
6491
6898
|
|
|
6492
6899
|
function renderNetworkOpsQueue(payload, context, { accountId }) {
|
|
@@ -7972,7 +8379,10 @@ const HELP_TOPICS = new Map([
|
|
|
7972
8379
|
" audienti operator failed-drafts requeue <row_id>",
|
|
7973
8380
|
"",
|
|
7974
8381
|
" Inbox Ops",
|
|
7975
|
-
" audienti inbox-ops queue",
|
|
8382
|
+
" audienti inbox-ops queue [--group-by domain]",
|
|
8383
|
+
" audienti inbox-ops ignore <numbers|ranges> --yes",
|
|
8384
|
+
" audienti inbox-ops filter-domain <numbers|ranges> | --domain <domain> --yes",
|
|
8385
|
+
" audienti inbox-ops filter-sender|allow-sender|allow-domain <numbers|ranges> --yes",
|
|
7976
8386
|
" audienti inbox-ops filters",
|
|
7977
8387
|
" audienti inbox-ops rule <row_id> --scope <sender|domain> --disposition <allow|filter>",
|
|
7978
8388
|
"",
|
|
@@ -10809,6 +11219,7 @@ const HELP_TOPICS = new Map([
|
|
|
10809
11219
|
"Usage:",
|
|
10810
11220
|
` ${INBOX_OPS_QUEUE_USAGE.slice("Usage: ".length)}`,
|
|
10811
11221
|
` ${INBOX_OPS_FILTERS_USAGE.slice("Usage: ".length)}`,
|
|
11222
|
+
...Object.keys(INBOX_OPS_BULK_VERBS).map((verb) => ` ${inboxOpsBulkUsage(verb).slice("Usage: ".length)}`),
|
|
10812
11223
|
` ${INBOX_OPS_RULE_USAGE.slice("Usage: ".length)}`,
|
|
10813
11224
|
` ${INBOX_OPS_RULE_SET_USAGE.slice("Usage: ".length)}`,
|
|
10814
11225
|
` ${INBOX_OPS_RULE_REMOVE_USAGE.slice("Usage: ".length)}`,
|
|
@@ -10816,10 +11227,16 @@ const HELP_TOPICS = new Map([
|
|
|
10816
11227
|
"Status: implemented",
|
|
10817
11228
|
"",
|
|
10818
11229
|
"Purpose:",
|
|
10819
|
-
" Inspect the authenticated owner's private Inbox Ops queue and personal/global email rules, then
|
|
11230
|
+
" Inspect the authenticated owner's private Inbox Ops queue and personal/global email rules, then clear rows in bulk by number or set and remove sender and domain rules.",
|
|
11231
|
+
"",
|
|
11232
|
+
"Workflow:",
|
|
11233
|
+
" 1. audienti inbox-ops queue (numbers every row and saves the list locally)",
|
|
11234
|
+
" 2. audienti inbox-ops queue --group-by domain (see which domains dominate)",
|
|
11235
|
+
" 3. audienti inbox-ops ignore 1-25 --yes (or filter-domain / filter-sender / allow-sender / allow-domain)",
|
|
10820
11236
|
"",
|
|
10821
11237
|
"Safety:",
|
|
10822
11238
|
" Rules always apply to the token owner. Row-based updates derive identity from an authorized current Inbox Ops row.",
|
|
11239
|
+
" Bulk verbs print a manifest and apply only with --yes or an interactive confirmation; --dry-run validates every row on the server without changing anything.",
|
|
10823
11240
|
" Key-based set/remove commands normalize and validate the supplied sender or domain on the server.",
|
|
10824
11241
|
"",
|
|
10825
11242
|
"Rule values:",
|
|
@@ -10829,6 +11246,7 @@ const HELP_TOPICS = new Map([
|
|
|
10829
11246
|
"API:",
|
|
10830
11247
|
" queue: GET /api/v1/accounts/:account_id/operator.json?opportunity_kind=inbox",
|
|
10831
11248
|
" filters: GET /api/v1/accounts/:account_id/inbox_ops/filters.json",
|
|
11249
|
+
" bulk: POST /api/v1/accounts/:account_id/inbox_ops/actions.json",
|
|
10832
11250
|
" rule: PATCH /api/v1/accounts/:account_id/inbox_ops/:row_id/rule.json",
|
|
10833
11251
|
" keyed rules: PATCH|DELETE /api/v1/accounts/:account_id/inbox_ops/rules.json"
|
|
10834
11252
|
].join("\n")],
|
|
@@ -10838,15 +11256,47 @@ const HELP_TOPICS = new Map([
|
|
|
10838
11256
|
"",
|
|
10839
11257
|
"Status: implemented",
|
|
10840
11258
|
"",
|
|
10841
|
-
...OPERATOR_PAGINATION_HELP,
|
|
10842
11259
|
"Purpose:",
|
|
10843
|
-
" List
|
|
10844
|
-
"
|
|
11260
|
+
" List every current private Inbox Ops row for the authenticated owner with a short number, the authoritative sender/domain rule identity, subject, and connected inbox.",
|
|
11261
|
+
" The numbered list is saved locally per account so the bulk verbs can select rows by number until you re-run this command.",
|
|
11262
|
+
" --group-by domain summarizes the same list by sender domain with the row numbers in each group.",
|
|
11263
|
+
"",
|
|
11264
|
+
"Paging:",
|
|
11265
|
+
" Without paging flags the command follows every page itself. Pass --page, --offset, or --cursor to read exactly one page instead;",
|
|
11266
|
+
" that page is numbered and saved the same way and prints the exact continuation command when more rows remain.",
|
|
10845
11267
|
"",
|
|
10846
11268
|
"API:",
|
|
10847
11269
|
" GET /api/v1/accounts/:account_id/operator.json?opportunity_kind=inbox"
|
|
10848
11270
|
].join("\n")],
|
|
10849
11271
|
|
|
11272
|
+
...Object.keys(INBOX_OPS_BULK_VERBS).map((verb) => [`inbox-ops ${verb}`, [
|
|
11273
|
+
inboxOpsBulkUsage(verb),
|
|
11274
|
+
"",
|
|
11275
|
+
"Status: implemented",
|
|
11276
|
+
"",
|
|
11277
|
+
"Purpose:",
|
|
11278
|
+
verb === "ignore"
|
|
11279
|
+
? " Hide the selected private inbound threads from Inbox Ops. A newer reply in the same thread reopens it."
|
|
11280
|
+
: ` ${INBOX_OPS_BULK_LABELS[verb]} the selected rows by writing one durable ${verb.endsWith("domain") ? "domain" : "sender"} rule per distinct key. Matching current and future messages ${verb.startsWith("allow") ? "always stay visible" : "leave the queue"}.`,
|
|
11281
|
+
"",
|
|
11282
|
+
"Selectors:",
|
|
11283
|
+
" Numbers and ranges from the last `audienti inbox-ops queue` list: 1 4 7-12 or 1-25,30",
|
|
11284
|
+
" Row ids: inbox_ops_message_<id>",
|
|
11285
|
+
" --domain <domain> selects every listed row whose sender domain matches (subdomains included)",
|
|
11286
|
+
" --sender <email> selects every listed row from that sender",
|
|
11287
|
+
...(verb === "ignore" ? [] : [
|
|
11288
|
+
` With only --${verb.endsWith("domain") ? "domain" : "sender"} and no row selectors, the rule is written directly by key even when the rows are no longer listed.`
|
|
11289
|
+
]),
|
|
11290
|
+
"",
|
|
11291
|
+
"Safety:",
|
|
11292
|
+
" Prints the manifest first. Applies only with --yes or an interactive confirmation.",
|
|
11293
|
+
" --dry-run sends the batch to the server with dry_run and reports planned/skipped/rejected per row without changing anything.",
|
|
11294
|
+
" Rows another owner controls, stale rows, and malformed ids are rejected individually; the rest still apply.",
|
|
11295
|
+
"",
|
|
11296
|
+
"API:",
|
|
11297
|
+
" POST /api/v1/accounts/:account_id/inbox_ops/actions.json {operation, row_ids, dry_run}"
|
|
11298
|
+
].join("\n")]),
|
|
11299
|
+
|
|
10850
11300
|
["inbox-ops filters", [
|
|
10851
11301
|
INBOX_OPS_FILTERS_USAGE,
|
|
10852
11302
|
"",
|
|
@@ -11498,6 +11948,10 @@ const HELP_TOPICS = new Map([
|
|
|
11498
11948
|
" audienti network-ops accept <row_id>",
|
|
11499
11949
|
" audienti network-ops decline <row_id>",
|
|
11500
11950
|
" audienti inbox-ops queue",
|
|
11951
|
+
" audienti inbox-ops queue --group-by domain",
|
|
11952
|
+
" audienti inbox-ops ignore 1-25 --dry-run",
|
|
11953
|
+
" audienti inbox-ops ignore 1-25 --yes",
|
|
11954
|
+
" audienti inbox-ops filter-domain --domain alerts.example.com --yes",
|
|
11501
11955
|
" audienti inbox-ops filters",
|
|
11502
11956
|
" audienti inbox-ops rule <row_id> --scope sender --disposition filter",
|
|
11503
11957
|
" audienti inbox-ops rule set --scope sender --key news@example.com --disposition filter",
|