Skip to content

feat(events): expose my personal RSVP (myRsvp) in events list + fix isHost via JWT identity #56

Description

@KalebCole

Summary

events list does not expose my own RSVP status for each event, even though the Partiful API returns it. The raw /getMyUpcomingEventsForHomePage response includes a per-event guest object — my personal attendance record — but the CLI's output mapping drops it. This issue adds a myRsvp field to the event output and fixes the related, currently-broken isHost flag (same root cause: we don't know who "me" is).

Motivation

Primary use case: a Partiful → Google Calendar sync that copies only the events I've actually accepted (GOING/MAYBE) into my personal calendar, so my calendar stops showing every invite as a false commitment. That sync needs to read my per-event RSVP — which the CLI can't currently surface. More broadly, "which of these am I going to?" is a basic question the event list should answer.

Root cause

The API returns everything we need; the CLI discards it.

src/commands/events.js (lines ~74–85) maps each event to just 9 fields:

const mapped = (eventList || []).map(e => ({
  id: e.id, title: e.title, startDate: e.startDate, endDate: e.endDate || null,
  location: e.location || null,
  status: e.status,                                   // EVENT status (PUBLISHED), NOT my RSVP
  isHost: e.ownerIds?.includes(config.userId) || false, // BROKEN: config.userId is undefined
  going: e.guestStatusCounts?.GOING || 0,             // aggregate count, not me
  maybe: e.guestStatusCounts?.MAYBE || 0,
  url: `https://partiful.com/e/${e.id}`,
}));
// DROPPED: e.guest = { userId: <me>, status: "GOING" | "MAYBE" | "DECLINED" | "SENT" }

Two defects fall out of this:

  1. myRsvp missinge.guest.status (my personal RSVP) is never surfaced.
  2. isHost always false — it tests config.userId, but that is undefined in the stored auth config (the same userId: null that partiful doctor flags). So no event is ever reported as hosted.

Data model (verified live against my account, 15 events)

  • If I'm a guest, the event carries guest: { userId: <me>, status: <MY_RSVP> } where status ∈ GOING | MAYBE | DECLINED | SENT (SENT = invited, no reply yet).
  • If I host, there is no guest object, and my id appears in ownerIds.
  • So: guest present → use guest.status; guest absent + my id in ownerIds → I'm the host.

Identity resolution — decode the JWT (fixes the root cause cleanly)

config.userId is unreliable (null/undefined). The Firebase access token already carries my id. Verified: the JWT payload's user_id (and sub) claim equals my guest.userId. Decoding it is the robust, self-healing source of truth for "me" — no dependency on what was saved at auth save time.

Proposed helper in src/lib/auth.js:

// Decode the Firebase JWT payload and return the authenticated user id.
export function getUserIdFromToken(token) {
  try {
    const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString('utf8'));
    return payload.user_id || payload.sub || null;
  } catch { return null; }
}

This also makes isHost correct without touching stored config, and can backfill config.userId on refresh if desired.

Proposed changes

Code

  • src/lib/auth.js — add getUserIdFromToken(token) (JWT decode). Optionally set config.userId from it inside getValidToken so the whole CLI benefits and doctor stops flagging null.
  • src/commands/events.js (events list) — resolve const me = config.userId || getUserIdFromToken(token); then add to the mapping:
    • myRsvp: e.guest?.status ?? null (null when I host / no record)
    • fix isHost: e.ownerIds?.includes(me) || false
  • Extract the inline .map() into a pure, exported mapEventSummary(e, me) so the mapping is unit-testable without a live API (all current event tests are --dry-run and never exercise real response shaping).
  • events getalso should return myRsvp, but see blocker below; /getEvent currently 404s, so this part is gated on that fix.

Docs

  • README.md — update the events list output example to include myRsvp + document the field's value set.
  • AGENTS.md — note that identity is now derived from the token (userId no longer needs to be stored); update the "userId can be null and things still work" section to mention myRsvp/isHost now work regardless.
  • Bundled skill skills/partiful-events/SKILL.md (ships in the npm files array) — document myRsvp on events list.

Tests (tests/, vitest)

  • Unit test mapEventSummary: guest present → myRsvp set + isHost false; host case (no guest, id in ownerIds) → myRsvp null + isHost true.
  • Unit test getUserIdFromToken with a hand-built unsigned JWT (header.base64(payload).sig), asserting user_id/sub extraction and graceful null on garbage.
  • Keep the existing 106 tests green.

Discovered blocker (separate bug) — events get returns 404

While researching I found partiful events get <id> fails for every event (mine and hosted), both via the CLI and a direct /getEvent call:

{"status":"error","error":{"code":4,"type":"not_found","message":"API POST /getEvent failed","details":{"statusCode":404}}}

This looks like a Partiful endpoint change unrelated to this feature. It blocks adding myRsvp to events get. Recommend filing as its own bug; this issue can ship the events list improvement independently (which is all the calendar-sync use case needs).

Release / distribution steps (I own this CLI)

"Making the change" end-to-end, beyond the code:

  1. Branch, implement, npm test green.
  2. Manual verify against live account: partiful events list shows correct myRsvp (GOING for Daily Mile 365; MAYBE for the two wild-waves; SENT for the five unanswered) and isHost: true for my 4 owned events.
  3. Bump package.json version (2.0.0 → 2.1.0, minor — additive field).
  4. Refresh the global install/opt/homebrew/lib/node_modules/partiful-cli is a real directory (installed via npm install -g ., not a symlink), so repo edits don't take effect until npm install -g . is re-run (or switch dev to npm link). The partiful command Hermes/agents call won't change until this step.
  5. Package is published to the npm registry at 2.0.0 — if public distribution is wanted, npm publish after the bump. Otherwise skip and just reinstall locally.
  6. No CI to update (no .github/ workflows present) and no CHANGELOG file exists — optionally add one entry if you start tracking.

Acceptance criteria

  • partiful events list output objects include myRsvp ∈ {GOING, MAYBE, DECLINED, SENT, null}.
  • isHost is true for events I own, false otherwise — independent of whether userId was saved.
  • Identity derives from the token when config.userId is absent.
  • New unit tests cover the mapping + JWT decode; full suite green.
  • README / AGENTS.md / bundled skill document myRsvp.

Related issues (orthogonal, not duplicate)

  • [P1] RSVP tracking and notifications #17 [P1] RSVP tracking and notifications (closed) — that was about tracking other guests' RSVPs to events I host (host-side guest list/summary). This issue is the inverse: my own RSVP as an invitee, on the events list. Different direction of the same relation.
  • [P1] Google Calendar sync #19 [P1] Google Calendar sync (closed) — the broader two-way sync vision. This issue is the missing primitive underneath it: you cannot "sync only events I've accepted" without first exposing my per-event RSVP. Landing myRsvp unblocks a focused, read-only "copy my GOING events to gcal" implementation without the full two-way scope.

Effort

Small — ~1–2 hours. Additive, backward-compatible, no API-contract risk (pure read-path shaping).

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions