Skip to content

feat: text blast API endpoint — no more browser needed#37

Merged
KalebCole merged 3 commits intomainfrom
sasha/text-blast-endpoint
Mar 24, 2026
Merged

feat: text blast API endpoint — no more browser needed#37
KalebCole merged 3 commits intomainfrom
sasha/text-blast-endpoint

Conversation

@KalebCole
Copy link
Copy Markdown
Owner

@KalebCole KalebCole commented Mar 24, 2026

What

Implements partiful blasts send using the discovered createTextBlast API endpoint.

Before: Stub that said 'use the web UI'
After: Fully working CLI command that sends text blasts via REST API

How it was found

Logged into partiful.com via browser automation, navigated to an event's Text Blast modal, and reverse-engineered the app bundle (SendTextBlastModal.tsx6631.793447c2446d40ae.js) to find:

POST https://api.partiful.com/createTextBlast

Uses the same Firebase callable function pattern as all other Partiful endpoints.

Usage

# Dry run (no actual send)
partiful blasts send <eventId> --message 'Hey everyone!' --dry-run

# Send to Going guests only (default)
partiful blasts send <eventId> --message 'See you tonight!'

# Send to Going + Maybe
partiful blasts send <eventId> --message 'Last chance to RSVP!' --to GOING,MAYBE

# Hide from activity feed
partiful blasts send <eventId> --message 'Private update' --no-show-on-event-page

# Skip confirmation (for automation)
partiful blasts send <eventId> --message 'Automated blast' -y

Safety

  • Interactive confirmation before sending (unless --yes)
  • Message length validation (480 char max)
  • Status validation (only valid guest statuses accepted)
  • Dry-run support

Research

Full endpoint documentation: docs/research/2026-03-24-text-blast-endpoint.md

Summary by CodeRabbit

  • New Features

    • Text blast sending now fully implemented in the CLI with message content, recipient-status filtering, and event-page visibility.
    • Interactive confirmation prompt and a dry-run mode that shows the outgoing payload.
    • CLI now emits structured success JSON on send and improved error reporting.
  • Chores

    • Deprecated legacy command syntax shows migration warnings and automatically redirects to new commands.
    • Added user-facing API documentation for text blast capabilities.

Replaced the stub with a working implementation based on browser
interception of partiful.com. The endpoint is:

  POST https://api.partiful.com/createTextBlast

Supports:
- Message text (max 480 chars)
- Target audience by guest status (GOING, MAYBE, DECLINED, etc.)
- Show/hide on event activity feed
- Dry-run mode for safe testing
- Interactive confirmation before sending (skip with --yes)
- Full input validation

Research doc: docs/research/2026-03-24-text-blast-endpoint.md
@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 24, 2026

📝 Walkthrough

Walkthrough

Adds documentation for Partiful's createTextBlast API, implements a REST-backed blasts send CLI command with validation, confirmation, dry-run and auth integration, and adds argv rewriting to support deprecated top-level CLI subcommands with deprecation notices.

Changes

Cohort / File(s) Summary
API Documentation
docs/research/2026-03-24-text-blast-endpoint.md
New document describing POST https://api.partiful.com/createTextBlast, Firebase callable-style request envelope, auth header, payload schema (eventId, message, to, showOnEventPage, optional images), guest-status values, image constraints, UI mapping, and related endpoints.
CLI Argument Rewriting
src/cli.js
Pre-parser argv rewriting for deprecated top-level aliases (list, get, cancel, clone) to events ..., plus special-case rewrite for partiful guests <id>guests list <id>, emitting stderr deprecation messages.
Blasts Send Implementation
src/commands/blasts.js
Replaced stub with REST-backed blasts send action: required --message (length-validated), --to (validated CSV of statuses), --show-on-event-page flags, --dry-run, interactive confirm() (skippable with --yes), config/auth loading, payload wrapping, POST to /createTextBlast, structured JSON success output, and enhanced error handling mapping PartifulError fields.

Sequence Diagram

