diff --git a/agents/yjs-fs-agents/package.json b/agents/yjs-fs-agents/package.json deleted file mode 100644 index 8dc04ab..0000000 --- a/agents/yjs-fs-agents/package.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "@humanlayer/yjs-fs-agents", - "module": "src/index.ts", - "type": "module", - "private": true, - "scripts": { - "typecheck": "tsgo --noEmit" - }, - "devDependencies": { - "@ai-sdk/anthropic": "catalog:", - "@ai-sdk/provider": "catalog:", - "@humanlayer/agentlayer-core": "workspace:*", - "@humanlayer/yjs-fs": "workspace:*", - "@types/bun": "latest", - "ai": "catalog:", - "zod": "catalog:" - }, - "peerDependencies": { - "typescript": "^5" - }, - "exports": { - ".": "./src/index.ts" - }, - "dependencies": {} -} diff --git a/agents/yjs-fs-agents/src/index.ts b/agents/yjs-fs-agents/src/index.ts deleted file mode 100644 index 336ce12..0000000 --- a/agents/yjs-fs-agents/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export {} diff --git a/bun.lock b/bun.lock index 02edbbd..3653022 100644 --- a/bun.lock +++ b/bun.lock @@ -62,19 +62,31 @@ "typescript": "^5", }, }, - "agents/yjs-fs-agents": { - "name": "@humanlayer/yjs-fs-agents", - "devDependencies": { - "@ai-sdk/anthropic": "catalog:", - "@ai-sdk/provider": "catalog:", - "@humanlayer/agentlayer-core": "workspace:*", - "@humanlayer/yjs-fs": "workspace:*", - "@types/bun": "latest", - "ai": "catalog:", - "zod": "catalog:", + "examples/yjs-rich-markdown-demo": { + "name": "@humanlayer/yjs-rich-markdown-demo", + "version": "0.0.1", + "dependencies": { + "@durable-streams/server": "^0.3.1", + "@durable-streams/y-durable-streams": "^0.2.4", + "@humanlayer/yjs-rich-markdown": "workspace:*", + "@tanstack/react-router": "^1.93.0", + "@tiptap/extension-collaboration": "3.22.5", + "@tiptap/extension-collaboration-caret": "3.22.5", + "@tiptap/pm": "3.22.5", + "@tiptap/react": "3.22.5", + "@tiptap/starter-kit": "3.22.5", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "y-protocols": "1.0.7", + "yjs": "^13.6.30", }, - "peerDependencies": { - "typescript": "^5", + "devDependencies": { + "@tanstack/router-plugin": "^1.93.0", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "typescript": "^5.6.3", + "vite": "^6.0.0", }, }, "packages/agentlayer-core": { @@ -202,6 +214,19 @@ "typescript": "^5", }, }, + "packages/quickjs-exec": { + "name": "@humanlayer/quickjs-exec", + "version": "0.0.0", + "dependencies": { + "quickjs-emscripten": "^0.31.0", + }, + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, "packages/yjs-fs": { "name": "@humanlayer/yjs-fs", "version": "0.0.0", @@ -222,7 +247,7 @@ "lib0": "^0.2.0", "typescript": "^5", "y-protocols": "1.0.7", - "yjs": "13.6.30", + "yjs": "^13.6.30", }, }, "packages/yjs-fs-editor": { @@ -244,7 +269,7 @@ "react-dom": "^18.3.1", "y-monaco": "^0.1.6", "y-protocols": "1.0.7", - "yjs": "13.6.30", + "yjs": "^13.6.30", }, "devDependencies": { "@tanstack/router-plugin": "^1.93.0", @@ -267,12 +292,42 @@ "react": "^18.3.1", "typescript": "catalog:", "y-protocols": "1.0.7", - "yjs": "13.6.30", + "yjs": "^13.6.30", }, "peerDependencies": { "react": "^18.3.1", "y-protocols": "1.0.7", - "yjs": "13.6.30", + "yjs": "^13.6.30", + }, + }, + "packages/yjs-rich-markdown": { + "name": "@humanlayer/yjs-rich-markdown", + "version": "0.0.0", + "dependencies": { + "@humanlayer/quickjs-exec": "workspace:*", + "@tiptap/core": "3.22.5", + "@tiptap/extension-collaboration": "3.22.5", + "@tiptap/markdown": "3.22.5", + "@tiptap/pm": "3.22.5", + "@tiptap/starter-kit": "3.22.5", + "ai": "catalog:", + "dedent": "1.7.2", + "y-prosemirror": "1.3.7", + "zod": "catalog:", + }, + "devDependencies": { + "@ai-sdk/anthropic": "catalog:", + "@durable-streams/server": "^0.3.1", + "@durable-streams/y-durable-streams": "^0.2.4", + "@types/bun": "latest", + }, + "peerDependencies": { + "@tiptap/core": "3.22.5", + "@tiptap/extension-collaboration": "3.22.5", + "@tiptap/starter-kit": "3.22.5", + "typescript": "^5", + "y-protocols": "1.0.7", + "yjs": "^13.6.30", }, }, }, @@ -478,6 +533,12 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + "@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="], + + "@floating-ui/dom": ["@floating-ui/dom@1.7.6", "", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="], + + "@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="], + "@gar/promise-retry": ["@gar/promise-retry@1.0.3", "", {}, "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA=="], "@grpc/grpc-js": ["@grpc/grpc-js@1.14.3", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA=="], @@ -504,14 +565,18 @@ "@humanlayer/docs-agent": ["@humanlayer/docs-agent@workspace:agents/docs-agent"], - "@humanlayer/yjs-fs": ["@humanlayer/yjs-fs@workspace:packages/yjs-fs"], + "@humanlayer/quickjs-exec": ["@humanlayer/quickjs-exec@workspace:packages/quickjs-exec"], - "@humanlayer/yjs-fs-agents": ["@humanlayer/yjs-fs-agents@workspace:agents/yjs-fs-agents"], + "@humanlayer/yjs-fs": ["@humanlayer/yjs-fs@workspace:packages/yjs-fs"], "@humanlayer/yjs-fs-editor": ["@humanlayer/yjs-fs-editor@workspace:packages/yjs-fs-editor"], "@humanlayer/yjs-fs-react": ["@humanlayer/yjs-fs-react@workspace:packages/yjs-fs-react"], + "@humanlayer/yjs-rich-markdown": ["@humanlayer/yjs-rich-markdown@workspace:packages/yjs-rich-markdown"], + + "@humanlayer/yjs-rich-markdown-demo": ["@humanlayer/yjs-rich-markdown-demo@workspace:examples/yjs-rich-markdown-demo"], + "@iconify-json/simple-icons": ["@iconify-json/simple-icons@1.2.78", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-I3lkNp0Qu7q2iZWkdcf/I2hqGhzK6qxdILh9T7XqowQrnpmG/BayDsiCf6PktDoWlW0U971xA5g+panm+NFrfQ=="], "@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="], @@ -524,15 +589,15 @@ "@isaacs/string-locale-compare": ["@isaacs/string-locale-compare@1.1.0", "", {}, "sha512-SQ7Kzhh9+D+ZW9MA0zkYv3VXhIDNx+LzM6EJ+/65I3QY+enU6Itte7E5XX7EWrqLW2FN4n06GWzBnPoC3th2aQ=="], - "@jitl/quickjs-ffi-types": ["@jitl/quickjs-ffi-types@0.32.0", "", {}, "sha512-v9T+GQpmk43VDJ7d72sf0Nexhk+ArvtUihW27dy7lqAl0zBObFKtSBBIm5RBjwIhE8VwsPPm9PNuvPvNqLWUEg=="], + "@jitl/quickjs-ffi-types": ["@jitl/quickjs-ffi-types@0.31.0", "", {}, "sha512-1yrgvXlmXH2oNj3eFTrkwacGJbmM0crwipA3ohCrjv52gBeDaD7PsTvFYinlAnqU8iPME3LGP437yk05a2oejw=="], - "@jitl/quickjs-wasmfile-debug-asyncify": ["@jitl/quickjs-wasmfile-debug-asyncify@0.32.0", "", { "dependencies": { "@jitl/quickjs-ffi-types": "0.32.0" } }, "sha512-EX8zbXwGqCgAE764M+qvkHtyXDi/FUoMBea0JnES7vCM3P7a2+EOZOjGv85wtZ2sJhI1oJ+nekmqpOODFDY+hw=="], + "@jitl/quickjs-wasmfile-debug-asyncify": ["@jitl/quickjs-wasmfile-debug-asyncify@0.31.0", "", { "dependencies": { "@jitl/quickjs-ffi-types": "0.31.0" } }, "sha512-YkdzQdr1uaftFhgEnTRjTTZHk2SFZdpWO7XhOmRVbi6CEVsH9g5oNF8Ta1q3OuSJHRwwT8YsuR1YzEiEIJEk6w=="], - "@jitl/quickjs-wasmfile-debug-sync": ["@jitl/quickjs-wasmfile-debug-sync@0.32.0", "", { "dependencies": { "@jitl/quickjs-ffi-types": "0.32.0" } }, "sha512-LeYWrPGC1uNCTBWvibo3ZLJj0CSVNYUXvJpXMCmuQ5Sap2cCACc3uvGvYV4homHHBAzfw5akoTqMMS4YFRtw+Q=="], + "@jitl/quickjs-wasmfile-debug-sync": ["@jitl/quickjs-wasmfile-debug-sync@0.31.0", "", { "dependencies": { "@jitl/quickjs-ffi-types": "0.31.0" } }, "sha512-8XvloaaWBONqcHXYs5tWOjdhQVxzULilIfB2hvZfS6S+fI4m2+lFiwQy7xeP8ExHmiZ7D8gZGChNkdLgjGfknw=="], - "@jitl/quickjs-wasmfile-release-asyncify": ["@jitl/quickjs-wasmfile-release-asyncify@0.32.0", "", { "dependencies": { "@jitl/quickjs-ffi-types": "0.32.0" } }, "sha512-3oSwPfja12ICz4aIblB58cuY8JlEq5Txt8Cut4VLo+LH47QN+mzCnSgnbB03hWzg1LBcc+VyyI9UOag7a1NF+Q=="], + "@jitl/quickjs-wasmfile-release-asyncify": ["@jitl/quickjs-wasmfile-release-asyncify@0.31.0", "", { "dependencies": { "@jitl/quickjs-ffi-types": "0.31.0" } }, "sha512-uz0BbQYTxNsFkvkurd7vk2dOg57ElTBLCuvNtRl4rgrtbC++NIndD5qv2+AXb6yXDD3Uy1O2PCwmoaH0eXgEOg=="], - "@jitl/quickjs-wasmfile-release-sync": ["@jitl/quickjs-wasmfile-release-sync@0.32.0", "", { "dependencies": { "@jitl/quickjs-ffi-types": "0.32.0" } }, "sha512-BKNDI/TPBfGlLNGYpLrhcDGXmIk4xHm4MRAisOBnOzpXVn9HZWsfmMAc9WMBrAHjvvds6HOikKeaOBKdPdpVrg=="], + "@jitl/quickjs-wasmfile-release-sync": ["@jitl/quickjs-wasmfile-release-sync@0.31.0", "", { "dependencies": { "@jitl/quickjs-ffi-types": "0.31.0" } }, "sha512-hYduecOByj9AsAfsJhZh5nA6exokmuFC8cls39+lYmTCGY51bgjJJJwReEu7Ff7vBWaQCL6TeDdVlnp2WYz0jw=="], "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], @@ -792,7 +857,7 @@ "@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.161.7", "", { "bin": { "intent": "bin/intent.js" } }, "sha512-olW33+Cn+bsCsZKPwEGhlkqS6w3M2slFv11JIobdnCFKMLG97oAI2kWKdx5/zsywTL8flpnoIgaZZPlQTFYhdQ=="], - "@tiptap/core": ["@tiptap/core@2.27.2", "", { "peerDependencies": { "@tiptap/pm": "^2.7.0" } }, "sha512-ABL1N6eoxzDzC1bYvkMbvyexHacszsKdVPYqhl5GwHLOvpZcv9VE9QaKwDILTyz5voCA0lGcAAXZp+qnXOk5lQ=="], + "@tiptap/core": ["@tiptap/core@3.22.5", "", { "peerDependencies": { "@tiptap/pm": "3.22.5" } }, "sha512-L1lhWz6ujGny8LduTJ7MBWYhzigwOvfUJUrJ7IzOJSuy3+OAzisdGDD1GV7LEO/hU0Hr2Mkm1wajRIHExvS9HQ=="], "@tiptap/extension-blockquote": ["@tiptap/extension-blockquote@2.27.2", "", { "peerDependencies": { "@tiptap/core": "^2.7.0" } }, "sha512-oIGZgiAeA4tG3YxbTDfrmENL4/CIwGuP3THtHsNhwRqwsl9SfMk58Ucopi2GXTQSdYXpRJ0ahE6nPqB5D6j/Zw=="], @@ -808,6 +873,8 @@ "@tiptap/extension-collaboration": ["@tiptap/extension-collaboration@2.27.2", "", { "peerDependencies": { "@tiptap/core": "^2.7.0", "@tiptap/pm": "^2.7.0", "y-prosemirror": "^1.2.11" } }, "sha512-Y61ItHxQ1uc/Ir27mBQRI/wY9JkOui194V+awNv+1YHeaKArTjC2cdSvNzj9+h8JIh5MyfvslSf8hBa7t7PzAg=="], + "@tiptap/extension-collaboration-caret": ["@tiptap/extension-collaboration-caret@3.22.5", "", { "peerDependencies": { "@tiptap/core": "3.22.5", "@tiptap/pm": "3.22.5", "@tiptap/y-tiptap": "^3.0.2" } }, "sha512-+2kOIb661vW16nh5ecSNw4vcRFdove1thkNkKJGGcwaTyORJ0Hc4Pds+tSc3dc9GxZw0bvwwAvX7bPdflnls6A=="], + "@tiptap/extension-collaboration-cursor": ["@tiptap/extension-collaboration-cursor@2.26.2", "", { "peerDependencies": { "@tiptap/core": "^2.7.0", "y-prosemirror": "^1.2.11" } }, "sha512-FdRb27mZ5Kr18hN6cbfBj1e9F0DOoHB1Gv3IYeic+g4h1C9BjDVMN0+JRBQc+4lamNA8TsHO0oKWRwaPe4sSlA=="], "@tiptap/extension-document": ["@tiptap/extension-document@2.27.2", "", { "peerDependencies": { "@tiptap/core": "^2.7.0" } }, "sha512-CFhAYsPnyYnosDC4639sCJnBUnYH4Cat9qH5NZWHVvdgtDwu8GZgZn2eSzaKSYXWH1vJ9DSlCK+7UyC3SNXIBA=="], @@ -828,8 +895,14 @@ "@tiptap/extension-italic": ["@tiptap/extension-italic@2.27.2", "", { "peerDependencies": { "@tiptap/core": "^2.7.0" } }, "sha512-1OFsw2SZqfaqx5Fa5v90iNlPRcqyt+lVSjBwTDzuPxTPFY4Q0mL89mKgkq2gVHYNCiaRkXvFLDxaSvBWbmthgg=="], + "@tiptap/extension-link": ["@tiptap/extension-link@3.22.5", "", { "dependencies": { "linkifyjs": "^4.3.2" }, "peerDependencies": { "@tiptap/core": "3.22.5", "@tiptap/pm": "3.22.5" } }, "sha512-d671MvF3GPKoS2OVxjIlQ7hIE7MS3hREdR+d4cvnnoiLLD+ZJ6KgDnxmWqF0a1s4qxLWK2KxKRSOIfYGE31QWQ=="], + + "@tiptap/extension-list": ["@tiptap/extension-list@3.22.5", "", { "peerDependencies": { "@tiptap/core": "3.22.5", "@tiptap/pm": "3.22.5" } }, "sha512-cVO3ZHCgxAWZ4zrFSs81FO2nyCk1wb2EHkpLpW98FzbJLkN9rDkazhW99P3HRWy/CvUldOT+8ecI1YrQtBojMg=="], + "@tiptap/extension-list-item": ["@tiptap/extension-list-item@2.27.2", "", { "peerDependencies": { "@tiptap/core": "^2.7.0" } }, "sha512-eJNee7IEGXMnmygM5SdMGDC8m/lMWmwNGf9fPCK6xk0NxuQRgmZHL6uApKcdH6gyNcRPHCqvTTkhEP7pbny/fg=="], + "@tiptap/extension-list-keymap": ["@tiptap/extension-list-keymap@3.22.5", "", { "peerDependencies": { "@tiptap/extension-list": "3.22.5" } }, "sha512-cGUnxJ0y515e1bVHNjUmbx7oWHoEon59w6BA5N2KwV9iW2mZZchlTX4yxJSOX+ixeVRChsa7YwC3Z1jUZ6AMEg=="], + "@tiptap/extension-ordered-list": ["@tiptap/extension-ordered-list@2.27.2", "", { "peerDependencies": { "@tiptap/core": "^2.7.0" } }, "sha512-M7A4tLGJcLPYdLC4CI2Gwl8LOrENQW59u3cMVa+KkwG1hzSJyPsbDpa1DI6oXPC2WtYiTf22zrbq3gVvH+KA2w=="], "@tiptap/extension-paragraph": ["@tiptap/extension-paragraph@2.27.2", "", { "peerDependencies": { "@tiptap/core": "^2.7.0" } }, "sha512-elYVn2wHJJ+zB9LESENWOAfI4TNT0jqEN34sMA/hCtA4im1ZG2DdLHwkHIshj/c4H0dzQhmsS/YmNC5Vbqab/A=="], @@ -840,12 +913,20 @@ "@tiptap/extension-text-style": ["@tiptap/extension-text-style@2.27.2", "", { "peerDependencies": { "@tiptap/core": "^2.7.0" } }, "sha512-Omk+uxjJLyEY69KStpCw5fA9asvV+MGcAX2HOxyISDFoLaL49TMrNjhGAuz09P1L1b0KGXo4ml7Q3v/Lfy4WPA=="], - "@tiptap/pm": ["@tiptap/pm@2.27.2", "", { "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-collab": "^1.3.1", "prosemirror-commands": "^1.6.2", "prosemirror-dropcursor": "^1.8.1", "prosemirror-gapcursor": "^1.3.2", "prosemirror-history": "^1.4.1", "prosemirror-inputrules": "^1.4.0", "prosemirror-keymap": "^1.2.2", "prosemirror-markdown": "^1.13.1", "prosemirror-menu": "^1.2.4", "prosemirror-model": "^1.23.0", "prosemirror-schema-basic": "^1.2.3", "prosemirror-schema-list": "^1.4.1", "prosemirror-state": "^1.4.3", "prosemirror-tables": "^1.6.4", "prosemirror-trailing-node": "^3.0.0", "prosemirror-transform": "^1.10.2", "prosemirror-view": "^1.37.0" } }, "sha512-kaEg7BfiJPDQMKbjVIzEPO3wlcA+pZb2tlcK9gPrdDnEFaec2QTF1sXz2ak2IIb2curvnIrQ4yrfHgLlVA72wA=="], + "@tiptap/extension-underline": ["@tiptap/extension-underline@3.22.5", "", { "peerDependencies": { "@tiptap/core": "3.22.5" } }, "sha512-9ut09rJD0iEbS6sk7yd2j6IwuFDLTNmDEGTDLodvqAfi+bq7ddsTDv0YviXoZaA9sdHAdTEVr2ITy2m6WK5jpA=="], + + "@tiptap/extensions": ["@tiptap/extensions@3.22.5", "", { "peerDependencies": { "@tiptap/core": "3.22.5", "@tiptap/pm": "3.22.5" } }, "sha512-Ifg4MzKCj3uRqe3ieTwYnomu2y4p7EXr2avVSKZYfh12i2dyWe2Gkn1KuZDREANVE+gHqFlQjJRYzhJFwzSCrg=="], + + "@tiptap/markdown": ["@tiptap/markdown@3.22.5", "", { "dependencies": { "marked": "^17.0.1" }, "peerDependencies": { "@tiptap/core": "3.22.5", "@tiptap/pm": "3.22.5" } }, "sha512-lLuAySaY5EYNYLe7e4507B9yQMAEDJdOKy0g85UNFV8giorYLQx56aV2O94Qb9gv3egs5inkwVRNfeJzOWAwig=="], + + "@tiptap/pm": ["@tiptap/pm@3.22.5", "", { "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-commands": "^1.6.2", "prosemirror-dropcursor": "^1.8.1", "prosemirror-gapcursor": "^1.3.2", "prosemirror-history": "^1.4.1", "prosemirror-keymap": "^1.2.2", "prosemirror-model": "^1.24.1", "prosemirror-schema-list": "^1.5.0", "prosemirror-state": "^1.4.3", "prosemirror-tables": "^1.6.4", "prosemirror-transform": "^1.10.2", "prosemirror-view": "^1.38.1" } }, "sha512-Cr9Mv4igxvI2tKMiahw48sZxva3PfDzypErH8IB82N+9qa9n9ygVMt0BOaDg53hLKxEEVeYr2S/wCcJIVFgBTw=="], "@tiptap/react": ["@tiptap/react@2.27.2", "", { "dependencies": { "@tiptap/extension-bubble-menu": "^2.27.2", "@tiptap/extension-floating-menu": "^2.27.2", "@types/use-sync-external-store": "^0.0.6", "fast-deep-equal": "^3", "use-sync-external-store": "^1" }, "peerDependencies": { "@tiptap/core": "^2.7.0", "@tiptap/pm": "^2.7.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-0EAs8Cpkfbvben1PZ34JN2Nd79Dhioynm2jML27DBbf1VWPk+FFWFGTMLUT0bu+Np5iVxio8fqV9t0mc4D6thA=="], "@tiptap/starter-kit": ["@tiptap/starter-kit@2.27.2", "", { "dependencies": { "@tiptap/core": "^2.27.2", "@tiptap/extension-blockquote": "^2.27.2", "@tiptap/extension-bold": "^2.27.2", "@tiptap/extension-bullet-list": "^2.27.2", "@tiptap/extension-code": "^2.27.2", "@tiptap/extension-code-block": "^2.27.2", "@tiptap/extension-document": "^2.27.2", "@tiptap/extension-dropcursor": "^2.27.2", "@tiptap/extension-gapcursor": "^2.27.2", "@tiptap/extension-hard-break": "^2.27.2", "@tiptap/extension-heading": "^2.27.2", "@tiptap/extension-history": "^2.27.2", "@tiptap/extension-horizontal-rule": "^2.27.2", "@tiptap/extension-italic": "^2.27.2", "@tiptap/extension-list-item": "^2.27.2", "@tiptap/extension-ordered-list": "^2.27.2", "@tiptap/extension-paragraph": "^2.27.2", "@tiptap/extension-strike": "^2.27.2", "@tiptap/extension-text": "^2.27.2", "@tiptap/extension-text-style": "^2.27.2", "@tiptap/pm": "^2.27.2" } }, "sha512-bb0gJvPoDuyRUQ/iuN52j1//EtWWttw+RXAv1uJxfR0uKf8X7uAqzaOOgwjknoCIDC97+1YHwpGdnRjpDkOBxw=="], + "@tiptap/y-tiptap": ["@tiptap/y-tiptap@3.0.3", "", { "dependencies": { "lib0": "^0.2.100" }, "peerDependencies": { "prosemirror-model": "^1.7.1", "prosemirror-state": "^1.2.3", "prosemirror-view": "^1.9.10", "y-protocols": "^1.0.1", "yjs": "^13.5.38" } }, "sha512-8UvuV4lTisCE9cMTc/X8kRyTn9edUO7Kball0I6wb17VwZSjNDfh/YKtP4O5vcPawEzFHQIvZGq/k1h37kAf0w=="], + "@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="], "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], @@ -1274,6 +1355,8 @@ "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + "fast-equals": ["fast-equals@5.4.0", "", {}, "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw=="], + "fast-xml-builder": ["fast-xml-builder@1.1.4", "", { "dependencies": { "path-expression-matcher": "^1.1.3" } }, "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg=="], "fast-xml-parser": ["fast-xml-parser@5.5.12", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-nUR0q8PPfoA/svPM43Gup7vLOZWppaNrYgGmrVqrAVJa7cOH4hMG6FX9M4mQ8dZA1/ObGZHzES7Ed88hxEBSJg=="], @@ -1430,6 +1513,8 @@ "linkify-it": ["linkify-it@5.0.0", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ=="], + "linkifyjs": ["linkifyjs@4.3.2", "", {}, "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA=="], + "lmdb": ["lmdb@3.5.3", "", { "dependencies": { "@harperfast/extended-iterable": "^1.0.3", "msgpackr": "^1.11.2", "node-addon-api": "^6.1.0", "node-gyp-build-optional-packages": "5.2.2", "ordered-binary": "^1.5.3", "weak-lru-cache": "^1.2.2" }, "optionalDependencies": { "@lmdb/lmdb-darwin-arm64": "3.5.3", "@lmdb/lmdb-darwin-x64": "3.5.3", "@lmdb/lmdb-linux-arm": "3.5.3", "@lmdb/lmdb-linux-arm64": "3.5.3", "@lmdb/lmdb-linux-x64": "3.5.3", "@lmdb/lmdb-win32-arm64": "3.5.3", "@lmdb/lmdb-win32-x64": "3.5.3" }, "bin": { "download-lmdb-prebuilds": "bin/download-prebuilds.js" } }, "sha512-6A0iRKOv/N62P1vU8qhBr32tHQIY6pQMe8b6zqI4VLELqV8fWHUMdnfNjMR+Kyh7ZeRCo8PDzAnW2g+SJSoP1A=="], "lodash-es": ["lodash-es@4.18.1", "", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="], @@ -1726,9 +1811,9 @@ "qsu": ["qsu@1.12.0", "", {}, "sha512-kcB2A7XS+ApkxAIc63yOqF2mmvqyA4AezObM1RxAhOB6O0bRA8xCzm6PLwU70CVW9R+0Rozqd0pgOFskNtX2eQ=="], - "quickjs-emscripten": ["quickjs-emscripten@0.32.0", "", { "dependencies": { "@jitl/quickjs-wasmfile-debug-asyncify": "0.32.0", "@jitl/quickjs-wasmfile-debug-sync": "0.32.0", "@jitl/quickjs-wasmfile-release-asyncify": "0.32.0", "@jitl/quickjs-wasmfile-release-sync": "0.32.0", "quickjs-emscripten-core": "0.32.0" } }, "sha512-So0Sqw869y/S2oE3Nuc0uT3Dhqgvsj8FSrwBdsuTosVsG8ME5/OcudU1GxsrIFdFABgy17GHnTVO9TYV/bLQcA=="], + "quickjs-emscripten": ["quickjs-emscripten@0.31.0", "", { "dependencies": { "@jitl/quickjs-wasmfile-debug-asyncify": "0.31.0", "@jitl/quickjs-wasmfile-debug-sync": "0.31.0", "@jitl/quickjs-wasmfile-release-asyncify": "0.31.0", "@jitl/quickjs-wasmfile-release-sync": "0.31.0", "quickjs-emscripten-core": "0.31.0" } }, "sha512-K7Yt78aRPLjPcqv3fIuLW1jW3pvwO21B9pmFOolsjM/57ZhdVXBr51GqJpalgBlkPu9foAvhEAuuQPnvIGvLvQ=="], - "quickjs-emscripten-core": ["quickjs-emscripten-core@0.32.0", "", { "dependencies": { "@jitl/quickjs-ffi-types": "0.32.0" } }, "sha512-QFnPfjFey8EqknSrSxe1hZrf1/8z7/6s1QzGOmKo6++02r7QRRX7ZoyNaZh7JuVjWsVW87KnQrbZqnHkOAzUyg=="], + "quickjs-emscripten-core": ["quickjs-emscripten-core@0.31.0", "", { "dependencies": { "@jitl/quickjs-ffi-types": "0.31.0" } }, "sha512-oQz8p0SiKDBc1TC7ZBK2fr0GoSHZKA0jZIeXxsnCyCs4y32FStzCW4d1h6E1sE0uHDMbGITbk2zhNaytaoJwXQ=="], "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], @@ -2052,10 +2137,24 @@ "@humanlayer/codelayer/@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="], + "@humanlayer/quickjs-exec/@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="], + "@humanlayer/yjs-fs/@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="], "@humanlayer/yjs-fs-editor/@durable-streams/server": ["@durable-streams/server@0.1.7", "", { "dependencies": { "@durable-streams/client": "0.2.0", "@durable-streams/state": "0.2.0", "@neophi/sieve-cache": "^1.0.0", "lmdb": "^3.3.0" } }, "sha512-JaCzfKS/GI3ZlbmInHvqX/fAh6XENTjc+acfhRlZH9iaRQiD1rOjfircj+jFDTIHgeT9oPqOVscmISGot8kI3w=="], + "@humanlayer/yjs-rich-markdown/@tiptap/extension-collaboration": ["@tiptap/extension-collaboration@3.22.5", "", { "peerDependencies": { "@tiptap/core": "3.22.5", "@tiptap/pm": "3.22.5", "@tiptap/y-tiptap": "^3.0.2", "yjs": "^13" } }, "sha512-3rax34HKSo8L5ihv5iGxISNBUIdVP0Fq9O6T/mq4kQOLhVhNbqwr9I/lWOvaz24nPE7u5Vkz0Ii3zWGlzxyPPA=="], + + "@humanlayer/yjs-rich-markdown/@tiptap/starter-kit": ["@tiptap/starter-kit@3.22.5", "", { "dependencies": { "@tiptap/core": "^3.22.5", "@tiptap/extension-blockquote": "^3.22.5", "@tiptap/extension-bold": "^3.22.5", "@tiptap/extension-bullet-list": "^3.22.5", "@tiptap/extension-code": "^3.22.5", "@tiptap/extension-code-block": "^3.22.5", "@tiptap/extension-document": "^3.22.5", "@tiptap/extension-dropcursor": "^3.22.5", "@tiptap/extension-gapcursor": "^3.22.5", "@tiptap/extension-hard-break": "^3.22.5", "@tiptap/extension-heading": "^3.22.5", "@tiptap/extension-horizontal-rule": "^3.22.5", "@tiptap/extension-italic": "^3.22.5", "@tiptap/extension-link": "^3.22.5", "@tiptap/extension-list": "^3.22.5", "@tiptap/extension-list-item": "^3.22.5", "@tiptap/extension-list-keymap": "^3.22.5", "@tiptap/extension-ordered-list": "^3.22.5", "@tiptap/extension-paragraph": "^3.22.5", "@tiptap/extension-strike": "^3.22.5", "@tiptap/extension-text": "^3.22.5", "@tiptap/extension-underline": "^3.22.5", "@tiptap/extensions": "^3.22.5", "@tiptap/pm": "^3.22.5" } }, "sha512-LZ/LYbwH6rnDi5DnRyagkuNsYAVyhM+yJvvz+ZuYA0JkPiTXJV86J5PWSKew8M0gVfMHcNVtKjfQCvViFCeIgw=="], + + "@humanlayer/yjs-rich-markdown/@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="], + + "@humanlayer/yjs-rich-markdown-demo/@tiptap/extension-collaboration": ["@tiptap/extension-collaboration@3.22.5", "", { "peerDependencies": { "@tiptap/core": "3.22.5", "@tiptap/pm": "3.22.5", "@tiptap/y-tiptap": "^3.0.2", "yjs": "^13" } }, "sha512-3rax34HKSo8L5ihv5iGxISNBUIdVP0Fq9O6T/mq4kQOLhVhNbqwr9I/lWOvaz24nPE7u5Vkz0Ii3zWGlzxyPPA=="], + + "@humanlayer/yjs-rich-markdown-demo/@tiptap/react": ["@tiptap/react@3.22.5", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "fast-equals": "^5.3.3", "use-sync-external-store": "^1.4.0" }, "optionalDependencies": { "@tiptap/extension-bubble-menu": "^3.22.5", "@tiptap/extension-floating-menu": "^3.22.5" }, "peerDependencies": { "@tiptap/core": "3.22.5", "@tiptap/pm": "3.22.5", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-36WHEs+vPmB//V1ff7Ujcnpz7Ey5g8lhpI/0+hoanSbdiPMTQ7qZVWwMovIkMKDlqWVp2fxBgeYM1861jyFzTw=="], + + "@humanlayer/yjs-rich-markdown-demo/@tiptap/starter-kit": ["@tiptap/starter-kit@3.22.5", "", { "dependencies": { "@tiptap/core": "^3.22.5", "@tiptap/extension-blockquote": "^3.22.5", "@tiptap/extension-bold": "^3.22.5", "@tiptap/extension-bullet-list": "^3.22.5", "@tiptap/extension-code": "^3.22.5", "@tiptap/extension-code-block": "^3.22.5", "@tiptap/extension-document": "^3.22.5", "@tiptap/extension-dropcursor": "^3.22.5", "@tiptap/extension-gapcursor": "^3.22.5", "@tiptap/extension-hard-break": "^3.22.5", "@tiptap/extension-heading": "^3.22.5", "@tiptap/extension-horizontal-rule": "^3.22.5", "@tiptap/extension-italic": "^3.22.5", "@tiptap/extension-link": "^3.22.5", "@tiptap/extension-list": "^3.22.5", "@tiptap/extension-list-item": "^3.22.5", "@tiptap/extension-list-keymap": "^3.22.5", "@tiptap/extension-ordered-list": "^3.22.5", "@tiptap/extension-paragraph": "^3.22.5", "@tiptap/extension-strike": "^3.22.5", "@tiptap/extension-text": "^3.22.5", "@tiptap/extension-underline": "^3.22.5", "@tiptap/extensions": "^3.22.5", "@tiptap/pm": "^3.22.5" } }, "sha512-LZ/LYbwH6rnDi5DnRyagkuNsYAVyhM+yJvvz+ZuYA0JkPiTXJV86J5PWSKew8M0gVfMHcNVtKjfQCvViFCeIgw=="], + "@mermaid-js/mermaid-mindmap/@braintree/sanitize-url": ["@braintree/sanitize-url@6.0.4", "", {}, "sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A=="], "@npmcli/git/ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], @@ -2072,6 +2171,78 @@ "@tanstack/router-plugin/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@tiptap/extension-blockquote/@tiptap/core": ["@tiptap/core@2.27.2", "", { "peerDependencies": { "@tiptap/pm": "^2.7.0" } }, "sha512-ABL1N6eoxzDzC1bYvkMbvyexHacszsKdVPYqhl5GwHLOvpZcv9VE9QaKwDILTyz5voCA0lGcAAXZp+qnXOk5lQ=="], + + "@tiptap/extension-bold/@tiptap/core": ["@tiptap/core@2.27.2", "", { "peerDependencies": { "@tiptap/pm": "^2.7.0" } }, "sha512-ABL1N6eoxzDzC1bYvkMbvyexHacszsKdVPYqhl5GwHLOvpZcv9VE9QaKwDILTyz5voCA0lGcAAXZp+qnXOk5lQ=="], + + "@tiptap/extension-bubble-menu/@tiptap/core": ["@tiptap/core@2.27.2", "", { "peerDependencies": { "@tiptap/pm": "^2.7.0" } }, "sha512-ABL1N6eoxzDzC1bYvkMbvyexHacszsKdVPYqhl5GwHLOvpZcv9VE9QaKwDILTyz5voCA0lGcAAXZp+qnXOk5lQ=="], + + "@tiptap/extension-bubble-menu/@tiptap/pm": ["@tiptap/pm@2.27.2", "", { "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-collab": "^1.3.1", "prosemirror-commands": "^1.6.2", "prosemirror-dropcursor": "^1.8.1", "prosemirror-gapcursor": "^1.3.2", "prosemirror-history": "^1.4.1", "prosemirror-inputrules": "^1.4.0", "prosemirror-keymap": "^1.2.2", "prosemirror-markdown": "^1.13.1", "prosemirror-menu": "^1.2.4", "prosemirror-model": "^1.23.0", "prosemirror-schema-basic": "^1.2.3", "prosemirror-schema-list": "^1.4.1", "prosemirror-state": "^1.4.3", "prosemirror-tables": "^1.6.4", "prosemirror-trailing-node": "^3.0.0", "prosemirror-transform": "^1.10.2", "prosemirror-view": "^1.37.0" } }, "sha512-kaEg7BfiJPDQMKbjVIzEPO3wlcA+pZb2tlcK9gPrdDnEFaec2QTF1sXz2ak2IIb2curvnIrQ4yrfHgLlVA72wA=="], + + "@tiptap/extension-bullet-list/@tiptap/core": ["@tiptap/core@2.27.2", "", { "peerDependencies": { "@tiptap/pm": "^2.7.0" } }, "sha512-ABL1N6eoxzDzC1bYvkMbvyexHacszsKdVPYqhl5GwHLOvpZcv9VE9QaKwDILTyz5voCA0lGcAAXZp+qnXOk5lQ=="], + + "@tiptap/extension-code/@tiptap/core": ["@tiptap/core@2.27.2", "", { "peerDependencies": { "@tiptap/pm": "^2.7.0" } }, "sha512-ABL1N6eoxzDzC1bYvkMbvyexHacszsKdVPYqhl5GwHLOvpZcv9VE9QaKwDILTyz5voCA0lGcAAXZp+qnXOk5lQ=="], + + "@tiptap/extension-code-block/@tiptap/core": ["@tiptap/core@2.27.2", "", { "peerDependencies": { "@tiptap/pm": "^2.7.0" } }, "sha512-ABL1N6eoxzDzC1bYvkMbvyexHacszsKdVPYqhl5GwHLOvpZcv9VE9QaKwDILTyz5voCA0lGcAAXZp+qnXOk5lQ=="], + + "@tiptap/extension-code-block/@tiptap/pm": ["@tiptap/pm@2.27.2", "", { "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-collab": "^1.3.1", "prosemirror-commands": "^1.6.2", "prosemirror-dropcursor": "^1.8.1", "prosemirror-gapcursor": "^1.3.2", "prosemirror-history": "^1.4.1", "prosemirror-inputrules": "^1.4.0", "prosemirror-keymap": "^1.2.2", "prosemirror-markdown": "^1.13.1", "prosemirror-menu": "^1.2.4", "prosemirror-model": "^1.23.0", "prosemirror-schema-basic": "^1.2.3", "prosemirror-schema-list": "^1.4.1", "prosemirror-state": "^1.4.3", "prosemirror-tables": "^1.6.4", "prosemirror-trailing-node": "^3.0.0", "prosemirror-transform": "^1.10.2", "prosemirror-view": "^1.37.0" } }, "sha512-kaEg7BfiJPDQMKbjVIzEPO3wlcA+pZb2tlcK9gPrdDnEFaec2QTF1sXz2ak2IIb2curvnIrQ4yrfHgLlVA72wA=="], + + "@tiptap/extension-collaboration/@tiptap/core": ["@tiptap/core@2.27.2", "", { "peerDependencies": { "@tiptap/pm": "^2.7.0" } }, "sha512-ABL1N6eoxzDzC1bYvkMbvyexHacszsKdVPYqhl5GwHLOvpZcv9VE9QaKwDILTyz5voCA0lGcAAXZp+qnXOk5lQ=="], + + "@tiptap/extension-collaboration/@tiptap/pm": ["@tiptap/pm@2.27.2", "", { "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-collab": "^1.3.1", "prosemirror-commands": "^1.6.2", "prosemirror-dropcursor": "^1.8.1", "prosemirror-gapcursor": "^1.3.2", "prosemirror-history": "^1.4.1", "prosemirror-inputrules": "^1.4.0", "prosemirror-keymap": "^1.2.2", "prosemirror-markdown": "^1.13.1", "prosemirror-menu": "^1.2.4", "prosemirror-model": "^1.23.0", "prosemirror-schema-basic": "^1.2.3", "prosemirror-schema-list": "^1.4.1", "prosemirror-state": "^1.4.3", "prosemirror-tables": "^1.6.4", "prosemirror-trailing-node": "^3.0.0", "prosemirror-transform": "^1.10.2", "prosemirror-view": "^1.37.0" } }, "sha512-kaEg7BfiJPDQMKbjVIzEPO3wlcA+pZb2tlcK9gPrdDnEFaec2QTF1sXz2ak2IIb2curvnIrQ4yrfHgLlVA72wA=="], + + "@tiptap/extension-collaboration-cursor/@tiptap/core": ["@tiptap/core@2.27.2", "", { "peerDependencies": { "@tiptap/pm": "^2.7.0" } }, "sha512-ABL1N6eoxzDzC1bYvkMbvyexHacszsKdVPYqhl5GwHLOvpZcv9VE9QaKwDILTyz5voCA0lGcAAXZp+qnXOk5lQ=="], + + "@tiptap/extension-document/@tiptap/core": ["@tiptap/core@2.27.2", "", { "peerDependencies": { "@tiptap/pm": "^2.7.0" } }, "sha512-ABL1N6eoxzDzC1bYvkMbvyexHacszsKdVPYqhl5GwHLOvpZcv9VE9QaKwDILTyz5voCA0lGcAAXZp+qnXOk5lQ=="], + + "@tiptap/extension-dropcursor/@tiptap/core": ["@tiptap/core@2.27.2", "", { "peerDependencies": { "@tiptap/pm": "^2.7.0" } }, "sha512-ABL1N6eoxzDzC1bYvkMbvyexHacszsKdVPYqhl5GwHLOvpZcv9VE9QaKwDILTyz5voCA0lGcAAXZp+qnXOk5lQ=="], + + "@tiptap/extension-dropcursor/@tiptap/pm": ["@tiptap/pm@2.27.2", "", { "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-collab": "^1.3.1", "prosemirror-commands": "^1.6.2", "prosemirror-dropcursor": "^1.8.1", "prosemirror-gapcursor": "^1.3.2", "prosemirror-history": "^1.4.1", "prosemirror-inputrules": "^1.4.0", "prosemirror-keymap": "^1.2.2", "prosemirror-markdown": "^1.13.1", "prosemirror-menu": "^1.2.4", "prosemirror-model": "^1.23.0", "prosemirror-schema-basic": "^1.2.3", "prosemirror-schema-list": "^1.4.1", "prosemirror-state": "^1.4.3", "prosemirror-tables": "^1.6.4", "prosemirror-trailing-node": "^3.0.0", "prosemirror-transform": "^1.10.2", "prosemirror-view": "^1.37.0" } }, "sha512-kaEg7BfiJPDQMKbjVIzEPO3wlcA+pZb2tlcK9gPrdDnEFaec2QTF1sXz2ak2IIb2curvnIrQ4yrfHgLlVA72wA=="], + + "@tiptap/extension-floating-menu/@tiptap/core": ["@tiptap/core@2.27.2", "", { "peerDependencies": { "@tiptap/pm": "^2.7.0" } }, "sha512-ABL1N6eoxzDzC1bYvkMbvyexHacszsKdVPYqhl5GwHLOvpZcv9VE9QaKwDILTyz5voCA0lGcAAXZp+qnXOk5lQ=="], + + "@tiptap/extension-floating-menu/@tiptap/pm": ["@tiptap/pm@2.27.2", "", { "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-collab": "^1.3.1", "prosemirror-commands": "^1.6.2", "prosemirror-dropcursor": "^1.8.1", "prosemirror-gapcursor": "^1.3.2", "prosemirror-history": "^1.4.1", "prosemirror-inputrules": "^1.4.0", "prosemirror-keymap": "^1.2.2", "prosemirror-markdown": "^1.13.1", "prosemirror-menu": "^1.2.4", "prosemirror-model": "^1.23.0", "prosemirror-schema-basic": "^1.2.3", "prosemirror-schema-list": "^1.4.1", "prosemirror-state": "^1.4.3", "prosemirror-tables": "^1.6.4", "prosemirror-trailing-node": "^3.0.0", "prosemirror-transform": "^1.10.2", "prosemirror-view": "^1.37.0" } }, "sha512-kaEg7BfiJPDQMKbjVIzEPO3wlcA+pZb2tlcK9gPrdDnEFaec2QTF1sXz2ak2IIb2curvnIrQ4yrfHgLlVA72wA=="], + + "@tiptap/extension-gapcursor/@tiptap/core": ["@tiptap/core@2.27.2", "", { "peerDependencies": { "@tiptap/pm": "^2.7.0" } }, "sha512-ABL1N6eoxzDzC1bYvkMbvyexHacszsKdVPYqhl5GwHLOvpZcv9VE9QaKwDILTyz5voCA0lGcAAXZp+qnXOk5lQ=="], + + "@tiptap/extension-gapcursor/@tiptap/pm": ["@tiptap/pm@2.27.2", "", { "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-collab": "^1.3.1", "prosemirror-commands": "^1.6.2", "prosemirror-dropcursor": "^1.8.1", "prosemirror-gapcursor": "^1.3.2", "prosemirror-history": "^1.4.1", "prosemirror-inputrules": "^1.4.0", "prosemirror-keymap": "^1.2.2", "prosemirror-markdown": "^1.13.1", "prosemirror-menu": "^1.2.4", "prosemirror-model": "^1.23.0", "prosemirror-schema-basic": "^1.2.3", "prosemirror-schema-list": "^1.4.1", "prosemirror-state": "^1.4.3", "prosemirror-tables": "^1.6.4", "prosemirror-trailing-node": "^3.0.0", "prosemirror-transform": "^1.10.2", "prosemirror-view": "^1.37.0" } }, "sha512-kaEg7BfiJPDQMKbjVIzEPO3wlcA+pZb2tlcK9gPrdDnEFaec2QTF1sXz2ak2IIb2curvnIrQ4yrfHgLlVA72wA=="], + + "@tiptap/extension-hard-break/@tiptap/core": ["@tiptap/core@2.27.2", "", { "peerDependencies": { "@tiptap/pm": "^2.7.0" } }, "sha512-ABL1N6eoxzDzC1bYvkMbvyexHacszsKdVPYqhl5GwHLOvpZcv9VE9QaKwDILTyz5voCA0lGcAAXZp+qnXOk5lQ=="], + + "@tiptap/extension-heading/@tiptap/core": ["@tiptap/core@2.27.2", "", { "peerDependencies": { "@tiptap/pm": "^2.7.0" } }, "sha512-ABL1N6eoxzDzC1bYvkMbvyexHacszsKdVPYqhl5GwHLOvpZcv9VE9QaKwDILTyz5voCA0lGcAAXZp+qnXOk5lQ=="], + + "@tiptap/extension-history/@tiptap/core": ["@tiptap/core@2.27.2", "", { "peerDependencies": { "@tiptap/pm": "^2.7.0" } }, "sha512-ABL1N6eoxzDzC1bYvkMbvyexHacszsKdVPYqhl5GwHLOvpZcv9VE9QaKwDILTyz5voCA0lGcAAXZp+qnXOk5lQ=="], + + "@tiptap/extension-history/@tiptap/pm": ["@tiptap/pm@2.27.2", "", { "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-collab": "^1.3.1", "prosemirror-commands": "^1.6.2", "prosemirror-dropcursor": "^1.8.1", "prosemirror-gapcursor": "^1.3.2", "prosemirror-history": "^1.4.1", "prosemirror-inputrules": "^1.4.0", "prosemirror-keymap": "^1.2.2", "prosemirror-markdown": "^1.13.1", "prosemirror-menu": "^1.2.4", "prosemirror-model": "^1.23.0", "prosemirror-schema-basic": "^1.2.3", "prosemirror-schema-list": "^1.4.1", "prosemirror-state": "^1.4.3", "prosemirror-tables": "^1.6.4", "prosemirror-trailing-node": "^3.0.0", "prosemirror-transform": "^1.10.2", "prosemirror-view": "^1.37.0" } }, "sha512-kaEg7BfiJPDQMKbjVIzEPO3wlcA+pZb2tlcK9gPrdDnEFaec2QTF1sXz2ak2IIb2curvnIrQ4yrfHgLlVA72wA=="], + + "@tiptap/extension-horizontal-rule/@tiptap/core": ["@tiptap/core@2.27.2", "", { "peerDependencies": { "@tiptap/pm": "^2.7.0" } }, "sha512-ABL1N6eoxzDzC1bYvkMbvyexHacszsKdVPYqhl5GwHLOvpZcv9VE9QaKwDILTyz5voCA0lGcAAXZp+qnXOk5lQ=="], + + "@tiptap/extension-horizontal-rule/@tiptap/pm": ["@tiptap/pm@2.27.2", "", { "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-collab": "^1.3.1", "prosemirror-commands": "^1.6.2", "prosemirror-dropcursor": "^1.8.1", "prosemirror-gapcursor": "^1.3.2", "prosemirror-history": "^1.4.1", "prosemirror-inputrules": "^1.4.0", "prosemirror-keymap": "^1.2.2", "prosemirror-markdown": "^1.13.1", "prosemirror-menu": "^1.2.4", "prosemirror-model": "^1.23.0", "prosemirror-schema-basic": "^1.2.3", "prosemirror-schema-list": "^1.4.1", "prosemirror-state": "^1.4.3", "prosemirror-tables": "^1.6.4", "prosemirror-trailing-node": "^3.0.0", "prosemirror-transform": "^1.10.2", "prosemirror-view": "^1.37.0" } }, "sha512-kaEg7BfiJPDQMKbjVIzEPO3wlcA+pZb2tlcK9gPrdDnEFaec2QTF1sXz2ak2IIb2curvnIrQ4yrfHgLlVA72wA=="], + + "@tiptap/extension-italic/@tiptap/core": ["@tiptap/core@2.27.2", "", { "peerDependencies": { "@tiptap/pm": "^2.7.0" } }, "sha512-ABL1N6eoxzDzC1bYvkMbvyexHacszsKdVPYqhl5GwHLOvpZcv9VE9QaKwDILTyz5voCA0lGcAAXZp+qnXOk5lQ=="], + + "@tiptap/extension-list-item/@tiptap/core": ["@tiptap/core@2.27.2", "", { "peerDependencies": { "@tiptap/pm": "^2.7.0" } }, "sha512-ABL1N6eoxzDzC1bYvkMbvyexHacszsKdVPYqhl5GwHLOvpZcv9VE9QaKwDILTyz5voCA0lGcAAXZp+qnXOk5lQ=="], + + "@tiptap/extension-ordered-list/@tiptap/core": ["@tiptap/core@2.27.2", "", { "peerDependencies": { "@tiptap/pm": "^2.7.0" } }, "sha512-ABL1N6eoxzDzC1bYvkMbvyexHacszsKdVPYqhl5GwHLOvpZcv9VE9QaKwDILTyz5voCA0lGcAAXZp+qnXOk5lQ=="], + + "@tiptap/extension-paragraph/@tiptap/core": ["@tiptap/core@2.27.2", "", { "peerDependencies": { "@tiptap/pm": "^2.7.0" } }, "sha512-ABL1N6eoxzDzC1bYvkMbvyexHacszsKdVPYqhl5GwHLOvpZcv9VE9QaKwDILTyz5voCA0lGcAAXZp+qnXOk5lQ=="], + + "@tiptap/extension-strike/@tiptap/core": ["@tiptap/core@2.27.2", "", { "peerDependencies": { "@tiptap/pm": "^2.7.0" } }, "sha512-ABL1N6eoxzDzC1bYvkMbvyexHacszsKdVPYqhl5GwHLOvpZcv9VE9QaKwDILTyz5voCA0lGcAAXZp+qnXOk5lQ=="], + + "@tiptap/extension-text/@tiptap/core": ["@tiptap/core@2.27.2", "", { "peerDependencies": { "@tiptap/pm": "^2.7.0" } }, "sha512-ABL1N6eoxzDzC1bYvkMbvyexHacszsKdVPYqhl5GwHLOvpZcv9VE9QaKwDILTyz5voCA0lGcAAXZp+qnXOk5lQ=="], + + "@tiptap/extension-text-style/@tiptap/core": ["@tiptap/core@2.27.2", "", { "peerDependencies": { "@tiptap/pm": "^2.7.0" } }, "sha512-ABL1N6eoxzDzC1bYvkMbvyexHacszsKdVPYqhl5GwHLOvpZcv9VE9QaKwDILTyz5voCA0lGcAAXZp+qnXOk5lQ=="], + + "@tiptap/markdown/marked": ["marked@17.0.6", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA=="], + + "@tiptap/react/@tiptap/core": ["@tiptap/core@2.27.2", "", { "peerDependencies": { "@tiptap/pm": "^2.7.0" } }, "sha512-ABL1N6eoxzDzC1bYvkMbvyexHacszsKdVPYqhl5GwHLOvpZcv9VE9QaKwDILTyz5voCA0lGcAAXZp+qnXOk5lQ=="], + + "@tiptap/react/@tiptap/pm": ["@tiptap/pm@2.27.2", "", { "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-collab": "^1.3.1", "prosemirror-commands": "^1.6.2", "prosemirror-dropcursor": "^1.8.1", "prosemirror-gapcursor": "^1.3.2", "prosemirror-history": "^1.4.1", "prosemirror-inputrules": "^1.4.0", "prosemirror-keymap": "^1.2.2", "prosemirror-markdown": "^1.13.1", "prosemirror-menu": "^1.2.4", "prosemirror-model": "^1.23.0", "prosemirror-schema-basic": "^1.2.3", "prosemirror-schema-list": "^1.4.1", "prosemirror-state": "^1.4.3", "prosemirror-tables": "^1.6.4", "prosemirror-trailing-node": "^3.0.0", "prosemirror-transform": "^1.10.2", "prosemirror-view": "^1.37.0" } }, "sha512-kaEg7BfiJPDQMKbjVIzEPO3wlcA+pZb2tlcK9gPrdDnEFaec2QTF1sXz2ak2IIb2curvnIrQ4yrfHgLlVA72wA=="], + + "@tiptap/starter-kit/@tiptap/core": ["@tiptap/core@2.27.2", "", { "peerDependencies": { "@tiptap/pm": "^2.7.0" } }, "sha512-ABL1N6eoxzDzC1bYvkMbvyexHacszsKdVPYqhl5GwHLOvpZcv9VE9QaKwDILTyz5voCA0lGcAAXZp+qnXOk5lQ=="], + + "@tiptap/starter-kit/@tiptap/pm": ["@tiptap/pm@2.27.2", "", { "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-collab": "^1.3.1", "prosemirror-commands": "^1.6.2", "prosemirror-dropcursor": "^1.8.1", "prosemirror-gapcursor": "^1.3.2", "prosemirror-history": "^1.4.1", "prosemirror-inputrules": "^1.4.0", "prosemirror-keymap": "^1.2.2", "prosemirror-markdown": "^1.13.1", "prosemirror-menu": "^1.2.4", "prosemirror-model": "^1.23.0", "prosemirror-schema-basic": "^1.2.3", "prosemirror-schema-list": "^1.4.1", "prosemirror-state": "^1.4.3", "prosemirror-tables": "^1.6.4", "prosemirror-trailing-node": "^3.0.0", "prosemirror-transform": "^1.10.2", "prosemirror-view": "^1.37.0" } }, "sha512-kaEg7BfiJPDQMKbjVIzEPO3wlcA+pZb2tlcK9gPrdDnEFaec2QTF1sXz2ak2IIb2curvnIrQ4yrfHgLlVA72wA=="], + "@vue/compiler-core/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], "anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], @@ -2092,6 +2263,8 @@ "just-bash/ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], + "just-bash/quickjs-emscripten": ["quickjs-emscripten@0.32.0", "", { "dependencies": { "@jitl/quickjs-wasmfile-debug-asyncify": "0.32.0", "@jitl/quickjs-wasmfile-debug-sync": "0.32.0", "@jitl/quickjs-wasmfile-release-asyncify": "0.32.0", "@jitl/quickjs-wasmfile-release-sync": "0.32.0", "quickjs-emscripten-core": "0.32.0" } }, "sha512-So0Sqw869y/S2oE3Nuc0uT3Dhqgvsj8FSrwBdsuTosVsG8ME5/OcudU1GxsrIFdFABgy17GHnTVO9TYV/bLQcA=="], + "katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], "lmdb/node-addon-api": ["node-addon-api@6.1.0", "", {}, "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA=="], @@ -2150,20 +2323,136 @@ "@humanlayer/codelayer/@types/bun/bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], + "@humanlayer/quickjs-exec/@types/bun/bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], + "@humanlayer/yjs-fs-editor/@durable-streams/server/@durable-streams/client": ["@durable-streams/client@0.2.0", "", { "dependencies": { "@microsoft/fetch-event-source": "^2.0.1", "fastq": "^1.19.1" } }, "sha512-nlrbXDjQDqOsFbVFplg0OHkXwy4EAtztARg4zvaTn53WHzMCUwuiIY+V+u3AGvUWA9GJnnjRrbPKIXGkvak1CQ=="], "@humanlayer/yjs-fs-editor/@durable-streams/server/@durable-streams/state": ["@durable-streams/state@0.2.0", "", { "dependencies": { "@durable-streams/client": "0.2.0", "@standard-schema/spec": "^1.0.0" }, "peerDependencies": { "@tanstack/db": ">=0.5.0" } }, "sha512-9XKSKkubcCbbRgUHJCENTw3351uJ4hfi8GLmYWD8l5OCEjvhVegUF06utFKehIRy3xTr57t5MC78weBfVlD2Dg=="], "@humanlayer/yjs-fs/@types/bun/bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], + "@humanlayer/yjs-rich-markdown-demo/@tiptap/react/@tiptap/extension-bubble-menu": ["@tiptap/extension-bubble-menu@3.22.5", "", { "dependencies": { "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "@tiptap/core": "3.22.5", "@tiptap/pm": "3.22.5" } }, "sha512-yrNlFQQJY5MmhBpmD8tnmaSmyUQrEvgyPKa3bzVeWEhDSG1CW4A0ZSMx3hrA9yFO0HWfw3IJmvSCycEZQBalpQ=="], + + "@humanlayer/yjs-rich-markdown-demo/@tiptap/react/@tiptap/extension-floating-menu": ["@tiptap/extension-floating-menu@3.22.5", "", { "peerDependencies": { "@floating-ui/dom": "^1.0.0", "@tiptap/core": "3.22.5", "@tiptap/pm": "3.22.5" } }, "sha512-dhem4sTPhyQgQ+pFp2Oud4k4FSQz9PVMgeQAC9288SmGwxBkJNveDAw6sKTMrumqDvwkJrtslXIupq9TZYQnzg=="], + + "@humanlayer/yjs-rich-markdown-demo/@tiptap/starter-kit/@tiptap/extension-blockquote": ["@tiptap/extension-blockquote@3.22.5", "", { "peerDependencies": { "@tiptap/core": "3.22.5" } }, "sha512-ajyP5W8fG5Hrru47T/eF3xMKOpNvWofgNJqBTeNuGl02sYxsy9a4EunyFxudsaZP9WW3VOD4SaIWr5+MqpbnOQ=="], + + "@humanlayer/yjs-rich-markdown-demo/@tiptap/starter-kit/@tiptap/extension-bold": ["@tiptap/extension-bold@3.22.5", "", { "peerDependencies": { "@tiptap/core": "3.22.5" } }, "sha512-l/uDtpJISiFFyfctvnODNWBN/XPZI1jVZRacTRDDnSn8+x6KQ7G2qgFYueU7KvVJGDFVT39Iio56mcFRG/Pozg=="], + + "@humanlayer/yjs-rich-markdown-demo/@tiptap/starter-kit/@tiptap/extension-bullet-list": ["@tiptap/extension-bullet-list@3.22.5", "", { "peerDependencies": { "@tiptap/extension-list": "3.22.5" } }, "sha512-cf54fG9AybU8NgPMv1TOcoqAkELeRc/VpnSCt/rIJZphWQx9nsFmrtkrlCatrIcCaGtNZYwlHlMnC5LVVMu0uA=="], + + "@humanlayer/yjs-rich-markdown-demo/@tiptap/starter-kit/@tiptap/extension-code": ["@tiptap/extension-code@3.22.5", "", { "peerDependencies": { "@tiptap/core": "3.22.5" } }, "sha512-mwDNOJC9rYbDu/JcqrN4dbUQRklJU8Fuk2raxD/IvFw9qUIcPCmxQ2XT9UTKmZz/Ju7Kdy72fss6XpgWv6gLAQ=="], + + "@humanlayer/yjs-rich-markdown-demo/@tiptap/starter-kit/@tiptap/extension-code-block": ["@tiptap/extension-code-block@3.22.5", "", { "peerDependencies": { "@tiptap/core": "3.22.5", "@tiptap/pm": "3.22.5" } }, "sha512-d123kCfLdJTi4fue1m0+TNFztDkmIRSZGZmGu6H9KqwG5Q7IzjT9o8lzRsz+pXxYqHvqgYmXoEpM6srbzXx/Ag=="], + + "@humanlayer/yjs-rich-markdown-demo/@tiptap/starter-kit/@tiptap/extension-document": ["@tiptap/extension-document@3.22.5", "", { "peerDependencies": { "@tiptap/core": "3.22.5" } }, "sha512-8NJERd+pCtvSuEP4C4WMGYmRRCV12ePZL7bC+QUdFlbdXg+kNZS0zZ7hh879tYA0Kidbi8rWWD1Tx+H2ezkmMw=="], + + "@humanlayer/yjs-rich-markdown-demo/@tiptap/starter-kit/@tiptap/extension-dropcursor": ["@tiptap/extension-dropcursor@3.22.5", "", { "peerDependencies": { "@tiptap/extensions": "3.22.5" } }, "sha512-Mp40DaFrY3sEUVtFqmxrR0BmU4G3k8GCYYNGqNa9OqWv7BrcFDC03V2n3okESDKt4MKkzhQQmypq+ouLy8dLfA=="], + + "@humanlayer/yjs-rich-markdown-demo/@tiptap/starter-kit/@tiptap/extension-gapcursor": ["@tiptap/extension-gapcursor@3.22.5", "", { "peerDependencies": { "@tiptap/extensions": "3.22.5" } }, "sha512-4WkMu7qqjbsm8hCQS+8X+la1wjriN0SKoRdvpfKH33qM50MB34tYJuGLAO+y7TTh4MMMco3AZCKPBL5JVMqNIg=="], + + "@humanlayer/yjs-rich-markdown-demo/@tiptap/starter-kit/@tiptap/extension-hard-break": ["@tiptap/extension-hard-break@3.22.5", "", { "peerDependencies": { "@tiptap/core": "3.22.5" } }, "sha512-n0R2mUVYZU2AVbJhg/WcY9+zx690wVwvsItHJf0DrYbf1tCYHx+PRHUt/AoXk6u8BSmnkb8/FDziS8m3mjfpSg=="], + + "@humanlayer/yjs-rich-markdown-demo/@tiptap/starter-kit/@tiptap/extension-heading": ["@tiptap/extension-heading@3.22.5", "", { "peerDependencies": { "@tiptap/core": "3.22.5" } }, "sha512-hjyEG4947PAhMBfP1G6B0QAh6+y9mp2C5BQmNjprA05/lQzDAT7KFZzNh8ZVp3ol6aICKq/N1gFOW9Dc/9FUOw=="], + + "@humanlayer/yjs-rich-markdown-demo/@tiptap/starter-kit/@tiptap/extension-horizontal-rule": ["@tiptap/extension-horizontal-rule@3.22.5", "", { "peerDependencies": { "@tiptap/core": "3.22.5", "@tiptap/pm": "3.22.5" } }, "sha512-vUV0/ugIbXOc8SJib0h8UMhgcqZXWu/dkEhlswZN4VVven1o5enkfxEiDw+OyIJHi5rUkrdhsQ/KTxG/Xb7X8A=="], + + "@humanlayer/yjs-rich-markdown-demo/@tiptap/starter-kit/@tiptap/extension-italic": ["@tiptap/extension-italic@3.22.5", "", { "peerDependencies": { "@tiptap/core": "3.22.5" } }, "sha512-4T8baSiLkeIymTgEwirxDFt5YgYofkP3m1+MGYdGy2HKcOK+1vpvlPhEO1X5qtZngtJW5S4+njKjinRg52A4PA=="], + + "@humanlayer/yjs-rich-markdown-demo/@tiptap/starter-kit/@tiptap/extension-list-item": ["@tiptap/extension-list-item@3.22.5", "", { "peerDependencies": { "@tiptap/extension-list": "3.22.5" } }, "sha512-W7uTmyKLhlsvuTPLv+8WwnsY+mlikBFIoLSvVcBaFt4MwpsZ+DeB6KQg02Y7tbtaAnG7rXu9Fvw2QORh2P728A=="], + + "@humanlayer/yjs-rich-markdown-demo/@tiptap/starter-kit/@tiptap/extension-ordered-list": ["@tiptap/extension-ordered-list@3.22.5", "", { "peerDependencies": { "@tiptap/extension-list": "3.22.5" } }, "sha512-OXdh4k4CNrukwiSdWdEQ49uvgnqvR0Z9aNSP4HI5/kZQ/Te1NtRtYCpUrzWyO/7CtjcCisXHti0o9C/TV8YMbQ=="], + + "@humanlayer/yjs-rich-markdown-demo/@tiptap/starter-kit/@tiptap/extension-paragraph": ["@tiptap/extension-paragraph@3.22.5", "", { "peerDependencies": { "@tiptap/core": "3.22.5" } }, "sha512-52KCto4+XKpnBWpIufspWLyq4UWxAWC72ANPdGuIhbi72NRTabiTbTVN40uwGSPkyakeESG0/vKdWJCVvB4f0g=="], + + "@humanlayer/yjs-rich-markdown-demo/@tiptap/starter-kit/@tiptap/extension-strike": ["@tiptap/extension-strike@3.22.5", "", { "peerDependencies": { "@tiptap/core": "3.22.5" } }, "sha512-42WrrFK5gOom/0znH85x12Mw5IQ/6O6DWdyUWoRIrNA/qJpuHtU8oVU+bIgU2tuomMGHruRjIzgBQv5sBjEtww=="], + + "@humanlayer/yjs-rich-markdown-demo/@tiptap/starter-kit/@tiptap/extension-text": ["@tiptap/extension-text@3.22.5", "", { "peerDependencies": { "@tiptap/core": "3.22.5" } }, "sha512-bzpDOdAEo1JeoVZDIyV0oY0jGXkEG+AzF70SzHoRSjOvFDtKWunyXf9eO1OnOr2/fmMcckT2qwUBNBMQplWBzw=="], + + "@humanlayer/yjs-rich-markdown/@tiptap/starter-kit/@tiptap/extension-blockquote": ["@tiptap/extension-blockquote@3.22.5", "", { "peerDependencies": { "@tiptap/core": "3.22.5" } }, "sha512-ajyP5W8fG5Hrru47T/eF3xMKOpNvWofgNJqBTeNuGl02sYxsy9a4EunyFxudsaZP9WW3VOD4SaIWr5+MqpbnOQ=="], + + "@humanlayer/yjs-rich-markdown/@tiptap/starter-kit/@tiptap/extension-bold": ["@tiptap/extension-bold@3.22.5", "", { "peerDependencies": { "@tiptap/core": "3.22.5" } }, "sha512-l/uDtpJISiFFyfctvnODNWBN/XPZI1jVZRacTRDDnSn8+x6KQ7G2qgFYueU7KvVJGDFVT39Iio56mcFRG/Pozg=="], + + "@humanlayer/yjs-rich-markdown/@tiptap/starter-kit/@tiptap/extension-bullet-list": ["@tiptap/extension-bullet-list@3.22.5", "", { "peerDependencies": { "@tiptap/extension-list": "3.22.5" } }, "sha512-cf54fG9AybU8NgPMv1TOcoqAkELeRc/VpnSCt/rIJZphWQx9nsFmrtkrlCatrIcCaGtNZYwlHlMnC5LVVMu0uA=="], + + "@humanlayer/yjs-rich-markdown/@tiptap/starter-kit/@tiptap/extension-code": ["@tiptap/extension-code@3.22.5", "", { "peerDependencies": { "@tiptap/core": "3.22.5" } }, "sha512-mwDNOJC9rYbDu/JcqrN4dbUQRklJU8Fuk2raxD/IvFw9qUIcPCmxQ2XT9UTKmZz/Ju7Kdy72fss6XpgWv6gLAQ=="], + + "@humanlayer/yjs-rich-markdown/@tiptap/starter-kit/@tiptap/extension-code-block": ["@tiptap/extension-code-block@3.22.5", "", { "peerDependencies": { "@tiptap/core": "3.22.5", "@tiptap/pm": "3.22.5" } }, "sha512-d123kCfLdJTi4fue1m0+TNFztDkmIRSZGZmGu6H9KqwG5Q7IzjT9o8lzRsz+pXxYqHvqgYmXoEpM6srbzXx/Ag=="], + + "@humanlayer/yjs-rich-markdown/@tiptap/starter-kit/@tiptap/extension-document": ["@tiptap/extension-document@3.22.5", "", { "peerDependencies": { "@tiptap/core": "3.22.5" } }, "sha512-8NJERd+pCtvSuEP4C4WMGYmRRCV12ePZL7bC+QUdFlbdXg+kNZS0zZ7hh879tYA0Kidbi8rWWD1Tx+H2ezkmMw=="], + + "@humanlayer/yjs-rich-markdown/@tiptap/starter-kit/@tiptap/extension-dropcursor": ["@tiptap/extension-dropcursor@3.22.5", "", { "peerDependencies": { "@tiptap/extensions": "3.22.5" } }, "sha512-Mp40DaFrY3sEUVtFqmxrR0BmU4G3k8GCYYNGqNa9OqWv7BrcFDC03V2n3okESDKt4MKkzhQQmypq+ouLy8dLfA=="], + + "@humanlayer/yjs-rich-markdown/@tiptap/starter-kit/@tiptap/extension-gapcursor": ["@tiptap/extension-gapcursor@3.22.5", "", { "peerDependencies": { "@tiptap/extensions": "3.22.5" } }, "sha512-4WkMu7qqjbsm8hCQS+8X+la1wjriN0SKoRdvpfKH33qM50MB34tYJuGLAO+y7TTh4MMMco3AZCKPBL5JVMqNIg=="], + + "@humanlayer/yjs-rich-markdown/@tiptap/starter-kit/@tiptap/extension-hard-break": ["@tiptap/extension-hard-break@3.22.5", "", { "peerDependencies": { "@tiptap/core": "3.22.5" } }, "sha512-n0R2mUVYZU2AVbJhg/WcY9+zx690wVwvsItHJf0DrYbf1tCYHx+PRHUt/AoXk6u8BSmnkb8/FDziS8m3mjfpSg=="], + + "@humanlayer/yjs-rich-markdown/@tiptap/starter-kit/@tiptap/extension-heading": ["@tiptap/extension-heading@3.22.5", "", { "peerDependencies": { "@tiptap/core": "3.22.5" } }, "sha512-hjyEG4947PAhMBfP1G6B0QAh6+y9mp2C5BQmNjprA05/lQzDAT7KFZzNh8ZVp3ol6aICKq/N1gFOW9Dc/9FUOw=="], + + "@humanlayer/yjs-rich-markdown/@tiptap/starter-kit/@tiptap/extension-horizontal-rule": ["@tiptap/extension-horizontal-rule@3.22.5", "", { "peerDependencies": { "@tiptap/core": "3.22.5", "@tiptap/pm": "3.22.5" } }, "sha512-vUV0/ugIbXOc8SJib0h8UMhgcqZXWu/dkEhlswZN4VVven1o5enkfxEiDw+OyIJHi5rUkrdhsQ/KTxG/Xb7X8A=="], + + "@humanlayer/yjs-rich-markdown/@tiptap/starter-kit/@tiptap/extension-italic": ["@tiptap/extension-italic@3.22.5", "", { "peerDependencies": { "@tiptap/core": "3.22.5" } }, "sha512-4T8baSiLkeIymTgEwirxDFt5YgYofkP3m1+MGYdGy2HKcOK+1vpvlPhEO1X5qtZngtJW5S4+njKjinRg52A4PA=="], + + "@humanlayer/yjs-rich-markdown/@tiptap/starter-kit/@tiptap/extension-list-item": ["@tiptap/extension-list-item@3.22.5", "", { "peerDependencies": { "@tiptap/extension-list": "3.22.5" } }, "sha512-W7uTmyKLhlsvuTPLv+8WwnsY+mlikBFIoLSvVcBaFt4MwpsZ+DeB6KQg02Y7tbtaAnG7rXu9Fvw2QORh2P728A=="], + + "@humanlayer/yjs-rich-markdown/@tiptap/starter-kit/@tiptap/extension-ordered-list": ["@tiptap/extension-ordered-list@3.22.5", "", { "peerDependencies": { "@tiptap/extension-list": "3.22.5" } }, "sha512-OXdh4k4CNrukwiSdWdEQ49uvgnqvR0Z9aNSP4HI5/kZQ/Te1NtRtYCpUrzWyO/7CtjcCisXHti0o9C/TV8YMbQ=="], + + "@humanlayer/yjs-rich-markdown/@tiptap/starter-kit/@tiptap/extension-paragraph": ["@tiptap/extension-paragraph@3.22.5", "", { "peerDependencies": { "@tiptap/core": "3.22.5" } }, "sha512-52KCto4+XKpnBWpIufspWLyq4UWxAWC72ANPdGuIhbi72NRTabiTbTVN40uwGSPkyakeESG0/vKdWJCVvB4f0g=="], + + "@humanlayer/yjs-rich-markdown/@tiptap/starter-kit/@tiptap/extension-strike": ["@tiptap/extension-strike@3.22.5", "", { "peerDependencies": { "@tiptap/core": "3.22.5" } }, "sha512-42WrrFK5gOom/0znH85x12Mw5IQ/6O6DWdyUWoRIrNA/qJpuHtU8oVU+bIgU2tuomMGHruRjIzgBQv5sBjEtww=="], + + "@humanlayer/yjs-rich-markdown/@tiptap/starter-kit/@tiptap/extension-text": ["@tiptap/extension-text@3.22.5", "", { "peerDependencies": { "@tiptap/core": "3.22.5" } }, "sha512-bzpDOdAEo1JeoVZDIyV0oY0jGXkEG+AzF70SzHoRSjOvFDtKWunyXf9eO1OnOr2/fmMcckT2qwUBNBMQplWBzw=="], + + "@humanlayer/yjs-rich-markdown/@types/bun/bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], + "@npmcli/git/which/isexe": ["isexe@4.0.0", "", {}, "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw=="], "@npmcli/promise-spawn/which/isexe": ["isexe@4.0.0", "", {}, "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw=="], + "@tiptap/extension-blockquote/@tiptap/core/@tiptap/pm": ["@tiptap/pm@2.27.2", "", { "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-collab": "^1.3.1", "prosemirror-commands": "^1.6.2", "prosemirror-dropcursor": "^1.8.1", "prosemirror-gapcursor": "^1.3.2", "prosemirror-history": "^1.4.1", "prosemirror-inputrules": "^1.4.0", "prosemirror-keymap": "^1.2.2", "prosemirror-markdown": "^1.13.1", "prosemirror-menu": "^1.2.4", "prosemirror-model": "^1.23.0", "prosemirror-schema-basic": "^1.2.3", "prosemirror-schema-list": "^1.4.1", "prosemirror-state": "^1.4.3", "prosemirror-tables": "^1.6.4", "prosemirror-trailing-node": "^3.0.0", "prosemirror-transform": "^1.10.2", "prosemirror-view": "^1.37.0" } }, "sha512-kaEg7BfiJPDQMKbjVIzEPO3wlcA+pZb2tlcK9gPrdDnEFaec2QTF1sXz2ak2IIb2curvnIrQ4yrfHgLlVA72wA=="], + + "@tiptap/extension-bold/@tiptap/core/@tiptap/pm": ["@tiptap/pm@2.27.2", "", { "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-collab": "^1.3.1", "prosemirror-commands": "^1.6.2", "prosemirror-dropcursor": "^1.8.1", "prosemirror-gapcursor": "^1.3.2", "prosemirror-history": "^1.4.1", "prosemirror-inputrules": "^1.4.0", "prosemirror-keymap": "^1.2.2", "prosemirror-markdown": "^1.13.1", "prosemirror-menu": "^1.2.4", "prosemirror-model": "^1.23.0", "prosemirror-schema-basic": "^1.2.3", "prosemirror-schema-list": "^1.4.1", "prosemirror-state": "^1.4.3", "prosemirror-tables": "^1.6.4", "prosemirror-trailing-node": "^3.0.0", "prosemirror-transform": "^1.10.2", "prosemirror-view": "^1.37.0" } }, "sha512-kaEg7BfiJPDQMKbjVIzEPO3wlcA+pZb2tlcK9gPrdDnEFaec2QTF1sXz2ak2IIb2curvnIrQ4yrfHgLlVA72wA=="], + + "@tiptap/extension-bullet-list/@tiptap/core/@tiptap/pm": ["@tiptap/pm@2.27.2", "", { "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-collab": "^1.3.1", "prosemirror-commands": "^1.6.2", "prosemirror-dropcursor": "^1.8.1", "prosemirror-gapcursor": "^1.3.2", "prosemirror-history": "^1.4.1", "prosemirror-inputrules": "^1.4.0", "prosemirror-keymap": "^1.2.2", "prosemirror-markdown": "^1.13.1", "prosemirror-menu": "^1.2.4", "prosemirror-model": "^1.23.0", "prosemirror-schema-basic": "^1.2.3", "prosemirror-schema-list": "^1.4.1", "prosemirror-state": "^1.4.3", "prosemirror-tables": "^1.6.4", "prosemirror-trailing-node": "^3.0.0", "prosemirror-transform": "^1.10.2", "prosemirror-view": "^1.37.0" } }, "sha512-kaEg7BfiJPDQMKbjVIzEPO3wlcA+pZb2tlcK9gPrdDnEFaec2QTF1sXz2ak2IIb2curvnIrQ4yrfHgLlVA72wA=="], + + "@tiptap/extension-code/@tiptap/core/@tiptap/pm": ["@tiptap/pm@2.27.2", "", { "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-collab": "^1.3.1", "prosemirror-commands": "^1.6.2", "prosemirror-dropcursor": "^1.8.1", "prosemirror-gapcursor": "^1.3.2", "prosemirror-history": "^1.4.1", "prosemirror-inputrules": "^1.4.0", "prosemirror-keymap": "^1.2.2", "prosemirror-markdown": "^1.13.1", "prosemirror-menu": "^1.2.4", "prosemirror-model": "^1.23.0", "prosemirror-schema-basic": "^1.2.3", "prosemirror-schema-list": "^1.4.1", "prosemirror-state": "^1.4.3", "prosemirror-tables": "^1.6.4", "prosemirror-trailing-node": "^3.0.0", "prosemirror-transform": "^1.10.2", "prosemirror-view": "^1.37.0" } }, "sha512-kaEg7BfiJPDQMKbjVIzEPO3wlcA+pZb2tlcK9gPrdDnEFaec2QTF1sXz2ak2IIb2curvnIrQ4yrfHgLlVA72wA=="], + + "@tiptap/extension-collaboration-cursor/@tiptap/core/@tiptap/pm": ["@tiptap/pm@2.27.2", "", { "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-collab": "^1.3.1", "prosemirror-commands": "^1.6.2", "prosemirror-dropcursor": "^1.8.1", "prosemirror-gapcursor": "^1.3.2", "prosemirror-history": "^1.4.1", "prosemirror-inputrules": "^1.4.0", "prosemirror-keymap": "^1.2.2", "prosemirror-markdown": "^1.13.1", "prosemirror-menu": "^1.2.4", "prosemirror-model": "^1.23.0", "prosemirror-schema-basic": "^1.2.3", "prosemirror-schema-list": "^1.4.1", "prosemirror-state": "^1.4.3", "prosemirror-tables": "^1.6.4", "prosemirror-trailing-node": "^3.0.0", "prosemirror-transform": "^1.10.2", "prosemirror-view": "^1.37.0" } }, "sha512-kaEg7BfiJPDQMKbjVIzEPO3wlcA+pZb2tlcK9gPrdDnEFaec2QTF1sXz2ak2IIb2curvnIrQ4yrfHgLlVA72wA=="], + + "@tiptap/extension-document/@tiptap/core/@tiptap/pm": ["@tiptap/pm@2.27.2", "", { "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-collab": "^1.3.1", "prosemirror-commands": "^1.6.2", "prosemirror-dropcursor": "^1.8.1", "prosemirror-gapcursor": "^1.3.2", "prosemirror-history": "^1.4.1", "prosemirror-inputrules": "^1.4.0", "prosemirror-keymap": "^1.2.2", "prosemirror-markdown": "^1.13.1", "prosemirror-menu": "^1.2.4", "prosemirror-model": "^1.23.0", "prosemirror-schema-basic": "^1.2.3", "prosemirror-schema-list": "^1.4.1", "prosemirror-state": "^1.4.3", "prosemirror-tables": "^1.6.4", "prosemirror-trailing-node": "^3.0.0", "prosemirror-transform": "^1.10.2", "prosemirror-view": "^1.37.0" } }, "sha512-kaEg7BfiJPDQMKbjVIzEPO3wlcA+pZb2tlcK9gPrdDnEFaec2QTF1sXz2ak2IIb2curvnIrQ4yrfHgLlVA72wA=="], + + "@tiptap/extension-hard-break/@tiptap/core/@tiptap/pm": ["@tiptap/pm@2.27.2", "", { "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-collab": "^1.3.1", "prosemirror-commands": "^1.6.2", "prosemirror-dropcursor": "^1.8.1", "prosemirror-gapcursor": "^1.3.2", "prosemirror-history": "^1.4.1", "prosemirror-inputrules": "^1.4.0", "prosemirror-keymap": "^1.2.2", "prosemirror-markdown": "^1.13.1", "prosemirror-menu": "^1.2.4", "prosemirror-model": "^1.23.0", "prosemirror-schema-basic": "^1.2.3", "prosemirror-schema-list": "^1.4.1", "prosemirror-state": "^1.4.3", "prosemirror-tables": "^1.6.4", "prosemirror-trailing-node": "^3.0.0", "prosemirror-transform": "^1.10.2", "prosemirror-view": "^1.37.0" } }, "sha512-kaEg7BfiJPDQMKbjVIzEPO3wlcA+pZb2tlcK9gPrdDnEFaec2QTF1sXz2ak2IIb2curvnIrQ4yrfHgLlVA72wA=="], + + "@tiptap/extension-heading/@tiptap/core/@tiptap/pm": ["@tiptap/pm@2.27.2", "", { "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-collab": "^1.3.1", "prosemirror-commands": "^1.6.2", "prosemirror-dropcursor": "^1.8.1", "prosemirror-gapcursor": "^1.3.2", "prosemirror-history": "^1.4.1", "prosemirror-inputrules": "^1.4.0", "prosemirror-keymap": "^1.2.2", "prosemirror-markdown": "^1.13.1", "prosemirror-menu": "^1.2.4", "prosemirror-model": "^1.23.0", "prosemirror-schema-basic": "^1.2.3", "prosemirror-schema-list": "^1.4.1", "prosemirror-state": "^1.4.3", "prosemirror-tables": "^1.6.4", "prosemirror-trailing-node": "^3.0.0", "prosemirror-transform": "^1.10.2", "prosemirror-view": "^1.37.0" } }, "sha512-kaEg7BfiJPDQMKbjVIzEPO3wlcA+pZb2tlcK9gPrdDnEFaec2QTF1sXz2ak2IIb2curvnIrQ4yrfHgLlVA72wA=="], + + "@tiptap/extension-italic/@tiptap/core/@tiptap/pm": ["@tiptap/pm@2.27.2", "", { "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-collab": "^1.3.1", "prosemirror-commands": "^1.6.2", "prosemirror-dropcursor": "^1.8.1", "prosemirror-gapcursor": "^1.3.2", "prosemirror-history": "^1.4.1", "prosemirror-inputrules": "^1.4.0", "prosemirror-keymap": "^1.2.2", "prosemirror-markdown": "^1.13.1", "prosemirror-menu": "^1.2.4", "prosemirror-model": "^1.23.0", "prosemirror-schema-basic": "^1.2.3", "prosemirror-schema-list": "^1.4.1", "prosemirror-state": "^1.4.3", "prosemirror-tables": "^1.6.4", "prosemirror-trailing-node": "^3.0.0", "prosemirror-transform": "^1.10.2", "prosemirror-view": "^1.37.0" } }, "sha512-kaEg7BfiJPDQMKbjVIzEPO3wlcA+pZb2tlcK9gPrdDnEFaec2QTF1sXz2ak2IIb2curvnIrQ4yrfHgLlVA72wA=="], + + "@tiptap/extension-list-item/@tiptap/core/@tiptap/pm": ["@tiptap/pm@2.27.2", "", { "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-collab": "^1.3.1", "prosemirror-commands": "^1.6.2", "prosemirror-dropcursor": "^1.8.1", "prosemirror-gapcursor": "^1.3.2", "prosemirror-history": "^1.4.1", "prosemirror-inputrules": "^1.4.0", "prosemirror-keymap": "^1.2.2", "prosemirror-markdown": "^1.13.1", "prosemirror-menu": "^1.2.4", "prosemirror-model": "^1.23.0", "prosemirror-schema-basic": "^1.2.3", "prosemirror-schema-list": "^1.4.1", "prosemirror-state": "^1.4.3", "prosemirror-tables": "^1.6.4", "prosemirror-trailing-node": "^3.0.0", "prosemirror-transform": "^1.10.2", "prosemirror-view": "^1.37.0" } }, "sha512-kaEg7BfiJPDQMKbjVIzEPO3wlcA+pZb2tlcK9gPrdDnEFaec2QTF1sXz2ak2IIb2curvnIrQ4yrfHgLlVA72wA=="], + + "@tiptap/extension-ordered-list/@tiptap/core/@tiptap/pm": ["@tiptap/pm@2.27.2", "", { "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-collab": "^1.3.1", "prosemirror-commands": "^1.6.2", "prosemirror-dropcursor": "^1.8.1", "prosemirror-gapcursor": "^1.3.2", "prosemirror-history": "^1.4.1", "prosemirror-inputrules": "^1.4.0", "prosemirror-keymap": "^1.2.2", "prosemirror-markdown": "^1.13.1", "prosemirror-menu": "^1.2.4", "prosemirror-model": "^1.23.0", "prosemirror-schema-basic": "^1.2.3", "prosemirror-schema-list": "^1.4.1", "prosemirror-state": "^1.4.3", "prosemirror-tables": "^1.6.4", "prosemirror-trailing-node": "^3.0.0", "prosemirror-transform": "^1.10.2", "prosemirror-view": "^1.37.0" } }, "sha512-kaEg7BfiJPDQMKbjVIzEPO3wlcA+pZb2tlcK9gPrdDnEFaec2QTF1sXz2ak2IIb2curvnIrQ4yrfHgLlVA72wA=="], + + "@tiptap/extension-paragraph/@tiptap/core/@tiptap/pm": ["@tiptap/pm@2.27.2", "", { "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-collab": "^1.3.1", "prosemirror-commands": "^1.6.2", "prosemirror-dropcursor": "^1.8.1", "prosemirror-gapcursor": "^1.3.2", "prosemirror-history": "^1.4.1", "prosemirror-inputrules": "^1.4.0", "prosemirror-keymap": "^1.2.2", "prosemirror-markdown": "^1.13.1", "prosemirror-menu": "^1.2.4", "prosemirror-model": "^1.23.0", "prosemirror-schema-basic": "^1.2.3", "prosemirror-schema-list": "^1.4.1", "prosemirror-state": "^1.4.3", "prosemirror-tables": "^1.6.4", "prosemirror-trailing-node": "^3.0.0", "prosemirror-transform": "^1.10.2", "prosemirror-view": "^1.37.0" } }, "sha512-kaEg7BfiJPDQMKbjVIzEPO3wlcA+pZb2tlcK9gPrdDnEFaec2QTF1sXz2ak2IIb2curvnIrQ4yrfHgLlVA72wA=="], + + "@tiptap/extension-strike/@tiptap/core/@tiptap/pm": ["@tiptap/pm@2.27.2", "", { "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-collab": "^1.3.1", "prosemirror-commands": "^1.6.2", "prosemirror-dropcursor": "^1.8.1", "prosemirror-gapcursor": "^1.3.2", "prosemirror-history": "^1.4.1", "prosemirror-inputrules": "^1.4.0", "prosemirror-keymap": "^1.2.2", "prosemirror-markdown": "^1.13.1", "prosemirror-menu": "^1.2.4", "prosemirror-model": "^1.23.0", "prosemirror-schema-basic": "^1.2.3", "prosemirror-schema-list": "^1.4.1", "prosemirror-state": "^1.4.3", "prosemirror-tables": "^1.6.4", "prosemirror-trailing-node": "^3.0.0", "prosemirror-transform": "^1.10.2", "prosemirror-view": "^1.37.0" } }, "sha512-kaEg7BfiJPDQMKbjVIzEPO3wlcA+pZb2tlcK9gPrdDnEFaec2QTF1sXz2ak2IIb2curvnIrQ4yrfHgLlVA72wA=="], + + "@tiptap/extension-text-style/@tiptap/core/@tiptap/pm": ["@tiptap/pm@2.27.2", "", { "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-collab": "^1.3.1", "prosemirror-commands": "^1.6.2", "prosemirror-dropcursor": "^1.8.1", "prosemirror-gapcursor": "^1.3.2", "prosemirror-history": "^1.4.1", "prosemirror-inputrules": "^1.4.0", "prosemirror-keymap": "^1.2.2", "prosemirror-markdown": "^1.13.1", "prosemirror-menu": "^1.2.4", "prosemirror-model": "^1.23.0", "prosemirror-schema-basic": "^1.2.3", "prosemirror-schema-list": "^1.4.1", "prosemirror-state": "^1.4.3", "prosemirror-tables": "^1.6.4", "prosemirror-trailing-node": "^3.0.0", "prosemirror-transform": "^1.10.2", "prosemirror-view": "^1.37.0" } }, "sha512-kaEg7BfiJPDQMKbjVIzEPO3wlcA+pZb2tlcK9gPrdDnEFaec2QTF1sXz2ak2IIb2curvnIrQ4yrfHgLlVA72wA=="], + + "@tiptap/extension-text/@tiptap/core/@tiptap/pm": ["@tiptap/pm@2.27.2", "", { "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-collab": "^1.3.1", "prosemirror-commands": "^1.6.2", "prosemirror-dropcursor": "^1.8.1", "prosemirror-gapcursor": "^1.3.2", "prosemirror-history": "^1.4.1", "prosemirror-inputrules": "^1.4.0", "prosemirror-keymap": "^1.2.2", "prosemirror-markdown": "^1.13.1", "prosemirror-menu": "^1.2.4", "prosemirror-model": "^1.23.0", "prosemirror-schema-basic": "^1.2.3", "prosemirror-schema-list": "^1.4.1", "prosemirror-state": "^1.4.3", "prosemirror-tables": "^1.6.4", "prosemirror-trailing-node": "^3.0.0", "prosemirror-transform": "^1.10.2", "prosemirror-view": "^1.37.0" } }, "sha512-kaEg7BfiJPDQMKbjVIzEPO3wlcA+pZb2tlcK9gPrdDnEFaec2QTF1sXz2ak2IIb2curvnIrQ4yrfHgLlVA72wA=="], + "cytoscape-fcose/cose-base/layout-base": ["layout-base@2.0.1", "", {}, "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="], "d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="], + "just-bash/quickjs-emscripten/@jitl/quickjs-wasmfile-debug-asyncify": ["@jitl/quickjs-wasmfile-debug-asyncify@0.32.0", "", { "dependencies": { "@jitl/quickjs-ffi-types": "0.32.0" } }, "sha512-EX8zbXwGqCgAE764M+qvkHtyXDi/FUoMBea0JnES7vCM3P7a2+EOZOjGv85wtZ2sJhI1oJ+nekmqpOODFDY+hw=="], + + "just-bash/quickjs-emscripten/@jitl/quickjs-wasmfile-debug-sync": ["@jitl/quickjs-wasmfile-debug-sync@0.32.0", "", { "dependencies": { "@jitl/quickjs-ffi-types": "0.32.0" } }, "sha512-LeYWrPGC1uNCTBWvibo3ZLJj0CSVNYUXvJpXMCmuQ5Sap2cCACc3uvGvYV4homHHBAzfw5akoTqMMS4YFRtw+Q=="], + + "just-bash/quickjs-emscripten/@jitl/quickjs-wasmfile-release-asyncify": ["@jitl/quickjs-wasmfile-release-asyncify@0.32.0", "", { "dependencies": { "@jitl/quickjs-ffi-types": "0.32.0" } }, "sha512-3oSwPfja12ICz4aIblB58cuY8JlEq5Txt8Cut4VLo+LH47QN+mzCnSgnbB03hWzg1LBcc+VyyI9UOag7a1NF+Q=="], + + "just-bash/quickjs-emscripten/@jitl/quickjs-wasmfile-release-sync": ["@jitl/quickjs-wasmfile-release-sync@0.32.0", "", { "dependencies": { "@jitl/quickjs-ffi-types": "0.32.0" } }, "sha512-BKNDI/TPBfGlLNGYpLrhcDGXmIk4xHm4MRAisOBnOzpXVn9HZWsfmMAc9WMBrAHjvvds6HOikKeaOBKdPdpVrg=="], + + "just-bash/quickjs-emscripten/quickjs-emscripten-core": ["quickjs-emscripten-core@0.32.0", "", { "dependencies": { "@jitl/quickjs-ffi-types": "0.32.0" } }, "sha512-QFnPfjFey8EqknSrSxe1hZrf1/8z7/6s1QzGOmKo6++02r7QRRX7ZoyNaZh7JuVjWsVW87KnQrbZqnHkOAzUyg=="], + "minipass-flush/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], "minipass-pipeline/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], @@ -2236,6 +2525,16 @@ "vitepress/vite/esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], + "just-bash/quickjs-emscripten/@jitl/quickjs-wasmfile-debug-asyncify/@jitl/quickjs-ffi-types": ["@jitl/quickjs-ffi-types@0.32.0", "", {}, "sha512-v9T+GQpmk43VDJ7d72sf0Nexhk+ArvtUihW27dy7lqAl0zBObFKtSBBIm5RBjwIhE8VwsPPm9PNuvPvNqLWUEg=="], + + "just-bash/quickjs-emscripten/@jitl/quickjs-wasmfile-debug-sync/@jitl/quickjs-ffi-types": ["@jitl/quickjs-ffi-types@0.32.0", "", {}, "sha512-v9T+GQpmk43VDJ7d72sf0Nexhk+ArvtUihW27dy7lqAl0zBObFKtSBBIm5RBjwIhE8VwsPPm9PNuvPvNqLWUEg=="], + + "just-bash/quickjs-emscripten/@jitl/quickjs-wasmfile-release-asyncify/@jitl/quickjs-ffi-types": ["@jitl/quickjs-ffi-types@0.32.0", "", {}, "sha512-v9T+GQpmk43VDJ7d72sf0Nexhk+ArvtUihW27dy7lqAl0zBObFKtSBBIm5RBjwIhE8VwsPPm9PNuvPvNqLWUEg=="], + + "just-bash/quickjs-emscripten/@jitl/quickjs-wasmfile-release-sync/@jitl/quickjs-ffi-types": ["@jitl/quickjs-ffi-types@0.32.0", "", {}, "sha512-v9T+GQpmk43VDJ7d72sf0Nexhk+ArvtUihW27dy7lqAl0zBObFKtSBBIm5RBjwIhE8VwsPPm9PNuvPvNqLWUEg=="], + + "just-bash/quickjs-emscripten/quickjs-emscripten-core/@jitl/quickjs-ffi-types": ["@jitl/quickjs-ffi-types@0.32.0", "", {}, "sha512-v9T+GQpmk43VDJ7d72sf0Nexhk+ArvtUihW27dy7lqAl0zBObFKtSBBIm5RBjwIhE8VwsPPm9PNuvPvNqLWUEg=="], + "vitepress/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], "vitepress/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], diff --git a/examples/yjs-rich-markdown-demo/index.html b/examples/yjs-rich-markdown-demo/index.html new file mode 100644 index 0000000..6702802 --- /dev/null +++ b/examples/yjs-rich-markdown-demo/index.html @@ -0,0 +1,62 @@ + + + + + + Rich Markdown Learning Demo + + + +
+ + + diff --git a/examples/yjs-rich-markdown-demo/package.json b/examples/yjs-rich-markdown-demo/package.json new file mode 100644 index 0000000..66fa816 --- /dev/null +++ b/examples/yjs-rich-markdown-demo/package.json @@ -0,0 +1,37 @@ +{ + "name": "@humanlayer/yjs-rich-markdown-demo", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "dev": "bun run --parallel dev:server dev:vite", + "dev:vite": "vite --port 5176", + "dev:server": "bun run server/index.ts", + "build": "tsc -b && vite build", + "preview": "vite preview", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@durable-streams/server": "^0.3.1", + "@durable-streams/y-durable-streams": "^0.2.4", + "@humanlayer/yjs-rich-markdown": "workspace:*", + "@tiptap/extension-collaboration": "3.22.5", + "@tiptap/extension-collaboration-caret": "3.22.5", + "@tiptap/pm": "3.22.5", + "@tiptap/react": "3.22.5", + "@tiptap/starter-kit": "3.22.5", + "@tanstack/react-router": "^1.93.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "yjs": "^13.6.30", + "y-protocols": "1.0.7" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@tanstack/router-plugin": "^1.93.0", + "@vitejs/plugin-react": "^4.3.4", + "typescript": "^5.6.3", + "vite": "^6.0.0" + } +} \ No newline at end of file diff --git a/examples/yjs-rich-markdown-demo/server/index.ts b/examples/yjs-rich-markdown-demo/server/index.ts new file mode 100644 index 0000000..64cd460 --- /dev/null +++ b/examples/yjs-rich-markdown-demo/server/index.ts @@ -0,0 +1,37 @@ +import { DurableStreamTestServer } from '@durable-streams/server' +import { YjsServer } from '@durable-streams/y-durable-streams/server' + +const DS_PORT = 4447 +const YJS_PORT = 4438 + +async function main() { + console.log('Starting Durable Streams server...') + const dsServer = new DurableStreamTestServer({ + port: DS_PORT, + host: '127.0.0.1', + }) + await dsServer.start() + console.log(`Durable Streams server running at http://127.0.0.1:${DS_PORT}`) + + console.log('Starting Y.js server...') + const yjsServer = new YjsServer({ + port: YJS_PORT, + host: '127.0.0.1', + dsServerUrl: `http://127.0.0.1:${DS_PORT}`, + compactionThreshold: 1024 * 1024, + }) + const yjsUrl = await yjsServer.start() + console.log(`Y.js server running at ${yjsUrl}`) + + console.log('\nServers ready. Press Ctrl+C to stop.') + + process.on('SIGINT', async () => { + console.log('\nShutting down...') + process.exit(0) + }) +} + +main().catch((err) => { + console.error('Failed to start servers:', err) + process.exit(1) +}) diff --git a/examples/yjs-rich-markdown-demo/src/components/ArtifactEditor.tsx b/examples/yjs-rich-markdown-demo/src/components/ArtifactEditor.tsx new file mode 100644 index 0000000..e6bd674 --- /dev/null +++ b/examples/yjs-rich-markdown-demo/src/components/ArtifactEditor.tsx @@ -0,0 +1,69 @@ +import { artifactCollaborationExtension } from '@humanlayer/yjs-rich-markdown' +import CollaborationCaret from '@tiptap/extension-collaboration-caret' +import { EditorContent, useEditor } from '@tiptap/react' +import StarterKit from '@tiptap/starter-kit' +import { useEffect } from 'react' +import { useArtifactSession } from '../providers/ArtifactProvider' + +type ArtifactEditorProps = { + path: string + mode?: 'fragment' | 'field' +} + +export function ArtifactEditor({ path, mode = 'fragment' }: ArtifactEditorProps) { + const { doc, store, awareness, provider, localUser } = useArtifactSession() + + store.ensureArtifact(path) + + const editor = useEditor( + { + extensions: [ + StarterKit.configure({ + undoRedo: false, + }), + artifactCollaborationExtension({ doc, path, mode }), + CollaborationCaret.configure({ + provider: provider as any, + user: { + name: localUser.name, + color: localUser.color, + }, + }), + ], + editorProps: { + attributes: { + class: 'tiptap', + }, + }, + }, + [path, mode], + ) + + useEffect(() => { + awareness.setLocalStateField('presence', { artifactPath: path }) + + return () => { + awareness.setLocalStateField('presence', { artifactPath: undefined }) + } + }, [path, awareness]) + + useEffect(() => { + return () => { + editor?.destroy() + } + }, [editor]) + + return ( +
+ +
+ ) +} diff --git a/examples/yjs-rich-markdown-demo/src/components/ArtifactList.tsx b/examples/yjs-rich-markdown-demo/src/components/ArtifactList.tsx new file mode 100644 index 0000000..6005af8 --- /dev/null +++ b/examples/yjs-rich-markdown-demo/src/components/ArtifactList.tsx @@ -0,0 +1,52 @@ +import type { ArtifactMetadata } from '@humanlayer/yjs-rich-markdown' +import { useEffect, useState } from 'react' +import { useArtifactSession } from '../providers/ArtifactProvider' + +type ArtifactListProps = { + onSelect?: (path: string) => void + selectedPath?: string +} + +export function ArtifactList({ onSelect, selectedPath }: ArtifactListProps) { + const { store } = useArtifactSession() + const [artifacts, setArtifacts] = useState([]) + + useEffect(() => { + const update = () => { + setArtifacts(store.listArtifacts()) + } + + update() + store.registry.observe(update) + return () => store.registry.unobserve(update) + }, [store]) + + return ( +
+

Artifacts ({artifacts.length})

+ {artifacts.length === 0 ? ( +
No artifacts yet
+ ) : ( + + )} +
+ ) +} diff --git a/examples/yjs-rich-markdown-demo/src/components/AwarenessDebugPanel.tsx b/examples/yjs-rich-markdown-demo/src/components/AwarenessDebugPanel.tsx new file mode 100644 index 0000000..f2f429f --- /dev/null +++ b/examples/yjs-rich-markdown-demo/src/components/AwarenessDebugPanel.tsx @@ -0,0 +1,82 @@ +import { useEffect, useState } from 'react' +import { type Collaborator, getCollaborators } from '../lib/collaboration' +import { useArtifactSession } from '../providers/ArtifactProvider' + +type AwarenessDebugPanelProps = { + filterByArtifact?: string +} + +export function AwarenessDebugPanel({ filterByArtifact }: AwarenessDebugPanelProps) { + const { awareness } = useArtifactSession() + const [states, setStates] = useState>(new Map()) + const [collaborators, setCollaborators] = useState([]) + + useEffect(() => { + const update = () => { + const newStates = new Map(awareness.getStates()) + setStates(newStates) + setCollaborators( + getCollaborators(newStates, awareness.clientID, { + includeSelf: true, + filterByArtifact, + }), + ) + } + + update() + awareness.on('change', update) + return () => awareness.off('change', update) + }, [awareness, filterByArtifact]) + + return ( +
+

+ Awareness Debug {filterByArtifact ? `(filtered: ${filterByArtifact})` : '(all)'} +

+
+ Local Client ID: {awareness.clientID} +
+ +
+ Collaborators ({collaborators.length}): + {collaborators.map((c) => ( +
+
+ {c.user.name} {c.isSelf ? '(you)' : ''} +
+
+ clientId: {c.clientId}, path: {c.presence.artifactPath ?? 'none'} +
+
+ ))} +
+ +
+ + Raw States ({states.size}) + +
+					{JSON.stringify(Array.from(states.entries()), null, 2)}
+				
+
+
+ ) +} diff --git a/examples/yjs-rich-markdown-demo/src/components/ConnectionStatus.tsx b/examples/yjs-rich-markdown-demo/src/components/ConnectionStatus.tsx new file mode 100644 index 0000000..fa3af8b --- /dev/null +++ b/examples/yjs-rich-markdown-demo/src/components/ConnectionStatus.tsx @@ -0,0 +1,46 @@ +import { useArtifactSession } from '../providers/ArtifactProvider' + +export function ConnectionStatus() { + const { connectionStatus, isSynced, localUser } = useArtifactSession() + + const statusColor = + connectionStatus === 'connected' ? '#4caf50' : connectionStatus === 'connecting' ? '#ff9800' : '#f44336' + + return ( +
+
+
+ {connectionStatus} +
+
synced: {isSynced ? 'yes' : 'no'}
+
+
+ {localUser.name} +
+
+ ) +} diff --git a/examples/yjs-rich-markdown-demo/src/components/MarkdownExportPanel.tsx b/examples/yjs-rich-markdown-demo/src/components/MarkdownExportPanel.tsx new file mode 100644 index 0000000..5b3eee6 --- /dev/null +++ b/examples/yjs-rich-markdown-demo/src/components/MarkdownExportPanel.tsx @@ -0,0 +1,71 @@ +import { artifactFragment } from '@humanlayer/yjs-rich-markdown' +import { useEffect, useState } from 'react' +import { useArtifactSession } from '../providers/ArtifactProvider' + +type MarkdownExportPanelProps = { + path: string +} + +export function MarkdownExportPanel({ path }: MarkdownExportPanelProps) { + const { doc } = useArtifactSession() + const [fragmentString, setFragmentString] = useState('') + const [fragmentJson, setFragmentJson] = useState('') + + useEffect(() => { + const fragment = artifactFragment(doc, path) + + const update = () => { + setFragmentString(fragment.toString()) + setFragmentJson(formatJson(fragment.toJSON())) + } + + update() + fragment.observeDeep(update) + return () => fragment.unobserveDeep(update) + }, [doc, path]) + + return ( +
+

Fragment Debug: {path}

+
+ Raw Y.XmlFragment serialization for agent-readable rich artifact inspection. +
+ + +
+ ) +} + +function DebugSection({ label, content }: { label: string; content: string }) { + return ( +
+
{label}
+
+				{content || '(empty fragment)'}
+			
+
+ ) +} + +function formatJson(value: unknown): string { + return JSON.stringify(value, null, 2) ?? '' +} diff --git a/examples/yjs-rich-markdown-demo/src/lib/collaboration.ts b/examples/yjs-rich-markdown-demo/src/lib/collaboration.ts new file mode 100644 index 0000000..bc03451 --- /dev/null +++ b/examples/yjs-rich-markdown-demo/src/lib/collaboration.ts @@ -0,0 +1,167 @@ +export type AwarenessUser = { + id: string + name: string + color: string +} + +export type CollaboratorPresence = { + artifactPath?: string +} + +export type Collaborator = { + clientId: number + isSelf: boolean + user: AwarenessUser + presence: CollaboratorPresence +} + +const LOCAL_USER_STORAGE_KEY = 'yjs-rich-markdown-demo.local-user' + +const COLORS = [ + '#958DF1', + '#F98181', + '#FBBC88', + '#FAF594', + '#70CFF8', + '#94FADB', + '#B9F18D', + '#C3E2C2', + '#EAECCC', + '#AFC8AD', + '#EEC759', + '#9BB8CD', + '#FF90BC', + '#FFC0D9', + '#DC8686', + '#7ED7C1', +] + +function colorFromId(id: string): string { + let hash = 0 + for (let i = 0; i < id.length; i++) { + hash = (hash << 5) - hash + id.charCodeAt(i) + hash = hash & hash + } + return COLORS[Math.abs(hash) % COLORS.length] ?? COLORS[0] ?? '#958DF1' +} + +export function getOrCreateLocalUser(): AwarenessUser { + const storedUser = readStoredLocalUser() + if (storedUser) { + return storedUser + } + + const id = crypto.randomUUID() + const user: AwarenessUser = { + id, + name: `User ${id.slice(0, 4)}`, + color: colorFromId(id), + } + + try { + window.localStorage.setItem(LOCAL_USER_STORAGE_KEY, JSON.stringify(user)) + } catch {} + + return user +} + +export function updateLocalUserName(name: string): AwarenessUser { + const user = getOrCreateLocalUser() + user.name = name + + try { + window.localStorage.setItem(LOCAL_USER_STORAGE_KEY, JSON.stringify(user)) + } catch {} + + return user +} + +export function getCollaborators( + states: Map, + localClientId: number, + options: { includeSelf?: boolean; filterByArtifact?: string } = {}, +): Collaborator[] { + const collaborators: Collaborator[] = [] + + states.forEach((state, clientId) => { + const collaborator = getCollaboratorFromAwarenessState(state, clientId, localClientId) + if (!collaborator) { + return + } + + if (!options.includeSelf && collaborator.isSelf) { + return + } + + if (options.filterByArtifact && collaborator.presence.artifactPath !== options.filterByArtifact) { + return + } + + collaborators.push(collaborator) + }) + + return collaborators.sort((left, right) => { + if (left.isSelf !== right.isSelf) { + return left.isSelf ? -1 : 1 + } + return left.user.name.localeCompare(right.user.name) + }) +} + +export function getCollaboratorFromAwarenessState( + state: unknown, + clientId: number, + localClientId: number, +): Collaborator | null { + if (!isRecord(state)) { + return null + } + + const user = readUser(state.user) + if (!user) { + return null + } + + const presence = isRecord(state.presence) ? state.presence : null + + return { + clientId, + isSelf: clientId === localClientId, + user, + presence: { + artifactPath: presence && typeof presence.artifactPath === 'string' ? presence.artifactPath : undefined, + }, + } +} + +function readUser(value: unknown): AwarenessUser | null { + if (!isRecord(value)) { + return null + } + + if (typeof value.id !== 'string' || typeof value.name !== 'string' || typeof value.color !== 'string') { + return null + } + + return { + id: value.id, + name: value.name, + color: value.color, + } +} + +function readStoredLocalUser(): AwarenessUser | null { + try { + const value = window.localStorage.getItem(LOCAL_USER_STORAGE_KEY) + if (!value) { + return null + } + return readUser(JSON.parse(value)) + } catch { + return null + } +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} diff --git a/examples/yjs-rich-markdown-demo/src/lib/provider-awareness.ts b/examples/yjs-rich-markdown-demo/src/lib/provider-awareness.ts new file mode 100644 index 0000000..e474617 --- /dev/null +++ b/examples/yjs-rich-markdown-demo/src/lib/provider-awareness.ts @@ -0,0 +1,124 @@ +import type { YjsProvider } from '@durable-streams/y-durable-streams' + +const AWARENESS_HEARTBEAT_INTERVAL_MS = 15_000 +const PROVIDER_AWARENESS_PATCHED = Symbol('yjs-rich-markdown-demo.provider-awareness-patched') + +type ConnectionContext = { + controller: AbortController + id: number +} + +type RuntimeProvider = { + [PROVIDER_AWARENESS_PATCHED]?: boolean + awareness?: { + clientID: number + } + awarenessHeartbeat: ReturnType | null + headers: HeadersInit | undefined + broadcastAwareness: () => void + applyAwarenessUpdates: (bytes: Uint8Array) => void + awarenessUrl: (name?: string) => string + startAwareness: (ctx: ConnectionContext) => void + subscribeAwareness: (ctx: ConnectionContext) => Promise + connected: boolean + connecting: boolean +} + +export function patchProviderAwareness(provider: YjsProvider): void { + const runtimeProvider = provider as unknown as RuntimeProvider + if (runtimeProvider[PROVIDER_AWARENESS_PATCHED]) { + return + } + + runtimeProvider[PROVIDER_AWARENESS_PATCHED] = true + + runtimeProvider.startAwareness = function startAwareness(ctx: ConnectionContext): void { + if (!this.awareness || ctx.controller.signal.aborted) { + return + } + + this.broadcastAwareness() + + this.awarenessHeartbeat = setInterval(() => { + this.broadcastAwareness() + }, AWARENESS_HEARTBEAT_INTERVAL_MS) + + void this.subscribeAwareness(ctx) + } + + runtimeProvider.subscribeAwareness = async function subscribeAwareness(ctx: ConnectionContext): Promise { + if (!this.awareness) { + return + } + + const signal = ctx.controller.signal + let offset = '-1' + + try { + while (!signal.aborted && (this.connected || this.connecting)) { + const url = new URL(this.awarenessUrl()) + url.searchParams.set('offset', offset) + + if (offset !== '-1') { + url.searchParams.set('live', 'long-poll') + } + + const response = await fetch(url, { + method: 'GET', + headers: this.headers, + signal, + }) + + if (!response.ok) { + throw new Error(`Awareness request failed: ${response.status} ${response.statusText}`) + } + + const nextOffset = response.headers.get('stream-next-offset') + const upToDate = response.headers.has('stream-up-to-date') + const bytes = new Uint8Array(await response.arrayBuffer()) + + if (bytes.length > 0) { + this.applyAwarenessUpdates(bytes) + } + + if (nextOffset) { + offset = nextOffset + } + + if (!upToDate && offset === '-1') { + } + } + } catch (error) { + if (signal.aborted || (!this.connected && !this.connecting)) { + return + } + + console.error('Provider awareness subscription failed:', error) + await delay(1000, signal) + + if (this.connected) { + await this.subscribeAwareness(ctx) + } + } + } +} + +async function delay(ms: number, signal: AbortSignal): Promise { + if (signal.aborted) { + return + } + + await new Promise((resolve) => { + const timeout = globalThis.setTimeout(() => { + signal.removeEventListener('abort', onAbort) + resolve() + }, ms) + + const onAbort = () => { + globalThis.clearTimeout(timeout) + resolve() + } + + signal.addEventListener('abort', onAbort, { once: true }) + }) +} diff --git a/examples/yjs-rich-markdown-demo/src/main.tsx b/examples/yjs-rich-markdown-demo/src/main.tsx new file mode 100644 index 0000000..248ab6d --- /dev/null +++ b/examples/yjs-rich-markdown-demo/src/main.tsx @@ -0,0 +1,18 @@ +import { createRouter, RouterProvider } from '@tanstack/react-router' +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import { routeTree } from './routeTree.gen' + +const router = createRouter({ routeTree }) + +declare module '@tanstack/react-router' { + interface Register { + router: typeof router + } +} + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/examples/yjs-rich-markdown-demo/src/providers/ArtifactProvider.tsx b/examples/yjs-rich-markdown-demo/src/providers/ArtifactProvider.tsx new file mode 100644 index 0000000..144d095 --- /dev/null +++ b/examples/yjs-rich-markdown-demo/src/providers/ArtifactProvider.tsx @@ -0,0 +1,132 @@ +import { YjsProvider } from '@durable-streams/y-durable-streams' +import { RichMarkdownArtifactStore } from '@humanlayer/yjs-rich-markdown' +import { createContext, type PropsWithChildren, useContext, useEffect, useState } from 'react' +import { Awareness } from 'y-protocols/awareness.js' +import * as Y from 'yjs' +import { type AwarenessUser, getOrCreateLocalUser } from '../lib/collaboration' +import { patchProviderAwareness } from '../lib/provider-awareness' + +export type ArtifactSession = { + doc: Y.Doc + store: RichMarkdownArtifactStore + awareness: Awareness + provider: YjsProvider + localUser: AwarenessUser + connectionStatus: 'connecting' | 'connected' | 'disconnected' + isSynced: boolean +} + +const ArtifactSessionContext = createContext(null) + +export function useArtifactSession(): ArtifactSession { + const session = useContext(ArtifactSessionContext) + if (!session) { + throw new Error('useArtifactSession must be used within ArtifactProvider') + } + return session +} + +export function ArtifactProvider({ children }: PropsWithChildren) { + const [session, setSession] = useState(null) + const [error, setError] = useState(null) + + useEffect(() => { + let destroyed = false + let provider: YjsProvider | null = null + + async function init() { + const doc = new Y.Doc() + const awareness = new Awareness(doc) + const store = new RichMarkdownArtifactStore(doc) + const localUser = getOrCreateLocalUser() + + awareness.setLocalStateField('user', localUser) + awareness.setLocalStateField('presence', { artifactPath: undefined }) + + const docId = new URLSearchParams(window.location.search).get('doc') ?? 'rich-markdown-learning' + const baseUrl = `${window.location.origin}/v1/yjs/rich-markdown-demo` + + provider = new YjsProvider({ + doc, + awareness, + baseUrl, + docId, + connect: false, + liveMode: 'long-poll', + }) + + patchProviderAwareness(provider) + + const updateStatus = () => { + if (destroyed) return + setSession((prev) => + prev + ? { + ...prev, + connectionStatus: provider!.connected + ? 'connected' + : provider!.connecting + ? 'connecting' + : 'disconnected', + isSynced: provider!.synced, + } + : prev, + ) + } + + provider.on('status', updateStatus) + provider.on('synced', updateStatus) + + await provider.connect() + + if (destroyed) { + provider.destroy() + return + } + + setSession({ + doc, + store, + awareness, + provider, + localUser, + connectionStatus: provider.connected ? 'connected' : 'connecting', + isSynced: provider.synced, + }) + } + + init().catch((err) => { + if (!destroyed) { + setError(err) + } + }) + + return () => { + destroyed = true + if (provider) { + provider.flush().finally(() => provider?.destroy()) + } + } + }, []) + + if (error) { + return ( +
+

Connection Error

+
{error.message}
+
+ ) + } + + if (!session) { + return ( +
+
Connecting to Durable Streams...
+
+ ) + } + + return {children} +} diff --git a/examples/yjs-rich-markdown-demo/src/routeTree.gen.ts b/examples/yjs-rich-markdown-demo/src/routeTree.gen.ts new file mode 100644 index 0000000..afa5470 --- /dev/null +++ b/examples/yjs-rich-markdown-demo/src/routeTree.gen.ts @@ -0,0 +1,131 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as SingleRouteImport } from './routes/single' +import { Route as SameDocRouteImport } from './routes/same-doc' +import { Route as CompareRouteImport } from './routes/compare' +import { Route as AwarenessRouteImport } from './routes/awareness' +import { Route as IndexRouteImport } from './routes/index' + +const SingleRoute = SingleRouteImport.update({ + id: '/single', + path: '/single', + getParentRoute: () => rootRouteImport, +} as any) +const SameDocRoute = SameDocRouteImport.update({ + id: '/same-doc', + path: '/same-doc', + getParentRoute: () => rootRouteImport, +} as any) +const CompareRoute = CompareRouteImport.update({ + id: '/compare', + path: '/compare', + getParentRoute: () => rootRouteImport, +} as any) +const AwarenessRoute = AwarenessRouteImport.update({ + id: '/awareness', + path: '/awareness', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/awareness': typeof AwarenessRoute + '/compare': typeof CompareRoute + '/same-doc': typeof SameDocRoute + '/single': typeof SingleRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/awareness': typeof AwarenessRoute + '/compare': typeof CompareRoute + '/same-doc': typeof SameDocRoute + '/single': typeof SingleRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/awareness': typeof AwarenessRoute + '/compare': typeof CompareRoute + '/same-doc': typeof SameDocRoute + '/single': typeof SingleRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/awareness' | '/compare' | '/same-doc' | '/single' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/awareness' | '/compare' | '/same-doc' | '/single' + id: '__root__' | '/' | '/awareness' | '/compare' | '/same-doc' | '/single' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + AwarenessRoute: typeof AwarenessRoute + CompareRoute: typeof CompareRoute + SameDocRoute: typeof SameDocRoute + SingleRoute: typeof SingleRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/single': { + id: '/single' + path: '/single' + fullPath: '/single' + preLoaderRoute: typeof SingleRouteImport + parentRoute: typeof rootRouteImport + } + '/same-doc': { + id: '/same-doc' + path: '/same-doc' + fullPath: '/same-doc' + preLoaderRoute: typeof SameDocRouteImport + parentRoute: typeof rootRouteImport + } + '/compare': { + id: '/compare' + path: '/compare' + fullPath: '/compare' + preLoaderRoute: typeof CompareRouteImport + parentRoute: typeof rootRouteImport + } + '/awareness': { + id: '/awareness' + path: '/awareness' + fullPath: '/awareness' + preLoaderRoute: typeof AwarenessRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + AwarenessRoute: AwarenessRoute, + CompareRoute: CompareRoute, + SameDocRoute: SameDocRoute, + SingleRoute: SingleRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/examples/yjs-rich-markdown-demo/src/routes/__root.tsx b/examples/yjs-rich-markdown-demo/src/routes/__root.tsx new file mode 100644 index 0000000..45ed9a4 --- /dev/null +++ b/examples/yjs-rich-markdown-demo/src/routes/__root.tsx @@ -0,0 +1,57 @@ +import { createRootRoute, Link, Outlet } from '@tanstack/react-router' +import { ConnectionStatus } from '../components/ConnectionStatus' +import { ArtifactProvider } from '../providers/ArtifactProvider' + +export const Route = createRootRoute({ + component: RootLayout, +}) + +function RootLayout() { + return ( + +
+ + +
+ +
+
+
+ ) +} diff --git a/examples/yjs-rich-markdown-demo/src/routes/awareness.tsx b/examples/yjs-rich-markdown-demo/src/routes/awareness.tsx new file mode 100644 index 0000000..c89cf4d --- /dev/null +++ b/examples/yjs-rich-markdown-demo/src/routes/awareness.tsx @@ -0,0 +1,116 @@ +import { createFileRoute } from '@tanstack/react-router' +import { useEffect, useState } from 'react' +import { AwarenessDebugPanel } from '../components/AwarenessDebugPanel' +import { useArtifactSession } from '../providers/ArtifactProvider' + +export const Route = createFileRoute('/awareness')({ + component: AwarenessRoute, +}) + +function AwarenessRoute() { + const { awareness, localUser } = useArtifactSession() + const [simulatedPath, setSimulatedPath] = useState('') + const [rawState, setRawState] = useState('') + + useEffect(() => { + const update = () => { + setRawState(JSON.stringify(awareness.getLocalState(), null, 2)) + } + update() + awareness.on('change', update) + return () => awareness.off('change', update) + }, [awareness]) + + const setPresencePath = (path: string) => { + setSimulatedPath(path) + awareness.setLocalStateField('presence', { artifactPath: path || undefined }) + } + + return ( +
+

Awareness Debug View

+ +
+

Local User

+
+
+ + {localUser.name} ({localUser.id.slice(0, 8)}) + +
+
+ +
+

Simulate Presence

+

+ Set your artifact path to simulate being in an editor. Open multiple tabs to test awareness + visibility. +

+
+ setPresencePath(e.target.value)} + placeholder="e.g. /artifacts/plan.md" + style={{ padding: '8px 12px', width: 300, border: '1px solid #ccc', borderRadius: 4 }} + /> + + + +
+
+ +
+

Local Awareness State

+
+					{rawState}
+				
+
+ +
+

All Clients

+ +
+ +
+

Filtered: /artifacts/plan.md

+ +
+ +
+

Filtered: /artifacts/research.md

+ +
+
+ ) +} diff --git a/examples/yjs-rich-markdown-demo/src/routes/compare.tsx b/examples/yjs-rich-markdown-demo/src/routes/compare.tsx new file mode 100644 index 0000000..92416a8 --- /dev/null +++ b/examples/yjs-rich-markdown-demo/src/routes/compare.tsx @@ -0,0 +1,77 @@ +import { createFileRoute, useNavigate } from '@tanstack/react-router' +import { ArtifactEditor } from '../components/ArtifactEditor' +import { AwarenessDebugPanel } from '../components/AwarenessDebugPanel' +import { MarkdownExportPanel } from '../components/MarkdownExportPanel' +import { useArtifactSession } from '../providers/ArtifactProvider' + +type CompareSearch = { + left: string + right: string +} + +export const Route = createFileRoute('/compare')({ + component: CompareRoute, + validateSearch: (search: Record): CompareSearch => ({ + left: typeof search.left === 'string' ? search.left : '/artifacts/plan.md', + right: typeof search.right === 'string' ? search.right : '/artifacts/research.md', + }), +}) + +function CompareRoute() { + const { left, right } = Route.useSearch() + const navigate = useNavigate({ from: '/compare' }) + const { store } = useArtifactSession() + + store.ensureArtifact(left) + store.ensureArtifact(right) + + return ( +
+

Compare: Two Different Artifacts

+

+ Open this page in two browser tabs. Edit different artifacts and verify cursors do NOT leak between + them. +

+ +
+ + +
+ +
+
+

{left}

+
+ +
+ +
+
+

{right}

+
+ +
+ +
+
+ + +
+ ) +} diff --git a/examples/yjs-rich-markdown-demo/src/routes/index.tsx b/examples/yjs-rich-markdown-demo/src/routes/index.tsx new file mode 100644 index 0000000..733dae1 --- /dev/null +++ b/examples/yjs-rich-markdown-demo/src/routes/index.tsx @@ -0,0 +1,103 @@ +import { createFileRoute, Link } from '@tanstack/react-router' +import { useState } from 'react' +import { ArtifactList } from '../components/ArtifactList' +import { useArtifactSession } from '../providers/ArtifactProvider' + +export const Route = createFileRoute('/')({ + component: IndexRoute, +}) + +function IndexRoute() { + const { store, connectionStatus, isSynced } = useArtifactSession() + const [newPath, setNewPath] = useState('/artifacts/new.md') + + const createArtifact = () => { + try { + store.ensureArtifact(newPath) + } catch (err) { + alert(String(err)) + } + } + + return ( +
+

Rich Markdown Learning Demo

+ +
+

Connection Status

+
    +
  • Status: {connectionStatus}
  • +
  • Synced: {isSynced ? 'Yes' : 'No'}
  • +
+
+ +
+

Create Artifact

+
+ setNewPath(e.target.value)} + style={{ padding: '8px 12px', border: '1px solid #ccc', borderRadius: 4, width: 300 }} + /> + +
+
+ +
+

Existing Artifacts

+ +
+ +
+

Learning Scenarios

+
    +
  • + + Single Editor + {' '} + - Edit one artifact +
  • +
  • + + Compare + {' '} + - Two editors, two different artifact paths +
  • +
  • + + Same Doc + {' '} + - Two editors, same artifact path +
  • +
  • + Awareness Debug - Raw awareness state inspection +
  • +
+
+ +
+

Learning Goals

+
    +
  1. Multiple TipTap fragments in one Y.Doc keep content isolated
  2. +
  3. Durable Streams syncs all fragments in one document
  4. +
  5. Users editing the same artifact see each other's cursors
  6. +
  7. Users editing different artifacts do NOT see each other's cursors
  8. +
  9. Markdown export works per artifact
  10. +
+
+
+ ) +} diff --git a/examples/yjs-rich-markdown-demo/src/routes/same-doc.tsx b/examples/yjs-rich-markdown-demo/src/routes/same-doc.tsx new file mode 100644 index 0000000..93dd7d9 --- /dev/null +++ b/examples/yjs-rich-markdown-demo/src/routes/same-doc.tsx @@ -0,0 +1,66 @@ +import { createFileRoute, useNavigate } from '@tanstack/react-router' +import { ArtifactEditor } from '../components/ArtifactEditor' +import { AwarenessDebugPanel } from '../components/AwarenessDebugPanel' +import { MarkdownExportPanel } from '../components/MarkdownExportPanel' +import { useArtifactSession } from '../providers/ArtifactProvider' + +type SameDocSearch = { + path: string +} + +export const Route = createFileRoute('/same-doc')({ + component: SameDocRoute, + validateSearch: (search: Record): SameDocSearch => ({ + path: typeof search.path === 'string' ? search.path : '/artifacts/plan.md', + }), +}) + +function SameDocRoute() { + const { path } = Route.useSearch() + const navigate = useNavigate({ from: '/same-doc' }) + const { store } = useArtifactSession() + + store.ensureArtifact(path) + + return ( +
+

Same Doc: Two Editors, One Artifact

+

+ Both editors show the same artifact. Open in two browser tabs to verify cursors are visible across + clients. +

+ +
+ +
+ +
+
+

Editor A: {path}

+
+ +
+
+
+

Editor B: {path}

+
+ +
+
+
+ +
+ + +
+
+ ) +} diff --git a/examples/yjs-rich-markdown-demo/src/routes/single.tsx b/examples/yjs-rich-markdown-demo/src/routes/single.tsx new file mode 100644 index 0000000..41e003f --- /dev/null +++ b/examples/yjs-rich-markdown-demo/src/routes/single.tsx @@ -0,0 +1,66 @@ +import { createFileRoute, useNavigate } from '@tanstack/react-router' +import { ArtifactEditor } from '../components/ArtifactEditor' +import { AwarenessDebugPanel } from '../components/AwarenessDebugPanel' +import { MarkdownExportPanel } from '../components/MarkdownExportPanel' +import { useArtifactSession } from '../providers/ArtifactProvider' + +type SingleSearch = { + path: string + mode: 'fragment' | 'field' +} + +export const Route = createFileRoute('/single')({ + component: SingleRoute, + validateSearch: (search: Record): SingleSearch => ({ + path: typeof search.path === 'string' ? search.path : '/artifacts/plan.md', + mode: search.mode === 'field' ? 'field' : 'fragment', + }), +}) + +function SingleRoute() { + const { path, mode } = Route.useSearch() + const navigate = useNavigate({ from: '/single' }) + const { store } = useArtifactSession() + + store.ensureArtifact(path) + + return ( +
+
+ + +
+ +
+
+

Editor: {path}

+ +
+
+ + +
+
+
+ ) +} diff --git a/examples/yjs-rich-markdown-demo/tsconfig.json b/examples/yjs-rich-markdown-demo/tsconfig.json new file mode 100644 index 0000000..3c6105c --- /dev/null +++ b/examples/yjs-rich-markdown-demo/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "customConditions": ["source"], + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + "paths": { + "@/*": ["./src/*"] + }, + "types": ["vite/client"] + }, + "include": ["src"] +} diff --git a/examples/yjs-rich-markdown-demo/vite.config.ts b/examples/yjs-rich-markdown-demo/vite.config.ts new file mode 100644 index 0000000..57c1a8e --- /dev/null +++ b/examples/yjs-rich-markdown-demo/vite.config.ts @@ -0,0 +1,20 @@ +import { TanStackRouterVite } from '@tanstack/router-plugin/vite' +import react from '@vitejs/plugin-react' +import { defineConfig } from 'vite' + +export default defineConfig({ + plugins: [TanStackRouterVite(), react()], + resolve: { + conditions: ['source'], + }, + server: { + port: 5176, + strictPort: true, + proxy: { + '/v1/yjs': { + target: 'http://127.0.0.1:4438', + changeOrigin: true, + }, + }, + }, +}) diff --git a/package.json b/package.json index 1ed744b..9efb3d6 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "test": "bun test --concurrency 16 **/*", "test:ci": "bun test --conditions=source ./packages/*/test/*.test.ts ./agents/*/test/*.test.ts", "typecheck": "bun --filter '*' --parallel typecheck", + "hook:prepush": "bun run test:ci && bun run typecheck && bun run biome:check", "release:prepare": "bun run scripts/release/prepare.ts", "release:publish": "bun run scripts/release/publish.ts" }, @@ -33,7 +34,8 @@ }, "workspaces": [ "packages/*", - "agents/*" + "agents/*", + "examples/*" ], "catalog": { "@ai-sdk/anthropic": "3.0.69", diff --git a/packages/agentlayer-core/models.json b/packages/agentlayer-core/models.json index e4ec7e4..ac40f0c 100644 --- a/packages/agentlayer-core/models.json +++ b/packages/agentlayer-core/models.json @@ -4187,6 +4187,37 @@ "output": 8192 } }, + "qwen3.6-max-preview": { + "id": "qwen3.6-max-preview", + "name": "Qwen3.6 Max Preview", + "family": "qwen", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "knowledge": "2025-04", + "release_date": "2026-04-20", + "last_updated": "2026-04-20", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 1.3, + "output": 7.8, + "cache_read": 0.13, + "cache_write": 1.625 + }, + "limit": { + "context": 262144, + "output": 65536 + } + }, "qwen2-5-omni-7b": { "id": "qwen2-5-omni-7b", "name": "Qwen2.5-Omni 7B", @@ -26205,6 +26236,35 @@ "output": 65536 } }, + "openrouter/owl-alpha": { + "id": "openrouter/owl-alpha", + "name": "Owl Alpha", + "attachment": false, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "temperature": true, + "release_date": "2026-04-28", + "last_updated": "2026-04-30", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0, + "output": 0 + }, + "limit": { + "context": 1048756, + "output": 262144 + }, + "status": "alpha" + }, "openrouter/pareto-code": { "id": "openrouter/pareto-code", "name": "Pareto Code Router", @@ -27362,6 +27422,70 @@ "output": 8192 } }, + "poolside/laguna-m.1:free": { + "id": "poolside/laguna-m.1:free", + "name": "Laguna M.1", + "attachment": false, + "reasoning": true, + "tool_call": true, + "interleaved": { + "field": "reasoning_content" + }, + "temperature": true, + "release_date": "2026-04-28", + "last_updated": "2026-04-28", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0, + "output": 0, + "cache_read": 0, + "cache_write": 0 + }, + "limit": { + "context": 131072, + "output": 8192 + } + }, + "poolside/laguna-xs.2:free": { + "id": "poolside/laguna-xs.2:free", + "name": "Laguna XS.2", + "attachment": false, + "reasoning": true, + "tool_call": true, + "interleaved": { + "field": "reasoning_content" + }, + "temperature": true, + "release_date": "2026-04-28", + "last_updated": "2026-04-28", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0, + "output": 0, + "cache_read": 0, + "cache_write": 0 + }, + "limit": { + "context": 131072, + "output": 8192 + } + }, "prime-intellect/intellect-3": { "id": "prime-intellect/intellect-3", "name": "Intellect 3", @@ -48564,42 +48688,6 @@ "npm": "@ai-sdk/openai-compatible" } }, - "glm-5": { - "id": "glm-5", - "name": "GLM-5", - "family": "glm", - "attachment": false, - "reasoning": true, - "tool_call": true, - "interleaved": { - "field": "reasoning_content" - }, - "structured_output": true, - "temperature": true, - "release_date": "2026-02-12", - "last_updated": "2026-02-12", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 1, - "output": 3.2, - "cache_read": 0.2 - }, - "limit": { - "context": 202752, - "output": 131000 - }, - "provider": { - "npm": "@ai-sdk/openai-compatible" - } - }, "gemini-3.1-pro-preview": { "id": "gemini-3.1-pro-preview", "name": "Gemini 3.1 Pro Preview", @@ -48914,6 +49002,85 @@ "context": 400000, "output": 128000 } + }, + "gpt-5.5": { + "id": "gpt-5.5", + "name": "GPT-5.5", + "family": "gpt", + "attachment": true, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "temperature": false, + "knowledge": "2025-12-01", + "release_date": "2026-04-23", + "last_updated": "2026-04-23", + "modalities": { + "input": [ + "text", + "image", + "pdf" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 5, + "output": 30, + "cache_read": 0.5, + "context_over_200k": { + "input": 10, + "output": 45, + "cache_read": 1 + } + }, + "limit": { + "context": 1050000, + "input": 922000, + "output": 128000 + }, + "provider": { + "npm": "@ai-sdk/openai-compatible" + } + }, + "deepseek-v4-pro": { + "id": "deepseek-v4-pro", + "name": "DeepSeek V4 Pro", + "family": "deepseek-thinking", + "attachment": false, + "reasoning": true, + "tool_call": true, + "interleaved": { + "field": "reasoning_content" + }, + "structured_output": true, + "temperature": true, + "knowledge": "2025-05", + "release_date": "2026-04-24", + "last_updated": "2026-04-24", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 1.74, + "output": 3.48, + "cache_read": 0.145 + }, + "limit": { + "context": 1000000, + "output": 384000 + }, + "provider": { + "npm": "@ai-sdk/openai-compatible" + } } } }, @@ -52481,6 +52648,34 @@ "output": 8192 } }, + "ibm-granite/granite-4.1-8b": { + "id": "ibm-granite/granite-4.1-8b", + "name": "IBM: Granite 4.1 8B", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2026-04-30", + "last_updated": "2026-05-01", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.05, + "output": 0.1, + "cache_read": 0.05 + }, + "limit": { + "context": 131072, + "output": 131072 + } + }, "ibm-granite/granite-4.0-h-micro": { "id": "ibm-granite/granite-4.0-h-micro", "name": "IBM: Granite 4.0 Micro", @@ -52866,6 +53061,62 @@ "output": 65536 } }, + "deepseek/deepseek-v4-flash": { + "id": "deepseek/deepseek-v4-flash", + "name": "DeepSeek: DeepSeek V4 Flash", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-04-24", + "last_updated": "2026-05-01", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.14, + "output": 0.28, + "cache_read": 0.0028 + }, + "limit": { + "context": 1048576, + "output": 384000 + } + }, + "deepseek/deepseek-v4-pro": { + "id": "deepseek/deepseek-v4-pro", + "name": "DeepSeek: DeepSeek V4 Pro", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-04-24", + "last_updated": "2026-05-01", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.435, + "output": 0.87, + "cache_read": 0.003625 + }, + "limit": { + "context": 1048576, + "output": 384000 + } + }, "deepseek/deepseek-v3.2": { "id": "deepseek/deepseek-v3.2", "name": "DeepSeek: DeepSeek V3.2", @@ -53037,16 +53288,16 @@ }, "status": "beta" }, - "openrouter/elephant-alpha": { - "id": "openrouter/elephant-alpha", - "name": "Elephant (new)", + "openrouter/owl-alpha": { + "id": "openrouter/owl-alpha", + "name": "Owl Alpha", "attachment": false, "reasoning": true, "tool_call": true, "structured_output": true, "temperature": true, - "release_date": "2026-01-15", - "last_updated": "2026-04-13", + "release_date": "2026-04-28", + "last_updated": "2026-04-30", "modalities": { "input": [ "text" @@ -53061,11 +53312,38 @@ "output": 0 }, "limit": { - "context": 262144, - "output": 32768 + "context": 1048756, + "output": 262144 }, "status": "alpha" }, + "openrouter/pareto-code": { + "id": "openrouter/pareto-code", + "name": "Pareto Code Router", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": false, + "release_date": "2026-04-21", + "last_updated": "2026-05-01", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0, + "output": 0 + }, + "limit": { + "context": 200000, + "output": 65536 + } + }, "openrouter/free": { "id": "openrouter/free", "name": "Free Models Router", @@ -53094,6 +53372,61 @@ "output": 32768 } }, + "inclusionai/ling-2.6-1t:free": { + "id": "inclusionai/ling-2.6-1t:free", + "name": "inclusionAI: Ling-2.6-1T (free)", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2026-04-23", + "last_updated": "2026-05-01", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0, + "output": 0 + }, + "limit": { + "context": 262144, + "output": 32768 + } + }, + "inclusionai/ling-2.6-flash": { + "id": "inclusionai/ling-2.6-flash", + "name": "inclusionAI: Ling-2.6 Flash", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2026-04-21", + "last_updated": "2026-05-01", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.08, + "output": 0.24, + "cache_read": 0.016 + }, + "limit": { + "context": 262144, + "output": 32768 + } + }, "arcee-ai/trinity-mini": { "id": "arcee-ai/trinity-mini", "name": "Arcee AI: Trinity Mini", @@ -53203,33 +53536,6 @@ "output": 65537 } }, - "arcee-ai/trinity-large-thinking:free": { - "id": "arcee-ai/trinity-large-thinking:free", - "name": "Arcee AI: Trinity Large Thinking (free)", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2025-08-26", - "last_updated": "2026-04-11", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0, - "output": 0 - }, - "limit": { - "context": 262144, - "output": 262144 - } - }, "arcee-ai/maestro-reasoning": { "id": "arcee-ai/maestro-reasoning", "name": "Arcee AI: Maestro Reasoning", @@ -53284,6 +53590,33 @@ "output": 32768 } }, + "arcee-ai/trinity-large-preview": { + "id": "arcee-ai/trinity-large-preview", + "name": "Arcee AI: Trinity Large Preview", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2026-01-28", + "last_updated": "2026-05-01", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.15, + "output": 0.45 + }, + "limit": { + "context": 131000, + "output": 32768 + } + }, "deepcogito/cogito-v2.1-671b": { "id": "deepcogito/cogito-v2.1-671b", "name": "Deep Cogito: Cogito v2.1 671B", @@ -53365,34 +53698,6 @@ "output": 163840 } }, - "bytedance-seed/dola-seed-2.0-pro:free": { - "id": "bytedance-seed/dola-seed-2.0-pro:free", - "name": "ByteDance Seed: Dola Seed 2.0 Pro (free)", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2025-08-26", - "last_updated": "2026-04-11", - "modalities": { - "input": [ - "image", - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0, - "output": 0 - }, - "limit": { - "context": 256000, - "output": 128000 - } - }, "bytedance-seed/seed-1.6": { "id": "bytedance-seed/seed-1.6", "name": "ByteDance Seed: Seed 1.6", @@ -53563,6 +53868,72 @@ "output": 2048 } }, + "~google/gemini-pro-latest": { + "id": "~google/gemini-pro-latest", + "name": "Google: Gemini Pro Latest", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-04-27", + "last_updated": "2026-05-01", + "modalities": { + "input": [ + "text", + "image", + "audio", + "video", + "pdf" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 2, + "output": 12, + "cache_read": 0.2, + "cache_write": 0.375 + }, + "limit": { + "context": 1048576, + "output": 65536 + } + }, + "~google/gemini-flash-latest": { + "id": "~google/gemini-flash-latest", + "name": "Google: Gemini Flash Latest", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-04-27", + "last_updated": "2026-05-01", + "modalities": { + "input": [ + "text", + "image", + "audio", + "video", + "pdf" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.5, + "output": 3, + "cache_read": 0.05, + "cache_write": 0.08333333333333334 + }, + "limit": { + "context": 1048576, + "output": 65536 + } + }, "kilo-auto/balanced": { "id": "kilo-auto/balanced", "name": "Kilo Auto Balanced", @@ -53700,32 +54071,6 @@ "output": 4096 } }, - "allenai/olmo-2-0325-32b-instruct": { - "id": "allenai/olmo-2-0325-32b-instruct", - "name": "AllenAI: Olmo 2 32B Instruct", - "attachment": false, - "reasoning": false, - "tool_call": false, - "release_date": "2025-03-15", - "last_updated": "2026-03-15", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.05, - "output": 0.2 - }, - "limit": { - "context": 128000, - "output": 32768 - } - }, "allenai/olmo-3.1-32b-instruct": { "id": "allenai/olmo-3.1-32b-instruct", "name": "AllenAI: Olmo 3.1 32B Instruct", @@ -53970,15 +54315,15 @@ "output": 131072 } }, - "eleutherai/llemma_7b": { - "id": "eleutherai/llemma_7b", - "name": "EleutherAI: Llemma 7b", + "stepfun/step-3.5-flash:free": { + "id": "stepfun/step-3.5-flash:free", + "name": "StepFun: Step 3.5 Flash (free)", "attachment": false, - "reasoning": false, - "tool_call": false, + "reasoning": true, + "tool_call": true, "temperature": true, - "release_date": "2025-04-14", - "last_updated": "2026-03-15", + "release_date": "2025-08-26", + "last_updated": "2026-05-01", "modalities": { "input": [ "text" @@ -53987,14 +54332,14 @@ "text" ] }, - "open_weights": true, + "open_weights": false, "cost": { - "input": 0.8, - "output": 1.2 + "input": 0, + "output": 0 }, "limit": { - "context": 4096, - "output": 4096 + "context": 262144, + "output": 262144 } }, "stepfun/step-3.5-flash": { @@ -54606,32 +54951,6 @@ "output": 131072 } }, - "mistralai/mistral-small-creative": { - "id": "mistralai/mistral-small-creative", - "name": "Mistral: Mistral Small Creative", - "attachment": false, - "reasoning": false, - "tool_call": true, - "release_date": "2025-12-17", - "last_updated": "2026-03-15", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.1, - "output": 0.3 - }, - "limit": { - "context": 32768, - "output": 32768 - } - }, "mistralai/devstral-small": { "id": "mistralai/devstral-small", "name": "Mistral: Devstral Small 1.1", @@ -54741,6 +55060,99 @@ "output": 52429 } }, + "~anthropic/claude-haiku-latest": { + "id": "~anthropic/claude-haiku-latest", + "name": "Anthropic: Claude Haiku Latest", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-04-27", + "last_updated": "2026-05-01", + "modalities": { + "input": [ + "text", + "image", + "pdf" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 1, + "output": 5, + "cache_read": 0.1, + "cache_write": 1.25 + }, + "limit": { + "context": 200000, + "output": 64000 + } + }, + "~anthropic/claude-sonnet-latest": { + "id": "~anthropic/claude-sonnet-latest", + "name": "Anthropic: Claude Sonnet Latest", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-04-27", + "last_updated": "2026-05-01", + "modalities": { + "input": [ + "text", + "image", + "pdf" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 3, + "output": 15, + "cache_read": 0.3, + "cache_write": 3.75 + }, + "limit": { + "context": 1000000, + "output": 128000 + } + }, + "~anthropic/claude-opus-latest": { + "id": "~anthropic/claude-opus-latest", + "name": "Anthropic: Claude Opus Latest", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": false, + "release_date": "2026-04-16", + "last_updated": "2026-05-01", + "modalities": { + "input": [ + "text", + "image", + "pdf" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 5, + "output": 25, + "cache_read": 0.5, + "cache_write": 6.25 + }, + "limit": { + "context": 1000000, + "output": 128000 + } + }, "meta-llama/llama-3.3-70b-instruct": { "id": "meta-llama/llama-3.3-70b-instruct", "name": "Meta: Llama 3.3 70B Instruct", @@ -55126,6 +55538,36 @@ "output": 10000 } }, + "x-ai/grok-4.3": { + "id": "x-ai/grok-4.3", + "name": "xAI: Grok 4.3", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-05-01", + "last_updated": "2026-05-01", + "modalities": { + "input": [ + "text", + "image", + "pdf" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 1.25, + "output": 2.5, + "cache_read": 0.2 + }, + "limit": { + "context": 1000000, + "output": 4096 + } + }, "x-ai/grok-4-fast": { "id": "x-ai/grok-4-fast", "name": "xAI: Grok 4 Fast", @@ -55383,6 +55825,33 @@ "output": 26215 } }, + "tencent/hy3-preview:free": { + "id": "tencent/hy3-preview:free", + "name": "Tencent: Hy3 Preview (free)", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-04-22", + "last_updated": "2026-05-01", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0, + "output": 0 + }, + "limit": { + "context": 262144, + "output": 262144 + } + }, "tencent/hunyuan-a13b-instruct": { "id": "tencent/hunyuan-a13b-instruct", "name": "Tencent: Hunyuan A13B Instruct", @@ -55626,6 +56095,60 @@ "output": 16384 } }, + "poolside/laguna-m.1:free": { + "id": "poolside/laguna-m.1:free", + "name": "Poolside: Laguna M.1 (free)", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-04-28", + "last_updated": "2026-05-01", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0, + "output": 0 + }, + "limit": { + "context": 131072, + "output": 8192 + } + }, + "poolside/laguna-xs.2:free": { + "id": "poolside/laguna-xs.2:free", + "name": "Poolside: Laguna XS.2 (free)", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-04-28", + "last_updated": "2026-05-01", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0, + "output": 0 + }, + "limit": { + "context": 131072, + "output": 8192 + } + }, "cohere/command-r7b-12-2024": { "id": "cohere/command-r7b-12-2024", "name": "Cohere: Command R7B (12-2024)", @@ -55816,42 +56339,45 @@ "output": 262144 } }, - "nvidia/nemotron-3-nano-30b-a3b": { - "id": "nvidia/nemotron-3-nano-30b-a3b", - "name": "NVIDIA: Nemotron 3 Nano 30B A3B", - "attachment": false, + "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free": { + "id": "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free", + "name": "NVIDIA: Nemotron 3 Nano Omni (free)", + "attachment": true, "reasoning": true, "tool_call": true, "temperature": true, - "release_date": "2024-12", - "last_updated": "2026-02-04", + "release_date": "2026-04-28", + "last_updated": "2026-05-01", "modalities": { "input": [ - "text" + "text", + "audio", + "image", + "video" ], "output": [ "text" ] }, - "open_weights": true, + "open_weights": false, "cost": { - "input": 0.05, - "output": 0.2 + "input": 0, + "output": 0 }, "limit": { - "context": 262144, - "output": 52429 + "context": 256000, + "output": 65536 } }, - "nvidia/llama-3.1-nemotron-ultra-253b-v1": { - "id": "nvidia/llama-3.1-nemotron-ultra-253b-v1", - "name": "NVIDIA: Llama 3.1 Nemotron Ultra 253B v1", + "nvidia/nemotron-3-nano-30b-a3b": { + "id": "nvidia/nemotron-3-nano-30b-a3b", + "name": "NVIDIA: Nemotron 3 Nano 30B A3B", "attachment": false, "reasoning": true, - "tool_call": false, + "tool_call": true, "temperature": true, - "release_date": "2025-04-08", - "last_updated": "2026-04-11", + "release_date": "2024-12", + "last_updated": "2026-02-04", "modalities": { "input": [ "text" @@ -55862,12 +56388,12 @@ }, "open_weights": true, "cost": { - "input": 0.6, - "output": 1.8 + "input": 0.05, + "output": 0.2 }, "limit": { - "context": 131072, - "output": 131072 + "context": 262144, + "output": 52429 } }, "nvidia/nemotron-nano-12b-v2-vl": { @@ -55980,34 +56506,6 @@ "output": 16384 } }, - "inception/mercury-edit-2": { - "id": "inception/mercury-edit-2", - "name": "Mercury Edit 2", - "attachment": false, - "reasoning": true, - "tool_call": false, - "temperature": true, - "release_date": "2026-03-30", - "last_updated": "2026-03-30", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.25, - "output": 0.75, - "cache_read": 0.025 - }, - "limit": { - "context": 128000, - "output": 8192 - } - }, "inception/mercury-2": { "id": "inception/mercury-2", "name": "Inception: Mercury 2", @@ -56294,6 +56792,36 @@ "output": 16384 } }, + "openai/gpt-5.5": { + "id": "openai/gpt-5.5", + "name": "OpenAI: GPT-5.5", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": false, + "release_date": "2026-04-24", + "last_updated": "2026-05-01", + "modalities": { + "input": [ + "text", + "image", + "pdf" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 5, + "output": 30, + "cache_read": 0.5 + }, + "limit": { + "context": 1050000, + "output": 128000 + } + }, "openai/gpt-5-mini": { "id": "openai/gpt-5-mini", "name": "OpenAI: GPT-5 Mini", @@ -57081,6 +57609,37 @@ "output": 100000 } }, + "openai/gpt-5.4-image-2": { + "id": "openai/gpt-5.4-image-2", + "name": "OpenAI: GPT-5.4 Image 2", + "attachment": true, + "reasoning": true, + "tool_call": false, + "temperature": false, + "release_date": "2026-04-21", + "last_updated": "2026-05-01", + "modalities": { + "input": [ + "image", + "text", + "pdf" + ], + "output": [ + "image", + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 8, + "output": 15, + "cache_read": 2 + }, + "limit": { + "context": 272000, + "output": 128000 + } + }, "openai/gpt-4": { "id": "openai/gpt-4", "name": "OpenAI: GPT-4", @@ -57221,35 +57780,6 @@ "output": 16384 } }, - "openai/gpt-4o:extended": { - "id": "openai/gpt-4o:extended", - "name": "OpenAI: GPT-4o (extended)", - "attachment": true, - "reasoning": false, - "tool_call": true, - "temperature": true, - "release_date": "2024-05-13", - "last_updated": "2026-03-15", - "modalities": { - "input": [ - "image", - "pdf", - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 6, - "output": 18 - }, - "limit": { - "context": 128000, - "output": 64000 - } - }, "openai/gpt-4o-search-preview": { "id": "openai/gpt-4o-search-preview", "name": "OpenAI: GPT-4o Search Preview", @@ -57621,6 +58151,35 @@ "output": 26215 } }, + "openai/gpt-5.5-pro": { + "id": "openai/gpt-5.5-pro", + "name": "OpenAI: GPT-5.5 Pro", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": false, + "release_date": "2026-04-24", + "last_updated": "2026-05-01", + "modalities": { + "input": [ + "text", + "image", + "pdf" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 30, + "output": 180 + }, + "limit": { + "context": 1050000, + "output": 128000 + } + }, "openai/gpt-3.5-turbo-instruct": { "id": "openai/gpt-3.5-turbo-instruct", "name": "OpenAI: GPT-3.5 Turbo Instruct", @@ -58273,6 +58832,34 @@ "output": 16000 } }, + "baidu/qianfan-ocr-fast:free": { + "id": "baidu/qianfan-ocr-fast:free", + "name": "Baidu: Qianfan-OCR-Fast (free)", + "attachment": true, + "reasoning": true, + "tool_call": false, + "temperature": true, + "release_date": "2026-04-20", + "last_updated": "2026-05-01", + "modalities": { + "input": [ + "image", + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0, + "output": 0 + }, + "limit": { + "context": 65536, + "output": 28672 + } + }, "baidu/ernie-4.5-vl-28b-a3b": { "id": "baidu/ernie-4.5-vl-28b-a3b", "name": "Baidu: ERNIE 4.5 VL 28B A3B", @@ -58630,6 +59217,66 @@ "output": 196608 } }, + "~openai/gpt-latest": { + "id": "~openai/gpt-latest", + "name": "OpenAI: GPT Latest", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": false, + "release_date": "2026-04-27", + "last_updated": "2026-05-01", + "modalities": { + "input": [ + "text", + "image", + "pdf" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 5, + "output": 30, + "cache_read": 0.5 + }, + "limit": { + "context": 1050000, + "output": 128000 + } + }, + "~openai/gpt-mini-latest": { + "id": "~openai/gpt-mini-latest", + "name": "OpenAI: GPT Mini Latest", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": false, + "release_date": "2026-04-27", + "last_updated": "2026-05-01", + "modalities": { + "input": [ + "text", + "image", + "pdf" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.75, + "output": 4.5, + "cache_read": 0.075 + }, + "limit": { + "context": 400000, + "output": 128000 + } + }, "qwen/qwen3-235b-a22b": { "id": "qwen/qwen3-235b-a22b", "name": "Qwen: Qwen3 235B A22B", @@ -58687,15 +59334,15 @@ "output": 65536 } }, - "qwen/qwen2.5-coder-7b-instruct": { - "id": "qwen/qwen2.5-coder-7b-instruct", - "name": "Qwen: Qwen2.5 Coder 7B Instruct", + "qwen/qwen3-coder-plus": { + "id": "qwen/qwen3-coder-plus", + "name": "Qwen: Qwen3 Coder Plus", "attachment": false, "reasoning": false, - "tool_call": false, + "tool_call": true, "temperature": true, - "release_date": "2024-09-17", - "last_updated": "2024-11", + "release_date": "2025-07-01", + "last_updated": "2026-03-15", "modalities": { "input": [ "text" @@ -58706,39 +59353,41 @@ }, "open_weights": true, "cost": { - "input": 0.03, - "output": 0.09 + "input": 0.65, + "output": 3.25, + "cache_read": 0.2 }, "limit": { - "context": 32768, - "output": 6554 + "context": 1000000, + "output": 65536 } }, - "qwen/qwen3-coder-plus": { - "id": "qwen/qwen3-coder-plus", - "name": "Qwen: Qwen3 Coder Plus", - "attachment": false, - "reasoning": false, + "qwen/qwen3.6-27b": { + "id": "qwen/qwen3.6-27b", + "name": "Qwen: Qwen3.6 27B", + "attachment": true, + "reasoning": true, "tool_call": true, "temperature": true, - "release_date": "2025-07-01", - "last_updated": "2026-03-15", + "release_date": "2026-04-27", + "last_updated": "2026-05-01", "modalities": { "input": [ - "text" + "text", + "image", + "video" ], "output": [ "text" ] }, - "open_weights": true, + "open_weights": false, "cost": { - "input": 0.65, - "output": 3.25, - "cache_read": 0.2 + "input": 0.325, + "output": 3.25 }, "limit": { - "context": 1000000, + "context": 256000, "output": 65536 } }, @@ -58997,42 +59646,44 @@ "output": 32768 } }, - "qwen/qwen3-vl-235b-a22b-thinking": { - "id": "qwen/qwen3-vl-235b-a22b-thinking", - "name": "Qwen: Qwen3 VL 235B A22B Thinking", + "qwen/qwen3.6-35b-a3b": { + "id": "qwen/qwen3.6-35b-a3b", + "name": "Qwen: Qwen3.6 35B A3B", "attachment": true, "reasoning": true, - "tool_call": true, + "tool_call": false, "temperature": true, - "release_date": "2025-09-24", - "last_updated": "2026-03-15", + "release_date": "2026-04-27", + "last_updated": "2026-05-01", "modalities": { "input": [ + "text", "image", - "text" + "video" ], "output": [ "text" ] }, - "open_weights": true, + "open_weights": false, "cost": { - "input": 0.26, - "output": 2.6 + "input": 0.1612, + "output": 0.96525, + "cache_read": 0.1612 }, "limit": { - "context": 131072, - "output": 32768 + "context": 262144, + "output": 65536 } }, - "qwen/qwen3-vl-30b-a3b-thinking": { - "id": "qwen/qwen3-vl-30b-a3b-thinking", - "name": "Qwen: Qwen3 VL 30B A3B Thinking", + "qwen/qwen3-vl-235b-a22b-thinking": { + "id": "qwen/qwen3-vl-235b-a22b-thinking", + "name": "Qwen: Qwen3 VL 235B A22B Thinking", "attachment": true, "reasoning": true, "tool_call": true, "temperature": true, - "release_date": "2025-10-11", + "release_date": "2025-09-24", "last_updated": "2026-03-15", "modalities": { "input": [ @@ -59045,22 +59696,22 @@ }, "open_weights": true, "cost": { - "input": 0.13, - "output": 1.56 + "input": 0.26, + "output": 2.6 }, "limit": { "context": 131072, "output": 32768 } }, - "qwen/qwen2.5-vl-32b-instruct": { - "id": "qwen/qwen2.5-vl-32b-instruct", - "name": "Qwen: Qwen2.5 VL 32B Instruct", + "qwen/qwen3-vl-30b-a3b-thinking": { + "id": "qwen/qwen3-vl-30b-a3b-thinking", + "name": "Qwen: Qwen3 VL 30B A3B Thinking", "attachment": true, - "reasoning": false, - "tool_call": false, + "reasoning": true, + "tool_call": true, "temperature": true, - "release_date": "2025-03-24", + "release_date": "2025-10-11", "last_updated": "2026-03-15", "modalities": { "input": [ @@ -59073,13 +59724,12 @@ }, "open_weights": true, "cost": { - "input": 0.2, - "output": 0.6, - "cache_read": 0.025 + "input": 0.13, + "output": 1.56 }, "limit": { - "context": 128000, - "output": 16384 + "context": 131072, + "output": 32768 } }, "qwen/qwen3-vl-8b-instruct": { @@ -59472,33 +60122,6 @@ "output": 40960 } }, - "qwen/qwq-32b": { - "id": "qwen/qwq-32b", - "name": "Qwen: QwQ 32B", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2024-11-28", - "last_updated": "2025-04-11", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.15, - "output": 0.4 - }, - "limit": { - "context": 32768, - "output": 32768 - } - }, "qwen/qwen3-next-80b-a3b-instruct": { "id": "qwen/qwen3-next-80b-a3b-instruct", "name": "Qwen: Qwen3 Next 80B A3B Instruct", @@ -59526,6 +60149,35 @@ "output": 52429 } }, + "qwen/qwen3.5-plus-20260420": { + "id": "qwen/qwen3.5-plus-20260420", + "name": "Qwen: Qwen3.5 Plus 2026-04-20", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-04-27", + "last_updated": "2026-05-01", + "modalities": { + "input": [ + "text", + "image", + "video" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.4, + "output": 2.4 + }, + "limit": { + "context": 1000000, + "output": 65536 + } + }, "qwen/qwen3-coder-next": { "id": "qwen/qwen3-coder-next", "name": "Qwen: Qwen3 Coder Next", @@ -59806,6 +60458,34 @@ "output": 32768 } }, + "qwen/qwen3.6-max-preview": { + "id": "qwen/qwen3.6-max-preview", + "name": "Qwen: Qwen3.6 Max Preview", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-04-27", + "last_updated": "2026-05-01", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 1.04, + "output": 6.24, + "cache_write": 1.3 + }, + "limit": { + "context": 262144, + "output": 65536 + } + }, "qwen/qwen-plus-2025-07-28:thinking": { "id": "qwen/qwen-plus-2025-07-28:thinking", "name": "Qwen: Qwen Plus 0728 (thinking)", @@ -59946,6 +60626,36 @@ "output": 65536 } }, + "qwen/qwen3.6-flash": { + "id": "qwen/qwen3.6-flash", + "name": "Qwen: Qwen3.6 Flash", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-04-27", + "last_updated": "2026-05-01", + "modalities": { + "input": [ + "text", + "image", + "video" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.25, + "output": 1.5, + "cache_write": 0.3125 + }, + "limit": { + "context": 1000000, + "output": 65536 + } + }, "alfredpros/codellama-7b-instruct-solidity": { "id": "alfredpros/codellama-7b-instruct-solidity", "name": "AlfredPros: CodeLLaMa 7B Instruct Solidity", @@ -60351,33 +61061,6 @@ "output": 65536 } }, - "google/gemma-2-9b-it": { - "id": "google/gemma-2-9b-it", - "name": "Google: Gemma 2 9B", - "attachment": false, - "reasoning": false, - "tool_call": false, - "temperature": true, - "release_date": "2024-06-28", - "last_updated": "2024-06-28", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.03, - "output": 0.09 - }, - "limit": { - "context": 8192, - "output": 1639 - } - }, "google/gemini-3-pro-image-preview": { "id": "google/gemini-3-pro-image-preview", "name": "Google: Nano Banana Pro (Gemini 3 Pro Image Preview)", @@ -61021,6 +61704,35 @@ "output": 32768 } }, + "~moonshotai/kimi-latest": { + "id": "~moonshotai/kimi-latest", + "name": "MoonshotAI: Kimi Latest", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-04-27", + "last_updated": "2026-05-01", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.74, + "output": 3.49, + "cache_read": 0.14 + }, + "limit": { + "context": 262142, + "output": 262142 + } + }, "thedrummer/unslopnemo-12b": { "id": "thedrummer/unslopnemo-12b", "name": "TheDrummer: UnslopNemo 12B", @@ -61284,6 +61996,37 @@ "output": 128000 } }, + "anthropic/claude-opus-4.7": { + "id": "anthropic/claude-opus-4.7", + "name": "Anthropic: Claude Opus 4.7", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": false, + "release_date": "2026-04-16", + "last_updated": "2026-05-01", + "modalities": { + "input": [ + "text", + "image", + "pdf" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 5, + "output": 25, + "cache_read": 0.5, + "cache_write": 6.25 + }, + "limit": { + "context": 1000000, + "output": 128000 + } + }, "anthropic/claude-sonnet-4": { "id": "anthropic/claude-sonnet-4", "name": "Anthropic: Claude Sonnet 4", @@ -61770,34 +62513,6 @@ "context": 163840, "output": 163840 } - }, - "meituan/longcat-flash-chat": { - "id": "meituan/longcat-flash-chat", - "name": "Meituan: LongCat Flash Chat", - "attachment": false, - "reasoning": false, - "tool_call": true, - "temperature": true, - "release_date": "2025-08-30", - "last_updated": "2026-03-15", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.2, - "output": 0.8, - "cache_read": 0.2 - }, - "limit": { - "context": 131072, - "output": 131072 - } } } }, @@ -76894,8 +77609,8 @@ }, "open_weights": true, "limit": { - "context": 204800, - "output": 131072 + "context": 196608, + "output": 196608 } }, "gpt-oss:20b": { @@ -77040,7 +77755,7 @@ "open_weights": true, "limit": { "context": 262144, - "output": 81920 + "output": 262144 } }, "deepseek-v3.1:671b": { @@ -77328,7 +78043,7 @@ "open_weights": true, "limit": { "context": 202752, - "output": 131072 + "output": 202752 } }, "mistral-large-3:675b": { @@ -85514,67 +86229,6 @@ "output": 128000 } }, - "gpt-5-4": { - "id": "gpt-5-4", - "name": "GPT-5.4", - "family": "gpt", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2025-08-31", - "release_date": "2026-03-05", - "last_updated": "2026-03-05", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.5, - "output": 15, - "cache_read": 0.25 - }, - "limit": { - "context": 272000, - "output": 128000 - } - }, - "deepseek-v3-2": { - "id": "deepseek-v3-2", - "name": "DeepSeek v3.2", - "family": "deepseek", - "attachment": true, - "reasoning": false, - "tool_call": true, - "temperature": true, - "knowledge": "2024-07", - "release_date": "2024-12-26", - "last_updated": "2025-09-29", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.58, - "output": 1.68, - "cache_read": 0.28 - }, - "limit": { - "context": 128000, - "output": 8192 - } - }, "claude-sonnet-4-6": { "id": "claude-sonnet-4-6", "name": "Claude Sonnet 4.6", @@ -85676,105 +86330,6 @@ "output": 128000 } }, - "gpt-5-mini": { - "id": "gpt-5-mini", - "name": "GPT-5 Mini", - "family": "gpt-mini", - "attachment": true, - "reasoning": true, - "tool_call": true, - "structured_output": true, - "temperature": false, - "knowledge": "2024-05-30", - "release_date": "2025-08-07", - "last_updated": "2025-08-07", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.25, - "output": 2, - "cache_read": 0.03 - }, - "limit": { - "context": 400000, - "output": 128000 - } - }, - "gpt-5-nano": { - "id": "gpt-5-nano", - "name": "GPT-5 Nano", - "family": "gpt-nano", - "attachment": true, - "reasoning": true, - "tool_call": true, - "structured_output": true, - "temperature": false, - "knowledge": "2024-05-30", - "release_date": "2025-08-07", - "last_updated": "2025-08-07", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.05, - "output": 0.4, - "cache_read": 0.01 - }, - "limit": { - "context": 400000, - "output": 128000 - } - }, - "gemini-3-pro-preview": { - "id": "gemini-3-pro-preview", - "name": "Gemini 3 Pro Preview", - "family": "gemini-pro", - "attachment": true, - "reasoning": true, - "tool_call": true, - "structured_output": true, - "temperature": true, - "knowledge": "2025-01", - "release_date": "2025-11-18", - "last_updated": "2025-11-18", - "modalities": { - "input": [ - "text", - "image", - "video", - "audio", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2, - "output": 12, - "cache_read": 0.2 - }, - "limit": { - "context": 1000000, - "output": 64000 - } - }, "zai-glm-5-1": { "id": "zai-glm-5-1", "name": "Z.AI GLM-5.1", @@ -85870,6 +86425,38 @@ "output": 128000 } }, + "gpt-5-4-nano": { + "id": "gpt-5-4-nano", + "name": "GPT-5.4 Nano", + "family": "gpt-nano", + "attachment": true, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "temperature": false, + "knowledge": "2024-05-30", + "release_date": "2025-08-07", + "last_updated": "2025-08-07", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.2, + "output": 1.25, + "cache_read": 0.02 + }, + "limit": { + "context": 400000, + "output": 128000 + } + }, "gemini-2.5-flash": { "id": "gemini-2.5-flash", "name": "Gemini 2.5 Flash", @@ -85934,6 +86521,102 @@ "output": 128000 } }, + "gpt-5-5": { + "id": "gpt-5-5", + "name": "GPT-5.5", + "family": "gpt", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": false, + "knowledge": "2025-08-31", + "release_date": "2026-03-05", + "last_updated": "2026-03-05", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 2.5, + "output": 15, + "cache_read": 0.25 + }, + "limit": { + "context": 272000, + "output": 128000 + } + }, + "grok-4-3": { + "id": "grok-4-3", + "name": "Grok 4.3", + "family": "grok", + "attachment": true, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "temperature": true, + "knowledge": "2024-11", + "release_date": "2026-04-30", + "last_updated": "2026-04-30", + "modalities": { + "input": [ + "text", + "image", + "video" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 1.25, + "output": 2.5, + "cache_read": 0.2 + }, + "limit": { + "context": 1000000, + "output": 128000 + } + }, + "gpt-5-4-mini": { + "id": "gpt-5-4-mini", + "name": "GPT-5.4 Mini", + "family": "gpt-mini", + "attachment": true, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "temperature": false, + "knowledge": "2025-08-31", + "release_date": "2026-03-17", + "last_updated": "2026-03-17", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.75, + "output": 4.5, + "cache_read": 0.075 + }, + "limit": { + "context": 400000, + "output": 128000 + } + }, "claude-opus-4-6": { "id": "claude-opus-4-6", "name": "Claude Opus 4.6", @@ -85967,6 +86650,36 @@ "output": 128000 } }, + "deepseek-v4-pro": { + "id": "deepseek-v4-pro", + "name": "DeepSeek v4 Pro", + "family": "deepseek", + "attachment": true, + "reasoning": false, + "tool_call": true, + "temperature": true, + "knowledge": "2026-01", + "release_date": "2026-04-24", + "last_updated": "2026-04-24", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 1.74, + "output": 3.48, + "cache_read": 0.14 + }, + "limit": { + "context": 128000, + "output": 8192 + } + }, "gpt-oss-20b": { "id": "gpt-oss-20b", "name": "GPT OSS 20B", @@ -86027,6 +86740,36 @@ "output": 64000 } }, + "minimax-m2-7": { + "id": "minimax-m2-7", + "name": "MiniMax-M2.7", + "family": "minimax", + "attachment": true, + "reasoning": false, + "tool_call": true, + "temperature": true, + "knowledge": "2024-09", + "release_date": "2026-03-18", + "last_updated": "2026-03-18", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.3, + "output": 1.2, + "cache_read": 0.06 + }, + "limit": { + "context": 192000, + "output": 8192 + } + }, "minimax-m2-5": { "id": "minimax-m2-5", "name": "MiniMax-M2.5", @@ -90512,6 +91255,41 @@ "output": 4096 } }, + "grok-4.3": { + "id": "grok-4.3", + "name": "Grok 4.3", + "family": "grok", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-05-01", + "last_updated": "2026-05-01", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 1.25, + "output": 2.5, + "cache_read": 0.2, + "context_over_200k": { + "input": 2.5, + "output": 5, + "cache_read": 0.4 + } + }, + "limit": { + "context": 1000000, + "output": 30000 + } + }, "grok-3-mini-fast": { "id": "grok-3-mini-fast", "name": "Grok 3 Mini Fast", @@ -100622,6 +101400,39 @@ "output": 64000 } }, + "deepseek-v4-pro": { + "id": "deepseek-v4-pro", + "name": "DeepSeek V4 Pro", + "family": "deepseek-thinking", + "attachment": false, + "reasoning": true, + "tool_call": true, + "interleaved": { + "field": "reasoning_content" + }, + "structured_output": true, + "temperature": true, + "knowledge": "2025-05", + "release_date": "2026-04-24", + "last_updated": "2026-04-24", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 1.74, + "output": 3.48 + }, + "limit": { + "context": 1048576, + "output": 393216 + } + }, "minimax-m2.5": { "id": "minimax-m2.5", "name": "MiniMax M2.5", @@ -103132,6 +103943,35 @@ "output": 200000 } }, + "qwen3-235b-a22b-instruct-2507": { + "id": "qwen3-235b-a22b-instruct-2507", + "name": "Qwen3 235B A22B Instruct 2507", + "family": "qwen", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "knowledge": "2025-04", + "release_date": "2025-07-23", + "last_updated": "2025-07-23", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.062, + "output": 0.408 + }, + "limit": { + "context": 131000, + "output": 131000 + } + }, "kimi-k2.5": { "id": "kimi-k2.5", "name": "Kimi K2.5", @@ -103378,6 +104218,36 @@ "output": 16384 } }, + "codestral-2508": { + "id": "codestral-2508", + "name": "Codestral 2508", + "family": "mistral", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "knowledge": "2025-03", + "release_date": "2025-07-30", + "last_updated": "2025-07-30", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.3, + "output": 0.9, + "cache_read": 0.03 + }, + "limit": { + "context": 256000, + "output": 256000 + } + }, "claude-4-5-sonnet": { "id": "claude-4-5-sonnet", "name": "Claude 4.5 Sonnet", @@ -103438,6 +104308,34 @@ "output": 131000 } }, + "nemotron-3-super-120b-a12b": { + "id": "nemotron-3-super-120b-a12b", + "name": "Nemotron 3 Super 120B A12B", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "knowledge": "2025-12", + "release_date": "2026-03-11", + "last_updated": "2026-03-11", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.266, + "output": 0.799 + }, + "limit": { + "context": 262144, + "output": 262144 + } + }, "minimax-m2": { "id": "minimax-m2", "name": "MiniMax-M2", @@ -103897,6 +104795,62 @@ "output": 262000 } }, + "mixtral-8x7B-instruct-v0.1": { + "id": "mixtral-8x7B-instruct-v0.1", + "name": "Mixtral 8x7B Instruct v0.1", + "attachment": false, + "reasoning": true, + "tool_call": false, + "temperature": true, + "knowledge": "2023-09", + "release_date": "2023-12-11", + "last_updated": "2023-12-11", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.438, + "output": 0.68 + }, + "limit": { + "context": 32000, + "output": 32000 + } + }, + "hermes-4-70b": { + "id": "hermes-4-70b", + "name": "Hermes 4 70B", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "knowledge": "2023-12", + "release_date": "2025-08-26", + "last_updated": "2025-08-26", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.116, + "output": 0.358 + }, + "limit": { + "context": 128000, + "output": 128000 + } + }, "minimax-m2.5": { "id": "minimax-m2.5", "name": "MiniMax-M2.5", @@ -103928,6 +104882,35 @@ "output": 196608 } }, + "deepseek-v3.2": { + "id": "deepseek-v3.2", + "name": "DeepSeek V3.2", + "family": "deepseek", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "knowledge": "2024-07", + "release_date": "2025-12-01", + "last_updated": "2025-12-01", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.266, + "output": 0.444 + }, + "limit": { + "context": 163840, + "output": 163840 + } + }, "intellect-3": { "id": "intellect-3", "name": "INTELLECT 3", @@ -104017,6 +105000,64 @@ "output": 128000 } }, + "qwen-2.5-72b-instruct": { + "id": "qwen-2.5-72b-instruct", + "name": "Qwen2.5 72B Instruct", + "family": "qwen", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "knowledge": "2024-06", + "release_date": "2024-09-19", + "last_updated": "2024-09-19", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.062, + "output": 0.231 + }, + "limit": { + "context": 33000, + "output": 33000 + } + }, + "deepseek-r1-0528": { + "id": "deepseek-r1-0528", + "name": "DeepSeek R1 0528", + "family": "deepseek-thinking", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "knowledge": "2024-07", + "release_date": "2025-05-28", + "last_updated": "2025-05-28", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.585, + "output": 2.307 + }, + "limit": { + "context": 164000, + "output": 164000 + } + }, "gpt-4.1": { "id": "gpt-4.1", "name": "GPT 4.1", @@ -104106,6 +105147,219 @@ "context": 128000, "output": 128000 } + }, + "qwen3.5-122b-a10b": { + "id": "qwen3.5-122b-a10b", + "name": "Qwen3.5 122B A10B", + "family": "qwen", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "knowledge": "2026-01", + "release_date": "2026-02-24", + "last_updated": "2026-02-24", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.444, + "output": 3.106 + }, + "limit": { + "context": 262144, + "output": 262144 + } + }, + "llama-3.3-70b-instruct": { + "id": "llama-3.3-70b-instruct", + "name": "Llama 3.3 70B Instruct", + "family": "llama", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "knowledge": "2023-12", + "release_date": "2024-12-06", + "last_updated": "2024-12-06", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.089, + "output": 0.275 + }, + "limit": { + "context": 131000, + "output": 131000 + } + }, + "mistral-large-2512": { + "id": "mistral-large-2512", + "name": "Mistral Large 3 2512", + "family": "mistral-large", + "attachment": true, + "reasoning": false, + "tool_call": true, + "temperature": true, + "knowledge": "2025-12", + "release_date": "2025-12-01", + "last_updated": "2025-12-01", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.5, + "output": 1.5, + "cache_read": 0.05 + }, + "limit": { + "context": 256000, + "output": 256000 + } + }, + "qwen3.5-397b-a17b": { + "id": "qwen3.5-397b-a17b", + "name": "Qwen3.5 397B A17B", + "family": "qwen", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "knowledge": "2026-01", + "release_date": "2026-02-16", + "last_updated": "2026-02-16", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.6, + "output": 3.6 + }, + "limit": { + "context": 250000, + "output": 250000 + } + }, + "deepseek-v4-flash": { + "id": "deepseek-v4-flash", + "name": "DeepSeek V4 Flash", + "family": "deepseek-flash", + "attachment": false, + "reasoning": true, + "tool_call": true, + "interleaved": { + "field": "reasoning_content" + }, + "temperature": true, + "knowledge": "2025-05", + "release_date": "2026-04-24", + "last_updated": "2026-04-24", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.133, + "output": 0.266, + "cache_read": 0.028 + }, + "limit": { + "context": 1048576, + "output": 384000 + } + }, + "deepseek-v4-pro": { + "id": "deepseek-v4-pro", + "name": "DeepSeek V4 Pro", + "family": "deepseek-thinking", + "attachment": false, + "reasoning": true, + "tool_call": true, + "interleaved": { + "field": "reasoning_content" + }, + "temperature": true, + "knowledge": "2025-05", + "release_date": "2026-04-24", + "last_updated": "2026-04-24", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 1.553, + "output": 3.106, + "cache_read": 0.145 + }, + "limit": { + "context": 1048576, + "output": 384000 + } + }, + "qwen3-coder-30b-a3b-instruct": { + "id": "qwen3-coder-30b-a3b-instruct", + "name": "Qwen3 Coder 30B A3B Instruct", + "family": "qwen", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "knowledge": "2025-04", + "release_date": "2025-07-31", + "last_updated": "2025-07-31", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.053, + "output": 0.222 + }, + "limit": { + "context": 262000, + "output": 262000 + } } } }, @@ -106346,6 +107600,36 @@ "output": 1000000 } }, + "alibaba/qwen3.6-27b": { + "id": "alibaba/qwen3.6-27b", + "name": "Qwen 3.6 27B", + "family": "qwen3.6", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-04-22", + "last_updated": "2026-05-01", + "modalities": { + "input": [ + "text", + "image", + "pdf" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.6, + "output": 3.5999999999999996 + }, + "limit": { + "context": 256000, + "output": 256000 + } + }, "alibaba/qwen3-embedding-8b": { "id": "alibaba/qwen3-embedding-8b", "name": "Qwen3 Embedding 8B", @@ -106819,6 +108103,35 @@ "output": 65536 } }, + "alibaba/qwen3-vl-235b-a22b-instruct": { + "id": "alibaba/qwen3-vl-235b-a22b-instruct", + "name": "Qwen3 VL 235B A22B Instruct", + "family": "qwen", + "attachment": true, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-09-24", + "last_updated": "2026-05-01", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.39999999999999997, + "output": 1.5999999999999999 + }, + "limit": { + "context": 131072, + "output": 129024 + } + }, "alibaba/qwen3-coder": { "id": "alibaba/qwen3-coder", "name": "Qwen3 Coder 480B A35B Instruct", @@ -108312,6 +109625,37 @@ "output": 131072 } }, + "xai/grok-4.3": { + "id": "xai/grok-4.3", + "name": "Grok 4.3", + "family": "grok", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-04-30", + "last_updated": "2026-05-01", + "modalities": { + "input": [ + "text", + "image", + "pdf" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 1.25, + "output": 2.5, + "cache_read": 0.19999999999999998 + }, + "limit": { + "context": 1000000, + "output": 1000000 + } + }, "xai/grok-4.20-non-reasoning": { "id": "xai/grok-4.20-non-reasoning", "name": "Grok 4.20 Non-Reasoning", @@ -109052,6 +110396,34 @@ "output": 128000 } }, + "inception/mercury-coder-small": { + "id": "inception/mercury-coder-small", + "name": "Mercury Coder Small Beta", + "family": "mercury", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2025-02-26", + "last_updated": "2026-05-01", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.25, + "output": 1 + }, + "limit": { + "context": 32000, + "output": 16384 + } + }, "openai/gpt-5.1-codex-max": { "id": "openai/gpt-5.1-codex-max", "name": "GPT 5.1 Codex Max", @@ -113243,6 +114615,68 @@ "output": 32000 } }, + "xiaomi/mimo-v2.5-pro": { + "id": "xiaomi/mimo-v2.5-pro", + "name": "MiMo V2.5 Pro", + "family": "mimo-v2.5-pro", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-04-22", + "last_updated": "2026-05-01", + "modalities": { + "input": [ + "text", + "image", + "pdf" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 1, + "output": 3, + "cache_read": 0.19999999999999998 + }, + "limit": { + "context": 1050000, + "output": 131000 + } + }, + "xiaomi/mimo-v2.5": { + "id": "xiaomi/mimo-v2.5", + "name": "MiMo M2.5", + "family": "mimo-v2.5", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-04-22", + "last_updated": "2026-05-01", + "modalities": { + "input": [ + "text", + "image", + "pdf" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.39999999999999997, + "output": 2, + "cache_read": 0.08 + }, + "limit": { + "context": 1050000, + "output": 131100 + } + }, "xiaomi/mimo-v2-pro": { "id": "xiaomi/mimo-v2-pro", "name": "MiMo V2 Pro", @@ -129764,6 +131198,435 @@ } } }, + "neuralwatt": { + "id": "neuralwatt", + "env": [ + "NEURALWATT_API_KEY" + ], + "npm": "@ai-sdk/openai-compatible", + "api": "https://api.neuralwatt.com/v1", + "name": "Neuralwatt", + "doc": "https://portal.neuralwatt.com/docs", + "models": { + "glm-5-fast": { + "id": "glm-5-fast", + "name": "GLM 5 Fast", + "family": "glm", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2026-04-07", + "last_updated": "2026-04-07", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 1.1, + "output": 3.6 + }, + "limit": { + "context": 200000, + "output": 200000 + } + }, + "kimi-k2.6-fast": { + "id": "kimi-k2.6-fast", + "name": "Kimi K2.6 Fast", + "family": "kimi", + "attachment": true, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2026-04-21", + "last_updated": "2026-04-21", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.69, + "output": 3.22 + }, + "limit": { + "context": 262144, + "output": 262144 + } + }, + "qwen3.5-397b-fast": { + "id": "qwen3.5-397b-fast", + "name": "Qwen3.5 397B Fast", + "family": "qwen", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2026-02-01", + "last_updated": "2026-02-01", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.69, + "output": 4.14 + }, + "limit": { + "context": 262144, + "output": 262144 + } + }, + "glm-5.1-fast": { + "id": "glm-5.1-fast", + "name": "GLM 5.1 Fast", + "family": "glm", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2026-04-07", + "last_updated": "2026-04-07", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 1.1, + "output": 3.6 + }, + "limit": { + "context": 200000, + "output": 200000 + } + }, + "qwen3.6-35b-fast": { + "id": "qwen3.6-35b-fast", + "name": "Qwen3.6 35B Fast", + "family": "qwen3.6", + "attachment": true, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2026-04-01", + "last_updated": "2026-04-01", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.05, + "output": 0.1 + }, + "limit": { + "context": 131072, + "output": 131072 + } + }, + "kimi-k2.5-fast": { + "id": "kimi-k2.5-fast", + "name": "Kimi K2.5 Fast", + "family": "kimi", + "attachment": true, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2026-01-27", + "last_updated": "2026-01-27", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.52, + "output": 2.59 + }, + "limit": { + "context": 262144, + "output": 262144 + } + }, + "Qwen/Qwen3.5-397B-A17B-FP8": { + "id": "Qwen/Qwen3.5-397B-A17B-FP8", + "name": "Qwen3.5 397B A17B FP8", + "family": "qwen", + "attachment": false, + "reasoning": true, + "tool_call": true, + "interleaved": { + "field": "reasoning_content" + }, + "temperature": true, + "release_date": "2026-02-01", + "last_updated": "2026-02-01", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.69, + "output": 4.14 + }, + "limit": { + "context": 262144, + "output": 262144 + } + }, + "Qwen/Qwen3.6-35B-A3B": { + "id": "Qwen/Qwen3.6-35B-A3B", + "name": "Qwen3.6 35B A3B", + "family": "qwen3.6", + "attachment": true, + "reasoning": true, + "tool_call": true, + "interleaved": { + "field": "reasoning_content" + }, + "temperature": true, + "release_date": "2026-04-01", + "last_updated": "2026-04-01", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.05, + "output": 0.1 + }, + "limit": { + "context": 131072, + "output": 131072 + } + }, + "zai-org/GLM-5.1-FP8": { + "id": "zai-org/GLM-5.1-FP8", + "name": "GLM 5.1 FP8", + "family": "glm", + "attachment": false, + "reasoning": true, + "tool_call": true, + "interleaved": { + "field": "reasoning_content" + }, + "temperature": true, + "release_date": "2026-04-07", + "last_updated": "2026-04-07", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 1.1, + "output": 3.6 + }, + "limit": { + "context": 200000, + "output": 200000 + } + }, + "mistralai/Devstral-Small-2-24B-Instruct-2512": { + "id": "mistralai/Devstral-Small-2-24B-Instruct-2512", + "name": "Devstral Small 2 24B Instruct 2512", + "family": "devstral", + "attachment": true, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2025-12-09", + "last_updated": "2025-12-09", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.12, + "output": 0.35 + }, + "limit": { + "context": 262144, + "output": 262144 + } + }, + "openai/gpt-oss-20b": { + "id": "openai/gpt-oss-20b", + "name": "GPT OSS 20B", + "family": "gpt-oss", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-08-05", + "last_updated": "2025-08-05", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.03, + "output": 0.16 + }, + "limit": { + "context": 16384, + "output": 16384 + } + }, + "moonshotai/Kimi-K2.6": { + "id": "moonshotai/Kimi-K2.6", + "name": "Kimi K2.6", + "family": "kimi", + "attachment": true, + "reasoning": true, + "tool_call": true, + "interleaved": { + "field": "reasoning_content" + }, + "temperature": true, + "release_date": "2026-04-21", + "last_updated": "2026-04-21", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.69, + "output": 3.22 + }, + "limit": { + "context": 262144, + "output": 262144 + } + }, + "moonshotai/Kimi-K2.5": { + "id": "moonshotai/Kimi-K2.5", + "name": "Kimi K2.5", + "family": "kimi", + "attachment": true, + "reasoning": true, + "tool_call": true, + "interleaved": { + "field": "reasoning_content" + }, + "temperature": true, + "release_date": "2026-01-27", + "last_updated": "2026-01-27", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.52, + "output": 2.59 + }, + "limit": { + "context": 262144, + "output": 262144 + } + }, + "MiniMaxAI/MiniMax-M2.5": { + "id": "MiniMaxAI/MiniMax-M2.5", + "name": "MiniMax M2.5", + "family": "minimax", + "attachment": false, + "reasoning": true, + "tool_call": true, + "interleaved": { + "field": "reasoning_content" + }, + "temperature": true, + "release_date": "2026-02-12", + "last_updated": "2026-02-12", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.35, + "output": 1.38 + }, + "limit": { + "context": 196608, + "output": 196608 + } + } + } + }, "togetherai": { "id": "togetherai", "env": [ diff --git a/packages/agentlayer-core/package.json b/packages/agentlayer-core/package.json index f777921..88ef629 100644 --- a/packages/agentlayer-core/package.json +++ b/packages/agentlayer-core/package.json @@ -32,36 +32,42 @@ }, "exports": { ".": { + "bun": "./src/index.ts", "source": "./src/index.ts", "types": "./dist/index.d.ts", "import": "./dist/index.js", "default": "./dist/index.js" }, "./prompts": { + "bun": "./src/prompts/index.ts", "source": "./src/prompts/index.ts", "types": "./dist/prompts/index.d.ts", "import": "./dist/prompts/index.js", "default": "./dist/prompts/index.js" }, "./tools": { + "bun": "./src/tools/index.ts", "source": "./src/tools/index.ts", "types": "./dist/tools/index.d.ts", "import": "./dist/tools/index.js", "default": "./dist/tools/index.js" }, "./hooks": { + "bun": "./src/hooks/index.ts", "source": "./src/hooks/index.ts", "types": "./dist/hooks/index.d.ts", "import": "./dist/hooks/index.js", "default": "./dist/hooks/index.js" }, "./utils": { + "bun": "./src/utils/index.ts", "source": "./src/utils/index.ts", "types": "./dist/utils/index.d.ts", "import": "./dist/utils/index.js", "default": "./dist/utils/index.js" }, "./interfaces": { + "bun": "./src/interfaces/index.ts", "source": "./src/interfaces/index.ts", "types": "./dist/interfaces/index.d.ts", "import": "./dist/interfaces/index.js", diff --git a/packages/quickjs-exec/build.ts b/packages/quickjs-exec/build.ts new file mode 100644 index 0000000..c7c718f --- /dev/null +++ b/packages/quickjs-exec/build.ts @@ -0,0 +1,3 @@ +import { buildPackageFromManifest } from '../../scripts/build/package-build.ts' + +await buildPackageFromManifest(import.meta.dir) diff --git a/packages/quickjs-exec/package.json b/packages/quickjs-exec/package.json new file mode 100644 index 0000000..9bac024 --- /dev/null +++ b/packages/quickjs-exec/package.json @@ -0,0 +1,45 @@ +{ + "name": "@humanlayer/quickjs-exec", + "source": "./src/index.ts", + "type": "module", + "version": "0.0.0", + "private": false, + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "publishConfig": { + "access": "public" + }, + "files": [ + "src", + "dist" + ], + "exports": { + ".": { + "bun": "./src/index.ts", + "source": "./src/index.ts", + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "scripts": { + "typecheck": "tsgo --noEmit", + "build": "bun run build:js && bunx tsc -p tsconfig.build.json", + "build:js": "bun run build.ts", + "build:types": "bunx tsc -p tsconfig.build.json" + }, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5" + }, + "dependencies": { + "quickjs-emscripten": "^0.31.0" + }, + "author": { + "name": "Kyle Mistele", + "url": "https://x.com/0xblacklight" + } +} diff --git a/packages/quickjs-exec/src/index.ts b/packages/quickjs-exec/src/index.ts new file mode 100644 index 0000000..8b606df --- /dev/null +++ b/packages/quickjs-exec/src/index.ts @@ -0,0 +1,335 @@ +import { getQuickJS, newAsyncContext, type QuickJSContext, type QuickJSHandle } from 'quickjs-emscripten' + +export type QuickJsBinding = (...args: unknown[]) => unknown | Promise +export type QuickJsBindings = Record + +export type QuickJsExecutionMode = 'sync' | 'async' + +export type QuickJsConsoleLevel = 'log' | 'info' | 'warn' | 'error' + +export type QuickJsConsoleEntry = { + level: QuickJsConsoleLevel + args: unknown[] + text: string +} + +export type QuickJsExecutionErrorDetails = { + name?: string + message: string + fileName?: string + lineNumber?: number + columnNumber?: number + codeFrame?: string +} + +export class QuickJsExecutionError extends Error { + readonly details: QuickJsExecutionErrorDetails + + constructor( + details: QuickJsExecutionErrorDetails, + options?: { + cause?: unknown + }, + ) { + super(formatQuickJsExecutionMessage(details)) + this.name = 'QuickJsExecutionError' + this.details = details + if (options && 'cause' in options) { + Object.defineProperty(this, 'cause', { + value: options.cause, + configurable: true, + writable: true, + }) + } + } +} + +export type QuickJsSerializedError = { + name?: string + message: string + fileName?: string + lineNumber?: number + columnNumber?: number + codeFrame?: string +} + +export type QuickJsRunWithConsoleResult = + | { + ok: true + value: T + console: QuickJsConsoleEntry[] + } + | { + ok: false + error: QuickJsSerializedError + console: QuickJsConsoleEntry[] + } + +export type QuickJsExec = { + readonly mode: QuickJsExecutionMode + run(code: string): T | Promise + dispose(): void +} + +export type QuickJsSyncExec = QuickJsExec & { + readonly mode: 'sync' + run(code: string): T + runWithConsole(code: string): QuickJsRunWithConsoleResult +} + +export type QuickJsAsyncExec = QuickJsExec & { + readonly mode: 'async' + run(code: string): Promise + runWithConsole(code: string): Promise> +} + +export type WithQuickJsModeCallback = (exec: QuickJsSyncExec) => T +export type WithAsyncQuickJsModeCallback = (exec: QuickJsAsyncExec) => Promise + +export async function withQuickJsMode(bindings: QuickJsBindings, cb: WithQuickJsModeCallback): Promise { + const QuickJS = await getQuickJS() + const vm = QuickJS.newContext() + const disposableHandles: QuickJSHandle[] = [] + + try { + installSyncBindings({ vm, bindings, disposableHandles }) + return cb({ + mode: 'sync', + run: (code) => runSync(vm, code) as never, + runWithConsole: (code) => runSyncWithConsole(vm, code) as never, + dispose: () => vm.dispose(), + }) + } finally { + disposeHandles(disposableHandles) + vm.dispose() + } +} + +export async function withAsyncQuickJsMode( + bindings: QuickJsBindings, + cb: WithAsyncQuickJsModeCallback, +): Promise { + const context = await newAsyncContext() + const disposableHandles: QuickJSHandle[] = [] + + try { + installAsyncifiedBindings({ context, bindings, disposableHandles }) + return await cb({ + mode: 'async', + run: (code) => runAsync(context, code) as never, + runWithConsole: (code) => runAsyncWithConsole(context, code) as never, + dispose: () => context.dispose(), + }) + } finally { + disposeHandles(disposableHandles) + context.dispose() + } +} + +type TypedUnknown = unknown + +function runSync(vm: QuickJSContext, code: string): T { + const result = vm.evalCode(code) + const value = unwrapQuickJsResult(vm, result, code) + try { + return vm.dump(value) as T + } finally { + value.dispose() + } +} + +async function runAsync(context: Awaited>, code: string): Promise { + const result = await context.evalCodeAsync(code) + const value = unwrapQuickJsResult(context, result, code) + try { + return context.dump(value) as T + } finally { + value.dispose() + } +} + +function runSyncWithConsole(vm: QuickJSContext, code: string): QuickJsRunWithConsoleResult { + clearConsole(vm) + try { + const value = runSync(vm, code) + return { ok: true, value, console: readConsole(vm) } + } catch (error) { + return { ok: false, error: serializeError(error), console: readConsole(vm) } + } +} + +async function runAsyncWithConsole( + context: Awaited>, + code: string, +): Promise> { + clearConsole(context) + try { + const value = await runAsync(context, code) + return { ok: true, value, console: readConsole(context) } + } catch (error) { + return { ok: false, error: serializeError(error), console: readConsole(context) } + } +} + +function unwrapQuickJsResult( + context: QuickJSContext | Awaited>, + result: unknown, + code: string, +): QuickJSHandle { + try { + return context.unwrapResult(result as never) as QuickJSHandle + } catch (error) { + throw createQuickJsExecutionError(error, code) + } +} + +function createQuickJsExecutionError(error: unknown, code: string): QuickJsExecutionError { + const cause = error && typeof error === 'object' && 'cause' in error ? (error as { cause?: unknown }).cause : error + const causeRecord = cause && typeof cause === 'object' ? (cause as Record) : undefined + const name = + typeof causeRecord?.name === 'string' ? causeRecord.name : error instanceof Error ? error.name : undefined + const message = + typeof causeRecord?.message === 'string' + ? causeRecord.message + : error instanceof Error + ? error.message + : String(error) + const fileName = typeof causeRecord?.fileName === 'string' ? causeRecord.fileName : undefined + const lineNumber = typeof causeRecord?.lineNumber === 'number' ? causeRecord.lineNumber : undefined + const columnNumber = typeof causeRecord?.columnNumber === 'number' ? causeRecord.columnNumber : undefined + + return new QuickJsExecutionError( + { + name, + message, + fileName, + lineNumber, + columnNumber, + codeFrame: lineNumber ? formatCodeFrame(code, lineNumber, columnNumber) : undefined, + }, + { cause }, + ) +} + +function serializeError(error: unknown): QuickJsSerializedError { + if (error instanceof QuickJsExecutionError) return error.details + if (error instanceof Error) return { name: error.name, message: error.message } + return { message: String(error) } +} + +function formatQuickJsExecutionMessage(details: QuickJsExecutionErrorDetails): string { + const location = details.lineNumber + ? `${details.fileName ?? 'eval.js'}:${details.lineNumber}${details.columnNumber ? `:${details.columnNumber}` : ''}` + : undefined + const headline = [details.name, details.message].filter(Boolean).join(': ') + return [location ? `${headline} at ${location}` : headline, details.codeFrame].filter(Boolean).join('\n') +} + +function formatCodeFrame(code: string, lineNumber: number, columnNumber?: number): string { + const lines = code.replace(/^\n/, '').split('\n') + const normalizedLineNumber = Math.max(1, Math.min(lineNumber, lines.length)) + const start = Math.max(1, normalizedLineNumber - 2) + const end = Math.min(lines.length, normalizedLineNumber + 2) + const width = String(end).length + const frame: string[] = [] + + for (let line = start; line <= end; line++) { + const marker = line === normalizedLineNumber ? '>' : ' ' + const source = lines[line - 1] ?? '' + frame.push(`${marker} ${String(line).padStart(width, ' ')} | ${source}`) + if (line === normalizedLineNumber && columnNumber && columnNumber > 0) { + frame.push(` ${' '.repeat(width)} | ${' '.repeat(columnNumber - 1)}^`) + } + } + + return frame.join('\n') +} + +function installSyncBindings(input: { + vm: QuickJSContext + bindings: QuickJsBindings + disposableHandles: QuickJSHandle[] +}): void { + for (const [name, fn] of Object.entries(input.bindings)) { + const handle = input.vm.newFunction(name, (...args) => { + const output = fn(...args.map((arg) => input.vm.dump(arg))) + if (output instanceof Promise) throw new Error(`Async binding ${name} used with withQuickJsMode`) + return input.vm.newString(JSON.stringify(output ?? null)) + }) + input.vm.setProp(input.vm.global, name, handle) + input.disposableHandles.push(handle) + } + installBindingFacade(input.vm, input.disposableHandles) +} + +function installAsyncifiedBindings(input: { + context: Awaited> + bindings: QuickJsBindings + disposableHandles: QuickJSHandle[] +}): void { + for (const [name, fn] of Object.entries(input.bindings)) { + const handle = input.context.newAsyncifiedFunction(name, async (...args) => { + const output = await fn(...args.map((arg) => input.context.dump(arg))) + return input.context.newString(JSON.stringify(output ?? null)) + }) + input.context.setProp(input.context.global, name, handle) + input.disposableHandles.push(handle) + } + installBindingFacade(input.context, input.disposableHandles) +} + +function installBindingFacade(vm: QuickJSContext, disposableHandles: QuickJSHandle[]): void { + const result = vm.evalCode(` + globalThis.__quickjsConsoleLogs = [] + function __quickjsConsoleText(args) { + return args.map((arg) => { + if (typeof arg === 'string') return arg + try { return JSON.stringify(arg) } + catch { return String(arg) } + }).join(' ') + } + function __quickjsConsolePush(level, args) { + globalThis.__quickjsConsoleLogs.push({ + level, + args, + text: __quickjsConsoleText(args), + }) + } + globalThis.console = { + log: (...args) => __quickjsConsolePush('log', args), + info: (...args) => __quickjsConsolePush('info', args), + warn: (...args) => __quickjsConsolePush('warn', args), + error: (...args) => __quickjsConsolePush('error', args), + } + globalThis.bindings = new Proxy({}, { + get(_target, prop) { + return (...args) => { + const raw = globalThis[prop](...args) + return typeof raw === 'string' ? JSON.parse(raw) : raw + } + } + }) + `) + const value = vm.unwrapResult(result) + disposableHandles.push(value) +} + +function clearConsole(vm: QuickJSContext | Awaited>): void { + const result = vm.evalCode('globalThis.__quickjsConsoleLogs.length = 0') + const value = vm.unwrapResult(result) + value.dispose() +} + +function readConsole(vm: QuickJSContext | Awaited>): QuickJsConsoleEntry[] { + const result = vm.evalCode('globalThis.__quickjsConsoleLogs') + const value = vm.unwrapResult(result) + try { + return vm.dump(value) as QuickJsConsoleEntry[] + } finally { + value.dispose() + } +} + +function disposeHandles(handles: QuickJSHandle[]): void { + for (const handle of handles) handle.dispose() +} diff --git a/packages/quickjs-exec/test/quickjs-exec.test.ts b/packages/quickjs-exec/test/quickjs-exec.test.ts new file mode 100644 index 0000000..c7688ad --- /dev/null +++ b/packages/quickjs-exec/test/quickjs-exec.test.ts @@ -0,0 +1,240 @@ +import { describe, expect, test } from 'bun:test' +import { QuickJsExecutionError, withAsyncQuickJsMode, withQuickJsMode } from '../src' + +describe('quickjs-exec', () => { + test('runs sync code with sync host bindings', async () => { + await withQuickJsMode( + { + add: (a, b) => (a as number) + (b as number), + }, + ({ run }) => { + const result = run<{ sum: number }>(` + ({ sum: bindings.add(20, 22) }) + `) + + expect(result).toEqual({ sum: 42 }) + }, + ) + }) + + test('script with no final expression does not throw and returns undefined', async () => { + await withQuickJsMode({}, ({ run }) => { + const result = run(`const x = 2`) + + expect(result).toBeUndefined() + }) + }) + + test('script can return final parenthesized object expression', async () => { + await withQuickJsMode({}, ({ run }) => { + const result = run<{ ok: boolean; value: number }>(` + const value = 20 + 22 + ;({ ok: true, value }) + `) + + expect(result).toEqual({ ok: true, value: 42 }) + }) + }) + + test('runWithConsole captures logs and return value', async () => { + await withQuickJsMode({}, ({ runWithConsole }) => { + const result = runWithConsole<{ ok: boolean }>(` + console.log('created', { id: 'n2', kind: 'element' }) + ;({ ok: true }) + `) + + expect(result).toEqual({ + ok: true, + value: { ok: true }, + console: [ + { + level: 'log', + args: ['created', { id: 'n2', kind: 'element' }], + text: 'created {"id":"n2","kind":"element"}', + }, + ], + }) + }) + }) + + test('runWithConsole captures logs before runtime errors', async () => { + await withQuickJsMode({}, ({ runWithConsole }) => { + const result = runWithConsole(` + console.log('before failure', { step: 1 }) + missingReference + `) + + expect(result.ok).toBe(false) + if (result.ok) throw new Error('Expected failure') + expect(result.error.name).toBe('ReferenceError') + expect(result.error.message).toContain('missingReference') + expect(result.console).toEqual([ + { + level: 'log', + args: ['before failure', { step: 1 }], + text: 'before failure {"step":1}', + }, + ]) + }) + }) + + test('runWithConsole captures empty logs for syntax errors', async () => { + await withQuickJsMode({}, ({ runWithConsole }) => { + const result = runWithConsole(` + console.log('not reached') + const broken = { index 0 } + `) + + expect(result.ok).toBe(false) + if (result.ok) throw new Error('Expected failure') + expect(result.error.name).toBe('SyntaxError') + expect(result.error.codeFrame).toContain('index 0') + expect(result.console).toEqual([]) + }) + }) + + test('runWithConsole clears logs between runs', async () => { + await withQuickJsMode({}, ({ runWithConsole }) => { + const first = runWithConsole(`console.log('first')`) + const second = runWithConsole(`console.log('second')`) + + expect(first.console.map((entry) => entry.text)).toEqual(['first']) + expect(second.console.map((entry) => entry.text)).toEqual(['second']) + }) + }) + + test('runs asyncified host bindings', async () => { + await withAsyncQuickJsMode( + { + addAsync: async (a, b) => (a as number) + (b as number), + }, + async ({ run }) => { + const result = await run<{ sum: number }>(` + ({ sum: bindings.addAsync(20, 22) }) + `) + + expect(result).toEqual({ sum: 42 }) + }, + ) + }) + + test('async runWithConsole captures logs and return value', async () => { + await withAsyncQuickJsMode( + { + addAsync: async (a, b) => (a as number) + (b as number), + }, + async ({ runWithConsole }) => { + const result = await runWithConsole<{ sum: number }>(` + console.info('before async') + ;({ sum: bindings.addAsync(20, 22) }) + `) + + expect(result).toEqual({ + ok: true, + value: { sum: 42 }, + console: [{ level: 'info', args: ['before async'], text: 'before async' }], + }) + }, + ) + }) + + test('sync harness rejects async binding functions', async () => { + await expect( + withQuickJsMode( + { + asyncBinding: async () => 42, + }, + ({ run }) => run(`bindings.asyncBinding()`), + ), + ).rejects.toThrow('Async binding asyncBinding used with withQuickJsMode') + }) + + test('throws concise QuickJS syntax errors with code frame', async () => { + await expect( + withQuickJsMode({}, ({ run }) => + run(` + const ok = 1 + const broken = { node: bindings.root(), index 0 } + broken + `), + ), + ).rejects.toThrow(QuickJsExecutionError) + + try { + await withQuickJsMode({}, ({ run }) => + run(` + const ok = 1 + const broken = { node: bindings.root(), index 0 } + broken + `), + ) + } catch (error) { + expect(error).toBeInstanceOf(QuickJsExecutionError) + expect((error as Error).message).toContain('SyntaxError: expecting') + expect((error as Error).message).toContain('at eval.js:3') + expect((error as Error).message).toContain('> 3 |') + expect((error as Error).message).toContain('index 0') + } + }) + + test('prints QuickJsExecutionError field shapes for learning', async () => { + const cases = [ + { + label: 'syntax error', + code: ` + const ok = 1 + const broken = { node: bindings.root(), index 0 } + broken + `, + }, + { + label: 'runtime reference error', + code: ` + const ok = 1 + missingReference.property + `, + }, + ] + + const examples: unknown[] = [] + + for (const example of cases) { + try { + await withQuickJsMode({}, ({ run }) => run(example.code)) + } catch (error) { + expect(error).toBeInstanceOf(QuickJsExecutionError) + + const quickJsError = error as QuickJsExecutionError & { cause?: unknown } + const cause = quickJsError.cause + const causeRecord = cause && typeof cause === 'object' ? (cause as Record) : undefined + const ownProperties = Object.fromEntries( + Object.getOwnPropertyNames(quickJsError).map((name) => [ + name, + (quickJsError as unknown as Record)[name], + ]), + ) + + examples.push({ + label: example.label, + name: quickJsError.name, + message: quickJsError.message, + details: quickJsError.details, + cause: causeRecord + ? { + name: causeRecord.name, + message: causeRecord.message, + fileName: causeRecord.fileName, + lineNumber: causeRecord.lineNumber, + columnNumber: causeRecord.columnNumber, + } + : cause, + ownProperties, + stack: quickJsError.stack, + }) + } + } + + expect(examples).toHaveLength(cases.length) + console.log('QuickJsExecutionError examples:', JSON.stringify(examples, null, 2)) + }) +}) diff --git a/agents/yjs-fs-agents/tsconfig.build.json b/packages/quickjs-exec/tsconfig.build.json similarity index 100% rename from agents/yjs-fs-agents/tsconfig.build.json rename to packages/quickjs-exec/tsconfig.build.json diff --git a/packages/quickjs-exec/tsconfig.json b/packages/quickjs-exec/tsconfig.json new file mode 100644 index 0000000..fe64dab --- /dev/null +++ b/packages/quickjs-exec/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "allowJs": true, + "types": ["bun"], + "moduleResolution": "bundler", + "customConditions": ["source"], + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} diff --git a/packages/yjs-fs-editor/package.json b/packages/yjs-fs-editor/package.json index 8c2b021..f268775 100644 --- a/packages/yjs-fs-editor/package.json +++ b/packages/yjs-fs-editor/package.json @@ -26,7 +26,7 @@ "y-monaco": "^0.1.6", "react": "^18.3.1", "react-dom": "^18.3.1", - "yjs": "13.6.30", + "yjs": "^13.6.30", "y-protocols": "1.0.7", "@humanlayer/yjs-fs": "workspace:*" }, diff --git a/packages/yjs-fs-react/package.json b/packages/yjs-fs-react/package.json index 14060df..a2a2dff 100644 --- a/packages/yjs-fs-react/package.json +++ b/packages/yjs-fs-react/package.json @@ -24,7 +24,7 @@ "react": "^18.3.1", "typescript": "catalog:", "y-protocols": "1.0.7", - "yjs": "13.6.30" + "yjs": "^13.6.30" }, "dependencies": { "@humanlayer/yjs-fs": "workspace:*" @@ -32,7 +32,7 @@ "peerDependencies": { "react": "^18.3.1", "y-protocols": "1.0.7", - "yjs": "13.6.30" + "yjs": "^13.6.30" }, "exports": { ".": { diff --git a/packages/yjs-fs/package.json b/packages/yjs-fs/package.json index 7f20204..c1bf2f7 100644 --- a/packages/yjs-fs/package.json +++ b/packages/yjs-fs/package.json @@ -36,7 +36,7 @@ "peerDependencies": { "lib0": "^0.2.0", "typescript": "^5", - "yjs": "13.6.30", + "yjs": "^13.6.30", "y-protocols": "1.0.7" }, "exports": { diff --git a/packages/yjs-rich-markdown/build.ts b/packages/yjs-rich-markdown/build.ts new file mode 100644 index 0000000..c7c718f --- /dev/null +++ b/packages/yjs-rich-markdown/build.ts @@ -0,0 +1,3 @@ +import { buildPackageFromManifest } from '../../scripts/build/package-build.ts' + +await buildPackageFromManifest(import.meta.dir) diff --git a/packages/yjs-rich-markdown/package.json b/packages/yjs-rich-markdown/package.json new file mode 100644 index 0000000..bb6420c --- /dev/null +++ b/packages/yjs-rich-markdown/package.json @@ -0,0 +1,63 @@ +{ + "name": "@humanlayer/yjs-rich-markdown", + "source": "./src/index.ts", + "type": "module", + "version": "0.0.0", + "private": false, + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "publishConfig": { + "access": "public" + }, + "files": [ + "src", + "dist" + ], + "exports": { + ".": { + "source": "./src/index.ts", + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js", + "bun": "./src/index.ts" + } + }, + "scripts": { + "agent": "NODE_USE_SYSTEM_CA=1 YJS_RICH_MARKDOWN_BASE_URL=${YJS_RICH_MARKDOWN_BASE_URL:-http://localhost:5176/v1/yjs/rich-markdown-demo} YJS_RICH_MARKDOWN_DOC_ID=${YJS_RICH_MARKDOWN_DOC_ID:-rich-markdown-learning} bun run src/cli/rich-markdown-agent.ts", + "typecheck": "tsgo --noEmit", + "build": "bun run build:js && bunx tsc -p tsconfig.build.json", + "build:js": "bun run build.ts", + "build:types": "bunx tsc -p tsconfig.build.json" + }, + "devDependencies": { + "@durable-streams/server": "^0.3.1", + "@durable-streams/y-durable-streams": "^0.2.4", + "@types/bun": "latest", + "@ai-sdk/anthropic": "catalog:" + }, + "peerDependencies": { + "@tiptap/core": "3.22.5", + "@tiptap/extension-collaboration": "3.22.5", + "@tiptap/starter-kit": "3.22.5", + "typescript": "^5", + "y-protocols": "1.0.7", + "yjs": "^13.6.30" + }, + "dependencies": { + "@humanlayer/quickjs-exec": "workspace:*", + "ai": "catalog:", + "@tiptap/core": "3.22.5", + "@tiptap/extension-collaboration": "3.22.5", + "@tiptap/markdown": "3.22.5", + "@tiptap/pm": "3.22.5", + "@tiptap/starter-kit": "3.22.5", + "dedent": "1.7.2", + "y-prosemirror": "1.3.7", + "zod": "catalog:" + }, + "author": { + "name": "Kyle Mistele", + "url": "https://x.com/0xblacklight" + } +} diff --git a/packages/yjs-rich-markdown/src/agents/index.ts b/packages/yjs-rich-markdown/src/agents/index.ts new file mode 100644 index 0000000..0794e4e --- /dev/null +++ b/packages/yjs-rich-markdown/src/agents/index.ts @@ -0,0 +1,2 @@ +export * from './rich-markdown-cli-agent' +export * from './yxml-fragment-editor' diff --git a/packages/yjs-rich-markdown/src/agents/rich-markdown-cli-agent.ts b/packages/yjs-rich-markdown/src/agents/rich-markdown-cli-agent.ts new file mode 100644 index 0000000..d6ebb3f --- /dev/null +++ b/packages/yjs-rich-markdown/src/agents/rich-markdown-cli-agent.ts @@ -0,0 +1,111 @@ +import { anthropic } from '@ai-sdk/anthropic' +import { Agent, defineTool, doomLoop, maxSteps, startState, userMessage } from '@humanlayer/agentlayer-core' +import dedent from 'dedent' +import z from 'zod/v4' +import type { RichMarkdownArtifactStore } from '../artifact-store' +import { readArtifactMarkdown, writeArtifactMarkdown } from '../markdown' +import { createEditRichMarkdownTool } from '../tools' + +export type CreateRichMarkdownCliAgentOptions = { + model: ConstructorParameters[0]['model'] + artifactStore: RichMarkdownArtifactStore +} + +export const anthropicEphemeralProviderOptions = { + anthropic: { + cacheControl: { type: 'ephemeral' }, + }, +} as const + +export function createRichMarkdownCliAgent(options: CreateRichMarkdownCliAgentOptions) { + const { model, artifactStore } = options + + const listArtifactsTool = defineTool({ + name: 'list_artifacts', + description: 'List rich markdown artifact paths available in the connected Yjs document.', + input: z.object({}), + execute: async () => { + const artifacts = artifactStore.listArtifacts() + if (artifacts.length === 0) return 'No rich markdown artifacts found.' + return artifacts.map((artifact) => artifact.path).join('\n') + }, + }) + + const readArtifactTool = defineTool({ + name: 'read_artifact', + description: 'Read one rich markdown artifact as Markdown.', + input: z.object({ + path: z.string().describe('Artifact path to read, e.g. /artifacts/README.md'), + }), + execute: async ({ path }) => { + artifactStore.getArtifact(path) + return readArtifactMarkdown(artifactStore.doc, path) + }, + }) + + const writeArtifactTool = defineTool({ + name: 'write_artifact', + description: 'Replace one rich markdown artifact with Markdown content.', + input: z.object({ + path: z.string().describe('Artifact path to write, e.g. /artifacts/README.md'), + markdown: z.string().describe('Full Markdown content for the artifact'), + }), + execute: async ({ path, markdown }) => { + artifactStore.ensureArtifact(path) + writeArtifactMarkdown(artifactStore.doc, path, markdown) + return `Wrote ${path}` + }, + }) + + const inspectArtifactTool = defineTool({ + name: 'inspect_artifact', + description: 'Inspect one rich markdown artifact as its internal YXml serialization.', + input: z.object({ + path: z.string().describe('Artifact path to inspect, e.g. /artifacts/README.md'), + }), + execute: async ({ path }) => { + artifactStore.getArtifact(path) + return artifactStore.getFragment(path).toJSON() + }, + }) + + return new Agent({ + system: cliAgentPrompt, + model, + tools: { + list_artifacts: listArtifactsTool, + read_artifact: readArtifactTool, + write_artifact: writeArtifactTool, + inspect_artifact: inspectArtifactTool, + edit: createEditRichMarkdownTool(artifactStore, { + model, + providerOptions: anthropicEphemeralProviderOptions as ConstructorParameters< + typeof Agent + >[0]['providerOptions'], + }), + }, + providerOptions: anthropicEphemeralProviderOptions as ConstructorParameters[0]['providerOptions'], + stopWhen: [doomLoop(), maxSteps(50)], + }) +} + +export function defaultRichMarkdownCliModel() { + return anthropic(process.env.ANTHROPIC_MODEL ?? 'claude-sonnet-4-6') +} + +export function createRichMarkdownCliAgentState(prompt: string) { + return startState([userMessage(prompt)]) +} + +const cliAgentPrompt = dedent` + You are a CLI agent connected to a live Durable Streams-backed Yjs document containing rich markdown artifacts. + + Use Markdown as the user-facing format: + - Use list_artifacts to discover available artifact paths. + - Use read_artifact to inspect current Markdown. + - Use write_artifact only when replacing an entire artifact is intended. + - Use edit for targeted old_string/new_string edits. The edit tool delegates to a rich markdown apply sub-agent that mutates the live Y.XmlFragment structurally through QuickJS proxy bindings. + - Use inspect_artifact only for debugging internal YXml structure. + + After completing the requested work, respond with a concise summary of changed artifact paths. +` diff --git a/packages/yjs-rich-markdown/src/agents/yxml-fragment-editor.ts b/packages/yjs-rich-markdown/src/agents/yxml-fragment-editor.ts new file mode 100644 index 0000000..9db9ca0 --- /dev/null +++ b/packages/yjs-rich-markdown/src/agents/yxml-fragment-editor.ts @@ -0,0 +1,123 @@ +import { + Agent, + defineTool, + doomLoop, + maxSteps, + type PostToolUseHook, + type PreToolUseHook, + toolCalled, +} from '@humanlayer/agentlayer-core' +import type { JSONContent } from '@tiptap/core' +import dedent from 'dedent' +import { yXmlFragmentToProsemirrorJSON } from 'y-prosemirror' +import type * as Y from 'yjs' +import z from 'zod/v4' +import { tiptapJsonToMarkdown } from '../markdown' +import { createCodeModeYXmlEditorTool } from '../tools/codemode-yxml-editor' +import { YXML_PROXY_AGENT_PROMPT } from '../yxml-proxy-prompt' + +const doneTool = defineTool({ + name: 'done', + description: 'Call this tool when you complete your edits', + input: z.object({}), + execute: async () => { + console.log('Done tool called') + }, +}) + +const stuckTool = defineTool({ + name: 'i_am_stuck', + description: 'Call this tool if you are stuck and unable to complete your edits', + input: z.object({ + message: z.string().describe('Indicate why you are stuck and unable to complete the requested edit'), + }), + execute: async ({ message }) => { + console.error('Agent declared it is stuck:', message) + }, +}) + +export const createYXmlFragmentEditorAgent = (agentConfig: { + modelConfig: ConstructorParameters[0]['model'] + fragment: Y.XmlFragment + preToolHooks?: Array + postToolHooks?: Array + providerOptions?: ConstructorParameters[0]['providerOptions'] +}) => { + const { modelConfig, fragment, preToolHooks, postToolHooks, providerOptions } = agentConfig + + const intructions = editorPrompt(fragment) + + console.log('Editor Prompt:\n\n', intructions) + return new Agent({ + system: [YXML_PROXY_AGENT_PROMPT, intructions], + model: modelConfig, + tools: { + edit_yjs_xml_fragment: createCodeModeYXmlEditorTool({ fragment }), + done: doneTool, + i_am_stuck: stuckTool, + }, + providerOptions, + stopWhen: [ + doomLoop(), + maxSteps(30), // probably too many but we will see. + toolCalled(doneTool.name), + toolCalled(stuckTool.name), + ], + hooks: { + preToolUse: preToolHooks ?? [], + postToolUse: postToolHooks ?? [], + }, + }) +} + +const editorPrompt = (fragment: Y.XmlFragment) => dedent` + + The document you are editing is a live Y.XMLFragment. Other users may be editing it in live time. + The information you see below is a point-in-time snapshot that may be out of date after 1+ edits. + **IMPORTANT**: If you are unable to complete an edit because the document has changed, use the bindings API to inspect the document. + + This is how the document is currently represented in XML (IMPORTANT: the DOCUMENT_XML tags are NOT part of the document's XML structure, they are delimiters in this prompt.): + + ${fragment.toJSON()} + + + The document is rendered to the following markdown format, which is what the user can see. The user cannot see the raw Y.XMLFragment or the derived TipTap JSON. + The markdown is NOT the raw form of the document. + The TipTap document, which is in TipTap's JSON protocol but translated to Y.XMLFragment for collaboration, is the canonical source of the document's state. + This is the document: + + + ${tiptapJsonToMarkdown(yXmlFragmentToProsemirrorJSON(fragment) as JSONContent)} + + + The user has requested the following edit to the document wrapped in XML tags - replace the contents with the contents., based on the markdown that the user is able to see. + + Your job is to interpret this edit instruction in the context of the markdown which the user can see and has requested the edit, and to use the DSL and code execution tool to apply the edit to the Y XML Fragment. + + + Important system reminder: + 1. Use your tools to inspect and edit the document, to indicate when you are finished, or to indicate that you are stuck + 2. Your job is NOT to insert markdown into the XML fragment - rather, it is to use the fragment and the DSL to add the XML Elements, attributes, and text to apply the markdown-requested edit to the document. + 3. You should NOT include literal / XML tags in the document. + 4. The document may change over time, the bindings API will allow you to inspect it as needed. + 5. The console API in the edit tool will help you to inspect as needed. + 6. If an edit attempt fails, think about how you can apply the edit in a different way. Do not give up easily. + + +` + +export const userInstructionForYXmlFragmentEditorAgent = (options: { + oldString: string + newString: string + replaceAll?: boolean +}) => dedent` + + Replace the Following old string with the new string. + + ${options.oldString} + ${options.newString} + + ${options.replaceAll ?? 'Make sure to replace ALL occurrences of old string with new string'} + + + ` diff --git a/packages/yjs-rich-markdown/src/artifact-path.ts b/packages/yjs-rich-markdown/src/artifact-path.ts new file mode 100644 index 0000000..39c3436 --- /dev/null +++ b/packages/yjs-rich-markdown/src/artifact-path.ts @@ -0,0 +1,38 @@ +export type ArtifactPath = string + +export function normalizeArtifactPath(path: string): ArtifactPath { + const trimmed = path.trim() + if (!trimmed) { + throw new InvalidArtifactPathError(path, 'Artifact path must not be empty') + } + + if (!trimmed.startsWith('/')) { + throw new InvalidArtifactPathError(path, 'Artifact path must be absolute') + } + + const normalized = trimmed.replace(/\/+/g, '/') + if ( + normalized.includes('/../') || + normalized.endsWith('/..') || + normalized.includes('/./') || + normalized.endsWith('/.') + ) { + throw new InvalidArtifactPathError(path, 'Artifact path must not contain relative segments') + } + + return normalized +} + +export function artifactFragmentName(path: ArtifactPath): string { + return `artifact:${normalizeArtifactPath(path)}` +} + +export class InvalidArtifactPathError extends Error { + constructor( + readonly path: string, + message: string, + ) { + super(message) + this.name = 'InvalidArtifactPathError' + } +} diff --git a/packages/yjs-rich-markdown/src/artifact-read.ts b/packages/yjs-rich-markdown/src/artifact-read.ts new file mode 100644 index 0000000..82ba2d0 --- /dev/null +++ b/packages/yjs-rich-markdown/src/artifact-read.ts @@ -0,0 +1,6 @@ +import type * as Y from 'yjs' +import { artifactFragment } from './tiptap-bindings' + +export function readArtifact(doc: Y.Doc, path: string): string { + return artifactFragment(doc, path).toString() +} diff --git a/packages/yjs-rich-markdown/src/artifact-store.ts b/packages/yjs-rich-markdown/src/artifact-store.ts new file mode 100644 index 0000000..cd813ca --- /dev/null +++ b/packages/yjs-rich-markdown/src/artifact-store.ts @@ -0,0 +1,86 @@ +import * as Y from 'yjs' +import { type ArtifactPath, artifactFragmentName, normalizeArtifactPath } from './artifact-path' + +export type ArtifactMetadata = { + path: ArtifactPath + title?: string + createdAt: number + modifiedAt: number +} + +export type CreateArtifactOptions = { + title?: string + now?: number +} + +const REGISTRY_NAME = 'artifacts' + +export class RichMarkdownArtifactStore { + readonly doc: Y.Doc + readonly registry: Y.Map + + constructor(doc = new Y.Doc()) { + this.doc = doc + this.registry = doc.getMap(REGISTRY_NAME) + } + + createArtifact(path: string, options: CreateArtifactOptions = {}): ArtifactMetadata { + const normalizedPath = normalizeArtifactPath(path) + const existing = this.registry.get(normalizedPath) + if (existing) { + throw new ArtifactAlreadyExistsError(normalizedPath) + } + + const now = options.now ?? Date.now() + const metadata: ArtifactMetadata = { + path: normalizedPath, + title: options.title, + createdAt: now, + modifiedAt: now, + } + + this.doc.transact(() => { + this.registry.set(normalizedPath, metadata) + this.doc.getXmlFragment(artifactFragmentName(normalizedPath)) + }) + + return metadata + } + + ensureArtifact(path: string, options: CreateArtifactOptions = {}): ArtifactMetadata { + const normalizedPath = normalizeArtifactPath(path) + return this.registry.get(normalizedPath) ?? this.createArtifact(normalizedPath, options) + } + + getArtifact(path: string): ArtifactMetadata { + const normalizedPath = normalizeArtifactPath(path) + const metadata = this.registry.get(normalizedPath) + if (!metadata) { + throw new ArtifactNotFoundError(normalizedPath) + } + return metadata + } + + listArtifacts(): ArtifactMetadata[] { + return Array.from(this.registry.values()).sort((a, b) => a.path.localeCompare(b.path)) + } + + getFragment(path: string): Y.XmlFragment { + const normalizedPath = normalizeArtifactPath(path) + return this.doc.getXmlFragment(artifactFragmentName(normalizedPath)) + } +} + +export class ArtifactAlreadyExistsError extends Error { + constructor(readonly path: ArtifactPath) { + super(`Artifact already exists: ${path}`) + this.name = 'ArtifactAlreadyExistsError' + } +} + +export class ArtifactNotFoundError extends Error { + constructor(readonly path: ArtifactPath) { + super(`Artifact not found: ${path}`) + this.name = 'ArtifactNotFoundError' + } +} diff --git a/packages/yjs-rich-markdown/src/cli/rich-markdown-agent.ts b/packages/yjs-rich-markdown/src/cli/rich-markdown-agent.ts new file mode 100644 index 0000000..ff082db --- /dev/null +++ b/packages/yjs-rich-markdown/src/cli/rich-markdown-agent.ts @@ -0,0 +1,175 @@ +#!/usr/bin/env bun +import readline from 'node:readline/promises' +import { YjsProvider } from '@durable-streams/y-durable-streams' +import type { Agent, AgentState } from '@humanlayer/agentlayer-core' +import { createOutputRenderer, extractLastAssistantText, renderFinish, startState } from '@humanlayer/agentlayer-core' +import { Awareness } from 'y-protocols/awareness' +import * as Y from 'yjs' +import { createRichMarkdownCliAgent, createRichMarkdownCliAgentState, defaultRichMarkdownCliModel } from '../agents' +import { RichMarkdownArtifactStore } from '../artifact-store' + +const DEFAULT_BASE_URL = 'https://localhost:4437/v1/yjs/rich-markdown' +const DEFAULT_DOC_ID = 'rich-artifacts-learning' + +async function main() { + const options = parseArgs(Bun.argv.slice(2)) + const prompt = options.prompt ?? options.positionals.join(' ').trim() + if (!prompt && !options.interactive) { + throw new Error('Usage: bun run rich-agent -- "your instruction" [--base-url URL] [--doc-id DOC_ID]') + } + + const doc = new Y.Doc() + const awareness = new Awareness(doc) + const provider = new YjsProvider({ + doc, + awareness, + docId: options.docId ?? process.env.YJS_RICH_MARKDOWN_DOC_ID ?? DEFAULT_DOC_ID, + baseUrl: options.baseUrl ?? process.env.YJS_RICH_MARKDOWN_BASE_URL ?? DEFAULT_BASE_URL, + liveMode: 'long-poll', + connect: false, + }) + + await provider.connect() + await waitForSync(provider) + + const artifactStore = new RichMarkdownArtifactStore(doc) + const agent = createRichMarkdownCliAgent({ + model: defaultRichMarkdownCliModel(), + artifactStore, + }) + const renderer = createOutputRenderer({ + output: process.stdout, + includeTokenUsage: options.verbose, + includeToolResults: options.verbose, + streamToolArgs: options.streamToolArgs, + }) + + if (options.interactive) { + await runInteractive(agent, prompt ? createRichMarkdownCliAgentState(prompt) : startState([]), renderer, { + runInitialPrompt: !!prompt, + }) + return + } + + const run = agent.run({ state: createRichMarkdownCliAgentState(prompt), stream: true }) + for await (const event of run) { + renderer.onEvent(event) + } + const result = await run.result + renderer.flush() + if (result.finishReason === 'error') { + throw result.error ?? new Error('Rich markdown CLI agent failed') + } + const finalText = extractLastAssistantText(result).trim() + if (finalText) + console.log(`__RICH_MARKDOWN_AGENT_FINAL_MESSAGE_START__${finalText}__RICH_MARKDOWN_AGENT_FINAL_MESSAGE_END__`) + renderFinish(result) +} + +type CliOptions = { + baseUrl?: string + docId?: string + prompt?: string + interactive?: boolean + streamToolArgs?: boolean + verbose?: boolean + positionals: string[] +} + +function parseArgs(args: string[]): CliOptions { + const options: CliOptions = { positionals: [] } + for (let index = 0; index < args.length; index++) { + const arg = args[index] + if (arg === '--base-url') options.baseUrl = requiredValue(args, ++index, arg) + else if (arg === '--doc-id') options.docId = requiredValue(args, ++index, arg) + else if (arg === '--prompt') options.prompt = requiredValue(args, ++index, arg) + else if (arg === '--interactive') options.interactive = true + else if (arg === '--stream') options.streamToolArgs = true + else if (arg === '--verbose') options.verbose = true + else if (arg) options.positionals.push(arg) + } + return options +} + +async function runInteractive( + agent: Agent, + initialState: AgentState, + renderer: ReturnType, + options: { runInitialPrompt?: boolean } = {}, +) { + let state = initialState + let currentAbortController: AbortController | null = null + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }) + + rl.on('SIGINT', () => { + if (currentAbortController) { + currentAbortController.abort() + return + } + rl.close() + process.exit(0) + }) + + try { + if (options.runInitialPrompt) { + const result = await runAgentTurn(agent, state, renderer) + state = result.state + } + + while (true) { + const line = await rl.question('rich-markdown> ') + const text = line.trim() + if (!text) continue + if (text === 'exit' || text === '/exit') return + + state.messages.push({ role: 'user', content: text }) + currentAbortController = new AbortController() + const result = await runAgentTurn(agent, state, renderer, currentAbortController.signal) + currentAbortController = null + state = result.state + } + } finally { + rl.close() + } +} + +async function runAgentTurn( + agent: Agent, + state: AgentState, + renderer: ReturnType, + signal?: AbortSignal, +) { + const run = agent.run({ state, signal, stream: true }) + for await (const event of run) renderer.onEvent(event) + const result = await run.result + renderer.flush() + renderFinish(result) + return result +} + +function requiredValue(args: string[], index: number, flag: string) { + const value = args[index] + if (!value) throw new Error(`Missing value for ${flag}`) + return value +} + +async function waitForSync(provider: YjsProvider, timeoutMs = 10_000) { + if (provider.synced) return + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + provider.off('synced', handleSynced) + reject(new Error(`Timed out waiting for Yjs provider sync after ${timeoutMs}ms`)) + }, timeoutMs) + + const handleSynced = (synced: boolean) => { + if (!synced) return + clearTimeout(timeout) + provider.off('synced', handleSynced) + resolve() + } + + provider.on('synced', handleSynced) + }) +} + +await main() diff --git a/packages/yjs-rich-markdown/src/index.ts b/packages/yjs-rich-markdown/src/index.ts new file mode 100644 index 0000000..5678908 --- /dev/null +++ b/packages/yjs-rich-markdown/src/index.ts @@ -0,0 +1,29 @@ +export type { ArtifactPath } from './artifact-path' +export { artifactFragmentName, InvalidArtifactPathError, normalizeArtifactPath } from './artifact-path' +export { readArtifact } from './artifact-read' +export type { ArtifactMetadata, CreateArtifactOptions } from './artifact-store' +export { ArtifactAlreadyExistsError, ArtifactNotFoundError, RichMarkdownArtifactStore } from './artifact-store' +export { markdownToTiptapJson, readArtifactMarkdown, tiptapJsonToMarkdown, writeArtifactMarkdown } from './markdown' +export type { ArtifactCollaborationMode, ArtifactCollaborationOptions } from './tiptap-bindings' +export { artifactCollaborationExtension, artifactFragment, defaultRichMarkdownExtensions } from './tiptap-bindings' +export type { + YXmlElementWrapperSpec, + YXmlNodeKind, + YXmlNodeRef, + YXmlNodeSpec, + YXmlNodeSummary, + YXmlProxy, + YXmlProxyHostBindings, +} from './yxml-proxy' +export { + DetachedYXmlNodeRefError, + UnknownYXmlNodeRefError, + YXmlChildIndexOutOfBoundsError, + YXmlInvalidNodeKindForOperationError, + YXmlNodeRefKindMismatchError, + YXmlProxyBindings, + YXmlProxyError, + YXmlRootOperationError, + YXmlTextRangeOutOfBoundsError, +} from './yxml-proxy' +export { YXML_PROXY_AGENT_PROMPT } from './yxml-proxy-prompt' diff --git a/packages/yjs-rich-markdown/src/markdown.ts b/packages/yjs-rich-markdown/src/markdown.ts new file mode 100644 index 0000000..4b72e9b --- /dev/null +++ b/packages/yjs-rich-markdown/src/markdown.ts @@ -0,0 +1,37 @@ +import type { JSONContent } from '@tiptap/core' +import { getSchema } from '@tiptap/core' +import { MarkdownManager } from '@tiptap/markdown' +import StarterKit from '@tiptap/starter-kit' +import { prosemirrorJSONToYXmlFragment, yXmlFragmentToProsemirrorJSON } from 'y-prosemirror' +import type * as Y from 'yjs' +import { artifactFragment } from './tiptap-bindings' + +const markdownExtensions = [StarterKit] + +export function markdownToTiptapJson(markdown: string): JSONContent { + return createMarkdownManager().parse(markdown) +} + +export function tiptapJsonToMarkdown(json: JSONContent): string { + return createMarkdownManager().serialize(json) +} + +export function writeArtifactMarkdown(doc: Y.Doc, path: string, markdown: string): void { + const fragment = artifactFragment(doc, path) + const json = markdownToTiptapJson(markdown) + replaceFragmentWithTiptapJson(fragment, json) +} + +export function readArtifactMarkdown(doc: Y.Doc, path: string): string { + const fragment = artifactFragment(doc, path) + return tiptapJsonToMarkdown(yXmlFragmentToProsemirrorJSON(fragment) as JSONContent) +} + +function createMarkdownManager(): MarkdownManager { + return new MarkdownManager({ extensions: markdownExtensions }) +} + +function replaceFragmentWithTiptapJson(fragment: Y.XmlFragment, json: JSONContent): void { + fragment.delete(0, fragment.length) + prosemirrorJSONToYXmlFragment(getSchema(markdownExtensions), json, fragment) +} diff --git a/packages/yjs-rich-markdown/src/tiptap-bindings.ts b/packages/yjs-rich-markdown/src/tiptap-bindings.ts new file mode 100644 index 0000000..364e241 --- /dev/null +++ b/packages/yjs-rich-markdown/src/tiptap-bindings.ts @@ -0,0 +1,40 @@ +import type { Extension } from '@tiptap/core' +import Collaboration from '@tiptap/extension-collaboration' +import StarterKit from '@tiptap/starter-kit' +import type * as Y from 'yjs' +import { type ArtifactPath, artifactFragmentName, normalizeArtifactPath } from './artifact-path' + +export type ArtifactCollaborationMode = 'fragment' | 'field' + +export type ArtifactCollaborationOptions = { + doc: Y.Doc + path: ArtifactPath + mode?: ArtifactCollaborationMode +} + +export function artifactFragment(doc: Y.Doc, path: string): Y.XmlFragment { + return doc.getXmlFragment(artifactFragmentName(normalizeArtifactPath(path))) +} + +export function artifactCollaborationExtension(options: ArtifactCollaborationOptions): Extension { + const path = normalizeArtifactPath(options.path) + if (options.mode === 'field') { + return Collaboration.configure({ + document: options.doc, + field: artifactFragmentName(path), + }) + } + + return Collaboration.configure({ + fragment: artifactFragment(options.doc, path), + }) +} + +export function defaultRichMarkdownExtensions(options: ArtifactCollaborationOptions): Extension[] { + return [ + StarterKit.configure({ + undoRedo: false, + }), + artifactCollaborationExtension(options), + ] +} diff --git a/packages/yjs-rich-markdown/src/tools/codemode-yxml-editor.ts b/packages/yjs-rich-markdown/src/tools/codemode-yxml-editor.ts new file mode 100644 index 0000000..ce93dfe --- /dev/null +++ b/packages/yjs-rich-markdown/src/tools/codemode-yxml-editor.ts @@ -0,0 +1,65 @@ +import { defineTool } from '@humanlayer/agentlayer-core' +import { + type QuickJsRunWithConsoleResult, + type QuickJsSerializedError, + withQuickJsMode, +} from '@humanlayer/quickjs-exec' +import dedent from 'dedent' +import type * as Y from 'yjs' +import z from 'zod/v4' +import { YXmlProxyBindings } from '../yxml-proxy' + +/** + * Given a Y.XMLFragment create a tool that allows an agent to edit it in a sandboxed QuickJS runtime using code mode and a proxy bindings object to wrap the Y doc + * @param options + * @returns + */ +export const createCodeModeYXmlEditorTool = (options: { fragment: Y.XmlFragment }) => { + return defineTool({ + name: 'edit_yjs_xml_fragment', + description: 'Edit Y.js XML Fragments using a JavaScript sandbox with binding APIs', + input: z.object({ + code: z + .string() + .describe( + `'Javascript code to edit the Y.js XML Fragment using the 'bindings' API for mutation and 'console' API for debugging. Only allows pure JavaScript and bindings and console. No Node.js or browser APIs are allowed, and no packages are provided.`, + ), + }), + execute: async ({ code }, ctx) => { + try { + const proxy = new YXmlProxyBindings(options.fragment) + return await withQuickJsMode(proxy.bindings, (qjs) => { + const sandboxedExecutionResult = qjs.runWithConsole(code) + return formatQuickJsResult(sandboxedExecutionResult) + }) + } catch (error: any) { + console.error('Error with quickJS Execution against proxy:', error) + throw error + } + }, + }) +} + +function formatQuickJsResult(result: QuickJsRunWithConsoleResult) { + return dedent` + Executing code... + + Log: + + ${result.console.map((consoleLog) => `[${consoleLog.level}] ${JSON.stringify(consoleLog.args)}`).join('\n')} + + + ${result.ok ? `Execution succeeded. Result:\n${JSON.stringify(result.value)}` : `\n\nExecution failed:\n${formatQuickJsError(result.error)}`} + ` +} + +function formatQuickJsError(error: QuickJsSerializedError) { + return dedent` + + ${error.name} + ${error.message} + ${error.lineNumber} + ${error.columnNumber} + + ` +} diff --git a/packages/yjs-rich-markdown/src/tools/edit-rich-markdown.ts b/packages/yjs-rich-markdown/src/tools/edit-rich-markdown.ts new file mode 100644 index 0000000..9dc4064 --- /dev/null +++ b/packages/yjs-rich-markdown/src/tools/edit-rich-markdown.ts @@ -0,0 +1,63 @@ +import { EDIT_DESCRIPTION, startState, userMessage } from '@humanlayer/agentlayer-core' +import { EditTool } from '@humanlayer/agentlayer-core/interfaces' +import type * as Y from 'yjs' +import { createYXmlFragmentEditorAgent, userInstructionForYXmlFragmentEditorAgent } from '../agents' +import type { RichMarkdownArtifactStore } from '../artifact-store' +import { readArtifactMarkdown } from '../markdown' + +export type CreateEditRichMarkdownToolOptions = { + model: Parameters[0]['modelConfig'] + providerOptions?: Parameters[0]['providerOptions'] +} + +/** + * Edit tool for editing markdown. Uses the EditTool interface but deletes to code-mode sub-agent for edits + * @param artifactStore + * @returns + */ +export function createEditRichMarkdownTool( + artifactStore: RichMarkdownArtifactStore, + options: CreateEditRichMarkdownToolOptions, +) { + return EditTool.define( + async (input, ctx) => { + artifactStore.getArtifact(input.file_path) + const fragment: Y.XmlFragment = artifactStore.getFragment(input.file_path) + const beforeMarkdown = readArtifactMarkdown(artifactStore.doc, input.file_path) + + const applyAgent = createYXmlFragmentEditorAgent({ + modelConfig: options.model, + fragment, + providerOptions: options.providerOptions, + }) + + const run = applyAgent.run({ + state: startState([ + userMessage( + userInstructionForYXmlFragmentEditorAgent({ + oldString: input.old_string, + newString: input.new_string, + replaceAll: input.replace_all, + }), + ), + ]), + stream: ctx.stream, + signal: ctx.signal, + }) + + if (ctx.awaitSubAgent && ctx.toolCallId) { + await ctx.awaitSubAgent(run, `rich-markdown-apply:${input.file_path}`, ctx.toolCallId) + } else { + await run.result + } + + const afterMarkdown = readArtifactMarkdown(artifactStore.doc, input.file_path) + const changed = beforeMarkdown !== afterMarkdown + + return { content: afterMarkdown, matchCount: changed ? 1 : 0 } + }, + { + description: EDIT_DESCRIPTION, + }, + ) +} diff --git a/packages/yjs-rich-markdown/src/tools/index.ts b/packages/yjs-rich-markdown/src/tools/index.ts new file mode 100644 index 0000000..f349ffe --- /dev/null +++ b/packages/yjs-rich-markdown/src/tools/index.ts @@ -0,0 +1,2 @@ +export * from './codemode-yxml-editor' +export * from './edit-rich-markdown' diff --git a/packages/yjs-rich-markdown/src/yxml-proxy-prompt.ts b/packages/yjs-rich-markdown/src/yxml-proxy-prompt.ts new file mode 100644 index 0000000..401778b --- /dev/null +++ b/packages/yjs-rich-markdown/src/yxml-proxy-prompt.ts @@ -0,0 +1,326 @@ +import dedent from 'dedent' + +export const YXML_PROXY_AGENT_PROMPT = dedent` + + You are editing one rich markdown artifact through a scoped Y.XmlFragment DSL. + + The artifact is a rich-text TipTap/ProseMirror document, modeled in Tiptap/ProseMirror's JSON DSL but modeled as a Y.JS XML Fragment for editing. + You have a custom DSL to inspect, manipuate, and edit the document. + + The DSL is available with a \`bindings\` object. Use only these bindings to inspect and mutate the document. Do not import packages, access files, access the network, or assume direct access to Yjs objects. Attempts to do so will fail. + + Node references are opaque JSON values that are only valid for the current execution. + + Available node reference shape: + + \`\`\`ts + type NodeRef = { + id: string + kind: 'fragment' | 'element' | 'text' + } + \`\`\` + + Available content shape: + + \`\`\`ts + type NodeSpec = + | { + kind: 'element' + nodeName: string + attributes?: Record + children?: NodeSpec[] + } + | { + kind: 'text' + text?: string + } + \`\`\` + + Available bindings: + + \`\`\`ts + bindings.root(): NodeRef + bindings.summary({ node: NodeRef }): { id: string; kind: string; nodeName: string | null; length: number; xml: string; attributes?: Record } + bindings.children({ node?: NodeRef }): NodeRef[] + bindings.get({ node?: NodeRef; index: number }): NodeRef + bindings.text({ node: NodeRef }): string + bindings.toString({ node?: NodeRef }): string + bindings.append({ parent?: NodeRef; content: NodeSpec[] }): NodeRef[] + bindings.prepend({ parent?: NodeRef; content: NodeSpec[] }): NodeRef[] + bindings.insertBefore({ ref: NodeRef; content: NodeSpec[] }): NodeRef[] + bindings.insertAfter({ ref: NodeRef; content: NodeSpec[] }): NodeRef[] + bindings.insertText({ node: NodeRef; index: number; text: string }): void + bindings.deleteText({ node: NodeRef; index: number; length: number }): void + bindings.splitText({ node: NodeRef; index: number }): { left: NodeRef; right: NodeRef } + bindings.wrapTextRange({ node: NodeRef; start: number; end: number; wrapper: { nodeName: string; attributes?: Record } }): { wrapper: NodeRef; child: NodeRef; before?: NodeRef; after?: NodeRef } + bindings.wrap({ node: NodeRef; wrapper: { nodeName: string; attributes?: Record } }): { wrapper: NodeRef; child: NodeRef } + bindings.remove({ node: NodeRef }): void + bindings.setAttribute({ node: NodeRef; name: string; value: string }): void + bindings.removeAttribute({ node: NodeRef; name: string }): void + \`\`\` + + Important rules: + + - Call \`bindings.toString({})\` to inspect the full current XML before editing. + - Use \`append\` or \`prepend\` when adding content to the root or to a known parent element. + - Use \`insertBefore\` or \`insertAfter\` when adding content relative to an existing node. These do not need a parent; the host infers it from the referenced node. + - Use \`text\` to read the literal contents of a text node. + - Use \`insertText\` and \`deleteText\` to edit characters inside an existing text node. + - Use \`splitText\` to break one text node into two sibling text nodes at a character offset. + - Use \`wrapTextRange\` to wrap only part of a text node, such as bolding a single word inside a longer text node. + - Use \`wrap\` to surround an existing element or text node with a new element, such as wrapping text in a \`bold\` mark. The old node ref becomes detached; use the returned \`child\` ref if you need to keep editing the wrapped child. + - Use \`remove\` to erase a node and all of its nested children. + - Use \`setAttribute\` to change an element attribute, such as changing a heading from level 2 to level 3. + - Use \`removeAttribute\` to delete an attribute. + - Do not append children to a text node. Only fragment and element nodes can contain children. + - Build nested content by nesting \`children\` arrays in \`NodeSpec\` objects. + - Prefer structural edits over string replacement of the full XML. + - When creating common markdown structures, use the canonical TipTap/ProseMirror node names listed below. Some of these serialize to lowercase in XML strings, but generated \`NodeSpec\` content should use canonical names. + - Generated code is evaluated as a script. Top-level \`return\` is invalid unless you wrap the code in a function yourself. + - If you want to return a final value, end the script with a parenthesized object expression like \`;({ ok: true, beforeXml, afterXml })\`. + - Use \`console.log\`, \`console.info\`, \`console.warn\`, or \`console.error\` for diagnostics such as before/after XML, selected refs, summaries, and branch decisions. + - Console output is captured by the host and returned to the apply agent even if generated code throws after logging. Runtime failures return \`{ ok: false, error, console }\`; successful runs return \`{ ok: true, value, console }\`. + - Log enough context to repair failures: selected \`NodeRef\` values, \`bindings.summary(...)\` output, target text snippets, and final XML. + + Common markdown YXml elements: + + - Paragraph: \`{ kind: 'element', nodeName: 'paragraph', children: [...] }\` + - Heading: \`{ kind: 'element', nodeName: 'heading', attributes: { level: '1' | '2' | '3' | '4' | '5' | '6' }, children: [...] }\` + - Bold mark: \`{ kind: 'element', nodeName: 'bold', children: [...] }\` + - Italic mark: \`{ kind: 'element', nodeName: 'italic', children: [...] }\` + - Strikethrough mark: \`{ kind: 'element', nodeName: 'strike', children: [...] }\` + - Inline code mark: \`{ kind: 'element', nodeName: 'code', children: [{ kind: 'text', text: 'code' }] }\` + - Link mark: \`{ kind: 'element', nodeName: 'link', attributes: { href: 'https://example.com', target: '_blank', rel: 'noopener noreferrer nofollow', class: 'null', title: 'null' }, children: [...] }\` + - Bullet list: \`{ kind: 'element', nodeName: 'bulletList', children: [...] }\` + - Ordered list: \`{ kind: 'element', nodeName: 'orderedList', attributes: { start: '1' }, children: [...] }\` + - List item: \`{ kind: 'element', nodeName: 'listItem', children: [{ kind: 'element', nodeName: 'paragraph', children: [...] }] }\` + - Blockquote: \`{ kind: 'element', nodeName: 'blockquote', children: [{ kind: 'element', nodeName: 'paragraph', children: [...] }] }\` + - Horizontal rule: \`{ kind: 'element', nodeName: 'horizontalRule' }\` + - Hard break inside a paragraph: \`{ kind: 'element', nodeName: 'hardBreak' }\` + - Fenced code block with language: \`{ kind: 'element', nodeName: 'codeBlock', attributes: { language: 'ts' }, children: [{ kind: 'text', text: 'const value = 1' }] }\` + - Multi-line code block: use one text child containing newline characters, e.g. \`{ kind: 'text', text: 'const value = 1\\nconsole.log(value)' }\`. + + Other elements may appear in normal usage. If you see these, do your best to follow existing patterns. + + Important casing note: use canonical camelCase names when creating multi-word TipTap nodes manually: \`bulletList\`, \`listItem\`, \`orderedList\`, \`codeBlock\`, \`hardBreak\`, and \`horizontalRule\`. The XML string returned by \`bindings.toString({})\` may show lowercase names like \`\` or \`\`; do not copy that casing into new \`NodeSpec\` content. + + Example: add a paragraph after the first top-level node. + + \`\`\`js + const beforeXml = bindings.toString({}) + const first = bindings.get({ index: 0 }) + console.log('beforeXml', beforeXml) + console.log('first child', first, bindings.summary({ node: first })) + + bindings.insertAfter({ + ref: first, + content: [ + { + kind: 'element', + nodeName: 'paragraph', + children: [{ kind: 'text', text: 'Added paragraph.' }], + }, + ], + }) + + const afterXml = bindings.toString({}) + console.log('afterXml', afterXml) + ;({ ok: true, beforeXml, afterXml }) + \`\`\` + + Example: add a new section containing a heading, paragraph, list, blockquote, horizontal rule, and code block. + + \`\`\`js + const beforeXml = bindings.toString({}) + console.log('beforeXml', beforeXml) + + bindings.append({ + content: [ + { + kind: 'element', + nodeName: 'heading', + attributes: { level: '2' }, + children: [{ kind: 'text', text: 'Implementation Notes' }], + }, + { + kind: 'element', + nodeName: 'paragraph', + children: [ + { kind: 'text', text: 'The agent edits through ' }, + { kind: 'element', nodeName: 'code', children: [{ kind: 'text', text: 'bindings' }] }, + { kind: 'text', text: ' instead of direct Yjs objects.' }, + ], + }, + { + kind: 'element', + nodeName: 'bulletList', + children: [ + { + kind: 'element', + nodeName: 'listItem', + children: [{ kind: 'element', nodeName: 'paragraph', children: [{ kind: 'text', text: 'Use scoped NodeRefs.' }] }], + }, + { + kind: 'element', + nodeName: 'listItem', + children: [{ kind: 'element', nodeName: 'paragraph', children: [{ kind: 'text', text: 'Validate after mutation.' }] }], + }, + ], + }, + { + kind: 'element', + nodeName: 'blockquote', + children: [{ kind: 'element', nodeName: 'paragraph', children: [{ kind: 'text', text: 'Bindings keep generated code sandbox-safe.' }] }], + }, + { kind: 'element', nodeName: 'horizontalRule' }, + { + kind: 'element', + nodeName: 'codeBlock', + attributes: { language: 'ts' }, + children: [{ kind: 'text', text: 'const root = bindings.root()\\nconst xml = bindings.toString({})' }], + }, + ], + }) + + const afterXml = bindings.toString({}) + console.log('afterXml', afterXml) + ;({ ok: true, beforeXml, afterXml }) + \`\`\` + + Example: add a bullet to an existing list. + + \`\`\`js + const beforeXml = bindings.toString({}) + const rootChildren = bindings.children({}) + const list = rootChildren.find((node) => bindings.summary({ node }).nodeName === 'bulletList') + if (!list) throw new Error('Could not find a bullet list') + console.log('list', list, bindings.summary({ node: list })) + + bindings.append({ + parent: list, + content: [ + { + kind: 'element', + nodeName: 'listItem', + children: [ + { + kind: 'element', + nodeName: 'paragraph', + children: [{ kind: 'text', text: 'New bullet' }], + }, + ], + }, + ], + }) + + const afterXml = bindings.toString({}) + console.log('afterXml', afterXml) + ;({ ok: true, beforeXml, afterXml }) + \`\`\` + + Example: change a heading level attribute. + + \`\`\`js + const beforeXml = bindings.toString({}) + const heading = bindings.children({}).find((node) => bindings.summary({ node }).nodeName === 'heading') + if (!heading) throw new Error('Could not find a heading') + console.log('heading before', heading, bindings.summary({ node: heading })) + + bindings.setAttribute({ node: heading, name: 'level', value: '3' }) + console.log('heading after', bindings.summary({ node: heading })) + + const afterXml = bindings.toString({}) + console.log('afterXml', afterXml) + ;({ ok: true, beforeXml, afterXml }) + \`\`\` + + Example: erase a section. + + \`\`\`js + const beforeXml = bindings.toString({}) + const section = bindings.children({}).find((node) => { + const summary = bindings.summary({ node }) + return summary.nodeName === 'section' && summary.attributes?.id === 'obsolete' + }) + if (!section) throw new Error('Could not find obsolete section') + console.log('removing section', section, bindings.summary({ node: section })) + + bindings.remove({ node: section }) + + const afterXml = bindings.toString({}) + console.log('afterXml', afterXml) + ;({ ok: true, beforeXml, afterXml }) + \`\`\` + + Example: wrap existing deeply nested text in a bold mark. + + \`\`\`js + const beforeXml = bindings.toString({}) + const list = bindings.children({}).find((node) => bindings.summary({ node }).nodeName === 'bulletList') + if (!list) throw new Error('Could not find a bullet list') + const secondItem = bindings.get({ node: list, index: 1 }) + const paragraph = bindings.get({ node: secondItem, index: 0 }) + const text = bindings.children({ node: paragraph }).find((node) => bindings.summary({ node }).kind === 'text') + if (!text) throw new Error('Could not find text to bold') + console.log('wrapping text', text, bindings.summary({ node: text })) + + bindings.wrap({ node: text, wrapper: { nodeName: 'bold' } }) + + const afterXml = bindings.toString({}) + console.log('afterXml', afterXml) + ;({ ok: true, beforeXml, afterXml }) + \`\`\` + + Example: wrap only one word inside an interleaved text node. + + \`\`\`js + const beforeXml = bindings.toString({}) + const example = bindings.children({}).find((node) => bindings.summary({ node }).nodeName === 'example') + if (!example) throw new Error('Could not find example element') + const firstChild = bindings.get({ node: example, index: 0 }) + const firstText = bindings.text({ node: firstChild }) + const start = firstText.indexOf('example') + if (start < 0) throw new Error('Could not find target word') + console.log('wrapping range', { start, end: start + 'example'.length, firstText }) + + bindings.wrapTextRange({ + node: firstChild, + start, + end: start + 'example'.length, + wrapper: { nodeName: 'bold' }, + }) + + const afterXml = bindings.toString({}) + console.log('afterXml', afterXml) + ;({ ok: true, beforeXml, afterXml }) + \`\`\` + + Example: wrap one word inside text that already lives under an inline mark. + + \`\`\`js + const beforeXml = bindings.toString({}) + const paragraph = bindings.children({}).find((node) => bindings.summary({ node }).nodeName === 'paragraph') + if (!paragraph) throw new Error('Could not find paragraph') + const italic = bindings.children({ node: paragraph }).find((node) => bindings.summary({ node }).nodeName === 'italic') + if (!italic) throw new Error('Could not find italic mark') + const italicTextNode = bindings.children({ node: italic }).find((node) => bindings.summary({ node }).kind === 'text') + if (!italicTextNode) throw new Error('Could not find italic text node') + + const italicText = bindings.text({ node: italicTextNode }) + const start = italicText.indexOf('example') + if (start < 0) throw new Error('Could not find target word') + console.log('wrapping italic range', { start, end: start + 'example'.length, italicText }) + + bindings.wrapTextRange({ + node: italicTextNode, + start, + end: start + 'example'.length, + wrapper: { nodeName: 'bold' }, + }) + + const afterXml = bindings.toString({}) + console.log('afterXml', afterXml) + ;({ ok: true, beforeXml, afterXml }) + \`\`\` + +` diff --git a/packages/yjs-rich-markdown/src/yxml-proxy.ts b/packages/yjs-rich-markdown/src/yxml-proxy.ts new file mode 100644 index 0000000..dab292b --- /dev/null +++ b/packages/yjs-rich-markdown/src/yxml-proxy.ts @@ -0,0 +1,514 @@ +import type { QuickJsBindings } from '@humanlayer/quickjs-exec' +import * as Y from 'yjs' + +export type YXmlNodeKind = 'fragment' | 'element' | 'text' + +export type YXmlNodeRef = { + id: string + kind: YXmlNodeKind +} + +export type YXmlNodeSummary = YXmlNodeRef & { + nodeName: string | null + length: number + xml: string + attributes?: Record +} + +export type YXmlNodeSpec = + | { + kind: 'element' + nodeName: string + attributes?: Record + children?: YXmlNodeSpec[] + } + | { + kind: 'text' + text?: string + } + +export type YXmlElementWrapperSpec = { + nodeName: string + attributes?: Record +} + +export type YXmlProxy = { + root(): YXmlNodeRef + summary(input: { node: YXmlNodeRef }): YXmlNodeSummary + children(input?: { node?: YXmlNodeRef }): YXmlNodeRef[] + get(input: { node?: YXmlNodeRef; index: number }): YXmlNodeRef + text(input: { node: YXmlNodeRef }): string + toString(input?: { node?: YXmlNodeRef }): string + append(input: { parent?: YXmlNodeRef; content: YXmlNodeSpec[] }): YXmlNodeRef[] + prepend(input: { parent?: YXmlNodeRef; content: YXmlNodeSpec[] }): YXmlNodeRef[] + insertBefore(input: { ref: YXmlNodeRef; content: YXmlNodeSpec[] }): YXmlNodeRef[] + insertAfter(input: { ref: YXmlNodeRef; content: YXmlNodeSpec[] }): YXmlNodeRef[] + insertText(input: { node: YXmlNodeRef; index: number; text: string }): void + deleteText(input: { node: YXmlNodeRef; index: number; length: number }): void + splitText(input: { node: YXmlNodeRef; index: number }): { left: YXmlNodeRef; right: YXmlNodeRef } + wrapTextRange(input: { node: YXmlNodeRef; start: number; end: number; wrapper: YXmlElementWrapperSpec }): { + wrapper: YXmlNodeRef + child: YXmlNodeRef + before?: YXmlNodeRef + after?: YXmlNodeRef + } + wrap(input: { node: YXmlNodeRef; wrapper: YXmlElementWrapperSpec }): { wrapper: YXmlNodeRef; child: YXmlNodeRef } + remove(input: { node: YXmlNodeRef }): void + setAttribute(input: { node: YXmlNodeRef; name: string; value: string }): void + removeAttribute(input: { node: YXmlNodeRef; name: string }): void +} + +export type YXmlProxyHostBindings = YXmlProxy & QuickJsBindings + +export class YXmlProxyError extends Error { + constructor( + message: string, + readonly code: string, + ) { + super(message) + this.name = 'YXmlProxyError' + } +} + +export class UnknownYXmlNodeRefError extends YXmlProxyError { + constructor(readonly ref: YXmlNodeRef) { + super(`Unknown YXml node ref: ${ref.id}`, 'UNKNOWN_NODE_REF') + this.name = 'UnknownYXmlNodeRefError' + } +} + +export class YXmlNodeRefKindMismatchError extends YXmlProxyError { + constructor( + readonly ref: YXmlNodeRef, + readonly actualKind: YXmlNodeKind, + ) { + super( + `YXml node ref kind mismatch for ${ref.id}: expected ${ref.kind}, got ${actualKind}`, + 'NODE_REF_KIND_MISMATCH', + ) + this.name = 'YXmlNodeRefKindMismatchError' + } +} + +export class YXmlInvalidNodeKindForOperationError extends YXmlProxyError { + constructor( + readonly operation: string, + readonly expected: string, + readonly actual: YXmlNodeKind, + ) { + super(`Cannot ${operation} on ${actual} node; expected ${expected}`, 'INVALID_NODE_KIND_FOR_OPERATION') + this.name = 'YXmlInvalidNodeKindForOperationError' + } +} + +export class YXmlChildIndexOutOfBoundsError extends YXmlProxyError { + constructor( + readonly index: number, + readonly length: number, + ) { + super(`No YXml child at index ${index}; child count is ${length}`, 'CHILD_INDEX_OUT_OF_BOUNDS') + this.name = 'YXmlChildIndexOutOfBoundsError' + } +} + +export class YXmlTextRangeOutOfBoundsError extends YXmlProxyError { + constructor( + readonly operation: string, + readonly start: number, + readonly end: number, + readonly length: number, + ) { + super(`Cannot ${operation} text range [${start}, ${end}) on text length ${length}`, 'TEXT_RANGE_OUT_OF_BOUNDS') + this.name = 'YXmlTextRangeOutOfBoundsError' + } +} + +export class DetachedYXmlNodeRefError extends YXmlProxyError { + constructor(readonly operation: string) { + super(`Cannot ${operation} deleted YXml node`, 'DETACHED_NODE_REF') + this.name = 'DetachedYXmlNodeRefError' + } +} + +export class YXmlRootOperationError extends YXmlProxyError { + constructor(readonly operation: string) { + super(`Cannot ${operation} the root YXml fragment`, 'ROOT_OPERATION') + this.name = 'YXmlRootOperationError' + } +} + +type YXmlContainer = Y.XmlFragment | Y.XmlElement +type YXmlNode = YXmlContainer | Y.XmlText +type InsertableYXmlNode = Y.XmlElement | Y.XmlText + +export class YXmlProxyBindings implements YXmlProxy { + private readonly nodes = new Map() + private readonly refs = new WeakMap() + private readonly deletedRefs = new Set() + private nextId = 1 + readonly bindings: YXmlProxyHostBindings + + constructor(private readonly rootFragment: Y.XmlFragment) { + this.register(rootFragment) + this.bindings = { + root: () => this.root(), + summary: (input) => this.summary(input as { node: YXmlNodeRef }), + children: (input) => this.children(input as { node?: YXmlNodeRef } | undefined), + get: (input) => this.get(input as { node?: YXmlNodeRef; index: number }), + text: (input) => this.text(input as { node: YXmlNodeRef }), + toString: (input) => this.toString(input as { node?: YXmlNodeRef } | undefined), + append: (input) => this.append(input as { parent?: YXmlNodeRef; content: YXmlNodeSpec[] }), + prepend: (input) => this.prepend(input as { parent?: YXmlNodeRef; content: YXmlNodeSpec[] }), + insertBefore: (input) => this.insertBefore(input as { ref: YXmlNodeRef; content: YXmlNodeSpec[] }), + insertAfter: (input) => this.insertAfter(input as { ref: YXmlNodeRef; content: YXmlNodeSpec[] }), + insertText: (input) => this.insertText(input as { node: YXmlNodeRef; index: number; text: string }), + deleteText: (input) => this.deleteText(input as { node: YXmlNodeRef; index: number; length: number }), + splitText: (input) => this.splitText(input as { node: YXmlNodeRef; index: number }), + wrapTextRange: (input) => + this.wrapTextRange( + input as { + node: YXmlNodeRef + start: number + end: number + wrapper: YXmlElementWrapperSpec + }, + ), + wrap: (input) => this.wrap(input as { node: YXmlNodeRef; wrapper: YXmlElementWrapperSpec }), + remove: (input) => this.remove(input as { node: YXmlNodeRef }), + setAttribute: (input) => this.setAttribute(input as { node: YXmlNodeRef; name: string; value: string }), + removeAttribute: (input) => this.removeAttribute(input as { node: YXmlNodeRef; name: string }), + } as YXmlProxyHostBindings + } + + root(): YXmlNodeRef { + return this.register(this.rootFragment) + } + + summary({ node }: { node: YXmlNodeRef }): YXmlNodeSummary { + const resolved = this.resolve(node) + const ref = this.register(resolved) + return { + ...ref, + nodeName: resolved instanceof Y.XmlElement ? resolved.nodeName : null, + length: resolved.length, + xml: resolved.toString(), + ...(resolved instanceof Y.XmlElement ? { attributes: resolved.getAttributes() } : {}), + } + } + + children(input: { node?: YXmlNodeRef } = {}): YXmlNodeRef[] { + return this.resolveContainer(input.node, 'read children from') + .slice() + .map((node) => this.register(node)) + } + + get({ node, index }: { node?: YXmlNodeRef; index: number }): YXmlNodeRef { + const container = this.resolveContainer(node, 'get child from') + const child = container.get(index) + if (!child) throw new YXmlChildIndexOutOfBoundsError(index, container.length) + return this.register(child) + } + + text({ node }: { node: YXmlNodeRef }): string { + return this.resolveText(node).toString() + } + + toString(input: { node?: YXmlNodeRef } = {}): string { + return this.resolve(input.node).toString() + } + + append({ parent, content }: { parent?: YXmlNodeRef; content: YXmlNodeSpec[] }): YXmlNodeRef[] { + const container = this.resolveContainer(parent, 'append children to') + const nodesToInsert = this.buildNodes(content) + container.push(nodesToInsert) + return this.refsFor(nodesToInsert) + } + + prepend({ parent, content }: { parent?: YXmlNodeRef; content: YXmlNodeSpec[] }): YXmlNodeRef[] { + const container = this.resolveContainer(parent, 'prepend children to') + const nodesToInsert = this.buildNodes(content) + container.unshift(nodesToInsert) + return this.refsFor(nodesToInsert) + } + + insertBefore({ ref, content }: { ref: YXmlNodeRef; content: YXmlNodeSpec[] }): YXmlNodeRef[] { + if (this.deletedRefs.has(ref.id)) throw new DetachedYXmlNodeRefError('insert before') + const target = this.resolve(ref) + if (target === this.rootFragment) throw new YXmlRootOperationError('insert before') + const parent = this.parentContainerOf(target, 'insert before') + const index = parent.slice().indexOf(target as Y.XmlElement | Y.XmlText) + if (index < 0) throw new DetachedYXmlNodeRefError('insert before') + const nodesToInsert = this.buildNodes(content) + parent.insert(index, nodesToInsert) + return this.refsFor(nodesToInsert) + } + + insertAfter({ ref, content }: { ref: YXmlNodeRef; content: YXmlNodeSpec[] }): YXmlNodeRef[] { + if (this.deletedRefs.has(ref.id)) throw new DetachedYXmlNodeRefError('insert after') + const target = this.resolve(ref) + if (target === this.rootFragment) throw new YXmlRootOperationError('insert after') + const parent = this.parentContainerOf(target, 'insert after') + const nodesToInsert = this.buildNodes(content) + parent.insertAfter(target as Y.XmlElement | Y.XmlText, nodesToInsert) + return this.refsFor(nodesToInsert) + } + + insertText({ node, index, text }: { node: YXmlNodeRef; index: number; text: string }): void { + if (this.deletedRefs.has(node.id)) throw new DetachedYXmlNodeRefError('insert text') + const target = this.resolveText(node) + this.assertTextRange({ operation: 'insert text into', length: target.length, start: index, end: index }) + if (text.length === 0) return + target.insert(index, text) + } + + deleteText({ node, index, length }: { node: YXmlNodeRef; index: number; length: number }): void { + if (this.deletedRefs.has(node.id)) throw new DetachedYXmlNodeRefError('delete text') + const target = this.resolveText(node) + this.assertTextRange({ + operation: 'delete text from', + length: target.length, + start: index, + end: index + length, + }) + if (length === 0) return + target.delete(index, length) + } + + splitText({ node, index }: { node: YXmlNodeRef; index: number }): { left: YXmlNodeRef; right: YXmlNodeRef } { + if (this.deletedRefs.has(node.id)) throw new DetachedYXmlNodeRefError('split text') + const target = this.resolveText(node) + this.assertTextRange({ operation: 'split text', length: target.length, start: index, end: index }) + if (index === 0 || index === target.length) { + throw new YXmlTextRangeOutOfBoundsError('split text', index, index, target.length) + } + + const value = target.toString() + const left = this.buildTextNode(value.slice(0, index)) + const right = this.buildTextNode(value.slice(index)) + this.replaceNode(target, 'split text', [left, right], node.id) + + return { left: this.register(left), right: this.register(right) } + } + + wrapTextRange({ + node, + start, + end, + wrapper, + }: { + node: YXmlNodeRef + start: number + end: number + wrapper: YXmlElementWrapperSpec + }): { wrapper: YXmlNodeRef; child: YXmlNodeRef; before?: YXmlNodeRef; after?: YXmlNodeRef } { + if (this.deletedRefs.has(node.id)) throw new DetachedYXmlNodeRefError('wrap text range') + const target = this.resolveText(node) + this.assertTextRange({ operation: 'wrap text range in', length: target.length, start, end }) + if (start === end) throw new YXmlTextRangeOutOfBoundsError('wrap text range', start, end, target.length) + + const value = target.toString() + const beforeText = value.slice(0, start) + const wrappedText = value.slice(start, end) + const afterText = value.slice(end) + + const nodesToInsert: InsertableYXmlNode[] = [] + let beforeNode: Y.XmlText | undefined + if (beforeText.length > 0) { + beforeNode = this.buildTextNode(beforeText) + nodesToInsert.push(beforeNode) + } + + const child = this.buildTextNode(wrappedText) + const wrapperNode = new Y.XmlElement(wrapper.nodeName) + for (const [name, value] of Object.entries(wrapper.attributes ?? {})) { + wrapperNode.setAttribute(name, value) + } + wrapperNode.insert(0, [child]) + nodesToInsert.push(wrapperNode) + + let afterNode: Y.XmlText | undefined + if (afterText.length > 0) { + afterNode = this.buildTextNode(afterText) + nodesToInsert.push(afterNode) + } + + this.replaceNode(target, 'wrap text range', nodesToInsert, node.id) + + return { + ...(beforeNode ? { before: this.register(beforeNode) } : {}), + wrapper: this.register(wrapperNode), + child: this.register(child), + ...(afterNode ? { after: this.register(afterNode) } : {}), + } + } + + wrap({ node, wrapper }: { node: YXmlNodeRef; wrapper: YXmlElementWrapperSpec }): { + wrapper: YXmlNodeRef + child: YXmlNodeRef + } { + if (this.deletedRefs.has(node.id)) throw new DetachedYXmlNodeRefError('wrap') + const target = this.resolve(node) + if (target === this.rootFragment) throw new YXmlRootOperationError('wrap') + const parent = this.parentContainerOf(target, 'wrap') + const index = parent.slice().indexOf(target as Y.XmlElement | Y.XmlText) + if (index < 0) throw new DetachedYXmlNodeRefError('wrap') + + const child = this.cloneNode(target as InsertableYXmlNode) + const wrapperNode = new Y.XmlElement(wrapper.nodeName) + for (const [name, value] of Object.entries(wrapper.attributes ?? {})) { + wrapperNode.setAttribute(name, value) + } + wrapperNode.insert(0, [child]) + + parent.delete(index, 1) + parent.insert(index, [wrapperNode]) + this.deletedRefs.add(node.id) + + return { wrapper: this.register(wrapperNode), child: this.register(child) } + } + + remove({ node }: { node: YXmlNodeRef }): void { + if (this.deletedRefs.has(node.id)) throw new DetachedYXmlNodeRefError('remove') + const target = this.resolve(node) + if (target === this.rootFragment) throw new YXmlRootOperationError('remove') + const parent = this.parentContainerOf(target, 'remove') + const index = parent.slice().indexOf(target as Y.XmlElement | Y.XmlText) + if (index < 0) throw new DetachedYXmlNodeRefError('remove') + parent.delete(index, 1) + this.deletedRefs.add(node.id) + } + + setAttribute({ node, name, value }: { node: YXmlNodeRef; name: string; value: string }): void { + this.resolveElement(node).setAttribute(name, value) + } + + removeAttribute({ node, name }: { node: YXmlNodeRef; name: string }): void { + this.resolveElement(node).removeAttribute(name) + } + + private register(node: YXmlNode): YXmlNodeRef { + const existing = this.refs.get(node) + if (existing) return existing + + const ref = { id: `n${this.nextId++}`, kind: kindOf(node) } + this.nodes.set(ref.id, node) + this.refs.set(node, ref) + return ref + } + + private resolve(ref?: YXmlNodeRef): YXmlNode { + if (!ref) return this.rootFragment + + const node = this.nodes.get(ref.id) + if (!node) throw new UnknownYXmlNodeRefError(ref) + const actualKind = kindOf(node) + if (actualKind !== ref.kind) throw new YXmlNodeRefKindMismatchError(ref, actualKind) + return node + } + + private resolveContainer(ref: YXmlNodeRef | undefined, operation: string): YXmlContainer { + const node = this.resolve(ref) + if (node instanceof Y.XmlText) + throw new YXmlInvalidNodeKindForOperationError(operation, 'fragment or element', 'text') + return node + } + + private resolveElement(ref: YXmlNodeRef): Y.XmlElement { + const node = this.resolve(ref) + if (!(node instanceof Y.XmlElement)) + throw new YXmlInvalidNodeKindForOperationError('set or remove attribute', 'element', kindOf(node)) + return node + } + + private resolveText(ref: YXmlNodeRef): Y.XmlText { + const node = this.resolve(ref) + if (!(node instanceof Y.XmlText)) + throw new YXmlInvalidNodeKindForOperationError('read or edit text', 'text', kindOf(node)) + return node + } + + private buildNodes(specs: YXmlNodeSpec[]): InsertableYXmlNode[] { + return specs.map((spec) => this.buildNode(spec)) + } + + private buildNode(spec: YXmlNodeSpec): InsertableYXmlNode { + if (spec.kind === 'text') { + return this.buildTextNode(spec.text ?? '') + } + + const element = new Y.XmlElement(spec.nodeName) + for (const [name, value] of Object.entries(spec.attributes ?? {})) { + element.setAttribute(name, value) + } + const children = this.buildNodes(spec.children ?? []) + if (children.length > 0) element.insert(0, children) + return element + } + + private refsFor(content: InsertableYXmlNode[]): YXmlNodeRef[] { + return content.map((node) => this.register(node)) + } + + private buildTextNode(value: string): Y.XmlText { + const text = new Y.XmlText() + if (value.length > 0) text.insert(0, value) + return text + } + + private cloneNode(node: InsertableYXmlNode): InsertableYXmlNode { + if (node instanceof Y.XmlText) { + return this.buildTextNode(node.toString()) + } + + const element = new Y.XmlElement(node.nodeName) + for (const [name, value] of Object.entries(node.getAttributes())) { + element.setAttribute(name, value ?? '') + } + const children = node.slice().map((child) => this.cloneNode(child as InsertableYXmlNode)) + if (children.length > 0) element.insert(0, children) + return element + } + + private replaceNode( + node: InsertableYXmlNode, + operation: string, + replacements: InsertableYXmlNode[], + deletedRefId: string, + ): void { + const parent = this.parentContainerOf(node, operation) + const index = parent.slice().indexOf(node) + if (index < 0) throw new DetachedYXmlNodeRefError(operation) + parent.delete(index, 1) + if (replacements.length > 0) parent.insert(index, replacements) + this.deletedRefs.add(deletedRefId) + } + + private assertTextRange({ + operation, + length, + start, + end, + }: { + operation: string + length: number + start: number + end: number + }): void { + if (start < 0 || end < start || end > length) { + throw new YXmlTextRangeOutOfBoundsError(operation, start, end, length) + } + } + + private parentContainerOf(node: YXmlNode, operation: string): YXmlContainer { + if (node.doc === null) throw new DetachedYXmlNodeRefError(operation) + const parent = node.parent + if (parent instanceof Y.XmlFragment || parent instanceof Y.XmlElement) return parent + throw new DetachedYXmlNodeRefError(operation) + } +} + +function kindOf(node: YXmlNode): YXmlNodeKind { + if (node instanceof Y.XmlText) return 'text' + if ('nodeName' in node) return 'element' + return 'fragment' +} diff --git a/packages/yjs-rich-markdown/test/apply-agent.test.ts b/packages/yjs-rich-markdown/test/apply-agent.test.ts new file mode 100644 index 0000000..123ce44 --- /dev/null +++ b/packages/yjs-rich-markdown/test/apply-agent.test.ts @@ -0,0 +1,275 @@ +import { beforeEach, describe, expect, setDefaultTimeout, test } from 'bun:test' +import { anthropic } from '@ai-sdk/anthropic' +import { + type Agent, + type PostToolUseHook, + type PreToolUseHook, + startState, + userMessage, +} from '@humanlayer/agentlayer-core' +import { yXmlFragmentToProsemirrorJSON } from 'y-prosemirror' +import * as Y from 'yjs' +import { tiptapJsonToMarkdown } from '../src' +import { createYXmlFragmentEditorAgent, userInstructionForYXmlFragmentEditorAgent } from '../src/agents' + +setDefaultTimeout(200_000) + +describe.skipIf(!process.env.ANTHROPIC_API_KEY || !!process.env.CI)('Live Apply Agent Tests', async () => { + let doc: Y.Doc + let fragment: Y.XmlFragment + let createApplyAgent: (oldString: string, newString: string, replaceAll?: boolean) => Agent + let toolCalls: string[] + + beforeEach(() => { + doc = new Y.Doc() + fragment = doc.getXmlFragment('fragment') + toolCalls = [] + + const inputLoggerHook: PreToolUseHook = (ctx) => { + toolCalls.push(ctx.toolName) + console.log(`Pre-tool-use hook for ${ctx.toolName} with input: ${JSON.stringify(ctx.input)}`) + return ctx.next() + } + + const outputLoggerHook: PostToolUseHook = (ctx) => { + console.log(`Post-tool-use hook for ${ctx.toolName} with output ${JSON.stringify(ctx.output)}`) + return ctx.done() + } + + createApplyAgent = (oldString, newString, replaceAll) => { + return createYXmlFragmentEditorAgent({ + modelConfig: anthropic('claude-sonnet-4-6'), + fragment, + preToolHooks: [inputLoggerHook], + postToolHooks: [outputLoggerHook], + }) + } + }) + + test('Simple apply should succeed', async () => { + const heading = new Y.XmlElement('heading') + fragment.insert(0, [heading]) + + heading.setAttribute('level', '1') + heading.push([new Y.XmlText('This is a second-level heading')]) + + const oldString = '#' + const newString = '##' + expect(tiptapJsonToMarkdown(yXmlFragmentToProsemirrorJSON(fragment))).toContain('#') + expect(tiptapJsonToMarkdown(yXmlFragmentToProsemirrorJSON(fragment))).not.toContain('##') + expect(fragment.toJSON()).toEqual('This is a second-level heading') + + const applyAgent = createApplyAgent(oldString, newString, true) + + const userInstruction = userInstructionForYXmlFragmentEditorAgent({ oldString, newString }) + console.log(`user instruction:`, userInstruction) + + const run = applyAgent.run({ + state: startState([userMessage(userInstruction)]), + }) + + await expect(run.result).resolves.not.toBeUndefined() + const _runResult = await run.result + logToolCallCount('simple apply', toolCalls) + + expect(tiptapJsonToMarkdown(yXmlFragmentToProsemirrorJSON(fragment))).toContain('##') + expect(fragment.toJSON()).toEqual('This is a second-level heading') + expect(toolCalls.length).toBeGreaterThan(0) + }) +}) + +describe.skipIf(!process.env.ANTHROPIC_API_KEY || !!process.env.CI)( + 'Live Apply Agent Tests - complex nested document', + async () => { + let doc: Y.Doc + let fragment: Y.XmlFragment + let createApplyAgent: (oldString: string, newString: string, replaceAll?: boolean) => Agent + let toolCalls: string[] + + beforeEach(() => { + doc = new Y.Doc() + fragment = doc.getXmlFragment('fragment') + fragment.insert(0, createComplexDocumentNodes()) + toolCalls = [] + + const inputLoggerHook: PreToolUseHook = (ctx) => { + toolCalls.push(ctx.toolName) + console.log(`Pre-tool-use hook for ${ctx.toolName} with input: ${JSON.stringify(ctx.input)}`) + return ctx.next() + } + + const outputLoggerHook: PostToolUseHook = (ctx) => { + console.log(`Post-tool-use hook for ${ctx.toolName} with output ${JSON.stringify(ctx.output)}`) + return ctx.done() + } + + createApplyAgent = () => { + return createYXmlFragmentEditorAgent({ + modelConfig: anthropic('claude-sonnet-4-6'), + fragment, + preToolHooks: [inputLoggerHook], + postToolHooks: [outputLoggerHook], + }) + } + }) + + test('removes text inside inline code nested in a bulleted list item', async () => { + expect(fragment.toJSON()).toContain('this is a code block in python in a bulleted list') + + await applyEdit({ + agent: createApplyAgent('in python', '', true), + oldString: 'in python', + newString: '', + replaceAll: true, + }) + logToolCallCount('removing nested inline code text', toolCalls) + + const codeContents = codeTagContents(fragment.toJSON()) + expect(codeContents).toContain('this is a code block') + expect(codeContents).not.toContain('this is a code block in python') + expect(codeContents.some((content) => content.includes('in python'))).toBe(false) + expect(toolCalls.length).toBeGreaterThan(0) + }) + + test('edits text inside a nested ordered list item', async () => { + expect(fragment.toJSON()).toContain('numbered ') + + await applyEdit({ + agent: createApplyAgent('numbered', 'ordered'), + oldString: 'numbered', + newString: 'ordered', + }) + logToolCallCount('editing ordered list item', toolCalls) + + expect(fragment.toJSON()).toContain('ordered ') + expect(fragment.toJSON()).not.toContain('numbered') + expect(toolCalls.length).toBeGreaterThan(0) + }) + + test('edits text inside an inline bold mark in a paragraph', async () => { + expect(fragment.toJSON()).toContain( + 'This is a bold and italic item in a paragraph', + ) + + await applyEdit({ + agent: createApplyAgent('is a bold', 'is bold'), + oldString: 'is a bold', + newString: 'is bold', + }) + logToolCallCount('editing inline bold text', toolCalls) + + expect(fragment.toJSON()).toContain( + 'This is bold and italic item in a paragraph', + ) + expect(fragment.toJSON()).not.toContain('is a bold') + expect(toolCalls.length).toBeGreaterThan(0) + }) + }, +) + +function logToolCallCount(label: string, toolCalls: string[]) { + const editToolCalls = toolCalls.filter((toolName) => toolName === 'edit_yjs_xml_fragment') + console.log( + `Tool calls for ${label}: ${toolCalls.length} total, ${editToolCalls.length} edit calls (${toolCalls.join(', ')})`, + ) +} + +function codeTagContents(xml: string) { + return [...xml.matchAll(/(.*?)<\/code>/g)].map((match) => match[1] ?? '') +} + +async function applyEdit(options: { agent: Agent; oldString: string; newString: string; replaceAll?: boolean }) { + const run = options.agent.run({ + state: startState([ + userMessage( + userInstructionForYXmlFragmentEditorAgent({ + oldString: options.oldString, + newString: options.newString, + replaceAll: options.replaceAll, + }), + ), + ]), + }) + + await expect(run.result).resolves.not.toBeUndefined() +} + +function createComplexDocumentNodes(): Array { + return [ + element('heading', { level: '1' }, [ + element( + 'link', + { + class: 'null', + href: 'http://README.md', + rel: 'noopener noreferrer nofollow', + target: '_blank', + title: 'null', + }, + [text('README.md')], + ), + text(' '), + ]), + element('paragraph', {}, [text('This is an example file')]), + element('paragraph'), + element('paragraph'), + list('bulletList', ['this ', 'is a ', 'bulleted list']), + element('paragraph'), + element('paragraph'), + list('orderedList', ['This ', 'is ', 'a', 'numbered ', 'list'], { start: '1' }), + element('paragraph'), + element('codeBlock', {}, [text('this is a code block')]), + element('paragraph'), + element('paragraph'), + element('paragraph'), + element('paragraph', {}, [element('bold', {}, [text("i don't like code blocks")])]), + element('paragraph'), + element('codeBlock', { language: 'typescript' }, [text('this is a typescript code block')]), + element('paragraph'), + element('paragraph'), + element('bulletList', {}, [ + element('listItem', {}, [ + element('paragraph', {}, [ + element('code', {}, [text('this is a code block in python')]), + text(' in a bulleted list'), + ]), + ]), + element('listItem', {}, [element('paragraph', {}, [text('this is a bulleted list item ')])]), + ]), + element('paragraph'), + element('paragraph'), + element('paragraph', {}, [ + text('This '), + element('bold', {}, [text('is a bold')]), + text(' and '), + element('italic', {}, [text('italic')]), + text(' item in a paragraph'), + ]), + element('paragraph'), + ] +} + +function list(nodeName: 'bulletList' | 'orderedList', items: string[], attributes: Record = {}) { + return element( + nodeName, + attributes, + items.map((item) => element('listItem', {}, [element('paragraph', {}, [text(item)])])), + ) +} + +function element( + name: string, + attributes: Record = {}, + children: Array = [], +) { + const node = new Y.XmlElement(name) + for (const [key, value] of Object.entries(attributes)) node.setAttribute(key, value) + if (children.length > 0) node.insert(0, children) + return node +} + +function text(value: string) { + const node = new Y.XmlText() + node.insert(0, value) + return node +} diff --git a/packages/yjs-rich-markdown/test/artifact-read.test.ts b/packages/yjs-rich-markdown/test/artifact-read.test.ts new file mode 100644 index 0000000..8a2a0dd --- /dev/null +++ b/packages/yjs-rich-markdown/test/artifact-read.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, test } from 'bun:test' +import * as Y from 'yjs' +import { artifactFragment, RichMarkdownArtifactStore, readArtifact } from '../src' + +function syncDoc(source: Y.Doc, target: Y.Doc): void { + Y.applyUpdate(target, Y.encodeStateAsUpdate(source)) +} + +describe('readArtifact', () => { + test('returns the XML fragment string for an artifact path', () => { + const doc = new Y.Doc() + const store = new RichMarkdownArtifactStore(doc) + + store.createArtifact('/artifacts/plan.md') + const heading = new Y.XmlElement('heading') + heading.setAttribute('level', '1') + heading.insert(0, [new Y.XmlText('Plan')]) + artifactFragment(doc, '/artifacts/plan.md').insert(0, [heading]) + + expect(readArtifact(doc, '/artifacts/plan.md')).toBe('Plan') + }) + + test('reads only the requested path-keyed fragment', () => { + const doc = new Y.Doc() + const store = new RichMarkdownArtifactStore(doc) + + store.createArtifact('/artifacts/a.md') + store.createArtifact('/artifacts/b.md') + artifactFragment(doc, '/artifacts/a.md').insert(0, [new Y.XmlText('Artifact A')]) + artifactFragment(doc, '/artifacts/b.md').insert(0, [new Y.XmlText('Artifact B')]) + + expect(readArtifact(doc, '/artifacts/a.md')).toBe('Artifact A') + expect(readArtifact(doc, '/artifacts/b.md')).toBe('Artifact B') + }) + + test('returns an empty string for an empty artifact fragment', () => { + const doc = new Y.Doc() + const store = new RichMarkdownArtifactStore(doc) + + store.createArtifact('/artifacts/empty.md') + + expect(readArtifact(doc, '/artifacts/empty.md')).toBe('') + }) + + test('returns synced fragment content after a Yjs update round trip', () => { + const doc1 = new Y.Doc() + const doc2 = new Y.Doc() + const store1 = new RichMarkdownArtifactStore(doc1) + + store1.createArtifact('/artifacts/synced.md') + artifactFragment(doc1, '/artifacts/synced.md').insert(0, [new Y.XmlText('Synced content')]) + + syncDoc(doc1, doc2) + + expect(readArtifact(doc2, '/artifacts/synced.md')).toBe('Synced content') + }) +}) diff --git a/packages/yjs-rich-markdown/test/fragment-isolation.test.ts b/packages/yjs-rich-markdown/test/fragment-isolation.test.ts new file mode 100644 index 0000000..8f7b12e --- /dev/null +++ b/packages/yjs-rich-markdown/test/fragment-isolation.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, test } from 'bun:test' +import * as Y from 'yjs' +import { artifactFragment, artifactFragmentName, RichMarkdownArtifactStore } from '../src' + +function syncDoc(source: Y.Doc, target: Y.Doc): void { + Y.applyUpdate(target, Y.encodeStateAsUpdate(source)) +} + +describe('rich markdown artifact fragments', () => { + test('stores multiple artifact fragments in one Y.Doc without cross-contamination', () => { + const doc1 = new Y.Doc() + const doc2 = new Y.Doc() + const store1 = new RichMarkdownArtifactStore(doc1) + const store2 = new RichMarkdownArtifactStore(doc2) + + store1.createArtifact('/artifacts/a.md', { now: 1, title: 'A' }) + store1.createArtifact('/artifacts/b.md', { now: 2, title: 'B' }) + + artifactFragment(doc1, '/artifacts/a.md').insert(0, [new Y.XmlText('Artifact A')]) + artifactFragment(doc1, '/artifacts/b.md').insert(0, [new Y.XmlText('Artifact B')]) + + syncDoc(doc1, doc2) + + expect(store2.listArtifacts().map((artifact) => artifact.path)).toEqual(['/artifacts/a.md', '/artifacts/b.md']) + expect(artifactFragment(doc2, '/artifacts/a.md').toString()).toBe('Artifact A') + expect(artifactFragment(doc2, '/artifacts/b.md').toString()).toBe('Artifact B') + expect(doc2.getXmlFragment(artifactFragmentName('/artifacts/a.md'))).not.toBe( + doc2.getXmlFragment(artifactFragmentName('/artifacts/b.md')), + ) + }) +}) diff --git a/packages/yjs-rich-markdown/test/markdown-yxml-shapes.test.ts b/packages/yjs-rich-markdown/test/markdown-yxml-shapes.test.ts new file mode 100644 index 0000000..49ca6f2 --- /dev/null +++ b/packages/yjs-rich-markdown/test/markdown-yxml-shapes.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, test } from 'bun:test' +import * as Y from 'yjs' +import { RichMarkdownArtifactStore, writeArtifactMarkdown } from '../src' + +function xmlFor(markdown: string): string { + const doc = new Y.Doc() + const store = new RichMarkdownArtifactStore(doc) + const path = '/artifacts/shapes.md' + store.createArtifact(path) + writeArtifactMarkdown(doc, path, markdown) + return store.getFragment(path).toString() +} + +describe('Markdown to YXml shapes', () => { + test('headings', () => { + expect(xmlFor('# H1\n\n## H2\n\n### H3')).toBe( + 'H1H2H3', + ) + }) + + test('paragraph with inline marks', () => { + expect(xmlFor('Plain **bold** *italic* `code` [link](https://example.com).')).toBe( + 'Plain bold italic code link.', + ) + }) + + test('unordered and ordered lists', () => { + expect(xmlFor('- First\n- Second\n\n1. One\n2. Two')).toBe( + 'FirstSecondOneTwo', + ) + }) + + test('nested unordered list', () => { + expect(xmlFor('- Parent\n - Child')).toBe( + 'ParentChild', + ) + }) + + test('blockquote', () => { + expect(xmlFor('> Quote line')).toBe('
Quote line
') + }) + + test('horizontal rule', () => { + expect(xmlFor('Before\n\n---\n\nAfter')).toBe( + 'BeforeAfter', + ) + }) + + test('hard break inside paragraph', () => { + expect(xmlFor('First line\\\nSecond line')).toBe( + 'First lineSecond line', + ) + }) + + test('single-line fenced code block', () => { + expect(xmlFor('```ts\nconst value = 1\n```')).toBe('const value = 1') + }) + + test('multi-line fenced code block', () => { + expect(xmlFor('```ts\nconst value = 1\nconsole.log(value)\n```')).toBe( + 'const value = 1\nconsole.log(value)', + ) + }) + + test('indented code block', () => { + expect(xmlFor(' const value = 1\n console.log(value)')).toBe( + 'const value = 1\nconsole.log(value)', + ) + }) + + test('strikethrough', () => { + expect(xmlFor('~~deleted~~')).toBe('deleted') + }) +}) diff --git a/packages/yjs-rich-markdown/test/markdown.test.ts b/packages/yjs-rich-markdown/test/markdown.test.ts new file mode 100644 index 0000000..5e323c4 --- /dev/null +++ b/packages/yjs-rich-markdown/test/markdown.test.ts @@ -0,0 +1,88 @@ +import { describe, expect, test } from 'bun:test' +import * as Y from 'yjs' +import { RichMarkdownArtifactStore, readArtifactMarkdown, writeArtifactMarkdown } from '../src' + +function syncDoc(source: Y.Doc, target: Y.Doc): void { + Y.applyUpdate(target, Y.encodeStateAsUpdate(source)) +} + +describe('markdown artifact bridge', () => { + test('writes markdown into a Yjs TipTap fragment and reads it back as markdown', () => { + const doc = new Y.Doc() + const store = new RichMarkdownArtifactStore(doc) + + store.createArtifact('/artifacts/plan.md') + writeArtifactMarkdown(doc, '/artifacts/plan.md', '# Plan\n\nThis is **bold** and *useful*.') + + expect(store.getFragment('/artifacts/plan.md').toString()).toBe( + 'PlanThis is bold and useful.', + ) + expect(readArtifactMarkdown(doc, '/artifacts/plan.md')).toBe('# Plan\n\nThis is **bold** and *useful*.') + }) + + test('round trips representative markdown blocks and marks', () => { + const doc = new Y.Doc() + const store = new RichMarkdownArtifactStore(doc) + const markdown = [ + '# Title', + '', + 'Paragraph with **bold**, *italic*, `code`, and [a link](https://example.com).', + '', + '- First', + '- Second', + '', + '1. One', + '2. Two', + '', + '> A quote', + '', + '```ts', + 'const value = 1', + '```', + ].join('\n') + + store.createArtifact('/artifacts/rich.md') + writeArtifactMarkdown(doc, '/artifacts/rich.md', markdown) + + expect(readArtifactMarkdown(doc, '/artifacts/rich.md')).toBe(markdown) + }) + + test('keeps markdown writes isolated by artifact path', () => { + const doc = new Y.Doc() + const store = new RichMarkdownArtifactStore(doc) + + store.createArtifact('/artifacts/a.md') + store.createArtifact('/artifacts/b.md') + writeArtifactMarkdown(doc, '/artifacts/a.md', '# A') + writeArtifactMarkdown(doc, '/artifacts/b.md', '# B') + + expect(readArtifactMarkdown(doc, '/artifacts/a.md')).toBe('# A') + expect(readArtifactMarkdown(doc, '/artifacts/b.md')).toBe('# B') + }) + + test('reads markdown after a Yjs update sync round trip', () => { + const doc1 = new Y.Doc() + const doc2 = new Y.Doc() + const store1 = new RichMarkdownArtifactStore(doc1) + + store1.createArtifact('/artifacts/synced.md') + writeArtifactMarkdown(doc1, '/artifacts/synced.md', '## Synced\n\nShared content.') + syncDoc(doc1, doc2) + + expect(readArtifactMarkdown(doc2, '/artifacts/synced.md')).toBe('## Synced\n\nShared content.') + }) + + test('write replaces existing fragment content from markdown', () => { + const doc = new Y.Doc() + const store = new RichMarkdownArtifactStore(doc) + + store.createArtifact('/artifacts/rewrite.md') + writeArtifactMarkdown(doc, '/artifacts/rewrite.md', '# Old') + writeArtifactMarkdown(doc, '/artifacts/rewrite.md', '# New\n\nReplacement.') + + expect(readArtifactMarkdown(doc, '/artifacts/rewrite.md')).toBe('# New\n\nReplacement.') + expect(store.getFragment('/artifacts/rewrite.md').toString()).toBe( + 'NewReplacement.', + ) + }) +}) diff --git a/packages/yjs-rich-markdown/test/quickjs-code-mode-tool.test.ts b/packages/yjs-rich-markdown/test/quickjs-code-mode-tool.test.ts new file mode 100644 index 0000000..cdab46b --- /dev/null +++ b/packages/yjs-rich-markdown/test/quickjs-code-mode-tool.test.ts @@ -0,0 +1,56 @@ +import { beforeEach, describe, expect, test } from 'bun:test' +import type { ToolContext } from '@humanlayer/agentlayer-core' +import dedent from 'dedent' +import * as Y from 'yjs' +import { createCodeModeYXmlEditorTool } from '../src/tools' + +describe('QuickJS Code Mode Tool Tests', async () => { + let doc: Y.Doc + let fragment: Y.XmlFragment + let root: Y.XmlElement + + beforeEach(() => { + doc = new Y.Doc() + fragment = doc.getXmlFragment('fragment') + root = new Y.XmlElement('root') + fragment.insert(0, [root]) + expect(fragment.toJSON()).toEqual('') + }) + test('Edits made through the tool should pass', async () => { + const tool = createCodeModeYXmlEditorTool({ fragment }) + + const toolExecution = tool.execute( + { + code: dedent` + const fragmentRef = bindings.root() + const rootRef = bindings.get({ node: fragmentRef, index: 0 }) + bindings.append({ + parent: rootRef, + content: [{ kind: 'element', nodeName: 'parent' }], + })`, + }, + {} as ToolContext, + ) + + await expect(toolExecution).resolves.toContain('Execution succeeded') + expect(fragment.toJSON()).toEqual('') + + const secondToolExecution = tool.execute( + { + code: dedent` + const fragmentRef = bindings.root() + const rootRef = bindings.get({ node: fragmentRef, index: 0 }) + const parentRef = bindings.get({ node: rootRef, index: 0 }) + bindings.append({ + parent: parentRef, + content: [{ kind: 'element', nodeName: 'child' }], + }) + `, + }, + {} as ToolContext, + ) + + await expect(secondToolExecution).resolves.toContain('Execution succeeded') + expect(fragment.toJSON()).toEqual('') + }) +}) diff --git a/packages/yjs-rich-markdown/test/yxml-invalid-structures.test.ts b/packages/yjs-rich-markdown/test/yxml-invalid-structures.test.ts new file mode 100644 index 0000000..32c1c9d --- /dev/null +++ b/packages/yjs-rich-markdown/test/yxml-invalid-structures.test.ts @@ -0,0 +1,104 @@ +import { describe, expect, test } from 'bun:test' +import type { JSONContent } from '@tiptap/core' +import { yXmlFragmentToProsemirrorJSON } from 'y-prosemirror' +import * as Y from 'yjs' +import { markdownToTiptapJson, tiptapJsonToMarkdown } from '../src' + +function text(value: string): Y.XmlText { + const node = new Y.XmlText() + node.insert(0, value) + return node +} + +function element(nodeName: string, children: Array = []): Y.XmlElement { + const node = new Y.XmlElement(nodeName) + if (children.length > 0) node.insert(0, children) + return node +} + +function jsonFor(fragment: Y.XmlFragment): JSONContent { + return yXmlFragmentToProsemirrorJSON(fragment) as JSONContent +} + +describe('invalid or lossy YXml structures', () => { + test('unsupported block node type survives YXml but is dropped during markdown serialization', () => { + const doc = new Y.Doc() + const fragment = doc.getXmlFragment('invalid') + fragment.insert(0, [element('madeupblock', [text('Unknown block')])]) + + expect(fragment.toString()).toBe('Unknown block') + expect(jsonFor(fragment)).toEqual({ + type: 'doc', + content: [{ type: 'madeupblock', content: [{ type: 'text', text: 'Unknown block' }] }], + }) + expect(tiptapJsonToMarkdown(jsonFor(fragment))).toBe('') + }) + + test('unsupported nested node type survives YXml but is dropped during markdown serialization', () => { + const doc = new Y.Doc() + const fragment = doc.getXmlFragment('invalid') + fragment.insert(0, [ + element('paragraph', [text('Before '), element('custommark', [text('marked')]), text(' after')]), + ]) + + expect(fragment.toString()).toBe('Before marked after') + expect(jsonFor(fragment)).toEqual({ + type: 'doc', + content: [ + { + type: 'paragraph', + content: [ + { type: 'text', text: 'Before ' }, + { type: 'custommark', content: [{ type: 'text', text: 'marked' }] }, + { type: 'text', text: ' after' }, + ], + }, + ], + }) + expect(tiptapJsonToMarkdown(jsonFor(fragment))).toBe('Before after') + }) + + test('schema-invalid paragraph nesting converts to JSON and flattens during markdown serialization', () => { + const doc = new Y.Doc() + const fragment = doc.getXmlFragment('invalid') + fragment.insert(0, [element('paragraph', [element('paragraph', [text('Nested paragraph')])])]) + + expect(fragment.toString()).toBe('Nested paragraph') + expect(jsonFor(fragment)).toEqual({ + type: 'doc', + content: [ + { + type: 'paragraph', + content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Nested paragraph' }] }], + }, + ], + }) + expect(tiptapJsonToMarkdown(jsonFor(fragment))).toBe('Nested paragraph') + }) + + test('whitespace in paragraphs is preserved by markdown round trip', () => { + const doc = new Y.Doc() + const fragment = doc.getXmlFragment('whitespace') + fragment.insert(0, [element('paragraph', [text('one two')])]) + + const markdown = tiptapJsonToMarkdown(jsonFor(fragment)) + const reparsedMarkdown = tiptapJsonToMarkdown(markdownToTiptapJson(markdown)) + + expect(markdown).toBe('one two') + expect(reparsedMarkdown).toBe('one two') + }) + + test('whitespace in fenced code blocks is preserved by markdown round trip', () => { + const doc = new Y.Doc() + const fragment = doc.getXmlFragment('code') + const code = element('codeBlock', [text('const value = 1\n console.log(value)')]) + code.setAttribute('language', 'ts') + fragment.insert(0, [code]) + + const markdown = tiptapJsonToMarkdown(jsonFor(fragment)) + const reparsedMarkdown = tiptapJsonToMarkdown(markdownToTiptapJson(markdown)) + + expect(markdown).toBe('```ts\nconst value = 1\n console.log(value)\n```') + expect(reparsedMarkdown).toBe(markdown) + }) +}) diff --git a/packages/yjs-rich-markdown/test/yxml-proxy-prompt-examples.test.ts b/packages/yjs-rich-markdown/test/yxml-proxy-prompt-examples.test.ts new file mode 100644 index 0000000..1b9f791 --- /dev/null +++ b/packages/yjs-rich-markdown/test/yxml-proxy-prompt-examples.test.ts @@ -0,0 +1,304 @@ +import { describe, expect, test } from 'bun:test' +import * as Y from 'yjs' +import { YXmlProxyBindings } from '../src' + +describe('YXML proxy prompt examples', () => { + test('add a paragraph after the first top-level node', () => { + const proxy = createPromptExampleProxy() + + const beforeXml = proxy.toString({}) + expect(beforeXml).toContain('Original Heading') + + const first = proxy.get({ index: 0 }) + proxy.insertAfter({ + ref: first, + content: [ + { + kind: 'element', + nodeName: 'paragraph', + children: [{ kind: 'text', text: 'Added paragraph.' }], + }, + ], + }) + + expect(proxy.toString({})).toBe( + 'Original HeadingAdded paragraph.Original paragraph.Use scoped NodeRefs.Validate after mutation.
Remove me.
', + ) + }) + + test('add a new section containing a heading, paragraph, list, blockquote, horizontal rule, and code block', () => { + const proxy = createPromptExampleProxy() + + proxy.append({ + content: [ + { + kind: 'element', + nodeName: 'heading', + attributes: { level: '2' }, + children: [{ kind: 'text', text: 'Implementation Notes' }], + }, + { + kind: 'element', + nodeName: 'paragraph', + children: [ + { kind: 'text', text: 'The agent edits through ' }, + { kind: 'element', nodeName: 'code', children: [{ kind: 'text', text: 'bindings' }] }, + { kind: 'text', text: ' instead of direct Yjs objects.' }, + ], + }, + { + kind: 'element', + nodeName: 'bulletList', + children: [ + { + kind: 'element', + nodeName: 'listItem', + children: [ + { + kind: 'element', + nodeName: 'paragraph', + children: [{ kind: 'text', text: 'Use scoped NodeRefs.' }], + }, + ], + }, + { + kind: 'element', + nodeName: 'listItem', + children: [ + { + kind: 'element', + nodeName: 'paragraph', + children: [{ kind: 'text', text: 'Validate after mutation.' }], + }, + ], + }, + ], + }, + { + kind: 'element', + nodeName: 'blockquote', + children: [ + { + kind: 'element', + nodeName: 'paragraph', + children: [{ kind: 'text', text: 'Bindings keep generated code sandbox-safe.' }], + }, + ], + }, + { kind: 'element', nodeName: 'horizontalRule' }, + { + kind: 'element', + nodeName: 'codeBlock', + attributes: { language: 'ts' }, + children: [ + { kind: 'text', text: 'const root = bindings.root()\nconst xml = bindings.toString({})' }, + ], + }, + ], + }) + + expect(proxy.toString({})).toContain('Implementation Notes') + expect(proxy.toString({})).toContain( + 'The agent edits through bindings instead of direct Yjs objects.', + ) + expect(proxy.toString({})).toContain( + '
Bindings keep generated code sandbox-safe.
', + ) + expect(proxy.toString({})).toContain('') + expect(proxy.toString({})).toContain( + 'const root = bindings.root()\nconst xml = bindings.toString({})', + ) + }) + + test('add a bullet to an existing list', () => { + const proxy = createPromptExampleProxy() + + const rootChildren = proxy.children({}) + const list = rootChildren.find((node) => proxy.summary({ node }).nodeName === 'bulletList') + if (!list) throw new Error('Could not find a bullet list') + + proxy.append({ + parent: list, + content: [ + { + kind: 'element', + nodeName: 'listItem', + children: [ + { + kind: 'element', + nodeName: 'paragraph', + children: [{ kind: 'text', text: 'New bullet' }], + }, + ], + }, + ], + }) + + expect(proxy.toString({})).toContain( + 'Use scoped NodeRefs.Validate after mutation.New bullet', + ) + }) + + test('change a heading level attribute', () => { + const proxy = createPromptExampleProxy() + + const heading = proxy.children({}).find((node) => proxy.summary({ node }).nodeName === 'heading') + if (!heading) throw new Error('Could not find a heading') + + proxy.setAttribute({ node: heading, name: 'level', value: '3' }) + + expect(proxy.summary({ node: heading }).attributes).toEqual({ level: '3' }) + expect(proxy.toString({})).toContain('Original Heading') + }) + + test('erase a section', () => { + const proxy = createPromptExampleProxy() + + const section = proxy.children({}).find((node) => { + const summary = proxy.summary({ node }) + return summary.nodeName === 'section' && summary.attributes?.id === 'obsolete' + }) + if (!section) throw new Error('Could not find obsolete section') + + proxy.remove({ node: section }) + + expect(proxy.toString({})).not.toContain('
') + expect(proxy.toString({})).toBe( + 'Original HeadingOriginal paragraph.Use scoped NodeRefs.Validate after mutation.', + ) + }) + + test('wrap existing deeply nested text in a bold mark', () => { + const proxy = createPromptExampleProxy() + + const list = proxy.children({}).find((node) => proxy.summary({ node }).nodeName === 'bulletList') + if (!list) throw new Error('Could not find a bullet list') + const secondItem = proxy.get({ node: list, index: 1 }) + const paragraph = proxy.get({ node: secondItem, index: 0 }) + const text = proxy.children({ node: paragraph }).find((node) => proxy.summary({ node }).kind === 'text') + if (!text) throw new Error('Could not find text to bold') + + proxy.wrap({ node: text, wrapper: { nodeName: 'bold' } }) + + expect(proxy.toString({})).toContain( + 'Validate after mutation.', + ) + }) + + test('wrap only one word inside an interleaved text node', () => { + const proxy = createInterleavedExampleProxy() + + const example = proxy.children({}).find((node) => proxy.summary({ node }).nodeName === 'example') + if (!example) throw new Error('Could not find example element') + const firstChild = proxy.get({ node: example, index: 0 }) + const firstText = proxy.text({ node: firstChild }) + const start = firstText.indexOf('example') + if (start < 0) throw new Error('Could not find target word') + + proxy.wrapTextRange({ + node: firstChild, + start, + end: start + 'example'.length, + wrapper: { nodeName: 'bold' }, + }) + + expect(proxy.toString({})).toBe( + 'this is an example this is a nested example This is another example', + ) + }) + + test('wrap one word inside text that already lives under an inline mark', () => { + const proxy = createInlineMarkedExampleProxy() + + const paragraph = proxy.children({}).find((node) => proxy.summary({ node }).nodeName === 'paragraph') + if (!paragraph) throw new Error('Could not find paragraph') + const italic = proxy.children({ node: paragraph }).find((node) => proxy.summary({ node }).nodeName === 'italic') + if (!italic) throw new Error('Could not find italic mark') + const italicTextNode = proxy.children({ node: italic }).find((node) => proxy.summary({ node }).kind === 'text') + if (!italicTextNode) throw new Error('Could not find italic text node') + + const italicText = proxy.text({ node: italicTextNode }) + const start = italicText.indexOf('example') + if (start < 0) throw new Error('Could not find target word') + + proxy.wrapTextRange({ + node: italicTextNode, + start, + end: start + 'example'.length, + wrapper: { nodeName: 'bold' }, + }) + + expect(proxy.toString({})).toBe( + 'Before nested example text after', + ) + }) +}) + +function createPromptExampleProxy(): YXmlProxyBindings { + const doc = new Y.Doc() + const fragment = doc.getXmlFragment('artifact') + seedPromptExampleDocument(fragment) + return new YXmlProxyBindings(fragment) +} + +function createInterleavedExampleProxy(): YXmlProxyBindings { + const doc = new Y.Doc() + const fragment = doc.getXmlFragment('artifact') + const example = new Y.XmlElement('example') + example.insert(0, [ + text('this is an example '), + createElement('nestedExample', [text('this is a nested example')]), + text(' This is another example'), + ]) + fragment.insert(0, [example]) + return new YXmlProxyBindings(fragment) +} + +function createInlineMarkedExampleProxy(): YXmlProxyBindings { + const doc = new Y.Doc() + const fragment = doc.getXmlFragment('artifact') + const paragraph = new Y.XmlElement('paragraph') + paragraph.insert(0, [text('Before '), createElement('italic', [text('nested example text')]), text(' after')]) + fragment.insert(0, [paragraph]) + return new YXmlProxyBindings(fragment) +} + +function seedPromptExampleDocument(fragment: Y.XmlFragment): void { + const heading = new Y.XmlElement('heading') + heading.setAttribute('level', '2') + heading.insert(0, [text('Original Heading')]) + + const paragraph = new Y.XmlElement('paragraph') + paragraph.insert(0, [text('Original paragraph.')]) + + const list = new Y.XmlElement('bulletList') + list.insert(0, [listItem('Use scoped NodeRefs.'), listItem('Validate after mutation.')]) + + const section = new Y.XmlElement('section') + section.setAttribute('id', 'obsolete') + const sectionParagraph = new Y.XmlElement('paragraph') + sectionParagraph.insert(0, [text('Remove me.')]) + section.insert(0, [sectionParagraph]) + + fragment.insert(0, [heading, paragraph, list, section]) +} + +function listItem(value: string): Y.XmlElement { + const item = new Y.XmlElement('listItem') + const paragraph = new Y.XmlElement('paragraph') + paragraph.insert(0, [text(value)]) + item.insert(0, [paragraph]) + return item +} + +function createElement(name: string, children: Array): Y.XmlElement { + const element = new Y.XmlElement(name) + element.insert(0, children) + return element +} + +function text(value: string): Y.XmlText { + const xmlText = new Y.XmlText() + xmlText.insert(0, value) + return xmlText +} diff --git a/packages/yjs-rich-markdown/test/yxml-proxy-quickjs.test.ts b/packages/yjs-rich-markdown/test/yxml-proxy-quickjs.test.ts new file mode 100644 index 0000000..753f9ae --- /dev/null +++ b/packages/yjs-rich-markdown/test/yxml-proxy-quickjs.test.ts @@ -0,0 +1,245 @@ +import { beforeEach, describe, expect, test } from 'bun:test' +import { withQuickJsMode } from '@humanlayer/quickjs-exec' +import * as Y from 'yjs' +import { YXmlProxyBindings } from '../src' + +describe('Y.XMLFragment Proxy tests', () => { + describe('Operations through proxy should propagate to document', () => { + let doc: Y.Doc + let fragment: Y.XmlFragment + let proxy: YXmlProxyBindings + let root: Y.XmlElement + + beforeEach(() => { + doc = new Y.Doc() + fragment = doc.getXmlFragment('artifact') + proxy = new YXmlProxyBindings(fragment) + root = new Y.XmlElement('root') + }) + + test('Adding fragment should succeed', () => { + fragment.insert(0, [root]) + expect(fragment.toJSON()).toEqual('') + }) + + test('Adding another fragment NOT through proxy should succeed', () => { + fragment.insert(0, [root]) + expect(fragment.toJSON()).toEqual('') + + const parent = new Y.XmlElement('parent') + const child = new Y.XmlElement('child') + + parent.insert(0, [child]) + root.insert(0, [parent]) + + expect(fragment.toJSON()).toEqual('') + }) + + test('Adding another fragment through proxy should succeed', () => { + fragment.insert(0, [root]) + expect(fragment.toJSON()).toEqual('') + + const fragmentRef = proxy.root() + const rootRef = proxy.get({ node: fragmentRef, index: 0 }) + expect(proxy.toString({ node: rootRef })).toEqual('') + + const insertedParent = proxy.append({ + parent: rootRef, + content: [{ kind: 'element', nodeName: 'parent' }], + }) + expect(insertedParent).toHaveLength(1) + const parentRef = insertedParent[0] + if (!parentRef) throw new Error('Expected parent ref') + + expect(proxy.toString({ node: parentRef })).toEqual('') + expect(fragment.toJSON()).toEqual('') + + const insertedChild = proxy.append({ + parent: parentRef, + content: [{ kind: 'element', nodeName: 'child' }], + }) + expect(insertedChild).toHaveLength(1) + const childRef = insertedChild[0] + if (!childRef) throw new Error('Expected child ref') + + expect(proxy.toString({ node: childRef })).toEqual('') + expect(proxy.toString({ node: parentRef })).toEqual('') + expect(fragment.toJSON()).toEqual('') + }) + + test('Adding another fragment through quickjs through proxy should succeed', async () => { + fragment.insert(0, [root]) + expect(fragment.toJSON()).toEqual('') + + await withQuickJsMode(proxy.bindings, (qjs) => { + qjs.run<{ root: string }>(` + ({ + root: bindings.toString({node: bindings.get({node: bindings.root(), index: 0})}) + }) + `) + }) + + await withQuickJsMode(proxy.bindings, (qjs) => { + qjs.run(` + const fragmentRef = bindings.root() + const rootRef = bindings.get({ node: fragmentRef, index: 0 }) + bindings.append({ + parent: rootRef, + content: [{ kind: 'element', nodeName: 'parent' }], + }) + `) + }) + expect(fragment.toJSON()).toEqual('') + + await withQuickJsMode(proxy.bindings, (qjs) => { + qjs.run(` + const fragmentRef = bindings.root() + const rootRef = bindings.get({ node: fragmentRef, index: 0 }) + const parentRef = bindings.get({ node: rootRef, index: 0 }) + bindings.append({ + parent: parentRef, + content: [{ kind: 'element', nodeName: 'child' }], + }) + `) + }) + + expect(fragment.toJSON()).toEqual('') + }) + }) + + describe('Complex operations can be expressed through proxy', async () => { + let doc: Y.Doc + let fragment: Y.XmlFragment + let proxy: YXmlProxyBindings + let _root: Y.XmlElement + beforeEach(() => { + doc = new Y.Doc() + fragment = doc.getXmlFragment('artifact') + proxy = new YXmlProxyBindings(fragment) + _root = new Y.XmlElement('root') + }) + + test('can edit a deeply nested fragment at multiple tree levels', () => { + fragment.insert(0, [createDeepTreeRoot()]) + + expect(fragment.toJSON()).toEqual( + '
AlphaIntroFirst bulletSecond bullet
Beta intro
Nested quote
', + ) + + const fragmentRef = proxy.root() + const rootRef = proxy.get({ node: fragmentRef, index: 0 }) + const alphaSection = proxy.get({ node: rootRef, index: 0 }) + const betaSection = proxy.get({ node: rootRef, index: 1 }) + const alphaList = proxy.get({ node: alphaSection, index: 2 }) + const secondListItem = proxy.get({ node: alphaList, index: 1 }) + const secondListParagraph = proxy.get({ node: secondListItem, index: 0 }) + const betaQuote = proxy.get({ node: betaSection, index: 1 }) + + proxy.insertBefore({ + ref: alphaSection, + content: [ + { + kind: 'element', + nodeName: 'heading', + attributes: { level: '1' }, + children: [{ kind: 'text', text: 'Document Title' }], + }, + ], + }) + expect(fragment.toJSON()).toEqual( + 'Document Title
AlphaIntroFirst bulletSecond bullet
Beta intro
Nested quote
', + ) + + proxy.insertAfter({ + ref: secondListParagraph, + content: [ + { + kind: 'element', + nodeName: 'paragraph', + children: [{ kind: 'text', text: 'Inserted after second bullet paragraph' }], + }, + ], + }) + expect(proxy.toString({ node: secondListItem })).toEqual( + 'Second bulletInserted after second bullet paragraph', + ) + + proxy.append({ + parent: betaQuote, + content: [ + { + kind: 'element', + nodeName: 'paragraph', + children: [{ kind: 'text', text: 'Quote follow-up' }], + }, + ], + }) + expect(proxy.toString({ node: betaQuote })).toEqual( + '
Nested quoteQuote follow-up
', + ) + + proxy.prepend({ + parent: betaSection, + content: [ + { + kind: 'element', + nodeName: 'heading', + attributes: { level: '3' }, + children: [{ kind: 'text', text: 'Beta heading' }], + }, + ], + }) + expect(proxy.toString({ node: betaSection })).toEqual( + '
Beta headingBeta intro
Nested quoteQuote follow-up
', + ) + + expect(fragment.toJSON()).toEqual( + 'Document Title
AlphaIntroFirst bulletSecond bulletInserted after second bullet paragraph
Beta headingBeta intro
Nested quoteQuote follow-up
', + ) + }) + }) +}) + +function createDeepTreeRoot(): Y.XmlElement { + const root = new Y.XmlElement('root') + + const alphaSection = new Y.XmlElement('section') + alphaSection.setAttribute('id', 'alpha') + alphaSection.insert(0, [ + createElement('heading', { level: '2' }, [createText('Alpha')]), + createElement('paragraph', {}, [createText('Intro')]), + createElement('bulletList', {}, [ + createElement('listItem', {}, [createElement('paragraph', {}, [createText('First bullet')])]), + createElement('listItem', {}, [createElement('paragraph', {}, [createText('Second bullet')])]), + ]), + ]) + + const betaSection = new Y.XmlElement('section') + betaSection.setAttribute('id', 'beta') + betaSection.insert(0, [ + createElement('paragraph', {}, [createText('Beta intro')]), + createElement('blockquote', {}, [createElement('paragraph', {}, [createText('Nested quote')])]), + ]) + + root.insert(0, [alphaSection, betaSection]) + return root +} + +function createElement( + name: string, + attributes: Record, + children: Array, +): Y.XmlElement { + const element = new Y.XmlElement(name) + for (const [key, value] of Object.entries(attributes)) { + element.setAttribute(key, value) + } + if (children.length > 0) element.insert(0, children) + return element +} + +function createText(value: string): Y.XmlText { + const text = new Y.XmlText() + text.insert(0, value) + return text +} diff --git a/packages/yjs-rich-markdown/test/yxml-proxy.test.ts b/packages/yjs-rich-markdown/test/yxml-proxy.test.ts new file mode 100644 index 0000000..83aa3bb --- /dev/null +++ b/packages/yjs-rich-markdown/test/yxml-proxy.test.ts @@ -0,0 +1,712 @@ +import { describe, expect, test } from 'bun:test' +import * as Y from 'yjs' +import { + DetachedYXmlNodeRefError, + UnknownYXmlNodeRefError, + YXmlChildIndexOutOfBoundsError, + YXmlInvalidNodeKindForOperationError, + YXmlNodeRefKindMismatchError, + YXmlProxyBindings, + YXmlRootOperationError, + YXmlTextRangeOutOfBoundsError, +} from '../src' + +describe('YXml proxy', () => { + test('appends XML elements and nested children', () => { + const doc = new Y.Doc() + const fragment = doc.getXmlFragment('artifact') + const proxy = new YXmlProxyBindings(fragment) + + const inserted = proxy.append({ + content: [ + { + kind: 'element', + nodeName: 'section', + attributes: { id: 'intro' }, + children: [ + { + kind: 'element', + nodeName: 'heading', + attributes: { level: '2' }, + children: [{ kind: 'text', text: 'Intro' }], + }, + { + kind: 'element', + nodeName: 'paragraph', + children: [{ kind: 'text', text: 'Nested content works.' }], + }, + ], + }, + ], + }) + + expect(inserted).toEqual([{ id: expect.any(String), kind: 'element' }]) + expect(proxy.toString()).toBe( + '
IntroNested content works.
', + ) + }) + + test('exposes bound bindings for host environments like QuickJS', () => { + const doc = new Y.Doc() + const fragment = doc.getXmlFragment('artifact') + const proxy = new YXmlProxyBindings(fragment) + + expect(proxy.bindings).not.toBe(proxy) + expect(proxy.bindings.root?.()).toEqual(proxy.root()) + + proxy.bindings.append?.({ + content: [{ kind: 'element', nodeName: 'paragraph', children: [{ kind: 'text', text: 'Via bindings' }] }], + }) + + expect(proxy.bindings.toString?.()).toBe('Via bindings') + expect(proxy.toString()).toBe('Via bindings') + }) + + test('inserts before and after refs without passing parent', () => { + const doc = new Y.Doc() + const fragment = doc.getXmlFragment('artifact') + const proxy = new YXmlProxyBindings(fragment) + + const [middle] = proxy.append({ + content: [{ kind: 'element', nodeName: 'paragraph', children: [{ kind: 'text', text: 'Middle' }] }], + }) + if (!middle) throw new Error('Expected middle node') + + proxy.insertBefore({ + ref: middle, + content: [{ kind: 'element', nodeName: 'paragraph', children: [{ kind: 'text', text: 'Before' }] }], + }) + proxy.insertAfter({ + ref: middle, + content: [{ kind: 'element', nodeName: 'paragraph', children: [{ kind: 'text', text: 'After' }] }], + }) + + expect(proxy.toString()).toBe( + 'BeforeMiddleAfter', + ) + }) + + test('appends nested content inside an element parent', () => { + const doc = new Y.Doc() + const fragment = doc.getXmlFragment('artifact') + const proxy = new YXmlProxyBindings(fragment) + + const [list] = proxy.append({ content: [{ kind: 'element', nodeName: 'bulletList' }] }) + if (!list) throw new Error('Expected list node') + + proxy.append({ + parent: list, + content: [ + { + kind: 'element', + nodeName: 'listItem', + children: [{ kind: 'element', nodeName: 'paragraph', children: [{ kind: 'text', text: 'First' }] }], + }, + { + kind: 'element', + nodeName: 'listItem', + children: [ + { kind: 'element', nodeName: 'paragraph', children: [{ kind: 'text', text: 'Second' }] }, + ], + }, + ], + }) + + expect(proxy.toString()).toBe( + 'FirstSecond', + ) + expect(proxy.summary({ node: list }).nodeName).toBe('bulletList') + }) + + test('reads interleaved text and element children inside an element', () => { + const doc = new Y.Doc() + const fragment = doc.getXmlFragment('artifact') + const proxy = new YXmlProxyBindings(fragment) + + const [example] = proxy.append({ + content: [ + { + kind: 'element', + nodeName: 'example', + children: [ + { kind: 'text', text: 'this is an example ' }, + { + kind: 'element', + nodeName: 'nestedExample', + children: [{ kind: 'text', text: 'this is a nested example' }], + }, + { kind: 'text', text: ' This is another example' }, + ], + }, + ], + }) + if (!example) throw new Error('Expected example node') + + const children = proxy.children({ node: example }) + const [firstText, nestedExample, secondText] = children + if (!firstText || !nestedExample || !secondText) throw new Error('Expected interleaved children') + + expect(children.map((node) => proxy.summary({ node }).kind)).toEqual(['text', 'element', 'text']) + expect(proxy.text({ node: firstText })).toBe('this is an example ') + expect(proxy.summary({ node: nestedExample }).nodeName).toBe('nestedExample') + expect(proxy.text({ node: secondText })).toBe(' This is another example') + }) + + test('edits text inside an interleaved text node', () => { + const doc = new Y.Doc() + const fragment = doc.getXmlFragment('artifact') + const proxy = new YXmlProxyBindings(fragment) + + const [example] = proxy.append({ + content: [ + { + kind: 'element', + nodeName: 'example', + children: [ + { kind: 'text', text: 'this is an example ' }, + { + kind: 'element', + nodeName: 'nestedExample', + children: [{ kind: 'text', text: 'this is a nested example' }], + }, + { kind: 'text', text: ' This is another example' }, + ], + }, + ], + }) + if (!example) throw new Error('Expected example node') + const [firstText, nestedExample, secondText] = proxy.children({ node: example }) + if (!firstText || !nestedExample || !secondText) throw new Error('Expected interleaved children') + + proxy.insertText({ node: firstText, index: 0, text: 'Actually, ' }) + proxy.deleteText({ node: secondText, index: 0, length: 9 }) + + expect(proxy.text({ node: firstText })).toBe('Actually, this is an example ') + expect(proxy.text({ node: secondText })).toBe('another example') + expect(proxy.toString()).toBe( + 'Actually, this is an example this is a nested exampleanother example', + ) + }) + + test('splits a text node into interleaved siblings', () => { + const doc = new Y.Doc() + const fragment = doc.getXmlFragment('artifact') + const proxy = new YXmlProxyBindings(fragment) + + const [example] = proxy.append({ + content: [ + { kind: 'element', nodeName: 'example', children: [{ kind: 'text', text: 'this is an example' }] }, + ], + }) + if (!example) throw new Error('Expected example node') + const [textNode] = proxy.children({ node: example }) + if (!textNode) throw new Error('Expected text node') + + const split = proxy.splitText({ node: textNode, index: 8 }) + + expect(proxy.text({ node: split.left })).toBe('this is ') + expect(proxy.text({ node: split.right })).toBe('an example') + expect(proxy.children({ node: example })).toEqual([split.left, split.right]) + }) + + test('wraps only part of a text node while preserving interleaved siblings', () => { + const doc = new Y.Doc() + const fragment = doc.getXmlFragment('artifact') + const proxy = new YXmlProxyBindings(fragment) + + const [example] = proxy.append({ + content: [ + { + kind: 'element', + nodeName: 'example', + children: [ + { kind: 'text', text: 'this is an example ' }, + { + kind: 'element', + nodeName: 'nestedExample', + children: [{ kind: 'text', text: 'this is a nested example' }], + }, + { kind: 'text', text: ' This is another example' }, + ], + }, + ], + }) + if (!example) throw new Error('Expected example node') + const [firstText, nestedExample, secondText] = proxy.children({ node: example }) + if (!firstText || !nestedExample || !secondText) throw new Error('Expected interleaved children') + + const wrapped = proxy.wrapTextRange({ node: firstText, start: 11, end: 18, wrapper: { nodeName: 'bold' } }) + + const children = proxy.children({ node: example }) + const [beforeText, boldNode, afterText, sameNestedExample, sameSecondText] = children + if (!beforeText || !boldNode || !afterText || !sameNestedExample || !sameSecondText) + throw new Error('Expected wrapped children') + + expect(proxy.text({ node: beforeText })).toBe('this is an ') + expect(proxy.toString({ node: boldNode })).toBe('example') + expect(proxy.text({ node: afterText })).toBe(' ') + expect(sameNestedExample).toEqual(nestedExample) + expect(sameSecondText).toEqual(secondText) + expect(wrapped.wrapper).toEqual(boldNode) + expect(proxy.toString()).toBe( + 'this is an example this is a nested example This is another example', + ) + }) + + test('wraps part of text inside an already marked inline element', () => { + const doc = new Y.Doc() + const fragment = doc.getXmlFragment('artifact') + const proxy = new YXmlProxyBindings(fragment) + + const [paragraph] = proxy.append({ + content: [ + { + kind: 'element', + nodeName: 'paragraph', + children: [ + { kind: 'text', text: 'prefix ' }, + { + kind: 'element', + nodeName: 'italic', + children: [{ kind: 'text', text: 'nested example text' }], + }, + { kind: 'text', text: ' suffix' }, + ], + }, + ], + }) + if (!paragraph) throw new Error('Expected paragraph node') + + const [, italic, suffix] = proxy.children({ node: paragraph }) + if (!italic || !suffix) throw new Error('Expected italic and suffix nodes') + const [italicText] = proxy.children({ node: italic }) + if (!italicText) throw new Error('Expected italic text node') + + const wrapped = proxy.wrapTextRange({ + node: italicText, + start: 7, + end: 14, + wrapper: { nodeName: 'bold' }, + }) + + const italicChildren = proxy.children({ node: italic }) + const [beforeText, boldNode, afterText] = italicChildren + if (!beforeText || !boldNode || !afterText) throw new Error('Expected split italic children') + + expect(proxy.text({ node: beforeText })).toBe('nested ') + expect(proxy.toString({ node: boldNode })).toBe('example') + expect(proxy.text({ node: afterText })).toBe(' text') + expect(wrapped.wrapper).toEqual(boldNode) + expect(proxy.toString({ node: italic })).toBe('nested example text') + expect(proxy.text({ node: suffix })).toBe(' suffix') + expect(proxy.toString()).toBe( + 'prefix nested example text suffix', + ) + }) + + test('can combine splitText and wrapTextRange inside an existing inline mark', () => { + const doc = new Y.Doc() + const fragment = doc.getXmlFragment('artifact') + const proxy = new YXmlProxyBindings(fragment) + + const [paragraph] = proxy.append({ + content: [ + { + kind: 'element', + nodeName: 'paragraph', + children: [ + { kind: 'element', nodeName: 'italic', children: [{ kind: 'text', text: 'alpha beta gamma' }] }, + ], + }, + ], + }) + if (!paragraph) throw new Error('Expected paragraph node') + + const [italic] = proxy.children({ node: paragraph }) + if (!italic) throw new Error('Expected italic node') + const [italicText] = proxy.children({ node: italic }) + if (!italicText) throw new Error('Expected italic text node') + + const firstSplit = proxy.splitText({ node: italicText, index: 6 }) + expect(proxy.text({ node: firstSplit.left })).toBe('alpha ') + expect(proxy.text({ node: firstSplit.right })).toBe('beta gamma') + + const secondSplit = proxy.splitText({ node: firstSplit.right, index: 4 }) + expect(proxy.text({ node: secondSplit.left })).toBe('beta') + expect(proxy.text({ node: secondSplit.right })).toBe(' gamma') + + proxy.wrap({ node: secondSplit.left, wrapper: { nodeName: 'bold' } }) + + expect(proxy.toString({ node: italic })).toBe('alpha beta gamma') + expect(proxy.toString()).toBe('alpha beta gamma') + }) + + test('can edit adjacent to a new bold node inside an existing inline mark', () => { + const doc = new Y.Doc() + const fragment = doc.getXmlFragment('artifact') + const proxy = new YXmlProxyBindings(fragment) + + const [paragraph] = proxy.append({ + content: [ + { + kind: 'element', + nodeName: 'paragraph', + children: [ + { kind: 'element', nodeName: 'italic', children: [{ kind: 'text', text: 'alpha beta gamma' }] }, + ], + }, + ], + }) + if (!paragraph) throw new Error('Expected paragraph node') + + const [italic] = proxy.children({ node: paragraph }) + if (!italic) throw new Error('Expected italic node') + const [italicText] = proxy.children({ node: italic }) + if (!italicText) throw new Error('Expected italic text node') + + const wrapped = proxy.wrapTextRange({ + node: italicText, + start: 6, + end: 10, + wrapper: { nodeName: 'bold' }, + }) + + proxy.insertBefore({ + ref: wrapped.wrapper, + content: [{ kind: 'text', text: '[' }], + }) + proxy.insertAfter({ + ref: wrapped.wrapper, + content: [{ kind: 'text', text: ']' }], + }) + + expect(proxy.toString({ node: italic })).toBe('alpha [beta] gamma') + expect(proxy.toString()).toBe('alpha [beta] gamma') + }) + + test('can nest bold inside italic and then add another mark beside it', () => { + const doc = new Y.Doc() + const fragment = doc.getXmlFragment('artifact') + const proxy = new YXmlProxyBindings(fragment) + + const [paragraph] = proxy.append({ + content: [ + { + kind: 'element', + nodeName: 'paragraph', + children: [ + { kind: 'element', nodeName: 'italic', children: [{ kind: 'text', text: 'alpha beta gamma' }] }, + ], + }, + ], + }) + if (!paragraph) throw new Error('Expected paragraph node') + + const [italic] = proxy.children({ node: paragraph }) + if (!italic) throw new Error('Expected italic node') + const [italicText] = proxy.children({ node: italic }) + if (!italicText) throw new Error('Expected italic text node') + + proxy.wrapTextRange({ + node: italicText, + start: 6, + end: 10, + wrapper: { nodeName: 'bold' }, + }) + + const italicChildren = proxy.children({ node: italic }) + const [, boldNode, afterText] = italicChildren + if (!boldNode || !afterText) throw new Error('Expected bold node and trailing text') + + const italicInserted = proxy.insertAfter({ + ref: boldNode, + content: [{ kind: 'element', nodeName: 'italic', children: [{ kind: 'text', text: '!' }] }], + }) + const extraItalic = italicInserted[0] + if (!extraItalic) throw new Error('Expected extra italic node') + + proxy.insertText({ node: afterText, index: 0, text: ' ' }) + + expect(proxy.toString({ node: extraItalic })).toBe('!') + expect(proxy.toString({ node: italic })).toBe( + 'alpha beta! gamma', + ) + expect(proxy.toString()).toBe( + 'alpha beta! gamma', + ) + }) + + test('sets and removes attributes on element refs', () => { + const doc = new Y.Doc() + const fragment = doc.getXmlFragment('artifact') + const proxy = new YXmlProxyBindings(fragment) + + const [heading] = proxy.append({ + content: [ + { + kind: 'element', + nodeName: 'heading', + attributes: { level: '2' }, + children: [{ kind: 'text', text: 'Title' }], + }, + ], + }) + if (!heading) throw new Error('Expected heading node') + + proxy.setAttribute({ node: heading, name: 'level', value: '3' }) + expect(proxy.summary({ node: heading }).attributes).toEqual({ level: '3' }) + expect(proxy.toString()).toBe('Title') + + proxy.removeAttribute({ node: heading, name: 'level' }) + expect(proxy.summary({ node: heading }).attributes).toEqual({}) + expect(proxy.toString()).toBe('Title') + }) + + test('adds a bullet to an existing list', () => { + const doc = new Y.Doc() + const fragment = doc.getXmlFragment('artifact') + const proxy = new YXmlProxyBindings(fragment) + + const [list] = proxy.append({ + content: [ + { + kind: 'element', + nodeName: 'bulletList', + children: [ + { + kind: 'element', + nodeName: 'listItem', + children: [ + { + kind: 'element', + nodeName: 'paragraph', + children: [{ kind: 'text', text: 'Existing bullet' }], + }, + ], + }, + ], + }, + ], + }) + if (!list) throw new Error('Expected list node') + + proxy.append({ + parent: list, + content: [ + { + kind: 'element', + nodeName: 'listItem', + children: [ + { kind: 'element', nodeName: 'paragraph', children: [{ kind: 'text', text: 'Added bullet' }] }, + ], + }, + ], + }) + + expect(proxy.toString()).toBe( + 'Existing bulletAdded bullet', + ) + expect(proxy.summary({ node: list }).nodeName).toBe('bulletList') + }) + + test('changes an existing element attribute', () => { + const doc = new Y.Doc() + const fragment = doc.getXmlFragment('artifact') + const proxy = new YXmlProxyBindings(fragment) + + const [heading] = proxy.append({ + content: [ + { + kind: 'element', + nodeName: 'heading', + attributes: { level: '2' }, + children: [{ kind: 'text', text: 'Details' }], + }, + ], + }) + if (!heading) throw new Error('Expected heading node') + + proxy.setAttribute({ node: heading, name: 'level', value: '3' }) + + expect(proxy.summary({ node: heading }).attributes).toEqual({ level: '3' }) + expect(proxy.toString()).toBe('Details') + }) + + test('erases a section by removing its element ref', () => { + const doc = new Y.Doc() + const fragment = doc.getXmlFragment('artifact') + const proxy = new YXmlProxyBindings(fragment) + + const [intro, obsolete, ending] = proxy.append({ + content: [ + { + kind: 'element', + nodeName: 'section', + attributes: { id: 'intro' }, + children: [ + { kind: 'element', nodeName: 'paragraph', children: [{ kind: 'text', text: 'Keep intro.' }] }, + ], + }, + { + kind: 'element', + nodeName: 'section', + attributes: { id: 'obsolete' }, + children: [ + { + kind: 'element', + nodeName: 'paragraph', + children: [{ kind: 'text', text: 'Delete this section.' }], + }, + ], + }, + { + kind: 'element', + nodeName: 'section', + attributes: { id: 'ending' }, + children: [ + { kind: 'element', nodeName: 'paragraph', children: [{ kind: 'text', text: 'Keep ending.' }] }, + ], + }, + ], + }) + if (!intro || !obsolete || !ending) throw new Error('Expected section nodes') + + proxy.remove({ node: obsolete }) + + expect(proxy.children()).toEqual([intro, ending]) + expect(proxy.toString()).toBe( + '
Keep intro.
Keep ending.
', + ) + }) + + test('removes nodes by ref', () => { + const doc = new Y.Doc() + const fragment = doc.getXmlFragment('artifact') + const proxy = new YXmlProxyBindings(fragment) + + const [first, second] = proxy.append({ + content: [ + { kind: 'element', nodeName: 'paragraph', children: [{ kind: 'text', text: 'Keep' }] }, + { kind: 'element', nodeName: 'paragraph', children: [{ kind: 'text', text: 'Remove' }] }, + ], + }) + if (!first || !second) throw new Error('Expected paragraph nodes') + + proxy.remove({ node: second }) + + expect(proxy.toString()).toBe('Keep') + expect(proxy.children()).toEqual([first]) + }) + + test('rejects child insertion into text refs', () => { + const doc = new Y.Doc() + const fragment = doc.getXmlFragment('artifact') + const proxy = new YXmlProxyBindings(fragment) + + const [paragraph] = proxy.append({ + content: [{ kind: 'element', nodeName: 'paragraph', children: [{ kind: 'text', text: 'Text' }] }], + }) + if (!paragraph) throw new Error('Expected paragraph node') + const [text] = proxy.children({ node: paragraph }) + if (!text) throw new Error('Expected text node') + + expect(() => + proxy.append({ + parent: text, + content: [{ kind: 'element', nodeName: 'paragraph', children: [{ kind: 'text', text: 'Nope' }] }], + }), + ).toThrow(YXmlInvalidNodeKindForOperationError) + }) + + test('throws for unknown node refs', () => { + const doc = new Y.Doc() + const fragment = doc.getXmlFragment('artifact') + const proxy = new YXmlProxyBindings(fragment) + + expect(() => proxy.summary({ node: { id: 'missing', kind: 'element' } })).toThrow(UnknownYXmlNodeRefError) + }) + + test('throws for node ref kind mismatches', () => { + const doc = new Y.Doc() + const fragment = doc.getXmlFragment('artifact') + const proxy = new YXmlProxyBindings(fragment) + const [paragraph] = proxy.append({ + content: [{ kind: 'element', nodeName: 'paragraph', children: [{ kind: 'text', text: 'Text' }] }], + }) + if (!paragraph) throw new Error('Expected paragraph node') + + expect(() => proxy.summary({ node: { ...paragraph, kind: 'text' } })).toThrow(YXmlNodeRefKindMismatchError) + }) + + test('throws for child index out of bounds', () => { + const doc = new Y.Doc() + const fragment = doc.getXmlFragment('artifact') + const proxy = new YXmlProxyBindings(fragment) + + expect(() => proxy.get({ index: 0 })).toThrow(YXmlChildIndexOutOfBoundsError) + }) + + test('throws when setting attributes on non-element refs', () => { + const doc = new Y.Doc() + const fragment = doc.getXmlFragment('artifact') + const proxy = new YXmlProxyBindings(fragment) + const root = proxy.root() + + expect(() => proxy.setAttribute({ node: root, name: 'id', value: 'root' })).toThrow( + YXmlInvalidNodeKindForOperationError, + ) + }) + + test('throws for invalid text ranges', () => { + const doc = new Y.Doc() + const fragment = doc.getXmlFragment('artifact') + const proxy = new YXmlProxyBindings(fragment) + const [paragraph] = proxy.append({ + content: [{ kind: 'element', nodeName: 'paragraph', children: [{ kind: 'text', text: 'Text' }] }], + }) + if (!paragraph) throw new Error('Expected paragraph node') + const [text] = proxy.children({ node: paragraph }) + if (!text) throw new Error('Expected text node') + + expect(() => proxy.insertText({ node: text, index: 10, text: '!' })).toThrow(YXmlTextRangeOutOfBoundsError) + expect(() => proxy.deleteText({ node: text, index: 1, length: 10 })).toThrow(YXmlTextRangeOutOfBoundsError) + expect(() => proxy.splitText({ node: text, index: 0 })).toThrow(YXmlTextRangeOutOfBoundsError) + expect(() => proxy.wrapTextRange({ node: text, start: 2, end: 2, wrapper: { nodeName: 'bold' } })).toThrow( + YXmlTextRangeOutOfBoundsError, + ) + }) + + test('throws when removing or inserting relative to the root fragment', () => { + const doc = new Y.Doc() + const fragment = doc.getXmlFragment('artifact') + const proxy = new YXmlProxyBindings(fragment) + const root = proxy.root() + + expect(() => proxy.remove({ node: root })).toThrow(YXmlRootOperationError) + expect(() => + proxy.insertAfter({ + ref: root, + content: [{ kind: 'element', nodeName: 'paragraph', children: [{ kind: 'text', text: 'Nope' }] }], + }), + ).toThrow(YXmlRootOperationError) + }) + + test('throws for operations on detached node refs', () => { + const doc = new Y.Doc() + const fragment = doc.getXmlFragment('artifact') + const proxy = new YXmlProxyBindings(fragment) + const [paragraph] = proxy.append({ + content: [{ kind: 'element', nodeName: 'paragraph', children: [{ kind: 'text', text: 'Delete me' }] }], + }) + if (!paragraph) throw new Error('Expected paragraph node') + + proxy.remove({ node: paragraph }) + + expect(() => + proxy.insertAfter({ + ref: paragraph, + content: [{ kind: 'element', nodeName: 'paragraph', children: [{ kind: 'text', text: 'Nope' }] }], + }), + ).toThrow(DetachedYXmlNodeRefError) + }) +}) diff --git a/packages/yjs-rich-markdown/tsconfig.build.json b/packages/yjs-rich-markdown/tsconfig.build.json new file mode 100644 index 0000000..e043bc7 --- /dev/null +++ b/packages/yjs-rich-markdown/tsconfig.build.json @@ -0,0 +1,23 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src"], + "exclude": [ + "test", + "dist", + "node_modules", + "**/*.test.*", + "**/*.bench.*", + "**/*.test-d.ts", + "**/__tests__/**", + "**/__mocks__/**", + "**/__snapshots__/**" + ] +} diff --git a/agents/yjs-fs-agents/tsconfig.json b/packages/yjs-rich-markdown/tsconfig.json similarity index 82% rename from agents/yjs-fs-agents/tsconfig.json rename to packages/yjs-rich-markdown/tsconfig.json index b9457eb..7cb9983 100644 --- a/agents/yjs-fs-agents/tsconfig.json +++ b/packages/yjs-rich-markdown/tsconfig.json @@ -1,6 +1,5 @@ { "compilerOptions": { - // Environment setup & latest features "lib": ["ESNext"], "target": "ESNext", "module": "Preserve", @@ -8,19 +7,16 @@ "jsx": "react-jsx", "allowJs": true, "types": ["bun"], - // Bundler mode "moduleResolution": "bundler", "customConditions": ["source"], "allowImportingTsExtensions": true, "verbatimModuleSyntax": true, "noEmit": true, - // Best practices "strict": true, "skipLibCheck": true, "noFallthroughCasesInSwitch": true, "noUncheckedIndexedAccess": true, "noImplicitOverride": true, - // Some stricter flags (disabled by default) "noUnusedLocals": false, "noUnusedParameters": false, "noPropertyAccessFromIndexSignature": false diff --git a/scripts/release/manifest.ts b/scripts/release/manifest.ts index 2c2148d..515b1e6 100644 --- a/scripts/release/manifest.ts +++ b/scripts/release/manifest.ts @@ -1,7 +1,7 @@ import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; -import type { PackageManifest } from '../build/package-build' +import type { PackageManifest } from '../build/package-build'; export type WorkspaceReleaseEntry = { name: string; @@ -33,6 +33,10 @@ export const publishablePackages = [ name: "@humanlayer/agentlayer-provider-openai-codex", dir: "packages/agentlayer-provider-openai-codex", }, + { + name: "@humanlayer/quickjs-exec", + dir: "packages/quickjs-exec", + }, { name: "@humanlayer/yjs-fs", dir: "packages/yjs-fs",