diff --git a/.github/workflows/rowboat-build.yml b/.github/workflows/rowboat-build.yml index 270e6263..6546dcdd 100644 --- a/.github/workflows/rowboat-build.yml +++ b/.github/workflows/rowboat-build.yml @@ -20,12 +20,16 @@ jobs: - name: Install dependencies run: npm ci working-directory: apps/rowboat - + + - name: Verify Rowboat + run: npm run verify + working-directory: apps/rowboat + - name: Build Rowboat run: npm run build working-directory: apps/rowboat - build-rowboatx: + build-cli: runs-on: ubuntu-latest steps: @@ -34,7 +38,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v6 with: - cache-dependency-path: 'apps/rowboat/package-lock.json' + cache-dependency-path: 'apps/cli/package-lock.json' node-version: '24' cache: 'npm' @@ -42,6 +46,32 @@ jobs: run: npm ci working-directory: apps/cli - - name: Build Rowboat - run: npm run build - working-directory: apps/cli \ No newline at end of file + - name: Verify CLI + run: npm run verify + working-directory: apps/cli + + test-desktop-core: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: '24' + cache: 'pnpm' + cache-dependency-path: 'apps/x/pnpm-lock.yaml' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + working-directory: apps/x + + - name: Verify desktop app + run: pnpm run verify + working-directory: apps/x diff --git a/.github/workflows/x-publish.yml b/.github/workflows/x-publish.yml index c411ab68..607a938a 100644 --- a/.github/workflows/x-publish.yml +++ b/.github/workflows/x-publish.yml @@ -27,11 +27,8 @@ jobs: run: npm ci working-directory: apps/cli - # optional: run tests - # - run: npm test - - - name: Build - run: npm run build + - name: Verify + run: npm run verify working-directory: apps/cli - name: Pack @@ -40,4 +37,4 @@ jobs: - name: Publish to npm run: npm publish --access public - working-directory: apps/cli \ No newline at end of file + working-directory: apps/cli diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 00000000..c134d896 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,89 @@ +# Architecture + +This repository contains multiple Rowboat product surfaces. The quickest way to get oriented is to start from the table below instead of treating the repo as a single application. + +## Product Map + +| Surface | Path | Status | Purpose | +|---|---|---|---| +| Desktop app | `apps/x` | Primary | Local-first Electron app with Markdown memory, knowledge graph sync, and on-device workflows | +| Hosted web app | `apps/rowboat` | Active | Next.js platform with project-scoped agents, RAG, jobs, billing, and integrations | +| CLI/runtime | `apps/cli` | Active | Local HTTP runtime, workflow packaging, and npm-distributed `rowboatx` tooling | +| New frontend | `apps/rowboatx` | Active, evolving | Static Next.js UI that talks to the local runtime and shell-provided APIs | +| Docs | `apps/docs` | Active | Mintlify documentation site | +| Python SDK | `apps/python-sdk` | Supporting | Thin Python client for the hosted chat API | +| Experiments | `apps/experimental` | Experimental | Prototypes and one-off services not considered part of the core product | + +## How The Pieces Fit Together + +### `apps/x` +- Nested `pnpm` workspace for the desktop product. +- `apps/main` runs the Electron main process. +- `apps/preload` exposes the validated IPC bridge. +- `apps/renderer` contains the React UI. +- `packages/shared` holds shared schemas and IPC contracts. +- `packages/core` contains workspace, knowledge graph, agent, and integration logic. + +### `apps/rowboat` +- Hosted or self-hosted Next.js application. +- Uses MongoDB, Redis, Qdrant, uploads storage, background workers, and external providers. +- Organized into `application`, `entities`, `infrastructure`, and `interface-adapters` layers. + +### `apps/cli` + `apps/rowboatx` +- `apps/cli` provides the local API and runtime for runs, tools, permissions, and event streaming. +- `apps/rowboatx` is the browser UI that expects a runtime behind `/api/stream`, `/api/rowboat/*`, or a configured `window.config.apiBase`. + +## Shared Runtime Concepts + +- Local data lives under `~/.rowboat` by default. +- The desktop product stores knowledge as Markdown files and maintains Git-backed history for those notes. +- The hosted app uses project-scoped data stores instead of the desktop Markdown vault. +- Both the desktop and hosted surfaces rely on model/provider abstraction, tool calling, and external integrations. + +## Recommended Entry Points + +- Working on desktop memory, sync, or Electron UX: start in `apps/x` +- Working on hosted APIs, jobs, RAG, or project management: start in `apps/rowboat` +- Working on local runtime, SSE events, or packaging flows: start in `apps/cli` +- Working on the newer dashboard UI for the local runtime: start in `apps/rowboatx` + +## Common Commands + +### Desktop app +```bash +cd apps/x +pnpm install +npm run verify +npm run dev +npm run test +``` + +### Hosted web app +```bash +cd apps/rowboat +npm install +npm run verify +npm run dev +``` + +### CLI runtime +```bash +cd apps/cli +npm install +npm run verify +npm run server +``` + +### Local runtime frontend +```bash +cd apps/rowboatx +npm install +npm run dev +``` + +## Contributor Rules Of Thumb + +- Prefer `apps/x` when the change is local-first or knowledge-vault oriented. +- Prefer `apps/rowboat` when the change requires server-side persistence, auth, billing, or hosted APIs. +- Treat `apps/experimental` as non-core unless you are intentionally working on a prototype. +- When adding documentation, update the README closest to the surface you changed. diff --git a/README.md b/README.md index 361b87a0..e5dea0e8 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,18 @@ All API key files use the same format: } ``` +## Repository Map + +This repository contains multiple Rowboat surfaces. Start with `ARCHITECTURE.md` if you are contributing or trying to understand which app owns a feature. + +- `apps/x` - primary local-first Electron desktop app +- `apps/rowboat` - hosted Next.js platform and APIs +- `apps/cli` - local runtime and npm-distributed CLI package +- `apps/rowboatx` - newer frontend for the local runtime +- `apps/docs` - Mintlify docs site +- `apps/python-sdk` - Python API client +- `apps/experimental` - prototypes and non-core experiments + ## What it does Rowboat is a **local-first AI coworker** that can: diff --git a/apps/cli/README.md b/apps/cli/README.md new file mode 100644 index 00000000..d48185d8 --- /dev/null +++ b/apps/cli/README.md @@ -0,0 +1,45 @@ +# Rowboat CLI And Local Runtime + +`apps/cli` contains the npm-distributed `@rowboatlabs/rowboatx` package and the local HTTP runtime used by the newer frontend. + +## What Lives Here + +- Hono server for runs, messages, permissions, and SSE streaming +- Model and MCP configuration repositories under `~/.rowboat` +- Workflow import and export helpers +- Packaged CLI entrypoint in `bin/app.js` + +## Local Development + +Install and build: + +```bash +npm install +npm run verify +``` + +Run the local server: + +```bash +npm run server +``` + +## Key Commands + +- `npm run build` - compile TypeScript into `dist/` +- `npm run lint` - run CLI lint checks +- `npm run typecheck` - run TypeScript checks without emitting +- `npm run server` - start the local Hono runtime +- `npm run verify` - run lint, typecheck, and tests together +- `npm run migrate-agents` - run bundled agent migration script + +## Data Location + +The CLI/runtime stores configuration and runtime state in `~/.rowboat` by default. + +## Related Surfaces + +- `apps/rowboatx` provides the newer frontend that talks to this runtime +- `apps/x` has its own local-first desktop runtime and is the primary desktop product + +See the root `ARCHITECTURE.md` for the repo-level map. diff --git a/apps/cli/eslint.config.mjs b/apps/cli/eslint.config.mjs new file mode 100644 index 00000000..f6c2b519 --- /dev/null +++ b/apps/cli/eslint.config.mjs @@ -0,0 +1,37 @@ +import js from "@eslint/js"; +import globals from "globals"; +import tseslint from "typescript-eslint"; +import { defineConfig, globalIgnores } from "eslint/config"; + +export default defineConfig([ + globalIgnores(["dist/**"]), + { + files: ["src/**/*.ts"], + extends: [js.configs.recommended, ...tseslint.configs.recommended], + languageOptions: { + globals: { + ...globals.node, + }, + }, + rules: { + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-unused-expressions": "off", + "@typescript-eslint/no-unused-vars": "off", + "no-case-declarations": "off", + "no-useless-escape": "off", + "prefer-const": "off", + }, + }, + { + files: ["test/**/*.mjs", "bin/**/*.js"], + extends: [js.configs.recommended], + languageOptions: { + globals: { + ...globals.node, + }, + }, + rules: { + "no-unused-vars": "off", + }, + }, +]); diff --git a/apps/cli/package-lock.json b/apps/cli/package-lock.json index 1660cd7a..d6654638 100644 --- a/apps/cli/package-lock.json +++ b/apps/cli/package-lock.json @@ -43,9 +43,13 @@ "rowboatx": "bin/app.js" }, "devDependencies": { + "@eslint/js": "^9.39.2", "@types/node": "^24.9.1", "@types/react": "^18.3.12", - "typescript": "^5.9.3" + "eslint": "^9.39.2", + "globals": "^16.5.0", + "typescript": "^5.9.3", + "typescript-eslint": "^8.50.1" } }, "node_modules/@ai-sdk/anthropic": { @@ -180,6 +184,235 @@ "node": ">=14.13.1" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@google-cloud/local-auth": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@google-cloud/local-auth/-/local-auth-3.0.1.tgz", @@ -306,6 +539,58 @@ "hono": ">=3.9.0" } }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -582,12 +867,18 @@ "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", "license": "MIT" }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/node": { "version": "24.10.1", @@ -617,6 +908,288 @@ "csstype": "^3.2.2" } }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.2.tgz", + "integrity": "sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.58.2", + "@typescript-eslint/type-utils": "8.58.2", + "@typescript-eslint/utils": "8.58.2", + "@typescript-eslint/visitor-keys": "8.58.2", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.58.2", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.2.tgz", + "integrity": "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.58.2", + "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/typescript-estree": "8.58.2", + "@typescript-eslint/visitor-keys": "8.58.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.2.tgz", + "integrity": "sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.58.2", + "@typescript-eslint/types": "^8.58.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.2.tgz", + "integrity": "sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/visitor-keys": "8.58.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.2.tgz", + "integrity": "sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.2.tgz", + "integrity": "sha512-Z7EloNR/B389FvabdGeTo2XMs4W9TjtPiO9DAsmT0yom0bwlPyRjkJ1uCdW1DvrrrYP50AJZ9Xc3sByZA9+dcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/typescript-estree": "8.58.2", + "@typescript-eslint/utils": "8.58.2", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.2.tgz", + "integrity": "sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.2.tgz", + "integrity": "sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.58.2", + "@typescript-eslint/tsconfig-utils": "8.58.2", + "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/visitor-keys": "8.58.2", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.2.tgz", + "integrity": "sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.58.2", + "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/typescript-estree": "8.58.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.2.tgz", + "integrity": "sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.2", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@vercel/oidc": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.0.5.tgz", @@ -639,6 +1212,29 @@ "node": ">= 0.6" } }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -738,6 +1334,13 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, "node_modules/arrify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", @@ -902,6 +1505,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/camel-case": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", @@ -1039,6 +1652,13 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, "node_modules/content-disposition": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", @@ -1176,6 +1796,13 @@ } } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -1378,6 +2005,267 @@ "node": ">=8" } }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -1503,6 +2391,20 @@ "node": ">=8.6.0" } }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", @@ -1566,6 +2468,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -1599,6 +2514,44 @@ "url": "https://opencollective.com/express" } }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -1785,6 +2738,19 @@ "node": ">= 6" } }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/google-auth-library": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz", @@ -1866,6 +2832,16 @@ "node": ">=18" } }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -1979,6 +2955,43 @@ "url": "https://opencollective.com/express" } }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, "node_modules/indent-string": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", @@ -2254,6 +3267,19 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/json-bigint": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", @@ -2263,6 +3289,13 @@ "bignumber.js": "^9.0.0" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, "node_modules/json-schema": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", @@ -2284,6 +3317,13 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/jwa": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", @@ -2305,6 +3345,53 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -2466,6 +3553,13 @@ "node": "^18 || >=20" } }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, "node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -2653,12 +3747,75 @@ "license": "MIT", "peer": true }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "license": "BlueOak-1.0.0" }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -2687,6 +3844,16 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -2743,6 +3910,16 @@ "node": ">=16.20.0" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -2756,6 +3933,16 @@ "node": ">= 0.10" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", @@ -2869,6 +4056,16 @@ "node": ">=0.10.0" } }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/restore-cursor": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", @@ -2984,6 +4181,19 @@ "loose-envify": "^1.1.0" } }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/send": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", @@ -3289,6 +4499,80 @@ "node": ">=8" } }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -3328,12 +4612,38 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "license": "MIT" }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/type-fest": { "version": "4.41.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", @@ -3374,6 +4684,30 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.2.tgz", + "integrity": "sha512-V8iSng9mRbdZjl54VJ9NKr6ZB+dW0J3TzRXRGcSbLIej9jV86ZRtlYeTKDR/QLxXykocJ5icNzbsl2+5TzIvcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.58.2", + "@typescript-eslint/parser": "8.58.2", + "@typescript-eslint/typescript-estree": "8.58.2", + "@typescript-eslint/utils": "8.58.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, "node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", @@ -3390,6 +4724,16 @@ "node": ">= 0.8" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/url-template": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", @@ -3473,6 +4817,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrap-ansi": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", @@ -3650,6 +5004,19 @@ "node": "^20.19.0 || ^22.12.0 || >=23" } }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/yoga-layout": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", diff --git a/apps/cli/package.json b/apps/cli/package.json index 95755a96..9dc871d8 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -4,8 +4,11 @@ "main": "index.js", "type": "module", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", + "lint": "eslint src test bin", + "typecheck": "tsc --noEmit", + "test": "npm run build && node ./test/run-tests.mjs", "build": "rm -rf dist && tsc", + "verify": "npm run lint && npm run typecheck && npm test", "server": "node dist/server.js", "migrate-agents": "node dist/scripts/migrate-agents.js" }, @@ -21,8 +24,12 @@ "license": "Apache-2.0", "description": "", "devDependencies": { + "@eslint/js": "^9.39.2", "@types/node": "^24.9.1", "@types/react": "^18.3.12", + "eslint": "^9.39.2", + "globals": "^16.5.0", + "typescript-eslint": "^8.50.1", "typescript": "^5.9.3" }, "dependencies": { diff --git a/apps/cli/src/agents/repo.ts b/apps/cli/src/agents/repo.ts index c770b1ee..d615d09f 100644 --- a/apps/cli/src/agents/repo.ts +++ b/apps/cli/src/agents/repo.ts @@ -1,6 +1,5 @@ import { WorkDir } from "../config/config.js"; import fs from "fs/promises"; -import { glob } from "node:fs/promises"; import path from "path"; import z from "zod"; import { Agent } from "./agents.js"; @@ -19,11 +18,32 @@ export interface IAgentsRepo { export class FSAgentsRepo implements IAgentsRepo { private readonly agentsDir = path.join(WorkDir, "agents"); + private async listMarkdownFiles(dir: string, prefix: string = ""): Promise { + const entries = await fs.readdir(dir, { withFileTypes: true }); + const results: string[] = []; + + for (const entry of entries) { + const relativePath = prefix ? path.posix.join(prefix, entry.name) : entry.name; + const absolutePath = path.join(dir, entry.name); + + if (entry.isDirectory()) { + results.push(...await this.listMarkdownFiles(absolutePath, relativePath)); + continue; + } + + if (entry.isFile() && entry.name.endsWith(".md")) { + results.push(relativePath); + } + } + + return results; + } + async list(): Promise[]> { const result: z.infer[] = []; // list all md files in workdir/agents/ - const matches = await Array.fromAsync(glob("**/*.md", { cwd: this.agentsDir })); + const matches = await this.listMarkdownFiles(this.agentsDir); for (const file of matches) { try { const agent = await this.parseAgentMd(path.join(this.agentsDir, file)); @@ -79,16 +99,20 @@ export class FSAgentsRepo implements IAgentsRepo { async create(agent: z.infer): Promise { const { instructions, ...rest } = agent; const contents = `---\n${stringify(rest)}\n---\n${instructions}`; - await fs.writeFile(path.join(this.agentsDir, `${agent.name}.md`), contents); + const filePath = path.join(this.agentsDir, `${agent.name}.md`); + await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.writeFile(filePath, contents); } async update(id: string, agent: z.infer): Promise { const { instructions, ...rest } = agent; const contents = `---\n${stringify(rest)}\n---\n${instructions}`; - await fs.writeFile(path.join(this.agentsDir, `${id}.md`), contents); + const filePath = path.join(this.agentsDir, `${id}.md`); + await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.writeFile(filePath, contents); } async delete(id: string): Promise { await fs.unlink(path.join(this.agentsDir, `${id}.md`)); } -} \ No newline at end of file +} diff --git a/apps/cli/src/app.ts b/apps/cli/src/app.ts index a2391f0e..c03c3847 100644 --- a/apps/cli/src/app.ts +++ b/apps/cli/src/app.ts @@ -12,7 +12,7 @@ import { Agent } from "./agents/agents.js"; import { McpServerConfig, McpServerDefinition } from "./mcp/schema.js"; import { Example } from "./entities/example.js"; import { z } from "zod"; -import { Flavor } from "./models/models.js"; +import { Flavor } from "./models/schema.js"; import { examples } from "./examples/index.js"; import container from "./di/container.js"; import { IModelConfigRepo } from "./models/repo.js"; diff --git a/apps/cli/src/config/config.ts b/apps/cli/src/config/config.ts index 94355b54..97cbc447 100644 --- a/apps/cli/src/config/config.ts +++ b/apps/cli/src/config/config.ts @@ -2,14 +2,29 @@ import path from "path"; import fs from "fs"; import { homedir } from "os"; -// Resolve app root relative to compiled file location (dist/...) -export const WorkDir = path.join(homedir(), ".rowboat"); +function resolveWorkDir(): string { + const configured = process.env.ROWBOAT_WORKDIR; + if (!configured) { + return path.join(homedir(), ".rowboat"); + } + + const expanded = configured === "~" + ? homedir() + : (configured.startsWith("~/") || configured.startsWith("~\\")) + ? path.join(homedir(), configured.slice(2)) + : configured; + + return path.resolve(expanded); +} + +export const WorkDir = resolveWorkDir(); function ensureDirs() { const ensure = (p: string) => { if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true }); }; ensure(WorkDir); ensure(path.join(WorkDir, "agents")); ensure(path.join(WorkDir, "config")); + ensure(path.join(WorkDir, "runs")); } -ensureDirs(); \ No newline at end of file +ensureDirs(); diff --git a/apps/cli/src/mcp/repo.ts b/apps/cli/src/mcp/repo.ts index fbb4106e..d80e57cf 100644 --- a/apps/cli/src/mcp/repo.ts +++ b/apps/cli/src/mcp/repo.ts @@ -12,9 +12,10 @@ export interface IMcpConfigRepo { export class FSMcpConfigRepo implements IMcpConfigRepo { private readonly configPath = path.join(WorkDir, "config", "mcp.json"); + private readonly initPromise: Promise; constructor() { - this.ensureDefaultConfig(); + this.initPromise = this.ensureDefaultConfig(); } private async ensureDefaultConfig(): Promise { @@ -25,18 +26,25 @@ export class FSMcpConfigRepo implements IMcpConfigRepo { } } + private async ensureInitialized(): Promise { + await this.initPromise; + } + async getConfig(): Promise> { + await this.ensureInitialized(); const config = await fs.readFile(this.configPath, "utf8"); return McpServerConfig.parse(JSON.parse(config)); } async upsert(serverName: string, config: z.infer): Promise { + await this.ensureInitialized(); const conf = await this.getConfig(); conf.mcpServers[serverName] = config; await fs.writeFile(this.configPath, JSON.stringify(conf, null, 2)); } async delete(serverName: string): Promise { + await this.ensureInitialized(); const conf = await this.getConfig(); delete conf.mcpServers[serverName]; await fs.writeFile(this.configPath, JSON.stringify(conf, null, 2)); diff --git a/apps/cli/src/models/models.ts b/apps/cli/src/models/models.ts index d2d846e5..da0134cb 100644 --- a/apps/cli/src/models/models.ts +++ b/apps/cli/src/models/models.ts @@ -8,33 +8,9 @@ import { createOpenRouter } from '@openrouter/ai-sdk-provider'; import { createOpenAICompatible } from '@ai-sdk/openai-compatible'; import { IModelConfigRepo } from "./repo.js"; import container from "../di/container.js"; -import z from "zod"; +import { Flavor, Provider, ModelConfig } from "./schema.js"; -export const Flavor = z.enum([ - "rowboat [free]", - "aigateway", - "anthropic", - "google", - "ollama", - "openai", - "openai-compatible", - "openrouter", -]); - -export const Provider = z.object({ - flavor: Flavor, - apiKey: z.string().optional(), - baseURL: z.string().optional(), - headers: z.record(z.string(), z.string()).optional(), -}); - -export const ModelConfig = z.object({ - providers: z.record(z.string(), Provider), - defaults: z.object({ - provider: z.string(), - model: z.string(), - }), -}); +export { Flavor, Provider, ModelConfig }; const providerMap: Record = {}; @@ -116,4 +92,4 @@ export async function getProvider(name: string = ""): Promise { throw new Error(`Provider ${name} not found`); } return providerMap[name]; -} \ No newline at end of file +} diff --git a/apps/cli/src/models/repo.ts b/apps/cli/src/models/repo.ts index 1041045c..cf0c49c8 100644 --- a/apps/cli/src/models/repo.ts +++ b/apps/cli/src/models/repo.ts @@ -1,4 +1,4 @@ -import { ModelConfig, Provider } from "./models.js"; +import { ModelConfig, Provider } from "./schema.js"; import { WorkDir } from "../config/config.js"; import fs from "fs/promises"; import path from "path"; @@ -25,9 +25,10 @@ const defaultConfig: z.infer = { export class FSModelConfigRepo implements IModelConfigRepo { private readonly configPath = path.join(WorkDir, "config", "models.json"); + private readonly initPromise: Promise; constructor() { - this.ensureDefaultConfig(); + this.initPromise = this.ensureDefaultConfig(); } private async ensureDefaultConfig(): Promise { @@ -38,12 +39,18 @@ export class FSModelConfigRepo implements IModelConfigRepo { } } + private async ensureInitialized(): Promise { + await this.initPromise; + } + async getConfig(): Promise> { + await this.ensureInitialized(); const config = await fs.readFile(this.configPath, "utf8"); return ModelConfig.parse(JSON.parse(config)); } private async setConfig(config: z.infer): Promise { + await this.ensureInitialized(); await fs.writeFile(this.configPath, JSON.stringify(config, null, 2)); } @@ -67,4 +74,4 @@ export class FSModelConfigRepo implements IModelConfigRepo { }; await this.setConfig(conf); } -} \ No newline at end of file +} diff --git a/apps/cli/src/models/schema.ts b/apps/cli/src/models/schema.ts new file mode 100644 index 00000000..01093cea --- /dev/null +++ b/apps/cli/src/models/schema.ts @@ -0,0 +1,27 @@ +import z from "zod"; + +export const Flavor = z.enum([ + "rowboat [free]", + "aigateway", + "anthropic", + "google", + "ollama", + "openai", + "openai-compatible", + "openrouter", +]); + +export const Provider = z.object({ + flavor: Flavor, + apiKey: z.string().optional(), + baseURL: z.string().optional(), + headers: z.record(z.string(), z.string()).optional(), +}); + +export const ModelConfig = z.object({ + providers: z.record(z.string(), Provider), + defaults: z.object({ + provider: z.string(), + model: z.string(), + }), +}); diff --git a/apps/cli/src/runs/repo.ts b/apps/cli/src/runs/repo.ts index 5b741f2b..3a8be135 100644 --- a/apps/cli/src/runs/repo.ts +++ b/apps/cli/src/runs/repo.ts @@ -1,4 +1,4 @@ -import { Run } from "./runs.js"; +import { Run } from "./schema.js"; import z from "zod"; import { IMonotonicallyIncreasingIdGenerator } from "../application/lib/id-gen.js"; import { WorkDir } from "../config/config.js"; @@ -37,6 +37,7 @@ export class FSRunsRepo implements IRunsRepo { } async appendEvents(runId: string, events: z.infer[]): Promise { + await fsp.mkdir(path.join(WorkDir, 'runs'), { recursive: true }); await fsp.appendFile( path.join(WorkDir, 'runs', `${runId}.jsonl`), events.map(event => JSON.stringify(event)).join("\n") + "\n" @@ -141,4 +142,4 @@ export class FSRunsRepo implements IRunsRepo { ...(nextCursor ? { nextCursor } : {}), }; } -} \ No newline at end of file +} diff --git a/apps/cli/src/runs/runs.ts b/apps/cli/src/runs/runs.ts index e4f8dc84..f2e60286 100644 --- a/apps/cli/src/runs/runs.ts +++ b/apps/cli/src/runs/runs.ts @@ -5,6 +5,7 @@ import { AskHumanResponseEvent, RunEvent, ToolPermissionResponseEvent } from ".. import { CreateRunOptions, IRunsRepo } from "./repo.js"; import { IAgentRuntime } from "../agents/runtime.js"; import { IBus } from "../application/lib/bus.js"; +import { Run } from "./schema.js"; export const ToolPermissionAuthorizePayload = ToolPermissionResponseEvent.pick({ subflow: true, @@ -18,12 +19,7 @@ export const AskHumanResponsePayload = AskHumanResponseEvent.pick({ response: true, }); -export const Run = z.object({ - id: z.string(), - createdAt: z.iso.datetime(), - agentId: z.string(), - log: z.array(RunEvent), -}); +export { Run }; export async function createRun(opts: z.infer): Promise> { const repo = container.resolve('runsRepo'); @@ -67,4 +63,4 @@ export async function replyToHumanInputRequest(runId: string, ev: z.infer { throw new Error('Not implemented'); -} \ No newline at end of file +} diff --git a/apps/cli/src/runs/schema.ts b/apps/cli/src/runs/schema.ts new file mode 100644 index 00000000..db503b35 --- /dev/null +++ b/apps/cli/src/runs/schema.ts @@ -0,0 +1,10 @@ +import z from "zod"; + +import { RunEvent } from "../entities/run-events.js"; + +export const Run = z.object({ + id: z.string(), + createdAt: z.iso.datetime(), + agentId: z.string(), + log: z.array(RunEvent), +}); diff --git a/apps/cli/src/server.ts b/apps/cli/src/server.ts index 6aa10c25..1b324b4c 100644 --- a/apps/cli/src/server.ts +++ b/apps/cli/src/server.ts @@ -4,193 +4,208 @@ import { streamSSE } from 'hono/streaming' import { describeRoute, validator, resolver, openAPIRouteHandler } from "hono-openapi" import z from 'zod'; import container from './di/container.js'; -import { executeTool, listServers, listTools } from "./mcp/mcp.js"; -import { ListToolsResponse, McpServerDefinition, McpServerList } from "./mcp/schema.js"; -import { IMcpConfigRepo } from './mcp/repo.js'; -import { IModelConfigRepo } from './models/repo.js'; -import { ModelConfig, Provider } from "./models/models.js"; -import { IAgentsRepo } from "./agents/repo.js"; -import { Agent } from "./agents/agents.js"; import { AskHumanResponsePayload, authorizePermission, createMessage, createRun, replyToHumanInputRequest, Run, stop, ToolPermissionAuthorizePayload } from './runs/runs.js'; -import { IRunsRepo, CreateRunOptions, ListRunsResponse } from './runs/repo.js'; import { IBus } from './application/lib/bus.js'; import { cors } from 'hono/cors'; +import { pathToFileURL } from 'node:url'; -let id = 0; +export interface ServerDependencies { + createMessage(runId: string, message: string): Promise; + authorizePermission(runId: string, payload: z.infer): Promise; + replyToHumanInputRequest(runId: string, payload: z.infer): Promise; + stop(runId: string): Promise; + subscribeToEvents(listener: (event: unknown) => Promise): Promise<() => void>; +} -const routes = new Hono() - .post( - '/runs/:runId/messages/new', - describeRoute({ - summary: 'Create a new message', - description: 'Create a new message', - responses: { - 200: { - description: 'Message created', - content: { - 'application/json': { - schema: resolver(z.object({ - messageId: z.string(), - })), +const defaultDependencies: ServerDependencies = { + createMessage, + authorizePermission, + replyToHumanInputRequest, + stop, + subscribeToEvents: async (listener) => { + const bus = container.resolve('bus'); + return bus.subscribe('*', listener); + }, +}; + +export function createApp(deps: ServerDependencies = defaultDependencies): Hono { + const routes = new Hono() + .post( + '/runs/:runId/messages/new', + describeRoute({ + summary: 'Create a new message', + description: 'Create a new message', + responses: { + 200: { + description: 'Message created', + content: { + 'application/json': { + schema: resolver(z.object({ + messageId: z.string(), + })), + }, }, }, }, - }, - }), - validator('param', z.object({ - runId: z.string(), - })), - validator('json', z.object({ - message: z.string(), - })), - async (c) => { - const messageId = await createMessage(c.req.valid('param').runId, c.req.valid('json').message); - return c.json({ - messageId, - }); - } - ) - .post( - '/runs/:runId/permissions/authorize', - describeRoute({ - summary: 'Authorize permission', - description: 'Authorize a permission', - responses: { - 200: { - description: 'Permission authorized', - content: { - 'application/json': { - schema: resolver(z.object({ - success: z.literal(true), - })), - }, - } - }, - }, - }), - validator('param', z.object({ - runId: z.string(), - })), - validator('json', ToolPermissionAuthorizePayload), - async (c) => { - const response = await authorizePermission( - c.req.valid('param').runId, - c.req.valid('json') - ); - return c.json({ - success: true, - }); - } - ) - .post( - '/runs/:runId/human-input-requests/:requestId/reply', - describeRoute({ - summary: 'Reply to human input request', - description: 'Reply to a human input request', - responses: { - 200: { - description: 'Human input request replied', - }, - }, - }), - validator('param', z.object({ - runId: z.string(), - })), - validator('json', AskHumanResponsePayload), - async (c) => { - const response = await replyToHumanInputRequest( - c.req.valid('param').runId, - c.req.valid('json') - ); - return c.json({ - success: true, - }); - } - ) - .post( - '/runs/:runId/stop', - describeRoute({ - summary: 'Stop run', - description: 'Stop a run', - responses: { - 200: { - description: 'Run stopped', - }, - }, - }), - validator('param', z.object({ - runId: z.string(), - })), - async (c) => { - const response = await stop(c.req.valid('param').runId); - return c.json({ - success: true, - }); - } - ) - .get( - '/stream', - describeRoute({ - summary: 'Subscribe to run events', - description: 'Subscribe to run events', - }), - async (c) => { - return streamSSE(c, async (stream) => { - const bus = container.resolve('bus'); - - let id = 0; - let unsub: (() => void) | null = null; - let aborted = false; - - stream.onAbort(() => { - aborted = true; - if (unsub) { - unsub(); - } + }), + validator('param', z.object({ + runId: z.string(), + })), + validator('json', z.object({ + message: z.string(), + })), + async (c) => { + const messageId = await deps.createMessage(c.req.valid('param').runId, c.req.valid('json').message); + return c.json({ + messageId, }); + } + ) + .post( + '/runs/:runId/permissions/authorize', + describeRoute({ + summary: 'Authorize permission', + description: 'Authorize a permission', + responses: { + 200: { + description: 'Permission authorized', + content: { + 'application/json': { + schema: resolver(z.object({ + success: z.literal(true), + })), + }, + } + }, + }, + }), + validator('param', z.object({ + runId: z.string(), + })), + validator('json', ToolPermissionAuthorizePayload), + async (c) => { + await deps.authorizePermission( + c.req.valid('param').runId, + c.req.valid('json') + ); + return c.json({ + success: true, + }); + } + ) + .post( + '/runs/:runId/human-input-requests/:requestId/reply', + describeRoute({ + summary: 'Reply to human input request', + description: 'Reply to a human input request', + responses: { + 200: { + description: 'Human input request replied', + }, + }, + }), + validator('param', z.object({ + runId: z.string(), + })), + validator('json', AskHumanResponsePayload), + async (c) => { + await deps.replyToHumanInputRequest( + c.req.valid('param').runId, + c.req.valid('json') + ); + return c.json({ + success: true, + }); + } + ) + .post( + '/runs/:runId/stop', + describeRoute({ + summary: 'Stop run', + description: 'Stop a run', + responses: { + 200: { + description: 'Run stopped', + }, + }, + }), + validator('param', z.object({ + runId: z.string(), + })), + async (c) => { + await deps.stop(c.req.valid('param').runId); + return c.json({ + success: true, + }); + } + ) + .get( + '/stream', + describeRoute({ + summary: 'Subscribe to run events', + description: 'Subscribe to run events', + }), + async (c) => { + return streamSSE(c, async (stream) => { + let eventId = 0; + let unsub: (() => void) | null = null; + let aborted = false; - // Subscribe to your bus - unsub = await bus.subscribe('*', async (event) => { - if (aborted) return; - - await stream.writeSSE({ - data: JSON.stringify(event), - event: "message", - id: String(id++), + stream.onAbort(() => { + aborted = true; + if (unsub) { + unsub(); + } }); + + unsub = await deps.subscribeToEvents(async (event) => { + if (aborted) return; + + await stream.writeSSE({ + data: JSON.stringify(event), + event: "message", + id: String(eventId++), + }); + }); + + while (!aborted) { + await stream.sleep(1000); + } }); + } + ); - // Keep the function alive until the client disconnects - while (!aborted) { - await stream.sleep(1000); // any interval is fine - } - }); - } - ) - ; - -const app = new Hono() - .use("/*", cors()) - .route("/", routes) - .get( - "/openapi.json", - openAPIRouteHandler(routes, { - documentation: { - info: { - title: "Hono", - version: "1.0.0", - description: "RowboatX API", + return new Hono() + .use("/*", cors()) + .route("/", routes) + .get( + "/openapi.json", + openAPIRouteHandler(routes, { + documentation: { + info: { + title: "Hono", + version: "1.0.0", + description: "RowboatX API", + }, }, - }, - }), - ); + }), + ); +} -// export default app; +export const app = createApp(); -serve({ - fetch: app.fetch, - port: Number(process.env.PORT) || 3000, -}); +export function startServer(port: number = Number(process.env.PORT) || 3000): void { + serve({ + fetch: app.fetch, + port, + }); +} + +const isMain = process.argv[1] ? import.meta.url === pathToFileURL(process.argv[1]).href : false; + +if (isMain) { + startServer(); +} // GET /skills // POST /skills/new diff --git a/apps/cli/src/tui/api.ts b/apps/cli/src/tui/api.ts index b54534ac..63e0c242 100644 --- a/apps/cli/src/tui/api.ts +++ b/apps/cli/src/tui/api.ts @@ -2,7 +2,7 @@ import { createParser } from "eventsource-parser"; import { Agent } from "../agents/agents.js"; import { AskHumanResponsePayload, Run, ToolPermissionAuthorizePayload } from "../runs/runs.js"; import { ListRunsResponse } from "../runs/repo.js"; -import { ModelConfig } from "../models/models.js"; +import { ModelConfig } from "../models/schema.js"; import { RunEvent } from "../entities/run-events.js"; import z from "zod"; diff --git a/apps/cli/test/repos.test.mjs b/apps/cli/test/repos.test.mjs new file mode 100644 index 00000000..0184ab54 --- /dev/null +++ b/apps/cli/test/repos.test.mjs @@ -0,0 +1,83 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import path from "node:path"; + +import { WorkDir } from "../dist/config/config.js"; +import { FSModelConfigRepo } from "../dist/models/repo.js"; +import { FSMcpConfigRepo } from "../dist/mcp/repo.js"; +import { FSAgentsRepo } from "../dist/agents/repo.js"; +import { FSRunsRepo } from "../dist/runs/repo.js"; + +test("uses ROWBOAT_WORKDIR override and eagerly creates expected directories", async () => { + assert.equal(WorkDir, process.env.ROWBOAT_WORKDIR); + + for (const dirName of ["agents", "config", "runs"]) { + const stats = await fs.stat(path.join(WorkDir, dirName)); + assert.equal(stats.isDirectory(), true); + } +}); + +test("FSModelConfigRepo returns defaults on a fresh workspace", async () => { + const repo = new FSModelConfigRepo(); + const config = await repo.getConfig(); + + assert.equal(config.defaults.provider, "openai"); + assert.equal(config.defaults.model, "gpt-5.1"); + assert.equal(config.providers.openai?.flavor, "openai"); +}); + +test("FSMcpConfigRepo returns an empty config on a fresh workspace", async () => { + const repo = new FSMcpConfigRepo(); + const config = await repo.getConfig(); + + assert.deepEqual(config, { mcpServers: {} }); +}); + +test("FSAgentsRepo can create and read nested agent files", async () => { + const repo = new FSAgentsRepo(); + await repo.create({ + name: "team/copilot", + description: "Team helper", + provider: "openai", + model: "gpt-5.1", + instructions: "Be helpful.", + }); + + const fetched = await repo.fetch("team/copilot"); + assert.equal(fetched.name, "team/copilot"); + assert.equal(fetched.description, "Team helper"); + assert.equal(fetched.instructions, "Be helpful."); +}); + +test("FSRunsRepo creates, fetches, and lists runs", async () => { + let nextId = 0; + const repo = new FSRunsRepo({ + idGenerator: { + next: async () => `run-${++nextId}`, + }, + }); + + const first = await repo.create({ agentId: "copilot" }); + await repo.appendEvents(first.id, [{ + type: "message", + runId: first.id, + subflow: [], + messageId: "msg-1", + message: { + role: "user", + content: "hello", + }, + }]); + + const second = await repo.create({ agentId: "planner" }); + + const fetched = await repo.fetch(first.id); + assert.equal(fetched.id, first.id); + assert.equal(fetched.agentId, "copilot"); + assert.equal(fetched.log.length, 2); + assert.equal(fetched.log[1].type, "message"); + + const listed = await repo.list(); + assert.deepEqual(listed.runs.map((run) => run.id), [second.id, first.id]); +}); diff --git a/apps/cli/test/run-tests.mjs b/apps/cli/test/run-tests.mjs new file mode 100644 index 00000000..18527177 --- /dev/null +++ b/apps/cli/test/run-tests.mjs @@ -0,0 +1,31 @@ +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { spawn } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const packageDir = path.resolve(__dirname, ".."); +const tempRoot = await mkdtemp(path.join(tmpdir(), "rowboat-cli-test-")); +const testWorkDir = path.join(tempRoot, "workspace"); + +try { + const exitCode = await new Promise((resolve, reject) => { + const child = spawn(process.execPath, ["--test", "./test/repos.test.mjs", "./test/server.test.mjs"], { + cwd: packageDir, + stdio: "inherit", + env: { + ...process.env, + ROWBOAT_WORKDIR: testWorkDir, + }, + }); + + child.on("error", reject); + child.on("exit", (code) => resolve(code ?? 1)); + }); + + process.exitCode = Number(exitCode); +} finally { + await rm(tempRoot, { recursive: true, force: true }); +} diff --git a/apps/cli/test/server.test.mjs b/apps/cli/test/server.test.mjs new file mode 100644 index 00000000..00fa0931 --- /dev/null +++ b/apps/cli/test/server.test.mjs @@ -0,0 +1,131 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { createApp } from "../dist/server.js"; + +test("message endpoint creates a message and returns its id", async () => { + const calls = []; + const app = createApp({ + createMessage: async (runId, message) => { + calls.push({ runId, message }); + return "msg-123"; + }, + authorizePermission: async () => {}, + replyToHumanInputRequest: async () => {}, + stop: async () => {}, + subscribeToEvents: async () => () => {}, + }); + + const response = await app.request("/runs/run-1/messages/new", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ message: "hello" }), + }); + + assert.equal(response.status, 200); + assert.deepEqual(await response.json(), { messageId: "msg-123" }); + assert.deepEqual(calls, [{ runId: "run-1", message: "hello" }]); +}); + +test("permission endpoint validates payload and calls dependency", async () => { + const calls = []; + const app = createApp({ + createMessage: async () => "unused", + authorizePermission: async (runId, payload) => { + calls.push({ runId, payload }); + }, + replyToHumanInputRequest: async () => {}, + stop: async () => {}, + subscribeToEvents: async () => () => {}, + }); + + const response = await app.request("/runs/run-2/permissions/authorize", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + subflow: ["child"], + toolCallId: "tool-1", + response: "approve", + }), + }); + + assert.equal(response.status, 200); + assert.deepEqual(await response.json(), { success: true }); + assert.deepEqual(calls, [{ + runId: "run-2", + payload: { + subflow: ["child"], + toolCallId: "tool-1", + response: "approve", + }, + }]); +}); + +test("invalid message payload returns a validation error", async () => { + const app = createApp({ + createMessage: async () => "unused", + authorizePermission: async () => {}, + replyToHumanInputRequest: async () => {}, + stop: async () => {}, + subscribeToEvents: async () => () => {}, + }); + + const response = await app.request("/runs/run-1/messages/new", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({}), + }); + + assert.equal(response.status, 400); +}); + +test("openapi endpoint is exposed", async () => { + const app = createApp({ + createMessage: async () => "unused", + authorizePermission: async () => {}, + replyToHumanInputRequest: async () => {}, + stop: async () => {}, + subscribeToEvents: async () => () => {}, + }); + + const response = await app.request("/openapi.json"); + const body = await response.json(); + + assert.equal(response.status, 200); + assert.equal(body.info.title, "Hono"); + assert.ok(body.paths["/runs/{runId}/messages/new"]); +}); + +test("stream endpoint emits SSE payloads and unsubscribes on cancel", async () => { + let listener; + let unsubscribed = false; + const app = createApp({ + createMessage: async () => "unused", + authorizePermission: async () => {}, + replyToHumanInputRequest: async () => {}, + stop: async () => {}, + subscribeToEvents: async (fn) => { + listener = fn; + return () => { + unsubscribed = true; + }; + }, + }); + + const response = await app.request("/stream"); + assert.equal(response.status, 200); + assert.equal(response.headers.get("content-type"), "text/event-stream"); + + await listener({ type: "message", data: { hello: "world" } }); + + const reader = response.body.getReader(); + const chunk = await reader.read(); + const text = new TextDecoder().decode(chunk.value); + + assert.match(text, /event: message/); + assert.match(text, /"hello":"world"/); + + await reader.cancel(); + await new Promise((resolve) => setTimeout(resolve, 0)); + assert.equal(unsubscribed, true); +}); diff --git a/apps/docs/docs.json b/apps/docs/docs.json index 8442b158..7f2b59c6 100644 --- a/apps/docs/docs.json +++ b/apps/docs/docs.json @@ -2,7 +2,7 @@ "$schema": "https://mintlify.com/docs.json", "theme": "maple", "name": "Rowboat", - "description": "Rowboat is an open-source platform for building multi-agent systems. It helps you orchestrate tools, RAG, memory, and deployable agents with ease.", + "description": "Rowboat is a local-first AI coworker with transparent Markdown memory, agent workflows, tools, and optional hosted services.", "favicon": "/favicon.ico", "colors": { "primary": "#6366F1", @@ -57,4 +57,4 @@ ] } } - \ No newline at end of file + diff --git a/apps/rowboat/README.md b/apps/rowboat/README.md index 310c46f5..99515726 100644 --- a/apps/rowboat/README.md +++ b/apps/rowboat/README.md @@ -1,6 +1,16 @@ -This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). +# Rowboat Web App -## Getting Started +`apps/rowboat` is the hosted or self-hosted Next.js application in this repository. It is the server-backed Rowboat surface with project-scoped conversations, data sources, jobs, integrations, billing hooks, and RAG infrastructure. + +## What Lives Here + +- Next.js 15 App Router application +- Project and conversation APIs under `app/api` +- Dependency injection container in `di/container.ts` +- Layered backend code under `src/application`, `src/entities`, `src/infrastructure`, and `src/interface-adapters` +- Background workers for jobs and RAG ingestion in `app/scripts` + +## Local Development Install dependencies: @@ -8,29 +18,45 @@ Install dependencies: npm install ``` -First, run the development server: +Run the app: ```bash npm run dev ``` -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +Useful commands: -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +```bash +npm run verify +npm run build +npm run lint +npm run typecheck +npm run rag-worker +npm run jobs-worker +npm run setupQdrant +npm run deleteQdrant +``` -This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. +## Infrastructure Dependencies -## Learn More +This app can depend on several services, depending on the features you enable: -To learn more about Next.js, take a look at the following resources: +- MongoDB for primary application data +- Redis for caching, pub/sub, and job coordination +- Qdrant for vector search +- Local uploads or S3 for document storage +- External model providers and integrations configured through environment variables -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +The root `docker-compose.yml` is the easiest way to see the expected service topology. -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! +## Environment -## Deploy on Vercel +Start from the repository `.env.example` and add the services you need. Common feature flags include auth, RAG, uploads, scraping, billing, and chat widget support. -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. +## Architectural Notes -Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. +- Route handlers stay thin and resolve controllers from the DI container. +- Use cases and repositories are split by domain. +- Workers in `app/scripts` handle asynchronous processing such as document ingestion and recurring jobs. + +If you are trying to understand where a feature belongs in the repo, read the root `ARCHITECTURE.md` first. diff --git a/apps/rowboat/app/api/v1/[projectId]/chat/route.ts b/apps/rowboat/app/api/v1/[projectId]/chat/route.ts index 95234692..c59a5929 100644 --- a/apps/rowboat/app/api/v1/[projectId]/chat/route.ts +++ b/apps/rowboat/app/api/v1/[projectId]/chat/route.ts @@ -1,22 +1,40 @@ -import { NextRequest } from "next/server"; import { z } from "zod"; import { ApiResponse } from "@/app/lib/types/api_types"; import { ApiRequest } from "@/app/lib/types/api_types"; import { PrefixLogger } from "../../../../lib/utils"; -import { container } from "@/di/container"; import { IRunTurnController } from "@/src/interface-adapters/controllers/conversations/run-turn.controller"; +type ChatRouteContext = { params: Promise<{ projectId: string }> }; + +interface LoggerLike { + log(message: string): void; +} + +interface ChatRouteDependencies { + createLogger(requestId: string): LoggerLike; + resolveRunTurnController(): IRunTurnController | Promise; +} + +const defaultDependencies: ChatRouteDependencies = { + createLogger: (requestId) => new PrefixLogger(`${requestId}`), + resolveRunTurnController: async () => { + const { container } = await import("@/di/container"); + return container.resolve("runTurnController"); + }, +}; + // get next turn / agent response -export async function POST( - req: NextRequest, - { params }: { params: Promise<{ projectId: string }> } +export async function handlePostChat( + req: Request, + { params }: ChatRouteContext, + deps: ChatRouteDependencies = defaultDependencies, ): Promise { const { projectId } = await params; const requestId = crypto.randomUUID(); - const logger = new PrefixLogger(`${requestId}`); + const logger = deps.createLogger(requestId); // parse and validate the request body - let data; + let data: z.infer; try { const body = await req.json(); data = ApiRequest.parse(body); @@ -26,7 +44,7 @@ export async function POST( } const { conversationId, messages, mockTools, stream } = data; - const runTurnController = container.resolve("runTurnController"); + const runTurnController = await deps.resolveRunTurnController(); // get assistant response const response = await runTurnController.execute({ @@ -81,3 +99,7 @@ export async function POST( }; return Response.json(responseBody); } + +export async function POST(req: Request, context: ChatRouteContext): Promise { + return handlePostChat(req, context); +} diff --git a/apps/rowboat/di/container.ts b/apps/rowboat/di/container.ts index 402a952e..3c40d1bb 100644 --- a/apps/rowboat/di/container.ts +++ b/apps/rowboat/di/container.ts @@ -1,162 +1,14 @@ -import { asClass, createContainer, InjectionMode } from "awilix"; +import { createContainer, InjectionMode } from "awilix"; -// Services -import { RedisPubSubService } from "@/src/infrastructure/services/redis.pub-sub.service"; -import { S3UploadsStorageService } from "@/src/infrastructure/services/s3.uploads-storage.service"; -import { LocalUploadsStorageService } from "@/src/infrastructure/services/local.uploads-storage.service"; - -import { RunConversationTurnUseCase } from "@/src/application/use-cases/conversations/run-conversation-turn.use-case"; -import { MongoDBConversationsRepository } from "@/src/infrastructure/repositories/mongodb.conversations.repository"; -import { RunCachedTurnController } from "@/src/interface-adapters/controllers/conversations/run-cached-turn.controller"; -import { CreatePlaygroundConversationController } from "@/src/interface-adapters/controllers/conversations/create-playground-conversation.controller"; -import { CreateConversationUseCase } from "@/src/application/use-cases/conversations/create-conversation.use-case"; -import { RedisCacheService } from "@/src/infrastructure/services/redis.cache.service"; -import { CreateCachedTurnUseCase } from "@/src/application/use-cases/conversations/create-cached-turn.use-case"; -import { FetchCachedTurnUseCase } from "@/src/application/use-cases/conversations/fetch-cached-turn.use-case"; -import { CreateCachedTurnController } from "@/src/interface-adapters/controllers/conversations/create-cached-turn.controller"; -import { RunTurnController } from "@/src/interface-adapters/controllers/conversations/run-turn.controller"; -import { RedisUsageQuotaPolicy } from "@/src/infrastructure/policies/redis.usage-quota.policy"; -import { ProjectActionAuthorizationPolicy } from "@/src/application/policies/project-action-authorization.policy"; -import { MongoDBProjectMembersRepository } from "@/src/infrastructure/repositories/mongodb.project-members.repository"; -import { MongoDBApiKeysRepository } from "@/src/infrastructure/repositories/mongodb.api-keys.repository"; -import { MongodbProjectsRepository } from "@/src/infrastructure/repositories/mongodb.projects.repository"; -import { MongodbComposioTriggerDeploymentsRepository } from "@/src/infrastructure/repositories/mongodb.composio-trigger-deployments.repository"; -import { CreateComposioTriggerDeploymentUseCase } from "@/src/application/use-cases/composio-trigger-deployments/create-composio-trigger-deployment.use-case"; -import { ListComposioTriggerDeploymentsUseCase } from "@/src/application/use-cases/composio-trigger-deployments/list-composio-trigger-deployments.use-case"; -import { FetchComposioTriggerDeploymentUseCase } from "@/src/application/use-cases/composio-trigger-deployments/fetch-composio-trigger-deployment.use-case"; -import { DeleteComposioTriggerDeploymentUseCase } from "@/src/application/use-cases/composio-trigger-deployments/delete-composio-trigger-deployment.use-case"; -import { ListComposioTriggerTypesUseCase } from "@/src/application/use-cases/composio-trigger-deployments/list-composio-trigger-types.use-case"; -import { HandleCompsioWebhookRequestUseCase } from "@/src/application/use-cases/composio/webhook/handle-composio-webhook-request.use-case"; -import { MongoDBJobsRepository } from "@/src/infrastructure/repositories/mongodb.jobs.repository"; -import { CreateComposioTriggerDeploymentController } from "@/src/interface-adapters/controllers/composio-trigger-deployments/create-composio-trigger-deployment.controller"; -import { DeleteComposioTriggerDeploymentController } from "@/src/interface-adapters/controllers/composio-trigger-deployments/delete-composio-trigger-deployment.controller"; -import { ListComposioTriggerDeploymentsController } from "@/src/interface-adapters/controllers/composio-trigger-deployments/list-composio-trigger-deployments.controller"; -import { FetchComposioTriggerDeploymentController } from "@/src/interface-adapters/controllers/composio-trigger-deployments/fetch-composio-trigger-deployment.controller"; -import { ListComposioTriggerTypesController } from "@/src/interface-adapters/controllers/composio-trigger-deployments/list-composio-trigger-types.controller"; -import { HandleComposioWebhookRequestController } from "@/src/interface-adapters/controllers/composio/webhook/handle-composio-webhook-request.controller"; -import { JobsWorker } from "@/src/application/workers/jobs.worker"; -import { JobRulesWorker } from "@/src/application/workers/job-rules.worker"; -import { ListJobsUseCase } from "@/src/application/use-cases/jobs/list-jobs.use-case"; -import { ListJobsController } from "@/src/interface-adapters/controllers/jobs/list-jobs.controller"; -import { ListConversationsUseCase } from "@/src/application/use-cases/conversations/list-conversations.use-case"; -import { ListConversationsController } from "@/src/interface-adapters/controllers/conversations/list-conversations.controller"; -import { FetchJobUseCase } from "@/src/application/use-cases/jobs/fetch-job.use-case"; -import { FetchJobController } from "@/src/interface-adapters/controllers/jobs/fetch-job.controller"; -import { FetchConversationUseCase } from "@/src/application/use-cases/conversations/fetch-conversation.use-case"; -import { FetchConversationController } from "@/src/interface-adapters/controllers/conversations/fetch-conversation.controller"; - -// Projects -import { CreateProjectUseCase } from "@/src/application/use-cases/projects/create-project.use-case"; -import { CreateProjectController } from "@/src/interface-adapters/controllers/projects/create-project.controller"; -import { DeleteComposioConnectedAccountUseCase } from "@/src/application/use-cases/projects/delete-composio-connected-account.use-case"; -import { DeleteComposioConnectedAccountController } from "@/src/interface-adapters/controllers/projects/delete-composio-connected-account.controller"; -import { CreateComposioManagedConnectedAccountUseCase } from "@/src/application/use-cases/projects/create-composio-managed-connected-account.use-case"; -import { CreateCustomConnectedAccountUseCase } from "@/src/application/use-cases/projects/create-custom-connected-account.use-case"; -import { SyncConnectedAccountUseCase } from "@/src/application/use-cases/projects/sync-connected-account.use-case"; -import { ListComposioToolkitsUseCase } from "@/src/application/use-cases/projects/list-composio-toolkits.use-case"; -import { GetComposioToolkitUseCase } from "@/src/application/use-cases/projects/get-composio-toolkit.use-case"; -import { ListComposioToolsUseCase } from "@/src/application/use-cases/projects/list-composio-tools.use-case"; -import { AddCustomMcpServerUseCase } from "@/src/application/use-cases/projects/add-custom-mcp-server.use-case"; -import { RemoveCustomMcpServerUseCase } from "@/src/application/use-cases/projects/remove-custom-mcp-server.use-case"; -import { CreateComposioManagedConnectedAccountController } from "@/src/interface-adapters/controllers/projects/create-composio-managed-connected-account.controller"; -import { CreateCustomConnectedAccountController } from "@/src/interface-adapters/controllers/projects/create-custom-connected-account.controller"; -import { SyncConnectedAccountController } from "@/src/interface-adapters/controllers/projects/sync-connected-account.controller"; -import { ListComposioToolkitsController } from "@/src/interface-adapters/controllers/projects/list-composio-toolkits.controller"; -import { GetComposioToolkitController } from "@/src/interface-adapters/controllers/projects/get-composio-toolkit.controller"; -import { ListComposioToolsController } from "@/src/interface-adapters/controllers/projects/list-composio-tools.controller"; -import { AddCustomMcpServerController } from "@/src/interface-adapters/controllers/projects/add-custom-mcp-server.controller"; -import { RemoveCustomMcpServerController } from "@/src/interface-adapters/controllers/projects/remove-custom-mcp-server.controller"; - -// Scheduled Job Rules -import { MongoDBScheduledJobRulesRepository } from "@/src/infrastructure/repositories/mongodb.scheduled-job-rules.repository"; -import { CreateScheduledJobRuleUseCase } from "@/src/application/use-cases/scheduled-job-rules/create-scheduled-job-rule.use-case"; -import { FetchScheduledJobRuleUseCase } from "@/src/application/use-cases/scheduled-job-rules/fetch-scheduled-job-rule.use-case"; -import { ListScheduledJobRulesUseCase } from "@/src/application/use-cases/scheduled-job-rules/list-scheduled-job-rules.use-case"; -import { DeleteScheduledJobRuleUseCase } from "@/src/application/use-cases/scheduled-job-rules/delete-scheduled-job-rule.use-case"; -import { UpdateScheduledJobRuleUseCase } from "@/src/application/use-cases/scheduled-job-rules/update-scheduled-job-rule.use-case"; -import { CreateScheduledJobRuleController } from "@/src/interface-adapters/controllers/scheduled-job-rules/create-scheduled-job-rule.controller"; -import { FetchScheduledJobRuleController } from "@/src/interface-adapters/controllers/scheduled-job-rules/fetch-scheduled-job-rule.controller"; -import { ListScheduledJobRulesController } from "@/src/interface-adapters/controllers/scheduled-job-rules/list-scheduled-job-rules.controller"; -import { DeleteScheduledJobRuleController } from "@/src/interface-adapters/controllers/scheduled-job-rules/delete-scheduled-job-rule.controller"; -import { UpdateScheduledJobRuleController } from "@/src/interface-adapters/controllers/scheduled-job-rules/update-scheduled-job-rule.controller"; - -// Recurring Job Rules -import { MongoDBRecurringJobRulesRepository } from "@/src/infrastructure/repositories/mongodb.recurring-job-rules.repository"; -import { CreateRecurringJobRuleUseCase } from "@/src/application/use-cases/recurring-job-rules/create-recurring-job-rule.use-case"; -import { FetchRecurringJobRuleUseCase } from "@/src/application/use-cases/recurring-job-rules/fetch-recurring-job-rule.use-case"; -import { ListRecurringJobRulesUseCase } from "@/src/application/use-cases/recurring-job-rules/list-recurring-job-rules.use-case"; -import { ToggleRecurringJobRuleUseCase } from "@/src/application/use-cases/recurring-job-rules/toggle-recurring-job-rule.use-case"; -import { DeleteRecurringJobRuleUseCase } from "@/src/application/use-cases/recurring-job-rules/delete-recurring-job-rule.use-case"; -import { UpdateRecurringJobRuleUseCase } from "@/src/application/use-cases/recurring-job-rules/update-recurring-job-rule.use-case"; -import { CreateRecurringJobRuleController } from "@/src/interface-adapters/controllers/recurring-job-rules/create-recurring-job-rule.controller"; -import { FetchRecurringJobRuleController } from "@/src/interface-adapters/controllers/recurring-job-rules/fetch-recurring-job-rule.controller"; -import { ListRecurringJobRulesController } from "@/src/interface-adapters/controllers/recurring-job-rules/list-recurring-job-rules.controller"; -import { ToggleRecurringJobRuleController } from "@/src/interface-adapters/controllers/recurring-job-rules/toggle-recurring-job-rule.controller"; -import { DeleteRecurringJobRuleController } from "@/src/interface-adapters/controllers/recurring-job-rules/delete-recurring-job-rule.controller"; -import { UpdateRecurringJobRuleController } from "@/src/interface-adapters/controllers/recurring-job-rules/update-recurring-job-rule.controller"; - -// API Keys -import { CreateApiKeyUseCase } from "@/src/application/use-cases/api-keys/create-api-key.use-case"; -import { ListApiKeysUseCase } from "@/src/application/use-cases/api-keys/list-api-keys.use-case"; -import { DeleteApiKeyUseCase } from "@/src/application/use-cases/api-keys/delete-api-key.use-case"; -import { CreateApiKeyController } from "@/src/interface-adapters/controllers/api-keys/create-api-key.controller"; -import { ListApiKeysController } from "@/src/interface-adapters/controllers/api-keys/list-api-keys.controller"; -import { DeleteApiKeyController } from "@/src/interface-adapters/controllers/api-keys/delete-api-key.controller"; - -// Data sources -import { MongoDBDataSourcesRepository } from "@/src/infrastructure/repositories/mongodb.data-sources.repository"; -import { MongoDBDataSourceDocsRepository } from "@/src/infrastructure/repositories/mongodb.data-source-docs.repository"; -import { CreateDataSourceUseCase } from "@/src/application/use-cases/data-sources/create-data-source.use-case"; -import { FetchDataSourceUseCase } from "@/src/application/use-cases/data-sources/fetch-data-source.use-case"; -import { ListDataSourcesUseCase } from "@/src/application/use-cases/data-sources/list-data-sources.use-case"; -import { UpdateDataSourceUseCase } from "@/src/application/use-cases/data-sources/update-data-source.use-case"; -import { DeleteDataSourceUseCase } from "@/src/application/use-cases/data-sources/delete-data-source.use-case"; -import { ToggleDataSourceUseCase } from "@/src/application/use-cases/data-sources/toggle-data-source.use-case"; -import { CreateDataSourceController } from "@/src/interface-adapters/controllers/data-sources/create-data-source.controller"; -import { FetchDataSourceController } from "@/src/interface-adapters/controllers/data-sources/fetch-data-source.controller"; -import { ListDataSourcesController } from "@/src/interface-adapters/controllers/data-sources/list-data-sources.controller"; -import { UpdateDataSourceController } from "@/src/interface-adapters/controllers/data-sources/update-data-source.controller"; -import { DeleteDataSourceController } from "@/src/interface-adapters/controllers/data-sources/delete-data-source.controller"; -import { ToggleDataSourceController } from "@/src/interface-adapters/controllers/data-sources/toggle-data-source.controller"; -import { AddDocsToDataSourceUseCase } from "@/src/application/use-cases/data-sources/add-docs-to-data-source.use-case"; -import { ListDocsInDataSourceUseCase } from "@/src/application/use-cases/data-sources/list-docs-in-data-source.use-case"; -import { DeleteDocFromDataSourceUseCase } from "@/src/application/use-cases/data-sources/delete-doc-from-data-source.use-case"; -import { RecrawlWebDataSourceUseCase } from "@/src/application/use-cases/data-sources/recrawl-web-data-source.use-case"; -import { GetUploadUrlsForFilesUseCase } from "@/src/application/use-cases/data-sources/get-upload-urls-for-files.use-case"; -import { GetDownloadUrlForFileUseCase } from "@/src/application/use-cases/data-sources/get-download-url-for-file.use-case"; -import { AddDocsToDataSourceController } from "@/src/interface-adapters/controllers/data-sources/add-docs-to-data-source.controller"; -import { ListDocsInDataSourceController } from "@/src/interface-adapters/controllers/data-sources/list-docs-in-data-source.controller"; -import { DeleteDocFromDataSourceController } from "@/src/interface-adapters/controllers/data-sources/delete-doc-from-data-source.controller"; -import { RecrawlWebDataSourceController } from "@/src/interface-adapters/controllers/data-sources/recrawl-web-data-source.controller"; -import { GetUploadUrlsForFilesController } from "@/src/interface-adapters/controllers/data-sources/get-upload-urls-for-files.controller"; -import { GetDownloadUrlForFileController } from "@/src/interface-adapters/controllers/data-sources/get-download-url-for-file.controller"; -import { DeleteProjectController } from "@/src/interface-adapters/controllers/projects/delete-project.controller"; -import { DeleteProjectUseCase } from "@/src/application/use-cases/projects/delete-project.use-case"; -import { ListProjectsUseCase } from "@/src/application/use-cases/projects/list-projects.use-case"; -import { ListProjectsController } from "@/src/interface-adapters/controllers/projects/list-projects.controller"; -import { FetchProjectUseCase } from "@/src/application/use-cases/projects/fetch-project.use-case"; -import { FetchProjectController } from "@/src/interface-adapters/controllers/projects/fetch-project.controller"; -import { RotateSecretUseCase } from "@/src/application/use-cases/projects/rotate-secret.use-case"; -import { RotateSecretController } from "@/src/interface-adapters/controllers/projects/rotate-secret.controller"; -import { UpdateWebhookUrlUseCase } from "@/src/application/use-cases/projects/update-webhook-url.use-case"; -import { UpdateWebhookUrlController } from "@/src/interface-adapters/controllers/projects/update-webhook-url.controller"; -import { UpdateProjectNameUseCase } from "@/src/application/use-cases/projects/update-project-name.use-case"; -import { UpdateProjectNameController } from "@/src/interface-adapters/controllers/projects/update-project-name.controller"; -import { UpdateDraftWorkflowUseCase } from "@/src/application/use-cases/projects/update-draft-workflow.use-case"; -import { UpdateDraftWorkflowController } from "@/src/interface-adapters/controllers/projects/update-draft-workflow.controller"; -import { UpdateLiveWorkflowUseCase } from "@/src/application/use-cases/projects/update-live-workflow.use-case"; -import { UpdateLiveWorkflowController } from "@/src/interface-adapters/controllers/projects/update-live-workflow.controller"; -import { RevertToLiveWorkflowUseCase } from "@/src/application/use-cases/projects/revert-to-live-workflow.use-case"; -import { RevertToLiveWorkflowController } from "@/src/interface-adapters/controllers/projects/revert-to-live-workflow.controller"; - -// copilot -import { CreateCopilotCachedTurnUseCase } from "@/src/application/use-cases/copilot/create-copilot-cached-turn.use-case"; -import { CreateCopilotCachedTurnController } from "@/src/interface-adapters/controllers/copilot/create-copilot-cached-turn.controller"; -import { RunCopilotCachedTurnUseCase } from "@/src/application/use-cases/copilot/run-copilot-cached-turn.use-case"; -import { RunCopilotCachedTurnController } from "@/src/interface-adapters/controllers/copilot/run-copilot-cached-turn.controller"; - -// users -import { MongoDBUsersRepository } from "@/src/infrastructure/repositories/mongodb.users.repository"; +import { coreRegistrations } from "@/di/modules/core"; +import { apiKeyRegistrations } from "@/di/modules/api-keys"; +import { dataSourceRegistrations } from "@/di/modules/data-sources"; +import { jobRegistrations } from "@/di/modules/jobs"; +import { projectRegistrations } from "@/di/modules/projects"; +import { composioRegistrations } from "@/di/modules/composio"; +import { conversationRegistrations } from "@/di/modules/conversations"; +import { copilotRegistrations } from "@/di/modules/copilot"; +import { userRegistrations } from "@/di/modules/users"; export const container = createContainer({ injectionMode: InjectionMode.PROXY, @@ -164,192 +16,13 @@ export const container = createContainer({ }); container.register({ - // workers - // --- - jobsWorker: asClass(JobsWorker).singleton(), - jobRulesWorker: asClass(JobRulesWorker).singleton(), - - // services - // --- - cacheService: asClass(RedisCacheService).singleton(), - pubSubService: asClass(RedisPubSubService).singleton(), - s3UploadsStorageService: asClass(S3UploadsStorageService).singleton(), - localUploadsStorageService: asClass(LocalUploadsStorageService).singleton(), - - // policies - // --- - usageQuotaPolicy: asClass(RedisUsageQuotaPolicy).singleton(), - projectActionAuthorizationPolicy: asClass(ProjectActionAuthorizationPolicy).singleton(), - - // projects - // --- - projectsRepository: asClass(MongodbProjectsRepository).singleton(), - - // project members - // --- - projectMembersRepository: asClass(MongoDBProjectMembersRepository).singleton(), - - // api keys - // --- - apiKeysRepository: asClass(MongoDBApiKeysRepository).singleton(), - createApiKeyUseCase: asClass(CreateApiKeyUseCase).singleton(), - listApiKeysUseCase: asClass(ListApiKeysUseCase).singleton(), - deleteApiKeyUseCase: asClass(DeleteApiKeyUseCase).singleton(), - createApiKeyController: asClass(CreateApiKeyController).singleton(), - listApiKeysController: asClass(ListApiKeysController).singleton(), - deleteApiKeyController: asClass(DeleteApiKeyController).singleton(), - - // data sources - // --- - dataSourcesRepository: asClass(MongoDBDataSourcesRepository).singleton(), - dataSourceDocsRepository: asClass(MongoDBDataSourceDocsRepository).singleton(), - createDataSourceUseCase: asClass(CreateDataSourceUseCase).singleton(), - fetchDataSourceUseCase: asClass(FetchDataSourceUseCase).singleton(), - listDataSourcesUseCase: asClass(ListDataSourcesUseCase).singleton(), - updateDataSourceUseCase: asClass(UpdateDataSourceUseCase).singleton(), - deleteDataSourceUseCase: asClass(DeleteDataSourceUseCase).singleton(), - toggleDataSourceUseCase: asClass(ToggleDataSourceUseCase).singleton(), - createDataSourceController: asClass(CreateDataSourceController).singleton(), - fetchDataSourceController: asClass(FetchDataSourceController).singleton(), - listDataSourcesController: asClass(ListDataSourcesController).singleton(), - updateDataSourceController: asClass(UpdateDataSourceController).singleton(), - deleteDataSourceController: asClass(DeleteDataSourceController).singleton(), - toggleDataSourceController: asClass(ToggleDataSourceController).singleton(), - addDocsToDataSourceUseCase: asClass(AddDocsToDataSourceUseCase).singleton(), - listDocsInDataSourceUseCase: asClass(ListDocsInDataSourceUseCase).singleton(), - deleteDocFromDataSourceUseCase: asClass(DeleteDocFromDataSourceUseCase).singleton(), - recrawlWebDataSourceUseCase: asClass(RecrawlWebDataSourceUseCase).singleton(), - getUploadUrlsForFilesUseCase: asClass(GetUploadUrlsForFilesUseCase).singleton(), - getDownloadUrlForFileUseCase: asClass(GetDownloadUrlForFileUseCase).singleton(), - addDocsToDataSourceController: asClass(AddDocsToDataSourceController).singleton(), - listDocsInDataSourceController: asClass(ListDocsInDataSourceController).singleton(), - deleteDocFromDataSourceController: asClass(DeleteDocFromDataSourceController).singleton(), - recrawlWebDataSourceController: asClass(RecrawlWebDataSourceController).singleton(), - getUploadUrlsForFilesController: asClass(GetUploadUrlsForFilesController).singleton(), - getDownloadUrlForFileController: asClass(GetDownloadUrlForFileController).singleton(), - - // jobs - // --- - jobsRepository: asClass(MongoDBJobsRepository).singleton(), - listJobsUseCase: asClass(ListJobsUseCase).singleton(), - listJobsController: asClass(ListJobsController).singleton(), - fetchJobUseCase: asClass(FetchJobUseCase).singleton(), - fetchJobController: asClass(FetchJobController).singleton(), - - // scheduled job rules - // --- - scheduledJobRulesRepository: asClass(MongoDBScheduledJobRulesRepository).singleton(), - createScheduledJobRuleUseCase: asClass(CreateScheduledJobRuleUseCase).singleton(), - fetchScheduledJobRuleUseCase: asClass(FetchScheduledJobRuleUseCase).singleton(), - listScheduledJobRulesUseCase: asClass(ListScheduledJobRulesUseCase).singleton(), - updateScheduledJobRuleUseCase: asClass(UpdateScheduledJobRuleUseCase).singleton(), - deleteScheduledJobRuleUseCase: asClass(DeleteScheduledJobRuleUseCase).singleton(), - createScheduledJobRuleController: asClass(CreateScheduledJobRuleController).singleton(), - fetchScheduledJobRuleController: asClass(FetchScheduledJobRuleController).singleton(), - listScheduledJobRulesController: asClass(ListScheduledJobRulesController).singleton(), - updateScheduledJobRuleController: asClass(UpdateScheduledJobRuleController).singleton(), - deleteScheduledJobRuleController: asClass(DeleteScheduledJobRuleController).singleton(), - - // recurring job rules - // --- - recurringJobRulesRepository: asClass(MongoDBRecurringJobRulesRepository).singleton(), - createRecurringJobRuleUseCase: asClass(CreateRecurringJobRuleUseCase).singleton(), - fetchRecurringJobRuleUseCase: asClass(FetchRecurringJobRuleUseCase).singleton(), - listRecurringJobRulesUseCase: asClass(ListRecurringJobRulesUseCase).singleton(), - toggleRecurringJobRuleUseCase: asClass(ToggleRecurringJobRuleUseCase).singleton(), - updateRecurringJobRuleUseCase: asClass(UpdateRecurringJobRuleUseCase).singleton(), - deleteRecurringJobRuleUseCase: asClass(DeleteRecurringJobRuleUseCase).singleton(), - createRecurringJobRuleController: asClass(CreateRecurringJobRuleController).singleton(), - fetchRecurringJobRuleController: asClass(FetchRecurringJobRuleController).singleton(), - listRecurringJobRulesController: asClass(ListRecurringJobRulesController).singleton(), - toggleRecurringJobRuleController: asClass(ToggleRecurringJobRuleController).singleton(), - updateRecurringJobRuleController: asClass(UpdateRecurringJobRuleController).singleton(), - deleteRecurringJobRuleController: asClass(DeleteRecurringJobRuleController).singleton(), - - // projects - // --- - createProjectUseCase: asClass(CreateProjectUseCase).singleton(), - createProjectController: asClass(CreateProjectController).singleton(), - fetchProjectUseCase: asClass(FetchProjectUseCase).singleton(), - fetchProjectController: asClass(FetchProjectController).singleton(), - listProjectsUseCase: asClass(ListProjectsUseCase).singleton(), - listProjectsController: asClass(ListProjectsController).singleton(), - rotateSecretUseCase: asClass(RotateSecretUseCase).singleton(), - rotateSecretController: asClass(RotateSecretController).singleton(), - updateWebhookUrlUseCase: asClass(UpdateWebhookUrlUseCase).singleton(), - updateWebhookUrlController: asClass(UpdateWebhookUrlController).singleton(), - updateProjectNameUseCase: asClass(UpdateProjectNameUseCase).singleton(), - updateProjectNameController: asClass(UpdateProjectNameController).singleton(), - updateDraftWorkflowUseCase: asClass(UpdateDraftWorkflowUseCase).singleton(), - updateDraftWorkflowController: asClass(UpdateDraftWorkflowController).singleton(), - updateLiveWorkflowUseCase: asClass(UpdateLiveWorkflowUseCase).singleton(), - updateLiveWorkflowController: asClass(UpdateLiveWorkflowController).singleton(), - revertToLiveWorkflowUseCase: asClass(RevertToLiveWorkflowUseCase).singleton(), - revertToLiveWorkflowController: asClass(RevertToLiveWorkflowController).singleton(), - deleteProjectUseCase: asClass(DeleteProjectUseCase).singleton(), - deleteProjectController: asClass(DeleteProjectController).singleton(), - deleteComposioConnectedAccountController: asClass(DeleteComposioConnectedAccountController).singleton(), - deleteComposioConnectedAccountUseCase: asClass(DeleteComposioConnectedAccountUseCase).singleton(), - createComposioManagedConnectedAccountUseCase: asClass(CreateComposioManagedConnectedAccountUseCase).singleton(), - createComposioManagedConnectedAccountController: asClass(CreateComposioManagedConnectedAccountController).singleton(), - createCustomConnectedAccountUseCase: asClass(CreateCustomConnectedAccountUseCase).singleton(), - createCustomConnectedAccountController: asClass(CreateCustomConnectedAccountController).singleton(), - syncConnectedAccountUseCase: asClass(SyncConnectedAccountUseCase).singleton(), - syncConnectedAccountController: asClass(SyncConnectedAccountController).singleton(), - listComposioToolkitsUseCase: asClass(ListComposioToolkitsUseCase).singleton(), - listComposioToolkitsController: asClass(ListComposioToolkitsController).singleton(), - getComposioToolkitUseCase: asClass(GetComposioToolkitUseCase).singleton(), - getComposioToolkitController: asClass(GetComposioToolkitController).singleton(), - listComposioToolsUseCase: asClass(ListComposioToolsUseCase).singleton(), - listComposioToolsController: asClass(ListComposioToolsController).singleton(), - addCustomMcpServerUseCase: asClass(AddCustomMcpServerUseCase).singleton(), - addCustomMcpServerController: asClass(AddCustomMcpServerController).singleton(), - removeCustomMcpServerUseCase: asClass(RemoveCustomMcpServerUseCase).singleton(), - removeCustomMcpServerController: asClass(RemoveCustomMcpServerController).singleton(), - - // composio - // --- - handleCompsioWebhookRequestUseCase: asClass(HandleCompsioWebhookRequestUseCase).singleton(), - handleComposioWebhookRequestController: asClass(HandleComposioWebhookRequestController).singleton(), - - // composio trigger deployments - // --- - composioTriggerDeploymentsRepository: asClass(MongodbComposioTriggerDeploymentsRepository).singleton(), - listComposioTriggerTypesUseCase: asClass(ListComposioTriggerTypesUseCase).singleton(), - createComposioTriggerDeploymentUseCase: asClass(CreateComposioTriggerDeploymentUseCase).singleton(), - listComposioTriggerDeploymentsUseCase: asClass(ListComposioTriggerDeploymentsUseCase).singleton(), - fetchComposioTriggerDeploymentUseCase: asClass(FetchComposioTriggerDeploymentUseCase).singleton(), - deleteComposioTriggerDeploymentUseCase: asClass(DeleteComposioTriggerDeploymentUseCase).singleton(), - createComposioTriggerDeploymentController: asClass(CreateComposioTriggerDeploymentController).singleton(), - deleteComposioTriggerDeploymentController: asClass(DeleteComposioTriggerDeploymentController).singleton(), - listComposioTriggerDeploymentsController: asClass(ListComposioTriggerDeploymentsController).singleton(), - fetchComposioTriggerDeploymentController: asClass(FetchComposioTriggerDeploymentController).singleton(), - listComposioTriggerTypesController: asClass(ListComposioTriggerTypesController).singleton(), - - // conversations - // --- - conversationsRepository: asClass(MongoDBConversationsRepository).singleton(), - createConversationUseCase: asClass(CreateConversationUseCase).singleton(), - createCachedTurnUseCase: asClass(CreateCachedTurnUseCase).singleton(), - fetchCachedTurnUseCase: asClass(FetchCachedTurnUseCase).singleton(), - runConversationTurnUseCase: asClass(RunConversationTurnUseCase).singleton(), - listConversationsUseCase: asClass(ListConversationsUseCase).singleton(), - fetchConversationUseCase: asClass(FetchConversationUseCase).singleton(), - createPlaygroundConversationController: asClass(CreatePlaygroundConversationController).singleton(), - createCachedTurnController: asClass(CreateCachedTurnController).singleton(), - runCachedTurnController: asClass(RunCachedTurnController).singleton(), - runTurnController: asClass(RunTurnController).singleton(), - listConversationsController: asClass(ListConversationsController).singleton(), - fetchConversationController: asClass(FetchConversationController).singleton(), - - // copilot - // --- - createCopilotCachedTurnUseCase: asClass(CreateCopilotCachedTurnUseCase).singleton(), - createCopilotCachedTurnController: asClass(CreateCopilotCachedTurnController).singleton(), - runCopilotCachedTurnUseCase: asClass(RunCopilotCachedTurnUseCase).singleton(), - runCopilotCachedTurnController: asClass(RunCopilotCachedTurnController).singleton(), - - // users - // --- - usersRepository: asClass(MongoDBUsersRepository).singleton(), + ...coreRegistrations, + ...projectRegistrations, + ...apiKeyRegistrations, + ...dataSourceRegistrations, + ...jobRegistrations, + ...composioRegistrations, + ...conversationRegistrations, + ...copilotRegistrations, + ...userRegistrations, }); diff --git a/apps/rowboat/di/modules/api-keys.ts b/apps/rowboat/di/modules/api-keys.ts new file mode 100644 index 00000000..b4a2e203 --- /dev/null +++ b/apps/rowboat/di/modules/api-keys.ts @@ -0,0 +1,19 @@ +import { asClass } from "awilix"; + +import { MongoDBApiKeysRepository } from "@/src/infrastructure/repositories/mongodb.api-keys.repository"; +import { CreateApiKeyUseCase } from "@/src/application/use-cases/api-keys/create-api-key.use-case"; +import { ListApiKeysUseCase } from "@/src/application/use-cases/api-keys/list-api-keys.use-case"; +import { DeleteApiKeyUseCase } from "@/src/application/use-cases/api-keys/delete-api-key.use-case"; +import { CreateApiKeyController } from "@/src/interface-adapters/controllers/api-keys/create-api-key.controller"; +import { ListApiKeysController } from "@/src/interface-adapters/controllers/api-keys/list-api-keys.controller"; +import { DeleteApiKeyController } from "@/src/interface-adapters/controllers/api-keys/delete-api-key.controller"; + +export const apiKeyRegistrations = { + apiKeysRepository: asClass(MongoDBApiKeysRepository).singleton(), + createApiKeyUseCase: asClass(CreateApiKeyUseCase).singleton(), + listApiKeysUseCase: asClass(ListApiKeysUseCase).singleton(), + deleteApiKeyUseCase: asClass(DeleteApiKeyUseCase).singleton(), + createApiKeyController: asClass(CreateApiKeyController).singleton(), + listApiKeysController: asClass(ListApiKeysController).singleton(), + deleteApiKeyController: asClass(DeleteApiKeyController).singleton(), +}; diff --git a/apps/rowboat/di/modules/composio.ts b/apps/rowboat/di/modules/composio.ts new file mode 100644 index 00000000..ca00bdd8 --- /dev/null +++ b/apps/rowboat/di/modules/composio.ts @@ -0,0 +1,31 @@ +import { asClass } from "awilix"; + +import { MongodbComposioTriggerDeploymentsRepository } from "@/src/infrastructure/repositories/mongodb.composio-trigger-deployments.repository"; +import { CreateComposioTriggerDeploymentUseCase } from "@/src/application/use-cases/composio-trigger-deployments/create-composio-trigger-deployment.use-case"; +import { ListComposioTriggerDeploymentsUseCase } from "@/src/application/use-cases/composio-trigger-deployments/list-composio-trigger-deployments.use-case"; +import { FetchComposioTriggerDeploymentUseCase } from "@/src/application/use-cases/composio-trigger-deployments/fetch-composio-trigger-deployment.use-case"; +import { DeleteComposioTriggerDeploymentUseCase } from "@/src/application/use-cases/composio-trigger-deployments/delete-composio-trigger-deployment.use-case"; +import { ListComposioTriggerTypesUseCase } from "@/src/application/use-cases/composio-trigger-deployments/list-composio-trigger-types.use-case"; +import { HandleCompsioWebhookRequestUseCase } from "@/src/application/use-cases/composio/webhook/handle-composio-webhook-request.use-case"; +import { CreateComposioTriggerDeploymentController } from "@/src/interface-adapters/controllers/composio-trigger-deployments/create-composio-trigger-deployment.controller"; +import { DeleteComposioTriggerDeploymentController } from "@/src/interface-adapters/controllers/composio-trigger-deployments/delete-composio-trigger-deployment.controller"; +import { ListComposioTriggerDeploymentsController } from "@/src/interface-adapters/controllers/composio-trigger-deployments/list-composio-trigger-deployments.controller"; +import { FetchComposioTriggerDeploymentController } from "@/src/interface-adapters/controllers/composio-trigger-deployments/fetch-composio-trigger-deployment.controller"; +import { ListComposioTriggerTypesController } from "@/src/interface-adapters/controllers/composio-trigger-deployments/list-composio-trigger-types.controller"; +import { HandleComposioWebhookRequestController } from "@/src/interface-adapters/controllers/composio/webhook/handle-composio-webhook-request.controller"; + +export const composioRegistrations = { + handleCompsioWebhookRequestUseCase: asClass(HandleCompsioWebhookRequestUseCase).singleton(), + handleComposioWebhookRequestController: asClass(HandleComposioWebhookRequestController).singleton(), + composioTriggerDeploymentsRepository: asClass(MongodbComposioTriggerDeploymentsRepository).singleton(), + listComposioTriggerTypesUseCase: asClass(ListComposioTriggerTypesUseCase).singleton(), + createComposioTriggerDeploymentUseCase: asClass(CreateComposioTriggerDeploymentUseCase).singleton(), + listComposioTriggerDeploymentsUseCase: asClass(ListComposioTriggerDeploymentsUseCase).singleton(), + fetchComposioTriggerDeploymentUseCase: asClass(FetchComposioTriggerDeploymentUseCase).singleton(), + deleteComposioTriggerDeploymentUseCase: asClass(DeleteComposioTriggerDeploymentUseCase).singleton(), + createComposioTriggerDeploymentController: asClass(CreateComposioTriggerDeploymentController).singleton(), + deleteComposioTriggerDeploymentController: asClass(DeleteComposioTriggerDeploymentController).singleton(), + listComposioTriggerDeploymentsController: asClass(ListComposioTriggerDeploymentsController).singleton(), + fetchComposioTriggerDeploymentController: asClass(FetchComposioTriggerDeploymentController).singleton(), + listComposioTriggerTypesController: asClass(ListComposioTriggerTypesController).singleton(), +}; diff --git a/apps/rowboat/di/modules/conversations.ts b/apps/rowboat/di/modules/conversations.ts new file mode 100644 index 00000000..c5e69eff --- /dev/null +++ b/apps/rowboat/di/modules/conversations.ts @@ -0,0 +1,31 @@ +import { asClass } from "awilix"; + +import { MongoDBConversationsRepository } from "@/src/infrastructure/repositories/mongodb.conversations.repository"; +import { CreateConversationUseCase } from "@/src/application/use-cases/conversations/create-conversation.use-case"; +import { CreateCachedTurnUseCase } from "@/src/application/use-cases/conversations/create-cached-turn.use-case"; +import { FetchCachedTurnUseCase } from "@/src/application/use-cases/conversations/fetch-cached-turn.use-case"; +import { RunConversationTurnUseCase } from "@/src/application/use-cases/conversations/run-conversation-turn.use-case"; +import { ListConversationsUseCase } from "@/src/application/use-cases/conversations/list-conversations.use-case"; +import { FetchConversationUseCase } from "@/src/application/use-cases/conversations/fetch-conversation.use-case"; +import { CreatePlaygroundConversationController } from "@/src/interface-adapters/controllers/conversations/create-playground-conversation.controller"; +import { CreateCachedTurnController } from "@/src/interface-adapters/controllers/conversations/create-cached-turn.controller"; +import { RunCachedTurnController } from "@/src/interface-adapters/controllers/conversations/run-cached-turn.controller"; +import { RunTurnController } from "@/src/interface-adapters/controllers/conversations/run-turn.controller"; +import { ListConversationsController } from "@/src/interface-adapters/controllers/conversations/list-conversations.controller"; +import { FetchConversationController } from "@/src/interface-adapters/controllers/conversations/fetch-conversation.controller"; + +export const conversationRegistrations = { + conversationsRepository: asClass(MongoDBConversationsRepository).singleton(), + createConversationUseCase: asClass(CreateConversationUseCase).singleton(), + createCachedTurnUseCase: asClass(CreateCachedTurnUseCase).singleton(), + fetchCachedTurnUseCase: asClass(FetchCachedTurnUseCase).singleton(), + runConversationTurnUseCase: asClass(RunConversationTurnUseCase).singleton(), + listConversationsUseCase: asClass(ListConversationsUseCase).singleton(), + fetchConversationUseCase: asClass(FetchConversationUseCase).singleton(), + createPlaygroundConversationController: asClass(CreatePlaygroundConversationController).singleton(), + createCachedTurnController: asClass(CreateCachedTurnController).singleton(), + runCachedTurnController: asClass(RunCachedTurnController).singleton(), + runTurnController: asClass(RunTurnController).singleton(), + listConversationsController: asClass(ListConversationsController).singleton(), + fetchConversationController: asClass(FetchConversationController).singleton(), +}; diff --git a/apps/rowboat/di/modules/copilot.ts b/apps/rowboat/di/modules/copilot.ts new file mode 100644 index 00000000..990bbea4 --- /dev/null +++ b/apps/rowboat/di/modules/copilot.ts @@ -0,0 +1,13 @@ +import { asClass } from "awilix"; + +import { CreateCopilotCachedTurnUseCase } from "@/src/application/use-cases/copilot/create-copilot-cached-turn.use-case"; +import { RunCopilotCachedTurnUseCase } from "@/src/application/use-cases/copilot/run-copilot-cached-turn.use-case"; +import { CreateCopilotCachedTurnController } from "@/src/interface-adapters/controllers/copilot/create-copilot-cached-turn.controller"; +import { RunCopilotCachedTurnController } from "@/src/interface-adapters/controllers/copilot/run-copilot-cached-turn.controller"; + +export const copilotRegistrations = { + createCopilotCachedTurnUseCase: asClass(CreateCopilotCachedTurnUseCase).singleton(), + createCopilotCachedTurnController: asClass(CreateCopilotCachedTurnController).singleton(), + runCopilotCachedTurnUseCase: asClass(RunCopilotCachedTurnUseCase).singleton(), + runCopilotCachedTurnController: asClass(RunCopilotCachedTurnController).singleton(), +}; diff --git a/apps/rowboat/di/modules/core.ts b/apps/rowboat/di/modules/core.ts new file mode 100644 index 00000000..b0325d53 --- /dev/null +++ b/apps/rowboat/di/modules/core.ts @@ -0,0 +1,21 @@ +import { asClass } from "awilix"; + +import { RedisPubSubService } from "@/src/infrastructure/services/redis.pub-sub.service"; +import { S3UploadsStorageService } from "@/src/infrastructure/services/s3.uploads-storage.service"; +import { LocalUploadsStorageService } from "@/src/infrastructure/services/local.uploads-storage.service"; +import { RedisCacheService } from "@/src/infrastructure/services/redis.cache.service"; +import { RedisUsageQuotaPolicy } from "@/src/infrastructure/policies/redis.usage-quota.policy"; +import { ProjectActionAuthorizationPolicy } from "@/src/application/policies/project-action-authorization.policy"; +import { JobsWorker } from "@/src/application/workers/jobs.worker"; +import { JobRulesWorker } from "@/src/application/workers/job-rules.worker"; + +export const coreRegistrations = { + jobsWorker: asClass(JobsWorker).singleton(), + jobRulesWorker: asClass(JobRulesWorker).singleton(), + cacheService: asClass(RedisCacheService).singleton(), + pubSubService: asClass(RedisPubSubService).singleton(), + s3UploadsStorageService: asClass(S3UploadsStorageService).singleton(), + localUploadsStorageService: asClass(LocalUploadsStorageService).singleton(), + usageQuotaPolicy: asClass(RedisUsageQuotaPolicy).singleton(), + projectActionAuthorizationPolicy: asClass(ProjectActionAuthorizationPolicy).singleton(), +}; diff --git a/apps/rowboat/di/modules/data-sources.ts b/apps/rowboat/di/modules/data-sources.ts new file mode 100644 index 00000000..9619d903 --- /dev/null +++ b/apps/rowboat/di/modules/data-sources.ts @@ -0,0 +1,57 @@ +import { asClass } from "awilix"; + +import { MongoDBDataSourcesRepository } from "@/src/infrastructure/repositories/mongodb.data-sources.repository"; +import { MongoDBDataSourceDocsRepository } from "@/src/infrastructure/repositories/mongodb.data-source-docs.repository"; +import { CreateDataSourceUseCase } from "@/src/application/use-cases/data-sources/create-data-source.use-case"; +import { FetchDataSourceUseCase } from "@/src/application/use-cases/data-sources/fetch-data-source.use-case"; +import { ListDataSourcesUseCase } from "@/src/application/use-cases/data-sources/list-data-sources.use-case"; +import { UpdateDataSourceUseCase } from "@/src/application/use-cases/data-sources/update-data-source.use-case"; +import { DeleteDataSourceUseCase } from "@/src/application/use-cases/data-sources/delete-data-source.use-case"; +import { ToggleDataSourceUseCase } from "@/src/application/use-cases/data-sources/toggle-data-source.use-case"; +import { AddDocsToDataSourceUseCase } from "@/src/application/use-cases/data-sources/add-docs-to-data-source.use-case"; +import { ListDocsInDataSourceUseCase } from "@/src/application/use-cases/data-sources/list-docs-in-data-source.use-case"; +import { DeleteDocFromDataSourceUseCase } from "@/src/application/use-cases/data-sources/delete-doc-from-data-source.use-case"; +import { RecrawlWebDataSourceUseCase } from "@/src/application/use-cases/data-sources/recrawl-web-data-source.use-case"; +import { GetUploadUrlsForFilesUseCase } from "@/src/application/use-cases/data-sources/get-upload-urls-for-files.use-case"; +import { GetDownloadUrlForFileUseCase } from "@/src/application/use-cases/data-sources/get-download-url-for-file.use-case"; +import { CreateDataSourceController } from "@/src/interface-adapters/controllers/data-sources/create-data-source.controller"; +import { FetchDataSourceController } from "@/src/interface-adapters/controllers/data-sources/fetch-data-source.controller"; +import { ListDataSourcesController } from "@/src/interface-adapters/controllers/data-sources/list-data-sources.controller"; +import { UpdateDataSourceController } from "@/src/interface-adapters/controllers/data-sources/update-data-source.controller"; +import { DeleteDataSourceController } from "@/src/interface-adapters/controllers/data-sources/delete-data-source.controller"; +import { ToggleDataSourceController } from "@/src/interface-adapters/controllers/data-sources/toggle-data-source.controller"; +import { AddDocsToDataSourceController } from "@/src/interface-adapters/controllers/data-sources/add-docs-to-data-source.controller"; +import { ListDocsInDataSourceController } from "@/src/interface-adapters/controllers/data-sources/list-docs-in-data-source.controller"; +import { DeleteDocFromDataSourceController } from "@/src/interface-adapters/controllers/data-sources/delete-doc-from-data-source.controller"; +import { RecrawlWebDataSourceController } from "@/src/interface-adapters/controllers/data-sources/recrawl-web-data-source.controller"; +import { GetUploadUrlsForFilesController } from "@/src/interface-adapters/controllers/data-sources/get-upload-urls-for-files.controller"; +import { GetDownloadUrlForFileController } from "@/src/interface-adapters/controllers/data-sources/get-download-url-for-file.controller"; + +export const dataSourceRegistrations = { + dataSourcesRepository: asClass(MongoDBDataSourcesRepository).singleton(), + dataSourceDocsRepository: asClass(MongoDBDataSourceDocsRepository).singleton(), + createDataSourceUseCase: asClass(CreateDataSourceUseCase).singleton(), + fetchDataSourceUseCase: asClass(FetchDataSourceUseCase).singleton(), + listDataSourcesUseCase: asClass(ListDataSourcesUseCase).singleton(), + updateDataSourceUseCase: asClass(UpdateDataSourceUseCase).singleton(), + deleteDataSourceUseCase: asClass(DeleteDataSourceUseCase).singleton(), + toggleDataSourceUseCase: asClass(ToggleDataSourceUseCase).singleton(), + createDataSourceController: asClass(CreateDataSourceController).singleton(), + fetchDataSourceController: asClass(FetchDataSourceController).singleton(), + listDataSourcesController: asClass(ListDataSourcesController).singleton(), + updateDataSourceController: asClass(UpdateDataSourceController).singleton(), + deleteDataSourceController: asClass(DeleteDataSourceController).singleton(), + toggleDataSourceController: asClass(ToggleDataSourceController).singleton(), + addDocsToDataSourceUseCase: asClass(AddDocsToDataSourceUseCase).singleton(), + listDocsInDataSourceUseCase: asClass(ListDocsInDataSourceUseCase).singleton(), + deleteDocFromDataSourceUseCase: asClass(DeleteDocFromDataSourceUseCase).singleton(), + recrawlWebDataSourceUseCase: asClass(RecrawlWebDataSourceUseCase).singleton(), + getUploadUrlsForFilesUseCase: asClass(GetUploadUrlsForFilesUseCase).singleton(), + getDownloadUrlForFileUseCase: asClass(GetDownloadUrlForFileUseCase).singleton(), + addDocsToDataSourceController: asClass(AddDocsToDataSourceController).singleton(), + listDocsInDataSourceController: asClass(ListDocsInDataSourceController).singleton(), + deleteDocFromDataSourceController: asClass(DeleteDocFromDataSourceController).singleton(), + recrawlWebDataSourceController: asClass(RecrawlWebDataSourceController).singleton(), + getUploadUrlsForFilesController: asClass(GetUploadUrlsForFilesController).singleton(), + getDownloadUrlForFileController: asClass(GetDownloadUrlForFileController).singleton(), +}; diff --git a/apps/rowboat/di/modules/jobs.ts b/apps/rowboat/di/modules/jobs.ts new file mode 100644 index 00000000..32acfee3 --- /dev/null +++ b/apps/rowboat/di/modules/jobs.ts @@ -0,0 +1,63 @@ +import { asClass } from "awilix"; + +import { MongoDBJobsRepository } from "@/src/infrastructure/repositories/mongodb.jobs.repository"; +import { MongoDBScheduledJobRulesRepository } from "@/src/infrastructure/repositories/mongodb.scheduled-job-rules.repository"; +import { MongoDBRecurringJobRulesRepository } from "@/src/infrastructure/repositories/mongodb.recurring-job-rules.repository"; +import { ListJobsUseCase } from "@/src/application/use-cases/jobs/list-jobs.use-case"; +import { FetchJobUseCase } from "@/src/application/use-cases/jobs/fetch-job.use-case"; +import { ListJobsController } from "@/src/interface-adapters/controllers/jobs/list-jobs.controller"; +import { FetchJobController } from "@/src/interface-adapters/controllers/jobs/fetch-job.controller"; +import { CreateScheduledJobRuleUseCase } from "@/src/application/use-cases/scheduled-job-rules/create-scheduled-job-rule.use-case"; +import { FetchScheduledJobRuleUseCase } from "@/src/application/use-cases/scheduled-job-rules/fetch-scheduled-job-rule.use-case"; +import { ListScheduledJobRulesUseCase } from "@/src/application/use-cases/scheduled-job-rules/list-scheduled-job-rules.use-case"; +import { DeleteScheduledJobRuleUseCase } from "@/src/application/use-cases/scheduled-job-rules/delete-scheduled-job-rule.use-case"; +import { UpdateScheduledJobRuleUseCase } from "@/src/application/use-cases/scheduled-job-rules/update-scheduled-job-rule.use-case"; +import { CreateScheduledJobRuleController } from "@/src/interface-adapters/controllers/scheduled-job-rules/create-scheduled-job-rule.controller"; +import { FetchScheduledJobRuleController } from "@/src/interface-adapters/controllers/scheduled-job-rules/fetch-scheduled-job-rule.controller"; +import { ListScheduledJobRulesController } from "@/src/interface-adapters/controllers/scheduled-job-rules/list-scheduled-job-rules.controller"; +import { DeleteScheduledJobRuleController } from "@/src/interface-adapters/controllers/scheduled-job-rules/delete-scheduled-job-rule.controller"; +import { UpdateScheduledJobRuleController } from "@/src/interface-adapters/controllers/scheduled-job-rules/update-scheduled-job-rule.controller"; +import { CreateRecurringJobRuleUseCase } from "@/src/application/use-cases/recurring-job-rules/create-recurring-job-rule.use-case"; +import { FetchRecurringJobRuleUseCase } from "@/src/application/use-cases/recurring-job-rules/fetch-recurring-job-rule.use-case"; +import { ListRecurringJobRulesUseCase } from "@/src/application/use-cases/recurring-job-rules/list-recurring-job-rules.use-case"; +import { ToggleRecurringJobRuleUseCase } from "@/src/application/use-cases/recurring-job-rules/toggle-recurring-job-rule.use-case"; +import { DeleteRecurringJobRuleUseCase } from "@/src/application/use-cases/recurring-job-rules/delete-recurring-job-rule.use-case"; +import { UpdateRecurringJobRuleUseCase } from "@/src/application/use-cases/recurring-job-rules/update-recurring-job-rule.use-case"; +import { CreateRecurringJobRuleController } from "@/src/interface-adapters/controllers/recurring-job-rules/create-recurring-job-rule.controller"; +import { FetchRecurringJobRuleController } from "@/src/interface-adapters/controllers/recurring-job-rules/fetch-recurring-job-rule.controller"; +import { ListRecurringJobRulesController } from "@/src/interface-adapters/controllers/recurring-job-rules/list-recurring-job-rules.controller"; +import { ToggleRecurringJobRuleController } from "@/src/interface-adapters/controllers/recurring-job-rules/toggle-recurring-job-rule.controller"; +import { DeleteRecurringJobRuleController } from "@/src/interface-adapters/controllers/recurring-job-rules/delete-recurring-job-rule.controller"; +import { UpdateRecurringJobRuleController } from "@/src/interface-adapters/controllers/recurring-job-rules/update-recurring-job-rule.controller"; + +export const jobRegistrations = { + jobsRepository: asClass(MongoDBJobsRepository).singleton(), + listJobsUseCase: asClass(ListJobsUseCase).singleton(), + listJobsController: asClass(ListJobsController).singleton(), + fetchJobUseCase: asClass(FetchJobUseCase).singleton(), + fetchJobController: asClass(FetchJobController).singleton(), + scheduledJobRulesRepository: asClass(MongoDBScheduledJobRulesRepository).singleton(), + createScheduledJobRuleUseCase: asClass(CreateScheduledJobRuleUseCase).singleton(), + fetchScheduledJobRuleUseCase: asClass(FetchScheduledJobRuleUseCase).singleton(), + listScheduledJobRulesUseCase: asClass(ListScheduledJobRulesUseCase).singleton(), + updateScheduledJobRuleUseCase: asClass(UpdateScheduledJobRuleUseCase).singleton(), + deleteScheduledJobRuleUseCase: asClass(DeleteScheduledJobRuleUseCase).singleton(), + createScheduledJobRuleController: asClass(CreateScheduledJobRuleController).singleton(), + fetchScheduledJobRuleController: asClass(FetchScheduledJobRuleController).singleton(), + listScheduledJobRulesController: asClass(ListScheduledJobRulesController).singleton(), + updateScheduledJobRuleController: asClass(UpdateScheduledJobRuleController).singleton(), + deleteScheduledJobRuleController: asClass(DeleteScheduledJobRuleController).singleton(), + recurringJobRulesRepository: asClass(MongoDBRecurringJobRulesRepository).singleton(), + createRecurringJobRuleUseCase: asClass(CreateRecurringJobRuleUseCase).singleton(), + fetchRecurringJobRuleUseCase: asClass(FetchRecurringJobRuleUseCase).singleton(), + listRecurringJobRulesUseCase: asClass(ListRecurringJobRulesUseCase).singleton(), + toggleRecurringJobRuleUseCase: asClass(ToggleRecurringJobRuleUseCase).singleton(), + updateRecurringJobRuleUseCase: asClass(UpdateRecurringJobRuleUseCase).singleton(), + deleteRecurringJobRuleUseCase: asClass(DeleteRecurringJobRuleUseCase).singleton(), + createRecurringJobRuleController: asClass(CreateRecurringJobRuleController).singleton(), + fetchRecurringJobRuleController: asClass(FetchRecurringJobRuleController).singleton(), + listRecurringJobRulesController: asClass(ListRecurringJobRulesController).singleton(), + toggleRecurringJobRuleController: asClass(ToggleRecurringJobRuleController).singleton(), + updateRecurringJobRuleController: asClass(UpdateRecurringJobRuleController).singleton(), + deleteRecurringJobRuleController: asClass(DeleteRecurringJobRuleController).singleton(), +}; diff --git a/apps/rowboat/di/modules/projects.ts b/apps/rowboat/di/modules/projects.ts new file mode 100644 index 00000000..cb060633 --- /dev/null +++ b/apps/rowboat/di/modules/projects.ts @@ -0,0 +1,85 @@ +import { asClass } from "awilix"; + +import { MongodbProjectsRepository } from "@/src/infrastructure/repositories/mongodb.projects.repository"; +import { MongoDBProjectMembersRepository } from "@/src/infrastructure/repositories/mongodb.project-members.repository"; +import { CreateProjectUseCase } from "@/src/application/use-cases/projects/create-project.use-case"; +import { DeleteComposioConnectedAccountUseCase } from "@/src/application/use-cases/projects/delete-composio-connected-account.use-case"; +import { CreateComposioManagedConnectedAccountUseCase } from "@/src/application/use-cases/projects/create-composio-managed-connected-account.use-case"; +import { CreateCustomConnectedAccountUseCase } from "@/src/application/use-cases/projects/create-custom-connected-account.use-case"; +import { SyncConnectedAccountUseCase } from "@/src/application/use-cases/projects/sync-connected-account.use-case"; +import { ListComposioToolkitsUseCase } from "@/src/application/use-cases/projects/list-composio-toolkits.use-case"; +import { GetComposioToolkitUseCase } from "@/src/application/use-cases/projects/get-composio-toolkit.use-case"; +import { ListComposioToolsUseCase } from "@/src/application/use-cases/projects/list-composio-tools.use-case"; +import { AddCustomMcpServerUseCase } from "@/src/application/use-cases/projects/add-custom-mcp-server.use-case"; +import { RemoveCustomMcpServerUseCase } from "@/src/application/use-cases/projects/remove-custom-mcp-server.use-case"; +import { DeleteProjectUseCase } from "@/src/application/use-cases/projects/delete-project.use-case"; +import { ListProjectsUseCase } from "@/src/application/use-cases/projects/list-projects.use-case"; +import { FetchProjectUseCase } from "@/src/application/use-cases/projects/fetch-project.use-case"; +import { RotateSecretUseCase } from "@/src/application/use-cases/projects/rotate-secret.use-case"; +import { UpdateWebhookUrlUseCase } from "@/src/application/use-cases/projects/update-webhook-url.use-case"; +import { UpdateProjectNameUseCase } from "@/src/application/use-cases/projects/update-project-name.use-case"; +import { UpdateDraftWorkflowUseCase } from "@/src/application/use-cases/projects/update-draft-workflow.use-case"; +import { UpdateLiveWorkflowUseCase } from "@/src/application/use-cases/projects/update-live-workflow.use-case"; +import { RevertToLiveWorkflowUseCase } from "@/src/application/use-cases/projects/revert-to-live-workflow.use-case"; +import { CreateProjectController } from "@/src/interface-adapters/controllers/projects/create-project.controller"; +import { DeleteComposioConnectedAccountController } from "@/src/interface-adapters/controllers/projects/delete-composio-connected-account.controller"; +import { CreateComposioManagedConnectedAccountController } from "@/src/interface-adapters/controllers/projects/create-composio-managed-connected-account.controller"; +import { CreateCustomConnectedAccountController } from "@/src/interface-adapters/controllers/projects/create-custom-connected-account.controller"; +import { SyncConnectedAccountController } from "@/src/interface-adapters/controllers/projects/sync-connected-account.controller"; +import { ListComposioToolkitsController } from "@/src/interface-adapters/controllers/projects/list-composio-toolkits.controller"; +import { GetComposioToolkitController } from "@/src/interface-adapters/controllers/projects/get-composio-toolkit.controller"; +import { ListComposioToolsController } from "@/src/interface-adapters/controllers/projects/list-composio-tools.controller"; +import { AddCustomMcpServerController } from "@/src/interface-adapters/controllers/projects/add-custom-mcp-server.controller"; +import { RemoveCustomMcpServerController } from "@/src/interface-adapters/controllers/projects/remove-custom-mcp-server.controller"; +import { DeleteProjectController } from "@/src/interface-adapters/controllers/projects/delete-project.controller"; +import { ListProjectsController } from "@/src/interface-adapters/controllers/projects/list-projects.controller"; +import { FetchProjectController } from "@/src/interface-adapters/controllers/projects/fetch-project.controller"; +import { RotateSecretController } from "@/src/interface-adapters/controllers/projects/rotate-secret.controller"; +import { UpdateWebhookUrlController } from "@/src/interface-adapters/controllers/projects/update-webhook-url.controller"; +import { UpdateProjectNameController } from "@/src/interface-adapters/controllers/projects/update-project-name.controller"; +import { UpdateDraftWorkflowController } from "@/src/interface-adapters/controllers/projects/update-draft-workflow.controller"; +import { UpdateLiveWorkflowController } from "@/src/interface-adapters/controllers/projects/update-live-workflow.controller"; +import { RevertToLiveWorkflowController } from "@/src/interface-adapters/controllers/projects/revert-to-live-workflow.controller"; + +export const projectRegistrations = { + projectsRepository: asClass(MongodbProjectsRepository).singleton(), + projectMembersRepository: asClass(MongoDBProjectMembersRepository).singleton(), + createProjectUseCase: asClass(CreateProjectUseCase).singleton(), + createProjectController: asClass(CreateProjectController).singleton(), + fetchProjectUseCase: asClass(FetchProjectUseCase).singleton(), + fetchProjectController: asClass(FetchProjectController).singleton(), + listProjectsUseCase: asClass(ListProjectsUseCase).singleton(), + listProjectsController: asClass(ListProjectsController).singleton(), + rotateSecretUseCase: asClass(RotateSecretUseCase).singleton(), + rotateSecretController: asClass(RotateSecretController).singleton(), + updateWebhookUrlUseCase: asClass(UpdateWebhookUrlUseCase).singleton(), + updateWebhookUrlController: asClass(UpdateWebhookUrlController).singleton(), + updateProjectNameUseCase: asClass(UpdateProjectNameUseCase).singleton(), + updateProjectNameController: asClass(UpdateProjectNameController).singleton(), + updateDraftWorkflowUseCase: asClass(UpdateDraftWorkflowUseCase).singleton(), + updateDraftWorkflowController: asClass(UpdateDraftWorkflowController).singleton(), + updateLiveWorkflowUseCase: asClass(UpdateLiveWorkflowUseCase).singleton(), + updateLiveWorkflowController: asClass(UpdateLiveWorkflowController).singleton(), + revertToLiveWorkflowUseCase: asClass(RevertToLiveWorkflowUseCase).singleton(), + revertToLiveWorkflowController: asClass(RevertToLiveWorkflowController).singleton(), + deleteProjectUseCase: asClass(DeleteProjectUseCase).singleton(), + deleteProjectController: asClass(DeleteProjectController).singleton(), + deleteComposioConnectedAccountController: asClass(DeleteComposioConnectedAccountController).singleton(), + deleteComposioConnectedAccountUseCase: asClass(DeleteComposioConnectedAccountUseCase).singleton(), + createComposioManagedConnectedAccountUseCase: asClass(CreateComposioManagedConnectedAccountUseCase).singleton(), + createComposioManagedConnectedAccountController: asClass(CreateComposioManagedConnectedAccountController).singleton(), + createCustomConnectedAccountUseCase: asClass(CreateCustomConnectedAccountUseCase).singleton(), + createCustomConnectedAccountController: asClass(CreateCustomConnectedAccountController).singleton(), + syncConnectedAccountUseCase: asClass(SyncConnectedAccountUseCase).singleton(), + syncConnectedAccountController: asClass(SyncConnectedAccountController).singleton(), + listComposioToolkitsUseCase: asClass(ListComposioToolkitsUseCase).singleton(), + listComposioToolkitsController: asClass(ListComposioToolkitsController).singleton(), + getComposioToolkitUseCase: asClass(GetComposioToolkitUseCase).singleton(), + getComposioToolkitController: asClass(GetComposioToolkitController).singleton(), + listComposioToolsUseCase: asClass(ListComposioToolsUseCase).singleton(), + listComposioToolsController: asClass(ListComposioToolsController).singleton(), + addCustomMcpServerUseCase: asClass(AddCustomMcpServerUseCase).singleton(), + addCustomMcpServerController: asClass(AddCustomMcpServerController).singleton(), + removeCustomMcpServerUseCase: asClass(RemoveCustomMcpServerUseCase).singleton(), + removeCustomMcpServerController: asClass(RemoveCustomMcpServerController).singleton(), +}; diff --git a/apps/rowboat/di/modules/users.ts b/apps/rowboat/di/modules/users.ts new file mode 100644 index 00000000..bbd74bee --- /dev/null +++ b/apps/rowboat/di/modules/users.ts @@ -0,0 +1,7 @@ +import { asClass } from "awilix"; + +import { MongoDBUsersRepository } from "@/src/infrastructure/repositories/mongodb.users.repository"; + +export const userRegistrations = { + usersRepository: asClass(MongoDBUsersRepository).singleton(), +}; diff --git a/apps/rowboat/package.json b/apps/rowboat/package.json index c6c5e2b5..3401f128 100644 --- a/apps/rowboat/package.json +++ b/apps/rowboat/package.json @@ -8,6 +8,9 @@ "build": "next build", "start": "next start", "lint": "next lint", + "typecheck": "tsc --noEmit", + "test": "node --import tsx --test --test-force-exit test/*.test.ts", + "verify": "npm run lint && npm run typecheck && npm test", "setupQdrant": "tsx app/scripts/setup_qdrant.ts", "deleteQdrant": "tsx app/scripts/delete_qdrant.ts", "rag-worker": "tsx app/scripts/rag-worker.ts", diff --git a/apps/rowboat/src/application/use-cases/projects/create-project.use-case.ts b/apps/rowboat/src/application/use-cases/projects/create-project.use-case.ts index aa14ab29..b2dd3130 100644 --- a/apps/rowboat/src/application/use-cases/projects/create-project.use-case.ts +++ b/apps/rowboat/src/application/use-cases/projects/create-project.use-case.ts @@ -4,7 +4,6 @@ import { IProjectsRepository } from "../../repositories/projects.repository.inte import { IUsageQuotaPolicy } from "../../policies/usage-quota.policy.interface"; import { BadRequestError, BillingError } from "@/src/entities/errors/common"; import { IProjectMembersRepository } from "../../repositories/project-members.repository.interface"; -import { authorize, getCustomerForUserId } from "@/app/lib/billing"; import { USE_BILLING } from "@/app/lib/feature_flags"; import { Project } from "@/src/entities/models/project"; import { Workflow } from "@/app/lib/types/workflow_types"; @@ -58,6 +57,8 @@ export class CreateProjectUseCase implements ICreateProjectUseCase { // Check billing auth if (USE_BILLING) { + const { authorize, getCustomerForUserId } = await import("@/app/lib/billing"); + // get billing customer id for project const customer = await getCustomerForUserId(request.userId); if (!customer) { diff --git a/apps/rowboat/test/chat-route.test.ts b/apps/rowboat/test/chat-route.test.ts new file mode 100644 index 00000000..2be66c45 --- /dev/null +++ b/apps/rowboat/test/chat-route.test.ts @@ -0,0 +1,107 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { handlePostChat } from "../app/api/v1/[projectId]/chat/route"; + +test("returns 400 for invalid request bodies", async () => { + const response = await handlePostChat( + new Request("http://localhost/api/v1/proj/chat", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ invalid: true }), + }), + { params: Promise.resolve({ projectId: "proj" }) }, + { + createLogger: () => ({ log() {} }), + resolveRunTurnController: () => ({ + execute: async () => { + throw new Error("should not be called"); + }, + } as any), + }, + ); + + assert.equal(response.status, 400); + assert.deepEqual(await response.json(), { error: "Invalid request" }); +}); + +test("returns json for non-streaming responses", async () => { + const calls: Array> = []; + const response = await handlePostChat( + new Request("http://localhost/api/v1/proj/chat", { + method: "POST", + headers: { + "content-type": "application/json", + Authorization: "Bearer test-key", + }, + body: JSON.stringify({ + messages: [{ role: "user", content: "hello" }], + stream: false, + }), + }), + { params: Promise.resolve({ projectId: "proj-1" }) }, + { + createLogger: () => ({ log() {} }), + resolveRunTurnController: () => ({ + execute: async (input: any) => { + calls.push(input); + return { + conversationId: "conv-1", + turn: { output: [{ role: "assistant", content: "hi" }] }, + }; + }, + } as any), + }, + ); + + assert.equal(response.status, 200); + assert.deepEqual(await response.json(), { + conversationId: "conv-1", + turn: { output: [{ role: "assistant", content: "hi" }] }, + }); + assert.deepEqual(calls, [{ + caller: "api", + apiKey: "test-key", + projectId: "proj-1", + input: { + messages: [{ role: "user", content: "hello" }], + mockTools: undefined, + }, + conversationId: undefined, + stream: false, + }]); +}); + +test("returns SSE for streaming responses", async () => { + async function* makeStream() { + yield { type: "text-delta", delta: "hello" }; + yield { type: "done" }; + } + + const response = await handlePostChat( + new Request("http://localhost/api/v1/proj/chat", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + messages: [{ role: "user", content: "stream please" }], + stream: true, + }), + }), + { params: Promise.resolve({ projectId: "proj-stream" }) }, + { + createLogger: () => ({ log() {} }), + resolveRunTurnController: () => ({ + execute: async () => ({ + conversationId: "conv-stream", + stream: makeStream(), + }), + } as any), + }, + ); + + assert.equal(response.status, 200); + assert.equal(response.headers.get("Content-Type"), "text/event-stream"); + const text = await response.text(); + assert.match(text, /event: message/); + assert.match(text, /"delta":"hello"/); +}); diff --git a/apps/rowboat/test/controller-usecase.test.ts b/apps/rowboat/test/controller-usecase.test.ts new file mode 100644 index 00000000..94c40dee --- /dev/null +++ b/apps/rowboat/test/controller-usecase.test.ts @@ -0,0 +1,364 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { BadRequestError, NotFoundError } from "../src/entities/errors/common"; +import { RunTurnController } from "../src/interface-adapters/controllers/conversations/run-turn.controller"; +import { CreateCachedTurnUseCase } from "../src/application/use-cases/conversations/create-cached-turn.use-case"; +import { CreateProjectController } from "../src/interface-adapters/controllers/projects/create-project.controller"; +import { CreateProjectUseCase } from "../src/application/use-cases/projects/create-project.use-case"; + +const isoNow = "2026-01-01T00:00:00.000Z"; + +function createUserMessage(content: string) { + return { role: "user" as const, content }; +} + +function createTurn() { + return { + id: "turn-1", + reason: { type: "chat" as const }, + input: { messages: [createUserMessage("hello")], mockTools: undefined }, + output: [{ role: "assistant" as const, content: "hi", agentName: null, responseType: "external" as const }], + createdAt: isoNow, + }; +} + +test("RunTurnController creates a conversation and returns the completed turn", async () => { + const createdRequests: unknown[] = []; + const runRequests: unknown[] = []; + const turn = createTurn(); + const controller = new RunTurnController({ + createConversationUseCase: { + execute: async (input: unknown) => { + createdRequests.push(input); + return { + id: "conv-1", + projectId: "proj-1", + workflow: { agents: [], prompts: [], tools: [], pipelines: [], startAgent: "", lastUpdatedAt: isoNow }, + reason: { type: "chat" as const }, + isLiveWorkflow: false, + createdAt: isoNow, + }; + }, + }, + runConversationTurnUseCase: { + execute: async function* (input: unknown) { + runRequests.push(input); + yield { type: "done" as const, conversationId: "conv-1", turn }; + }, + }, + }); + + const result = await controller.execute({ + caller: "user", + userId: "user-1", + projectId: "proj-1", + input: { messages: [createUserMessage("hello")] }, + stream: false, + }); + + assert.deepEqual(createdRequests, [{ + caller: "user", + userId: "user-1", + apiKey: undefined, + projectId: "proj-1", + reason: { type: "chat" }, + }]); + assert.deepEqual(runRequests, [{ + caller: "user", + userId: "user-1", + apiKey: undefined, + conversationId: "conv-1", + reason: { type: "chat" }, + input: { messages: [createUserMessage("hello")] }, + }]); + assert.deepEqual(result, { + conversationId: "conv-1", + turn, + }); +}); + +test("RunTurnController returns a stream without creating a conversation when one is supplied", async () => { + async function* makeStream() { + yield { type: "message" as const, data: createUserMessage("hello") }; + } + + const controller = new RunTurnController({ + createConversationUseCase: { + execute: async () => { + throw new Error("should not create a conversation"); + }, + }, + runConversationTurnUseCase: { + execute: () => makeStream(), + }, + }); + + const result = await controller.execute({ + caller: "api", + apiKey: "key-1", + projectId: "proj-1", + conversationId: "conv-existing", + input: { messages: [createUserMessage("hello")] }, + stream: true, + }); + + assert.equal(result.conversationId, "conv-existing"); + assert.ok("stream" in result); +}); + +test("CreateCachedTurnUseCase rejects missing conversations", async () => { + const useCase = new CreateCachedTurnUseCase({ + cacheService: { + get: async () => null, + set: async () => {}, + delete: async () => false, + }, + conversationsRepository: { + create: async () => { throw new Error("unused"); }, + fetch: async () => null, + list: async () => ({ items: [], nextCursor: null }), + addTurn: async () => { throw new Error("unused"); }, + deleteByProjectId: async () => {}, + }, + usageQuotaPolicy: { + assertAndConsumeProjectAction: async () => {}, + assertAndConsumeRunJobAction: async () => {}, + }, + projectActionAuthorizationPolicy: { + authorize: async () => {}, + }, + }); + + await assert.rejects( + () => useCase.execute({ + caller: "user", + userId: "user-1", + conversationId: "missing", + input: { messages: [createUserMessage("hello")] }, + }), + NotFoundError, + ); +}); + +test("CreateCachedTurnUseCase authorizes, consumes quota, and stores the payload", async () => { + const calls: Record[] = []; + const useCase = new CreateCachedTurnUseCase({ + cacheService: { + get: async () => null, + set: async (key, value, ttl) => { + calls.push({ key, value, ttl }); + }, + delete: async () => false, + }, + conversationsRepository: { + create: async () => { throw new Error("unused"); }, + fetch: async () => ({ + id: "conv-1", + projectId: "proj-1", + workflow: { agents: [], prompts: [], tools: [], pipelines: [], startAgent: "", lastUpdatedAt: isoNow }, + reason: { type: "chat" as const }, + isLiveWorkflow: false, + createdAt: isoNow, + }), + list: async () => ({ items: [], nextCursor: null }), + addTurn: async () => { throw new Error("unused"); }, + deleteByProjectId: async () => {}, + }, + usageQuotaPolicy: { + assertAndConsumeProjectAction: async (projectId) => { + calls.push({ quotaProjectId: projectId }); + }, + assertAndConsumeRunJobAction: async () => {}, + }, + projectActionAuthorizationPolicy: { + authorize: async (input) => { + calls.push({ authorization: input }); + }, + }, + }); + + const result = await useCase.execute({ + caller: "api", + apiKey: "api-key", + conversationId: "conv-1", + input: { messages: [createUserMessage("hello")], mockTools: { calc: "42" } }, + }); + + assert.equal(typeof result.key, "string"); + assert.ok(result.key.length > 0); + assert.deepEqual(calls[0], { + authorization: { + caller: "api", + userId: undefined, + apiKey: "api-key", + projectId: "proj-1", + }, + }); + assert.deepEqual(calls[1], { quotaProjectId: "proj-1" }); + assert.deepEqual(calls[2], { + key: `turn-${result.key}`, + value: JSON.stringify({ + conversationId: "conv-1", + input: { messages: [createUserMessage("hello")], mockTools: { calc: "42" } }, + }), + ttl: 600, + }); +}); + +test("CreateProjectController validates input before delegating", async () => { + const calls: unknown[] = []; + const controller = new CreateProjectController({ + createProjectUseCase: { + execute: async (input: unknown) => { + calls.push(input); + return { + id: "550e8400-e29b-41d4-a716-446655440000", + name: "Assistant 1", + createdAt: isoNow, + createdByUserId: "user-1", + secret: "secret", + draftWorkflow: { agents: [], prompts: [], tools: [], pipelines: [], startAgent: "", lastUpdatedAt: isoNow }, + liveWorkflow: { agents: [], prompts: [], tools: [], pipelines: [], startAgent: "", lastUpdatedAt: isoNow }, + }; + }, + }, + }); + + await assert.rejects( + () => controller.execute({ userId: "user-1" } as any), + BadRequestError, + ); + + const result = await controller.execute({ + userId: "user-1", + data: { + mode: { template: "default" }, + }, + }); + + assert.equal(result.name, "Assistant 1"); + assert.deepEqual(calls, [{ + userId: "user-1", + data: { + mode: { template: "default" }, + }, + }]); +}); + +test("CreateProjectUseCase builds a default template project and membership", async () => { + const createCalls: unknown[] = []; + const memberCalls: unknown[] = []; + const quotaCalls: string[] = []; + + const useCase = new CreateProjectUseCase({ + projectsRepository: { + countCreatedProjects: async () => 0, + create: async (data: any) => { + createCalls.push(data); + return { + id: "550e8400-e29b-41d4-a716-446655440000", + name: data.name, + createdAt: isoNow, + createdByUserId: data.createdByUserId, + secret: data.secret, + draftWorkflow: { ...data.workflow, lastUpdatedAt: isoNow }, + liveWorkflow: { ...data.workflow, lastUpdatedAt: isoNow }, + }; + }, + fetch: async () => null, + listProjects: async () => ({ items: [], nextCursor: null }), + addComposioConnectedAccount: async () => { throw new Error("unused"); }, + deleteComposioConnectedAccount: async () => false, + addCustomMcpServer: async () => { throw new Error("unused"); }, + deleteCustomMcpServer: async () => false, + updateSecret: async () => { throw new Error("unused"); }, + updateWebhookUrl: async () => { throw new Error("unused"); }, + updateName: async () => { throw new Error("unused"); }, + updateDraftWorkflow: async () => { throw new Error("unused"); }, + updateLiveWorkflow: async () => { throw new Error("unused"); }, + delete: async () => false, + }, + projectMembersRepository: { + create: async (data) => { + memberCalls.push(data); + return { + id: "member-1", + userId: data.userId, + projectId: data.projectId, + createdAt: isoNow, + lastUpdatedAt: isoNow, + }; + }, + findByUserId: async () => ({ items: [], nextCursor: null }), + deleteByProjectId: async () => {}, + exists: async () => true, + }, + usageQuotaPolicy: { + assertAndConsumeProjectAction: async (projectId) => { + quotaCalls.push(projectId); + }, + assertAndConsumeRunJobAction: async () => {}, + }, + }); + + const result = await useCase.execute({ + userId: "user-1", + data: { + mode: { template: "default" }, + }, + }); + + assert.equal(result.name, "Assistant 1"); + assert.equal(createCalls.length, 1); + assert.deepEqual(memberCalls, [{ + projectId: "550e8400-e29b-41d4-a716-446655440000", + userId: "user-1", + }]); + assert.deepEqual(quotaCalls, ["550e8400-e29b-41d4-a716-446655440000"]); + assert.equal((createCalls[0] as any).name, "Assistant 1"); + assert.equal((createCalls[0] as any).createdByUserId, "user-1"); + assert.equal(typeof (createCalls[0] as any).secret, "string"); + assert.equal((createCalls[0] as any).workflow.startAgent, ""); +}); + +test("CreateProjectUseCase rejects invalid workflow JSON", async () => { + const useCase = new CreateProjectUseCase({ + projectsRepository: { + countCreatedProjects: async () => 0, + create: async () => { throw new Error("should not create"); }, + fetch: async () => null, + listProjects: async () => ({ items: [], nextCursor: null }), + addComposioConnectedAccount: async () => { throw new Error("unused"); }, + deleteComposioConnectedAccount: async () => false, + addCustomMcpServer: async () => { throw new Error("unused"); }, + deleteCustomMcpServer: async () => false, + updateSecret: async () => { throw new Error("unused"); }, + updateWebhookUrl: async () => { throw new Error("unused"); }, + updateName: async () => { throw new Error("unused"); }, + updateDraftWorkflow: async () => { throw new Error("unused"); }, + updateLiveWorkflow: async () => { throw new Error("unused"); }, + delete: async () => false, + }, + projectMembersRepository: { + create: async () => { throw new Error("unused"); }, + findByUserId: async () => ({ items: [], nextCursor: null }), + deleteByProjectId: async () => {}, + exists: async () => true, + }, + usageQuotaPolicy: { + assertAndConsumeProjectAction: async () => {}, + assertAndConsumeRunJobAction: async () => {}, + }, + }); + + await assert.rejects( + () => useCase.execute({ + userId: "user-1", + data: { + mode: { workflowJson: "{" }, + }, + }), + BadRequestError, + ); +}); diff --git a/apps/rowboat/test/workers.test.ts b/apps/rowboat/test/workers.test.ts new file mode 100644 index 00000000..5f29665a --- /dev/null +++ b/apps/rowboat/test/workers.test.ts @@ -0,0 +1,398 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { QuotaExceededError } from "../src/entities/errors/common"; +import { JobsWorker } from "../src/application/workers/jobs.worker"; +import { JobRulesWorker } from "../src/application/workers/job-rules.worker"; + +const isoNow = "2026-01-01T00:00:00.000Z"; + +function userMessage(content: string) { + return { role: "user" as const, content }; +} + +function createJob(id: string) { + return { + id, + reason: { type: "scheduled_job_rule" as const, ruleId: "rule-1" }, + projectId: "proj-1", + input: { messages: [userMessage("hello")] }, + workerId: null, + lastWorkerId: null, + status: "pending" as const, + createdAt: isoNow, + }; +} + +async function waitFor(condition: () => boolean, timeoutMs: number = 100): Promise { + const start = Date.now(); + while (!condition()) { + if (Date.now() - start > timeoutMs) { + throw new Error("Timed out waiting for async worker state"); + } + await new Promise((resolve) => setTimeout(resolve, 5)); + } +} + +test("JobsWorker processes subscription-delivered jobs end to end", async () => { + const updates: Array<{ id: string; data: unknown }> = []; + const released: string[] = []; + const conversations: unknown[] = []; + const runInputs: unknown[] = []; + let subscriptionHandler: ((message: string) => void) | null = null; + + const worker = new JobsWorker({ + jobsRepository: { + create: async () => { throw new Error("unused"); }, + fetch: async () => null, + poll: async () => null, + lock: async (id: string) => createJob(id), + update: async (id: string, data: any) => { + updates.push({ id, data }); + return { ...createJob(id), ...data }; + }, + release: async (id: string) => { + released.push(id); + }, + list: async () => ({ items: [], nextCursor: null }), + deleteByProjectId: async () => {}, + }, + projectsRepository: { + create: async () => { throw new Error("unused"); }, + fetch: async () => ({ + id: "proj-1", + name: "Project", + createdAt: isoNow, + createdByUserId: "user-1", + secret: "secret", + draftWorkflow: { agents: [], prompts: [], tools: [], pipelines: [], startAgent: "", lastUpdatedAt: isoNow }, + liveWorkflow: { agents: [], prompts: [], tools: [], pipelines: [], startAgent: "", lastUpdatedAt: isoNow }, + }), + countCreatedProjects: async () => 0, + listProjects: async () => ({ items: [], nextCursor: null }), + addComposioConnectedAccount: async () => { throw new Error("unused"); }, + deleteComposioConnectedAccount: async () => false, + addCustomMcpServer: async () => { throw new Error("unused"); }, + deleteCustomMcpServer: async () => false, + updateSecret: async () => { throw new Error("unused"); }, + updateWebhookUrl: async () => { throw new Error("unused"); }, + updateName: async () => { throw new Error("unused"); }, + updateDraftWorkflow: async () => { throw new Error("unused"); }, + updateLiveWorkflow: async () => { throw new Error("unused"); }, + delete: async () => false, + }, + createConversationUseCase: { + execute: async (input: unknown) => { + conversations.push(input); + return { + id: "conv-1", + projectId: "proj-1", + workflow: { agents: [], prompts: [], tools: [], pipelines: [], startAgent: "", lastUpdatedAt: isoNow }, + reason: { type: "job", jobId: "job-1" }, + isLiveWorkflow: true, + createdAt: isoNow, + }; + }, + }, + runConversationTurnUseCase: { + execute: async function* (input: unknown) { + runInputs.push(input); + yield { + type: "done" as const, + conversationId: "conv-1", + turn: { + id: "turn-1", + reason: { type: "job" as const, jobId: "job-1" }, + input: { messages: [userMessage("hello")] }, + output: [{ role: "assistant" as const, content: "done", agentName: null, responseType: "external" as const }], + createdAt: isoNow, + }, + }; + }, + }, + pubSubService: { + publish: async () => {}, + subscribe: async (_channel: string, handler: (message: string) => void) => { + subscriptionHandler = handler; + return { unsubscribe: async () => {} }; + }, + }, + usageQuotaPolicy: { + assertAndConsumeProjectAction: async () => {}, + assertAndConsumeRunJobAction: async () => {}, + }, + }); + + await worker.run(); + if (!subscriptionHandler) { + throw new Error("subscription handler was not registered"); + } + const handler = subscriptionHandler as (message: string) => void; + handler("job-1"); + await waitFor(() => updates.length === 1 && runInputs.length === 1 && released.length === 1); + await worker.stop(); + + assert.deepEqual(conversations, [{ + caller: "job_worker", + projectId: "proj-1", + reason: { type: "job", jobId: "job-1" }, + isLiveWorkflow: true, + }]); + assert.equal(runInputs.length, 1); + assert.equal((runInputs[0] as any).caller, "job_worker"); + assert.equal((runInputs[0] as any).conversationId, "conv-1"); + assert.deepEqual((runInputs[0] as any).reason, { type: "job", jobId: "job-1" }); + assert.deepEqual((runInputs[0] as any).input, { messages: [userMessage("hello")] }); + assert.deepEqual(updates, [{ + id: "job-1", + data: { + status: "completed", + output: { + conversationId: "conv-1", + turnId: "turn-1", + }, + }, + }]); + assert.deepEqual(released, ["job-1"]); +}); + +test("JobsWorker marks quota-exceeded jobs as failed with a user-facing error", async () => { + const updates: Array<{ id: string; data: unknown }> = []; + const released: string[] = []; + + const worker = new JobsWorker({ + jobsRepository: { + create: async () => { throw new Error("unused"); }, + fetch: async () => null, + poll: async () => null, + lock: async (id: string) => createJob(id), + update: async (id: string, data: any) => { + updates.push({ id, data }); + return { ...createJob(id), ...data }; + }, + release: async (id: string) => { + released.push(id); + }, + list: async () => ({ items: [], nextCursor: null }), + deleteByProjectId: async () => {}, + }, + projectsRepository: { + create: async () => { throw new Error("unused"); }, + fetch: async () => ({ + id: "proj-1", + name: "Project", + createdAt: isoNow, + createdByUserId: "user-1", + secret: "secret", + draftWorkflow: { agents: [], prompts: [], tools: [], pipelines: [], startAgent: "", lastUpdatedAt: isoNow }, + liveWorkflow: { agents: [], prompts: [], tools: [], pipelines: [], startAgent: "", lastUpdatedAt: isoNow }, + }), + countCreatedProjects: async () => 0, + listProjects: async () => ({ items: [], nextCursor: null }), + addComposioConnectedAccount: async () => { throw new Error("unused"); }, + deleteComposioConnectedAccount: async () => false, + addCustomMcpServer: async () => { throw new Error("unused"); }, + deleteCustomMcpServer: async () => false, + updateSecret: async () => { throw new Error("unused"); }, + updateWebhookUrl: async () => { throw new Error("unused"); }, + updateName: async () => { throw new Error("unused"); }, + updateDraftWorkflow: async () => { throw new Error("unused"); }, + updateLiveWorkflow: async () => { throw new Error("unused"); }, + delete: async () => false, + }, + createConversationUseCase: { + execute: async () => { throw new Error("should not create conversation"); }, + }, + runConversationTurnUseCase: { + execute: async function* () { + yield* [] as Array<{ type: "error"; error: string }>; + throw new Error("should not run"); + }, + }, + pubSubService: { + publish: async () => {}, + subscribe: async () => ({ unsubscribe: async () => {} }), + }, + usageQuotaPolicy: { + assertAndConsumeProjectAction: async () => {}, + assertAndConsumeRunJobAction: async () => { + throw new QuotaExceededError("No credits left"); + }, + }, + }); + + await (worker as any).processJob(createJob("job-2")); + + assert.deepEqual(updates, [{ + id: "job-2", + data: { + status: "failed", + output: { + error: "No credits left", + }, + }, + }]); + assert.deepEqual(released, ["job-2"]); +}); + +test("JobRulesWorker creates jobs from scheduled and recurring rules and publishes them", async () => { + const createdJobs: unknown[] = []; + const published: Array<{ channel: string; message: string }> = []; + const scheduledUpdates: Array<{ id: string; data: unknown }> = []; + const scheduledReleases: string[] = []; + const recurringReleases: string[] = []; + + const worker = new JobRulesWorker({ + scheduledJobRulesRepository: { + create: async () => { throw new Error("unused"); }, + fetch: async () => null, + poll: async () => null, + update: async (id: string, data: unknown) => { + scheduledUpdates.push({ id, data }); + return { + id, + projectId: "proj-1", + input: { messages: [userMessage("hello")] }, + nextRunAt: isoNow, + workerId: null, + lastWorkerId: null, + status: "triggered" as const, + output: (data as any).output, + createdAt: isoNow, + }; + }, + updateRule: async () => { throw new Error("unused"); }, + release: async (id: string) => { + scheduledReleases.push(id); + return { + id, + projectId: "proj-1", + input: { messages: [userMessage("hello")] }, + nextRunAt: isoNow, + workerId: null, + lastWorkerId: null, + status: "triggered" as const, + createdAt: isoNow, + }; + }, + list: async () => ({ items: [], nextCursor: null }), + delete: async () => false, + deleteByProjectId: async () => {}, + }, + recurringJobRulesRepository: { + create: async () => { throw new Error("unused"); }, + fetch: async () => null, + poll: async () => null, + release: async (id: string) => { + recurringReleases.push(id); + return { + id, + projectId: "proj-1", + input: { messages: [userMessage("hello")] }, + cron: "* * * * *", + nextRunAt: isoNow, + workerId: null, + lastWorkerId: null, + disabled: false, + createdAt: isoNow, + }; + }, + list: async () => ({ items: [], nextCursor: null }), + toggle: async () => { throw new Error("unused"); }, + update: async () => { throw new Error("unused"); }, + delete: async () => false, + deleteByProjectId: async () => {}, + }, + jobsRepository: { + create: async (data: any) => { + createdJobs.push(data); + return { + id: `job-${createdJobs.length}`, + ...data, + workerId: null, + lastWorkerId: null, + status: "pending" as const, + createdAt: isoNow, + }; + }, + fetch: async () => null, + poll: async () => null, + lock: async () => { throw new Error("unused"); }, + update: async () => { throw new Error("unused"); }, + release: async () => {}, + list: async () => ({ items: [], nextCursor: null }), + deleteByProjectId: async () => {}, + }, + projectsRepository: { + create: async () => { throw new Error("unused"); }, + fetch: async () => null, + countCreatedProjects: async () => 0, + listProjects: async () => ({ items: [], nextCursor: null }), + addComposioConnectedAccount: async () => { throw new Error("unused"); }, + deleteComposioConnectedAccount: async () => false, + addCustomMcpServer: async () => { throw new Error("unused"); }, + deleteCustomMcpServer: async () => false, + updateSecret: async () => { throw new Error("unused"); }, + updateWebhookUrl: async () => { throw new Error("unused"); }, + updateName: async () => { throw new Error("unused"); }, + updateDraftWorkflow: async () => { throw new Error("unused"); }, + updateLiveWorkflow: async () => { throw new Error("unused"); }, + delete: async () => false, + }, + pubSubService: { + publish: async (channel: string, message: string) => { + published.push({ channel, message }); + }, + subscribe: async () => ({ unsubscribe: async () => {} }), + }, + }); + + await (worker as any).processScheduledRule({ + id: "scheduled-1", + projectId: "proj-1", + input: { messages: [userMessage("scheduled")] }, + nextRunAt: isoNow, + workerId: null, + lastWorkerId: null, + status: "pending", + createdAt: isoNow, + }); + + await (worker as any).processRecurringRule({ + id: "recurring-1", + projectId: "proj-1", + input: { messages: [userMessage("recurring")] }, + cron: "* * * * *", + nextRunAt: isoNow, + workerId: null, + lastWorkerId: null, + disabled: false, + createdAt: isoNow, + }); + + assert.deepEqual(createdJobs, [ + { + reason: { type: "scheduled_job_rule", ruleId: "scheduled-1" }, + projectId: "proj-1", + input: { messages: [userMessage("scheduled")] }, + }, + { + reason: { type: "recurring_job_rule", ruleId: "recurring-1" }, + projectId: "proj-1", + input: { messages: [userMessage("recurring")] }, + }, + ]); + assert.deepEqual(published, [ + { channel: "new_jobs", message: "job-1" }, + { channel: "new_jobs", message: "job-2" }, + ]); + assert.deepEqual(scheduledUpdates, [{ + id: "scheduled-1", + data: { + output: { jobId: "job-1" }, + status: "triggered", + }, + }]); + assert.deepEqual(scheduledReleases, ["scheduled-1"]); + assert.deepEqual(recurringReleases, ["recurring-1"]); +}); diff --git a/apps/rowboatx/README.md b/apps/rowboatx/README.md index e215bc4c..3b13ca83 100644 --- a/apps/rowboatx/README.md +++ b/apps/rowboatx/README.md @@ -1,36 +1,38 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +# RowboatX Frontend -## Getting Started +`apps/rowboatx` is the newer frontend for the local Rowboat runtime. It is a Next.js UI that renders chat, artifacts, tools, and resource views on top of a runtime provided by `apps/cli` or another host shell. -First, run the development server: +## What Lives Here + +- Main chat/dashboard page in `app/page.tsx` +- Shared UI primitives and AI-oriented components under `components/` +- Static export configuration in `next.config.ts` + +## Runtime Expectations + +This frontend is not self-contained. It expects one of the following to exist at runtime: + +- `window.config.apiBase` for direct backend requests +- `/api/stream` for SSE run events +- `/api/rowboat/*` endpoints for local resource browsing and editing + +In practice, this means the UI is meant to be served by a shell or proxy that also provides the local runtime APIs. + +## Local Development ```bash +npm install npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev ``` -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +Build the static export: -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +```bash +npm run build +``` -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. +## Notes For Contributors -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. +- Changes here should preserve the assumption that the backend lives outside this app. +- If you add a new runtime endpoint, document the expected contract in the host surface that provides it. +- For repo-level ownership and status, see the root `ARCHITECTURE.md`. diff --git a/apps/x/README.md b/apps/x/README.md new file mode 100644 index 00000000..2843ac81 --- /dev/null +++ b/apps/x/README.md @@ -0,0 +1,54 @@ +# Rowboat Desktop App + +`apps/x` is the primary local-first Rowboat desktop product. It is a nested `pnpm` workspace that packages the Electron app, renderer, preload bridge, shared contracts, and core knowledge/runtime logic. + +## Workspace Layout + +- `apps/main` - Electron main process +- `apps/renderer` - React and Vite renderer UI +- `apps/preload` - validated IPC bridge +- `packages/shared` - shared schemas and IPC contracts +- `packages/core` - workspace, knowledge graph, integrations, agents, and background services + +## Local Development + +Install dependencies: + +```bash +pnpm install +``` + +Build shared dependencies used by the app: + +```bash +npm run deps +``` + +Run the desktop app in development: + +```bash +npm run dev +``` + +Useful verification commands: + +```bash +npm run lint +npm run typecheck +npm run test +npm run verify +``` + +## Build Notes + +- `npm run deps` builds `shared`, `core`, and `preload` +- `apps/main` bundles the Electron main process with esbuild for packaging +- The renderer uses Vite and hot reloads during development + +## Local Data Model + +- Default work directory: `~/.rowboat` +- Knowledge is stored as Markdown +- Knowledge note history is Git-backed for transparent local versioning + +If you are new to the repo, read the root `ARCHITECTURE.md` before making cross-surface changes. diff --git a/apps/x/apps/main/src/composio-handler.ts b/apps/x/apps/main/src/composio-handler.ts index 111eb5a5..59532373 100644 --- a/apps/x/apps/main/src/composio-handler.ts +++ b/apps/x/apps/main/src/composio-handler.ts @@ -151,7 +151,7 @@ export async function initiateConnection(toolkitSlug: string): Promise<{ // Set up callback server const timeoutRef: { current: NodeJS.Timeout | null } = { current: null }; let callbackHandled = false; - const { server } = await createAuthServer(8081, async (_callbackUrl) => { + const { server } = await createAuthServer(8081, async () => { // Guard against duplicate callbacks (browser may send multiple requests) if (callbackHandled) return; callbackHandled = true; diff --git a/apps/x/apps/renderer/README.md b/apps/x/apps/renderer/README.md index d2e77611..96e189f3 100644 --- a/apps/x/apps/renderer/README.md +++ b/apps/x/apps/renderer/README.md @@ -1,73 +1,25 @@ -# React + TypeScript + Vite +# Desktop Renderer -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. +This package contains the React and Vite renderer for the Electron desktop app in `apps/x`. -Currently, two official plugins are available: +## Responsibilities -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh +- Render the desktop UI +- Talk to the Electron preload bridge instead of Node APIs directly +- Display workspace, chat, notes, graph, and other local-first product surfaces -## React Compiler +## Commands -The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). - -## Expanding the ESLint configuration - -If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: - -```js -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... - - // Remove tseslint.configs.recommended and replace with this - tseslint.configs.recommendedTypeChecked, - // Alternatively, use this for stricter rules - tseslint.configs.strictTypeChecked, - // Optionally, add this for stylistic rules - tseslint.configs.stylisticTypeChecked, - - // Other configs... - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]) +```bash +npm run dev +npm run build +npm run lint ``` -You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: +Run these from `apps/x/apps/renderer` when working only on the renderer, or use `apps/x` and run `npm run dev` to launch the full desktop stack. -```js -// eslint.config.js -import reactX from 'eslint-plugin-react-x' -import reactDom from 'eslint-plugin-react-dom' +## Constraints -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... - // Enable lint rules for React - reactX.configs['recommended-typescript'], - // Enable lint rules for React DOM - reactDom.configs.recommended, - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]) -``` +- Assume `nodeIntegration` is disabled in the renderer +- Use the preload IPC bridge for privileged operations +- Keep shared contracts in `packages/shared` when a renderer and the main process need the same schema diff --git a/apps/x/package.json b/apps/x/package.json index 5d875653..83b6ddc4 100644 --- a/apps/x/package.json +++ b/apps/x/package.json @@ -7,10 +7,19 @@ "dev": "npm run deps && concurrently -k \"npm:renderer\" \"npm:main\"", "renderer": "cd apps/renderer && npm run dev", "shared": "cd packages/shared && npm run build", + "shared:typecheck": "cd packages/shared && npx tsc --noEmit", "core": "cd packages/core && npm run build", + "core:typecheck": "cd packages/core && npx tsc --noEmit", + "core:test": "cd packages/core && npm test", "preload": "cd apps/preload && npm run build", + "preload:typecheck": "cd apps/preload && npx tsc --noEmit", "deps": "npm run shared && npm run core && npm run preload", "main": "wait-on http://localhost:5173 && cd apps/main && npm run build && npm run start", + "main:typecheck": "cd apps/main && npx tsc --noEmit", + "renderer:typecheck": "cd apps/renderer && npx tsc -p tsconfig.app.json --noEmit && npx tsc -p tsconfig.node.json --noEmit", + "typecheck": "npm run shared:typecheck && npm run core:typecheck && npm run deps && npm run preload:typecheck && npm run main:typecheck && npm run renderer:typecheck", + "test": "npm run shared && npm run core:test", + "verify": "npm run lint && npm run typecheck && npm run test", "lint": "eslint .", "lint:fix": "eslint . --fix" }, @@ -26,4 +35,4 @@ "typescript-eslint": "^8.50.1", "wait-on": "^9.0.3" } -} \ No newline at end of file +} diff --git a/apps/x/packages/core/package.json b/apps/x/packages/core/package.json index 72d6f079..62da4712 100644 --- a/apps/x/packages/core/package.json +++ b/apps/x/packages/core/package.json @@ -6,7 +6,8 @@ "types": "./dist/index.d.ts", "scripts": { "build": "rm -rf dist && tsc", - "dev": "tsc -w" + "dev": "tsc -w", + "test": "npm run build && node ./test/run-tests.mjs" }, "dependencies": { "@ai-sdk/anthropic": "^2.0.63", diff --git a/apps/x/packages/core/src/application/lib/builtin-tools.ts b/apps/x/packages/core/src/application/lib/builtin-tools.ts index f4fe42d6..c63e0542 100644 --- a/apps/x/packages/core/src/application/lib/builtin-tools.ts +++ b/apps/x/packages/core/src/application/lib/builtin-tools.ts @@ -561,7 +561,7 @@ export const BuiltinTools: z.infer = { count: matches.length, tool: 'ripgrep', }; - } catch (rgError) { + } catch { // Fallback to basic grep if ripgrep not available or failed const grepArgs = [ '-rn', diff --git a/apps/x/packages/core/src/config/config.ts b/apps/x/packages/core/src/config/config.ts index 3d320172..f3fe556b 100644 --- a/apps/x/packages/core/src/config/config.ts +++ b/apps/x/packages/core/src/config/config.ts @@ -1,7 +1,6 @@ import path from "path"; import fs from "fs"; import { homedir } from "os"; -import { fileURLToPath } from "url"; function resolveWorkDir(): string { const configured = process.env.ROWBOAT_WORKDIR; @@ -23,10 +22,6 @@ function resolveWorkDir(): string { // Normalize to an absolute path so workspace boundary checks behave consistently. export const WorkDir = resolveWorkDir(); -// Get the directory of this file (for locating bundled assets) -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - function ensureDirs() { const ensure = (p: string) => { if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true }); }; ensure(WorkDir); diff --git a/apps/x/packages/core/src/config/strictness_analyzer.ts b/apps/x/packages/core/src/config/strictness_analyzer.ts index d7516ccb..1b392083 100644 --- a/apps/x/packages/core/src/config/strictness_analyzer.ts +++ b/apps/x/packages/core/src/config/strictness_analyzer.ts @@ -390,9 +390,6 @@ export function analyzeEmailsAndRecommend(): AnalysisResult { let reason: string; const totalHumanSenders = lowWouldCreate; - const noiseRatio = uniqueSenders.size > 0 - ? (newsletterSenders.size + automatedSenders.size) / uniqueSenders.size - : 0; const consumerRatio = totalHumanSenders > 0 ? consumerServiceSenders.size / totalHumanSenders : 0; diff --git a/apps/x/packages/core/src/knowledge/agent_notes.ts b/apps/x/packages/core/src/knowledge/agent_notes.ts index 5ec3e801..53b48c68 100644 --- a/apps/x/packages/core/src/knowledge/agent_notes.ts +++ b/apps/x/packages/core/src/knowledge/agent_notes.ts @@ -193,13 +193,16 @@ function extractConversationMessages(runFilePath: string): { role: string; text: // --- Wait for agent run completion --- async function waitForRunCompletion(runId: string): Promise { - return new Promise(async (resolve) => { - const unsubscribe = await bus.subscribe('*', async (event) => { + return new Promise((resolve, reject) => { + let unsubscribe = () => {}; + bus.subscribe('*', async (event) => { if (event.type === 'run-processing-end' && event.runId === runId) { unsubscribe(); resolve(); } - }); + }).then((nextUnsubscribe) => { + unsubscribe = nextUnsubscribe; + }).catch(reject); }); } diff --git a/apps/x/packages/core/src/knowledge/build_graph.ts b/apps/x/packages/core/src/knowledge/build_graph.ts index f408a844..c3cc2d6e 100644 --- a/apps/x/packages/core/src/knowledge/build_graph.ts +++ b/apps/x/packages/core/src/knowledge/build_graph.ts @@ -189,13 +189,16 @@ async function readFileContents(filePaths: string[]): Promise<{ path: string; co * Wait for a run to complete by listening for run-processing-end event */ async function waitForRunCompletion(runId: string): Promise { - return new Promise(async (resolve) => { - const unsubscribe = await bus.subscribe('*', async (event) => { + return new Promise((resolve, reject) => { + let unsubscribe = () => {}; + bus.subscribe('*', async (event) => { if (event.type === 'run-processing-end' && event.runId === runId) { unsubscribe(); resolve(); } - }); + }).then((nextUnsubscribe) => { + unsubscribe = nextUnsubscribe; + }).catch(reject); }); } diff --git a/apps/x/packages/core/src/knowledge/chrome-extension/server/server.ts b/apps/x/packages/core/src/knowledge/chrome-extension/server/server.ts index 0cb127b9..4bb6a695 100644 --- a/apps/x/packages/core/src/knowledge/chrome-extension/server/server.ts +++ b/apps/x/packages/core/src/knowledge/chrome-extension/server/server.ts @@ -43,7 +43,7 @@ function pathToSlug(url: string): string { const parsed = new URL(url); const p = parsed.pathname + (parsed.search || ''); if (!p || p === '/') return 'index'; - let slug = p.replace(/[^a-zA-Z0-9]+/g, '_').replace(/^_|_$/g, ''); + const slug = p.replace(/[^a-zA-Z0-9]+/g, '_').replace(/^_|_$/g, ''); return slug.substring(0, 80) || 'index'; } catch { return 'index'; @@ -184,12 +184,13 @@ function saveConfig(config: Config): void { fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8'); } -function validateConfig(data: any): data is Config { +function validateConfig(data: unknown): data is Config { if (typeof data !== 'object' || data === null) return false; - if (data.mode !== 'all' && data.mode !== 'ask') return false; - if (!Array.isArray(data.whitelist)) return false; - if (!Array.isArray(data.blacklist)) return false; - if (typeof data.enabled !== 'boolean') return false; + const candidate = data as Partial; + if (candidate.mode !== 'all' && candidate.mode !== 'ask') return false; + if (!Array.isArray(candidate.whitelist)) return false; + if (!Array.isArray(candidate.blacklist)) return false; + if (typeof candidate.enabled !== 'boolean') return false; return true; } diff --git a/apps/x/packages/core/src/knowledge/inline_tasks.ts b/apps/x/packages/core/src/knowledge/inline_tasks.ts index 3f7c5ffa..7891d171 100644 --- a/apps/x/packages/core/src/knowledge/inline_tasks.ts +++ b/apps/x/packages/core/src/knowledge/inline_tasks.ts @@ -133,13 +133,16 @@ function scanDirectoryRecursive(dir: string): string[] { * Wait for a run to complete by listening for run-processing-end event */ async function waitForRunCompletion(runId: string): Promise { - return new Promise(async (resolve) => { - const unsubscribe = await bus.subscribe('*', async (event) => { + return new Promise((resolve, reject) => { + let unsubscribe = () => {}; + bus.subscribe('*', async (event) => { if (event.type === 'run-processing-end' && event.runId === runId) { unsubscribe(); resolve(); } - }); + }).then((nextUnsubscribe) => { + unsubscribe = nextUnsubscribe; + }).catch(reject); }); } diff --git a/apps/x/packages/core/src/knowledge/label_emails.ts b/apps/x/packages/core/src/knowledge/label_emails.ts index 68bca5a1..6258f1ca 100644 --- a/apps/x/packages/core/src/knowledge/label_emails.ts +++ b/apps/x/packages/core/src/knowledge/label_emails.ts @@ -66,13 +66,16 @@ function getUnlabeledEmails(state: LabelingState): string[] { * Wait for a run to complete by listening for run-processing-end event */ async function waitForRunCompletion(runId: string): Promise { - return new Promise(async (resolve) => { - const unsubscribe = await bus.subscribe('*', async (event) => { + return new Promise((resolve, reject) => { + let unsubscribe = () => {}; + bus.subscribe('*', async (event) => { if (event.type === 'run-processing-end' && event.runId === runId) { unsubscribe(); resolve(); } - }); + }).then((nextUnsubscribe) => { + unsubscribe = nextUnsubscribe; + }).catch(reject); }); } diff --git a/apps/x/packages/core/src/knowledge/sync_calendar.ts b/apps/x/packages/core/src/knowledge/sync_calendar.ts index c6a10f8e..79d5f595 100644 --- a/apps/x/packages/core/src/knowledge/sync_calendar.ts +++ b/apps/x/packages/core/src/knowledge/sync_calendar.ts @@ -348,24 +348,6 @@ async function performSync(syncDir: string, lookbackDays: number) { // --- Composio-based Sync --- -interface ComposioCalendarState { - last_sync: string; // ISO string -} - -function loadComposioState(stateFile: string): ComposioCalendarState | null { - if (fs.existsSync(stateFile)) { - try { - const data = JSON.parse(fs.readFileSync(stateFile, 'utf-8')); - if (data.last_sync) { - return { last_sync: data.last_sync }; - } - } catch (e) { - console.error('[Calendar] Failed to load composio state:', e); - } - } - return null; -} - function saveComposioState(stateFile: string, lastSync: string): void { fs.writeFileSync(stateFile, JSON.stringify({ last_sync: lastSync }, null, 2)); } diff --git a/apps/x/packages/core/src/knowledge/tag_notes.ts b/apps/x/packages/core/src/knowledge/tag_notes.ts index 39d46a3b..c5fee1b2 100644 --- a/apps/x/packages/core/src/knowledge/tag_notes.ts +++ b/apps/x/packages/core/src/knowledge/tag_notes.ts @@ -79,13 +79,16 @@ function getUntaggedNotes(state: NoteTaggingState): string[] { * Wait for a run to complete by listening for run-processing-end event */ async function waitForRunCompletion(runId: string): Promise { - return new Promise(async (resolve) => { - const unsubscribe = await bus.subscribe('*', async (event) => { + return new Promise((resolve, reject) => { + let unsubscribe = () => {}; + bus.subscribe('*', async (event) => { if (event.type === 'run-processing-end' && event.runId === runId) { unsubscribe(); resolve(); } - }); + }).then((nextUnsubscribe) => { + unsubscribe = nextUnsubscribe; + }).catch(reject); }); } diff --git a/apps/x/packages/core/src/pre_built/runner.ts b/apps/x/packages/core/src/pre_built/runner.ts index 4856dd7f..adcb8dd7 100644 --- a/apps/x/packages/core/src/pre_built/runner.ts +++ b/apps/x/packages/core/src/pre_built/runner.ts @@ -22,13 +22,16 @@ const PREBUILT_DIR = path.join(WorkDir, 'pre-built'); * Wait for a run to complete by listening for run-processing-end event */ async function waitForRunCompletion(runId: string): Promise { - return new Promise(async (resolve) => { - const unsubscribe = await bus.subscribe('*', async (event) => { + return new Promise((resolve, reject) => { + let unsubscribe = () => {}; + bus.subscribe('*', async (event) => { if (event.type === 'run-processing-end' && event.runId === runId) { unsubscribe(); resolve(); } - }); + }).then((nextUnsubscribe) => { + unsubscribe = nextUnsubscribe; + }).catch(reject); }); } @@ -89,8 +92,6 @@ Process new items and use the user context above to identify yourself when draft * Check all agents and run those that are due */ async function checkAndRunAgents(): Promise { - const config = loadConfig(); - for (const agentName of PREBUILT_AGENTS) { try { if (shouldRunAgent(agentName)) { @@ -149,7 +150,7 @@ export async function init(): Promise { * Manually trigger an agent run (useful for testing) */ export async function triggerAgent(agentName: string): Promise { - if (!PREBUILT_AGENTS.includes(agentName as any)) { + if (!PREBUILT_AGENTS.includes(agentName as (typeof PREBUILT_AGENTS)[number])) { throw new Error(`Unknown agent: ${agentName}. Available: ${PREBUILT_AGENTS.join(', ')}`); } await runAgent(agentName); diff --git a/apps/x/packages/core/src/workspace/watcher.ts b/apps/x/packages/core/src/workspace/watcher.ts index 3460f014..d7467a58 100644 --- a/apps/x/packages/core/src/workspace/watcher.ts +++ b/apps/x/packages/core/src/workspace/watcher.ts @@ -1,12 +1,12 @@ import chokidar, { type FSWatcher } from 'chokidar'; import fs from 'node:fs/promises'; +import { workspace } from '@x/shared'; import { ensureWorkspaceRoot, absToRelPosix } from './workspace.js'; import { WorkDir } from '../config/config.js'; -import { WorkspaceChangeEvent } from 'packages/shared/dist/workspace.js'; import z from 'zod'; import { Stats } from 'node:fs'; -export type WorkspaceChangeCallback = (event: z.infer) => void; +export type WorkspaceChangeCallback = (event: z.infer) => void; /** * Create a workspace watcher diff --git a/apps/x/packages/core/src/workspace/workspace.ts b/apps/x/packages/core/src/workspace/workspace.ts index de1fe212..bfbe3ede 100644 --- a/apps/x/packages/core/src/workspace/workspace.ts +++ b/apps/x/packages/core/src/workspace/workspace.ts @@ -3,7 +3,6 @@ import type { Stats } from 'node:fs'; import path from 'node:path'; import { workspace } from '@x/shared'; import { z } from 'zod'; -import { RemoveOptions, WriteFileOptions, WriteFileResult } from 'packages/shared/dist/workspace.js'; import { WorkDir } from '../config/config.js'; import { rewriteWikiLinksForRenamedKnowledgeFile } from './wiki-link-rewrite.js'; import { commitAll } from '../knowledge/version_history.js'; @@ -237,8 +236,8 @@ function scheduleKnowledgeCommit(filename: string): void { export async function writeFile( relPath: string, data: string, - opts?: z.infer -): Promise> { + opts?: z.infer +): Promise> { const filePath = resolveWorkspacePath(relPath); const encoding = opts?.encoding || 'utf8'; const atomic = opts?.atomic !== false; // default true @@ -381,7 +380,7 @@ export async function copy( export async function remove( relPath: string, - opts?: z.infer + opts?: z.infer ): Promise<{ ok: true }> { const filePath = resolveWorkspacePath(relPath); const trash = opts?.trash !== false; // default true diff --git a/apps/x/packages/core/test/run-tests.mjs b/apps/x/packages/core/test/run-tests.mjs new file mode 100644 index 00000000..d8ab7c17 --- /dev/null +++ b/apps/x/packages/core/test/run-tests.mjs @@ -0,0 +1,31 @@ +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { spawn } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const packageDir = path.resolve(__dirname, ".."); +const tempRoot = await mkdtemp(path.join(tmpdir(), "rowboat-core-test-")); +const testWorkDir = path.join(tempRoot, "workspace"); + +try { + const exitCode = await new Promise((resolve, reject) => { + const child = spawn(process.execPath, ["--test", "./test/workspace-path-safety.test.mjs"], { + cwd: packageDir, + stdio: "inherit", + env: { + ...process.env, + ROWBOAT_WORKDIR: testWorkDir, + }, + }); + + child.on("error", reject); + child.on("exit", (code) => resolve(code ?? 1)); + }); + + process.exitCode = Number(exitCode); +} finally { + await rm(tempRoot, { recursive: true, force: true }); +} diff --git a/apps/x/packages/core/test/workspace-path-safety.test.mjs b/apps/x/packages/core/test/workspace-path-safety.test.mjs new file mode 100644 index 00000000..c19f40a2 --- /dev/null +++ b/apps/x/packages/core/test/workspace-path-safety.test.mjs @@ -0,0 +1,44 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import path from "node:path"; + +import { WorkDir } from "../dist/config/config.js"; +import { + absToRelPosix, + assertSafeRelPath, + resolveWorkspacePath, +} from "../dist/workspace/workspace.js"; + +test("uses ROWBOAT_WORKDIR override for test isolation", () => { + assert.equal(WorkDir, process.env.ROWBOAT_WORKDIR); +}); + +test("assertSafeRelPath allows simple relative paths", () => { + assert.doesNotThrow(() => assertSafeRelPath("notes/today.md")); +}); + +test("assertSafeRelPath rejects absolute paths", () => { + assert.throws(() => assertSafeRelPath("/tmp/notes.md"), /Absolute paths are not allowed/); +}); + +test("assertSafeRelPath rejects traversal attempts", () => { + assert.throws(() => assertSafeRelPath("../notes.md"), /Path traversal/); + assert.throws(() => assertSafeRelPath("notes/../secret.md"), /Path traversal|Invalid path/); +}); + +test("resolveWorkspacePath returns the configured root for empty path", () => { + assert.equal(resolveWorkspacePath(""), WorkDir); +}); + +test("resolveWorkspacePath resolves safe relative paths inside the workspace", () => { + assert.equal(resolveWorkspacePath("knowledge/alpha.md"), path.join(WorkDir, "knowledge", "alpha.md")); +}); + +test("absToRelPosix returns POSIX relative paths inside the workspace", () => { + const absolutePath = path.join(WorkDir, "knowledge", "nested", "alpha.md"); + assert.equal(absToRelPosix(absolutePath), "knowledge/nested/alpha.md"); +}); + +test("absToRelPosix rejects paths outside the workspace", () => { + assert.equal(absToRelPosix("/tmp/outside.md"), null); +}); diff --git a/build-electron.sh b/build-electron.sh index 93df5731..8718688a 100644 --- a/build-electron.sh +++ b/build-electron.sh @@ -1,12 +1,14 @@ #!/bin/bash set -e -# build rowboatx next.js app +# Prepare the frontend and local runtime embedded by the desktop shell. + +# Build the RowboatX Next.js frontend. (cd apps/rowboatx && \ npm install && \ npm run build) -# build rowboat server +# Build the local CLI/runtime service. (cd apps/cli && \ npm install && \ - npm run build) \ No newline at end of file + npm run build)