sequenceDiagram
    actor User
    participant CLI as "CLI Parser"
    participant Rewriter as "Arg Rewriter"
    participant Blasts as "blasts send"
    participant Config as "Config Loader"
    participant Auth as "Auth Handler"
    participant API as "Partiful API"

    User->>CLI: run `partiful blasts send <eventId> --message "..." --to "GOING,MAYBE"`
    CLI->>Rewriter: inspect argv for deprecated forms
    Rewriter-->>CLI: (rewrite or pass-through) + stderr deprecation note if rewritten
    CLI->>Blasts: dispatch `blasts send` action
    Blasts->>Config: load config & credentials
    Config-->>Blasts: return config
    Blasts->>Auth: request access token
    Auth-->>Blasts: return token
    Blasts->>Blasts: validate options, build payload (wrap in data.params)
    Blasts->>User: prompt confirm (unless --yes)
    User-->>Blasts: confirm
    Blasts->>API: POST /createTextBlast (Authorization: Bearer <token>)
    API-->>Blasts: response
    Blasts-->>User: output JSON result or mapped error
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through argv, old paths made new,

I stitched a payload and whispered it through,
A little confirm, a dry-run to peep,
Then off flew the blast where the guests softly sleep,
Docs tucked the map — now the CLI can leap.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately summarizes the main change: implementing the text blast API endpoint for the CLI, removing the need for browser-based alternatives.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sasha/text-blast-endpoint

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
docs/research/2026-03-24-text-blast-endpoint.md (1)

11-13: Add language identifier to fenced code block.

The code block is missing a language specifier. Adding one improves syntax highlighting and silences the markdownlint warning.

Proposed fix
-```
+```http
 POST https://api.partiful.com/createTextBlast
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @docs/research/2026-03-24-text-blast-endpoint.md around lines 11 - 13, Add
the language identifier "http" to the fenced code block that contains the POST
request "POST https://api.partiful.com/createTextBlast" so it becomes http ... ; this will enable proper syntax highlighting and remove the markdownlint
warning for the code block.


</details>

</blockquote></details>
<details>
<summary>src/cli.js (1)</summary><blockquote>

`48-60`: **Deprecation message for `clone` may be confusing.**

When `clone` is used, `aliasMap['clone']` returns `['+clone']`, so `newArgs.slice(0, 2).join(' ')` would produce `"+clone <arg>"` in the deprecation message, which doesn't follow the expected `"partiful <group> <command>"` pattern shown for other aliases.

Consider either:
1. Adjusting the message logic to handle single-element aliases, or
2. Mapping `clone` to `['events', '+clone']` if that's the intended path.



<details>
<summary>Suggested fix for clearer deprecation message</summary>

```diff
   const aliasMap = {
     'list': ['events', 'list'],
     'get': ['events', 'get'],
     'cancel': ['events', 'cancel'],
-    'clone': ['+clone'],
+    'clone': ['events', '+clone'],  // or handle special case in message
   };
```

Alternatively, adjust the message logic:

