diff --git a/apps/.gitignore b/apps/.gitignore deleted file mode 100644 index 860f418f..00000000 --- a/apps/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -.DS_Store -.vscode/ \ No newline at end of file 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..2d55efd4 100755 --- a/apps/cli/bin/app.js +++ b/apps/cli/bin/app.js @@ -1,7 +1,8 @@ #!/usr/bin/env node import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; -import { app, modelConfig, updateState, importExample, listExamples, exportWorkflow } from '../dist/app.js'; +import { app, modelConfig, importExample, listExamples, exportWorkflow } from '../dist/app.js'; +import { runTui } from '../dist/tui/index.js'; yargs(hideBin(process.argv)) @@ -36,6 +37,20 @@ yargs(hideBin(process.argv)) }); } ) + .command( + "ui", + "Launch the interactive Rowboat dashboard", + (y) => y + .option("server-url", { + type: "string", + description: "Rowboat server base URL", + }), + (argv) => { + runTui({ + serverUrl: argv.serverUrl, + }); + } + ) .command( "import", "Import an example workflow (--example) or custom workflow from file (--file)", @@ -116,20 +131,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..1660cd7a 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,12 +14,28 @@ "@ai-sdk/openai": "^2.0.53", "@ai-sdk/openai-compatible": "^1.0.27", "@ai-sdk/provider": "^2.0.0", + "@google-cloud/local-auth": "^3.0.1", + "@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", + "eventsource-parser": "^1.1.2", + "google-auth-library": "^10.5.0", + "googleapis": "^169.0.0", + "hono": "^4.10.7", + "hono-openapi": "^1.1.1", + "ink": "^5.1.0", + "ink-select-input": "^6.2.0", + "ink-spinner": "^5.0.0", + "ink-text-input": "^6.0.0", "json-schema-to-zod": "^2.6.1", "nanoid": "^5.1.6", + "node-html-markdown": "^2.0.0", "ollama-ai-provider-v2": "^1.5.4", + "react": "^18.3.1", + "yaml": "^2.8.2", "yargs": "^18.0.0", "zod": "^4.1.12" }, @@ -28,35 +44,18 @@ }, "devDependencies": { "@types/node": "^24.9.1", - "ts-node": "^10.9.2", + "@types/react": "^18.3.12", "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 +65,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 +81,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 +98,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 +114,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 +142,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 +158,219 @@ "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/@ai-sdk/provider-utils/node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@alcalzone/ansi-tokenize": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.1.3.tgz", + "integrity": "sha512-3yWxPTq3UQ/FY9p1ErPxIyfT64elWaMvM9lIHnaqpyft63tkxodF5aUElYHrdisWve5cETkh1+KBw1yJuW0aRw==", "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=14.13.1" + } + }, + "node_modules/@google-cloud/local-auth": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@google-cloud/local-auth/-/local-auth-3.0.1.tgz", + "integrity": "sha512-YJ3GFbksfHyEarbVHPSCzhKpjbnlAhdzg2SEf79l6ODukrSM1qUOqfopY232Xkw26huKSndyzmJz+A6b2WYn7Q==", + "license": "Apache-2.0", + "dependencies": { + "arrify": "^2.0.1", + "google-auth-library": "^9.0.0", + "open": "^7.0.3", + "server-destroy": "^1.0.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/local-auth/node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/local-auth/node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/local-auth/node_modules/google-auth-library": { + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", + "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/local-auth/node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/local-auth/node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "license": "MIT", + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/local-auth/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": 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", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "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", + "peerDependencies": { + "@standard-schema/spec": "1.0.0", + "hono": ">=3.9.0" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" }, "engines": { "node": ">=12" } }, - "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, - "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, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "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, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "license": "MIT", "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, "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 +378,76 @@ "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/@modelcontextprotocol/sdk/node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "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 +461,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,50 +478,145 @@ "node": ">=8.0.0" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "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": { "undici-types": "~7.16.0" } }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.27", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", + "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, "node_modules/@vercel/oidc": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.0.5.tgz", @@ -408,41 +639,24 @@ "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, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "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": ">= 14" } }, "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 +666,54 @@ "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-escapes": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz", + "integrity": "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==", + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", @@ -509,13 +738,75 @@ "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, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/auto-bind": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-5.0.1.tgz", + "integrity": "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/body-parser": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", @@ -540,6 +831,39 @@ "url": "https://opencollective.com/express" } }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "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/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -578,6 +902,99 @@ "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/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "license": "MIT", + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, "node_modules/cliui": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", @@ -592,16 +1009,47 @@ "node": ">=20" } }, - "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==", + "node_modules/code-excerpt": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-4.0.0.tgz", + "integrity": "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==", "license": "MIT", "dependencies": { - "safe-buffer": "5.2.1" + "convert-to-spaces": "^2.0.1" }, "engines": { - "node": ">= 0.6" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/content-type": { @@ -613,6 +1061,15 @@ "node": ">= 0.6" } }, + "node_modules/convert-to-spaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz", + "integrity": "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/cookie": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", @@ -644,13 +1101,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", @@ -665,6 +1115,50 @@ "node": ">= 8" } }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -691,14 +1185,59 @@ "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", + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, "engines": { - "node": ">=0.3.1" + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" } }, "node_modules/dunder-proto": { @@ -715,6 +1254,21 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -736,6 +1290,30 @@ "node": ">= 0.8" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -766,6 +1344,16 @@ "node": ">= 0.4" } }, + "node_modules/es-toolkit": { + "version": "1.42.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.42.0.tgz", + "integrity": "sha512-SLHIyY7VfDJBM8clz4+T2oquwTQxEzu263AyhVK4jREOAwJ+8eebaa4wM3nlvnAqhDrMm2EsA6hWHaQsMPQ1nA==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -781,6 +1369,15 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -803,6 +1400,15 @@ } }, "node_modules/eventsource-parser": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-1.1.2.tgz", + "integrity": "sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==", + "license": "MIT", + "engines": { + "node": ">=14.18" + } + }, + "node_modules/eventsource/node_modules/eventsource-parser": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", @@ -812,18 +1418,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", @@ -868,22 +1475,113 @@ "express": ">= 4.11" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "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/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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 +1592,51 @@ "statuses": "^2.0.1" }, "engines": { - "node": ">= 0.8" + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" } }, "node_modules/forwarded": { @@ -924,6 +1666,35 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gaxios": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", + "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "rimraf": "^5.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -982,6 +1753,94 @@ "node": ">= 0.4" } }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/google-auth-library": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz", + "integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.0.0", + "gcp-metadata": "^8.0.0", + "google-logging-utils": "^1.0.0", + "gtoken": "^8.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/googleapis": { + "version": "169.0.0", + "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-169.0.0.tgz", + "integrity": "sha512-IOGMG8tljCZSLvYgdojRu6mB10KEsK0J7X62sXXlQz9koe5BUAW+rqkY3qhQM9wXM6hVL3/Hase7XbxoMyeYiQ==", + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.2.0", + "googleapis-common": "^8.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/googleapis-common": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-8.0.1.tgz", + "integrity": "sha512-eCzNACUXPb1PW5l0ULTzMHaL/ltPRADoPgjBlT8jWsTbxkCp6siv+qKJ/1ldaybCthGwsYFYallF7u9AkU4L+A==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "gaxios": "^7.0.0-rc.4", + "google-auth-library": "^10.1.0", + "qs": "^6.7.0", + "url-template": "^2.0.8" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -994,6 +1853,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gtoken": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", + "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==", + "license": "MIT", + "dependencies": { + "gaxios": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -1018,29 +1890,77 @@ "node": ">= 0.4" } }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "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==", + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, "engines": { - "node": ">= 0.8" + "node": ">= 14" } }, "node_modules/iconv-lite": { @@ -1059,12 +1979,122 @@ "url": "https://opencollective.com/express" } }, + "node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ink": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/ink/-/ink-5.2.1.tgz", + "integrity": "sha512-BqcUyWrG9zq5HIwW6JcfFHsIYebJkWWb4fczNah1goUO0vv5vneIlfwuS85twyJ5hYR/y18FlAYUxrO9ChIWVg==", + "license": "MIT", + "dependencies": { + "@alcalzone/ansi-tokenize": "^0.1.3", + "ansi-escapes": "^7.0.0", + "ansi-styles": "^6.2.1", + "auto-bind": "^5.0.1", + "chalk": "^5.3.0", + "cli-boxes": "^3.0.0", + "cli-cursor": "^4.0.0", + "cli-truncate": "^4.0.0", + "code-excerpt": "^4.0.0", + "es-toolkit": "^1.22.0", + "indent-string": "^5.0.0", + "is-in-ci": "^1.0.0", + "patch-console": "^2.0.0", + "react-reconciler": "^0.29.0", + "scheduler": "^0.23.0", + "signal-exit": "^3.0.7", + "slice-ansi": "^7.1.0", + "stack-utils": "^2.0.6", + "string-width": "^7.2.0", + "type-fest": "^4.27.0", + "widest-line": "^5.0.0", + "wrap-ansi": "^9.0.0", + "ws": "^8.18.0", + "yoga-layout": "~3.2.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "react": ">=18.0.0", + "react-devtools-core": "^4.19.1" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react-devtools-core": { + "optional": true + } + } + }, + "node_modules/ink-select-input": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ink-select-input/-/ink-select-input-6.2.0.tgz", + "integrity": "sha512-304fZXxkpYxJ9si5lxRCaX01GNlmPBgOZumXXRnPYbHW/iI31cgQynqk2tRypGLOF1cMIwPUzL2LSm6q4I5rQQ==", + "license": "MIT", + "dependencies": { + "figures": "^6.1.0", + "to-rotated": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "ink": ">=5.0.0", + "react": ">=18.0.0" + } + }, + "node_modules/ink-spinner": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ink-spinner/-/ink-spinner-5.0.0.tgz", + "integrity": "sha512-EYEasbEjkqLGyPOUc8hBJZNuC5GvXGMLu0w5gdTNskPc7Izc5vO3tdQEYnzvshucyGCBXc86ig0ujXPMWaQCdA==", + "license": "MIT", + "dependencies": { + "cli-spinners": "^2.7.0" + }, + "engines": { + "node": ">=14.16" + }, + "peerDependencies": { + "ink": ">=4.0.0", + "react": ">=18.0.0" + } + }, + "node_modules/ink-text-input": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ink-text-input/-/ink-text-input-6.0.0.tgz", + "integrity": "sha512-Fw64n7Yha5deb1rHY137zHTAbSTNelUKuB5Kkk2HACXEtwIHBCf9OH2tP/LQ9fRYTl1F0dZgbW0zPnZk6FA9Lw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "type-fest": "^4.18.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "ink": ">=5", + "react": ">=18" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -1074,18 +2104,165 @@ "node": ">= 0.10" } }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-in-ci": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-in-ci/-/is-in-ci-1.0.0.tgz", + "integrity": "sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg==", + "license": "MIT", + "bin": { + "is-in-ci": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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", "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "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/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-schema": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", @@ -1093,25 +2270,66 @@ "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, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "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/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "license": "ISC" }, "node_modules/math-intrinsics": { @@ -1144,6 +2362,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 +2394,52 @@ } }, "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/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" } }, "node_modules/ms": { @@ -1198,6 +2475,88 @@ "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/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/node-html-markdown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/node-html-markdown/-/node-html-markdown-2.0.0.tgz", + "integrity": "sha512-DqUC3GGP7pwSYxS93SwHoP+qCw78xcMP6C6H2DuC8rPD2AweJRjBzQb5SdXpKtDlqAQ7hVotJcfhgU7hU5Gthw==", + "license": "MIT", + "dependencies": { + "node-html-parser": "^6.1.13" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/node-html-parser": { + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.13.tgz", + "integrity": "sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==", + "license": "MIT", + "dependencies": { + "css-select": "^5.1.0", + "he": "1.2.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -1220,9 +2579,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 +2594,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 +2615,50 @@ "wrappy": "1" } }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -1282,6 +2668,25 @@ "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/patch-console": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/patch-console/-/patch-console-2.0.0.tgz", + "integrity": "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -1291,6 +2696,22 @@ "node": ">=8" } }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/path-to-regexp": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", @@ -1301,10 +2722,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 +2756,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 +2771,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 +2818,98 @@ } }, "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/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-reconciler": { + "version": "0.29.2", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.29.2.tgz", + "integrity": "sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "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/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -1387,6 +2926,29 @@ "node": ">= 18" } }, + "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", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1413,6 +2975,15 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, "node_modules/send": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", @@ -1450,6 +3021,12 @@ "node": ">= 18" } }, + "node_modules/server-destroy": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz", + "integrity": "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==", + "license": "ISC" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -1549,6 +3126,55 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -1575,6 +3201,57 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", @@ -1590,6 +3267,52 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/to-rotated": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-rotated/-/to-rotated-1.0.0.tgz", + "integrity": "sha512-KsEID8AfgUy+pxVRLsWp0VzCa69wxzUDZnzGbyIST/bcgcrMvTYoFBX/QORH4YApoD89EDuUovx4BTdpOn319Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -1599,48 +3322,28 @@ "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" + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "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-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" }, - "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 - } + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/type-is": { @@ -1687,21 +3390,24 @@ "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/url-template": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", + "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==", + "license": "BSD" }, - "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/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } }, "node_modules/vary": { "version": "1.1.2", @@ -1712,6 +3418,31 @@ "node": ">= 0.8" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -1727,6 +3458,21 @@ "node": ">= 8" } }, + "node_modules/widest-line": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz", + "integrity": "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==", + "license": "MIT", + "dependencies": { + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/wrap-ansi": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", @@ -1744,12 +3490,116 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -1759,6 +3609,21 @@ "node": ">=10" } }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yargs": { "version": "18.0.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", @@ -1785,24 +3650,29 @@ "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/yoga-layout": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", + "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==", + "license": "MIT" }, "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..95755a96 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -6,7 +6,8 @@ "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", + "migrate-agents": "node dist/scripts/migrate-agents.js" }, "files": [ "dist", @@ -21,7 +22,7 @@ "description": "", "devDependencies": { "@types/node": "^24.9.1", - "ts-node": "^10.9.2", + "@types/react": "^18.3.12", "typescript": "^5.9.3" }, "dependencies": { @@ -30,12 +31,28 @@ "@ai-sdk/openai": "^2.0.53", "@ai-sdk/openai-compatible": "^1.0.27", "@ai-sdk/provider": "^2.0.0", + "@google-cloud/local-auth": "^3.0.1", + "@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", + "eventsource-parser": "^1.1.2", + "google-auth-library": "^10.5.0", + "googleapis": "^169.0.0", + "hono": "^4.10.7", + "hono-openapi": "^1.1.1", + "ink": "^5.1.0", + "ink-select-input": "^6.2.0", + "ink-spinner": "^5.0.0", + "ink-text-input": "^6.0.0", "json-schema-to-zod": "^2.6.1", "nanoid": "^5.1.6", + "node-html-markdown": "^2.0.0", "ollama-ai-provider-v2": "^1.5.4", + "react": "^18.3.1", + "yaml": "^2.8.2", "yargs": "^18.0.0", "zod": "^4.1.12" } diff --git a/apps/cli/src/agents/agents.ts b/apps/cli/src/agents/agents.ts new file mode 100644 index 00000000..2ebfaef8 --- /dev/null +++ b/apps/cli/src/agents/agents.ts @@ -0,0 +1,35 @@ +import { z } from "zod"; + +export const BaseTool = z.object({ + name: z.string(), +}); + +export const BuiltinTool = BaseTool.extend({ + type: z.literal("builtin"), +}); + +export const McpTool = BaseTool.extend({ + type: z.literal("mcp"), + description: z.string(), + inputSchema: z.any(), + mcpServerName: z.string(), +}); + +export const AgentAsATool = BaseTool.extend({ + type: z.literal("agent"), +}); + +export const ToolAttachment = z.discriminatedUnion("type", [ + BuiltinTool, + McpTool, + AgentAsATool, +]); + +export const Agent = z.object({ + name: z.string(), + provider: z.string().optional(), + model: z.string().optional(), + description: z.string().optional(), + instructions: z.string(), + tools: z.record(z.string(), ToolAttachment).optional(), +}); diff --git a/apps/cli/src/agents/repo.ts b/apps/cli/src/agents/repo.ts new file mode 100644 index 00000000..c770b1ee --- /dev/null +++ b/apps/cli/src/agents/repo.ts @@ -0,0 +1,94 @@ +import { WorkDir } from "../config/config.js"; +import fs from "fs/promises"; +import { glob } from "node:fs/promises"; +import path from "path"; +import z from "zod"; +import { Agent } from "./agents.js"; +import { parse, stringify } from "yaml"; + +const UpdateAgentSchema = Agent.omit({ name: true }); + +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 { + private readonly agentsDir = path.join(WorkDir, "agents"); + + async list(): Promise[]> { + const result: z.infer[] = []; + + // list all md files in workdir/agents/ + const matches = await Array.fromAsync(glob("**/*.md", { cwd: this.agentsDir })); + for (const file of matches) { + try { + const agent = await this.parseAgentMd(path.join(this.agentsDir, file)); + result.push(agent); + } catch (error) { + console.error(`Error parsing agent ${file}: ${error instanceof Error ? error.message : String(error)}`); + continue; + } + } + return result; + } + + private async parseAgentMd(filePath: string): Promise> { + const raw = await fs.readFile(filePath, "utf8"); + + // strip the path prefix from the file name + // and the .md extension + const agentName = filePath + .replace(this.agentsDir + "/", "") + .replace(/\.md$/, ""); + let agent: z.infer = { + name: agentName, + instructions: raw, + }; + let content = raw; + + // check for frontmatter markers at start + if (raw.startsWith("---")) { + const end = raw.indexOf("\n---", 3); + + if (end !== -1) { + const fm = raw.slice(3, end).trim(); // YAML text + content = raw.slice(end + 4).trim(); // body after frontmatter + const yaml = parse(fm); + const parsed = Agent + .omit({ name: true, instructions: true }) + .parse(yaml); + agent = { + ...agent, + ...parsed, + instructions: content, + }; + } + } + + return agent; + } + + async fetch(id: string): Promise> { + return this.parseAgentMd(path.join(this.agentsDir, `${id}.md`)); + } + + async create(agent: z.infer): Promise { + const { instructions, ...rest } = agent; + const contents = `---\n${stringify(rest)}\n---\n${instructions}`; + await fs.writeFile(path.join(this.agentsDir, `${agent.name}.md`), contents); + } + + async update(id: string, agent: z.infer): Promise { + const { instructions, ...rest } = agent; + const contents = `---\n${stringify(rest)}\n---\n${instructions}`; + await fs.writeFile(path.join(this.agentsDir, `${id}.md`), contents); + } + + async delete(id: string): Promise { + await fs.unlink(path.join(this.agentsDir, `${id}.md`)); + } +} \ No newline at end of file diff --git a/apps/cli/src/application/lib/agent.ts b/apps/cli/src/agents/runtime.ts similarity index 72% rename from apps/cli/src/application/lib/agent.ts rename to apps/cli/src/agents/runtime.ts index 5ee019ac..92c3fea7 100644 --- a/apps/cli/src/application/lib/agent.ts +++ b/apps/cli/src/agents/runtime.ts @@ -1,19 +1,113 @@ 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"; +import { PrefixLogger } from "../shared/prefix-logger.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 { + await this.bus.publish({ + runId, + type: "run-processing-start", + subflow: [], + }); + 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); + await this.bus.publish({ + runId, + type: "run-processing-end", + subflow: [], + }); + } + } +} export async function mapAgentTool(t: z.infer): Promise { switch (t.type) { @@ -128,8 +222,8 @@ export class StreamStepMessageBuilder { }); break; case "finish-step": - this.providerOptions = event.providerOptions; - break; + this.providerOptions = event.providerOptions; + break; } } @@ -168,12 +262,11 @@ function normaliseAskHumanToolCall(message: z.infer) { } export async function loadAgent(id: string): Promise> { - if (id === "copilot") { + if (id === "copilot" || id === "rowboatx") { 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 +355,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 +368,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 +424,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 +434,17 @@ export class AgentState { return; } switch (event.type) { + case "start": + this.runId = event.runId; + this.agentName = event.agentName; + break; + case "spawn-subflow": + // Seed the subflow state with its agent so downstream loadAgent works. + if (!this.subflowStates[event.toolCallId]) { + this.subflowStates[event.toolCallId] = new AgentState(); + } + this.subflowStates[event.toolCallId].agentName = event.agentName; + break; case "message": this.messages.push(event.message); if (event.message.content instanceof Array) { @@ -371,9 +463,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 +495,35 @@ 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> { + const logger = new PrefixLogger(`run-${runId}-${state.agentName}`); + + 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); @@ -434,34 +531,31 @@ export async function* streamAgent(state: AgentState): AsyncGenerator part.type === "tool-call") - ) - ) { - // console.error("Nothing to do, exiting (a.)") - return; - } + loopCounter++; + let loopLogger = logger.child(`iter-${loopCounter}`); + loopLogger.log('starting loop iteration'); // execute any pending tool calls for (const toolCallId of Object.keys(state.pendingToolCalls)) { const toolCall = state.toolCallIdMap[toolCallId]; + let _logger = loopLogger.child(`tc-${toolCallId}-${toolCall.toolName}`); + _logger.log('processing'); // if ask-human, skip if (toolCall.toolName === "ask-human") { + _logger.log('skipping, reason: ask-human'); continue; } // if tool has been denied, deny if (state.deniedToolCallIds[toolCallId]) { - yield* state.ingestAndLogAndYield({ + _logger.log('returning denied tool message, reason: tool has been denied'); + yield* processEvent({ + runId, + messageId: await idGenerator.next(), type: "message", message: { role: "tool", @@ -474,14 +568,18 @@ export async function* streamAgent(state: AgentState): AsyncGenerator part.type === "tool-call") + ) + ) { + loopLogger.log('exiting loop, reason: last message is from assistant and text'); return; } // run one LLM turn. + loopLogger.log('running llm turn'); // stream agent response and build message const messageBuilder = new StreamStepMessageBuilder(); for await (const event of streamLlm( @@ -543,8 +677,10 @@ export async function* streamAgent(state: AgentState): AsyncGenerator('modelConfigRepo'); + const config = await repo.getConfig(); const rl = createInterface({ input, output }); try { @@ -333,14 +311,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 +362,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..0bf11cdc 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/schema.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..0987978e --- /dev/null +++ b/apps/cli/src/application/lib/bus.ts @@ -0,0 +1,35 @@ +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 { + 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)); + } + 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); + 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/entities/example.ts b/apps/cli/src/entities/example.ts new file mode 100644 index 00000000..779ffe75 --- /dev/null +++ b/apps/cli/src/entities/example.ts @@ -0,0 +1,12 @@ +import z from "zod" +import { Agent } from "../agents/agents.js" +import { McpServerDefinition } from "../mcp/schema.js"; + +export const Example = z.object({ + id: z.string(), + instructions: z.string().optional(), + description: z.string().optional(), + entryAgent: z.string().optional(), + agents: z.array(Agent).optional(), + mcpServers: z.record(z.string(), McpServerDefinition).optional(), +}); 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 84% rename from apps/cli/src/application/entities/run-events.ts rename to apps/cli/src/entities/run-events.ts index bdd0c13a..63180354 100644 --- a/apps/cli/src/application/entities/run-events.ts +++ b/apps/cli/src/entities/run-events.ts @@ -1,16 +1,23 @@ 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 RunProcessingStartEvent = BaseRunEvent.extend({ + type: z.literal("run-processing-start"), +}); + +export const RunProcessingEndEvent = BaseRunEvent.extend({ + type: z.literal("run-processing-end"), +}); + export const StartEvent = BaseRunEvent.extend({ type: z.literal("start"), - runId: z.string(), agentName: z.string(), }); @@ -27,17 +34,20 @@ export const LlmStreamEvent = BaseRunEvent.extend({ export const MessageEvent = BaseRunEvent.extend({ type: z.literal("message"), + messageId: z.string(), message: Message, }); export const ToolInvocationEvent = BaseRunEvent.extend({ type: z.literal("tool-invocation"), + toolCallId: z.string().optional(), toolName: z.string(), input: z.string(), }); export const ToolResultEvent = BaseRunEvent.extend({ type: z.literal("tool-result"), + toolCallId: z.string().optional(), toolName: z.string(), result: z.any(), }); @@ -71,6 +81,8 @@ export const RunErrorEvent = BaseRunEvent.extend({ }); export const RunEvent = z.union([ + RunProcessingStartEvent, + RunProcessingEndEvent, StartEvent, SpawnSubFlowEvent, LlmStreamEvent, @@ -82,4 +94,4 @@ export const RunEvent = z.union([ ToolPermissionRequestEvent, ToolPermissionResponseEvent, RunErrorEvent, -]); \ No newline at end of file +]); 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/knowledge/sync_calendar.ts b/apps/cli/src/knowledge/sync_calendar.ts new file mode 100644 index 00000000..4168e2e4 --- /dev/null +++ b/apps/cli/src/knowledge/sync_calendar.ts @@ -0,0 +1,286 @@ +import fs from 'fs'; +import path from 'path'; +import { google } from 'googleapis'; +import { authenticate } from '@google-cloud/local-auth'; +import { OAuth2Client } from 'google-auth-library'; +import { NodeHtmlMarkdown } from 'node-html-markdown' + +// Configuration +const CREDENTIALS_PATH = path.join(process.cwd(), 'credentials.json'); +const TOKEN_PATH = path.join(process.cwd(), 'token_calendar_notes.json'); // Changed to force re-auth with new scopes +const SYNC_INTERVAL_MS = 60 * 1000; +const SCOPES = [ + 'https://www.googleapis.com/auth/calendar.readonly', + 'https://www.googleapis.com/auth/drive.readonly' +]; + +const nhm = new NodeHtmlMarkdown(); + +// --- Auth Functions --- + +async function loadSavedCredentialsIfExist(): Promise { + try { + if (!fs.existsSync(TOKEN_PATH)) return null; + const tokenContent = fs.readFileSync(TOKEN_PATH, 'utf-8'); + const tokenData = JSON.parse(tokenContent); + + const credsContent = fs.readFileSync(CREDENTIALS_PATH, 'utf-8'); + const keys = JSON.parse(credsContent); + const key = keys.installed || keys.web; + + const client = new google.auth.OAuth2( + key.client_id, + key.client_secret, + key.redirect_uris ? key.redirect_uris[0] : 'http://localhost' + ); + + client.setCredentials({ + refresh_token: tokenData.refresh_token || tokenData.refreshToken, + access_token: tokenData.token || tokenData.access_token, + expiry_date: tokenData.expiry || tokenData.expiry_date, + scope: tokenData.scope + }); + + return client; + } catch (err) { + console.error("Error loading saved credentials:", err); + return null; + } +} + +async function saveCredentials(client: OAuth2Client) { + const content = fs.readFileSync(CREDENTIALS_PATH, 'utf-8'); + const keys = JSON.parse(content); + const key = keys.installed || keys.web; + const payload = JSON.stringify({ + type: 'authorized_user', + client_id: key.client_id, + client_secret: key.client_secret, + refresh_token: client.credentials.refresh_token, + access_token: client.credentials.access_token, + expiry_date: client.credentials.expiry_date, + }, null, 2); + fs.writeFileSync(TOKEN_PATH, payload); +} + +async function authorize(): Promise { + let client = await loadSavedCredentialsIfExist(); + if (client && client.credentials && client.credentials.expiry_date && client.credentials.expiry_date > Date.now()) { + console.log("Using existing valid token."); + return client; + } + + if (client && client.credentials && (!client.credentials.expiry_date || client.credentials.expiry_date <= Date.now()) && client.credentials.refresh_token) { + console.log("Refreshing expired token..."); + try { + await client.refreshAccessToken(); + await saveCredentials(client); + return client; + } catch (e) { + console.error("Failed to refresh token:", e); + if (fs.existsSync(TOKEN_PATH)) fs.unlinkSync(TOKEN_PATH); + } + } + + console.log("Performing new OAuth authentication..."); + client = await authenticate({ + scopes: SCOPES, + keyfilePath: CREDENTIALS_PATH, + }) as any; + if (client && client.credentials) { + await saveCredentials(client); + } + return client!; +} + +// --- Helper Functions --- + +function cleanFilename(name: string): string { + return name.replace(/[\\/*?:\"<>|]/g, "").replace(/\s+/g, "_").substring(0, 100).trim(); +} + +// --- Sync Logic --- + +function cleanUpOldFiles(currentEventIds: Set, syncDir: string) { + if (!fs.existsSync(syncDir)) return; + + const files = fs.readdirSync(syncDir); + for (const filename of files) { + if (filename === 'sync_state.json') continue; + + // We expect files like: + // {eventId}.json + // {eventId}_doc_{docId}.md + + let eventId: string | null = null; + + if (filename.endsWith('.json')) { + eventId = filename.replace('.json', ''); + } else if (filename.endsWith('.md')) { + // Try to extract eventId from prefix + // Assuming eventId doesn't contain underscores usually, but if it does, this split might be fragile. + // Google Calendar IDs are usually alphanumeric. + // Let's rely on the delimiter we use: "_doc_" + const parts = filename.split('_doc_'); + if (parts.length > 1) { + eventId = parts[0]; + } + } + + if (eventId && !currentEventIds.has(eventId)) { + try { + fs.unlinkSync(path.join(syncDir, filename)); + console.log(`Removed old/out-of-window file: ${filename}`); + } catch (e) { + console.error(`Error deleting file ${filename}:`, e); + } + } + } +} + +async function saveEvent(event: any, syncDir: string): Promise { + const eventId = event.id; + if (!eventId) return false; + + const filePath = path.join(syncDir, `${eventId}.json`); + + try { + fs.writeFileSync(filePath, JSON.stringify(event, null, 2)); + return true; + } catch (e) { + console.error(`Error saving event ${eventId}:`, e); + return false; + } +} + +async function processAttachments(drive: any, event: any, syncDir: string) { + if (!event.attachments || event.attachments.length === 0) return; + + const eventId = event.id; + const eventTitle = event.summary || 'Untitled'; + const eventDate = event.start?.dateTime || event.start?.date || 'Unknown'; + const organizer = event.organizer?.email || 'Unknown'; + + for (const att of event.attachments) { + // We only care about Google Docs + if (att.mimeType === 'application/vnd.google-apps.document') { + const fileId = att.fileId; + const safeTitle = cleanFilename(att.title); + // Unique filename linked to event + const filename = `${eventId}_doc_${safeTitle}.md`; + const filePath = path.join(syncDir, filename); + + // Simple cache check: if file exists, skip. + // Ideally we check modifiedTime, but that requires an extra API call per file. + // Given the loop interval, we can just check existence to save quota. + // If user updates notes, they might want them re-synced. + // For now, let's just check existence. To be smarter, we'd need a state file or check API. + if (fs.existsSync(filePath)) continue; + + try { + const res = await drive.files.export({ + fileId: fileId, + mimeType: 'text/html' + }); + + const html = res.data; + const md = nhm.translate(html); + + const frontmatter = [ + `# ${att.title}`, + `**Event:** ${eventTitle}`, + `**Date:** ${eventDate}`, + `**Organizer:** ${organizer}`, + `**Link:** ${att.fileUrl}`, + `---`, + `` + ].join('\n'); + + fs.writeFileSync(filePath, frontmatter + md); + console.log(`Synced Note: ${att.title} for event ${eventTitle}`); + } catch (e) { + console.error(`Failed to download note ${att.title}:`, e); + } + } + } +} + +async function syncCalendarWindow(auth: OAuth2Client, syncDir: string, lookbackDays: number) { + // Calculate window + const now = new Date(); + const lookbackMs = lookbackDays * 24 * 60 * 60 * 1000; + const twoWeeksForwardMs = 14 * 24 * 60 * 60 * 1000; + + const timeMin = new Date(now.getTime() - lookbackMs).toISOString(); + const timeMax = new Date(now.getTime() + twoWeeksForwardMs).toISOString(); + + console.log(`Syncing calendar from ${timeMin} to ${timeMax} (lookback: ${lookbackDays} days)...`); + + const calendar = google.calendar({ version: 'v3', auth }); + const drive = google.drive({ version: 'v3', auth }); + + try { + const res = await calendar.events.list({ + calendarId: 'primary', + timeMin: timeMin, + timeMax: timeMax, + singleEvents: true, + orderBy: 'startTime' + }); + + const events = res.data.items || []; + const currentEventIds = new Set(); + + if (events.length === 0) { + console.log("No events found in this window."); + } else { + console.log(`Found ${events.length} events.`); + for (const event of events) { + if (event.id) { + await saveEvent(event, syncDir); + await processAttachments(drive, event, syncDir); + currentEventIds.add(event.id); + } + } + } + + cleanUpOldFiles(currentEventIds, syncDir); + + } catch (error) { + console.error("An error occurred during calendar sync:", error); + } +} + +async function main() { + console.log("Starting Google Calendar & Notes Sync (TS)..."); + + const syncDirArg = process.argv[2]; + const lookbackDaysArg = process.argv[3]; + + const SYNC_DIR = syncDirArg || 'synced_calendar_events'; + const LOOKBACK_DAYS = lookbackDaysArg ? parseInt(lookbackDaysArg, 10) : 14; + + if (isNaN(LOOKBACK_DAYS) || LOOKBACK_DAYS <= 0) { + console.error("Error: Lookback days must be a positive number."); + process.exit(1); + } + + if (!fs.existsSync(SYNC_DIR)) { + fs.mkdirSync(SYNC_DIR, { recursive: true }); + } + + try { + const auth = await authorize(); + console.log("Authorization successful."); + + while (true) { + await syncCalendarWindow(auth, SYNC_DIR, LOOKBACK_DAYS); + console.log(`Sleeping for ${SYNC_INTERVAL_MS / 1000} seconds...`); + await new Promise(resolve => setTimeout(resolve, SYNC_INTERVAL_MS)); + } + } catch (error) { + console.error("Fatal error in main loop:", error); + } +} + +main().catch(console.error); \ No newline at end of file diff --git a/apps/cli/src/knowledge/sync_gmail.ts b/apps/cli/src/knowledge/sync_gmail.ts new file mode 100644 index 00000000..b795e619 --- /dev/null +++ b/apps/cli/src/knowledge/sync_gmail.ts @@ -0,0 +1,368 @@ +import fs from 'fs'; +import path from 'path'; +import { google } from 'googleapis'; +import { authenticate } from '@google-cloud/local-auth'; +import { NodeHtmlMarkdown } from 'node-html-markdown' +import { OAuth2Client } from 'google-auth-library'; + +// Configuration +const DEFAULT_SYNC_DIR = 'synced_emails_ts'; +const CREDENTIALS_PATH = path.join(process.cwd(), 'credentials.json'); +const TOKEN_PATH = path.join(process.cwd(), 'token_api.json'); // Reuse Python's token +const SYNC_INTERVAL_MS = 60 * 1000; +const SCOPES = ['https://www.googleapis.com/auth/gmail.readonly']; + +const nhm = new NodeHtmlMarkdown(); + +// --- Auth Functions --- + +async function loadSavedCredentialsIfExist(): Promise { + try { + const tokenContent = fs.readFileSync(TOKEN_PATH, 'utf-8'); + const tokenData = JSON.parse(tokenContent); + + const credsContent = fs.readFileSync(CREDENTIALS_PATH, 'utf-8'); + const keys = JSON.parse(credsContent); + const key = keys.installed || keys.web; + + // Manually construct credentials for google.auth.fromJSON + const credentials = { + type: 'authorized_user', + client_id: key.client_id, + client_secret: key.client_secret, + refresh_token: tokenData.refresh_token || tokenData.refreshToken, // Handle both cases + access_token: tokenData.token || tokenData.access_token, // Handle both cases + expiry_date: tokenData.expiry || tokenData.expiry_date + }; + return google.auth.fromJSON(credentials) as OAuth2Client; + } catch (err) { + console.error("Error loading saved credentials:", err); + return null; + } +} + +async function saveCredentials(client: OAuth2Client) { + const content = fs.readFileSync(CREDENTIALS_PATH, 'utf-8'); + const keys = JSON.parse(content); + const key = keys.installed || keys.web; + const payload = JSON.stringify({ + type: 'authorized_user', + client_id: key.client_id, + client_secret: key.client_secret, + refresh_token: client.credentials.refresh_token, + access_token: client.credentials.access_token, + expiry_date: client.credentials.expiry_date, + }, null, 2); + fs.writeFileSync(TOKEN_PATH, payload); +} + +async function authorize(): Promise { + let client = await loadSavedCredentialsIfExist(); + if (client && client.credentials && client.credentials.expiry_date && client.credentials.expiry_date > Date.now()) { + console.log("Using existing valid token."); + return client; + } + + if (client && client.credentials && (!client.credentials.expiry_date || client.credentials.expiry_date <= Date.now()) && client.credentials.refresh_token) { + console.log("Refreshing expired token..."); + try { + await client.refreshAccessToken(); + await saveCredentials(client); // Save refreshed token + return client; + } catch (e) { + console.error("Failed to refresh token:", e); + // Fall through to full re-auth if refresh fails + fs.existsSync(TOKEN_PATH) && fs.unlinkSync(TOKEN_PATH); + } + } + + console.log("Performing new OAuth authentication..."); + client = await authenticate({ + scopes: SCOPES, + keyfilePath: CREDENTIALS_PATH, + }) as any; + if (client && client.credentials) { + await saveCredentials(client); + } + return client!; +} + +// --- Helper Functions --- + +function cleanFilename(name: string): string { + return name.replace(/[\\/*?:":<>|]/g, "").substring(0, 100).trim(); +} + +function decodeBase64(data: string): string { + return Buffer.from(data, 'base64').toString('utf-8'); +} + +function getBody(payload: any): string { + let body = ""; + if (payload.parts) { + for (const part of payload.parts) { + if (part.mimeType === 'text/plain' && part.body && part.body.data) { + const text = decodeBase64(part.body.data); + // Strip quoted lines + const cleanLines = text.split('\n').filter((line: string) => !line.trim().startsWith('>')); + body += cleanLines.join('\n'); + } else if (part.mimeType === 'text/html' && part.body && part.body.data) { + const html = decodeBase64(part.body.data); + let md = nhm.translate(html); + // Simple quote stripping for MD + const cleanLines = md.split('\n').filter((line: string) => !line.trim().startsWith('>')); + body += cleanLines.join('\n'); + } else if (part.parts) { + body += getBody(part); + } + } + } else if (payload.body && payload.body.data) { + const data = decodeBase64(payload.body.data); + if (payload.mimeType === 'text/html') { + let md = nhm.translate(data); + body += md.split('\n').filter((line: string) => !line.trim().startsWith('>')).join('\n'); + } else { + body += data.split('\n').filter((line: string) => !line.trim().startsWith('>')).join('\n'); + } + } + return body; +} + +async function saveAttachment(gmail: any, userId: string, msgId: string, part: any, attachmentsDir: string): Promise { + const filename = part.filename; + const attId = part.body?.attachmentId; + if (!filename || !attId) return null; + + const safeName = `${msgId}_${cleanFilename(filename)}`; + const filePath = path.join(attachmentsDir, safeName); + + if (fs.existsSync(filePath)) return safeName; + + try { + const res = await gmail.users.messages.attachments.get({ + userId, + messageId: msgId, + id: attId + }); + + const data = res.data.data; + if (data) { + fs.writeFileSync(filePath, Buffer.from(data, 'base64')); + console.log(`Saved attachment: ${safeName}`); + return safeName; + } + } catch (e) { + console.error(`Error saving attachment ${filename}:`, e); + } + return null; +} + +// --- Sync Logic --- + +async function processThread(auth: OAuth2Client, threadId: string, syncDir: string, attachmentsDir: string) { + const gmail = google.gmail({ version: 'v1', auth }); + try { + const res = await gmail.users.threads.get({ userId: 'me', id: threadId }); + const thread = res.data; + const messages = thread.messages; + + if (!messages || messages.length === 0) return; + + // Subject from first message + const firstHeader = messages[0].payload?.headers; + const subject = firstHeader?.find(h => h.name === 'Subject')?.value || '(No Subject)'; + + let mdContent = `# ${subject}\n\n`; + mdContent += `**Thread ID:** ${threadId}\n`; + mdContent += `**Message Count:** ${messages.length}\n\n---\n\n`; + + for (const msg of messages) { + const msgId = msg.id!; + const headers = msg.payload?.headers || []; + const from = headers.find(h => h.name === 'From')?.value || 'Unknown'; + const date = headers.find(h => h.name === 'Date')?.value || 'Unknown'; + + mdContent += `### From: ${from}\n`; + mdContent += `**Date:** ${date}\n\n`; + + const body = getBody(msg.payload); + mdContent += `${body}\n\n`; + + // Attachments + const parts: any[] = []; + const traverseParts = (pList: any[]) => { + for (const p of pList) { + parts.push(p); + if (p.parts) traverseParts(p.parts); + } + }; + if (msg.payload?.parts) traverseParts(msg.payload.parts); + + let attachmentsFound = false; + for (const part of parts) { + if (part.filename && part.body?.attachmentId) { + const savedName = await saveAttachment(gmail, 'me', msgId, part, attachmentsDir); + if (savedName) { + if (!attachmentsFound) { + mdContent += "**Attachments:**\n"; + attachmentsFound = true; + } + mdContent += `- [${part.filename}](attachments/${savedName})\n`; + } + } + } + mdContent += "\n---\n\n"; + } + + fs.writeFileSync(path.join(syncDir, `${threadId}.md`), mdContent); + console.log(`Synced Thread: ${subject} (${threadId})`); + + } catch (error) { + console.error(`Error processing thread ${threadId}:`, error); + } +} + +function loadState(stateFile: string): { historyId?: string } { + if (fs.existsSync(stateFile)) { + return JSON.parse(fs.readFileSync(stateFile, 'utf-8')); + } + return {}; +} + +function saveState(historyId: string, stateFile: string) { + fs.writeFileSync(stateFile, JSON.stringify({ + historyId, + last_sync: new Date().toISOString() + }, null, 2)); +} + +async function fullSync(auth: OAuth2Client, syncDir: string, attachmentsDir: string, stateFile: string, lookbackDays: number) { + console.log(`Performing full sync of last ${lookbackDays} days...`); + const gmail = google.gmail({ version: 'v1', auth }); + + const pastDate = new Date(); + pastDate.setDate(pastDate.getDate() - lookbackDays); + const dateQuery = pastDate.toISOString().split('T')[0].replace(/-/g, '/'); + + // Get History ID + const profile = await gmail.users.getProfile({ userId: 'me' }); + const currentHistoryId = profile.data.historyId!; + + let pageToken: string | undefined; + do { + const res: any = await gmail.users.threads.list({ + userId: 'me', + q: `after:${dateQuery}`, + pageToken + }); + + const threads = res.data.threads; + if (threads) { + for (const thread of threads) { + await processThread(auth, thread.id!, syncDir, attachmentsDir); + } + } + pageToken = res.data.nextPageToken; + } while (pageToken); + + saveState(currentHistoryId, stateFile); + console.log("Full sync complete."); +} + +async function partialSync(auth: OAuth2Client, startHistoryId: string, syncDir: string, attachmentsDir: string, stateFile: string, lookbackDays: number) { + console.log(`Checking updates since historyId ${startHistoryId}...`); + const gmail = google.gmail({ version: 'v1', auth }); + + try { + const res = await gmail.users.history.list({ + userId: 'me', + startHistoryId, + historyTypes: ['messageAdded'] + }); + + const changes = res.data.history; + if (!changes || changes.length === 0) { + console.log("No new changes."); + const profile = await gmail.users.getProfile({ userId: 'me' }); + saveState(profile.data.historyId!, stateFile); + return; + } + + console.log(`Found ${changes.length} history records.`); + const threadIds = new Set(); + + for (const record of changes) { + if (record.messagesAdded) { + for (const item of record.messagesAdded) { + if (item.message?.threadId) { + threadIds.add(item.message.threadId); + } + } + } + } + + for (const tid of threadIds) { + await processThread(auth, tid, syncDir, attachmentsDir); + } + + const profile = await gmail.users.getProfile({ userId: 'me' }); + saveState(profile.data.historyId!, stateFile); + + } catch (error: any) { + if (error.response?.status === 404) { + console.log("History ID expired. Falling back to full sync."); + await fullSync(auth, syncDir, attachmentsDir, stateFile, lookbackDays); + } else { + console.error("Error during partial sync:", error); + // If 401, remove token to force re-auth next run + if (error.response?.status === 401 && fs.existsSync(TOKEN_PATH)) { + console.log("401 Unauthorized. Deleting token to force re-authentication."); + fs.unlinkSync(TOKEN_PATH); + } + } + } +} + +async function main() { + console.log("Starting Gmail Sync (TS)..."); + const syncDirArg = process.argv[2]; + const lookbackDaysArg = process.argv[3]; + + const SYNC_DIR = syncDirArg || DEFAULT_SYNC_DIR; + const LOOKBACK_DAYS = lookbackDaysArg ? parseInt(lookbackDaysArg, 10) : 7; // Default to 7 days + + if (isNaN(LOOKBACK_DAYS) || LOOKBACK_DAYS <= 0) { + console.error("Error: Lookback days must be a positive number."); + process.exit(1); + } + + const ATTACHMENTS_DIR = path.join(SYNC_DIR, 'attachments'); + const STATE_FILE = path.join(SYNC_DIR, 'sync_state.json'); + + // Ensure directories exist + if (!fs.existsSync(SYNC_DIR)) fs.mkdirSync(SYNC_DIR, { recursive: true }); + if (!fs.existsSync(ATTACHMENTS_DIR)) fs.mkdirSync(ATTACHMENTS_DIR, { recursive: true }); + + try { + const auth = await authorize(); + console.log("Authorization successful."); + + while (true) { + const state = loadState(STATE_FILE); + if (!state.historyId) { + console.log("No history ID found, starting full sync..."); + await fullSync(auth, SYNC_DIR, ATTACHMENTS_DIR, STATE_FILE, LOOKBACK_DAYS); + } else { + console.log("History ID found, starting partial sync..."); + await partialSync(auth, state.historyId, SYNC_DIR, ATTACHMENTS_DIR, STATE_FILE, LOOKBACK_DAYS); + } + + console.log(`Sleeping for ${SYNC_INTERVAL_MS / 1000} seconds...`); + await new Promise(resolve => setTimeout(resolve, SYNC_INTERVAL_MS)); + } + } catch (error) { + console.error("Fatal error in main loop:", error); + } +} + +main().catch(console.error); diff --git a/apps/cli/src/mcp/mcp.ts b/apps/cli/src/mcp/mcp.ts new file mode 100644 index 00000000..7131de12 --- /dev/null +++ b/apps/cli/src/mcp/mcp.ts @@ -0,0 +1,123 @@ +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"; +import { + connectionState, + ListToolsResponse, + McpServerDefinition, + McpServerList, +} from "./schema.js"; + +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; +} diff --git a/apps/cli/src/mcp/repo.ts b/apps/cli/src/mcp/repo.ts new file mode 100644 index 00000000..fbb4106e --- /dev/null +++ b/apps/cli/src/mcp/repo.ts @@ -0,0 +1,44 @@ +import { WorkDir } from "../config/config.js"; +import { McpServerConfig, McpServerDefinition } from "./schema.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)); + } +} diff --git a/apps/cli/src/mcp/schema.ts b/apps/cli/src/mcp/schema.ts new file mode 100644 index 00000000..2637397f --- /dev/null +++ b/apps/cli/src/mcp/schema.ts @@ -0,0 +1,50 @@ +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), +}); + +export 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(), + })), +}); + +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(), +}); 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..5b741f2b --- /dev/null +++ b/apps/cli/src/runs/repo.ts @@ -0,0 +1,144 @@ +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>; + list(cursor?: 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, + }; + } + + 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/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/scripts/migrate-agents.ts b/apps/cli/src/scripts/migrate-agents.ts new file mode 100644 index 00000000..93f66fcc --- /dev/null +++ b/apps/cli/src/scripts/migrate-agents.ts @@ -0,0 +1,23 @@ +import { Agent } from "../agents/agents.js"; +import { IAgentsRepo } from "../agents/repo.js"; +import { WorkDir } from "../config/config.js"; +import container from "../di/container.js"; +import { glob, readFile } from "node:fs/promises"; +import path from "path"; + +const main = async () => { + const agentsRepo = container.resolve("agentsRepo"); + const matches = await Array.fromAsync(glob("**/*.json", { cwd: path.join(WorkDir, "agents") })); + for (const file of matches) { + try { + const agent = Agent.parse(JSON.parse(await readFile(path.join(WorkDir, "agents", file), "utf8"))); + await agentsRepo.create(agent); + console.error(`migrated agent ${file}`); + } catch (error) { + console.error(`Error parsing agent ${file}: ${error instanceof Error ? error.message : String(error)}`); + continue; + } + } +} + +main(); \ 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..6aa10c25 --- /dev/null +++ b/apps/cli/src/server.ts @@ -0,0 +1,201 @@ +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 } from "./mcp/mcp.js"; +import { ListToolsResponse, McpServerDefinition, McpServerList } from "./mcp/schema.js"; +import { IMcpConfigRepo } from './mcp/repo.js'; +import { IModelConfigRepo } from './models/repo.js'; +import { ModelConfig, Provider } from "./models/models.js"; +import { IAgentsRepo } from "./agents/repo.js"; +import { Agent } from "./agents/agents.js"; +import { AskHumanResponsePayload, authorizePermission, createMessage, createRun, replyToHumanInputRequest, Run, stop, ToolPermissionAuthorizePayload } from './runs/runs.js'; +import { IRunsRepo, CreateRunOptions, ListRunsResponse } from './runs/repo.js'; +import { IBus } from './application/lib/bus.js'; +import { cors } from 'hono/cors'; + +let id = 0; + +const routes = new Hono() + .post( + '/runs/:runId/messages/new', + describeRoute({ + summary: 'Create a new message', + description: 'Create a new message', + responses: { + 200: { + description: 'Message created', + content: { + 'application/json': { + schema: resolver(z.object({ + messageId: z.string(), + })), + }, + }, + }, + }, + }), + validator('param', z.object({ + runId: z.string(), + })), + validator('json', z.object({ + message: z.string(), + })), + async (c) => { + const messageId = await createMessage(c.req.valid('param').runId, c.req.valid('json').message); + return c.json({ + messageId, + }); + } + ) + .post( + '/runs/:runId/permissions/authorize', + describeRoute({ + summary: 'Authorize permission', + description: 'Authorize a permission', + responses: { + 200: { + description: 'Permission authorized', + content: { + 'application/json': { + schema: resolver(z.object({ + success: z.literal(true), + })), + }, + } + }, + }, + }), + validator('param', z.object({ + runId: z.string(), + })), + validator('json', ToolPermissionAuthorizePayload), + async (c) => { + const response = await authorizePermission( + c.req.valid('param').runId, + c.req.valid('json') + ); + return c.json({ + success: true, + }); + } + ) + .post( + '/runs/:runId/human-input-requests/:requestId/reply', + describeRoute({ + summary: 'Reply to human input request', + description: 'Reply to a human input request', + responses: { + 200: { + description: 'Human input request replied', + }, + }, + }), + validator('param', z.object({ + runId: z.string(), + })), + validator('json', AskHumanResponsePayload), + async (c) => { + const response = await replyToHumanInputRequest( + c.req.valid('param').runId, + c.req.valid('json') + ); + return c.json({ + success: true, + }); + } + ) + .post( + '/runs/:runId/stop', + describeRoute({ + summary: 'Stop run', + description: 'Stop a run', + responses: { + 200: { + description: 'Run stopped', + }, + }, + }), + validator('param', z.object({ + runId: z.string(), + })), + async (c) => { + const response = await stop(c.req.valid('param').runId); + return c.json({ + success: true, + }); + } + ) + .get( + '/stream', + describeRoute({ + summary: 'Subscribe to run events', + description: 'Subscribe to run events', + }), + async (c) => { + return streamSSE(c, async (stream) => { + const bus = container.resolve('bus'); + + let id = 0; + let unsub: (() => void) | null = null; + let aborted = false; + + stream.onAbort(() => { + aborted = true; + if (unsub) { + unsub(); + } + }); + + // Subscribe to your bus + unsub = await bus.subscribe('*', async (event) => { + if (aborted) return; + + await stream.writeSSE({ + data: JSON.stringify(event), + event: "message", + id: String(id++), + }); + }); + + // Keep the function alive until the client disconnects + while (!aborted) { + await stream.sleep(1000); // any interval is fine + } + }); + } + ) + ; + +const app = new Hono() + .use("/*", cors()) + .route("/", routes) + .get( + "/openapi.json", + openAPIRouteHandler(routes, { + documentation: { + info: { + title: "Hono", + version: "1.0.0", + description: "RowboatX API", + }, + }, + }), + ); + +// 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 diff --git a/apps/cli/src/shared/prefix-logger.ts b/apps/cli/src/shared/prefix-logger.ts new file mode 100644 index 00000000..8d199b56 --- /dev/null +++ b/apps/cli/src/shared/prefix-logger.ts @@ -0,0 +1,26 @@ +// create a PrefixLogger class that wraps console.log with a prefix +// and allows chaining with a parent logger +export class PrefixLogger { + private prefix: string; + private parent: PrefixLogger | null; + + constructor(prefix: string, parent: PrefixLogger | null = null) { + this.prefix = prefix; + this.parent = parent; + } + + log(...args: any[]) { + const timestamp = new Date().toISOString(); + const prefix = '[' + this.prefix + ']'; + + if (this.parent) { + this.parent.log(prefix, ...args); + } else { + console.log(timestamp, prefix, ...args); + } + } + + child(childPrefix: string): PrefixLogger { + return new PrefixLogger(childPrefix, this); + } +} \ No newline at end of file diff --git a/apps/cli/src/tui/api.ts b/apps/cli/src/tui/api.ts new file mode 100644 index 00000000..b54534ac --- /dev/null +++ b/apps/cli/src/tui/api.ts @@ -0,0 +1,190 @@ +import { createParser } from "eventsource-parser"; +import { Agent } from "../agents/agents.js"; +import { AskHumanResponsePayload, Run, ToolPermissionAuthorizePayload } from "../runs/runs.js"; +import { ListRunsResponse } from "../runs/repo.js"; +import { ModelConfig } from "../models/models.js"; +import { RunEvent } from "../entities/run-events.js"; +import z from "zod"; + +const HealthSchema = z.object({ + status: z.literal("ok"), +}); + +const MessageResponse = z.object({ + messageId: z.string(), +}); + +const SuccessSchema = z.object({ + success: z.literal(true), +}); + +type RunEventType = z.infer; + +export interface RowboatApiOptions { + baseUrl?: string; +} + +export class RowboatApi { + readonly baseUrl: string; + constructor({ baseUrl }: RowboatApiOptions = {}) { + this.baseUrl = baseUrl ?? process.env.ROWBOATX_SERVER_URL ?? "http://127.0.0.1:3000"; + } + + private buildUrl(pathname: string): string { + return new URL(pathname, this.baseUrl).toString(); + } + + private async request(pathname: string, init?: RequestInit): Promise { + const headers: Record = { + Accept: "application/json", + }; + if (init?.headers instanceof Headers) { + init.headers.forEach((value, key) => { + headers[key] = value; + }); + } else if (Array.isArray(init?.headers)) { + for (const [key, value] of init.headers) { + headers[key] = value; + } + } else if (init?.headers) { + Object.assign(headers, init.headers as Record); + } + if (init?.body && !headers["Content-Type"]) { + headers["Content-Type"] = "application/json"; + } + const response = await fetch(this.buildUrl(pathname), { + method: "GET", + ...init, + headers, + }); + if (!response.ok) { + const text = await response.text().catch(() => ""); + throw new Error(`Request to ${pathname} failed (${response.status}): ${text || response.statusText}`); + } + if (response.status === 204) { + return undefined as T; + } + const text = await response.text(); + if (!text) { + return undefined as T; + } + return JSON.parse(text) as T; + } + + async getHealth(): Promise> { + const payload = await this.request("/health"); + return HealthSchema.parse(payload); + } + + async getModelConfig(): Promise> { + const payload = await this.request("/models"); + return ModelConfig.parse(payload); + } + + async listAgents(): Promise[]> { + const payload = await this.request("/agents"); + return Agent.array().parse(payload); + } + + async listRuns(cursor?: string): Promise> { + const searchParams = new URLSearchParams(); + if (cursor) { + searchParams.set("cursor", cursor); + } + const payload = await this.request(`/runs${searchParams.size ? `?${searchParams.toString()}` : ""}`); + return ListRunsResponse.parse(payload); + } + + async getRun(runId: string): Promise> { + const payload = await this.request(`/runs/${encodeURIComponent(runId)}`); + return Run.parse(payload); + } + + async createRun(agentId: string): Promise> { + const payload = await this.request("/runs/new", { + method: "POST", + body: JSON.stringify({ agentId }), + }); + return Run.parse(payload); + } + + async sendMessage(runId: string, message: string): Promise> { + const payload = await this.request(`/runs/${encodeURIComponent(runId)}/messages/new`, { + method: "POST", + body: JSON.stringify({ message }), + }); + return MessageResponse.parse(payload); + } + + async authorizeTool(runId: string, payload: z.infer): Promise { + const response = await this.request(`/runs/${encodeURIComponent(runId)}/permissions/authorize`, { + method: "POST", + body: JSON.stringify(payload), + }); + SuccessSchema.parse(response); + } + + async replyToHuman(runId: string, requestId: string, payload: z.infer): Promise { + const response = await this.request(`/runs/${encodeURIComponent(runId)}/human-input-requests/${encodeURIComponent(requestId)}/reply`, { + method: "POST", + body: JSON.stringify(payload), + }); + SuccessSchema.parse(response); + } + + async stopRun(runId: string): Promise { + const response = await this.request(`/runs/${encodeURIComponent(runId)}/stop`, { + method: "POST", + }); + SuccessSchema.parse(response); + } + + async subscribeToEvents(onEvent: (event: RunEventType) => void, onError?: (error: Error) => void): Promise<() => void> { + const controller = new AbortController(); + const response = await fetch(this.buildUrl("/stream"), { + method: "GET", + headers: { + Accept: "text/event-stream", + }, + signal: controller.signal, + }); + if (!response.ok || !response.body) { + throw new Error(`Failed to subscribe to event stream (${response.status})`); + } + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + const parser = createParser((event) => { + if (event.type !== "event" || !event.data) { + return; + } + try { + const parsed = RunEvent.parse(JSON.parse(event.data)); + onEvent(parsed); + } catch (error) { + onError?.(error instanceof Error ? error : new Error(String(error))); + } + }); + + (async () => { + try { + while (true) { + const { value, done } = await reader.read(); + if (done) { + break; + } + parser.feed(decoder.decode(value, { stream: true })); + } + } catch (error) { + if (controller.signal.aborted) { + return; + } + onError?.(error instanceof Error ? error : new Error(String(error))); + } + })(); + + return () => { + controller.abort(); + reader.cancel().catch(() => undefined); + }; + } +} diff --git a/apps/cli/src/tui/index.tsx b/apps/cli/src/tui/index.tsx new file mode 100644 index 00000000..7e3dd3c0 --- /dev/null +++ b/apps/cli/src/tui/index.tsx @@ -0,0 +1,8 @@ +import React from "react"; +import { render } from "ink"; +import { RowboatTui } from "./ui.js"; + +export function runTui({ serverUrl }: { serverUrl?: string }) { + const baseUrl = serverUrl ?? process.env.ROWBOATX_SERVER_URL ?? "http://127.0.0.1:3000"; + render(); +} diff --git a/apps/cli/src/tui/ui.tsx b/apps/cli/src/tui/ui.tsx new file mode 100644 index 00000000..b11bad5e --- /dev/null +++ b/apps/cli/src/tui/ui.tsx @@ -0,0 +1,1174 @@ +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { Box, Text, useApp, useInput, useStdout } from "ink"; +import Spinner from "ink-spinner"; +import SelectInput from "ink-select-input"; +import TextInput from "ink-text-input"; +import z from "zod"; +import { RowboatApi } from "./api.js"; +import { ModelConfig } from "../models/models.js"; +import { Agent } from "../agents/agents.js"; +import { ListRunsResponse } from "../runs/repo.js"; +import { Run } from "../runs/runs.js"; +import { RunEvent } from "../entities/run-events.js"; + +type AgentType = z.infer; +type ModelConfigType = z.infer; +type RunSummary = z.infer["runs"][number]; +type RunType = z.infer; +type RunEventType = z.infer; + +type Toast = { + type: "info" | "error" | "success"; + text: string; +}; + +type ChatLine = { + text: string; + color?: string; + variant?: "user" | "assistant" | "streaming" | "thinking" | "system" | "tool" | "other"; +}; + +type ModalState = + | { type: "agent-picker" } + | { + type: "human-response"; + runId: string; + requestId: string; + subflow: string[]; + prompt: string; + value: string; + submitting: boolean; + }; + +type ConnectionState = "connecting" | "ready" | "error"; +type FocusTarget = "chat" | "sidebar"; + +type PendingPermission = { + toolCallId: string; + toolName: string; + args: unknown; + subflow: string[]; +}; + +type PendingHuman = { + toolCallId: string; + query: string; + subflow: string[]; +}; + +type SidebarItem = + | { kind: "action"; action: "new-copilot" | "new-agent"; label: string; hint?: string } + | { kind: "run"; run: RunSummary; status: { label: string; color: string } }; + +export function RowboatTui({ serverUrl }: { serverUrl: string }) { + const api = useMemo(() => new RowboatApi({ baseUrl: serverUrl }), [serverUrl]); + const { exit } = useApp(); + const { stdout } = useStdout(); + + const [connectionState, setConnectionState] = useState("connecting"); + const [connectionError, setConnectionError] = useState(null); + const [modelConfig, setModelConfig] = useState(null); + const [agents, setAgents] = useState([]); + const [runs, setRuns] = useState([]); + const [runsCursor, setRunsCursor] = useState(); + const [runsLoading, setRunsLoading] = useState(false); + const [runDetails, setRunDetails] = useState>({}); + const [activeRunId, setActiveRunId] = useState(null); + const [draftAgent, setDraftAgent] = useState("copilot"); + const [composerValue, setComposerValue] = useState(""); + const [composerBusy, setComposerBusy] = useState(false); + const [focusTarget, setFocusTarget] = useState("chat"); + const [sidebarIndex, setSidebarIndex] = useState(0); + const [toast, setToast] = useState(null); + const [modal, setModal] = useState(null); + const [streamError, setStreamError] = useState(null); + const [eventStreamActive, setEventStreamActive] = useState(false); + const [chatScrollOffset, setChatScrollOffset] = useState(0); + + const selectedRun = activeRunId ? runDetails[activeRunId] : undefined; + const pendingPermissions = useMemo(() => derivePendingPermissions(selectedRun), [selectedRun]); + const pendingHuman = useMemo(() => derivePendingHuman(selectedRun), [selectedRun]); + + const defaultCopilot = useMemo(() => { + return "copilot"; + }, [agents]); + + useEffect(() => { + if (!agents.length) { + return; + } + setDraftAgent((prev) => prev || defaultCopilot); + }, [agents, defaultCopilot]); + + const runStatusMap = useMemo(() => { + const map: Record = {}; + for (const summary of runs) { + map[summary.id] = getRunStatus(runDetails[summary.id]); + } + return map; + }, [runs, runDetails]); + + const sidebarItems: SidebarItem[] = useMemo(() => { + const items: SidebarItem[] = [ + { + kind: "action", + action: "new-copilot", + label: `+ New chat (${defaultCopilot})`, + hint: "Ctrl+N", + }, + { + kind: "action", + action: "new-agent", + label: "+ New chat (choose agent)", + hint: "Ctrl+G", + }, + ]; + for (const run of runs) { + items.push({ + kind: "run", + run, + status: runStatusMap[run.id] ?? { label: "loading…", color: "gray" }, + }); + } + return items; + }, [defaultCopilot, runStatusMap, runs]); + + useEffect(() => { + setSidebarIndex((idx) => { + if (sidebarItems.length === 0) { + return 0; + } + return Math.min(idx, sidebarItems.length - 1); + }); + }, [sidebarItems.length]); + + const showToast = useCallback((next: Toast) => { + setToast(next); + }, []); + + useEffect(() => { + if (!toast) { + return; + } + const timer = setTimeout(() => { + setToast(null); + }, 4000); + return () => clearTimeout(timer); + }, [toast]); + + const loadInitial = useCallback(async () => { + setConnectionState("connecting"); + setConnectionError(null); + try { + const [health, config, agentList, runsResponse] = await Promise.all([ + api.getHealth(), + api.getModelConfig(), + api.listAgents(), + api.listRuns(), + ]); + if (health.status !== "ok") { + throw new Error("Server is not healthy"); + } + setModelConfig(config); + setAgents(agentList); + setRuns(runsResponse.runs); + setRunsCursor(runsResponse.nextCursor); + setConnectionState("ready"); + } catch (error) { + setConnectionState("error"); + setConnectionError(error instanceof Error ? error.message : String(error)); + } + }, [api]); + + useEffect(() => { + loadInitial(); + }, [loadInitial]); + + useEffect(() => { + if (!activeRunId) { + return; + } + if (runDetails[activeRunId]) { + return; + } + let cancelled = false; + (async () => { + try { + const run = await api.getRun(activeRunId); + if (!cancelled) { + setRunDetails((prev) => ({ + ...prev, + [run.id]: run, + })); + } + } catch (error) { + if (!cancelled) { + showToast({ + type: "error", + text: `Failed to load run: ${error instanceof Error ? error.message : String(error)}`, + }); + } + } + })(); + return () => { + cancelled = true; + }; + }, [activeRunId, api, runDetails, showToast]); + + const refreshRuns = useCallback(async () => { + setRunsLoading(true); + try { + const response = await api.listRuns(); + setRuns(response.runs); + setRunsCursor(response.nextCursor); + } catch (error) { + showToast({ + type: "error", + text: `Failed to refresh runs: ${error instanceof Error ? error.message : String(error)}`, + }); + } finally { + setRunsLoading(false); + } + }, [api, showToast]); + + useEffect(() => { + if (connectionState !== "ready") { + return; + } + let unsub: (() => void) | null = null; + let cancelled = false; + setStreamError(null); + setEventStreamActive(false); + (async () => { + try { + unsub = await api.subscribeToEvents((event) => { + if (cancelled) { + return; + } + setEventStreamActive(true); + if (event.type === "start") { + setRuns((prev) => { + const next = [...prev]; + const idx = next.findIndex((r) => r.id === event.runId); + const summary: RunSummary = { + id: event.runId, + agentId: event.agentName, + createdAt: event.ts ?? new Date().toISOString(), + }; + if (idx >= 0) { + next[idx] = summary; + return next; + } + return [summary, ...next]; + }); + } + setRunDetails((prev) => { + const existing = prev[event.runId]; + if (!existing) { + return prev; + } + return { + ...prev, + [event.runId]: { + ...existing, + log: [...existing.log, event], + }, + }; + }); + }, (error) => { + setStreamError(error.message); + }); + } catch (error) { + if (!cancelled) { + setStreamError(error instanceof Error ? error.message : String(error)); + } + } + })(); + return () => { + cancelled = true; + unsub?.(); + }; + }, [api, connectionState]); + + const startDraftChat = useCallback((agentName: string) => { + setActiveRunId(null); + setDraftAgent(agentName); + setComposerValue(""); + setFocusTarget("chat"); + setSidebarIndex(0); + }, []); + + const composeMessage = useCallback(async (value: string) => { + const trimmed = value.trim(); + if (!trimmed) { + return; + } + setComposerBusy(true); + try { + let runId = activeRunId; + if (!runId) { + const agentName = draftAgent || defaultCopilot; + const run = await api.createRun(agentName); + runId = run.id; + setRuns((prev) => { + const without = prev.filter((r) => r.id !== run.id); + return [ + { + id: run.id, + createdAt: run.createdAt, + agentId: run.agentId, + }, + ...without, + ]; + }); + setRunDetails((prev) => ({ + ...prev, + [run.id]: run, + })); + setActiveRunId(run.id); + } + await api.sendMessage(runId, trimmed); + setComposerValue(""); + showToast({ + type: "success", + text: "Message queued", + }); + } catch (error) { + showToast({ + type: "error", + text: `Failed to send message: ${error instanceof Error ? error.message : String(error)}`, + }); + } finally { + setComposerBusy(false); + } + }, [activeRunId, api, defaultCopilot, draftAgent, showToast]); + + const handleApprovePermission = useCallback(async () => { + const run = selectedRun; + const pending = pendingPermissions[0]; + if (!run || !pending) { + showToast({ type: "info", text: "No pending tool permissions" }); + return; + } + try { + await api.authorizeTool(run.id, { + toolCallId: pending.toolCallId, + response: "approve", + subflow: pending.subflow, + }); + showToast({ type: "success", text: `Approved ${pending.toolName}` }); + } catch (error) { + showToast({ + type: "error", + text: `Failed to approve: ${error instanceof Error ? error.message : String(error)}`, + }); + } + }, [api, pendingPermissions, selectedRun, showToast]); + + const handleDenyPermission = useCallback(async () => { + const run = selectedRun; + const pending = pendingPermissions[0]; + if (!run || !pending) { + showToast({ type: "info", text: "No pending tool permissions" }); + return; + } + try { + await api.authorizeTool(run.id, { + toolCallId: pending.toolCallId, + response: "deny", + subflow: pending.subflow, + }); + showToast({ type: "success", text: `Denied ${pending.toolName}` }); + } catch (error) { + showToast({ + type: "error", + text: `Failed to deny: ${error instanceof Error ? error.message : String(error)}`, + }); + } + }, [api, pendingPermissions, selectedRun, showToast]); + + const handleStopRun = useCallback(async () => { + if (!selectedRun) { + showToast({ type: "info", text: "No run selected" }); + return; + } + try { + await api.stopRun(selectedRun.id); + showToast({ type: "success", text: `Stop requested for ${selectedRun.id}` }); + } catch (error) { + showToast({ + type: "error", + text: `Failed to stop: ${error instanceof Error ? error.message : String(error)}`, + }); + } + }, [api, selectedRun, showToast]); + + const handleReplyHuman = useCallback(async (value: string, context: PendingHuman | undefined) => { + if (!selectedRun || !context) { + showToast({ type: "info", text: "No pending human requests" }); + return; + } + try { + await api.replyToHuman(selectedRun.id, context.toolCallId, { + toolCallId: context.toolCallId, + response: value, + subflow: context.subflow, + }); + showToast({ type: "success", text: "Reply sent" }); + } catch (error) { + showToast({ + type: "error", + text: `Failed to send reply: ${error instanceof Error ? error.message : String(error)}`, + }); + throw error; + } + }, [api, selectedRun, showToast]); + + const currentHumanRequest = pendingHuman[0]; + const maxVisibleEvents = Math.max(8, (stdout?.rows ?? 40) - 14); + + const chatTimeline = useMemo(() => { + if (!selectedRun) { + return { + visibleEvents: [] as ChatLine[], + maxOffset: 0, + total: 0, + }; + } + const lines: ChatLine[] = []; + let streamingText = ""; + let streamingActive = false; + let reasoningText = ""; + let reasoningActive = false; + for (const event of selectedRun.log) { + if (event.type === "llm-stream-event") { + const step = event.event; + switch (step.type) { + case "text-start": + streamingActive = true; + streamingText = ""; + break; + case "text-delta": + streamingActive = true; + streamingText += step.delta; + break; + case "text-end": + case "finish-step": + streamingActive = false; + break; + case "reasoning-start": + reasoningActive = true; + reasoningText = ""; + break; + case "reasoning-delta": + reasoningActive = true; + reasoningText += step.delta; + break; + case "reasoning-end": + reasoningActive = false; + break; + default: + break; + } + continue; + } + const formatted = formatEvent(event); + if (formatted) { + lines.push(formatted); + } + } + if (reasoningActive && reasoningText) { + lines.push({ + text: `assistant (thinking): ${reasoningText}`, + color: "black", + variant: "thinking", + }); + } + if (streamingActive && streamingText) { + lines.push({ + text: `assistant (streaming): ${streamingText}`, + color: "black", + variant: "streaming", + }); + } + const total = lines.length; + const maxOffset = Math.max(0, total - maxVisibleEvents); + const clampedOffset = Math.min(chatScrollOffset, maxOffset); + const end = total - clampedOffset; + const start = Math.max(0, end - maxVisibleEvents); + return { + visibleEvents: lines.slice(start, end), + maxOffset, + total, + }; + }, [chatScrollOffset, maxVisibleEvents, selectedRun]); + + useEffect(() => { + setChatScrollOffset(0); + }, [selectedRun?.id]); + + useEffect(() => { + setChatScrollOffset((offset) => Math.min(offset, chatTimeline.maxOffset)); + }, [chatTimeline.maxOffset]); + + useInput((input, key) => { + if (modal) { + if (key.escape) { + setModal(null); + } + return; + } + if (key.tab) { + setFocusTarget((prev) => (prev === "chat" ? "sidebar" : "chat")); + return; + } + if (key.ctrl && input === "q") { + exit(); + return; + } + if (key.ctrl && input === "n") { + startDraftChat(defaultCopilot); + return; + } + if (key.ctrl && input === "g") { + if (agents.length === 0) { + showToast({ type: "error", text: "No agents available" }); + return; + } + setModal({ type: "agent-picker" }); + return; + } + if (key.ctrl && input === "l") { + refreshRuns(); + return; + } + if (key.ctrl && input === "a") { + handleApprovePermission(); + return; + } + if (key.ctrl && input === "d") { + handleDenyPermission(); + return; + } + if (key.ctrl && input === "s") { + handleStopRun(); + return; + } + if (key.ctrl && input === "h") { + if (!currentHumanRequest) { + showToast({ type: "info", text: "No pending human input requests" }); + return; + } + if (!selectedRun) { + showToast({ type: "info", text: "Select a run to respond" }); + return; + } + setModal({ + type: "human-response", + runId: selectedRun.id, + requestId: currentHumanRequest.toolCallId, + subflow: currentHumanRequest.subflow, + prompt: currentHumanRequest.query, + value: "", + submitting: false, + }); + return; + } + if (focusTarget === "sidebar") { + if (key.upArrow) { + setSidebarIndex((idx) => Math.max(0, idx - 1)); + return; + } + if (key.downArrow) { + setSidebarIndex((idx) => Math.min(sidebarItems.length - 1, idx + 1)); + return; + } + if (key.return) { + const item = sidebarItems[sidebarIndex]; + if (!item) { + return; + } + if (item.kind === "action") { + if (item.action === "new-copilot") { + startDraftChat(defaultCopilot); + } else { + if (agents.length === 0) { + showToast({ type: "error", text: "No agents available" }); + } else { + setModal({ type: "agent-picker" }); + } + } + } else { + setActiveRunId(item.run.id); + setFocusTarget("chat"); + } + } + } + if (focusTarget === "chat") { + const scrollStep = Math.max(3, Math.floor(maxVisibleEvents / 2)); + if (key.pageUp) { + setChatScrollOffset((offset) => Math.min(chatTimeline.maxOffset, offset + scrollStep)); + return; + } + if (key.pageDown) { + setChatScrollOffset((offset) => Math.max(0, offset - scrollStep)); + return; + } + } + }); + + return ( + +
+ + + + 0} + scrollHint={chatTimeline.maxOffset > 0} + /> + + + + + Tab toggles focus · Ctrl+N new Copilot chat · Ctrl+G choose agent · Ctrl+L refresh chats · Ctrl+Q quit + + + + {toast && ( + + + {toast.text} + + + )} + + {modal && ( + + {modal.type === "agent-picker" && ( + { + setModal(null); + startDraftChat(agent); + }} + onCancel={() => setModal(null)} + /> + )} + {modal.type === "human-response" && ( + setModal({ ...modal, value })} + onSubmit={async (value) => { + const ctx: PendingHuman = { + toolCallId: modal.requestId, + query: modal.prompt, + subflow: modal.subflow, + }; + setModal({ ...modal, submitting: true }); + try { + await handleReplyHuman(value.trim(), ctx); + setModal(null); + } catch { + setModal({ ...modal, submitting: false }); + } + }} + onCancel={() => setModal(null)} + /> + )} + + )} + + ); +} + +function Header({ + serverUrl, + state, + error, + modelConfig, + agentsCount, + runsCount, + runsCursor, + streamError, + listening, +}: { + serverUrl: string; + state: ConnectionState; + error: string | null; + modelConfig: ModelConfigType | null; + agentsCount: number; + runsCount: number; + runsCursor: string | undefined; + streamError: string | null; + listening: boolean; +}) { + return ( + + + RowboatX chat · Server {serverUrl} + + + {state === "connecting" && ( + <> + + + {" "} + Connecting… + + )} + {state === "ready" && ( + + Connected · default {modelConfig?.defaults?.provider ?? "n/a"}/{modelConfig?.defaults?.model ?? "n/a"} + + )} + {state === "error" && ( + + Offline: {error ?? "Unknown error"} · Ctrl+L to retry + + )} + + + Agents: {agentsCount} · Chats loaded: {runsCount} + {runsCursor ? " (+ more)" : ""} + + {streamError && ( + Event stream issue: {streamError} + )} + {state === "ready" && listening === false && ( + Listening for run events… + )} + + ); +} + +function Sidebar({ + items, + focus, + index, + activeRunId, + runsLoading, +}: { + items: SidebarItem[]; + focus: boolean; + index: number; + activeRunId: string | null; + runsLoading: boolean; +}) { + return ( + + Chats + {focus ? "↑/↓ move · Enter select · Esc to leave" : "Tab to focus sidebar"} + + {runsLoading && ( + + refreshing… + + )} + {items.length === 0 && No chats yet.} + {items.map((item, idx) => { + let divider: React.ReactNode = null; + const isCursor = focus && idx === index; + if (item.kind === "action") { + return ( + + {isCursor ? "❯" : " "} {item.label} {item.hint ? `(${item.hint})` : ""} + + ); + } + const previousRuns = items.slice(0, idx).some((entry) => entry.kind === "run"); + if (!previousRuns) { + divider = ( + + ── recent chats ── + + ); + } + const isActiveRun = item.run.id === activeRunId; + return ( + + {divider} + + + {isCursor ? "❯" : isActiveRun ? "●" : " "} + {" "} + {item.run.agentId}{" "} + {item.run.id}{" "} + {item.status.label}{" "} + {timeAgo(item.run.createdAt)} + + + ); + })} + + + ); +} + +function ChatPanel({ + focus, + draftAgent, + run, + events, + composerValue, + composerBusy, + onChangeComposer, + onSubmitComposer, + pendingPermissions, + pendingHuman, + showHumanHint, + showPermissionHint, + scrollHint, +}: { + focus: boolean; + draftAgent: string; + run: RunType | undefined; + events: ChatLine[]; + composerValue: string; + composerBusy: boolean; + onChangeComposer: (value: string) => void; + onSubmitComposer: (value: string) => void; + pendingPermissions: PendingPermission[]; + pendingHuman: PendingHuman[]; + showHumanHint: boolean; + showPermissionHint: boolean; + scrollHint: boolean; +}) { + return ( + + + + {run ? run.agentId : draftAgent} + {" "} + {run ? ( + <> + · Run {run.id} · started {formatTimestamp(run.createdAt)} ({timeAgo(run.createdAt)}) + + ) : ( + · new chat + )} + + {!run && ( + Type a prompt and press enter to spin up a new {draftAgent} chat. + )} + {showPermissionHint && ( + Tool approval pending · Ctrl+A approve · Ctrl+D deny + )} + {showHumanHint && ( + Agent asked for help · Ctrl+H to reply + )} + + {run && events.length === 0 && ( + Loading chat log… + )} + {!run && ( + No messages yet. + )} + {events.map((event, idx) => ( + + ))} + + + + {focus + ? `Enter to send · Ctrl+N new chat${scrollHint ? " · PgUp/PgDn scroll" : ""}` + : "Tab to focus composer"} + + onSubmitComposer(value)} + focus={focus && !composerBusy} + placeholder="Send a message…" + /> + {composerBusy && ( + + Sending… + + )} + + + ); +} + +function ModalSurface({ children }: { children: React.ReactNode }) { + return ( + + + {children} + + + ); +} + +function AgentPickerModal({ + agents, + onSelect, + onCancel, +}: { + agents: AgentType[]; + onSelect: (agentName: string) => void; + onCancel: () => void; +}) { + const items = agents.map((agent) => ({ + label: `${agent.name}${agent.description ? ` – ${truncate(agent.description, 40)}` : ""}`, + value: agent.name, + })); + return ( + + Select an agent (esc to cancel) + {items.length === 0 ? ( + No agents configured. + ) : ( + + items={items} + onSelect={(item) => onSelect(item.value)} + /> + )} + {items.length} agents available. + + ); +} + +function MessageModal({ + typeLabel, + prompt, + value, + submitting, + onChange, + onSubmit, + onCancel, +}: { + typeLabel: string; + prompt?: string; + value: string; + submitting: boolean; + onChange: (value: string) => void; + onSubmit: (value: string) => Promise; + onCancel: () => void; +}) { + return ( + + {typeLabel} (esc to cancel) + {prompt && ( + {truncate(prompt, 120)} + )} + { + if (!text.trim()) { + return; + } + onSubmit(text); + }} + focus={!submitting} + placeholder="Type your response…" + /> + {submitting ? ( + + Sending… + + ) : ( + Enter to submit · esc to cancel + )} + + ); +} + +function derivePendingPermissions(run: RunType | undefined): PendingPermission[] { + if (!run) { + return []; + } + const responded = new Set( + run.log + .filter((event) => event.type === "tool-permission-response") + .map((event) => event.toolCallId), + ); + const pending: PendingPermission[] = []; + for (const event of run.log) { + if (event.type === "tool-permission-request") { + const id = event.toolCall.toolCallId; + if (!responded.has(id)) { + pending.push({ + toolCallId: id, + toolName: event.toolCall.toolName, + args: event.toolCall.arguments, + subflow: event.subflow, + }); + } + } + } + return pending; +} + +function derivePendingHuman(run: RunType | undefined): PendingHuman[] { + if (!run) { + return []; + } + const responded = new Set( + run.log + .filter((event) => event.type === "ask-human-response") + .map((event) => event.toolCallId), + ); + const pending: PendingHuman[] = []; + for (const event of run.log) { + if (event.type === "ask-human-request" && !responded.has(event.toolCallId)) { + pending.push({ + toolCallId: event.toolCallId, + query: event.query, + subflow: event.subflow, + }); + } + } + return pending; +} + +function getRunStatus(run: RunType | undefined): { label: string; color: string } { + if (!run) { + return { label: "loading…", color: "gray" }; + } + const last = run.log[run.log.length - 1]; + if (last?.type === "error") { + return { label: "error", color: "red" }; + } + if (derivePendingHuman(run).length > 0) { + return { label: "awaiting human", color: "magenta" }; + } + if (derivePendingPermissions(run).length > 0) { + return { label: "needs approval", color: "yellow" }; + } + return { label: "running", color: "green" }; +} + +function MessageBubble({ event }: { event: ChatLine }) { + const isUser = event.variant === "user"; + const isAssistant = event.variant === "assistant" || event.variant === "streaming"; + const align = isUser ? "flex-end" : "flex-start"; + const bubbleColor = isUser ? "blue" : undefined; + const textColor = isUser ? "white" : event.color; + return ( + + + + {event.text} + + + + ); +} + +function formatEvent(event: RunEventType): ChatLine | null { + switch (event.type) { + case "start": + return { text: `▶ Start · ${event.agentName}`, color: "green", variant: "system" }; + case "message": { + const content = typeof event.message.content === "string" + ? event.message.content + : event.message.content + .map((part) => { + if (part.type === "text" || part.type === "reasoning") { + return part.text; + } + if (part.type === "tool-call") { + return `[tool:${part.toolName}] ${JSON.stringify(part.arguments)}`; + } + return ""; + }) + .join("\n"); + return { + text: `${event.message.role}: ${content}`, + color: event.message.role === "user" ? "black" : event.message.role === "assistant" ? "black" : "white", + variant: event.message.role === "user" + ? "user" + : event.message.role === "assistant" + ? "assistant" + : "system", + }; + } + case "tool-invocation": + return { text: `🔧 Invoking ${event.toolName} ${JSON.stringify(event.input)}`, color: "yellow", variant: "tool" }; + case "tool-result": + return { text: `✅ ${event.toolName} → ${truncate(JSON.stringify(event.result), 120)}`, color: "green", variant: "tool" }; + case "tool-permission-request": + return { text: `⚠️ Permission needed for ${event.toolCall.toolName}`, color: "yellow", variant: "system" }; + case "tool-permission-response": + return { text: `Permission ${event.response} for ${event.toolCallId}`, color: event.response === "approve" ? "green" : "red", variant: "system" }; + case "ask-human-request": + return { text: `🧑 Agent asks: ${truncate(event.query, 120)}`, color: "magenta", variant: "system" }; + case "ask-human-response": + return { text: `🙋 Human replied`, color: "magenta", variant: "system" }; + case "llm-stream-event": + return { text: `… ${event.event.type}`, color: "gray" }; + case "error": + return { text: `✖ ${event.error}`, color: "red", variant: "system" }; + case "spawn-subflow": + return { text: `↳ Spawned ${event.agentName}`, color: "cyan", variant: "system" }; + default: + return { text: "unknown event", color: "white", variant: "other" }; + } +} + +function truncate(input: string, len = 60): string { + if (input.length <= len) { + return input; + } + return `${input.slice(0, len - 1)}…`; +} + +function formatTimestamp(iso: string): string { + const date = new Date(iso); + if (Number.isNaN(date.getTime())) { + return iso; + } + return date.toLocaleString(); +} + +function timeAgo(iso: string): string { + const date = new Date(iso); + if (Number.isNaN(date.getTime())) { + return iso; + } + const diff = Date.now() - date.getTime(); + const seconds = Math.floor(diff / 1000); + if (seconds < 60) return `${seconds}s ago`; + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes}m ago`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h ago`; + const days = Math.floor(hours / 24); + return `${days}d ago`; +} diff --git a/apps/cli/tsconfig.json b/apps/cli/tsconfig.json index 1fad84d4..a0cffd24 100644 --- a/apps/cli/tsconfig.json +++ b/apps/cli/tsconfig.json @@ -11,6 +11,7 @@ "esModuleInterop": true, "skipLibCheck": true, "sourceMap": true, + "jsx": "react-jsx", "paths": { "@/*": [ "./src/*" diff --git a/apps/rowboatx/.gitignore b/apps/rowboatx/.gitignore new file mode 100644 index 00000000..00bba9bb --- /dev/null +++ b/apps/rowboatx/.gitignore @@ -0,0 +1,37 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local +.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 00000000..718d6fea Binary files /dev/null and b/apps/rowboatx/app/favicon.ico differ 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..7241a206 --- /dev/null +++ b/apps/rowboatx/app/layout.tsx @@ -0,0 +1,21 @@ +import type { Metadata } from "next"; +import "./globals.css"; + +export const metadata: Metadata = { + title: "RowboatX", + description: "RowboatX interface", +}; + +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..44f38855 --- /dev/null +++ b/apps/rowboatx/app/page.tsx @@ -0,0 +1,1045 @@ +"use client"; + +import { AppSidebar } from "@/components/app-sidebar"; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from "@/components/ui/breadcrumb"; +import { Separator } from "@/components/ui/separator"; +import { + SidebarInset, + SidebarProvider, + SidebarTrigger, +} from "@/components/ui/sidebar"; +import { + PromptInput, + PromptInputBody, + PromptInputTextarea, + PromptInputFooter, + PromptInputTools, + PromptInputButton, + PromptInputSubmit, + PromptInputAttachments, + PromptInputAttachment, + PromptInputActionMenu, + PromptInputActionMenuTrigger, + PromptInputActionMenuContent, + PromptInputActionAddAttachments, + PromptInputHeader, + type PromptInputMessage, +} from "@/components/ai-elements/prompt-input"; +import { Message, MessageContent, MessageResponse } from "@/components/ai-elements/message"; +import { Conversation, ConversationContent } from "@/components/ai-elements/conversation"; +import { Tool, ToolContent, ToolHeader, ToolInput, ToolOutput } from "@/components/ai-elements/tool"; +import { Reasoning, ReasoningTrigger, ReasoningContent } from "@/components/ai-elements/reasoning"; +import { + Artifact, + ArtifactAction, + ArtifactActions, + ArtifactClose, + ArtifactContent, + ArtifactDescription, + ArtifactHeader, + ArtifactTitle, +} from "@/components/ai-elements/artifact"; +import { useState, useEffect, useRef, type ReactNode, useCallback } from "react"; +import { MicIcon, Save, Loader2, Lock } from "lucide-react"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { JsonEditor } from "@/components/json-editor"; +import { TiptapMarkdownEditor } from "@/components/tiptap-markdown-editor"; +import { MarkdownViewer } from "@/components/markdown-viewer"; + +interface ChatMessage { + id: string; + type: 'message'; + role: 'user' | 'assistant'; + content: string; + timestamp: number; +} + +interface ToolCall { + id: string; + type: 'tool'; + name: string; + input: unknown; + result?: unknown; + status: 'pending' | 'running' | 'completed' | 'error'; + timestamp: number; +} + +interface ReasoningBlock { + id: string; + type: 'reasoning'; + content: string; + isStreaming: boolean; + timestamp: number; +} + +type ConversationItem = ChatMessage | ToolCall | ReasoningBlock; + +type ResourceKind = "agent" | "config" | "run"; + +type SelectedResource = { + kind: ResourceKind; + name: string; +}; + +type ToolCallContentPart = { + type: 'tool-call'; + toolCallId: string; + toolName: string; + arguments: unknown; +}; + +type RunEvent = { + type: string; + [key: string]: unknown; +}; + +function PageBody() { + const [apiBase, setApiBase] = useState("http://localhost:3000") + const streamUrl = "/api/stream"; + const [text, setText] = useState(""); + const [useMicrophone, setUseMicrophone] = useState(false); + const [status, setStatus] = useState<"submitted" | "streaming" | "ready" | "error">("ready"); + + // Chat state + const [runId, setRunId] = useState(null); + const [isRunProcessing, setIsRunProcessing] = useState(false); + const [conversation, setConversation] = useState([]); + const [currentAssistantMessage, setCurrentAssistantMessage] = useState(""); + const [currentReasoning, setCurrentReasoning] = useState(""); + const eventSourceRef = useRef(null); + const committedMessageIds = useRef>(new Set()); + const isEmptyConversation = + conversation.length === 0 && !currentAssistantMessage && !currentReasoning; + const [selectedResource, setSelectedResource] = useState(null); + const [artifactTitle, setArtifactTitle] = useState(""); + const [artifactSubtitle, setArtifactSubtitle] = useState(""); + const [artifactText, setArtifactText] = useState(""); + const [artifactOriginal, setArtifactOriginal] = useState(""); + const [artifactLoading, setArtifactLoading] = useState(false); + const [artifactError, setArtifactError] = useState(null); + const [artifactReadOnly, setArtifactReadOnly] = useState(false); + const [artifactFileType, setArtifactFileType] = useState<"json" | "markdown">("json"); + const [agentOptions, setAgentOptions] = useState(["copilot"]); + const [selectedAgent, setSelectedAgent] = useState("copilot"); + + const artifactDirty = !artifactReadOnly && artifactText !== artifactOriginal; + const stripExtension = (name: string) => name.replace(/\.[^/.]+$/, ""); + const detectFileType = (name: string): "json" | "markdown" => + name.toLowerCase().match(/\.(md|markdown)$/) ? "markdown" : "json"; + + useEffect(() => { + setApiBase(window.config.apiBase); + }, []); + + const requestJson = useCallback(async ( + url: string, + options?: (RequestInit & { allow404?: boolean }) | undefined + ) => { + const fullUrl = new URL(url, apiBase).toString(); + console.log('fullUrl', fullUrl); + const { allow404, ...rest } = options || {}; + const res = await fetch(fullUrl, { + ...rest, + headers: { + "Content-Type": "application/json", + ...(rest.headers || {}), + }, + }); + + const contentType = res.headers.get("content-type")?.toLowerCase() ?? ""; + const isJson = contentType.includes("application/json"); + const text = await res.text(); + + if (!res.ok) { + if (res.status === 404 && allow404) return null; + if (isJson) { + try { + const errObj = JSON.parse(text); + const errMsg = + typeof errObj === "string" + ? errObj + : errObj?.message || errObj?.error || JSON.stringify(errObj); + throw new Error(errMsg || `Request failed: ${res.status} ${res.statusText}`); + } catch { + /* fall through to generic error */ + } + } + if (res.status === 404) { + throw new Error("Resource not found on the CLI backend (404)"); + } + throw new Error(`Request failed: ${res.status} ${res.statusText}`); + } + + if (!text) return null; + if (!isJson) return null; + try { + return JSON.parse(text); + } catch { + return null; + } + }, [apiBase]); + + const renderPromptInput = () => ( + + + + {(attachment) => } + + + + setText(event.target.value)} + value={text} + placeholder="Ask me anything..." + className="min-h-[46px] max-h-[200px]" + /> + + + + + + + + + + setUseMicrophone(!useMicrophone)} + variant={useMicrophone ? "default" : "ghost"} + > + + Microphone + + + + + + + ); + + // Connect to SSE stream + useEffect(() => { + // Prevent multiple connections + if (eventSourceRef.current) { + console.log('⚠️ EventSource already exists, not creating new one'); + return; + } + + console.log('🔌 Creating new EventSource connection'); + const eventSource = new EventSource(streamUrl); + eventSourceRef.current = eventSource; + + const handleMessage = (e: MessageEvent) => { + try { + const event: RunEvent = JSON.parse(e.data); + handleEvent(event); + } catch (error) { + console.error('Failed to parse event:', error); + } + }; + + const handleError = (e: Event) => { + const target = e.target as EventSource; + + // Only log if it's not a normal close + if (target.readyState === EventSource.CLOSED) { + console.log('SSE connection closed, will reconnect on next message'); + } else if (target.readyState === EventSource.CONNECTING) { + console.log('SSE reconnecting...'); + } else { + console.error('SSE error:', e); + } + }; + + eventSource.addEventListener('message', handleMessage); + eventSource.addEventListener('error', handleError); + + return () => { + console.log('🔌 Closing EventSource connection'); + eventSource.removeEventListener('message', handleMessage); + eventSource.removeEventListener('error', handleError); + eventSource.close(); + eventSourceRef.current = null; + }; + }, [streamUrl]); + + // Handle different event types from the copilot + const handleEvent = (event: RunEvent) => { + console.log('Event received:', event.type, event); + + switch (event.type) { + case 'run-processing-start': + setIsRunProcessing(true); + setStatus((prev) => (prev === 'error' ? prev : 'streaming')); + break; + + case 'run-processing-end': + setIsRunProcessing(false); + setStatus('ready'); + break; + + case 'start': + setStatus('streaming'); + setCurrentAssistantMessage(''); + setCurrentReasoning(''); + break; + + case 'llm-stream-event': + { + const llmEvent = (event.event as { + type?: string; + delta?: string; + toolCallId?: string; + toolName?: string; + input?: unknown; + }) || {}; + console.log('LLM stream event type:', llmEvent.type); + + if (llmEvent.type === 'reasoning-delta' && llmEvent.delta) { + setCurrentReasoning(prev => prev + llmEvent.delta); + } else if (llmEvent.type === 'reasoning-end') { + // Commit reasoning block if we have content + setCurrentReasoning(reasoning => { + if (reasoning) { + setConversation(prev => [...prev, { + id: `reasoning-${Date.now()}`, + type: 'reasoning', + content: reasoning, + isStreaming: false, + timestamp: Date.now(), + }]); + } + return ''; + }); + } else if (llmEvent.type === 'text-delta' && llmEvent.delta) { + setCurrentAssistantMessage(prev => prev + llmEvent.delta); + setStatus('streaming'); + } else if (llmEvent.type === 'text-end') { + console.log('TEXT END received - waiting for message event'); + } else if (llmEvent.type === 'tool-call') { + // Add tool call to conversation immediately + setConversation(prev => [...prev, { + id: llmEvent.toolCallId || `tool-${Date.now()}`, + type: 'tool', + name: llmEvent.toolName || 'tool', + input: llmEvent.input, + status: 'running', + timestamp: Date.now(), + }]); + } else if (llmEvent.type === 'finish-step') { + console.log('FINISH STEP received - waiting for message event'); + } + } + break; + + case 'message': { + console.log('MESSAGE event received:', event); + const message = (event.message as { role?: string; content?: unknown }) || {}; + if (message.role !== 'assistant') { + break; + } + + if (Array.isArray(message.content)) { + const toolCalls = message.content.filter( + (part): part is ToolCallContentPart => + (part as ToolCallContentPart)?.type === 'tool-call' + ); + if (toolCalls.length) { + setConversation((prev) => { + let updated: ConversationItem[] = prev.map((item) => { + if (item.type !== 'tool') return item; + const match = toolCalls.find( + (part) => part.toolCallId === item.id + ); + return match + ? { + ...item, + name: match.toolName, + input: match.arguments, + status: 'pending', + } + : item; + }); + + for (const part of toolCalls) { + const exists = updated.some( + (item) => item.type === 'tool' && item.id === part.toolCallId + ); + if (!exists) { + updated = [ + ...updated, + { + id: part.toolCallId, + type: 'tool', + name: part.toolName, + input: part.arguments, + status: 'pending', + timestamp: Date.now(), + }, + ]; + } + } + return updated; + }); + } + } + + const messageId = + typeof event.messageId === "string" + ? event.messageId + : `assistant-${Date.now()}`; + + if (committedMessageIds.current.has(messageId)) { + console.log('⚠️ Message already committed, skipping:', messageId); + break; + } + + committedMessageIds.current.add(messageId); + + setCurrentAssistantMessage(currentMsg => { + console.log('✅ Committing message:', messageId, currentMsg); + if (currentMsg) { + setConversation(prev => { + const exists = prev.some(m => m.id === messageId); + if (exists) { + console.log('⚠️ Message ID already in array, skipping:', messageId); + return prev; + } + return [...prev, { + id: messageId, + type: 'message', + role: 'assistant', + content: currentMsg, + timestamp: Date.now(), + }]; + }); + } + return ''; + }); + setStatus('ready'); + console.log('Status set to ready'); + break; + } + + case 'tool-invocation': + setConversation(prev => prev.map(item => + item.type === 'tool' && (item.id === event.toolCallId || item.name === event.toolName) + ? { ...item, status: 'running' as const } + : item + )); + break; + + case 'tool-result': + setConversation(prev => prev.map(item => + item.type === 'tool' && (item.id === event.toolCallId || item.name === event.toolName) + ? { ...item, result: event.result, status: 'completed' as const } + : item + )); + break; + + case 'error': + // Only set error status for actual errors, not connection issues + { + const errorMsg = typeof event.error === "string" ? event.error : ""; + if (errorMsg && !errorMsg.includes('terminated')) { + setStatus('error'); + console.error('Agent error:', errorMsg); + } else { + console.log('Connection error (will auto-reconnect):', errorMsg); + setStatus('ready'); + } + setIsRunProcessing(false); + } + break; + + default: + console.log('Unhandled event type:', event.type); + } + }; + + const handleSubmit = async (message: PromptInputMessage) => { + const hasText = Boolean(message.text); + const hasAttachments = Boolean(message.files?.length); + + if (!(hasText || hasAttachments)) { + return; + } + + const userMessage = message.text || ''; + + // Add user message immediately with unique ID + const userMessageId = `user-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + setConversation(prev => [...prev, { + id: userMessageId, + type: 'message', + role: 'user', + content: userMessage, + timestamp: Date.now(), + }]); + + setStatus("submitted"); + setText(""); + + try { + let nextRunId = runId; + if (!nextRunId) { + const runData = await requestJson("/runs/new", { + method: "POST", + body: JSON.stringify({ + agentId: selectedAgent, + }), + }); + nextRunId = runData?.id; + setRunId(nextRunId); + } + + if (!nextRunId) { + throw new Error("Run ID unavailable after creation"); + } + + await requestJson(`/runs/${encodeURIComponent(nextRunId)}/messages/new`, { + method: "POST", + body: JSON.stringify({ + message: userMessage, + }), + }); + + setStatus('streaming'); + } catch (error) { + console.error('Failed to send message:', error); + setStatus('error'); + setTimeout(() => setStatus('ready'), 2000); + } + }; + + useEffect(() => { + if (!selectedResource) return; + let cancelled = false; + const load = async () => { + setArtifactLoading(true); + setArtifactError(null); + try { + const title = selectedResource.name; + let subtitle = ""; + let text = ""; + let readOnly = false; + const detectedType = detectFileType(selectedResource.name); + setArtifactFileType(detectedType); + + if (selectedResource.kind === "agent") { + const raw = selectedResource.name; + const isMarkdown = /\.(md|markdown)$/i.test(raw); + + if (isMarkdown) { + subtitle = "Agent (Markdown)"; + const response = await fetch( + `/api/rowboat/agent?file=${encodeURIComponent(raw)}` + ); + if (!response.ok) { + if (response.status === 404) { + text = ""; + } else { + throw new Error(`Failed to load agent file: ${response.status}`); + } + } else { + const data = await response.json(); + text = data?.content || data?.raw || ""; + } + setArtifactFileType("markdown"); + } else { + const id = stripExtension(raw) || raw; + const data = await requestJson(`/agents/${encodeURIComponent(id)}`); + + subtitle = "Agent"; + text = JSON.stringify(data ?? {}, null, 2); + setArtifactFileType("json"); + } + } else if (selectedResource.kind === "config") { + const lower = selectedResource.name.toLowerCase(); + if (lower.endsWith(".md") || lower.endsWith(".markdown")) { + // Load markdown file as plain text from local API + try { + const response = await fetch( + `/api/rowboat/config?file=${encodeURIComponent(selectedResource.name)}` + ); + if (!response.ok) { + if (response.status === 404) { + // File doesn't exist, start with empty content + text = ""; + } else { + throw new Error(`Failed to load markdown file: ${response.status}`); + } + } else { + const data = await response.json(); + text = data.content || data.raw || ""; + } + subtitle = "Markdown"; + setArtifactFileType("markdown"); + } catch (error: unknown) { + const err = error as Error; + console.error("Error loading markdown file:", error); + // Show error but still allow editing + setArtifactError(err?.message || "Failed to load markdown file"); + text = ""; + subtitle = "Markdown"; + setArtifactFileType("markdown"); + } + } else if (lower.includes("mcp")) { + const data = await requestJson("/mcp"); + subtitle = "MCP config"; + text = JSON.stringify(data ?? {}, null, 2); + setArtifactFileType("json"); + } else if (lower.includes("model")) { + const data = await requestJson("/models"); + subtitle = "Models config"; + text = JSON.stringify(data ?? {}, null, 2); + setArtifactFileType("json"); + } else { + // Try to load as JSON by default + try { + const data = await requestJson(`/config/${encodeURIComponent(selectedResource.name)}`); + subtitle = "Config"; + text = JSON.stringify(data ?? {}, null, 2); + setArtifactFileType("json"); + } catch { + throw new Error("Unsupported config file"); + } + } + } else if (selectedResource.kind === "run") { + subtitle = "Run (read-only)"; + readOnly = true; + setArtifactFileType(detectedType); + + const local = await requestJson( + `/api/rowboat/run?file=${encodeURIComponent(selectedResource.name)}` + ); + if (local?.parsed) { + text = JSON.stringify(local.parsed, null, 2); + } else if (local?.raw) { + text = local.raw; + } else { + text = ""; + } + } + + if (cancelled) return; + setArtifactTitle(title); + setArtifactSubtitle(subtitle); + setArtifactText(text); + setArtifactOriginal(text); + setArtifactReadOnly(readOnly); + } catch (error: unknown) { + if (!cancelled) { + const err = error as Error; + setArtifactError(err?.message || "Failed to load resource"); + setArtifactText(""); + } + } finally { + if (!cancelled) { + setArtifactLoading(false); + } + } + }; + load(); + return () => { + cancelled = true; + }; + }, [selectedResource, requestJson]); + + useEffect(() => { + const loadAgents = async () => { + try { + const res = await fetch("/api/rowboat/summary"); + if (!res.ok) return; + const data = await res.json(); + const agents = Array.isArray(data.agents) + ? data.agents.map((a: string) => stripExtension(a)) + : []; + const merged = Array.from(new Set(["copilot", ...agents])); + setAgentOptions(merged); + } catch (e) { + console.error("Failed to load agent list", e); + } + }; + loadAgents(); + }, []); + + useEffect(() => { + // Changing agent starts a fresh conversation context + setRunId(null); + setConversation([]); + setCurrentAssistantMessage(""); + setCurrentReasoning(""); + setIsRunProcessing(false); + }, [selectedAgent]); + + const handleSave = async () => { + if (!selectedResource || artifactReadOnly || !artifactDirty) return; + setArtifactLoading(true); + setArtifactError(null); + try { + if (selectedResource.kind === "agent") { + if (artifactFileType === "markdown") { + const response = await fetch( + `/api/rowboat/agent?file=${encodeURIComponent(selectedResource.name)}`, + { + method: "PUT", + headers: { "Content-Type": "text/plain" }, + body: artifactText, + } + ); + if (!response.ok) { + throw new Error("Failed to save agent file"); + } + setArtifactOriginal(artifactText); + } else { + const parsed = JSON.parse(artifactText); + const raw = selectedResource.name; + const targetId = stripExtension(raw) || raw; + + await requestJson(`/agents/${encodeURIComponent(targetId)}`, { + method: "PUT", + body: JSON.stringify(parsed), + }); + setArtifactOriginal(JSON.stringify(parsed, null, 2)); + } + } else if (selectedResource.kind === "config") { + const lower = selectedResource.name.toLowerCase(); + + if (lower.endsWith(".md") || lower.endsWith(".markdown")) { + // Save markdown file as plain text via local API + const response = await fetch( + `/api/rowboat/config?file=${encodeURIComponent(selectedResource.name)}`, + { + method: "PUT", + headers: { "Content-Type": "text/plain" }, + body: artifactText, + } + ); + if (!response.ok) { + throw new Error("Failed to save markdown file"); + } + setArtifactOriginal(artifactText); + } else { + // Handle JSON config files + const parsed = JSON.parse(artifactText); + const previous = artifactOriginal ? JSON.parse(artifactOriginal) : {}; + + if (lower.includes("model")) { + const newProviders = parsed.providers || {}; + const oldProviders = previous.providers || {}; + const toDelete = Object.keys(oldProviders).filter( + (name) => !Object.prototype.hasOwnProperty.call(newProviders, name) + ); + for (const name of toDelete) { + await requestJson(`/models/providers/${encodeURIComponent(name)}`, { + method: "DELETE", + }); + } + for (const name of Object.keys(newProviders)) { + await requestJson(`/models/providers/${encodeURIComponent(name)}`, { + method: "PUT", + body: JSON.stringify(newProviders[name]), + }); + } + if (parsed.defaults) { + await requestJson("/models/default", { + method: "PUT", + body: JSON.stringify(parsed.defaults), + }); + } + } else if (lower.includes("mcp")) { + const newServers = parsed.mcpServers || parsed || {}; + const oldServers = previous.mcpServers || {}; + const toDelete = Object.keys(oldServers).filter( + (name) => !Object.prototype.hasOwnProperty.call(newServers, name) + ); + for (const name of toDelete) { + await requestJson(`/mcp/${encodeURIComponent(name)}`, { + method: "DELETE", + }); + } + for (const name of Object.keys(newServers)) { + await requestJson(`/mcp/${encodeURIComponent(name)}`, { + method: "PUT", + body: JSON.stringify(newServers[name]), + }); + } + } else { + throw new Error("Unsupported config file"); + } + setArtifactOriginal(JSON.stringify(parsed, null, 2)); + } + } + } catch (error: unknown) { + const err = error as Error; + setArtifactError(err?.message || "Failed to save changes"); + } finally { + setArtifactLoading(false); + } + }; + + return ( + <> + + +
+
+ + + + + + RowboatX + + + + Chat + + + +
+
+ +
+
+ {isRunProcessing && ( +
+ + Working... +
+ )} + {/* Messages area */} + +
+ +
+ + {/* Render conversation items in order */} + {conversation.map((item) => { + if (item.type === 'message') { + return ( + + + + {item.content} + + + + ); + } else if (item.type === 'tool') { + const stateMap: Record = { + pending: 'input-streaming', + running: 'input-available', + completed: 'output-available', + error: 'output-error', + }; + + return ( +
+ + + + + {item.result != null && ( + + )} + + +
+ ); + } else if (item.type === 'reasoning') { + return ( +
+ + + + {item.content} + + +
+ ); + } + return null; + })} + + {/* Streaming reasoning */} + {currentReasoning && ( +
+ + + + {currentReasoning} + + +
+ )} + + {/* Streaming message */} + {currentAssistantMessage && ( + + + + {currentAssistantMessage} + + + + + )} +
+
+ + + {/* Input area */} + {isEmptyConversation ? ( +
+
+

+ RowboatX +

+ {renderPromptInput()} +
+
+ ) : ( +
+
+ {renderPromptInput()} +
+
+ )} +
+ + {selectedResource && ( +
+ + +
+ {artifactTitle} + + {artifactSubtitle || selectedResource.kind} + {artifactReadOnly && ( + + Read-only + + )} + +
+ + {!artifactReadOnly && ( + + {artifactLoading ? ( + + ) : ( + + )} + + )} + setSelectedResource(null)} /> + +
+ + {artifactLoading ? ( +
+ Loading +
+ ) : artifactError ? ( +
+ {artifactError} +
+ ) : ( +
+ {artifactReadOnly ? ( + artifactFileType === "markdown" ? ( + + ) : ( +
+                            {artifactText}
+                          
+ ) + ) : artifactFileType === "markdown" ? ( + setArtifactText(newContent)} + readOnly={false} + placeholder="Start writing your markdown..." + /> + ) : ( + setArtifactText(newContent)} + readOnly={false} + /> + )} + {artifactReadOnly && ( +

+ Runs are read-only; use the API to replay or inspect in detail. +

+ )} +
+ )} +
+
+
+ )} +
+ + + ); +} + +export default function HomePage() { + return ( + + + + ); +} 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..8eb765f5 --- /dev/null +++ b/apps/rowboatx/components/ai-elements/checkpoint.tsx @@ -0,0 +1,80 @@ +"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..e8b25b08 --- /dev/null +++ b/apps/rowboatx/components/ai-elements/image.tsx @@ -0,0 +1,24 @@ +/* eslint-disable @next/next/no-img-element */ +import { cn } from "@/lib/utils"; +import type { Experimental_GeneratedImage } from "ai"; + +export type ImageProps = Experimental_GeneratedImage & { + className?: string; + alt?: string; +}; + +export const Image = ({ + base64, + 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..deb0bbfb --- /dev/null +++ b/apps/rowboatx/components/ai-elements/message.tsx @@ -0,0 +1,453 @@ +"use client"; + +/* eslint-disable @next/next/no-img-element */ +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, useMemo, 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 = useMemo( + () => (Array.isArray(children) ? 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; + +export const MessageBranchSelector = ({ + className, + ...props +}: MessageBranchSelectorProps) => { + const { totalBranches } = useMessageBranch(); + + // Don't render if there's only one branch + if (totalBranches <= 1) { + return null; + } + + return ( + *:not(:first-child)]:rounded-l-md [&>*:not(:last-child)]:rounded-r-md", + className + )} + orientation="horizontal" + {...props} + /> + ); +}; + +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..baa5a6ea --- /dev/null +++ b/apps/rowboatx/components/ai-elements/model-selector.tsx @@ -0,0 +1,206 @@ +/* eslint-disable @next/next/no-img-element */ +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..1dcd6e27 --- /dev/null +++ b/apps/rowboatx/components/ai-elements/prompt-input.tsx @@ -0,0 +1,1413 @@ +"use client"; + +/* eslint-disable @next/next/no-img-element */ +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); + } + } + }, + [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 ({ ...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) => void) | null; + onend: ((this: SpeechRecognition, ev: Event) => void) | null; + onresult: + | ((this: SpeechRecognition, ev: SpeechRecognitionEvent) => void) + | null; + onerror: + | ((this: SpeechRecognition, ev: SpeechRecognitionErrorEvent) => void) + | 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 ( +
+