diff --git a/docs/plans/2026-03-26-distribution-plan.md b/docs/plans/2026-03-26-distribution-plan.md index b4fe26f..94edadd 100644 --- a/docs/plans/2026-03-26-distribution-plan.md +++ b/docs/plans/2026-03-26-distribution-plan.md @@ -1,5 +1,7 @@ # Distribution Plan — partiful-cli +> **Superseded:** `partiful setup openclaw` and the five `partiful-*` skills were removed by the singular `partiful` skill migration tracked in issue #70. This document remains as historical planning context, not current setup guidance. + **Date:** 2026-03-26 **Status:** Draft **Goal:** One-command install for humans and AI agents diff --git a/package.json b/package.json index f42e687..8179a86 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,7 @@ "party", "rsvp", "agent", - "ai", - "openclaw" + "ai" ], "repository": { "type": "git", diff --git a/skills/partiful-blasts/SKILL.md b/skills/partiful-blasts/SKILL.md deleted file mode 100644 index afd8b7b..0000000 --- a/skills/partiful-blasts/SKILL.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -name: partiful-blasts -description: Send text blasts to event guests via Partiful ---- - -# Partiful CLI — Text Blasts - -> ⚠️ **Real SMS messages.** Blasts send actual text messages to real people's phones. Partiful prepends: *"The host of {Event} sent a Text Blast —"* before your message. - -## Commands - -### Send a Text Blast -```bash -partiful blasts send --message "See you tonight! Parking is on the left." --to GOING,MAYBE -partiful blasts send --message "Updated address: 123 Main St" --to GOING --show-on-event-page -``` - -| Flag | Description | -|------|-------------| -| `--message ` | Message body (**max 480 characters**) | -| `--to ` | Comma-separated guest statuses to target (see below) | -| `--show-on-event-page` | Also display the blast on the event page | -| `--yes` | Skip the safety confirmation prompt | -| `--dry-run` | Preview recipients and message without sending | - -### Valid `--to` Values - -| Status | Description | -|--------|-------------| -| `GOING` | Confirmed guests | -| `MAYBE` | Tentative guests | -| `DECLINED` | Guests who declined | -| `SENT` | Invited but no response | -| `INTERESTED` | Expressed interest | -| `WAITLIST` | On the waitlist | -| `APPROVED` | Approved from waitlist | -| `RESPONDED_TO_FIND_A_TIME` | Responded to scheduling poll | - -## Tips - -- **Always use `--dry-run` first** to verify the recipient list and message. -- The 480-character limit is enforced by Partiful's API — the CLI will reject longer messages before sending. -- Without `--yes`, the CLI shows a confirmation prompt with the recipient count and message preview. -- Combine with `partiful guests list --status going` to preview who will receive the blast. diff --git a/skills/partiful-events/SKILL.md b/skills/partiful-events/SKILL.md deleted file mode 100644 index 4d59344..0000000 --- a/skills/partiful-events/SKILL.md +++ /dev/null @@ -1,157 +0,0 @@ ---- -name: partiful-events -description: Manage Partiful events — list, create, update, cancel, clone, set posters/images ---- - -# Partiful CLI — Events - -## Commands - -### List Events -```bash -partiful events list -partiful events list --past -partiful events list --past --include-cancelled -``` - -Each event in the list includes **your own** RSVP and host status: - -| Field | Meaning | -|-------|---------| -| `myRsvp` | Your personal RSVP: `GOING`, `MAYBE`, `DECLINED`, `SENT` (invited, not yet answered), or `null` on events you host | -| `isHost` | `true` when you own the event | -| `going` / `maybe` | Aggregate guest counts (everyone), **not** your status | - -```bash -# Only the events you've said yes to (e.g. to sync to a calendar) -partiful events list | jq '.data[] | select(.myRsvp == "GOING")' -``` - -### Get Event Details -```bash -partiful events get -partiful events get --format table -``` - -### Create Event -```bash -partiful events create --title "Game Night" --date "2026-04-01T19:00" --location "My Place" -partiful events create --title "Birthday" --date "2026-05-15T20:00" --description "Bring snacks" --dry-run -``` - -| Flag | Description | -|------|-------------| -| `--title ` | Event title (required) | -| `--date <date>` | Start date/time (required) | -| `--end-date <date>` | End date/time | -| `--location <name>` | Location name | -| `--address <addr>` | Street address | -| `--description <text>` | Event description | -| `--capacity <n>` | Guest limit | -| `--private` | Make event private | -| `--timezone <tz>` | Timezone (default: `America/Los_Angeles`) | -| `--theme <theme>` | Color theme (default: `oxblood`) | -| `--effect <effect>` | Visual effect (default: `sunbeams`) | -| `--poster <id>` | Built-in poster ID (see `posters search`) | -| `--poster-search <query>` | Fuzzy-search poster catalog, use best match | -| `--image <path\|url>` | Upload a custom image (local file or URL) | - -### Update Event -```bash -partiful events update <event-id> --title "New Title" -partiful events update <event-id> --date "2026-04-02T19:00" --location "New Venue" -partiful events update <event-id> --poster "piscesairbrush.png" -partiful events update <event-id> --image ./new-flyer.png -``` -Accepts all the same flags as `create`. - -### Cancel Event -```bash -partiful events cancel <event-id> -partiful events cancel <event-id> --yes # skip confirmation -``` - -### RSVP to an Event -```bash -partiful events rsvp <event-id> # RSVP going (default) -partiful events rsvp <event-id> --status maybe # going | maybe | declined -partiful events rsvp <event-id> --plus-one Maddie --plus-one Justin -partiful events rsvp <event-id> --name "Kaleb Cole" --message "See you there" -partiful events rsvp <event-id> --dry-run --yes # preview the addGuest payload -``` -Read-before-write: updates your existing guest record if present (`updated:true`), -else creates one (`guestId:null` on the wire). Ticketed/paid events and events -that require a host questionnaire are refused with a validation error (exit 3), -since the CLI cannot purchase tickets or submit questionnaire answers yet, use -the app for those. `--yes` / `--force` skips the write confirmation. - -### Mark Interest -```bash -partiful events interested <event-id> # mark interested -partiful events interested <event-id> --remove # remove interest -``` - -> These four verbs are also aliased under `explore` (`partiful explore rsvp ...`, -> `partiful explore interested ...`) for the public-event discovery flow. They -> forward to the same handler and behave identically. - -## Posters & Images - -Three ways to set event imagery (all require Partiful auth since they're used on `events create/update`): - -| Method | Flag | Extra Auth | Notes | -|--------|------|------------|-------| -| Built-in poster (by ID) | `--poster <id>` | None | Catalog is public; use `posters search` to find IDs | -| Built-in poster (fuzzy) | `--poster-search <query>` | None | Picks best match automatically | -| Custom upload | `--image <path\|url>` | Firebase token | File path or URL, 10MB limit | - -```bash -# Find a poster -partiful posters search "birthday" - -# Create with that poster -partiful events create --title "Birthday Bash" --date "2026-06-15T19:00" --poster "birthdaypresent.png" - -# Or let fuzzy search pick one -partiful events create --title "Birthday Bash" --date "2026-06-15T19:00" --poster-search "birthday cake" - -# Upload your own image -partiful events create --title "Party" --date "2026-06-15T19:00" --image ./flyer.png -partiful events create --title "Party" --date "2026-06-15T19:00" --image "https://example.com/poster.jpg" -``` - -## Helpers - -### Clone Event (+clone) -```bash -partiful events +clone <event-id> --date "2026-06-01T19:00" -partiful events +clone <event-id> --date "2026-06-01T19:00" --title "Game Night v2" -``` -Copies title, description, location, and settings. Guests are NOT copied (use `guests +share`). - -## ⚠️ Formatting Rules - -### Dates — Always Include Full Year -```text -✅ 2026-04-01T19:00 -✅ 2026-04-01 7pm -❌ Apr 1 7pm -❌ 04/01 7pm -``` -The CLI defaults to `America/Los_Angeles`. Pass `--timezone` explicitly if the event is in another timezone. - -### Descriptions — Plain Text Only -Partiful renders descriptions as **plain text**. No markdown. -```text -✅ "🎮 Game Night!\n\nBring your favorite board games.\nSnacks provided.\n\n📍 Parking on the left side" -❌ "**Game Night!**\n\n- Bring your favorite board games\n- Snacks provided" -``` -Use emoji for visual breaks. Use `\n` for line breaks. No `**`, no `-` lists, no `#` headers. - -### Times — Double-Check AM vs PM -Early morning events (e.g., brunch at 10am) — verify you didn't accidentally set 10pm. The CLI will echo back the parsed time in its response. - -## Tips -- Use `--dry-run` on create/update/cancel to preview changes without executing. -- Pipe `events list --format json` to `jq` for scripting. -- Event IDs are returned in list output and can be used across all commands. diff --git a/skills/partiful-guests/SKILL.md b/skills/partiful-guests/SKILL.md deleted file mode 100644 index 1a05fa0..0000000 --- a/skills/partiful-guests/SKILL.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -name: partiful-guests -description: Manage Partiful event guests — list RSVPs, send invites, watch changes, export ---- - -# Partiful CLI — Guests - -## Commands - -### List Guests / RSVPs -```bash -partiful guests list <event-id> -partiful guests list <event-id> --status going --format table -partiful guests list <event-id> --status maybe,declined -``` - -### Send Invites -```bash -partiful guests invite <event-id> --phone "+12065551234" -partiful guests invite <event-id> --phones "+12065551234,+12065555678" -partiful guests invite <event-id> --name "Alex Smith" # lookup from contacts -``` - -## Helpers - -### Watch for RSVP Changes (+watch) -Stream RSVP changes in real-time: -```bash -partiful guests +watch <event-id> -partiful guests +watch <event-id> --format json # NDJSON stream -partiful guests +watch <event-id> --interval 30 # poll every 30s -``` -Outputs new RSVPs and status changes as they arrive. - -### Export Guest List (+export) -```bash -partiful guests +export <event-id> --format csv --output guests.csv -partiful guests +export <event-id> --format json --output guests.json -partiful guests +export <event-id> --status going --format table -``` - -### Share / Re-invite (+share) -Bulk invite guests from a previous event: -```bash -partiful guests +share <source-event-id> --to <target-event-id> -partiful guests +share <source-event-id> --to <target-event-id> --status going # only those who went -partiful guests +share <source-event-id> --to <target-event-id> --dry-run -``` - -## Tips -- Guest statuses: `going`, `maybe`, `declined`, `invited` (no response yet). -- `+watch` is useful for day-of monitoring of RSVPs. -- `+export` respects `--status` to filter before export. -- `+share` combined with `events +clone` enables full event duplication. diff --git a/skills/partiful-posters/SKILL.md b/skills/partiful-posters/SKILL.md deleted file mode 100644 index 70b0389..0000000 --- a/skills/partiful-posters/SKILL.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -name: partiful-posters -description: Browse and search the Partiful poster catalog for event imagery ---- - -# Partiful CLI — Posters - -Browse the built-in poster catalog. No auth required — the catalog is public. - -## Commands - -### List Posters -```bash -partiful posters list -partiful posters list --category "Dinner Party" --limit 5 -partiful posters list --type gif --limit 10 -``` - -| Flag | Description | -|------|-------------| -| `--category <name>` | Filter by category (e.g., "Birthday", "Dinner Party") | -| `--type <ext>` | Filter by file type: `png`, `gif`, `jpeg` | -| `--limit <n>` | Max results (default: 20) | - -### Search Posters -```bash -partiful posters search "birthday" -partiful posters search "disco" --limit 5 -``` -Fuzzy search across poster names, tags, and categories. - -### Get Poster Details -```bash -partiful posters get <posterId> -``` -Returns full metadata: name, categories, tags, preview URL. - -## Usage with Events - -Once you find a poster ID, pass it to `events create` or `events update`: -```bash -partiful events create --title "Party" --date "2026-04-05T19:00" --poster "piscesairbrush.png" -``` - -Or skip the manual search — `--poster-search` does it in one step: -```bash -partiful events create --title "Party" --date "2026-04-05T19:00" --poster-search "disco" -``` diff --git a/skills/partiful-shared/SKILL.md b/skills/partiful-shared/SKILL.md deleted file mode 100644 index 7ced2aa..0000000 --- a/skills/partiful-shared/SKILL.md +++ /dev/null @@ -1,105 +0,0 @@ ---- -name: partiful-shared -description: Shared auth, global flags, formatting rules, and security for the Partiful CLI ---- - -# Partiful CLI — Shared Patterns - -## Authentication - -### Login -```bash -partiful auth login <phone> -partiful auth login +12065551234 -``` -Phone-based authentication. Prefer **E.164 format** (e.g., `+12065551234`); US numbers without `+1` are normalized automatically. - -1. Sends an SMS verification code. -2. If supported on your platform (macOS via `imsg`, Android via `termux-sms-list`), auto-retrieves the code. -3. Otherwise (or on timeout), prompts for manual code entry. - -| Flag | Description | -|------|-------------| -| `--code <code>` | Provide verification code directly (skip SMS wait) | -| `--no-auto` | Disable automatic SMS retrieval | - -### Check Status -```bash -partiful auth status -``` - -### Credential Resolution (priority order) -1. `PARTIFUL_TOKEN` environment variable -2. `~/.config/partiful/auth.json` - -## Global Flags - -| Flag | Description | -|------|-------------| -| `--format <json\|table\|csv>` | Output format (default: `json`) | -| `--dry-run` | Preview without making changes | -| `--yes` | Skip confirmation prompts | -| `--force` | Override safety checks | -| `--verbose` | Verbose logging to stderr | -| `--output <file>` | Write output to file | -| `--no-color` | Disable colored output | - -## JSON Envelope - -### Success -```json -{ "status": "success", "data": { ... }, "metadata": { "count": 5 } } -``` - -### Error -```json -{ "status": "error", "error": { "code": 2, "type": "AUTH_ERROR", "message": "Token expired" } } -``` - -## Exit Codes - -| Code | Meaning | -|------|---------| -| 0 | Success | -| 1 | API error | -| 2 | Auth error | -| 3 | Validation error | -| 4 | Not found | -| 5 | Internal error | - -## ⚠️ Formatting Rules (Important!) - -### Dates — Always Include Full Year -```text -✅ 2026-04-01T19:00 -✅ 2026-04-01 7pm -❌ Apr 1 7pm -❌ 04/01 7pm -``` -Default timezone: `America/Los_Angeles`. Use `--timezone` for other zones. - -### Descriptions — Plain Text Only -Partiful renders plain text. **No markdown.** -```text -✅ "🎮 Game Night!\n\nBring your favorite board games.\nSnacks provided." -❌ "**Game Night!**\n\n- Bring board games\n- Snacks provided" -``` -Use emoji for visual breaks. Use `\n` for newlines. No `**`, `-` lists, or `#` headers. - -### Times — Verify AM vs PM -Always double-check AM/PM, especially for morning events. The CLI echoes back the parsed time. - -## Schema Introspection - -Discover command parameters programmatically: -```bash -partiful schema events.create -partiful schema guests.invite -``` - -## Security - -- **Never** log or print tokens in output. -- Use `--dry-run` to preview destructive operations. -- Credentials stored in `~/.config/partiful/` with `0600` permissions. -- Token is excluded from `--verbose` output. diff --git a/skills/partiful/SKILL.md b/skills/partiful/SKILL.md new file mode 100644 index 0000000..cf4e11d --- /dev/null +++ b/skills/partiful/SKILL.md @@ -0,0 +1,38 @@ +--- +name: partiful +description: Use when the user wants to check Partiful, discover events to attend, RSVP or express interest, create or manage an event, invite or review guests, choose event imagery, or send a text blast. +--- + +# Partiful CLI + +Operate Partiful through the JSON-first `partiful` CLI. Load only the reference needed for the current task. + +## Start Here + +1. Run `partiful doctor` before assuming authentication works. +2. Keep default JSON output for agent workflows. Use `partiful schema <command.path>` or `<command> --help` when exact parameters matter. +3. Preview writes with `--dry-run` whenever supported. +4. Ask for confirmation before cancelling events, sending text blasts, inviting or modifying guests in bulk, or any other action that affects real people. Use `--yes` only after approval. + +## Route by Task + +| Task | Read | +|---|---| +| Login, auth status, credential resolution, or authentication diagnostics | [Authentication](references/authentication.md) | +| Output formats, global flags, schema discovery, errors, or cross-command safety | [CLI output and safety](references/cli-output-and-safety.md) | +| List, inspect, create, update, cancel, clone, template, or bulk-manage events | [Events](references/events.md) | +| RSVP to an event or express interest as an attendee | [RSVPs and interest](references/rsvps-and-interest.md) | +| List/export/watch guests, invite people, find contacts, or manage cohosts as a host | [Guests, invitations, and cohosts](references/guests-invitations-and-cohosts.md) | +| Browse posters, select event imagery, or upload a custom image | [Posters and images](references/posters-and-images.md) | +| Draft, preview, or send a text blast | [Text blasts](references/text-blasts.md) | + +Read multiple references only when a task crosses those boundaries. + +## Universal Rules + +- Dates must include the full year. Default timezone is `America/Los_Angeles`; pass `--timezone` explicitly elsewhere. Verify the parsed date, time, and AM/PM in output. +- Event descriptions are plain text, not Markdown. Use line breaks and emoji rather than Markdown headings, bullets, or emphasis. +- Never print authentication tokens. Do not expose phone numbers or Partiful user IDs in user-facing summaries. +- Guest lists are host-only. A `403` while attending but not hosting is expected. +- Contacts expose names and user IDs, not email addresses or phone numbers. +- Prefer built-in posters when custom image upload fails. diff --git a/skills/partiful/references/authentication.md b/skills/partiful/references/authentication.md new file mode 100644 index 0000000..fbbf7c9 --- /dev/null +++ b/skills/partiful/references/authentication.md @@ -0,0 +1,21 @@ +# Authentication + +```bash +partiful auth login +120****1234 +partiful auth login +120****1234 --code 123456 +partiful auth login +120****1234 --no-auto +partiful auth status +partiful doctor +``` + +Login sends an SMS verification code. The CLI may retrieve it automatically on supported platforms; otherwise it prompts. Prefer E.164 phone numbers. + +Credentials resolve in this order: + +1. `PARTIFUL_TOKEN` +2. Credential file selected by `PARTIFUL_CREDENTIALS_FILE` +3. `~/.config/partiful/auth.json` + +A `userId: null` diagnostic does not necessarily block operations because Firebase token authentication remains valid. A later refresh can backfill the user ID. + +Never log, display, or persist tokens outside the CLI credential store. Do not include phone numbers or Partiful user IDs in user-facing summaries. \ No newline at end of file diff --git a/skills/partiful/references/cli-output-and-safety.md b/skills/partiful/references/cli-output-and-safety.md new file mode 100644 index 0000000..a2ad18c --- /dev/null +++ b/skills/partiful/references/cli-output-and-safety.md @@ -0,0 +1,43 @@ +# CLI Output and Safety + +## Discovery + +```bash +partiful --help +partiful events create --help +partiful schema events.create +partiful schema guests.invite +``` + +Treat live help and schema output as authoritative when this reference and the installed CLI differ. + +## Global Flags + +| Flag | Purpose | +|---|---| +| `--format <json, table, csv, or ndjson>` | Output format; JSON is default | +| `--dry-run` | Preview without executing | +| `-y, --yes` | Skip confirmation after approval | +| `--force` | Override confirmation or overwrite protection after approval | +| `-v, --verbose` | Write request diagnostics to stderr | +| `-o, --output <path>` | Write output to a file | +| `--no-color` | Disable color | + +Success and failure use structured envelopes: + +```json +{"status":"success","data":{},"metadata":{}} +``` + +```json +{"status":"error","error":{"code":2,"type":"auth_error","message":"Token expired"}} +``` + +Exit codes: `0` success, `1` API, `2` auth, `3` validation, `4` not found, `5` internal. + +## Safety + +- Use `--dry-run` before mutations when available. +- Get explicit approval before sending blasts, cancelling events, inviting guests, or bulk actions. +- Keep JSON on stdout and diagnostics on stderr for reliable automation. +- Do not include phone numbers or Partiful user IDs in user-facing summaries. \ No newline at end of file diff --git a/skills/partiful/references/events.md b/skills/partiful/references/events.md new file mode 100644 index 0000000..94ccfe8 --- /dev/null +++ b/skills/partiful/references/events.md @@ -0,0 +1,61 @@ +# Events + +## Find and Inspect + +```bash +partiful events list +partiful events list --past --include-cancelled +partiful events get <event-id> +``` + +List results include `myRsvp` and `isHost`; aggregate `going` and `maybe` fields are guest counts, not the authenticated user's status. + +## Create + +```bash +partiful events create \ + --title "Game Night" \ + --date "2026-08-01T19:00" \ + --timezone "America/Los_Angeles" \ + --location "My Place" \ + --description $'🎮 Game Night!\n\nBring a favorite game.' \ + --dry-run +``` + +`--title` and `--date` are required unless supplied by a template. Common options: `--end-date`, `--address`, `--capacity`, `--private`, `--theme`, `--effect`, `--poster`, `--poster-search`, `--image`, `--link`, `--link-text`, `--cohost`, `--template`, and `--var`. + +Dates must include a full year. The default timezone is `America/Los_Angeles`. Descriptions are plain text, not Markdown. + +## Update and Cancel + +```bash +partiful events update <event-id> --title "New Title" --dry-run +partiful events update <event-id> --date "2026-08-02T19:00" --location "New Venue" --dry-run +partiful events cancel <event-id> --dry-run +partiful events cancel <event-id> --yes +``` + +Get approval before cancellation. Verify event ID, title, date, timezone, and target fields first. + +## Clone, Share Links, Templates, and Bulk Work + +```bash +partiful +clone <event-id> --date "2026-09-01T19:00" --dry-run +partiful +share <event-id> +partiful template list +partiful template save --name <name> --title "Game Night" --location "My Place" +partiful events create --template <name> --date "2026-09-01T19:00" +partiful bulk --help +``` + +`+clone`, `+share`, and `+export` are top-level helper commands, not subcommands under `events`. Cloning copies event details, not guests. `+share` returns the event's shareable URL. Templates are assembled from explicit fields rather than imported from an event. Inspect `partiful template --help`, `partiful bulk --help`, or schema output before less common flows. Preview bulk operations and get approval before execution. + +## Verify + +After any write, inspect returned JSON and, when useful, run: + +```bash +partiful events get <event-id> +``` + +Confirm title, full timestamp, timezone, location, visibility, and URL rather than relying only on exit status. diff --git a/skills/partiful/references/guests-invitations-and-cohosts.md b/skills/partiful/references/guests-invitations-and-cohosts.md new file mode 100644 index 0000000..ca20a7e --- /dev/null +++ b/skills/partiful/references/guests-invitations-and-cohosts.md @@ -0,0 +1,46 @@ +# Guests, Invitations, and Cohosts + +These workflows are for event hosts. A `403` for an event merely attended is expected. + +## List, Watch, and Export Guests + +```bash +partiful guests list <event-id> +partiful guests list <event-id> --status GOING +partiful +watch <event-id> --interval 30 --duration 60 +partiful +export <event-id> --format csv --output guests.csv +``` + +`+watch` and `+export` are top-level helper commands. Statuses include `GOING`, `MAYBE`, `SENT`, `DECLINED`, and `WAITLIST`. + +## Invite People + +```bash +partiful guests invite <event-id> --phone +120****1234 --dry-run +partiful guests invite <event-id> --user-id <partiful-user-id> --dry-run +partiful guests invite <event-id> --user-id <id> --message "Hope you can make it" +``` + +There is no direct `--name` invite. Resolve a name first: + +```bash +partiful contacts list "Alex" +partiful guests invite <event-id> --user-id <resolved-id> --dry-run +``` + +Contacts return names, IDs, and shared-event counts. They do not expose email addresses or phone numbers. Get approval before sending invites, especially in bulk. + +## Cohosts + +```bash +partiful cohosts list <event-id> +partiful cohosts add <event-id> --name "Alex Smith" --dry-run +partiful cohosts add <event-id> --user-id <partiful-user-id> --dry-run +partiful cohosts remove <event-id> --user-id <partiful-user-id> --dry-run +``` + +Resolve ambiguous contact names before adding cohosts. Verify the event and proposed changes, then get approval before adding or removing cohosts. + +## Privacy + +Phone numbers and Partiful user IDs may be needed as command inputs. Do not echo them in user-facing completion messages or logs. \ No newline at end of file diff --git a/skills/partiful/references/posters-and-images.md b/skills/partiful/references/posters-and-images.md new file mode 100644 index 0000000..8e77b10 --- /dev/null +++ b/skills/partiful/references/posters-and-images.md @@ -0,0 +1,30 @@ +# Posters and Images + +The built-in poster catalog is public and does not require Partiful authentication. + +## Browse and Select + +```bash +partiful posters list --limit 20 +partiful posters list --category "Birthday" --type gif --limit 10 +partiful posters search "disco" --limit 5 +partiful posters get <poster-id> +``` + +Search covers names, tags, and categories. Apply a selected poster with either an exact ID or a fuzzy query: + +```bash +partiful events create --title "Party" --date "2026-08-01T19:00" --poster <poster-id> --dry-run +partiful events update <event-id> --poster-search "disco" --dry-run +``` + +## Custom Images + +```bash +partiful events create --title "Party" --date "2026-08-01T19:00" --image ./flyer.png --dry-run +partiful events update <event-id> --image "https://example.com/poster.jpg" --dry-run +``` + +Custom image upload requires authentication and may fail with a `404` even when other operations work. If it does, use a built-in poster through `--poster` or `--poster-search` rather than repeatedly retrying upload. + +Only choose one of `--poster`, `--poster-search`, or `--image` per command. Verify the event afterward with `partiful events get <event-id>`. diff --git a/skills/partiful/references/rsvps-and-interest.md b/skills/partiful/references/rsvps-and-interest.md new file mode 100644 index 0000000..6436c20 --- /dev/null +++ b/skills/partiful/references/rsvps-and-interest.md @@ -0,0 +1,15 @@ +# RSVPs and Interest + +```bash +partiful events rsvp <event-id> --dry-run +partiful events rsvp <event-id> --status going +partiful events rsvp <event-id> --status going --plus-one "Alex Smith" +partiful events rsvp <event-id> --status maybe --message "I may be late" +partiful events rsvp <event-id> --status declined +partiful events interested <event-id> +partiful events interested <event-id> --remove +``` + +`explore rsvp` and `explore interested` are equivalent aliases. + +The current CLI cannot complete ticket purchases or host questionnaires. Questionnaire response fields are known internally, but `events rsvp` exposes no answer option and deliberately rejects questionnaire-gated events. Use Partiful directly for either flow. \ No newline at end of file diff --git a/skills/partiful/references/text-blasts.md b/skills/partiful/references/text-blasts.md new file mode 100644 index 0000000..1e297cb --- /dev/null +++ b/skills/partiful/references/text-blasts.md @@ -0,0 +1,39 @@ +# Text Blasts + +Text blasts send real SMS messages. Partiful prepends its own host/event attribution. + +## Draft and Preview + +Messages are limited to 480 characters. Valid target statuses: `GOING`, `MAYBE`, `DECLINED`, `SENT`, `INTERESTED`, `WAITLIST`, `APPROVED`, and `RESPONDED_TO_FIND_A_TIME`. + +```bash +partiful guests list <event-id> --status GOING +partiful blasts send <event-id> \ + --message "See you tonight. Parking is on the left." \ + --to GOING,MAYBE \ + --dry-run +``` + +Before sending, show the exact message, event, target statuses, and dry-run result. Get explicit approval. Never infer approval from a request to draft or preview. + +## Send + +```bash +partiful blasts send <event-id> \ + --message "See you tonight. Parking is on the left." \ + --to GOING,MAYBE \ + --yes +``` + +Blasts appear on the event page by default. Use `--no-show-on-event-page` to hide one from the activity feed. + +## Safety Checklist + +- Correct event ID and title +- Exact final text, at most 480 characters +- Correct target statuses +- Dry run completed +- Explicit approval received +- `--yes` added only after approval + +Report delivery result and aggregate targeting, but do not expose recipient phone numbers or Partiful user IDs. diff --git a/src/cli.ts b/src/cli.ts index 1e471cd..25b91f2 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -15,7 +15,6 @@ import { registerPosterCommands } from './commands/posters.js'; import { registerDoctorCommands } from './commands/doctor.js'; import { registerTemplateCommands } from './commands/templates.js'; import { registerBulkCommands } from './commands/bulk.js'; -import { registerSetupCommands } from './commands/setup.js'; import { registerRsvpCommands } from './commands/rsvp.js'; import { jsonOutput } from './lib/output.js'; @@ -53,8 +52,6 @@ export function run(): void { registerDoctorCommands(program); registerTemplateCommands(program); registerBulkCommands(program); - registerSetupCommands(program); - // RSVP / interest verbs, shared across the canonical `events` group and the // `explore` alias group. Look up the `events` command created above; create // the `explore` group here. diff --git a/src/commands/setup.ts b/src/commands/setup.ts deleted file mode 100644 index 9a5a46a..0000000 --- a/src/commands/setup.ts +++ /dev/null @@ -1,140 +0,0 @@ -/** - * Setup command: configure OpenClaw integration. - */ - -import fs from 'fs'; -import path from 'path'; -import { fileURLToPath } from 'url'; -import { Command } from 'commander'; -import { jsonOutput, jsonError } from '../lib/output.js'; - -function getPackageSkillsDir(): string { - const thisFile = fileURLToPath(import.meta.url); - const packageRoot = path.resolve(path.dirname(thisFile), '..', '..'); - return path.join(packageRoot, 'skills'); -} - -function resolveWorkspace(optPath: string | undefined): string | null { - if (optPath) return optPath; - if (process.env['OPENCLAW_WORKSPACE']) return process.env['OPENCLAW_WORKSPACE']!; - const home = process.env['HOME'] ?? ''; - const defaultPath = path.join(home, '.openclaw', 'workspace'); - if (fs.existsSync(defaultPath)) return defaultPath; - return null; -} - -function getSkillDirs(skillsSource: string): string[] { - return fs.readdirSync(skillsSource).filter( - d => d.startsWith('partiful-') && fs.statSync(path.join(skillsSource, d)).isDirectory() - ); -} - -export function registerSetupCommands(program: Command): void { - const setup = program - .command('setup') - .description('Setup and integration commands'); - - setup - .command('openclaw') - .description('Link partiful skills into an OpenClaw workspace') - .option('--workspace <path>', 'OpenClaw workspace path') - .option('--uninstall', 'Remove symlinks instead of creating them') - .action(async (opts: Record<string, unknown>, cmd: Command) => { - const globalOpts = cmd.optsWithGlobals<Record<string, unknown>>(); - const force = globalOpts['force'] || false; - const dryRun = globalOpts['dryRun'] || false; - - const workspace = resolveWorkspace(opts['workspace'] as string | undefined); - if (!workspace) { - jsonError( - 'Could not find OpenClaw workspace. Set $OPENCLAW_WORKSPACE, ensure ~/.openclaw/workspace exists, or pass --workspace <path>.', - 3, 'validation_error' - ); - return; - } - - const skillsSource = getPackageSkillsDir(); - if (!fs.existsSync(skillsSource)) { - jsonError(`Skills directory not found: ${skillsSource}`, 5, 'internal_error'); - return; - } - - const workspaceSkills = path.join(workspace, 'skills'); - let sourceDirs: string[]; - try { - sourceDirs = getSkillDirs(skillsSource); - } catch (e) { - jsonError(`Cannot read skills directory: ${(e as Error).message}`, 5, 'internal_error'); - return; - } - - if (sourceDirs.length === 0) { - jsonError('No partiful-* skill directories found in package.', 5, 'internal_error'); - return; - } - - // Uninstall mode - if (opts['uninstall']) { - const removed: Array<{ skill: string; path: string }> = []; - const skipped: Array<{ skill: string; reason: string }> = []; - - for (const dir of sourceDirs) { - const linkPath = path.join(workspaceSkills, dir); - let stat: fs.Stats | undefined; - try { stat = fs.lstatSync(linkPath); } catch { skipped.push({ skill: dir, reason: 'not found' }); continue; } - - if (!stat.isSymbolicLink()) { - skipped.push({ skill: dir, reason: 'not a symlink' }); - continue; - } - - if (!dryRun) fs.unlinkSync(linkPath); - removed.push({ skill: dir, path: linkPath }); - } - - jsonOutput({ action: 'uninstall', dryRun, workspace, removed, skipped }, {}, globalOpts); - return; - } - - // Install mode - if (!dryRun && !fs.existsSync(workspaceSkills)) { - fs.mkdirSync(workspaceSkills, { recursive: true }); - } - - const linked: Array<{ skill: string; from: string; to: string }> = []; - const skipped: Array<{ skill: string; reason: string }> = []; - - for (const dir of sourceDirs) { - const target = path.join(skillsSource, dir); - const linkPath = path.join(workspaceSkills, dir); - - let stat: fs.Stats | null; - try { stat = fs.lstatSync(linkPath); } catch { stat = null; } - - if (stat) { - if (stat.isSymbolicLink()) { - const existing = fs.readlinkSync(linkPath); - const resolvedExisting = path.resolve(path.dirname(linkPath), existing); - if (resolvedExisting === target) { - skipped.push({ skill: dir, reason: 'already linked' }); - continue; - } - if (force) { - if (!dryRun) fs.unlinkSync(linkPath); - } else { - skipped.push({ skill: dir, reason: `symlink exists → ${existing}` }); - continue; - } - } else { - skipped.push({ skill: dir, reason: 'path exists (not a symlink)' }); - continue; - } - } - - if (!dryRun) fs.symlinkSync(target, linkPath); - linked.push({ skill: dir, from: target, to: linkPath }); - } - - jsonOutput({ action: 'install', dryRun, workspace, linked, skipped }, {}, globalOpts); - }); -} diff --git a/tests/setup.test.js b/tests/setup.test.js deleted file mode 100644 index 8f508ef..0000000 --- a/tests/setup.test.js +++ /dev/null @@ -1,74 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import fs from 'fs'; -import path from 'path'; -import os from 'os'; -import { run, runRaw } from './helpers.js'; - -describe('setup openclaw', () => { - let tmpDir; - - beforeEach(() => { - tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'partiful-setup-')); - }); - - afterEach(() => { - fs.rmSync(tmpDir, { recursive: true, force: true }); - }); - - it('--dry-run lists what would be linked', () => { - const result = run(['setup', 'openclaw', '--workspace', tmpDir, '--dry-run']); - expect(result.status).toBe('success'); - expect(result.data.dryRun).toBe(true); - expect(result.data.action).toBe('install'); - expect(result.data.linked.length).toBeGreaterThan(0); - // No actual symlinks created - const skillsDir = path.join(tmpDir, 'skills'); - expect(fs.existsSync(skillsDir)).toBe(false); - }); - - it('creates symlinks in workspace', () => { - const result = run(['setup', 'openclaw', '--workspace', tmpDir]); - expect(result.status).toBe('success'); - expect(result.data.action).toBe('install'); - expect(result.data.linked.length).toBeGreaterThan(0); - - // Verify symlinks exist - for (const item of result.data.linked) { - const linkPath = path.join(tmpDir, 'skills', item.skill); - expect(fs.existsSync(linkPath)).toBe(true); - expect(fs.lstatSync(linkPath).isSymbolicLink()).toBe(true); - } - }); - - it('is idempotent — second run skips already-linked skills', () => { - run(['setup', 'openclaw', '--workspace', tmpDir]); - const result2 = run(['setup', 'openclaw', '--workspace', tmpDir]); - expect(result2.status).toBe('success'); - expect(result2.data.linked.length).toBe(0); - expect(result2.data.skipped.length).toBeGreaterThan(0); - expect(result2.data.skipped[0].reason).toBe('already linked'); - }); - - it('--uninstall removes symlinks', () => { - run(['setup', 'openclaw', '--workspace', tmpDir]); - const result = run(['setup', 'openclaw', '--workspace', tmpDir, '--uninstall']); - expect(result.status).toBe('success'); - expect(result.data.action).toBe('uninstall'); - expect(result.data.removed.length).toBeGreaterThan(0); - - // Verify symlinks are gone - for (const item of result.data.removed) { - expect(fs.existsSync(item.path)).toBe(false); - } - }); - - it('errors when workspace path does not exist and no env set', () => { - const { stdout, exitCode } = runRaw(['setup', 'openclaw'], { - env: { OPENCLAW_WORKSPACE: '', HOME: '/tmp/nonexistent-home-' + Date.now() }, - }); - expect(exitCode).not.toBe(0); - const parsed = JSON.parse(stdout.trim()); - expect(parsed.status).toBe('error'); - expect(parsed.error.message).toMatch(/Could not find OpenClaw workspace/); - }); -}); diff --git a/tests/skill-structure.test.js b/tests/skill-structure.test.js new file mode 100644 index 0000000..3c51aba --- /dev/null +++ b/tests/skill-structure.test.js @@ -0,0 +1,132 @@ +import { describe, it, expect } from 'vitest'; +import { execFileSync } from 'child_process'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { runRaw } from './helpers.js'; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const skillsRoot = path.join(repoRoot, 'skills'); +const skillRoot = path.join(skillsRoot, 'partiful'); + +function skillDirectories() { + return fs.readdirSync(skillsRoot) + .filter((entry) => fs.statSync(path.join(skillsRoot, entry)).isDirectory()) + .sort(); +} + +describe('bundled Partiful skill', () => { + it('ships one model-invoked skill named partiful', () => { + expect(skillDirectories()).toEqual(['partiful']); + + const skill = fs.readFileSync(path.join(skillRoot, 'SKILL.md'), 'utf8'); + expect(skill).toMatch(/^---\nname: partiful\n/); + expect(skill).toMatch(/description: .*Partiful/i); + expect(skill).not.toMatch(/disable-model-invocation:\s*true/); + }); + + it('describes user intents that should invoke the skill', () => { + const skill = fs.readFileSync(path.join(skillRoot, 'SKILL.md'), 'utf8'); + const description = skill.match(/^description: (.+)$/m)?.[1] ?? ''; + + for (const intent of ['check Partiful', 'discover events', 'RSVP', 'create or manage an event']) { + expect(description).toContain(intent); + } + }); + + it('has no broken markdown context pointers', () => { + const skill = fs.readFileSync(path.join(skillRoot, 'SKILL.md'), 'utf8'); + const links = [...skill.matchAll(/\[[^\]]+\]\((references\/[^)]+\.md)\)/g)] + .map((match) => match[1]); + + expect(links.length).toBeGreaterThan(0); + for (const link of links) { + expect(fs.existsSync(path.join(skillRoot, link))).toBe(true); + } + }); + + it('routes every supported task branch to focused reference documentation', () => { + const skill = fs.readFileSync(path.join(skillRoot, 'SKILL.md'), 'utf8'); + const expectedReferences = [ + 'events.md', + 'rsvps-and-interest.md', + 'guests-invitations-and-cohosts.md', + 'posters-and-images.md', + 'text-blasts.md', + 'authentication.md', + 'cli-output-and-safety.md', + ]; + const routedReferences = [...skill.matchAll(/^\|[^\n]+\]\(references\/([^)]+\.md)\) \|$/gm)] + .map((match) => match[1]); + + expect(routedReferences).toHaveLength(7); + expect(new Set(routedReferences).size).toBe(7); + expect(routedReferences.sort()).toEqual(expectedReferences.sort()); + for (const reference of expectedReferences) { + expect(skill).toContain(`references/${reference}`); + } + }); + + it('documents helper commands at their real top-level paths', () => { + const references = fs.readdirSync(path.join(skillRoot, 'references')) + .map((file) => fs.readFileSync(path.join(skillRoot, 'references', file), 'utf8')) + .join('\n'); + + expect(references).toContain('partiful +clone'); + expect(references).toContain('partiful +watch'); + expect(references).toContain('partiful +export'); + expect(references).toContain('partiful +share'); + expect(references).toContain('--plus-one'); + expect(references).toContain('--no-show-on-event-page'); + expect(references).not.toMatch(/partiful (?:events|guests) \+(?:clone|watch|export|share)/); + + for (const command of [ + ['+clone', '--help'], + ['+watch', '--help'], + ['+export', '--help'], + ['+share', '--help'], + ['events', 'rsvp', '--help'], + ['blasts', 'send', '--help'], + ]) { + const { stdout, exitCode } = runRaw(command); + expect(exitCode, `${command.join(' ')} should resolve`).toBe(0); + expect(stdout).toContain('Usage: partiful'); + } + + expect(runRaw(['events', 'rsvp', '--help']).stdout).toContain('--plus-one'); + expect(runRaw(['blasts', 'send', '--help']).stdout).toContain('--no-show-on-event-page'); + }); + + it('publishes SKILL.md and exactly seven routed reference files', () => { + const packDir = fs.mkdtempSync(path.join(os.tmpdir(), 'partiful-pack-')); + try { + const raw = execFileSync('npm', ['pack', '--dry-run', '--json'], { + cwd: repoRoot, + encoding: 'utf8', + env: { ...process.env, npm_config_cache: path.join(packDir, 'npm-cache') }, + }); + const files = JSON.parse(raw)[0].files.map((entry) => entry.path); + const shippedSkillFiles = files.filter((file) => file.startsWith('skills/')); + const shippedReferences = files.filter((file) => file.startsWith('skills/partiful/references/')); + + expect(shippedSkillFiles).toContain('skills/partiful/SKILL.md'); + expect(shippedReferences).toHaveLength(7); + expect(shippedSkillFiles).toHaveLength(8); + expect(shippedSkillFiles.every((file) => file.startsWith('skills/partiful/'))).toBe(true); + } finally { + fs.rmSync(packDir, { recursive: true, force: true }); + } + }); + + it('removes the obsolete OpenClaw setup command', () => { + expect(fs.existsSync(path.join(repoRoot, 'src', 'commands', 'setup.ts'))).toBe(false); + const packageJson = fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'); + const cli = fs.readFileSync(path.join(repoRoot, 'src', 'cli.ts'), 'utf8'); + expect(packageJson).not.toMatch(/openclaw/i); + expect(cli).not.toMatch(/registerSetupCommands|commands\/setup/); + const { stdout, exitCode } = runRaw(['setup', 'openclaw']); + expect(exitCode).not.toBe(0); + expect(stdout).not.toContain('"status":"success"'); + }); +});