```diff
   if (args.length > 0 && aliasMap[args[0]]) {
     const newArgs = [...aliasMap[args[0]], ...args.slice(1)];
-    process.stderr.write(`[deprecated] "partiful ${args[0]}" → use "partiful ${newArgs.slice(0, 2).join(' ')}" instead\n`);
+    const suggestion = newArgs.length === 1 ? newArgs[0] : newArgs.slice(0, 2).join(' ');
+    process.stderr.write(`[deprecated] "partiful ${args[0]}" → use "partiful ${suggestion}" instead\n`);
     process.argv = [...process.argv.slice(0, 2), ...newArgs];
   }
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@src/cli.js` around lines 48 - 60, The deprecation message logic uses aliasMap
and newArgs to build the user-facing string, but for the 'clone' key
aliasMap['clone'] is ['+clone'] so process.stderr.write uses newArgs.slice(0,
2).join(' ') and prints a misleading "+clone <arg>" instead of the expected
"events +clone"; update either aliasMap to map 'clone' to ['events', '+clone']
or change the message generation to handle single-element aliases (e.g., if
aliasMap[args[0]].length === 1, prepend the default group 'events' when
constructing the display string) and ensure process.argv is still rewritten to
[...process.argv.slice(0,2), ...newArgs] as before.
```

</details>

</blockquote></details>
<details>
<summary>src/commands/blasts.js (1)</summary><blockquote>

`17-17`: **Unused constant `MAX_BLASTS_PER_EVENT`.**

This constant is defined but never used in the implementation. Consider either removing it or adding validation to check the blast count before sending (which would require an additional API call).

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@src/commands/blasts.js` at line 17, The constant MAX_BLASTS_PER_EVENT is
defined but unused; either remove MAX_BLASTS_PER_EVENT to avoid dead code, or
enforce it by adding validation where blasts are sent (the command handler that
performs the blast/send operation) to check the number of blasts before
proceeding and return an error if it exceeds MAX_BLASTS_PER_EVENT; if you choose
validation, fetch the current blast count (via the existing API or add a
lightweight API call), compare against MAX_BLASTS_PER_EVENT, and short-circuit
with a clear error/response from the blast-sending function.
```

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🤖 Prompt for all review comments with AI agents</summary>

Verify each finding against the current code and only fix it if needed.

Inline comments:
In @src/commands/blasts.js:

  • Around line 80-83: The jsonOutput calls inside the createTextBlast flow are
    missing the opts argument, so the --output path from globalOpts isn’t honored;
    update each jsonOutput invocation (the dry-run branch where globalOpts.dryRun is
    checked, the cancelled branch, and the success branch returning the response) to
    pass globalOpts as the second parameter (i.e., jsonOutput(outputData,
    globalOpts)) so the output option is respected; look for calls to jsonOutput in
    this file (blasts.js) around the createTextBlast handling and add the opts
    argument accordingly.
  • Around line 110-116: In the catch block handling PartifulError in the
    blasts.js command (where you check "if (err instanceof PartifulError)"), replace
    the undefined property err.code with the correct err.type when calling jsonError
    so the error type is preserved; keep passing err.message and err.exitCode as
    before and ensure the jsonError call uses err.type as the third argument.
  • Around line 65-78: The payload creation sends userId directly from config
    which can be undefined when loadConfig() (from src/lib/auth.js) returns only
    accessToken/tokenExpiry; update the payload construction in the payload object
    (the block that builds data: wrapPayload(...)) to use a safe fallback such as
    config.userId || null for the amplitude/userId field so it matches the pattern
    used in auth command; ensure you only change the userId expression and keep the
    rest of wrapPayload/params/amplitudeSessionId logic intact.

Nitpick comments:
In @docs/research/2026-03-24-text-blast-endpoint.md:

  • Around line 11-13: Add the language identifier "http" to the fenced code block
    that contains the POST request "POST https://api.partiful.com/createTextBlast"
    so it becomes http ... ; this will enable proper syntax highlighting and
    remove the markdownlint warning for the code block.

In @src/cli.js:

  • Around line 48-60: The deprecation message logic uses aliasMap and newArgs to
    build the user-facing string, but for the 'clone' key aliasMap['clone'] is
    ['+clone'] so process.stderr.write uses newArgs.slice(0, 2).join(' ') and prints
    a misleading "+clone " instead of the expected "events +clone"; update
    either aliasMap to map 'clone' to ['events', '+clone'] or change the message
    generation to handle single-element aliases (e.g., if aliasMap[args[0]].length
    === 1, prepend the default group 'events' when constructing the display string)
    and ensure process.argv is still rewritten to [...process.argv.slice(0,2),
    ...newArgs] as before.

In @src/commands/blasts.js:

  • Line 17: The constant MAX_BLASTS_PER_EVENT is defined but unused; either
    remove MAX_BLASTS_PER_EVENT to avoid dead code, or enforce it by adding
    validation where blasts are sent (the command handler that performs the
    blast/send operation) to check the number of blasts before proceeding and return
    an error if it exceeds MAX_BLASTS_PER_EVENT; if you choose validation, fetch the
    current blast count (via the existing API or add a lightweight API call),
    compare against MAX_BLASTS_PER_EVENT, and short-circuit with a clear
    error/response from the blast-sending function.

</details>

---

<details>
<summary>ℹ️ Review info</summary>

<details>
<summary>⚙️ Run configuration</summary>

**Configuration used**: defaults

**Review profile**: CHILL

**Plan**: Pro

**Run ID**: `4da96d24-c92b-4252-9dd6-a52985df7134`

</details>

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 9d3013446a696001995967ed5812242c4babf678 and 8c903efe766e80e7d63ad60462399822361ae7a2.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `docs/research/2026-03-24-text-blast-endpoint.md`
* `src/cli.js`
* `src/commands/blasts.js`

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Comment on lines +80 to +83
if (globalOpts.dryRun) {
jsonOutput({ dryRun: true, endpoint: '/createTextBlast', payload });
return;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing globalOpts in jsonOutput calls breaks --output flag.

The jsonOutput function accepts an opts parameter to support --output <path>, but all three calls omit it. This means --output won't work for dry-run, cancelled, or successful responses.

Proposed fix
         if (globalOpts.dryRun) {
-          jsonOutput({ dryRun: true, endpoint: '/createTextBlast', payload });
+          jsonOutput({ dryRun: true, endpoint: '/createTextBlast', payload }, {}, globalOpts);
           return;
         }
           if (!ok) {
-            jsonOutput({ cancelled: true, message: 'Blast not sent' });
+            jsonOutput({ cancelled: true, message: 'Blast not sent' }, {}, globalOpts);
             return;
           }
-        jsonOutput({
+        jsonOutput({
           sent: true,
           eventId,
           to: toStatuses,
           messageLength: opts.message.length,
           showOnEventPage,
           response: result?.result?.data || result?.result || result,
-        });
+        }, {}, globalOpts);

Also applies to: 95-96, 102-109

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/blasts.js` around lines 80 - 83, The jsonOutput calls inside the
createTextBlast flow are missing the opts argument, so the --output path from
globalOpts isn’t honored; update each jsonOutput invocation (the dry-run branch
where globalOpts.dryRun is checked, the cancelled branch, and the success branch
returning the response) to pass globalOpts as the second parameter (i.e.,
jsonOutput(outputData, globalOpts)) so the output option is respected; look for
calls to jsonOutput in this file (blasts.js) around the createTextBlast handling
and add the opts argument accordingly.

