diff --git a/package.json b/package.json
index bc546db..20d47ed 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,7 @@
"dev": "electron-vite dev",
"build": "npm run typecheck && electron-vite build",
"build:modules": "node scripts/package-modules.cjs",
+ "build:connector": "node scripts/package-connector.cjs",
"module:new": "node scripts/create-module.cjs",
"postinstall": "electron-builder install-app-deps",
"build:unpack": "npm run build && electron-builder --dir",
diff --git a/packages/aurora-dockside-connector/aurora-dockside-connector.php b/packages/aurora-dockside-connector/aurora-dockside-connector.php
new file mode 100644
index 0000000..cf53909
--- /dev/null
+++ b/packages/aurora-dockside-connector/aurora-dockside-connector.php
@@ -0,0 +1,60 @@
+
+
+ $scope,
+ 'cursor' => $cursor,
+ 'next_cursor' => $cursor + count($slice) < count($files) ? $cursor + count($slice) : null,
+ 'total' => count($files),
+ 'files' => $slice,
+ );
+ }
+
+ private static function scan(string $root): array
+ {
+ if (!is_dir($root) || !is_readable($root)) {
+ return array();
+ }
+ $result = array();
+ $iterator = new RecursiveIteratorIterator(
+ new RecursiveCallbackFilterIterator(
+ new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS),
+ function (SplFileInfo $current): bool {
+ return !in_array($current->getFilename(), self::EXCLUDED_PARTS, true) && !$current->isLink();
+ }
+ ),
+ RecursiveIteratorIterator::LEAVES_ONLY
+ );
+ foreach ($iterator as $file) {
+ if (!$file->isFile() || !$file->isReadable()) {
+ continue;
+ }
+ $absolute = $file->getPathname();
+ $relative = ltrim(str_replace('\\', '/', substr($absolute, strlen($root))), '/');
+ $result[] = array(
+ 'path' => $relative,
+ 'bytes' => $file->getSize(),
+ 'modified' => $file->getMTime(),
+ 'sha256' => hash_file('sha256', $absolute),
+ );
+ }
+ usort($result, fn(array $a, array $b): int => strcmp($a['path'], $b['path']));
+ return $result;
+ }
+}
diff --git a/packages/aurora-dockside-connector/includes/class-aurora-dockside-rest.php b/packages/aurora-dockside-connector/includes/class-aurora-dockside-rest.php
new file mode 100644
index 0000000..5148654
--- /dev/null
+++ b/packages/aurora-dockside-connector/includes/class-aurora-dockside-rest.php
@@ -0,0 +1,90 @@
+ WP_REST_Server::READABLE,
+ 'callback' => array(self::class, 'status'),
+ 'permission_callback' => array(self::class, 'authorize'),
+ ));
+ register_rest_route(self::NAMESPACE, '/manifest', array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array(self::class, 'manifest'),
+ 'permission_callback' => array(self::class, 'authorize'),
+ 'args' => array(
+ 'scope' => array('type' => 'string', 'enum' => array('content', 'uploads'), 'default' => 'content'),
+ 'cursor' => array('type' => 'integer', 'minimum' => 0, 'default' => 0),
+ 'limit' => array('type' => 'integer', 'minimum' => 1, 'maximum' => 1000, 'default' => 250),
+ ),
+ ));
+ register_rest_route(self::NAMESPACE, '/database', array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array(self::class, 'database'),
+ 'permission_callback' => array(self::class, 'authorize'),
+ ));
+ }
+
+ public static function authorize(): bool
+ {
+ return is_user_logged_in() && current_user_can('manage_options');
+ }
+
+ public static function status(): WP_REST_Response
+ {
+ global $wp_version, $wpdb;
+ $theme = wp_get_theme();
+ return new WP_REST_Response(array(
+ 'connector' => array('name' => 'Aurora Dockside Connector', 'version' => AURORA_DOCKSIDE_CONNECTOR_VERSION, 'mode' => 'read-only'),
+ 'site' => array(
+ 'name' => get_bloginfo('name'),
+ 'url' => home_url('/'),
+ 'admin_url' => admin_url('/'),
+ 'multisite' => is_multisite(),
+ 'language' => get_locale(),
+ ),
+ 'runtime' => array('wordpress' => $wp_version, 'php' => PHP_VERSION, 'database' => $wpdb->db_version()),
+ 'theme' => array('stylesheet' => $theme->get_stylesheet(), 'version' => $theme->get('Version')),
+ 'capabilities' => array('status' => true, 'file_manifest' => true, 'database_inventory' => true, 'pull' => false, 'push' => false),
+ ));
+ }
+
+ public static function manifest(WP_REST_Request $request): WP_REST_Response
+ {
+ $result = Aurora_Dockside_Manifest::page(
+ (string) $request->get_param('scope'),
+ (int) $request->get_param('cursor'),
+ (int) $request->get_param('limit')
+ );
+ return new WP_REST_Response($result);
+ }
+
+ public static function database(): WP_REST_Response
+ {
+ global $wpdb;
+ $tables = $wpdb->get_col('SHOW TABLES');
+ $inventory = array();
+ foreach ($tables as $table) {
+ $safe_table = esc_sql($table);
+ $rows = (int) $wpdb->get_var("SELECT COUNT(*) FROM `{$safe_table}`"); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+ $size = $wpdb->get_row($wpdb->prepare(
+ 'SELECT DATA_LENGTH, INDEX_LENGTH FROM information_schema.TABLES WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s',
+ DB_NAME,
+ $table
+ ), ARRAY_A);
+ $inventory[] = array(
+ 'name' => $table,
+ 'rows' => $rows,
+ 'bytes' => (int) (($size['DATA_LENGTH'] ?? 0) + ($size['INDEX_LENGTH'] ?? 0)),
+ );
+ }
+ return new WP_REST_Response(array('engine_version' => $wpdb->db_version(), 'prefix' => $wpdb->prefix, 'tables' => $inventory));
+ }
+}
diff --git a/packages/aurora-dockside-connector/readme.txt b/packages/aurora-dockside-connector/readme.txt
new file mode 100644
index 0000000..43a3741
--- /dev/null
+++ b/packages/aurora-dockside-connector/readme.txt
@@ -0,0 +1,31 @@
+=== Aurora Dockside Connector ===
+Contributors: auroradockside
+Tags: local development, migration, development
+Requires at least: 6.5
+Tested up to: 6.8
+Requires PHP: 8.0
+Stable tag: 0.1.0
+License: GPLv2 or later
+
+An original, secure connector between WordPress and Aurora Dockside.
+
+== Description ==
+
+Aurora Dockside Connector exposes authenticated site metadata, a paginated file manifest, and a database inventory to Aurora Dockside. Authentication uses WordPress Application Passwords and requires an administrator account.
+
+Version 0.1 is deliberately read-only. It does not modify the production database or production files.
+
+== Installation ==
+
+1. Upload the plugin ZIP in Plugins → Add New → Upload Plugin.
+2. Activate Aurora Dockside Connector.
+3. Open Tools → Aurora Dockside.
+4. Create an Application Password from Users → Profile.
+
+== Changelog ==
+
+= 0.1.0 =
+* Initial read-only connection API.
+* Site and runtime discovery.
+* Paginated content and uploads manifests.
+* Database table inventory.
diff --git a/scripts/package-connector.cjs b/scripts/package-connector.cjs
new file mode 100644
index 0000000..2bc0ef6
--- /dev/null
+++ b/scripts/package-connector.cjs
@@ -0,0 +1,19 @@
+'use strict'
+
+const { mkdirSync, rmSync } = require('fs')
+const { join, resolve } = require('path')
+const { spawnSync } = require('child_process')
+
+const root = resolve(__dirname, '..')
+const source = join(root, 'packages', 'aurora-dockside-connector')
+const outputDir = join(root, 'dist', 'connectors')
+const output = join(outputDir, 'aurora-dockside-connector-0.1.0.zip')
+mkdirSync(outputDir, { recursive: true })
+rmSync(output, { force: true })
+const result = spawnSync('zip', ['-qr', output, 'aurora-dockside-connector'], {
+ cwd: join(root, 'packages'),
+ stdio: 'inherit'
+})
+if (result.error) throw result.error
+if (result.status !== 0) throw new Error(`zip failed with exit code ${result.status}`)
+process.stdout.write(`${output}\n`)