outrank-cli 0.10.0 → 0.11.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.
@@ -0,0 +1,760 @@
1
+ import { AI_CITATIONS_PERIOD_WEEKS, AI_CITED_LISTS_ENTRY_MODES, AI_CITED_LISTS_LIST_VIEWS } from '../core/constants.js';
2
+ import { Surface } from '../core/context.js';
3
+ import { usageError } from '../core/errors.js';
4
+ import { bool, list, object, oneOf, string, uuid } from '../core/schema.js';
5
+ import {
6
+ ATTEMPT_ID_PLACEHOLDERS,
7
+ PRODUCT_CLI,
8
+ attemptIdInput,
9
+ attemptIdQuery,
10
+ productInput,
11
+ productParams,
12
+ productParamsWith,
13
+ withProduct,
14
+ } from './products/shared.js';
15
+ import { BodyMode, CommandKind, Toolset, defineCommand, defineGroup } from './registry.js';
16
+ import { subGroup } from './subGroup.js';
17
+
18
+ const SENDER_NAME_HINT = 'Name that signs the outreach emails.';
19
+ const SENDER_TITLE_HINT = 'Job title under the signature.';
20
+ const POSTAL_ADDRESS_HINT = 'Company postal address printed in every email (required by anti-spam law).';
21
+ const OUTRANK_ANSWERS_HINT = "true lets Outrank answer owner replies from the customer's mailbox.";
22
+ const OUTREACH_PAUSED_HINT = 'true stops new offers; live spots and open offers stay.';
23
+ const ALLOW_SECOND_HINT = 'true also pitches the #2 spot (default); false asks for #1 only.';
24
+ const LISTING_BLURB_HINT = 'Short product description owners see on the offer page, or null to clear it.';
25
+ const BLOCKED_SITES_HINT = 'Hosts never to contact.';
26
+ const ENTRY_MODE_HINT = 'generated uses the entry Outrank wrote; custom uses custom_entry.';
27
+ const CUSTOM_ENTRY_HINT = 'Entry written by the customer; links may only point to the product site.';
28
+ const CUSTOM_ENTRY_MARKDOWN_HINT = 'The entry text as markdown.';
29
+
30
+ const SETTINGS_BODY = object().shape({
31
+ sender_name: string().describe(SENDER_NAME_HINT),
32
+ sender_title: string().describe(SENDER_TITLE_HINT),
33
+ postal_address: string().describe(POSTAL_ADDRESS_HINT),
34
+ outrank_answers_replies: bool().describe(OUTRANK_ANSWERS_HINT),
35
+ outreach_paused: bool().describe(OUTREACH_PAUSED_HINT),
36
+ allow_second_position: bool().describe(ALLOW_SECOND_HINT),
37
+ listing_blurb: string().nullable().describe(LISTING_BLURB_HINT),
38
+ blocked_sites: list().describe(BLOCKED_SITES_HINT),
39
+ entry_mode: oneOf(AI_CITED_LISTS_ENTRY_MODES).describe(ENTRY_MODE_HINT),
40
+ custom_entry: object()
41
+ .shape({
42
+ markdown: string().required().describe(CUSTOM_ENTRY_MARKDOWN_HINT),
43
+ })
44
+ .describe(CUSTOM_ENTRY_HINT),
45
+ });
46
+
47
+ export const AI_CITED_LISTS_HELP = `Usage:
48
+ outrank-cli ai-cited-lists status <product-id>
49
+ outrank-cli ai-cited-lists overview <product-id>
50
+ outrank-cli ai-cited-lists lists list <product-id> [--view all|to_send|skipped]
51
+ outrank-cli ai-cited-lists lists skip <product-id> <list-id>
52
+ outrank-cli ai-cited-lists lists unskip <product-id> <list-id>
53
+ outrank-cli ai-cited-lists outreach <product-id>
54
+ outrank-cli ai-cited-lists offers get <product-id> <offer-id>
55
+ outrank-cli ai-cited-lists offers withdraw <product-id> <offer-id>
56
+ outrank-cli ai-cited-lists wins get <product-id> <offer-id>
57
+ outrank-cli ai-cited-lists questions list <product-id>
58
+ outrank-cli ai-cited-lists questions add <product-id> --question "<text>"
59
+ outrank-cli ai-cited-lists questions edit <product-id> <question-id> --question "<text>"
60
+ outrank-cli ai-cited-lists questions remove <product-id> <question-id>
61
+ outrank-cli ai-cited-lists settings get <product-id>
62
+ outrank-cli ai-cited-lists settings update <product-id> --json <object>
63
+ outrank-cli ai-cited-lists mailbox get <product-id>
64
+ outrank-cli ai-cited-lists mailbox connect <product-id> [--email <address>]
65
+ outrank-cli ai-cited-lists mailbox poll <product-id> --attempt-id <attempt-id>
66
+ outrank-cli ai-cited-lists mailbox domain-check <product-id>
67
+ outrank-cli ai-cited-lists mailbox inbox-test <product-id>
68
+ outrank-cli ai-cited-lists mailbox filter-test <product-id>
69
+ outrank-cli ai-cited-lists mailbox dkim-selector <product-id> --selector <name> | --reset
70
+ outrank-cli ai-cited-lists mailbox disconnect <product-id>
71
+ outrank-cli ai-cited-lists top-up-consent get <product-id>
72
+ outrank-cli ai-cited-lists top-up-consent confirm <product-id>
73
+ outrank-cli ai-cited-lists entry generate <product-id>
74
+ outrank-cli ai-cited-lists setup finish <product-id>
75
+ outrank-cli ai-cited-lists citations summary <product-id> [--weeks 4|12]
76
+ outrank-cli ai-cited-lists citations check <product-id>
77
+
78
+ JSON Input:
79
+ Use one of: --json '<object>', --json-file payload.json, or --json-stdin
80
+
81
+ Notes:
82
+ AI-Cited Lists is part of the Backlinks Enterprise plan: Outrank asks ChatGPT the product's buyer questions
83
+ several times a week, finds the "best of" lists it cites, and emails each list's owner from the customer's own
84
+ Google mailbox with a paid offer for the #1 or #2 spot. Credits are charged only once the owner has placed
85
+ the product (top-up-consent get shows the prices); a short balance is covered by an automatic credit-pack
86
+ purchase that an organization admin confirms once with top-up-consent confirm. Sending needs a connected
87
+ mailbox with four green checks, the sender details and that consent: status lists what is still missing.
88
+ mailbox connect returns a connect_url the human opens in a browser; entry generate takes a few minutes.
89
+
90
+ Examples:
91
+ outrank-cli ai-cited-lists status 00000000-0000-0000-0000-000000000000
92
+ outrank-cli ai-cited-lists lists list 00000000-0000-0000-0000-000000000000 --view to_send
93
+ outrank-cli ai-cited-lists lists skip 00000000-0000-0000-0000-000000000000 00000000-0000-0000-0000-000000000000
94
+ outrank-cli ai-cited-lists questions add 00000000-0000-0000-0000-000000000000 --question "What is the best privacy-friendly analytics tool?"
95
+ outrank-cli ai-cited-lists settings update 00000000-0000-0000-0000-000000000000 --json '{"sender_name":"Ada Lovelace","sender_title":"Founder","postal_address":"1 Main St, Austin, TX 78701"}'
96
+ outrank-cli ai-cited-lists mailbox connect 00000000-0000-0000-0000-000000000000 --email ada@acme.com
97
+ outrank-cli ai-cited-lists mailbox poll 00000000-0000-0000-0000-000000000000 --attempt-id <attempt-id>
98
+ outrank-cli ai-cited-lists top-up-consent confirm 00000000-0000-0000-0000-000000000000
99
+ outrank-cli ai-cited-lists citations summary 00000000-0000-0000-0000-000000000000 --weeks 12
100
+ `;
101
+
102
+ const LISTS_HELP = `Usage:
103
+ outrank-cli ai-cited-lists lists list <product-id> [--view all|to_send|skipped]
104
+ outrank-cli ai-cited-lists lists skip <product-id> <list-id>
105
+ outrank-cli ai-cited-lists lists unskip <product-id> <list-id>
106
+ `;
107
+ const OFFERS_HELP = `Usage:
108
+ outrank-cli ai-cited-lists offers get <product-id> <offer-id>
109
+ outrank-cli ai-cited-lists offers withdraw <product-id> <offer-id>
110
+ `;
111
+ const WINS_HELP = `Usage:
112
+ outrank-cli ai-cited-lists wins get <product-id> <offer-id>
113
+ `;
114
+ const QUESTIONS_HELP = `Usage:
115
+ outrank-cli ai-cited-lists questions list <product-id>
116
+ outrank-cli ai-cited-lists questions add <product-id> --question "<text>"
117
+ outrank-cli ai-cited-lists questions edit <product-id> <question-id> --question "<text>"
118
+ outrank-cli ai-cited-lists questions remove <product-id> <question-id>
119
+ `;
120
+ const SETTINGS_HELP = `Usage:
121
+ outrank-cli ai-cited-lists settings get <product-id>
122
+ outrank-cli ai-cited-lists settings update <product-id> --json <object>
123
+ `;
124
+ const MAILBOX_HELP = `Usage:
125
+ outrank-cli ai-cited-lists mailbox get <product-id>
126
+ outrank-cli ai-cited-lists mailbox connect <product-id> [--email <address>]
127
+ outrank-cli ai-cited-lists mailbox poll <product-id> --attempt-id <attempt-id>
128
+ outrank-cli ai-cited-lists mailbox domain-check <product-id>
129
+ outrank-cli ai-cited-lists mailbox inbox-test <product-id>
130
+ outrank-cli ai-cited-lists mailbox filter-test <product-id>
131
+ outrank-cli ai-cited-lists mailbox dkim-selector <product-id> --selector <name> | --reset
132
+ outrank-cli ai-cited-lists mailbox disconnect <product-id>
133
+ `;
134
+ const TOP_UP_CONSENT_HELP = `Usage:
135
+ outrank-cli ai-cited-lists top-up-consent get <product-id>
136
+ outrank-cli ai-cited-lists top-up-consent confirm <product-id>
137
+ `;
138
+ const ENTRY_HELP = `Usage:
139
+ outrank-cli ai-cited-lists entry generate <product-id>
140
+ `;
141
+ const SETUP_HELP = `Usage:
142
+ outrank-cli ai-cited-lists setup finish <product-id>
143
+ `;
144
+ const CITATIONS_HELP = `Usage:
145
+ outrank-cli ai-cited-lists citations summary <product-id> [--weeks 4|12]
146
+ outrank-cli ai-cited-lists citations check <product-id>
147
+ `;
148
+
149
+ const TOOLSETS = [Toolset.AI_CITED_LISTS];
150
+ const GROUP_ID = 'ai-cited-lists';
151
+ const BASE_PATH = '/products/{productId}/ai-cited-lists';
152
+ const CITATIONS_PATH = '/products/{productId}/ai-citations';
153
+ const LIST_CLI = withProduct(['listId']);
154
+ const OFFER_CLI = withProduct(['offerId']);
155
+ const QUESTION_CLI = withProduct(['questionId']);
156
+ const POLL_SETTINGS_GET = 'ai-cited-lists.settings.get';
157
+ const QUESTION_REQUIRED = {
158
+ guidance: 'Pass the buyer question as text, e.g. `--question "What is the best CRM?"`.',
159
+ };
160
+ const ATTEMPT_ID_GUIDANCE = 'Pass the attempt_id returned by `ai-cited-lists mailbox connect`.';
161
+ const DKIM_SELECTOR_WORDING = Object.freeze({
162
+ [Surface.CLI]: {
163
+ message: 'Pass exactly one of --selector or --reset.',
164
+ guidance: 'Pass `--selector <name>` to set the Workspace DKIM selector, or `--reset` to go back to the default.',
165
+ },
166
+ [Surface.MCP]: {
167
+ message: 'Pass exactly one of selector or reset.',
168
+ guidance: 'Send selector: "<name>" to set the Workspace DKIM selector, or reset: true to go back to the default.',
169
+ },
170
+ });
171
+ const EMPTY_SELECTOR = null;
172
+
173
+ const OVERVIEW_UNTRUSTED_FIELDS = [
174
+ 'wins[].title',
175
+ 'wins[].url',
176
+ 'wins[].host',
177
+ 'wins[].author_name',
178
+ 'latest_win.before',
179
+ 'latest_win.match',
180
+ 'latest_win.after',
181
+ 'latest_win.list_host',
182
+ ];
183
+ const LISTS_UNTRUSTED_FIELDS = [
184
+ 'lists[].title',
185
+ 'lists[].url',
186
+ 'lists[].host',
187
+ 'lists[].top_entries[].name',
188
+ 'left_out[].example',
189
+ ];
190
+ const OUTREACH_UNTRUSTED_FIELDS = [
191
+ 'offers[].title',
192
+ 'offers[].url',
193
+ 'offers[].host',
194
+ 'offers[].contact',
195
+ 'replies[].from',
196
+ 'replies[].excerpt',
197
+ ];
198
+ const OFFER_UNTRUSTED_FIELDS = [
199
+ 'title',
200
+ 'url',
201
+ 'host',
202
+ 'contact',
203
+ 'top_entries[].name',
204
+ 'timeline[].quote',
205
+ 'first_email.subject',
206
+ 'first_email.body',
207
+ ];
208
+ const WIN_UNTRUSTED_FIELDS = [
209
+ 'title',
210
+ 'url',
211
+ 'host',
212
+ 'author_name',
213
+ 'answers[].before',
214
+ 'answers[].match',
215
+ 'answers[].after',
216
+ 'answers[].sources',
217
+ 'owner_reply.text',
218
+ 'timeline[].quote',
219
+ ];
220
+ const CITATIONS_UNTRUSTED_FIELDS = [
221
+ 'latest_named_answer.excerpt.before',
222
+ 'latest_named_answer.excerpt.match',
223
+ 'latest_named_answer.excerpt.after',
224
+ 'latest_named_answer.via.url',
225
+ 'latest_named_answer.via.domain',
226
+ 'pages[].url',
227
+ 'pages[].domain',
228
+ 'pages[].title',
229
+ ];
230
+
231
+ const untrusted = (untrustedFields) => ({
232
+ untrustedOutput: true,
233
+ untrustedFields,
234
+ });
235
+
236
+ const status = defineCommand({
237
+ id: `${GROUP_ID}.status`,
238
+ summary: 'Get the AI-Cited Lists state of a product: counters, setup requirements and onboarding.',
239
+ kind: CommandKind.READ,
240
+ toolsets: TOOLSETS,
241
+ entryPoint: true,
242
+ input: productInput(),
243
+ cli: PRODUCT_CLI,
244
+ run: (input, ctx) =>
245
+ ctx.http({
246
+ method: 'GET',
247
+ path: `${BASE_PATH}/status`,
248
+ pathParams: productParams(input),
249
+ }),
250
+ });
251
+
252
+ const overview = defineCommand({
253
+ id: `${GROUP_ID}.overview`,
254
+ summary: 'Get the AI-Cited Lists results of a product: won lists, credits used, open offers.',
255
+ kind: CommandKind.READ,
256
+ toolsets: TOOLSETS,
257
+ mcp: untrusted(OVERVIEW_UNTRUSTED_FIELDS),
258
+ input: productInput(),
259
+ cli: PRODUCT_CLI,
260
+ run: (input, ctx) =>
261
+ ctx.http({
262
+ method: 'GET',
263
+ path: `${BASE_PATH}/overview`,
264
+ pathParams: productParams(input),
265
+ }),
266
+ });
267
+
268
+ const listsList = defineCommand({
269
+ id: `${GROUP_ID}.lists.list`,
270
+ summary: "List this week's ChatGPT-cited lists and the position we would ask for on each.",
271
+ kind: CommandKind.READ,
272
+ toolsets: TOOLSETS,
273
+ mcp: untrusted(LISTS_UNTRUSTED_FIELDS),
274
+ input: { ...productInput(), view: oneOf(AI_CITED_LISTS_LIST_VIEWS) },
275
+ cli: PRODUCT_CLI,
276
+ run: (input, ctx) =>
277
+ ctx.http({
278
+ method: 'GET',
279
+ path: `${BASE_PATH}/lists`,
280
+ pathParams: productParams(input),
281
+ query: { view: input.view },
282
+ }),
283
+ });
284
+
285
+ const listAction = (action, summary) =>
286
+ defineCommand({
287
+ id: `${GROUP_ID}.lists.${action}`,
288
+ summary,
289
+ kind: CommandKind.WRITE,
290
+ toolsets: TOOLSETS,
291
+ input: { ...productInput(), listId: uuid().required() },
292
+ cli: LIST_CLI,
293
+ run: (input, ctx) =>
294
+ ctx.http({
295
+ method: 'POST',
296
+ path: `${BASE_PATH}/lists/{listId}/${action}`,
297
+ pathParams: productParamsWith(input, 'listId'),
298
+ }),
299
+ });
300
+
301
+ const listsSkip = listAction('skip', 'Skip a cited list so no offer goes out for it.');
302
+ const listsUnskip = listAction('unskip', 'Put a skipped list back in the offer queue.');
303
+
304
+ const outreach = defineCommand({
305
+ id: `${GROUP_ID}.outreach`,
306
+ summary: 'List every offer of a product with its stage, the latest owner replies and the sending health.',
307
+ kind: CommandKind.READ,
308
+ toolsets: TOOLSETS,
309
+ mcp: untrusted(OUTREACH_UNTRUSTED_FIELDS),
310
+ input: productInput(),
311
+ cli: PRODUCT_CLI,
312
+ run: (input, ctx) =>
313
+ ctx.http({
314
+ method: 'GET',
315
+ path: `${BASE_PATH}/outreach`,
316
+ pathParams: productParams(input),
317
+ }),
318
+ });
319
+
320
+ const offerInput = () => ({ ...productInput(), offerId: uuid().required() });
321
+
322
+ const offersGet = defineCommand({
323
+ id: `${GROUP_ID}.offers.get`,
324
+ summary: 'Get one offer: stage, owner contact, email timeline and the entry we asked for.',
325
+ kind: CommandKind.READ,
326
+ toolsets: TOOLSETS,
327
+ mcp: untrusted(OFFER_UNTRUSTED_FIELDS),
328
+ input: offerInput(),
329
+ cli: OFFER_CLI,
330
+ run: (input, ctx) =>
331
+ ctx.http({
332
+ method: 'GET',
333
+ path: `${BASE_PATH}/offers/{offerId}`,
334
+ pathParams: productParamsWith(input, 'offerId'),
335
+ }),
336
+ });
337
+
338
+ const offersWithdraw = defineCommand({
339
+ id: `${GROUP_ID}.offers.withdraw`,
340
+ summary: 'Withdraw a draft or sent offer; the owner can no longer claim the spot.',
341
+ kind: CommandKind.DESTRUCTIVE,
342
+ toolsets: TOOLSETS,
343
+ input: offerInput(),
344
+ cli: OFFER_CLI,
345
+ run: (input, ctx) =>
346
+ ctx.http({
347
+ method: 'POST',
348
+ path: `${BASE_PATH}/offers/{offerId}/withdraw`,
349
+ pathParams: productParamsWith(input, 'offerId'),
350
+ }),
351
+ });
352
+
353
+ const winsGet = defineCommand({
354
+ id: `${GROUP_ID}.wins.get`,
355
+ summary: 'Get the proof page of a won list: ChatGPT answers naming the product and the daily checks.',
356
+ kind: CommandKind.READ,
357
+ toolsets: TOOLSETS,
358
+ mcp: untrusted(WIN_UNTRUSTED_FIELDS),
359
+ input: offerInput(),
360
+ cli: OFFER_CLI,
361
+ run: (input, ctx) =>
362
+ ctx.http({
363
+ method: 'GET',
364
+ path: `${BASE_PATH}/wins/{offerId}`,
365
+ pathParams: productParamsWith(input, 'offerId'),
366
+ }),
367
+ });
368
+
369
+ const questionsList = defineCommand({
370
+ id: `${GROUP_ID}.questions.list`,
371
+ summary: 'List the buyer questions asked to ChatGPT for a product.',
372
+ kind: CommandKind.READ,
373
+ toolsets: TOOLSETS,
374
+ input: productInput(),
375
+ cli: PRODUCT_CLI,
376
+ run: (input, ctx) =>
377
+ ctx.http({
378
+ method: 'GET',
379
+ path: `${BASE_PATH}/questions`,
380
+ pathParams: productParams(input),
381
+ }),
382
+ });
383
+
384
+ const questionsAdd = defineCommand({
385
+ id: `${GROUP_ID}.questions.add`,
386
+ summary: 'Add a buyer question to ask ChatGPT.',
387
+ kind: CommandKind.WRITE,
388
+ toolsets: TOOLSETS,
389
+ input: { ...productInput(), question: string().required(QUESTION_REQUIRED) },
390
+ cli: { ...PRODUCT_CLI, placeholders: { question: '<text>' } },
391
+ run: (input, ctx) =>
392
+ ctx.http({
393
+ method: 'POST',
394
+ path: `${BASE_PATH}/questions`,
395
+ pathParams: productParams(input),
396
+ body: { question: input.question },
397
+ }),
398
+ });
399
+
400
+ const questionsEdit = defineCommand({
401
+ id: `${GROUP_ID}.questions.edit`,
402
+ summary: 'Change the wording of a tracked buyer question.',
403
+ kind: CommandKind.WRITE,
404
+ toolsets: TOOLSETS,
405
+ input: {
406
+ ...productInput(),
407
+ questionId: uuid().required(),
408
+ question: string().required(QUESTION_REQUIRED),
409
+ },
410
+ cli: { ...QUESTION_CLI, placeholders: { question: '<text>' } },
411
+ run: (input, ctx) =>
412
+ ctx.http({
413
+ method: 'PATCH',
414
+ path: `${BASE_PATH}/questions/{questionId}`,
415
+ pathParams: productParamsWith(input, 'questionId'),
416
+ body: { question: input.question },
417
+ }),
418
+ });
419
+
420
+ const questionsRemove = defineCommand({
421
+ id: `${GROUP_ID}.questions.remove`,
422
+ summary: 'Stop asking a buyer question.',
423
+ kind: CommandKind.DESTRUCTIVE,
424
+ toolsets: TOOLSETS,
425
+ input: { ...productInput(), questionId: uuid().required() },
426
+ cli: QUESTION_CLI,
427
+ run: (input, ctx) =>
428
+ ctx.http({
429
+ method: 'DELETE',
430
+ path: `${BASE_PATH}/questions/{questionId}`,
431
+ pathParams: productParamsWith(input, 'questionId'),
432
+ }),
433
+ });
434
+
435
+ const settingsGet = defineCommand({
436
+ id: `${GROUP_ID}.settings.get`,
437
+ summary: 'Get the AI-Cited Lists settings of a product: sender details, positions, pause, entry.',
438
+ kind: CommandKind.READ,
439
+ toolsets: TOOLSETS,
440
+ input: productInput(),
441
+ cli: PRODUCT_CLI,
442
+ run: (input, ctx) =>
443
+ ctx.http({
444
+ method: 'GET',
445
+ path: `${BASE_PATH}/settings`,
446
+ pathParams: productParams(input),
447
+ }),
448
+ });
449
+
450
+ const settingsUpdate = defineCommand({
451
+ id: `${GROUP_ID}.settings.update`,
452
+ summary: 'Update the AI-Cited Lists settings of a product from a JSON body.',
453
+ kind: CommandKind.WRITE,
454
+ toolsets: TOOLSETS,
455
+ input: productInput(),
456
+ cli: PRODUCT_CLI,
457
+ body: BodyMode.JSON,
458
+ bodyField: SETTINGS_BODY,
459
+ run: (input, ctx) =>
460
+ ctx.http({
461
+ method: 'PATCH',
462
+ path: `${BASE_PATH}/settings`,
463
+ pathParams: productParams(input),
464
+ body: input.body,
465
+ }),
466
+ });
467
+
468
+ const mailboxGet = defineCommand({
469
+ id: `${GROUP_ID}.mailbox.get`,
470
+ summary: 'Get the connected Google mailbox and its four sending checks.',
471
+ kind: CommandKind.READ,
472
+ toolsets: TOOLSETS,
473
+ input: productInput(),
474
+ cli: PRODUCT_CLI,
475
+ run: (input, ctx) =>
476
+ ctx.http({
477
+ method: 'GET',
478
+ path: `${BASE_PATH}/mailbox`,
479
+ pathParams: productParams(input),
480
+ }),
481
+ });
482
+
483
+ const mailboxDisconnect = defineCommand({
484
+ id: `${GROUP_ID}.mailbox.disconnect`,
485
+ summary: 'Disconnect the Google mailbox and revoke its token.',
486
+ kind: CommandKind.DESTRUCTIVE,
487
+ toolsets: TOOLSETS,
488
+ input: productInput(),
489
+ cli: PRODUCT_CLI,
490
+ run: (input, ctx) =>
491
+ ctx.http({
492
+ method: 'DELETE',
493
+ path: `${BASE_PATH}/mailbox`,
494
+ pathParams: productParams(input),
495
+ }),
496
+ });
497
+
498
+ const mailboxConnect = defineCommand({
499
+ id: `${GROUP_ID}.mailbox.connect`,
500
+ summary: 'Start the Google mailbox connection: returns a connect_url for the human and an attempt_id to poll.',
501
+ kind: CommandKind.WRITE,
502
+ toolsets: TOOLSETS,
503
+ input: { ...productInput(), email: string() },
504
+ cli: { ...PRODUCT_CLI, placeholders: { email: '<address>' } },
505
+ run: (input, ctx) =>
506
+ ctx.http({
507
+ method: 'POST',
508
+ path: `${BASE_PATH}/mailbox/connect`,
509
+ pathParams: productParams(input),
510
+ body: { email: input.email },
511
+ }),
512
+ });
513
+
514
+ const mailboxPoll = defineCommand({
515
+ id: `${GROUP_ID}.mailbox.poll`,
516
+ summary: 'Poll a mailbox connection attempt until Google consent completed.',
517
+ kind: CommandKind.READ,
518
+ toolsets: TOOLSETS,
519
+ input: { ...productInput(), ...attemptIdInput(ATTEMPT_ID_GUIDANCE) },
520
+ cli: { ...PRODUCT_CLI, placeholders: ATTEMPT_ID_PLACEHOLDERS },
521
+ run: (input, ctx) =>
522
+ ctx.http({
523
+ method: 'GET',
524
+ path: `${BASE_PATH}/mailbox/connect`,
525
+ pathParams: productParams(input),
526
+ query: attemptIdQuery(input),
527
+ }),
528
+ });
529
+
530
+ const mailboxAction = (action, summary) =>
531
+ defineCommand({
532
+ id: `${GROUP_ID}.mailbox.${action}`,
533
+ summary,
534
+ kind: CommandKind.WRITE,
535
+ toolsets: TOOLSETS,
536
+ input: productInput(),
537
+ cli: PRODUCT_CLI,
538
+ run: (input, ctx) =>
539
+ ctx.http({
540
+ method: 'POST',
541
+ path: `${BASE_PATH}/mailbox/${action}`,
542
+ pathParams: productParams(input),
543
+ }),
544
+ });
545
+
546
+ const mailboxDomainCheck = mailboxAction('domain-check', 'Re-run the SPF, DKIM and DMARC check of the mailbox domain.');
547
+ const mailboxInboxTest = mailboxAction(
548
+ 'inbox-test',
549
+ 'Send the seed-list inbox test from the mailbox (server rate-limited).',
550
+ );
551
+ const mailboxFilterTest = mailboxAction(
552
+ 'filter-test',
553
+ 'Send the reply-filter test email to confirm replies reach Outrank.',
554
+ );
555
+
556
+ const mailboxDkimSelector = defineCommand({
557
+ id: `${GROUP_ID}.mailbox.dkim-selector`,
558
+ summary: 'Set the Google Workspace DKIM selector of the mailbox domain, or reset it to the default.',
559
+ kind: CommandKind.WRITE,
560
+ toolsets: TOOLSETS,
561
+ input: { ...productInput(), selector: string(), reset: bool() },
562
+ cli: { ...PRODUCT_CLI, placeholders: { selector: '<name>' } },
563
+ refine: (input, ctx) => {
564
+ const hasSelector = input.selector !== undefined;
565
+ if (hasSelector === Boolean(input.reset)) {
566
+ const wording = DKIM_SELECTOR_WORDING[ctx.surface === Surface.MCP ? Surface.MCP : Surface.CLI];
567
+ throw usageError(wording.message, wording.guidance);
568
+ }
569
+ },
570
+ run: (input, ctx) =>
571
+ ctx.http({
572
+ method: 'PATCH',
573
+ path: `${BASE_PATH}/mailbox/dkim-selector`,
574
+ pathParams: productParams(input),
575
+ body: { selector: input.reset ? EMPTY_SELECTOR : input.selector },
576
+ }),
577
+ });
578
+
579
+ const topUpConsentGet = defineCommand({
580
+ id: `${GROUP_ID}.top-up-consent.get`,
581
+ summary: 'Read the automatic credit top-up terms and whether they were confirmed.',
582
+ kind: CommandKind.READ,
583
+ toolsets: TOOLSETS,
584
+ input: productInput(),
585
+ cli: PRODUCT_CLI,
586
+ run: (input, ctx) =>
587
+ ctx.http({
588
+ method: 'GET',
589
+ path: `${BASE_PATH}/top-up-consent`,
590
+ pathParams: productParams(input),
591
+ }),
592
+ });
593
+
594
+ const topUpConsentConfirm = defineCommand({
595
+ id: `${GROUP_ID}.top-up-consent.confirm`,
596
+ summary: 'Confirm the automatic credit top-up: authorizes credit-pack purchases on the card on file.',
597
+ kind: CommandKind.MONEY,
598
+ toolsets: TOOLSETS,
599
+ input: productInput(),
600
+ cli: PRODUCT_CLI,
601
+ run: (input, ctx) =>
602
+ ctx.http({
603
+ method: 'POST',
604
+ path: `${BASE_PATH}/top-up-consent`,
605
+ pathParams: productParams(input),
606
+ body: { accepted: true },
607
+ }),
608
+ });
609
+
610
+ const entryGenerate = defineCommand({
611
+ id: `${GROUP_ID}.entry.generate`,
612
+ summary: 'Write a fresh base entry for the product from its site (multi-minute).',
613
+ kind: CommandKind.WRITE,
614
+ toolsets: TOOLSETS,
615
+ input: productInput(),
616
+ cli: PRODUCT_CLI,
617
+ longRunning: { pollTool: POLL_SETTINGS_GET },
618
+ run: (input, ctx) =>
619
+ ctx.http({
620
+ method: 'POST',
621
+ path: `${BASE_PATH}/entry/base`,
622
+ pathParams: productParams(input),
623
+ }),
624
+ });
625
+
626
+ const setupFinish = defineCommand({
627
+ id: `${GROUP_ID}.setup.finish`,
628
+ summary: 'Mark the dashboard onboarding as finished so the human lands on the tabs.',
629
+ kind: CommandKind.WRITE,
630
+ toolsets: TOOLSETS,
631
+ input: productInput(),
632
+ cli: PRODUCT_CLI,
633
+ run: (input, ctx) =>
634
+ ctx.http({
635
+ method: 'POST',
636
+ path: `${BASE_PATH}/setup/finish`,
637
+ pathParams: productParams(input),
638
+ }),
639
+ });
640
+
641
+ const citationsSummary = defineCommand({
642
+ id: `${GROUP_ID}.citations.summary`,
643
+ summary: 'Get the AI Citations summary: answers naming the product and the pages ChatGPT cited.',
644
+ kind: CommandKind.READ,
645
+ toolsets: TOOLSETS,
646
+ mcp: untrusted(CITATIONS_UNTRUSTED_FIELDS),
647
+ input: { ...productInput(), weeks: oneOf(AI_CITATIONS_PERIOD_WEEKS) },
648
+ cli: PRODUCT_CLI,
649
+ run: (input, ctx) =>
650
+ ctx.http({
651
+ method: 'GET',
652
+ path: `${CITATIONS_PATH}/summary`,
653
+ pathParams: productParams(input),
654
+ query: { weeks: input.weeks },
655
+ }),
656
+ });
657
+
658
+ const citationsCheck = defineCommand({
659
+ id: `${GROUP_ID}.citations.check`,
660
+ summary: 'Ask ChatGPT the buyer questions now, unless fresh data or a running check exists.',
661
+ kind: CommandKind.WRITE,
662
+ toolsets: TOOLSETS,
663
+ input: productInput(),
664
+ cli: PRODUCT_CLI,
665
+ run: (input, ctx) =>
666
+ ctx.http({
667
+ method: 'POST',
668
+ path: `${CITATIONS_PATH}/check`,
669
+ pathParams: productParams(input),
670
+ }),
671
+ });
672
+
673
+ export const aiCitedListsGroup = defineGroup({
674
+ id: GROUP_ID,
675
+ help: {
676
+ usage: AI_CITED_LISTS_HELP,
677
+ guidance:
678
+ 'Start with status: it says whether the product is on the Enterprise plan and what the outreach setup ' +
679
+ 'still needs (mailbox, sender details, top-up consent). Then overview, lists, outreach, offers, wins, ' +
680
+ 'questions, settings, mailbox, top-up-consent, entry, setup or citations.',
681
+ },
682
+ unknown: (word) => ({
683
+ message: `Unknown ai-cited-lists sub-noun: ${word}.`,
684
+ guidance: 'Run `outrank-cli ai-cited-lists --help` to see the available AI-Cited Lists commands.',
685
+ }),
686
+ children: [
687
+ status,
688
+ overview,
689
+ subGroup({
690
+ id: `${GROUP_ID}.lists`,
691
+ usage: LISTS_HELP,
692
+ actions: 'list|skip|unskip',
693
+ children: [listsList, listsSkip, listsUnskip],
694
+ }),
695
+ outreach,
696
+ subGroup({
697
+ id: `${GROUP_ID}.offers`,
698
+ usage: OFFERS_HELP,
699
+ actions: 'get|withdraw',
700
+ children: [offersGet, offersWithdraw],
701
+ }),
702
+ subGroup({
703
+ id: `${GROUP_ID}.wins`,
704
+ usage: WINS_HELP,
705
+ actions: 'get',
706
+ children: [winsGet],
707
+ }),
708
+ subGroup({
709
+ id: `${GROUP_ID}.questions`,
710
+ usage: QUESTIONS_HELP,
711
+ actions: 'list|add|edit|remove',
712
+ children: [questionsList, questionsAdd, questionsEdit, questionsRemove],
713
+ }),
714
+ subGroup({
715
+ id: `${GROUP_ID}.settings`,
716
+ usage: SETTINGS_HELP,
717
+ actions: 'get|update',
718
+ children: [settingsGet, settingsUpdate],
719
+ }),
720
+ subGroup({
721
+ id: `${GROUP_ID}.mailbox`,
722
+ usage: MAILBOX_HELP,
723
+ actions: 'get|connect|poll|domain-check|inbox-test|filter-test|dkim-selector|disconnect',
724
+ children: [
725
+ mailboxGet,
726
+ mailboxConnect,
727
+ mailboxPoll,
728
+ mailboxDomainCheck,
729
+ mailboxInboxTest,
730
+ mailboxFilterTest,
731
+ mailboxDkimSelector,
732
+ mailboxDisconnect,
733
+ ],
734
+ }),
735
+ subGroup({
736
+ id: `${GROUP_ID}.top-up-consent`,
737
+ usage: TOP_UP_CONSENT_HELP,
738
+ actions: 'get|confirm',
739
+ children: [topUpConsentGet, topUpConsentConfirm],
740
+ }),
741
+ subGroup({
742
+ id: `${GROUP_ID}.entry`,
743
+ usage: ENTRY_HELP,
744
+ actions: 'generate',
745
+ children: [entryGenerate],
746
+ }),
747
+ subGroup({
748
+ id: `${GROUP_ID}.setup`,
749
+ usage: SETUP_HELP,
750
+ actions: 'finish',
751
+ children: [setupFinish],
752
+ }),
753
+ subGroup({
754
+ id: `${GROUP_ID}.citations`,
755
+ usage: CITATIONS_HELP,
756
+ actions: 'summary|check',
757
+ children: [citationsSummary, citationsCheck],
758
+ }),
759
+ ],
760
+ });