…back, alias rewrite

- blasts.js: err.code → err.type (PartifulError uses .type not .code)
- blasts.js: pass globalOpts to all jsonOutput calls (fixes --output flag)
- blasts.js: config.userId || null fallback for PARTIFUL_TOKEN env var
- cli.js: alias rewrite scans past global opts (--format table list works)
- cli.js: clone maps to ['events', '+clone'] for correct deprecation msg
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/commands/blasts.js (2)

15-17: Unused constant MAX_BLASTS_PER_EVENT.

This constant is defined but never used in the file. If it's a placeholder for future rate-limit validation, consider adding a TODO comment. Otherwise, remove it to avoid dead code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/blasts.js` around lines 15 - 17, Remove the dead constant
MAX_BLASTS_PER_EVENT from src/commands/blasts.js if it's not used, or if you
intend to enforce a per-event blast limit add a clear TODO comment referencing
MAX_BLASTS_PER_EVENT and implement the validation in the relevant function
(e.g., where blasts are created or sent) so the constant is either actively used
for rate-limit checks or explicitly marked for future work.

38-39: Redundant positive flag definition.

In Commander.js, defining --no-show-on-event-page automatically creates showOnEventPage with a default of true. The explicit --show-on-event-page option is redundant and could be removed for cleaner CLI definitions.

♻️ Proposed simplification
     .requiredOption('--message <msg>', 'Message to send (max 480 chars)')
     .option('--to <statuses>', 'Comma-separated guest statuses to send to (default: GOING)', 'GOING')
-    .option('--show-on-event-page', 'Show blast in event activity feed (default: true)')
     .option('--no-show-on-event-page', 'Hide blast from event activity feed')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/blasts.js` around lines 38 - 39, Remove the redundant positive
flag definition for show-on-event-page: delete the
.option('--show-on-event-page', ...) line and keep only
.option('--no-show-on-event-page', 'Hide blast from event activity feed'),
relying on Commander to automatically provide a boolean showOnEventPage
defaulting to true; verify any code that reads showOnEventPage (e.g., where
options.showOnEventPage is used) still works without change and update tests or
docs if they referenced the explicit flag.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/cli.js`:
- Around line 77-81: The special-case rewrite for "partiful guests <id>" fails
to account for global options; update the logic to use the same cmdIndex
scanning used by the alias handling instead of checking args[0]. Locate the code
around the existing alias handling that computes cmdIndex and change the guests
branch to find the command token at args[cmdIndex], emit the same deprecation
message, and rewrite process.argv by splicing in 'guests','list' at the original
command position (using cmdIndex+2 relative to process.argv.slice(0,2)) so
global flags remain in place; ensure you still guard against allowed subcommands
like 'list','invite','--help','-h' by checking args[cmdIndex+1].

