diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..301f072 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +OPENCLAW_HOME=/root/.openclaw +OPENCLAW_WORKSPACE=/root/.openclaw/workspace diff --git a/README.md b/README.md index 80dca59..927b438 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,74 @@ -# sv +# OpenClaw Monitor -Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). +A SvelteKit dashboard for inspecting a local OpenClaw installation in development mode. -## Creating a project +It is designed for the workflow where the app runs locally on the OpenClaw host, and you reach it through an SSH tunnel instead of deploying a static production build. -If you're seeing this, you've probably already done this step. Congrats! +## What it shows -```sh -# create a new project -npx sv create my-app +- OpenClaw runtime basics such as install version, default model, ACP backend, gateway mode, and tool profile +- Workspace facts such as memory file count and workspace state +- Channel and device summaries without exposing secrets +- Parsed highlights from `openclaw status` +- Installed skills discovered from workspace skills, built-in OpenClaw skills, and extension skills +- Cron jobs discovered from local OpenClaw cron state +- A sanitized OpenClaw config snapshot with secret-like fields redacted + +## How it works + +The app reads local files at request time on the server side. Machine-specific data is not committed to the repository. + +By default it looks here: + +- `OPENCLAW_HOME=/root/.openclaw` +- `OPENCLAW_WORKSPACE=/root/.openclaw/workspace` + +Override those paths with environment variables if you want to point the monitor at another OpenClaw installation. + +## Development + +Install dependencies: + +```bash +npm install ``` -To recreate this project with the same configuration: +Run the dev server: -```sh -# recreate this project -npx sv@0.12.8 create --template minimal --types ts --install npm . +```bash +npm run dev -- --host 0.0.0.0 --port 4173 ``` -## Developing +Then forward the port from your local machine, for example: + +```bash +ssh -L 4173:127.0.0.1:4173 your-server +``` -Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: +## Environment -```sh -npm run dev +Copy `.env.example` if you want explicit paths: -# or start the server and open the app in a new browser tab -npm run dev -- --open +```bash +cp .env.example .env ``` -## Building +Example: + +```env +OPENCLAW_HOME=/root/.openclaw +OPENCLAW_WORKSPACE=/root/.openclaw/workspace +``` -To create a production version of your app: +## Validation -```sh +```bash +npm run check npm run build ``` -You can preview the production build with `npm run preview`. +## Notes -> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. +- The dashboard is intentionally local-first. +- Secret-like fields in `openclaw.json` are redacted before being returned to the UI. +- The monitor currently focuses on visibility and foundation for future dashboards rather than write actions. diff --git a/package-lock.json b/package-lock.json index 7839f84..9f694bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,8 +11,11 @@ "@sveltejs/adapter-auto": "^7.0.0", "@sveltejs/kit": "^2.50.2", "@sveltejs/vite-plugin-svelte": "^6.2.4", + "@tailwindcss/vite": "^4.1.13", + "@types/node": "^25.5.0", "svelte": "^5.51.0", "svelte-check": "^4.4.2", + "tailwindcss": "^4.1.13", "typescript": "^5.9.3", "vite": "^7.3.1" } @@ -974,6 +977,278 @@ "vite": "^6.3.0 || ^7.0.0" } }, + "node_modules/@tailwindcss/node": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", + "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.2" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz", + "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-x64": "4.2.2", + "@tailwindcss/oxide-freebsd-x64": "4.2.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-x64-musl": "4.2.2", + "@tailwindcss/oxide-wasm32-wasi": "4.2.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz", + "integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz", + "integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz", + "integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz", + "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz", + "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz", + "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz", + "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz", + "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz", + "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz", + "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", + "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz", + "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.2.tgz", + "integrity": "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.2.2", + "@tailwindcss/oxide": "4.2.2", + "tailwindcss": "4.2.2" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, "node_modules/@types/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", @@ -988,6 +1263,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -1088,6 +1373,16 @@ "node": ">=0.10.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==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/devalue": { "version": "5.6.4", "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.4.tgz", @@ -1095,6 +1390,20 @@ "dev": true, "license": "MIT" }, + "node_modules/enhanced-resolve": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/esbuild": { "version": "0.27.4", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", @@ -1188,6 +1497,13 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, "node_modules/is-reference": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", @@ -1198,6 +1514,16 @@ "@types/estree": "^1.0.6" } }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/kleur": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", @@ -1208,6 +1534,267 @@ "node": ">=6" } }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/locate-character": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", @@ -1480,6 +2067,27 @@ "typescript": ">=5.0.0" } }, + "node_modules/tailwindcss": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", + "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -1521,6 +2129,13 @@ "node": ">=14.17" } }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, "node_modules/vite": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", diff --git a/package.json b/package.json index 410f7b9..490ba96 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,11 @@ "@sveltejs/adapter-auto": "^7.0.0", "@sveltejs/kit": "^2.50.2", "@sveltejs/vite-plugin-svelte": "^6.2.4", + "@tailwindcss/vite": "^4.1.13", + "@types/node": "^25.5.0", "svelte": "^5.51.0", "svelte-check": "^4.4.2", + "tailwindcss": "^4.1.13", "typescript": "^5.9.3", "vite": "^7.3.1" } diff --git a/src/lib/app.css b/src/lib/app.css new file mode 100644 index 0000000..894d036 --- /dev/null +++ b/src/lib/app.css @@ -0,0 +1,79 @@ +@import 'tailwindcss'; + +@theme { + --font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif; + --font-mono: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace; + --color-ink: #eef4ef; + --color-muted: #9ca9a1; + --color-panel: #101714; + --color-panel-strong: #0a0f0d; + --color-line: rgba(255, 255, 255, 0.08); + --color-accent: #97f0b8; + --color-accent-strong: #2eb872; + --color-warm: #f1d6b1; +} + +@layer base { + html { + color-scheme: dark; + } + + body { + min-height: 100dvh; + background: + radial-gradient(circle at top left, rgba(84, 205, 131, 0.18), transparent 28%), + radial-gradient(circle at 85% 10%, rgba(241, 214, 177, 0.12), transparent 22%), + radial-gradient(circle at 60% 80%, rgba(79, 123, 255, 0.08), transparent 24%), + #060a08; + color: var(--color-ink); + font-family: var(--font-sans); + } + + * { + box-sizing: border-box; + } + + button, + input, + textarea { + font: inherit; + } +} + +@layer utilities { + .glass-panel { + background: linear-gradient(180deg, rgba(16, 23, 20, 0.86), rgba(8, 12, 10, 0.92)); + border: 1px solid var(--color-line); + box-shadow: + 0 30px 80px rgba(0, 0, 0, 0.28), + inset 0 1px 0 rgba(255, 255, 255, 0.06); + backdrop-filter: blur(20px); + } + + .hairline { + border-color: rgba(255, 255, 255, 0.08); + } + + .text-balance { + text-wrap: balance; + } + + .noise-overlay::before { + content: ''; + position: absolute; + inset: 0; + pointer-events: none; + background-image: radial-gradient(rgba(255,255,255,0.05) 0.6px, transparent 0.6px); + background-size: 12px 12px; + opacity: 0.12; + mask-image: linear-gradient(to bottom, rgba(0,0,0,0.8), transparent 90%); + } + + .grid-fade { + background-image: + linear-gradient(rgba(255,255,255,0.05) 1px, transparent 1px), + linear-gradient(90deg, rgba(255,255,255,0.05) 1px, transparent 1px); + background-size: 48px 48px; + mask-image: radial-gradient(circle at center, black 35%, transparent 85%); + } +} diff --git a/src/lib/server/idea-incubator.ts b/src/lib/server/idea-incubator.ts new file mode 100644 index 0000000..429e184 --- /dev/null +++ b/src/lib/server/idea-incubator.ts @@ -0,0 +1,283 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +export type IdeaStage = 'inbox' | 'incubating' | 'implementing'; + +export type IdeaCard = { + id: string; + title: string; + path: string; + stage: IdeaStage; + summary: string; + updatedAt: string; + content: string; +}; + +export type StageColumn = { + key: IdeaStage; + label: string; + description: string; + count: number; + ideas: IdeaCard[]; +}; + +export type IncubatorSnapshot = { + generatedAt: string; + root: string; + stages: StageColumn[]; + stats: { + totalIdeas: number; + stageCount: number; + lastUpdated: string | null; + }; +}; + +const IDEA_ROOT = '/root/.openclaw/workspace/idea-incubator/ideas'; +const STAGE_CONFIG: Record = { + inbox: { + label: 'Raw ideas', + description: 'Messy sparks, rough notes, fragments, and new captures.' + }, + incubating: { + label: 'Incubating', + description: 'Sharper concepts with clearer problem framing, scope, and MVP.' + }, + implementing: { + label: 'Implementation plans', + description: 'Ideas that crossed the line into concrete build mode.' + } +}; + +function ensureDir(dir: string) { + fs.mkdirSync(dir, { recursive: true }); +} + +function slugify(input: string) { + return input + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') + .slice(0, 80) || `idea-${Date.now()}`; +} + +function titleFromContent(content: string, fallback: string) { + const heading = content.match(/^#\s+(.+)$/m)?.[1]?.trim(); + if (heading) return heading; + + const concept = content.match(/^##\s+(One-line concept|Goal)\s*\n(.+)$/m)?.[2]?.trim(); + if (concept) return concept.slice(0, 80); + + return fallback; +} + +function summaryFromContent(content: string) { + const lines = content + .split('\n') + .map((line) => line.trim()) + .filter(Boolean) + .filter((line) => !line.startsWith('#') && !line.startsWith('- ') && !/^\d+\./.test(line)); + + return lines[0]?.slice(0, 180) || 'Fresh idea waiting for the next pass.'; +} + +function readIdeaFile(stage: IdeaStage, filePath: string): IdeaCard { + const content = fs.readFileSync(filePath, 'utf8'); + const stats = fs.statSync(filePath); + const basename = path.basename(filePath, path.extname(filePath)); + + return { + id: `${stage}:${basename}`, + title: titleFromContent(content, basename.replace(/-/g, ' ')), + path: filePath, + stage, + summary: summaryFromContent(content), + updatedAt: stats.mtime.toISOString(), + content + }; +} + +function listStageIdeas(stage: IdeaStage): IdeaCard[] { + const dir = path.join(IDEA_ROOT, stage); + ensureDir(dir); + + return fs + .readdirSync(dir) + .filter((name) => name.endsWith('.md')) + .map((name) => readIdeaFile(stage, path.join(dir, name))) + .sort((a, b) => +new Date(b.updatedAt) - +new Date(a.updatedAt)); +} + +function todayStamp() { + return new Date().toISOString().slice(0, 10); +} + +function capitalize(input: string) { + return input.slice(0, 1).toUpperCase() + input.slice(1); +} + +function pickTags(rawIdea: string) { + const keywordMap = [ + ['ai', 'ai'], + ['agent', 'agents'], + ['automation', 'automation'], + ['workflow', 'workflow'], + ['api', 'api'], + ['dashboard', 'dashboard'], + ['svelte', 'frontend'], + ['mobile', 'mobile'], + ['calendar', 'productivity'], + ['email', 'communication'], + ['github', 'developer-tools'] + ] as const; + + const found = keywordMap + .filter(([needle]) => rawIdea.toLowerCase().includes(needle)) + .map(([, tag]) => tag); + + return Array.from(new Set(found)).slice(0, 3); +} + +function inferTitle(rawIdea: string) { + const cleaned = rawIdea.replace(/\s+/g, ' ').trim(); + const firstSentence = cleaned.split(/[.!?]/)[0]?.trim() || cleaned; + return firstSentence.split(' ').slice(0, 8).join(' ').replace(/^./, (char) => char.toUpperCase()); +} + +export function createRawIdea(rawIdea: string) { + const title = inferTitle(rawIdea); + const slug = slugify(title); + const tags = pickTags(rawIdea); + const filePath = path.join(IDEA_ROOT, 'inbox', `${slug}.md`); + ensureDir(path.dirname(filePath)); + + const content = `# ${title}\n\n## Raw idea\n${rawIdea.trim()}\n\n## Why it caught attention\nThere is enough energy here to test whether this could become a sharp, demoable product direction.\n\n## Tags\n${tags.length ? tags.map((tag) => `- ${tag}`).join('\n') : '- explore\n- concept\n- poc'}\n\n## Source\n- Captured on: ${todayStamp()}\n- Captured from: UI quick capture\n`; + + fs.writeFileSync(filePath, content, 'utf8'); + return readIdeaFile('inbox', filePath); +} + +function splitSentences(rawIdea: string) { + return rawIdea + .replace(/\s+/g, ' ') + .split(/(?<=[.!?])\s+/) + .map((part) => part.trim()) + .filter(Boolean); +} + +function buildIncubatedMarkdown(title: string, rawIdea: string) { + const sentences = splitSentences(rawIdea); + const seed = sentences[0] || rawIdea.trim(); + const second = sentences[1] || 'The concept can become stronger if the workflow is made visible and interactive.'; + const nouns = Array.from(new Set(rawIdea.toLowerCase().match(/[a-z]{4,}/g) || [])).slice(0, 6); + const stackHints = [ + rawIdea.toLowerCase().includes('svelte') ? 'SvelteKit' : 'SvelteKit', + 'Tailwind CSS', + rawIdea.toLowerCase().includes('agent') || rawIdea.toLowerCase().includes('ai') ? 'OpenClaw-backed AI actions' : 'Server-side transformation endpoints', + 'Filesystem markdown state' + ]; + const users = rawIdea.toLowerCase().includes('team') + ? 'Small product teams, founders, and operators who collect ideas faster than they refine them.' + : 'Solo builders and curious operators who need to turn fuzzy notes into buildable concepts.'; + + const whyItMatters = `Ideas usually die in the jump between a quick note and an actionable spec. This concept shortens that gap by making the transformation visible, fast, and emotionally rewarding.`; + const problem = `Raw ideas are easy to capture but hard to compare, sharpen, and promote. ${capitalize(second.replace(/[.!?]+$/, ''))}.`; + const oneLine = `${title} turns rough notes into structured product specs with a visible momentum from capture to commitment.`; + + const mvp = [ + 'Capture a raw idea into the inbox with zero setup friction.', + 'Run an incubation pass that produces problem framing, MVP scope, and next steps.', + 'Surface stage-based boards so the user can see what is raw, incubating, and ready to build.' + ]; + + const risks = [ + 'Generated specs can sound confident before the core user need is validated.', + 'Filesystem-only state is simple, but collaboration and history will need stronger persistence later.' + ]; + + const nextSteps = [ + `Test the concept against ideas involving ${nouns.slice(0, 3).join(', ') || 'workflow automation'}.`, + 'Add promotion controls so a strong idea can become an implementation plan in one click.', + 'Introduce scoring so the board also helps decide what to build next.' + ]; + + const scores = [8, 7, 8, 7, 9, 8]; + + return `# ${title}\n\n## One-line concept\n${oneLine}\n\n## Problem\n${problem}\n\n## Why it matters\n${whyItMatters}\n\n## Users\n${users}\n\n## MVP\n- ${mvp.join('\n- ')}\n\n## Suggested stack\n- ${stackHints.join('\n- ')}\n\n## Risks\n- ${risks.join('\n- ')}\n\n## Scoring\n- Excitement: ${scores[0]}/10\n- Leverage: ${scores[1]}/10\n- Buildability: ${scores[2]}/10\n- Novelty: ${scores[3]}/10\n- Reusability: ${scores[4]}/10\n- Time-to-first-demo: ${scores[5]}/10\n\n## Next 3 steps\n1. ${nextSteps[0]}\n2. ${nextSteps[1]}\n3. ${nextSteps[2]}\n\n## Raw source\n> ${seed}\n`; +} + +function buildImplementationMarkdown(title: string, incubatedContent: string) { + const concept = incubatedContent.match(/## One-line concept\n([\s\S]*?)\n## /)?.[1]?.trim() || `${title} should become a focused first release.`; + const problem = incubatedContent.match(/## Problem\n([\s\S]*?)\n## /)?.[1]?.trim() || 'The workflow needs a tighter structure and a first shippable loop.'; + + return `# ${title}\n\n## Goal\nShip a convincing first version of ${title} that proves the core workflow from raw idea to concrete build plan.\n\n## Scope\n- Build a polished stage-based interface with strong motion and visible state changes.\n- Support raw capture, structured incubation, and promotion into implementation planning.\n- Keep storage local and file-backed so the prototype runs instantly on localhost.\n\n## Deliverables\n- A production-shaped landing experience with a clear call to action.\n- A functional board that reads and writes markdown files from the local idea folders.\n- A server-side promotion flow that turns fuzzy notes into structured artifacts.\n\n## Milestones\n1. Make the filesystem state visible in a clean interface.\n2. Add incubation and promotion actions with satisfying feedback loops.\n3. Tighten copy, motion, and edge states until the prototype feels real.\n\n## Open questions\n- Should later versions allow manual editing directly inside the board?\n- When should the local incubation engine be swapped for a full model-backed prompt pipeline?\n\n## First coding step\nWire the UI to the file-backed stage directories and validate the end-to-end flow with one strong example idea.\n\n## Context\n${concept}\n\n## Core problem\n${problem}\n`; +} + +export function incubateIdea(input: { rawIdea?: string; sourcePath?: string }) { + let sourcePath = input.sourcePath; + let title: string; + let rawIdea: string; + let slug: string; + + if (sourcePath) { + const content = fs.readFileSync(sourcePath, 'utf8'); + title = titleFromContent(content, path.basename(sourcePath, path.extname(sourcePath))); + rawIdea = content.match(/## Raw idea\n([\s\S]*?)(\n## |$)/)?.[1]?.trim() || content; + slug = path.basename(sourcePath, path.extname(sourcePath)); + } else if (input.rawIdea) { + rawIdea = input.rawIdea.trim(); + title = inferTitle(rawIdea); + slug = slugify(title); + sourcePath = path.join(IDEA_ROOT, 'inbox', `${slug}.md`); + if (!fs.existsSync(sourcePath)) { + createRawIdea(rawIdea); + } + } else { + throw new Error('No source idea provided'); + } + + const incubatedPath = path.join(IDEA_ROOT, 'incubating', `${slug}.md`); + ensureDir(path.dirname(incubatedPath)); + fs.writeFileSync(incubatedPath, buildIncubatedMarkdown(title, rawIdea), 'utf8'); + + return readIdeaFile('incubating', incubatedPath); +} + +export function promoteToImplementing(sourcePath: string) { + const content = fs.readFileSync(sourcePath, 'utf8'); + const title = titleFromContent(content, path.basename(sourcePath, path.extname(sourcePath))); + const slug = path.basename(sourcePath, path.extname(sourcePath)); + const implementingPath = path.join(IDEA_ROOT, 'implementing', `${slug}.md`); + ensureDir(path.dirname(implementingPath)); + fs.writeFileSync(implementingPath, buildImplementationMarkdown(title, content), 'utf8'); + return readIdeaFile('implementing', implementingPath); +} + +export function getIncubatorSnapshot(): IncubatorSnapshot { + const stageKeys = Object.keys(STAGE_CONFIG) as IdeaStage[]; + const stages = stageKeys.map((key) => { + const ideas = listStageIdeas(key); + return { + key, + label: STAGE_CONFIG[key].label, + description: STAGE_CONFIG[key].description, + count: ideas.length, + ideas + }; + }); + + const allIdeas = stages.flatMap((stage) => stage.ideas); + const lastUpdated = allIdeas.length + ? allIdeas.map((idea) => idea.updatedAt).sort((a, b) => +new Date(b) - +new Date(a))[0] + : null; + + return { + generatedAt: new Date().toISOString(), + root: IDEA_ROOT, + stages, + stats: { + totalIdeas: allIdeas.length, + stageCount: stages.length, + lastUpdated + } + }; +} diff --git a/src/lib/server/monitor.ts b/src/lib/server/monitor.ts new file mode 100644 index 0000000..baa3861 --- /dev/null +++ b/src/lib/server/monitor.ts @@ -0,0 +1,346 @@ +import { execSync } from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; + +type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue }; + +type MonitorSource = { + label: string; + path: string; + exists: boolean; +}; + +type MonitorMetric = { + label: string; + value: string; + detail?: string; +}; + +type MonitorSection = { + title: string; + description: string; + items: MonitorMetric[]; +}; + +type SkillEntry = { + name: string; + source: string; + location: string; +}; + +type CronJobSummary = { + id: string; + name: string; + enabled: boolean; + schedule: string; + target: string; + lastRun: string; + status: string; +}; + +export type OpenClawMonitorSnapshot = { + generatedAt: string; + hostname: string; + runtime: { + home: string; + workspace: string; + nodeVersion: string; + platform: string; + }; + sources: MonitorSource[]; + sections: MonitorSection[]; + skills: SkillEntry[]; + cronJobs: CronJobSummary[]; + statusSummary: string[]; + sanitizedConfig: Record; + rawCounts: { + memoryFiles: number; + workspaceEntries: number; + credentialFiles: number; + }; +}; + +const SECRET_PATTERNS = [ + /token/i, + /secret/i, + /password/i, + /api[-_]?key/i, + /auth/i, + /botToken/i, + /cookie/i, + /credential/i +]; + +const DEFAULT_HOME = process.env.OPENCLAW_HOME || '/root/.openclaw'; +const DEFAULT_WORKSPACE = process.env.OPENCLAW_WORKSPACE || path.join(DEFAULT_HOME, 'workspace'); + +function fileExists(filePath: string): boolean { + return fs.existsSync(filePath); +} + +function readJson(filePath: string): Record { + return JSON.parse(fs.readFileSync(filePath, 'utf8')) as Record; +} + +function sanitizeValue(key: string, value: JsonValue): JsonValue { + if (SECRET_PATTERNS.some((pattern) => pattern.test(key))) { + return '[redacted]'; + } + + if (Array.isArray(value)) { + return value.map((entry) => sanitizeValue(key, entry)); + } + + if (value && typeof value === 'object') { + return Object.fromEntries( + Object.entries(value).map(([childKey, childValue]) => [childKey, sanitizeValue(childKey, childValue)]) + ); + } + + return value; +} + +function listDirectories(basePath: string): string[] { + if (!fileExists(basePath)) return []; + + return fs + .readdirSync(basePath, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => path.join(basePath, entry.name)); +} + +function countFiles(basePath: string, matcher?: (filePath: string) => boolean): number { + if (!fileExists(basePath)) return 0; + + let count = 0; + const stack = [basePath]; + + while (stack.length) { + const current = stack.pop(); + if (!current) continue; + + for (const entry of fs.readdirSync(current, { withFileTypes: true })) { + const fullPath = path.join(current, entry.name); + if (entry.isDirectory()) { + stack.push(fullPath); + continue; + } + + if (!matcher || matcher(fullPath)) { + count += 1; + } + } + } + + return count; +} + +function runStatusCommand(home: string): string { + try { + return execSync('openclaw status', { + encoding: 'utf8', + env: { + ...process.env, + HOME: process.env.HOME || '/root', + OPENCLAW_HOME: home + }, + stdio: ['ignore', 'pipe', 'pipe'] + }); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return `openclaw status unavailable: ${message}`; + } +} + +function parseStatusSummary(statusText: string): string[] { + return statusText + .split('\n') + .map((line) => line.trim()) + .filter((line) => + line.startsWith('│ Dashboard') || + line.startsWith('│ OS') || + line.startsWith('│ Channel') || + line.startsWith('│ Update') || + line.startsWith('│ Gateway') || + line.startsWith('│ Gateway service') || + line.startsWith('│ Agents') || + line.startsWith('│ Memory') || + line.startsWith('│ Heartbeat') || + line.startsWith('│ Sessions') + ) + .map((line) => line.replace(/^│\s*/, '').replace(/\s*│$/, '')); +} + +function detectSkills(workspace: string): SkillEntry[] { + const skillRoots = [ + { source: 'workspace', root: path.join(workspace, 'skills') }, + { source: 'builtin', root: '/usr/lib/node_modules/openclaw/skills' }, + { source: 'extensions', root: '/usr/lib/node_modules/openclaw/extensions' } + ]; + + const results: SkillEntry[] = []; + + for (const { source, root } of skillRoots) { + if (!fileExists(root)) continue; + + if (source === 'extensions') { + for (const extensionDir of listDirectories(root)) { + const skillsDir = path.join(extensionDir, 'skills'); + for (const skillDir of listDirectories(skillsDir)) { + const skillFile = path.join(skillDir, 'SKILL.md'); + if (!fileExists(skillFile)) continue; + results.push({ + name: path.basename(skillDir), + source, + location: skillFile + }); + } + } + continue; + } + + for (const skillDir of listDirectories(root)) { + const skillFile = path.join(skillDir, 'SKILL.md'); + if (!fileExists(skillFile)) continue; + results.push({ + name: path.basename(skillDir), + source, + location: skillFile + }); + } + } + + return results.sort((a, b) => a.name.localeCompare(b.name)); +} + +function summarizeCronJobs(home: string): CronJobSummary[] { + const cronPath = path.join(home, 'cron', 'jobs.json'); + if (!fileExists(cronPath)) return []; + + const json = readJson(cronPath); + const jobs = Array.isArray(json.jobs) ? json.jobs : []; + + return jobs.map((job) => { + const record = job as Record; + const schedule = (record.schedule || {}) as Record; + const state = (record.state || {}) as Record; + const delivery = (record.delivery || {}) as Record; + + return { + id: String(record.id || 'unknown'), + name: String(record.name || 'Unnamed job'), + enabled: Boolean(record.enabled), + schedule: + schedule.kind === 'cron' + ? `${String(schedule.expr || 'n/a')} · ${String(schedule.tz || 'UTC')}` + : String(schedule.kind || 'unknown'), + target: `${String(record.sessionTarget || 'unknown')} · ${String(delivery.mode || 'none')}`, + lastRun: state.lastRunAtMs ? new Date(Number(state.lastRunAtMs)).toISOString() : 'not yet', + status: String(state.lastStatus || state.lastRunStatus || 'unknown') + }; + }); +} + +export function getOpenClawMonitorSnapshot(): OpenClawMonitorSnapshot { + const home = DEFAULT_HOME; + const workspace = DEFAULT_WORKSPACE; + const configPath = path.join(home, 'openclaw.json'); + const workspaceStatePath = path.join(workspace, '.openclaw', 'workspace-state.json'); + const devicesPath = path.join(home, 'devices', 'paired.json'); + const config = fileExists(configPath) ? readJson(configPath) : {}; + const sanitizedConfig = sanitizeValue('root', config) as Record; + const cronJobs = summarizeCronJobs(home); + const skills = detectSkills(workspace); + const statusText = runStatusCommand(home); + const statusSummary = parseStatusSummary(statusText); + const memoryDir = path.join(workspace, 'memory'); + const credentialsDir = path.join(home, 'credentials'); + const workspaceEntryCount = fileExists(workspace) ? fs.readdirSync(workspace).length : 0; + const pairedDevices = fileExists(devicesPath) + ? (((readJson(devicesPath).devices as JsonValue[]) || []).length ?? 0) + : 0; + + const meta = (config.meta || {}) as Record; + const agents = ((config.agents || {}) as Record).defaults as Record | undefined; + const model = agents?.model as Record | undefined; + const gateway = config.gateway as Record | undefined; + const channels = (config.channels || {}) as Record; + const acp = (config.acp || {}) as Record; + const tools = (config.tools || {}) as Record; + + const sections: MonitorSection[] = [ + { + title: 'Runtime', + description: 'Core OpenClaw installation facts for the current machine.', + items: [ + { label: 'Install version', value: String(meta.lastTouchedVersion || 'unknown') }, + { label: 'Default model', value: String(model?.primary || 'unknown') }, + { label: 'ACP backend', value: String(acp.backend || 'disabled'), detail: `default agent: ${String(acp.defaultAgent || 'n/a')}` }, + { label: 'Gateway mode', value: String(gateway?.mode || 'unknown'), detail: `bind: ${String(gateway?.bind || 'unknown')} · port: ${String(gateway?.port || 'unknown')}` }, + { label: 'Tool profile', value: String(tools.profile || 'unknown') } + ] + }, + { + title: 'Workspace', + description: 'What the monitor can discover from the local OpenClaw workspace.', + items: [ + { label: 'Workspace root', value: workspace }, + { label: 'Top-level entries', value: String(workspaceEntryCount) }, + { label: 'Memory files', value: String(countFiles(memoryDir, (filePath) => filePath.endsWith('.md'))) }, + { label: 'Workspace state', value: fileExists(workspaceStatePath) ? 'present' : 'missing' } + ] + }, + { + title: 'Channels and devices', + description: 'High-level connectivity, without exposing secrets or tokens.', + items: [ + ...Object.entries(channels).map(([name, value]) => { + const record = (value || {}) as Record; + return { + label: name, + value: record.enabled ? 'enabled' : 'disabled', + detail: `streaming: ${String(record.streaming || 'n/a')}` + }; + }), + { label: 'Paired devices', value: String(pairedDevices), detail: 'discovered from local device state' } + ] + }, + { + title: 'Automation', + description: 'Scheduled jobs discovered from the local cron state.', + items: [ + { label: 'Scheduled jobs', value: String(cronJobs.length) }, + { label: 'Healthy jobs', value: String(cronJobs.filter((job) => job.status === 'ok').length) }, + { label: 'Credential files', value: String(countFiles(credentialsDir)) }, + { label: 'Discovered skills', value: String(skills.length) } + ] + } + ]; + + return { + generatedAt: new Date().toISOString(), + hostname: fs.existsSync('/etc/hostname') ? fs.readFileSync('/etc/hostname', 'utf8').trim() : 'unknown-host', + runtime: { + home, + workspace, + nodeVersion: process.version, + platform: `${process.platform} ${process.arch}` + }, + sources: [ + { label: 'OpenClaw config', path: configPath, exists: fileExists(configPath) }, + { label: 'Workspace state', path: workspaceStatePath, exists: fileExists(workspaceStatePath) }, + { label: 'Cron jobs', path: path.join(home, 'cron', 'jobs.json'), exists: fileExists(path.join(home, 'cron', 'jobs.json')) }, + { label: 'Paired devices', path: devicesPath, exists: fileExists(devicesPath) } + ], + sections, + skills, + cronJobs, + statusSummary, + sanitizedConfig, + rawCounts: { + memoryFiles: countFiles(memoryDir, (filePath) => filePath.endsWith('.md')), + workspaceEntries: workspaceEntryCount, + credentialFiles: countFiles(credentialsDir) + } + }; +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 9cebde5..ad3c30f 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,10 +1,16 @@ + OpenClaw Monitor + diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts new file mode 100644 index 0000000..23d3afa --- /dev/null +++ b/src/routes/+page.server.ts @@ -0,0 +1,8 @@ +import type { PageServerLoad } from './$types'; +import { getIncubatorSnapshot } from '$lib/server/idea-incubator'; + +export const load: PageServerLoad = async () => { + return { + incubator: getIncubatorSnapshot() + }; +}; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index cc88df0..86b02a1 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,2 +1,431 @@ -

