From 9ad6331fbcd933e8ffb348118b583f049c345fa2 Mon Sep 17 00:00:00 2001 From: Ramnique Singh <30795890+ramnique@users.noreply.github.com> Date: Tue, 2 Dec 2025 13:24:58 +0530 Subject: [PATCH 01/66] server for rowboatx --- apps/cli/.gitignore | 3 +- apps/cli/bin/app.js | 16 - apps/cli/package-lock.json | 1073 ++++++++++------- apps/cli/package.json | 8 +- .../entities/agent.ts => agents/agents.ts} | 0 apps/cli/src/agents/repo.ts | 45 + .../lib/agent.ts => agents/runtime.ts} | 267 +++- apps/cli/src/app.ts | 87 +- apps/cli/src/application/assistant/agent.ts | 2 +- .../src/application/assistant/instructions.ts | 2 +- apps/cli/src/application/config/config.ts | 63 - apps/cli/src/application/entities/mcp.ts | 20 - apps/cli/src/application/entities/models.ts | 27 - apps/cli/src/application/lib/builtin-tools.ts | 302 +---- apps/cli/src/application/lib/bus.ts | 38 + .../src/application/lib/command-executor.ts | 2 +- apps/cli/src/application/lib/exec-tool.ts | 49 +- .../lib/{run-id-gen.ts => id-gen.ts} | 16 +- apps/cli/src/application/lib/mcp.ts | 31 - apps/cli/src/application/lib/message-queue.ts | 44 + apps/cli/src/application/lib/step.ts | 13 - .../src/application/lib/stream-renderer.ts | 4 +- apps/cli/src/config/config.ts | 15 + .../src/{application => }/config/security.ts | 0 apps/cli/src/di/container.ts | 30 + .../src/{application => }/entities/example.ts | 4 +- .../entities/llm-step-events.ts | 0 .../src/{application => }/entities/message.ts | 0 .../{application => }/entities/run-events.ts | 4 +- apps/cli/src/examples/index.ts | 2 +- apps/cli/src/mcp/mcp.ts | 174 +++ apps/cli/src/mcp/repo.ts | 45 + .../src/{application/lib => models}/models.ts | 33 +- apps/cli/src/models/repo.ts | 70 ++ apps/cli/src/runs/lock.ts | 20 + apps/cli/src/runs/repo.ts | 79 ++ apps/cli/src/runs/runs.ts | 70 ++ apps/cli/src/server.ts | 653 ++++++++++ 38 files changed, 2223 insertions(+), 1088 deletions(-) rename apps/cli/src/{application/entities/agent.ts => agents/agents.ts} (100%) create mode 100644 apps/cli/src/agents/repo.ts rename apps/cli/src/{application/lib/agent.ts => agents/runtime.ts} (76%) delete mode 100644 apps/cli/src/application/config/config.ts delete mode 100644 apps/cli/src/application/entities/mcp.ts delete mode 100644 apps/cli/src/application/entities/models.ts create mode 100644 apps/cli/src/application/lib/bus.ts rename apps/cli/src/application/lib/{run-id-gen.ts => id-gen.ts} (80%) delete mode 100644 apps/cli/src/application/lib/mcp.ts create mode 100644 apps/cli/src/application/lib/message-queue.ts delete mode 100644 apps/cli/src/application/lib/step.ts create mode 100644 apps/cli/src/config/config.ts rename apps/cli/src/{application => }/config/security.ts (100%) create mode 100644 apps/cli/src/di/container.ts rename apps/cli/src/{application => }/entities/example.ts (75%) rename apps/cli/src/{application => }/entities/llm-step-events.ts (100%) rename apps/cli/src/{application => }/entities/message.ts (100%) rename apps/cli/src/{application => }/entities/run-events.ts (98%) create mode 100644 apps/cli/src/mcp/mcp.ts create mode 100644 apps/cli/src/mcp/repo.ts rename apps/cli/src/{application/lib => models}/models.ts (77%) create mode 100644 apps/cli/src/models/repo.ts create mode 100644 apps/cli/src/runs/lock.ts create mode 100644 apps/cli/src/runs/repo.ts create mode 100644 apps/cli/src/runs/runs.ts create mode 100644 apps/cli/src/server.ts diff --git a/apps/cli/.gitignore b/apps/cli/.gitignore index 04c01ba7..21795603 100644 --- a/apps/cli/.gitignore +++ b/apps/cli/.gitignore @@ -1,2 +1,3 @@ node_modules/ -dist/ \ No newline at end of file +dist/ +.vercel diff --git a/apps/cli/bin/app.js b/apps/cli/bin/app.js index 3eeace50..b865bc95 100755 --- a/apps/cli/bin/app.js +++ b/apps/cli/bin/app.js @@ -116,20 +116,4 @@ yargs(hideBin(process.argv)) modelConfig(); } ) - .command( - "update-state ", - "Update state for a run", - (y) => y - .positional("agent", { - type: "string", - description: "The agent to run", - }) - .positional("run_id", { - type: "string", - description: "The run id to update", - }), - (argv) => { - updateState(argv.agent, argv.run_id); - } - ) .parse(); diff --git a/apps/cli/package-lock.json b/apps/cli/package-lock.json index a5dcf009..90c2818a 100644 --- a/apps/cli/package-lock.json +++ b/apps/cli/package-lock.json @@ -1,12 +1,12 @@ { "name": "@rowboatlabs/rowboatx", - "version": "0.15.0", + "version": "0.16.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@rowboatlabs/rowboatx", - "version": "0.15.0", + "version": "0.16.0", "license": "Apache-2.0", "dependencies": { "@ai-sdk/anthropic": "^2.0.44", @@ -14,9 +14,14 @@ "@ai-sdk/openai": "^2.0.53", "@ai-sdk/openai-compatible": "^1.0.27", "@ai-sdk/provider": "^2.0.0", + "@hono/node-server": "^1.19.6", + "@hono/standard-validator": "^0.1.5", "@modelcontextprotocol/sdk": "^1.20.2", "@openrouter/ai-sdk-provider": "^1.2.6", "ai": "^5.0.102", + "awilix": "^12.0.5", + "hono": "^4.10.7", + "hono-openapi": "^1.1.1", "json-schema-to-zod": "^2.6.1", "nanoid": "^5.1.6", "ollama-ai-provider-v2": "^1.5.4", @@ -28,35 +33,17 @@ }, "devDependencies": { "@types/node": "^24.9.1", - "ts-node": "^10.9.2", "typescript": "^5.9.3" } }, "node_modules/@ai-sdk/anthropic": { - "version": "2.0.44", - "resolved": "https://registry.npmjs.org/@ai-sdk/anthropic/-/anthropic-2.0.44.tgz", - "integrity": "sha512-o8TfNXRzO/KZkBrcx+CL9LQsPhx7PHyqzUGjza3TJaF9WxfH1S5UQLAmEw8F7lQoHNLU0IX03WT8o8R/4JbUxQ==", + "version": "2.0.53", + "resolved": "https://registry.npmjs.org/@ai-sdk/anthropic/-/anthropic-2.0.53.tgz", + "integrity": "sha512-ih7NV+OFSNWZCF+tYYD7ovvvM+gv7TRKQblpVohg2ipIwC9Y0TirzocJVREzZa/v9luxUwFbsPji++DUDWWxsg==", "license": "Apache-2.0", "dependencies": { "@ai-sdk/provider": "2.0.0", - "@ai-sdk/provider-utils": "3.0.17" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.25.76 || ^4.1.8" - } - }, - "node_modules/@ai-sdk/anthropic/node_modules/@ai-sdk/provider-utils": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.17.tgz", - "integrity": "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/provider": "2.0.0", - "@standard-schema/spec": "^1.0.0", - "eventsource-parser": "^3.0.6" + "@ai-sdk/provider-utils": "3.0.18" }, "engines": { "node": ">=18" @@ -66,13 +53,13 @@ } }, "node_modules/@ai-sdk/gateway": { - "version": "2.0.15", - "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-2.0.15.tgz", - "integrity": "sha512-i1YVKzC1dg9LGvt+GthhD7NlRhz9J4+ZRj3KELU14IZ/MHPsOBiFeEoCCIDLR+3tqT8/+5nIsK3eZ7DFRfMfdw==", + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-2.0.18.tgz", + "integrity": "sha512-sDQcW+6ck2m0pTIHW6BPHD7S125WD3qNkx/B8sEzJp/hurocmJ5Cni0ybExg6sQMGo+fr/GWOwpHF1cmCdg5rQ==", "license": "Apache-2.0", "dependencies": { "@ai-sdk/provider": "2.0.0", - "@ai-sdk/provider-utils": "3.0.17", + "@ai-sdk/provider-utils": "3.0.18", "@vercel/oidc": "3.0.5" }, "engines": { @@ -82,48 +69,14 @@ "zod": "^3.25.76 || ^4.1.8" } }, - "node_modules/@ai-sdk/gateway/node_modules/@ai-sdk/provider-utils": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.17.tgz", - "integrity": "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/provider": "2.0.0", - "@standard-schema/spec": "^1.0.0", - "eventsource-parser": "^3.0.6" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.25.76 || ^4.1.8" - } - }, "node_modules/@ai-sdk/google": { - "version": "2.0.25", - "resolved": "https://registry.npmjs.org/@ai-sdk/google/-/google-2.0.25.tgz", - "integrity": "sha512-tH2rA3428jnY6COoPfKB/BoQMs57sv9t+PEdyIB9ePtlV9dnVUbfKcdKoEcAaVffNZ6pzk8otrQYnu67pyn8TQ==", + "version": "2.0.44", + "resolved": "https://registry.npmjs.org/@ai-sdk/google/-/google-2.0.44.tgz", + "integrity": "sha512-c5dck36FjqiVoeeMJQLTEmUheoURcGTU/nBT6iJu8/nZiKFT/y8pD85KMDRB7RerRYaaQOtslR2d6/5PditiRw==", "license": "Apache-2.0", "dependencies": { "@ai-sdk/provider": "2.0.0", - "@ai-sdk/provider-utils": "3.0.14" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.25.76 || ^4.1.8" - } - }, - "node_modules/@ai-sdk/google/node_modules/@ai-sdk/provider-utils": { - "version": "3.0.14", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.14.tgz", - "integrity": "sha512-CYRU6L7IlR7KslSBVxvlqlybQvXJln/PI57O8swhOaDIURZbjRP2AY3igKgUsrmWqqnFFUHP+AwTN8xqJAknnA==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/provider": "2.0.0", - "@standard-schema/spec": "^1.0.0", - "eventsource-parser": "^3.0.5" + "@ai-sdk/provider-utils": "3.0.18" }, "engines": { "node": ">=18" @@ -133,13 +86,13 @@ } }, "node_modules/@ai-sdk/openai": { - "version": "2.0.53", - "resolved": "https://registry.npmjs.org/@ai-sdk/openai/-/openai-2.0.53.tgz", - "integrity": "sha512-GIkR3+Fyif516ftXv+YPSPstnAHhcZxNoR2s8uSHhQ1yBT7I7aQYTVwpjAuYoT3GR+TeP50q7onj2/nDRbT2FQ==", + "version": "2.0.76", + "resolved": "https://registry.npmjs.org/@ai-sdk/openai/-/openai-2.0.76.tgz", + "integrity": "sha512-ryUkhTDVxe3D1GSAGc94vPZsJlSY8ZuBDLkpf4L81Dm7Ik5AgLfhQrZa8+0hD4kp0dxdVaIoxhpa3QOt1CmncA==", "license": "Apache-2.0", "dependencies": { "@ai-sdk/provider": "2.0.0", - "@ai-sdk/provider-utils": "3.0.12" + "@ai-sdk/provider-utils": "3.0.18" }, "engines": { "node": ">=18" @@ -149,30 +102,13 @@ } }, "node_modules/@ai-sdk/openai-compatible": { - "version": "1.0.27", - "resolved": "https://registry.npmjs.org/@ai-sdk/openai-compatible/-/openai-compatible-1.0.27.tgz", - "integrity": "sha512-bpYruxVLhrTbVH6CCq48zMJNeHu6FmHtEedl9FXckEgcIEAi036idFhJlcRwC1jNCwlacbzb8dPD7OAH1EKJaQ==", + "version": "1.0.28", + "resolved": "https://registry.npmjs.org/@ai-sdk/openai-compatible/-/openai-compatible-1.0.28.tgz", + "integrity": "sha512-yKubDxLYtXyGUzkr9lNStf/lE/I+Okc8tmotvyABhsQHHieLKk6oV5fJeRJxhr67Ejhg+FRnwUOxAmjRoFM4dA==", "license": "Apache-2.0", "dependencies": { "@ai-sdk/provider": "2.0.0", - "@ai-sdk/provider-utils": "3.0.17" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.25.76 || ^4.1.8" - } - }, - "node_modules/@ai-sdk/openai-compatible/node_modules/@ai-sdk/provider-utils": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.17.tgz", - "integrity": "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/provider": "2.0.0", - "@standard-schema/spec": "^1.0.0", - "eventsource-parser": "^3.0.6" + "@ai-sdk/provider-utils": "3.0.18" }, "engines": { "node": ">=18" @@ -194,14 +130,14 @@ } }, "node_modules/@ai-sdk/provider-utils": { - "version": "3.0.12", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.12.tgz", - "integrity": "sha512-ZtbdvYxdMoria+2SlNarEk6Hlgyf+zzcznlD55EAl+7VZvJaSg2sqPvwArY7L6TfDEDJsnCq0fdhBSkYo0Xqdg==", + "version": "3.0.18", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.18.tgz", + "integrity": "sha512-ypv1xXMsgGcNKUP+hglKqtdDuMg68nWHucPPAhIENrbFAI+xCHiqPVN8Zllxyv1TNZwGWUghPxJXU+Mqps0YRQ==", "license": "Apache-2.0", "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", - "eventsource-parser": "^3.0.5" + "eventsource-parser": "^3.0.6" }, "engines": { "node": ">=18" @@ -210,54 +146,36 @@ "zod": "^3.25.76 || ^4.1.8" } }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, + "node_modules/@hono/node-server": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.6.tgz", + "integrity": "sha512-Shz/KjlIeAhfiuE93NDKVdZ7HdBVLQAfdbaXEaoAVO3ic9ibRSLGIQGkcBbFyuLr+7/1D5ZCINM8B+6IvXeMtw==", "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" + "engines": { + "node": ">=18.14.1" }, - "engines": { - "node": ">=12" + "peerDependencies": { + "hono": "^4" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, + "node_modules/@hono/standard-validator": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@hono/standard-validator/-/standard-validator-0.1.5.tgz", + "integrity": "sha512-EIyZPPwkyLn6XKwFj5NBEWHXhXbgmnVh2ceIFo5GO7gKI9WmzTjPDKnppQB0KrqKeAkq3kpoW4SIbu5X1dgx3w==", "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "peerDependencies": { + "@standard-schema/spec": "1.0.0", + "hono": ">=3.9.0" } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.20.2.tgz", - "integrity": "sha512-6rqTdFt67AAAzln3NOKsXRmv5ZzPkgbfaebKBqUbts7vK1GZudqnrun5a8d3M/h955cam9RHZ6Jb4Y1XhnmFPg==", + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.24.1.tgz", + "integrity": "sha512-YTg4v6bKSst8EJM8NXHC3nGm8kgHD08IbIBbognUeLAgGLVgLpYrgQswzLQd4OyTL4l614ejhqsDrV1//t02Qw==", "license": "MIT", "dependencies": { - "ajv": "^6.12.6", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", @@ -265,37 +183,67 @@ "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", + "jose": "^6.1.1", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", - "zod": "^3.23.8", - "zod-to-json-schema": "^3.24.1" + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.0" }, "engines": { "node": ">=18" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/zod-to-json-schema": { - "version": "3.24.6", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", - "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", - "license": "ISC", + }, "peerDependencies": { - "zod": "^3.24.1" + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" } }, "node_modules/@openrouter/ai-sdk-provider": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@openrouter/ai-sdk-provider/-/ai-sdk-provider-1.2.6.tgz", - "integrity": "sha512-DExO4FXod5vEdLFpQGsyNva8u3FWHj2IPaP8to+zEGsBEUY7lu5t24uIMxmmLKZ0sYYWAtmTLSV4Y9uOVqQoAg==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@openrouter/ai-sdk-provider/-/ai-sdk-provider-1.2.8.tgz", + "integrity": "sha512-pQT8AzZBKg9f4bkt4doF486ZlhK0XjKkevrLkiqYgfh1Jplovieu28nK4Y+xy3sF18/mxjqh9/2y6jh01qzLrA==", "license": "Apache-2.0", "dependencies": { "@openrouter/sdk": "^0.1.8" @@ -309,28 +257,12 @@ } }, "node_modules/@openrouter/sdk": { - "version": "0.1.17", - "resolved": "https://registry.npmjs.org/@openrouter/sdk/-/sdk-0.1.17.tgz", - "integrity": "sha512-RFN0sfe83G85MirfpkZSuoX8hLLucemnwqrTr53vlrJmBJZ244CCnuZ33vpVUI8rLg+hP1i/smW6IExzYRDGDg==", + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/@openrouter/sdk/-/sdk-0.1.27.tgz", + "integrity": "sha512-RH//L10bSmc81q25zAZudiI4kNkLgxF2E+WU42vghp3N6TEvZ6F0jK7uT3tOxkEn91gzmMw9YVmDENy7SJsajQ==", "license": "Apache-2.0", "dependencies": { "zod": "^3.25.0 || ^4.0.0" - }, - "peerDependencies": { - "@tanstack/react-query": "^5", - "react": "^18 || ^19", - "react-dom": "^18 || ^19" - }, - "peerDependenciesMeta": { - "@tanstack/react-query": { - "optional": true - }, - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } } }, "node_modules/@opentelemetry/api": { @@ -342,44 +274,111 @@ "node": ">=8.0.0" } }, + "node_modules/@standard-community/standard-json": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@standard-community/standard-json/-/standard-json-0.3.5.tgz", + "integrity": "sha512-4+ZPorwDRt47i+O7RjyuaxHRK/37QY/LmgxlGrRrSTLYoFatEOzvqIc85GTlM18SFZ5E91C+v0o/M37wZPpUHA==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/json-schema": "^7.0.15", + "@valibot/to-json-schema": "^1.3.0", + "arktype": "^2.1.20", + "effect": "^3.16.8", + "quansync": "^0.2.11", + "sury": "^10.0.0", + "typebox": "^1.0.17", + "valibot": "^1.1.0", + "zod": "^3.25.0 || ^4.0.0", + "zod-to-json-schema": "^3.24.5" + }, + "peerDependenciesMeta": { + "@valibot/to-json-schema": { + "optional": true + }, + "arktype": { + "optional": true + }, + "effect": { + "optional": true + }, + "sury": { + "optional": true + }, + "typebox": { + "optional": true + }, + "valibot": { + "optional": true + }, + "zod": { + "optional": true + }, + "zod-to-json-schema": { + "optional": true + } + } + }, + "node_modules/@standard-community/standard-openapi": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@standard-community/standard-openapi/-/standard-openapi-0.2.9.tgz", + "integrity": "sha512-htj+yldvN1XncyZi4rehbf9kLbu8os2Ke/rfqoZHCMHuw34kiF3LP/yQPdA0tQ940y8nDq3Iou8R3wG+AGGyvg==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@standard-community/standard-json": "^0.3.5", + "@standard-schema/spec": "^1.0.0", + "arktype": "^2.1.20", + "effect": "^3.17.14", + "openapi-types": "^12.1.3", + "sury": "^10.0.0", + "typebox": "^1.0.0", + "valibot": "^1.1.0", + "zod": "^3.25.0 || ^4.0.0", + "zod-openapi": "^4" + }, + "peerDependenciesMeta": { + "arktype": { + "optional": true + }, + "effect": { + "optional": true + }, + "sury": { + "optional": true + }, + "typebox": { + "optional": true + }, + "valibot": { + "optional": true + }, + "zod": { + "optional": true + }, + "zod-openapi": { + "optional": true + } + } + }, "node_modules/@standard-schema/spec": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", "license": "MIT" }, - "node_modules/@tsconfig/node10": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", - "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "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 }, "node_modules/@types/node": { - "version": "24.9.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz", - "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "dev": true, "license": "MIT", "dependencies": { @@ -408,41 +407,15 @@ "node": ">= 0.6" } }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/ai": { - "version": "5.0.102", - "resolved": "https://registry.npmjs.org/ai/-/ai-5.0.102.tgz", - "integrity": "sha512-snRK3nS5DESOjjpq7S74g8YszWVMzjagfHqlJWZsbtl9PyOS+2XUd8dt2wWg/jdaq/jh0aU66W1mx5qFjUQyEg==", + "version": "5.0.106", + "resolved": "https://registry.npmjs.org/ai/-/ai-5.0.106.tgz", + "integrity": "sha512-M5obwavxSJJ3tGlAFqI6eltYNJB0D20X6gIBCFx/KVorb/X1fxVVfiZZpZb+Gslu4340droSOjT0aKQFCarNVg==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/gateway": "2.0.15", + "@ai-sdk/gateway": "2.0.18", "@ai-sdk/provider": "2.0.0", - "@ai-sdk/provider-utils": "3.0.17", + "@ai-sdk/provider-utils": "3.0.18", "@opentelemetry/api": "1.9.0" }, "engines": { @@ -452,39 +425,39 @@ "zod": "^3.25.76 || ^4.1.8" } }, - "node_modules/ai/node_modules/@ai-sdk/provider-utils": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.17.tgz", - "integrity": "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/provider": "2.0.0", - "@standard-schema/spec": "^1.0.0", - "eventsource-parser": "^3.0.6" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.25.76 || ^4.1.8" - } - }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "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" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, "funding": { "type": "github", "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/ansi-regex": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", @@ -509,12 +482,18 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true, - "license": "MIT" + "node_modules/awilix": { + "version": "12.0.5", + "resolved": "https://registry.npmjs.org/awilix/-/awilix-12.0.5.tgz", + "integrity": "sha512-Qf/V/hRo6DK0FoBKJ9QiObasRxHAhcNi0mV6kW2JMawxS3zq6Un+VsZmVAZDUfvB+MjTEiJ2tUJUl4cr0JiUAw==", + "license": "MIT", + "dependencies": { + "camel-case": "^4.1.2", + "fast-glob": "^3.3.3" + }, + "engines": { + "node": ">=16.3.0" + } }, "node_modules/body-parser": { "version": "2.2.1", @@ -540,6 +519,18 @@ "url": "https://opencollective.com/express" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -578,6 +569,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "license": "MIT", + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, "node_modules/cliui": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", @@ -593,15 +594,16 @@ } }, "node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/content-type": { @@ -644,13 +646,6 @@ "node": ">= 0.10" } }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true, - "license": "MIT" - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -691,16 +686,6 @@ "node": ">= 0.8" } }, - "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -812,18 +797,19 @@ } }, "node_modules/express": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", "dependencies": { "accepts": "^2.0.0", - "body-parser": "^2.2.0", + "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", + "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", @@ -874,16 +860,63 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, - "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==", - "license": "MIT" + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } }, "node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", "license": "MIT", "dependencies": { "debug": "^4.4.0", @@ -894,7 +927,11 @@ "statuses": "^2.0.1" }, "engines": { - "node": ">= 0.8" + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/forwarded": { @@ -982,6 +1019,18 @@ "node": ">= 0.4" } }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -1018,29 +1067,55 @@ "node": ">= 0.4" } }, + "node_modules/hono": { + "version": "4.10.7", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.10.7.tgz", + "integrity": "sha512-icXIITfw/07Q88nLSkB9aiUrd8rYzSweK681Kjo/TSggaGbOX4RRyxxm71v+3PC8C/j+4rlxGeoTRxQDkaJkUw==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/hono-openapi": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/hono-openapi/-/hono-openapi-1.1.1.tgz", + "integrity": "sha512-AC3HNhZYPHhnZdSy2Je7GDoTTNxPos6rKRQKVDBbSilY3cWJPqsxRnN6zA4pU7tfxmQEMTqkiLXbw6sAaemB8Q==", + "license": "MIT", + "peerDependencies": { + "@hono/standard-validator": "^0.1.2", + "@standard-community/standard-json": "^0.3.5", + "@standard-community/standard-openapi": "^0.2.8", + "@types/json-schema": "^7.0.15", + "hono": "^4.8.3", + "openapi-types": "^12.1.3" + }, + "peerDependenciesMeta": { + "@hono/standard-validator": { + "optional": true + }, + "hono": { + "optional": true + } + } + }, "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "license": "MIT", "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { "node": ">= 0.8" - } - }, - "node_modules/http-errors/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/iconv-lite": { @@ -1074,6 +1149,36 @@ "node": ">= 0.10" } }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", @@ -1086,6 +1191,15 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, + "node_modules/jose": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", + "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/json-schema": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", @@ -1093,26 +1207,28 @@ "license": "(AFL-2.1 OR BSD-3-Clause)" }, "node_modules/json-schema-to-zod": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/json-schema-to-zod/-/json-schema-to-zod-2.6.1.tgz", - "integrity": "sha512-uiHmWH21h9FjKJkRBntfVGTLpYlCZ1n98D0izIlByqQLqpmkQpNTBtfbdP04Na6+43lgsvrShFh2uWLkQDKJuQ==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/json-schema-to-zod/-/json-schema-to-zod-2.7.0.tgz", + "integrity": "sha512-eW59l3NQ6sa3HcB+Ahf7pP6iGU7MY4we5JsPqXQ2ZcIPF8QxSg/lkY8lN0Js/AG0NjMbk+nZGUfHlceiHF+bwQ==", "license": "ISC", "bin": { "json-schema-to-zod": "dist/cjs/cli.js" } }, "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==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true, - "license": "ISC" + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } }, "node_modules/math-intrinsics": { "version": "1.1.0", @@ -1144,6 +1260,28 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -1154,15 +1292,19 @@ } }, "node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "license": "MIT", "dependencies": { "mime-db": "^1.54.0" }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/ms": { @@ -1198,6 +1340,16 @@ "node": ">= 0.6" } }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -1220,9 +1372,9 @@ } }, "node_modules/ollama-ai-provider-v2": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/ollama-ai-provider-v2/-/ollama-ai-provider-v2-1.5.4.tgz", - "integrity": "sha512-OTxzIvxW7GutgkyYe55Y4lJeUbnDjH1jDkAQhjGiynffkDn0wyWbv/dD92A8HX1ni5Ec+i+ksYMXXlVOYPQR4g==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/ollama-ai-provider-v2/-/ollama-ai-provider-v2-1.5.5.tgz", + "integrity": "sha512-1YwTFdPjhPNHny/DrOHO+s8oVGGIE5Jib61/KnnjPRNWQhVVimrJJdaAX3e6nNRRDXrY5zbb9cfm2+yVvgsrqw==", "license": "Apache-2.0", "dependencies": { "@ai-sdk/provider": "^2.0.0", @@ -1235,23 +1387,6 @@ "zod": "^4.0.16" } }, - "node_modules/ollama-ai-provider-v2/node_modules/@ai-sdk/provider-utils": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.17.tgz", - "integrity": "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/provider": "2.0.0", - "@standard-schema/spec": "^1.0.0", - "eventsource-parser": "^3.0.6" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.25.76 || ^4.1.8" - } - }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -1273,6 +1408,13 @@ "wrappy": "1" } }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "license": "MIT", + "peer": true + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -1282,6 +1424,16 @@ "node": ">= 0.8" } }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -1301,10 +1453,22 @@ "url": "https://opencollective.com/express" } }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/pkce-challenge": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", - "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", "license": "MIT", "engines": { "node": ">=16.20.0" @@ -1323,15 +1487,6 @@ "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==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", @@ -1347,6 +1502,43 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/quansync": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT", + "peer": true + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -1357,20 +1549,39 @@ } }, "node_modules/raw-body": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", - "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.7.0", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.10" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -1387,10 +1598,10 @@ "node": ">= 18" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "funding": [ { "type": "github", @@ -1405,7 +1616,10 @@ "url": "https://feross.org/support" } ], - "license": "MIT" + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } }, "node_modules/safer-buffer": { "version": "2.1.2", @@ -1590,6 +1804,18 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -1599,49 +1825,11 @@ "node": ">=0.6" } }, - "node_modules/ts-node": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - } + "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-is": { "version": "2.0.1", @@ -1687,22 +1875,6 @@ "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==", - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true, - "license": "MIT" - }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -1785,24 +1957,23 @@ "node": "^20.19.0 || ^22.12.0 || >=23" } }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/zod": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", - "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", + "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.0", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.0.tgz", + "integrity": "sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } } } } diff --git a/apps/cli/package.json b/apps/cli/package.json index 1f92cb54..7b42ffb9 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -6,7 +6,7 @@ "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "rm -rf dist && tsc", - "copilot": "npm run build && node dist/x.js" + "server": "node dist/server.js" }, "files": [ "dist", @@ -21,7 +21,6 @@ "description": "", "devDependencies": { "@types/node": "^24.9.1", - "ts-node": "^10.9.2", "typescript": "^5.9.3" }, "dependencies": { @@ -30,9 +29,14 @@ "@ai-sdk/openai": "^2.0.53", "@ai-sdk/openai-compatible": "^1.0.27", "@ai-sdk/provider": "^2.0.0", + "@hono/node-server": "^1.19.6", + "@hono/standard-validator": "^0.1.5", "@modelcontextprotocol/sdk": "^1.20.2", "@openrouter/ai-sdk-provider": "^1.2.6", "ai": "^5.0.102", + "awilix": "^12.0.5", + "hono": "^4.10.7", + "hono-openapi": "^1.1.1", "json-schema-to-zod": "^2.6.1", "nanoid": "^5.1.6", "ollama-ai-provider-v2": "^1.5.4", diff --git a/apps/cli/src/application/entities/agent.ts b/apps/cli/src/agents/agents.ts similarity index 100% rename from apps/cli/src/application/entities/agent.ts rename to apps/cli/src/agents/agents.ts diff --git a/apps/cli/src/agents/repo.ts b/apps/cli/src/agents/repo.ts new file mode 100644 index 00000000..615a8afc --- /dev/null +++ b/apps/cli/src/agents/repo.ts @@ -0,0 +1,45 @@ +import { WorkDir } from "../config/config.js"; +import fs from "fs/promises"; +import path from "path"; +import z from "zod"; +import { Agent } from "./agents.js"; + +export interface IAgentsRepo { + list(): Promise[]>; + fetch(id: string): Promise>; + create(agent: z.infer): Promise; + update(id: string, agent: z.infer): Promise; + delete(id: string): Promise; +} + +export class FSAgentsRepo implements IAgentsRepo { + async list(): Promise[]> { + const result: z.infer[] = []; + // list all json files in workdir/agents/ + const agentsDir = path.join(WorkDir, "agents"); + const files = await fs.readdir(agentsDir); + + for (const file of files) { + const contents = await fs.readFile(path.join(agentsDir, file), "utf8"); + result.push(Agent.parse(JSON.parse(contents))); + } + return result; + } + + async fetch(id: string): Promise> { + const contents = await fs.readFile(path.join(WorkDir, "agents", `${id}.json`), "utf8"); + return Agent.parse(JSON.parse(contents)); + } + + async create(agent: z.infer): Promise { + await fs.writeFile(path.join(WorkDir, "agents", `${agent.name}.json`), JSON.stringify(agent, null, 2)); + } + + async update(id: string, agent: z.infer): Promise { + await fs.writeFile(path.join(WorkDir, "agents", `${id}.json`), JSON.stringify(agent, null, 2)); + } + + async delete(id: string): Promise { + await fs.unlink(path.join(WorkDir, "agents", `${id}.json`)); + } +} \ No newline at end of file diff --git a/apps/cli/src/application/lib/agent.ts b/apps/cli/src/agents/runtime.ts similarity index 76% rename from apps/cli/src/application/lib/agent.ts rename to apps/cli/src/agents/runtime.ts index 5ee019ac..530aa6e7 100644 --- a/apps/cli/src/application/lib/agent.ts +++ b/apps/cli/src/agents/runtime.ts @@ -1,19 +1,102 @@ import { jsonSchema, ModelMessage, modelMessageSchema } from "ai"; import fs from "fs"; import path from "path"; -import { getModelConfig, WorkDir } from "../config/config.js"; -import { Agent, ToolAttachment } from "../entities/agent.js"; +import { WorkDir } from "../config/config.js"; +import { Agent, ToolAttachment } from "./agents.js"; import { AssistantContentPart, AssistantMessage, Message, MessageList, ProviderOptions, ToolCallPart, ToolMessage, UserMessage } from "../entities/message.js"; -import { runIdGenerator } from "./run-id-gen.js"; import { LanguageModel, stepCountIs, streamText, tool, Tool, ToolSet } from "ai"; import { z } from "zod"; -import { getProvider } from "./models.js"; import { LlmStepStreamEvent } from "../entities/llm-step-events.js"; -import { execTool } from "./exec-tool.js"; -import { AskHumanRequestEvent, RunEvent, ToolPermissionRequestEvent, ToolPermissionResponseEvent } from "../entities/run-events.js"; -import { BuiltinTools } from "./builtin-tools.js"; -import { CopilotAgent } from "../assistant/agent.js"; -import { isBlocked } from "./command-executor.js"; +import { execTool } from "../application/lib/exec-tool.js"; +import { MessageEvent, AskHumanRequestEvent, RunEvent, ToolInvocationEvent, ToolPermissionRequestEvent, ToolPermissionResponseEvent } from "../entities/run-events.js"; +import { BuiltinTools } from "../application/lib/builtin-tools.js"; +import { CopilotAgent } from "../application/assistant/agent.js"; +import { isBlocked } from "../application/lib/command-executor.js"; +import container from "../di/container.js"; +import { IModelConfigRepo } from "../models/repo.js"; +import { getProvider } from "../models/models.js"; +import { IAgentsRepo } from "./repo.js"; +import { IdGen, IMonotonicallyIncreasingIdGenerator } from "../application/lib/id-gen.js"; +import { IBus } from "../application/lib/bus.js"; +import { IMessageQueue } from "../application/lib/message-queue.js"; +import { IRunsRepo } from "../runs/repo.js"; +import { IRunsLock } from "../runs/lock.js"; + +export interface IAgentRuntime { + trigger(runId: string): Promise; +} + +export class AgentRuntime implements IAgentRuntime { + private runsRepo: IRunsRepo; + private idGenerator: IMonotonicallyIncreasingIdGenerator; + private bus: IBus; + private messageQueue: IMessageQueue; + private modelConfigRepo: IModelConfigRepo; + private runsLock: IRunsLock; + + constructor({ + runsRepo, + idGenerator, + bus, + messageQueue, + modelConfigRepo, + runsLock, + }: { + runsRepo: IRunsRepo; + idGenerator: IMonotonicallyIncreasingIdGenerator; + bus: IBus; + messageQueue: IMessageQueue; + modelConfigRepo: IModelConfigRepo; + runsLock: IRunsLock; + }) { + this.runsRepo = runsRepo; + this.idGenerator = idGenerator; + this.bus = bus; + this.messageQueue = messageQueue; + this.modelConfigRepo = modelConfigRepo; + this.runsLock = runsLock; + } + + async trigger(runId: string): Promise { + if (!await this.runsLock.lock(runId)) { + console.log(`unable to acquire lock on run ${runId}`); + return; + } + try { + while (true) { + let eventCount = 0; + const run = await this.runsRepo.fetch(runId); + if (!run) { + throw new Error(`Run ${runId} not found`); + } + const state = new AgentState(); + for (const event of run.log) { + state.ingest(event); + } + for await (const event of streamAgent({ + state, + idGenerator: this.idGenerator, + runId, + messageQueue: this.messageQueue, + modelConfigRepo: this.modelConfigRepo, + })) { + eventCount++; + if (event.type !== "llm-stream-event") { + await this.runsRepo.appendEvents(runId, [event]); + } + await this.bus.publish(event); + } + + // if no events, break + if (!eventCount) { + break; + } + } + } finally { + await this.runsLock.release(runId); + } + } +} export async function mapAgentTool(t: z.infer): Promise { switch (t.type) { @@ -128,8 +211,8 @@ export class StreamStepMessageBuilder { }); break; case "finish-step": - this.providerOptions = event.providerOptions; - break; + this.providerOptions = event.providerOptions; + break; } } @@ -171,9 +254,8 @@ export async function loadAgent(id: string): Promise> { if (id === "copilot") { return CopilotAgent; } - const agentPath = path.join(WorkDir, "agents", `${id}.json`); - const agent = fs.readFileSync(agentPath, "utf8"); - return Agent.parse(JSON.parse(agent)); + const repo = container.resolve('agentsRepo'); + return await repo.fetch(id); } export function convertFromMessages(messages: z.infer[]): ModelMessage[] { @@ -262,10 +344,9 @@ async function buildTools(agent: z.infer): Promise { } export class AgentState { - logger: RunLogger | null = null; runId: string | null = null; agent: z.infer | null = null; - agentName: string; + agentName: string | null = null; messages: z.infer = []; lastAssistantMsg: z.infer | null = null; subflowStates: Record = {}; @@ -276,20 +357,6 @@ export class AgentState { allowedToolCallIds: Record = {}; deniedToolCallIds: Record = {}; - constructor(agentName: string, runId?: string) { - this.agentName = agentName; - this.runId = runId || runIdGenerator.next(); - this.logger = new RunLogger(this.runId); - if (!runId) { - this.logger.log({ - type: "start", - runId: this.runId, - agentName: this.agentName, - subflow: [], - }); - } - } - getPendingPermissions(): z.infer[] { const response: z.infer[] = []; for (const [id, subflowState] of Object.entries(this.subflowStates)) { @@ -346,6 +413,9 @@ export class AgentState { ingest(event: z.infer) { if (event.subflow.length > 0) { const { subflow, ...rest } = event; + if (!this.subflowStates[subflow[0]]) { + this.subflowStates[subflow[0]] = new AgentState(); + } this.subflowStates[subflow[0]].ingest({ ...rest, subflow: subflow.slice(1), @@ -353,6 +423,10 @@ export class AgentState { return; } switch (event.type) { + case "start": + this.runId = event.runId; + this.agentName = event.agentName; + break; case "message": this.messages.push(event.message); if (event.message.content instanceof Array) { @@ -371,9 +445,6 @@ export class AgentState { this.lastAssistantMsg = event.message; } break; - case "spawn-subflow": - this.subflowStates[event.toolCallId] = new AgentState(event.agentName); - break; case "tool-permission-request": this.pendingToolPermissionRequests[event.toolCall.toolCallId] = event; break; @@ -406,27 +477,33 @@ export class AgentState { break; } } - - ingestAndLog(event: z.infer) { - this.ingest(event); - this.logger!.log(event); - } - - *ingestAndLogAndYield(event: z.infer): Generator, void, unknown> { - this.ingestAndLog(event); - yield event; - } } -export async function* streamAgent(state: AgentState): AsyncGenerator, void, unknown> { - // get model config - const modelConfig = await getModelConfig(); +export async function* streamAgent({ + state, + idGenerator, + runId, + messageQueue, + modelConfigRepo, +}: { + state: AgentState, + idGenerator: IMonotonicallyIncreasingIdGenerator; + runId: string; + messageQueue: IMessageQueue; + modelConfigRepo: IModelConfigRepo; +}): AsyncGenerator, void, unknown> { + async function* processEvent(event: z.infer): AsyncGenerator, void, unknown> { + state.ingest(event); + yield event; + } + + const modelConfig = await modelConfigRepo.getConfig(); if (!modelConfig) { throw new Error("Model config not found"); } // set up agent - const agent = await loadAgent(state.agentName); + const agent = await loadAgent(state.agentName!); // set up tools const tools = await buildTools(agent); @@ -436,9 +513,16 @@ export async function* streamAgent(state: AgentState): AsyncGenerator part.type === "tool-call") ) ) { - // console.error("Nothing to do, exiting (a.)") - return; + let pending = 0; + while(true) { + const msg = await messageQueue.dequeue(runId); + if (!msg) { + break; + } + pending++; + yield *processEvent({ + runId, + type: "message", + messageId: msg.messageId, + message: { + role: "user", + content: msg.message, + }, + subflow: [], + }); + } + // if no msgs found, return + if (!pending) { + return; + } } // execute any pending tool calls @@ -461,7 +565,9 @@ export async function* streamAgent(state: AgentState): AsyncGenerator('modelConfigRepo'); + const config = await repo.getConfig(); const rl = createInterface({ input, output }); try { @@ -333,14 +312,7 @@ export async function modelConfig() { ); const model = modelAns.trim() || modelDefault; - const newConfig = { - providers: { ...(config?.providers || {}) }, - defaults: { - provider: providerName!, - model, - }, - }; - await updateModelConfig(newConfig as any); + await repo.setDefault(providerName!, model); console.log(`Model configuration updated. Provider set to '${providerName}'.`); return; } @@ -391,24 +363,13 @@ export async function modelConfig() { ); const model = modelAns.trim() || modelDefault; - const mergedProviders = { - ...(config?.providers || {}), - [providerName]: { - flavor: selectedFlavor, - ...(apiKey ? { apiKey } : {}), - ...(baseURL ? { baseURL } : {}), - ...(headers ? { headers } : {}), - }, - }; - const newConfig = { - providers: mergedProviders, - defaults: { - provider: providerName, - model, - }, - }; - - await updateModelConfig(newConfig as any); + await repo.upsert(providerName, { + flavor: selectedFlavor, + apiKey, + baseURL, + headers, + }); + await repo.setDefault(providerName, model); renderCurrentModel(providerName, selectedFlavor, model); console.log(`Configuration written to ${WorkDir}/config/models.json. You can also edit this file manually`); } finally { diff --git a/apps/cli/src/application/assistant/agent.ts b/apps/cli/src/application/assistant/agent.ts index afcefafa..94ab982c 100644 --- a/apps/cli/src/application/assistant/agent.ts +++ b/apps/cli/src/application/assistant/agent.ts @@ -1,4 +1,4 @@ -import { Agent, ToolAttachment } from "../entities/agent.js"; +import { Agent, ToolAttachment } from "../../agents/agents.js"; import z from "zod"; import { CopilotInstructions } from "./instructions.js"; import { BuiltinTools } from "../lib/builtin-tools.js"; diff --git a/apps/cli/src/application/assistant/instructions.ts b/apps/cli/src/application/assistant/instructions.ts index 7d8aad14..b6e49cf0 100644 --- a/apps/cli/src/application/assistant/instructions.ts +++ b/apps/cli/src/application/assistant/instructions.ts @@ -1,5 +1,5 @@ import { skillCatalog } from "./skills/index.js"; -import { WorkDir as BASE_DIR } from "../config/config.js"; +import { WorkDir as BASE_DIR } from "../../config/config.js"; export const CopilotInstructions = `You are an intelligent workflow assistant helping users manage their workflows in ${BASE_DIR}. You can also help the user with general tasks. diff --git a/apps/cli/src/application/config/config.ts b/apps/cli/src/application/config/config.ts deleted file mode 100644 index 4702a765..00000000 --- a/apps/cli/src/application/config/config.ts +++ /dev/null @@ -1,63 +0,0 @@ -import path from "path"; -import fs from "fs"; -import { McpServerConfig } from "../entities/mcp.js"; -import { ModelConfig } from "../entities/models.js"; -import { z } from "zod"; -import { homedir } from "os"; - -// Resolve app root relative to compiled file location (dist/...) -export const WorkDir = path.join(homedir(), ".rowboat"); - -let modelConfig: z.infer | null = null; - -const baseMcpConfig: z.infer = { - mcpServers: { - } -}; - -function ensureMcpConfig() { - const configPath = path.join(WorkDir, "config", "mcp.json"); - if (!fs.existsSync(configPath)) { - fs.writeFileSync(configPath, JSON.stringify(baseMcpConfig, null, 2)); - } -} - -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")); - ensureMcpConfig(); -} - -function loadMcpServerConfig(): z.infer { - const configPath = path.join(WorkDir, "config", "mcp.json"); - if (!fs.existsSync(configPath)) return { mcpServers: {} }; - const config = fs.readFileSync(configPath, "utf8"); - return McpServerConfig.parse(JSON.parse(config)); -} - -export async function getModelConfig(): Promise | null> { - if (modelConfig) { - return modelConfig; - } - const configPath = path.join(WorkDir, "config", "models.json"); - try { - const config = await fs.promises.readFile(configPath, "utf8"); - modelConfig = ModelConfig.parse(JSON.parse(config)); - return modelConfig; - } catch (error) { - console.error(`Warning! model config not found!`); - return null; - } -} - -export async function updateModelConfig(config: z.infer) { - modelConfig = config; - const configPath = path.join(WorkDir, "config", "models.json"); - await fs.promises.writeFile(configPath, JSON.stringify(config, null, 2)); -} - -ensureDirs(); -const { mcpServers } = loadMcpServerConfig(); -export const McpServers = mcpServers; \ No newline at end of file diff --git a/apps/cli/src/application/entities/mcp.ts b/apps/cli/src/application/entities/mcp.ts deleted file mode 100644 index e47ebb95..00000000 --- a/apps/cli/src/application/entities/mcp.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { z } from "zod"; - -export const StdioMcpServerConfig = z.object({ - type: z.literal("stdio").optional(), - command: z.string(), - args: z.array(z.string()).optional(), - env: z.record(z.string(), z.string()).optional(), -}); - -export const HttpMcpServerConfig = z.object({ - type: z.literal("http").optional(), - url: z.string(), - headers: z.record(z.string(), z.string()).optional(), -}); - -export const McpServerDefinition = z.union([StdioMcpServerConfig, HttpMcpServerConfig]); - -export const McpServerConfig = z.object({ - mcpServers: z.record(z.string(), McpServerDefinition), -}); diff --git a/apps/cli/src/application/entities/models.ts b/apps/cli/src/application/entities/models.ts deleted file mode 100644 index 0dabad3f..00000000 --- a/apps/cli/src/application/entities/models.ts +++ /dev/null @@ -1,27 +0,0 @@ -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(), - }), -}); \ No newline at end of file diff --git a/apps/cli/src/application/lib/builtin-tools.ts b/apps/cli/src/application/lib/builtin-tools.ts index e0c02f48..b7839079 100644 --- a/apps/cli/src/application/lib/builtin-tools.ts +++ b/apps/cli/src/application/lib/builtin-tools.ts @@ -1,14 +1,13 @@ import { z, ZodType } from "zod"; import * as fs from "fs/promises"; import * as path from "path"; -import { WorkDir as BASE_DIR } from "../config/config.js"; +import { WorkDir as BASE_DIR } from "../../config/config.js"; import { executeCommand } from "./command-executor.js"; -import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; -import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; -import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; -import { Client } from "@modelcontextprotocol/sdk/client"; import { resolveSkill, availableSkills } from "../assistant/skills/index.js"; -import { McpServerDefinition, McpServerConfig } from "../entities/mcp.js"; +import { executeTool, listServers, listTools } from "../../mcp/mcp.js"; +import container from "../../di/container.js"; +import { IMcpConfigRepo } from "../..//mcp/repo.js"; +import { McpServerDefinition } from "../../mcp/mcp.js"; const BuiltinToolsSchema = z.record(z.string(), z.object({ description: z.string(), @@ -310,109 +309,33 @@ export const BuiltinTools: z.infer = { description: 'Add or update an MCP server in the configuration with validation. This ensures the server definition is valid before saving.', inputSchema: z.object({ serverName: z.string().describe('Name/alias for the MCP server'), - serverType: z.enum(['stdio', 'http']).describe('Type of MCP server: "stdio" for command-based or "http" for HTTP/SSE-based'), - command: z.string().optional().describe('Command to execute (required for stdio type, e.g., "npx", "python", "node")'), - args: z.array(z.string()).optional().describe('Command arguments (optional, for stdio type)'), - env: z.record(z.string(), z.string()).optional().describe('Environment variables (optional, for stdio type)'), - url: z.string().optional().describe('HTTP/SSE endpoint URL (required for http type)'), - headers: z.record(z.string(), z.string()).optional().describe('HTTP headers (optional, for http type)'), + config: McpServerDefinition, }), - execute: async ({ serverName, serverType, command, args, env, url, headers }: { + execute: async ({ serverName, config }: { serverName: string; - serverType: 'stdio' | 'http'; - command?: string; - args?: string[]; - env?: Record; - url?: string; - headers?: Record; + config: z.infer; }) => { try { - // Build server definition based on type - let serverDef: any; - if (serverType === 'stdio') { - if (!command) { - return { - success: false, - message: 'For stdio type servers, "command" is required. Example: "npx" or "python"', - validationErrors: ['Missing required field: command'], - }; - } - serverDef = { - type: 'stdio', - command, - ...(args ? { args } : {}), - ...(env ? { env } : {}), - }; - } else if (serverType === 'http') { - if (!url) { - return { - success: false, - message: 'For http type servers, "url" is required. Example: "http://localhost:3000/sse"', - validationErrors: ['Missing required field: url'], - }; - } - serverDef = { - type: 'http', - url, - ...(headers ? { headers } : {}), - }; - } else { - return { - success: false, - message: `Invalid serverType: ${serverType}. Must be "stdio" or "http"`, - validationErrors: [`Invalid serverType: ${serverType}`], - }; - } - - // Validate against Zod schema - const validationResult = McpServerDefinition.safeParse(serverDef); + const validationResult = McpServerDefinition.safeParse(config); if (!validationResult.success) { return { success: false, message: 'Server definition failed validation. Check the errors below.', validationErrors: validationResult.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`), - providedDefinition: serverDef, + providedDefinition: config, }; } - - // Read existing config - const configPath = path.join(BASE_DIR, 'config', 'mcp.json'); - let currentConfig: z.infer = { mcpServers: {} }; - try { - const content = await fs.readFile(configPath, 'utf-8'); - currentConfig = McpServerConfig.parse(JSON.parse(content)); - } catch (error: any) { - if (error?.code !== 'ENOENT') { - return { - success: false, - message: `Failed to read existing MCP config: ${error.message}`, - }; - } - // File doesn't exist, use empty config - } - - // Check if server already exists - const isUpdate = !!currentConfig.mcpServers[serverName]; - - // Add/update server - currentConfig.mcpServers[serverName] = validationResult.data; - - // Write back to file - await fs.mkdir(path.dirname(configPath), { recursive: true }); - await fs.writeFile(configPath, JSON.stringify(currentConfig, null, 2), 'utf-8'); + + const repo = container.resolve('mcpConfigRepo'); + await repo.upsert(serverName, config); return { success: true, - message: `MCP server '${serverName}' ${isUpdate ? 'updated' : 'added'} successfully`, serverName, - serverType, - isUpdate, - configuration: validationResult.data, }; } catch (error) { return { - success: false, - message: `Failed to add MCP server: ${error instanceof Error ? error.message : 'Unknown error'}`, + error: `Failed to update MCP server: ${error instanceof Error ? error.message : 'Unknown error'}`, }; } }, @@ -421,47 +344,17 @@ export const BuiltinTools: z.infer = { listMcpServers: { description: 'List all available MCP servers from the configuration', inputSchema: z.object({}), - execute: async (): Promise<{ success: boolean, servers: any[], count: number, message: string }> => { + execute: async () => { try { - const configPath = path.join(BASE_DIR, 'config', 'mcp.json'); - - // Check if config exists - try { - await fs.access(configPath); - } catch { - return { - success: true, - servers: [], - count: 0, - message: 'No MCP servers configured yet', - }; - } - - const content = await fs.readFile(configPath, 'utf-8'); - const config = JSON.parse(content); - - const servers = Object.keys(config.mcpServers || {}).map(name => { - const server = config.mcpServers[name]; - return { - name, - type: 'command' in server ? 'stdio' : 'http', - command: server.command, - url: server.url, - }; - }); + const result = await listServers(); return { - success: true, - servers, - count: servers.length, - message: `Found ${servers.length} MCP server(s)`, + result, + count: Object.keys(result.mcpServers).length, }; } catch (error) { return { - success: false, - servers: [], - count: 0, - message: `Failed to list MCP servers: ${error instanceof Error ? error.message : 'Unknown error'}`, + error: `Failed to list MCP servers: ${error instanceof Error ? error.message : 'Unknown error'}`, }; } }, @@ -471,69 +364,19 @@ export const BuiltinTools: z.infer = { description: 'List all available tools from a specific MCP server', inputSchema: z.object({ serverName: z.string().describe('Name of the MCP server to query'), + cursor: z.string().optional(), }), - execute: async ({ serverName }: { serverName: string }) => { + execute: async ({ serverName, cursor }: { serverName: string, cursor?: string }) => { try { - const configPath = path.join(BASE_DIR, 'config', 'mcp.json'); - const content = await fs.readFile(configPath, 'utf-8'); - const config = JSON.parse(content); - - const mcpConfig = config.mcpServers[serverName]; - if (!mcpConfig) { - return { - success: false, - message: `MCP server '${serverName}' not found in configuration`, - }; - } - - // Create transport based on config type - let transport; - if ('command' in mcpConfig) { - transport = new StdioClientTransport({ - command: mcpConfig.command, - args: mcpConfig.args || [], - env: mcpConfig.env || {}, - }); - } else { - try { - transport = new StreamableHTTPClientTransport(new URL(mcpConfig.url)); - } catch { - transport = new SSEClientTransport(new URL(mcpConfig.url)); - } - } - - // Create and connect client - const client = new Client({ - name: 'rowboat-copilot', - version: '1.0.0', - }); - - await client.connect(transport); - - // List available tools - const toolsList = await client.listTools(); - - // Close connection - client.close(); - transport.close(); - - const tools = toolsList.tools.map((t: any) => ({ - name: t.name, - description: t.description || 'No description', - inputSchema: t.inputSchema, - })); - + const result = await listTools(serverName, cursor); return { - success: true, serverName, - tools, - count: tools.length, - message: `Found ${tools.length} tool(s) in MCP server '${serverName}'`, + result, + count: result.tools.length, }; } catch (error) { return { - success: false, - message: `Failed to list MCP tools: ${error instanceof Error ? error.message : 'Unknown error'}`, + error: `Failed to list MCP tools: ${error instanceof Error ? error.message : 'Unknown error'}`, }; } }, @@ -547,108 +390,19 @@ export const BuiltinTools: z.infer = { arguments: z.record(z.string(), z.any()).optional().describe('Arguments to pass to the tool (as key-value pairs matching the tool\'s input schema). MUST include all required parameters from the tool\'s inputSchema.'), }), execute: async ({ serverName, toolName, arguments: args = {} }: { serverName: string, toolName: string, arguments?: Record }) => { - let transport: any; - let client: any; - try { - const configPath = path.join(BASE_DIR, 'config', 'mcp.json'); - const content = await fs.readFile(configPath, 'utf-8'); - const config = JSON.parse(content); - - const mcpConfig = config.mcpServers[serverName]; - if (!mcpConfig) { - return { - success: false, - message: `MCP server '${serverName}' not found in configuration. Use listMcpServers to see available servers.`, - }; - } - - // Create transport based on config type - if ('command' in mcpConfig) { - transport = new StdioClientTransport({ - command: mcpConfig.command, - args: mcpConfig.args || [], - env: mcpConfig.env || {}, - }); - } else { - try { - transport = new StreamableHTTPClientTransport(new URL(mcpConfig.url)); - } catch { - transport = new SSEClientTransport(new URL(mcpConfig.url)); - } - } - - // Create and connect client - client = new Client({ - name: 'rowboat-copilot', - version: '1.0.0', - }); - - await client.connect(transport); - - // Get tool list to validate the tool exists and check schema - const toolsList = await client.listTools(); - const toolDef = toolsList.tools.find((t: any) => t.name === toolName); - - if (!toolDef) { - await client.close(); - transport.close(); - return { - success: false, - message: `Tool '${toolName}' not found in server '${serverName}'. Use listMcpTools to see available tools.`, - availableTools: toolsList.tools.map((t: any) => t.name), - }; - } - - // Validate required parameters - const inputSchema = toolDef.inputSchema; - if (inputSchema && inputSchema.required && Array.isArray(inputSchema.required)) { - const missingParams = inputSchema.required.filter((param: string) => !(param in args)); - if (missingParams.length > 0) { - await client.close(); - transport.close(); - return { - success: false, - message: `Missing required parameters: ${missingParams.join(', ')}`, - requiredParameters: inputSchema.required, - providedArguments: Object.keys(args), - toolSchema: inputSchema, - hint: `Use listMcpTools to see the full schema for '${toolName}' and ensure all required parameters are included in the arguments field.`, - }; - } - } - - // Call the tool - const result = await client.callTool({ - name: toolName, - arguments: args, - }); - - // Close connection - await client.close(); - transport.close(); - + const result = await executeTool(serverName, toolName, args); return { success: true, serverName, toolName, - result: result.content, + result, message: `Successfully executed tool '${toolName}' from server '${serverName}'`, }; } catch (error) { - // Ensure cleanup - try { - if (client) await client.close(); - if (transport) transport.close(); - } catch (cleanupError) { - // Ignore cleanup errors - } - return { success: false, - message: `Failed to execute MCP tool: ${error instanceof Error ? error.message : 'Unknown error'}`, - serverName, - toolName, + error: `Failed to execute MCP tool: ${error instanceof Error ? error.message : 'Unknown error'}`, hint: 'Use listMcpTools to verify the tool exists and check its schema. Ensure all required parameters are provided in the arguments field.', }; } diff --git a/apps/cli/src/application/lib/bus.ts b/apps/cli/src/application/lib/bus.ts new file mode 100644 index 00000000..d49c5d36 --- /dev/null +++ b/apps/cli/src/application/lib/bus.ts @@ -0,0 +1,38 @@ +import { RunEvent } from "../../entities/run-events.js"; +import z from "zod"; + +export interface IBus { + publish(event: z.infer): Promise; + + // subscribe accepts a handler to handle events + // and returns a function to unsubscribe + subscribe(runId: string, handler: (event: z.infer) => Promise): Promise<() => void>; +} + +export class InMemoryBus implements IBus { + private subscribers: Map) => Promise)[]> = new Map(); + + async publish(event: z.infer): Promise { + console.log(this.subscribers); + const pending: Promise[] = []; + for (const subscriber of this.subscribers.get(event.runId) || []) { + pending.push(subscriber(event)); + } + for (const subscriber of this.subscribers.get('*') || []) { + pending.push(subscriber(event)); + } + console.log(pending.length); + await Promise.all(pending); + } + + async subscribe(runId: string, handler: (event: z.infer) => Promise): Promise<() => void> { + if (!this.subscribers.has(runId)) { + this.subscribers.set(runId, []); + } + this.subscribers.get(runId)!.push(handler); + console.log(this.subscribers); + return () => { + this.subscribers.get(runId)!.splice(this.subscribers.get(runId)!.indexOf(handler), 1); + }; + } +} \ No newline at end of file diff --git a/apps/cli/src/application/lib/command-executor.ts b/apps/cli/src/application/lib/command-executor.ts index db030542..814d9801 100644 --- a/apps/cli/src/application/lib/command-executor.ts +++ b/apps/cli/src/application/lib/command-executor.ts @@ -1,6 +1,6 @@ import { exec, execSync } from 'child_process'; import { promisify } from 'util'; -import { getSecurityAllowList, SECURITY_CONFIG_PATH } from '../config/security.js'; +import { getSecurityAllowList, SECURITY_CONFIG_PATH } from '../../config/security.js'; const execPromise = promisify(exec); const COMMAND_SPLIT_REGEX = /(?:\|\||&&|;|\||\n)/; diff --git a/apps/cli/src/application/lib/exec-tool.ts b/apps/cli/src/application/lib/exec-tool.ts index ddd05e52..4ed32883 100644 --- a/apps/cli/src/application/lib/exec-tool.ts +++ b/apps/cli/src/application/lib/exec-tool.ts @@ -1,53 +1,10 @@ -import { ToolAttachment } from "../entities/agent.js"; +import { ToolAttachment } from "../../agents/agents.js"; import { z } from "zod"; -import { McpServers } from "../config/config.js"; -import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; -import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; -import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; -import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; -import { Client } from "@modelcontextprotocol/sdk/client"; import { BuiltinTools } from "./builtin-tools.js"; +import { executeTool } from "../../mcp/mcp.js"; async function execMcpTool(agentTool: z.infer & { type: "mcp" }, input: any): Promise { - // load mcp configuration from the tool - const mcpConfig = McpServers[agentTool.mcpServerName]; - if (!mcpConfig) { - throw new Error(`MCP server ${agentTool.mcpServerName} not found`); - } - - // create transport - let transport: Transport; - if ("command" in mcpConfig) { - transport = new StdioClientTransport({ - command: mcpConfig.command, - args: mcpConfig.args, - env: mcpConfig.env, - }); - } else { - // first try streamable http transport - try { - transport = new StreamableHTTPClientTransport(new URL(mcpConfig.url)); - } catch (error) { - // if that fails, try sse transport - transport = new SSEClientTransport(new URL(mcpConfig.url)); - } - } - - if (!transport) { - throw new Error(`No transport found for ${agentTool.mcpServerName}`); - } - - // create client - const client = new Client({ - name: 'rowboatx', - version: '1.0.0', - }); - await client.connect(transport); - - // call tool - const result = await client.callTool({ name: agentTool.name, arguments: input }); - client.close(); - transport.close(); + const result = await executeTool(agentTool.mcpServerName, agentTool.name, input); return result; } diff --git a/apps/cli/src/application/lib/run-id-gen.ts b/apps/cli/src/application/lib/id-gen.ts similarity index 80% rename from apps/cli/src/application/lib/run-id-gen.ts rename to apps/cli/src/application/lib/id-gen.ts index 4bccf259..e5bdddcd 100644 --- a/apps/cli/src/application/lib/run-id-gen.ts +++ b/apps/cli/src/application/lib/id-gen.ts @@ -1,19 +1,23 @@ -class RunIdGenerator { +export interface IMonotonicallyIncreasingIdGenerator { + next(): Promise; +} + +export class IdGen implements IMonotonicallyIncreasingIdGenerator { private lastMs = 0; private seq = 0; private readonly pid: string; private readonly hostTag: string; - constructor(hostTag: string = "") { + constructor() { this.pid = String(process.pid).padStart(7, "0"); - this.hostTag = hostTag ? `-${hostTag}` : ""; + this.hostTag = ""; } /** * Returns an ISO8601-based, lexicographically sortable id string. * Example: 2025-11-11T04-36-29Z-0001234-h1-000 */ - next(): string { + async next(): Promise { const now = Date.now(); const ms = now >= this.lastMs ? now : this.lastMs; // monotonic clamp this.seq = ms === this.lastMs ? this.seq + 1 : 0; @@ -27,6 +31,4 @@ class RunIdGenerator { const seqStr = String(this.seq).padStart(3, "0"); return `${iso}-${this.pid}${this.hostTag}-${seqStr}`; } -} - -export const runIdGenerator = new RunIdGenerator(); \ No newline at end of file +} \ No newline at end of file diff --git a/apps/cli/src/application/lib/mcp.ts b/apps/cli/src/application/lib/mcp.ts deleted file mode 100644 index 041710f5..00000000 --- a/apps/cli/src/application/lib/mcp.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Client } from '@modelcontextprotocol/sdk/client/index.js'; -import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; -import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'; - -export async function getMcpClient(serverUrl: string, serverName: string): Promise { - let client: Client | undefined = undefined; - const baseUrl = new URL(serverUrl); - - // Try to connect using Streamable HTTP transport - try { - client = new Client({ - name: 'streamable-http-client', - version: '1.0.0' - }); - const transport = new StreamableHTTPClientTransport(baseUrl); - await client.connect(transport); - console.log(`[MCP] Connected using Streamable HTTP transport to ${serverName}`); - return client; - } catch (error) { - // If that fails with a 4xx error, try the older SSE transport - console.log(`[MCP] Streamable HTTP connection failed, falling back to SSE transport for ${serverName}`); - client = new Client({ - name: 'sse-client', - version: '1.0.0' - }); - const sseTransport = new SSEClientTransport(baseUrl); - await client.connect(sseTransport); - console.log(`[MCP] Connected using SSE transport to ${serverName}`); - return client; - } -} diff --git a/apps/cli/src/application/lib/message-queue.ts b/apps/cli/src/application/lib/message-queue.ts new file mode 100644 index 00000000..7242a999 --- /dev/null +++ b/apps/cli/src/application/lib/message-queue.ts @@ -0,0 +1,44 @@ +import z from "zod"; +import { IMonotonicallyIncreasingIdGenerator } from "./id-gen.js"; + +const EnqueuedMessage = z.object({ + messageId: z.string(), + message: z.string(), +}); + +export interface IMessageQueue { + enqueue(runId: string, message: string): Promise; + dequeue(runId: string): Promise | null>; +} + +export class InMemoryMessageQueue implements IMessageQueue { + private store: Record[]> = {}; + private idGenerator: IMonotonicallyIncreasingIdGenerator; + + constructor({ + idGenerator, + }: { + idGenerator: IMonotonicallyIncreasingIdGenerator; + }) { + this.idGenerator = idGenerator; + } + + async enqueue(runId: string, message: string): Promise { + if (!this.store[runId]) { + this.store[runId] = []; + } + const id = await this.idGenerator.next(); + this.store[runId].push({ + messageId: id, + message, + }); + return id; + } + + async dequeue(runId: string): Promise | null> { + if (!this.store[runId]) { + return null; + } + return this.store[runId].shift() ?? null; + } +} \ No newline at end of file diff --git a/apps/cli/src/application/lib/step.ts b/apps/cli/src/application/lib/step.ts deleted file mode 100644 index 3fae98bc..00000000 --- a/apps/cli/src/application/lib/step.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { MessageList } from "../entities/message.js"; -import { LlmStepStreamEvent } from "../entities/llm-step-events.js"; -import { z } from "zod"; -import { ToolAttachment } from "../entities/agent.js"; - -export type StepInputT = z.infer; -export type StepOutputT = AsyncGenerator, void, unknown>; - -export interface Step { - execute(input: StepInputT): StepOutputT; - - tools(): Record>; -} \ No newline at end of file diff --git a/apps/cli/src/application/lib/stream-renderer.ts b/apps/cli/src/application/lib/stream-renderer.ts index 5fc83bab..bbf87c4a 100644 --- a/apps/cli/src/application/lib/stream-renderer.ts +++ b/apps/cli/src/application/lib/stream-renderer.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { RunEvent } from "../entities/run-events.js"; -import { LlmStepStreamEvent } from "../entities/llm-step-events.js"; +import { RunEvent } from "../../entities/run-events.js"; +import { LlmStepStreamEvent } from "../../entities/llm-step-events.js"; export interface StreamRendererOptions { showHeaders?: boolean; diff --git a/apps/cli/src/config/config.ts b/apps/cli/src/config/config.ts new file mode 100644 index 00000000..94355b54 --- /dev/null +++ b/apps/cli/src/config/config.ts @@ -0,0 +1,15 @@ +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 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")); +} + +ensureDirs(); \ No newline at end of file diff --git a/apps/cli/src/application/config/security.ts b/apps/cli/src/config/security.ts similarity index 100% rename from apps/cli/src/application/config/security.ts rename to apps/cli/src/config/security.ts diff --git a/apps/cli/src/di/container.ts b/apps/cli/src/di/container.ts new file mode 100644 index 00000000..bfcd5a47 --- /dev/null +++ b/apps/cli/src/di/container.ts @@ -0,0 +1,30 @@ +import { asClass, createContainer, InjectionMode } from "awilix"; +import { FSModelConfigRepo, IModelConfigRepo } from "../models/repo.js"; +import { FSMcpConfigRepo, IMcpConfigRepo } from "../mcp/repo.js"; +import { FSAgentsRepo, IAgentsRepo } from "../agents/repo.js"; +import { FSRunsRepo, IRunsRepo } from "../runs/repo.js"; +import { IMonotonicallyIncreasingIdGenerator, IdGen } from "../application/lib/id-gen.js"; +import { IMessageQueue, InMemoryMessageQueue } from "../application/lib/message-queue.js"; +import { IBus, InMemoryBus } from "../application/lib/bus.js"; +import { IRunsLock, InMemoryRunsLock } from "../runs/lock.js"; +import { IAgentRuntime, AgentRuntime } from "../agents/runtime.js"; + +const container = createContainer({ + injectionMode: InjectionMode.PROXY, + strict: true, +}); + +container.register({ + idGenerator: asClass(IdGen).singleton(), + messageQueue: asClass(InMemoryMessageQueue).singleton(), + bus: asClass(InMemoryBus).singleton(), + runsLock: asClass(InMemoryRunsLock).singleton(), + agentRuntime: asClass(AgentRuntime).singleton(), + + mcpConfigRepo: asClass(FSMcpConfigRepo).singleton(), + modelConfigRepo: asClass(FSModelConfigRepo).singleton(), + agentsRepo: asClass(FSAgentsRepo).singleton(), + runsRepo: asClass(FSRunsRepo).singleton(), +}); + +export default container; \ No newline at end of file diff --git a/apps/cli/src/application/entities/example.ts b/apps/cli/src/entities/example.ts similarity index 75% rename from apps/cli/src/application/entities/example.ts rename to apps/cli/src/entities/example.ts index 8857a55c..92e2f3f7 100644 --- a/apps/cli/src/application/entities/example.ts +++ b/apps/cli/src/entities/example.ts @@ -1,6 +1,6 @@ import z from "zod" -import { Agent } from "./agent.js" -import { McpServerDefinition } from "./mcp.js" +import { Agent } from "../agents/agents.js" +import { McpServerDefinition } from "../mcp/mcp.js"; export const Example = z.object({ id: z.string(), diff --git a/apps/cli/src/application/entities/llm-step-events.ts b/apps/cli/src/entities/llm-step-events.ts similarity index 100% rename from apps/cli/src/application/entities/llm-step-events.ts rename to apps/cli/src/entities/llm-step-events.ts diff --git a/apps/cli/src/application/entities/message.ts b/apps/cli/src/entities/message.ts similarity index 100% rename from apps/cli/src/application/entities/message.ts rename to apps/cli/src/entities/message.ts diff --git a/apps/cli/src/application/entities/run-events.ts b/apps/cli/src/entities/run-events.ts similarity index 98% rename from apps/cli/src/application/entities/run-events.ts rename to apps/cli/src/entities/run-events.ts index bdd0c13a..edd6d7e9 100644 --- a/apps/cli/src/application/entities/run-events.ts +++ b/apps/cli/src/entities/run-events.ts @@ -1,16 +1,15 @@ import { LlmStepStreamEvent } from "./llm-step-events.js"; import { Message, ToolCallPart } from "./message.js"; -import { Agent } from "./agent.js"; import z from "zod"; const BaseRunEvent = z.object({ + runId: z.string(), ts: z.iso.datetime().optional(), subflow: z.array(z.string()), }); export const StartEvent = BaseRunEvent.extend({ type: z.literal("start"), - runId: z.string(), agentName: z.string(), }); @@ -27,6 +26,7 @@ export const LlmStreamEvent = BaseRunEvent.extend({ export const MessageEvent = BaseRunEvent.extend({ type: z.literal("message"), + messageId: z.string(), message: Message, }); diff --git a/apps/cli/src/examples/index.ts b/apps/cli/src/examples/index.ts index 428356d2..04122f6b 100644 --- a/apps/cli/src/examples/index.ts +++ b/apps/cli/src/examples/index.ts @@ -1,5 +1,5 @@ import twitterPodcast from './twitter-podcast.json' with { type: 'json' }; -import { Example } from '../application/entities/example.js'; +import { Example } from '../entities/example.js'; import z from 'zod'; export const examples: Record> = { diff --git a/apps/cli/src/mcp/mcp.ts b/apps/cli/src/mcp/mcp.ts new file mode 100644 index 00000000..6e38bd98 --- /dev/null +++ b/apps/cli/src/mcp/mcp.ts @@ -0,0 +1,174 @@ +import container from "../di/container.js"; +import { Client } from "@modelcontextprotocol/sdk/client"; +import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; +import z from "zod"; +import { IMcpConfigRepo } from "./repo.js"; +import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; +import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; + +export const StdioMcpServerConfig = z.object({ + type: z.literal("stdio").optional(), + command: z.string(), + args: z.array(z.string()).optional(), + env: z.record(z.string(), z.string()).optional(), +}); + +export const HttpMcpServerConfig = z.object({ + type: z.literal("http").optional(), + url: z.string(), + headers: z.record(z.string(), z.string()).optional(), +}); + +export const McpServerDefinition = z.union([StdioMcpServerConfig, HttpMcpServerConfig]); + +export const McpServerConfig = z.object({ + mcpServers: z.record(z.string(), McpServerDefinition), +}); + +const connectionState = z.enum(["disconnected", "connected", "error"]); + +export const McpServerList = z.object({ + mcpServers: z.record(z.string(), z.object({ + config: McpServerDefinition, + state: connectionState, + error: z.string().nullable(), + })), +}); + +/* + inputSchema: { + [x: string]: unknown; + type: "object"; + properties?: Record | undefined; + required?: string[] | undefined; + }; +*/ +export const Tool = z.object({ + name: z.string(), + description: z.string().optional(), + inputSchema: z.object({ + type: z.literal("object"), + properties: z.record(z.string(), z.any()).optional(), + required: z.array(z.string()).optional(), + }), + outputSchema: z.object({ + type: z.literal("object"), + properties: z.record(z.string(), z.any()).optional(), + required: z.array(z.string()).optional(), + }).optional(), +}) + +export const ListToolsResponse = z.object({ + tools: z.array(Tool), + nextCursor: z.string().optional(), +}); + +type mcpState = { + state: z.infer, + client: Client | null, + error: string | null, +}; +const clients: Record = {}; + +async function getClient(serverName: string): Promise { + if (clients[serverName] && clients[serverName].state === "connected") { + return clients[serverName].client!; + } + const repo = container.resolve('mcpConfigRepo'); + const { mcpServers } = await repo.getConfig(); + const config = mcpServers[serverName]; + if (!config) { + throw new Error(`MCP server ${serverName} not found`); + } + let transport: Transport | undefined = undefined; + try { + // create transport + if ("command" in config) { + transport = new StdioClientTransport({ + command: config.command, + args: config.args, + env: config.env, + }); + } else { + try { + transport = new StreamableHTTPClientTransport(new URL(config.url)); + } catch (error) { + // if that fails, try sse transport + transport = new SSEClientTransport(new URL(config.url)); + } + } + + if (!transport) { + throw new Error(`No transport found for ${serverName}`); + } + + // create client + const client = new Client({ + name: 'rowboatx', + version: '1.0.0', + }); + await client.connect(transport); + + // store + clients[serverName] = { + state: "connected", + client, + error: null, + }; + return client; + } catch (error) { + clients[serverName] = { + state: "error", + client: null, + error: error instanceof Error ? error.message : "Unknown error", + }; + transport?.close(); + throw error; + } +} + +export async function cleanup() { + for (const [serverName, { client }] of Object.entries(clients)) { + await client?.transport?.close(); + await client?.close(); + delete clients[serverName]; + } +} + +export async function listServers(): Promise> { + const repo = container.resolve('mcpConfigRepo'); + const { mcpServers } = await repo.getConfig(); + const result: z.infer = { + mcpServers: {}, + }; + for (const [serverName, config] of Object.entries(mcpServers)) { + const state = clients[serverName]; + result.mcpServers[serverName] = { + config, + state: state ? state.state : "disconnected", + error: state ? state.error : null, + }; + } + return result; +} + +export async function listTools(serverName: string, cursor?: string): Promise> { + const client = await getClient(serverName); + const { tools, nextCursor } = await client.listTools({ + cursor, + }); + return { + tools, + nextCursor, + } +} + +export async function executeTool(serverName: string, toolName: string, input: any): Promise { + const client = await getClient(serverName); + const result = await client.callTool({ + name: toolName, + arguments: input, + }); + return result; +} \ No newline at end of file diff --git a/apps/cli/src/mcp/repo.ts b/apps/cli/src/mcp/repo.ts new file mode 100644 index 00000000..d43569af --- /dev/null +++ b/apps/cli/src/mcp/repo.ts @@ -0,0 +1,45 @@ +import { WorkDir } from "../config/config.js"; +import { McpServerConfig } from "./mcp.js"; +import { McpServerDefinition } from "./mcp.js"; +import fs from "fs/promises"; +import path from "path"; +import z from "zod"; + +export interface IMcpConfigRepo { + getConfig(): Promise>; + upsert(serverName: string, config: z.infer): Promise; + delete(serverName: string): Promise; +} + +export class FSMcpConfigRepo implements IMcpConfigRepo { + private readonly configPath = path.join(WorkDir, "config", "mcp.json"); + + constructor() { + this.ensureDefaultConfig(); + } + + private async ensureDefaultConfig(): Promise { + try { + await fs.access(this.configPath); + } catch (error) { + await fs.writeFile(this.configPath, JSON.stringify({ mcpServers: {} }, null, 2)); + } + } + + async getConfig(): Promise> { + const config = await fs.readFile(this.configPath, "utf8"); + return McpServerConfig.parse(JSON.parse(config)); + } + + async upsert(serverName: string, config: z.infer): Promise { + const conf = await this.getConfig(); + conf.mcpServers[serverName] = config; + await fs.writeFile(this.configPath, JSON.stringify(conf, null, 2)); + } + + async delete(serverName: string): Promise { + const conf = await this.getConfig(); + delete conf.mcpServers[serverName]; + await fs.writeFile(this.configPath, JSON.stringify(conf, null, 2)); + } +} \ No newline at end of file diff --git a/apps/cli/src/application/lib/models.ts b/apps/cli/src/models/models.ts similarity index 77% rename from apps/cli/src/application/lib/models.ts rename to apps/cli/src/models/models.ts index 1416d276..d2d846e5 100644 --- a/apps/cli/src/application/lib/models.ts +++ b/apps/cli/src/models/models.ts @@ -6,13 +6,42 @@ import { createAnthropic } from "@ai-sdk/anthropic"; import { createOllama } from "ollama-ai-provider-v2"; import { createOpenRouter } from '@openrouter/ai-sdk-provider'; import { createOpenAICompatible } from '@ai-sdk/openai-compatible'; -import { getModelConfig } from "../config/config.js"; +import { IModelConfigRepo } from "./repo.js"; +import container from "../di/container.js"; +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(), + }), +}); const providerMap: Record = {}; export async function getProvider(name: string = ""): Promise { // get model conf - const modelConfig = await getModelConfig(); + const repo = container.resolve("modelConfigRepo"); + const modelConfig = await repo.getConfig(); if (!modelConfig) { throw new Error("Model config not found"); } diff --git a/apps/cli/src/models/repo.ts b/apps/cli/src/models/repo.ts new file mode 100644 index 00000000..1041045c --- /dev/null +++ b/apps/cli/src/models/repo.ts @@ -0,0 +1,70 @@ +import { ModelConfig, Provider } from "./models.js"; +import { WorkDir } from "../config/config.js"; +import fs from "fs/promises"; +import path from "path"; +import z from "zod"; + +export interface IModelConfigRepo { + getConfig(): Promise>; + upsert(providerName: string, config: z.infer): Promise; + delete(providerName: string): Promise; + setDefault(providerName: string, model: string): Promise; +} + +const defaultConfig: z.infer = { + providers: { + "openai": { + flavor: "openai", + } + }, + defaults: { + provider: "openai", + model: "gpt-5.1", + } +}; + +export class FSModelConfigRepo implements IModelConfigRepo { + private readonly configPath = path.join(WorkDir, "config", "models.json"); + + constructor() { + this.ensureDefaultConfig(); + } + + private async ensureDefaultConfig(): Promise { + try { + await fs.access(this.configPath); + } catch (error) { + await fs.writeFile(this.configPath, JSON.stringify(defaultConfig, null, 2)); + } + } + + async getConfig(): Promise> { + const config = await fs.readFile(this.configPath, "utf8"); + return ModelConfig.parse(JSON.parse(config)); + } + + private async setConfig(config: z.infer): Promise { + await fs.writeFile(this.configPath, JSON.stringify(config, null, 2)); + } + + async upsert(providerName: string, config: z.infer): Promise { + const conf = await this.getConfig(); + conf.providers[providerName] = config; + await this.setConfig(conf); + } + + async delete(providerName: string): Promise { + const conf = await this.getConfig(); + delete conf.providers[providerName]; + await this.setConfig(conf); + } + + async setDefault(providerName: string, model: string): Promise { + const conf = await this.getConfig(); + conf.defaults = { + provider: providerName, + model, + }; + await this.setConfig(conf); + } +} \ No newline at end of file diff --git a/apps/cli/src/runs/lock.ts b/apps/cli/src/runs/lock.ts new file mode 100644 index 00000000..10515fc6 --- /dev/null +++ b/apps/cli/src/runs/lock.ts @@ -0,0 +1,20 @@ +export interface IRunsLock { + lock(runId: string): Promise; + release(runId: string): Promise; +} + +export class InMemoryRunsLock implements IRunsLock { + private locks: Record = {}; + + async lock(runId: string): Promise { + if (this.locks[runId]) { + return false; + } + this.locks[runId] = true; + return true; + } + + async release(runId: string): Promise { + delete this.locks[runId]; + } +} diff --git a/apps/cli/src/runs/repo.ts b/apps/cli/src/runs/repo.ts new file mode 100644 index 00000000..ed87ebae --- /dev/null +++ b/apps/cli/src/runs/repo.ts @@ -0,0 +1,79 @@ +import { Run } from "./runs.js"; +import z from "zod"; +import { IMonotonicallyIncreasingIdGenerator } from "../application/lib/id-gen.js"; +import { WorkDir } from "../config/config.js"; +import path from "path"; +import fsp from "fs/promises"; +import { RunEvent, StartEvent } from "../entities/run-events.js"; + +export const ListRunsResponse = z.object({ + runs: z.array(Run.pick({ + id: true, + createdAt: true, + agentId: true, + })), + nextCursor: z.string().optional(), +}); + +export const CreateRunOptions = Run.pick({ + agentId: true, +}); + +export interface IRunsRepo { + create(options: z.infer): Promise>; + fetch(id: string): Promise>; + appendEvents(runId: string, events: z.infer[]): Promise; +} + +export class FSRunsRepo implements IRunsRepo { + private idGenerator: IMonotonicallyIncreasingIdGenerator; + constructor({ + idGenerator, + }: { + idGenerator: IMonotonicallyIncreasingIdGenerator; + }) { + this.idGenerator = idGenerator; + } + + async appendEvents(runId: string, events: z.infer[]): Promise { + await fsp.appendFile( + path.join(WorkDir, 'runs', `${runId}.jsonl`), + events.map(event => JSON.stringify(event)).join("\n") + "\n" + ); + } + + async create(options: z.infer): Promise> { + const runId = await this.idGenerator.next(); + const ts = new Date().toISOString(); + const start: z.infer = { + type: "start", + runId, + agentName: options.agentId, + subflow: [], + ts, + }; + await this.appendEvents(runId, [start]); + return { + id: runId, + createdAt: ts, + agentId: options.agentId, + log: [start], + }; + } + + async fetch(id: string): Promise> { + const contents = await fsp.readFile(path.join(WorkDir, 'runs', `${id}.jsonl`), 'utf8'); + const events = contents.split('\n') + .filter(line => line.trim() !== '') + .map(line => RunEvent.parse(JSON.parse(line))); + if (events.length === 0 || events[0].type !== 'start') { + throw new Error('Corrupt run data'); + } + return { + id, + createdAt: events[0].ts!, + agentId: events[0].agentName, + log: events, + }; + } +} \ No newline at end of file diff --git a/apps/cli/src/runs/runs.ts b/apps/cli/src/runs/runs.ts new file mode 100644 index 00000000..e4f8dc84 --- /dev/null +++ b/apps/cli/src/runs/runs.ts @@ -0,0 +1,70 @@ +import z from "zod"; +import container from "../di/container.js"; +import { IMessageQueue } from "../application/lib/message-queue.js"; +import { AskHumanResponseEvent, RunEvent, ToolPermissionResponseEvent } from "../entities/run-events.js"; +import { CreateRunOptions, IRunsRepo } from "./repo.js"; +import { IAgentRuntime } from "../agents/runtime.js"; +import { IBus } from "../application/lib/bus.js"; + +export const ToolPermissionAuthorizePayload = ToolPermissionResponseEvent.pick({ + subflow: true, + toolCallId: true, + response: true, +}); + +export const AskHumanResponsePayload = AskHumanResponseEvent.pick({ + subflow: true, + toolCallId: true, + response: true, +}); + +export const Run = z.object({ + id: z.string(), + createdAt: z.iso.datetime(), + agentId: z.string(), + log: z.array(RunEvent), +}); + +export async function createRun(opts: z.infer): Promise> { + const repo = container.resolve('runsRepo'); + const bus = container.resolve('bus'); + const run = await repo.create(opts); + await bus.publish(run.log[0]); + return run; +} + +export async function createMessage(runId: string, message: string): Promise { + const queue = container.resolve('messageQueue'); + const id = await queue.enqueue(runId, message); + const runtime = container.resolve('agentRuntime'); + runtime.trigger(runId); + return id; +} + +export async function authorizePermission(runId: string, ev: z.infer): Promise { + const repo = container.resolve('runsRepo'); + const event: z.infer = { + ...ev, + runId, + type: "tool-permission-response", + }; + await repo.appendEvents(runId, [event]); + const runtime = container.resolve('agentRuntime'); + runtime.trigger(runId); +} + +export async function replyToHumanInputRequest(runId: string, ev: z.infer): Promise { + const repo = container.resolve('runsRepo'); + const event: z.infer = { + ...ev, + runId, + type: "ask-human-response", + }; + await repo.appendEvents(runId, [event]); + const runtime = container.resolve('agentRuntime'); + runtime.trigger(runId); +} + +export async function stop(runId: string): Promise { + throw new Error('Not implemented'); +} \ No newline at end of file diff --git a/apps/cli/src/server.ts b/apps/cli/src/server.ts new file mode 100644 index 00000000..8bf71d1d --- /dev/null +++ b/apps/cli/src/server.ts @@ -0,0 +1,653 @@ +import { Hono } from 'hono'; +import { serve } from '@hono/node-server' +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, ListToolsResponse, McpServerList } from "./mcp/mcp.js"; +import { McpServerDefinition } from "./mcp/mcp.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, ListRunsResponse, CreateRunOptions } from './runs/repo.js'; +import { IBus } from './application/lib/bus.js'; +import { RunEvent } from './entities/run-events.js'; + +let id = 0; + +const routes = new Hono() + .get( + '/health', + describeRoute({ + summary: 'Health check', + description: 'Check if the server is running', + responses: { + 200: { + description: 'Server is running', + content: { + 'application/json': { + schema: resolver(z.object({ + status: z.literal("ok"), + })), + }, + }, + }, + }, + }), + async (c) => { + return c.json({ status: 'ok' }); + } + ) + .get( + '/mcp', + describeRoute({ + summary: 'List MCP servers', + description: 'List the MCP servers', + responses: { + 200: { + description: 'Server list', + content: { + 'application/json': { + schema: resolver(McpServerList), + }, + }, + }, + }, + }), + async (c) => { + return c.json(await listServers()); + } + ) + .put( + '/mcp/:serverName', + describeRoute({ + summary: 'Upsert MCP server', + description: 'Add or edit MCP server', + responses: { + 200: { + description: 'MCP server added / updated', + content: { + 'application/json': { + schema: resolver(z.object({ + success: z.literal(true), + })), + }, + }, + }, + }, + }), + validator('param', z.object({ + serverName: z.string(), + })), + validator('json', McpServerDefinition), + async (c) => { + const repo = container.resolve('mcpConfigRepo'); + await repo.upsert(c.req.valid('param').serverName, c.req.valid('json')); + return c.json({ success: true }); + } + ) + .delete( + '/mcp/:serverName', + describeRoute({ + summary: 'Delete MCP server', + description: 'Delete a MCP server', + responses: { + 200: { + description: 'MCP server deleted', + content: { + 'application/json': { + schema: resolver(z.object({ + success: z.literal(true), + })), + }, + }, + }, + }, + }), + validator('param', z.object({ + serverName: z.string(), + })), + async (c) => { + const repo = container.resolve('mcpConfigRepo'); + await repo.delete(c.req.valid('param').serverName); + return c.json({ success: true }); + } + ) + .get( + '/mcp/:serverName/tools', + describeRoute({ + summary: 'Get MCP tools', + description: 'Get the MCP tools', + responses: { + 200: { + description: 'MCP tools', + content: { + 'application/json': { + schema: resolver(ListToolsResponse), + }, + }, + }, + }, + }), + validator('query', z.object({ + cursor: z.string().optional(), + })), + validator('param', z.object({ + serverName: z.string(), + })), + async (c) => { + const result = await listTools(c.req.valid('param').serverName, c.req.valid('query').cursor); + return c.json(result); + } + ) + .post( + '/mcp/:serverName/tools/:toolName/execute', + describeRoute({ + summary: 'Execute MCP tool', + description: 'Execute a MCP tool', + responses: { + 200: { + description: 'Tool executed', + content: { + 'application/json': { + schema: resolver(z.object({ + result: z.any(), + })), + }, + }, + }, + }, + }), + validator('param', z.object({ + serverName: z.string(), + toolName: z.string(), + })), + validator('json', z.object({ + input: z.any(), + })), + async (c) => { + const result = await executeTool( + c.req.valid('param').serverName, + c.req.valid('param').toolName, + c.req.valid('json').input + ); + return c.json(result); + } + ) + .get( + '/models', + describeRoute({ + summary: 'Get model config', + description: 'Get the current model and provider configuration', + responses: { + 200: { + description: 'Model config', + content: { + 'application/json': { + schema: resolver(ModelConfig), + }, + }, + }, + }, + }), + async (c) => { + const repo = container.resolve('modelConfigRepo'); + const config = await repo.getConfig(); + return c.json(config); + } + ) + .put( + '/models/providers/:providerName', + describeRoute({ + summary: 'Upsert provider config', + description: 'Add or update a provider configuration', + responses: { + 200: { + description: 'Provider upserted', + content: { + 'application/json': { + schema: resolver(z.object({ + success: z.literal(true), + })), + }, + }, + }, + }, + }), + validator('param', z.object({ + providerName: z.string(), + })), + validator('json', Provider), + async (c) => { + const repo = container.resolve('modelConfigRepo'); + await repo.upsert(c.req.valid('param').providerName, c.req.valid('json')); + return c.json({ success: true }); + } + ) + .delete( + '/models/providers/:providerName', + describeRoute({ + summary: 'Delete provider config', + description: 'Delete a provider configuration', + responses: { + 200: { + description: 'Provider deleted', + content: { + 'application/json': { + schema: resolver(z.object({ + success: z.literal(true), + })), + }, + }, + }, + }, + }), + validator('param', z.object({ + providerName: z.string(), + })), + async (c) => { + const repo = container.resolve('modelConfigRepo'); + await repo.delete(c.req.valid('param').providerName); + return c.json({ success: true }); + } + ) + .put( + '/models/default', + describeRoute({ + summary: 'Set default model', + description: 'Set the default provider and model', + responses: { + 200: { + description: 'Default set', + content: { + 'application/json': { + schema: resolver(z.object({ + success: z.literal(true), + })), + }, + }, + }, + }, + }), + validator('json', z.object({ + provider: z.string(), + model: z.string(), + })), + async (c) => { + const repo = container.resolve('modelConfigRepo'); + const body = c.req.valid('json'); + await repo.setDefault(body.provider, body.model); + return c.json({ success: true }); + } + ) + // GET /agents + .get( + '/agents', + describeRoute({ + summary: 'List agents', + description: 'List all configured agents', + responses: { + 200: { + description: 'Agents list', + content: { + 'application/json': { + schema: resolver(z.array(Agent)), + }, + }, + }, + }, + }), + async (c) => { + const repo = container.resolve('agentsRepo'); + const agents = await repo.list(); + return c.json(agents); + } + ) + // POST /agents/new + .post( + '/agents/new', + describeRoute({ + summary: 'Create agent', + description: 'Create a new agent', + responses: { + 200: { + description: 'Agent created', + content: { + 'application/json': { + schema: resolver(z.object({ + success: z.literal(true), + })), + }, + }, + }, + }, + }), + validator('json', Agent), + async (c) => { + const repo = container.resolve('agentsRepo'); + await repo.create(c.req.valid('json')); + return c.json({ success: true }); + } + ) + // GET /agents/ + .get( + '/agents/:id', + describeRoute({ + summary: 'Get agent', + description: 'Fetch a specific agent by id', + responses: { + 200: { + description: 'Agent', + content: { + 'application/json': { + schema: resolver(Agent), + }, + }, + }, + }, + }), + validator('param', z.object({ + id: z.string(), + })), + async (c) => { + const repo = container.resolve('agentsRepo'); + const agent = await repo.fetch(c.req.valid('param').id); + return c.json(agent); + } + ) + // PUT /agents/ + .put( + '/agents/:id', + describeRoute({ + summary: 'Update agent', + description: 'Update an existing agent', + responses: { + 200: { + description: 'Agent updated', + content: { + 'application/json': { + schema: resolver(z.object({ + success: z.literal(true), + })), + }, + }, + }, + }, + }), + validator('param', z.object({ + id: z.string(), + })), + validator('json', Agent), + async (c) => { + const repo = container.resolve('agentsRepo'); + await repo.update(c.req.valid('param').id, c.req.valid('json')); + return c.json({ success: true }); + } + ) + // DELETE /agents/ + .delete( + '/agents/:id', + describeRoute({ + summary: 'Delete agent', + description: 'Delete an agent by id', + responses: { + 200: { + description: 'Agent deleted', + content: { + 'application/json': { + schema: resolver(z.object({ + success: z.literal(true), + })), + }, + }, + }, + }, + }), + validator('param', z.object({ + id: z.string(), + })), + async (c) => { + const repo = container.resolve('agentsRepo'); + await repo.delete(c.req.valid('param').id); + return c.json({ success: true }); + } + ) + .get( + '/runs/:runId', + describeRoute({ + summary: 'Get run', + description: 'Get a run by id', + responses: { + 200: { + description: 'Run', + content: { + 'application/json': { + schema: resolver(Run), + }, + }, + }, + }, + }), + validator('param', z.object({ + runId: z.string(), + })), + async (c) => { + const repo = container.resolve('runsRepo'); + const run = await repo.fetch(c.req.valid('param').runId); + return c.json(run); + } + ) + .post( + '/runs/new', + describeRoute({ + summary: 'Create run', + description: 'Create a new run', + responses: { + 200: { + description: 'Run created', + content: { + 'application/json': { + schema: resolver(Run), + }, + }, + }, + }, + }), + validator('json', CreateRunOptions), + async (c) => { + const run = await createRun(c.req.valid('json')); + return c.json(run); + } + ) + .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', + 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(); + } + }); + + // Subscribe to your bus + unsub = await bus.subscribe('*', async (event) => { + if (aborted) return; + + console.log('got ev', event); + await stream.writeSSE({ + data: JSON.stringify(event), + event: "message", + id: String(id++), + }); + }); + + // Keep the function alive until the client disconnects + while (!aborted) { + await stream.sleep(1000); // any interval is fine + } + }); + } + ) + .get('/sse', async (c) => { + return streamSSE(c, async (stream) => { + while (true) { + const message = `It is ${new Date().toISOString()}` + await stream.writeSSE({ + data: message, + event: 'time-update', + id: String(id++), + }) + await stream.sleep(1000) + } + }) + }) + ; + +const app = new Hono() + .route("/", routes) + .get( + "/openapi.json", + openAPIRouteHandler(routes, { + documentation: { + info: { + title: "Hono", + version: "1.0.0", + description: "RowboatX API", + }, + }, + }), + ); + +// export default app; + +serve({ + fetch: app.fetch, + port: Number(process.env.PORT) || 3000, +}); + +// GET /skills +// POST /skills/new +// GET /skills/ +// PUT /skills/ +// DELETE /skills/ + +// GET /sse \ No newline at end of file From 1822deadc1e297b8335d309857f9270d677a0f53 Mon Sep 17 00:00:00 2001 From: Ramnique Singh <30795890+ramnique@users.noreply.github.com> Date: Mon, 8 Dec 2025 12:33:49 +0530 Subject: [PATCH 02/66] add describe for /stream --- apps/cli/src/server.ts | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/apps/cli/src/server.ts b/apps/cli/src/server.ts index 8bf71d1d..b56f7331 100644 --- a/apps/cli/src/server.ts +++ b/apps/cli/src/server.ts @@ -573,6 +573,10 @@ const routes = new Hono() ) .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'); @@ -607,19 +611,6 @@ const routes = new Hono() }); } ) - .get('/sse', async (c) => { - return streamSSE(c, async (stream) => { - while (true) { - const message = `It is ${new Date().toISOString()}` - await stream.writeSSE({ - data: message, - event: 'time-update', - id: String(id++), - }) - await stream.sleep(1000) - } - }) - }) ; const app = new Hono() From 338cc3d2f9fe9a5cace4ac8ca76e743564b53989 Mon Sep 17 00:00:00 2001 From: Ramnique Singh <30795890+ramnique@users.noreply.github.com> Date: Tue, 9 Dec 2025 16:35:27 +0530 Subject: [PATCH 03/66] add list runs endpoint --- apps/cli/src/runs/repo.ts | 65 +++++++++++++++++++++++++++++++++++++++ apps/cli/src/server.ts | 28 +++++++++++++++-- 2 files changed, 91 insertions(+), 2 deletions(-) diff --git a/apps/cli/src/runs/repo.ts b/apps/cli/src/runs/repo.ts index ed87ebae..5b741f2b 100644 --- a/apps/cli/src/runs/repo.ts +++ b/apps/cli/src/runs/repo.ts @@ -22,6 +22,7 @@ export const CreateRunOptions = Run.pick({ export interface IRunsRepo { create(options: z.infer): Promise>; fetch(id: string): Promise>; + list(cursor?: string): Promise>; appendEvents(runId: string, events: z.infer[]): Promise; } @@ -76,4 +77,68 @@ export class FSRunsRepo implements IRunsRepo { log: events, }; } + + async list(cursor?: string): Promise> { + const runsDir = path.join(WorkDir, 'runs'); + const PAGE_SIZE = 20; + + let files: string[] = []; + try { + const entries = await fsp.readdir(runsDir, { withFileTypes: true }); + files = entries + .filter(e => e.isFile() && e.name.endsWith('.jsonl')) + .map(e => e.name); + } catch (err: any) { + if (err && err.code === 'ENOENT') { + return { runs: [] }; + } + throw err; + } + + files.sort((a, b) => b.localeCompare(a)); + + const cursorFile = cursor; + let startIndex = 0; + if (cursorFile) { + const exact = files.indexOf(cursorFile); + if (exact >= 0) { + startIndex = exact + 1; + } else { + const firstOlder = files.findIndex(name => name.localeCompare(cursorFile) < 0); + startIndex = firstOlder === -1 ? files.length : firstOlder; + } + } + + const selected = files.slice(startIndex, startIndex + PAGE_SIZE); + const runs: z.infer['runs'] = []; + + for (const name of selected) { + const runId = name.slice(0, -'.jsonl'.length); + try { + const contents = await fsp.readFile(path.join(runsDir, name), 'utf8'); + const firstLine = contents.split('\n').find(line => line.trim() !== ''); + if (!firstLine) { + continue; + } + const start = StartEvent.parse(JSON.parse(firstLine)); + runs.push({ + id: runId, + createdAt: start.ts!, + agentId: start.agentName, + }); + } catch { + continue; + } + } + + const hasMore = startIndex + PAGE_SIZE < files.length; + const nextCursor = hasMore && selected.length > 0 + ? selected[selected.length - 1] + : undefined; + + return { + runs, + ...(nextCursor ? { nextCursor } : {}), + }; + } } \ No newline at end of file diff --git a/apps/cli/src/server.ts b/apps/cli/src/server.ts index b56f7331..fde66419 100644 --- a/apps/cli/src/server.ts +++ b/apps/cli/src/server.ts @@ -12,9 +12,8 @@ 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, ListRunsResponse, CreateRunOptions } from './runs/repo.js'; +import { IRunsRepo, CreateRunOptions, ListRunsResponse } from './runs/repo.js'; import { IBus } from './application/lib/bus.js'; -import { RunEvent } from './entities/run-events.js'; let id = 0; @@ -462,6 +461,31 @@ const routes = new Hono() return c.json(run); } ) + .get( + '/runs', + describeRoute({ + summary: 'List runs', + description: 'List all runs', + responses: { + 200: { + description: 'Runs list', + content: { + 'application/json': { + schema: resolver(ListRunsResponse), + }, + }, + }, + }, + }), + validator('query', z.object({ + cursor: z.string().optional(), + })), + async (c) => { + const repo = container.resolve('runsRepo'); + const runs = await repo.list(c.req.valid('query').cursor); + return c.json(runs); + } + ) .post( '/runs/:runId/messages/new', describeRoute({ From f21558e9e585f95cea7d41a5db1485eedb7419bd Mon Sep 17 00:00:00 2001 From: tusharmagar Date: Wed, 10 Dec 2025 23:48:19 +0530 Subject: [PATCH 04/66] first init of next.js proj --- apps/rowboatx/.gitignore | 41 + apps/rowboatx/README.md | 36 + apps/rowboatx/app/favicon.ico | Bin 0 -> 25931 bytes apps/rowboatx/app/globals.css | 122 + apps/rowboatx/app/layout.tsx | 34 + apps/rowboatx/app/page.tsx | 0 apps/rowboatx/components.json | 22 + .../components/ai-elements/artifact.tsx | 147 + .../components/ai-elements/canvas.tsx | 22 + .../ai-elements/chain-of-thought.tsx | 231 + .../components/ai-elements/checkpoint.tsx | 68 + .../components/ai-elements/code-block.tsx | 178 + .../components/ai-elements/confirmation.tsx | 182 + .../components/ai-elements/connection.tsx | 28 + .../components/ai-elements/context.tsx | 408 + .../components/ai-elements/controls.tsx | 18 + .../components/ai-elements/conversation.tsx | 100 + apps/rowboatx/components/ai-elements/edge.tsx | 140 + .../rowboatx/components/ai-elements/image.tsx | 24 + .../ai-elements/inline-citation.tsx | 287 + .../components/ai-elements/loader.tsx | 96 + .../components/ai-elements/message.tsx | 448 + .../components/ai-elements/model-selector.tsx | 205 + apps/rowboatx/components/ai-elements/node.tsx | 71 + .../components/ai-elements/open-in-chat.tsx | 365 + .../rowboatx/components/ai-elements/panel.tsx | 15 + apps/rowboatx/components/ai-elements/plan.tsx | 142 + .../components/ai-elements/prompt-input.tsx | 1413 ++ .../rowboatx/components/ai-elements/queue.tsx | 274 + .../components/ai-elements/reasoning.tsx | 180 + .../components/ai-elements/shimmer.tsx | 64 + .../components/ai-elements/sources.tsx | 77 + .../components/ai-elements/suggestion.tsx | 56 + apps/rowboatx/components/ai-elements/task.tsx | 87 + apps/rowboatx/components/ai-elements/tool.tsx | 165 + .../components/ai-elements/toolbar.tsx | 16 + .../components/ai-elements/web-preview.tsx | 263 + apps/rowboatx/components/ui/alert.tsx | 66 + apps/rowboatx/components/ui/badge.tsx | 46 + apps/rowboatx/components/ui/button-group.tsx | 83 + apps/rowboatx/components/ui/button.tsx | 60 + apps/rowboatx/components/ui/card.tsx | 92 + apps/rowboatx/components/ui/carousel.tsx | 241 + apps/rowboatx/components/ui/collapsible.tsx | 33 + apps/rowboatx/components/ui/command.tsx | 184 + apps/rowboatx/components/ui/dialog.tsx | 143 + apps/rowboatx/components/ui/dropdown-menu.tsx | 257 + apps/rowboatx/components/ui/hover-card.tsx | 44 + apps/rowboatx/components/ui/input-group.tsx | 170 + apps/rowboatx/components/ui/input.tsx | 21 + apps/rowboatx/components/ui/progress.tsx | 31 + apps/rowboatx/components/ui/scroll-area.tsx | 58 + apps/rowboatx/components/ui/select.tsx | 187 + apps/rowboatx/components/ui/separator.tsx | 28 + apps/rowboatx/components/ui/textarea.tsx | 18 + apps/rowboatx/components/ui/tooltip.tsx | 61 + apps/rowboatx/eslint.config.mjs | 25 + apps/rowboatx/lib/utils.ts | 7 + apps/rowboatx/next.config.ts | 7 + apps/rowboatx/package-lock.json | 11103 ++++++++++++++++ apps/rowboatx/package.json | 55 + apps/rowboatx/postcss.config.mjs | 5 + apps/rowboatx/public/file.svg | 1 + apps/rowboatx/public/globe.svg | 1 + apps/rowboatx/public/next.svg | 1 + apps/rowboatx/public/vercel.svg | 1 + apps/rowboatx/public/window.svg | 1 + apps/rowboatx/tsconfig.json | 27 + 68 files changed, 19082 insertions(+) create mode 100644 apps/rowboatx/.gitignore create mode 100644 apps/rowboatx/README.md create mode 100644 apps/rowboatx/app/favicon.ico create mode 100644 apps/rowboatx/app/globals.css create mode 100644 apps/rowboatx/app/layout.tsx create mode 100644 apps/rowboatx/app/page.tsx create mode 100644 apps/rowboatx/components.json create mode 100644 apps/rowboatx/components/ai-elements/artifact.tsx create mode 100644 apps/rowboatx/components/ai-elements/canvas.tsx create mode 100644 apps/rowboatx/components/ai-elements/chain-of-thought.tsx create mode 100644 apps/rowboatx/components/ai-elements/checkpoint.tsx create mode 100644 apps/rowboatx/components/ai-elements/code-block.tsx create mode 100644 apps/rowboatx/components/ai-elements/confirmation.tsx create mode 100644 apps/rowboatx/components/ai-elements/connection.tsx create mode 100644 apps/rowboatx/components/ai-elements/context.tsx create mode 100644 apps/rowboatx/components/ai-elements/controls.tsx create mode 100644 apps/rowboatx/components/ai-elements/conversation.tsx create mode 100644 apps/rowboatx/components/ai-elements/edge.tsx create mode 100644 apps/rowboatx/components/ai-elements/image.tsx create mode 100644 apps/rowboatx/components/ai-elements/inline-citation.tsx create mode 100644 apps/rowboatx/components/ai-elements/loader.tsx create mode 100644 apps/rowboatx/components/ai-elements/message.tsx create mode 100644 apps/rowboatx/components/ai-elements/model-selector.tsx create mode 100644 apps/rowboatx/components/ai-elements/node.tsx create mode 100644 apps/rowboatx/components/ai-elements/open-in-chat.tsx create mode 100644 apps/rowboatx/components/ai-elements/panel.tsx create mode 100644 apps/rowboatx/components/ai-elements/plan.tsx create mode 100644 apps/rowboatx/components/ai-elements/prompt-input.tsx create mode 100644 apps/rowboatx/components/ai-elements/queue.tsx create mode 100644 apps/rowboatx/components/ai-elements/reasoning.tsx create mode 100644 apps/rowboatx/components/ai-elements/shimmer.tsx create mode 100644 apps/rowboatx/components/ai-elements/sources.tsx create mode 100644 apps/rowboatx/components/ai-elements/suggestion.tsx create mode 100644 apps/rowboatx/components/ai-elements/task.tsx create mode 100644 apps/rowboatx/components/ai-elements/tool.tsx create mode 100644 apps/rowboatx/components/ai-elements/toolbar.tsx create mode 100644 apps/rowboatx/components/ai-elements/web-preview.tsx create mode 100644 apps/rowboatx/components/ui/alert.tsx create mode 100644 apps/rowboatx/components/ui/badge.tsx create mode 100644 apps/rowboatx/components/ui/button-group.tsx create mode 100644 apps/rowboatx/components/ui/button.tsx create mode 100644 apps/rowboatx/components/ui/card.tsx create mode 100644 apps/rowboatx/components/ui/carousel.tsx create mode 100644 apps/rowboatx/components/ui/collapsible.tsx create mode 100644 apps/rowboatx/components/ui/command.tsx create mode 100644 apps/rowboatx/components/ui/dialog.tsx create mode 100644 apps/rowboatx/components/ui/dropdown-menu.tsx create mode 100644 apps/rowboatx/components/ui/hover-card.tsx create mode 100644 apps/rowboatx/components/ui/input-group.tsx create mode 100644 apps/rowboatx/components/ui/input.tsx create mode 100644 apps/rowboatx/components/ui/progress.tsx create mode 100644 apps/rowboatx/components/ui/scroll-area.tsx create mode 100644 apps/rowboatx/components/ui/select.tsx create mode 100644 apps/rowboatx/components/ui/separator.tsx create mode 100644 apps/rowboatx/components/ui/textarea.tsx create mode 100644 apps/rowboatx/components/ui/tooltip.tsx create mode 100644 apps/rowboatx/eslint.config.mjs create mode 100644 apps/rowboatx/lib/utils.ts create mode 100644 apps/rowboatx/next.config.ts create mode 100644 apps/rowboatx/package-lock.json create mode 100644 apps/rowboatx/package.json create mode 100644 apps/rowboatx/postcss.config.mjs create mode 100644 apps/rowboatx/public/file.svg create mode 100644 apps/rowboatx/public/globe.svg create mode 100644 apps/rowboatx/public/next.svg create mode 100644 apps/rowboatx/public/vercel.svg create mode 100644 apps/rowboatx/public/window.svg create mode 100644 apps/rowboatx/tsconfig.json diff --git a/apps/rowboatx/.gitignore b/apps/rowboatx/.gitignore new file mode 100644 index 00000000..5ef6a520 --- /dev/null +++ b/apps/rowboatx/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/apps/rowboatx/README.md b/apps/rowboatx/README.md new file mode 100644 index 00000000..e215bc4c --- /dev/null +++ b/apps/rowboatx/README.md @@ -0,0 +1,36 @@ +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). + +## Getting Started + +First, run the development server: + +```bash +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. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +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. + +## 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. diff --git a/apps/rowboatx/app/favicon.ico b/apps/rowboatx/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..718d6fea4835ec2d246af9800eddb7ffb276240c GIT binary patch literal 25931 zcmeHv30#a{`}aL_*G&7qml|y<+KVaDM2m#dVr!KsA!#An?kSQM(q<_dDNCpjEux83 zLb9Z^XxbDl(w>%i@8hT6>)&Gu{h#Oeyszu?xtw#Zb1mO{pgX9699l+Qppw7jXaYf~-84xW z)w4x8?=youko|}Vr~(D$UXIbiXABHh`p1?nn8Po~fxRJv}|0e(BPs|G`(TT%kKVJAdg5*Z|x0leQq0 zkdUBvb#>9F()jo|T~kx@OM8$9wzs~t2l;K=woNssA3l6|sx2r3+kdfVW@e^8e*E}v zA1y5{bRi+3Z`uD3{F7LgFJDdvm;nJilkzDku>BwXH(8ItVCXk*-lSJnR?-2UN%hJ){&rlvg`CDTj z)Bzo!3v7Ou#83zEDEFcKt(f1E0~=rqeEbTnMvWR#{+9pg%7G8y>u1OVRUSoox-ovF z2Ydma(;=YuBY(eI|04{hXzZD6_f(v~H;C~y5=DhAC{MMS>2fm~1H_t2$56pc$NH8( z5bH|<)71dV-_oCHIrzrT`2s-5w_+2CM0$95I6X8p^r!gHp+j_gd;9O<1~CEQQGS8) zS9Qh3#p&JM-G8rHekNmKVewU;pJRcTAog68KYo^dRo}(M>36U4Us zfgYWSiHZL3;lpWT=zNAW>Dh#mB!_@Lg%$ms8N-;aPqMn+C2HqZgz&9~Eu z4|Kp<`$q)Uw1R?y(~S>ePdonHxpV1#eSP1B;Ogo+-Pk}6#0GsZZ5!||ev2MGdh}_m z{DeR7?0-1^zVs&`AV6Vt;r3`I`OI_wgs*w=eO%_#7Kepl{B@xiyCANc(l zzIyd4y|c6PXWq9-|KM8(zIk8LPk(>a)zyFWjhT!$HJ$qX1vo@d25W<fvZQ2zUz5WRc(UnFMKHwe1| zWmlB1qdbiA(C0jmnV<}GfbKtmcu^2*P^O?MBLZKt|As~ge8&AAO~2K@zbXelK|4T<{|y4`raF{=72kC2Kn(L4YyenWgrPiv z@^mr$t{#X5VuIMeL!7Ab6_kG$&#&5p*Z{+?5U|TZ`B!7llpVmp@skYz&n^8QfPJzL z0G6K_OJM9x+Wu2gfN45phANGt{7=C>i34CV{Xqlx(fWpeAoj^N0Biu`w+MVcCUyU* zDZuzO0>4Z6fbu^T_arWW5n!E45vX8N=bxTVeFoep_G#VmNlQzAI_KTIc{6>c+04vr zx@W}zE5JNSU>!THJ{J=cqjz+4{L4A{Ob9$ZJ*S1?Ggg3klFp!+Y1@K+pK1DqI|_gq z5ZDXVpge8-cs!o|;K73#YXZ3AShj50wBvuq3NTOZ`M&qtjj#GOFfgExjg8Gn8>Vq5 z`85n+9|!iLCZF5$HJ$Iu($dm?8~-ofu}tEc+-pyke=3!im#6pk_Wo8IA|fJwD&~~F zc16osQ)EBo58U7XDuMexaPRjU@h8tXe%S{fA0NH3vGJFhuyyO!Uyl2^&EOpX{9As0 zWj+P>{@}jxH)8|r;2HdupP!vie{sJ28b&bo!8`D^x}TE$%zXNb^X1p@0PJ86`dZyj z%ce7*{^oo+6%&~I!8hQy-vQ7E)0t0ybH4l%KltWOo~8cO`T=157JqL(oq_rC%ea&4 z2NcTJe-HgFjNg-gZ$6!Y`SMHrlj}Etf7?r!zQTPPSv}{so2e>Fjs1{gzk~LGeesX%r(Lh6rbhSo_n)@@G-FTQy93;l#E)hgP@d_SGvyCp0~o(Y;Ee8{ zdVUDbHm5`2taPUOY^MAGOw*>=s7=Gst=D+p+2yON!0%Hk` zz5mAhyT4lS*T3LS^WSxUy86q&GnoHxzQ6vm8)VS}_zuqG?+3td68_x;etQAdu@sc6 zQJ&5|4(I?~3d-QOAODHpZ=hlSg(lBZ!JZWCtHHSj`0Wh93-Uk)_S%zsJ~aD>{`A0~ z9{AG(e|q3g5B%wYKRxiL2Y$8(4w6bzchKuloQW#e&S3n+P- z8!ds-%f;TJ1>)v)##>gd{PdS2Oc3VaR`fr=`O8QIO(6(N!A?pr5C#6fc~Ge@N%Vvu zaoAX2&(a6eWy_q&UwOhU)|P3J0Qc%OdhzW=F4D|pt0E4osw;%<%Dn58hAWD^XnZD= z>9~H(3bmLtxpF?a7su6J7M*x1By7YSUbxGi)Ot0P77`}P3{)&5Un{KD?`-e?r21!4vTTnN(4Y6Lin?UkSM z`MXCTC1@4A4~mvz%Rh2&EwY))LeoT=*`tMoqcEXI>TZU9WTP#l?uFv+@Dn~b(>xh2 z;>B?;Tz2SR&KVb>vGiBSB`@U7VIWFSo=LDSb9F{GF^DbmWAfpms8Sx9OX4CnBJca3 zlj9(x!dIjN?OG1X4l*imJNvRCk}F%!?SOfiOq5y^mZW)jFL@a|r-@d#f7 z2gmU8L3IZq0ynIws=}~m^#@&C%J6QFo~Mo4V`>v7MI-_!EBMMtb%_M&kvAaN)@ZVw z+`toz&WG#HkWDjnZE!6nk{e-oFdL^$YnbOCN}JC&{$#$O27@|Tn-skXr)2ml2~O!5 zX+gYoxhoc7qoU?C^3~&!U?kRFtnSEecWuH0B0OvLodgUAi}8p1 zrO6RSXHH}DMc$&|?D004DiOVMHV8kXCP@7NKB zgaZq^^O<7PoKEp72kby@W0Z!Y*Ay{&vfg#C&gG@YVR9g?FEocMUi1gSN$+V+ayF45{a zuDZDTN}mS|;BO%gEf}pjBfN2-gIrU#G5~cucA;dokXW89%>AyXJJI z9X4UlIWA|ZYHgbI z5?oFk@A=Ik7lrEQPDH!H+b`7_Y~aDb_qa=B2^Y&Ow41cU=4WDd40dp5(QS-WMN-=Y z9g;6_-JdNU;|6cPwf$ak*aJIcwL@1n$#l~zi{c{EW?T;DaW*E8DYq?Umtz{nJ&w-M zEMyTDrC&9K$d|kZe2#ws6)L=7K+{ zQw{XnV6UC$6-rW0emqm8wJoeZK)wJIcV?dST}Z;G0Arq{dVDu0&4kd%N!3F1*;*pW zR&qUiFzK=@44#QGw7k1`3t_d8&*kBV->O##t|tonFc2YWrL7_eqg+=+k;!F-`^b8> z#KWCE8%u4k@EprxqiV$VmmtiWxDLgnGu$Vs<8rppV5EajBXL4nyyZM$SWVm!wnCj-B!Wjqj5-5dNXukI2$$|Bu3Lrw}z65Lc=1G z^-#WuQOj$hwNGG?*CM_TO8Bg-1+qc>J7k5c51U8g?ZU5n?HYor;~JIjoWH-G>AoUP ztrWWLbRNqIjW#RT*WqZgPJXU7C)VaW5}MiijYbABmzoru6EmQ*N8cVK7a3|aOB#O& zBl8JY2WKfmj;h#Q!pN%9o@VNLv{OUL?rixHwOZuvX7{IJ{(EdPpuVFoQqIOa7giLVkBOKL@^smUA!tZ1CKRK}#SSM)iQHk)*R~?M!qkCruaS!#oIL1c z?J;U~&FfH#*98^G?i}pA{ z9Jg36t4=%6mhY(quYq*vSxptes9qy|7xSlH?G=S@>u>Ebe;|LVhs~@+06N<4CViBk zUiY$thvX;>Tby6z9Y1edAMQaiH zm^r3v#$Q#2T=X>bsY#D%s!bhs^M9PMAcHbCc0FMHV{u-dwlL;a1eJ63v5U*?Q_8JO zT#50!RD619#j_Uf))0ooADz~*9&lN!bBDRUgE>Vud-i5ck%vT=r^yD*^?Mp@Q^v+V zG#-?gKlr}Eeqifb{|So?HM&g91P8|av8hQoCmQXkd?7wIJwb z_^v8bbg`SAn{I*4bH$u(RZ6*xUhuA~hc=8czK8SHEKTzSxgbwi~9(OqJB&gwb^l4+m`k*Q;_?>Y-APi1{k zAHQ)P)G)f|AyjSgcCFps)Fh6Bca*Xznq36!pV6Az&m{O8$wGFD? zY&O*3*J0;_EqM#jh6^gMQKpXV?#1?>$ml1xvh8nSN>-?H=V;nJIwB07YX$e6vLxH( zqYwQ>qxwR(i4f)DLd)-$P>T-no_c!LsN@)8`e;W@)-Hj0>nJ-}Kla4-ZdPJzI&Mce zv)V_j;(3ERN3_@I$N<^|4Lf`B;8n+bX@bHbcZTopEmDI*Jfl)-pFDvo6svPRoo@(x z);_{lY<;);XzT`dBFpRmGrr}z5u1=pC^S-{ce6iXQlLGcItwJ^mZx{m$&DA_oEZ)B{_bYPq-HA zcH8WGoBG(aBU_j)vEy+_71T34@4dmSg!|M8Vf92Zj6WH7Q7t#OHQqWgFE3ARt+%!T z?oLovLVlnf?2c7pTc)~cc^($_8nyKwsN`RA-23ed3sdj(ys%pjjM+9JrctL;dy8a( z@en&CQmnV(()bu|Y%G1-4a(6x{aLytn$T-;(&{QIJB9vMox11U-1HpD@d(QkaJdEb zG{)+6Dos_L+O3NpWo^=gR?evp|CqEG?L&Ut#D*KLaRFOgOEK(Kq1@!EGcTfo+%A&I z=dLbB+d$u{sh?u)xP{PF8L%;YPPW53+@{>5W=Jt#wQpN;0_HYdw1{ksf_XhO4#2F= zyPx6Lx2<92L-;L5PD`zn6zwIH`Jk($?Qw({erA$^bC;q33hv!d!>%wRhj# zal^hk+WGNg;rJtb-EB(?czvOM=H7dl=vblBwAv>}%1@{}mnpUznfq1cE^sgsL0*4I zJ##!*B?=vI_OEVis5o+_IwMIRrpQyT_Sq~ZU%oY7c5JMIADzpD!Upz9h@iWg_>>~j zOLS;wp^i$-E?4<_cp?RiS%Rd?i;f*mOz=~(&3lo<=@(nR!_Rqiprh@weZlL!t#NCc zO!QTcInq|%#>OVgobj{~ixEUec`E25zJ~*DofsQdzIa@5^nOXj2T;8O`l--(QyU^$t?TGY^7#&FQ+2SS3B#qK*k3`ye?8jUYSajE5iBbJls75CCc(m3dk{t?- zopcER9{Z?TC)mk~gpi^kbbu>b-+a{m#8-y2^p$ka4n60w;Sc2}HMf<8JUvhCL0B&Btk)T`ctE$*qNW8L$`7!r^9T+>=<=2qaq-;ll2{`{Rg zc5a0ZUI$oG&j-qVOuKa=*v4aY#IsoM+1|c4Z)<}lEDvy;5huB@1RJPquU2U*U-;gu z=En2m+qjBzR#DEJDO`WU)hdd{Vj%^0V*KoyZ|5lzV87&g_j~NCjwv0uQVqXOb*QrQ zy|Qn`hxx(58c70$E;L(X0uZZ72M1!6oeg)(cdKO ze0gDaTz+ohR-#d)NbAH4x{I(21yjwvBQfmpLu$)|m{XolbgF!pmsqJ#D}(ylp6uC> z{bqtcI#hT#HW=wl7>p!38sKsJ`r8}lt-q%Keqy%u(xk=yiIJiUw6|5IvkS+#?JTBl z8H5(Q?l#wzazujH!8o>1xtn8#_w+397*_cy8!pQGP%K(Ga3pAjsaTbbXJlQF_+m+-UpUUent@xM zg%jqLUExj~o^vQ3Gl*>wh=_gOr2*|U64_iXb+-111aH}$TjeajM+I20xw(((>fej-@CIz4S1pi$(#}P7`4({6QS2CaQS4NPENDp>sAqD z$bH4KGzXGffkJ7R>V>)>tC)uax{UsN*dbeNC*v}#8Y#OWYwL4t$ePR?VTyIs!wea+ z5Urmc)X|^`MG~*dS6pGSbU+gPJoq*^a=_>$n4|P^w$sMBBy@f*Z^Jg6?n5?oId6f{ z$LW4M|4m502z0t7g<#Bx%X;9<=)smFolV&(V^(7Cv2-sxbxopQ!)*#ZRhTBpx1)Fc zNm1T%bONzv6@#|dz(w02AH8OXe>kQ#1FMCzO}2J_mST)+ExmBr9cva-@?;wnmWMOk z{3_~EX_xadgJGv&H@zK_8{(x84`}+c?oSBX*Ge3VdfTt&F}yCpFP?CpW+BE^cWY0^ zb&uBN!Ja3UzYHK-CTyA5=L zEMW{l3Usky#ly=7px648W31UNV@K)&Ub&zP1c7%)`{);I4b0Q<)B}3;NMG2JH=X$U zfIW4)4n9ZM`-yRj67I)YSLDK)qfUJ_ij}a#aZN~9EXrh8eZY2&=uY%2N0UFF7<~%M zsB8=erOWZ>Ct_#^tHZ|*q`H;A)5;ycw*IcmVxi8_0Xk}aJA^ath+E;xg!x+As(M#0=)3!NJR6H&9+zd#iP(m0PIW8$ z1Y^VX`>jm`W!=WpF*{ioM?C9`yOR>@0q=u7o>BP-eSHqCgMDj!2anwH?s%i2p+Q7D zzszIf5XJpE)IG4;d_(La-xenmF(tgAxK`Y4sQ}BSJEPs6N_U2vI{8=0C_F?@7<(G; zo$~G=8p+076G;`}>{MQ>t>7cm=zGtfbdDXm6||jUU|?X?CaE?(<6bKDYKeHlz}DA8 zXT={X=yp_R;HfJ9h%?eWvQ!dRgz&Su*JfNt!Wu>|XfU&68iRikRrHRW|ZxzRR^`eIGt zIeiDgVS>IeExKVRWW8-=A=yA`}`)ZkWBrZD`hpWIxBGkh&f#ijr449~m`j6{4jiJ*C!oVA8ZC?$1RM#K(_b zL9TW)kN*Y4%^-qPpMP7d4)o?Nk#>aoYHT(*g)qmRUb?**F@pnNiy6Fv9rEiUqD(^O zzyS?nBrX63BTRYduaG(0VVG2yJRe%o&rVrLjbxTaAFTd8s;<<@Qs>u(<193R8>}2_ zuwp{7;H2a*X7_jryzriZXMg?bTuegABb^87@SsKkr2)0Gyiax8KQWstw^v#ix45EVrcEhr>!NMhprl$InQMzjSFH54x5k9qHc`@9uKQzvL4ihcq{^B zPrVR=o_ic%Y>6&rMN)hTZsI7I<3&`#(nl+3y3ys9A~&^=4?PL&nd8)`OfG#n zwAMN$1&>K++c{^|7<4P=2y(B{jJsQ0a#U;HTo4ZmWZYvI{+s;Td{Yzem%0*k#)vjpB zia;J&>}ICate44SFYY3vEelqStQWFihx%^vQ@Do(sOy7yR2@WNv7Y9I^yL=nZr3mb zXKV5t@=?-Sk|b{XMhA7ZGB@2hqsx}4xwCW!in#C zI@}scZlr3-NFJ@NFaJlhyfcw{k^vvtGl`N9xSo**rDW4S}i zM9{fMPWo%4wYDG~BZ18BD+}h|GQKc-g^{++3MY>}W_uq7jGHx{mwE9fZiPCoxN$+7 zrODGGJrOkcPQUB(FD5aoS4g~7#6NR^ma7-!>mHuJfY5kTe6PpNNKC9GGRiu^L31uG z$7v`*JknQHsYB!Tm_W{a32TM099djW%5e+j0Ve_ct}IM>XLF1Ap+YvcrLV=|CKo6S zb+9Nl3_YdKP6%Cxy@6TxZ>;4&nTneadr z_ES90ydCev)LV!dN=#(*f}|ZORFdvkYBni^aLbUk>BajeWIOcmHP#8S)*2U~QKI%S zyrLmtPqb&TphJ;>yAxri#;{uyk`JJqODDw%(Z=2`1uc}br^V%>j!gS)D*q*f_-qf8&D;W1dJgQMlaH5er zN2U<%Smb7==vE}dDI8K7cKz!vs^73o9f>2sgiTzWcwY|BMYHH5%Vn7#kiw&eItCqa zIkR2~Q}>X=Ar8W|^Ms41Fm8o6IB2_j60eOeBB1Br!boW7JnoeX6Gs)?7rW0^5psc- zjS16yb>dFn>KPOF;imD}e!enuIniFzv}n$m2#gCCv4jM#ArwlzZ$7@9&XkFxZ4n!V zj3dyiwW4Ki2QG{@i>yuZXQizw_OkZI^-3otXC{!(lUpJF33gI60ak;Uqitp74|B6I zgg{b=Iz}WkhCGj1M=hu4#Aw173YxIVbISaoc z-nLZC*6Tgivd5V`K%GxhBsp@SUU60-rfc$=wb>zdJzXS&-5(NRRodFk;Kxk!S(O(a0e7oY=E( zAyS;Ow?6Q&XA+cnkCb{28_1N8H#?J!*$MmIwLq^*T_9-z^&UE@A(z9oGYtFy6EZef LrJugUA?W`A8`#=m literal 0 HcmV?d00001 diff --git a/apps/rowboatx/app/globals.css b/apps/rowboatx/app/globals.css new file mode 100644 index 00000000..dc98be74 --- /dev/null +++ b/apps/rowboatx/app/globals.css @@ -0,0 +1,122 @@ +@import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); +} + +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/apps/rowboatx/app/layout.tsx b/apps/rowboatx/app/layout.tsx new file mode 100644 index 00000000..f7fa87eb --- /dev/null +++ b/apps/rowboatx/app/layout.tsx @@ -0,0 +1,34 @@ +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import "./globals.css"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +export const metadata: Metadata = { + title: "Create Next App", + description: "Generated by create next app", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + ); +} diff --git a/apps/rowboatx/app/page.tsx b/apps/rowboatx/app/page.tsx new file mode 100644 index 00000000..e69de29b diff --git a/apps/rowboatx/components.json b/apps/rowboatx/components.json new file mode 100644 index 00000000..b7b9791c --- /dev/null +++ b/apps/rowboatx/components.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": {} +} diff --git a/apps/rowboatx/components/ai-elements/artifact.tsx b/apps/rowboatx/components/ai-elements/artifact.tsx new file mode 100644 index 00000000..c90cb5fe --- /dev/null +++ b/apps/rowboatx/components/ai-elements/artifact.tsx @@ -0,0 +1,147 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { cn } from "@/lib/utils"; +import { type LucideIcon, XIcon } from "lucide-react"; +import type { ComponentProps, HTMLAttributes } from "react"; + +export type ArtifactProps = HTMLAttributes; + +export const Artifact = ({ className, ...props }: ArtifactProps) => ( +
+); + +export type ArtifactHeaderProps = HTMLAttributes; + +export const ArtifactHeader = ({ + className, + ...props +}: ArtifactHeaderProps) => ( +
+); + +export type ArtifactCloseProps = ComponentProps; + +export const ArtifactClose = ({ + className, + children, + size = "sm", + variant = "ghost", + ...props +}: ArtifactCloseProps) => ( + +); + +export type ArtifactTitleProps = HTMLAttributes; + +export const ArtifactTitle = ({ className, ...props }: ArtifactTitleProps) => ( +

+); + +export type ArtifactDescriptionProps = HTMLAttributes; + +export const ArtifactDescription = ({ + className, + ...props +}: ArtifactDescriptionProps) => ( +

+); + +export type ArtifactActionsProps = HTMLAttributes; + +export const ArtifactActions = ({ + className, + ...props +}: ArtifactActionsProps) => ( +

+); + +export type ArtifactActionProps = ComponentProps & { + tooltip?: string; + label?: string; + icon?: LucideIcon; +}; + +export const ArtifactAction = ({ + tooltip, + label, + icon: Icon, + children, + className, + size = "sm", + variant = "ghost", + ...props +}: ArtifactActionProps) => { + const button = ( + + ); + + if (tooltip) { + return ( + + + {button} + +

{tooltip}

+
+
+
+ ); + } + + return button; +}; + +export type ArtifactContentProps = HTMLAttributes; + +export const ArtifactContent = ({ + className, + ...props +}: ArtifactContentProps) => ( +
+); diff --git a/apps/rowboatx/components/ai-elements/canvas.tsx b/apps/rowboatx/components/ai-elements/canvas.tsx new file mode 100644 index 00000000..5aa83cb5 --- /dev/null +++ b/apps/rowboatx/components/ai-elements/canvas.tsx @@ -0,0 +1,22 @@ +import { Background, ReactFlow, type ReactFlowProps } from "@xyflow/react"; +import type { ReactNode } from "react"; +import "@xyflow/react/dist/style.css"; + +type CanvasProps = ReactFlowProps & { + children?: ReactNode; +}; + +export const Canvas = ({ children, ...props }: CanvasProps) => ( + + + {children} + +); diff --git a/apps/rowboatx/components/ai-elements/chain-of-thought.tsx b/apps/rowboatx/components/ai-elements/chain-of-thought.tsx new file mode 100644 index 00000000..195c465c --- /dev/null +++ b/apps/rowboatx/components/ai-elements/chain-of-thought.tsx @@ -0,0 +1,231 @@ +"use client"; + +import { useControllableState } from "@radix-ui/react-use-controllable-state"; +import { Badge } from "@/components/ui/badge"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; +import { cn } from "@/lib/utils"; +import { + BrainIcon, + ChevronDownIcon, + DotIcon, + type LucideIcon, +} from "lucide-react"; +import type { ComponentProps, ReactNode } from "react"; +import { createContext, memo, useContext, useMemo } from "react"; + +type ChainOfThoughtContextValue = { + isOpen: boolean; + setIsOpen: (open: boolean) => void; +}; + +const ChainOfThoughtContext = createContext( + null +); + +const useChainOfThought = () => { + const context = useContext(ChainOfThoughtContext); + if (!context) { + throw new Error( + "ChainOfThought components must be used within ChainOfThought" + ); + } + return context; +}; + +export type ChainOfThoughtProps = ComponentProps<"div"> & { + open?: boolean; + defaultOpen?: boolean; + onOpenChange?: (open: boolean) => void; +}; + +export const ChainOfThought = memo( + ({ + className, + open, + defaultOpen = false, + onOpenChange, + children, + ...props + }: ChainOfThoughtProps) => { + const [isOpen, setIsOpen] = useControllableState({ + prop: open, + defaultProp: defaultOpen, + onChange: onOpenChange, + }); + + const chainOfThoughtContext = useMemo( + () => ({ isOpen, setIsOpen }), + [isOpen, setIsOpen] + ); + + return ( + +
+ {children} +
+
+ ); + } +); + +export type ChainOfThoughtHeaderProps = ComponentProps< + typeof CollapsibleTrigger +>; + +export const ChainOfThoughtHeader = memo( + ({ className, children, ...props }: ChainOfThoughtHeaderProps) => { + const { isOpen, setIsOpen } = useChainOfThought(); + + return ( + + + + + {children ?? "Chain of Thought"} + + + + + ); + } +); + +export type ChainOfThoughtStepProps = ComponentProps<"div"> & { + icon?: LucideIcon; + label: ReactNode; + description?: ReactNode; + status?: "complete" | "active" | "pending"; +}; + +export const ChainOfThoughtStep = memo( + ({ + className, + icon: Icon = DotIcon, + label, + description, + status = "complete", + children, + ...props + }: ChainOfThoughtStepProps) => { + const statusStyles = { + complete: "text-muted-foreground", + active: "text-foreground", + pending: "text-muted-foreground/50", + }; + + return ( +
+
+ +
+
+
+
{label}
+ {description && ( +
{description}
+ )} + {children} +
+
+ ); + } +); + +export type ChainOfThoughtSearchResultsProps = ComponentProps<"div">; + +export const ChainOfThoughtSearchResults = memo( + ({ className, ...props }: ChainOfThoughtSearchResultsProps) => ( +
+ ) +); + +export type ChainOfThoughtSearchResultProps = ComponentProps; + +export const ChainOfThoughtSearchResult = memo( + ({ className, children, ...props }: ChainOfThoughtSearchResultProps) => ( + + {children} + + ) +); + +export type ChainOfThoughtContentProps = ComponentProps< + typeof CollapsibleContent +>; + +export const ChainOfThoughtContent = memo( + ({ className, children, ...props }: ChainOfThoughtContentProps) => { + const { isOpen } = useChainOfThought(); + + return ( + + + {children} + + + ); + } +); + +export type ChainOfThoughtImageProps = ComponentProps<"div"> & { + caption?: string; +}; + +export const ChainOfThoughtImage = memo( + ({ className, children, caption, ...props }: ChainOfThoughtImageProps) => ( +
+
+ {children} +
+ {caption &&

{caption}

} +
+ ) +); + +ChainOfThought.displayName = "ChainOfThought"; +ChainOfThoughtHeader.displayName = "ChainOfThoughtHeader"; +ChainOfThoughtStep.displayName = "ChainOfThoughtStep"; +ChainOfThoughtSearchResults.displayName = "ChainOfThoughtSearchResults"; +ChainOfThoughtSearchResult.displayName = "ChainOfThoughtSearchResult"; +ChainOfThoughtContent.displayName = "ChainOfThoughtContent"; +ChainOfThoughtImage.displayName = "ChainOfThoughtImage"; diff --git a/apps/rowboatx/components/ai-elements/checkpoint.tsx b/apps/rowboatx/components/ai-elements/checkpoint.tsx new file mode 100644 index 00000000..d9a5d326 --- /dev/null +++ b/apps/rowboatx/components/ai-elements/checkpoint.tsx @@ -0,0 +1,68 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { Separator } from "@/components/ui/separator"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { cn } from "@/lib/utils"; +import { BookmarkIcon, type LucideProps } from "lucide-react"; +import type { ComponentProps, HTMLAttributes } from "react"; + +export type CheckpointProps = HTMLAttributes; + +export const Checkpoint = ({ + className, + children, + ...props +}: CheckpointProps) => ( +
+ {children} + +
+); + +export type CheckpointIconProps = LucideProps; + +export const CheckpointIcon = ({ + className, + children, + ...props +}: CheckpointIconProps) => + children ?? ( + + ); + +export type CheckpointTriggerProps = ComponentProps & { + tooltip?: string; +}; + +export const CheckpointTrigger = ({ + children, + className, + variant = "ghost", + size = "sm", + tooltip, + ...props +}: CheckpointTriggerProps) => + tooltip ? ( + + + + + + {tooltip} + + + ) : ( + + ); diff --git a/apps/rowboatx/components/ai-elements/code-block.tsx b/apps/rowboatx/components/ai-elements/code-block.tsx new file mode 100644 index 00000000..b6865f0d --- /dev/null +++ b/apps/rowboatx/components/ai-elements/code-block.tsx @@ -0,0 +1,178 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; +import { CheckIcon, CopyIcon } from "lucide-react"; +import { + type ComponentProps, + createContext, + type HTMLAttributes, + useContext, + useEffect, + useRef, + useState, +} from "react"; +import { type BundledLanguage, codeToHtml, type ShikiTransformer } from "shiki"; + +type CodeBlockProps = HTMLAttributes & { + code: string; + language: BundledLanguage; + showLineNumbers?: boolean; +}; + +type CodeBlockContextType = { + code: string; +}; + +const CodeBlockContext = createContext({ + code: "", +}); + +const lineNumberTransformer: ShikiTransformer = { + name: "line-numbers", + line(node, line) { + node.children.unshift({ + type: "element", + tagName: "span", + properties: { + className: [ + "inline-block", + "min-w-10", + "mr-4", + "text-right", + "select-none", + "text-muted-foreground", + ], + }, + children: [{ type: "text", value: String(line) }], + }); + }, +}; + +export async function highlightCode( + code: string, + language: BundledLanguage, + showLineNumbers = false +) { + const transformers: ShikiTransformer[] = showLineNumbers + ? [lineNumberTransformer] + : []; + + return await Promise.all([ + codeToHtml(code, { + lang: language, + theme: "one-light", + transformers, + }), + codeToHtml(code, { + lang: language, + theme: "one-dark-pro", + transformers, + }), + ]); +} + +export const CodeBlock = ({ + code, + language, + showLineNumbers = false, + className, + children, + ...props +}: CodeBlockProps) => { + const [html, setHtml] = useState(""); + const [darkHtml, setDarkHtml] = useState(""); + const mounted = useRef(false); + + useEffect(() => { + highlightCode(code, language, showLineNumbers).then(([light, dark]) => { + if (!mounted.current) { + setHtml(light); + setDarkHtml(dark); + mounted.current = true; + } + }); + + return () => { + mounted.current = false; + }; + }, [code, language, showLineNumbers]); + + return ( + +
+
+
+
+ {children && ( +
+ {children} +
+ )} +
+
+ + ); +}; + +export type CodeBlockCopyButtonProps = ComponentProps & { + onCopy?: () => void; + onError?: (error: Error) => void; + timeout?: number; +}; + +export const CodeBlockCopyButton = ({ + onCopy, + onError, + timeout = 2000, + children, + className, + ...props +}: CodeBlockCopyButtonProps) => { + const [isCopied, setIsCopied] = useState(false); + const { code } = useContext(CodeBlockContext); + + const copyToClipboard = async () => { + if (typeof window === "undefined" || !navigator?.clipboard?.writeText) { + onError?.(new Error("Clipboard API not available")); + return; + } + + try { + await navigator.clipboard.writeText(code); + setIsCopied(true); + onCopy?.(); + setTimeout(() => setIsCopied(false), timeout); + } catch (error) { + onError?.(error as Error); + } + }; + + const Icon = isCopied ? CheckIcon : CopyIcon; + + return ( + + ); +}; diff --git a/apps/rowboatx/components/ai-elements/confirmation.tsx b/apps/rowboatx/components/ai-elements/confirmation.tsx new file mode 100644 index 00000000..1cd9b7ff --- /dev/null +++ b/apps/rowboatx/components/ai-elements/confirmation.tsx @@ -0,0 +1,182 @@ +"use client"; + +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; +import type { ToolUIPart } from "ai"; +import { + type ComponentProps, + createContext, + type ReactNode, + useContext, +} from "react"; + +type ToolUIPartApproval = + | { + id: string; + approved?: never; + reason?: never; + } + | { + id: string; + approved: boolean; + reason?: string; + } + | { + id: string; + approved: true; + reason?: string; + } + | { + id: string; + approved: true; + reason?: string; + } + | { + id: string; + approved: false; + reason?: string; + } + | undefined; + +type ConfirmationContextValue = { + approval: ToolUIPartApproval; + state: ToolUIPart["state"]; +}; + +const ConfirmationContext = createContext( + null +); + +const useConfirmation = () => { + const context = useContext(ConfirmationContext); + + if (!context) { + throw new Error("Confirmation components must be used within Confirmation"); + } + + return context; +}; + +export type ConfirmationProps = ComponentProps & { + approval?: ToolUIPartApproval; + state: ToolUIPart["state"]; +}; + +export const Confirmation = ({ + className, + approval, + state, + ...props +}: ConfirmationProps) => { + if (!approval || state === "input-streaming" || state === "input-available") { + return null; + } + + return ( + + + + ); +}; + +export type ConfirmationTitleProps = ComponentProps; + +export const ConfirmationTitle = ({ + className, + ...props +}: ConfirmationTitleProps) => ( + +); + +export type ConfirmationRequestProps = { + children?: ReactNode; +}; + +export const ConfirmationRequest = ({ children }: ConfirmationRequestProps) => { + const { state } = useConfirmation(); + + // Only show when approval is requested + // @ts-expect-error state only available in AI SDK v6 + if (state !== "approval-requested") { + return null; + } + + return children; +}; + +export type ConfirmationAcceptedProps = { + children?: ReactNode; +}; + +export const ConfirmationAccepted = ({ + children, +}: ConfirmationAcceptedProps) => { + const { approval, state } = useConfirmation(); + + // Only show when approved and in response states + if ( + !approval?.approved || + // @ts-expect-error state only available in AI SDK v6 + (state !== "approval-responded" && + // @ts-expect-error state only available in AI SDK v6 + state !== "output-denied" && + state !== "output-available") + ) { + return null; + } + + return children; +}; + +export type ConfirmationRejectedProps = { + children?: ReactNode; +}; + +export const ConfirmationRejected = ({ + children, +}: ConfirmationRejectedProps) => { + const { approval, state } = useConfirmation(); + + // Only show when rejected and in response states + if ( + approval?.approved !== false || + // @ts-expect-error state only available in AI SDK v6 + (state !== "approval-responded" && + // @ts-expect-error state only available in AI SDK v6 + state !== "output-denied" && + state !== "output-available") + ) { + return null; + } + + return children; +}; + +export type ConfirmationActionsProps = ComponentProps<"div">; + +export const ConfirmationActions = ({ + className, + ...props +}: ConfirmationActionsProps) => { + const { state } = useConfirmation(); + + // Only show when approval is requested + // @ts-expect-error state only available in AI SDK v6 + if (state !== "approval-requested") { + return null; + } + + return ( +
+ ); +}; + +export type ConfirmationActionProps = ComponentProps; + +export const ConfirmationAction = (props: ConfirmationActionProps) => ( + + )} + + ); +}; + +export type ContextContentProps = ComponentProps; + +export const ContextContent = ({ + className, + ...props +}: ContextContentProps) => ( + +); + +export type ContextContentHeaderProps = ComponentProps<"div">; + +export const ContextContentHeader = ({ + children, + className, + ...props +}: ContextContentHeaderProps) => { + const { usedTokens, maxTokens } = useContextValue(); + const usedPercent = usedTokens / maxTokens; + const displayPct = new Intl.NumberFormat("en-US", { + style: "percent", + maximumFractionDigits: 1, + }).format(usedPercent); + const used = new Intl.NumberFormat("en-US", { + notation: "compact", + }).format(usedTokens); + const total = new Intl.NumberFormat("en-US", { + notation: "compact", + }).format(maxTokens); + + return ( +
+ {children ?? ( + <> +
+

{displayPct}

+

+ {used} / {total} +

+
+
+ +
+ + )} +
+ ); +}; + +export type ContextContentBodyProps = ComponentProps<"div">; + +export const ContextContentBody = ({ + children, + className, + ...props +}: ContextContentBodyProps) => ( +
+ {children} +
+); + +export type ContextContentFooterProps = ComponentProps<"div">; + +export const ContextContentFooter = ({ + children, + className, + ...props +}: ContextContentFooterProps) => { + const { modelId, usage } = useContextValue(); + const costUSD = modelId + ? getUsage({ + modelId, + usage: { + input: usage?.inputTokens ?? 0, + output: usage?.outputTokens ?? 0, + }, + }).costUSD?.totalUSD + : undefined; + const totalCost = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }).format(costUSD ?? 0); + + return ( +
+ {children ?? ( + <> + Total cost + {totalCost} + + )} +
+ ); +}; + +export type ContextInputUsageProps = ComponentProps<"div">; + +export const ContextInputUsage = ({ + className, + children, + ...props +}: ContextInputUsageProps) => { + const { usage, modelId } = useContextValue(); + const inputTokens = usage?.inputTokens ?? 0; + + if (children) { + return children; + } + + if (!inputTokens) { + return null; + } + + const inputCost = modelId + ? getUsage({ + modelId, + usage: { input: inputTokens, output: 0 }, + }).costUSD?.totalUSD + : undefined; + const inputCostText = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }).format(inputCost ?? 0); + + return ( +
+ Input + +
+ ); +}; + +export type ContextOutputUsageProps = ComponentProps<"div">; + +export const ContextOutputUsage = ({ + className, + children, + ...props +}: ContextOutputUsageProps) => { + const { usage, modelId } = useContextValue(); + const outputTokens = usage?.outputTokens ?? 0; + + if (children) { + return children; + } + + if (!outputTokens) { + return null; + } + + const outputCost = modelId + ? getUsage({ + modelId, + usage: { input: 0, output: outputTokens }, + }).costUSD?.totalUSD + : undefined; + const outputCostText = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }).format(outputCost ?? 0); + + return ( +
+ Output + +
+ ); +}; + +export type ContextReasoningUsageProps = ComponentProps<"div">; + +export const ContextReasoningUsage = ({ + className, + children, + ...props +}: ContextReasoningUsageProps) => { + const { usage, modelId } = useContextValue(); + const reasoningTokens = usage?.reasoningTokens ?? 0; + + if (children) { + return children; + } + + if (!reasoningTokens) { + return null; + } + + const reasoningCost = modelId + ? getUsage({ + modelId, + usage: { reasoningTokens }, + }).costUSD?.totalUSD + : undefined; + const reasoningCostText = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }).format(reasoningCost ?? 0); + + return ( +
+ Reasoning + +
+ ); +}; + +export type ContextCacheUsageProps = ComponentProps<"div">; + +export const ContextCacheUsage = ({ + className, + children, + ...props +}: ContextCacheUsageProps) => { + const { usage, modelId } = useContextValue(); + const cacheTokens = usage?.cachedInputTokens ?? 0; + + if (children) { + return children; + } + + if (!cacheTokens) { + return null; + } + + const cacheCost = modelId + ? getUsage({ + modelId, + usage: { cacheReads: cacheTokens, input: 0, output: 0 }, + }).costUSD?.totalUSD + : undefined; + const cacheCostText = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }).format(cacheCost ?? 0); + + return ( +
+ Cache + +
+ ); +}; + +const TokensWithCost = ({ + tokens, + costText, +}: { + tokens?: number; + costText?: string; +}) => ( + + {tokens === undefined + ? "—" + : new Intl.NumberFormat("en-US", { + notation: "compact", + }).format(tokens)} + {costText ? ( + • {costText} + ) : null} + +); diff --git a/apps/rowboatx/components/ai-elements/controls.tsx b/apps/rowboatx/components/ai-elements/controls.tsx new file mode 100644 index 00000000..770a8262 --- /dev/null +++ b/apps/rowboatx/components/ai-elements/controls.tsx @@ -0,0 +1,18 @@ +"use client"; + +import { cn } from "@/lib/utils"; +import { Controls as ControlsPrimitive } from "@xyflow/react"; +import type { ComponentProps } from "react"; + +export type ControlsProps = ComponentProps; + +export const Controls = ({ className, ...props }: ControlsProps) => ( + button]:rounded-md [&>button]:border-none! [&>button]:bg-transparent! [&>button]:hover:bg-secondary!", + className + )} + {...props} + /> +); diff --git a/apps/rowboatx/components/ai-elements/conversation.tsx b/apps/rowboatx/components/ai-elements/conversation.tsx new file mode 100644 index 00000000..aa380f57 --- /dev/null +++ b/apps/rowboatx/components/ai-elements/conversation.tsx @@ -0,0 +1,100 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; +import { ArrowDownIcon } from "lucide-react"; +import type { ComponentProps } from "react"; +import { useCallback } from "react"; +import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom"; + +export type ConversationProps = ComponentProps; + +export const Conversation = ({ className, ...props }: ConversationProps) => ( + +); + +export type ConversationContentProps = ComponentProps< + typeof StickToBottom.Content +>; + +export const ConversationContent = ({ + className, + ...props +}: ConversationContentProps) => ( + +); + +export type ConversationEmptyStateProps = ComponentProps<"div"> & { + title?: string; + description?: string; + icon?: React.ReactNode; +}; + +export const ConversationEmptyState = ({ + className, + title = "No messages yet", + description = "Start a conversation to see messages here", + icon, + children, + ...props +}: ConversationEmptyStateProps) => ( +
+ {children ?? ( + <> + {icon &&
{icon}
} +
+

{title}

+ {description && ( +

{description}

+ )} +
+ + )} +
+); + +export type ConversationScrollButtonProps = ComponentProps; + +export const ConversationScrollButton = ({ + className, + ...props +}: ConversationScrollButtonProps) => { + const { isAtBottom, scrollToBottom } = useStickToBottomContext(); + + const handleScrollToBottom = useCallback(() => { + scrollToBottom(); + }, [scrollToBottom]); + + return ( + !isAtBottom && ( + + ) + ); +}; diff --git a/apps/rowboatx/components/ai-elements/edge.tsx b/apps/rowboatx/components/ai-elements/edge.tsx new file mode 100644 index 00000000..3cec409d --- /dev/null +++ b/apps/rowboatx/components/ai-elements/edge.tsx @@ -0,0 +1,140 @@ +import { + BaseEdge, + type EdgeProps, + getBezierPath, + getSimpleBezierPath, + type InternalNode, + type Node, + Position, + useInternalNode, +} from "@xyflow/react"; + +const Temporary = ({ + id, + sourceX, + sourceY, + targetX, + targetY, + sourcePosition, + targetPosition, +}: EdgeProps) => { + const [edgePath] = getSimpleBezierPath({ + sourceX, + sourceY, + sourcePosition, + targetX, + targetY, + targetPosition, + }); + + return ( + + ); +}; + +const getHandleCoordsByPosition = ( + node: InternalNode, + handlePosition: Position +) => { + // Choose the handle type based on position - Left is for target, Right is for source + const handleType = handlePosition === Position.Left ? "target" : "source"; + + const handle = node.internals.handleBounds?.[handleType]?.find( + (h) => h.position === handlePosition + ); + + if (!handle) { + return [0, 0] as const; + } + + let offsetX = handle.width / 2; + let offsetY = handle.height / 2; + + // this is a tiny detail to make the markerEnd of an edge visible. + // The handle position that gets calculated has the origin top-left, so depending which side we are using, we add a little offset + // when the handlePosition is Position.Right for example, we need to add an offset as big as the handle itself in order to get the correct position + switch (handlePosition) { + case Position.Left: + offsetX = 0; + break; + case Position.Right: + offsetX = handle.width; + break; + case Position.Top: + offsetY = 0; + break; + case Position.Bottom: + offsetY = handle.height; + break; + default: + throw new Error(`Invalid handle position: ${handlePosition}`); + } + + const x = node.internals.positionAbsolute.x + handle.x + offsetX; + const y = node.internals.positionAbsolute.y + handle.y + offsetY; + + return [x, y] as const; +}; + +const getEdgeParams = ( + source: InternalNode, + target: InternalNode +) => { + const sourcePos = Position.Right; + const [sx, sy] = getHandleCoordsByPosition(source, sourcePos); + const targetPos = Position.Left; + const [tx, ty] = getHandleCoordsByPosition(target, targetPos); + + return { + sx, + sy, + tx, + ty, + sourcePos, + targetPos, + }; +}; + +const Animated = ({ id, source, target, markerEnd, style }: EdgeProps) => { + const sourceNode = useInternalNode(source); + const targetNode = useInternalNode(target); + + if (!(sourceNode && targetNode)) { + return null; + } + + const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams( + sourceNode, + targetNode + ); + + const [edgePath] = getBezierPath({ + sourceX: sx, + sourceY: sy, + sourcePosition: sourcePos, + targetX: tx, + targetY: ty, + targetPosition: targetPos, + }); + + return ( + <> + + + + + + ); +}; + +export const Edge = { + Temporary, + Animated, +}; diff --git a/apps/rowboatx/components/ai-elements/image.tsx b/apps/rowboatx/components/ai-elements/image.tsx new file mode 100644 index 00000000..542812a3 --- /dev/null +++ b/apps/rowboatx/components/ai-elements/image.tsx @@ -0,0 +1,24 @@ +import { cn } from "@/lib/utils"; +import type { Experimental_GeneratedImage } from "ai"; + +export type ImageProps = Experimental_GeneratedImage & { + className?: string; + alt?: string; +}; + +export const Image = ({ + base64, + uint8Array, + mediaType, + ...props +}: ImageProps) => ( + {props.alt} +); diff --git a/apps/rowboatx/components/ai-elements/inline-citation.tsx b/apps/rowboatx/components/ai-elements/inline-citation.tsx new file mode 100644 index 00000000..5977081b --- /dev/null +++ b/apps/rowboatx/components/ai-elements/inline-citation.tsx @@ -0,0 +1,287 @@ +"use client"; + +import { Badge } from "@/components/ui/badge"; +import { + Carousel, + type CarouselApi, + CarouselContent, + CarouselItem, +} from "@/components/ui/carousel"; +import { + HoverCard, + HoverCardContent, + HoverCardTrigger, +} from "@/components/ui/hover-card"; +import { cn } from "@/lib/utils"; +import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react"; +import { + type ComponentProps, + createContext, + useCallback, + useContext, + useEffect, + useState, +} from "react"; + +export type InlineCitationProps = ComponentProps<"span">; + +export const InlineCitation = ({ + className, + ...props +}: InlineCitationProps) => ( + +); + +export type InlineCitationTextProps = ComponentProps<"span">; + +export const InlineCitationText = ({ + className, + ...props +}: InlineCitationTextProps) => ( + +); + +export type InlineCitationCardProps = ComponentProps; + +export const InlineCitationCard = (props: InlineCitationCardProps) => ( + +); + +export type InlineCitationCardTriggerProps = ComponentProps & { + sources: string[]; +}; + +export const InlineCitationCardTrigger = ({ + sources, + className, + ...props +}: InlineCitationCardTriggerProps) => ( + + + {sources[0] ? ( + <> + {new URL(sources[0]).hostname}{" "} + {sources.length > 1 && `+${sources.length - 1}`} + + ) : ( + "unknown" + )} + + +); + +export type InlineCitationCardBodyProps = ComponentProps<"div">; + +export const InlineCitationCardBody = ({ + className, + ...props +}: InlineCitationCardBodyProps) => ( + +); + +const CarouselApiContext = createContext(undefined); + +const useCarouselApi = () => { + const context = useContext(CarouselApiContext); + return context; +}; + +export type InlineCitationCarouselProps = ComponentProps; + +export const InlineCitationCarousel = ({ + className, + children, + ...props +}: InlineCitationCarouselProps) => { + const [api, setApi] = useState(); + + return ( + + + {children} + + + ); +}; + +export type InlineCitationCarouselContentProps = ComponentProps<"div">; + +export const InlineCitationCarouselContent = ( + props: InlineCitationCarouselContentProps +) => ; + +export type InlineCitationCarouselItemProps = ComponentProps<"div">; + +export const InlineCitationCarouselItem = ({ + className, + ...props +}: InlineCitationCarouselItemProps) => ( + +); + +export type InlineCitationCarouselHeaderProps = ComponentProps<"div">; + +export const InlineCitationCarouselHeader = ({ + className, + ...props +}: InlineCitationCarouselHeaderProps) => ( +
+); + +export type InlineCitationCarouselIndexProps = ComponentProps<"div">; + +export const InlineCitationCarouselIndex = ({ + children, + className, + ...props +}: InlineCitationCarouselIndexProps) => { + const api = useCarouselApi(); + const [current, setCurrent] = useState(0); + const [count, setCount] = useState(0); + + useEffect(() => { + if (!api) { + return; + } + + setCount(api.scrollSnapList().length); + setCurrent(api.selectedScrollSnap() + 1); + + api.on("select", () => { + setCurrent(api.selectedScrollSnap() + 1); + }); + }, [api]); + + return ( +
+ {children ?? `${current}/${count}`} +
+ ); +}; + +export type InlineCitationCarouselPrevProps = ComponentProps<"button">; + +export const InlineCitationCarouselPrev = ({ + className, + ...props +}: InlineCitationCarouselPrevProps) => { + const api = useCarouselApi(); + + const handleClick = useCallback(() => { + if (api) { + api.scrollPrev(); + } + }, [api]); + + return ( + + ); +}; + +export type InlineCitationCarouselNextProps = ComponentProps<"button">; + +export const InlineCitationCarouselNext = ({ + className, + ...props +}: InlineCitationCarouselNextProps) => { + const api = useCarouselApi(); + + const handleClick = useCallback(() => { + if (api) { + api.scrollNext(); + } + }, [api]); + + return ( + + ); +}; + +export type InlineCitationSourceProps = ComponentProps<"div"> & { + title?: string; + url?: string; + description?: string; +}; + +export const InlineCitationSource = ({ + title, + url, + description, + className, + children, + ...props +}: InlineCitationSourceProps) => ( +
+ {title && ( +

{title}

+ )} + {url && ( +

{url}

+ )} + {description && ( +

+ {description} +

+ )} + {children} +
+); + +export type InlineCitationQuoteProps = ComponentProps<"blockquote">; + +export const InlineCitationQuote = ({ + children, + className, + ...props +}: InlineCitationQuoteProps) => ( +
+ {children} +
+); diff --git a/apps/rowboatx/components/ai-elements/loader.tsx b/apps/rowboatx/components/ai-elements/loader.tsx new file mode 100644 index 00000000..5f0cfce4 --- /dev/null +++ b/apps/rowboatx/components/ai-elements/loader.tsx @@ -0,0 +1,96 @@ +import { cn } from "@/lib/utils"; +import type { HTMLAttributes } from "react"; + +type LoaderIconProps = { + size?: number; +}; + +const LoaderIcon = ({ size = 16 }: LoaderIconProps) => ( + + Loader + + + + + + + + + + + + + + + + + + +); + +export type LoaderProps = HTMLAttributes & { + size?: number; +}; + +export const Loader = ({ className, size = 16, ...props }: LoaderProps) => ( +
+ +
+); diff --git a/apps/rowboatx/components/ai-elements/message.tsx b/apps/rowboatx/components/ai-elements/message.tsx new file mode 100644 index 00000000..73d6997e --- /dev/null +++ b/apps/rowboatx/components/ai-elements/message.tsx @@ -0,0 +1,448 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { + ButtonGroup, + ButtonGroupText, +} from "@/components/ui/button-group"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { cn } from "@/lib/utils"; +import type { FileUIPart, UIMessage } from "ai"; +import { + ChevronLeftIcon, + ChevronRightIcon, + PaperclipIcon, + XIcon, +} from "lucide-react"; +import type { ComponentProps, HTMLAttributes, ReactElement } from "react"; +import { createContext, memo, useContext, useEffect, useState } from "react"; +import { Streamdown } from "streamdown"; + +export type MessageProps = HTMLAttributes & { + from: UIMessage["role"]; +}; + +export const Message = ({ className, from, ...props }: MessageProps) => ( +
+); + +export type MessageContentProps = HTMLAttributes; + +export const MessageContent = ({ + children, + className, + ...props +}: MessageContentProps) => ( +
+ {children} +
+); + +export type MessageActionsProps = ComponentProps<"div">; + +export const MessageActions = ({ + className, + children, + ...props +}: MessageActionsProps) => ( +
+ {children} +
+); + +export type MessageActionProps = ComponentProps & { + tooltip?: string; + label?: string; +}; + +export const MessageAction = ({ + tooltip, + children, + label, + variant = "ghost", + size = "icon-sm", + ...props +}: MessageActionProps) => { + const button = ( + + ); + + if (tooltip) { + return ( + + + {button} + +

{tooltip}

+
+
+
+ ); + } + + return button; +}; + +type MessageBranchContextType = { + currentBranch: number; + totalBranches: number; + goToPrevious: () => void; + goToNext: () => void; + branches: ReactElement[]; + setBranches: (branches: ReactElement[]) => void; +}; + +const MessageBranchContext = createContext( + null +); + +const useMessageBranch = () => { + const context = useContext(MessageBranchContext); + + if (!context) { + throw new Error( + "MessageBranch components must be used within MessageBranch" + ); + } + + return context; +}; + +export type MessageBranchProps = HTMLAttributes & { + defaultBranch?: number; + onBranchChange?: (branchIndex: number) => void; +}; + +export const MessageBranch = ({ + defaultBranch = 0, + onBranchChange, + className, + ...props +}: MessageBranchProps) => { + const [currentBranch, setCurrentBranch] = useState(defaultBranch); + const [branches, setBranches] = useState([]); + + const handleBranchChange = (newBranch: number) => { + setCurrentBranch(newBranch); + onBranchChange?.(newBranch); + }; + + const goToPrevious = () => { + const newBranch = + currentBranch > 0 ? currentBranch - 1 : branches.length - 1; + handleBranchChange(newBranch); + }; + + const goToNext = () => { + const newBranch = + currentBranch < branches.length - 1 ? currentBranch + 1 : 0; + handleBranchChange(newBranch); + }; + + const contextValue: MessageBranchContextType = { + currentBranch, + totalBranches: branches.length, + goToPrevious, + goToNext, + branches, + setBranches, + }; + + return ( + +
div]:pb-0", className)} + {...props} + /> + + ); +}; + +export type MessageBranchContentProps = HTMLAttributes; + +export const MessageBranchContent = ({ + children, + ...props +}: MessageBranchContentProps) => { + const { currentBranch, setBranches, branches } = useMessageBranch(); + const childrenArray = Array.isArray(children) ? children : [children]; + + // Use useEffect to update branches when they change + useEffect(() => { + if (branches.length !== childrenArray.length) { + setBranches(childrenArray); + } + }, [childrenArray, branches, setBranches]); + + return childrenArray.map((branch, index) => ( +
div]:pb-0", + index === currentBranch ? "block" : "hidden" + )} + key={branch.key} + {...props} + > + {branch} +
+ )); +}; + +export type MessageBranchSelectorProps = HTMLAttributes & { + from: UIMessage["role"]; +}; + +export const MessageBranchSelector = ({ + className, + from, + ...props +}: MessageBranchSelectorProps) => { + const { totalBranches } = useMessageBranch(); + + // Don't render if there's only one branch + if (totalBranches <= 1) { + return null; + } + + return ( + + ); +}; + +export type MessageBranchPreviousProps = ComponentProps; + +export const MessageBranchPrevious = ({ + children, + ...props +}: MessageBranchPreviousProps) => { + const { goToPrevious, totalBranches } = useMessageBranch(); + + return ( + + ); +}; + +export type MessageBranchNextProps = ComponentProps; + +export const MessageBranchNext = ({ + children, + className, + ...props +}: MessageBranchNextProps) => { + const { goToNext, totalBranches } = useMessageBranch(); + + return ( + + ); +}; + +export type MessageBranchPageProps = HTMLAttributes; + +export const MessageBranchPage = ({ + className, + ...props +}: MessageBranchPageProps) => { + const { currentBranch, totalBranches } = useMessageBranch(); + + return ( + + {currentBranch + 1} of {totalBranches} + + ); +}; + +export type MessageResponseProps = ComponentProps; + +export const MessageResponse = memo( + ({ className, ...props }: MessageResponseProps) => ( + *:first-child]:mt-0 [&>*:last-child]:mb-0", + className + )} + {...props} + /> + ), + (prevProps, nextProps) => prevProps.children === nextProps.children +); + +MessageResponse.displayName = "MessageResponse"; + +export type MessageAttachmentProps = HTMLAttributes & { + data: FileUIPart; + className?: string; + onRemove?: () => void; +}; + +export function MessageAttachment({ + data, + className, + onRemove, + ...props +}: MessageAttachmentProps) { + const filename = data.filename || ""; + const mediaType = + data.mediaType?.startsWith("image/") && data.url ? "image" : "file"; + const isImage = mediaType === "image"; + const attachmentLabel = filename || (isImage ? "Image" : "Attachment"); + + return ( +
+ {isImage ? ( + <> + {filename + {onRemove && ( + + )} + + ) : ( + <> + + +
+ +
+
+ +

{attachmentLabel}

+
+
+ {onRemove && ( + + )} + + )} +
+ ); +} + +export type MessageAttachmentsProps = ComponentProps<"div">; + +export function MessageAttachments({ + children, + className, + ...props +}: MessageAttachmentsProps) { + if (!children) { + return null; + } + + return ( +
+ {children} +
+ ); +} + +export type MessageToolbarProps = ComponentProps<"div">; + +export const MessageToolbar = ({ + className, + children, + ...props +}: MessageToolbarProps) => ( +
+ {children} +
+); diff --git a/apps/rowboatx/components/ai-elements/model-selector.tsx b/apps/rowboatx/components/ai-elements/model-selector.tsx new file mode 100644 index 00000000..ef6ebd7e --- /dev/null +++ b/apps/rowboatx/components/ai-elements/model-selector.tsx @@ -0,0 +1,205 @@ +import { + Command, + CommandDialog, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, + CommandShortcut, +} from "@/components/ui/command"; +import { + Dialog, + DialogContent, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { cn } from "@/lib/utils"; +import type { ComponentProps, ReactNode } from "react"; + +export type ModelSelectorProps = ComponentProps; + +export const ModelSelector = (props: ModelSelectorProps) => ( + +); + +export type ModelSelectorTriggerProps = ComponentProps; + +export const ModelSelectorTrigger = (props: ModelSelectorTriggerProps) => ( + +); + +export type ModelSelectorContentProps = ComponentProps & { + title?: ReactNode; +}; + +export const ModelSelectorContent = ({ + className, + children, + title = "Model Selector", + ...props +}: ModelSelectorContentProps) => ( + + {title} + + {children} + + +); + +export type ModelSelectorDialogProps = ComponentProps; + +export const ModelSelectorDialog = (props: ModelSelectorDialogProps) => ( + +); + +export type ModelSelectorInputProps = ComponentProps; + +export const ModelSelectorInput = ({ + className, + ...props +}: ModelSelectorInputProps) => ( + +); + +export type ModelSelectorListProps = ComponentProps; + +export const ModelSelectorList = (props: ModelSelectorListProps) => ( + +); + +export type ModelSelectorEmptyProps = ComponentProps; + +export const ModelSelectorEmpty = (props: ModelSelectorEmptyProps) => ( + +); + +export type ModelSelectorGroupProps = ComponentProps; + +export const ModelSelectorGroup = (props: ModelSelectorGroupProps) => ( + +); + +export type ModelSelectorItemProps = ComponentProps; + +export const ModelSelectorItem = (props: ModelSelectorItemProps) => ( + +); + +export type ModelSelectorShortcutProps = ComponentProps; + +export const ModelSelectorShortcut = (props: ModelSelectorShortcutProps) => ( + +); + +export type ModelSelectorSeparatorProps = ComponentProps< + typeof CommandSeparator +>; + +export const ModelSelectorSeparator = (props: ModelSelectorSeparatorProps) => ( + +); + +export type ModelSelectorLogoProps = Omit< + ComponentProps<"img">, + "src" | "alt" +> & { + provider: + | "moonshotai-cn" + | "lucidquery" + | "moonshotai" + | "zai-coding-plan" + | "alibaba" + | "xai" + | "vultr" + | "nvidia" + | "upstage" + | "groq" + | "github-copilot" + | "mistral" + | "vercel" + | "nebius" + | "deepseek" + | "alibaba-cn" + | "google-vertex-anthropic" + | "venice" + | "chutes" + | "cortecs" + | "github-models" + | "togetherai" + | "azure" + | "baseten" + | "huggingface" + | "opencode" + | "fastrouter" + | "google" + | "google-vertex" + | "cloudflare-workers-ai" + | "inception" + | "wandb" + | "openai" + | "zhipuai-coding-plan" + | "perplexity" + | "openrouter" + | "zenmux" + | "v0" + | "iflowcn" + | "synthetic" + | "deepinfra" + | "zhipuai" + | "submodel" + | "zai" + | "inference" + | "requesty" + | "morph" + | "lmstudio" + | "anthropic" + | "aihubmix" + | "fireworks-ai" + | "modelscope" + | "llama" + | "scaleway" + | "amazon-bedrock" + | "cerebras" + | (string & {}); +}; + +export const ModelSelectorLogo = ({ + provider, + className, + ...props +}: ModelSelectorLogoProps) => ( + {`${provider} +); + +export type ModelSelectorLogoGroupProps = ComponentProps<"div">; + +export const ModelSelectorLogoGroup = ({ + className, + ...props +}: ModelSelectorLogoGroupProps) => ( +
img]:rounded-full [&>img]:bg-background [&>img]:p-px [&>img]:ring-1 dark:[&>img]:bg-foreground", + className + )} + {...props} + /> +); + +export type ModelSelectorNameProps = ComponentProps<"span">; + +export const ModelSelectorName = ({ + className, + ...props +}: ModelSelectorNameProps) => ( + +); diff --git a/apps/rowboatx/components/ai-elements/node.tsx b/apps/rowboatx/components/ai-elements/node.tsx new file mode 100644 index 00000000..75ac59a1 --- /dev/null +++ b/apps/rowboatx/components/ai-elements/node.tsx @@ -0,0 +1,71 @@ +import { + Card, + CardAction, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { cn } from "@/lib/utils"; +import { Handle, Position } from "@xyflow/react"; +import type { ComponentProps } from "react"; + +export type NodeProps = ComponentProps & { + handles: { + target: boolean; + source: boolean; + }; +}; + +export const Node = ({ handles, className, ...props }: NodeProps) => ( + + {handles.target && } + {handles.source && } + {props.children} + +); + +export type NodeHeaderProps = ComponentProps; + +export const NodeHeader = ({ className, ...props }: NodeHeaderProps) => ( + +); + +export type NodeTitleProps = ComponentProps; + +export const NodeTitle = (props: NodeTitleProps) => ; + +export type NodeDescriptionProps = ComponentProps; + +export const NodeDescription = (props: NodeDescriptionProps) => ( + +); + +export type NodeActionProps = ComponentProps; + +export const NodeAction = (props: NodeActionProps) => ; + +export type NodeContentProps = ComponentProps; + +export const NodeContent = ({ className, ...props }: NodeContentProps) => ( + +); + +export type NodeFooterProps = ComponentProps; + +export const NodeFooter = ({ className, ...props }: NodeFooterProps) => ( + +); diff --git a/apps/rowboatx/components/ai-elements/open-in-chat.tsx b/apps/rowboatx/components/ai-elements/open-in-chat.tsx new file mode 100644 index 00000000..0c62a6ac --- /dev/null +++ b/apps/rowboatx/components/ai-elements/open-in-chat.tsx @@ -0,0 +1,365 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { cn } from "@/lib/utils"; +import { + ChevronDownIcon, + ExternalLinkIcon, + MessageCircleIcon, +} from "lucide-react"; +import { type ComponentProps, createContext, useContext } from "react"; + +const providers = { + github: { + title: "Open in GitHub", + createUrl: (url: string) => url, + icon: ( + + GitHub + + + ), + }, + scira: { + title: "Open in Scira", + createUrl: (q: string) => + `https://scira.ai/?${new URLSearchParams({ + q, + })}`, + icon: ( + + Scira AI + + + + + + + + + ), + }, + chatgpt: { + title: "Open in ChatGPT", + createUrl: (prompt: string) => + `https://chatgpt.com/?${new URLSearchParams({ + hints: "search", + prompt, + })}`, + icon: ( + + OpenAI + + + ), + }, + claude: { + title: "Open in Claude", + createUrl: (q: string) => + `https://claude.ai/new?${new URLSearchParams({ + q, + })}`, + icon: ( + + Claude + + + ), + }, + t3: { + title: "Open in T3 Chat", + createUrl: (q: string) => + `https://t3.chat/new?${new URLSearchParams({ + q, + })}`, + icon: , + }, + v0: { + title: "Open in v0", + createUrl: (q: string) => + `https://v0.app?${new URLSearchParams({ + q, + })}`, + icon: ( + + v0 + + + + ), + }, + cursor: { + title: "Open in Cursor", + createUrl: (text: string) => { + const url = new URL("https://cursor.com/link/prompt"); + url.searchParams.set("text", text); + return url.toString(); + }, + icon: ( + + Cursor + + + ), + }, +}; + +const OpenInContext = createContext<{ query: string } | undefined>(undefined); + +const useOpenInContext = () => { + const context = useContext(OpenInContext); + if (!context) { + throw new Error("OpenIn components must be used within an OpenIn provider"); + } + return context; +}; + +export type OpenInProps = ComponentProps & { + query: string; +}; + +export const OpenIn = ({ query, ...props }: OpenInProps) => ( + + + +); + +export type OpenInContentProps = ComponentProps; + +export const OpenInContent = ({ className, ...props }: OpenInContentProps) => ( + +); + +export type OpenInItemProps = ComponentProps; + +export const OpenInItem = (props: OpenInItemProps) => ( + +); + +export type OpenInLabelProps = ComponentProps; + +export const OpenInLabel = (props: OpenInLabelProps) => ( + +); + +export type OpenInSeparatorProps = ComponentProps; + +export const OpenInSeparator = (props: OpenInSeparatorProps) => ( + +); + +export type OpenInTriggerProps = ComponentProps; + +export const OpenInTrigger = ({ children, ...props }: OpenInTriggerProps) => ( + + {children ?? ( + + )} + +); + +export type OpenInChatGPTProps = ComponentProps; + +export const OpenInChatGPT = (props: OpenInChatGPTProps) => { + const { query } = useOpenInContext(); + return ( + + + {providers.chatgpt.icon} + {providers.chatgpt.title} + + + + ); +}; + +export type OpenInClaudeProps = ComponentProps; + +export const OpenInClaude = (props: OpenInClaudeProps) => { + const { query } = useOpenInContext(); + return ( + + + {providers.claude.icon} + {providers.claude.title} + + + + ); +}; + +export type OpenInT3Props = ComponentProps; + +export const OpenInT3 = (props: OpenInT3Props) => { + const { query } = useOpenInContext(); + return ( + + + {providers.t3.icon} + {providers.t3.title} + + + + ); +}; + +export type OpenInSciraProps = ComponentProps; + +export const OpenInScira = (props: OpenInSciraProps) => { + const { query } = useOpenInContext(); + return ( + + + {providers.scira.icon} + {providers.scira.title} + + + + ); +}; + +export type OpenInv0Props = ComponentProps; + +export const OpenInv0 = (props: OpenInv0Props) => { + const { query } = useOpenInContext(); + return ( + + + {providers.v0.icon} + {providers.v0.title} + + + + ); +}; + +export type OpenInCursorProps = ComponentProps; + +export const OpenInCursor = (props: OpenInCursorProps) => { + const { query } = useOpenInContext(); + return ( + + + {providers.cursor.icon} + {providers.cursor.title} + + + + ); +}; diff --git a/apps/rowboatx/components/ai-elements/panel.tsx b/apps/rowboatx/components/ai-elements/panel.tsx new file mode 100644 index 00000000..059cb7ac --- /dev/null +++ b/apps/rowboatx/components/ai-elements/panel.tsx @@ -0,0 +1,15 @@ +import { cn } from "@/lib/utils"; +import { Panel as PanelPrimitive } from "@xyflow/react"; +import type { ComponentProps } from "react"; + +type PanelProps = ComponentProps; + +export const Panel = ({ className, ...props }: PanelProps) => ( + +); diff --git a/apps/rowboatx/components/ai-elements/plan.tsx b/apps/rowboatx/components/ai-elements/plan.tsx new file mode 100644 index 00000000..be04d883 --- /dev/null +++ b/apps/rowboatx/components/ai-elements/plan.tsx @@ -0,0 +1,142 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { + Card, + CardAction, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; +import { cn } from "@/lib/utils"; +import { ChevronsUpDownIcon } from "lucide-react"; +import type { ComponentProps } from "react"; +import { createContext, useContext } from "react"; +import { Shimmer } from "./shimmer"; + +type PlanContextValue = { + isStreaming: boolean; +}; + +const PlanContext = createContext(null); + +const usePlan = () => { + const context = useContext(PlanContext); + if (!context) { + throw new Error("Plan components must be used within Plan"); + } + return context; +}; + +export type PlanProps = ComponentProps & { + isStreaming?: boolean; +}; + +export const Plan = ({ + className, + isStreaming = false, + children, + ...props +}: PlanProps) => ( + + + {children} + + +); + +export type PlanHeaderProps = ComponentProps; + +export const PlanHeader = ({ className, ...props }: PlanHeaderProps) => ( + +); + +export type PlanTitleProps = Omit< + ComponentProps, + "children" +> & { + children: string; +}; + +export const PlanTitle = ({ children, ...props }: PlanTitleProps) => { + const { isStreaming } = usePlan(); + + return ( + + {isStreaming ? {children} : children} + + ); +}; + +export type PlanDescriptionProps = Omit< + ComponentProps, + "children" +> & { + children: string; +}; + +export const PlanDescription = ({ + className, + children, + ...props +}: PlanDescriptionProps) => { + const { isStreaming } = usePlan(); + + return ( + + {isStreaming ? {children} : children} + + ); +}; + +export type PlanActionProps = ComponentProps; + +export const PlanAction = (props: PlanActionProps) => ( + +); + +export type PlanContentProps = ComponentProps; + +export const PlanContent = (props: PlanContentProps) => ( + + + +); + +export type PlanFooterProps = ComponentProps<"div">; + +export const PlanFooter = (props: PlanFooterProps) => ( + +); + +export type PlanTriggerProps = ComponentProps; + +export const PlanTrigger = ({ className, ...props }: PlanTriggerProps) => ( + + + +); diff --git a/apps/rowboatx/components/ai-elements/prompt-input.tsx b/apps/rowboatx/components/ai-elements/prompt-input.tsx new file mode 100644 index 00000000..5ede475f --- /dev/null +++ b/apps/rowboatx/components/ai-elements/prompt-input.tsx @@ -0,0 +1,1413 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, +} from "@/components/ui/command"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + HoverCard, + HoverCardContent, + HoverCardTrigger, +} from "@/components/ui/hover-card"; +import { + InputGroup, + InputGroupAddon, + InputGroupButton, + InputGroupTextarea, +} from "@/components/ui/input-group"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { cn } from "@/lib/utils"; +import type { ChatStatus, FileUIPart } from "ai"; +import { + CornerDownLeftIcon, + ImageIcon, + Loader2Icon, + MicIcon, + PaperclipIcon, + PlusIcon, + SquareIcon, + XIcon, +} from "lucide-react"; +import { nanoid } from "nanoid"; +import { + type ChangeEvent, + type ChangeEventHandler, + Children, + type ClipboardEventHandler, + type ComponentProps, + createContext, + type FormEvent, + type FormEventHandler, + Fragment, + type HTMLAttributes, + type KeyboardEventHandler, + type PropsWithChildren, + type ReactNode, + type RefObject, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from "react"; + +// ============================================================================ +// Provider Context & Types +// ============================================================================ + +export type AttachmentsContext = { + files: (FileUIPart & { id: string })[]; + add: (files: File[] | FileList) => void; + remove: (id: string) => void; + clear: () => void; + openFileDialog: () => void; + fileInputRef: RefObject; +}; + +export type TextInputContext = { + value: string; + setInput: (v: string) => void; + clear: () => void; +}; + +export type PromptInputControllerProps = { + textInput: TextInputContext; + attachments: AttachmentsContext; + /** INTERNAL: Allows PromptInput to register its file textInput + "open" callback */ + __registerFileInput: ( + ref: RefObject, + open: () => void + ) => void; +}; + +const PromptInputController = createContext( + null +); +const ProviderAttachmentsContext = createContext( + null +); + +export const usePromptInputController = () => { + const ctx = useContext(PromptInputController); + if (!ctx) { + throw new Error( + "Wrap your component inside to use usePromptInputController()." + ); + } + return ctx; +}; + +// Optional variants (do NOT throw). Useful for dual-mode components. +const useOptionalPromptInputController = () => + useContext(PromptInputController); + +export const useProviderAttachments = () => { + const ctx = useContext(ProviderAttachmentsContext); + if (!ctx) { + throw new Error( + "Wrap your component inside to use useProviderAttachments()." + ); + } + return ctx; +}; + +const useOptionalProviderAttachments = () => + useContext(ProviderAttachmentsContext); + +export type PromptInputProviderProps = PropsWithChildren<{ + initialInput?: string; +}>; + +/** + * Optional global provider that lifts PromptInput state outside of PromptInput. + * If you don't use it, PromptInput stays fully self-managed. + */ +export function PromptInputProvider({ + initialInput: initialTextInput = "", + children, +}: PromptInputProviderProps) { + // ----- textInput state + const [textInput, setTextInput] = useState(initialTextInput); + const clearInput = useCallback(() => setTextInput(""), []); + + // ----- attachments state (global when wrapped) + const [attachmentFiles, setAttachmentFiles] = useState< + (FileUIPart & { id: string })[] + >([]); + const fileInputRef = useRef(null); + const openRef = useRef<() => void>(() => {}); + + const add = useCallback((files: File[] | FileList) => { + const incoming = Array.from(files); + if (incoming.length === 0) { + return; + } + + setAttachmentFiles((prev) => + prev.concat( + incoming.map((file) => ({ + id: nanoid(), + type: "file" as const, + url: URL.createObjectURL(file), + mediaType: file.type, + filename: file.name, + })) + ) + ); + }, []); + + const remove = useCallback((id: string) => { + setAttachmentFiles((prev) => { + const found = prev.find((f) => f.id === id); + if (found?.url) { + URL.revokeObjectURL(found.url); + } + return prev.filter((f) => f.id !== id); + }); + }, []); + + const clear = useCallback(() => { + setAttachmentFiles((prev) => { + for (const f of prev) { + if (f.url) { + URL.revokeObjectURL(f.url); + } + } + return []; + }); + }, []); + + // Keep a ref to attachments for cleanup on unmount (avoids stale closure) + const attachmentsRef = useRef(attachmentFiles); + attachmentsRef.current = attachmentFiles; + + // Cleanup blob URLs on unmount to prevent memory leaks + useEffect(() => { + return () => { + for (const f of attachmentsRef.current) { + if (f.url) { + URL.revokeObjectURL(f.url); + } + } + }; + }, []); + + const openFileDialog = useCallback(() => { + openRef.current?.(); + }, []); + + const attachments = useMemo( + () => ({ + files: attachmentFiles, + add, + remove, + clear, + openFileDialog, + fileInputRef, + }), + [attachmentFiles, add, remove, clear, openFileDialog] + ); + + const __registerFileInput = useCallback( + (ref: RefObject, open: () => void) => { + fileInputRef.current = ref.current; + openRef.current = open; + }, + [] + ); + + const controller = useMemo( + () => ({ + textInput: { + value: textInput, + setInput: setTextInput, + clear: clearInput, + }, + attachments, + __registerFileInput, + }), + [textInput, clearInput, attachments, __registerFileInput] + ); + + return ( + + + {children} + + + ); +} + +// ============================================================================ +// Component Context & Hooks +// ============================================================================ + +const LocalAttachmentsContext = createContext(null); + +export const usePromptInputAttachments = () => { + // Dual-mode: prefer provider if present, otherwise use local + const provider = useOptionalProviderAttachments(); + const local = useContext(LocalAttachmentsContext); + const context = provider ?? local; + if (!context) { + throw new Error( + "usePromptInputAttachments must be used within a PromptInput or PromptInputProvider" + ); + } + return context; +}; + +export type PromptInputAttachmentProps = HTMLAttributes & { + data: FileUIPart & { id: string }; + className?: string; +}; + +export function PromptInputAttachment({ + data, + className, + ...props +}: PromptInputAttachmentProps) { + const attachments = usePromptInputAttachments(); + + const filename = data.filename || ""; + + const mediaType = + data.mediaType?.startsWith("image/") && data.url ? "image" : "file"; + const isImage = mediaType === "image"; + + const attachmentLabel = filename || (isImage ? "Image" : "Attachment"); + + return ( + + +
+
+
+ {isImage ? ( + {filename + ) : ( +
+ +
+ )} +
+ +
+ + {attachmentLabel} +
+
+ +
+ {isImage && ( +
+ {filename +
+ )} +
+
+

+ {filename || (isImage ? "Image" : "Attachment")} +

+ {data.mediaType && ( +

+ {data.mediaType} +

+ )} +
+
+
+
+
+ ); +} + +export type PromptInputAttachmentsProps = Omit< + HTMLAttributes, + "children" +> & { + children: (attachment: FileUIPart & { id: string }) => ReactNode; +}; + +export function PromptInputAttachments({ + children, + className, + ...props +}: PromptInputAttachmentsProps) { + const attachments = usePromptInputAttachments(); + + if (!attachments.files.length) { + return null; + } + + return ( +
+ {attachments.files.map((file) => ( + {children(file)} + ))} +
+ ); +} + +export type PromptInputActionAddAttachmentsProps = ComponentProps< + typeof DropdownMenuItem +> & { + label?: string; +}; + +export const PromptInputActionAddAttachments = ({ + label = "Add photos or files", + ...props +}: PromptInputActionAddAttachmentsProps) => { + const attachments = usePromptInputAttachments(); + + return ( + { + e.preventDefault(); + attachments.openFileDialog(); + }} + > + {label} + + ); +}; + +export type PromptInputMessage = { + text: string; + files: FileUIPart[]; +}; + +export type PromptInputProps = Omit< + HTMLAttributes, + "onSubmit" | "onError" +> & { + accept?: string; // e.g., "image/*" or leave undefined for any + multiple?: boolean; + // When true, accepts drops anywhere on document. Default false (opt-in). + globalDrop?: boolean; + // Render a hidden input with given name and keep it in sync for native form posts. Default false. + syncHiddenInput?: boolean; + // Minimal constraints + maxFiles?: number; + maxFileSize?: number; // bytes + onError?: (err: { + code: "max_files" | "max_file_size" | "accept"; + message: string; + }) => void; + onSubmit: ( + message: PromptInputMessage, + event: FormEvent + ) => void | Promise; +}; + +export const PromptInput = ({ + className, + accept, + multiple, + globalDrop, + syncHiddenInput, + maxFiles, + maxFileSize, + onError, + onSubmit, + children, + ...props +}: PromptInputProps) => { + // Try to use a provider controller if present + const controller = useOptionalPromptInputController(); + const usingProvider = !!controller; + + // Refs + const inputRef = useRef(null); + const formRef = useRef(null); + + // ----- Local attachments (only used when no provider) + const [items, setItems] = useState<(FileUIPart & { id: string })[]>([]); + const files = usingProvider ? controller.attachments.files : items; + + // Keep a ref to files for cleanup on unmount (avoids stale closure) + const filesRef = useRef(files); + filesRef.current = files; + + const openFileDialogLocal = useCallback(() => { + inputRef.current?.click(); + }, []); + + const matchesAccept = useCallback( + (f: File) => { + if (!accept || accept.trim() === "") { + return true; + } + + const patterns = accept + .split(",") + .map((s) => s.trim()) + .filter(Boolean); + + return patterns.some((pattern) => { + if (pattern.endsWith("/*")) { + const prefix = pattern.slice(0, -1); // e.g: image/* -> image/ + return f.type.startsWith(prefix); + } + return f.type === pattern; + }); + }, + [accept] + ); + + const addLocal = useCallback( + (fileList: File[] | FileList) => { + const incoming = Array.from(fileList); + const accepted = incoming.filter((f) => matchesAccept(f)); + if (incoming.length && accepted.length === 0) { + onError?.({ + code: "accept", + message: "No files match the accepted types.", + }); + return; + } + const withinSize = (f: File) => + maxFileSize ? f.size <= maxFileSize : true; + const sized = accepted.filter(withinSize); + if (accepted.length > 0 && sized.length === 0) { + onError?.({ + code: "max_file_size", + message: "All files exceed the maximum size.", + }); + return; + } + + setItems((prev) => { + const capacity = + typeof maxFiles === "number" + ? Math.max(0, maxFiles - prev.length) + : undefined; + const capped = + typeof capacity === "number" ? sized.slice(0, capacity) : sized; + if (typeof capacity === "number" && sized.length > capacity) { + onError?.({ + code: "max_files", + message: "Too many files. Some were not added.", + }); + } + const next: (FileUIPart & { id: string })[] = []; + for (const file of capped) { + next.push({ + id: nanoid(), + type: "file", + url: URL.createObjectURL(file), + mediaType: file.type, + filename: file.name, + }); + } + return prev.concat(next); + }); + }, + [matchesAccept, maxFiles, maxFileSize, onError] + ); + + const removeLocal = useCallback( + (id: string) => + setItems((prev) => { + const found = prev.find((file) => file.id === id); + if (found?.url) { + URL.revokeObjectURL(found.url); + } + return prev.filter((file) => file.id !== id); + }), + [] + ); + + const clearLocal = useCallback( + () => + setItems((prev) => { + for (const file of prev) { + if (file.url) { + URL.revokeObjectURL(file.url); + } + } + return []; + }), + [] + ); + + const add = usingProvider ? controller.attachments.add : addLocal; + const remove = usingProvider ? controller.attachments.remove : removeLocal; + const clear = usingProvider ? controller.attachments.clear : clearLocal; + const openFileDialog = usingProvider + ? controller.attachments.openFileDialog + : openFileDialogLocal; + + // Let provider know about our hidden file input so external menus can call openFileDialog() + useEffect(() => { + if (!usingProvider) return; + controller.__registerFileInput(inputRef, () => inputRef.current?.click()); + }, [usingProvider, controller]); + + // Note: File input cannot be programmatically set for security reasons + // The syncHiddenInput prop is no longer functional + useEffect(() => { + if (syncHiddenInput && inputRef.current && files.length === 0) { + inputRef.current.value = ""; + } + }, [files, syncHiddenInput]); + + // Attach drop handlers on nearest form and document (opt-in) + useEffect(() => { + const form = formRef.current; + if (!form) return; + if (globalDrop) return // when global drop is on, let the document-level handler own drops + + const onDragOver = (e: DragEvent) => { + if (e.dataTransfer?.types?.includes("Files")) { + e.preventDefault(); + } + }; + const onDrop = (e: DragEvent) => { + if (e.dataTransfer?.types?.includes("Files")) { + e.preventDefault(); + } + if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) { + add(e.dataTransfer.files); + } + }; + form.addEventListener("dragover", onDragOver); + form.addEventListener("drop", onDrop); + return () => { + form.removeEventListener("dragover", onDragOver); + form.removeEventListener("drop", onDrop); + }; + }, [add, globalDrop]); + + useEffect(() => { + if (!globalDrop) return; + + const onDragOver = (e: DragEvent) => { + if (e.dataTransfer?.types?.includes("Files")) { + e.preventDefault(); + } + }; + const onDrop = (e: DragEvent) => { + if (e.dataTransfer?.types?.includes("Files")) { + e.preventDefault(); + } + if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) { + add(e.dataTransfer.files); + } + }; + document.addEventListener("dragover", onDragOver); + document.addEventListener("drop", onDrop); + return () => { + document.removeEventListener("dragover", onDragOver); + document.removeEventListener("drop", onDrop); + }; + }, [add, globalDrop]); + + useEffect( + () => () => { + if (!usingProvider) { + for (const f of filesRef.current) { + if (f.url) URL.revokeObjectURL(f.url); + } + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps -- cleanup only on unmount; filesRef always current + [usingProvider] + ); + + const handleChange: ChangeEventHandler = (event) => { + if (event.currentTarget.files) { + add(event.currentTarget.files); + } + // Reset input value to allow selecting files that were previously removed + event.currentTarget.value = ""; + }; + + const convertBlobUrlToDataUrl = async ( + url: string + ): Promise => { + try { + const response = await fetch(url); + const blob = await response.blob(); + return new Promise((resolve) => { + const reader = new FileReader(); + reader.onloadend = () => resolve(reader.result as string); + reader.onerror = () => resolve(null); + reader.readAsDataURL(blob); + }); + } catch { + return null; + } + }; + + const ctx = useMemo( + () => ({ + files: files.map((item) => ({ ...item, id: item.id })), + add, + remove, + clear, + openFileDialog, + fileInputRef: inputRef, + }), + [files, add, remove, clear, openFileDialog] + ); + + const handleSubmit: FormEventHandler = (event) => { + event.preventDefault(); + + const form = event.currentTarget; + const text = usingProvider + ? controller.textInput.value + : (() => { + const formData = new FormData(form); + return (formData.get("message") as string) || ""; + })(); + + // Reset form immediately after capturing text to avoid race condition + // where user input during async blob conversion would be lost + if (!usingProvider) { + form.reset(); + } + + // Convert blob URLs to data URLs asynchronously + Promise.all( + files.map(async ({ id, ...item }) => { + if (item.url && item.url.startsWith("blob:")) { + const dataUrl = await convertBlobUrlToDataUrl(item.url); + // If conversion failed, keep the original blob URL + return { + ...item, + url: dataUrl ?? item.url, + }; + } + return item; + }) + ) + .then((convertedFiles: FileUIPart[]) => { + try { + const result = onSubmit({ text, files: convertedFiles }, event); + + // Handle both sync and async onSubmit + if (result instanceof Promise) { + result + .then(() => { + clear(); + if (usingProvider) { + controller.textInput.clear(); + } + }) + .catch(() => { + // Don't clear on error - user may want to retry + }); + } else { + // Sync function completed without throwing, clear attachments + clear(); + if (usingProvider) { + controller.textInput.clear(); + } + } + } catch { + // Don't clear on error - user may want to retry + } + }) + .catch(() => { + // Don't clear on error - user may want to retry + }); + }; + + // Render with or without local provider + const inner = ( + <> + +
+ {children} +
+ + ); + + return usingProvider ? ( + inner + ) : ( + + {inner} + + ); +}; + +export type PromptInputBodyProps = HTMLAttributes; + +export const PromptInputBody = ({ + className, + ...props +}: PromptInputBodyProps) => ( +
+); + +export type PromptInputTextareaProps = ComponentProps< + typeof InputGroupTextarea +>; + +export const PromptInputTextarea = ({ + onChange, + className, + placeholder = "What would you like to know?", + ...props +}: PromptInputTextareaProps) => { + const controller = useOptionalPromptInputController(); + const attachments = usePromptInputAttachments(); + const [isComposing, setIsComposing] = useState(false); + + const handleKeyDown: KeyboardEventHandler = (e) => { + if (e.key === "Enter") { + if (isComposing || e.nativeEvent.isComposing) { + return; + } + if (e.shiftKey) { + return; + } + e.preventDefault(); + + // Check if the submit button is disabled before submitting + const form = e.currentTarget.form; + const submitButton = form?.querySelector( + 'button[type="submit"]' + ) as HTMLButtonElement | null; + if (submitButton?.disabled) { + return; + } + + form?.requestSubmit(); + } + + // Remove last attachment when Backspace is pressed and textarea is empty + if ( + e.key === "Backspace" && + e.currentTarget.value === "" && + attachments.files.length > 0 + ) { + e.preventDefault(); + const lastAttachment = attachments.files.at(-1); + if (lastAttachment) { + attachments.remove(lastAttachment.id); + } + } + }; + + const handlePaste: ClipboardEventHandler = (event) => { + const items = event.clipboardData?.items; + + if (!items) { + return; + } + + const files: File[] = []; + + for (const item of items) { + if (item.kind === "file") { + const file = item.getAsFile(); + if (file) { + files.push(file); + } + } + } + + if (files.length > 0) { + event.preventDefault(); + attachments.add(files); + } + }; + + const controlledProps = controller + ? { + value: controller.textInput.value, + onChange: (e: ChangeEvent) => { + controller.textInput.setInput(e.currentTarget.value); + onChange?.(e); + }, + } + : { + onChange, + }; + + return ( + setIsComposing(false)} + onCompositionStart={() => setIsComposing(true)} + onKeyDown={handleKeyDown} + onPaste={handlePaste} + placeholder={placeholder} + {...props} + {...controlledProps} + /> + ); +}; + +export type PromptInputHeaderProps = Omit< + ComponentProps, + "align" +>; + +export const PromptInputHeader = ({ + className, + ...props +}: PromptInputHeaderProps) => ( + +); + +export type PromptInputFooterProps = Omit< + ComponentProps, + "align" +>; + +export const PromptInputFooter = ({ + className, + ...props +}: PromptInputFooterProps) => ( + +); + +export type PromptInputToolsProps = HTMLAttributes; + +export const PromptInputTools = ({ + className, + ...props +}: PromptInputToolsProps) => ( +
+); + +export type PromptInputButtonProps = ComponentProps; + +export const PromptInputButton = ({ + variant = "ghost", + className, + size, + ...props +}: PromptInputButtonProps) => { + const newSize = + size ?? (Children.count(props.children) > 1 ? "sm" : "icon-sm"); + + return ( + + ); +}; + +export type PromptInputActionMenuProps = ComponentProps; +export const PromptInputActionMenu = (props: PromptInputActionMenuProps) => ( + +); + +export type PromptInputActionMenuTriggerProps = PromptInputButtonProps; + +export const PromptInputActionMenuTrigger = ({ + className, + children, + ...props +}: PromptInputActionMenuTriggerProps) => ( + + + {children ?? } + + +); + +export type PromptInputActionMenuContentProps = ComponentProps< + typeof DropdownMenuContent +>; +export const PromptInputActionMenuContent = ({ + className, + ...props +}: PromptInputActionMenuContentProps) => ( + +); + +export type PromptInputActionMenuItemProps = ComponentProps< + typeof DropdownMenuItem +>; +export const PromptInputActionMenuItem = ({ + className, + ...props +}: PromptInputActionMenuItemProps) => ( + +); + +// Note: Actions that perform side-effects (like opening a file dialog) +// are provided in opt-in modules (e.g., prompt-input-attachments). + +export type PromptInputSubmitProps = ComponentProps & { + status?: ChatStatus; +}; + +export const PromptInputSubmit = ({ + className, + variant = "default", + size = "icon-sm", + status, + children, + ...props +}: PromptInputSubmitProps) => { + let Icon = ; + + if (status === "submitted") { + Icon = ; + } else if (status === "streaming") { + Icon = ; + } else if (status === "error") { + Icon = ; + } + + return ( + + {children ?? Icon} + + ); +}; + +interface SpeechRecognition extends EventTarget { + continuous: boolean; + interimResults: boolean; + lang: string; + start(): void; + stop(): void; + onstart: ((this: SpeechRecognition, ev: Event) => any) | null; + onend: ((this: SpeechRecognition, ev: Event) => any) | null; + onresult: + | ((this: SpeechRecognition, ev: SpeechRecognitionEvent) => any) + | null; + onerror: + | ((this: SpeechRecognition, ev: SpeechRecognitionErrorEvent) => any) + | null; +} + +interface SpeechRecognitionEvent extends Event { + results: SpeechRecognitionResultList; + resultIndex: number; +} + +type SpeechRecognitionResultList = { + readonly length: number; + item(index: number): SpeechRecognitionResult; + [index: number]: SpeechRecognitionResult; +}; + +type SpeechRecognitionResult = { + readonly length: number; + item(index: number): SpeechRecognitionAlternative; + [index: number]: SpeechRecognitionAlternative; + isFinal: boolean; +}; + +type SpeechRecognitionAlternative = { + transcript: string; + confidence: number; +}; + +interface SpeechRecognitionErrorEvent extends Event { + error: string; +} + +declare global { + interface Window { + SpeechRecognition: { + new (): SpeechRecognition; + }; + webkitSpeechRecognition: { + new (): SpeechRecognition; + }; + } +} + +export type PromptInputSpeechButtonProps = ComponentProps< + typeof PromptInputButton +> & { + textareaRef?: RefObject; + onTranscriptionChange?: (text: string) => void; +}; + +export const PromptInputSpeechButton = ({ + className, + textareaRef, + onTranscriptionChange, + ...props +}: PromptInputSpeechButtonProps) => { + const [isListening, setIsListening] = useState(false); + const [recognition, setRecognition] = useState( + null + ); + const recognitionRef = useRef(null); + + useEffect(() => { + if ( + typeof window !== "undefined" && + ("SpeechRecognition" in window || "webkitSpeechRecognition" in window) + ) { + const SpeechRecognition = + window.SpeechRecognition || window.webkitSpeechRecognition; + const speechRecognition = new SpeechRecognition(); + + speechRecognition.continuous = true; + speechRecognition.interimResults = true; + speechRecognition.lang = "en-US"; + + speechRecognition.onstart = () => { + setIsListening(true); + }; + + speechRecognition.onend = () => { + setIsListening(false); + }; + + speechRecognition.onresult = (event) => { + let finalTranscript = ""; + + for (let i = event.resultIndex; i < event.results.length; i++) { + const result = event.results[i]; + if (result.isFinal) { + finalTranscript += result[0]?.transcript ?? ""; + } + } + + if (finalTranscript && textareaRef?.current) { + const textarea = textareaRef.current; + const currentValue = textarea.value; + const newValue = + currentValue + (currentValue ? " " : "") + finalTranscript; + + textarea.value = newValue; + textarea.dispatchEvent(new Event("input", { bubbles: true })); + onTranscriptionChange?.(newValue); + } + }; + + speechRecognition.onerror = (event) => { + console.error("Speech recognition error:", event.error); + setIsListening(false); + }; + + recognitionRef.current = speechRecognition; + setRecognition(speechRecognition); + } + + return () => { + if (recognitionRef.current) { + recognitionRef.current.stop(); + } + }; + }, [textareaRef, onTranscriptionChange]); + + const toggleListening = useCallback(() => { + if (!recognition) { + return; + } + + if (isListening) { + recognition.stop(); + } else { + recognition.start(); + } + }, [recognition, isListening]); + + return ( + + + + ); +}; + +export type PromptInputSelectProps = ComponentProps; + +export const PromptInputSelect = (props: PromptInputSelectProps) => ( + + ); +}; + +export type WebPreviewBodyProps = ComponentProps<"iframe"> & { + loading?: ReactNode; +}; + +export const WebPreviewBody = ({ + className, + loading, + src, + ...props +}: WebPreviewBodyProps) => { + const { url } = useWebPreview(); + + return ( +
+