custom_rules( $token, $zone ); if ( isset( $rules['error'] ) ) { return; } foreach ( (array) $rules['rules'] as $rule ) { if ( isset( $rule['ref'], $rule['id'] ) && \Agentimus\Cloudflare\EdgeRule::REF === $rule['ref'] ) { $client->delete_custom_rule( $token, $zone, (string) $rules['ruleset_id'], (string) $rule['id'] ); } } } /** * Tear down one site's data (must run in that site's context). */ function agentimus_uninstall_site() { global $wpdb; agentimus_uninstall_edge_rule(); delete_option( 'agentimus_settings' ); delete_option( 'agentimus_onboarded' ); delete_option( 'agentimus_defaults_migrated' ); delete_option( 'agentimus_signing_keys' ); delete_option( 'agentimus_rewrite_version' ); delete_option( 'agentimus_rewrite_flushed_at' ); delete_option( 'agentimus_tombstones' ); delete_option( 'agentimus_bot_ranges' ); delete_option( 'agentimus_whatsnew_seen' ); delete_option( 'agentimus_review_ask' ); delete_option( 'agentimus_route_probe' ); delete_option( 'agentimus_robots_watch' ); delete_option( 'agentimus_post_traffic_backfill' ); // How far older traffic rows have been matched to their posts. delete_option( 'agentimus_identity_probes' ); // Self-declared crawler home pages and what answered there. delete_option( 'agentimus_mcp_token' ); delete_option( 'agentimus_oauth' ); delete_option( 'agentimus_seo_mode' ); // SeoContext verdict cache — survived a real uninstall, caught 2026-08-01. delete_option( 'agentimus_next_steps' ); // The post-setup "Worth a look next" card state. delete_option( 'agentimus_digest_last' ); // Weekly digest snapshot + unsubscribe key. delete_option( 'agentimus_digest_stop_key' ); delete_option( 'agentimus_integrations_webhook_secret' ); // Webhook signing secret. delete_option( 'agentimus_integrations_telegram_token' ); // Telegram bot token (pre-store home). delete_option( 'agentimus_integrations_connections' ); // Shared credential store ({ id → row }). delete_option( 'agentimus_announcements' ); // Scheduled announcements — the queue and its ledger. delete_option( 'agentimus_integrations_state' ); // Per-connection delivery state. delete_option( 'agentimus_integrations_queue' ); // Undelivered outgoing events. delete_option( 'agentimus_integrations_findings_seen' ); // The findings diff's baseline. delete_option( 'agentimus_integrations_impostor_seen' ); // Impostor announce debounce. delete_transient( 'agentimus_ranges_pending' ); delete_transient( 'agentimus_llms_txt' ); delete_transient( 'agentimus_llms_full' ); delete_transient( 'agentimus_changes' ); delete_transient( 'agentimus' ); delete_transient( 'agentimus_activation_redirect' ); delete_transient( 'agentimus_topic_suggest' ); delete_transient( 'agentimus_review_count' ); delete_transient( 'agentimus_selfcheck' ); delete_transient( 'agentimus_selfcheck_probe' ); // Markdown body cache: the settings epoch + the filesystem cache directory. delete_option( 'agentimus_md_cache_epoch' ); $agentimus_up = wp_upload_dir(); if ( empty( $agentimus_up['error'] ) && ! empty( $agentimus_up['basedir'] ) ) { $agentimus_md_dir = $agentimus_up['basedir'] . '/agentimus-md-cache'; if ( is_dir( $agentimus_md_dir ) ) { foreach ( (array) glob( $agentimus_md_dir . '/*.md' ) as $agentimus_md_file ) { @unlink( $agentimus_md_file ); // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink, WordPress.PHP.NoSilencedErrors -- our own cache files. } // Direct rmdir, not WP_Filesystem: uninstall runs with no admin context, so requesting // filesystem credentials could prompt (or fail) and leave the directory behind. This is // our own now-empty cache dir under uploads. @rmdir( $agentimus_md_dir ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir, WordPress.WP.AlternativeFunctions.directory_rmdir, WordPress.PHP.NoSilencedErrors -- our own now-empty cache dir. } } // Activity log: drop the tables, version flags and prune schedule. $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}agentimus_agent_hits" ); // phpcs:ignore WordPress.DB $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}agentimus_ai_referrals" ); // phpcs:ignore WordPress.DB $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}agentimus_flagged_ips" ); // phpcs:ignore WordPress.DB $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}agentimus_unknown_sources" ); // phpcs:ignore WordPress.DB delete_option( 'agentimus_activity_db_version' ); delete_option( 'agentimus_referrals_db_version' ); delete_option( 'agentimus_unknown_sources_db_version' ); delete_option( 'agentimus_flagged_ips_db_version' ); delete_option( 'agentimus_review_dismissed' ); delete_option( 'agentimus_review_reverified' ); delete_option( 'agentimus_client_decisions' ); wp_clear_scheduled_hook( 'agentimus_prune_activity' ); wp_clear_scheduled_hook( 'agentimus_warm_llms_full' ); wp_clear_scheduled_hook( 'agentimus_weekly_digest' ); wp_clear_scheduled_hook( 'agentimus_refresh_bot_ranges' ); wp_clear_scheduled_hook( 'agentimus_route_probe_refresh' ); wp_clear_scheduled_hook( 'agentimus_identity_probe_run' ); // Agent Access: the credential/ability event log, its schema flag and the proven // execute-hook verdict. $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}agentimus_agent_events" ); // phpcs:ignore WordPress.DB delete_option( 'agentimus_agent_access_db_version' ); delete_option( 'agentimus_agent_access_hooks' ); // Cloudflare edge source: aggregates table, connection option, lock and poll. $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}agentimus_edge_hourly" ); // phpcs:ignore WordPress.DB delete_option( 'agentimus_cloudflare' ); delete_option( 'agentimus_edge_db_version' ); delete_option( 'agentimus_edge_probes_backfilled' ); delete_option( 'agentimus_edge_lock' ); wp_clear_scheduled_hook( 'agentimus_edge_poll' ); // Bing search source: daily-stats table, connection option, lock and poll. $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}agentimus_bing_daily" ); // phpcs:ignore WordPress.DB delete_option( 'agentimus_bing' ); delete_option( 'agentimus_bing_db_version' ); delete_option( 'agentimus_bing_lock' ); delete_option( 'agentimus_bing_query_backfill' ); wp_clear_scheduled_hook( 'agentimus_bing_poll' ); // IndexNow: the key + last-ping bookkeeping. delete_option( 'agentimus_indexnow' ); // Google search source + the shared search-queries snapshot table. $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}agentimus_search_queries" ); // phpcs:ignore WordPress.DB // The record of which pages each engine has been asked about. $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}agentimus_search_asks" ); // phpcs:ignore WordPress.DB delete_option( 'agentimus_search_asks_db_version' ); // The stored content grades the worklist ranks by. $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}agentimus_content_grades" ); // phpcs:ignore WordPress.DB delete_option( 'agentimus_grades_db_version' ); delete_option( 'agentimus_google' ); delete_option( 'agentimus_google_lock' ); delete_option( 'agentimus_google_index' ); delete_option( 'agentimus_google_index_pause' ); delete_option( 'agentimus_google_trend' ); delete_option( 'agentimus_search_db_version' ); delete_transient( 'agentimus_google_token' ); // Non-default scope token slots (Google\Auth::slot hashes the scope URL; // the plugin isn't loaded here, so the prefixes are precomputed): delete_transient( 'agentimus_google_token_59618d64f3805a74' ); // analytics.readonly delete_transient( 'agentimus_google_token_72359770ac5e1d20' ); // spreadsheets (the Sheets integration) wp_clear_scheduled_hook( 'agentimus_google_poll' ); wp_clear_scheduled_hook( 'agentimus_google_index_chunk' ); // AI Visibility monitoring: results table, options and scheduled run. $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}agentimus_visibility" ); // phpcs:ignore WordPress.DB delete_option( 'agentimus_visibility' ); delete_option( 'agentimus_visibility_last_run' ); delete_option( 'agentimus_visibility_lock' ); delete_option( 'agentimus_visibility_cryptokey' ); delete_option( 'agentimus_visibility_db_version' ); delete_option( 'agentimus_visibility_demo' ); wp_clear_scheduled_hook( 'agentimus_visibility_run' ); wp_clear_scheduled_hook( 'agentimus_visibility_run_now' ); // the one-off "run now" event. } if ( is_multisite() ) { // Page through EVERY site in bounded batches rather than a single capped query — uninstall's // contract is that nothing is left orphaned, and a fixed cap silently stranded tables/options // on sites beyond it. Teardown is light (drop tables, delete options, clear cron), so paging // is safe where a full activation loop would risk a timeout. Prefixed vars: uninstall.php runs // at global scope, so an unprefixed name could collide with another plugin's uninstaller. $agentimus_batch = 500; $agentimus_offset = 0; do { $agentimus_site_ids = get_sites( array( 'fields' => 'ids', 'number' => $agentimus_batch, 'offset' => $agentimus_offset ) ); foreach ( (array) $agentimus_site_ids as $agentimus_site_id ) { switch_to_blog( (int) $agentimus_site_id ); agentimus_uninstall_site(); restore_current_blog(); } $agentimus_offset += $agentimus_batch; } while ( count( (array) $agentimus_site_ids ) === $agentimus_batch ); } else { agentimus_uninstall_site(); }