# Seekmodo for WordPress (+ WooCommerce)

[![Latest release](https://img.shields.io/badge/release-v0.1.0-2563eb)](https://github.com/numinix/seekmodo-wordpress-connector/releases/latest)
[![Download (seekmodo.com)](https://img.shields.io/badge/download-seekmodo.com%2Fplugins%2Fwordpress-22c55e)](https://seekmodo.com/plugins/wordpress)
[![PHP](https://img.shields.io/badge/php-%5E8.0-777bb4)](composer.json)
[![WordPress](https://img.shields.io/badge/wordpress-%5E6.2-21759b)](readme.txt)
[![License](https://img.shields.io/badge/license-GPLv2--or--later-blue)](LICENSE)

The official **Seekmodo** plugin for **WordPress 6.2+** and **WooCommerce
8.0+**. A single plugin with two activatable verticals — `content` for WP
core (via `pre_get_posts` + `posts_pre_query`) and `commerce` for
WooCommerce (via `woocommerce_product_query` + `posts_search`). Routes
storefront search, indexer cron, click beacon, and type-ahead through
the Seekmodo platform at `mcp.seekmodo.com`, while keeping the native
WP_Query / Woo product query permanently armed as a graceful fallback.

> Looking for the SaaS product? Visit **<https://seekmodo.com>**.
> Looking for a different platform (Zen Cart / Magento / Shopify /
> custom)? See `seekmodo.com/plugins`.

## Why Seekmodo

- **AI search out of the box.** Vector recall + commerce-aware reranking
  on top of Typesense — no model wrangling, no prompt engineering.
- **Zero ops.** Tune everything from <https://admin.seekmodo.com>;
  the plugin pulls config + secrets in real time. No keys to copy on
  upgrade, no cron to wire by hand.
- **Auto-pairing.** Click "Connect to Seekmodo" in the WordPress admin,
  approve on seekmodo.com, and the tenant ID + HMAC secret round-trip
  back into your site automatically.
- **Self-tuning.** Default `active` mode auto-promotes from shadow to
  enforce based on observed gateway health, and auto-demotes on
  sustained failures.
- **Always-on safety net.** If the gateway is unreachable, the storefront
  falls back to native WordPress / Woo search inside the request — your
  visitors never see an empty page.

## Quick start

```bash
# Download the latest signed zip from seekmodo.com:
curl -fLO https://seekmodo.com/plugins/seekmodo-wordpress-v0.1.0.zip
curl -fLO https://seekmodo.com/plugins/seekmodo-wordpress-v0.1.0.zip.sha256
sha256sum -c seekmodo-wordpress-v0.1.0.zip.sha256
```

Then in WordPress admin → **Plugins → Add New → Upload Plugin** →
select the zip → **Install Now** → **Activate**. Defaults are
`MODE=off`, `AUTO_PROMOTE=true`, and the content vertical
auto-detects public CPTs.

After activation, click **Settings → Seekmodo Search → Connect to
Seekmodo** to round-trip your tenant ID + HMAC secret from
<https://seekmodo.com/connect>. Then flip the mode to `active` from
<https://admin.seekmodo.com>.

Full instructions: [`docs/INSTALL.md`](docs/INSTALL.md).

### Headless / CI install

```bash
wp plugin install seekmodo-wordpress.zip --activate
wp seekmodo connect --tenant=<tenant_id> --secret=<shared_secret>
wp seekmodo index --full
wp option update seekmodo_wp_mode shadow
```

## Verticals

| Vertical | Default | What it swaps |
|---|---|---|
| `content` | auto-on | WP core search — `pre_get_posts` + `posts_pre_query` for `is_search()` queries. Defaults to indexing Posts + Pages + every public CPT; filter via `seekmodo_indexable_post_types`. |
| `commerce` | auto-on if WooCommerce active | WooCommerce product search — `woocommerce_product_query` + `posts_search`. Indexes products + variations + stock/visibility/price/SKU/attributes. |

When WooCommerce is not active, the `commerce` vertical stays dormant
and the plugin produces zero errors. No conditional `if (woo)` plumbing
required in your theme.

## Modes

| Mode | Behaviour |
|---|---|
| `off` | Bypass Seekmodo entirely. Native WP_Query / Woo product query. |
| `active` | **Recommended.** Auto-promotes from `shadow` → `enforce` based on observed gateway health; auto-demotes on sustained failures. |
| `shadow` | Calls the gateway for observation/diff but always returns the native result to the visitor. |
| `enforce` | Gateway is primary; native query only when the breaker is open or the gateway returns null. |

## What the plugin swaps

The plugin attaches at **four** discrete swap-points in your
storefront. Each is opt-in and mode-aware — passing `MODE=off`
short-circuits all of them:

1. **Storefront search** — `pre_get_posts` + `posts_pre_query` (content)
   or `woocommerce_product_query` + `posts_search` (commerce).
2. **Indexer cron** — `wp_cron` walker `seekmodo_wp_index_tick`,
   auto-chunking at 500 docs / request. Delta hooks on `save_post`,
   `transition_post_status`, `before_delete_post`, plus
   `woocommerce_update_product` / stock changes.
3. **Click beacon** — `POST /wp-json/seekmodo/v1/click` mirrors clicks
   to the gateway in addition to local logging.
4. **Type-ahead** — `GET /wp-json/seekmodo/v1/typeahead?q=...` with
   prefix-tuned scoring and surface tags. Ships with an optional
   shortcode (`[seekmodo_search]`) and Gutenberg block.

Detailed contract: [`docs/INTEGRATION.md`](docs/INTEGRATION.md). All
four paths degrade gracefully — gateway down or circuit breaker open
falls through to native WP_Query / Woo query inside the same request.

## Multi-environment installs

You can install the plugin on dev / staging / preview clones of the
same site without polluting your production tenant's search index or
click stream. Each tenant on
[admin.seekmodo.com](https://admin.seekmodo.com) is locked to a single
canonical storefront host via the **Storefront domain** setting.
Connectors running on any other host return null from every
gateway-talking entry — same code path as `MODE=off` — and your site
keeps serving search via the native WordPress / Woo fallback. **Zero
visitor-facing outage on the locked-out host.**

The gateway auto-picks the right host for you: every authenticated
call carries `X-Seekmodo-Storefront-Host: <home_url host>`, and the
admin UI's dropdown is sourced from those observations. Default
heuristic skips hosts whose leftmost label is `dev|staging|stage|test|
testing|qa|preview|beta|preprod|pre-prod`. One-click operator override
on the admin UI fixes any mis-guess.

Implications:

- **One domain per tenant**, hard rule. A second site on a different
  domain is a separate Seekmodo tenant. Add it from
  admin.seekmodo.com → "+ Add storefront".
- **Indexer safety:** the bulk-upsert cron returns null on a locked-
  out host, so the production Typesense index is never overwritten
  with a dev clone's content.
- **5-min change latency:** lock changes propagate on the connector's
  next `tenant.snapshot` pull. The WP admin's "Refresh now" button
  forces immediate propagation.

## Documentation

- [`docs/INSTALL.md`](docs/INSTALL.md) — install + first-pair + verify.
- [`docs/CONFIGURATION.md`](docs/CONFIGURATION.md) — every option, what it does, and where it's authoritative (local vs. admin.seekmodo.com).
- [`docs/UPGRADE.md`](docs/UPGRADE.md) — version-to-version notes, rollback recipe.
- [`docs/TROUBLESHOOTING.md`](docs/TROUBLESHOOTING.md) — common pitfalls, log sources, circuit breaker diagnostics.
- [`docs/INTEGRATION.md`](docs/INTEGRATION.md) — internals: the four swap-points, boot order, versioning rules.
- [`docs/CONTENT_VERTICAL.md`](docs/CONTENT_VERTICAL.md) — WP core (posts, pages, CPTs).
- [`docs/COMMERCE_VERTICAL.md`](docs/COMMERCE_VERTICAL.md) — WooCommerce wiring, facets, variations.
- [`docs/TYPEAHEAD.md`](docs/TYPEAHEAD.md) — autocomplete dropdown wiring + click-beacon pattern.
- [`docs/CLICK_ATTRIBUTION.md`](docs/CLICK_ATTRIBUTION.md) — event model, surface tags, bot-check verdicts.
- [`docs/PLATFORM_NOTES.md`](docs/PLATFORM_NOTES.md) — WordPress-specific caveats (caching plugins, object cache, multisite).

## Configuration options

The minimal set you must touch on first install. Most other options
default sensibly and most are managed remotely by admin.seekmodo.com
via `tenant.snapshot`.

| Option | Default | Notes |
|---|---|---|
| `seekmodo_wp_gateway_url` | `https://mcp.seekmodo.com` | Gateway base URL; rarely changed. |
| `seekmodo_wp_tenant_id` | _empty_ | Auto-populated by the **Connect to Seekmodo** flow. |
| `seekmodo_wp_shared_secret` | _empty_ | Auto-populated by the **Connect to Seekmodo** flow. |
| `seekmodo_wp_mode` | `off` | Flip to `active` from admin.seekmodo.com once paired. |

Full list and semantics: [`docs/CONFIGURATION.md`](docs/CONFIGURATION.md).

## Sprint matrix

The plugin is built in six sprints, each landing on its own
`feat/sprint-N-*` branch and PR'd into `main`:

| Sprint | Scope | Status |
|---|---|---|
| 0 | Repo scaffold, autoloader, PHPUnit harness, release pipeline stub | shipped |
| 1 | Gateway/Client (HMAC + breaker), Mode FSM, ConnectFlow, Content vertical (SearchSwap, Indexer, PostTypeRegistry), trek.ca pilot install in shadow | shipped |
| 2 | AutoPromoter, ShadowDiffLogger, RemoteConfig, DomainLock, `wp seekmodo shadow-report` | shipped |
| 3 | Commerce vertical (Woo SearchSwap, product Indexer, ProductFields, Typeahead) | shipped |
| 4 | ClickBeacon REST + beacon.js, EventsQueue + flush cron, HealthWidget, first signed release | shipped |
| 5 | `wp seekmodo cutover-readiness`, WordPress.org readme.txt + screenshots + i18n .pot, docs full pass | shipped |

## Releases

Each release publishes a signed zip at
`https://seekmodo.com/plugins/seekmodo-wordpress-v<X.Y.Z>.zip` plus a
SHA-256 sidecar and an ed25519 detached signature. The `latest`
pointer is in `https://seekmodo.com/plugins/manifest.json` — the
in-plugin auto-updater consumes that endpoint.

The release pipeline is **operator-local**, not GitHub Actions. Per
the seekmodo monorepo's `.cursor/rules/deploy.mdc` and `AGENTS.md` §1,
GHA is not used for routine deploys (it costs us billable minutes).
Running the release locally and letting the existing `seek-api01`
deploy webhook handle the *deployment* of the resulting PR keeps the
routine release at zero GHA minutes.

### How to cut a release (maintainers)

```bash
# 1. Update CHANGELOG.md.
# 2. Commit + tag + push.
git add .
git commit -m "release: vX.Y.Z"
git tag vX.Y.Z
git push origin main vX.Y.Z

# 3. Build + sign + publish (operator-local).
python tools/build_release.py --auto-pr
```

`tools/build_release.py --auto-pr`:

1. Reads the version from `seekmodo-wordpress.php`'s plugin header.
2. Builds the zip, writes `dist/seekmodo-wordpress-v<X.Y.Z>.zip`,
   computes its SHA-256 (`<zip>.sha256`), and signs it with the
   operator-local ed25519 key at `RELEASE_SIGNING_KEY_PATH` /
   `/etc/numinix/release-signing.key` /
   `/etc/numinix/marketing-jwt.ed25519` (writes `<zip>.sig`).
3. Updates `readme.txt`'s `Stable tag:` to match.
4. Opens an auto-merging PR against
   [`numinix/seekmodo`](https://github.com/numinix/seekmodo) carrying
   the zip, signature, and manifest update under
   `services/marketing-site/public/plugins/`. When that PR merges to
   `main`, the seek-api01 deploy webhook redeploys marketing-site,
   publishing the new download. **No GHA minutes are consumed.**

## Support

- Public docs: <https://seekmodo.com/docs>
- Issues: <https://github.com/numinix/seekmodo-wordpress-connector/issues>
- Email: `support@seekmodo.com`
- Security disclosures: `security@seekmodo.com` (also <https://seekmodo.com/security>)

## License

This plugin is published by **Numinix Technology Inc.** under the
GPL v2-or-later license inherited from WordPress core. See
[`LICENSE`](LICENSE) for the full terms.