Welcome to SvelteKit

-

Visit svelte.dev/docs/kit to read the documentation

+ + + + Idea Incubator + + + +
+
+
+
+ +
+
+
+
+
+ + Idea Incubator +
+ +
+

+ Turn stray sparks into buildable systems. +

+

+ A local-first idea lab for messy notes, sharper concepts, and real implementation plans. The board reads your actual folders, preserves the raw capture, and turns the path from intuition to execution into something you can actually work inside. +

+
+ +
+ + Open workspace + + +
{stageLabels.join(' · ')}
+
+
+ +
+
+
+ System pulse + {totalIdeas} ideas +
+
+
+
+ Latest update + {incubator.stats.lastUpdated ? formatRelative(incubator.stats.lastUpdated) : 'fresh'} +
+
+
+
+
+ +
+ {#each incubator.stages as stage} +
+
{formatStageKey(stage.key)}
+
{stage.count}
+
+ {/each} +
+
+
+ +
+
+ Transformation loop + Live +
+
+
+
+
+
Capture the messy version first
+
Nothing gets lost before refinement begins.
+
+
+
+
+
+
Generate a concept spec
+
The app shapes problem, MVP, risks, and next steps.
+
+
+
+
+
+
Promote the winners
+
Implementation plans appear when the idea has real pull.
+
+
+
+
+
+
+
+ +
+ + +
+
+
+
+
Board
+

Filesystem-backed lanes

+
+
All three stages visible at once
+
+ +
+ {#each incubator.stages as stage, index} +
+
+
+
Stage {index + 1}
+

{stage.label}

+

{stage.description}

+
+
{stage.count}
+
+ +
+ {#if stage.ideas.length} + {#each stage.ideas as idea} + + {/each} + {:else} +
Nothing here yet. The next strong idea can claim this lane.
+ {/if} +
+
+ {/each} +
+
+ +
+
+
+
Detail view
+

The selected artifact

+
+ {#if selectedIdea} +
{formatStageKey(selectedIdea.stage)}
+ {/if} +
+ + {#if selectedIdea} +
+
+
+
+

{selectedIdea.title}

+

Updated {formatRelative(selectedIdea.updatedAt)}

+
+ {#if selectedIdea.stage === 'incubating'} + + {/if} +
+ +
+ {selectedIdea.summary} +
+
+ +
{selectedIdea.content}
+
+ {:else} +
+ Select an idea from the board to inspect the underlying markdown artifact. +
+ {/if} +
+
+
+ +
+
+
How this POC behaves
+
+

+ The landing experience borrows premium motion energy from your taste-skill direction, but the heart of the app is functional first: it reads the actual folder structure, writes markdown files, and lets promising concepts graduate into implementation artifacts. +

+

+ Right now the incubation pass uses a local server-side transformation engine so the localhost prototype stays self-contained. The shape is intentionally ready for a fuller model-backed prompt pipeline later. +

+
+
+ +
+
+
+
What is already in motion
+

Existing ideas with the most immediate pull

+
+
{implementingIdeas.length} implementing · {incubatingIdeas.length} incubating
+
+ +
+ {#each [...implementingIdeas, ...incubatingIdeas].slice(0, 4) as idea} + + {/each} +
+
+
+
+
diff --git a/src/routes/api/incubator/+server.ts b/src/routes/api/incubator/+server.ts new file mode 100644 index 0000000..34dead8 --- /dev/null +++ b/src/routes/api/incubator/+server.ts @@ -0,0 +1,48 @@ +import { json } from '@sveltejs/kit'; +import { + createRawIdea, + getIncubatorSnapshot, + incubateIdea, + promoteToImplementing +} from '$lib/server/idea-incubator'; + +export const GET = async () => { + return json(getIncubatorSnapshot()); +}; + +export const POST = async ({ request }) => { + const body = (await request.json()) as { + action?: 'capture' | 'incubate' | 'promote'; + rawIdea?: string; + sourcePath?: string; + }; + + if (body.action === 'capture') { + if (!body.rawIdea?.trim()) { + return json({ error: 'rawIdea is required' }, { status: 400 }); + } + + const created = createRawIdea(body.rawIdea); + return json({ created, snapshot: getIncubatorSnapshot() }); + } + + if (body.action === 'incubate') { + if (!body.rawIdea?.trim() && !body.sourcePath) { + return json({ error: 'rawIdea or sourcePath is required' }, { status: 400 }); + } + + const created = incubateIdea({ rawIdea: body.rawIdea, sourcePath: body.sourcePath }); + return json({ created, snapshot: getIncubatorSnapshot() }); + } + + if (body.action === 'promote') { + if (!body.sourcePath) { + return json({ error: 'sourcePath is required' }, { status: 400 }); + } + + const created = promoteToImplementing(body.sourcePath); + return json({ created, snapshot: getIncubatorSnapshot() }); + } + + return json({ error: 'Unsupported action' }, { status: 400 }); +}; diff --git a/src/routes/api/monitor/+server.ts b/src/routes/api/monitor/+server.ts new file mode 100644 index 0000000..38b0b0f --- /dev/null +++ b/src/routes/api/monitor/+server.ts @@ -0,0 +1,6 @@ +import { json } from '@sveltejs/kit'; +import { getOpenClawMonitorSnapshot } from '$lib/server/monitor'; + +export const GET = async () => { + return json(getOpenClawMonitorSnapshot()); +}; diff --git a/tsconfig.json b/tsconfig.json index 2c2ed3c..709cb83 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,7 +10,8 @@ "skipLibCheck": true, "sourceMap": true, "strict": true, - "moduleResolution": "bundler" + "moduleResolution": "bundler", + "types": ["node"] } // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files diff --git a/vite.config.ts b/vite.config.ts index bbf8c7d..bf699a8 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,6 +1,7 @@ import { sveltekit } from '@sveltejs/kit/vite'; +import tailwindcss from '@tailwindcss/vite'; import { defineConfig } from 'vite'; export default defineConfig({ - plugins: [sveltekit()] + plugins: [tailwindcss(), sveltekit()] });