diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 86081c8..3cf7c4a 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -21,6 +21,9 @@ importers: '@ai-sdk/openai': specifier: ^2.0.0 version: 2.0.106(zod@3.25.76) + '@fledgling/agent-core': + specifier: workspace:* + version: link:../agent-core '@fledgling/common': specifier: workspace:* version: link:../common @@ -30,9 +33,12 @@ importers: '@fledgling/mcp-workspace': specifier: workspace:* version: link:../mcp-workspace - '@fledgling/session-log': + '@fledgling/session-file-system': + specifier: workspace:* + version: link:../session-file-system + '@fledgling/tools-mcp-node': specifier: workspace:* - version: link:../session-log + version: link:../tools-mcp-node ai: specifier: ^5.0.0 version: 5.0.193(zod@3.25.76) @@ -48,13 +54,13 @@ importers: version: 22.19.19 '@vitest/coverage-v8': specifier: ^3.1.1 - version: 3.2.6(vitest@3.2.6(@types/node@22.19.19)) + version: 3.2.6(vitest@3.2.6(@types/node@22.19.19)(terser@5.48.0)) typescript: specifier: ~5.9.0 version: 5.9.3 vitest: specifier: ^3.1.1 - version: 3.2.6(@types/node@22.19.19) + version: 3.2.6(@types/node@22.19.19)(terser@5.48.0) ../../packages/acp-host-log: dependencies: @@ -90,6 +96,40 @@ importers: specifier: ~5.9.0 version: 5.9.3 + ../../packages/agent-core: + dependencies: + '@agentclientprotocol/sdk': + specifier: ^0.4.3 + version: 0.4.9 + '@fledgling/common': + specifier: workspace:* + version: link:../common + '@fledgling/context-builder': + specifier: workspace:* + version: link:../context-builder + ai: + specifier: ^5.0.0 + version: 5.0.193(zod@3.25.76) + devDependencies: + '@fledgling/heft-rig': + specifier: workspace:* + version: link:../../rigs/heft-rig + '@rushstack/heft': + specifier: 1.2.7 + version: 1.2.7(@types/node@22.19.19) + '@types/node': + specifier: ^22.0.0 + version: 22.19.19 + '@vitest/coverage-v8': + specifier: ^3.1.1 + version: 3.2.6(vitest@3.2.6(@types/node@22.19.19)(terser@5.48.0)) + typescript: + specifier: ~5.9.0 + version: 5.9.3 + vitest: + specifier: ^3.1.1 + version: 3.2.6(@types/node@22.19.19)(terser@5.48.0) + ../../packages/common: devDependencies: '@fledgling/heft-rig': @@ -122,13 +162,13 @@ importers: version: 22.19.19 '@vitest/coverage-v8': specifier: ^3.1.1 - version: 3.2.6(vitest@3.2.6(@types/node@22.19.19)) + version: 3.2.6(vitest@3.2.6(@types/node@22.19.19)(terser@5.48.0)) typescript: specifier: ~5.9.0 version: 5.9.3 vitest: specifier: ^3.1.1 - version: 3.2.6(@types/node@22.19.19) + version: 3.2.6(@types/node@22.19.19)(terser@5.48.0) ../../packages/mcp-workspace: dependencies: @@ -155,6 +195,93 @@ importers: specifier: ~5.9.0 version: 5.9.3 + ../../packages/mcp-workspace-webcontainer: + dependencies: + '@modelcontextprotocol/sdk': + specifier: ^1.21.0 + version: 1.29.0(zod@3.25.76) + '@webcontainer/api': + specifier: ^1.5.1 + version: 1.6.4 + zod: + specifier: ^3.25.0 + version: 3.25.76 + devDependencies: + '@fledgling/heft-rig': + specifier: workspace:* + version: link:../../rigs/heft-rig + '@rushstack/heft': + specifier: 1.2.7 + version: 1.2.7(@types/node@22.19.19) + '@types/node': + specifier: ^22.0.0 + version: 22.19.19 + '@vitest/coverage-v8': + specifier: ^3.1.1 + version: 3.2.6(vitest@3.2.6(@types/node@22.19.19)(terser@5.48.0)) + typescript: + specifier: ~5.9.0 + version: 5.9.3 + vitest: + specifier: ^3.1.1 + version: 3.2.6(@types/node@22.19.19)(terser@5.48.0) + + ../../packages/session-file-system: + dependencies: + '@fledgling/agent-core': + specifier: workspace:* + version: link:../agent-core + '@fledgling/common': + specifier: workspace:* + version: link:../common + devDependencies: + '@fledgling/heft-rig': + specifier: workspace:* + version: link:../../rigs/heft-rig + '@rushstack/heft': + specifier: 1.2.7 + version: 1.2.7(@types/node@22.19.19) + '@types/node': + specifier: ^22.0.0 + version: 22.19.19 + '@vitest/coverage-v8': + specifier: ^3.1.1 + version: 3.2.6(vitest@3.2.6(@types/node@22.19.19)(terser@5.48.0)) + typescript: + specifier: ~5.9.0 + version: 5.9.3 + vitest: + specifier: ^3.1.1 + version: 3.2.6(@types/node@22.19.19)(terser@5.48.0) + + ../../packages/session-local-storage: + dependencies: + '@fledgling/agent-core': + specifier: workspace:* + version: link:../agent-core + '@fledgling/common': + specifier: workspace:* + version: link:../common + devDependencies: + '@fledgling/heft-rig': + specifier: workspace:* + version: link:../../rigs/heft-rig + '@rushstack/heft': + specifier: 1.2.7 + version: 1.2.7(@types/node@22.19.19) + '@types/node': + specifier: ^22.0.0 + version: 22.19.19 + '@vitest/coverage-v8': + specifier: ^3.1.1 + version: 3.2.6(vitest@3.2.6(@types/node@22.19.19)(terser@5.48.0)) + typescript: + specifier: ~5.9.0 + version: 5.9.3 + vitest: + specifier: ^3.1.1 + version: 3.2.6(@types/node@22.19.19)(terser@5.48.0) + ../../packages/session-log: dependencies: '@fledgling/common': @@ -172,13 +299,124 @@ importers: version: 22.19.19 '@vitest/coverage-v8': specifier: ^3.1.1 - version: 3.2.6(vitest@3.2.6(@types/node@22.19.19)) + version: 3.2.6(vitest@3.2.6(@types/node@22.19.19)(terser@5.48.0)) typescript: specifier: ~5.9.0 version: 5.9.3 vitest: specifier: ^3.1.1 - version: 3.2.6(@types/node@22.19.19) + version: 3.2.6(@types/node@22.19.19)(terser@5.48.0) + + ../../packages/session-memory: + dependencies: + '@fledgling/agent-core': + specifier: workspace:* + version: link:../agent-core + '@fledgling/common': + specifier: workspace:* + version: link:../common + devDependencies: + '@fledgling/heft-rig': + specifier: workspace:* + version: link:../../rigs/heft-rig + '@rushstack/heft': + specifier: 1.2.7 + version: 1.2.7(@types/node@22.19.19) + '@types/node': + specifier: ^22.0.0 + version: 22.19.19 + '@vitest/coverage-v8': + specifier: ^3.1.1 + version: 3.2.6(vitest@3.2.6(@types/node@22.19.19)(terser@5.48.0)) + typescript: + specifier: ~5.9.0 + version: 5.9.3 + vitest: + specifier: ^3.1.1 + version: 3.2.6(@types/node@22.19.19)(terser@5.48.0) + + ../../packages/tools-mcp-node: + dependencies: + '@agentclientprotocol/sdk': + specifier: ^0.4.3 + version: 0.4.9 + '@ai-sdk/mcp': + specifier: 0.0.18 + version: 0.0.18(zod@3.25.76) + '@fledgling/agent-core': + specifier: workspace:* + version: link:../agent-core + '@fledgling/mcp-workspace': + specifier: workspace:* + version: link:../mcp-workspace + ai: + specifier: ^5.0.0 + version: 5.0.193(zod@3.25.76) + devDependencies: + '@fledgling/heft-rig': + specifier: workspace:* + version: link:../../rigs/heft-rig + '@rushstack/heft': + specifier: 1.2.7 + version: 1.2.7(@types/node@22.19.19) + '@types/node': + specifier: ^22.0.0 + version: 22.19.19 + '@vitest/coverage-v8': + specifier: ^3.1.1 + version: 3.2.6(vitest@3.2.6(@types/node@22.19.19)(terser@5.48.0)) + typescript: + specifier: ~5.9.0 + version: 5.9.3 + vitest: + specifier: ^3.1.1 + version: 3.2.6(@types/node@22.19.19)(terser@5.48.0) + + ../../packages/web-agent: + dependencies: + '@agentclientprotocol/sdk': + specifier: ^0.4.3 + version: 0.4.9 + '@ai-sdk/mcp': + specifier: 0.0.18 + version: 0.0.18(zod@3.25.76) + '@fledgling/agent-core': + specifier: workspace:* + version: link:../agent-core + '@fledgling/mcp-workspace-webcontainer': + specifier: workspace:* + version: link:../mcp-workspace-webcontainer + '@fledgling/session-local-storage': + specifier: workspace:* + version: link:../session-local-storage + ai: + specifier: ^5.0.0 + version: 5.0.193(zod@3.25.76) + devDependencies: + '@fledgling/heft-rig': + specifier: workspace:* + version: link:../../rigs/heft-rig + '@rushstack/heft': + specifier: 1.2.7 + version: 1.2.7(@types/node@22.19.19) + '@types/node': + specifier: ^22.0.0 + version: 22.19.19 + '@vitest/coverage-v8': + specifier: ^3.1.1 + version: 3.2.6(vitest@3.2.6(@types/node@22.19.19)(terser@5.48.0)) + typescript: + specifier: ~5.9.0 + version: 5.9.3 + vitest: + specifier: ^3.1.1 + version: 3.2.6(@types/node@22.19.19)(terser@5.48.0) + webpack: + specifier: ^5.99.9 + version: 5.107.2(webpack-cli@6.0.1) + webpack-cli: + specifier: ^6.0.1 + version: 6.0.1(webpack@5.107.2) ../../rigs/heft-rig: dependencies: @@ -196,7 +434,7 @@ importers: version: 1.3.2(@rushstack/heft@1.2.7(@types/node@22.19.19))(@types/node@22.19.19) '@vitest/coverage-v8': specifier: ^3.1.1 - version: 3.2.6(vitest@3.2.6(@types/node@22.19.19)) + version: 3.2.6(vitest@3.2.6(@types/node@22.19.19)(terser@5.48.0)) eslint: specifier: ~9.39.4 version: 9.39.4 @@ -205,7 +443,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.1.1 - version: 3.2.6(@types/node@22.19.19) + version: 3.2.6(@types/node@22.19.19)(terser@5.48.0) packages: @@ -265,6 +503,10 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} + '@discoveryjs/json-ext@0.6.3': + resolution: {integrity: sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==} + engines: {node: '>=14.17.0'} + '@esbuild/aix-ppc64@0.27.7': resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} engines: {node: '>=18'} @@ -500,6 +742,9 @@ packages: resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} + '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} @@ -894,10 +1139,95 @@ packages: '@vitest/utils@3.2.6': resolution: {integrity: sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg==} + '@webassemblyjs/ast@1.14.1': + resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} + + '@webassemblyjs/floating-point-hex-parser@1.13.2': + resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==} + + '@webassemblyjs/helper-api-error@1.13.2': + resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==} + + '@webassemblyjs/helper-buffer@1.14.1': + resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==} + + '@webassemblyjs/helper-numbers@1.13.2': + resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==} + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': + resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==} + + '@webassemblyjs/helper-wasm-section@1.14.1': + resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==} + + '@webassemblyjs/ieee754@1.13.2': + resolution: {integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==} + + '@webassemblyjs/leb128@1.13.2': + resolution: {integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==} + + '@webassemblyjs/utf8@1.13.2': + resolution: {integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==} + + '@webassemblyjs/wasm-edit@1.14.1': + resolution: {integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==} + + '@webassemblyjs/wasm-gen@1.14.1': + resolution: {integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==} + + '@webassemblyjs/wasm-opt@1.14.1': + resolution: {integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==} + + '@webassemblyjs/wasm-parser@1.14.1': + resolution: {integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==} + + '@webassemblyjs/wast-printer@1.14.1': + resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} + + '@webcontainer/api@1.6.4': + resolution: {integrity: sha512-r9sHCXg1FcC1AMgppGwAc0vYWaQhqvg282cnsuPbJEzYnWifAdCVvg+8ngJUEHyHcomhJJp+/zuytite4ITHLw==} + + '@webpack-cli/configtest@3.0.1': + resolution: {integrity: sha512-u8d0pJ5YFgneF/GuvEiDA61Tf1VDomHHYMjv/wc9XzYj7nopltpG96nXN5dJRstxZhcNpV1g+nT6CydO7pHbjA==} + engines: {node: '>=18.12.0'} + peerDependencies: + webpack: ^5.82.0 + webpack-cli: 6.x.x + + '@webpack-cli/info@3.0.1': + resolution: {integrity: sha512-coEmDzc2u/ffMvuW9aCjoRzNSPDl/XLuhPdlFRpT9tZHmJ/039az33CE7uH+8s0uL1j5ZNtfdv0HkfaKRBGJsQ==} + engines: {node: '>=18.12.0'} + peerDependencies: + webpack: ^5.82.0 + webpack-cli: 6.x.x + + '@webpack-cli/serve@3.0.1': + resolution: {integrity: sha512-sbgw03xQaCLiT6gcY/6u3qBDn01CWw/nbaXl3gTdTFuJJ75Gffv3E3DBpgvY2fkkrdS1fpjaXNOmJlnbtKauKg==} + engines: {node: '>=18.12.0'} + peerDependencies: + webpack: ^5.82.0 + webpack-cli: 6.x.x + webpack-dev-server: '*' + peerDependenciesMeta: + webpack-dev-server: + optional: true + + '@xtuc/ieee754@1.2.0': + resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} + + '@xtuc/long@4.2.2': + resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} + acorn-import-phases@1.0.4: + resolution: {integrity: sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==} + engines: {node: '>=10.13.0'} + peerDependencies: + acorn: ^8.14.0 + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -922,9 +1252,17 @@ packages: ajv: optional: true + ajv-formats@2.1.1: + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + ajv-formats@3.0.1: resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + ajv-keywords@5.1.0: + resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} + peerDependencies: + ajv: ^8.8.2 + ajv@6.15.0: resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} @@ -1010,6 +1348,11 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} + baseline-browser-mapping@2.10.33: + resolution: {integrity: sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==} + engines: {node: '>=6.0.0'} + hasBin: true + body-parser@2.2.2: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} @@ -1028,6 +1371,14 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -1052,6 +1403,9 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} + caniuse-lite@1.0.30001793: + resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} + chai@5.3.3: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} @@ -1064,6 +1418,14 @@ packages: resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} engines: {node: '>= 16'} + chrome-trace-event@1.0.4: + resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} + engines: {node: '>=6.0'} + + clone-deep@4.0.1: + resolution: {integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==} + engines: {node: '>=6'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1074,6 +1436,13 @@ packages: colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -1166,6 +1535,9 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + electron-to-chromium@1.5.366: + resolution: {integrity: sha512-OlRuhb688YTCzzU3gXPLn6nGyd+F+53INE1qaKKlu6kETErE8FYsyDh0XqXEU+uBRn0MpCzz2vfNwORhkap8qg==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -1179,6 +1551,15 @@ packages: end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + enhanced-resolve@5.22.1: + resolution: {integrity: sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==} + engines: {node: '>=10.13.0'} + + envinfo@7.21.0: + resolution: {integrity: sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==} + engines: {node: '>=4'} + hasBin: true + es-abstract@1.24.2: resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==} engines: {node: '>= 0.4'} @@ -1198,6 +1579,9 @@ packages: es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-module-lexer@2.1.0: + resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + es-object-atoms@1.1.2: resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} engines: {node: '>= 0.4'} @@ -1219,6 +1603,10 @@ packages: engines: {node: '>=18'} hasBin: true + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} @@ -1241,6 +1629,10 @@ packages: eslint-plugin-tsdoc@0.5.2: resolution: {integrity: sha512-BlvqjWZdBJDIPO/YU3zcPCF23CvjYT3gyu63yo6b609NNV3D1b6zceAREy2xnweuBoDpZcLNuPyAUq9cvx6bbQ==} + eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + eslint-scope@8.4.0: resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1279,6 +1671,10 @@ packages: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} engines: {node: '>=4.0'} + estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + estraverse@5.3.0: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} @@ -1294,6 +1690,10 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + eventsource-parser@3.1.0: resolution: {integrity: sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==} engines: {node: '>=18.0.0'} @@ -1338,6 +1738,10 @@ packages: fast-uri@3.1.2: resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + fastest-levenshtein@1.0.16: + resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} + engines: {node: '>= 4.9.1'} + fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -1362,6 +1766,10 @@ packages: resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} engines: {node: '>= 18.0.0'} + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -1370,6 +1778,10 @@ packages: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} + flat@5.0.2: + resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} + hasBin: true + flatted@3.4.2: resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} @@ -1524,6 +1936,11 @@ packages: resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} engines: {node: '>=8'} + import-local@3.2.0: + resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} + engines: {node: '>=8'} + hasBin: true + imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -1535,6 +1952,10 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} + interpret@3.1.1: + resolution: {integrity: sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==} + engines: {node: '>=10.13.0'} + ip-address@10.2.0: resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} engines: {node: '>= 12'} @@ -1611,6 +2032,10 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-plain-object@2.0.4: + resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} + engines: {node: '>=0.10.0'} + is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} @@ -1656,6 +2081,10 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isobject@3.0.1: + resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} + engines: {node: '>=0.10.0'} + istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} @@ -1679,6 +2108,10 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jest-worker@27.5.1: + resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} + engines: {node: '>= 10.13.0'} + jju@1.4.0: resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} @@ -1739,10 +2172,22 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + loader-runner@4.3.2: + resolution: {integrity: sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==} + engines: {node: '>=6.11.5'} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -1786,6 +2231,9 @@ packages: resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} engines: {node: '>=18'} + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -1835,10 +2283,17 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + node-exports-info@1.6.0: resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==} engines: {node: '>= 0.4'} + node-releases@2.0.47: + resolution: {integrity: sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==} + engines: {node: '>=18'} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -1886,14 +2341,26 @@ packages: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + p-locate@5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} @@ -1959,6 +2426,10 @@ packages: resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} engines: {node: '>=16.20.0'} + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -2016,6 +2487,10 @@ packages: real-require@1.0.0: resolution: {integrity: sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==} + rechoir@0.8.0: + resolution: {integrity: sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==} + engines: {node: '>= 10.13.0'} + reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} @@ -2028,10 +2503,18 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + resolve@1.22.12: resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} engines: {node: '>= 0.4'} @@ -2077,6 +2560,10 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + schema-utils@4.3.3: + resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} + engines: {node: '>= 10.13.0'} + secure-json-parse@4.1.0: resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} @@ -2117,6 +2604,10 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + shallow-clone@3.0.1: + resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} + engines: {node: '>=8'} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -2155,6 +2646,13 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + split2@4.2.0: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} @@ -2242,6 +2740,58 @@ packages: resolution: {integrity: sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==} engines: {node: '>=6'} + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + terser-webpack-plugin@5.6.1: + resolution: {integrity: sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@minify-html/node': '*' + '@swc/core': '*' + '@swc/css': '*' + '@swc/html': '*' + clean-css: '*' + cssnano: '*' + csso: '*' + esbuild: '*' + html-minifier-terser: '*' + lightningcss: '*' + postcss: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@minify-html/node': + optional: true + '@swc/core': + optional: true + '@swc/css': + optional: true + '@swc/html': + optional: true + clean-css: + optional: true + cssnano: + optional: true + csso: + optional: true + esbuild: + optional: true + html-minifier-terser: + optional: true + lightningcss: + optional: true + postcss: + optional: true + uglify-js: + optional: true + + terser@5.48.0: + resolution: {integrity: sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==} + engines: {node: '>=10'} + hasBin: true + test-exclude@7.0.2: resolution: {integrity: sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==} engines: {node: '>=18'} @@ -2330,6 +2880,12 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -2414,6 +2970,42 @@ packages: resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==} engines: {node: '>=10.13.0'} + watchpack@2.5.1: + resolution: {integrity: sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==} + engines: {node: '>=10.13.0'} + + webpack-cli@6.0.1: + resolution: {integrity: sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==} + engines: {node: '>=18.12.0'} + hasBin: true + peerDependencies: + webpack: ^5.82.0 + webpack-bundle-analyzer: '*' + webpack-dev-server: '*' + peerDependenciesMeta: + webpack-bundle-analyzer: + optional: true + webpack-dev-server: + optional: true + + webpack-merge@6.0.1: + resolution: {integrity: sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==} + engines: {node: '>=18.0.0'} + + webpack-sources@3.5.0: + resolution: {integrity: sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==} + engines: {node: '>=10.13.0'} + + webpack@5.107.2: + resolution: {integrity: sha512-v7RhXaJbpMlV0D7hC7lb2EbnxkoeUqf9qhKr6lozx3Q48pmFrqqNRmZFUEGmi7pSwm6fCQ2H1IjvCkHqdpVdjQ==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -2440,6 +3032,9 @@ packages: engines: {node: '>=8'} hasBin: true + wildcard@2.0.1: + resolution: {integrity: sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==} + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -2527,6 +3122,8 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} + '@discoveryjs/json-ext@0.6.3': {} + '@esbuild/aix-ppc64@0.27.7': optional: true @@ -2689,6 +3286,11 @@ snapshots: '@jridgewell/resolve-uri@3.1.2': {} + '@jridgewell/source-map@0.3.11': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/sourcemap-codec@1.5.5': {} '@jridgewell/trace-mapping@0.3.31': @@ -3087,7 +3689,7 @@ snapshots: '@vercel/oidc@3.1.0': {} - '@vitest/coverage-v8@3.2.6(vitest@3.2.6(@types/node@22.19.19))': + '@vitest/coverage-v8@3.2.6(vitest@3.2.6(@types/node@22.19.19)(terser@5.48.0))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -3102,7 +3704,7 @@ snapshots: std-env: 3.10.0 test-exclude: 7.0.2 tinyrainbow: 2.0.0 - vitest: 3.2.6(@types/node@22.19.19) + vitest: 3.2.6(@types/node@22.19.19)(terser@5.48.0) transitivePeerDependencies: - supports-color @@ -3114,13 +3716,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.6(vite@7.3.5(@types/node@22.19.19))': + '@vitest/mocker@3.2.6(vite@7.3.5(@types/node@22.19.19)(terser@5.48.0))': dependencies: '@vitest/spy': 3.2.6 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.5(@types/node@22.19.19) + vite: 7.3.5(@types/node@22.19.19)(terser@5.48.0) '@vitest/pretty-format@3.2.6': dependencies: @@ -3148,11 +3750,112 @@ snapshots: loupe: 3.2.1 tinyrainbow: 2.0.0 + '@webassemblyjs/ast@1.14.1': + dependencies: + '@webassemblyjs/helper-numbers': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + + '@webassemblyjs/floating-point-hex-parser@1.13.2': {} + + '@webassemblyjs/helper-api-error@1.13.2': {} + + '@webassemblyjs/helper-buffer@1.14.1': {} + + '@webassemblyjs/helper-numbers@1.13.2': + dependencies: + '@webassemblyjs/floating-point-hex-parser': 1.13.2 + '@webassemblyjs/helper-api-error': 1.13.2 + '@xtuc/long': 4.2.2 + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': {} + + '@webassemblyjs/helper-wasm-section@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/wasm-gen': 1.14.1 + + '@webassemblyjs/ieee754@1.13.2': + dependencies: + '@xtuc/ieee754': 1.2.0 + + '@webassemblyjs/leb128@1.13.2': + dependencies: + '@xtuc/long': 4.2.2 + + '@webassemblyjs/utf8@1.13.2': {} + + '@webassemblyjs/wasm-edit@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/helper-wasm-section': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-opt': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + '@webassemblyjs/wast-printer': 1.14.1 + + '@webassemblyjs/wasm-gen@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wasm-opt@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + + '@webassemblyjs/wasm-parser@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-api-error': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wast-printer@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@xtuc/long': 4.2.2 + + '@webcontainer/api@1.6.4': {} + + '@webpack-cli/configtest@3.0.1(webpack-cli@6.0.1)(webpack@5.107.2)': + dependencies: + webpack: 5.107.2(webpack-cli@6.0.1) + webpack-cli: 6.0.1(webpack@5.107.2) + + '@webpack-cli/info@3.0.1(webpack-cli@6.0.1)(webpack@5.107.2)': + dependencies: + webpack: 5.107.2(webpack-cli@6.0.1) + webpack-cli: 6.0.1(webpack@5.107.2) + + '@webpack-cli/serve@3.0.1(webpack-cli@6.0.1)(webpack@5.107.2)': + dependencies: + webpack: 5.107.2(webpack-cli@6.0.1) + webpack-cli: 6.0.1(webpack@5.107.2) + + '@xtuc/ieee754@1.2.0': {} + + '@xtuc/long@4.2.2': {} + accepts@2.0.0: dependencies: mime-types: 3.0.2 negotiator: 1.0.0 + acorn-import-phases@1.0.4(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + acorn-jsx@5.3.2(acorn@8.16.0): dependencies: acorn: 8.16.0 @@ -3171,10 +3874,19 @@ snapshots: optionalDependencies: ajv: 8.18.0 + ajv-formats@2.1.1: + dependencies: + ajv: 8.20.0 + ajv-formats@3.0.1: dependencies: ajv: 8.20.0 + ajv-keywords@5.1.0(ajv@8.20.0): + dependencies: + ajv: 8.20.0 + fast-deep-equal: 3.1.3 + ajv@6.15.0: dependencies: fast-deep-equal: 3.1.3 @@ -3289,6 +4001,8 @@ snapshots: balanced-match@4.0.4: {} + baseline-browser-mapping@2.10.33: {} + body-parser@2.2.2: dependencies: bytes: 3.1.2 @@ -3320,6 +4034,16 @@ snapshots: dependencies: fill-range: 7.1.1 + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.33 + caniuse-lite: 1.0.30001793 + electron-to-chromium: 1.5.366 + node-releases: 2.0.47 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + + buffer-from@1.1.2: {} + bytes@3.1.2: {} cac@6.7.14: {} @@ -3343,6 +4067,8 @@ snapshots: callsites@3.1.0: {} + caniuse-lite@1.0.30001793: {} + chai@5.3.3: dependencies: assertion-error: 2.0.1 @@ -3358,6 +4084,14 @@ snapshots: check-error@2.1.3: {} + chrome-trace-event@1.0.4: {} + + clone-deep@4.0.1: + dependencies: + is-plain-object: 2.0.4 + kind-of: 6.0.3 + shallow-clone: 3.0.1 + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -3366,6 +4100,10 @@ snapshots: colorette@2.0.20: {} + commander@12.1.0: {} + + commander@2.20.3: {} + concat-map@0.0.1: {} content-disposition@1.1.0: {} @@ -3447,6 +4185,8 @@ snapshots: ee-first@1.1.1: {} + electron-to-chromium@1.5.366: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} @@ -3457,6 +4197,13 @@ snapshots: dependencies: once: 1.4.0 + enhanced-resolve@5.22.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + + envinfo@7.21.0: {} + es-abstract@1.24.2: dependencies: array-buffer-byte-length: 1.0.2 @@ -3539,6 +4286,8 @@ snapshots: es-module-lexer@1.7.0: {} + es-module-lexer@2.1.0: {} + es-object-atoms@1.1.2: dependencies: es-errors: 1.3.0 @@ -3589,6 +4338,8 @@ snapshots: '@esbuild/win32-ia32': 0.27.7 '@esbuild/win32-x64': 0.27.7 + escalade@3.2.0: {} + escape-html@1.0.3: {} escape-string-regexp@4.0.0: {} @@ -3630,6 +4381,11 @@ snapshots: - supports-color - typescript + eslint-scope@5.1.1: + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + eslint-scope@8.4.0: dependencies: esrecurse: 4.3.0 @@ -3694,6 +4450,8 @@ snapshots: dependencies: estraverse: 5.3.0 + estraverse@4.3.0: {} + estraverse@5.3.0: {} estree-walker@3.0.3: @@ -3704,6 +4462,8 @@ snapshots: etag@1.8.1: {} + events@3.3.0: {} + eventsource-parser@3.1.0: {} eventsource@3.0.7: @@ -3770,6 +4530,8 @@ snapshots: fast-uri@3.1.2: {} + fastest-levenshtein@1.0.16: {} + fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -3797,6 +4559,11 @@ snapshots: transitivePeerDependencies: - supports-color + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -3807,6 +4574,8 @@ snapshots: flatted: 3.4.2 keyv: 4.5.4 + flat@5.0.2: {} + flatted@3.4.2: {} for-each@0.3.5: @@ -3955,6 +4724,11 @@ snapshots: import-lazy@4.0.0: {} + import-local@3.2.0: + dependencies: + pkg-dir: 4.2.0 + resolve-cwd: 3.0.0 + imurmurhash@0.1.4: {} inherits@2.0.4: {} @@ -3965,6 +4739,8 @@ snapshots: hasown: 2.0.4 side-channel: 1.1.0 + interpret@3.1.1: {} + ip-address@10.2.0: {} ipaddr.js@1.9.1: {} @@ -4040,6 +4816,10 @@ snapshots: is-number@7.0.0: {} + is-plain-object@2.0.4: + dependencies: + isobject: 3.0.1 + is-promise@4.0.0: {} is-regex@1.2.1: @@ -4085,6 +4865,8 @@ snapshots: isexe@2.0.0: {} + isobject@3.0.1: {} + istanbul-lib-coverage@3.2.2: {} istanbul-lib-report@3.0.1: @@ -4121,6 +4903,12 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 + jest-worker@27.5.1: + dependencies: + '@types/node': 22.19.19 + merge-stream: 2.0.0 + supports-color: 8.1.1 + jju@1.4.0: {} jose@6.2.3: {} @@ -4174,11 +4962,19 @@ snapshots: dependencies: json-buffer: 3.0.1 + kind-of@6.0.3: {} + levn@0.4.1: dependencies: prelude-ls: 1.2.1 type-check: 0.4.0 + loader-runner@4.3.2: {} + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -4217,6 +5013,8 @@ snapshots: merge-descriptors@2.0.0: {} + merge-stream@2.0.0: {} + merge2@1.4.1: {} micromatch@4.0.8: @@ -4254,6 +5052,8 @@ snapshots: negotiator@1.0.0: {} + neo-async@2.6.2: {} + node-exports-info@1.6.0: dependencies: array.prototype.flatmap: 1.3.3 @@ -4261,6 +5061,8 @@ snapshots: object.entries: 1.1.9 semver: 6.3.1 + node-releases@2.0.47: {} + object-assign@4.1.1: {} object-inspect@1.13.4: {} @@ -4322,14 +5124,24 @@ snapshots: object-keys: 1.1.1 safe-push-apply: 1.0.0 + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + p-locate@5.0.0: dependencies: p-limit: 3.1.0 + p-try@2.2.0: {} + package-json-from-dist@1.0.1: {} parent-module@1.0.1: @@ -4399,6 +5211,10 @@ snapshots: pkce-challenge@5.0.1: {} + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + possible-typed-array-names@1.1.0: {} postcss@8.5.15: @@ -4452,6 +5268,10 @@ snapshots: real-require@1.0.0: {} + rechoir@0.8.0: + dependencies: + resolve: 1.22.12 + reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.9 @@ -4474,8 +5294,14 @@ snapshots: require-from-string@2.0.2: {} + resolve-cwd@3.0.0: + dependencies: + resolve-from: 5.0.0 + resolve-from@4.0.0: {} + resolve-from@5.0.0: {} + resolve@1.22.12: dependencies: es-errors: 1.3.0 @@ -4562,6 +5388,13 @@ snapshots: safer-buffer@2.1.2: {} + schema-utils@4.3.3: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 8.20.0 + ajv-formats: 2.1.1 + ajv-keywords: 5.1.0(ajv@8.20.0) + secure-json-parse@4.1.0: {} semver@6.3.1: {} @@ -4621,6 +5454,10 @@ snapshots: setprototypeof@1.2.0: {} + shallow-clone@3.0.1: + dependencies: + kind-of: 6.0.3 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -4665,6 +5502,13 @@ snapshots: source-map-js@1.2.1: {} + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + split2@4.2.0: {} sprintf-js@1.0.3: {} @@ -4766,6 +5610,23 @@ snapshots: tapable@1.1.3: {} + tapable@2.3.3: {} + + terser-webpack-plugin@5.6.1(webpack@5.107.2): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + jest-worker: 27.5.1 + schema-utils: 4.3.3 + terser: 5.48.0 + webpack: 5.107.2(webpack-cli@6.0.1) + + terser@5.48.0: + dependencies: + '@jridgewell/source-map': 0.3.11 + acorn: 8.16.0 + commander: 2.20.3 + source-map-support: 0.5.21 + test-exclude@7.0.2: dependencies: '@istanbuljs/schema': 0.1.6 @@ -4859,19 +5720,25 @@ snapshots: unpipe@1.0.0: {} + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + uri-js@4.4.1: dependencies: punycode: 2.3.1 vary@1.1.2: {} - vite-node@3.2.4(@types/node@22.19.19): + vite-node@3.2.4(@types/node@22.19.19)(terser@5.48.0): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.3.5(@types/node@22.19.19) + vite: 7.3.5(@types/node@22.19.19)(terser@5.48.0) transitivePeerDependencies: - '@types/node' - jiti @@ -4886,7 +5753,7 @@ snapshots: - tsx - yaml - vite@7.3.5(@types/node@22.19.19): + vite@7.3.5(@types/node@22.19.19)(terser@5.48.0): dependencies: esbuild: 0.27.7 fdir: 6.5.0(picomatch@4.0.4) @@ -4897,12 +5764,13 @@ snapshots: optionalDependencies: '@types/node': 22.19.19 fsevents: 2.3.3 + terser: 5.48.0 - vitest@3.2.6(@types/node@22.19.19): + vitest@3.2.6(@types/node@22.19.19)(terser@5.48.0): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.6 - '@vitest/mocker': 3.2.6(vite@7.3.5(@types/node@22.19.19)) + '@vitest/mocker': 3.2.6(vite@7.3.5(@types/node@22.19.19)(terser@5.48.0)) '@vitest/pretty-format': 3.2.6 '@vitest/runner': 3.2.6 '@vitest/snapshot': 3.2.6 @@ -4920,8 +5788,8 @@ snapshots: tinyglobby: 0.2.17 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.3.5(@types/node@22.19.19) - vite-node: 3.2.4(@types/node@22.19.19) + vite: 7.3.5(@types/node@22.19.19)(terser@5.48.0) + vite-node: 3.2.4(@types/node@22.19.19)(terser@5.48.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.19.19 @@ -4944,6 +5812,77 @@ snapshots: glob-to-regexp: 0.4.1 graceful-fs: 4.2.11 + watchpack@2.5.1: + dependencies: + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + + webpack-cli@6.0.1(webpack@5.107.2): + dependencies: + '@discoveryjs/json-ext': 0.6.3 + '@webpack-cli/configtest': 3.0.1(webpack-cli@6.0.1)(webpack@5.107.2) + '@webpack-cli/info': 3.0.1(webpack-cli@6.0.1)(webpack@5.107.2) + '@webpack-cli/serve': 3.0.1(webpack-cli@6.0.1)(webpack@5.107.2) + colorette: 2.0.20 + commander: 12.1.0 + cross-spawn: 7.0.6 + envinfo: 7.21.0 + fastest-levenshtein: 1.0.16 + import-local: 3.2.0 + interpret: 3.1.1 + rechoir: 0.8.0 + webpack: 5.107.2(webpack-cli@6.0.1) + webpack-merge: 6.0.1 + + webpack-merge@6.0.1: + dependencies: + clone-deep: 4.0.1 + flat: 5.0.2 + wildcard: 2.0.1 + + webpack-sources@3.5.0: {} + + webpack@5.107.2(webpack-cli@6.0.1): + dependencies: + '@types/estree': 1.0.9 + '@types/json-schema': 7.0.15 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.16.0 + acorn-import-phases: 1.0.4(acorn@8.16.0) + browserslist: 4.28.2 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.22.1 + es-module-lexer: 2.1.0 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + loader-runner: 4.3.2 + mime-db: 1.54.0 + neo-async: 2.6.2 + schema-utils: 4.3.3 + tapable: 2.3.3 + terser-webpack-plugin: 5.6.1(webpack@5.107.2) + watchpack: 2.5.1 + webpack-sources: 3.5.0 + optionalDependencies: + webpack-cli: 6.0.1(webpack@5.107.2) + transitivePeerDependencies: + - '@minify-html/node' + - '@swc/core' + - '@swc/css' + - '@swc/html' + - clean-css + - cssnano + - csso + - esbuild + - html-minifier-terser + - lightningcss + - postcss + - uglify-js + which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 @@ -4994,6 +5933,8 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + wildcard@2.0.1: {} + word-wrap@1.2.5: {} wrap-ansi@7.0.0: diff --git a/packages/acp-agent/package.json b/packages/acp-agent/package.json index 4fc151f..4008efa 100644 --- a/packages/acp-agent/package.json +++ b/packages/acp-agent/package.json @@ -21,10 +21,12 @@ "@agentclientprotocol/sdk": "^0.4.3", "@ai-sdk/mcp": "0.0.18", "@ai-sdk/openai": "^2.0.0", + "@fledgling/agent-core": "workspace:*", "@fledgling/common": "workspace:*", "@fledgling/context-builder": "workspace:*", "@fledgling/mcp-workspace": "workspace:*", - "@fledgling/session-log": "workspace:*", + "@fledgling/session-file-system": "workspace:*", + "@fledgling/tools-mcp-node": "workspace:*", "ai": "^5.0.0" }, "devDependencies": { diff --git a/packages/acp-agent/src/agent-prompt-cancellation.test.ts b/packages/acp-agent/src/agent-prompt-cancellation.test.ts index af7ceea..d79add5 100644 --- a/packages/acp-agent/src/agent-prompt-cancellation.test.ts +++ b/packages/acp-agent/src/agent-prompt-cancellation.test.ts @@ -5,7 +5,7 @@ import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; import type { SessionEvent } from "@fledgling/common"; -import { SessionStore } from "@fledgling/session-log"; +import { FileSystemSessionManager } from "@fledgling/session-file-system"; import type { FledglingAgent, FledglingAgentDependencies } from "./agent.js"; @@ -324,14 +324,14 @@ describe("FledglingAgent prompt cancellation", () => { }); it("replays stored user and assistant messages when loading a session", async () => { - const { agent, sessionId, sessionStore, sessionUpdates, tempDir } = await createTestAgent(); - await sessionStore.append({ - ...sessionStore.createEventBase(sessionId), + const { agent, sessionId, sessionManager, sessionUpdates, tempDir } = await createTestAgent(); + await sessionManager.appendEvent({ + ...sessionManager.createEventBase(sessionId), type: "message.user", text: "previous user" }); - await sessionStore.append({ - ...sessionStore.createEventBase(sessionId), + await sessionManager.appendEvent({ + ...sessionManager.createEventBase(sessionId), type: "message.assistant", text: "previous assistant" }); @@ -358,13 +358,17 @@ describe("FledglingAgent prompt cancellation", () => { it("rejects session creation when injected tool setup fails", async () => { const { FledglingAgent } = await import("./agent.js"); - const sessionStore = await createTempSessionStore(); + const sessionStore = await createTempSessionManager(); const agent = new FledglingAgent(createFakeConnection() as never, { - createSessionTools: vi.fn(async () => { - throw new Error("setup failed"); - }), - sessionStore: sessionStore.store, - runModelTurn: vi.fn() + toolProvider: { + createSessionTools: vi.fn(async () => { + throw new Error("setup failed"); + }) + }, + sessionManager: sessionStore.manager, + modelTurnRunner: { + runModelTurn: vi.fn() + } }); await expect(agent.newSession({ cwd: sessionStore.tempDir, mcpServers: [] })).rejects.toThrow("setup failed"); @@ -374,19 +378,23 @@ describe("FledglingAgent prompt cancellation", () => { readonly agent: FledglingAgent; readonly sessionId: string; readonly sessionFile: string; - readonly sessionStore: SessionStore; + readonly sessionManager: FileSystemSessionManager; readonly sessionUpdates: FakeSessionUpdate[]; readonly tempDir: string; readonly streamText: ReturnType; }> { - const { store: sessionStore, sessionFile, tempDir: createdTempDir } = await createTempSessionStore(); + const { manager: sessionManager, sessionFile, tempDir: createdTempDir } = await createTempSessionManager(); const sessionUpdates: FakeSessionUpdate[] = []; const streamText = vi.fn(); const { FledglingAgent } = await import("./agent.js"); const agent = new FledglingAgent(createFakeConnection(sessionUpdates) as never, { - createSessionTools: vi.fn(async () => ({ mcpClients: [], tools: {} })), - sessionStore, - runModelTurn: streamText + toolProvider: { + createSessionTools: vi.fn(async () => ({ clients: [], tools: {} })) + }, + sessionManager, + modelTurnRunner: { + runModelTurn: streamText + } } satisfies FledglingAgentDependencies); const session = await agent.newSession({ cwd: createdTempDir, mcpServers: [] }); @@ -394,7 +402,7 @@ describe("FledglingAgent prompt cancellation", () => { agent, sessionId: session.sessionId, sessionFile, - sessionStore, + sessionManager, sessionUpdates, tempDir: createdTempDir, streamText @@ -418,8 +426,8 @@ function createFakeConnection(sessionUpdates: FakeSessionUpdate[] = []): { sessi }; } -async function createTempSessionStore(): Promise<{ - readonly store: SessionStore; +async function createTempSessionManager(): Promise<{ + readonly manager: FileSystemSessionManager; readonly sessionFile: string; readonly tempDir: string; }> { @@ -428,7 +436,11 @@ async function createTempSessionStore(): Promise<{ process.env.FLEDGLING_CONFIG = path.join(createdTempDir, "missing-config.json"); const sessionFile = path.join(createdTempDir, "session.jsonl"); process.env.FLEDGLING_SESSION_FILE = sessionFile; - return { store: new SessionStore(createdTempDir, sessionFile), sessionFile, tempDir: createdTempDir }; + return { + manager: new FileSystemSessionManager(createdTempDir, sessionFile), + sessionFile, + tempDir: createdTempDir + }; } function createControlledStream(parts: readonly StreamPart[]): ControlledStream { diff --git a/packages/acp-agent/src/agent-session-lifecycle.test.ts b/packages/acp-agent/src/agent-session-lifecycle.test.ts index 720a349..86f4f25 100644 --- a/packages/acp-agent/src/agent-session-lifecycle.test.ts +++ b/packages/acp-agent/src/agent-session-lifecycle.test.ts @@ -4,6 +4,8 @@ import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; +import { FileSystemSessionManager } from "@fledgling/session-file-system"; + describe("FledglingAgent session lifecycle", () => { const originalConfig = process.env.FLEDGLING_CONFIG; const originalSessionFile = process.env.FLEDGLING_SESSION_FILE; @@ -38,26 +40,33 @@ describe("FledglingAgent session lifecycle", () => { }; const clients = [firstClient, secondClient]; - vi.doMock("@ai-sdk/mcp", () => ({ - experimental_createMCPClient: vi.fn(async () => { - const client = clients.shift(); - if (!client) { - throw new Error("unexpected client request"); - } + const { FledglingAgent } = await import("./agent.js"); + const agent = new FledglingAgent( + { + sessionUpdate: vi.fn(async () => {}) + } as never, + { + sessionManager: new FileSystemSessionManager(tempDir, process.env.FLEDGLING_SESSION_FILE), + toolProvider: { + createSessionTools: vi.fn(async () => { + const client = clients.shift(); + if (!client) { + throw new Error("unexpected client request"); + } - return client; - }) - })); - vi.doMock("@ai-sdk/mcp/mcp-stdio", () => ({ - Experimental_StdioMCPTransport: class Experimental_StdioMCPTransport { - public constructor(_options: unknown) {} + return { + clients: [client], + tools: {} + }; + }) + }, + modelTurnRunner: { + runModelTurn: vi.fn(() => ({ + fullStream: (async function* () {})() + })) + } } - })); - - const { FledglingAgent } = await import("./agent.js"); - const agent = new FledglingAgent({ - sessionUpdate: vi.fn(async () => {}) - } as never); + ); const mcpServers = [{ name: "workspace", command: "workspace-command", args: [], env: [] }]; const session = await agent.newSession({ cwd: tempDir, mcpServers }); diff --git a/packages/acp-agent/src/agent.ts b/packages/acp-agent/src/agent.ts index 532f139..e59b739 100644 --- a/packages/acp-agent/src/agent.ts +++ b/packages/acp-agent/src/agent.ts @@ -1,601 +1,53 @@ -import * as acp from "@agentclientprotocol/sdk"; -import type { experimental_MCPClient as MCPClient } from "@ai-sdk/mcp"; import { createOpenAI, type OpenAIProvider } from "@ai-sdk/openai"; -import type { SessionErrorEvent } from "@fledgling/common"; -import { buildContext } from "@fledgling/context-builder"; -import { SessionStore } from "@fledgling/session-log"; -import { stepCountIs, streamText, type CoreMessage, type LanguageModel, type ToolSet } from "ai"; - -import { createSessionTools } from "./mcp-session-tools.js"; import { - extractContextHint, - extractPromptText, - messageContentToText, - stringifyToolOutput, - toRawObject -} from "./prompt-content.js"; -import { SessionCleanup } from "./session-cleanup.js"; - -type SessionEventBase = ReturnType; - -export interface SessionStoreLike { - createId(): string; - createEventBase(sessionId: string): SessionEventBase; - append(event: Parameters[0]): Promise; - load(sessionId: string): Promise>>; -} - -interface SessionState { - readonly id: string; - readonly cwd: string | undefined; - readonly history: CoreMessage[]; - readonly mcpClients: MCPClient[]; - readonly tools: ToolSet; - readonly toolCallNames: Map; - pendingPrompt: AbortController | undefined; - promptQueue: Promise; -} - -type ModelStreamPart = - | { - readonly type: "text-delta"; - readonly text: string; - } - | { - readonly type: "tool-call"; - readonly toolCallId: string; - readonly toolName: string; - readonly input: unknown; - } - | { - readonly type: "tool-result"; - readonly toolCallId: string; - readonly output: unknown; - } - | { - readonly type: "tool-error"; - readonly toolCallId: string; - readonly error: unknown; - }; - -interface ModelTurnRequest { - readonly messages: CoreMessage[]; - readonly tools: ToolSet; - readonly abortSignal: AbortSignal; -} - -interface ModelTurnResult { - readonly fullStream: AsyncIterable; -} - -type PromptErrorKind = SessionErrorEvent["kind"]; -type PromptErrorPhase = SessionErrorEvent["phase"]; - -interface NormalizedPromptError { - readonly kind: PromptErrorKind; - readonly phase: PromptErrorPhase; - readonly message: string; - readonly recoverable: boolean; - readonly errorName: string | undefined; - readonly errorCode: string | undefined; -} - -export interface FledglingAgentDependencies { - readonly createSessionTools: typeof createSessionTools; - readonly sessionStore: SessionStoreLike; - readonly runModelTurn: (request: ModelTurnRequest) => ModelTurnResult; -} + type FledglingAgentDependencies, + type IModelTurnRunner, + type ModelTurnRequest, + type ModelTurnResult, + type ModelStreamPart +} from "@fledgling/agent-core"; +import { FileSystemSessionManager } from "@fledgling/session-file-system"; +import { NodeMcpToolProvider } from "@fledgling/tools-mcp-node"; +import { stepCountIs, streamText, type LanguageModel, type ToolSet } from "ai"; + +export { + FledglingAgent, + type FledglingAgentDependencies, + type IModelTurnRunner, + type IRuntimeLogger, + type ISessionManager, + type IToolProvider, + type ModelStreamPart, + type ModelTurnRequest, + type ModelTurnResult +} from "@fledgling/agent-core"; const DEFAULT_SYSTEM_PROMPT: string = "You are Fledgling, a small ACP-native assistant. Answer directly. Use tools when they are available and useful. If the user asks you to inspect, create, modify, delete, search, or execute something in the workspace, use the relevant workspace tool instead of only describing what you would do. If the user asks you to write content to a file, call the file-writing tool. Do not claim you cannot access files when a relevant workspace tool is available. Tool results may include Fledgling context hints that describe identity, retention, and prompt placement for future context assembly."; -export class FledglingAgent implements acp.Agent { - readonly #connection: acp.AgentSideConnection; - readonly #sessions: Map = new Map(); - readonly #dependencies: FledglingAgentDependencies; - readonly #sessionCleanup: SessionCleanup = new SessionCleanup( - () => this.#sessions.values(), - () => this.#sessions.clear() - ); - - public constructor( - connection: acp.AgentSideConnection, - dependencies: FledglingAgentDependencies = createDefaultDependencies() - ) { - this.#connection = connection; - this.#dependencies = dependencies; - } - - public async initialize(_params: acp.InitializeRequest): Promise { - return { - protocolVersion: acp.PROTOCOL_VERSION, - agentCapabilities: { - loadSession: true, - mcpCapabilities: { - http: true, - sse: true - } - } - }; - } - - public async newSession(params: acp.NewSessionRequest): Promise { - const { mcpClients, tools } = await this.#dependencies.createSessionTools(params.cwd, params.mcpServers); - const session: SessionState = { - id: this.#dependencies.sessionStore.createId(), - cwd: params.cwd, - history: [], - mcpClients, - tools, - toolCallNames: new Map(), - pendingPrompt: undefined, - promptQueue: Promise.resolve() - }; - - this.#sessions.set(session.id, session); - await this.#dependencies.sessionStore.append({ - ...this.#dependencies.sessionStore.createEventBase(session.id), - type: "session.created", - cwd: params.cwd, - mcpServers: params.mcpServers.map((server) => server.name) +export class VercelAiSdkModelTurnRunner implements IModelTurnRunner { + public runModelTurn(request: ModelTurnRequest): ModelTurnResult { + const openai = createOpenAI({ + apiKey: process.env.OPENAI_API_KEY, + baseURL: process.env.OPENAI_BASE_URL + }); + const modelName = process.env.OPENAI_MODEL ?? "gpt-4.1-mini"; + const model = selectOpenAiModel(openai, modelName); + + const result = streamText({ + model, + system: process.env.FLEDGLING_SYSTEM_PROMPT ?? DEFAULT_SYSTEM_PROMPT, + messages: request.messages, + tools: request.tools, + toolChoice: getToolChoice(request.tools), + stopWhen: stepCountIs(5), + abortSignal: request.abortSignal }); return { - sessionId: session.id + fullStream: result.fullStream as AsyncIterable }; } - - public async loadSession(params: acp.LoadSessionRequest): Promise { - const events = await this.#dependencies.sessionStore.load(params.sessionId); - const context = buildContext(events, { mode: "replay" }); - const { mcpClients, tools } = await this.#dependencies.createSessionTools(params.cwd, params.mcpServers); - const existingSession = this.#sessions.get(params.sessionId); - const session: SessionState = { - id: params.sessionId, - cwd: params.cwd, - history: context.messages.map((message) => ({ role: message.role, content: message.content }) as CoreMessage), - mcpClients, - tools, - toolCallNames: new Map(), - pendingPrompt: undefined, - promptQueue: Promise.resolve() - }; - - if (existingSession) { - await this.#sessionCleanup.closeSession(existingSession, "session-replaced"); - } - - this.#sessions.set(session.id, session); - await this.#dependencies.sessionStore.append({ - ...this.#dependencies.sessionStore.createEventBase(session.id), - type: "session.loaded", - cwd: params.cwd, - source: "session.load" - }); - await replaySessionHistory(this.#connection, session); - - return {}; - } - - public async authenticate(_params: acp.AuthenticateRequest): Promise { - return {}; - } - - public async setSessionMode(_params: acp.SetSessionModeRequest): Promise { - return {}; - } - - public async prompt(params: acp.PromptRequest): Promise { - const session = this.#sessions.get(params.sessionId); - if (!session) { - throw new Error(`Unknown ACP session: ${sanitizeErrorMessage(params.sessionId)}`); - } - - const response = session.promptQueue - .catch(() => undefined) - .then(() => { - if (this.#sessions.get(session.id) !== session) { - return { stopReason: "cancelled" } satisfies acp.PromptResponse; - } - - return this.#runPrompt(session, params); - }); - session.promptQueue = response.then( - () => undefined, - () => undefined - ); - - return response; - } - - async #runPrompt(session: SessionState, params: acp.PromptRequest): Promise { - const userText = extractPromptText(params); - const promptController = new AbortController(); - session.pendingPrompt = promptController; - - let assistantText = ""; - - try { - session.history.push({ role: "user", content: userText }); - await this.#dependencies.sessionStore.append({ - ...this.#dependencies.sessionStore.createEventBase(session.id), - type: "message.user", - text: userText - }); - - let result: ModelTurnResult; - try { - result = this.#dependencies.runModelTurn({ - messages: session.history, - tools: session.tools, - abortSignal: promptController.signal - }); - } catch (error: unknown) { - await this.#recordPromptError(session, params.sessionId, { - error, - kind: "model_start_failed", - phase: "model_start", - assistantTextPersisted: false - }); - throw createPromptRpcError(error, "model_start"); - } - - try { - for await (const part of result.fullStream) { - if (process.env.FLEDGLING_DEBUG_STREAM === "1") { - console.error(JSON.stringify({ streamPart: part.type })); - } - - switch (part.type) { - case "text-delta": { - assistantText += part.text; - await this.#connection.sessionUpdate({ - sessionId: params.sessionId, - update: { - sessionUpdate: "agent_message_chunk", - content: { - type: "text", - text: part.text - } - } - }); - break; - } - - case "tool-call": { - session.toolCallNames.set(part.toolCallId, part.toolName); - await this.#dependencies.sessionStore.append({ - ...this.#dependencies.sessionStore.createEventBase(session.id), - type: "tool.call", - toolCallId: part.toolCallId, - toolName: part.toolName, - title: part.toolName, - rawInput: part.input - }); - await this.#connection.sessionUpdate({ - sessionId: params.sessionId, - update: { - sessionUpdate: "tool_call", - toolCallId: part.toolCallId, - title: part.toolName, - kind: "other", - status: "pending", - rawInput: toRawObject(part.input) - } - }); - break; - } - - case "tool-result": { - await this.#dependencies.sessionStore.append({ - ...this.#dependencies.sessionStore.createEventBase(session.id), - type: "tool.result", - toolCallId: part.toolCallId, - toolName: session.toolCallNames.get(part.toolCallId), - status: "completed", - text: stringifyToolOutput(part.output), - rawOutput: part.output, - contextHint: extractContextHint(part.output) - }); - await this.#connection.sessionUpdate({ - sessionId: params.sessionId, - update: { - sessionUpdate: "tool_call_update", - toolCallId: part.toolCallId, - status: "completed", - content: [ - { - type: "content", - content: { - type: "text", - text: stringifyToolOutput(part.output) - } - } - ], - rawOutput: toRawObject(part.output) - } - }); - break; - } - - case "tool-error": { - await this.#dependencies.sessionStore.append({ - ...this.#dependencies.sessionStore.createEventBase(session.id), - type: "tool.result", - toolCallId: part.toolCallId, - toolName: session.toolCallNames.get(part.toolCallId), - status: "failed", - text: stringifyToolOutput(part.error), - rawOutput: toRawObject(part.error), - contextHint: undefined - }); - await this.#connection.sessionUpdate({ - sessionId: params.sessionId, - update: { - sessionUpdate: "tool_call_update", - toolCallId: part.toolCallId, - status: "failed", - content: [ - { - type: "content", - content: { - type: "text", - text: stringifyToolOutput(part.error) - } - } - ], - rawOutput: toRawObject(part.error) - } - }); - break; - } - } - } - } catch (error: unknown) { - if (promptController.signal.aborted) { - return { stopReason: "cancelled" }; - } - - const assistantTextPersisted = assistantText.length > 0; - if (assistantTextPersisted) { - await this.#persistAssistantMessage(session, assistantText); - } - - await this.#recordPromptError(session, params.sessionId, { - error, - kind: "model_stream_failed", - phase: "model_stream", - assistantTextPersisted - }); - throw createPromptRpcError(error, "model_stream"); - } - - await this.#persistAssistantMessage(session, assistantText); - - return { - stopReason: "end_turn" - }; - } catch (error: unknown) { - if (promptController.signal.aborted) { - return { stopReason: "cancelled" }; - } - - throw error; - } finally { - if (session.pendingPrompt === promptController) { - session.pendingPrompt = undefined; - } - } - } - - async #persistAssistantMessage(session: SessionState, assistantText: string): Promise { - session.history.push({ role: "assistant", content: assistantText }); - await this.#dependencies.sessionStore.append({ - ...this.#dependencies.sessionStore.createEventBase(session.id), - type: "message.assistant", - text: assistantText - }); - } - - async #recordPromptError( - session: SessionState, - sessionId: string, - options: { - readonly error: unknown; - readonly kind: PromptErrorKind; - readonly phase: PromptErrorPhase; - readonly assistantTextPersisted: boolean; - } - ): Promise { - const normalized = normalizePromptError(options.error, options.kind, options.phase); - await this.#connection.sessionUpdate({ - sessionId, - update: { - sessionUpdate: "agent_message_chunk", - content: { - type: "text", - text: `\n\n[Fledgling error: ${formatPromptErrorKind(normalized.kind)}. ${normalized.message}]` - } - } - }); - await this.#dependencies.sessionStore.append({ - ...this.#dependencies.sessionStore.createEventBase(session.id), - type: "session.error", - kind: normalized.kind, - phase: normalized.phase, - message: normalized.message, - recoverable: normalized.recoverable, - assistantTextPersisted: options.assistantTextPersisted, - errorName: normalized.errorName, - errorCode: normalized.errorCode - }); - } - - public async cancel(_params: acp.CancelNotification): Promise { - this.#sessions.get(_params.sessionId)?.pendingPrompt?.abort(); - } - - public closeAllSessions(reason: string): Promise { - return this.#sessionCleanup.closeAll(reason); - } -} - -function normalizePromptError(error: unknown, kind: PromptErrorKind, phase: PromptErrorPhase): NormalizedPromptError { - return { - kind, - phase, - message: sanitizeErrorMessage(extractErrorMessage(error)), - recoverable: true, - errorName: extractErrorName(error), - errorCode: extractErrorCode(error) - }; -} - -function createPromptRpcError(error: unknown, phase: "model_start" | "model_stream"): Error { - const normalized = normalizePromptError( - error, - phase === "model_start" ? "model_start_failed" : "model_stream_failed", - phase - ); - return new Error(`Fledgling ${formatPromptErrorKind(normalized.kind)}: ${normalized.message}`); -} - -function formatPromptErrorKind(kind: PromptErrorKind): string { - switch (kind) { - case "model_start_failed": - return "model start failed"; - - case "model_stream_failed": - return "model stream failed"; - - case "prompt_cleanup_failed": - return "prompt cleanup failed"; - } -} - -function extractErrorMessage(error: unknown): string { - if (error instanceof Error) { - return error.message; - } - - if (typeof error === "string") { - return error; - } - - if (error && typeof error === "object" && "message" in error) { - const message = (error as { readonly message?: unknown }).message; - if (typeof message === "string") { - return message; - } - } - - return "Unknown error"; -} - -function extractErrorName(error: unknown): string | undefined { - if (error instanceof Error) { - return error.name; - } - - if (error && typeof error === "object" && "name" in error) { - const name = (error as { readonly name?: unknown }).name; - return typeof name === "string" ? sanitizeErrorMessage(name) : undefined; - } - - return undefined; -} - -function extractErrorCode(error: unknown): string | undefined { - if (!error || typeof error !== "object" || !("code" in error)) { - return undefined; - } - - const code = (error as { readonly code?: unknown }).code; - if (typeof code === "string" || typeof code === "number") { - return sanitizeErrorMessage(String(code)); - } - - return undefined; -} - -function sanitizeErrorMessage(message: string): string { - const withoutControlCharacters = replaceControlCharacters(stripAnsiEscapeSequences(message)); - const normalized = withoutControlCharacters.replace(/\s+/g, " ").trim() || "Unknown error"; - const redacted = normalized - .replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [redacted]") - .replace(/\bsk-[A-Za-z0-9_-]{8,}\b/g, "sk-[redacted]") - .replace(/\b(api[_-]?key|token|secret|password)=\S+/gi, "$1=[redacted]"); - - return redacted.length > 500 ? `${redacted.slice(0, 497)}...` : redacted; -} - -function stripAnsiEscapeSequences(message: string): string { - let stripped = ""; - for (let index = 0; index < message.length; index++) { - const code = message.charCodeAt(index); - if (code !== 0x1b) { - stripped += message[index]; - continue; - } - - const next = message.charCodeAt(index + 1); - if (next === 0x5b) { - index += 2; - while (index < message.length) { - const sequenceCode = message.charCodeAt(index); - if (sequenceCode >= 0x40 && sequenceCode <= 0x7e) { - break; - } - - index++; - } - continue; - } - - if (next >= 0x40 && next <= 0x5f) { - index++; - } - } - - return stripped; -} - -function replaceControlCharacters(message: string): string { - let replaced = ""; - for (let index = 0; index < message.length; index++) { - const code = message.charCodeAt(index); - replaced += isUnsafeControlCharacter(code) ? " " : message[index]; - } - - return replaced; -} - -function isUnsafeControlCharacter(code: number): boolean { - return code <= 0x08 || code === 0x0b || code === 0x0c || (code >= 0x0e && code <= 0x1f) || (code >= 0x7f && code <= 0x9f); -} - -async function replaySessionHistory(connection: acp.AgentSideConnection, session: SessionState): Promise { - for (const message of session.history) { - if (message.role !== "user" && message.role !== "assistant") { - continue; - } - - const text = messageContentToText(message.content); - if (!text) { - continue; - } - - await connection.sessionUpdate({ - sessionId: session.id, - update: { - sessionUpdate: message.role === "user" ? "user_message_chunk" : "agent_message_chunk", - content: { - type: "text", - text - } - } - }); - } } function getToolChoice(tools: ToolSet): "auto" | { type: "tool"; toolName: string } { @@ -611,37 +63,26 @@ function getToolChoice(tools: ToolSet): "auto" | { type: "tool"; toolName: strin return { type: "tool", toolName }; } -function createDefaultDependencies(): FledglingAgentDependencies { - return { - createSessionTools, - sessionStore: new SessionStore(), - runModelTurn: runDefaultModelTurn - }; +function selectOpenAiModel(openai: OpenAIProvider, modelName: string): LanguageModel { + return process.env.FLEDGLING_OPENAI_API === "responses" ? openai.responses(modelName) : openai.chat(modelName); } -function runDefaultModelTurn(request: ModelTurnRequest): ModelTurnResult { - const openai = createOpenAI({ - apiKey: process.env.OPENAI_API_KEY, - baseURL: process.env.OPENAI_BASE_URL - }); - const modelName = process.env.OPENAI_MODEL ?? "gpt-4.1-mini"; - const model = selectOpenAiModel(openai, modelName); - - const result = streamText({ - model, - system: process.env.FLEDGLING_SYSTEM_PROMPT ?? DEFAULT_SYSTEM_PROMPT, - messages: request.messages, - tools: request.tools, - toolChoice: getToolChoice(request.tools), - stopWhen: stepCountIs(5), - abortSignal: request.abortSignal - }); - +export function createDefaultDependencies(): FledglingAgentDependencies { return { - fullStream: result.fullStream as AsyncIterable + toolProvider: new NodeMcpToolProvider(), + sessionManager: new FileSystemSessionManager(), + modelTurnRunner: new VercelAiSdkModelTurnRunner(), + logger: { + debug(value: unknown): void { + console.error(JSON.stringify(value)); + }, + warn(record: unknown): void { + console.warn(record); + }, + error(record: unknown): void { + console.error(record); + } + }, + debugStream: process.env.FLEDGLING_DEBUG_STREAM === "1" }; } - -function selectOpenAiModel(openai: OpenAIProvider, modelName: string): LanguageModel { - return process.env.FLEDGLING_OPENAI_API === "responses" ? openai.responses(modelName) : openai.chat(modelName); -} diff --git a/packages/acp-agent/src/index.ts b/packages/acp-agent/src/index.ts index 1873876..cfb5ea2 100644 --- a/packages/acp-agent/src/index.ts +++ b/packages/acp-agent/src/index.ts @@ -2,7 +2,7 @@ import { Readable, Writable } from "node:stream"; import * as acp from "@agentclientprotocol/sdk"; -import { FledglingAgent } from "./agent.js"; +import { createDefaultDependencies, FledglingAgent } from "./agent.js"; import { registerProcessLifecycle } from "./process-lifecycle.js"; const input: WritableStream = Writable.toWeb(process.stdout); @@ -15,6 +15,6 @@ registerProcessLifecycle(() => activeAgent); // eslint-disable-next-line no-new -- AgentSideConnection owns the stdio lifecycle. new acp.AgentSideConnection((connection) => { - activeAgent = new FledglingAgent(connection); + activeAgent = new FledglingAgent(connection, createDefaultDependencies()); return activeAgent; }, stream); diff --git a/packages/agent-core/config/rig.json b/packages/agent-core/config/rig.json new file mode 100644 index 0000000..64b84c1 --- /dev/null +++ b/packages/agent-core/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@fledgling/heft-rig" +} diff --git a/packages/agent-core/eslint.config.cjs b/packages/agent-core/eslint.config.cjs new file mode 100644 index 0000000..1c0a01e --- /dev/null +++ b/packages/agent-core/eslint.config.cjs @@ -0,0 +1,3 @@ +const rigConfig = require("@fledgling/heft-rig/profiles/default/config/eslint.config.cjs"); + +module.exports = [...rigConfig]; diff --git a/packages/agent-core/package.json b/packages/agent-core/package.json new file mode 100644 index 0000000..29041de --- /dev/null +++ b/packages/agent-core/package.json @@ -0,0 +1,30 @@ +{ + "name": "@fledgling/agent-core", + "version": "0.0.0", + "private": true, + "description": "Browser-safe ACP agent loop core for Fledgling.", + "license": "MIT", + "type": "module", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "scripts": { + "build": "heft build --clean", + "_phase:build": "heft run --only build -- --clean", + "test": "vitest run", + "_phase:test": "vitest run --coverage" + }, + "dependencies": { + "@agentclientprotocol/sdk": "^0.4.3", + "@fledgling/common": "workspace:*", + "@fledgling/context-builder": "workspace:*", + "ai": "^5.0.0" + }, + "devDependencies": { + "@fledgling/heft-rig": "workspace:*", + "@rushstack/heft": "1.2.7", + "@types/node": "^22.0.0", + "@vitest/coverage-v8": "^3.1.1", + "vitest": "^3.1.1", + "typescript": "~5.9.0" + } +} diff --git a/packages/agent-core/src/agent.ts b/packages/agent-core/src/agent.ts new file mode 100644 index 0000000..2ee85f5 --- /dev/null +++ b/packages/agent-core/src/agent.ts @@ -0,0 +1,410 @@ +import * as acp from "@agentclientprotocol/sdk"; +import { buildContext } from "@fledgling/context-builder"; +import type { CoreMessage, ToolSet } from "ai"; + +import type { FledglingAgentDependencies, IClosable } from "./interfaces.js"; +import { + createPromptRpcError, + formatPromptErrorKind, + normalizePromptError, + sanitizeErrorMessage +} from "./prompt-errors.js"; +import { + extractContextHint, + extractPromptText, + messageContentToText, + stringifyToolOutput, + toRawObject +} from "./prompt-content.js"; +import { SessionCleanup } from "./session-cleanup.js"; + +interface SessionState { + readonly id: string; + readonly cwd: string | undefined; + readonly history: CoreMessage[]; + readonly clients: IClosable[]; + readonly tools: ToolSet; + readonly toolCallNames: Map; + pendingPrompt: AbortController | undefined; + promptQueue: Promise; +} + +export class FledglingAgent implements acp.Agent { + readonly #connection: acp.AgentSideConnection; + readonly #sessions: Map = new Map(); + readonly #dependencies: FledglingAgentDependencies; + readonly #sessionCleanup: SessionCleanup; + + public constructor(connection: acp.AgentSideConnection, dependencies: FledglingAgentDependencies) { + this.#connection = connection; + this.#dependencies = dependencies; + this.#sessionCleanup = new SessionCleanup( + () => this.#sessions.values(), + () => this.#sessions.clear(), + dependencies.logger + ); + } + + public async initialize(_params: acp.InitializeRequest): Promise { + return { + protocolVersion: acp.PROTOCOL_VERSION, + agentCapabilities: { + loadSession: true, + mcpCapabilities: { + http: true, + sse: true + } + } + }; + } + + public async newSession(params: acp.NewSessionRequest): Promise { + const { clients, tools } = await this.#dependencies.toolProvider.createSessionTools({ + cwd: params.cwd, + mcpServers: params.mcpServers + }); + const session: SessionState = { + id: this.#dependencies.sessionManager.createSessionId(), + cwd: params.cwd, + history: [], + clients, + tools, + toolCallNames: new Map(), + pendingPrompt: undefined, + promptQueue: Promise.resolve() + }; + + this.#sessions.set(session.id, session); + await this.#dependencies.sessionManager.appendEvent({ + ...this.#dependencies.sessionManager.createEventBase(session.id), + type: "session.created", + cwd: params.cwd, + mcpServers: params.mcpServers.map((server) => server.name) + }); + + return { + sessionId: session.id + }; + } + + public async loadSession(params: acp.LoadSessionRequest): Promise { + const events = await this.#dependencies.sessionManager.loadEvents(params.sessionId); + const context = buildContext(events, { mode: "replay" }); + const { clients, tools } = await this.#dependencies.toolProvider.createSessionTools({ + cwd: params.cwd, + mcpServers: params.mcpServers + }); + const existingSession = this.#sessions.get(params.sessionId); + const session: SessionState = { + id: params.sessionId, + cwd: params.cwd, + history: context.messages.map((message) => ({ role: message.role, content: message.content }) as CoreMessage), + clients, + tools, + toolCallNames: new Map(), + pendingPrompt: undefined, + promptQueue: Promise.resolve() + }; + + if (existingSession) { + await this.#sessionCleanup.closeSession(existingSession, "session-replaced"); + } + + this.#sessions.set(session.id, session); + await this.#dependencies.sessionManager.appendEvent({ + ...this.#dependencies.sessionManager.createEventBase(session.id), + type: "session.loaded", + cwd: params.cwd, + source: "session.load" + }); + await replaySessionHistory(this.#connection, session); + + return {}; + } + + public async authenticate(_params: acp.AuthenticateRequest): Promise { + return {}; + } + + public async setSessionMode(_params: acp.SetSessionModeRequest): Promise { + return {}; + } + + public async prompt(params: acp.PromptRequest): Promise { + const session = this.#sessions.get(params.sessionId); + if (!session) { + throw new Error(`Unknown ACP session: ${sanitizeErrorMessage(params.sessionId)}`); + } + + const response = session.promptQueue + .catch(() => undefined) + .then(() => { + if (this.#sessions.get(session.id) !== session) { + return { stopReason: "cancelled" } satisfies acp.PromptResponse; + } + + return this.#runPrompt(session, params); + }); + session.promptQueue = response.then( + () => undefined, + () => undefined + ); + + return response; + } + + async #runPrompt(session: SessionState, params: acp.PromptRequest): Promise { + const userText = extractPromptText(params); + const promptController = new AbortController(); + session.pendingPrompt = promptController; + + let assistantText = ""; + + try { + session.history.push({ role: "user", content: userText }); + await this.#dependencies.sessionManager.appendEvent({ + ...this.#dependencies.sessionManager.createEventBase(session.id), + type: "message.user", + text: userText + }); + + let result: ReturnType; + try { + result = this.#dependencies.modelTurnRunner.runModelTurn({ + messages: session.history, + tools: session.tools, + abortSignal: promptController.signal + }); + } catch (error: unknown) { + await this.#recordPromptError(session, params.sessionId, { + error, + kind: "model_start_failed", + phase: "model_start", + assistantTextPersisted: false + }); + throw createPromptRpcError(error, "model_start"); + } + + try { + for await (const part of result.fullStream) { + if (this.#dependencies.debugStream) { + this.#dependencies.logger?.debug?.({ streamPart: part.type }); + } + + switch (part.type) { + case "text-delta": { + assistantText += part.text; + await this.#connection.sessionUpdate({ + sessionId: params.sessionId, + update: { + sessionUpdate: "agent_message_chunk", + content: { + type: "text", + text: part.text + } + } + }); + break; + } + + case "tool-call": { + session.toolCallNames.set(part.toolCallId, part.toolName); + await this.#dependencies.sessionManager.appendEvent({ + ...this.#dependencies.sessionManager.createEventBase(session.id), + type: "tool.call", + toolCallId: part.toolCallId, + toolName: part.toolName, + title: part.toolName, + rawInput: part.input + }); + await this.#connection.sessionUpdate({ + sessionId: params.sessionId, + update: { + sessionUpdate: "tool_call", + toolCallId: part.toolCallId, + title: part.toolName, + kind: "other", + status: "pending", + rawInput: toRawObject(part.input) + } + }); + break; + } + + case "tool-result": { + await this.#dependencies.sessionManager.appendEvent({ + ...this.#dependencies.sessionManager.createEventBase(session.id), + type: "tool.result", + toolCallId: part.toolCallId, + toolName: session.toolCallNames.get(part.toolCallId), + status: "completed", + text: stringifyToolOutput(part.output), + rawOutput: part.output, + contextHint: extractContextHint(part.output) + }); + await this.#connection.sessionUpdate({ + sessionId: params.sessionId, + update: { + sessionUpdate: "tool_call_update", + toolCallId: part.toolCallId, + status: "completed", + content: [ + { + type: "content", + content: { + type: "text", + text: stringifyToolOutput(part.output) + } + } + ], + rawOutput: toRawObject(part.output) + } + }); + break; + } + + case "tool-error": { + await this.#dependencies.sessionManager.appendEvent({ + ...this.#dependencies.sessionManager.createEventBase(session.id), + type: "tool.result", + toolCallId: part.toolCallId, + toolName: session.toolCallNames.get(part.toolCallId), + status: "failed", + text: stringifyToolOutput(part.error), + rawOutput: toRawObject(part.error), + contextHint: undefined + }); + await this.#connection.sessionUpdate({ + sessionId: params.sessionId, + update: { + sessionUpdate: "tool_call_update", + toolCallId: part.toolCallId, + status: "failed", + content: [ + { + type: "content", + content: { + type: "text", + text: stringifyToolOutput(part.error) + } + } + ], + rawOutput: toRawObject(part.error) + } + }); + break; + } + } + } + } catch (error: unknown) { + if (promptController.signal.aborted) { + return { stopReason: "cancelled" }; + } + + const assistantTextPersisted = assistantText.length > 0; + if (assistantTextPersisted) { + await this.#persistAssistantMessage(session, assistantText); + } + + await this.#recordPromptError(session, params.sessionId, { + error, + kind: "model_stream_failed", + phase: "model_stream", + assistantTextPersisted + }); + throw createPromptRpcError(error, "model_stream"); + } + + await this.#persistAssistantMessage(session, assistantText); + + return { + stopReason: "end_turn" + }; + } catch (error: unknown) { + if (promptController.signal.aborted) { + return { stopReason: "cancelled" }; + } + + throw error; + } finally { + if (session.pendingPrompt === promptController) { + session.pendingPrompt = undefined; + } + } + } + + async #persistAssistantMessage(session: SessionState, assistantText: string): Promise { + session.history.push({ role: "assistant", content: assistantText }); + await this.#dependencies.sessionManager.appendEvent({ + ...this.#dependencies.sessionManager.createEventBase(session.id), + type: "message.assistant", + text: assistantText + }); + } + + async #recordPromptError( + session: SessionState, + sessionId: string, + options: { + readonly error: unknown; + readonly kind: "model_start_failed" | "model_stream_failed" | "prompt_cleanup_failed"; + readonly phase: "model_start" | "model_stream" | "cleanup"; + readonly assistantTextPersisted: boolean; + } + ): Promise { + const normalized = normalizePromptError(options.error, options.kind, options.phase); + await this.#connection.sessionUpdate({ + sessionId, + update: { + sessionUpdate: "agent_message_chunk", + content: { + type: "text", + text: `\n\n[Fledgling error: ${formatPromptErrorKind(normalized.kind)}. ${normalized.message}]` + } + } + }); + await this.#dependencies.sessionManager.appendEvent({ + ...this.#dependencies.sessionManager.createEventBase(session.id), + type: "session.error", + kind: normalized.kind, + phase: normalized.phase, + message: normalized.message, + recoverable: normalized.recoverable, + assistantTextPersisted: options.assistantTextPersisted, + errorName: normalized.errorName, + errorCode: normalized.errorCode + }); + } + + public async cancel(_params: acp.CancelNotification): Promise { + this.#sessions.get(_params.sessionId)?.pendingPrompt?.abort(); + } + + public closeAllSessions(reason: string): Promise { + return this.#sessionCleanup.closeAll(reason); + } +} + +async function replaySessionHistory(connection: acp.AgentSideConnection, session: SessionState): Promise { + for (const message of session.history) { + if (message.role !== "user" && message.role !== "assistant") { + continue; + } + + const text = messageContentToText(message.content); + if (!text) { + continue; + } + + await connection.sessionUpdate({ + sessionId: session.id, + update: { + sessionUpdate: message.role === "user" ? "user_message_chunk" : "agent_message_chunk", + content: { + type: "text", + text + } + } + }); + } +} diff --git a/packages/agent-core/src/index.ts b/packages/agent-core/src/index.ts new file mode 100644 index 0000000..bf4dd88 --- /dev/null +++ b/packages/agent-core/src/index.ts @@ -0,0 +1,5 @@ +export * from "./agent.js"; +export * from "./interfaces.js"; +export * from "./prompt-content.js"; +export * from "./prompt-errors.js"; +export * from "./session-cleanup.js"; diff --git a/packages/agent-core/src/interfaces.ts b/packages/agent-core/src/interfaces.ts new file mode 100644 index 0000000..4655833 --- /dev/null +++ b/packages/agent-core/src/interfaces.ts @@ -0,0 +1,78 @@ +import type * as acp from "@agentclientprotocol/sdk"; +import type { SessionEvent } from "@fledgling/common"; +import type { CoreMessage, ToolSet } from "ai"; + +export type SessionEventBase = Pick; + +export interface IClosable { + close(): Promise | void; +} + +export interface ISessionManager { + createSessionId(): string; + createEventBase(sessionId: string): SessionEventBase; + appendEvent(event: SessionEvent): Promise; + loadEvents(sessionId: string): Promise; +} + +export type ModelStreamPart = + | { + readonly type: "text-delta"; + readonly text: string; + } + | { + readonly type: "tool-call"; + readonly toolCallId: string; + readonly toolName: string; + readonly input: unknown; + } + | { + readonly type: "tool-result"; + readonly toolCallId: string; + readonly output: unknown; + } + | { + readonly type: "tool-error"; + readonly toolCallId: string; + readonly error: unknown; + }; + +export interface ModelTurnRequest { + readonly messages: CoreMessage[]; + readonly tools: ToolSet; + readonly abortSignal: AbortSignal; +} + +export interface ModelTurnResult { + readonly fullStream: AsyncIterable; +} + +export interface IModelTurnRunner { + runModelTurn(request: ModelTurnRequest): ModelTurnResult; +} + +export interface SessionTools { + readonly clients: IClosable[]; + readonly tools: ToolSet; +} + +export interface IToolProvider { + createSessionTools(request: { + readonly cwd: string | undefined; + readonly mcpServers: acp.McpServer[]; + }): Promise; +} + +export interface IRuntimeLogger { + debug?(record: unknown): void; + warn(record: unknown): void; + error(record: unknown): void; +} + +export interface FledglingAgentDependencies { + readonly toolProvider: IToolProvider; + readonly sessionManager: ISessionManager; + readonly modelTurnRunner: IModelTurnRunner; + readonly logger?: IRuntimeLogger; + readonly debugStream?: boolean; +} diff --git a/packages/agent-core/src/prompt-content.ts b/packages/agent-core/src/prompt-content.ts new file mode 100644 index 0000000..9cbdfce --- /dev/null +++ b/packages/agent-core/src/prompt-content.ts @@ -0,0 +1,92 @@ +import type { PromptRequest } from "@agentclientprotocol/sdk"; +import type { CoreMessage } from "ai"; + +export function messageContentToText(content: CoreMessage["content"]): string { + if (typeof content === "string") { + return content; + } + + if (Array.isArray(content)) { + return content + .map((part) => { + if (typeof part === "string") { + return part; + } + + if (typeof part === "object" && "text" in part && typeof part.text === "string") { + return part.text; + } + + return JSON.stringify(part); + }) + .join("\n"); + } + + return JSON.stringify(content); +} + +export function extractPromptText(params: PromptRequest): string { + const prompt = params.prompt; + + if (typeof prompt === "string") { + return prompt; + } + + if (Array.isArray(prompt)) { + return prompt + .map((part) => { + if (typeof part === "string") { + return part; + } + + if (typeof part === "object" && "text" in part && typeof part.text === "string") { + return part.text; + } + + return JSON.stringify(part); + }) + .join("\n"); + } + + return JSON.stringify(prompt); +} + +export function stringifyToolOutput(output: unknown): string { + if (typeof output === "string") { + return output; + } + + return JSON.stringify(output, null, 2); +} + +export function extractContextHint(output: unknown): unknown { + if (!output || typeof output !== "object") { + return undefined; + } + + if ("structuredContent" in output) { + const structuredContent = (output as { readonly structuredContent?: unknown }).structuredContent; + if (structuredContent && typeof structuredContent === "object" && "contextHint" in structuredContent) { + return (structuredContent as { readonly contextHint?: unknown }).contextHint; + } + } + + if ("_meta" in output) { + const meta = (output as { readonly _meta?: unknown })._meta; + if (meta && typeof meta === "object" && "house.pape.fledgling/context-hint" in meta) { + return (meta as { readonly ["house.pape.fledgling/context-hint"]?: unknown })[ + "house.pape.fledgling/context-hint" + ]; + } + } + + return undefined; +} + +export function toRawObject(value: unknown): Record { + if (value && typeof value === "object" && !Array.isArray(value)) { + return value as Record; + } + + return { value }; +} diff --git a/packages/agent-core/src/prompt-errors.test.ts b/packages/agent-core/src/prompt-errors.test.ts new file mode 100644 index 0000000..81c1d99 --- /dev/null +++ b/packages/agent-core/src/prompt-errors.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from "vitest"; + +import { createPromptRpcError, normalizePromptError } from "./prompt-errors.js"; + +describe("prompt error helpers", () => { + it("sanitizes model errors before durable reporting", () => { + const error = new Error("\u001B[31mfailed\nBearer abc.def.ghi sk-testsecret123"); + const normalized = normalizePromptError(error, "model_start_failed", "model_start"); + + expect(normalized).toMatchObject({ + kind: "model_start_failed", + phase: "model_start", + message: "failed Bearer [redacted] sk-[redacted]", + recoverable: true, + errorName: "Error" + }); + }); + + it("creates sanitized RPC errors", () => { + expect(createPromptRpcError(new Error("token=secret-value"), "model_stream").message).toBe( + "Fledgling model stream failed: token=[redacted]" + ); + }); +}); diff --git a/packages/agent-core/src/prompt-errors.ts b/packages/agent-core/src/prompt-errors.ts new file mode 100644 index 0000000..d7400bd --- /dev/null +++ b/packages/agent-core/src/prompt-errors.ts @@ -0,0 +1,157 @@ +import type { SessionErrorEvent } from "@fledgling/common"; + +export type PromptErrorKind = SessionErrorEvent["kind"]; +export type PromptErrorPhase = SessionErrorEvent["phase"]; + +export interface NormalizedPromptError { + readonly kind: PromptErrorKind; + readonly phase: PromptErrorPhase; + readonly message: string; + readonly recoverable: boolean; + readonly errorName: string | undefined; + readonly errorCode: string | undefined; +} + +export function normalizePromptError( + error: unknown, + kind: PromptErrorKind, + phase: PromptErrorPhase +): NormalizedPromptError { + return { + kind, + phase, + message: sanitizeErrorMessage(extractErrorMessage(error)), + recoverable: true, + errorName: extractErrorName(error), + errorCode: extractErrorCode(error) + }; +} + +export function createPromptRpcError(error: unknown, phase: "model_start" | "model_stream"): Error { + const normalized = normalizePromptError( + error, + phase === "model_start" ? "model_start_failed" : "model_stream_failed", + phase + ); + return new Error(`Fledgling ${formatPromptErrorKind(normalized.kind)}: ${normalized.message}`); +} + +export function formatPromptErrorKind(kind: PromptErrorKind): string { + switch (kind) { + case "model_start_failed": + return "model start failed"; + + case "model_stream_failed": + return "model stream failed"; + + case "prompt_cleanup_failed": + return "prompt cleanup failed"; + } +} + +export function sanitizeErrorMessage(message: string): string { + const withoutControlCharacters = replaceControlCharacters(stripAnsiEscapeSequences(message)); + const normalized = withoutControlCharacters.replace(/\s+/g, " ").trim() || "Unknown error"; + const redacted = normalized + .replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [redacted]") + .replace(/\bsk-[A-Za-z0-9_-]{8,}\b/g, "sk-[redacted]") + .replace(/\b(api[_-]?key|token|secret|password)=\S+/gi, "$1=[redacted]"); + + return redacted.length > 500 ? `${redacted.slice(0, 497)}...` : redacted; +} + +function extractErrorMessage(error: unknown): string { + if (error instanceof Error) { + return error.message; + } + + if (typeof error === "string") { + return error; + } + + if (error && typeof error === "object" && "message" in error) { + const message = (error as { readonly message?: unknown }).message; + if (typeof message === "string") { + return message; + } + } + + return "Unknown error"; +} + +function extractErrorName(error: unknown): string | undefined { + if (error instanceof Error) { + return error.name; + } + + if (error && typeof error === "object" && "name" in error) { + const name = (error as { readonly name?: unknown }).name; + return typeof name === "string" ? sanitizeErrorMessage(name) : undefined; + } + + return undefined; +} + +function extractErrorCode(error: unknown): string | undefined { + if (!error || typeof error !== "object" || !("code" in error)) { + return undefined; + } + + const code = (error as { readonly code?: unknown }).code; + if (typeof code === "string" || typeof code === "number") { + return sanitizeErrorMessage(String(code)); + } + + return undefined; +} + +function stripAnsiEscapeSequences(message: string): string { + let stripped = ""; + for (let index = 0; index < message.length; index++) { + const code = message.charCodeAt(index); + if (code !== 0x1b) { + stripped += message[index]; + continue; + } + + const next = message.charCodeAt(index + 1); + if (next === 0x5b) { + index += 2; + while (index < message.length) { + const sequenceCode = message.charCodeAt(index); + if (sequenceCode >= 0x40 && sequenceCode <= 0x7e) { + break; + } + + index++; + } + continue; + } + + if (next >= 0x40 && next <= 0x5f) { + index++; + } + } + + return stripped; +} + +function replaceControlCharacters(message: string): string { + let replaced = ""; + for (let index = 0; index < message.length; index++) { + const code = message.charCodeAt(index); + replaced += isUnsafeControlCharacter(code) ? " " : message[index]; + } + + return replaced; +} + +function isUnsafeControlCharacter(code: number): boolean { + return ( + code <= 0x08 || + code === 0x0b || + code === 0x0c || + (code >= 0x0e && code <= 0x1f) || + (code >= 0x7f && code <= 0x9f) + ); +} diff --git a/packages/agent-core/src/session-cleanup.ts b/packages/agent-core/src/session-cleanup.ts new file mode 100644 index 0000000..15a4502 --- /dev/null +++ b/packages/agent-core/src/session-cleanup.ts @@ -0,0 +1,106 @@ +import type { IClosable, IRuntimeLogger } from "./interfaces.js"; + +export interface PromptAbortControllerLike { + abort(): void; +} + +export interface SessionCleanupState { + readonly id: string; + readonly clients: readonly IClosable[]; + readonly pendingPrompt: PromptAbortControllerLike | undefined; +} + +export interface McpCloseFailureRecord { + readonly level: "warn"; + readonly event: "mcp_close_failed"; + readonly sessionId: string | undefined; + readonly reason: string; + readonly error: string; +} + +const defaultLogger: IRuntimeLogger = { + warn(record: unknown): void { + console.warn(JSON.stringify(record)); + }, + error(record: unknown): void { + console.error(JSON.stringify(record)); + } +}; + +export class SessionCleanup { + readonly #getSessions: () => Iterable; + readonly #clearSessions: () => void; + readonly #logger: IRuntimeLogger; + #closeAllPromise: Promise | undefined; + + public constructor( + getSessions: () => Iterable, + clearSessions: () => void, + logger: IRuntimeLogger = defaultLogger + ) { + this.#getSessions = getSessions; + this.#clearSessions = clearSessions; + this.#logger = logger; + } + + public closeAll(reason: string): Promise { + this.#closeAllPromise ??= this.#closeAll(reason); + return this.#closeAllPromise; + } + + public async closeSession(session: SessionCleanupState, reason: string): Promise { + session.pendingPrompt?.abort(); + await closeClients(session.clients, reason, session.id, this.#logger); + } + + async #closeAll(reason: string): Promise { + const closeAttempts: Promise[] = []; + + for (const session of this.#getSessions()) { + closeAttempts.push(this.closeSession(session, reason)); + } + + await Promise.allSettled(closeAttempts); + this.#clearSessions(); + } +} + +export async function closeClients( + clients: readonly IClosable[], + reason: string, + sessionId: string | undefined = undefined, + logger: IRuntimeLogger = defaultLogger +): Promise { + await Promise.allSettled( + clients.map(async (client) => { + try { + await client.close(); + } catch (error: unknown) { + logger.warn({ + level: "warn", + event: "mcp_close_failed", + sessionId, + reason, + error: serializeError(error) + } satisfies McpCloseFailureRecord); + } + }) + ); +} + +export function serializeError(error: unknown): string { + if (error instanceof Error) { + return error.message; + } + + if (typeof error === "string") { + return error; + } + + try { + const serialized: unknown = JSON.stringify(error); + return typeof serialized === "string" ? serialized : String(error); + } catch { + return String(error); + } +} diff --git a/packages/agent-core/tsconfig.json b/packages/agent-core/tsconfig.json new file mode 100644 index 0000000..7eedcec --- /dev/null +++ b/packages/agent-core/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../rigs/heft-rig/profiles/default/tsconfig-base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib" + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/agent-core/vitest.config.ts b/packages/agent-core/vitest.config.ts new file mode 100644 index 0000000..1175b9f --- /dev/null +++ b/packages/agent-core/vitest.config.ts @@ -0,0 +1,3 @@ +import { createVitestConfig } from "@fledgling/heft-rig/vitest-base.mjs"; + +export default createVitestConfig(import.meta.url); diff --git a/packages/mcp-workspace-webcontainer/config/rig.json b/packages/mcp-workspace-webcontainer/config/rig.json new file mode 100644 index 0000000..64b84c1 --- /dev/null +++ b/packages/mcp-workspace-webcontainer/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@fledgling/heft-rig" +} diff --git a/packages/mcp-workspace-webcontainer/eslint.config.cjs b/packages/mcp-workspace-webcontainer/eslint.config.cjs new file mode 100644 index 0000000..1c0a01e --- /dev/null +++ b/packages/mcp-workspace-webcontainer/eslint.config.cjs @@ -0,0 +1,3 @@ +const rigConfig = require("@fledgling/heft-rig/profiles/default/config/eslint.config.cjs"); + +module.exports = [...rigConfig]; diff --git a/packages/mcp-workspace-webcontainer/package.json b/packages/mcp-workspace-webcontainer/package.json new file mode 100644 index 0000000..300c625 --- /dev/null +++ b/packages/mcp-workspace-webcontainer/package.json @@ -0,0 +1,29 @@ +{ + "name": "@fledgling/mcp-workspace-webcontainer", + "version": "0.0.0", + "private": true, + "description": "Browser MCP workspace sidecar backed by WebContainer-compatible runtimes.", + "license": "MIT", + "type": "module", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "scripts": { + "build": "heft build --clean", + "_phase:build": "heft run --only build -- --clean", + "test": "vitest run", + "_phase:test": "vitest run --coverage" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.21.0", + "@webcontainer/api": "^1.5.1", + "zod": "^3.25.0" + }, + "devDependencies": { + "@fledgling/heft-rig": "workspace:*", + "@rushstack/heft": "1.2.7", + "@types/node": "^22.0.0", + "@vitest/coverage-v8": "^3.1.1", + "vitest": "^3.1.1", + "typescript": "~5.9.0" + } +} diff --git a/packages/mcp-workspace-webcontainer/src/index.ts b/packages/mcp-workspace-webcontainer/src/index.ts new file mode 100644 index 0000000..a70d94f --- /dev/null +++ b/packages/mcp-workspace-webcontainer/src/index.ts @@ -0,0 +1,2 @@ +export * from "./runtime.js"; +export * from "./sidecar.js"; diff --git a/packages/mcp-workspace-webcontainer/src/runtime.test.ts b/packages/mcp-workspace-webcontainer/src/runtime.test.ts new file mode 100644 index 0000000..4f9b2ba --- /dev/null +++ b/packages/mcp-workspace-webcontainer/src/runtime.test.ts @@ -0,0 +1,107 @@ +import { describe, expect, it, vi } from "vitest"; + +import { WebContainerWorkspaceRuntime } from "./runtime.js"; + +class FakeDirent { + public readonly name: string; + readonly #directory: boolean; + + public constructor(name: string, directory: boolean) { + this.name = name; + this.#directory = directory; + } + + public isDirectory(): boolean { + return this.#directory; + } +} + +describe("WebContainerWorkspaceRuntime", () => { + it("normalizes paths for file and directory operations", async () => { + const reads: string[] = []; + const writes: [string, string][] = []; + const runtime = new WebContainerWorkspaceRuntime({ + fs: { + async readFile(path: string): Promise { + reads.push(path); + return `content:${path}`; + }, + async writeFile(path: string, content: string): Promise { + writes.push([path, content]); + }, + async readdir(path: string): Promise { + expect(path).toBe("."); + return [new FakeDirent("src", true), new FakeDirent("README.md", false)]; + } + } + } as never); + + await expect(runtime.readFile("/src\\index.ts")).resolves.toBe("content:src/index.ts"); + await runtime.writeFile("/README.md", "hello"); + await expect(runtime.listDirectory("/")).resolves.toEqual([ + { type: "directory", name: "src", path: "src" }, + { type: "file", name: "README.md", path: "README.md", sizeBytes: 0 } + ]); + + expect(reads).toEqual(["src/index.ts"]); + expect(writes).toEqual([["README.md", "hello"]]); + }); + + it("searches nested text files", async () => { + const files = new Map([ + ["README.md", "hello\nneedle here"], + ["src/index.ts", "export const value = 'needle';"] + ]); + const directories = new Map([ + [".", [new FakeDirent("src", true), new FakeDirent("README.md", false)]], + ["src", [new FakeDirent("index.ts", false)]] + ]); + const runtime = new WebContainerWorkspaceRuntime({ + fs: { + async readFile(path: string): Promise { + return files.get(path) ?? ""; + }, + async writeFile(): Promise {}, + async readdir(path: string): Promise { + return directories.get(path) ?? []; + } + } + } as never); + + await expect(runtime.searchText("needle", ".")).resolves.toEqual([ + { path: "src/index.ts", line: 1, text: "export const value = 'needle';" }, + { path: "README.md", line: 2, text: "needle here" } + ]); + }); + + it("aborts commands that exceed the timeout", async () => { + const kill = vi.fn(); + const runtime = new WebContainerWorkspaceRuntime({ + fs: { + async readFile(): Promise { + return ""; + }, + async writeFile(): Promise {}, + async readdir(): Promise { + return []; + } + }, + async spawn(): Promise { + return { + output: new ReadableStream(), + exit: new Promise(() => undefined), + kill + }; + } + } as never); + + await expect(runtime.runCommand("sleep 10", ".", 1, 1000)).resolves.toEqual({ + exitCode: 124, + stdout: "", + stderr: "Command timed out", + timedOut: true, + truncated: false + }); + expect(kill).toHaveBeenCalledOnce(); + }); +}); diff --git a/packages/mcp-workspace-webcontainer/src/runtime.ts b/packages/mcp-workspace-webcontainer/src/runtime.ts new file mode 100644 index 0000000..7d67d7f --- /dev/null +++ b/packages/mcp-workspace-webcontainer/src/runtime.ts @@ -0,0 +1,216 @@ +import type { WebContainer } from "@webcontainer/api"; + +export interface WorkspaceFileEntry { + readonly type: "file"; + readonly name: string; + readonly path: string; + readonly sizeBytes: number; +} + +export interface WorkspaceDirectoryEntry { + readonly type: "directory"; + readonly name: string; + readonly path: string; +} + +export type WorkspaceEntry = WorkspaceFileEntry | WorkspaceDirectoryEntry; + +export interface CommandResult { + readonly exitCode: number; + readonly stdout: string; + readonly stderr: string; + readonly timedOut: boolean; + readonly truncated: boolean; +} + +export interface IWorkspaceRuntime { + readFile(path: string): Promise; + writeFile(path: string, content: string): Promise; + listDirectory(path: string): Promise; + searchText(query: string, path: string): Promise; + runCommand(command: string, cwd: string, timeoutMs: number, maxOutputBytes: number): Promise; +} + +export interface SearchMatch { + readonly path: string; + readonly line: number; + readonly text: string; +} + +export class WebContainerWorkspaceRuntime implements IWorkspaceRuntime { + readonly #container: WebContainer; + + public constructor(container: WebContainer) { + this.#container = container; + } + + public async readFile(path: string): Promise { + return this.#container.fs.readFile(normalizeWorkspacePath(path), "utf8"); + } + + public async writeFile(path: string, content: string): Promise { + await this.#container.fs.writeFile(normalizeWorkspacePath(path), content); + } + + public async listDirectory(path: string): Promise { + const root = normalizeWorkspacePath(path); + const entries = await this.#container.fs.readdir(root, { withFileTypes: true }); + return entries.map((entry) => { + const entryPath = joinWorkspacePath(root, entry.name); + if (entry.isDirectory()) { + return { + type: "directory", + name: entry.name, + path: entryPath + }; + } + + return { + type: "file", + name: entry.name, + path: entryPath, + sizeBytes: 0 + }; + }); + } + + public async searchText(query: string, path: string): Promise { + const matches: SearchMatch[] = []; + await this.#searchDirectory(normalizeWorkspacePath(path), query, matches); + return matches; + } + + public async runCommand( + command: string, + cwd: string, + timeoutMs: number, + maxOutputBytes: number + ): Promise { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), timeoutMs); + + try { + const process = await this.#container.spawn("jsh", ["-c", command], { + cwd: normalizeWorkspacePath(cwd) + }); + try { + const stdout = await readProcessOutput(process.output, maxOutputBytes, controller.signal); + const exitCode = await waitForProcessExit(process.exit, controller.signal); + + return { + exitCode, + stdout, + stderr: "", + timedOut: false, + truncated: stdout.length >= maxOutputBytes + }; + } catch (error: unknown) { + if (!controller.signal.aborted) { + throw error; + } + + killProcess(process); + return { + exitCode: 124, + stdout: "", + stderr: "Command timed out", + timedOut: true, + truncated: false + }; + } + } finally { + clearTimeout(timeout); + } + } + + async #searchDirectory(path: string, query: string, matches: SearchMatch[]): Promise { + for (const entry of await this.#container.fs.readdir(path, { withFileTypes: true })) { + const entryPath = joinWorkspacePath(path, entry.name); + if (entry.isDirectory()) { + await this.#searchDirectory(entryPath, query, matches); + continue; + } + + const content = await this.#container.fs.readFile(entryPath, "utf8"); + appendSearchMatches(matches, entryPath, content, query); + } + } +} + +async function readProcessOutput( + output: ReadableStream, + maxOutputBytes: number, + abortSignal: AbortSignal +): Promise { + let text = ""; + const reader = output.getReader(); + + try { + for (;;) { + const result = await readWithAbort(reader, abortSignal); + if (result.done) { + return text.slice(0, maxOutputBytes); + } + + text += result.value; + if (text.length >= maxOutputBytes) { + return text.slice(0, maxOutputBytes); + } + } + } finally { + reader.releaseLock(); + } +} + +function waitForProcessExit(exit: Promise, abortSignal: AbortSignal): Promise { + return raceAbort(exit, abortSignal); +} + +async function readWithAbort( + reader: ReadableStreamDefaultReader, + abortSignal: AbortSignal +): Promise> { + if (abortSignal.aborted) { + throw new DOMException("Command timed out", "AbortError"); + } + + return raceAbort(reader.read(), abortSignal); +} + +function raceAbort(promise: Promise, abortSignal: AbortSignal): Promise { + if (abortSignal.aborted) { + return Promise.reject(new DOMException("Command timed out", "AbortError")); + } + + let removeAbortListener: () => void = () => undefined; + const abortPromise = new Promise((_resolve, reject) => { + const onAbort = (): void => reject(new DOMException("Command timed out", "AbortError")); + abortSignal.addEventListener("abort", onAbort, { once: true }); + removeAbortListener = () => abortSignal.removeEventListener("abort", onAbort); + }); + + return Promise.race([promise, abortPromise]).finally(() => removeAbortListener()); +} + +function killProcess(process: unknown): void { + const candidate = process as { kill?: () => void }; + candidate.kill?.(); +} + +function appendSearchMatches(matches: SearchMatch[], path: string, content: string, query: string): void { + const lines = content.split(/\r?\n/); + for (const [index, line] of lines.entries()) { + if (line.includes(query)) { + matches.push({ path, line: index + 1, text: line }); + } + } +} + +function normalizeWorkspacePath(path: string): string { + const normalized = path.replaceAll("\\", "/").replace(/^\/+/, ""); + return normalized.length === 0 || normalized === "." ? "." : normalized; +} + +function joinWorkspacePath(parent: string, child: string): string { + return parent === "." ? child : `${parent}/${child}`; +} diff --git a/packages/mcp-workspace-webcontainer/src/sidecar.ts b/packages/mcp-workspace-webcontainer/src/sidecar.ts new file mode 100644 index 0000000..31bd197 --- /dev/null +++ b/packages/mcp-workspace-webcontainer/src/sidecar.ts @@ -0,0 +1,203 @@ +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { z } from "zod"; + +import type { IWorkspaceRuntime } from "./runtime.js"; + +interface ContextHint { + readonly kind: "durable_resource" | "command_output"; + readonly identity?: string; + readonly contentHash?: string; + readonly tokenEstimate?: number; + readonly placement: "session_context" | "latest_evidence"; + readonly retention: "discard_after_turn" | "retain_until_changed"; + readonly priority?: number; + readonly routingTags?: string[]; +} + +export interface WebWorkspaceSidecar { + readonly clientTransport: InMemoryTransport; + close(): Promise; +} + +export async function createWebContainerWorkspaceSidecar(runtime: IWorkspaceRuntime): Promise { + const server = new McpServer( + { + name: "fledgling-webcontainer-workspace", + version: "0.0.0" + }, + { + instructions: + "Browser workspace tools run through a WebContainer-backed MCP sidecar. Use read_file before edits; command output is latest evidence." + } + ); + registerWebWorkspaceTools(server, runtime); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + await server.connect(serverTransport); + + return { + clientTransport, + async close(): Promise { + await server.close(); + await clientTransport.close(); + } + }; +} + +export function registerWebWorkspaceTools(server: McpServer, runtime: IWorkspaceRuntime): void { + server.registerTool( + "workspace.read_file", + { + title: "Read file", + description: "Read a UTF-8 text file in the browser workspace.", + inputSchema: { + path: z.string() + }, + annotations: { + readOnlyHint: true + } + }, + async ({ path }) => { + const content = await runtime.readFile(path); + const contextHint = fileContextHint(path, content); + return toolResult(`Read ${path}.\n\n${content}`, { path, content, contextHint }, contextHint); + } + ); + + server.registerTool( + "workspace.write_file", + { + title: "Write file", + description: "Write a UTF-8 text file in the browser workspace.", + inputSchema: { + path: z.string(), + content: z.string() + }, + annotations: { + destructiveHint: true + } + }, + async ({ path, content }) => { + await runtime.writeFile(path, content); + const contextHint = fileContextHint(path, content); + return toolResult(`Wrote ${path}.`, { path, contentHash: hashText(content), contextHint }, contextHint); + } + ); + + server.registerTool( + "workspace.list_directory", + { + title: "List directory", + description: "List files and directories in the browser workspace.", + inputSchema: { + path: z.string().default(".") + }, + annotations: { + readOnlyHint: true + } + }, + async ({ path }) => { + const entries = await runtime.listDirectory(path); + const text = entries.map((entry) => `${entry.type}\t${entry.path}`).join("\n"); + return toolResult(text.length > 0 ? text : "Directory is empty.", { path, entries }, undefined); + } + ); + + server.registerTool( + "workspace.search_text", + { + title: "Search text", + description: "Search text across files in the browser workspace.", + inputSchema: { + query: z.string(), + path: z.string().default(".") + }, + annotations: { + readOnlyHint: true + } + }, + async ({ query, path }) => { + const matches = await runtime.searchText(query, path); + const text = matches.map((match) => `${match.path}:${match.line}: ${match.text}`).join("\n"); + return toolResult(text.length > 0 ? text : "No matches.", { query, path, matches }, undefined); + } + ); + + server.registerTool( + "workspace.run_command", + { + title: "Run command", + description: "Run a shell command in the browser workspace sidecar.", + inputSchema: { + command: z.string(), + cwd: z.string().default("."), + timeoutMs: z.number().int().positive().default(30_000), + maxOutputBytes: z.number().int().positive().default(20_000) + }, + annotations: { + destructiveHint: true + } + }, + async ({ command, cwd, timeoutMs, maxOutputBytes }) => { + const result = await runtime.runCommand(command, cwd, timeoutMs, maxOutputBytes); + const output = `exitCode: ${result.exitCode}\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`; + const contextHint = commandContextHint(command, output); + return toolResult(output, { command, cwd, ...result, contextHint }, contextHint); + } + ); +} + +function toolResult( + text: string, + structuredContent: Record, + contextHint: ContextHint | undefined +): CallToolResult { + const meta = contextHint ? { "house.pape.fledgling/context-hint": contextHint } : undefined; + return { + content: [{ type: "text", text }], + structuredContent, + _meta: meta + }; +} + +function fileContextHint(path: string, content: string): ContextHint { + return { + kind: "durable_resource", + identity: `webcontainer://${path}`, + contentHash: hashText(content), + tokenEstimate: estimateTokens(content), + placement: "session_context", + retention: "retain_until_changed", + priority: 60, + routingTags: ["file", path] + }; +} + +function commandContextHint(command: string, output: string): ContextHint { + return { + kind: "command_output", + identity: `webcontainer-command://${hashText(command)}`, + contentHash: hashText(output), + tokenEstimate: estimateTokens(output), + placement: "latest_evidence", + retention: "discard_after_turn", + priority: 90, + routingTags: ["command", "shell"] + }; +} + +function estimateTokens(text: string): number { + return Math.ceil(text.length / 4); +} + +function hashText(text: string): string { + let hash = 0x811c9dc5; + for (let index = 0; index < text.length; index++) { + hash ^= text.charCodeAt(index); + hash = Math.imul(hash, 0x01000193); + } + + return `fnv1a32:${(hash >>> 0).toString(16).padStart(8, "0")}`; +} diff --git a/packages/mcp-workspace-webcontainer/tsconfig.json b/packages/mcp-workspace-webcontainer/tsconfig.json new file mode 100644 index 0000000..83592d4 --- /dev/null +++ b/packages/mcp-workspace-webcontainer/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../rigs/heft-rig/profiles/default/tsconfig-base.json", + "compilerOptions": { + "lib": ["es2022", "dom"], + "rootDir": "src", + "outDir": "lib" + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/mcp-workspace-webcontainer/vitest.config.ts b/packages/mcp-workspace-webcontainer/vitest.config.ts new file mode 100644 index 0000000..99f4bce --- /dev/null +++ b/packages/mcp-workspace-webcontainer/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node" + } +}); diff --git a/packages/session-file-system/config/rig.json b/packages/session-file-system/config/rig.json new file mode 100644 index 0000000..64b84c1 --- /dev/null +++ b/packages/session-file-system/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@fledgling/heft-rig" +} diff --git a/packages/session-file-system/eslint.config.cjs b/packages/session-file-system/eslint.config.cjs new file mode 100644 index 0000000..1c0a01e --- /dev/null +++ b/packages/session-file-system/eslint.config.cjs @@ -0,0 +1,3 @@ +const rigConfig = require("@fledgling/heft-rig/profiles/default/config/eslint.config.cjs"); + +module.exports = [...rigConfig]; diff --git a/packages/session-file-system/package.json b/packages/session-file-system/package.json new file mode 100644 index 0000000..987a0c3 --- /dev/null +++ b/packages/session-file-system/package.json @@ -0,0 +1,28 @@ +{ + "name": "@fledgling/session-file-system", + "version": "0.0.0", + "private": true, + "description": "Filesystem-backed JSONL Fledgling session manager.", + "license": "MIT", + "type": "module", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "scripts": { + "build": "heft build --clean", + "_phase:build": "heft run --only build -- --clean", + "test": "vitest run", + "_phase:test": "vitest run --coverage" + }, + "dependencies": { + "@fledgling/agent-core": "workspace:*", + "@fledgling/common": "workspace:*" + }, + "devDependencies": { + "@fledgling/heft-rig": "workspace:*", + "@rushstack/heft": "1.2.7", + "@types/node": "^22.0.0", + "@vitest/coverage-v8": "^3.1.1", + "vitest": "^3.1.1", + "typescript": "~5.9.0" + } +} diff --git a/packages/session-file-system/src/index.ts b/packages/session-file-system/src/index.ts new file mode 100644 index 0000000..5f16de6 --- /dev/null +++ b/packages/session-file-system/src/index.ts @@ -0,0 +1,73 @@ +import { randomUUID } from "node:crypto"; +import { appendFile, mkdir, readFile } from "node:fs/promises"; +import path from "node:path"; + +import type { ISessionManager, SessionEventBase } from "@fledgling/agent-core"; +import type { SessionEvent } from "@fledgling/common"; + +export class FileSystemSessionManager implements ISessionManager { + readonly #root: string; + readonly #sessionFile: string | undefined; + + public constructor( + root: string = defaultSessionStoreRoot(), + sessionFile: string | undefined = process.env.FLEDGLING_SESSION_FILE + ) { + this.#root = root; + this.#sessionFile = sessionFile ? path.resolve(sessionFile) : undefined; + } + + public createSessionId(): string { + return randomUUID(); + } + + public createEventBase(sessionId: string): SessionEventBase { + return { + eventId: randomUUID(), + sessionId, + timestamp: new Date().toISOString() + }; + } + + public async appendEvent(event: SessionEvent): Promise { + const sessionPath = this.#sessionPath(event.sessionId); + await mkdir(path.dirname(sessionPath), { recursive: true }); + await appendFile(sessionPath, `${JSON.stringify(event)}\n`, "utf8"); + } + + public async loadEvents(sessionId: string): Promise { + let raw: string; + try { + raw = await readFile(this.#sessionPath(sessionId), "utf8"); + } catch (error: unknown) { + throw new Error(`Unknown ACP session: ${sessionId}`, { cause: error }); + } + + const events = raw + .split(/\r?\n/) + .filter((line) => line.trim().length > 0) + .map((line) => JSON.parse(line) as SessionEvent); + + if (events.some((event) => event.sessionId !== sessionId)) { + throw new Error(`Stored ACP session contains events for another session: ${sessionId}`); + } + + return events; + } + + #sessionPath(sessionId: string): string { + if (this.#sessionFile) { + return this.#sessionFile; + } + + return path.join(this.#root, `${sessionIdToFileName(sessionId)}.jsonl`); + } +} + +export function defaultSessionStoreRoot(): string { + return path.resolve(process.env.FLEDGLING_SESSION_DIR ?? path.join(process.cwd(), ".fledgling", "sessions")); +} + +function sessionIdToFileName(sessionId: string): string { + return sessionId.replaceAll(/[^A-Za-z0-9._-]/g, "_"); +} diff --git a/packages/session-file-system/src/session-file-system.test.ts b/packages/session-file-system/src/session-file-system.test.ts new file mode 100644 index 0000000..149fb78 --- /dev/null +++ b/packages/session-file-system/src/session-file-system.test.ts @@ -0,0 +1,43 @@ +import { mkdtemp, readFile, rm } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; + +import { afterEach, describe, expect, it } from "vitest"; + +import { FileSystemSessionManager } from "./index.js"; + +let tempDir: string | undefined; + +describe("FileSystemSessionManager", () => { + afterEach(async () => { + const cleanupDir = tempDir; + tempDir = undefined; + if (cleanupDir) { + await rm(cleanupDir, { recursive: true, force: true }); + } + }); + + it("appends and loads JSONL session events", async () => { + tempDir = await mkdtemp(path.join(os.tmpdir(), "fledgling-session-file-system-test-")); + const sessionFile = path.join(tempDir, "session.jsonl"); + const manager = new FileSystemSessionManager(tempDir, sessionFile); + const sessionId = manager.createSessionId(); + const event = { + ...manager.createEventBase(sessionId), + type: "message.user", + text: "hello" + } as const; + + await manager.appendEvent(event); + + await expect(manager.loadEvents(sessionId)).resolves.toEqual([event]); + expect(await readFile(sessionFile, "utf8")).toContain('"type":"message.user"'); + }); + + it("rejects unknown sessions", async () => { + tempDir = await mkdtemp(path.join(os.tmpdir(), "fledgling-session-file-system-test-")); + const manager = new FileSystemSessionManager(tempDir); + + await expect(manager.loadEvents("missing")).rejects.toThrow("Unknown ACP session: missing"); + }); +}); diff --git a/packages/session-file-system/tsconfig.json b/packages/session-file-system/tsconfig.json new file mode 100644 index 0000000..7eedcec --- /dev/null +++ b/packages/session-file-system/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../rigs/heft-rig/profiles/default/tsconfig-base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib" + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/session-file-system/vitest.config.ts b/packages/session-file-system/vitest.config.ts new file mode 100644 index 0000000..1175b9f --- /dev/null +++ b/packages/session-file-system/vitest.config.ts @@ -0,0 +1,3 @@ +import { createVitestConfig } from "@fledgling/heft-rig/vitest-base.mjs"; + +export default createVitestConfig(import.meta.url); diff --git a/packages/session-local-storage/config/rig.json b/packages/session-local-storage/config/rig.json new file mode 100644 index 0000000..64b84c1 --- /dev/null +++ b/packages/session-local-storage/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@fledgling/heft-rig" +} diff --git a/packages/session-local-storage/eslint.config.cjs b/packages/session-local-storage/eslint.config.cjs new file mode 100644 index 0000000..1c0a01e --- /dev/null +++ b/packages/session-local-storage/eslint.config.cjs @@ -0,0 +1,3 @@ +const rigConfig = require("@fledgling/heft-rig/profiles/default/config/eslint.config.cjs"); + +module.exports = [...rigConfig]; diff --git a/packages/session-local-storage/package.json b/packages/session-local-storage/package.json new file mode 100644 index 0000000..2baf34b --- /dev/null +++ b/packages/session-local-storage/package.json @@ -0,0 +1,28 @@ +{ + "name": "@fledgling/session-local-storage", + "version": "0.0.0", + "private": true, + "description": "localStorage-backed Fledgling session manager.", + "license": "MIT", + "type": "module", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "scripts": { + "build": "heft build --clean", + "_phase:build": "heft run --only build -- --clean", + "test": "vitest run", + "_phase:test": "vitest run --coverage" + }, + "dependencies": { + "@fledgling/agent-core": "workspace:*", + "@fledgling/common": "workspace:*" + }, + "devDependencies": { + "@fledgling/heft-rig": "workspace:*", + "@rushstack/heft": "1.2.7", + "@types/node": "^22.0.0", + "@vitest/coverage-v8": "^3.1.1", + "vitest": "^3.1.1", + "typescript": "~5.9.0" + } +} diff --git a/packages/session-local-storage/src/index.ts b/packages/session-local-storage/src/index.ts new file mode 100644 index 0000000..1766203 --- /dev/null +++ b/packages/session-local-storage/src/index.ts @@ -0,0 +1,96 @@ +import type { ISessionManager, SessionEventBase } from "@fledgling/agent-core"; +import type { SessionEvent } from "@fledgling/common"; + +export interface LocalStorageSessionManagerOptions { + readonly storage?: Storage; + readonly keyPrefix?: string; +} + +export class LocalStorageSessionManager implements ISessionManager { + readonly #storage: Storage; + readonly #keyPrefix: string; + + public constructor(options: LocalStorageSessionManagerOptions = {}) { + this.#storage = options.storage ?? getDefaultStorage(); + this.#keyPrefix = options.keyPrefix ?? "fledgling:sessions:"; + } + + public createSessionId(): string { + return createId(); + } + + public createEventBase(sessionId: string): SessionEventBase { + return { + eventId: createId(), + sessionId, + timestamp: new Date().toISOString() + }; + } + + public async appendEvent(event: SessionEvent): Promise { + const events = this.#loadExistingEvents(event.sessionId); + events.push(event); + this.#storage.setItem(this.#sessionKey(event.sessionId), JSON.stringify(events)); + } + + public async loadEvents(sessionId: string): Promise { + const raw = this.#storage.getItem(this.#sessionKey(sessionId)); + if (!raw) { + throw new Error(`Unknown ACP session: ${sessionId}`); + } + + const events = JSON.parse(raw) as SessionEvent[]; + if (events.some((event) => event.sessionId !== sessionId)) { + throw new Error(`Stored ACP session contains events for another session: ${sessionId}`); + } + + return events; + } + + #loadExistingEvents(sessionId: string): SessionEvent[] { + const raw = this.#storage.getItem(this.#sessionKey(sessionId)); + return raw ? (JSON.parse(raw) as SessionEvent[]) : []; + } + + #sessionKey(sessionId: string): string { + return `${this.#keyPrefix}${sessionId.replaceAll(/[^A-Za-z0-9._-]/g, "_")}`; + } +} + +function getDefaultStorage(): Storage { + try { + const storage = (globalThis as { readonly localStorage?: Storage }).localStorage; + if (storage !== undefined) { + return storage; + } + } catch { + // Fall through to the actionable error below. + } + + throw new Error( + "LocalStorageSessionManager requires browser localStorage; pass an explicit Storage implementation in restricted or non-window environments." + ); +} + +function createId(): string { + const crypto = (globalThis as { readonly crypto?: Crypto }).crypto; + if (!crypto) { + throw new Error("Web Crypto is required to create ACP session IDs"); + } + + if (typeof crypto.randomUUID === "function") { + return crypto.randomUUID(); + } + + if (typeof crypto.getRandomValues === "function") { + const bytes = crypto.getRandomValues(new Uint8Array(16)); + bytes[6] = (bytes[6] % 16) + 64; + bytes[8] = (bytes[8] % 64) + 128; + const hex = [...bytes].map((byte) => byte.toString(16).padStart(2, "0")); + return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex + .slice(8, 10) + .join("")}-${hex.slice(10, 16).join("")}`; + } + + throw new Error("Web Crypto is required to create ACP session IDs"); +} diff --git a/packages/session-local-storage/src/session-local-storage.test.ts b/packages/session-local-storage/src/session-local-storage.test.ts new file mode 100644 index 0000000..26e01ed --- /dev/null +++ b/packages/session-local-storage/src/session-local-storage.test.ts @@ -0,0 +1,93 @@ +import { describe, expect, it, vi } from "vitest"; + +import { LocalStorageSessionManager } from "./index.js"; + +class MemoryStorage implements Storage { + readonly #items: Map = new Map(); + + public get length(): number { + return this.#items.size; + } + + public clear(): void { + this.#items.clear(); + } + + // eslint-disable-next-line @rushstack/no-new-null -- Storage.getItem is a DOM API. + public getItem(key: string): string | null { + return this.#items.get(key) ?? null; + } + + // eslint-disable-next-line @rushstack/no-new-null -- Storage.key is a DOM API. + public key(index: number): string | null { + return this.keys()[index] ?? null; + } + + public removeItem(key: string): void { + this.#items.delete(key); + } + + public setItem(key: string, value: string): void { + this.#items.set(key, value); + } + + public keys(): string[] { + return [...this.#items.keys()]; + } +} + +describe("LocalStorageSessionManager", () => { + it("appends and loads session events with key prefix isolation", async () => { + const storage = new MemoryStorage(); + const manager = new LocalStorageSessionManager({ storage, keyPrefix: "test:" }); + const sessionId = manager.createSessionId(); + const event = { + ...manager.createEventBase(sessionId), + type: "message.user", + text: "hello" + } as const; + + await manager.appendEvent(event); + + await expect(manager.loadEvents(sessionId)).resolves.toEqual([event]); + expect(storage.keys()).toEqual([`test:${sessionId}`]); + }); + + it("rejects unknown sessions", async () => { + const manager = new LocalStorageSessionManager({ storage: new MemoryStorage() }); + + await expect(manager.loadEvents("missing")).rejects.toThrow("Unknown ACP session: missing"); + }); + + it("throws an actionable error when default localStorage is unavailable", () => { + const descriptor = Object.getOwnPropertyDescriptor(globalThis, "localStorage"); + vi.stubGlobal("localStorage", undefined); + + try { + expect(() => new LocalStorageSessionManager()).toThrow( + "LocalStorageSessionManager requires browser localStorage" + ); + } finally { + vi.unstubAllGlobals(); + if (descriptor) { + Object.defineProperty(globalThis, "localStorage", descriptor); + } + } + }); + + it("throws an actionable error when Web Crypto is unavailable", () => { + const descriptor = Object.getOwnPropertyDescriptor(globalThis, "crypto"); + vi.stubGlobal("crypto", undefined); + + try { + expect(() => + new LocalStorageSessionManager({ storage: new MemoryStorage() }).createSessionId() + ).toThrow("Web Crypto is required to create ACP session IDs"); + } finally { + vi.unstubAllGlobals(); + if (descriptor) { + Object.defineProperty(globalThis, "crypto", descriptor); + } + } + }); +}); diff --git a/packages/session-local-storage/tsconfig.json b/packages/session-local-storage/tsconfig.json new file mode 100644 index 0000000..83592d4 --- /dev/null +++ b/packages/session-local-storage/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../rigs/heft-rig/profiles/default/tsconfig-base.json", + "compilerOptions": { + "lib": ["es2022", "dom"], + "rootDir": "src", + "outDir": "lib" + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/session-local-storage/vitest.config.ts b/packages/session-local-storage/vitest.config.ts new file mode 100644 index 0000000..1175b9f --- /dev/null +++ b/packages/session-local-storage/vitest.config.ts @@ -0,0 +1,3 @@ +import { createVitestConfig } from "@fledgling/heft-rig/vitest-base.mjs"; + +export default createVitestConfig(import.meta.url); diff --git a/packages/session-memory/config/rig.json b/packages/session-memory/config/rig.json new file mode 100644 index 0000000..64b84c1 --- /dev/null +++ b/packages/session-memory/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@fledgling/heft-rig" +} diff --git a/packages/session-memory/eslint.config.cjs b/packages/session-memory/eslint.config.cjs new file mode 100644 index 0000000..1c0a01e --- /dev/null +++ b/packages/session-memory/eslint.config.cjs @@ -0,0 +1,3 @@ +const rigConfig = require("@fledgling/heft-rig/profiles/default/config/eslint.config.cjs"); + +module.exports = [...rigConfig]; diff --git a/packages/session-memory/package.json b/packages/session-memory/package.json new file mode 100644 index 0000000..0cea896 --- /dev/null +++ b/packages/session-memory/package.json @@ -0,0 +1,28 @@ +{ + "name": "@fledgling/session-memory", + "version": "0.0.0", + "private": true, + "description": "In-memory Fledgling session manager.", + "license": "MIT", + "type": "module", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "scripts": { + "build": "heft build --clean", + "_phase:build": "heft run --only build -- --clean", + "test": "vitest run", + "_phase:test": "vitest run --coverage" + }, + "dependencies": { + "@fledgling/agent-core": "workspace:*", + "@fledgling/common": "workspace:*" + }, + "devDependencies": { + "@fledgling/heft-rig": "workspace:*", + "@rushstack/heft": "1.2.7", + "@types/node": "^22.0.0", + "@vitest/coverage-v8": "^3.1.1", + "vitest": "^3.1.1", + "typescript": "~5.9.0" + } +} diff --git a/packages/session-memory/src/index.ts b/packages/session-memory/src/index.ts new file mode 100644 index 0000000..21bf0c4 --- /dev/null +++ b/packages/session-memory/src/index.ts @@ -0,0 +1,61 @@ +import type { ISessionManager, SessionEventBase } from "@fledgling/agent-core"; +import type { SessionEvent } from "@fledgling/common"; + +interface WebCryptoLike { + randomUUID?: () => string; + getRandomValues?: (array: T) => T; +} + +export class MemorySessionManager implements ISessionManager { + readonly #eventsBySessionId: Map = new Map(); + + public createSessionId(): string { + return createId(); + } + + public createEventBase(sessionId: string): SessionEventBase { + return { + eventId: createId(), + sessionId, + timestamp: new Date().toISOString() + }; + } + + public async appendEvent(event: SessionEvent): Promise { + const events = this.#eventsBySessionId.get(event.sessionId) ?? []; + events.push(event); + this.#eventsBySessionId.set(event.sessionId, events); + } + + public async loadEvents(sessionId: string): Promise { + const events = this.#eventsBySessionId.get(sessionId); + if (!events) { + throw new Error(`Unknown ACP session: ${sessionId}`); + } + + return [...events]; + } +} + +function createId(): string { + const crypto = (globalThis as { readonly crypto?: WebCryptoLike }).crypto; + if (!crypto) { + throw new Error("Web Crypto is required to create ACP session IDs"); + } + + if (typeof crypto.randomUUID === "function") { + return crypto.randomUUID(); + } + + if (typeof crypto.getRandomValues === "function") { + const bytes = crypto.getRandomValues(new Uint8Array(16)); + bytes[6] = (bytes[6] % 16) + 64; + bytes[8] = (bytes[8] % 64) + 128; + const hex = [...bytes].map((byte) => byte.toString(16).padStart(2, "0")); + return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex + .slice(8, 10) + .join("")}-${hex.slice(10, 16).join("")}`; + } + + throw new Error("Web Crypto is required to create ACP session IDs"); +} diff --git a/packages/session-memory/src/session-memory.test.ts b/packages/session-memory/src/session-memory.test.ts new file mode 100644 index 0000000..fedc069 --- /dev/null +++ b/packages/session-memory/src/session-memory.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it, vi } from "vitest"; + +import { MemorySessionManager } from "./index.js"; + +describe("MemorySessionManager", () => { + it("appends and loads session events", async () => { + const manager = new MemorySessionManager(); + const sessionId = manager.createSessionId(); + const event = { + ...manager.createEventBase(sessionId), + type: "message.user", + text: "hello" + } as const; + + await manager.appendEvent(event); + + await expect(manager.loadEvents(sessionId)).resolves.toEqual([event]); + }); + + it("rejects unknown sessions", async () => { + const manager = new MemorySessionManager(); + + await expect(manager.loadEvents("missing")).rejects.toThrow("Unknown ACP session: missing"); + }); + + it("throws an actionable error when Web Crypto is unavailable", () => { + const descriptor = Object.getOwnPropertyDescriptor(globalThis, "crypto"); + vi.stubGlobal("crypto", undefined); + + try { + expect(() => new MemorySessionManager().createSessionId()).toThrow( + "Web Crypto is required to create ACP session IDs" + ); + } finally { + vi.unstubAllGlobals(); + if (descriptor) { + Object.defineProperty(globalThis, "crypto", descriptor); + } + } + }); +}); diff --git a/packages/session-memory/tsconfig.json b/packages/session-memory/tsconfig.json new file mode 100644 index 0000000..7eedcec --- /dev/null +++ b/packages/session-memory/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../rigs/heft-rig/profiles/default/tsconfig-base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib" + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/session-memory/vitest.config.ts b/packages/session-memory/vitest.config.ts new file mode 100644 index 0000000..1175b9f --- /dev/null +++ b/packages/session-memory/vitest.config.ts @@ -0,0 +1,3 @@ +import { createVitestConfig } from "@fledgling/heft-rig/vitest-base.mjs"; + +export default createVitestConfig(import.meta.url); diff --git a/packages/tools-mcp-node/config/rig.json b/packages/tools-mcp-node/config/rig.json new file mode 100644 index 0000000..64b84c1 --- /dev/null +++ b/packages/tools-mcp-node/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@fledgling/heft-rig" +} diff --git a/packages/tools-mcp-node/eslint.config.cjs b/packages/tools-mcp-node/eslint.config.cjs new file mode 100644 index 0000000..1c0a01e --- /dev/null +++ b/packages/tools-mcp-node/eslint.config.cjs @@ -0,0 +1,3 @@ +const rigConfig = require("@fledgling/heft-rig/profiles/default/config/eslint.config.cjs"); + +module.exports = [...rigConfig]; diff --git a/packages/tools-mcp-node/package.json b/packages/tools-mcp-node/package.json new file mode 100644 index 0000000..97695aa --- /dev/null +++ b/packages/tools-mcp-node/package.json @@ -0,0 +1,31 @@ +{ + "name": "@fledgling/tools-mcp-node", + "version": "0.0.0", + "private": true, + "description": "Node MCP tool provider for Fledgling.", + "license": "MIT", + "type": "module", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "scripts": { + "build": "heft build --clean", + "_phase:build": "heft run --only build -- --clean", + "test": "vitest run", + "_phase:test": "vitest run --coverage" + }, + "dependencies": { + "@agentclientprotocol/sdk": "^0.4.3", + "@ai-sdk/mcp": "0.0.18", + "@fledgling/agent-core": "workspace:*", + "@fledgling/mcp-workspace": "workspace:*", + "ai": "^5.0.0" + }, + "devDependencies": { + "@fledgling/heft-rig": "workspace:*", + "@rushstack/heft": "1.2.7", + "@types/node": "^22.0.0", + "@vitest/coverage-v8": "^3.1.1", + "vitest": "^3.1.1", + "typescript": "~5.9.0" + } +} diff --git a/packages/tools-mcp-node/src/config.test.ts b/packages/tools-mcp-node/src/config.test.ts new file mode 100644 index 0000000..de53bdc --- /dev/null +++ b/packages/tools-mcp-node/src/config.test.ts @@ -0,0 +1,97 @@ +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; + +import { afterEach, describe, expect, it } from "vitest"; + +import { loadConfig } from "./config.js"; +import { NodeMcpToolProvider } from "./index.js"; + +let tempDir: string | undefined; + +describe("loadMcpServersFromConfig", () => { + const originalConfig = process.env.FLEDGLING_CONFIG; + + afterEach(async () => { + restoreEnv("FLEDGLING_CONFIG", originalConfig); + + const cleanupDir = tempDir; + tempDir = undefined; + if (cleanupDir) { + await rm(cleanupDir, { recursive: true, force: true }); + } + }); + + it("loads MCP servers from Fledgling config files", async () => { + tempDir = await mkdtemp(path.join(os.tmpdir(), "fledgling-tools-mcp-node-test-")); + const configFile = path.join(tempDir, "config.json"); + process.env.FLEDGLING_CONFIG = configFile; + await writeFile( + configFile, + JSON.stringify({ + mcpServers: { + workspace: { + type: "stdio", + command: "node", + args: ["server.js"], + env: { + A: "B" + } + } + } + }), + "utf8" + ); + + expect(await loadConfig()).toEqual({ + mcpServers: { + workspace: { + type: "stdio", + command: "node", + args: ["server.js"], + env: { + A: "B" + } + } + } + }); + }); + + it("rejects ACP server names that collide after sanitization", async () => { + const provider = new NodeMcpToolProvider(Promise.resolve({})); + + await expect( + provider.createSessionTools({ + cwd: undefined, + mcpServers: [ + { name: "one.two", command: "node", args: [], env: [] }, + { name: "one_two", command: "node", args: [], env: [] } + ] + }) + ).rejects.toThrow("MCP server name collision after sanitization: one_two"); + }); + + it("rejects configured server names that collide after sanitization", async () => { + const provider = new NodeMcpToolProvider( + Promise.resolve({ + mcpServers: { + "one.two": { type: "stdio", command: "node" }, + one_two: { type: "stdio", command: "node" } + } + }) + ); + + await expect(provider.createSessionTools({ cwd: undefined, mcpServers: [] })).rejects.toThrow( + "Configured MCP server name collision after sanitization: one_two" + ); + }); +}); + +function restoreEnv(name: string, value: string | undefined): void { + if (value === undefined) { + delete process.env[name]; + return; + } + + process.env[name] = value; +} diff --git a/packages/tools-mcp-node/src/config.ts b/packages/tools-mcp-node/src/config.ts new file mode 100644 index 0000000..fce24c2 --- /dev/null +++ b/packages/tools-mcp-node/src/config.ts @@ -0,0 +1,44 @@ +import { existsSync } from "node:fs"; +import { readFile } from "node:fs/promises"; +import path from "node:path"; + +export interface FledglingConfig { + readonly mcpServers?: Record; +} + +export type McpOrigin = "acp_client" | "config" | "first_party"; + +export interface ResolvedMcpServer { + readonly name: string; + readonly origin: McpOrigin; + readonly config: McpServerConfig; +} + +export type McpServerConfig = + | { + readonly type: "firstPartyWorkspace"; + } + | { + readonly type: "stdio"; + readonly command: string; + readonly args?: string[]; + readonly cwd?: string; + readonly env?: Record; + } + | { + readonly type: "http" | "sse"; + readonly url: string; + readonly headers?: Record; + }; + +export async function loadConfig(): Promise { + const configPath = process.env.FLEDGLING_CONFIG + ? path.resolve(process.env.FLEDGLING_CONFIG) + : path.resolve(process.cwd(), "fledgling.config.json"); + + if (!existsSync(configPath)) { + return {}; + } + + return JSON.parse(await readFile(configPath, "utf8")) as FledglingConfig; +} diff --git a/packages/tools-mcp-node/src/index.ts b/packages/tools-mcp-node/src/index.ts new file mode 100644 index 0000000..566dfe8 --- /dev/null +++ b/packages/tools-mcp-node/src/index.ts @@ -0,0 +1,182 @@ +import { fileURLToPath } from "node:url"; + +import * as acp from "@agentclientprotocol/sdk"; +import { + experimental_createMCPClient as createMCPClient, + type experimental_MCPClient as MCPClient +} from "@ai-sdk/mcp"; +import { Experimental_StdioMCPTransport } from "@ai-sdk/mcp/mcp-stdio"; +import { closeClients, type IToolProvider, type SessionTools } from "@fledgling/agent-core"; +import type { ToolSet } from "ai"; + +import { loadConfig, type FledglingConfig, type McpServerConfig, type ResolvedMcpServer } from "./config.js"; + +export class NodeMcpToolProvider implements IToolProvider { + readonly #configPromise: Promise; + + public constructor(configPromise: Promise = loadConfig()) { + this.#configPromise = configPromise; + } + + public async createSessionTools(request: { + readonly cwd: string | undefined; + readonly mcpServers: acp.McpServer[]; + }): Promise { + const entries = await this.#resolveMcpServers(request.mcpServers); + const clients: MCPClient[] = []; + const tools: ToolSet = {}; + + try { + for (const entry of entries) { + const client = await createMCPClient({ + name: `fledgling-${entry.origin}-${entry.name}`, + transport: await createTransport(entry.config, request.cwd) + }); + clients.push(client); + + const serverTools = await client.tools(); + for (const [toolName, tool] of Object.entries(serverTools)) { + const exposedToolName = toExposedToolName(entry.name, toolName); + if (exposedToolName in tools) { + throw new Error(`MCP tool name collision after sanitization: ${exposedToolName}`); + } + + tools[exposedToolName] = tool as unknown as ToolSet[string]; + } + } + } catch (error: unknown) { + await closeClients(clients, "session-tools-setup-failed"); + throw error; + } + + return { clients, tools }; + } + + async #resolveMcpServers(clientMcpServers: acp.McpServer[]): Promise { + const resolved = new Map(); + + for (const server of clientMcpServers) { + const name = sanitizeToolName(server.name); + if (resolved.has(name)) { + throw new Error(`MCP server name collision after sanitization: ${name}`); + } + + resolved.set(name, { + name, + origin: "acp_client", + config: fromAcpMcpServer(server) + }); + } + + const config = await this.#configPromise; + for (const [serverName, serverConfig] of Object.entries(config.mcpServers ?? {})) { + const name = sanitizeToolName(serverName); + const existing = resolved.get(name); + if (existing?.origin === "acp_client") { + console.error( + JSON.stringify({ + level: "warn", + message: "Skipping configured MCP server because ACP client provided the same server name.", + serverName: name + }) + ); + continue; + } + + if (existing) { + throw new Error(`Configured MCP server name collision after sanitization: ${name}`); + } + + resolved.set(name, { + name, + origin: serverConfig.type === "firstPartyWorkspace" ? "first_party" : "config", + config: serverConfig + }); + } + + return [...resolved.values()]; + } +} + +function fromAcpMcpServer(server: acp.McpServer): McpServerConfig { + if ("type" in server) { + return { + type: server.type, + url: server.url, + headers: Object.fromEntries(server.headers.map((header: acp.HttpHeader) => [header.name, header.value])) + }; + } + + return { + type: "stdio", + command: server.command, + args: server.args, + env: Object.fromEntries(server.env.map((entry: acp.EnvVariable) => [entry.name, entry.value])) + }; +} + +async function createTransport( + config: McpServerConfig, + sessionCwd: string | undefined +): Promise[0]["transport"]> { + switch (config.type) { + case "firstPartyWorkspace": { + return new Experimental_StdioMCPTransport({ + command: process.execPath, + args: [fileURLToPath(import.meta.resolve("@fledgling/mcp-workspace"))], + cwd: sessionCwd ?? process.cwd(), + env: { + FLEDGLING_WORKSPACE_ROOT: sessionCwd ?? process.cwd() + }, + stderr: "inherit" + }); + } + + case "stdio": { + return new Experimental_StdioMCPTransport({ + command: config.command, + args: config.args, + cwd: expandConfigString(config.cwd, sessionCwd), + env: expandEnv(config.env, sessionCwd), + stderr: "inherit" + }); + } + + case "http": + case "sse": + return { + type: config.type, + url: config.url, + headers: config.headers + }; + } +} + +function expandEnv( + env: Record | undefined, + sessionCwd: string | undefined +): Record | undefined { + if (!env) { + return undefined; + } + + return Object.fromEntries( + Object.entries(env).map(([key, value]) => [key, expandConfigString(value, sessionCwd) ?? value]) + ); +} + +function expandConfigString(value: string | undefined, sessionCwd: string | undefined): string | undefined { + return value?.replaceAll("${sessionCwd}", sessionCwd ?? process.cwd()); +} + +function sanitizeToolName(toolName: string): string { + return toolName.replaceAll(/[^A-Za-z0-9_-]/g, "_").slice(0, 64); +} + +function toExposedToolName(serverName: string, toolName: string): string { + const localToolName = toolName.startsWith(`${serverName}.`) + ? toolName.slice(serverName.length + 1) + : toolName; + + return sanitizeToolName(`${serverName}_${localToolName}`); +} diff --git a/packages/tools-mcp-node/tsconfig.json b/packages/tools-mcp-node/tsconfig.json new file mode 100644 index 0000000..7eedcec --- /dev/null +++ b/packages/tools-mcp-node/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../rigs/heft-rig/profiles/default/tsconfig-base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib" + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/tools-mcp-node/vitest.config.ts b/packages/tools-mcp-node/vitest.config.ts new file mode 100644 index 0000000..1175b9f --- /dev/null +++ b/packages/tools-mcp-node/vitest.config.ts @@ -0,0 +1,3 @@ +import { createVitestConfig } from "@fledgling/heft-rig/vitest-base.mjs"; + +export default createVitestConfig(import.meta.url); diff --git a/packages/web-agent/config/rig.json b/packages/web-agent/config/rig.json new file mode 100644 index 0000000..64b84c1 --- /dev/null +++ b/packages/web-agent/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@fledgling/heft-rig" +} diff --git a/packages/web-agent/eslint.config.cjs b/packages/web-agent/eslint.config.cjs new file mode 100644 index 0000000..1c0a01e --- /dev/null +++ b/packages/web-agent/eslint.config.cjs @@ -0,0 +1,3 @@ +const rigConfig = require("@fledgling/heft-rig/profiles/default/config/eslint.config.cjs"); + +module.exports = [...rigConfig]; diff --git a/packages/web-agent/package.json b/packages/web-agent/package.json new file mode 100644 index 0000000..aa20f79 --- /dev/null +++ b/packages/web-agent/package.json @@ -0,0 +1,36 @@ +{ + "name": "@fledgling/web-agent", + "version": "0.0.0", + "private": true, + "description": "Browser composition package for the Fledgling ACP agent loop.", + "license": "MIT", + "type": "module", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "scripts": { + "build": "heft build --clean", + "_phase:build": "heft run --only build -- --clean", + "test": "vitest run", + "_phase:test": "vitest run --coverage", + "bundle": "webpack --config webpack.config.cjs", + "_phase:bundle": "webpack --config webpack.config.cjs" + }, + "dependencies": { + "@agentclientprotocol/sdk": "^0.4.3", + "@ai-sdk/mcp": "0.0.18", + "@fledgling/agent-core": "workspace:*", + "@fledgling/mcp-workspace-webcontainer": "workspace:*", + "@fledgling/session-local-storage": "workspace:*", + "ai": "^5.0.0" + }, + "devDependencies": { + "@fledgling/heft-rig": "workspace:*", + "@rushstack/heft": "1.2.7", + "@types/node": "^22.0.0", + "@vitest/coverage-v8": "^3.1.1", + "typescript": "~5.9.0", + "vitest": "^3.1.1", + "webpack": "^5.99.9", + "webpack-cli": "^6.0.1" + } +} diff --git a/packages/web-agent/src/dependencies.ts b/packages/web-agent/src/dependencies.ts new file mode 100644 index 0000000..74cc0ee --- /dev/null +++ b/packages/web-agent/src/dependencies.ts @@ -0,0 +1,34 @@ +import type * as acp from "@agentclientprotocol/sdk"; +import { + type FledglingAgentDependencies, + type IModelTurnRunner, + FledglingAgent +} from "@fledgling/agent-core"; +import { + createWebContainerWorkspaceSidecar, + type IWorkspaceRuntime +} from "@fledgling/mcp-workspace-webcontainer"; +import { LocalStorageSessionManager } from "@fledgling/session-local-storage"; + +import { WebMcpToolProvider } from "./tool-provider.js"; + +export interface WebAgentOptions { + readonly workspaceRuntime: IWorkspaceRuntime; + readonly modelTurnRunner: IModelTurnRunner; + readonly storage?: Storage; +} + +export function createWebAgentDependencies(options: WebAgentOptions): FledglingAgentDependencies { + return { + sessionManager: new LocalStorageSessionManager({ storage: options.storage }), + toolProvider: new WebMcpToolProvider({ + createSidecar: () => createWebContainerWorkspaceSidecar(options.workspaceRuntime) + }), + modelTurnRunner: options.modelTurnRunner, + logger: console + }; +} + +export function createWebAgent(connection: acp.AgentSideConnection, options: WebAgentOptions): FledglingAgent { + return new FledglingAgent(connection, createWebAgentDependencies(options)); +} diff --git a/packages/web-agent/src/index.ts b/packages/web-agent/src/index.ts new file mode 100644 index 0000000..a746f10 --- /dev/null +++ b/packages/web-agent/src/index.ts @@ -0,0 +1,4 @@ +export * from "@fledgling/agent-core"; +export * from "@fledgling/mcp-workspace-webcontainer"; +export * from "./dependencies.js"; +export * from "./tool-provider.js"; diff --git a/packages/web-agent/src/tool-provider.ts b/packages/web-agent/src/tool-provider.ts new file mode 100644 index 0000000..9ff546f --- /dev/null +++ b/packages/web-agent/src/tool-provider.ts @@ -0,0 +1,61 @@ +import { + experimental_createMCPClient as createMCPClient, + type experimental_MCPClient as MCPClient, + type MCPTransport +} from "@ai-sdk/mcp"; +import type { IToolProvider, SessionTools } from "@fledgling/agent-core"; +import type * as acp from "@agentclientprotocol/sdk"; +import type { WebWorkspaceSidecar } from "@fledgling/mcp-workspace-webcontainer"; +import type { ToolSet } from "ai"; + +export interface WebMcpToolProviderOptions { + readonly createSidecar: () => Promise; +} + +class WebMcpClientHandle { + readonly #client: MCPClient; + readonly #sidecar: WebWorkspaceSidecar; + + public constructor(client: MCPClient, sidecar: WebWorkspaceSidecar) { + this.#client = client; + this.#sidecar = sidecar; + } + + public async close(): Promise { + await this.#client.close(); + await this.#sidecar.close(); + } +} + +export class WebMcpToolProvider implements IToolProvider { + readonly #createSidecar: () => Promise; + + public constructor(options: WebMcpToolProviderOptions) { + this.#createSidecar = options.createSidecar; + } + + public async createSessionTools(_request: { + readonly cwd: string | undefined; + readonly mcpServers: acp.McpServer[]; + }): Promise { + const sidecar = await this.#createSidecar(); + let client: MCPClient | undefined; + + try { + client = await createMCPClient({ + name: "fledgling-web-agent", + transport: sidecar.clientTransport as MCPTransport + }); + const tools: ToolSet = await client.tools(); + + return { + clients: [new WebMcpClientHandle(client, sidecar)], + tools + }; + } catch (error: unknown) { + await client?.close(); + await sidecar.close(); + throw error; + } + } +} diff --git a/packages/web-agent/src/web-agent.test.ts b/packages/web-agent/src/web-agent.test.ts new file mode 100644 index 0000000..32617ed --- /dev/null +++ b/packages/web-agent/src/web-agent.test.ts @@ -0,0 +1,111 @@ +import type { IModelTurnRunner, ModelTurnRequest } from "@fledgling/agent-core"; +import { describe, expect, it, vi } from "vitest"; + +import type { IWorkspaceRuntime } from "@fledgling/mcp-workspace-webcontainer"; + +import { createWebAgent } from "./index.js"; + +class MemoryStorage implements Storage { + readonly #items: Map = new Map(); + + public get length(): number { + return this.#items.size; + } + + public clear(): void { + this.#items.clear(); + } + + // eslint-disable-next-line @rushstack/no-new-null -- Storage.getItem is a DOM API. + public getItem(key: string): string | null { + return this.#items.get(key) ?? null; + } + + // eslint-disable-next-line @rushstack/no-new-null -- Storage.key is a DOM API. + public key(index: number): string | null { + return [...this.#items.keys()][index] ?? null; + } + + public removeItem(key: string): void { + this.#items.delete(key); + } + + public setItem(key: string, value: string): void { + this.#items.set(key, value); + } +} + +describe("createWebAgent", () => { + it("runs initialize, newSession, and prompt through the browser entrypoint", async () => { + const sessionUpdates: unknown[] = []; + const runModelTurn = vi.fn(() => ({ + fullStream: (async function* () { + yield { type: "text-delta", text: "pong" } as const; + })() + })); + const agent = createWebAgent( + { + async sessionUpdate(params: unknown): Promise { + sessionUpdates.push(params); + } + } as never, + { + workspaceRuntime: createFakeWorkspaceRuntime(), + modelTurnRunner: { runModelTurn }, + storage: new MemoryStorage() + } + ); + + const initializeResponse = await agent.initialize({ protocolVersion: 1, clientCapabilities: {} } as never); + expect(typeof initializeResponse.protocolVersion).toBe("number"); + const session = await agent.newSession({ cwd: ".", mcpServers: [] }); + await expect(agent.prompt({ sessionId: session.sessionId, prompt: [{ type: "text", text: "ping" }] })).resolves.toEqual({ + stopReason: "end_turn" + }); + + expect(runModelTurn).toHaveBeenCalledOnce(); + const [[modelRequest]] = runModelTurn.mock.calls as [[ModelTurnRequest]]; + expect(Object.keys(modelRequest.tools)).toEqual( + expect.arrayContaining([ + "workspace.read_file", + "workspace.write_file", + "workspace.list_directory", + "workspace.search_text", + "workspace.run_command" + ]) + ); + expect(sessionUpdates).toEqual([ + { + sessionId: session.sessionId, + update: { + sessionUpdate: "agent_message_chunk", + content: { type: "text", text: "pong" } + } + } + ]); + }); +}); + +function createFakeWorkspaceRuntime(): IWorkspaceRuntime { + return { + async readFile(path: string): Promise { + return `content:${path}`; + }, + async writeFile(): Promise {}, + async listDirectory(): Promise<[]> { + return []; + }, + async searchText(): Promise<[]> { + return []; + }, + async runCommand() { + return { + exitCode: 0, + stdout: "", + stderr: "", + timedOut: false, + truncated: false + }; + } + }; +} diff --git a/packages/web-agent/src/webpack-entry.ts b/packages/web-agent/src/webpack-entry.ts new file mode 100644 index 0000000..8e893de --- /dev/null +++ b/packages/web-agent/src/webpack-entry.ts @@ -0,0 +1 @@ +export * from "./index.js"; diff --git a/packages/web-agent/tsconfig.json b/packages/web-agent/tsconfig.json new file mode 100644 index 0000000..83592d4 --- /dev/null +++ b/packages/web-agent/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../rigs/heft-rig/profiles/default/tsconfig-base.json", + "compilerOptions": { + "lib": ["es2022", "dom"], + "rootDir": "src", + "outDir": "lib" + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/web-agent/vitest.config.ts b/packages/web-agent/vitest.config.ts new file mode 100644 index 0000000..99f4bce --- /dev/null +++ b/packages/web-agent/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node" + } +}); diff --git a/packages/web-agent/webpack.config.cjs b/packages/web-agent/webpack.config.cjs new file mode 100644 index 0000000..7dc4c66 --- /dev/null +++ b/packages/web-agent/webpack.config.cjs @@ -0,0 +1,43 @@ +const { builtinModules } = require("node:module"); +const path = require("node:path"); +const webpack = require("webpack"); + +const nodeBuiltins = new Set( + builtinModules.flatMap((moduleName) => [moduleName, `node:${moduleName}`]) +); + +class FailOnNodeBuiltinPlugin { + apply(compiler) { + compiler.hooks.normalModuleFactory.tap("FailOnNodeBuiltinPlugin", (factory) => { + factory.hooks.beforeResolve.tap("FailOnNodeBuiltinPlugin", (request) => { + if (request && nodeBuiltins.has(request.request)) { + throw new Error(`Browser bundle imported Node builtin '${request.request}' from ${request.context}`); + } + }); + }); + } +} + +module.exports = { + mode: "production", + target: "web", + entry: path.resolve(__dirname, "lib/webpack-entry.js"), + output: { + path: path.resolve(__dirname, "dist"), + filename: "fledgling-web-agent.js", + library: { + name: "FledglingWebAgent", + type: "umd" + }, + clean: true + }, + resolve: { + extensions: [".js"] + }, + plugins: [ + new FailOnNodeBuiltinPlugin(), + new webpack.DefinePlugin({ + "process.env.NODE_ENV": JSON.stringify("production") + }) + ] +}; diff --git a/rush.json b/rush.json index 4672aa0..8f963f4 100644 --- a/rush.json +++ b/rush.json @@ -14,6 +14,10 @@ "packageName": "@fledgling/acp-agent", "projectFolder": "packages/acp-agent" }, + { + "packageName": "@fledgling/agent-core", + "projectFolder": "packages/agent-core" + }, { "packageName": "@fledgling/common", "projectFolder": "packages/common" @@ -26,6 +30,30 @@ "packageName": "@fledgling/session-log", "projectFolder": "packages/session-log" }, + { + "packageName": "@fledgling/session-memory", + "projectFolder": "packages/session-memory" + }, + { + "packageName": "@fledgling/session-local-storage", + "projectFolder": "packages/session-local-storage" + }, + { + "packageName": "@fledgling/session-file-system", + "projectFolder": "packages/session-file-system" + }, + { + "packageName": "@fledgling/tools-mcp-node", + "projectFolder": "packages/tools-mcp-node" + }, + { + "packageName": "@fledgling/web-agent", + "projectFolder": "packages/web-agent" + }, + { + "packageName": "@fledgling/mcp-workspace-webcontainer", + "projectFolder": "packages/mcp-workspace-webcontainer" + }, { "packageName": "@fledgling/acp-host-log", "projectFolder": "packages/acp-host-log"