From 14d19666339581597ec295a0223ac3e1302896a9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Mar 2026 00:21:02 +0000 Subject: [PATCH 01/13] =?UTF-8?q?feat:=20add=20Mothership=20backend=20(Pha?= =?UTF-8?q?se=201)=20=E2=80=94=20persistent=20events,=20cross-session=20sy?= =?UTF-8?q?nc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces the Mothership, a headless Node.js/TypeScript backend that browser extension regents connect to via WebSocket. Extension works standalone when disconnected (zero breaking changes). Backend (mothership/): - Hono HTTP + ws WebSocket on single port (default 3001) - SQLite with WAL mode (users, workspaces, sessions, events, messages) - JWT auth (register, login, long-lived API tokens for extensions) - Lane Queue pattern (from OpenClaw) for per-session serial execution - Event bus broadcasting DB changes to WebSocket subscribers - REST API: /auth, /workspaces, /sessions, /events Extension changes: - background.js: WebSocket connection manager with auto-reconnect - RegentSidecar: forwards extracted events to mothership (fire-and-forget) - RegentOrchestrator: listens for cross-session events from mothership - RegentSidebar: connection status dot, remote session event display - Popup: Mothership settings section (URL, token, connect/disconnect) https://claude.ai/code/session_019ZQfmDaAnvVAj18SgwxGPP --- mothership/.env.example | 4 + mothership/.gitignore | 6 + mothership/data/.gitkeep | 0 mothership/package-lock.json | 1267 +++++++++++++++++ mothership/package.json | 28 + mothership/src/api/index.ts | 19 + mothership/src/api/middleware/auth.ts | 26 + mothership/src/api/routes/auth.ts | 64 + mothership/src/api/routes/events.ts | 69 + mothership/src/api/routes/sessions.ts | 46 + mothership/src/api/routes/workspaces.ts | 49 + mothership/src/config.ts | 8 + mothership/src/db/index.ts | 50 + .../src/db/migrations/001_foundation.sql | 75 + mothership/src/db/schema.ts | 66 + mothership/src/events/bus.ts | 19 + mothership/src/index.ts | 27 + mothership/src/queue/laneQueue.ts | 28 + mothership/src/utils/crypto.ts | 35 + mothership/src/utils/id.ts | 3 + mothership/src/utils/logger.ts | 4 + mothership/src/ws/gateway.ts | 103 ++ mothership/src/ws/handlers/eventsStore.ts | 73 + mothership/src/ws/handlers/tabRegister.ts | 8 + mothership/src/ws/registry.ts | 52 + mothership/tsconfig.json | 19 + src/background.js | 112 ++ src/content/regent/RegentOrchestrator.js | 23 + src/content/regent/RegentSidebar.js | 57 + src/content/regent/RegentSidecar.js | 25 + src/content/regent/regent.css | 20 + src/popup/MothershipManager.js | 96 ++ src/popup/popup.html | 25 + src/popup/popup.js | 2 + 34 files changed, 2508 insertions(+) create mode 100644 mothership/.env.example create mode 100644 mothership/.gitignore create mode 100644 mothership/data/.gitkeep create mode 100644 mothership/package-lock.json create mode 100644 mothership/package.json create mode 100644 mothership/src/api/index.ts create mode 100644 mothership/src/api/middleware/auth.ts create mode 100644 mothership/src/api/routes/auth.ts create mode 100644 mothership/src/api/routes/events.ts create mode 100644 mothership/src/api/routes/sessions.ts create mode 100644 mothership/src/api/routes/workspaces.ts create mode 100644 mothership/src/config.ts create mode 100644 mothership/src/db/index.ts create mode 100644 mothership/src/db/migrations/001_foundation.sql create mode 100644 mothership/src/db/schema.ts create mode 100644 mothership/src/events/bus.ts create mode 100644 mothership/src/index.ts create mode 100644 mothership/src/queue/laneQueue.ts create mode 100644 mothership/src/utils/crypto.ts create mode 100644 mothership/src/utils/id.ts create mode 100644 mothership/src/utils/logger.ts create mode 100644 mothership/src/ws/gateway.ts create mode 100644 mothership/src/ws/handlers/eventsStore.ts create mode 100644 mothership/src/ws/handlers/tabRegister.ts create mode 100644 mothership/src/ws/registry.ts create mode 100644 mothership/tsconfig.json create mode 100644 src/popup/MothershipManager.js diff --git a/mothership/.env.example b/mothership/.env.example new file mode 100644 index 0000000..d6ffcdf --- /dev/null +++ b/mothership/.env.example @@ -0,0 +1,4 @@ +PORT=3001 +JWT_SECRET=change-me-to-a-random-string +DB_PATH=./data/mothership.db +LOG_LEVEL=info diff --git a/mothership/.gitignore b/mothership/.gitignore new file mode 100644 index 0000000..c0d536a --- /dev/null +++ b/mothership/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +data/*.db +data/*.db-wal +data/*.db-shm +.env diff --git a/mothership/data/.gitkeep b/mothership/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/mothership/package-lock.json b/mothership/package-lock.json new file mode 100644 index 0000000..1a4baa7 --- /dev/null +++ b/mothership/package-lock.json @@ -0,0 +1,1267 @@ +{ + "name": "regent-mothership", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "regent-mothership", + "version": "0.1.0", + "dependencies": { + "@hono/node-server": "^1.13.7", + "better-sqlite3": "^11.7.0", + "hono": "^4.6.0", + "jose": "^5.9.0", + "nanoid": "^5.0.9", + "pino": "^9.5.0", + "ws": "^8.18.0" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.12", + "@types/node": "^22.10.0", + "@types/ws": "^8.5.13", + "tsx": "^4.19.0", + "typescript": "^5.7.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.9", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", + "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "22.19.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.13.tgz", + "integrity": "sha512-akNQMv0wW5uyRpD2v2IEyRSZiR+BeGuoB6L310EgGObO44HSMNT8z1xzio28V8qOrgYaopIDNA18YgdXd+qTiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", + "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/hono": { + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.3.tgz", + "integrity": "sha512-SFsVSjp8sj5UumXOOFlkZOG6XS9SJDKw0TbwFeV+AJ8xlST8kxK5Z/5EYa111UY8732lK2S/xB653ceuaoGwpg==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", + "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz", + "integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.87.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz", + "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pino": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", + "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/mothership/package.json b/mothership/package.json new file mode 100644 index 0000000..a3f1c17 --- /dev/null +++ b/mothership/package.json @@ -0,0 +1,28 @@ +{ + "name": "regent-mothership", + "version": "0.1.0", + "type": "module", + "private": true, + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc", + "start": "node dist/index.js", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@hono/node-server": "^1.13.7", + "better-sqlite3": "^11.7.0", + "hono": "^4.6.0", + "jose": "^5.9.0", + "nanoid": "^5.0.9", + "pino": "^9.5.0", + "ws": "^8.18.0" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.12", + "@types/node": "^22.10.0", + "@types/ws": "^8.5.13", + "tsx": "^4.19.0", + "typescript": "^5.7.0" + } +} diff --git a/mothership/src/api/index.ts b/mothership/src/api/index.ts new file mode 100644 index 0000000..13e1998 --- /dev/null +++ b/mothership/src/api/index.ts @@ -0,0 +1,19 @@ +import { Hono } from 'hono'; +import { cors } from 'hono/cors'; +import { authRoutes } from './routes/auth.js'; +import { workspaceRoutes } from './routes/workspaces.js'; +import { sessionRoutes } from './routes/sessions.js'; +import { eventRoutes } from './routes/events.js'; + +export const api = new Hono().basePath('/api/v1'); + +api.use('*', cors()); + +// Health check +api.get('/health', (c) => c.json({ status: 'ok', ts: Date.now() })); + +// Mount routes +api.route('/auth', authRoutes); +api.route('/workspaces', workspaceRoutes); +api.route('/workspaces/:wsId/sessions', sessionRoutes); +api.route('/workspaces/:wsId/events', eventRoutes); diff --git a/mothership/src/api/middleware/auth.ts b/mothership/src/api/middleware/auth.ts new file mode 100644 index 0000000..1ddd877 --- /dev/null +++ b/mothership/src/api/middleware/auth.ts @@ -0,0 +1,26 @@ +import { createMiddleware } from 'hono/factory'; +import { verifyToken } from '../../utils/crypto.js'; + +export type AuthPayload = { userId: string; username: string }; + +// Augment Hono's context variables +declare module 'hono' { + interface ContextVariableMap { + auth: AuthPayload; + } +} + +export const authMiddleware = createMiddleware(async (c, next) => { + const header = c.req.header('Authorization'); + if (!header?.startsWith('Bearer ')) { + return c.json({ error: 'Missing or invalid Authorization header' }, 401); + } + + try { + const payload = await verifyToken(header.slice(7)); + c.set('auth', { userId: payload.sub as string, username: payload.username as string }); + await next(); + } catch { + return c.json({ error: 'Invalid or expired token' }, 401); + } +}); diff --git a/mothership/src/api/routes/auth.ts b/mothership/src/api/routes/auth.ts new file mode 100644 index 0000000..ddbe779 --- /dev/null +++ b/mothership/src/api/routes/auth.ts @@ -0,0 +1,64 @@ +import { Hono } from 'hono'; +import { getDb } from '../../db/index.js'; +import { newId } from '../../utils/id.js'; +import { hashPassword, verifyPassword, signToken } from '../../utils/crypto.js'; +import type { User } from '../../db/schema.js'; + +export const authRoutes = new Hono(); + +// POST /auth/register +authRoutes.post('/register', async (c) => { + const { username, password } = await c.req.json<{ username: string; password: string }>(); + if (!username || !password || password.length < 8) { + return c.json({ error: 'Username required, password min 8 chars' }, 400); + } + + const db = getDb(); + const existing = db.prepare('SELECT id FROM users WHERE username = ?').get(username); + if (existing) return c.json({ error: 'Username taken' }, 409); + + const id = newId(); + db.prepare('INSERT INTO users (id, username, password_hash) VALUES (?, ?, ?)').run( + id, username, hashPassword(password) + ); + + // Auto-create default workspace + const wsId = newId(); + db.prepare('INSERT INTO workspaces (id, name, owner_id) VALUES (?, ?, ?)').run( + wsId, `${username}'s workspace`, id + ); + db.prepare('INSERT INTO workspace_members (workspace_id, user_id, role) VALUES (?, ?, ?)').run( + wsId, id, 'owner' + ); + + const token = await signToken({ sub: id, username }); + return c.json({ id, username, token, workspaceId: wsId }, 201); +}); + +// POST /auth/login +authRoutes.post('/login', async (c) => { + const { username, password } = await c.req.json<{ username: string; password: string }>(); + const db = getDb(); + const user = db.prepare('SELECT * FROM users WHERE username = ?').get(username) as User | undefined; + + if (!user || !verifyPassword(password, user.password_hash)) { + return c.json({ error: 'Invalid credentials' }, 401); + } + + const token = await signToken({ sub: user.id, username: user.username }); + return c.json({ id: user.id, username: user.username, token }); +}); + +// POST /auth/token/generate — issue a long-lived API token for the extension +authRoutes.post('/token/generate', async (c) => { + const { username, password } = await c.req.json<{ username: string; password: string }>(); + const db = getDb(); + const user = db.prepare('SELECT * FROM users WHERE username = ?').get(username) as User | undefined; + + if (!user || !verifyPassword(password, user.password_hash)) { + return c.json({ error: 'Invalid credentials' }, 401); + } + + const token = await signToken({ sub: user.id, username: user.username }, '365d'); + return c.json({ token, expiresIn: '365d' }); +}); diff --git a/mothership/src/api/routes/events.ts b/mothership/src/api/routes/events.ts new file mode 100644 index 0000000..b040748 --- /dev/null +++ b/mothership/src/api/routes/events.ts @@ -0,0 +1,69 @@ +import { Hono } from 'hono'; +import { getDb } from '../../db/index.js'; +import { newId } from '../../utils/id.js'; +import { authMiddleware } from '../middleware/auth.js'; +import { bus } from '../../events/bus.js'; +import type { RegentEvent } from '../../db/schema.js'; + +export const eventRoutes = new Hono(); +eventRoutes.use('*', authMiddleware); + +// GET /workspaces/:wsId/events — list events, optionally filtered by session +eventRoutes.get('/', (c) => { + const wsId = c.req.param('wsId'); + const sessionId = c.req.query('sessionId'); + const limit = parseInt(c.req.query('limit') || '100', 10); + const db = getDb(); + + const sql = sessionId + ? 'SELECT * FROM events WHERE workspace_id = ? AND session_id = ? ORDER BY created_at DESC LIMIT ?' + : 'SELECT * FROM events WHERE workspace_id = ? ORDER BY created_at DESC LIMIT ?'; + const params = sessionId ? [wsId, sessionId, limit] : [wsId, limit]; + + return c.json(db.prepare(sql).all(...params) as RegentEvent[]); +}); + +// POST /workspaces/:wsId/events/bulk — store pre-extracted events from extension +eventRoutes.post('/bulk', async (c) => { + const wsId = c.req.param('wsId')!; + const { sessionId, events } = await c.req.json<{ + sessionId: string; + events: Array<{ title: string; summary: string; importance?: string; messageIndex?: number }>; + }>(); + + if (!sessionId || !events?.length) { + return c.json({ error: 'sessionId and events[] required' }, 400); + } + + const db = getDb(); + + // Ensure session exists (upsert) + const existingSession = db.prepare('SELECT id FROM sessions WHERE id = ?').get(sessionId); + if (!existingSession) { + db.prepare(`INSERT INTO sessions (id, workspace_id, status) VALUES (?, ?, 'active')`).run(sessionId, wsId); + } + + const insert = db.prepare(`INSERT INTO events (id, session_id, workspace_id, title, summary, importance, message_index) + VALUES (?, ?, ?, ?, ?, ?, ?)`); + + const stored: RegentEvent[] = []; + const tx = db.transaction(() => { + for (const evt of events) { + const id = newId(); + insert.run(id, sessionId, wsId, evt.title, evt.summary, evt.importance || 'medium', evt.messageIndex ?? null); + stored.push({ + id, session_id: sessionId, workspace_id: wsId, + title: evt.title, summary: evt.summary, + importance: (evt.importance || 'medium') as RegentEvent['importance'], + message_index: evt.messageIndex ?? null, + source_tab_id: null, created_at: new Date().toISOString(), + }); + } + }); + tx(); + + // Broadcast to all connected tabs in this workspace + bus.emit('events:new', { workspaceId: wsId, sessionId, events: stored }); + + return c.json({ stored: stored.length }, 201); +}); diff --git a/mothership/src/api/routes/sessions.ts b/mothership/src/api/routes/sessions.ts new file mode 100644 index 0000000..9c2c399 --- /dev/null +++ b/mothership/src/api/routes/sessions.ts @@ -0,0 +1,46 @@ +import { Hono } from 'hono'; +import { getDb } from '../../db/index.js'; +import { newId } from '../../utils/id.js'; +import { authMiddleware } from '../middleware/auth.js'; +import type { Session } from '../../db/schema.js'; + +export const sessionRoutes = new Hono(); +sessionRoutes.use('*', authMiddleware); + +// GET /workspaces/:wsId/sessions +sessionRoutes.get('/', (c) => { + const wsId = c.req.param('wsId'); + const status = c.req.query('status'); // optional filter: 'active' | 'closed' + const db = getDb(); + + const sql = status + ? 'SELECT * FROM sessions WHERE workspace_id = ? AND status = ? ORDER BY updated_at DESC' + : 'SELECT * FROM sessions WHERE workspace_id = ? ORDER BY updated_at DESC'; + const params = status ? [wsId, status] : [wsId]; + + return c.json(db.prepare(sql).all(...params) as Session[]); +}); + +// POST /workspaces/:wsId/sessions +sessionRoutes.post('/', async (c) => { + const wsId = c.req.param('wsId'); + const { externalId, name, url, hostname } = await c.req.json<{ + externalId?: string; name?: string; url?: string; hostname?: string; + }>(); + + const db = getDb(); + const id = newId(); + db.prepare(`INSERT INTO sessions (id, workspace_id, external_id, name, url, hostname) + VALUES (?, ?, ?, ?, ?, ?)`).run(id, wsId, externalId ?? null, name ?? null, url ?? null, hostname ?? null); + + return c.json({ id, workspace_id: wsId, status: 'active' }, 201); +}); + +// PATCH /workspaces/:wsId/sessions/:id — close session +sessionRoutes.patch('/:id', async (c) => { + const { status } = await c.req.json<{ status: 'closed' }>(); + const db = getDb(); + db.prepare("UPDATE sessions SET status = ?, updated_at = datetime('now') WHERE id = ?") + .run(status, c.req.param('id')); + return c.json({ ok: true }); +}); diff --git a/mothership/src/api/routes/workspaces.ts b/mothership/src/api/routes/workspaces.ts new file mode 100644 index 0000000..a46322a --- /dev/null +++ b/mothership/src/api/routes/workspaces.ts @@ -0,0 +1,49 @@ +import { Hono } from 'hono'; +import { getDb } from '../../db/index.js'; +import { newId } from '../../utils/id.js'; +import { authMiddleware } from '../middleware/auth.js'; +import type { Workspace } from '../../db/schema.js'; + +export const workspaceRoutes = new Hono(); +workspaceRoutes.use('*', authMiddleware); + +// GET /workspaces — list user's workspaces +workspaceRoutes.get('/', (c) => { + const { userId } = c.get('auth'); + const db = getDb(); + const rows = db.prepare(` + SELECT w.* FROM workspaces w + JOIN workspace_members wm ON wm.workspace_id = w.id + WHERE wm.user_id = ? + ORDER BY w.created_at DESC + `).all(userId) as Workspace[]; + return c.json(rows); +}); + +// POST /workspaces +workspaceRoutes.post('/', async (c) => { + const { userId } = c.get('auth'); + const { name } = await c.req.json<{ name: string }>(); + if (!name) return c.json({ error: 'Name required' }, 400); + + const db = getDb(); + const id = newId(); + db.prepare('INSERT INTO workspaces (id, name, owner_id) VALUES (?, ?, ?)').run(id, name, userId); + db.prepare('INSERT INTO workspace_members (workspace_id, user_id, role) VALUES (?, ?, ?)').run(id, userId, 'owner'); + + return c.json({ id, name, owner_id: userId }, 201); +}); + +// GET /workspaces/:id +workspaceRoutes.get('/:id', (c) => { + const { userId } = c.get('auth'); + const db = getDb(); + const ws = db.prepare(` + SELECT w.* FROM workspaces w + JOIN workspace_members wm ON wm.workspace_id = w.id + WHERE w.id = ? AND wm.user_id = ? + `).get(c.req.param('id'), userId) as Workspace | undefined; + + if (!ws) return c.json({ error: 'Not found' }, 404); + return c.json(ws); +}); diff --git a/mothership/src/config.ts b/mothership/src/config.ts new file mode 100644 index 0000000..1b27776 --- /dev/null +++ b/mothership/src/config.ts @@ -0,0 +1,8 @@ +import { resolve } from 'node:path'; + +export const config = { + port: parseInt(process.env.PORT || '3001', 10), + jwtSecret: process.env.JWT_SECRET || 'dev-secret-change-me', + dbPath: resolve(process.env.DB_PATH || './data/mothership.db'), + logLevel: process.env.LOG_LEVEL || 'info', +} as const; diff --git a/mothership/src/db/index.ts b/mothership/src/db/index.ts new file mode 100644 index 0000000..722e40d --- /dev/null +++ b/mothership/src/db/index.ts @@ -0,0 +1,50 @@ +import Database from 'better-sqlite3'; +import { readFileSync } from 'node:fs'; +import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { config } from '../config.js'; +import { log } from '../utils/logger.js'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +let _db: Database.Database | null = null; + +export function getDb(): Database.Database { + if (_db) return _db; + + _db = new Database(config.dbPath); + _db.pragma('journal_mode = WAL'); + _db.pragma('foreign_keys = ON'); + _db.pragma('busy_timeout = 5000'); + + runMigrations(_db); + log.info('Database initialized at %s', config.dbPath); + return _db; +} + +function runMigrations(db: Database.Database) { + db.exec(`CREATE TABLE IF NOT EXISTS _migrations ( + name TEXT PRIMARY KEY, + applied_at TEXT NOT NULL DEFAULT (datetime('now')) + )`); + + const applied = new Set( + db.prepare('SELECT name FROM _migrations').all().map((r: any) => r.name) + ); + + const migrationsDir = resolve(__dirname, 'migrations'); + const files = ['001_foundation.sql']; // Explicit ordering + + for (const file of files) { + if (applied.has(file)) continue; + const sql = readFileSync(resolve(migrationsDir, file), 'utf-8'); + db.exec(sql); + db.prepare('INSERT INTO _migrations (name) VALUES (?)').run(file); + log.info('Applied migration: %s', file); + } +} + +export function closeDb() { + _db?.close(); + _db = null; +} diff --git a/mothership/src/db/migrations/001_foundation.sql b/mothership/src/db/migrations/001_foundation.sql new file mode 100644 index 0000000..42b4d6c --- /dev/null +++ b/mothership/src/db/migrations/001_foundation.sql @@ -0,0 +1,75 @@ +-- Phase 1: Foundation schema + +CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY, + username TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE TABLE IF NOT EXISTS workspaces ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + owner_id TEXT NOT NULL REFERENCES users(id), + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE TABLE IF NOT EXISTS workspace_members ( + workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE, + user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + role TEXT NOT NULL DEFAULT 'member' CHECK(role IN ('owner','admin','member','viewer')), + joined_at TEXT NOT NULL DEFAULT (datetime('now')), + PRIMARY KEY (workspace_id, user_id) +); + +CREATE TABLE IF NOT EXISTS sessions ( + id TEXT PRIMARY KEY, + workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE, + external_id TEXT, + name TEXT, + url TEXT, + hostname TEXT, + status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','closed')), + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE TABLE IF NOT EXISTS events ( + id TEXT PRIMARY KEY, + session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE, + workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE, + title TEXT NOT NULL, + summary TEXT NOT NULL, + importance TEXT NOT NULL DEFAULT 'medium' CHECK(importance IN ('high','medium','low')), + message_index INTEGER, + source_tab_id TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE TABLE IF NOT EXISTS messages ( + id TEXT PRIMARY KEY, + session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE, + content TEXT NOT NULL, + role TEXT DEFAULT 'assistant', + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE TABLE IF NOT EXISTS audit_log ( + id TEXT PRIMARY KEY, + user_id TEXT REFERENCES users(id), + action TEXT NOT NULL, + resource_type TEXT, + resource_id TEXT, + detail TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +-- Indexes +CREATE INDEX IF NOT EXISTS idx_sessions_workspace ON sessions(workspace_id); +CREATE INDEX IF NOT EXISTS idx_events_session ON events(session_id); +CREATE INDEX IF NOT EXISTS idx_events_workspace ON events(workspace_id); +CREATE INDEX IF NOT EXISTS idx_events_created ON events(created_at); +CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id); +CREATE INDEX IF NOT EXISTS idx_audit_user ON audit_log(user_id); diff --git a/mothership/src/db/schema.ts b/mothership/src/db/schema.ts new file mode 100644 index 0000000..7835565 --- /dev/null +++ b/mothership/src/db/schema.ts @@ -0,0 +1,66 @@ +// TypeScript types matching the SQLite schema + +export interface User { + id: string; + username: string; + password_hash: string; + created_at: string; + updated_at: string; +} + +export interface Workspace { + id: string; + name: string; + owner_id: string; + created_at: string; + updated_at: string; +} + +export interface WorkspaceMember { + workspace_id: string; + user_id: string; + role: 'owner' | 'admin' | 'member' | 'viewer'; + joined_at: string; +} + +export interface Session { + id: string; + workspace_id: string; + external_id: string | null; + name: string | null; + url: string | null; + hostname: string | null; + status: 'active' | 'closed'; + created_at: string; + updated_at: string; +} + +export interface RegentEvent { + id: string; + session_id: string; + workspace_id: string; + title: string; + summary: string; + importance: 'high' | 'medium' | 'low'; + message_index: number | null; + source_tab_id: string | null; + created_at: string; +} + +export interface Message { + id: string; + session_id: string; + content: string; + role: string | null; + created_at: string; +} + +export interface AuditEntry { + id: string; + user_id: string | null; + action: string; + resource_type: string | null; + resource_id: string | null; + detail: string | null; + created_at: string; +} diff --git a/mothership/src/events/bus.ts b/mothership/src/events/bus.ts new file mode 100644 index 0000000..fddb365 --- /dev/null +++ b/mothership/src/events/bus.ts @@ -0,0 +1,19 @@ +import { EventEmitter } from 'node:events'; + +/** + * Singleton event bus for broadcasting DB changes to WebSocket subscribers. + * Pattern borrowed from OpenClaw's architecture. + * + * Events: + * 'events:new' → { workspaceId, sessionId, events[] } + * 'session:open' → { workspaceId, session } + * 'session:close' → { workspaceId, sessionId } + */ +class Bus extends EventEmitter { + constructor() { + super(); + this.setMaxListeners(1000); // Support many concurrent WS connections + } +} + +export const bus = new Bus(); diff --git a/mothership/src/index.ts b/mothership/src/index.ts new file mode 100644 index 0000000..18ed1a0 --- /dev/null +++ b/mothership/src/index.ts @@ -0,0 +1,27 @@ +import { serve } from '@hono/node-server'; +import { config } from './config.js'; +import { api } from './api/index.js'; +import { attachWebSocket } from './ws/gateway.js'; +import { getDb, closeDb } from './db/index.js'; +import { log } from './utils/logger.js'; + +// Initialize database (runs migrations on first start) +getDb(); + +// Start HTTP server +const server = serve({ fetch: api.fetch, port: config.port }, (info) => { + log.info(`Mothership listening on http://localhost:${info.port}`); +}); + +// Attach WebSocket to the same HTTP server +attachWebSocket(server as any); + +// Graceful shutdown +for (const sig of ['SIGINT', 'SIGTERM'] as const) { + process.on(sig, () => { + log.info('Shutting down...'); + closeDb(); + server.close(); + process.exit(0); + }); +} diff --git a/mothership/src/queue/laneQueue.ts b/mothership/src/queue/laneQueue.ts new file mode 100644 index 0000000..5d6ceee --- /dev/null +++ b/mothership/src/queue/laneQueue.ts @@ -0,0 +1,28 @@ +/** + * Lane Queue — per-session serial execution. + * Pattern from OpenClaw: prevents race conditions by ensuring + * tasks for the same session execute one at a time. + * + * Key format: "ws:{workspaceId}:sess:{sessionId}" + * Each key chains promises so tasks run sequentially. + */ + +const lanes = new Map>(); + +export function enqueue(key: string, task: () => Promise): Promise { + const prev = lanes.get(key) ?? Promise.resolve(); + const next = prev + .then(task) + .catch(() => {}) // Don't let one failure block the lane + .finally(() => { + // Clean up lane if nothing else queued + if (lanes.get(key) === next) lanes.delete(key); + }); + + lanes.set(key, next); + return next; +} + +export function laneKey(workspaceId: string, sessionId: string) { + return `ws:${workspaceId}:sess:${sessionId}`; +} diff --git a/mothership/src/utils/crypto.ts b/mothership/src/utils/crypto.ts new file mode 100644 index 0000000..5a2d2ea --- /dev/null +++ b/mothership/src/utils/crypto.ts @@ -0,0 +1,35 @@ +import { randomBytes, scryptSync, timingSafeEqual } from 'node:crypto'; +import { SignJWT, jwtVerify } from 'jose'; +import { config } from '../config.js'; + +const encoder = new TextEncoder(); +const secret = () => encoder.encode(config.jwtSecret); + +// --- Password hashing (scrypt) --- + +export function hashPassword(password: string): string { + const salt = randomBytes(16).toString('hex'); + const hash = scryptSync(password, salt, 64).toString('hex'); + return `${salt}:${hash}`; +} + +export function verifyPassword(password: string, stored: string): boolean { + const [salt, hash] = stored.split(':'); + const candidate = scryptSync(password, salt, 64); + return timingSafeEqual(candidate, Buffer.from(hash, 'hex')); +} + +// --- JWT --- + +export async function signToken(payload: Record, expiresIn = '30d') { + return new SignJWT(payload) + .setProtectedHeader({ alg: 'HS256' }) + .setIssuedAt() + .setExpirationTime(expiresIn) + .sign(secret()); +} + +export async function verifyToken(token: string) { + const { payload } = await jwtVerify(token, secret()); + return payload as Record; +} diff --git a/mothership/src/utils/id.ts b/mothership/src/utils/id.ts new file mode 100644 index 0000000..8b2bce6 --- /dev/null +++ b/mothership/src/utils/id.ts @@ -0,0 +1,3 @@ +import { nanoid } from 'nanoid'; + +export const newId = (size = 21) => nanoid(size); diff --git a/mothership/src/utils/logger.ts b/mothership/src/utils/logger.ts new file mode 100644 index 0000000..1ce64c1 --- /dev/null +++ b/mothership/src/utils/logger.ts @@ -0,0 +1,4 @@ +import pino from 'pino'; +import { config } from '../config.js'; + +export const log = pino({ level: config.logLevel }); diff --git a/mothership/src/ws/gateway.ts b/mothership/src/ws/gateway.ts new file mode 100644 index 0000000..a0bc05d --- /dev/null +++ b/mothership/src/ws/gateway.ts @@ -0,0 +1,103 @@ +import { WebSocketServer, WebSocket } from 'ws'; +import type { Server } from 'node:http'; +import { verifyToken } from '../utils/crypto.js'; +import { log } from '../utils/logger.js'; +import { bus } from '../events/bus.js'; +import { addConnection, removeConnection, broadcastToWorkspace } from './registry.js'; +import { handleTabRegister } from './handlers/tabRegister.js'; +import { handleEventsStore } from './handlers/eventsStore.js'; +import type { Connection } from './registry.js'; + +const HEARTBEAT_INTERVAL = 30_000; + +interface WsEnvelope { + type: string; + payload?: unknown; + correlationId?: string; + ts?: number; +} + +function send(ws: WebSocket, msg: WsEnvelope) { + if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(msg)); +} + +export function attachWebSocket(server: Server) { + const wss = new WebSocketServer({ noServer: true }); + + // Handle HTTP upgrade with token auth + server.on('upgrade', async (req, socket, head) => { + try { + const url = new URL(req.url || '/', `http://${req.headers.host}`); + const token = url.searchParams.get('token'); + if (!token) throw new Error('No token'); + + const payload = await verifyToken(token); + const userId = payload.sub as string; + const username = payload.username as string; + const tabId = url.searchParams.get('tabId') || `tab-${Date.now()}`; + + wss.handleUpgrade(req, socket, head, (ws) => { + wss.emit('connection', ws, { userId, username, tabId }); + }); + } catch { + socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n'); + socket.destroy(); + } + }); + + wss.on('connection', (ws: WebSocket, meta: { userId: string; username: string; tabId: string }) => { + const conn: Connection = { ws, userId: meta.userId, tabId: meta.tabId, workspaceId: null }; + addConnection(conn); + log.info({ userId: meta.userId, tabId: meta.tabId }, 'WS connected'); + + send(ws, { type: 'connected', payload: { userId: meta.userId, username: meta.username } }); + + // Heartbeat + let alive = true; + ws.on('pong', () => { alive = true; }); + const heartbeat = setInterval(() => { + if (!alive) { ws.terminate(); return; } + alive = false; + ws.ping(); + }, HEARTBEAT_INTERVAL); + + // Listen for bus events → forward to this connection + const onNewEvents = (data: { workspaceId: string; sessionId: string; events: unknown[]; sourceTabId: string }) => { + if (conn.workspaceId === data.workspaceId && conn.tabId !== data.sourceTabId) { + send(ws, { type: 'events:cross', payload: { sessionId: data.sessionId, events: data.events } }); + } + }; + bus.on('events:new', onNewEvents); + + // Message dispatch + ws.on('message', (raw) => { + try { + const msg = JSON.parse(raw.toString()) as WsEnvelope; + switch (msg.type) { + case 'tab:register': + handleTabRegister(conn, msg.payload as { workspaceId: string }); + break; + case 'events:store': + handleEventsStore(conn, msg.payload as any); + break; + case 'ping': + send(ws, { type: 'pong', ts: Date.now() }); + break; + default: + send(ws, { type: 'error', payload: { message: `Unknown type: ${msg.type}` } }); + } + } catch (err) { + log.warn({ err }, 'Invalid WS message'); + } + }); + + ws.on('close', () => { + clearInterval(heartbeat); + bus.off('events:new', onNewEvents); + removeConnection(meta.userId, meta.tabId); + log.info({ userId: meta.userId, tabId: meta.tabId }, 'WS disconnected'); + }); + }); + + return wss; +} diff --git a/mothership/src/ws/handlers/eventsStore.ts b/mothership/src/ws/handlers/eventsStore.ts new file mode 100644 index 0000000..bf2924c --- /dev/null +++ b/mothership/src/ws/handlers/eventsStore.ts @@ -0,0 +1,73 @@ +import { getDb } from '../../db/index.js'; +import { newId } from '../../utils/id.js'; +import { bus } from '../../events/bus.js'; +import { enqueue, laneKey } from '../../queue/laneQueue.js'; +import { log } from '../../utils/logger.js'; +import type { Connection } from '../registry.js'; +import type { RegentEvent } from '../../db/schema.js'; + +interface EventPayload { + sessionId: string; + sessionName?: string; + url?: string; + hostname?: string; + events: Array<{ + title: string; + summary: string; + importance?: string; + messageIndex?: number; + }>; +} + +/** + * Handle events:store — extension forwards pre-extracted events. + * Uses lane queue for per-session serial execution. + */ +export function handleEventsStore(conn: Connection, payload: EventPayload) { + const { workspaceId } = conn; + if (!workspaceId) return; + + const { sessionId, events } = payload; + if (!sessionId || !events?.length) return; + + const key = laneKey(workspaceId, sessionId); + + enqueue(key, async () => { + const db = getDb(); + + // Upsert session + const existing = db.prepare('SELECT id FROM sessions WHERE id = ?').get(sessionId); + if (!existing) { + db.prepare(`INSERT INTO sessions (id, workspace_id, name, url, hostname, status) + VALUES (?, ?, ?, ?, ?, 'active')`) + .run(sessionId, workspaceId, payload.sessionName ?? null, payload.url ?? null, payload.hostname ?? null); + } else { + db.prepare("UPDATE sessions SET updated_at = datetime('now') WHERE id = ?").run(sessionId); + } + + // Store events + const insert = db.prepare(`INSERT INTO events (id, session_id, workspace_id, title, summary, importance, message_index, source_tab_id) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)`); + + const stored: RegentEvent[] = []; + const tx = db.transaction(() => { + for (const evt of events) { + const id = newId(); + insert.run(id, sessionId, workspaceId, evt.title, evt.summary, evt.importance || 'medium', evt.messageIndex ?? null, conn.tabId); + stored.push({ + id, session_id: sessionId, workspace_id: workspaceId, + title: evt.title, summary: evt.summary, + importance: (evt.importance || 'medium') as RegentEvent['importance'], + message_index: evt.messageIndex ?? null, + source_tab_id: conn.tabId, created_at: new Date().toISOString(), + }); + } + }); + tx(); + + log.info({ sessionId, count: stored.length }, 'Events stored'); + + // Broadcast to other tabs in workspace + bus.emit('events:new', { workspaceId, sessionId, events: stored, sourceTabId: conn.tabId }); + }); +} diff --git a/mothership/src/ws/handlers/tabRegister.ts b/mothership/src/ws/handlers/tabRegister.ts new file mode 100644 index 0000000..3699b9c --- /dev/null +++ b/mothership/src/ws/handlers/tabRegister.ts @@ -0,0 +1,8 @@ +import type { Connection } from '../registry.js'; + +/** + * Handle tab:register — extension tab announces itself and its workspace. + */ +export function handleTabRegister(conn: Connection, payload: { workspaceId: string }) { + conn.workspaceId = payload.workspaceId; +} diff --git a/mothership/src/ws/registry.ts b/mothership/src/ws/registry.ts new file mode 100644 index 0000000..32255ee --- /dev/null +++ b/mothership/src/ws/registry.ts @@ -0,0 +1,52 @@ +import type { WebSocket } from 'ws'; + +export interface Connection { + ws: WebSocket; + userId: string; + tabId: string; + workspaceId: string | null; +} + +/** userId → Map */ +const connections = new Map>(); + +export function addConnection(conn: Connection) { + let userConns = connections.get(conn.userId); + if (!userConns) { + userConns = new Map(); + connections.set(conn.userId, userConns); + } + userConns.set(conn.tabId, conn); +} + +export function removeConnection(userId: string, tabId: string) { + const userConns = connections.get(userId); + if (!userConns) return; + userConns.delete(tabId); + if (userConns.size === 0) connections.delete(userId); +} + +export function getConnection(userId: string, tabId: string): Connection | undefined { + return connections.get(userId)?.get(tabId); +} + +/** Get all connections for a workspace (across all users) */ +export function getWorkspaceConnections(workspaceId: string): Connection[] { + const result: Connection[] = []; + for (const userConns of connections.values()) { + for (const conn of userConns.values()) { + if (conn.workspaceId === workspaceId) result.push(conn); + } + } + return result; +} + +/** Broadcast a message to all tabs in a workspace, optionally excluding a tabId */ +export function broadcastToWorkspace(workspaceId: string, message: object, excludeTabId?: string) { + const payload = JSON.stringify(message); + for (const conn of getWorkspaceConnections(workspaceId)) { + if (conn.tabId !== excludeTabId && conn.ws.readyState === 1) { + conn.ws.send(payload); + } + } +} diff --git a/mothership/tsconfig.json b/mothership/tsconfig.json new file mode 100644 index 0000000..68dc434 --- /dev/null +++ b/mothership/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "outDir": "dist", + "rootDir": "src", + "declaration": true, + "sourceMap": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true + }, + "include": ["src"], + "exclude": ["node_modules", "dist", "data"] +} diff --git a/src/background.js b/src/background.js index b9bf6b0..020d6eb 100644 --- a/src/background.js +++ b/src/background.js @@ -319,6 +319,118 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { } }); +// ─── Mothership WebSocket Connection Manager ─── + +let mothershipWs = null; +let mothershipReconnectTimer = null; +let mothershipReconnectDelay = 1000; +const MOTHERSHIP_MAX_RECONNECT = 30000; + +function mothershipConnect(url, token, tabId) { + mothershipDisconnect(); + + const wsUrl = `${url.replace(/^http/, 'ws')}/ws?token=${encodeURIComponent(token)}&tabId=${tabId || Date.now()}`; + mothershipWs = new WebSocket(wsUrl); + + mothershipWs.onopen = () => { + mothershipReconnectDelay = 1000; + // Register with workspace + chrome.storage.sync.get('mothershipWorkspaceId', (data) => { + if (data.mothershipWorkspaceId && mothershipWs?.readyState === WebSocket.OPEN) { + mothershipWs.send(JSON.stringify({ type: 'tab:register', payload: { workspaceId: data.mothershipWorkspaceId } })); + } + }); + broadcastMothershipStatus('connected'); + }; + + mothershipWs.onmessage = (event) => { + try { + const msg = JSON.parse(event.data); + // Forward cross-session events to all tabs running regent + if (msg.type === 'events:cross' || msg.type === 'connected') { + chrome.tabs.query({}, (tabs) => { + for (const tab of tabs) { + chrome.tabs.sendMessage(tab.id, { type: 'mothershipEvent', data: msg }).catch(() => {}); + } + }); + } + } catch {} + }; + + mothershipWs.onclose = () => { + mothershipWs = null; + broadcastMothershipStatus('disconnected'); + // Exponential backoff reconnect + mothershipReconnectTimer = setTimeout(() => { + chrome.storage.sync.get(['mothershipUrl', 'mothershipToken'], (data) => { + if (data.mothershipUrl && data.mothershipToken) { + mothershipConnect(data.mothershipUrl, data.mothershipToken); + } + }); + }, mothershipReconnectDelay); + mothershipReconnectDelay = Math.min(mothershipReconnectDelay * 2, MOTHERSHIP_MAX_RECONNECT); + }; + + mothershipWs.onerror = () => {}; // onclose handles reconnect +} + +function mothershipDisconnect() { + clearTimeout(mothershipReconnectTimer); + mothershipReconnectTimer = null; + if (mothershipWs) { + mothershipWs.onclose = null; // Prevent reconnect + mothershipWs.close(); + mothershipWs = null; + } + broadcastMothershipStatus('disconnected'); +} + +function mothershipSend(payload) { + if (mothershipWs?.readyState === WebSocket.OPEN) { + mothershipWs.send(JSON.stringify(payload)); + return true; + } + return false; +} + +function broadcastMothershipStatus(status) { + chrome.tabs.query({}, (tabs) => { + for (const tab of tabs) { + chrome.tabs.sendMessage(tab.id, { type: 'mothershipStatus', status }).catch(() => {}); + } + }); +} + +// Handle mothership messages from content scripts +chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.action === 'mothershipConnect') { + mothershipConnect(request.url, request.token, sender?.tab?.id); + sendResponse({ ok: true }); + return true; + } + if (request.action === 'mothershipDisconnect') { + mothershipDisconnect(); + sendResponse({ ok: true }); + return true; + } + if (request.action === 'mothershipSend') { + const sent = mothershipSend(request.payload); + sendResponse({ sent }); + return true; + } + if (request.action === 'mothershipStatus') { + sendResponse({ connected: mothershipWs?.readyState === WebSocket.OPEN }); + return true; + } +}); + +// Auto-connect on service worker startup if credentials are stored +chrome.storage.sync.get(['mothershipUrl', 'mothershipToken'], (data) => { + if (data.mothershipUrl && data.mothershipToken) { + mothershipConnect(data.mothershipUrl, data.mothershipToken); + } +}); + // Create context menu on extension installation chrome.runtime.onInstalled.addListener(() => { chrome.contextMenus.create({ diff --git a/src/content/regent/RegentOrchestrator.js b/src/content/regent/RegentOrchestrator.js index 4e86e3f..114ce9d 100644 --- a/src/content/regent/RegentOrchestrator.js +++ b/src/content/regent/RegentOrchestrator.js @@ -69,6 +69,18 @@ class RegentOrchestratorClass { // Periodic meta-summary this._metaTimer = setInterval(() => this._generateMetaSummary(), META_SUMMARY_INTERVAL); + + // Listen for mothership events (cross-session from other tabs/devices) + this._mothershipListener = (msg) => { + if (msg.type === 'mothershipEvent') this._onMothershipEvent(msg.data); + if (msg.type === 'mothershipStatus') this.sidebar.setConnectionStatus(msg.status); + }; + chrome.runtime.onMessage.addListener(this._mothershipListener); + + // Check initial mothership status + chrome.runtime.sendMessage({ action: 'mothershipStatus' }, (res) => { + this.sidebar.setConnectionStatus(res?.connected ? 'connected' : 'disconnected'); + }); } /** Create a sidecar for a session */ @@ -178,9 +190,20 @@ class RegentOrchestratorClass { this.sidebar.updateMeta(meta); } + /** Handle cross-session events from mothership */ + _onMothershipEvent(data) { + if (data.type !== 'events:cross') return; + const { sessionId, events } = data.payload || {}; + if (!sessionId || !events?.length) return; + + // Display cross-session events in sidebar (no DOM element to scroll to) + this.sidebar.addCrossSessionEvents(sessionId, events); + } + /** Destroy the entire regent system */ destroy() { clearInterval(this._metaTimer); + if (this._mothershipListener) chrome.runtime.onMessage.removeListener(this._mothershipListener); this.detector.destroy(); this.sidecars.forEach(s => s.destroy()); this.sidecars.clear(); diff --git a/src/content/regent/RegentSidebar.js b/src/content/regent/RegentSidebar.js index eb152a1..82d6dd2 100644 --- a/src/content/regent/RegentSidebar.js +++ b/src/content/regent/RegentSidebar.js @@ -46,6 +46,7 @@ export class RegentSidebar {
Regent + 0 sessions
@@ -265,6 +266,62 @@ export class RegentSidebar { this.sessionsContainer.appendChild(cal); } + /** Set mothership connection status indicator */ + setConnectionStatus(status) { + const dot = this.sidebar?.querySelector('.regent-connection-dot'); + if (!dot) return; + const connected = status === 'connected'; + dot.classList.toggle('connected', connected); + dot.title = `Mothership: ${connected ? 'connected' : 'disconnected'}`; + } + + /** Display cross-session events received from mothership (other tabs/devices) */ + addCrossSessionEvents(sessionId, events) { + if (!events?.length) return; + + // Get or create a "remote" session section + let section = this._sessionElements.get(`remote:${sessionId}`); + if (!section) { + const empty = this.sessionsContainer?.querySelector('.regent-empty'); + if (empty) empty.remove(); + + section = document.createElement('div'); + section.className = 'regent-session regent-session-remote'; + section.dataset.sessionId = `remote:${sessionId}`; + section.innerHTML = ` +
+ Remote: ${this._escapeHtml(sessionId.slice(-8))} + Remote +
+
+ `; + this.sessionsContainer?.appendChild(section); + this._sessionElements.set(`remote:${sessionId}`, section); + } + + const container = section.querySelector('.regent-events'); + for (const evt of events) { + const el = document.createElement('div'); + el.className = 'regent-event entering'; + el.dataset.importance = evt.importance || 'medium'; + const time = new Date(evt.created_at || Date.now()).toLocaleTimeString([], { + hour: '2-digit', minute: '2-digit', + }); + el.innerHTML = ` +
+
+ ${this._escapeHtml(evt.title)} + ${time} +
+
${this._escapeHtml(evt.summary)}
+
+ `; + container.appendChild(el); + } + + this._updateBadge(); + } + /** Update meta-summary */ updateMeta(text) { if (!text) { diff --git a/src/content/regent/RegentSidecar.js b/src/content/regent/RegentSidecar.js index 2b8cd2c..e9e8a4a 100644 --- a/src/content/regent/RegentSidecar.js +++ b/src/content/regent/RegentSidecar.js @@ -92,6 +92,9 @@ export class RegentSidecar { this._processedCount += batch.length; this.onEventsUpdate?.(this.sessionId, this.events); + + // Forward extracted events to mothership (fire-and-forget) + this._forwardToMothership(aiEvents); } catch (err) { console.warn(`[Regent:Sidecar:${this.sessionId}] Summarization failed:`, err.message); // Drop failed batch to avoid infinite retry loop — messages are lost but system stays stable @@ -108,6 +111,28 @@ export class RegentSidecar { } } + /** Forward extracted events to mothership via background WS */ + _forwardToMothership(aiEvents) { + if (!aiEvents?.length) return; + chrome.runtime.sendMessage({ + action: 'mothershipSend', + payload: { + type: 'events:store', + payload: { + sessionId: this.sessionId, + sessionName: this.getDisplayName(), + url: location.href, + hostname: location.hostname, + events: aiEvents.map(e => ({ + title: e.title, summary: e.summary, + importance: e.importance || 'medium', + messageIndex: e.messageIndex, + })), + }, + }, + }).catch(() => {}); // Silent fail — mothership is optional + } + /** Get session display name */ getDisplayName() { // Try to extract from URL or element content diff --git a/src/content/regent/regent.css b/src/content/regent/regent.css index 2091cb9..87e96b4 100644 --- a/src/content/regent/regent.css +++ b/src/content/regent/regent.css @@ -111,6 +111,26 @@ letter-spacing: -0.01em; } +.regent-connection-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--regent-low); + display: inline-block; + margin: 0 4px; + transition: background 0.3s; +} + +.regent-connection-dot.connected { + background: #34c759; + box-shadow: 0 0 6px rgba(52, 199, 89, 0.4); +} + +.regent-session-remote .session-status.remote { + background: rgba(88, 86, 214, 0.12); + color: #5856d6; +} + .regent-badge { font-size: 11px; font-weight: 500; diff --git a/src/popup/MothershipManager.js b/src/popup/MothershipManager.js new file mode 100644 index 0000000..87d3839 --- /dev/null +++ b/src/popup/MothershipManager.js @@ -0,0 +1,96 @@ +/** + * MothershipManager — Popup UI for connecting the extension to the Mothership backend. + * Handles URL/token input, connect/disconnect, and status display. + */ +export class MothershipManager { + constructor() { + this.urlInput = document.getElementById('mothershipUrl'); + this.tokenInput = document.getElementById('mothershipToken'); + this.connectBtn = document.getElementById('mothershipConnectBtn'); + this.disconnectBtn = document.getElementById('mothershipDisconnectBtn'); + this.statusDot = document.getElementById('mothershipStatusDot'); + this.statusText = document.getElementById('mothershipStatusText'); + + this._bindEvents(); + this._loadState(); + } + + _bindEvents() { + this.connectBtn.addEventListener('click', () => this._connect()); + this.disconnectBtn.addEventListener('click', () => this._disconnect()); + } + + async _loadState() { + const data = await new Promise(r => + chrome.storage.sync.get(['mothershipUrl', 'mothershipToken', 'mothershipWorkspaceId'], r) + ); + + if (data.mothershipUrl) this.urlInput.value = data.mothershipUrl; + if (data.mothershipToken) this.tokenInput.value = data.mothershipToken; + + // Check connection status + chrome.runtime.sendMessage({ action: 'mothershipStatus' }, (res) => { + this._updateUI(res?.connected); + }); + } + + async _connect() { + const url = this.urlInput.value.trim().replace(/\/+$/, ''); + const token = this.tokenInput.value.trim(); + if (!url || !token) return; + + this.connectBtn.textContent = 'Connecting...'; + this.connectBtn.disabled = true; + + // Verify token by calling health + auth check + try { + const res = await fetch(`${url}/api/v1/health`); + if (!res.ok) throw new Error('Server unreachable'); + } catch { + this._showError('Cannot reach server'); + this.connectBtn.textContent = 'Connect'; + this.connectBtn.disabled = false; + return; + } + + // Store credentials + chrome.storage.sync.set({ mothershipUrl: url, mothershipToken: token }); + + // Tell background to connect + chrome.runtime.sendMessage({ action: 'mothershipConnect', url, token }, () => { + // Give it a moment to connect + setTimeout(() => { + chrome.runtime.sendMessage({ action: 'mothershipStatus' }, (res) => { + this._updateUI(res?.connected); + this.connectBtn.textContent = 'Connect'; + this.connectBtn.disabled = false; + }); + }, 1000); + }); + } + + _disconnect() { + chrome.runtime.sendMessage({ action: 'mothershipDisconnect' }); + chrome.storage.sync.remove(['mothershipUrl', 'mothershipToken', 'mothershipWorkspaceId']); + this.urlInput.value = ''; + this.tokenInput.value = ''; + this._updateUI(false); + } + + _updateUI(connected) { + this.statusDot.style.background = connected ? '#34c759' : '#aaa'; + this.statusDot.style.boxShadow = connected ? '0 0 6px rgba(52,199,89,0.4)' : 'none'; + this.statusText.textContent = connected ? 'Connected' : 'Disconnected'; + this.connectBtn.style.display = connected ? 'none' : ''; + this.disconnectBtn.style.display = connected ? '' : 'none'; + } + + _showError(msg) { + this.statusText.textContent = msg; + this.statusText.style.color = '#ff3b30'; + setTimeout(() => { + this.statusText.style.color = ''; + this.statusText.textContent = 'Disconnected'; + }, 3000); + } +} diff --git a/src/popup/popup.html b/src/popup/popup.html index 4e67d23..81412fd 100644 --- a/src/popup/popup.html +++ b/src/popup/popup.html @@ -1705,6 +1705,31 @@
+ +
+
+
+ + + + + Mothership +
+
+
+
+ + Disconnected +
+ + +
+ + +
+
+
+