---

Nitpick comments:
In `@src/commands/blasts.js`:
- Around line 15-17: Remove the dead constant MAX_BLASTS_PER_EVENT from
src/commands/blasts.js if it's not used, or if you intend to enforce a per-event
blast limit add a clear TODO comment referencing MAX_BLASTS_PER_EVENT and
implement the validation in the relevant function (e.g., where blasts are
created or sent) so the constant is either actively used for rate-limit checks
or explicitly marked for future work.
- Around line 38-39: Remove the redundant positive flag definition for
show-on-event-page: delete the .option('--show-on-event-page', ...) line and
keep only .option('--no-show-on-event-page', 'Hide blast from event activity
feed'), relying on Commander to automatically provide a boolean showOnEventPage
defaulting to true; verify any code that reads showOnEventPage (e.g., where
options.showOnEventPage is used) still works without change and update tests or
docs if they referenced the explicit flag.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a008d79f-d8bc-4dcd-b47a-147c1aca84ce

📥 Commits

Reviewing files that changed from the base of the PR and between 8c903ef and 8d1f9c7.

📒 Files selected for processing (2)
  • src/cli.js
  • src/commands/blasts.js

Comment on lines +77 to +81
// Special case: `partiful guests <id>` → `partiful guests list <id>`
if (args[0] === 'guests' && args[1] && !['list', 'invite', '--help', '-h'].includes(args[1])) {
process.stderr.write(`[deprecated] "partiful guests <id>" → use "partiful guests list <id>" instead\n`);
process.argv = [...process.argv.slice(0, 2), 'guests', 'list', ...args.slice(1)];
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guests special case doesn't skip global options, inconsistent with alias handling above.

The alias rewriting at lines 57-64 correctly scans past global options to find the command token. However, this special case checks args[0] directly. If a user runs partiful --format json guests abc123, the deprecation rewrite won't trigger because args[0] is '--format', not 'guests'.

🔧 Proposed fix to use cmdIndex for consistency
   // Special case: `partiful guests <id>` → `partiful guests list <id>`
-  if (args[0] === 'guests' && args[1] && !['list', 'invite', '--help', '-h'].includes(args[1])) {
+  const rewrittenArgs = process.argv.slice(2);
+  // Re-scan for command index after potential alias rewrite
+  let guestsCmdIndex = 0;
+  while (guestsCmdIndex < rewrittenArgs.length && rewrittenArgs[guestsCmdIndex].startsWith('-')) {
+    guestsCmdIndex += optsWithValue.has(rewrittenArgs[guestsCmdIndex]) ? 2 : 1;
+  }
+  if (rewrittenArgs[guestsCmdIndex] === 'guests' && rewrittenArgs[guestsCmdIndex + 1] && !['list', 'invite', '--help', '-h'].includes(rewrittenArgs[guestsCmdIndex + 1])) {
     process.stderr.write(`[deprecated] "partiful guests <id>" → use "partiful guests list <id>" instead\n`);
-    process.argv = [...process.argv.slice(0, 2), 'guests', 'list', ...args.slice(1)];
+    process.argv = [
+      ...process.argv.slice(0, 2),
+      ...rewrittenArgs.slice(0, guestsCmdIndex),
+      'guests', 'list',
+      ...rewrittenArgs.slice(guestsCmdIndex + 1),
+    ];
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli.js` around lines 77 - 81, The special-case rewrite for "partiful
guests <id>" fails to account for global options; update the logic to use the
same cmdIndex scanning used by the alias handling instead of checking args[0].
Locate the code around the existing alias handling that computes cmdIndex and
change the guests branch to find the command token at args[cmdIndex], emit the
same deprecation message, and rewrite process.argv by splicing in
'guests','list' at the original command position (using cmdIndex+2 relative to
process.argv.slice(0,2)) so global flags remain in place; ensure you still guard
against allowed subcommands like 'list','invite','--help','-h' by checking
args[cmdIndex+1].

@KalebCole KalebCole merged commit ba73e39 into main Mar 24, 2026
1 check passed
@KalebCole KalebCole deleted the sasha/text-blast-endpoint branch March 24, 2026 14:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant