diff --git a/bun.lock b/bun.lock index ec75cee..023a317 100644 --- a/bun.lock +++ b/bun.lock @@ -6,10 +6,11 @@ "name": "commandly", "dependencies": { "@ai-sdk/anthropic": "^3.0.69", - "@ai-sdk/google": "^3.0.62", + "@ai-sdk/google": "^3.0.63", "@ai-sdk/groq": "^3.0.35", "@ai-sdk/mistral": "^3.0.30", - "@ai-sdk/openai": "^3.0.52", + "@ai-sdk/openai": "^3.0.53", + "@ai-sdk/react": "3.0.163", "@ai-sdk/xai": "^3.0.82", "@base-ui/react": "^1.4.0", "@dnd-kit/core": "^6.3.1", @@ -27,12 +28,13 @@ "@tanstack/react-query": "^5.99.0", "@tanstack/react-router": "1.168.19", "@tanstack/react-router-with-query": "1.130.17", - "@tanstack/react-start": "^1.167.34", - "ai": "^6.0.158", + "@tanstack/react-start": "^1.167.41", + "ai": "^6.0.162", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.1.0", + "jsonpath-plus": "^10.4.0", "lucide-react": "^1.8.0", "motion": "^12.38.0", "next-themes": "^0.4.6", @@ -46,6 +48,7 @@ "tailwind-merge": "^3.5.0", "tailwindcss": "^4.2.2", "tailwindcss-animate": "^1.0.7", + "tokenlens": "^1.3.1", "tw-animate-css": "^1.4.0", "use-stick-to-bottom": "^1.1.3", "zod": "^4.3.6", @@ -63,10 +66,11 @@ "@types/json-schema": "^7.0.15", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", - "@typescript/native-preview": "^7.0.0-dev.20260408.1", + "@typescript/native-preview": "^7.0.0-dev.20260415.1", "@vitejs/plugin-react": "^6.0.1", "@vitest/coverage-v8": "4.1.4", "ajv": "^8.18.0", + "fake-indexeddb": "^6.2.5", "jsdom": "^29.0.2", "oxfmt": "^0.45.0", "oxlint": "^1.60.0", @@ -88,15 +92,15 @@ "@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.69", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-LshR7X3pFugY0o41G2VKTmg1XoGpSl7uoYWfzk6zjVZLhCfeFiwgpOga+eTV4XY1VVpZwKVqRnkDbIL7K2eH5g=="], - "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.95", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZmUNNbZl3V42xwQzPaNUi+s8eqR2lnrxf0bvB6YbLXpLjHYv0k2Y78t12cNOfY0bxGeuVVTLyk856uLuQIuXEQ=="], + "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.99", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8/UuzFY8p+T8j4XP/9m841pUb5bhnFt8cecSnJpd2zhBttNZ6GbfjZTmsqnvM/RwJOvzIsdFULZrU+E9QFREsQ=="], - "@ai-sdk/google": ["@ai-sdk/google@3.0.62", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cC9HAjR5WZxjqGyEJrJqFTlVqyPE9UOFmmGdf5dINaimgfPmzqXYN1qTYEJ+1knbyTVsNMub0KAF5SOqqtO8IQ=="], + "@ai-sdk/google": ["@ai-sdk/google@3.0.63", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-RfOZWVMYSPu2sPRfGajrauWAZ9BSaRopSn+AszkKWQ1MFj8nhaXvCqRHB5pBQUaHTfZKagvOmMpNfa/s3gPLgQ=="], "@ai-sdk/groq": ["@ai-sdk/groq@3.0.35", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-LXoPwSKaqXst9LyLN2J7gK8n7RldQLbP2zsnBYxXcOsXKrtceksqtbsmGXujvab2TM9FisquAw/ZG2hTbD5vnQ=="], "@ai-sdk/mistral": ["@ai-sdk/mistral@3.0.30", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-+j4IXRSk9E661cFSafmIr+XHOzwjFagawwzMOlSqwL6U4Sq4PCFLDF+oHbX5NUqNjUL7FD1zi/9lBIfa41pUvw=="], - "@ai-sdk/openai": ["@ai-sdk/openai@3.0.52", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-4Rr8NCGmfWTz6DCUvixn9UmyZcMatiHn0zWoMzI3JCUe9R1P/vsPOpCBALKoSzVYOjyJnhtnVIbfUKujcS39uw=="], + "@ai-sdk/openai": ["@ai-sdk/openai@3.0.53", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Wld+Rbc05KaUn08uBt06eEuwcgalcIFtIl32Yp+GxuZXUQwOb6YeAuq+C6da4ch6BurFoqEaLemJVwjBb7x+PQ=="], "@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="], @@ -104,15 +108,17 @@ "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.23", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-z8GlDaCmRSDlqkMF2f4/RFgWxdarvIbyuk+m6WXT1LYgsnGiXRJGTD2Z1+SDl3LqtFuRtGX1aghYvQLoHL/9pg=="], + "@ai-sdk/react": ["@ai-sdk/react@3.0.163", "", { "dependencies": { "@ai-sdk/provider-utils": "4.0.23", "ai": "6.0.161", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ~19.0.1 || ~19.1.2 || ^19.2.1" } }, "sha512-UM8BwNx4YFcG1XIBSTepIGx48RXk974qVSplVZc2JPiY86tC4Qpb8trquh5MdtSKzlS6yrUX46n8gS2WZaUIXQ=="], + "@ai-sdk/xai": ["@ai-sdk/xai@3.0.82", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-A0VFMufnVf4wODcT3SPQUUzvYXiIO1VhFuXj9r6z/vP4rlo+QRDPw3WSTchcz93ROQWSfBE3I6Szqz342OHi5w=="], "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], "@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="], - "@asamuzakjp/css-color": ["@asamuzakjp/css-color@5.1.8", "", { "dependencies": { "@csstools/css-calc": "^3.1.1", "@csstools/css-color-parser": "^4.0.2", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-OISPR9c2uPo23rUdvfEQiLPjoMLOpEeLNnP5iGkxr6tDDxJd3NjD+6fxY0mdaMbIPUjFGL4HFOJqLvow5q4aqQ=="], + "@asamuzakjp/css-color": ["@asamuzakjp/css-color@5.1.10", "", { "dependencies": { "@csstools/css-calc": "^3.1.1", "@csstools/css-color-parser": "^4.0.2", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-02OhhkKtgNRuicQ/nF3TRnGsxL9wp0r3Y7VlKWyOHHGmGyvXv03y+PnymU8FKFJMTjIr1Bk8U2g1HWSLrpAHww=="], - "@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@7.0.8", "", { "dependencies": { "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.2.1", "is-potential-custom-element-name": "^1.0.1" } }, "sha512-erMO6FgtM02dC24NGm0xufMzWz5OF0wXKR7BpvGD973bq/GbmR8/DbxNZbj0YevQ5hlToJaWSVK/G9/NDgGEVw=="], + "@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@7.0.9", "", { "dependencies": { "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.2.1", "is-potential-custom-element-name": "^1.0.1" } }, "sha512-r3ElRr7y8ucyN2KdICwGsmj19RoN13CLCa/pvGydghWK6ZzeKQ+TcDjVdtEZz2ElpndM5jXw//B9CEee0mWnVg=="], "@asamuzakjp/nwsapi": ["@asamuzakjp/nwsapi@2.3.9", "", {}, "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q=="], @@ -198,13 +204,13 @@ "@csstools/color-helpers": ["@csstools/color-helpers@6.0.2", "", {}, "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q=="], - "@csstools/css-calc": ["@csstools/css-calc@3.1.1", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ=="], + "@csstools/css-calc": ["@csstools/css-calc@3.2.0", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w=="], - "@csstools/css-color-parser": ["@csstools/css-color-parser@4.0.2", "", { "dependencies": { "@csstools/color-helpers": "^6.0.2", "@csstools/css-calc": "^3.1.1" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw=="], + "@csstools/css-color-parser": ["@csstools/css-color-parser@4.1.0", "", { "dependencies": { "@csstools/color-helpers": "^6.0.2", "@csstools/css-calc": "^3.2.0" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ=="], "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@4.0.0", "", { "peerDependencies": { "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w=="], - "@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.1.2", "", { "peerDependencies": { "css-tree": "^3.2.1" }, "optionalPeers": ["css-tree"] }, "sha512-5GkLzz4prTIpoyeUiIu3iV6CSG3Plo7xRVOFPKI7FVEJ3mZ0A8SwK0XU3Gl7xAkiQ+mDyam+NNp875/C5y+jSA=="], + "@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.1.3", "", { "peerDependencies": { "css-tree": "^3.2.1" }, "optionalPeers": ["css-tree"] }, "sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg=="], "@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.0", "", {}, "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA=="], @@ -330,6 +336,10 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@jsep-plugin/assignment": ["@jsep-plugin/assignment@1.3.0", "", { "peerDependencies": { "jsep": "^0.4.0||^1.0.0" } }, "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ=="], + + "@jsep-plugin/regex": ["@jsep-plugin/regex@1.0.4", "", { "peerDependencies": { "jsep": "^0.4.0||^1.0.0" } }, "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg=="], + "@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="], "@mdx-js/rollup": ["@mdx-js/rollup@3.1.1", "", { "dependencies": { "@mdx-js/mdx": "^3.0.0", "@rollup/pluginutils": "^5.0.0", "source-map": "^0.7.0", "vfile": "^6.0.0" }, "peerDependencies": { "rollup": ">=2" } }, "sha512-v8satFmBB+DqDzYohnm1u2JOvxx6Hl3pUvqzJvfs2Zk/ngZ1aRUhsWpXvwPkNeGN9c2NCm/38H29ZqXQUjf8dw=="], @@ -744,17 +754,17 @@ "@tanstack/react-router-with-query": ["@tanstack/react-router-with-query@1.130.17", "", { "peerDependencies": { "@tanstack/react-query": ">=5.49.2", "@tanstack/react-router": ">=1.43.2", "@tanstack/router-core": ">=1.114.7", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-TNaSocW20KuPwUojEm130DLWTr9M5hsSzxiu4QqS2jNCnrGLuDrwMHyP+6fq13lG3YuU4u9O1qajxfJIGomZCg=="], - "@tanstack/react-start": ["@tanstack/react-start@1.167.34", "", { "dependencies": { "@tanstack/react-router": "1.168.19", "@tanstack/react-start-client": "1.166.36", "@tanstack/react-start-rsc": "0.0.14", "@tanstack/react-start-server": "1.166.37", "@tanstack/router-utils": "^1.161.6", "@tanstack/start-client-core": "1.167.16", "@tanstack/start-plugin-core": "1.167.31", "@tanstack/start-server-core": "1.167.18", "pathe": "^2.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0", "vite": ">=7.0.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-gBT7PLCtN1LE4fo6S85P3WX7gQRDH6qCuBGVWOZeHwH/tzkp5HQWUWliHZFp3UyhY+pBdsLuiCBqdJ6pQk3bxQ=="], + "@tanstack/react-start": ["@tanstack/react-start@1.167.41", "", { "dependencies": { "@tanstack/react-router": "1.168.22", "@tanstack/react-start-client": "1.166.39", "@tanstack/react-start-rsc": "0.0.20", "@tanstack/react-start-server": "1.166.40", "@tanstack/router-utils": "^1.161.6", "@tanstack/start-client-core": "1.167.17", "@tanstack/start-plugin-core": "1.167.35", "@tanstack/start-server-core": "1.167.19", "pathe": "^2.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0", "vite": ">=7.0.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-w51yu/VQHNecIgN3ku+EmCxPjVbBftbnucDZBnxns43zPAhL5T5bnaBi0Fx4yJ3sDAmiceWEJD2F6IUaXTlNFA=="], - "@tanstack/react-start-client": ["@tanstack/react-start-client@1.166.36", "", { "dependencies": { "@tanstack/react-router": "1.168.19", "@tanstack/router-core": "1.168.14", "@tanstack/start-client-core": "1.167.16" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-gfvevr2UB5B7Arv9pJl8VB0loeK7QgVOX1xiz1Wf1I52kbv1efaEJie0zzOWO9msR6brmFplMojAe5PNz9Gbug=="], + "@tanstack/react-start-client": ["@tanstack/react-start-client@1.166.39", "", { "dependencies": { "@tanstack/react-router": "1.168.22", "@tanstack/router-core": "1.168.15", "@tanstack/start-client-core": "1.167.17" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-NDao1nwwM4hXiVwQjVO8ZPlL9gZ0yeIl7w0PV+75jz+V9bhXr2nGhBuBw4zBuG7px4fEVmO5E8b7kpjWgCDJtA=="], - "@tanstack/react-start-rsc": ["@tanstack/react-start-rsc@0.0.14", "", { "dependencies": { "@tanstack/react-router": "1.168.19", "@tanstack/react-start-server": "1.166.37", "@tanstack/router-core": "1.168.14", "@tanstack/router-utils": "1.161.6", "@tanstack/start-client-core": "1.167.16", "@tanstack/start-fn-stubs": "1.161.6", "@tanstack/start-plugin-core": "1.167.31", "@tanstack/start-server-core": "1.167.18", "@tanstack/start-storage-context": "1.166.28", "pathe": "^2.0.3" }, "peerDependencies": { "@vitejs/plugin-rsc": ">=0.5.20", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" }, "optionalPeers": ["@vitejs/plugin-rsc"] }, "sha512-CgpRZ8u7egckKtaTkHuwiWJHeZjD6YbYXF6GmD+mukuA7NBBdR0NgX+M4IlR9w4xS95vVtVLruPk3B9GXY6+Nw=="], + "@tanstack/react-start-rsc": ["@tanstack/react-start-rsc@0.0.20", "", { "dependencies": { "@tanstack/react-router": "1.168.22", "@tanstack/react-start-server": "1.166.40", "@tanstack/router-core": "1.168.15", "@tanstack/router-utils": "1.161.6", "@tanstack/start-client-core": "1.167.17", "@tanstack/start-fn-stubs": "1.161.6", "@tanstack/start-plugin-core": "1.167.35", "@tanstack/start-server-core": "1.167.19", "@tanstack/start-storage-context": "1.166.29", "pathe": "^2.0.3" }, "peerDependencies": { "@vitejs/plugin-rsc": ">=0.5.20", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" }, "optionalPeers": ["@vitejs/plugin-rsc"] }, "sha512-KPsaq/asQxu/scMduMuQlwURIXwu+qZ7SO/8xbY/PQ/DJzfp9q1ZEHs9JN7dBEOHcme/YPeHta+UxBapB5Syyw=="], - "@tanstack/react-start-server": ["@tanstack/react-start-server@1.166.37", "", { "dependencies": { "@tanstack/history": "1.161.6", "@tanstack/react-router": "1.168.19", "@tanstack/router-core": "1.168.14", "@tanstack/start-client-core": "1.167.16", "@tanstack/start-server-core": "1.167.18" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-Ow9s9AvhnNpYaXvuOy6HLBRbev7lLDRK3GLeqF9IFDfynRYAdR8OZWluTvRTPQPBp4eQqHNylqJ9rQ6BQnZMJA=="], + "@tanstack/react-start-server": ["@tanstack/react-start-server@1.166.40", "", { "dependencies": { "@tanstack/history": "1.161.6", "@tanstack/react-router": "1.168.22", "@tanstack/router-core": "1.168.15", "@tanstack/start-client-core": "1.167.17", "@tanstack/start-server-core": "1.167.19" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-g5OU/eHmT1MleDbebdhNybpYgDu3QeNlemMCyXxEJNewOFM4iBlfoUBdMPhONJDlHQrOBXwMk/RUEFemY94yRw=="], "@tanstack/react-store": ["@tanstack/react-store@0.9.3", "", { "dependencies": { "@tanstack/store": "0.9.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg=="], - "@tanstack/router-core": ["@tanstack/router-core@1.168.9", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^2.0.0", "seroval": "^1.4.2", "seroval-plugins": "^1.4.2" }, "bin": { "intent": "bin/intent.js" } }, "sha512-18oeEwEDyXOIuO1VBP9ACaK7tYHZUjynGDCoUh/5c/BNhia9vCJCp9O0LfhZXOorDc/PmLSgvmweFhVmIxF10g=="], + "@tanstack/router-core": ["@tanstack/router-core@1.168.14", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.0", "seroval-plugins": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-UhCJtjNrd5wcTmhgB2HyUP0+Rj1M7BD4dS11YsF9x6VC2KH/eqxzs/vK+nN5f+cOhPOLZdmLkWMW+WGmacZ8HA=="], "@tanstack/router-devtools-core": ["@tanstack/router-devtools-core@1.167.3", "", { "dependencies": { "clsx": "^2.1.1", "goober": "^2.1.16" }, "peerDependencies": { "@tanstack/router-core": "^1.168.11", "csstype": "^3.0.10" }, "optionalPeers": ["csstype"] }, "sha512-fJ1VMhyQgnoashTrP763c2HRc9kofgF61L7Jb3F6eTHAmCKtGVx8BRtiFt37sr3U0P0jmaaiiSPGP6nT5JtVNg=="], @@ -764,15 +774,15 @@ "@tanstack/router-utils": ["@tanstack/router-utils@1.161.6", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "ansis": "^4.1.0", "babel-dead-code-elimination": "^1.0.12", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-nRcYw+w2OEgK6VfjirYvGyPLOK+tZQz1jkYcmH5AjMamQ9PycnlxZF2aEZtPpNoUsaceX2bHptn6Ub5hGXqNvw=="], - "@tanstack/start-client-core": ["@tanstack/start-client-core@1.167.16", "", { "dependencies": { "@tanstack/router-core": "1.168.14", "@tanstack/start-fn-stubs": "1.161.6", "@tanstack/start-storage-context": "1.166.28", "seroval": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-ftQ+f5SB2gLqU24QeofmbkfXiSSPS/VlLRdNC6DmshUIrDp8SQFb8gP/7hbkORs7jx4YULGA4z2xQrz/RmC+Kg=="], + "@tanstack/start-client-core": ["@tanstack/start-client-core@1.167.17", "", { "dependencies": { "@tanstack/router-core": "1.168.15", "@tanstack/start-fn-stubs": "1.161.6", "@tanstack/start-storage-context": "1.166.29", "seroval": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-3ZnpQ0LPnhrm/GX+HT7XfRxTcqnmBE1KJd7LtaJNuN13NH0C4ZOWchKLPEed2/gluhgsT6UgWm+Ec0kEFtxSaw=="], "@tanstack/start-fn-stubs": ["@tanstack/start-fn-stubs@1.161.6", "", {}, "sha512-Y6QSlGiLga8cHfvxGGaonXIlt2bIUTVdH6AMjmpMp7+ANNCp+N96GQbjjhLye3JkaxDfP68x5iZA8NK4imgRig=="], - "@tanstack/start-plugin-core": ["@tanstack/start-plugin-core@1.167.31", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@babel/core": "^7.28.5", "@babel/types": "^7.28.5", "@rolldown/pluginutils": "1.0.0-beta.40", "@tanstack/router-core": "1.168.14", "@tanstack/router-generator": "1.166.30", "@tanstack/router-plugin": "1.167.20", "@tanstack/router-utils": "1.161.6", "@tanstack/start-client-core": "1.167.16", "@tanstack/start-server-core": "1.167.18", "cheerio": "^1.0.0", "exsolve": "^1.0.7", "pathe": "^2.0.3", "picomatch": "^4.0.3", "seroval": "^1.5.0", "source-map": "^0.7.6", "srvx": "^0.11.9", "tinyglobby": "^0.2.15", "ufo": "^1.5.4", "vitefu": "^1.1.1", "xmlbuilder2": "^4.0.3", "zod": "^3.24.2" }, "peerDependencies": { "vite": ">=7.0.0" } }, "sha512-aOxHOgLx8OdkcSxYNDdL09t+VhdWg/rYB5iBJAIKzXE/fp2WBezPcIcyLYgzWnv4KCLHc1VcSNQ6ASLuwrLjtg=="], + "@tanstack/start-plugin-core": ["@tanstack/start-plugin-core@1.167.35", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@babel/core": "^7.28.5", "@babel/types": "^7.28.5", "@rolldown/pluginutils": "1.0.0-beta.40", "@tanstack/router-core": "1.168.15", "@tanstack/router-generator": "1.166.32", "@tanstack/router-plugin": "1.167.22", "@tanstack/router-utils": "1.161.6", "@tanstack/start-client-core": "1.167.17", "@tanstack/start-server-core": "1.167.19", "cheerio": "^1.0.0", "exsolve": "^1.0.7", "pathe": "^2.0.3", "picomatch": "^4.0.3", "seroval": "^1.5.0", "source-map": "^0.7.6", "srvx": "^0.11.9", "tinyglobby": "^0.2.15", "ufo": "^1.5.4", "vitefu": "^1.1.1", "xmlbuilder2": "^4.0.3", "zod": "^3.24.2" }, "peerDependencies": { "vite": ">=7.0.0" } }, "sha512-Ww511KfsXd7TbPYzjiUDCMUI5VbO0chmrTgFi1oOUT0jmk5U0Xh9WVIun1cvRmaq+KBZwvWGvmeIn0UwO3mHEA=="], - "@tanstack/start-server-core": ["@tanstack/start-server-core@1.167.18", "", { "dependencies": { "@tanstack/history": "1.161.6", "@tanstack/router-core": "1.168.14", "@tanstack/start-client-core": "1.167.16", "@tanstack/start-storage-context": "1.166.28", "h3-v2": "npm:h3@2.0.1-rc.20", "seroval": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-JiV0Eqyj5SfEjGZrm/u3nBICeLZRQyeYNI7ine4PjTh0TVL7IklfN5Xfka+p80a/DuUvpHWqKvE4s3tdFZyc1A=="], + "@tanstack/start-server-core": ["@tanstack/start-server-core@1.167.19", "", { "dependencies": { "@tanstack/history": "1.161.6", "@tanstack/router-core": "1.168.15", "@tanstack/start-client-core": "1.167.17", "@tanstack/start-storage-context": "1.166.29", "h3-v2": "npm:h3@2.0.1-rc.20", "seroval": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-wzOdfzLsK91CnjoywnEjXSlVlaRVK99HJhyVijNU1TECBI2JEKvW9S6d14YfS4gD4fFH4V86tFYhkcLPe6nzWg=="], - "@tanstack/start-storage-context": ["@tanstack/start-storage-context@1.166.28", "", { "dependencies": { "@tanstack/router-core": "1.168.14" } }, "sha512-CUQMd6YtJ7hejKXDqT1R4N5gQ9PYyxUCv/ERNJ6/c/8ohNuhMPlOGSFVvqy2BLYNTFSj9GWjahMazeQpQomPgw=="], + "@tanstack/start-storage-context": ["@tanstack/start-storage-context@1.166.29", "", { "dependencies": { "@tanstack/router-core": "1.168.15" } }, "sha512-KrJYudc1nbnTY43jdN+hQFMYkhz7+3T+hkgBoGnIP1OspSe6vGQaYGDB4EUXYnkLfyQp+iUuKubgS8hSKeJ0ng=="], "@tanstack/store": ["@tanstack/store@0.9.3", "", {}, "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw=="], @@ -786,6 +796,14 @@ "@testing-library/user-event": ["@testing-library/user-event@14.6.1", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw=="], + "@tokenlens/core": ["@tokenlens/core@1.3.0", "", {}, "sha512-d8YNHNC+q10bVpi95fELJwJyPVf1HfvBEI18eFQxRSZTdByXrP+f/ZtlhSzkx0Jl0aEmYVeBA5tPeeYRioLViQ=="], + + "@tokenlens/fetch": ["@tokenlens/fetch@1.3.0", "", { "dependencies": { "@tokenlens/core": "1.3.0" } }, "sha512-RONDRmETYly9xO8XMKblmrZjKSwCva4s5ebJwQNfNlChZoA5kplPoCgnWceHnn1J1iRjLVlrCNB43ichfmGBKQ=="], + + "@tokenlens/helpers": ["@tokenlens/helpers@1.3.1", "", { "dependencies": { "@tokenlens/core": "1.3.0", "@tokenlens/fetch": "1.3.0" } }, "sha512-t6yL8N6ES8337E6eVSeH4hCKnPdWkZRFpupy9w5E66Q9IeqQ9IO7XQ6gh12JKjvWiRHuyyJ8MBP5I549Cr41EQ=="], + + "@tokenlens/models": ["@tokenlens/models@1.3.0", "", { "dependencies": { "@tokenlens/core": "1.3.0" } }, "sha512-9mx7ZGeewW4ndXAiD7AT1bbCk4OpJeortbjHHyNkgap+pMPPn1chY6R5zqe1ggXIUzZ2l8VOAKfPqOvpcrisJw=="], + "@ts-morph/common": ["@ts-morph/common@0.27.0", "", { "dependencies": { "fast-glob": "^3.3.3", "minimatch": "^10.0.1", "path-browserify": "^1.0.1" } }, "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ=="], "@tsconfig/node10": ["@tsconfig/node10@1.0.12", "", {}, "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ=="], @@ -914,21 +932,21 @@ "@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="], - "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260408.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260408.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260408.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260408.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260408.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260408.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260408.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260408.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-N0MZLEUnAoP/aRVk7MY119LDsESkbtEwIw+YeXi/jjx2XCqf7ni3GxIVsUYtf/troyuSedq3V/OUrkoCh5A9gA=="], + "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260415.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260415.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260415.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260415.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260415.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260415.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260415.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260415.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-kRQ0x4DgXZBI0bNTck65EUaj48+hbMlCHiJKfc0Se5ZVUG0SKRC6JBPLwIBCX5TfljKsm8SstuJ3qn6uw1IWpA=="], - "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260408.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-YcPczNLfPDB13eUBYHkTOkL7HyWqqqEhho4eSxhAvigZuxvtHQ1uyILIvLVAwipEVzhJ8QciKmLdLucpfi4XyA=="], + "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260415.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-yGyyDb9bP3XfaIm8VUiaq7xkKwFSxLQ44XGYV78lrne12GhXgZ7Smbf2BVnT5MrTgT5uooMzww85P3I3XNVZng=="], - "@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260408.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-cHqkDg53xxxz21MThLBf4vx1kyIpRPEYNdEiQlvu9O35Tth49+aub6F+/YEMd9MG4TYZmxh1bEjkjErTUIElpA=="], + "@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260415.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-vzac8BSbSkGPV/FMKyY/3cNZN+FgvjT1E+NNR8xWO8DfvSz4hYqbxvAL+zWPUno6R8afNFLZeJTfuIge0tJJ1g=="], - "@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260408.1", "", { "os": "linux", "cpu": "arm" }, "sha512-w26Gv9yq9LIYIhxjkQC+i0wBPDdQdX+H06ZhyVRL5grKWTIsk9Xwjp9mDRB/dGlXBKcvnM25JH16OyAA0rFH3A=="], + "@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260415.1", "", { "os": "linux", "cpu": "arm" }, "sha512-1CI2nLfsoEibEAt6ApMVEr5M/v9EeXHmn9iD2nyyomO34ky3zqBtEPHakvgXD4QmpZg9O4WxRKc6u6EIy0Imxg=="], - "@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260408.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-iHG0FEXq/QFsn+qlTPllxdcbvfQ9aRYggy4lc1z0+f11Nyk4YDNCSiR8WW7pbnOTx/VreGbbXhlpuJXTidqL8g=="], + "@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260415.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jiXu+SrpCL/6J3LwuUSxU8scYs5H0wBkqu3CopdSTcJxQuzUDe6QAEoEW3O81cdrsq5qrIlfFxWH3bdohsvhJA=="], - "@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260408.1", "", { "os": "linux", "cpu": "x64" }, "sha512-hMcUlUIzYbvbdq6j/B4RPL+kZR917NGnE9AgPZ7dJ92yamH/7LGT1Mnlc6McUx31yqTFBFHdTc7Cfx+ynua7Iw=="], + "@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260415.1", "", { "os": "linux", "cpu": "x64" }, "sha512-+4r4UqGE5ccy9vJNOhjXTqbZPkVGlqdiEgTJbdz8+EpUNQaLGMBhDj1cBMdrdK1YRuj/3C+pLfE3PD9kEsxf6Q=="], - "@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260408.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-avJWIEKSx4rdBLZD1FOOTuxTU51dQfYb3jZvZMaXD4thJjq+6eSwfzu2elwL36AZDlnaxggGjB5nBxp0t54iOA=="], + "@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260415.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-QsCAG7la4GE4Dp7i9MUz7Qv+HbKVDdmwlmn+8sOo0M3aSC6WssCU5gKVBUjp43WPtml/JticwzSz1qXLfdxHFA=="], - "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260408.1", "", { "os": "win32", "cpu": "x64" }, "sha512-gpvEHkF/WoxkA3711c4uWNCZO9WAuwrq49COdNwxgOTzYHnMc1yCj8CpkCUJwU0f/Ydwp2s6/efn6gTMvtckPg=="], + "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260415.1", "", { "os": "win32", "cpu": "x64" }, "sha512-Z+rclKC7FqkUsqQ+ErgWJmf5J55LEl/rooFq71prC6V0vCBa5yLMmLBmFxZLLj2BsCBuwbN2O7aWUWrpCUEkmw=="], "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], @@ -964,7 +982,7 @@ "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], - "ai": ["ai@6.0.158", "", { "dependencies": { "@ai-sdk/gateway": "3.0.95", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-gLTp1UXFtMqKUi3XHs33K7UFglbvojkxF/aq337TxnLGOhHIW9+GyP2jwW4hYX87f1es+wId3VQoPRRu9zEStQ=="], + "ai": ["ai@6.0.162", "", { "dependencies": { "@ai-sdk/gateway": "3.0.99", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-1PSvNEK1PEbpUXahnFrcey6l7DJXMVWmg0ibQ8h8oMSe9V1Vx5d+R3xNu0hzBtwqfxYj21ddZo+EUYVs6GOEyA=="], "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], @@ -1000,7 +1018,7 @@ "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.10.16", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-Lyf3aK28zpsD1yQMiiHD4RvVb6UdMoo8xzG2XzFIfR9luPzOpcBlAsT/qfB1XWS1bxWT+UtE4WmQgsp297FYOA=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.18", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-VSnGQAOLtP5mib/DPyg2/t+Tlv65NTBz83BJBJvmLVHHuKJVaDOBvJJykiT5TR++em5nfAySPccDZDa4oSrn8A=="], "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="], @@ -1010,7 +1028,7 @@ "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], - "brace-expansion": ["brace-expansion@1.1.13", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w=="], + "brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], @@ -1094,7 +1112,7 @@ "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], - "cookie-es": ["cookie-es@2.0.1", "", {}, "sha512-aVf4A4hI2w70LnF7GG+7xDQUkliwiXWXFvTjkip4+b64ygDQ2sJPRSKFDHbxn8o0xu9QzPkMuuiWIXyFSE2slA=="], + "cookie-es": ["cookie-es@3.1.1", "", {}, "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg=="], "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], @@ -1244,7 +1262,7 @@ "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], - "dotenv": ["dotenv@17.4.1", "", {}, "sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw=="], + "dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], @@ -1252,7 +1270,7 @@ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - "electron-to-chromium": ["electron-to-chromium@1.5.334", "", {}, "sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog=="], + "electron-to-chromium": ["electron-to-chromium@1.5.335", "", {}, "sha512-q9n5T4BR4Xwa2cwbrwcsDJtHD/enpQ5S1xF1IAtdqf5AAgqDFmR/aakqH3ChFdqd/QXJhS3rnnXFtexU7rax6Q=="], "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], @@ -1324,6 +1342,8 @@ "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + "fake-indexeddb": ["fake-indexeddb@6.2.5", "", {}, "sha512-CGnyrvbhPlWYMngksqrSSUT1BAVP49dZocrHuK0SvtR0D5TMs5wP0o3j7jexDJW01KSadjBp1M/71o/KR3nD1w=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], @@ -1542,6 +1562,8 @@ "jsdom": ["jsdom@29.0.2", "", { "dependencies": { "@asamuzakjp/css-color": "^5.1.5", "@asamuzakjp/dom-selector": "^7.0.6", "@bramus/specificity": "^2.4.2", "@csstools/css-syntax-patches-for-csstree": "^1.1.1", "@exodus/bytes": "^1.15.0", "css-tree": "^3.2.1", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.2.7", "parse5": "^8.0.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.1", "undici": "^7.24.5", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.1", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-9VnGEBosc/ZpwyOsJBCQ/3I5p7Q5ngOY14a9bf5btenAORmZfDse1ZEheMiWcJ3h81+Fv7HmJFdS0szo/waF2w=="], + "jsep": ["jsep@1.4.0", "", {}, "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw=="], + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], @@ -1556,6 +1578,8 @@ "jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], + "jsonpath-plus": ["jsonpath-plus@10.4.0", "", { "dependencies": { "@jsep-plugin/assignment": "^1.3.0", "@jsep-plugin/regex": "^1.0.4", "jsep": "^1.4.0" }, "bin": { "jsonpath": "bin/jsonpath-cli.js", "jsonpath-plus": "bin/jsonpath-cli.js" } }, "sha512-T92WWatJXmhBbKsgH/0hl+jxjdXrifi5IKeMY02DWggRxX0UElcbVzPlmgLTbvsPeW1PasQ6xE2Q75stkhGbsA=="], + "katex": ["katex@0.16.45", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA=="], "khroma": ["khroma@2.1.0", "", {}, "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="], @@ -1866,7 +1890,7 @@ "powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="], - "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], + "prettier": ["prettier@3.8.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-8c3mgTe0ASwWAJK+78dpviD+A8EqhndQPUBpNUIPt6+xWlIigCwfN01lWr9MAede4uqXGTEKeQWTvzb3vjia0Q=="], "pretty-format": ["pretty-format@30.3.0", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ=="], @@ -2072,6 +2096,8 @@ "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "swr": ["swr@2.4.1", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA=="], + "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], @@ -2084,6 +2110,8 @@ "tapable": ["tapable@2.3.2", "", {}, "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA=="], + "throttleit": ["throttleit@2.1.0", "", {}, "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw=="], + "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], @@ -2104,6 +2132,8 @@ "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + "tokenlens": ["tokenlens@1.3.1", "", { "dependencies": { "@tokenlens/core": "1.3.0", "@tokenlens/fetch": "1.3.0", "@tokenlens/helpers": "1.3.1", "@tokenlens/models": "1.3.0" } }, "sha512-7oxmsS5PNCX3z+b+z07hL5vCzlgHKkCGrEQjQmWl5l+v5cUrtL7S1cuST4XThaL1XyjbTX8J5hfP0cjDJRkaLA=="], + "tough-cookie": ["tough-cookie@6.0.1", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw=="], "tr46": ["tr46@6.0.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw=="], @@ -2268,6 +2298,8 @@ "@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@ai-sdk/react/ai": ["ai@6.0.161", "", { "dependencies": { "@ai-sdk/gateway": "3.0.98", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ufhmijmx2YyWTPAicGgtpLOB/xD7mG8zKs1pT1Trj+JL/3r1rS8fkMi/cHZoChSAQSGB4pgmcWVxDrVTUvK2IQ=="], + "@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], @@ -2316,37 +2348,41 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@tanstack/react-router/@tanstack/router-core": ["@tanstack/router-core@1.168.14", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.0", "seroval-plugins": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-UhCJtjNrd5wcTmhgB2HyUP0+Rj1M7BD4dS11YsF9x6VC2KH/eqxzs/vK+nN5f+cOhPOLZdmLkWMW+WGmacZ8HA=="], + "@tanstack/react-start/@tanstack/react-router": ["@tanstack/react-router@1.168.22", "", { "dependencies": { "@tanstack/history": "1.161.6", "@tanstack/react-store": "^0.9.3", "@tanstack/router-core": "1.168.15", "isbot": "^5.1.22" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-W2LyfkfJtDCf//jOjZeUBWwOVl8iDRVTECpGHa2M28MT3T5/VVnjgicYNHR/ax0Filk1iU67MRjcjHheTYvK1Q=="], - "@tanstack/react-start-client/@tanstack/router-core": ["@tanstack/router-core@1.168.14", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.0", "seroval-plugins": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-UhCJtjNrd5wcTmhgB2HyUP0+Rj1M7BD4dS11YsF9x6VC2KH/eqxzs/vK+nN5f+cOhPOLZdmLkWMW+WGmacZ8HA=="], + "@tanstack/react-start-client/@tanstack/react-router": ["@tanstack/react-router@1.168.22", "", { "dependencies": { "@tanstack/history": "1.161.6", "@tanstack/react-store": "^0.9.3", "@tanstack/router-core": "1.168.15", "isbot": "^5.1.22" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-W2LyfkfJtDCf//jOjZeUBWwOVl8iDRVTECpGHa2M28MT3T5/VVnjgicYNHR/ax0Filk1iU67MRjcjHheTYvK1Q=="], - "@tanstack/react-start-rsc/@tanstack/router-core": ["@tanstack/router-core@1.168.14", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.0", "seroval-plugins": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-UhCJtjNrd5wcTmhgB2HyUP0+Rj1M7BD4dS11YsF9x6VC2KH/eqxzs/vK+nN5f+cOhPOLZdmLkWMW+WGmacZ8HA=="], + "@tanstack/react-start-client/@tanstack/router-core": ["@tanstack/router-core@1.168.15", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.0", "seroval-plugins": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-Wr0424NDtD8fT/uALobMZ9DdcfsTyXtW5IPR++7zvW8/7RaIOeaqXpVDId8ywaGtqPWLWOfaUg2zUtYtukoXYA=="], - "@tanstack/react-start-server/@tanstack/router-core": ["@tanstack/router-core@1.168.14", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.0", "seroval-plugins": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-UhCJtjNrd5wcTmhgB2HyUP0+Rj1M7BD4dS11YsF9x6VC2KH/eqxzs/vK+nN5f+cOhPOLZdmLkWMW+WGmacZ8HA=="], + "@tanstack/react-start-rsc/@tanstack/react-router": ["@tanstack/react-router@1.168.22", "", { "dependencies": { "@tanstack/history": "1.161.6", "@tanstack/react-store": "^0.9.3", "@tanstack/router-core": "1.168.15", "isbot": "^5.1.22" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-W2LyfkfJtDCf//jOjZeUBWwOVl8iDRVTECpGHa2M28MT3T5/VVnjgicYNHR/ax0Filk1iU67MRjcjHheTYvK1Q=="], - "@tanstack/router-devtools-core/@tanstack/router-core": ["@tanstack/router-core@1.168.14", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.0", "seroval-plugins": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-UhCJtjNrd5wcTmhgB2HyUP0+Rj1M7BD4dS11YsF9x6VC2KH/eqxzs/vK+nN5f+cOhPOLZdmLkWMW+WGmacZ8HA=="], + "@tanstack/react-start-rsc/@tanstack/router-core": ["@tanstack/router-core@1.168.15", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.0", "seroval-plugins": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-Wr0424NDtD8fT/uALobMZ9DdcfsTyXtW5IPR++7zvW8/7RaIOeaqXpVDId8ywaGtqPWLWOfaUg2zUtYtukoXYA=="], - "@tanstack/router-generator/@tanstack/router-core": ["@tanstack/router-core@1.168.14", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.0", "seroval-plugins": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-UhCJtjNrd5wcTmhgB2HyUP0+Rj1M7BD4dS11YsF9x6VC2KH/eqxzs/vK+nN5f+cOhPOLZdmLkWMW+WGmacZ8HA=="], + "@tanstack/react-start-server/@tanstack/react-router": ["@tanstack/react-router@1.168.22", "", { "dependencies": { "@tanstack/history": "1.161.6", "@tanstack/react-store": "^0.9.3", "@tanstack/router-core": "1.168.15", "isbot": "^5.1.22" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-W2LyfkfJtDCf//jOjZeUBWwOVl8iDRVTECpGHa2M28MT3T5/VVnjgicYNHR/ax0Filk1iU67MRjcjHheTYvK1Q=="], - "@tanstack/router-generator/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@tanstack/react-start-server/@tanstack/router-core": ["@tanstack/router-core@1.168.15", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.0", "seroval-plugins": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-Wr0424NDtD8fT/uALobMZ9DdcfsTyXtW5IPR++7zvW8/7RaIOeaqXpVDId8ywaGtqPWLWOfaUg2zUtYtukoXYA=="], - "@tanstack/router-plugin/@tanstack/router-core": ["@tanstack/router-core@1.168.14", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.0", "seroval-plugins": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-UhCJtjNrd5wcTmhgB2HyUP0+Rj1M7BD4dS11YsF9x6VC2KH/eqxzs/vK+nN5f+cOhPOLZdmLkWMW+WGmacZ8HA=="], + "@tanstack/router-generator/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "@tanstack/router-plugin/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "@tanstack/start-client-core/@tanstack/router-core": ["@tanstack/router-core@1.168.14", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.0", "seroval-plugins": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-UhCJtjNrd5wcTmhgB2HyUP0+Rj1M7BD4dS11YsF9x6VC2KH/eqxzs/vK+nN5f+cOhPOLZdmLkWMW+WGmacZ8HA=="], + "@tanstack/start-client-core/@tanstack/router-core": ["@tanstack/router-core@1.168.15", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.0", "seroval-plugins": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-Wr0424NDtD8fT/uALobMZ9DdcfsTyXtW5IPR++7zvW8/7RaIOeaqXpVDId8ywaGtqPWLWOfaUg2zUtYtukoXYA=="], "@tanstack/start-plugin-core/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], "@tanstack/start-plugin-core/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.40", "", {}, "sha512-s3GeJKSQOwBlzdUrj4ISjJj5SfSh+aqn0wjOar4Bx95iV1ETI7F6S/5hLcfAxZ9kXDcyrAkxPlqmd1ZITttf+w=="], - "@tanstack/start-plugin-core/@tanstack/router-core": ["@tanstack/router-core@1.168.14", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.0", "seroval-plugins": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-UhCJtjNrd5wcTmhgB2HyUP0+Rj1M7BD4dS11YsF9x6VC2KH/eqxzs/vK+nN5f+cOhPOLZdmLkWMW+WGmacZ8HA=="], + "@tanstack/start-plugin-core/@tanstack/router-core": ["@tanstack/router-core@1.168.15", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.0", "seroval-plugins": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-Wr0424NDtD8fT/uALobMZ9DdcfsTyXtW5IPR++7zvW8/7RaIOeaqXpVDId8ywaGtqPWLWOfaUg2zUtYtukoXYA=="], + + "@tanstack/start-plugin-core/@tanstack/router-generator": ["@tanstack/router-generator@1.166.32", "", { "dependencies": { "@babel/types": "^7.28.5", "@tanstack/router-core": "1.168.15", "@tanstack/router-utils": "1.161.6", "@tanstack/virtual-file-routes": "1.161.7", "magic-string": "^0.30.21", "prettier": "^3.5.0", "tsx": "^4.19.2", "zod": "^3.24.2" } }, "sha512-VuusKwEXcgKq+myq1JQfZogY8scTXIIeFls50dJ/UXgCXWp5n14iFreYNlg41wURcak2oA3M+t2TVfD0xUUD6g=="], + + "@tanstack/start-plugin-core/@tanstack/router-plugin": ["@tanstack/router-plugin@1.167.22", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.168.15", "@tanstack/router-generator": "1.166.32", "@tanstack/router-utils": "1.161.6", "@tanstack/virtual-file-routes": "1.161.7", "chokidar": "^3.6.0", "unplugin": "^2.1.2", "zod": "^3.24.2" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2", "@tanstack/react-router": "^1.168.21", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0 || >=8.0.0", "vite-plugin-solid": "^2.11.10 || ^3.0.0-0", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"], "bin": { "intent": "bin/intent.js" } }, "sha512-wYPzIvBK8bcmXVUpZfSgGBXOrfBAdF4odKevz6rejio5rEd947NtKDF5R7eYdwlAOmRqYpLJnJ1QHkc5t8bY4w=="], "@tanstack/start-plugin-core/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "@tanstack/start-server-core/@tanstack/router-core": ["@tanstack/router-core@1.168.14", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.0", "seroval-plugins": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-UhCJtjNrd5wcTmhgB2HyUP0+Rj1M7BD4dS11YsF9x6VC2KH/eqxzs/vK+nN5f+cOhPOLZdmLkWMW+WGmacZ8HA=="], + "@tanstack/start-server-core/@tanstack/router-core": ["@tanstack/router-core@1.168.15", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.0", "seroval-plugins": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-Wr0424NDtD8fT/uALobMZ9DdcfsTyXtW5IPR++7zvW8/7RaIOeaqXpVDId8ywaGtqPWLWOfaUg2zUtYtukoXYA=="], - "@tanstack/start-storage-context/@tanstack/router-core": ["@tanstack/router-core@1.168.14", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.0", "seroval-plugins": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-UhCJtjNrd5wcTmhgB2HyUP0+Rj1M7BD4dS11YsF9x6VC2KH/eqxzs/vK+nN5f+cOhPOLZdmLkWMW+WGmacZ8HA=="], + "@tanstack/start-storage-context/@tanstack/router-core": ["@tanstack/router-core@1.168.15", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.0", "seroval-plugins": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-Wr0424NDtD8fT/uALobMZ9DdcfsTyXtW5IPR++7zvW8/7RaIOeaqXpVDId8ywaGtqPWLWOfaUg2zUtYtukoXYA=="], "@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="], @@ -2368,6 +2404,8 @@ "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "cmdk/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], + "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "cytoscape-fcose/cose-base": ["cose-base@2.2.0", "", { "dependencies": { "layout-base": "^2.0.0" } }, "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g=="], @@ -2454,6 +2492,8 @@ "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "@ai-sdk/react/ai/@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.98", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Ol+nP8PIlj8FjN8qKlxhE89N0woqAaGi9CUBGp1boe3RafpphJ7WMuq/RErSvxtwTqje03TP+zIdzP113krxRg=="], + "@dotenvx/dotenvx/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], "@dotenvx/dotenvx/execa/human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], @@ -2474,30 +2514,10 @@ "@jest/types/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "@tanstack/react-router/@tanstack/router-core/cookie-es": ["cookie-es@3.1.1", "", {}, "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg=="], - - "@tanstack/react-start-client/@tanstack/router-core/cookie-es": ["cookie-es@3.1.1", "", {}, "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg=="], - - "@tanstack/react-start-rsc/@tanstack/router-core/cookie-es": ["cookie-es@3.1.1", "", {}, "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg=="], - - "@tanstack/react-start-server/@tanstack/router-core/cookie-es": ["cookie-es@3.1.1", "", {}, "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg=="], - - "@tanstack/router-devtools-core/@tanstack/router-core/cookie-es": ["cookie-es@3.1.1", "", {}, "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg=="], - - "@tanstack/router-generator/@tanstack/router-core/cookie-es": ["cookie-es@3.1.1", "", {}, "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg=="], - - "@tanstack/router-plugin/@tanstack/router-core/cookie-es": ["cookie-es@3.1.1", "", {}, "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg=="], - - "@tanstack/start-client-core/@tanstack/router-core/cookie-es": ["cookie-es@3.1.1", "", {}, "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg=="], + "@tanstack/react-start/@tanstack/react-router/@tanstack/router-core": ["@tanstack/router-core@1.168.15", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.0", "seroval-plugins": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-Wr0424NDtD8fT/uALobMZ9DdcfsTyXtW5IPR++7zvW8/7RaIOeaqXpVDId8ywaGtqPWLWOfaUg2zUtYtukoXYA=="], "@tanstack/start-plugin-core/@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - "@tanstack/start-plugin-core/@tanstack/router-core/cookie-es": ["cookie-es@3.1.1", "", {}, "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg=="], - - "@tanstack/start-server-core/@tanstack/router-core/cookie-es": ["cookie-es@3.1.1", "", {}, "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg=="], - - "@tanstack/start-storage-context/@tanstack/router-core/cookie-es": ["cookie-es@3.1.1", "", {}, "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg=="], - "@testing-library/dom/pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], "@ts-morph/common/minimatch/brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], diff --git a/package.json b/package.json index 4b6c5a2..7c1f8dd 100644 --- a/package.json +++ b/package.json @@ -23,10 +23,11 @@ }, "dependencies": { "@ai-sdk/anthropic": "^3.0.69", - "@ai-sdk/google": "^3.0.62", + "@ai-sdk/google": "^3.0.63", "@ai-sdk/groq": "^3.0.35", "@ai-sdk/mistral": "^3.0.30", - "@ai-sdk/openai": "^3.0.52", + "@ai-sdk/openai": "^3.0.53", + "@ai-sdk/react": "3.0.163", "@ai-sdk/xai": "^3.0.82", "@base-ui/react": "^1.4.0", "@dnd-kit/core": "^6.3.1", @@ -44,12 +45,13 @@ "@tanstack/react-query": "^5.99.0", "@tanstack/react-router": "1.168.19", "@tanstack/react-router-with-query": "1.130.17", - "@tanstack/react-start": "^1.167.34", - "ai": "^6.0.158", + "@tanstack/react-start": "^1.167.41", + "ai": "^6.0.162", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.1.0", + "jsonpath-plus": "^10.4.0", "lucide-react": "^1.8.0", "motion": "^12.38.0", "next-themes": "^0.4.6", @@ -63,6 +65,7 @@ "tailwind-merge": "^3.5.0", "tailwindcss": "^4.2.2", "tailwindcss-animate": "^1.0.7", + "tokenlens": "^1.3.1", "tw-animate-css": "^1.4.0", "use-stick-to-bottom": "^1.1.3", "zod": "^4.3.6" @@ -80,10 +83,11 @@ "@types/json-schema": "^7.0.15", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", - "@typescript/native-preview": "^7.0.0-dev.20260408.1", + "@typescript/native-preview": "^7.0.0-dev.20260415.1", "@vitejs/plugin-react": "^6.0.1", "@vitest/coverage-v8": "4.1.4", "ajv": "^8.18.0", + "fake-indexeddb": "^6.2.5", "jsdom": "^29.0.2", "oxfmt": "^0.45.0", "oxlint": "^1.60.0", diff --git a/public/r/generated-command.json b/public/r/generated-command.json index 3bcf4bf..3cee7cd 100644 --- a/public/r/generated-command.json +++ b/public/r/generated-command.json @@ -20,25 +20,25 @@ }, { "path": "registry/commandly/types/flat.ts", - "content": "export interface ToolInfo {\n description?: string;\n version?: string;\n url?: string;\n}\n\nexport interface Command {\n key: string;\n parentCommandKey?: string;\n name: string;\n description?: string;\n interactive?: boolean;\n isDefault?: boolean;\n sortOrder?: number;\n}\n\nexport interface ParameterEnumValue {\n value: string;\n displayName: string;\n description?: string;\n isDefault?: boolean;\n sortOrder?: number;\n}\n\nexport interface ParameterEnumValues {\n values: ParameterEnumValue[];\n allowMultiple?: boolean;\n separator?: string;\n}\n\nexport type ParameterValidationType =\n | \"min_length\"\n | \"max_length\"\n | \"min_value\"\n | \"max_value\"\n | \"regex\";\n\nexport interface ParameterValidation {\n key: string;\n validationType: ParameterValidationType;\n validationValue: string;\n errorMessage: string;\n}\n\nexport type ParameterDependencyType = \"requires\" | \"conflicts_with\";\n\nexport interface ParameterDependency {\n key: string;\n parameterKey: string;\n dependsOnParameterKey: string;\n dependencyType: ParameterDependencyType;\n conditionValue?: string;\n}\n\nexport type ParameterValue = string | number | boolean | string[];\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface ToolMetadata {}\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface ParameterMetadata {\n tags?: string[];\n}\n\nexport type ParameterType = \"Flag\" | \"Option\" | \"Argument\";\n\nexport type ParameterDataType = \"String\" | \"Number\" | \"Boolean\" | \"Enum\";\n\nexport interface Parameter {\n key: string;\n name: string;\n commandKey?: string;\n description?: string;\n group?: string;\n metadata?: ParameterMetadata;\n parameterType: ParameterType;\n dataType: ParameterDataType;\n isRequired?: boolean;\n isRepeatable?: boolean;\n isGlobal?: boolean;\n shortFlag?: string;\n longFlag?: string;\n position?: number;\n sortOrder?: number;\n arraySeparator?: string;\n keyValueSeparator?: string;\n enum?: ParameterEnumValues;\n validations?: ParameterValidation[];\n dependencies?: ParameterDependency[];\n}\n\nexport type ExclusionType = \"mutual_exclusive\" | \"required_one_of\";\n\nexport interface ExclusionGroup {\n key?: string;\n commandKey?: string;\n name: string;\n exclusionType: ExclusionType;\n parameterKeys: string[];\n}\n\nexport interface Tool {\n name: string;\n displayName: string;\n info?: ToolInfo;\n commands: Command[];\n parameters: Parameter[];\n exclusionGroups?: ExclusionGroup[];\n metadata?: ToolMetadata;\n}\n", + "content": "export interface ToolInfo {\n /** A brief human-readable description of what the tool does. */\n description?: string;\n /** The version string of the tool (e.g. \"1.0.0\"). */\n version?: string;\n /** The homepage or documentation URL for the tool. */\n url?: string;\n}\n\nexport interface Command {\n /** Unique identifier for this command within the tool. */\n key: string;\n /** Key of the parent command; used to represent subcommand nesting. */\n parentCommandKey?: string;\n /** Human-readable display name of the command. */\n name: string;\n /** Brief description of what this command does. */\n description?: string;\n /** Whether this command opens an interactive session or prompt. */\n interactive?: boolean;\n /** Display sort position relative to sibling commands. */\n sortOrder?: number;\n}\n\nexport interface ParameterEnumValue {\n /** The raw value passed to the CLI for this choice. */\n value: string;\n /** Human-readable label shown to the user for this enum choice. */\n displayName: string;\n /** Description of what this enum value does or represents. */\n description?: string;\n /** Whether this is the default selection when no value is provided. */\n isDefault?: boolean;\n /** Display sort position relative to sibling enum values. */\n sortOrder?: number;\n}\n\nexport interface ParameterEnumValues {\n /** The list of allowed enum choices. */\n values: ParameterEnumValue[];\n /** Whether the user can select multiple values at once. */\n allowMultiple?: boolean;\n /** Separator character used when joining multiple selected values. */\n separator?: string;\n}\n\nexport type ParameterValidationType =\n | \"min_length\"\n | \"max_length\"\n | \"min_value\"\n | \"max_value\"\n | \"regex\";\n\nexport interface ParameterValidation {\n /** Unique identifier for this validation rule. */\n key: string;\n /** The type of validation to apply. */\n validationType: ParameterValidationType;\n /** The value to validate against (e.g. the max length number, or a regex pattern). */\n validationValue: string;\n /** The error message to display when validation fails. */\n errorMessage: string;\n}\n\nexport type ParameterDependencyType = \"requires\" | \"conflicts_with\";\n\nexport interface ParameterDependency {\n /** Unique identifier for this dependency rule. */\n key: string;\n /** Key of the parameter that owns this dependency. */\n parameterKey: string;\n /** Key of the parameter this dependency references. */\n dependsOnParameterKey: string;\n /** Whether this parameter requires or conflicts with the referenced parameter. */\n dependencyType: ParameterDependencyType;\n /** Optional value that the referenced parameter must have for this dependency to apply. */\n conditionValue?: string;\n}\n\nexport type ParameterValue = string | number | boolean | string[];\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface ToolMetadata {}\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface ParameterMetadata {\n /** Arbitrary tags for categorising or filtering parameters. */\n tags?: string[];\n}\n\nexport type ParameterType = \"Flag\" | \"Option\" | \"Argument\";\n\nexport type ParameterDataType = \"String\" | \"Number\" | \"Boolean\" | \"Enum\";\n\nexport interface Parameter {\n /** Unique identifier for this parameter within the tool. */\n key: string;\n /** Human-readable display name of the parameter. */\n name: string;\n /** Key of the command this parameter belongs to; omit for global parameters. */\n commandKey?: string;\n /** Brief description of what this parameter does or accepts. */\n description?: string;\n /** Optional grouping label for organising related parameters in the UI. */\n group?: string;\n /** Additional metadata such as tags. */\n metadata?: ParameterMetadata;\n /** Whether this is a boolean flag, a key-value option, or a positional argument. */\n parameterType: ParameterType;\n /** The data type of the parameter's value. */\n dataType: ParameterDataType;\n /** Whether the user must provide this parameter. */\n isRequired?: boolean;\n /** Whether this parameter can be specified multiple times. */\n isRepeatable?: boolean;\n /** Whether this parameter applies to all commands rather than a single command. */\n isGlobal?: boolean;\n /** The single-character short flag (e.g. \"-v\"). */\n shortFlag?: string;\n /** The long-form flag or option name (e.g. \"--verbose\"). */\n longFlag?: string;\n /** Zero-based position index for positional arguments. */\n position?: number;\n /** Display sort position relative to sibling parameters. */\n sortOrder?: number;\n /** Separator character used when the option accepts multiple values in one argument (e.g. \",\"). */\n arraySeparator?: string;\n /** Separator between key and value for key=value style options (e.g. \"=\"). */\n keyValueSeparator?: string;\n /** Allowed enum choices when dataType is \"Enum\". */\n enum?: ParameterEnumValues;\n /** Validation rules applied to this parameter's value. */\n validations?: ParameterValidation[];\n /** Dependencies on other parameters (requires or conflicts-with relationships). */\n dependencies?: ParameterDependency[];\n}\n\nexport type ExclusionType = \"mutual_exclusive\" | \"required_one_of\";\n\nexport interface ExclusionGroup {\n /** Unique identifier for this exclusion group. */\n key?: string;\n /** Key of the command this exclusion group belongs to; omit for global groups. */\n commandKey?: string;\n /** Human-readable name for this exclusion group. */\n name: string;\n /** Whether parameters in this group are mutually exclusive or one is required. */\n exclusionType: ExclusionType;\n /** Keys of the parameters that participate in this exclusion group. */\n parameterKeys: string[];\n}\n\nexport interface Tool {\n /** Unique machine-readable identifier for the tool (e.g. \"httpx\"). */\n name: string;\n /** Human-readable display name for the tool (e.g. \"HTTPx\"). */\n displayName: string;\n /** General information about the tool such as description, version, and URL. */\n info?: ToolInfo;\n /** List of all commands and subcommands defined for this tool. */\n commands: Command[];\n /** Flat list of all parameters across all commands and global scope. */\n parameters: Parameter[];\n /** Groups of parameters with mutual exclusion or required-one-of constraints. */\n exclusionGroups?: ExclusionGroup[];\n /** Arbitrary metadata attached to the tool. */\n metadata?: ToolMetadata;\n}\n", "type": "registry:file", "target": "components/commandly/types/flat.ts" }, { "path": "registry/commandly/types/nested.ts", - "content": "import type {\n ExclusionType,\n ParameterDataType,\n ParameterDependencyType,\n ParameterEnumValues,\n ParameterMetadata,\n ParameterType,\n ParameterValidationType,\n ToolInfo,\n ToolMetadata,\n} from \"@/components/commandly/types/flat\";\n\nexport interface NestedParameterValidation {\n validationType: ParameterValidationType;\n validationValue: string;\n errorMessage: string;\n}\n\nexport interface NestedParameterDependency {\n dependsOnParameter: string;\n dependencyType: ParameterDependencyType;\n conditionValue?: string;\n}\n\nexport interface NestedParameter {\n name: string;\n description?: string;\n group?: string;\n parameterType: ParameterType;\n dataType: ParameterDataType;\n metadata?: ParameterMetadata;\n isRequired?: boolean;\n isRepeatable?: boolean;\n isGlobal?: boolean;\n shortFlag?: string;\n longFlag?: string;\n position?: number;\n sortOrder?: number;\n arraySeparator?: string;\n keyValueSeparator?: string;\n enum?: ParameterEnumValues;\n validations?: NestedParameterValidation[];\n dependencies?: NestedParameterDependency[];\n}\n\nexport interface NestedCommand {\n name: string;\n description?: string;\n interactive?: boolean;\n isDefault: boolean;\n sortOrder: number;\n parameters: NestedParameter[];\n subcommands: NestedCommand[];\n}\n\nexport interface NestedExclusionGroup {\n name: string;\n exclusionType: ExclusionType;\n parameters: string[];\n}\n\nexport interface NestedTool {\n $schema?: string;\n name: string;\n displayName: string;\n info?: ToolInfo;\n url?: string;\n globalParameters: NestedParameter[];\n commands: NestedCommand[];\n exclusionGroups?: NestedExclusionGroup[] | null;\n metadata?: ToolMetadata;\n}\n", + "content": "import type {\n ExclusionType,\n ParameterDataType,\n ParameterDependencyType,\n ParameterEnumValues,\n ParameterMetadata,\n ParameterType,\n ParameterValidationType,\n ToolInfo,\n ToolMetadata,\n} from \"@/components/commandly/types/flat\";\n\nexport interface NestedParameterValidation {\n /** The type of validation to apply. */\n validationType: ParameterValidationType;\n /** The value to validate against (e.g. max length number or regex pattern). */\n validationValue: string;\n /** The error message to display when validation fails. */\n errorMessage: string;\n}\n\nexport interface NestedParameterDependency {\n /** Name of the parameter this dependency references. */\n dependsOnParameter: string;\n /** Whether this parameter requires or conflicts with the referenced parameter. */\n dependencyType: ParameterDependencyType;\n /** Optional value that the referenced parameter must have for this dependency to apply. */\n conditionValue?: string;\n}\n\nexport interface NestedParameter {\n /** Human-readable display name of the parameter. */\n name: string;\n /** Brief description of what this parameter does or accepts. */\n description?: string;\n /** Optional grouping label for organising related parameters in the UI. */\n group?: string;\n /** Whether this is a boolean flag, a key-value option, or a positional argument. */\n parameterType: ParameterType;\n /** The data type of the parameter's value. */\n dataType: ParameterDataType;\n /** Additional metadata such as tags. */\n metadata?: ParameterMetadata;\n /** Whether the user must provide this parameter. */\n isRequired?: boolean;\n /** Whether this parameter can be specified multiple times. */\n isRepeatable?: boolean;\n /** Whether this parameter applies to all commands rather than a single command. */\n isGlobal?: boolean;\n /** The single-character short flag (e.g. \"-v\"). */\n shortFlag?: string;\n /** The long-form flag or option name (e.g. \"--verbose\"). */\n longFlag?: string;\n /** Zero-based position index for positional arguments. */\n position?: number;\n /** Display sort position relative to sibling parameters. */\n sortOrder?: number;\n /** Separator character used when the option accepts multiple values in one argument (e.g. \",\"). */\n arraySeparator?: string;\n /** Separator between key and value for key=value style options (e.g. \"=\"). */\n keyValueSeparator?: string;\n /** Allowed enum choices when dataType is \"Enum\". */\n enum?: ParameterEnumValues;\n /** Validation rules applied to this parameter's value. */\n validations?: NestedParameterValidation[];\n /** Dependencies on other parameters (requires or conflicts-with relationships). */\n dependencies?: NestedParameterDependency[];\n}\n\nexport interface NestedCommand {\n /** Human-readable display name of the command. */\n name: string;\n /** Brief description of what this command does. */\n description?: string;\n /** Whether this command opens an interactive session or prompt. */\n interactive?: boolean;\n /** Display sort position relative to sibling commands. */\n sortOrder: number;\n /** Parameters that belong directly to this command. */\n parameters: NestedParameter[];\n /** Nested subcommands of this command. */\n subcommands: NestedCommand[];\n}\n\nexport interface NestedExclusionGroup {\n /** Human-readable name for this exclusion group. */\n name: string;\n /** Whether parameters in this group are mutually exclusive or one is required. */\n exclusionType: ExclusionType;\n /** Names of the parameters that participate in this exclusion group. */\n parameters: string[];\n}\n\nexport interface NestedTool {\n /** Optional JSON schema URI for validation. */\n $schema?: string;\n /** Unique machine-readable identifier for the tool (e.g. \"httpx\"). */\n name: string;\n /** Human-readable display name for the tool (e.g. \"HTTPx\"). */\n displayName: string;\n /** General information about the tool such as description, version, and URL. */\n info?: ToolInfo;\n /** The homepage or documentation URL for the tool. */\n url?: string;\n /** Parameters that apply to all commands globally. */\n globalParameters: NestedParameter[];\n /** Hierarchical list of commands and their nested subcommands. */\n commands: NestedCommand[];\n /** Groups of parameters with mutual exclusion or required-one-of constraints. */\n exclusionGroups?: NestedExclusionGroup[] | null;\n /** Arbitrary metadata attached to the tool. */\n metadata?: ToolMetadata;\n}\n", "type": "registry:file", "target": "components/commandly/types/nested.ts" }, { "path": "registry/commandly/utils/flat.ts", - "content": "import type { Command, Parameter, Tool } from \"@/components/commandly/types/flat\";\n\nexport const slugify = (text: string): string => {\n return text\n .toString()\n .toLowerCase()\n .trim()\n .replace(/\\s+/g, \"-\") // Replace spaces with -\n .replace(/[^\\w-]+/g, \"\") // Remove all non-word chars\n .replace(/--+/g, \"-\") // Replace multiple - with single -\n .replace(/^-+/, \"\") // Trim - from start of text\n .replace(/-+$/, \"\"); // Trim - from end of text\n};\n\nexport const getCommandPath = (command: Command, tool: Tool): string => {\n const findCommandPath = (\n targetKey: string,\n commands: Command[],\n path: string[] = [],\n ): string[] | null => {\n for (const cmd of commands) {\n if (cmd.name === targetKey) {\n return [...path, cmd.name];\n }\n\n const childCommands = commands.filter((c) => c.parentCommandKey === cmd.key);\n if (childCommands.length > 0) {\n const subPath = findCommandPath(targetKey, childCommands, [...path, cmd.name]);\n if (subPath) {\n return subPath;\n }\n }\n }\n return null;\n };\n\n const rootCommands = tool.commands.filter((c) => !c.parentCommandKey);\n const path = findCommandPath(command.name, rootCommands);\n\n if (!path) return command.name;\n\n if (command.name === tool.name && command.isDefault) {\n return tool.name;\n }\n\n const rootCommand = tool.commands.find((c) => c.name === tool.name);\n if (rootCommand?.isDefault && path[0] === tool.name) {\n path[0] = tool.name;\n }\n\n return path.join(\" \");\n};\n\nexport const getAllSubcommands = (commandKey: string, commands: Command[]): Command[] => {\n const result: Command[] = [];\n\n const findSubcommands = (parentKey: string) => {\n commands.forEach((cmd) => {\n if (cmd.parentCommandKey === parentKey) {\n result.push(cmd);\n findSubcommands(cmd.key);\n }\n });\n };\n\n findSubcommands(commandKey);\n return result;\n};\n\nconst SCHEMA_URL = \"https://commandly.divyeshio.in/specification/flat.json\";\n\nexport const sanitizeToolJSON = (tool: Tool) => {\n const parameters = tool.parameters.map(({ metadata: _metadata, ...param }) => param);\n\n return {\n $schema: SCHEMA_URL,\n ...tool,\n parameters,\n };\n};\n\nexport const exportToStructuredJSON = (tool: Tool) => {\n return {\n $schema: SCHEMA_URL,\n name: tool.name,\n displayName: tool.displayName,\n info: tool.info,\n url: tool.info?.url,\n commands: tool.commands.map((cmd) => ({ ...cmd })),\n parameters: tool.parameters.map(({ metadata: _metadata, ...param }) => param),\n exclusionGroups: tool.exclusionGroups,\n metadata: tool.metadata,\n };\n};\n\nexport const createNewCommand = (parentKey?: string): Command => {\n const name = randomCommandName();\n return {\n key: slugify(name),\n parentCommandKey: parentKey,\n name,\n isDefault: false,\n sortOrder: 1,\n };\n};\n\nexport const createNewParameter = (isGlobal: boolean, commandKey?: string): Parameter => {\n return {\n key: \"\",\n name: \"\",\n commandKey: isGlobal ? undefined : commandKey,\n parameterType: \"Option\",\n dataType: \"String\",\n ...(isGlobal ? { isGlobal: true } : {}),\n longFlag: \"\",\n };\n};\n\nexport const randomCommandName = () => {\n const characters = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\";\n let result = \"\";\n const charactersLength = characters.length;\n for (let i = 0; i < 7; i++) {\n result += characters.charAt(Math.floor(Math.random() * charactersLength));\n }\n return result;\n};\n\nconst isEmpty = (value: object | null | undefined): boolean => {\n if (value == null) return true;\n if (Array.isArray(value)) return value.length === 0;\n return Object.keys(value).length === 0;\n};\n\nconst cleanParameter = (param: Parameter): Parameter => {\n const cleaned = { ...param };\n\n if (!cleaned.enum || cleaned.enum.values.length === 0) delete cleaned.enum;\n if (isEmpty(cleaned.validations)) delete cleaned.validations;\n if (isEmpty(cleaned.dependencies)) delete cleaned.dependencies;\n\n if (cleaned.metadata) {\n const meta = { ...cleaned.metadata };\n if (isEmpty(meta.tags)) delete meta.tags;\n if (isEmpty(meta)) {\n delete cleaned.metadata;\n } else {\n cleaned.metadata = meta;\n }\n }\n\n return cleaned;\n};\n\nexport const cleanupTool = (tool: Tool): Tool => {\n const cleaned = { ...tool };\n\n if (isEmpty(cleaned.exclusionGroups)) delete cleaned.exclusionGroups;\n if (isEmpty(cleaned.metadata)) delete cleaned.metadata;\n\n cleaned.parameters = cleaned.parameters.map(cleanParameter);\n\n return cleaned;\n};\n", + "content": "import type { Command, Parameter, Tool } from \"@/components/commandly/types/flat\";\n\nexport const slugify = (text: string): string => {\n return text\n .toString()\n .toLowerCase()\n .trim()\n .replace(/\\s+/g, \"-\") // Replace spaces with -\n .replace(/[^\\w-]+/g, \"\") // Remove all non-word chars\n .replace(/--+/g, \"-\") // Replace multiple - with single -\n .replace(/^-+/, \"\") // Trim - from start of text\n .replace(/-+$/, \"\"); // Trim - from end of text\n};\n\nexport const getCommandPath = (command: Command, tool: Tool): string => {\n const findCommandPath = (\n targetKey: string,\n commands: Command[],\n path: string[] = [],\n ): string[] | null => {\n for (const cmd of commands) {\n if (cmd.name === targetKey) {\n return [...path, cmd.name];\n }\n\n const childCommands = commands.filter((c) => c.parentCommandKey === cmd.key);\n if (childCommands.length > 0) {\n const subPath = findCommandPath(targetKey, childCommands, [...path, cmd.name]);\n if (subPath) {\n return subPath;\n }\n }\n }\n return null;\n };\n\n const rootCommands = tool.commands.filter((c) => !c.parentCommandKey);\n const path = findCommandPath(command.name, rootCommands);\n\n if (!path) return command.name;\n\n return path.join(\" \");\n};\n\nexport const getAllSubcommands = (commandKey: string, commands: Command[]): Command[] => {\n const result: Command[] = [];\n\n const findSubcommands = (parentKey: string) => {\n commands.forEach((cmd) => {\n if (cmd.parentCommandKey === parentKey) {\n result.push(cmd);\n findSubcommands(cmd.key);\n }\n });\n };\n\n findSubcommands(commandKey);\n return result;\n};\n\nconst SCHEMA_URL = \"https://commandly.divyeshio.in/specification/flat.json\";\n\nexport const sanitizeToolJSON = (tool: Tool) => {\n const parameters = tool.parameters.map(({ metadata: _metadata, ...param }) => param);\n\n return {\n $schema: SCHEMA_URL,\n ...tool,\n parameters,\n };\n};\n\nexport const exportToStructuredJSON = (tool: Tool) => {\n return {\n $schema: SCHEMA_URL,\n name: tool.name,\n displayName: tool.displayName,\n info: tool.info,\n commands: tool.commands.map((cmd) => ({ ...cmd })),\n parameters: tool.parameters.map(({ metadata: _metadata, ...param }) => param),\n exclusionGroups: tool.exclusionGroups,\n metadata: tool.metadata,\n };\n};\n\nexport const createNewParameter = (isGlobal: boolean, commandKey?: string): Parameter => {\n return {\n key: \"\",\n name: \"\",\n commandKey: isGlobal ? undefined : commandKey,\n parameterType: \"Option\",\n dataType: \"String\",\n ...(isGlobal ? { isGlobal: true } : {}),\n longFlag: \"\",\n };\n};\n\nconst isEmpty = (value: object | null | undefined): boolean => {\n if (value == null) return true;\n if (Array.isArray(value)) return value.length === 0;\n return Object.keys(value).length === 0;\n};\n\nconst cleanParameter = (param: Parameter): Parameter => {\n const cleaned = { ...param };\n\n if (!cleaned.enum || cleaned.enum.values.length === 0) delete cleaned.enum;\n if (isEmpty(cleaned.validations)) delete cleaned.validations;\n if (isEmpty(cleaned.dependencies)) delete cleaned.dependencies;\n\n if (cleaned.metadata) {\n const meta = { ...cleaned.metadata };\n if (isEmpty(meta.tags)) delete meta.tags;\n if (isEmpty(meta)) {\n delete cleaned.metadata;\n } else {\n cleaned.metadata = meta;\n }\n }\n\n return cleaned;\n};\n\nexport const cleanupTool = (tool: Tool): Tool => {\n const cleaned = { ...tool };\n\n if (isEmpty(cleaned.exclusionGroups)) delete cleaned.exclusionGroups;\n if (isEmpty(cleaned.metadata)) delete cleaned.metadata;\n\n cleaned.parameters = cleaned.parameters.map(cleanParameter);\n\n return cleaned;\n};\n", "type": "registry:file", "target": "components/commandly/utils/flat.ts" }, { "path": "registry/commandly/utils/nested.ts", - "content": "import type { Tool, Command, Parameter } from \"@/components/commandly/types/flat\";\nimport {\n NestedCommand,\n NestedExclusionGroup,\n NestedParameter,\n NestedTool,\n} from \"@/components/commandly/types/nested\";\n\nexport const convertToNestedStructure = (tool: Tool): NestedTool => {\n const globalParameters = tool.parameters.filter((p) => p.isGlobal);\n\n const convertParameter = (param: Parameter): NestedParameter => {\n const { ...rest } = param;\n return {\n ...rest,\n validations: param.validations?.map((v) => {\n return {\n validationType: v.validationType,\n validationValue: v.validationValue,\n errorMessage: v.errorMessage,\n };\n }),\n metadata: param.metadata,\n dataType: param.dataType,\n dependencies: param.dependencies?.map((dep) => {\n const dependsOnParam = tool.parameters.find((p) => p.key === dep.dependsOnParameterKey);\n return {\n dependsOnParameter: dependsOnParam?.longFlag || \"\",\n dependencyType: dep.dependencyType,\n conditionValue: dep.conditionValue,\n };\n }),\n };\n };\n\n const buildNestedCommands = (commands: Command[], parentKey?: string): NestedCommand[] => {\n return commands\n .filter((cmd) => cmd.parentCommandKey === parentKey)\n .map((cmd) => {\n const commandParameters = tool.parameters.filter(\n (p) => p.commandKey === cmd.key && !p.isGlobal,\n );\n return {\n name: cmd.name,\n description: cmd.description,\n interactive: cmd.interactive,\n isDefault: cmd.isDefault ?? false,\n sortOrder: cmd.sortOrder ?? 0,\n parameters: commandParameters.map(convertParameter),\n subcommands: buildNestedCommands(commands, cmd.key),\n };\n });\n };\n\n const nestedExclusionGroups: NestedExclusionGroup[] | undefined = tool.exclusionGroups?.map(\n (group) => {\n return {\n name: group.name,\n exclusionType: group.exclusionType,\n parameters: group.parameterKeys.map((pk) => {\n const param = tool.parameters.find((p) => p.key === pk);\n return param?.longFlag || \"\";\n }),\n };\n },\n );\n\n return {\n $schema: \"https://commandly.divyeshio.in/specification/nested.json\",\n name: tool.name,\n url: tool.info?.url,\n displayName: tool.displayName,\n info: tool.info,\n metadata: tool.metadata,\n globalParameters: globalParameters.map(convertParameter),\n commands: buildNestedCommands(tool.commands),\n exclusionGroups: nestedExclusionGroups,\n };\n};\n", + "content": "import type { Tool, Command, Parameter } from \"@/components/commandly/types/flat\";\nimport {\n NestedCommand,\n NestedExclusionGroup,\n NestedParameter,\n NestedTool,\n} from \"@/components/commandly/types/nested\";\n\nexport const convertToNestedStructure = (tool: Tool): NestedTool => {\n const globalParameters = tool.parameters.filter((p) => p.isGlobal);\n\n const convertParameter = (param: Parameter): NestedParameter => {\n const { ...rest } = param;\n return {\n ...rest,\n validations: param.validations?.map((v) => {\n return {\n validationType: v.validationType,\n validationValue: v.validationValue,\n errorMessage: v.errorMessage,\n };\n }),\n metadata: param.metadata,\n dataType: param.dataType,\n dependencies: param.dependencies?.map((dep) => {\n const dependsOnParam = tool.parameters.find((p) => p.key === dep.dependsOnParameterKey);\n return {\n dependsOnParameter: dependsOnParam?.longFlag || \"\",\n dependencyType: dep.dependencyType,\n conditionValue: dep.conditionValue,\n };\n }),\n };\n };\n\n const buildNestedCommands = (commands: Command[], parentKey?: string): NestedCommand[] => {\n return commands\n .filter((cmd) => cmd.parentCommandKey === parentKey)\n .map((cmd) => {\n const commandParameters = tool.parameters.filter(\n (p) => p.commandKey === cmd.key && !p.isGlobal,\n );\n return {\n name: cmd.name,\n description: cmd.description,\n interactive: cmd.interactive,\n sortOrder: cmd.sortOrder ?? 0,\n parameters: commandParameters.map(convertParameter),\n subcommands: buildNestedCommands(commands, cmd.key),\n };\n });\n };\n\n const nestedExclusionGroups: NestedExclusionGroup[] | undefined = tool.exclusionGroups?.map(\n (group) => {\n return {\n name: group.name,\n exclusionType: group.exclusionType,\n parameters: group.parameterKeys.map((pk) => {\n const param = tool.parameters.find((p) => p.key === pk);\n return param?.longFlag || \"\";\n }),\n };\n },\n );\n\n return {\n $schema: \"https://commandly.divyeshio.in/specification/nested.json\",\n name: tool.name,\n url: tool.info?.url,\n displayName: tool.displayName,\n info: tool.info,\n metadata: tool.metadata,\n globalParameters: globalParameters.map(convertParameter),\n commands: buildNestedCommands(tool.commands),\n exclusionGroups: nestedExclusionGroups,\n };\n};\n", "type": "registry:file", "target": "components/commandly/utils/nested.ts" } diff --git a/public/r/json-output.json b/public/r/json-output.json index 93f7e55..d7340ba 100644 --- a/public/r/json-output.json +++ b/public/r/json-output.json @@ -13,31 +13,31 @@ "files": [ { "path": "registry/commandly/json-output.tsx", - "content": "import { Tool } from \"@/components/commandly/types/flat\";\nimport { exportToStructuredJSON } from \"@/components/commandly/utils/flat\";\nimport { convertToNestedStructure } from \"@/components/commandly/utils/nested\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardAction, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport {\n Command as UICommand,\n CommandGroup,\n CommandItem,\n CommandList,\n} from \"@/components/ui/command\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\";\nimport { ScrollArea, ScrollBar } from \"@/components/ui/scroll-area\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { cn } from \"@/lib/utils\";\nimport { CheckIcon, ChevronsUpDownIcon, CopyIcon, Edit2Icon, XIcon } from \"lucide-react\";\nimport { useEffect, useState } from \"react\";\nimport { toast } from \"sonner\";\n\nconst jsonOptions = [\n { value: \"nested\", label: \"Nested\" },\n { value: \"flat\", label: \"Flat\" },\n];\n\ninterface JsonTypeComponentProps {\n tool: Tool;\n onApply?: (tool: Tool) => void;\n}\n\nexport function JsonOutput({ tool, onApply }: JsonTypeComponentProps) {\n const [open, setOpen] = useState(false);\n const [jsonString, setJsonString] = useState();\n const [jsonType, setJsonType] = useState<\"nested\" | \"flat\">(\"flat\");\n const [isEditing, setIsEditing] = useState(false);\n const [editValue, setEditValue] = useState(\"\");\n\n useEffect(() => {\n const config =\n jsonType === \"flat\" ? exportToStructuredJSON(tool) : convertToNestedStructure(tool);\n setJsonString(JSON.stringify(config, null, 2));\n }, [jsonType, tool]);\n\n const handleEditToggle = () => {\n setEditValue(jsonString ?? \"\");\n setIsEditing(true);\n };\n\n const handleApply = () => {\n try {\n const parsed = JSON.parse(editValue) as Tool;\n onApply!(parsed);\n setIsEditing(false);\n } catch {\n toast.error(\"Invalid JSON\", { description: \"Please fix the JSON before applying.\" });\n }\n };\n\n const handleCancel = () => {\n setIsEditing(false);\n setEditValue(\"\");\n };\n\n return (\n \n \n \n Output type: \n \n \n \n {jsonOptions.find((option) => option.value === jsonType)?.label}\n \n \n \n \n \n \n \n {jsonOptions.map((option) => (\n {\n setJsonType(currentValue as \"nested\" | \"flat\");\n setOpen(false);\n }}\n >\n {option.label}\n \n \n ))}\n \n \n \n \n \n \n
\n {onApply && !isEditing && (\n \n \n \n )}\n {onApply && isEditing && (\n \n \n \n )}\n {\n navigator.clipboard.writeText(jsonString!);\n toast(\"Copied!\");\n }}\n >\n \n \n
\n
\n \n {isEditing ? (\n
\n \n setEditValue(e.target.value)}\n spellCheck={false}\n />\n \n \n \n
\n \n Cancel\n \n \n Apply\n \n
\n
\n ) : (\n \n
\n              {jsonString}\n            
\n \n \n \n )}\n
\n
\n );\n}\n", + "content": "import { Tool } from \"@/components/commandly/types/flat\";\nimport { exportToStructuredJSON } from \"@/components/commandly/utils/flat\";\nimport { convertToNestedStructure } from \"@/components/commandly/utils/nested\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardAction, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport {\n Command as UICommand,\n CommandGroup,\n CommandItem,\n CommandList,\n} from \"@/components/ui/command\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\";\nimport { ScrollArea, ScrollBar } from \"@/components/ui/scroll-area\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { cn } from \"@/lib/utils\";\nimport { CheckIcon, ChevronsUpDownIcon, CopyIcon, Edit2Icon, XIcon } from \"lucide-react\";\nimport { useEffect, useMemo, useState } from \"react\";\nimport { toast } from \"sonner\";\n\nconst jsonOptions = [\n { value: \"nested\", label: \"Nested\" },\n { value: \"flat\", label: \"Flat\" },\n];\n\ntype DiffLine = { type: \"same\" | \"added\" | \"removed\"; text: string };\n\nfunction diffLines(before: string, after: string): DiffLine[] {\n const a = before.split(\"\\n\");\n const b = after.split(\"\\n\");\n const m = a.length;\n const n = b.length;\n const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));\n for (let i = 1; i <= m; i++)\n for (let j = 1; j <= n; j++)\n dp[i][j] =\n a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] + 1 : Math.max(dp[i - 1][j], dp[i][j - 1]);\n const result: DiffLine[] = [];\n let i = m,\n j = n;\n while (i > 0 || j > 0) {\n if (i > 0 && j > 0 && a[i - 1] === b[j - 1]) {\n result.unshift({ type: \"same\", text: a[i - 1] });\n i--;\n j--;\n } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {\n result.unshift({ type: \"added\", text: b[j - 1] });\n j--;\n } else {\n result.unshift({ type: \"removed\", text: a[i - 1] });\n i--;\n }\n }\n return result;\n}\n\ninterface JsonTypeComponentProps {\n tool: Tool;\n originalTool?: Tool;\n onApply?: (tool: Tool) => void;\n}\n\nexport function JsonOutput({ tool, originalTool, onApply }: JsonTypeComponentProps) {\n const [open, setOpen] = useState(false);\n const [jsonString, setJsonString] = useState();\n const [originalJsonString, setOriginalJsonString] = useState();\n const [jsonType, setJsonType] = useState<\"nested\" | \"flat\">(\"flat\");\n const [isEditing, setIsEditing] = useState(false);\n const [editValue, setEditValue] = useState(\"\");\n const [showDiff, setShowDiff] = useState(true);\n\n useEffect(() => {\n const config =\n jsonType === \"flat\" ? exportToStructuredJSON(tool) : convertToNestedStructure(tool);\n setJsonString(JSON.stringify(config, null, 2));\n }, [jsonType, tool]);\n\n useEffect(() => {\n if (!originalTool) {\n setOriginalJsonString(undefined);\n return;\n }\n const config =\n jsonType === \"flat\"\n ? exportToStructuredJSON(originalTool)\n : convertToNestedStructure(originalTool);\n setOriginalJsonString(JSON.stringify(config, null, 2));\n }, [jsonType, originalTool]);\n\n const diff = useMemo(() => {\n if (!originalJsonString || !jsonString || originalJsonString === jsonString) return null;\n return diffLines(originalJsonString, jsonString);\n }, [originalJsonString, jsonString]);\n\n const diffStats = useMemo(() => {\n if (!diff) return null;\n const added = diff.filter((l) => l.type === \"added\").length;\n const removed = diff.filter((l) => l.type === \"removed\").length;\n return { added, removed };\n }, [diff]);\n\n const handleEditToggle = () => {\n setEditValue(jsonString ?? \"\");\n setIsEditing(true);\n };\n\n const handleApply = () => {\n try {\n const parsed = JSON.parse(editValue) as Tool;\n onApply!(parsed);\n setIsEditing(false);\n } catch {\n toast.error(\"Invalid JSON\", { description: \"Please fix the JSON before applying.\" });\n }\n };\n\n const handleCancel = () => {\n setIsEditing(false);\n setEditValue(\"\");\n };\n\n return (\n \n \n \n Output type: \n \n \n \n {jsonOptions.find((option) => option.value === jsonType)?.label}\n \n \n \n \n \n \n \n {jsonOptions.map((option) => (\n {\n setJsonType(currentValue as \"nested\" | \"flat\");\n setOpen(false);\n }}\n >\n {option.label}\n \n \n ))}\n \n \n \n \n \n \n
\n {onApply && !isEditing && (\n \n \n \n )}\n {onApply && isEditing && (\n \n \n \n )}\n {\n navigator.clipboard.writeText(jsonString!);\n toast(\"Copied!\");\n }}\n >\n \n \n
\n
\n \n {diffStats && !isEditing && (\n
\n {diffStats.added > 0 && (\n \n +{diffStats.added} added\n \n )}\n {diffStats.removed > 0 && (\n \n -{diffStats.removed} removed\n \n )}\n setShowDiff((v) => !v)}\n >\n {showDiff ? \"Full view\" : \"Diff view\"}\n \n
\n )}\n {isEditing ? (\n
\n \n setEditValue(e.target.value)}\n spellCheck={false}\n />\n \n \n \n
\n \n Cancel\n \n \n Apply\n \n
\n
\n ) : diff && showDiff ? (\n \n
\n              {diff.map((line, idx) => (\n                \n                  \n                    {line.type === \"added\" ? \"+ \" : line.type === \"removed\" ? \"- \" : \"  \"}\n                  \n                  {line.text}\n                \n              ))}\n            
\n \n \n \n ) : (\n \n
\n              {jsonString}\n            
\n \n \n \n )}\n
\n
\n );\n}\n", "type": "registry:component", "target": "components/commandly/json-output.tsx" }, { "path": "registry/commandly/types/flat.ts", - "content": "export interface ToolInfo {\n description?: string;\n version?: string;\n url?: string;\n}\n\nexport interface Command {\n key: string;\n parentCommandKey?: string;\n name: string;\n description?: string;\n interactive?: boolean;\n isDefault?: boolean;\n sortOrder?: number;\n}\n\nexport interface ParameterEnumValue {\n value: string;\n displayName: string;\n description?: string;\n isDefault?: boolean;\n sortOrder?: number;\n}\n\nexport interface ParameterEnumValues {\n values: ParameterEnumValue[];\n allowMultiple?: boolean;\n separator?: string;\n}\n\nexport type ParameterValidationType =\n | \"min_length\"\n | \"max_length\"\n | \"min_value\"\n | \"max_value\"\n | \"regex\";\n\nexport interface ParameterValidation {\n key: string;\n validationType: ParameterValidationType;\n validationValue: string;\n errorMessage: string;\n}\n\nexport type ParameterDependencyType = \"requires\" | \"conflicts_with\";\n\nexport interface ParameterDependency {\n key: string;\n parameterKey: string;\n dependsOnParameterKey: string;\n dependencyType: ParameterDependencyType;\n conditionValue?: string;\n}\n\nexport type ParameterValue = string | number | boolean | string[];\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface ToolMetadata {}\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface ParameterMetadata {\n tags?: string[];\n}\n\nexport type ParameterType = \"Flag\" | \"Option\" | \"Argument\";\n\nexport type ParameterDataType = \"String\" | \"Number\" | \"Boolean\" | \"Enum\";\n\nexport interface Parameter {\n key: string;\n name: string;\n commandKey?: string;\n description?: string;\n group?: string;\n metadata?: ParameterMetadata;\n parameterType: ParameterType;\n dataType: ParameterDataType;\n isRequired?: boolean;\n isRepeatable?: boolean;\n isGlobal?: boolean;\n shortFlag?: string;\n longFlag?: string;\n position?: number;\n sortOrder?: number;\n arraySeparator?: string;\n keyValueSeparator?: string;\n enum?: ParameterEnumValues;\n validations?: ParameterValidation[];\n dependencies?: ParameterDependency[];\n}\n\nexport type ExclusionType = \"mutual_exclusive\" | \"required_one_of\";\n\nexport interface ExclusionGroup {\n key?: string;\n commandKey?: string;\n name: string;\n exclusionType: ExclusionType;\n parameterKeys: string[];\n}\n\nexport interface Tool {\n name: string;\n displayName: string;\n info?: ToolInfo;\n commands: Command[];\n parameters: Parameter[];\n exclusionGroups?: ExclusionGroup[];\n metadata?: ToolMetadata;\n}\n", + "content": "export interface ToolInfo {\n /** A brief human-readable description of what the tool does. */\n description?: string;\n /** The version string of the tool (e.g. \"1.0.0\"). */\n version?: string;\n /** The homepage or documentation URL for the tool. */\n url?: string;\n}\n\nexport interface Command {\n /** Unique identifier for this command within the tool. */\n key: string;\n /** Key of the parent command; used to represent subcommand nesting. */\n parentCommandKey?: string;\n /** Human-readable display name of the command. */\n name: string;\n /** Brief description of what this command does. */\n description?: string;\n /** Whether this command opens an interactive session or prompt. */\n interactive?: boolean;\n /** Display sort position relative to sibling commands. */\n sortOrder?: number;\n}\n\nexport interface ParameterEnumValue {\n /** The raw value passed to the CLI for this choice. */\n value: string;\n /** Human-readable label shown to the user for this enum choice. */\n displayName: string;\n /** Description of what this enum value does or represents. */\n description?: string;\n /** Whether this is the default selection when no value is provided. */\n isDefault?: boolean;\n /** Display sort position relative to sibling enum values. */\n sortOrder?: number;\n}\n\nexport interface ParameterEnumValues {\n /** The list of allowed enum choices. */\n values: ParameterEnumValue[];\n /** Whether the user can select multiple values at once. */\n allowMultiple?: boolean;\n /** Separator character used when joining multiple selected values. */\n separator?: string;\n}\n\nexport type ParameterValidationType =\n | \"min_length\"\n | \"max_length\"\n | \"min_value\"\n | \"max_value\"\n | \"regex\";\n\nexport interface ParameterValidation {\n /** Unique identifier for this validation rule. */\n key: string;\n /** The type of validation to apply. */\n validationType: ParameterValidationType;\n /** The value to validate against (e.g. the max length number, or a regex pattern). */\n validationValue: string;\n /** The error message to display when validation fails. */\n errorMessage: string;\n}\n\nexport type ParameterDependencyType = \"requires\" | \"conflicts_with\";\n\nexport interface ParameterDependency {\n /** Unique identifier for this dependency rule. */\n key: string;\n /** Key of the parameter that owns this dependency. */\n parameterKey: string;\n /** Key of the parameter this dependency references. */\n dependsOnParameterKey: string;\n /** Whether this parameter requires or conflicts with the referenced parameter. */\n dependencyType: ParameterDependencyType;\n /** Optional value that the referenced parameter must have for this dependency to apply. */\n conditionValue?: string;\n}\n\nexport type ParameterValue = string | number | boolean | string[];\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface ToolMetadata {}\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface ParameterMetadata {\n /** Arbitrary tags for categorising or filtering parameters. */\n tags?: string[];\n}\n\nexport type ParameterType = \"Flag\" | \"Option\" | \"Argument\";\n\nexport type ParameterDataType = \"String\" | \"Number\" | \"Boolean\" | \"Enum\";\n\nexport interface Parameter {\n /** Unique identifier for this parameter within the tool. */\n key: string;\n /** Human-readable display name of the parameter. */\n name: string;\n /** Key of the command this parameter belongs to; omit for global parameters. */\n commandKey?: string;\n /** Brief description of what this parameter does or accepts. */\n description?: string;\n /** Optional grouping label for organising related parameters in the UI. */\n group?: string;\n /** Additional metadata such as tags. */\n metadata?: ParameterMetadata;\n /** Whether this is a boolean flag, a key-value option, or a positional argument. */\n parameterType: ParameterType;\n /** The data type of the parameter's value. */\n dataType: ParameterDataType;\n /** Whether the user must provide this parameter. */\n isRequired?: boolean;\n /** Whether this parameter can be specified multiple times. */\n isRepeatable?: boolean;\n /** Whether this parameter applies to all commands rather than a single command. */\n isGlobal?: boolean;\n /** The single-character short flag (e.g. \"-v\"). */\n shortFlag?: string;\n /** The long-form flag or option name (e.g. \"--verbose\"). */\n longFlag?: string;\n /** Zero-based position index for positional arguments. */\n position?: number;\n /** Display sort position relative to sibling parameters. */\n sortOrder?: number;\n /** Separator character used when the option accepts multiple values in one argument (e.g. \",\"). */\n arraySeparator?: string;\n /** Separator between key and value for key=value style options (e.g. \"=\"). */\n keyValueSeparator?: string;\n /** Allowed enum choices when dataType is \"Enum\". */\n enum?: ParameterEnumValues;\n /** Validation rules applied to this parameter's value. */\n validations?: ParameterValidation[];\n /** Dependencies on other parameters (requires or conflicts-with relationships). */\n dependencies?: ParameterDependency[];\n}\n\nexport type ExclusionType = \"mutual_exclusive\" | \"required_one_of\";\n\nexport interface ExclusionGroup {\n /** Unique identifier for this exclusion group. */\n key?: string;\n /** Key of the command this exclusion group belongs to; omit for global groups. */\n commandKey?: string;\n /** Human-readable name for this exclusion group. */\n name: string;\n /** Whether parameters in this group are mutually exclusive or one is required. */\n exclusionType: ExclusionType;\n /** Keys of the parameters that participate in this exclusion group. */\n parameterKeys: string[];\n}\n\nexport interface Tool {\n /** Unique machine-readable identifier for the tool (e.g. \"httpx\"). */\n name: string;\n /** Human-readable display name for the tool (e.g. \"HTTPx\"). */\n displayName: string;\n /** General information about the tool such as description, version, and URL. */\n info?: ToolInfo;\n /** List of all commands and subcommands defined for this tool. */\n commands: Command[];\n /** Flat list of all parameters across all commands and global scope. */\n parameters: Parameter[];\n /** Groups of parameters with mutual exclusion or required-one-of constraints. */\n exclusionGroups?: ExclusionGroup[];\n /** Arbitrary metadata attached to the tool. */\n metadata?: ToolMetadata;\n}\n", "type": "registry:file", "target": "components/commandly/types/flat.ts" }, { "path": "registry/commandly/types/nested.ts", - "content": "import type {\n ExclusionType,\n ParameterDataType,\n ParameterDependencyType,\n ParameterEnumValues,\n ParameterMetadata,\n ParameterType,\n ParameterValidationType,\n ToolInfo,\n ToolMetadata,\n} from \"@/components/commandly/types/flat\";\n\nexport interface NestedParameterValidation {\n validationType: ParameterValidationType;\n validationValue: string;\n errorMessage: string;\n}\n\nexport interface NestedParameterDependency {\n dependsOnParameter: string;\n dependencyType: ParameterDependencyType;\n conditionValue?: string;\n}\n\nexport interface NestedParameter {\n name: string;\n description?: string;\n group?: string;\n parameterType: ParameterType;\n dataType: ParameterDataType;\n metadata?: ParameterMetadata;\n isRequired?: boolean;\n isRepeatable?: boolean;\n isGlobal?: boolean;\n shortFlag?: string;\n longFlag?: string;\n position?: number;\n sortOrder?: number;\n arraySeparator?: string;\n keyValueSeparator?: string;\n enum?: ParameterEnumValues;\n validations?: NestedParameterValidation[];\n dependencies?: NestedParameterDependency[];\n}\n\nexport interface NestedCommand {\n name: string;\n description?: string;\n interactive?: boolean;\n isDefault: boolean;\n sortOrder: number;\n parameters: NestedParameter[];\n subcommands: NestedCommand[];\n}\n\nexport interface NestedExclusionGroup {\n name: string;\n exclusionType: ExclusionType;\n parameters: string[];\n}\n\nexport interface NestedTool {\n $schema?: string;\n name: string;\n displayName: string;\n info?: ToolInfo;\n url?: string;\n globalParameters: NestedParameter[];\n commands: NestedCommand[];\n exclusionGroups?: NestedExclusionGroup[] | null;\n metadata?: ToolMetadata;\n}\n", + "content": "import type {\n ExclusionType,\n ParameterDataType,\n ParameterDependencyType,\n ParameterEnumValues,\n ParameterMetadata,\n ParameterType,\n ParameterValidationType,\n ToolInfo,\n ToolMetadata,\n} from \"@/components/commandly/types/flat\";\n\nexport interface NestedParameterValidation {\n /** The type of validation to apply. */\n validationType: ParameterValidationType;\n /** The value to validate against (e.g. max length number or regex pattern). */\n validationValue: string;\n /** The error message to display when validation fails. */\n errorMessage: string;\n}\n\nexport interface NestedParameterDependency {\n /** Name of the parameter this dependency references. */\n dependsOnParameter: string;\n /** Whether this parameter requires or conflicts with the referenced parameter. */\n dependencyType: ParameterDependencyType;\n /** Optional value that the referenced parameter must have for this dependency to apply. */\n conditionValue?: string;\n}\n\nexport interface NestedParameter {\n /** Human-readable display name of the parameter. */\n name: string;\n /** Brief description of what this parameter does or accepts. */\n description?: string;\n /** Optional grouping label for organising related parameters in the UI. */\n group?: string;\n /** Whether this is a boolean flag, a key-value option, or a positional argument. */\n parameterType: ParameterType;\n /** The data type of the parameter's value. */\n dataType: ParameterDataType;\n /** Additional metadata such as tags. */\n metadata?: ParameterMetadata;\n /** Whether the user must provide this parameter. */\n isRequired?: boolean;\n /** Whether this parameter can be specified multiple times. */\n isRepeatable?: boolean;\n /** Whether this parameter applies to all commands rather than a single command. */\n isGlobal?: boolean;\n /** The single-character short flag (e.g. \"-v\"). */\n shortFlag?: string;\n /** The long-form flag or option name (e.g. \"--verbose\"). */\n longFlag?: string;\n /** Zero-based position index for positional arguments. */\n position?: number;\n /** Display sort position relative to sibling parameters. */\n sortOrder?: number;\n /** Separator character used when the option accepts multiple values in one argument (e.g. \",\"). */\n arraySeparator?: string;\n /** Separator between key and value for key=value style options (e.g. \"=\"). */\n keyValueSeparator?: string;\n /** Allowed enum choices when dataType is \"Enum\". */\n enum?: ParameterEnumValues;\n /** Validation rules applied to this parameter's value. */\n validations?: NestedParameterValidation[];\n /** Dependencies on other parameters (requires or conflicts-with relationships). */\n dependencies?: NestedParameterDependency[];\n}\n\nexport interface NestedCommand {\n /** Human-readable display name of the command. */\n name: string;\n /** Brief description of what this command does. */\n description?: string;\n /** Whether this command opens an interactive session or prompt. */\n interactive?: boolean;\n /** Display sort position relative to sibling commands. */\n sortOrder: number;\n /** Parameters that belong directly to this command. */\n parameters: NestedParameter[];\n /** Nested subcommands of this command. */\n subcommands: NestedCommand[];\n}\n\nexport interface NestedExclusionGroup {\n /** Human-readable name for this exclusion group. */\n name: string;\n /** Whether parameters in this group are mutually exclusive or one is required. */\n exclusionType: ExclusionType;\n /** Names of the parameters that participate in this exclusion group. */\n parameters: string[];\n}\n\nexport interface NestedTool {\n /** Optional JSON schema URI for validation. */\n $schema?: string;\n /** Unique machine-readable identifier for the tool (e.g. \"httpx\"). */\n name: string;\n /** Human-readable display name for the tool (e.g. \"HTTPx\"). */\n displayName: string;\n /** General information about the tool such as description, version, and URL. */\n info?: ToolInfo;\n /** The homepage or documentation URL for the tool. */\n url?: string;\n /** Parameters that apply to all commands globally. */\n globalParameters: NestedParameter[];\n /** Hierarchical list of commands and their nested subcommands. */\n commands: NestedCommand[];\n /** Groups of parameters with mutual exclusion or required-one-of constraints. */\n exclusionGroups?: NestedExclusionGroup[] | null;\n /** Arbitrary metadata attached to the tool. */\n metadata?: ToolMetadata;\n}\n", "type": "registry:file", "target": "components/commandly/types/nested.ts" }, { "path": "registry/commandly/utils/flat.ts", - "content": "import type { Command, Parameter, Tool } from \"@/components/commandly/types/flat\";\n\nexport const slugify = (text: string): string => {\n return text\n .toString()\n .toLowerCase()\n .trim()\n .replace(/\\s+/g, \"-\") // Replace spaces with -\n .replace(/[^\\w-]+/g, \"\") // Remove all non-word chars\n .replace(/--+/g, \"-\") // Replace multiple - with single -\n .replace(/^-+/, \"\") // Trim - from start of text\n .replace(/-+$/, \"\"); // Trim - from end of text\n};\n\nexport const getCommandPath = (command: Command, tool: Tool): string => {\n const findCommandPath = (\n targetKey: string,\n commands: Command[],\n path: string[] = [],\n ): string[] | null => {\n for (const cmd of commands) {\n if (cmd.name === targetKey) {\n return [...path, cmd.name];\n }\n\n const childCommands = commands.filter((c) => c.parentCommandKey === cmd.key);\n if (childCommands.length > 0) {\n const subPath = findCommandPath(targetKey, childCommands, [...path, cmd.name]);\n if (subPath) {\n return subPath;\n }\n }\n }\n return null;\n };\n\n const rootCommands = tool.commands.filter((c) => !c.parentCommandKey);\n const path = findCommandPath(command.name, rootCommands);\n\n if (!path) return command.name;\n\n if (command.name === tool.name && command.isDefault) {\n return tool.name;\n }\n\n const rootCommand = tool.commands.find((c) => c.name === tool.name);\n if (rootCommand?.isDefault && path[0] === tool.name) {\n path[0] = tool.name;\n }\n\n return path.join(\" \");\n};\n\nexport const getAllSubcommands = (commandKey: string, commands: Command[]): Command[] => {\n const result: Command[] = [];\n\n const findSubcommands = (parentKey: string) => {\n commands.forEach((cmd) => {\n if (cmd.parentCommandKey === parentKey) {\n result.push(cmd);\n findSubcommands(cmd.key);\n }\n });\n };\n\n findSubcommands(commandKey);\n return result;\n};\n\nconst SCHEMA_URL = \"https://commandly.divyeshio.in/specification/flat.json\";\n\nexport const sanitizeToolJSON = (tool: Tool) => {\n const parameters = tool.parameters.map(({ metadata: _metadata, ...param }) => param);\n\n return {\n $schema: SCHEMA_URL,\n ...tool,\n parameters,\n };\n};\n\nexport const exportToStructuredJSON = (tool: Tool) => {\n return {\n $schema: SCHEMA_URL,\n name: tool.name,\n displayName: tool.displayName,\n info: tool.info,\n url: tool.info?.url,\n commands: tool.commands.map((cmd) => ({ ...cmd })),\n parameters: tool.parameters.map(({ metadata: _metadata, ...param }) => param),\n exclusionGroups: tool.exclusionGroups,\n metadata: tool.metadata,\n };\n};\n\nexport const createNewCommand = (parentKey?: string): Command => {\n const name = randomCommandName();\n return {\n key: slugify(name),\n parentCommandKey: parentKey,\n name,\n isDefault: false,\n sortOrder: 1,\n };\n};\n\nexport const createNewParameter = (isGlobal: boolean, commandKey?: string): Parameter => {\n return {\n key: \"\",\n name: \"\",\n commandKey: isGlobal ? undefined : commandKey,\n parameterType: \"Option\",\n dataType: \"String\",\n ...(isGlobal ? { isGlobal: true } : {}),\n longFlag: \"\",\n };\n};\n\nexport const randomCommandName = () => {\n const characters = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\";\n let result = \"\";\n const charactersLength = characters.length;\n for (let i = 0; i < 7; i++) {\n result += characters.charAt(Math.floor(Math.random() * charactersLength));\n }\n return result;\n};\n\nconst isEmpty = (value: object | null | undefined): boolean => {\n if (value == null) return true;\n if (Array.isArray(value)) return value.length === 0;\n return Object.keys(value).length === 0;\n};\n\nconst cleanParameter = (param: Parameter): Parameter => {\n const cleaned = { ...param };\n\n if (!cleaned.enum || cleaned.enum.values.length === 0) delete cleaned.enum;\n if (isEmpty(cleaned.validations)) delete cleaned.validations;\n if (isEmpty(cleaned.dependencies)) delete cleaned.dependencies;\n\n if (cleaned.metadata) {\n const meta = { ...cleaned.metadata };\n if (isEmpty(meta.tags)) delete meta.tags;\n if (isEmpty(meta)) {\n delete cleaned.metadata;\n } else {\n cleaned.metadata = meta;\n }\n }\n\n return cleaned;\n};\n\nexport const cleanupTool = (tool: Tool): Tool => {\n const cleaned = { ...tool };\n\n if (isEmpty(cleaned.exclusionGroups)) delete cleaned.exclusionGroups;\n if (isEmpty(cleaned.metadata)) delete cleaned.metadata;\n\n cleaned.parameters = cleaned.parameters.map(cleanParameter);\n\n return cleaned;\n};\n", + "content": "import type { Command, Parameter, Tool } from \"@/components/commandly/types/flat\";\n\nexport const slugify = (text: string): string => {\n return text\n .toString()\n .toLowerCase()\n .trim()\n .replace(/\\s+/g, \"-\") // Replace spaces with -\n .replace(/[^\\w-]+/g, \"\") // Remove all non-word chars\n .replace(/--+/g, \"-\") // Replace multiple - with single -\n .replace(/^-+/, \"\") // Trim - from start of text\n .replace(/-+$/, \"\"); // Trim - from end of text\n};\n\nexport const getCommandPath = (command: Command, tool: Tool): string => {\n const findCommandPath = (\n targetKey: string,\n commands: Command[],\n path: string[] = [],\n ): string[] | null => {\n for (const cmd of commands) {\n if (cmd.name === targetKey) {\n return [...path, cmd.name];\n }\n\n const childCommands = commands.filter((c) => c.parentCommandKey === cmd.key);\n if (childCommands.length > 0) {\n const subPath = findCommandPath(targetKey, childCommands, [...path, cmd.name]);\n if (subPath) {\n return subPath;\n }\n }\n }\n return null;\n };\n\n const rootCommands = tool.commands.filter((c) => !c.parentCommandKey);\n const path = findCommandPath(command.name, rootCommands);\n\n if (!path) return command.name;\n\n return path.join(\" \");\n};\n\nexport const getAllSubcommands = (commandKey: string, commands: Command[]): Command[] => {\n const result: Command[] = [];\n\n const findSubcommands = (parentKey: string) => {\n commands.forEach((cmd) => {\n if (cmd.parentCommandKey === parentKey) {\n result.push(cmd);\n findSubcommands(cmd.key);\n }\n });\n };\n\n findSubcommands(commandKey);\n return result;\n};\n\nconst SCHEMA_URL = \"https://commandly.divyeshio.in/specification/flat.json\";\n\nexport const sanitizeToolJSON = (tool: Tool) => {\n const parameters = tool.parameters.map(({ metadata: _metadata, ...param }) => param);\n\n return {\n $schema: SCHEMA_URL,\n ...tool,\n parameters,\n };\n};\n\nexport const exportToStructuredJSON = (tool: Tool) => {\n return {\n $schema: SCHEMA_URL,\n name: tool.name,\n displayName: tool.displayName,\n info: tool.info,\n commands: tool.commands.map((cmd) => ({ ...cmd })),\n parameters: tool.parameters.map(({ metadata: _metadata, ...param }) => param),\n exclusionGroups: tool.exclusionGroups,\n metadata: tool.metadata,\n };\n};\n\nexport const createNewParameter = (isGlobal: boolean, commandKey?: string): Parameter => {\n return {\n key: \"\",\n name: \"\",\n commandKey: isGlobal ? undefined : commandKey,\n parameterType: \"Option\",\n dataType: \"String\",\n ...(isGlobal ? { isGlobal: true } : {}),\n longFlag: \"\",\n };\n};\n\nconst isEmpty = (value: object | null | undefined): boolean => {\n if (value == null) return true;\n if (Array.isArray(value)) return value.length === 0;\n return Object.keys(value).length === 0;\n};\n\nconst cleanParameter = (param: Parameter): Parameter => {\n const cleaned = { ...param };\n\n if (!cleaned.enum || cleaned.enum.values.length === 0) delete cleaned.enum;\n if (isEmpty(cleaned.validations)) delete cleaned.validations;\n if (isEmpty(cleaned.dependencies)) delete cleaned.dependencies;\n\n if (cleaned.metadata) {\n const meta = { ...cleaned.metadata };\n if (isEmpty(meta.tags)) delete meta.tags;\n if (isEmpty(meta)) {\n delete cleaned.metadata;\n } else {\n cleaned.metadata = meta;\n }\n }\n\n return cleaned;\n};\n\nexport const cleanupTool = (tool: Tool): Tool => {\n const cleaned = { ...tool };\n\n if (isEmpty(cleaned.exclusionGroups)) delete cleaned.exclusionGroups;\n if (isEmpty(cleaned.metadata)) delete cleaned.metadata;\n\n cleaned.parameters = cleaned.parameters.map(cleanParameter);\n\n return cleaned;\n};\n", "type": "registry:file", "target": "components/commandly/utils/flat.ts" }, { "path": "registry/commandly/utils/nested.ts", - "content": "import type { Tool, Command, Parameter } from \"@/components/commandly/types/flat\";\nimport {\n NestedCommand,\n NestedExclusionGroup,\n NestedParameter,\n NestedTool,\n} from \"@/components/commandly/types/nested\";\n\nexport const convertToNestedStructure = (tool: Tool): NestedTool => {\n const globalParameters = tool.parameters.filter((p) => p.isGlobal);\n\n const convertParameter = (param: Parameter): NestedParameter => {\n const { ...rest } = param;\n return {\n ...rest,\n validations: param.validations?.map((v) => {\n return {\n validationType: v.validationType,\n validationValue: v.validationValue,\n errorMessage: v.errorMessage,\n };\n }),\n metadata: param.metadata,\n dataType: param.dataType,\n dependencies: param.dependencies?.map((dep) => {\n const dependsOnParam = tool.parameters.find((p) => p.key === dep.dependsOnParameterKey);\n return {\n dependsOnParameter: dependsOnParam?.longFlag || \"\",\n dependencyType: dep.dependencyType,\n conditionValue: dep.conditionValue,\n };\n }),\n };\n };\n\n const buildNestedCommands = (commands: Command[], parentKey?: string): NestedCommand[] => {\n return commands\n .filter((cmd) => cmd.parentCommandKey === parentKey)\n .map((cmd) => {\n const commandParameters = tool.parameters.filter(\n (p) => p.commandKey === cmd.key && !p.isGlobal,\n );\n return {\n name: cmd.name,\n description: cmd.description,\n interactive: cmd.interactive,\n isDefault: cmd.isDefault ?? false,\n sortOrder: cmd.sortOrder ?? 0,\n parameters: commandParameters.map(convertParameter),\n subcommands: buildNestedCommands(commands, cmd.key),\n };\n });\n };\n\n const nestedExclusionGroups: NestedExclusionGroup[] | undefined = tool.exclusionGroups?.map(\n (group) => {\n return {\n name: group.name,\n exclusionType: group.exclusionType,\n parameters: group.parameterKeys.map((pk) => {\n const param = tool.parameters.find((p) => p.key === pk);\n return param?.longFlag || \"\";\n }),\n };\n },\n );\n\n return {\n $schema: \"https://commandly.divyeshio.in/specification/nested.json\",\n name: tool.name,\n url: tool.info?.url,\n displayName: tool.displayName,\n info: tool.info,\n metadata: tool.metadata,\n globalParameters: globalParameters.map(convertParameter),\n commands: buildNestedCommands(tool.commands),\n exclusionGroups: nestedExclusionGroups,\n };\n};\n", + "content": "import type { Tool, Command, Parameter } from \"@/components/commandly/types/flat\";\nimport {\n NestedCommand,\n NestedExclusionGroup,\n NestedParameter,\n NestedTool,\n} from \"@/components/commandly/types/nested\";\n\nexport const convertToNestedStructure = (tool: Tool): NestedTool => {\n const globalParameters = tool.parameters.filter((p) => p.isGlobal);\n\n const convertParameter = (param: Parameter): NestedParameter => {\n const { ...rest } = param;\n return {\n ...rest,\n validations: param.validations?.map((v) => {\n return {\n validationType: v.validationType,\n validationValue: v.validationValue,\n errorMessage: v.errorMessage,\n };\n }),\n metadata: param.metadata,\n dataType: param.dataType,\n dependencies: param.dependencies?.map((dep) => {\n const dependsOnParam = tool.parameters.find((p) => p.key === dep.dependsOnParameterKey);\n return {\n dependsOnParameter: dependsOnParam?.longFlag || \"\",\n dependencyType: dep.dependencyType,\n conditionValue: dep.conditionValue,\n };\n }),\n };\n };\n\n const buildNestedCommands = (commands: Command[], parentKey?: string): NestedCommand[] => {\n return commands\n .filter((cmd) => cmd.parentCommandKey === parentKey)\n .map((cmd) => {\n const commandParameters = tool.parameters.filter(\n (p) => p.commandKey === cmd.key && !p.isGlobal,\n );\n return {\n name: cmd.name,\n description: cmd.description,\n interactive: cmd.interactive,\n sortOrder: cmd.sortOrder ?? 0,\n parameters: commandParameters.map(convertParameter),\n subcommands: buildNestedCommands(commands, cmd.key),\n };\n });\n };\n\n const nestedExclusionGroups: NestedExclusionGroup[] | undefined = tool.exclusionGroups?.map(\n (group) => {\n return {\n name: group.name,\n exclusionType: group.exclusionType,\n parameters: group.parameterKeys.map((pk) => {\n const param = tool.parameters.find((p) => p.key === pk);\n return param?.longFlag || \"\";\n }),\n };\n },\n );\n\n return {\n $schema: \"https://commandly.divyeshio.in/specification/nested.json\",\n name: tool.name,\n url: tool.info?.url,\n displayName: tool.displayName,\n info: tool.info,\n metadata: tool.metadata,\n globalParameters: globalParameters.map(convertParameter),\n commands: buildNestedCommands(tool.commands),\n exclusionGroups: nestedExclusionGroups,\n };\n};\n", "type": "registry:file", "target": "components/commandly/utils/nested.ts" } diff --git a/public/r/tool-renderer.json b/public/r/tool-renderer.json index 7e79cc8..04535ee 100644 --- a/public/r/tool-renderer.json +++ b/public/r/tool-renderer.json @@ -14,7 +14,7 @@ "files": [ { "path": "registry/commandly/tool-renderer.tsx", - "content": "import { ParameterValue } from \"@/components/commandly/types/flat\";\nimport { Command, Tool } from \"@/components/commandly/types/flat\";\nimport {\n ParameterRenderContext,\n ParameterRendererEntry,\n} from \"@/components/commandly/types/renderer\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n Command as UICommand,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n} from \"@/components/ui/command\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { MultiSelect } from \"@/components/ui/multi-select\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\";\nimport { Switch } from \"@/components/ui/switch\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"@/components/ui/tooltip\";\nimport { cn } from \"@/lib/utils\";\nimport { CheckIcon, ChevronsUpDownIcon, InfoIcon, PlusIcon, XIcon } from \"lucide-react\";\nimport React from \"react\";\n\nconst findDefaultCommand = (tool: Tool): Command | null => {\n const defaultCommand = tool.commands.find((command) => command.isDefault);\n if (defaultCommand) return defaultCommand;\n\n const nameMatchCommand = tool.commands.find(\n (command) => command.name.toLowerCase() === tool.name.toLowerCase(),\n );\n if (nameMatchCommand) return nameMatchCommand;\n\n return tool.commands.length > 0 ? tool.commands[0] : null;\n};\n\ninterface ParameterLabelProps {\n name: string;\n longFlag?: string;\n shortFlag?: string;\n isRequired?: boolean;\n isGlobal?: boolean;\n description?: string;\n className?: string;\n children?: React.ReactNode;\n}\n\nfunction ParameterLabel({\n name,\n longFlag,\n shortFlag,\n isRequired,\n isGlobal,\n description,\n className,\n children,\n}: ParameterLabelProps) {\n return (\n \n );\n}\n\nfunction FlagInput({ parameter, value, onUpdate }: ParameterRenderContext) {\n return (\n
\n \n \n
\n );\n}\n\nfunction OptionEnumInput({ parameter, value, onUpdate }: ParameterRenderContext) {\n const [open, setOpen] = React.useState(false);\n const separator = parameter.enum?.separator || \",\";\n const options =\n parameter.enum?.values?.map((e) => ({\n value: e.value,\n label: e.displayName || e.value,\n })) ?? [];\n\n const label = (\n \n );\n\n if (parameter.enum?.allowMultiple) {\n const selected = value ? (value as string).split(separator).filter(Boolean) : [];\n return (\n
\n {label}\n onUpdate(vals.join(separator))}\n placeholder=\"Select options\"\n />\n
\n );\n }\n\n return (\n
\n {label}\n \n \n \n {options.find((o) => o.value === value)?.label ?? \"Select an option\"}\n \n \n \n \n \n \n \n No option found.\n \n {options.map((option) => (\n {\n onUpdate(currentValue === value ? \"\" : currentValue);\n setOpen(false);\n }}\n >\n {option.label}\n \n \n ))}\n \n \n \n \n \n
\n );\n}\n\nfunction OptionBooleanInput({ parameter, value, onUpdate }: ParameterRenderContext) {\n return (\n
\n onUpdate(checked.toString())}\n />\n \n
\n );\n}\n\nfunction OptionTextInput({ parameter, value, onUpdate }: ParameterRenderContext) {\n return (\n
\n \n onUpdate(e.target.value)}\n placeholder=\"Enter value\"\n />\n
\n );\n}\n\nfunction ArgumentInput({ parameter, value, onUpdate }: ParameterRenderContext) {\n return (\n
\n \n \n {parameter.parameterType}\n {parameter.position !== undefined && ` (${parameter.position})`}\n \n \n onUpdate(e.target.value)}\n placeholder=\"Enter value\"\n />\n
\n );\n}\n\ninterface RepeatableWrapperProps {\n parameter: ParameterRenderContext[\"parameter\"];\n value: ParameterRenderContext[\"value\"];\n onUpdate: ParameterRenderContext[\"onUpdate\"];\n renderEntry: ParameterRendererEntry[\"component\"];\n}\n\nfunction RepeatableWrapper({ parameter, value, onUpdate, renderEntry }: RepeatableWrapperProps) {\n const toArray = (v: ParameterRenderContext[\"value\"]): string[] => {\n if (Array.isArray(v)) return v;\n if (v !== undefined && v !== \"\" && v !== false) return [String(v)];\n return [\"\"];\n };\n\n const values = toArray(value);\n\n const updateAt = (index: number, val: ParameterRenderContext[\"value\"]) => {\n const next = [...values];\n next[index] = String(val);\n onUpdate(next);\n };\n\n const addRow = () => onUpdate([...values, \"\"]);\n\n const removeAt = (index: number) => {\n const next = values.filter((_, i) => i !== index);\n onUpdate(next);\n };\n\n return (\n
\n {values.map((val, index) => (\n \n
\n {renderEntry({ parameter, value: val, onUpdate: (v) => updateAt(index, v) })}\n
\n {index > 0 && (\n removeAt(index)}\n >\n \n \n )}\n
\n ))}\n \n \n Add another\n \n \n );\n}\n\nexport function defaultComponents(): ParameterRendererEntry[] {\n return [\n { condition: (p) => p.parameterType === \"Flag\", component: (ctx) => },\n {\n condition: (p) => p.parameterType === \"Argument\",\n component: (ctx) => ,\n },\n {\n condition: (p) => p.parameterType === \"Option\" && p.dataType === \"Enum\",\n component: (ctx) => ,\n },\n {\n condition: (p) => p.parameterType === \"Option\" && p.dataType === \"Boolean\",\n component: (ctx) => ,\n },\n {\n condition: (p) => p.parameterType === \"Option\",\n component: (ctx) => ,\n },\n ];\n}\n\ninterface ToolRendererProps {\n selectedCommand?: Command | null;\n tool: Tool;\n catalog?: ParameterRendererEntry[];\n parameterValues: Record;\n updateParameterValue: (parameterKey: string, value: ParameterValue) => void;\n}\n\nexport function ToolRenderer({\n selectedCommand: providedCommand,\n tool,\n catalog = defaultComponents(),\n parameterValues,\n updateParameterValue,\n}: ToolRendererProps) {\n const selectedCommand = providedCommand ?? findDefaultCommand(tool);\n\n return (\n \n {selectedCommand && tool.commands.length === 0 ? (\n

No commands available for this tool.

\n ) : (\n
\n {tool.parameters.length > 0 ? (\n tool.parameters\n .filter((param) => param.commandKey === selectedCommand?.key || param.isGlobal)\n .map((parameter) => {\n const value = parameterValues[parameter.key] ?? \"\";\n const onUpdate = (val: ParameterValue) => updateParameterValue(parameter.key, val);\n const entry = catalog.find((e) => e.condition(parameter));\n if (!entry) return null;\n return (\n \n {parameter.isRepeatable ? (\n \n ) : (\n entry.component({ parameter, value, onUpdate })\n )}\n \n );\n })\n ) : (\n

\n No parameters available for this command.\n

\n )}\n
\n )}\n
\n );\n}\n", + "content": "import { ParameterValue } from \"@/components/commandly/types/flat\";\nimport { Command, Tool } from \"@/components/commandly/types/flat\";\nimport {\n ParameterRenderContext,\n ParameterRendererEntry,\n} from \"@/components/commandly/types/renderer\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n Command as UICommand,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n} from \"@/components/ui/command\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { MultiSelect } from \"@/components/ui/multi-select\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\";\nimport { Switch } from \"@/components/ui/switch\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"@/components/ui/tooltip\";\nimport { cn } from \"@/lib/utils\";\nimport { CheckIcon, ChevronsUpDownIcon, InfoIcon, PlusIcon, XIcon } from \"lucide-react\";\nimport React from \"react\";\n\nconst findDefaultCommand = (tool: Tool): Command | null => {\n const nameMatchCommand = tool.commands.find(\n (command) => command.name.toLowerCase() === tool.name.toLowerCase(),\n );\n if (nameMatchCommand) return nameMatchCommand;\n\n return tool.commands.length > 0 ? tool.commands[0] : null;\n};\n\ninterface ParameterLabelProps {\n name: string;\n longFlag?: string;\n shortFlag?: string;\n isRequired?: boolean;\n isGlobal?: boolean;\n description?: string;\n className?: string;\n children?: React.ReactNode;\n}\n\nfunction ParameterLabel({\n name,\n longFlag,\n shortFlag,\n isRequired,\n isGlobal,\n description,\n className,\n children,\n}: ParameterLabelProps) {\n return (\n \n );\n}\n\nfunction FlagInput({ parameter, value, onUpdate }: ParameterRenderContext) {\n return (\n
\n \n \n
\n );\n}\n\nfunction OptionEnumInput({ parameter, value, onUpdate }: ParameterRenderContext) {\n const [open, setOpen] = React.useState(false);\n const separator = parameter.enum?.separator || \",\";\n const options =\n parameter.enum?.values?.map((e) => ({\n value: e.value,\n label: e.displayName || e.value,\n })) ?? [];\n\n const label = (\n \n );\n\n if (parameter.enum?.allowMultiple) {\n const selected = Array.isArray(value)\n ? (value as string[]).filter(Boolean)\n : value\n ? (value as string).split(separator).filter(Boolean)\n : [];\n return (\n
\n {label}\n onUpdate(vals.join(separator))}\n placeholder=\"Select options\"\n />\n
\n );\n }\n\n return (\n
\n {label}\n \n \n \n {options.find((o) => o.value === value)?.label ?? \"Select an option\"}\n \n \n \n \n \n \n \n No option found.\n \n {options.map((option) => (\n {\n onUpdate(currentValue === value ? \"\" : currentValue);\n setOpen(false);\n }}\n >\n {option.label}\n \n \n ))}\n \n \n \n \n \n
\n );\n}\n\nfunction OptionBooleanInput({ parameter, value, onUpdate }: ParameterRenderContext) {\n return (\n
\n onUpdate(checked.toString())}\n />\n \n
\n );\n}\n\nfunction OptionTextInput({ parameter, value, onUpdate }: ParameterRenderContext) {\n return (\n
\n \n onUpdate(e.target.value)}\n placeholder=\"Enter value\"\n />\n
\n );\n}\n\nfunction ArgumentInput({ parameter, value, onUpdate }: ParameterRenderContext) {\n return (\n
\n \n \n {parameter.parameterType}\n {parameter.position !== undefined && ` (${parameter.position})`}\n \n \n onUpdate(e.target.value)}\n placeholder=\"Enter value\"\n />\n
\n );\n}\n\ninterface RepeatableWrapperProps {\n parameter: ParameterRenderContext[\"parameter\"];\n value: ParameterRenderContext[\"value\"];\n onUpdate: ParameterRenderContext[\"onUpdate\"];\n renderEntry: ParameterRendererEntry[\"component\"];\n}\n\nfunction RepeatableWrapper({ parameter, value, onUpdate, renderEntry }: RepeatableWrapperProps) {\n const toArray = (v: ParameterRenderContext[\"value\"]): string[] => {\n if (Array.isArray(v)) return v;\n if (v !== undefined && v !== \"\" && v !== false) return [String(v)];\n return [\"\"];\n };\n\n const values = toArray(value);\n\n const updateAt = (index: number, val: ParameterRenderContext[\"value\"]) => {\n const next = [...values];\n next[index] = String(val);\n onUpdate(next);\n };\n\n const addRow = () => onUpdate([...values, \"\"]);\n\n const removeAt = (index: number) => {\n const next = values.filter((_, i) => i !== index);\n onUpdate(next);\n };\n\n return (\n
\n {values.map((val, index) => (\n \n
\n {renderEntry({ parameter, value: val, onUpdate: (v) => updateAt(index, v) })}\n
\n {index > 0 && (\n removeAt(index)}\n >\n \n \n )}\n
\n ))}\n \n \n Add another\n \n \n );\n}\n\nexport function defaultComponents(): ParameterRendererEntry[] {\n return [\n { condition: (p) => p.parameterType === \"Flag\", component: (ctx) => },\n {\n condition: (p) => p.parameterType === \"Argument\",\n component: (ctx) => ,\n },\n {\n condition: (p) => p.parameterType === \"Option\" && p.dataType === \"Enum\",\n component: (ctx) => ,\n },\n {\n condition: (p) => p.parameterType === \"Option\" && p.dataType === \"Boolean\",\n component: (ctx) => ,\n },\n {\n condition: (p) => p.parameterType === \"Option\",\n component: (ctx) => ,\n },\n ];\n}\n\ninterface ToolRendererProps {\n selectedCommand?: Command | null;\n tool: Tool;\n catalog?: ParameterRendererEntry[];\n parameterValues: Record;\n updateParameterValue: (parameterKey: string, value: ParameterValue) => void;\n}\n\nexport function ToolRenderer({\n selectedCommand: providedCommand,\n tool,\n catalog = defaultComponents(),\n parameterValues,\n updateParameterValue,\n}: ToolRendererProps) {\n const selectedCommand = providedCommand ?? findDefaultCommand(tool);\n\n return (\n \n {selectedCommand && tool.commands.length === 0 ? (\n

No commands available for this tool.

\n ) : (\n
\n {tool.parameters.length > 0 ? (\n tool.parameters\n .filter((param) => param.commandKey === selectedCommand?.key || param.isGlobal)\n .map((parameter) => {\n const value = parameterValues[parameter.key] ?? \"\";\n const onUpdate = (val: ParameterValue) => updateParameterValue(parameter.key, val);\n const entry = catalog.find((e) => e.condition(parameter));\n if (!entry) return null;\n return (\n \n {parameter.isRepeatable ? (\n \n ) : (\n entry.component({ parameter, value, onUpdate })\n )}\n \n );\n })\n ) : (\n

\n No parameters available for this command.\n

\n )}\n
\n )}\n
\n );\n}\n", "type": "registry:component", "target": "components/commandly/tool-renderer.tsx" }, @@ -26,13 +26,13 @@ }, { "path": "registry/commandly/types/flat.ts", - "content": "export interface ToolInfo {\n description?: string;\n version?: string;\n url?: string;\n}\n\nexport interface Command {\n key: string;\n parentCommandKey?: string;\n name: string;\n description?: string;\n interactive?: boolean;\n isDefault?: boolean;\n sortOrder?: number;\n}\n\nexport interface ParameterEnumValue {\n value: string;\n displayName: string;\n description?: string;\n isDefault?: boolean;\n sortOrder?: number;\n}\n\nexport interface ParameterEnumValues {\n values: ParameterEnumValue[];\n allowMultiple?: boolean;\n separator?: string;\n}\n\nexport type ParameterValidationType =\n | \"min_length\"\n | \"max_length\"\n | \"min_value\"\n | \"max_value\"\n | \"regex\";\n\nexport interface ParameterValidation {\n key: string;\n validationType: ParameterValidationType;\n validationValue: string;\n errorMessage: string;\n}\n\nexport type ParameterDependencyType = \"requires\" | \"conflicts_with\";\n\nexport interface ParameterDependency {\n key: string;\n parameterKey: string;\n dependsOnParameterKey: string;\n dependencyType: ParameterDependencyType;\n conditionValue?: string;\n}\n\nexport type ParameterValue = string | number | boolean | string[];\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface ToolMetadata {}\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface ParameterMetadata {\n tags?: string[];\n}\n\nexport type ParameterType = \"Flag\" | \"Option\" | \"Argument\";\n\nexport type ParameterDataType = \"String\" | \"Number\" | \"Boolean\" | \"Enum\";\n\nexport interface Parameter {\n key: string;\n name: string;\n commandKey?: string;\n description?: string;\n group?: string;\n metadata?: ParameterMetadata;\n parameterType: ParameterType;\n dataType: ParameterDataType;\n isRequired?: boolean;\n isRepeatable?: boolean;\n isGlobal?: boolean;\n shortFlag?: string;\n longFlag?: string;\n position?: number;\n sortOrder?: number;\n arraySeparator?: string;\n keyValueSeparator?: string;\n enum?: ParameterEnumValues;\n validations?: ParameterValidation[];\n dependencies?: ParameterDependency[];\n}\n\nexport type ExclusionType = \"mutual_exclusive\" | \"required_one_of\";\n\nexport interface ExclusionGroup {\n key?: string;\n commandKey?: string;\n name: string;\n exclusionType: ExclusionType;\n parameterKeys: string[];\n}\n\nexport interface Tool {\n name: string;\n displayName: string;\n info?: ToolInfo;\n commands: Command[];\n parameters: Parameter[];\n exclusionGroups?: ExclusionGroup[];\n metadata?: ToolMetadata;\n}\n", + "content": "export interface ToolInfo {\n /** A brief human-readable description of what the tool does. */\n description?: string;\n /** The version string of the tool (e.g. \"1.0.0\"). */\n version?: string;\n /** The homepage or documentation URL for the tool. */\n url?: string;\n}\n\nexport interface Command {\n /** Unique identifier for this command within the tool. */\n key: string;\n /** Key of the parent command; used to represent subcommand nesting. */\n parentCommandKey?: string;\n /** Human-readable display name of the command. */\n name: string;\n /** Brief description of what this command does. */\n description?: string;\n /** Whether this command opens an interactive session or prompt. */\n interactive?: boolean;\n /** Display sort position relative to sibling commands. */\n sortOrder?: number;\n}\n\nexport interface ParameterEnumValue {\n /** The raw value passed to the CLI for this choice. */\n value: string;\n /** Human-readable label shown to the user for this enum choice. */\n displayName: string;\n /** Description of what this enum value does or represents. */\n description?: string;\n /** Whether this is the default selection when no value is provided. */\n isDefault?: boolean;\n /** Display sort position relative to sibling enum values. */\n sortOrder?: number;\n}\n\nexport interface ParameterEnumValues {\n /** The list of allowed enum choices. */\n values: ParameterEnumValue[];\n /** Whether the user can select multiple values at once. */\n allowMultiple?: boolean;\n /** Separator character used when joining multiple selected values. */\n separator?: string;\n}\n\nexport type ParameterValidationType =\n | \"min_length\"\n | \"max_length\"\n | \"min_value\"\n | \"max_value\"\n | \"regex\";\n\nexport interface ParameterValidation {\n /** Unique identifier for this validation rule. */\n key: string;\n /** The type of validation to apply. */\n validationType: ParameterValidationType;\n /** The value to validate against (e.g. the max length number, or a regex pattern). */\n validationValue: string;\n /** The error message to display when validation fails. */\n errorMessage: string;\n}\n\nexport type ParameterDependencyType = \"requires\" | \"conflicts_with\";\n\nexport interface ParameterDependency {\n /** Unique identifier for this dependency rule. */\n key: string;\n /** Key of the parameter that owns this dependency. */\n parameterKey: string;\n /** Key of the parameter this dependency references. */\n dependsOnParameterKey: string;\n /** Whether this parameter requires or conflicts with the referenced parameter. */\n dependencyType: ParameterDependencyType;\n /** Optional value that the referenced parameter must have for this dependency to apply. */\n conditionValue?: string;\n}\n\nexport type ParameterValue = string | number | boolean | string[];\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface ToolMetadata {}\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface ParameterMetadata {\n /** Arbitrary tags for categorising or filtering parameters. */\n tags?: string[];\n}\n\nexport type ParameterType = \"Flag\" | \"Option\" | \"Argument\";\n\nexport type ParameterDataType = \"String\" | \"Number\" | \"Boolean\" | \"Enum\";\n\nexport interface Parameter {\n /** Unique identifier for this parameter within the tool. */\n key: string;\n /** Human-readable display name of the parameter. */\n name: string;\n /** Key of the command this parameter belongs to; omit for global parameters. */\n commandKey?: string;\n /** Brief description of what this parameter does or accepts. */\n description?: string;\n /** Optional grouping label for organising related parameters in the UI. */\n group?: string;\n /** Additional metadata such as tags. */\n metadata?: ParameterMetadata;\n /** Whether this is a boolean flag, a key-value option, or a positional argument. */\n parameterType: ParameterType;\n /** The data type of the parameter's value. */\n dataType: ParameterDataType;\n /** Whether the user must provide this parameter. */\n isRequired?: boolean;\n /** Whether this parameter can be specified multiple times. */\n isRepeatable?: boolean;\n /** Whether this parameter applies to all commands rather than a single command. */\n isGlobal?: boolean;\n /** The single-character short flag (e.g. \"-v\"). */\n shortFlag?: string;\n /** The long-form flag or option name (e.g. \"--verbose\"). */\n longFlag?: string;\n /** Zero-based position index for positional arguments. */\n position?: number;\n /** Display sort position relative to sibling parameters. */\n sortOrder?: number;\n /** Separator character used when the option accepts multiple values in one argument (e.g. \",\"). */\n arraySeparator?: string;\n /** Separator between key and value for key=value style options (e.g. \"=\"). */\n keyValueSeparator?: string;\n /** Allowed enum choices when dataType is \"Enum\". */\n enum?: ParameterEnumValues;\n /** Validation rules applied to this parameter's value. */\n validations?: ParameterValidation[];\n /** Dependencies on other parameters (requires or conflicts-with relationships). */\n dependencies?: ParameterDependency[];\n}\n\nexport type ExclusionType = \"mutual_exclusive\" | \"required_one_of\";\n\nexport interface ExclusionGroup {\n /** Unique identifier for this exclusion group. */\n key?: string;\n /** Key of the command this exclusion group belongs to; omit for global groups. */\n commandKey?: string;\n /** Human-readable name for this exclusion group. */\n name: string;\n /** Whether parameters in this group are mutually exclusive or one is required. */\n exclusionType: ExclusionType;\n /** Keys of the parameters that participate in this exclusion group. */\n parameterKeys: string[];\n}\n\nexport interface Tool {\n /** Unique machine-readable identifier for the tool (e.g. \"httpx\"). */\n name: string;\n /** Human-readable display name for the tool (e.g. \"HTTPx\"). */\n displayName: string;\n /** General information about the tool such as description, version, and URL. */\n info?: ToolInfo;\n /** List of all commands and subcommands defined for this tool. */\n commands: Command[];\n /** Flat list of all parameters across all commands and global scope. */\n parameters: Parameter[];\n /** Groups of parameters with mutual exclusion or required-one-of constraints. */\n exclusionGroups?: ExclusionGroup[];\n /** Arbitrary metadata attached to the tool. */\n metadata?: ToolMetadata;\n}\n", "type": "registry:file", "target": "components/commandly/types/flat.ts" }, { "path": "registry/commandly/types/nested.ts", - "content": "import type {\n ExclusionType,\n ParameterDataType,\n ParameterDependencyType,\n ParameterEnumValues,\n ParameterMetadata,\n ParameterType,\n ParameterValidationType,\n ToolInfo,\n ToolMetadata,\n} from \"@/components/commandly/types/flat\";\n\nexport interface NestedParameterValidation {\n validationType: ParameterValidationType;\n validationValue: string;\n errorMessage: string;\n}\n\nexport interface NestedParameterDependency {\n dependsOnParameter: string;\n dependencyType: ParameterDependencyType;\n conditionValue?: string;\n}\n\nexport interface NestedParameter {\n name: string;\n description?: string;\n group?: string;\n parameterType: ParameterType;\n dataType: ParameterDataType;\n metadata?: ParameterMetadata;\n isRequired?: boolean;\n isRepeatable?: boolean;\n isGlobal?: boolean;\n shortFlag?: string;\n longFlag?: string;\n position?: number;\n sortOrder?: number;\n arraySeparator?: string;\n keyValueSeparator?: string;\n enum?: ParameterEnumValues;\n validations?: NestedParameterValidation[];\n dependencies?: NestedParameterDependency[];\n}\n\nexport interface NestedCommand {\n name: string;\n description?: string;\n interactive?: boolean;\n isDefault: boolean;\n sortOrder: number;\n parameters: NestedParameter[];\n subcommands: NestedCommand[];\n}\n\nexport interface NestedExclusionGroup {\n name: string;\n exclusionType: ExclusionType;\n parameters: string[];\n}\n\nexport interface NestedTool {\n $schema?: string;\n name: string;\n displayName: string;\n info?: ToolInfo;\n url?: string;\n globalParameters: NestedParameter[];\n commands: NestedCommand[];\n exclusionGroups?: NestedExclusionGroup[] | null;\n metadata?: ToolMetadata;\n}\n", + "content": "import type {\n ExclusionType,\n ParameterDataType,\n ParameterDependencyType,\n ParameterEnumValues,\n ParameterMetadata,\n ParameterType,\n ParameterValidationType,\n ToolInfo,\n ToolMetadata,\n} from \"@/components/commandly/types/flat\";\n\nexport interface NestedParameterValidation {\n /** The type of validation to apply. */\n validationType: ParameterValidationType;\n /** The value to validate against (e.g. max length number or regex pattern). */\n validationValue: string;\n /** The error message to display when validation fails. */\n errorMessage: string;\n}\n\nexport interface NestedParameterDependency {\n /** Name of the parameter this dependency references. */\n dependsOnParameter: string;\n /** Whether this parameter requires or conflicts with the referenced parameter. */\n dependencyType: ParameterDependencyType;\n /** Optional value that the referenced parameter must have for this dependency to apply. */\n conditionValue?: string;\n}\n\nexport interface NestedParameter {\n /** Human-readable display name of the parameter. */\n name: string;\n /** Brief description of what this parameter does or accepts. */\n description?: string;\n /** Optional grouping label for organising related parameters in the UI. */\n group?: string;\n /** Whether this is a boolean flag, a key-value option, or a positional argument. */\n parameterType: ParameterType;\n /** The data type of the parameter's value. */\n dataType: ParameterDataType;\n /** Additional metadata such as tags. */\n metadata?: ParameterMetadata;\n /** Whether the user must provide this parameter. */\n isRequired?: boolean;\n /** Whether this parameter can be specified multiple times. */\n isRepeatable?: boolean;\n /** Whether this parameter applies to all commands rather than a single command. */\n isGlobal?: boolean;\n /** The single-character short flag (e.g. \"-v\"). */\n shortFlag?: string;\n /** The long-form flag or option name (e.g. \"--verbose\"). */\n longFlag?: string;\n /** Zero-based position index for positional arguments. */\n position?: number;\n /** Display sort position relative to sibling parameters. */\n sortOrder?: number;\n /** Separator character used when the option accepts multiple values in one argument (e.g. \",\"). */\n arraySeparator?: string;\n /** Separator between key and value for key=value style options (e.g. \"=\"). */\n keyValueSeparator?: string;\n /** Allowed enum choices when dataType is \"Enum\". */\n enum?: ParameterEnumValues;\n /** Validation rules applied to this parameter's value. */\n validations?: NestedParameterValidation[];\n /** Dependencies on other parameters (requires or conflicts-with relationships). */\n dependencies?: NestedParameterDependency[];\n}\n\nexport interface NestedCommand {\n /** Human-readable display name of the command. */\n name: string;\n /** Brief description of what this command does. */\n description?: string;\n /** Whether this command opens an interactive session or prompt. */\n interactive?: boolean;\n /** Display sort position relative to sibling commands. */\n sortOrder: number;\n /** Parameters that belong directly to this command. */\n parameters: NestedParameter[];\n /** Nested subcommands of this command. */\n subcommands: NestedCommand[];\n}\n\nexport interface NestedExclusionGroup {\n /** Human-readable name for this exclusion group. */\n name: string;\n /** Whether parameters in this group are mutually exclusive or one is required. */\n exclusionType: ExclusionType;\n /** Names of the parameters that participate in this exclusion group. */\n parameters: string[];\n}\n\nexport interface NestedTool {\n /** Optional JSON schema URI for validation. */\n $schema?: string;\n /** Unique machine-readable identifier for the tool (e.g. \"httpx\"). */\n name: string;\n /** Human-readable display name for the tool (e.g. \"HTTPx\"). */\n displayName: string;\n /** General information about the tool such as description, version, and URL. */\n info?: ToolInfo;\n /** The homepage or documentation URL for the tool. */\n url?: string;\n /** Parameters that apply to all commands globally. */\n globalParameters: NestedParameter[];\n /** Hierarchical list of commands and their nested subcommands. */\n commands: NestedCommand[];\n /** Groups of parameters with mutual exclusion or required-one-of constraints. */\n exclusionGroups?: NestedExclusionGroup[] | null;\n /** Arbitrary metadata attached to the tool. */\n metadata?: ToolMetadata;\n}\n", "type": "registry:file", "target": "components/commandly/types/nested.ts" } diff --git a/public/r/ui.json b/public/r/ui.json index c5e6b5c..f62eedd 100644 --- a/public/r/ui.json +++ b/public/r/ui.json @@ -29,13 +29,13 @@ }, { "path": "registry/commandly/json-output.tsx", - "content": "import { Tool } from \"@/components/commandly/types/flat\";\nimport { exportToStructuredJSON } from \"@/components/commandly/utils/flat\";\nimport { convertToNestedStructure } from \"@/components/commandly/utils/nested\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardAction, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport {\n Command as UICommand,\n CommandGroup,\n CommandItem,\n CommandList,\n} from \"@/components/ui/command\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\";\nimport { ScrollArea, ScrollBar } from \"@/components/ui/scroll-area\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { cn } from \"@/lib/utils\";\nimport { CheckIcon, ChevronsUpDownIcon, CopyIcon, Edit2Icon, XIcon } from \"lucide-react\";\nimport { useEffect, useState } from \"react\";\nimport { toast } from \"sonner\";\n\nconst jsonOptions = [\n { value: \"nested\", label: \"Nested\" },\n { value: \"flat\", label: \"Flat\" },\n];\n\ninterface JsonTypeComponentProps {\n tool: Tool;\n onApply?: (tool: Tool) => void;\n}\n\nexport function JsonOutput({ tool, onApply }: JsonTypeComponentProps) {\n const [open, setOpen] = useState(false);\n const [jsonString, setJsonString] = useState();\n const [jsonType, setJsonType] = useState<\"nested\" | \"flat\">(\"flat\");\n const [isEditing, setIsEditing] = useState(false);\n const [editValue, setEditValue] = useState(\"\");\n\n useEffect(() => {\n const config =\n jsonType === \"flat\" ? exportToStructuredJSON(tool) : convertToNestedStructure(tool);\n setJsonString(JSON.stringify(config, null, 2));\n }, [jsonType, tool]);\n\n const handleEditToggle = () => {\n setEditValue(jsonString ?? \"\");\n setIsEditing(true);\n };\n\n const handleApply = () => {\n try {\n const parsed = JSON.parse(editValue) as Tool;\n onApply!(parsed);\n setIsEditing(false);\n } catch {\n toast.error(\"Invalid JSON\", { description: \"Please fix the JSON before applying.\" });\n }\n };\n\n const handleCancel = () => {\n setIsEditing(false);\n setEditValue(\"\");\n };\n\n return (\n \n \n \n Output type: \n \n \n \n {jsonOptions.find((option) => option.value === jsonType)?.label}\n \n \n \n \n \n \n \n {jsonOptions.map((option) => (\n {\n setJsonType(currentValue as \"nested\" | \"flat\");\n setOpen(false);\n }}\n >\n {option.label}\n \n \n ))}\n \n \n \n \n \n \n
\n {onApply && !isEditing && (\n \n \n \n )}\n {onApply && isEditing && (\n \n \n \n )}\n {\n navigator.clipboard.writeText(jsonString!);\n toast(\"Copied!\");\n }}\n >\n \n \n
\n
\n \n {isEditing ? (\n
\n \n setEditValue(e.target.value)}\n spellCheck={false}\n />\n \n \n \n
\n \n Cancel\n \n \n Apply\n \n
\n
\n ) : (\n \n
\n              {jsonString}\n            
\n \n \n \n )}\n
\n
\n );\n}\n", + "content": "import { Tool } from \"@/components/commandly/types/flat\";\nimport { exportToStructuredJSON } from \"@/components/commandly/utils/flat\";\nimport { convertToNestedStructure } from \"@/components/commandly/utils/nested\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardAction, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport {\n Command as UICommand,\n CommandGroup,\n CommandItem,\n CommandList,\n} from \"@/components/ui/command\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\";\nimport { ScrollArea, ScrollBar } from \"@/components/ui/scroll-area\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { cn } from \"@/lib/utils\";\nimport { CheckIcon, ChevronsUpDownIcon, CopyIcon, Edit2Icon, XIcon } from \"lucide-react\";\nimport { useEffect, useMemo, useState } from \"react\";\nimport { toast } from \"sonner\";\n\nconst jsonOptions = [\n { value: \"nested\", label: \"Nested\" },\n { value: \"flat\", label: \"Flat\" },\n];\n\ntype DiffLine = { type: \"same\" | \"added\" | \"removed\"; text: string };\n\nfunction diffLines(before: string, after: string): DiffLine[] {\n const a = before.split(\"\\n\");\n const b = after.split(\"\\n\");\n const m = a.length;\n const n = b.length;\n const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));\n for (let i = 1; i <= m; i++)\n for (let j = 1; j <= n; j++)\n dp[i][j] =\n a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] + 1 : Math.max(dp[i - 1][j], dp[i][j - 1]);\n const result: DiffLine[] = [];\n let i = m,\n j = n;\n while (i > 0 || j > 0) {\n if (i > 0 && j > 0 && a[i - 1] === b[j - 1]) {\n result.unshift({ type: \"same\", text: a[i - 1] });\n i--;\n j--;\n } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {\n result.unshift({ type: \"added\", text: b[j - 1] });\n j--;\n } else {\n result.unshift({ type: \"removed\", text: a[i - 1] });\n i--;\n }\n }\n return result;\n}\n\ninterface JsonTypeComponentProps {\n tool: Tool;\n originalTool?: Tool;\n onApply?: (tool: Tool) => void;\n}\n\nexport function JsonOutput({ tool, originalTool, onApply }: JsonTypeComponentProps) {\n const [open, setOpen] = useState(false);\n const [jsonString, setJsonString] = useState();\n const [originalJsonString, setOriginalJsonString] = useState();\n const [jsonType, setJsonType] = useState<\"nested\" | \"flat\">(\"flat\");\n const [isEditing, setIsEditing] = useState(false);\n const [editValue, setEditValue] = useState(\"\");\n const [showDiff, setShowDiff] = useState(true);\n\n useEffect(() => {\n const config =\n jsonType === \"flat\" ? exportToStructuredJSON(tool) : convertToNestedStructure(tool);\n setJsonString(JSON.stringify(config, null, 2));\n }, [jsonType, tool]);\n\n useEffect(() => {\n if (!originalTool) {\n setOriginalJsonString(undefined);\n return;\n }\n const config =\n jsonType === \"flat\"\n ? exportToStructuredJSON(originalTool)\n : convertToNestedStructure(originalTool);\n setOriginalJsonString(JSON.stringify(config, null, 2));\n }, [jsonType, originalTool]);\n\n const diff = useMemo(() => {\n if (!originalJsonString || !jsonString || originalJsonString === jsonString) return null;\n return diffLines(originalJsonString, jsonString);\n }, [originalJsonString, jsonString]);\n\n const diffStats = useMemo(() => {\n if (!diff) return null;\n const added = diff.filter((l) => l.type === \"added\").length;\n const removed = diff.filter((l) => l.type === \"removed\").length;\n return { added, removed };\n }, [diff]);\n\n const handleEditToggle = () => {\n setEditValue(jsonString ?? \"\");\n setIsEditing(true);\n };\n\n const handleApply = () => {\n try {\n const parsed = JSON.parse(editValue) as Tool;\n onApply!(parsed);\n setIsEditing(false);\n } catch {\n toast.error(\"Invalid JSON\", { description: \"Please fix the JSON before applying.\" });\n }\n };\n\n const handleCancel = () => {\n setIsEditing(false);\n setEditValue(\"\");\n };\n\n return (\n \n \n \n Output type: \n \n \n \n {jsonOptions.find((option) => option.value === jsonType)?.label}\n \n \n \n \n \n \n \n {jsonOptions.map((option) => (\n {\n setJsonType(currentValue as \"nested\" | \"flat\");\n setOpen(false);\n }}\n >\n {option.label}\n \n \n ))}\n \n \n \n \n \n \n
\n {onApply && !isEditing && (\n \n \n \n )}\n {onApply && isEditing && (\n \n \n \n )}\n {\n navigator.clipboard.writeText(jsonString!);\n toast(\"Copied!\");\n }}\n >\n \n \n
\n
\n \n {diffStats && !isEditing && (\n
\n {diffStats.added > 0 && (\n \n +{diffStats.added} added\n \n )}\n {diffStats.removed > 0 && (\n \n -{diffStats.removed} removed\n \n )}\n setShowDiff((v) => !v)}\n >\n {showDiff ? \"Full view\" : \"Diff view\"}\n \n
\n )}\n {isEditing ? (\n
\n \n setEditValue(e.target.value)}\n spellCheck={false}\n />\n \n \n \n
\n \n Cancel\n \n \n Apply\n \n
\n
\n ) : diff && showDiff ? (\n \n
\n              {diff.map((line, idx) => (\n                \n                  \n                    {line.type === \"added\" ? \"+ \" : line.type === \"removed\" ? \"- \" : \"  \"}\n                  \n                  {line.text}\n                \n              ))}\n            
\n \n \n \n ) : (\n \n
\n              {jsonString}\n            
\n \n \n \n )}\n
\n
\n );\n}\n", "type": "registry:component", "target": "components/commandly/json-output.tsx" }, { "path": "registry/commandly/tool-renderer.tsx", - "content": "import { ParameterValue } from \"@/components/commandly/types/flat\";\nimport { Command, Tool } from \"@/components/commandly/types/flat\";\nimport {\n ParameterRenderContext,\n ParameterRendererEntry,\n} from \"@/components/commandly/types/renderer\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n Command as UICommand,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n} from \"@/components/ui/command\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { MultiSelect } from \"@/components/ui/multi-select\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\";\nimport { Switch } from \"@/components/ui/switch\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"@/components/ui/tooltip\";\nimport { cn } from \"@/lib/utils\";\nimport { CheckIcon, ChevronsUpDownIcon, InfoIcon, PlusIcon, XIcon } from \"lucide-react\";\nimport React from \"react\";\n\nconst findDefaultCommand = (tool: Tool): Command | null => {\n const defaultCommand = tool.commands.find((command) => command.isDefault);\n if (defaultCommand) return defaultCommand;\n\n const nameMatchCommand = tool.commands.find(\n (command) => command.name.toLowerCase() === tool.name.toLowerCase(),\n );\n if (nameMatchCommand) return nameMatchCommand;\n\n return tool.commands.length > 0 ? tool.commands[0] : null;\n};\n\ninterface ParameterLabelProps {\n name: string;\n longFlag?: string;\n shortFlag?: string;\n isRequired?: boolean;\n isGlobal?: boolean;\n description?: string;\n className?: string;\n children?: React.ReactNode;\n}\n\nfunction ParameterLabel({\n name,\n longFlag,\n shortFlag,\n isRequired,\n isGlobal,\n description,\n className,\n children,\n}: ParameterLabelProps) {\n return (\n \n );\n}\n\nfunction FlagInput({ parameter, value, onUpdate }: ParameterRenderContext) {\n return (\n
\n \n \n
\n );\n}\n\nfunction OptionEnumInput({ parameter, value, onUpdate }: ParameterRenderContext) {\n const [open, setOpen] = React.useState(false);\n const separator = parameter.enum?.separator || \",\";\n const options =\n parameter.enum?.values?.map((e) => ({\n value: e.value,\n label: e.displayName || e.value,\n })) ?? [];\n\n const label = (\n \n );\n\n if (parameter.enum?.allowMultiple) {\n const selected = value ? (value as string).split(separator).filter(Boolean) : [];\n return (\n
\n {label}\n onUpdate(vals.join(separator))}\n placeholder=\"Select options\"\n />\n
\n );\n }\n\n return (\n
\n {label}\n \n \n \n {options.find((o) => o.value === value)?.label ?? \"Select an option\"}\n \n \n \n \n \n \n \n No option found.\n \n {options.map((option) => (\n {\n onUpdate(currentValue === value ? \"\" : currentValue);\n setOpen(false);\n }}\n >\n {option.label}\n \n \n ))}\n \n \n \n \n \n
\n );\n}\n\nfunction OptionBooleanInput({ parameter, value, onUpdate }: ParameterRenderContext) {\n return (\n
\n onUpdate(checked.toString())}\n />\n \n
\n );\n}\n\nfunction OptionTextInput({ parameter, value, onUpdate }: ParameterRenderContext) {\n return (\n
\n \n onUpdate(e.target.value)}\n placeholder=\"Enter value\"\n />\n
\n );\n}\n\nfunction ArgumentInput({ parameter, value, onUpdate }: ParameterRenderContext) {\n return (\n
\n \n \n {parameter.parameterType}\n {parameter.position !== undefined && ` (${parameter.position})`}\n \n \n onUpdate(e.target.value)}\n placeholder=\"Enter value\"\n />\n
\n );\n}\n\ninterface RepeatableWrapperProps {\n parameter: ParameterRenderContext[\"parameter\"];\n value: ParameterRenderContext[\"value\"];\n onUpdate: ParameterRenderContext[\"onUpdate\"];\n renderEntry: ParameterRendererEntry[\"component\"];\n}\n\nfunction RepeatableWrapper({ parameter, value, onUpdate, renderEntry }: RepeatableWrapperProps) {\n const toArray = (v: ParameterRenderContext[\"value\"]): string[] => {\n if (Array.isArray(v)) return v;\n if (v !== undefined && v !== \"\" && v !== false) return [String(v)];\n return [\"\"];\n };\n\n const values = toArray(value);\n\n const updateAt = (index: number, val: ParameterRenderContext[\"value\"]) => {\n const next = [...values];\n next[index] = String(val);\n onUpdate(next);\n };\n\n const addRow = () => onUpdate([...values, \"\"]);\n\n const removeAt = (index: number) => {\n const next = values.filter((_, i) => i !== index);\n onUpdate(next);\n };\n\n return (\n
\n {values.map((val, index) => (\n \n
\n {renderEntry({ parameter, value: val, onUpdate: (v) => updateAt(index, v) })}\n
\n {index > 0 && (\n removeAt(index)}\n >\n \n \n )}\n
\n ))}\n \n \n Add another\n \n \n );\n}\n\nexport function defaultComponents(): ParameterRendererEntry[] {\n return [\n { condition: (p) => p.parameterType === \"Flag\", component: (ctx) => },\n {\n condition: (p) => p.parameterType === \"Argument\",\n component: (ctx) => ,\n },\n {\n condition: (p) => p.parameterType === \"Option\" && p.dataType === \"Enum\",\n component: (ctx) => ,\n },\n {\n condition: (p) => p.parameterType === \"Option\" && p.dataType === \"Boolean\",\n component: (ctx) => ,\n },\n {\n condition: (p) => p.parameterType === \"Option\",\n component: (ctx) => ,\n },\n ];\n}\n\ninterface ToolRendererProps {\n selectedCommand?: Command | null;\n tool: Tool;\n catalog?: ParameterRendererEntry[];\n parameterValues: Record;\n updateParameterValue: (parameterKey: string, value: ParameterValue) => void;\n}\n\nexport function ToolRenderer({\n selectedCommand: providedCommand,\n tool,\n catalog = defaultComponents(),\n parameterValues,\n updateParameterValue,\n}: ToolRendererProps) {\n const selectedCommand = providedCommand ?? findDefaultCommand(tool);\n\n return (\n \n {selectedCommand && tool.commands.length === 0 ? (\n

No commands available for this tool.

\n ) : (\n
\n {tool.parameters.length > 0 ? (\n tool.parameters\n .filter((param) => param.commandKey === selectedCommand?.key || param.isGlobal)\n .map((parameter) => {\n const value = parameterValues[parameter.key] ?? \"\";\n const onUpdate = (val: ParameterValue) => updateParameterValue(parameter.key, val);\n const entry = catalog.find((e) => e.condition(parameter));\n if (!entry) return null;\n return (\n \n {parameter.isRepeatable ? (\n \n ) : (\n entry.component({ parameter, value, onUpdate })\n )}\n \n );\n })\n ) : (\n

\n No parameters available for this command.\n

\n )}\n
\n )}\n
\n );\n}\n", + "content": "import { ParameterValue } from \"@/components/commandly/types/flat\";\nimport { Command, Tool } from \"@/components/commandly/types/flat\";\nimport {\n ParameterRenderContext,\n ParameterRendererEntry,\n} from \"@/components/commandly/types/renderer\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n Command as UICommand,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n} from \"@/components/ui/command\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { MultiSelect } from \"@/components/ui/multi-select\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\";\nimport { Switch } from \"@/components/ui/switch\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"@/components/ui/tooltip\";\nimport { cn } from \"@/lib/utils\";\nimport { CheckIcon, ChevronsUpDownIcon, InfoIcon, PlusIcon, XIcon } from \"lucide-react\";\nimport React from \"react\";\n\nconst findDefaultCommand = (tool: Tool): Command | null => {\n const nameMatchCommand = tool.commands.find(\n (command) => command.name.toLowerCase() === tool.name.toLowerCase(),\n );\n if (nameMatchCommand) return nameMatchCommand;\n\n return tool.commands.length > 0 ? tool.commands[0] : null;\n};\n\ninterface ParameterLabelProps {\n name: string;\n longFlag?: string;\n shortFlag?: string;\n isRequired?: boolean;\n isGlobal?: boolean;\n description?: string;\n className?: string;\n children?: React.ReactNode;\n}\n\nfunction ParameterLabel({\n name,\n longFlag,\n shortFlag,\n isRequired,\n isGlobal,\n description,\n className,\n children,\n}: ParameterLabelProps) {\n return (\n \n );\n}\n\nfunction FlagInput({ parameter, value, onUpdate }: ParameterRenderContext) {\n return (\n
\n \n \n
\n );\n}\n\nfunction OptionEnumInput({ parameter, value, onUpdate }: ParameterRenderContext) {\n const [open, setOpen] = React.useState(false);\n const separator = parameter.enum?.separator || \",\";\n const options =\n parameter.enum?.values?.map((e) => ({\n value: e.value,\n label: e.displayName || e.value,\n })) ?? [];\n\n const label = (\n \n );\n\n if (parameter.enum?.allowMultiple) {\n const selected = Array.isArray(value)\n ? (value as string[]).filter(Boolean)\n : value\n ? (value as string).split(separator).filter(Boolean)\n : [];\n return (\n
\n {label}\n onUpdate(vals.join(separator))}\n placeholder=\"Select options\"\n />\n
\n );\n }\n\n return (\n
\n {label}\n \n \n \n {options.find((o) => o.value === value)?.label ?? \"Select an option\"}\n \n \n \n \n \n \n \n No option found.\n \n {options.map((option) => (\n {\n onUpdate(currentValue === value ? \"\" : currentValue);\n setOpen(false);\n }}\n >\n {option.label}\n \n \n ))}\n \n \n \n \n \n
\n );\n}\n\nfunction OptionBooleanInput({ parameter, value, onUpdate }: ParameterRenderContext) {\n return (\n
\n onUpdate(checked.toString())}\n />\n \n
\n );\n}\n\nfunction OptionTextInput({ parameter, value, onUpdate }: ParameterRenderContext) {\n return (\n
\n \n onUpdate(e.target.value)}\n placeholder=\"Enter value\"\n />\n
\n );\n}\n\nfunction ArgumentInput({ parameter, value, onUpdate }: ParameterRenderContext) {\n return (\n
\n \n \n {parameter.parameterType}\n {parameter.position !== undefined && ` (${parameter.position})`}\n \n \n onUpdate(e.target.value)}\n placeholder=\"Enter value\"\n />\n
\n );\n}\n\ninterface RepeatableWrapperProps {\n parameter: ParameterRenderContext[\"parameter\"];\n value: ParameterRenderContext[\"value\"];\n onUpdate: ParameterRenderContext[\"onUpdate\"];\n renderEntry: ParameterRendererEntry[\"component\"];\n}\n\nfunction RepeatableWrapper({ parameter, value, onUpdate, renderEntry }: RepeatableWrapperProps) {\n const toArray = (v: ParameterRenderContext[\"value\"]): string[] => {\n if (Array.isArray(v)) return v;\n if (v !== undefined && v !== \"\" && v !== false) return [String(v)];\n return [\"\"];\n };\n\n const values = toArray(value);\n\n const updateAt = (index: number, val: ParameterRenderContext[\"value\"]) => {\n const next = [...values];\n next[index] = String(val);\n onUpdate(next);\n };\n\n const addRow = () => onUpdate([...values, \"\"]);\n\n const removeAt = (index: number) => {\n const next = values.filter((_, i) => i !== index);\n onUpdate(next);\n };\n\n return (\n
\n {values.map((val, index) => (\n \n
\n {renderEntry({ parameter, value: val, onUpdate: (v) => updateAt(index, v) })}\n
\n {index > 0 && (\n removeAt(index)}\n >\n \n \n )}\n
\n ))}\n \n \n Add another\n \n \n );\n}\n\nexport function defaultComponents(): ParameterRendererEntry[] {\n return [\n { condition: (p) => p.parameterType === \"Flag\", component: (ctx) => },\n {\n condition: (p) => p.parameterType === \"Argument\",\n component: (ctx) => ,\n },\n {\n condition: (p) => p.parameterType === \"Option\" && p.dataType === \"Enum\",\n component: (ctx) => ,\n },\n {\n condition: (p) => p.parameterType === \"Option\" && p.dataType === \"Boolean\",\n component: (ctx) => ,\n },\n {\n condition: (p) => p.parameterType === \"Option\",\n component: (ctx) => ,\n },\n ];\n}\n\ninterface ToolRendererProps {\n selectedCommand?: Command | null;\n tool: Tool;\n catalog?: ParameterRendererEntry[];\n parameterValues: Record;\n updateParameterValue: (parameterKey: string, value: ParameterValue) => void;\n}\n\nexport function ToolRenderer({\n selectedCommand: providedCommand,\n tool,\n catalog = defaultComponents(),\n parameterValues,\n updateParameterValue,\n}: ToolRendererProps) {\n const selectedCommand = providedCommand ?? findDefaultCommand(tool);\n\n return (\n \n {selectedCommand && tool.commands.length === 0 ? (\n

No commands available for this tool.

\n ) : (\n
\n {tool.parameters.length > 0 ? (\n tool.parameters\n .filter((param) => param.commandKey === selectedCommand?.key || param.isGlobal)\n .map((parameter) => {\n const value = parameterValues[parameter.key] ?? \"\";\n const onUpdate = (val: ParameterValue) => updateParameterValue(parameter.key, val);\n const entry = catalog.find((e) => e.condition(parameter));\n if (!entry) return null;\n return (\n \n {parameter.isRepeatable ? (\n \n ) : (\n entry.component({ parameter, value, onUpdate })\n )}\n \n );\n })\n ) : (\n

\n No parameters available for this command.\n

\n )}\n
\n )}\n
\n );\n}\n", "type": "registry:component", "target": "components/commandly/tool-renderer.tsx" }, @@ -47,25 +47,25 @@ }, { "path": "registry/commandly/types/flat.ts", - "content": "export interface ToolInfo {\n description?: string;\n version?: string;\n url?: string;\n}\n\nexport interface Command {\n key: string;\n parentCommandKey?: string;\n name: string;\n description?: string;\n interactive?: boolean;\n isDefault?: boolean;\n sortOrder?: number;\n}\n\nexport interface ParameterEnumValue {\n value: string;\n displayName: string;\n description?: string;\n isDefault?: boolean;\n sortOrder?: number;\n}\n\nexport interface ParameterEnumValues {\n values: ParameterEnumValue[];\n allowMultiple?: boolean;\n separator?: string;\n}\n\nexport type ParameterValidationType =\n | \"min_length\"\n | \"max_length\"\n | \"min_value\"\n | \"max_value\"\n | \"regex\";\n\nexport interface ParameterValidation {\n key: string;\n validationType: ParameterValidationType;\n validationValue: string;\n errorMessage: string;\n}\n\nexport type ParameterDependencyType = \"requires\" | \"conflicts_with\";\n\nexport interface ParameterDependency {\n key: string;\n parameterKey: string;\n dependsOnParameterKey: string;\n dependencyType: ParameterDependencyType;\n conditionValue?: string;\n}\n\nexport type ParameterValue = string | number | boolean | string[];\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface ToolMetadata {}\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface ParameterMetadata {\n tags?: string[];\n}\n\nexport type ParameterType = \"Flag\" | \"Option\" | \"Argument\";\n\nexport type ParameterDataType = \"String\" | \"Number\" | \"Boolean\" | \"Enum\";\n\nexport interface Parameter {\n key: string;\n name: string;\n commandKey?: string;\n description?: string;\n group?: string;\n metadata?: ParameterMetadata;\n parameterType: ParameterType;\n dataType: ParameterDataType;\n isRequired?: boolean;\n isRepeatable?: boolean;\n isGlobal?: boolean;\n shortFlag?: string;\n longFlag?: string;\n position?: number;\n sortOrder?: number;\n arraySeparator?: string;\n keyValueSeparator?: string;\n enum?: ParameterEnumValues;\n validations?: ParameterValidation[];\n dependencies?: ParameterDependency[];\n}\n\nexport type ExclusionType = \"mutual_exclusive\" | \"required_one_of\";\n\nexport interface ExclusionGroup {\n key?: string;\n commandKey?: string;\n name: string;\n exclusionType: ExclusionType;\n parameterKeys: string[];\n}\n\nexport interface Tool {\n name: string;\n displayName: string;\n info?: ToolInfo;\n commands: Command[];\n parameters: Parameter[];\n exclusionGroups?: ExclusionGroup[];\n metadata?: ToolMetadata;\n}\n", + "content": "export interface ToolInfo {\n /** A brief human-readable description of what the tool does. */\n description?: string;\n /** The version string of the tool (e.g. \"1.0.0\"). */\n version?: string;\n /** The homepage or documentation URL for the tool. */\n url?: string;\n}\n\nexport interface Command {\n /** Unique identifier for this command within the tool. */\n key: string;\n /** Key of the parent command; used to represent subcommand nesting. */\n parentCommandKey?: string;\n /** Human-readable display name of the command. */\n name: string;\n /** Brief description of what this command does. */\n description?: string;\n /** Whether this command opens an interactive session or prompt. */\n interactive?: boolean;\n /** Display sort position relative to sibling commands. */\n sortOrder?: number;\n}\n\nexport interface ParameterEnumValue {\n /** The raw value passed to the CLI for this choice. */\n value: string;\n /** Human-readable label shown to the user for this enum choice. */\n displayName: string;\n /** Description of what this enum value does or represents. */\n description?: string;\n /** Whether this is the default selection when no value is provided. */\n isDefault?: boolean;\n /** Display sort position relative to sibling enum values. */\n sortOrder?: number;\n}\n\nexport interface ParameterEnumValues {\n /** The list of allowed enum choices. */\n values: ParameterEnumValue[];\n /** Whether the user can select multiple values at once. */\n allowMultiple?: boolean;\n /** Separator character used when joining multiple selected values. */\n separator?: string;\n}\n\nexport type ParameterValidationType =\n | \"min_length\"\n | \"max_length\"\n | \"min_value\"\n | \"max_value\"\n | \"regex\";\n\nexport interface ParameterValidation {\n /** Unique identifier for this validation rule. */\n key: string;\n /** The type of validation to apply. */\n validationType: ParameterValidationType;\n /** The value to validate against (e.g. the max length number, or a regex pattern). */\n validationValue: string;\n /** The error message to display when validation fails. */\n errorMessage: string;\n}\n\nexport type ParameterDependencyType = \"requires\" | \"conflicts_with\";\n\nexport interface ParameterDependency {\n /** Unique identifier for this dependency rule. */\n key: string;\n /** Key of the parameter that owns this dependency. */\n parameterKey: string;\n /** Key of the parameter this dependency references. */\n dependsOnParameterKey: string;\n /** Whether this parameter requires or conflicts with the referenced parameter. */\n dependencyType: ParameterDependencyType;\n /** Optional value that the referenced parameter must have for this dependency to apply. */\n conditionValue?: string;\n}\n\nexport type ParameterValue = string | number | boolean | string[];\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface ToolMetadata {}\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface ParameterMetadata {\n /** Arbitrary tags for categorising or filtering parameters. */\n tags?: string[];\n}\n\nexport type ParameterType = \"Flag\" | \"Option\" | \"Argument\";\n\nexport type ParameterDataType = \"String\" | \"Number\" | \"Boolean\" | \"Enum\";\n\nexport interface Parameter {\n /** Unique identifier for this parameter within the tool. */\n key: string;\n /** Human-readable display name of the parameter. */\n name: string;\n /** Key of the command this parameter belongs to; omit for global parameters. */\n commandKey?: string;\n /** Brief description of what this parameter does or accepts. */\n description?: string;\n /** Optional grouping label for organising related parameters in the UI. */\n group?: string;\n /** Additional metadata such as tags. */\n metadata?: ParameterMetadata;\n /** Whether this is a boolean flag, a key-value option, or a positional argument. */\n parameterType: ParameterType;\n /** The data type of the parameter's value. */\n dataType: ParameterDataType;\n /** Whether the user must provide this parameter. */\n isRequired?: boolean;\n /** Whether this parameter can be specified multiple times. */\n isRepeatable?: boolean;\n /** Whether this parameter applies to all commands rather than a single command. */\n isGlobal?: boolean;\n /** The single-character short flag (e.g. \"-v\"). */\n shortFlag?: string;\n /** The long-form flag or option name (e.g. \"--verbose\"). */\n longFlag?: string;\n /** Zero-based position index for positional arguments. */\n position?: number;\n /** Display sort position relative to sibling parameters. */\n sortOrder?: number;\n /** Separator character used when the option accepts multiple values in one argument (e.g. \",\"). */\n arraySeparator?: string;\n /** Separator between key and value for key=value style options (e.g. \"=\"). */\n keyValueSeparator?: string;\n /** Allowed enum choices when dataType is \"Enum\". */\n enum?: ParameterEnumValues;\n /** Validation rules applied to this parameter's value. */\n validations?: ParameterValidation[];\n /** Dependencies on other parameters (requires or conflicts-with relationships). */\n dependencies?: ParameterDependency[];\n}\n\nexport type ExclusionType = \"mutual_exclusive\" | \"required_one_of\";\n\nexport interface ExclusionGroup {\n /** Unique identifier for this exclusion group. */\n key?: string;\n /** Key of the command this exclusion group belongs to; omit for global groups. */\n commandKey?: string;\n /** Human-readable name for this exclusion group. */\n name: string;\n /** Whether parameters in this group are mutually exclusive or one is required. */\n exclusionType: ExclusionType;\n /** Keys of the parameters that participate in this exclusion group. */\n parameterKeys: string[];\n}\n\nexport interface Tool {\n /** Unique machine-readable identifier for the tool (e.g. \"httpx\"). */\n name: string;\n /** Human-readable display name for the tool (e.g. \"HTTPx\"). */\n displayName: string;\n /** General information about the tool such as description, version, and URL. */\n info?: ToolInfo;\n /** List of all commands and subcommands defined for this tool. */\n commands: Command[];\n /** Flat list of all parameters across all commands and global scope. */\n parameters: Parameter[];\n /** Groups of parameters with mutual exclusion or required-one-of constraints. */\n exclusionGroups?: ExclusionGroup[];\n /** Arbitrary metadata attached to the tool. */\n metadata?: ToolMetadata;\n}\n", "type": "registry:file", "target": "components/commandly/types/flat.ts" }, { "path": "registry/commandly/types/nested.ts", - "content": "import type {\n ExclusionType,\n ParameterDataType,\n ParameterDependencyType,\n ParameterEnumValues,\n ParameterMetadata,\n ParameterType,\n ParameterValidationType,\n ToolInfo,\n ToolMetadata,\n} from \"@/components/commandly/types/flat\";\n\nexport interface NestedParameterValidation {\n validationType: ParameterValidationType;\n validationValue: string;\n errorMessage: string;\n}\n\nexport interface NestedParameterDependency {\n dependsOnParameter: string;\n dependencyType: ParameterDependencyType;\n conditionValue?: string;\n}\n\nexport interface NestedParameter {\n name: string;\n description?: string;\n group?: string;\n parameterType: ParameterType;\n dataType: ParameterDataType;\n metadata?: ParameterMetadata;\n isRequired?: boolean;\n isRepeatable?: boolean;\n isGlobal?: boolean;\n shortFlag?: string;\n longFlag?: string;\n position?: number;\n sortOrder?: number;\n arraySeparator?: string;\n keyValueSeparator?: string;\n enum?: ParameterEnumValues;\n validations?: NestedParameterValidation[];\n dependencies?: NestedParameterDependency[];\n}\n\nexport interface NestedCommand {\n name: string;\n description?: string;\n interactive?: boolean;\n isDefault: boolean;\n sortOrder: number;\n parameters: NestedParameter[];\n subcommands: NestedCommand[];\n}\n\nexport interface NestedExclusionGroup {\n name: string;\n exclusionType: ExclusionType;\n parameters: string[];\n}\n\nexport interface NestedTool {\n $schema?: string;\n name: string;\n displayName: string;\n info?: ToolInfo;\n url?: string;\n globalParameters: NestedParameter[];\n commands: NestedCommand[];\n exclusionGroups?: NestedExclusionGroup[] | null;\n metadata?: ToolMetadata;\n}\n", + "content": "import type {\n ExclusionType,\n ParameterDataType,\n ParameterDependencyType,\n ParameterEnumValues,\n ParameterMetadata,\n ParameterType,\n ParameterValidationType,\n ToolInfo,\n ToolMetadata,\n} from \"@/components/commandly/types/flat\";\n\nexport interface NestedParameterValidation {\n /** The type of validation to apply. */\n validationType: ParameterValidationType;\n /** The value to validate against (e.g. max length number or regex pattern). */\n validationValue: string;\n /** The error message to display when validation fails. */\n errorMessage: string;\n}\n\nexport interface NestedParameterDependency {\n /** Name of the parameter this dependency references. */\n dependsOnParameter: string;\n /** Whether this parameter requires or conflicts with the referenced parameter. */\n dependencyType: ParameterDependencyType;\n /** Optional value that the referenced parameter must have for this dependency to apply. */\n conditionValue?: string;\n}\n\nexport interface NestedParameter {\n /** Human-readable display name of the parameter. */\n name: string;\n /** Brief description of what this parameter does or accepts. */\n description?: string;\n /** Optional grouping label for organising related parameters in the UI. */\n group?: string;\n /** Whether this is a boolean flag, a key-value option, or a positional argument. */\n parameterType: ParameterType;\n /** The data type of the parameter's value. */\n dataType: ParameterDataType;\n /** Additional metadata such as tags. */\n metadata?: ParameterMetadata;\n /** Whether the user must provide this parameter. */\n isRequired?: boolean;\n /** Whether this parameter can be specified multiple times. */\n isRepeatable?: boolean;\n /** Whether this parameter applies to all commands rather than a single command. */\n isGlobal?: boolean;\n /** The single-character short flag (e.g. \"-v\"). */\n shortFlag?: string;\n /** The long-form flag or option name (e.g. \"--verbose\"). */\n longFlag?: string;\n /** Zero-based position index for positional arguments. */\n position?: number;\n /** Display sort position relative to sibling parameters. */\n sortOrder?: number;\n /** Separator character used when the option accepts multiple values in one argument (e.g. \",\"). */\n arraySeparator?: string;\n /** Separator between key and value for key=value style options (e.g. \"=\"). */\n keyValueSeparator?: string;\n /** Allowed enum choices when dataType is \"Enum\". */\n enum?: ParameterEnumValues;\n /** Validation rules applied to this parameter's value. */\n validations?: NestedParameterValidation[];\n /** Dependencies on other parameters (requires or conflicts-with relationships). */\n dependencies?: NestedParameterDependency[];\n}\n\nexport interface NestedCommand {\n /** Human-readable display name of the command. */\n name: string;\n /** Brief description of what this command does. */\n description?: string;\n /** Whether this command opens an interactive session or prompt. */\n interactive?: boolean;\n /** Display sort position relative to sibling commands. */\n sortOrder: number;\n /** Parameters that belong directly to this command. */\n parameters: NestedParameter[];\n /** Nested subcommands of this command. */\n subcommands: NestedCommand[];\n}\n\nexport interface NestedExclusionGroup {\n /** Human-readable name for this exclusion group. */\n name: string;\n /** Whether parameters in this group are mutually exclusive or one is required. */\n exclusionType: ExclusionType;\n /** Names of the parameters that participate in this exclusion group. */\n parameters: string[];\n}\n\nexport interface NestedTool {\n /** Optional JSON schema URI for validation. */\n $schema?: string;\n /** Unique machine-readable identifier for the tool (e.g. \"httpx\"). */\n name: string;\n /** Human-readable display name for the tool (e.g. \"HTTPx\"). */\n displayName: string;\n /** General information about the tool such as description, version, and URL. */\n info?: ToolInfo;\n /** The homepage or documentation URL for the tool. */\n url?: string;\n /** Parameters that apply to all commands globally. */\n globalParameters: NestedParameter[];\n /** Hierarchical list of commands and their nested subcommands. */\n commands: NestedCommand[];\n /** Groups of parameters with mutual exclusion or required-one-of constraints. */\n exclusionGroups?: NestedExclusionGroup[] | null;\n /** Arbitrary metadata attached to the tool. */\n metadata?: ToolMetadata;\n}\n", "type": "registry:file", "target": "components/commandly/types/nested.ts" }, { "path": "registry/commandly/utils/flat.ts", - "content": "import type { Command, Parameter, Tool } from \"@/components/commandly/types/flat\";\n\nexport const slugify = (text: string): string => {\n return text\n .toString()\n .toLowerCase()\n .trim()\n .replace(/\\s+/g, \"-\") // Replace spaces with -\n .replace(/[^\\w-]+/g, \"\") // Remove all non-word chars\n .replace(/--+/g, \"-\") // Replace multiple - with single -\n .replace(/^-+/, \"\") // Trim - from start of text\n .replace(/-+$/, \"\"); // Trim - from end of text\n};\n\nexport const getCommandPath = (command: Command, tool: Tool): string => {\n const findCommandPath = (\n targetKey: string,\n commands: Command[],\n path: string[] = [],\n ): string[] | null => {\n for (const cmd of commands) {\n if (cmd.name === targetKey) {\n return [...path, cmd.name];\n }\n\n const childCommands = commands.filter((c) => c.parentCommandKey === cmd.key);\n if (childCommands.length > 0) {\n const subPath = findCommandPath(targetKey, childCommands, [...path, cmd.name]);\n if (subPath) {\n return subPath;\n }\n }\n }\n return null;\n };\n\n const rootCommands = tool.commands.filter((c) => !c.parentCommandKey);\n const path = findCommandPath(command.name, rootCommands);\n\n if (!path) return command.name;\n\n if (command.name === tool.name && command.isDefault) {\n return tool.name;\n }\n\n const rootCommand = tool.commands.find((c) => c.name === tool.name);\n if (rootCommand?.isDefault && path[0] === tool.name) {\n path[0] = tool.name;\n }\n\n return path.join(\" \");\n};\n\nexport const getAllSubcommands = (commandKey: string, commands: Command[]): Command[] => {\n const result: Command[] = [];\n\n const findSubcommands = (parentKey: string) => {\n commands.forEach((cmd) => {\n if (cmd.parentCommandKey === parentKey) {\n result.push(cmd);\n findSubcommands(cmd.key);\n }\n });\n };\n\n findSubcommands(commandKey);\n return result;\n};\n\nconst SCHEMA_URL = \"https://commandly.divyeshio.in/specification/flat.json\";\n\nexport const sanitizeToolJSON = (tool: Tool) => {\n const parameters = tool.parameters.map(({ metadata: _metadata, ...param }) => param);\n\n return {\n $schema: SCHEMA_URL,\n ...tool,\n parameters,\n };\n};\n\nexport const exportToStructuredJSON = (tool: Tool) => {\n return {\n $schema: SCHEMA_URL,\n name: tool.name,\n displayName: tool.displayName,\n info: tool.info,\n url: tool.info?.url,\n commands: tool.commands.map((cmd) => ({ ...cmd })),\n parameters: tool.parameters.map(({ metadata: _metadata, ...param }) => param),\n exclusionGroups: tool.exclusionGroups,\n metadata: tool.metadata,\n };\n};\n\nexport const createNewCommand = (parentKey?: string): Command => {\n const name = randomCommandName();\n return {\n key: slugify(name),\n parentCommandKey: parentKey,\n name,\n isDefault: false,\n sortOrder: 1,\n };\n};\n\nexport const createNewParameter = (isGlobal: boolean, commandKey?: string): Parameter => {\n return {\n key: \"\",\n name: \"\",\n commandKey: isGlobal ? undefined : commandKey,\n parameterType: \"Option\",\n dataType: \"String\",\n ...(isGlobal ? { isGlobal: true } : {}),\n longFlag: \"\",\n };\n};\n\nexport const randomCommandName = () => {\n const characters = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\";\n let result = \"\";\n const charactersLength = characters.length;\n for (let i = 0; i < 7; i++) {\n result += characters.charAt(Math.floor(Math.random() * charactersLength));\n }\n return result;\n};\n\nconst isEmpty = (value: object | null | undefined): boolean => {\n if (value == null) return true;\n if (Array.isArray(value)) return value.length === 0;\n return Object.keys(value).length === 0;\n};\n\nconst cleanParameter = (param: Parameter): Parameter => {\n const cleaned = { ...param };\n\n if (!cleaned.enum || cleaned.enum.values.length === 0) delete cleaned.enum;\n if (isEmpty(cleaned.validations)) delete cleaned.validations;\n if (isEmpty(cleaned.dependencies)) delete cleaned.dependencies;\n\n if (cleaned.metadata) {\n const meta = { ...cleaned.metadata };\n if (isEmpty(meta.tags)) delete meta.tags;\n if (isEmpty(meta)) {\n delete cleaned.metadata;\n } else {\n cleaned.metadata = meta;\n }\n }\n\n return cleaned;\n};\n\nexport const cleanupTool = (tool: Tool): Tool => {\n const cleaned = { ...tool };\n\n if (isEmpty(cleaned.exclusionGroups)) delete cleaned.exclusionGroups;\n if (isEmpty(cleaned.metadata)) delete cleaned.metadata;\n\n cleaned.parameters = cleaned.parameters.map(cleanParameter);\n\n return cleaned;\n};\n", + "content": "import type { Command, Parameter, Tool } from \"@/components/commandly/types/flat\";\n\nexport const slugify = (text: string): string => {\n return text\n .toString()\n .toLowerCase()\n .trim()\n .replace(/\\s+/g, \"-\") // Replace spaces with -\n .replace(/[^\\w-]+/g, \"\") // Remove all non-word chars\n .replace(/--+/g, \"-\") // Replace multiple - with single -\n .replace(/^-+/, \"\") // Trim - from start of text\n .replace(/-+$/, \"\"); // Trim - from end of text\n};\n\nexport const getCommandPath = (command: Command, tool: Tool): string => {\n const findCommandPath = (\n targetKey: string,\n commands: Command[],\n path: string[] = [],\n ): string[] | null => {\n for (const cmd of commands) {\n if (cmd.name === targetKey) {\n return [...path, cmd.name];\n }\n\n const childCommands = commands.filter((c) => c.parentCommandKey === cmd.key);\n if (childCommands.length > 0) {\n const subPath = findCommandPath(targetKey, childCommands, [...path, cmd.name]);\n if (subPath) {\n return subPath;\n }\n }\n }\n return null;\n };\n\n const rootCommands = tool.commands.filter((c) => !c.parentCommandKey);\n const path = findCommandPath(command.name, rootCommands);\n\n if (!path) return command.name;\n\n return path.join(\" \");\n};\n\nexport const getAllSubcommands = (commandKey: string, commands: Command[]): Command[] => {\n const result: Command[] = [];\n\n const findSubcommands = (parentKey: string) => {\n commands.forEach((cmd) => {\n if (cmd.parentCommandKey === parentKey) {\n result.push(cmd);\n findSubcommands(cmd.key);\n }\n });\n };\n\n findSubcommands(commandKey);\n return result;\n};\n\nconst SCHEMA_URL = \"https://commandly.divyeshio.in/specification/flat.json\";\n\nexport const sanitizeToolJSON = (tool: Tool) => {\n const parameters = tool.parameters.map(({ metadata: _metadata, ...param }) => param);\n\n return {\n $schema: SCHEMA_URL,\n ...tool,\n parameters,\n };\n};\n\nexport const exportToStructuredJSON = (tool: Tool) => {\n return {\n $schema: SCHEMA_URL,\n name: tool.name,\n displayName: tool.displayName,\n info: tool.info,\n commands: tool.commands.map((cmd) => ({ ...cmd })),\n parameters: tool.parameters.map(({ metadata: _metadata, ...param }) => param),\n exclusionGroups: tool.exclusionGroups,\n metadata: tool.metadata,\n };\n};\n\nexport const createNewParameter = (isGlobal: boolean, commandKey?: string): Parameter => {\n return {\n key: \"\",\n name: \"\",\n commandKey: isGlobal ? undefined : commandKey,\n parameterType: \"Option\",\n dataType: \"String\",\n ...(isGlobal ? { isGlobal: true } : {}),\n longFlag: \"\",\n };\n};\n\nconst isEmpty = (value: object | null | undefined): boolean => {\n if (value == null) return true;\n if (Array.isArray(value)) return value.length === 0;\n return Object.keys(value).length === 0;\n};\n\nconst cleanParameter = (param: Parameter): Parameter => {\n const cleaned = { ...param };\n\n if (!cleaned.enum || cleaned.enum.values.length === 0) delete cleaned.enum;\n if (isEmpty(cleaned.validations)) delete cleaned.validations;\n if (isEmpty(cleaned.dependencies)) delete cleaned.dependencies;\n\n if (cleaned.metadata) {\n const meta = { ...cleaned.metadata };\n if (isEmpty(meta.tags)) delete meta.tags;\n if (isEmpty(meta)) {\n delete cleaned.metadata;\n } else {\n cleaned.metadata = meta;\n }\n }\n\n return cleaned;\n};\n\nexport const cleanupTool = (tool: Tool): Tool => {\n const cleaned = { ...tool };\n\n if (isEmpty(cleaned.exclusionGroups)) delete cleaned.exclusionGroups;\n if (isEmpty(cleaned.metadata)) delete cleaned.metadata;\n\n cleaned.parameters = cleaned.parameters.map(cleanParameter);\n\n return cleaned;\n};\n", "type": "registry:file", "target": "components/commandly/utils/flat.ts" }, { "path": "registry/commandly/utils/nested.ts", - "content": "import type { Tool, Command, Parameter } from \"@/components/commandly/types/flat\";\nimport {\n NestedCommand,\n NestedExclusionGroup,\n NestedParameter,\n NestedTool,\n} from \"@/components/commandly/types/nested\";\n\nexport const convertToNestedStructure = (tool: Tool): NestedTool => {\n const globalParameters = tool.parameters.filter((p) => p.isGlobal);\n\n const convertParameter = (param: Parameter): NestedParameter => {\n const { ...rest } = param;\n return {\n ...rest,\n validations: param.validations?.map((v) => {\n return {\n validationType: v.validationType,\n validationValue: v.validationValue,\n errorMessage: v.errorMessage,\n };\n }),\n metadata: param.metadata,\n dataType: param.dataType,\n dependencies: param.dependencies?.map((dep) => {\n const dependsOnParam = tool.parameters.find((p) => p.key === dep.dependsOnParameterKey);\n return {\n dependsOnParameter: dependsOnParam?.longFlag || \"\",\n dependencyType: dep.dependencyType,\n conditionValue: dep.conditionValue,\n };\n }),\n };\n };\n\n const buildNestedCommands = (commands: Command[], parentKey?: string): NestedCommand[] => {\n return commands\n .filter((cmd) => cmd.parentCommandKey === parentKey)\n .map((cmd) => {\n const commandParameters = tool.parameters.filter(\n (p) => p.commandKey === cmd.key && !p.isGlobal,\n );\n return {\n name: cmd.name,\n description: cmd.description,\n interactive: cmd.interactive,\n isDefault: cmd.isDefault ?? false,\n sortOrder: cmd.sortOrder ?? 0,\n parameters: commandParameters.map(convertParameter),\n subcommands: buildNestedCommands(commands, cmd.key),\n };\n });\n };\n\n const nestedExclusionGroups: NestedExclusionGroup[] | undefined = tool.exclusionGroups?.map(\n (group) => {\n return {\n name: group.name,\n exclusionType: group.exclusionType,\n parameters: group.parameterKeys.map((pk) => {\n const param = tool.parameters.find((p) => p.key === pk);\n return param?.longFlag || \"\";\n }),\n };\n },\n );\n\n return {\n $schema: \"https://commandly.divyeshio.in/specification/nested.json\",\n name: tool.name,\n url: tool.info?.url,\n displayName: tool.displayName,\n info: tool.info,\n metadata: tool.metadata,\n globalParameters: globalParameters.map(convertParameter),\n commands: buildNestedCommands(tool.commands),\n exclusionGroups: nestedExclusionGroups,\n };\n};\n", + "content": "import type { Tool, Command, Parameter } from \"@/components/commandly/types/flat\";\nimport {\n NestedCommand,\n NestedExclusionGroup,\n NestedParameter,\n NestedTool,\n} from \"@/components/commandly/types/nested\";\n\nexport const convertToNestedStructure = (tool: Tool): NestedTool => {\n const globalParameters = tool.parameters.filter((p) => p.isGlobal);\n\n const convertParameter = (param: Parameter): NestedParameter => {\n const { ...rest } = param;\n return {\n ...rest,\n validations: param.validations?.map((v) => {\n return {\n validationType: v.validationType,\n validationValue: v.validationValue,\n errorMessage: v.errorMessage,\n };\n }),\n metadata: param.metadata,\n dataType: param.dataType,\n dependencies: param.dependencies?.map((dep) => {\n const dependsOnParam = tool.parameters.find((p) => p.key === dep.dependsOnParameterKey);\n return {\n dependsOnParameter: dependsOnParam?.longFlag || \"\",\n dependencyType: dep.dependencyType,\n conditionValue: dep.conditionValue,\n };\n }),\n };\n };\n\n const buildNestedCommands = (commands: Command[], parentKey?: string): NestedCommand[] => {\n return commands\n .filter((cmd) => cmd.parentCommandKey === parentKey)\n .map((cmd) => {\n const commandParameters = tool.parameters.filter(\n (p) => p.commandKey === cmd.key && !p.isGlobal,\n );\n return {\n name: cmd.name,\n description: cmd.description,\n interactive: cmd.interactive,\n sortOrder: cmd.sortOrder ?? 0,\n parameters: commandParameters.map(convertParameter),\n subcommands: buildNestedCommands(commands, cmd.key),\n };\n });\n };\n\n const nestedExclusionGroups: NestedExclusionGroup[] | undefined = tool.exclusionGroups?.map(\n (group) => {\n return {\n name: group.name,\n exclusionType: group.exclusionType,\n parameters: group.parameterKeys.map((pk) => {\n const param = tool.parameters.find((p) => p.key === pk);\n return param?.longFlag || \"\";\n }),\n };\n },\n );\n\n return {\n $schema: \"https://commandly.divyeshio.in/specification/nested.json\",\n name: tool.name,\n url: tool.info?.url,\n displayName: tool.displayName,\n info: tool.info,\n metadata: tool.metadata,\n globalParameters: globalParameters.map(convertParameter),\n commands: buildNestedCommands(tool.commands),\n exclusionGroups: nestedExclusionGroups,\n };\n};\n", "type": "registry:file", "target": "components/commandly/utils/nested.ts" } diff --git a/public/specification/flat.json b/public/specification/flat.json index bc38b91..e847c65 100644 --- a/public/specification/flat.json +++ b/public/specification/flat.json @@ -2,33 +2,40 @@ "type": "object", "properties": { "name": { + "description": "Unique machine-readable identifier for the tool (e.g. \"httpx\").", "type": "string" }, "displayName": { + "description": "Human-readable display name for the tool (e.g. \"HTTPx\").", "type": "string" }, "info": { + "description": "General information about the tool such as description, version, and URL.", "$ref": "#/definitions/ToolInfo" }, "commands": { + "description": "List of all commands and subcommands defined for this tool.", "type": "array", "items": { "$ref": "#/definitions/Command" } }, "parameters": { + "description": "Flat list of all parameters across all commands and global scope.", "type": "array", "items": { "$ref": "#/definitions/Parameter" } }, "exclusionGroups": { + "description": "Groups of parameters with mutual exclusion or required-one-of constraints.", "type": "array", "items": { "$ref": "#/definitions/ExclusionGroup" } }, "metadata": { + "description": "Arbitrary metadata attached to the tool.", "$ref": "#/definitions/ToolMetadata" } }, @@ -43,12 +50,15 @@ "type": "object", "properties": { "description": { + "description": "A brief human-readable description of what the tool does.", "type": "string" }, "version": { + "description": "The version string of the tool (e.g. \"1.0.0\").", "type": "string" }, "url": { + "description": "The homepage or documentation URL for the tool.", "type": "string" } } @@ -57,24 +67,27 @@ "type": "object", "properties": { "key": { + "description": "Unique identifier for this command within the tool.", "type": "string" }, "parentCommandKey": { + "description": "Key of the parent command; used to represent subcommand nesting.", "type": "string" }, "name": { + "description": "Human-readable display name of the command.", "type": "string" }, "description": { + "description": "Brief description of what this command does.", "type": "string" }, "interactive": { - "type": "boolean" - }, - "isDefault": { + "description": "Whether this command opens an interactive session or prompt.", "type": "boolean" }, "sortOrder": { + "description": "Display sort position relative to sibling commands.", "type": "number" } }, @@ -87,66 +100,86 @@ "type": "object", "properties": { "key": { + "description": "Unique identifier for this parameter within the tool.", "type": "string" }, "name": { + "description": "Human-readable display name of the parameter.", "type": "string" }, "commandKey": { + "description": "Key of the command this parameter belongs to; omit for global parameters.", "type": "string" }, "description": { + "description": "Brief description of what this parameter does or accepts.", "type": "string" }, "group": { + "description": "Optional grouping label for organising related parameters in the UI.", "type": "string" }, "metadata": { + "description": "Additional metadata such as tags.", "$ref": "#/definitions/ParameterMetadata" }, "parameterType": { - "$ref": "#/definitions/ParameterType" + "$ref": "#/definitions/ParameterType", + "description": "Whether this is a boolean flag, a key-value option, or a positional argument." }, "dataType": { - "$ref": "#/definitions/ParameterDataType" + "$ref": "#/definitions/ParameterDataType", + "description": "The data type of the parameter's value." }, "isRequired": { + "description": "Whether the user must provide this parameter.", "type": "boolean" }, "isRepeatable": { + "description": "Whether this parameter can be specified multiple times.", "type": "boolean" }, "isGlobal": { + "description": "Whether this parameter applies to all commands rather than a single command.", "type": "boolean" }, "shortFlag": { + "description": "The single-character short flag (e.g. \"-v\").", "type": "string" }, "longFlag": { + "description": "The long-form flag or option name (e.g. \"--verbose\").", "type": "string" }, "position": { + "description": "Zero-based position index for positional arguments.", "type": "number" }, "sortOrder": { + "description": "Display sort position relative to sibling parameters.", "type": "number" }, "arraySeparator": { + "description": "Separator character used when the option accepts multiple values in one argument (e.g. \",\").", "type": "string" }, "keyValueSeparator": { + "description": "Separator between key and value for key=value style options (e.g. \"=\").", "type": "string" }, "enum": { + "description": "Allowed enum choices when dataType is \"Enum\".", "$ref": "#/definitions/ParameterEnumValues" }, "validations": { + "description": "Validation rules applied to this parameter's value.", "type": "array", "items": { "$ref": "#/definitions/ParameterValidation" } }, "dependencies": { + "description": "Dependencies on other parameters (requires or conflicts-with relationships).", "type": "array", "items": { "$ref": "#/definitions/ParameterDependency" @@ -164,6 +197,7 @@ "type": "object", "properties": { "tags": { + "description": "Arbitrary tags for categorising or filtering parameters.", "type": "array", "items": { "type": "string" @@ -192,15 +226,18 @@ "type": "object", "properties": { "values": { + "description": "The list of allowed enum choices.", "type": "array", "items": { "$ref": "#/definitions/ParameterEnumValue" } }, "allowMultiple": { + "description": "Whether the user can select multiple values at once.", "type": "boolean" }, "separator": { + "description": "Separator character used when joining multiple selected values.", "type": "string" } }, @@ -212,18 +249,23 @@ "type": "object", "properties": { "value": { + "description": "The raw value passed to the CLI for this choice.", "type": "string" }, "displayName": { + "description": "Human-readable label shown to the user for this enum choice.", "type": "string" }, "description": { + "description": "Description of what this enum value does or represents.", "type": "string" }, "isDefault": { + "description": "Whether this is the default selection when no value is provided.", "type": "boolean" }, "sortOrder": { + "description": "Display sort position relative to sibling enum values.", "type": "number" } }, @@ -236,15 +278,19 @@ "type": "object", "properties": { "key": { + "description": "Unique identifier for this validation rule.", "type": "string" }, "validationType": { - "$ref": "#/definitions/ParameterValidationType" + "$ref": "#/definitions/ParameterValidationType", + "description": "The type of validation to apply." }, "validationValue": { + "description": "The value to validate against (e.g. the max length number, or a regex pattern).", "type": "string" }, "errorMessage": { + "description": "The error message to display when validation fails.", "type": "string" } }, @@ -269,18 +315,23 @@ "type": "object", "properties": { "key": { + "description": "Unique identifier for this dependency rule.", "type": "string" }, "parameterKey": { + "description": "Key of the parameter that owns this dependency.", "type": "string" }, "dependsOnParameterKey": { + "description": "Key of the parameter this dependency references.", "type": "string" }, "dependencyType": { - "$ref": "#/definitions/ParameterDependencyType" + "$ref": "#/definitions/ParameterDependencyType", + "description": "Whether this parameter requires or conflicts with the referenced parameter." }, "conditionValue": { + "description": "Optional value that the referenced parameter must have for this dependency to apply.", "type": "string" } }, @@ -302,18 +353,23 @@ "type": "object", "properties": { "key": { + "description": "Unique identifier for this exclusion group.", "type": "string" }, "commandKey": { + "description": "Key of the command this exclusion group belongs to; omit for global groups.", "type": "string" }, "name": { + "description": "Human-readable name for this exclusion group.", "type": "string" }, "exclusionType": { - "$ref": "#/definitions/ExclusionType" + "$ref": "#/definitions/ExclusionType", + "description": "Whether parameters in this group are mutually exclusive or one is required." }, "parameterKeys": { + "description": "Keys of the parameters that participate in this exclusion group.", "type": "array", "items": { "type": "string" diff --git a/public/specification/nested.json b/public/specification/nested.json index b7dd801..e7361c1 100644 --- a/public/specification/nested.json +++ b/public/specification/nested.json @@ -2,33 +2,41 @@ "type": "object", "properties": { "$schema": { + "description": "Optional JSON schema URI for validation.", "type": "string" }, "name": { + "description": "Unique machine-readable identifier for the tool (e.g. \"httpx\").", "type": "string" }, "displayName": { + "description": "Human-readable display name for the tool (e.g. \"HTTPx\").", "type": "string" }, "info": { + "description": "General information about the tool such as description, version, and URL.", "$ref": "#/definitions/ToolInfo" }, "url": { + "description": "The homepage or documentation URL for the tool.", "type": "string" }, "globalParameters": { + "description": "Parameters that apply to all commands globally.", "type": "array", "items": { "$ref": "#/definitions/NestedParameter" } }, "commands": { + "description": "Hierarchical list of commands and their nested subcommands.", "type": "array", "items": { "$ref": "#/definitions/NestedCommand" } }, "exclusionGroups": { + "description": "Groups of parameters with mutual exclusion or required-one-of constraints.", "anyOf": [ { "type": "array", @@ -42,6 +50,7 @@ ] }, "metadata": { + "description": "Arbitrary metadata attached to the tool.", "$ref": "#/definitions/ToolMetadata" } }, @@ -56,12 +65,15 @@ "type": "object", "properties": { "description": { + "description": "A brief human-readable description of what the tool does.", "type": "string" }, "version": { + "description": "The version string of the tool (e.g. \"1.0.0\").", "type": "string" }, "url": { + "description": "The homepage or documentation URL for the tool.", "type": "string" } } @@ -70,60 +82,78 @@ "type": "object", "properties": { "name": { + "description": "Human-readable display name of the parameter.", "type": "string" }, "description": { + "description": "Brief description of what this parameter does or accepts.", "type": "string" }, "group": { + "description": "Optional grouping label for organising related parameters in the UI.", "type": "string" }, "parameterType": { - "$ref": "#/definitions/ParameterType" + "$ref": "#/definitions/ParameterType", + "description": "Whether this is a boolean flag, a key-value option, or a positional argument." }, "dataType": { - "$ref": "#/definitions/ParameterDataType" + "$ref": "#/definitions/ParameterDataType", + "description": "The data type of the parameter's value." }, "metadata": { + "description": "Additional metadata such as tags.", "$ref": "#/definitions/ParameterMetadata" }, "isRequired": { + "description": "Whether the user must provide this parameter.", "type": "boolean" }, "isRepeatable": { + "description": "Whether this parameter can be specified multiple times.", "type": "boolean" }, "isGlobal": { + "description": "Whether this parameter applies to all commands rather than a single command.", "type": "boolean" }, "shortFlag": { + "description": "The single-character short flag (e.g. \"-v\").", "type": "string" }, "longFlag": { + "description": "The long-form flag or option name (e.g. \"--verbose\").", "type": "string" }, "position": { + "description": "Zero-based position index for positional arguments.", "type": "number" }, "sortOrder": { + "description": "Display sort position relative to sibling parameters.", "type": "number" }, "arraySeparator": { + "description": "Separator character used when the option accepts multiple values in one argument (e.g. \",\").", "type": "string" }, "keyValueSeparator": { + "description": "Separator between key and value for key=value style options (e.g. \"=\").", "type": "string" }, "enum": { + "description": "Allowed enum choices when dataType is \"Enum\".", "$ref": "#/definitions/ParameterEnumValues" }, "validations": { + "description": "Validation rules applied to this parameter's value.", "type": "array", "items": { "$ref": "#/definitions/NestedParameterValidation" } }, "dependencies": { + "description": "Dependencies on other parameters (requires or conflicts-with relationships).", "type": "array", "items": { "$ref": "#/definitions/NestedParameterDependency" @@ -157,6 +187,7 @@ "type": "object", "properties": { "tags": { + "description": "Arbitrary tags for categorising or filtering parameters.", "type": "array", "items": { "type": "string" @@ -168,15 +199,18 @@ "type": "object", "properties": { "values": { + "description": "The list of allowed enum choices.", "type": "array", "items": { "$ref": "#/definitions/ParameterEnumValue" } }, "allowMultiple": { + "description": "Whether the user can select multiple values at once.", "type": "boolean" }, "separator": { + "description": "Separator character used when joining multiple selected values.", "type": "string" } }, @@ -188,18 +222,23 @@ "type": "object", "properties": { "value": { + "description": "The raw value passed to the CLI for this choice.", "type": "string" }, "displayName": { + "description": "Human-readable label shown to the user for this enum choice.", "type": "string" }, "description": { + "description": "Description of what this enum value does or represents.", "type": "string" }, "isDefault": { + "description": "Whether this is the default selection when no value is provided.", "type": "boolean" }, "sortOrder": { + "description": "Display sort position relative to sibling enum values.", "type": "number" } }, @@ -212,12 +251,15 @@ "type": "object", "properties": { "validationType": { - "$ref": "#/definitions/ParameterValidationType" + "$ref": "#/definitions/ParameterValidationType", + "description": "The type of validation to apply." }, "validationValue": { + "description": "The value to validate against (e.g. max length number or regex pattern).", "type": "string" }, "errorMessage": { + "description": "The error message to display when validation fails.", "type": "string" } }, @@ -241,12 +283,15 @@ "type": "object", "properties": { "dependsOnParameter": { + "description": "Name of the parameter this dependency references.", "type": "string" }, "dependencyType": { - "$ref": "#/definitions/ParameterDependencyType" + "$ref": "#/definitions/ParameterDependencyType", + "description": "Whether this parameter requires or conflicts with the referenced parameter." }, "conditionValue": { + "description": "Optional value that the referenced parameter must have for this dependency to apply.", "type": "string" } }, @@ -266,27 +311,30 @@ "type": "object", "properties": { "name": { + "description": "Human-readable display name of the command.", "type": "string" }, "description": { + "description": "Brief description of what this command does.", "type": "string" }, "interactive": { - "type": "boolean" - }, - "isDefault": { + "description": "Whether this command opens an interactive session or prompt.", "type": "boolean" }, "sortOrder": { + "description": "Display sort position relative to sibling commands.", "type": "number" }, "parameters": { + "description": "Parameters that belong directly to this command.", "type": "array", "items": { "$ref": "#/definitions/NestedParameter" } }, "subcommands": { + "description": "Nested subcommands of this command.", "type": "array", "items": { "$ref": "#/definitions/NestedCommand" @@ -294,7 +342,6 @@ } }, "required": [ - "isDefault", "name", "parameters", "sortOrder", @@ -305,12 +352,15 @@ "type": "object", "properties": { "name": { + "description": "Human-readable name for this exclusion group.", "type": "string" }, "exclusionType": { - "$ref": "#/definitions/ExclusionType" + "$ref": "#/definitions/ExclusionType", + "description": "Whether parameters in this group are mutually exclusive or one is required." }, "parameters": { + "description": "Names of the parameters that participate in this exclusion group.", "type": "array", "items": { "type": "string" diff --git a/public/tools-collection/asnmap.json b/public/tools-collection/asnmap.json index bc16d05..3add69a 100644 --- a/public/tools-collection/asnmap.json +++ b/public/tools-collection/asnmap.json @@ -11,7 +11,6 @@ { "name": "asnmap", "description": "Map organization network ranges using ASN information.", - "isDefault": true, "sortOrder": 1, "key": "asnmap" } diff --git a/public/tools-collection/cdncheck.json b/public/tools-collection/cdncheck.json index 8c430e6..c94b5e6 100644 --- a/public/tools-collection/cdncheck.json +++ b/public/tools-collection/cdncheck.json @@ -10,7 +10,6 @@ "commands": [ { "name": "cdncheck", - "isDefault": true, "sortOrder": 1, "key": "cdncheck" } diff --git a/public/tools-collection/curl.json b/public/tools-collection/curl.json index fe3b5d1..8948d85 100644 --- a/public/tools-collection/curl.json +++ b/public/tools-collection/curl.json @@ -11,7 +11,6 @@ { "name": "curl", "description": "Run curl to download files.", - "isDefault": true, "sortOrder": 1, "key": "curl" } diff --git a/public/tools-collection/dnsx.json b/public/tools-collection/dnsx.json index f435ec7..c9bd643 100644 --- a/public/tools-collection/dnsx.json +++ b/public/tools-collection/dnsx.json @@ -11,7 +11,6 @@ { "name": "dnsx", "description": "Root command for dnsx CLI.", - "isDefault": true, "sortOrder": 1, "key": "dnsx" } diff --git a/public/tools-collection/gospider.json b/public/tools-collection/gospider.json index 8ec28ec..70404d0 100644 --- a/public/tools-collection/gospider.json +++ b/public/tools-collection/gospider.json @@ -11,7 +11,6 @@ { "name": "gospider", "description": "Fast web spider written in Go.", - "isDefault": true, "sortOrder": 1, "key": "gospider" } diff --git a/public/tools-collection/httpx.json b/public/tools-collection/httpx.json index 858b756..a2083c5 100644 --- a/public/tools-collection/httpx.json +++ b/public/tools-collection/httpx.json @@ -11,7 +11,6 @@ { "name": "httpx", "description": "Run httpx with the specified parameters.", - "isDefault": true, "sortOrder": 1, "key": "httpx" } diff --git a/public/tools-collection/katana.json b/public/tools-collection/katana.json index 0174f2b..c8803d2 100644 --- a/public/tools-collection/katana.json +++ b/public/tools-collection/katana.json @@ -11,7 +11,6 @@ { "name": "katana", "description": "Primary command for katana crawler.", - "isDefault": true, "sortOrder": 0, "key": "katana" } @@ -1114,4 +1113,4 @@ "commandKey": "katana" } ] -} \ No newline at end of file +} diff --git a/public/tools-collection/naabu.json b/public/tools-collection/naabu.json index 35a1b43..8dcbd0f 100644 --- a/public/tools-collection/naabu.json +++ b/public/tools-collection/naabu.json @@ -11,7 +11,6 @@ { "name": "naabu", "description": "Run naabu with flags.", - "isDefault": true, "sortOrder": 1, "key": "naabu" } @@ -85,22 +84,22 @@ "value": "full", "displayName": "Full", "description": "Scan full set of top ports.", - "isDefault": false, - "sortOrder": 1 + "sortOrder": 1, + "isDefault": false }, { "value": "100", "displayName": "100", "description": "Scan top 100 ports.", - "isDefault": true, - "sortOrder": 2 + "sortOrder": 2, + "isDefault": true }, { "value": "1000", "displayName": "1000", "description": "Scan top 1000 ports.", - "isDefault": false, - "sortOrder": 3 + "sortOrder": 3, + "isDefault": false } ] }, @@ -271,15 +270,15 @@ "value": "4", "displayName": "4", "description": "IPv4.", - "isDefault": true, - "sortOrder": 1 + "sortOrder": 1, + "isDefault": true }, { "value": "6", "displayName": "6", "description": "IPv6.", - "isDefault": false, - "sortOrder": 2 + "sortOrder": 2, + "isDefault": false } ] }, @@ -300,15 +299,15 @@ "value": "SYN", "displayName": "SYN", "description": "TCP SYN scan.", - "isDefault": false, - "sortOrder": 1 + "sortOrder": 1, + "isDefault": false }, { "value": "CONNECT", "displayName": "CONNECT", "description": "TCP connect scan.", - "isDefault": true, - "sortOrder": 2 + "sortOrder": 2, + "isDefault": true } ] }, @@ -845,4 +844,4 @@ "commandKey": "naabu" } ] -} +} \ No newline at end of file diff --git a/public/tools-collection/nuclei.json b/public/tools-collection/nuclei.json index 4404cc4..44b73ab 100644 --- a/public/tools-collection/nuclei.json +++ b/public/tools-collection/nuclei.json @@ -11,7 +11,6 @@ { "name": "nuclei", "description": "Run nuclei vulnerability scanner.", - "isDefault": true, "sortOrder": 1, "key": "nuclei" } @@ -82,15 +81,15 @@ "value": "4", "displayName": "4", "description": "Use IPv4.", - "isDefault": true, - "sortOrder": 1 + "sortOrder": 1, + "isDefault": true }, { "value": "6", "displayName": "6", "description": "Use IPv6.", - "isDefault": false, - "sortOrder": 2 + "sortOrder": 2, + "isDefault": false } ] }, @@ -110,43 +109,43 @@ "value": "list", "displayName": "List", "description": "Use list input mode.", - "isDefault": true, - "sortOrder": 1 + "sortOrder": 1, + "isDefault": true }, { "value": "burp", "displayName": "Burp", "description": "Use burp input mode.", - "isDefault": false, - "sortOrder": 2 + "sortOrder": 2, + "isDefault": false }, { "value": "jsonl", "displayName": "JSONL", "description": "Use jsonl input mode.", - "isDefault": false, - "sortOrder": 3 + "sortOrder": 3, + "isDefault": false }, { "value": "yaml", "displayName": "Yaml", "description": "Use yaml input mode.", - "isDefault": false, - "sortOrder": 4 + "sortOrder": 4, + "isDefault": false }, { "value": "openapi", "displayName": "OpenAPI", "description": "Use openapi input mode.", - "isDefault": false, - "sortOrder": 5 + "sortOrder": 5, + "isDefault": false }, { "value": "swagger", "displayName": "Swagger", "description": "Use swagger input mode.", - "isDefault": false, - "sortOrder": 6 + "sortOrder": 6, + "isDefault": false } ] }, @@ -474,43 +473,43 @@ "value": "info", "displayName": "Info", "description": "Information severity.", - "isDefault": false, - "sortOrder": 1 + "sortOrder": 1, + "isDefault": false }, { "value": "low", "displayName": "Low", "description": "Low severity.", - "isDefault": false, - "sortOrder": 2 + "sortOrder": 2, + "isDefault": false }, { "value": "medium", "displayName": "Medium", "description": "Medium severity.", - "isDefault": false, - "sortOrder": 3 + "sortOrder": 3, + "isDefault": false }, { "value": "high", "displayName": "High", "description": "High severity.", - "isDefault": false, - "sortOrder": 4 + "sortOrder": 4, + "isDefault": false }, { "value": "critical", "displayName": "Critical", "description": "Critical severity.", - "isDefault": false, - "sortOrder": 5 + "sortOrder": 5, + "isDefault": false }, { "value": "unknown", "displayName": "Unknown", "description": "Unknown severity.", - "isDefault": false, - "sortOrder": 6 + "sortOrder": 6, + "isDefault": false } ] }, @@ -531,43 +530,43 @@ "value": "info", "displayName": "Info", "description": "Exclude information severity.", - "isDefault": false, - "sortOrder": 1 + "sortOrder": 1, + "isDefault": false }, { "value": "low", "displayName": "Low", "description": "Exclude low severity.", - "isDefault": false, - "sortOrder": 2 + "sortOrder": 2, + "isDefault": false }, { "value": "medium", "displayName": "Medium", "description": "Exclude medium severity.", - "isDefault": false, - "sortOrder": 3 + "sortOrder": 3, + "isDefault": false }, { "value": "high", "displayName": "High", "description": "Exclude high severity.", - "isDefault": false, - "sortOrder": 4 + "sortOrder": 4, + "isDefault": false }, { "value": "critical", "displayName": "Critical", "description": "Exclude critical severity.", - "isDefault": false, - "sortOrder": 5 + "sortOrder": 5, + "isDefault": false }, { "value": "unknown", "displayName": "Unknown", "description": "Exclude unknown severity.", - "isDefault": false, - "sortOrder": 6 + "sortOrder": 6, + "isDefault": false } ] }, @@ -588,78 +587,78 @@ "value": "dns", "displayName": "Dns", "description": "DNS templates.", - "isDefault": false, - "sortOrder": 1 + "sortOrder": 1, + "isDefault": false }, { "value": "file", "displayName": "File", "description": "File templates.", - "isDefault": false, - "sortOrder": 2 + "sortOrder": 2, + "isDefault": false }, { "value": "http", "displayName": "Http", "description": "HTTP templates.", - "isDefault": false, - "sortOrder": 3 + "sortOrder": 3, + "isDefault": false }, { "value": "headless", "displayName": "Headless", "description": "Headless templates.", - "isDefault": false, - "sortOrder": 4 + "sortOrder": 4, + "isDefault": false }, { "value": "tcp", "displayName": "Tcp", "description": "TCP templates.", - "isDefault": false, - "sortOrder": 5 + "sortOrder": 5, + "isDefault": false }, { "value": "workflow", "displayName": "Workflow", "description": "Workflow templates.", - "isDefault": false, - "sortOrder": 6 + "sortOrder": 6, + "isDefault": false }, { "value": "ssl", "displayName": "Ssl", "description": "SSL templates.", - "isDefault": false, - "sortOrder": 7 + "sortOrder": 7, + "isDefault": false }, { "value": "websocket", "displayName": "Websocket", "description": "Websocket templates.", - "isDefault": false, - "sortOrder": 8 + "sortOrder": 8, + "isDefault": false }, { "value": "whois", "displayName": "Whois", "description": "WHOIS templates.", - "isDefault": false, - "sortOrder": 9 + "sortOrder": 9, + "isDefault": false }, { "value": "code", "displayName": "Code", "description": "Code templates.", - "isDefault": false, - "sortOrder": 10 + "sortOrder": 10, + "isDefault": false }, { "value": "javascript", "displayName": "Javascript", "description": "JavaScript templates.", - "isDefault": false, - "sortOrder": 11 + "sortOrder": 11, + "isDefault": false } ] }, @@ -680,78 +679,78 @@ "value": "dns", "displayName": "Dns", "description": "Exclude DNS templates.", - "isDefault": false, - "sortOrder": 1 + "sortOrder": 1, + "isDefault": false }, { "value": "file", "displayName": "File", "description": "Exclude file templates.", - "isDefault": false, - "sortOrder": 2 + "sortOrder": 2, + "isDefault": false }, { "value": "http", "displayName": "Http", "description": "Exclude HTTP templates.", - "isDefault": false, - "sortOrder": 3 + "sortOrder": 3, + "isDefault": false }, { "value": "headless", "displayName": "Headless", "description": "Exclude headless templates.", - "isDefault": false, - "sortOrder": 4 + "sortOrder": 4, + "isDefault": false }, { "value": "tcp", "displayName": "Tcp", "description": "Exclude TCP templates.", - "isDefault": false, - "sortOrder": 5 + "sortOrder": 5, + "isDefault": false }, { "value": "workflow", "displayName": "Workflow", "description": "Exclude workflow templates.", - "isDefault": false, - "sortOrder": 6 + "sortOrder": 6, + "isDefault": false }, { "value": "ssl", "displayName": "Ssl", "description": "Exclude SSL templates.", - "isDefault": false, - "sortOrder": 7 + "sortOrder": 7, + "isDefault": false }, { "value": "websocket", "displayName": "Websocket", "description": "Exclude websocket templates.", - "isDefault": false, - "sortOrder": 8 + "sortOrder": 8, + "isDefault": false }, { "value": "whois", "displayName": "Whois", "description": "Exclude WHOIS templates.", - "isDefault": false, - "sortOrder": 9 + "sortOrder": 9, + "isDefault": false }, { "value": "code", "displayName": "Code", "description": "Exclude code templates.", - "isDefault": false, - "sortOrder": 10 + "sortOrder": 10, + "isDefault": false }, { "value": "javascript", "displayName": "Javascript", "description": "Exclude JavaScript templates.", - "isDefault": false, - "sortOrder": 11 + "sortOrder": 11, + "isDefault": false } ] }, @@ -1219,22 +1218,22 @@ "value": "batteringram", "displayName": "Batteringram", "description": "Use battering ram attack type.", - "isDefault": false, - "sortOrder": 1 + "sortOrder": 1, + "isDefault": false }, { "value": "pitchfork", "displayName": "Pitchfork", "description": "Use pitchfork attack type.", - "isDefault": false, - "sortOrder": 2 + "sortOrder": 2, + "isDefault": false }, { "value": "clusterbomb", "displayName": "Clusterbomb", "description": "Use cluster bomb attack type.", - "isDefault": false, - "sortOrder": 3 + "sortOrder": 3, + "isDefault": false } ] }, @@ -1379,29 +1378,29 @@ "value": "replace", "displayName": "Replace", "description": "Replace fuzzing type.", - "isDefault": false, - "sortOrder": 1 + "sortOrder": 1, + "isDefault": false }, { "value": "prefix", "displayName": "Prefix", "description": "Prefix fuzzing type.", - "isDefault": false, - "sortOrder": 2 + "sortOrder": 2, + "isDefault": false }, { "value": "postfix", "displayName": "Postfix", "description": "Postfix fuzzing type.", - "isDefault": false, - "sortOrder": 3 + "sortOrder": 3, + "isDefault": false }, { "value": "infix", "displayName": "Infix", "description": "Infix fuzzing type.", - "isDefault": false, - "sortOrder": 4 + "sortOrder": 4, + "isDefault": false } ] }, @@ -1421,15 +1420,15 @@ "value": "multiple", "displayName": "Multiple", "description": "Multiple fuzzing mode.", - "isDefault": false, - "sortOrder": 1 + "sortOrder": 1, + "isDefault": false }, { "value": "single", "displayName": "Single", "description": "Single fuzzing mode.", - "isDefault": false, - "sortOrder": 2 + "sortOrder": 2, + "isDefault": false } ] }, @@ -1525,22 +1524,22 @@ "value": "low", "displayName": "Low", "description": "Low fuzzing aggression.", - "isDefault": true, - "sortOrder": 1 + "sortOrder": 1, + "isDefault": true }, { "value": "medium", "displayName": "Medium", "description": "Medium fuzzing aggression.", - "isDefault": false, - "sortOrder": 2 + "sortOrder": 2, + "isDefault": false }, { "value": "high", "displayName": "High", "description": "High fuzzing aggression.", - "isDefault": false, - "sortOrder": 3 + "sortOrder": 3, + "isDefault": false } ] }, @@ -1839,22 +1838,22 @@ "value": "auto", "displayName": "Auto", "description": "Automatic scan strategy.", - "isDefault": true, - "sortOrder": 1 + "sortOrder": 1, + "isDefault": true }, { "value": "host-spray", "displayName": "Host-spray", "description": "Host spray scan strategy.", - "isDefault": false, - "sortOrder": 2 + "sortOrder": 2, + "isDefault": false }, { "value": "template-spray", "displayName": "Template-spray", "description": "Template spray scan strategy.", - "isDefault": false, - "sortOrder": 3 + "sortOrder": 3, + "isDefault": false } ] }, @@ -2306,4 +2305,4 @@ "commandKey": "nuclei" } ] -} +} \ No newline at end of file diff --git a/public/tools-collection/shuffledns.json b/public/tools-collection/shuffledns.json index 1fa6eb3..917428e 100644 --- a/public/tools-collection/shuffledns.json +++ b/public/tools-collection/shuffledns.json @@ -11,7 +11,6 @@ { "name": "shuffledns", "description": "shuffleDNS is a wrapper around massdns written in go that allows you to enumerate valid subdomains using active bruteforce as well as resolve subdomains with wildcard handling and easy input-output support.", - "isDefault": true, "sortOrder": 1, "key": "shuffledns" } diff --git a/public/tools-collection/subfinder.json b/public/tools-collection/subfinder.json index 28bf51a..26cd4f8 100644 --- a/public/tools-collection/subfinder.json +++ b/public/tools-collection/subfinder.json @@ -11,7 +11,6 @@ { "name": "subfinder", "description": "Subfinder is a subdomain discovery tool that discovers subdomains for websites by using passive online sources.", - "isDefault": true, "sortOrder": 1, "key": "subfinder" } diff --git a/public/tools-collection/urlfinder.json b/public/tools-collection/urlfinder.json index 58070ce..887958d 100644 --- a/public/tools-collection/urlfinder.json +++ b/public/tools-collection/urlfinder.json @@ -11,7 +11,6 @@ { "name": "urlfinder", "description": "A streamlined tool for discovering associated URLs.", - "isDefault": true, "sortOrder": 1, "key": "urlfinder" } diff --git a/public/tools-collection/yt-dlp.json b/public/tools-collection/yt-dlp.json index 2308297..853d16e 100644 --- a/public/tools-collection/yt-dlp.json +++ b/public/tools-collection/yt-dlp.json @@ -11,7 +11,6 @@ { "name": "yt-dlp", "description": "yt-dlp is a command-line program to download videos from YouTube and other sites.", - "isDefault": true, "sortOrder": 1, "commandKey": "yt-dlp", "key": "yt-dlp" diff --git a/public/tools.json b/public/tools.json index 164d488..3cdb231 100644 --- a/public/tools.json +++ b/public/tools.json @@ -2,66 +2,131 @@ { "name": "asnmap", "displayName": "ASNMap", - "description": "Go CLI and Library for quickly mapping organization network ranges using ASN information." + "description": "Go CLI and Library for quickly mapping organization network ranges using ASN information.", + "info": { + "description": "Go CLI and Library for quickly mapping organization network ranges using ASN information.", + "version": "1.1.1", + "url": "https://github.com/projectdiscovery/asnmap" + } }, { "name": "cdncheck", "displayName": "CDNCheck", - "description": "cdncheck is a tool for identifying the technology associated with dns / ip network addresses." + "description": "cdncheck is a tool for identifying the technology associated with dns / ip network addresses.", + "info": { + "description": "cdncheck is a tool for identifying the technology associated with dns / ip network addresses.", + "version": "1.2.27", + "url": "https://github.com/projectdiscovery/cdncheck" + } }, { "name": "curl", "displayName": "Curl", - "description": "curl is a command line tool and library for transferring data with URLs." + "description": "curl is a command line tool and library for transferring data with URLs.", + "info": { + "description": "curl is a command line tool and library for transferring data with URLs.", + "version": "8.19.0", + "url": "https://curl.se/" + } }, { "name": "dnsx", "displayName": "DNSX", - "description": "A fast and multi-purpose DNS toolkit designed for running DNS queries." + "description": "A fast and multi-purpose DNS toolkit designed for running DNS queries.", + "info": { + "description": "A fast and multi-purpose DNS toolkit designed for running DNS queries.", + "version": "1.2.3", + "url": "https://github.com/projectdiscovery/dnsx" + } }, { "name": "gospider", "displayName": "GoSpider", - "description": "Fast web spider written in Go." + "description": "Fast web spider written in Go.", + "info": { + "description": "Fast web spider written in Go.", + "version": "1.1.6", + "url": "https://github.com/jaeles-project/gospider" + } }, { "name": "httpx", "displayName": "Httpx", - "description": "Httpx is a fast and multi-purpose HTTP toolkit that allows running multiple probes using the retryablehttp library." + "description": "Httpx is a fast and multi-purpose HTTP toolkit that allows running multiple probes using the retryablehttp library.", + "info": { + "description": "Httpx is a fast and multi-purpose HTTP toolkit that allows running multiple probes using the retryablehttp library.", + "version": "1.9.0", + "url": "https://github.com/projectdiscovery/httpx" + } }, { "name": "katana", "displayName": "Katana", - "description": "Katana is a fast crawler focused on execution in automation pipelines offering both headless and non-headless crawling." + "description": "Katana is a fast crawler focused on execution in automation pipelines offering both headless and non-headless crawling.", + "info": { + "description": "Katana is a fast crawler focused on execution in automation pipelines offering both headless and non-headless crawling.", + "version": "1.5.0", + "url": "https://github.com/projectdiscovery/katana" + } }, { "name": "naabu", "displayName": "Naabu", - "description": "Fast port scanner for discovering open ports on hosts." + "description": "Fast port scanner for discovering open ports on hosts.", + "info": { + "description": "Fast port scanner for discovering open ports on hosts.", + "version": "2.5.0", + "url": "https://github.com/projectdiscovery/naabu" + } }, { "name": "nuclei", "displayName": "Nuclei", - "description": "Nuclei is a fast, template based vulnerability scanner focusing on extensive configurability, massive extensibility and ease of use." + "description": "Nuclei is a fast, template based vulnerability scanner focusing on extensive configurability, massive extensibility and ease of use.", + "info": { + "description": "Nuclei is a fast, template based vulnerability scanner focusing on extensive configurability, massive extensibility and ease of use.", + "version": "3.7.1", + "url": "https://github.com/projectdiscovery/nuclei" + } }, { "name": "shuffledns", "displayName": "ShuffleDNS", - "description": "MassDNS wrapper written in go to enumerate valid subdomains using active bruteforce as well as resolve subdomains with wildcard filtering and easy input-output support." + "description": "MassDNS wrapper written in go to enumerate valid subdomains using active bruteforce as well as resolve subdomains with wildcard filtering and easy input-output support.", + "info": { + "description": "MassDNS wrapper written in go to enumerate valid subdomains using active bruteforce as well as resolve subdomains with wildcard filtering and easy input-output support.", + "version": "1.2.1", + "url": "https://github.com/projectdiscovery/shuffledns" + } }, { "name": "subfinder", "displayName": "Subfinder", - "description": "Subfinder is a subdomain discovery tool that discovers subdomains for websites by using passive online sources." + "description": "Subfinder is a subdomain discovery tool that discovers subdomains for websites by using passive online sources.", + "info": { + "description": "Subfinder is a subdomain discovery tool that discovers subdomains for websites by using passive online sources.", + "version": "2.13.0", + "url": "https://github.com/projectdiscovery/subfinder" + } }, { "name": "urlfinder", "displayName": "URLFinder", - "description": "A streamlined tool for discovering associated URLs." + "description": "A streamlined tool for discovering associated URLs.", + "info": { + "description": "A streamlined tool for discovering associated URLs.", + "version": "0.0.3", + "url": "https://github.com/projectdiscovery/urlfinder" + } }, { "name": "yt-dlp", "displayName": "yt-dlp", - "description": "yt-dlp is a command-line program to download videos from YouTube and other sites." + "description": "yt-dlp is a command-line program to download videos from YouTube and other sites.", + "info": { + "description": "yt-dlp is a command-line program to download videos from YouTube and other sites.", + "version": "2026.03.17", + "url": "https://github.com/yt-dlp/yt-dlp" + } } ] \ No newline at end of file diff --git a/registry/commandly/__tests__/generated-command.test.tsx b/registry/commandly/__tests__/generated-command.test.tsx index 581e806..20ad239 100644 --- a/registry/commandly/__tests__/generated-command.test.tsx +++ b/registry/commandly/__tests__/generated-command.test.tsx @@ -4,7 +4,7 @@ import { render, screen } from "@testing-library/react"; const testTool = { name: "tool", displayName: "Tool", - commands: [{ key: "test-key", name: "test", isDefault: true, sortOrder: 0 }], + commands: [{ key: "test-key", name: "test", sortOrder: 0 }], parameters: [], }; @@ -23,7 +23,7 @@ describe("GeneratedCommand", () => { const tool = { name: "curl", displayName: "Curl", - commands: [{ key: "curl", name: "curl", isDefault: true, sortOrder: 1 }], + commands: [{ key: "curl", name: "curl", sortOrder: 1 }], parameters: [ { key: "header", @@ -55,7 +55,7 @@ describe("GeneratedCommand", () => { const tool = { name: "mytool", displayName: "My Tool", - commands: [{ key: "mytool", name: "mytool", isDefault: true, sortOrder: 1 }], + commands: [{ key: "mytool", name: "mytool", sortOrder: 1 }], parameters: [ { key: "filter", @@ -84,7 +84,7 @@ describe("GeneratedCommand", () => { const tool = { name: "curl", displayName: "Curl", - commands: [{ key: "curl", name: "curl", isDefault: true, sortOrder: 1 }], + commands: [{ key: "curl", name: "curl", sortOrder: 1 }], parameters: [ { key: "header", @@ -111,7 +111,7 @@ describe("GeneratedCommand", () => { const tool = { name: "ssh", displayName: "SSH", - commands: [{ key: "ssh", name: "ssh", isDefault: true, sortOrder: 1 }], + commands: [{ key: "ssh", name: "ssh", sortOrder: 1 }], parameters: [ { key: "verbose", @@ -139,7 +139,7 @@ describe("GeneratedCommand", () => { const tool = { name: "curl", displayName: "Curl", - commands: [{ key: "curl", name: "curl", isDefault: true, sortOrder: 1 }], + commands: [{ key: "curl", name: "curl", sortOrder: 1 }], parameters: [ { key: "target", diff --git a/registry/commandly/__tests__/json-output.test.tsx b/registry/commandly/__tests__/json-output.test.tsx index 548763e..53404bf 100644 --- a/registry/commandly/__tests__/json-output.test.tsx +++ b/registry/commandly/__tests__/json-output.test.tsx @@ -45,7 +45,6 @@ describe("exportToStructuredJSON", () => { value: "val", displayName: "Val", description: "", - isDefault: true, sortOrder: 0, }, ], diff --git a/registry/commandly/__tests__/tool-renderer.test.tsx b/registry/commandly/__tests__/tool-renderer.test.tsx index 9cb222e..565afa2 100644 --- a/registry/commandly/__tests__/tool-renderer.test.tsx +++ b/registry/commandly/__tests__/tool-renderer.test.tsx @@ -4,7 +4,7 @@ import { createNewParameter } from "@/components/commandly/utils/flat"; import { defaultTool } from "@/lib/utils"; import { render, screen } from "@testing-library/react"; -const baseCommand = { key: "my-tool", name: "my-tool", isDefault: true, sortOrder: 0 }; +const baseCommand = { key: "my-tool", name: "my-tool", sortOrder: 0 }; const baseTool = { ...defaultTool(), commands: [baseCommand] }; describe("ToolRenderer", () => { @@ -165,6 +165,36 @@ describe("ToolRenderer", () => { expect(inputs).toHaveLength(2); }); + it("renders an allowMultiple Enum parameter without crashing when value is an array (repeatable-to-non-repeatable transition)", () => { + const param = { + ...createNewParameter(false, "my-tool"), + key: "format", + name: "Format", + parameterType: "Option" as const, + dataType: "Enum" as const, + isRepeatable: false, + enum: { + values: [ + { value: "json", displayName: "JSON" }, + { value: "xml", displayName: "XML" }, + ], + allowMultiple: true, + separator: ",", + }, + }; + expect(() => + render( + {}} + />, + ), + ).not.toThrow(); + expect(screen.getByText("Format")).toBeInTheDocument(); + }); + it("custom catalog entry takes precedence over built-in", () => { const param = { ...createNewParameter(false, "my-tool"), diff --git a/registry/commandly/json-output.tsx b/registry/commandly/json-output.tsx index dbed8a2..f241483 100644 --- a/registry/commandly/json-output.tsx +++ b/registry/commandly/json-output.tsx @@ -1,6 +1,7 @@ import { Tool } from "@/components/commandly/types/flat"; import { exportToStructuredJSON } from "@/components/commandly/utils/flat"; import { convertToNestedStructure } from "@/components/commandly/utils/nested"; +import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { @@ -14,7 +15,7 @@ import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; import { Textarea } from "@/components/ui/textarea"; import { cn } from "@/lib/utils"; import { CheckIcon, ChevronsUpDownIcon, CopyIcon, Edit2Icon, XIcon } from "lucide-react"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; const jsonOptions = [ @@ -22,17 +23,51 @@ const jsonOptions = [ { value: "flat", label: "Flat" }, ]; +type DiffLine = { type: "same" | "added" | "removed"; text: string }; + +function diffLines(before: string, after: string): DiffLine[] { + const a = before.split("\n"); + const b = after.split("\n"); + const m = a.length; + const n = b.length; + const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0)); + for (let i = 1; i <= m; i++) + for (let j = 1; j <= n; j++) + dp[i][j] = + a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] + 1 : Math.max(dp[i - 1][j], dp[i][j - 1]); + const result: DiffLine[] = []; + let i = m, + j = n; + while (i > 0 || j > 0) { + if (i > 0 && j > 0 && a[i - 1] === b[j - 1]) { + result.unshift({ type: "same", text: a[i - 1] }); + i--; + j--; + } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) { + result.unshift({ type: "added", text: b[j - 1] }); + j--; + } else { + result.unshift({ type: "removed", text: a[i - 1] }); + i--; + } + } + return result; +} + interface JsonTypeComponentProps { tool: Tool; + originalTool?: Tool; onApply?: (tool: Tool) => void; } -export function JsonOutput({ tool, onApply }: JsonTypeComponentProps) { +export function JsonOutput({ tool, originalTool, onApply }: JsonTypeComponentProps) { const [open, setOpen] = useState(false); const [jsonString, setJsonString] = useState(); + const [originalJsonString, setOriginalJsonString] = useState(); const [jsonType, setJsonType] = useState<"nested" | "flat">("flat"); const [isEditing, setIsEditing] = useState(false); const [editValue, setEditValue] = useState(""); + const [showDiff, setShowDiff] = useState(true); useEffect(() => { const config = @@ -40,6 +75,30 @@ export function JsonOutput({ tool, onApply }: JsonTypeComponentProps) { setJsonString(JSON.stringify(config, null, 2)); }, [jsonType, tool]); + useEffect(() => { + if (!originalTool) { + setOriginalJsonString(undefined); + return; + } + const config = + jsonType === "flat" + ? exportToStructuredJSON(originalTool) + : convertToNestedStructure(originalTool); + setOriginalJsonString(JSON.stringify(config, null, 2)); + }, [jsonType, originalTool]); + + const diff = useMemo(() => { + if (!originalJsonString || !jsonString || originalJsonString === jsonString) return null; + return diffLines(originalJsonString, jsonString); + }, [originalJsonString, jsonString]); + + const diffStats = useMemo(() => { + if (!diff) return null; + const added = diff.filter((l) => l.type === "added").length; + const removed = diff.filter((l) => l.type === "removed").length; + return { added, removed }; + }, [diff]); + const handleEditToggle = () => { setEditValue(jsonString ?? ""); setIsEditing(true); @@ -137,6 +196,34 @@ export function JsonOutput({ tool, onApply }: JsonTypeComponentProps) { + {diffStats && !isEditing && ( +
+ {diffStats.added > 0 && ( + + +{diffStats.added} added + + )} + {diffStats.removed > 0 && ( + + -{diffStats.removed} removed + + )} + +
+ )} {isEditing ? (
+ ) : diff && showDiff ? ( + +
+              {diff.map((line, idx) => (
+                
+ + {line.type === "added" ? "+ " : line.type === "removed" ? "- " : " "} + + {line.text} +
+ ))} +
+ + +
) : ( { - const defaultCommand = tool.commands.find((command) => command.isDefault); - if (defaultCommand) return defaultCommand; - const nameMatchCommand = tool.commands.find( (command) => command.name.toLowerCase() === tool.name.toLowerCase(), ); @@ -129,7 +126,11 @@ function OptionEnumInput({ parameter, value, onUpdate }: ParameterRenderContext) ); if (parameter.enum?.allowMultiple) { - const selected = value ? (value as string).split(separator).filter(Boolean) : []; + const selected = Array.isArray(value) + ? (value as string[]).filter(Boolean) + : value + ? (value as string).split(separator).filter(Boolean) + : []; return (
{label} diff --git a/registry/commandly/types/flat.ts b/registry/commandly/types/flat.ts index 7d31e20..5728c12 100644 --- a/registry/commandly/types/flat.ts +++ b/registry/commandly/types/flat.ts @@ -1,30 +1,46 @@ export interface ToolInfo { + /** A brief human-readable description of what the tool does. */ description?: string; + /** The version string of the tool (e.g. "1.0.0"). */ version?: string; + /** The homepage or documentation URL for the tool. */ url?: string; } export interface Command { + /** Unique identifier for this command within the tool. */ key: string; + /** Key of the parent command; used to represent subcommand nesting. */ parentCommandKey?: string; + /** Human-readable display name of the command. */ name: string; + /** Brief description of what this command does. */ description?: string; + /** Whether this command opens an interactive session or prompt. */ interactive?: boolean; - isDefault?: boolean; + /** Display sort position relative to sibling commands. */ sortOrder?: number; } export interface ParameterEnumValue { + /** The raw value passed to the CLI for this choice. */ value: string; + /** Human-readable label shown to the user for this enum choice. */ displayName: string; + /** Description of what this enum value does or represents. */ description?: string; + /** Whether this is the default selection when no value is provided. */ isDefault?: boolean; + /** Display sort position relative to sibling enum values. */ sortOrder?: number; } export interface ParameterEnumValues { + /** The list of allowed enum choices. */ values: ParameterEnumValue[]; + /** Whether the user can select multiple values at once. */ allowMultiple?: boolean; + /** Separator character used when joining multiple selected values. */ separator?: string; } @@ -36,19 +52,28 @@ export type ParameterValidationType = | "regex"; export interface ParameterValidation { + /** Unique identifier for this validation rule. */ key: string; + /** The type of validation to apply. */ validationType: ParameterValidationType; + /** The value to validate against (e.g. the max length number, or a regex pattern). */ validationValue: string; + /** The error message to display when validation fails. */ errorMessage: string; } export type ParameterDependencyType = "requires" | "conflicts_with"; export interface ParameterDependency { + /** Unique identifier for this dependency rule. */ key: string; + /** Key of the parameter that owns this dependency. */ parameterKey: string; + /** Key of the parameter this dependency references. */ dependsOnParameterKey: string; + /** Whether this parameter requires or conflicts with the referenced parameter. */ dependencyType: ParameterDependencyType; + /** Optional value that the referenced parameter must have for this dependency to apply. */ conditionValue?: string; } @@ -59,6 +84,7 @@ export interface ToolMetadata {} // eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface ParameterMetadata { + /** Arbitrary tags for categorising or filtering parameters. */ tags?: string[]; } @@ -67,44 +93,76 @@ export type ParameterType = "Flag" | "Option" | "Argument"; export type ParameterDataType = "String" | "Number" | "Boolean" | "Enum"; export interface Parameter { + /** Unique identifier for this parameter within the tool. */ key: string; + /** Human-readable display name of the parameter. */ name: string; + /** Key of the command this parameter belongs to; omit for global parameters. */ commandKey?: string; + /** Brief description of what this parameter does or accepts. */ description?: string; + /** Optional grouping label for organising related parameters in the UI. */ group?: string; + /** Additional metadata such as tags. */ metadata?: ParameterMetadata; + /** Whether this is a boolean flag, a key-value option, or a positional argument. */ parameterType: ParameterType; + /** The data type of the parameter's value. */ dataType: ParameterDataType; + /** Whether the user must provide this parameter. */ isRequired?: boolean; + /** Whether this parameter can be specified multiple times. */ isRepeatable?: boolean; + /** Whether this parameter applies to all commands rather than a single command. */ isGlobal?: boolean; + /** The single-character short flag (e.g. "-v"). */ shortFlag?: string; + /** The long-form flag or option name (e.g. "--verbose"). */ longFlag?: string; + /** Zero-based position index for positional arguments. */ position?: number; + /** Display sort position relative to sibling parameters. */ sortOrder?: number; + /** Separator character used when the option accepts multiple values in one argument (e.g. ","). */ arraySeparator?: string; + /** Separator between key and value for key=value style options (e.g. "="). */ keyValueSeparator?: string; + /** Allowed enum choices when dataType is "Enum". */ enum?: ParameterEnumValues; + /** Validation rules applied to this parameter's value. */ validations?: ParameterValidation[]; + /** Dependencies on other parameters (requires or conflicts-with relationships). */ dependencies?: ParameterDependency[]; } export type ExclusionType = "mutual_exclusive" | "required_one_of"; export interface ExclusionGroup { + /** Unique identifier for this exclusion group. */ key?: string; + /** Key of the command this exclusion group belongs to; omit for global groups. */ commandKey?: string; + /** Human-readable name for this exclusion group. */ name: string; + /** Whether parameters in this group are mutually exclusive or one is required. */ exclusionType: ExclusionType; + /** Keys of the parameters that participate in this exclusion group. */ parameterKeys: string[]; } export interface Tool { + /** Unique machine-readable identifier for the tool (e.g. "httpx"). */ name: string; + /** Human-readable display name for the tool (e.g. "HTTPx"). */ displayName: string; + /** General information about the tool such as description, version, and URL. */ info?: ToolInfo; + /** List of all commands and subcommands defined for this tool. */ commands: Command[]; + /** Flat list of all parameters across all commands and global scope. */ parameters: Parameter[]; + /** Groups of parameters with mutual exclusion or required-one-of constraints. */ exclusionGroups?: ExclusionGroup[]; + /** Arbitrary metadata attached to the tool. */ metadata?: ToolMetadata; } diff --git a/registry/commandly/types/nested.ts b/registry/commandly/types/nested.ts index 8606997..4bd6b54 100644 --- a/registry/commandly/types/nested.ts +++ b/registry/commandly/types/nested.ts @@ -11,62 +11,103 @@ import type { } from "@/components/commandly/types/flat"; export interface NestedParameterValidation { + /** The type of validation to apply. */ validationType: ParameterValidationType; + /** The value to validate against (e.g. max length number or regex pattern). */ validationValue: string; + /** The error message to display when validation fails. */ errorMessage: string; } export interface NestedParameterDependency { + /** Name of the parameter this dependency references. */ dependsOnParameter: string; + /** Whether this parameter requires or conflicts with the referenced parameter. */ dependencyType: ParameterDependencyType; + /** Optional value that the referenced parameter must have for this dependency to apply. */ conditionValue?: string; } export interface NestedParameter { + /** Human-readable display name of the parameter. */ name: string; + /** Brief description of what this parameter does or accepts. */ description?: string; + /** Optional grouping label for organising related parameters in the UI. */ group?: string; + /** Whether this is a boolean flag, a key-value option, or a positional argument. */ parameterType: ParameterType; + /** The data type of the parameter's value. */ dataType: ParameterDataType; + /** Additional metadata such as tags. */ metadata?: ParameterMetadata; + /** Whether the user must provide this parameter. */ isRequired?: boolean; + /** Whether this parameter can be specified multiple times. */ isRepeatable?: boolean; + /** Whether this parameter applies to all commands rather than a single command. */ isGlobal?: boolean; + /** The single-character short flag (e.g. "-v"). */ shortFlag?: string; + /** The long-form flag or option name (e.g. "--verbose"). */ longFlag?: string; + /** Zero-based position index for positional arguments. */ position?: number; + /** Display sort position relative to sibling parameters. */ sortOrder?: number; + /** Separator character used when the option accepts multiple values in one argument (e.g. ","). */ arraySeparator?: string; + /** Separator between key and value for key=value style options (e.g. "="). */ keyValueSeparator?: string; + /** Allowed enum choices when dataType is "Enum". */ enum?: ParameterEnumValues; + /** Validation rules applied to this parameter's value. */ validations?: NestedParameterValidation[]; + /** Dependencies on other parameters (requires or conflicts-with relationships). */ dependencies?: NestedParameterDependency[]; } export interface NestedCommand { + /** Human-readable display name of the command. */ name: string; + /** Brief description of what this command does. */ description?: string; + /** Whether this command opens an interactive session or prompt. */ interactive?: boolean; - isDefault: boolean; + /** Display sort position relative to sibling commands. */ sortOrder: number; + /** Parameters that belong directly to this command. */ parameters: NestedParameter[]; + /** Nested subcommands of this command. */ subcommands: NestedCommand[]; } export interface NestedExclusionGroup { + /** Human-readable name for this exclusion group. */ name: string; + /** Whether parameters in this group are mutually exclusive or one is required. */ exclusionType: ExclusionType; + /** Names of the parameters that participate in this exclusion group. */ parameters: string[]; } export interface NestedTool { + /** Optional JSON schema URI for validation. */ $schema?: string; + /** Unique machine-readable identifier for the tool (e.g. "httpx"). */ name: string; + /** Human-readable display name for the tool (e.g. "HTTPx"). */ displayName: string; + /** General information about the tool such as description, version, and URL. */ info?: ToolInfo; + /** The homepage or documentation URL for the tool. */ url?: string; + /** Parameters that apply to all commands globally. */ globalParameters: NestedParameter[]; + /** Hierarchical list of commands and their nested subcommands. */ commands: NestedCommand[]; + /** Groups of parameters with mutual exclusion or required-one-of constraints. */ exclusionGroups?: NestedExclusionGroup[] | null; + /** Arbitrary metadata attached to the tool. */ metadata?: ToolMetadata; } diff --git a/registry/commandly/utils/flat.ts b/registry/commandly/utils/flat.ts index f2b24c0..1ee93bd 100644 --- a/registry/commandly/utils/flat.ts +++ b/registry/commandly/utils/flat.ts @@ -39,15 +39,6 @@ export const getCommandPath = (command: Command, tool: Tool): string => { if (!path) return command.name; - if (command.name === tool.name && command.isDefault) { - return tool.name; - } - - const rootCommand = tool.commands.find((c) => c.name === tool.name); - if (rootCommand?.isDefault && path[0] === tool.name) { - path[0] = tool.name; - } - return path.join(" "); }; @@ -85,7 +76,6 @@ export const exportToStructuredJSON = (tool: Tool) => { name: tool.name, displayName: tool.displayName, info: tool.info, - url: tool.info?.url, commands: tool.commands.map((cmd) => ({ ...cmd })), parameters: tool.parameters.map(({ metadata: _metadata, ...param }) => param), exclusionGroups: tool.exclusionGroups, @@ -93,17 +83,6 @@ export const exportToStructuredJSON = (tool: Tool) => { }; }; -export const createNewCommand = (parentKey?: string): Command => { - const name = randomCommandName(); - return { - key: slugify(name), - parentCommandKey: parentKey, - name, - isDefault: false, - sortOrder: 1, - }; -}; - export const createNewParameter = (isGlobal: boolean, commandKey?: string): Parameter => { return { key: "", @@ -116,16 +95,6 @@ export const createNewParameter = (isGlobal: boolean, commandKey?: string): Para }; }; -export const randomCommandName = () => { - const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; - let result = ""; - const charactersLength = characters.length; - for (let i = 0; i < 7; i++) { - result += characters.charAt(Math.floor(Math.random() * charactersLength)); - } - return result; -}; - const isEmpty = (value: object | null | undefined): boolean => { if (value == null) return true; if (Array.isArray(value)) return value.length === 0; diff --git a/registry/commandly/utils/nested.ts b/registry/commandly/utils/nested.ts index a215760..9b9ce2e 100644 --- a/registry/commandly/utils/nested.ts +++ b/registry/commandly/utils/nested.ts @@ -44,7 +44,6 @@ export const convertToNestedStructure = (tool: Tool): NestedTool => { name: cmd.name, description: cmd.description, interactive: cmd.interactive, - isDefault: cmd.isDefault ?? false, sortOrder: cmd.sortOrder ?? 0, parameters: commandParameters.map(convertParameter), subcommands: buildNestedCommands(commands, cmd.key), diff --git a/scripts/generate-tools-json.ts b/scripts/generate-tools-json.ts index 2dd8cea..71d0ec9 100644 --- a/scripts/generate-tools-json.ts +++ b/scripts/generate-tools-json.ts @@ -13,7 +13,7 @@ const tools = files.sort().map((file) => { name: tool.name, displayName: tool.displayName || tool.name, description: tool.info?.description, - url: tool.info?.url, + info: tool.info, }; }); diff --git a/src/components/ai-chat/ai-chat-message-mapping.ts b/src/components/ai-chat/ai-chat-message-mapping.ts new file mode 100644 index 0000000..943e1a1 --- /dev/null +++ b/src/components/ai-chat/ai-chat-message-mapping.ts @@ -0,0 +1,165 @@ +import { Tool } from "@/components/commandly/types/flat"; +import { getToolName, isReasoningUIPart, isTextUIPart, isToolUIPart, type UIMessage } from "ai"; + +export interface ToolCallEntry { + toolCallId: string; + toolName: string; + approvalId?: string; + title: string; + input: Record; + state: + | "input-streaming" + | "input-available" + | "approval-requested" + | "approval-responded" + | "output-available" + | "output-denied" + | "output-error"; + output?: unknown; + errorText?: string; + originalTool?: Tool; + previewTool?: Tool; +} + +export interface ChatMessage { + id: string; + role: "user" | "assistant"; + content: string; + toolApplied?: boolean; + toolCalls?: ToolCallEntry[]; + isEditing?: boolean; + editingContent?: string; + reasoningContent?: string; +} + +export interface ApprovalArtifact { + approvalId: string; + previewTool: Tool; + originalTool: Tool; + summary: string; +} + +function getToolTitle(toolName: string, input: Record, fallbackTitle?: string) { + if (fallbackTitle) { + return fallbackTitle; + } + + if (toolName === "editTool") { + return typeof input.summary === "string" ? input.summary : "Editing…"; + } + + if (toolName === "readTool") { + const summary = input.summary as string | undefined; + const jsonPath = input.jsonPath as string | undefined; + const pathLabel = jsonPath && jsonPath !== "$" ? jsonPath : "whole tool"; + return summary ? `${summary} (${pathLabel})` : `Reading ${pathLabel}…`; + } + + if (toolName === "applyToolDefinition") { + return "Preparing to apply"; + } + + if (toolName === "tavilySearch") { + return "Web Search"; + } + + if (toolName === "tavilyExtract") { + return "Extract Content"; + } + + return toolName; +} + +function toToolInput(value: unknown): Record { + return value && typeof value === "object" && !Array.isArray(value) + ? (value as Record) + : {}; +} + +export function toChatMessage( + message: UIMessage, + approvalArtifacts: Record, +): ChatMessage { + const content = message.parts + .filter(isTextUIPart) + .map((part) => part.text) + .join(""); + + const reasoningContent = message.parts + .filter(isReasoningUIPart) + .map((part) => part.text) + .join(""); + + const toolCalls = message.parts.filter(isToolUIPart).map((part) => { + const toolName = getToolName(part); + const input = toToolInput("input" in part ? part.input : undefined); + const approvalId = part.approval?.id; + const artifact = approvalId ? approvalArtifacts[approvalId] : undefined; + + return { + toolCallId: part.toolCallId, + toolName, + approvalId, + title: getToolTitle(toolName, input, part.title), + input, + state: part.state, + output: "output" in part ? part.output : undefined, + errorText: "errorText" in part ? part.errorText : undefined, + originalTool: artifact?.originalTool, + previewTool: artifact?.previewTool, + } satisfies ToolCallEntry; + }); + + return { + id: message.id, + role: message.role === "assistant" ? "assistant" : "user", + content, + toolApplied: toolCalls.some( + (toolCall) => + toolCall.toolName === "applyToolDefinition" && toolCall.state === "output-available", + ), + toolCalls: toolCalls.length > 0 ? toolCalls : undefined, + reasoningContent: reasoningContent || undefined, + }; +} + +export function countCompletedToolCalls(messages: UIMessage[]): number { + return messages.reduce( + (count, message) => + count + + message.parts.filter( + (part) => + isToolUIPart(part) && + (part.state === "output-available" || + part.state === "output-error" || + part.state === "output-denied"), + ).length, + 0, + ); +} + +export function findPendingApproval(messages: ChatMessage[]) { + for (let index = messages.length - 1; index >= 0; index--) { + const applyToolCall = messages[index].toolCalls?.find( + (toolCall) => + toolCall.toolName === "applyToolDefinition" && + toolCall.state === "approval-requested" && + toolCall.approvalId, + ); + + if (applyToolCall?.approvalId && applyToolCall.originalTool && applyToolCall.previewTool) { + return { + approvalId: applyToolCall.approvalId, + previewTool: applyToolCall.previewTool, + originalTool: applyToolCall.originalTool, + summary: + typeof applyToolCall.input.summary === "string" + ? applyToolCall.input.summary + : "Apply AI changes", + messageIndex: index, + }; + } + } + + return null; +} diff --git a/src/components/ai-chat/ai-chat-persistence.ts b/src/components/ai-chat/ai-chat-persistence.ts new file mode 100644 index 0000000..17fd826 --- /dev/null +++ b/src/components/ai-chat/ai-chat-persistence.ts @@ -0,0 +1,276 @@ +import type { UIMessage } from "ai"; + +const DB_NAME = "commandly"; +const DB_VERSION = 4; + +export interface Chat { + id: string; + toolName: string; + createdAt: number; +} + +export interface StoredMessage { + id: string; + chatId: string; + role: UIMessage["role"]; + parts: UIMessage["parts"]; + order: number; +} + +export interface ChatWithPreview extends Chat { + preview: string; + updatedAt: number; + messageCount: number; +} + +function openDB(): Promise { + return new Promise((resolve, reject) => { + const req = indexedDB.open(DB_NAME, DB_VERSION); + req.onupgradeneeded = (event) => { + const db = req.result; + const oldVersion = (event as IDBVersionChangeEvent).oldVersion; + + if (!db.objectStoreNames.contains("keys")) { + db.createObjectStore("keys"); + } + + if (oldVersion < 4) { + if (db.objectStoreNames.contains("sessions")) { + db.deleteObjectStore("sessions"); + } + if (db.objectStoreNames.contains("chats")) { + db.deleteObjectStore("chats"); + } + if (db.objectStoreNames.contains("messages")) { + db.deleteObjectStore("messages"); + } + } + + if (!db.objectStoreNames.contains("chats")) { + const chatStore = db.createObjectStore("chats", { keyPath: "id" }); + chatStore.createIndex("toolName", "toolName", { unique: false }); + } + if (!db.objectStoreNames.contains("messages")) { + const msgStore = db.createObjectStore("messages", { keyPath: "id" }); + msgStore.createIndex("chatId", "chatId", { unique: false }); + } + }; + req.onsuccess = () => resolve(req.result); + req.onerror = () => reject(req.error); + }); +} + +function toSerializable(value: T): T { + return JSON.parse(JSON.stringify(value)) as T; +} + +function getMessageText(message: UIMessage): string { + return message.parts + .filter( + (part): part is Extract => part.type === "text", + ) + .map((part) => part.text) + .join(""); +} + +export function hasPersistableContent(message: UIMessage): boolean { + return message.parts.some((part) => { + if (part.type === "text" || part.type === "reasoning") { + return part.text.trim().length > 0; + } + return part.type !== "step-start"; + }); +} + +export function getPersistableMessages(messages: UIMessage[]): UIMessage[] { + return messages.filter( + (message) => message.role !== "assistant" || hasPersistableContent(message), + ); +} + +export function getSessionPreview(messages: UIMessage[]): string { + const firstUserMessage = messages.find((message) => message.role === "user"); + return getMessageText(firstUserMessage ?? { id: "", role: "user", parts: [] }).slice(0, 80); +} + +export async function truncateMessages(chatId: string, keepCount: number): Promise { + const db = await openDB(); + return new Promise((resolve, reject) => { + const tx = db.transaction("messages", "readwrite"); + const index = tx.objectStore("messages").index("chatId"); + const req = index.getAll(chatId); + req.onsuccess = () => { + for (const msg of req.result as StoredMessage[]) { + if (msg.order >= keepCount) { + tx.objectStore("messages").delete(msg.id); + } + } + }; + tx.oncomplete = () => resolve(); + tx.onerror = () => reject(tx.error); + }); +} + +export async function createChat(id: string, toolName: string): Promise { + const db = await openDB(); + return new Promise((resolve, reject) => { + const tx = db.transaction("chats", "readwrite"); + tx.objectStore("chats").put({ id, toolName, createdAt: Date.now() } satisfies Chat); + tx.oncomplete = () => resolve(); + tx.onerror = () => reject(tx.error); + }); +} + +export async function upsertMessage( + chatId: string, + message: UIMessage, + order: number, +): Promise { + const db = await openDB(); + return new Promise((resolve, reject) => { + const tx = db.transaction(["chats", "messages"], "readwrite"); + + const chatStore = tx.objectStore("chats"); + const getChat = chatStore.get(chatId); + getChat.onsuccess = () => { + if (!getChat.result) { + tx.abort(); + reject(new Error(`Chat ${chatId} not found`)); + return; + } + + const stored: StoredMessage = { + id: message.id, + chatId, + role: message.role, + parts: toSerializable(message.parts), + order, + }; + tx.objectStore("messages").put(stored); + }; + + tx.oncomplete = () => resolve(); + tx.onerror = () => reject(tx.error); + }); +} + +export async function loadChat( + chatId: string, +): Promise { + const db = await openDB(); + return new Promise((resolve, reject) => { + const tx = db.transaction("messages", "readonly"); + const index = tx.objectStore("messages").index("chatId"); + const req = index.getAll(chatId); + req.onsuccess = () => { + const stored = (req.result as StoredMessage[]).sort((a, b) => a.order - b.order); + resolve( + stored.map((msg) => ({ + id: msg.id, + role: msg.role, + parts: msg.parts, + })) as UI_MESSAGE[], + ); + }; + req.onerror = () => reject(req.error); + }); +} + +export async function getChats(toolName: string, limit = 5): Promise { + const db = await openDB(); + return new Promise((resolve, reject) => { + const tx = db.transaction(["chats", "messages"], "readonly"); + const chatIndex = tx.objectStore("chats").index("toolName"); + const chatReq = chatIndex.getAll(toolName); + + chatReq.onsuccess = () => { + const chats = (chatReq.result as Chat[]).sort((a, b) => b.createdAt - a.createdAt); + const msgIndex = tx.objectStore("messages").index("chatId"); + const results: ChatWithPreview[] = []; + let pending = chats.length; + + if (pending === 0) { + resolve([]); + return; + } + + for (const chat of chats) { + const msgReq = msgIndex.getAll(chat.id); + msgReq.onsuccess = () => { + const stored = (msgReq.result as StoredMessage[]).sort((a, b) => a.order - b.order); + const messages = stored.map( + (m) => ({ id: m.id, role: m.role, parts: m.parts }) as UIMessage, + ); + results.push({ + ...chat, + preview: getSessionPreview(messages), + updatedAt: chat.createdAt, + messageCount: stored.length, + }); + pending--; + if (pending === 0) { + results.sort((a, b) => b.createdAt - a.createdAt); + resolve(results.slice(0, limit)); + } + }; + msgReq.onerror = () => { + pending--; + if (pending === 0) { + results.sort((a, b) => b.createdAt - a.createdAt); + resolve(results.slice(0, limit)); + } + }; + } + }; + chatReq.onerror = () => reject(chatReq.error); + }); +} + +export async function deleteChat(chatId: string): Promise { + const db = await openDB(); + return new Promise((resolve, reject) => { + const tx = db.transaction(["chats", "messages"], "readwrite"); + + tx.objectStore("chats").delete(chatId); + + const msgIndex = tx.objectStore("messages").index("chatId"); + const req = msgIndex.getAll(chatId); + req.onsuccess = () => { + for (const msg of req.result as StoredMessage[]) { + tx.objectStore("messages").delete(msg.id); + } + }; + + tx.oncomplete = () => resolve(); + tx.onerror = () => reject(tx.error); + }); +} + +export async function deleteMessage(messageId: string): Promise { + const db = await openDB(); + return new Promise((resolve, reject) => { + const tx = db.transaction("messages", "readwrite"); + + const getReq = tx.objectStore("messages").get(messageId); + getReq.onsuccess = () => { + const target = getReq.result as StoredMessage | undefined; + if (!target) { + resolve(); + return; + } + + const chatIndex = tx.objectStore("messages").index("chatId"); + const allReq = chatIndex.getAll(target.chatId); + allReq.onsuccess = () => { + for (const msg of allReq.result as StoredMessage[]) { + if (msg.order >= target.order) { + tx.objectStore("messages").delete(msg.id); + } + } + }; + }; + + tx.oncomplete = () => resolve(); + tx.onerror = () => reject(tx.error); + }); +} diff --git a/src/components/ai-chat/api-key-settings.tsx b/src/components/ai-chat/api-key-settings.tsx new file mode 100644 index 0000000..ddd459e --- /dev/null +++ b/src/components/ai-chat/api-key-settings.tsx @@ -0,0 +1,147 @@ +import { MODEL_GROUPS } from "./model-picker"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Separator } from "@/components/ui/separator"; +import { type AIProvider } from "@/lib/ai-keys"; +import { CheckIcon, EyeIcon, EyeOffIcon, GlobeIcon, Settings2Icon } from "lucide-react"; +import { useState } from "react"; + +interface AIKeyState { + key: string; + setKey: (key: string) => void; + isSaved: boolean; + setSaved: (saved: boolean) => void; +} + +interface ApiKeySettingsProps { + allProviderKeys: Record, AIKeyState>; + tavilyKeys: AIKeyState; +} + +export function ApiKeySettings({ allProviderKeys, tavilyKeys }: ApiKeySettingsProps) { + const [revealed, setRevealed] = useState>({}); + + const toggleReveal = (key: string) => setRevealed((prev) => ({ ...prev, [key]: !prev[key] })); + return ( + + + + + +

API Keys

+
+ {MODEL_GROUPS.map((g, gi) => { + const keys = allProviderKeys[g.provider as Exclude]; + return ( +
+ {gi > 0 && } +
+
+ {g.label} + {g.label} + {keys.isSaved && } +
+
+ keys.setKey(e.target.value)} + className="h-8 pr-8 text-xs" + placeholder={g.keyPlaceholder} + /> + +
+
+ keys.setSaved(c === "indeterminate" ? false : c)} + /> + +
+
+
+ ); + })} + +
+
+ + Tavily (Web Search) + {tavilyKeys.isSaved && } +
+
+ tavilyKeys.setKey(e.target.value)} + className="h-8 pr-8 text-xs" + placeholder="tvly-..." + /> + +
+
+ tavilyKeys.setSaved(c === "indeterminate" ? false : c)} + /> + +
+
+
+
+
+ ); +} diff --git a/src/components/ai-chat/diff-view.tsx b/src/components/ai-chat/diff-view.tsx new file mode 100644 index 0000000..e5753e0 --- /dev/null +++ b/src/components/ai-chat/diff-view.tsx @@ -0,0 +1,114 @@ +import { Tool } from "@/components/commandly/types/flat"; +import { exportToStructuredJSON } from "@/components/commandly/utils/flat"; +import { cn } from "@/lib/utils"; + +function computeLineDiff( + a: string, + b: string, +): Array<{ type: "same" | "add" | "remove"; text: string }> { + const aLines = a.split("\n"); + const bLines = b.split("\n"); + const m = aLines.length; + const n = bLines.length; + const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0)); + + for (let i = 1; i <= m; i++) { + for (let j = 1; j <= n; j++) { + dp[i][j] = + aLines[i - 1] === bLines[j - 1] + ? dp[i - 1][j - 1] + 1 + : Math.max(dp[i - 1][j], dp[i][j - 1]); + } + } + + const result: Array<{ type: "same" | "add" | "remove"; text: string }> = []; + let i = m; + let j = n; + + while (i > 0 || j > 0) { + if (i > 0 && j > 0 && aLines[i - 1] === bLines[j - 1]) { + result.unshift({ type: "same", text: aLines[i - 1] }); + i--; + j--; + } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) { + result.unshift({ type: "add", text: bLines[j - 1] }); + j--; + } else { + result.unshift({ type: "remove", text: aLines[i - 1] }); + i--; + } + } + + return result; +} + +export function DiffView({ original, updated }: { original: Tool; updated: Tool }) { + const aJson = JSON.stringify(exportToStructuredJSON(original), null, 2); + const bJson = JSON.stringify(exportToStructuredJSON(updated), null, 2); + const diff = computeLineDiff(aJson, bJson); + const visibleSet = new Set(); + + for (const index of diff.flatMap((line, currentIndex) => + line.type !== "same" ? [currentIndex] : [], + )) { + for ( + let cursor = Math.max(0, index - 2); + cursor <= Math.min(diff.length - 1, index + 2); + cursor++ + ) { + visibleSet.add(cursor); + } + } + + if (visibleSet.size === 0) { + return

No changes

; + } + + const sortedIndices = [...visibleSet].sort((a, b) => a - b); + const chunks: number[][] = []; + let currentChunk: number[] = []; + + for (let index = 0; index < sortedIndices.length; index++) { + if (currentChunk.length === 0 || sortedIndices[index] === sortedIndices[index - 1] + 1) { + currentChunk.push(sortedIndices[index]); + continue; + } + + chunks.push(currentChunk); + currentChunk = [sortedIndices[index]]; + } + + if (currentChunk.length > 0) { + chunks.push(currentChunk); + } + + return ( +
+ {chunks.map((chunk, chunkIndex) => ( +
+ {chunkIndex > 0 && ( +
···
+ )} + {chunk.map((lineIndex) => { + const line = diff[lineIndex]; + return ( +
+ {line.type === "add" ? "+ " : line.type === "remove" ? "- " : " "} + {line.text} +
+ ); + })} +
+ ))} +
+ ); +} diff --git a/src/components/ai-chat/extracted-content.tsx b/src/components/ai-chat/extracted-content.tsx new file mode 100644 index 0000000..e9d6410 --- /dev/null +++ b/src/components/ai-chat/extracted-content.tsx @@ -0,0 +1,43 @@ +export interface ExtractResult { + url: string; + raw_content: string; + hasMore?: boolean; +} + +export function ExtractedContent({ results }: { results: ExtractResult[] }) { + return ( +
+ {results.map((result) => { + let hostname = result.url; + + try { + hostname = new URL(result.url).hostname; + } catch { + // Ignore invalid URLs in partial tool output. + } + + return ( +
+
+ + {hostname} + + {result.hasMore && partial} +
+
+              {result.raw_content}
+            
+
+ ); + })} +
+ ); +} diff --git a/src/components/ai-chat/model-picker.tsx b/src/components/ai-chat/model-picker.tsx new file mode 100644 index 0000000..8957469 --- /dev/null +++ b/src/components/ai-chat/model-picker.tsx @@ -0,0 +1,333 @@ +import { Button } from "@/components/ui/button"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { type AIProvider } from "@/lib/ai-keys"; +import { cn } from "@/lib/utils"; +import { CheckIcon, ChevronsUpDownIcon, ChevronUpIcon } from "lucide-react"; +import { useState } from "react"; + +interface AIKeyState { + key: string; + setKey: (key: string) => void; + isSaved: boolean; + setSaved: (saved: boolean) => void; +} + +export const MODEL_GROUPS: { + provider: AIProvider; + label: string; + logoUrl: string; + keyPlaceholder: string; + models: { value: string; label: string; reasoning?: true }[]; +}[] = [ + { + provider: "openai", + label: "OpenAI", + logoUrl: "https://www.google.com/s2/favicons?domain=openai.com&sz=32", + keyPlaceholder: "sk-...", + models: [ + { value: "gpt-5.4", label: "GPT-5.4", reasoning: true }, + { value: "gpt-5.4-pro", label: "GPT-5.4 pro", reasoning: true }, + { value: "gpt-5.4-mini", label: "GPT-5.4 mini", reasoning: true }, + { value: "gpt-5.4-nano", label: "GPT-5.4 nano", reasoning: true }, + { value: "gpt-5", label: "GPT-5", reasoning: true }, + { value: "gpt-5-pro", label: "GPT-5 pro", reasoning: true }, + { value: "gpt-5-mini", label: "GPT-5 mini", reasoning: true }, + { value: "gpt-5-nano", label: "GPT-5 nano", reasoning: true }, + { value: "gpt-5.2", label: "GPT-5.2", reasoning: true }, + { value: "gpt-5.2-pro", label: "GPT-5.2 pro", reasoning: true }, + { value: "gpt-5.1", label: "GPT-5.1", reasoning: true }, + { value: "gpt-4.1", label: "GPT-4.1" }, + { value: "gpt-4.1-mini", label: "GPT-4.1 mini" }, + { value: "gpt-4.1-nano", label: "GPT-4.1 nano" }, + { value: "o4-mini", label: "o4-mini", reasoning: true }, + { value: "o3", label: "o3", reasoning: true }, + { value: "o3-pro", label: "o3-pro", reasoning: true }, + { value: "o1-pro", label: "o1-pro", reasoning: true }, + { value: "gpt-5-codex", label: "GPT-5 Codex", reasoning: true }, + { value: "gpt-5.3-codex", label: "GPT-5.3 Codex", reasoning: true }, + { value: "gpt-5.2-codex", label: "GPT-5.2 Codex", reasoning: true }, + { value: "gpt-5.1-codex", label: "GPT-5.1 Codex", reasoning: true }, + { value: "gpt-5.1-codex-max", label: "GPT-5.1 Codex Max", reasoning: true }, + { value: "gpt-5.1-codex-mini", label: "GPT-5.1 Codex mini", reasoning: true }, + ], + }, + { + provider: "anthropic", + label: "Anthropic", + logoUrl: "https://www.google.com/s2/favicons?domain=anthropic.com&sz=32", + keyPlaceholder: "sk-ant-...", + models: [ + { value: "claude-opus-4-6", label: "Claude Opus 4.6", reasoning: true }, + { value: "claude-sonnet-4-6", label: "Claude Sonnet 4.6", reasoning: true }, + { value: "claude-haiku-4-5", label: "Claude Haiku 4.5" }, + { value: "claude-opus-4-5", label: "Claude Opus 4.5", reasoning: true }, + { value: "claude-sonnet-4-5", label: "Claude Sonnet 4.5", reasoning: true }, + ], + }, + { + provider: "google", + label: "Google", + logoUrl: "https://www.google.com/s2/favicons?domain=google.com&sz=32", + keyPlaceholder: "AIza...", + models: [ + { value: "gemini-3.1-pro-preview", label: "Gemini 3.1 Pro", reasoning: true }, + { value: "gemini-3-flash-preview", label: "Gemini 3 Flash", reasoning: true }, + { value: "gemini-2.5-pro", label: "Gemini 2.5 Pro", reasoning: true }, + { value: "gemini-2.5-flash", label: "Gemini 2.5 Flash", reasoning: true }, + { value: "gemini-2.5-flash-lite", label: "Gemini 2.5 Flash Lite" }, + ], + }, + { + provider: "groq", + label: "Groq", + logoUrl: "https://www.google.com/s2/favicons?domain=groq.com&sz=32", + keyPlaceholder: "gsk_...", + models: [ + { value: "meta-llama/llama-4-maverick-17b-128e-instruct", label: "Llama 4 Maverick" }, + { value: "meta-llama/llama-4-scout-17b-16e-instruct", label: "Llama 4 Scout" }, + { value: "qwen/qwen3-32b", label: "Qwen 3 32B" }, + { value: "llama-3.3-70b-versatile", label: "Llama 3.3 70B" }, + { value: "llama-3.1-8b-instant", label: "Llama 3.1 8B" }, + ], + }, + { + provider: "mistral", + label: "Mistral", + logoUrl: "https://www.google.com/s2/favicons?domain=mistral.ai&sz=32", + keyPlaceholder: "...", + models: [ + { value: "mistral-large-latest", label: "Mistral Large" }, + { value: "magistral-medium-latest", label: "Magistral Medium" }, + { value: "codestral-latest", label: "Codestral" }, + { value: "mistral-medium-latest", label: "Mistral Medium" }, + { value: "mistral-small-latest", label: "Mistral Small" }, + ], + }, + { + provider: "xai", + label: "xAI", + logoUrl: "https://www.google.com/s2/favicons?domain=x.ai&sz=32", + keyPlaceholder: "xai-...", + models: [ + { value: "grok-4-0709", label: "Grok 4" }, + { value: "grok-3", label: "Grok 3" }, + { value: "grok-3-mini", label: "Grok 3 mini" }, + ], + }, +]; + +export function providerForModel(model: string): AIProvider { + if (model.startsWith("claude")) return "anthropic"; + if (model.startsWith("gemini")) return "google"; + if (model.startsWith("llama") || model.startsWith("meta-llama") || model.startsWith("qwen")) + return "groq"; + if (model.startsWith("mistral") || model.startsWith("codestral") || model.startsWith("magistral")) + return "mistral"; + if (model.startsWith("grok")) return "xai"; + return "openai"; +} + +export type ReasoningEffort = "none" | "minimal" | "low" | "medium" | "high"; + +export function getProviderOptions( + provider: AIProvider, + model: string, + effort: ReasoningEffort, +): Record { + if (provider === "openai") { + if (effort === "none") return {}; + const effortMap = { minimal: "low", low: "low", medium: "medium", high: "high" } as const; + return { openai: { reasoningEffort: effortMap[effort] } }; + } + if (provider === "anthropic") { + if (effort === "none") return { anthropic: { thinking: { type: "disabled" } } }; + const budgetTokens = { minimal: 256, low: 1024, medium: 5000, high: 16000 }[effort]; + return { anthropic: { thinking: { type: "enabled", budgetTokens } } }; + } + if (provider === "google") { + if (effort === "none") return { google: { thinkingConfig: { thinkingBudget: 0 } } }; + const thinkingBudget = { minimal: 128, low: 512, medium: 4096, high: 16000 }[effort]; + return { google: { thinkingConfig: { thinkingBudget } } }; + } + return {}; +} + +interface ModelPickerProps { + model: string; + setModel: (m: string) => void; + provider: AIProvider; + allProviderKeys: Record, AIKeyState>; +} + +export function ModelPicker({ model, setModel, provider, allProviderKeys }: ModelPickerProps) { + const [open, setOpen] = useState(false); + const [selectedProvider, setSelectedProvider] = useState(provider); + + const currentGroup = MODEL_GROUPS.find((g) => g.provider === provider); + const currentModelLabel = + MODEL_GROUPS.flatMap((g) => g.models).find((m) => m.value === model)?.label ?? model; + const selectedGroup = + MODEL_GROUPS.find((g) => g.provider === selectedProvider) ?? MODEL_GROUPS[0]; + const selectedProviderKeys = allProviderKeys[selectedProvider as Exclude]; + + return ( + { + setOpen(o); + if (o) setSelectedProvider(provider); + }} + > + + + + +
+
+ {MODEL_GROUPS.map((g) => { + const isKeyConfigured = + allProviderKeys[g.provider as Exclude]?.isSaved; + return ( + + ); + })} +
+
+

+ {selectedGroup.label} +

+ {!selectedProviderKeys?.isSaved && ( +

No API key configured

+ )} +
+ {selectedGroup.models.map((m) => { + const isKeyConfigured = selectedProviderKeys?.isSaved; + return ( + + ); + })} +
+
+
+
+
+ ); +} + +const EFFORT_OPTIONS: { value: ReasoningEffort; label: string }[] = [ + { value: "none", label: "Off" }, + { value: "minimal", label: "Min" }, + { value: "low", label: "Low" }, + { value: "medium", label: "Med" }, + { value: "high", label: "High" }, +]; + +interface ReasoningEffortPickerProps { + model: string; + effort: ReasoningEffort | null; + onEffortChange: (e: ReasoningEffort | null) => void; +} + +export function ReasoningEffortPicker({ + model, + effort, + onEffortChange, +}: ReasoningEffortPickerProps) { + const [open, setOpen] = useState(false); + + const isReasoningModel = + MODEL_GROUPS.flatMap((g) => g.models).find((m) => m.value === model)?.reasoning === true; + if (!isReasoningModel) return null; + + const current = EFFORT_OPTIONS.find((o) => o.value === effort); + + return ( + + + + + + {EFFORT_OPTIONS.map(({ value, label }) => ( + + ))} + + + ); +} diff --git a/src/components/ai-chat/web-search-results.tsx b/src/components/ai-chat/web-search-results.tsx new file mode 100644 index 0000000..66cc596 --- /dev/null +++ b/src/components/ai-chat/web-search-results.tsx @@ -0,0 +1,46 @@ +export interface WebSearchResult { + title: string; + url: string; + content: string; +} + +export function WebSearchResults({ results }: { results: WebSearchResult[] }) { + return ( +
+ {results.slice(0, 3).map((result) => { + let hostname = result.url; + + try { + hostname = new URL(result.url).hostname; + } catch { + // Ignore invalid URLs in partial tool output. + } + + return ( + +
+ + {result.title} + + + {hostname} + +
+ {result.content && ( +

{result.content}

+ )} +

+ {hostname} +

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

{caption}

} +
+ ), +); + +ChainOfThought.displayName = "ChainOfThought"; +ChainOfThoughtHeader.displayName = "ChainOfThoughtHeader"; +ChainOfThoughtStep.displayName = "ChainOfThoughtStep"; +ChainOfThoughtSearchResults.displayName = "ChainOfThoughtSearchResults"; +ChainOfThoughtSearchResult.displayName = "ChainOfThoughtSearchResult"; +ChainOfThoughtContent.displayName = "ChainOfThoughtContent"; +ChainOfThoughtImage.displayName = "ChainOfThoughtImage"; diff --git a/src/components/ai-elements/context.tsx b/src/components/ai-elements/context.tsx new file mode 100644 index 0000000..96128a9 --- /dev/null +++ b/src/components/ai-elements/context.tsx @@ -0,0 +1,399 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card"; +import { Progress } from "@/components/ui/progress"; +import { cn } from "@/lib/utils"; +import type { LanguageModelUsage } from "ai"; +import type { ComponentProps } from "react"; +import { createContext, useContext, useMemo } from "react"; +import { getUsage } from "tokenlens"; + +const PERCENT_MAX = 100; +const ICON_RADIUS = 10; +const ICON_VIEWBOX = 24; +const ICON_CENTER = 12; +const ICON_STROKE_WIDTH = 2; + +type ModelId = string; + +interface ContextSchema { + usedTokens: number; + maxTokens: number; + usage?: LanguageModelUsage; + modelId?: ModelId; +} + +const ContextContext = createContext(null); + +const useContextValue = () => { + const context = useContext(ContextContext); + + if (!context) { + throw new Error("Context components must be used within Context"); + } + + return context; +}; + +export type ContextProps = ComponentProps & ContextSchema; + +export const Context = ({ usedTokens, maxTokens, usage, modelId, ...props }: ContextProps) => { + const contextValue = useMemo( + () => ({ maxTokens, modelId, usage, usedTokens }), + [maxTokens, modelId, usage, usedTokens], + ); + + return ( + + + + ); +}; + +const ContextIcon = () => { + const { usedTokens, maxTokens } = useContextValue(); + const circumference = 2 * Math.PI * ICON_RADIUS; + const usedPercent = usedTokens / maxTokens; + const dashOffset = circumference * (1 - usedPercent); + + return ( + + + + + ); +}; + +export type ContextTriggerProps = ComponentProps; + +export const ContextTrigger = ({ children, ...props }: ContextTriggerProps) => { + const { usedTokens, maxTokens } = useContextValue(); + const usedPercent = usedTokens / maxTokens; + const renderedPercent = new Intl.NumberFormat("en-US", { + maximumFractionDigits: 1, + style: "percent", + }).format(usedPercent); + + return ( + + {children ?? ( + + )} + + ); +}; + +export type ContextContentProps = ComponentProps; + +export const ContextContent = ({ className, ...props }: ContextContentProps) => ( + +); + +export type ContextContentHeaderProps = ComponentProps<"div">; + +export const ContextContentHeader = ({ + children, + className, + ...props +}: ContextContentHeaderProps) => { + const { usedTokens, maxTokens } = useContextValue(); + const usedPercent = usedTokens / maxTokens; + const displayPct = new Intl.NumberFormat("en-US", { + maximumFractionDigits: 1, + style: "percent", + }).format(usedPercent); + const used = new Intl.NumberFormat("en-US", { + notation: "compact", + }).format(usedTokens); + const total = new Intl.NumberFormat("en-US", { + notation: "compact", + }).format(maxTokens); + + return ( +
+ {children ?? ( + <> +
+

{displayPct}

+

+ {used} / {total} +

+
+
+ +
+ + )} +
+ ); +}; + +export type ContextContentBodyProps = ComponentProps<"div">; + +export const ContextContentBody = ({ children, className, ...props }: ContextContentBodyProps) => ( +
+ {children} +
+); + +export type ContextContentFooterProps = ComponentProps<"div">; + +export const ContextContentFooter = ({ + children, + className, + ...props +}: ContextContentFooterProps) => { + const { modelId, usage } = useContextValue(); + const costUSD = modelId + ? getUsage({ + modelId, + usage: { + input: usage?.inputTokens ?? 0, + output: usage?.outputTokens ?? 0, + }, + }).costUSD?.totalUSD + : undefined; + const totalCost = new Intl.NumberFormat("en-US", { + currency: "USD", + style: "currency", + }).format(costUSD ?? 0); + + return ( +
+ {children ?? ( + <> + Total cost + {totalCost} + + )} +
+ ); +}; + +const TokensWithCost = ({ tokens, costText }: { tokens?: number; costText?: string }) => ( + + {tokens === undefined + ? "—" + : new Intl.NumberFormat("en-US", { + notation: "compact", + }).format(tokens)} + {costText ? • {costText} : null} + +); + +export type ContextInputUsageProps = ComponentProps<"div">; + +export const ContextInputUsage = ({ className, children, ...props }: ContextInputUsageProps) => { + const { usage, modelId } = useContextValue(); + const inputTokens = usage?.inputTokens ?? 0; + + if (children) { + return children; + } + + if (!inputTokens) { + return null; + } + + const inputCost = modelId + ? getUsage({ + modelId, + usage: { input: inputTokens, output: 0 }, + }).costUSD?.totalUSD + : undefined; + const inputCostText = new Intl.NumberFormat("en-US", { + currency: "USD", + style: "currency", + }).format(inputCost ?? 0); + + return ( +
+ Input + +
+ ); +}; + +export type ContextOutputUsageProps = ComponentProps<"div">; + +export const ContextOutputUsage = ({ className, children, ...props }: ContextOutputUsageProps) => { + const { usage, modelId } = useContextValue(); + const outputTokens = usage?.outputTokens ?? 0; + + if (children) { + return children; + } + + if (!outputTokens) { + return null; + } + + const outputCost = modelId + ? getUsage({ + modelId, + usage: { input: 0, output: outputTokens }, + }).costUSD?.totalUSD + : undefined; + const outputCostText = new Intl.NumberFormat("en-US", { + currency: "USD", + style: "currency", + }).format(outputCost ?? 0); + + return ( +
+ Output + +
+ ); +}; + +export type ContextReasoningUsageProps = ComponentProps<"div">; + +export const ContextReasoningUsage = ({ + className, + children, + ...props +}: ContextReasoningUsageProps) => { + const { usage, modelId } = useContextValue(); + const reasoningTokens = usage?.reasoningTokens ?? 0; + + if (children) { + return children; + } + + if (!reasoningTokens) { + return null; + } + + const reasoningCost = modelId + ? getUsage({ + modelId, + usage: { reasoningTokens }, + }).costUSD?.totalUSD + : undefined; + const reasoningCostText = new Intl.NumberFormat("en-US", { + currency: "USD", + style: "currency", + }).format(reasoningCost ?? 0); + + return ( +
+ Reasoning + +
+ ); +}; + +export type ContextCacheUsageProps = ComponentProps<"div">; + +export const ContextCacheUsage = ({ className, children, ...props }: ContextCacheUsageProps) => { + const { usage, modelId } = useContextValue(); + const cacheTokens = usage?.cachedInputTokens ?? 0; + + if (children) { + return children; + } + + if (!cacheTokens) { + return null; + } + + const cacheCost = modelId + ? getUsage({ + modelId, + usage: { cacheReads: cacheTokens, input: 0, output: 0 }, + }).costUSD?.totalUSD + : undefined; + const cacheCostText = new Intl.NumberFormat("en-US", { + currency: "USD", + style: "currency", + }).format(cacheCost ?? 0); + + return ( +
+ Cache + +
+ ); +}; diff --git a/src/components/ai-elements/prompt-input.tsx b/src/components/ai-elements/prompt-input.tsx new file mode 100644 index 0000000..2f81d6e --- /dev/null +++ b/src/components/ai-elements/prompt-input.tsx @@ -0,0 +1,1391 @@ +"use client"; + +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, +} from "@/components/ui/command"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card"; +import { + InputGroup, + InputGroupAddon, + InputGroupButton, + InputGroupTextarea, +} from "@/components/ui/input-group"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Spinner } from "@/components/ui/spinner"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; +import { cn } from "@/lib/utils"; +import type { ChatStatus, FileUIPart, SourceDocumentUIPart } from "ai"; +import { ArrowUpIcon, ImageIcon, MonitorIcon, PlusIcon, SquareIcon, XIcon } from "lucide-react"; +import { nanoid } from "nanoid"; +import type { + ChangeEvent, + ChangeEventHandler, + ClipboardEventHandler, + ComponentProps, + FormEvent, + FormEventHandler, + HTMLAttributes, + KeyboardEventHandler, + PropsWithChildren, + ReactNode, + RefObject, +} from "react"; +import { + Children, + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from "react"; + +// ============================================================================ +// Helpers +// ============================================================================ + +const convertBlobUrlToDataUrl = async (url: string): Promise => { + try { + const response = await fetch(url); + const blob = await response.blob(); + // FileReader uses callback-based API, wrapping in Promise is necessary + // oxlint-disable-next-line eslint-plugin-promise(avoid-new) + return new Promise((resolve) => { + const reader = new FileReader(); + // oxlint-disable-next-line eslint-plugin-unicorn(prefer-add-event-listener) + reader.onloadend = () => resolve(reader.result as string); + // oxlint-disable-next-line eslint-plugin-unicorn(prefer-add-event-listener) + reader.onerror = () => resolve(null); + reader.readAsDataURL(blob); + }); + } catch { + return null; + } +}; + +const captureScreenshot = async (): Promise => { + if (typeof navigator === "undefined" || !navigator.mediaDevices?.getDisplayMedia) { + return null; + } + + let stream: MediaStream | null = null; + const video = document.createElement("video"); + video.muted = true; + video.playsInline = true; + + try { + stream = await navigator.mediaDevices.getDisplayMedia({ + audio: false, + video: true, + }); + + video.srcObject = stream; + + // Video element uses callback-based API, wrapping in Promise is necessary + // oxlint-disable-next-line eslint-plugin-promise(avoid-new) + await new Promise((resolve, reject) => { + // oxlint-disable-next-line eslint-plugin-unicorn(prefer-add-event-listener) + video.onloadedmetadata = () => resolve(); + // oxlint-disable-next-line eslint-plugin-unicorn(prefer-add-event-listener) + video.onerror = () => reject(new Error("Failed to load screen stream")); + }); + + await video.play(); + + const width = video.videoWidth; + const height = video.videoHeight; + if (!width || !height) { + return null; + } + + const canvas = document.createElement("canvas"); + canvas.width = width; + canvas.height = height; + const context = canvas.getContext("2d"); + if (!context) { + return null; + } + + context.drawImage(video, 0, 0, width, height); + // canvas.toBlob uses callback-based API, wrapping in Promise is necessary + // oxlint-disable-next-line eslint-plugin-promise(avoid-new) + const blob = await new Promise((resolve) => { + canvas.toBlob(resolve, "image/png"); + }); + if (!blob) { + return null; + } + + const timestamp = new Date() + .toISOString() + .replaceAll(/[:.]/g, "-") + .replace("T", "_") + .replace("Z", ""); + + return new File([blob], `screenshot-${timestamp}.png`, { + lastModified: Date.now(), + type: "image/png", + }); + } finally { + if (stream) { + for (const track of stream.getTracks()) { + track.stop(); + } + } + video.pause(); + video.srcObject = null; + } +}; + +// ============================================================================ +// Provider Context & Types +// ============================================================================ + +export interface AttachmentsContext { + files: (FileUIPart & { id: string })[]; + add: (files: File[] | FileList) => void; + remove: (id: string) => void; + clear: () => void; + openFileDialog: () => void; + fileInputRef: RefObject; +} + +export interface TextInputContext { + value: string; + setInput: (v: string) => void; + clear: () => void; +} + +export interface PromptInputControllerProps { + textInput: TextInputContext; + attachments: AttachmentsContext; + /** INTERNAL: Allows PromptInput to register its file textInput + "open" callback */ + __registerFileInput: (ref: RefObject, open: () => void) => void; +} + +const PromptInputController = createContext(null); +const ProviderAttachmentsContext = createContext(null); + +export const usePromptInputController = () => { + const ctx = useContext(PromptInputController); + if (!ctx) { + throw new Error( + "Wrap your component inside to use usePromptInputController().", + ); + } + return ctx; +}; + +// Optional variants (do NOT throw). Useful for dual-mode components. +const useOptionalPromptInputController = () => useContext(PromptInputController); + +export const useProviderAttachments = () => { + const ctx = useContext(ProviderAttachmentsContext); + if (!ctx) { + throw new Error( + "Wrap your component inside to use useProviderAttachments().", + ); + } + return ctx; +}; + +const useOptionalProviderAttachments = () => useContext(ProviderAttachmentsContext); + +export type PromptInputProviderProps = PropsWithChildren<{ + initialInput?: string; +}>; + +/** + * Optional global provider that lifts PromptInput state outside of PromptInput. + * If you don't use it, PromptInput stays fully self-managed. + */ +export const PromptInputProvider = ({ + initialInput: initialTextInput = "", + children, +}: PromptInputProviderProps) => { + // ----- textInput state + const [textInput, setTextInput] = useState(initialTextInput); + const clearInput = useCallback(() => setTextInput(""), []); + + // ----- attachments state (global when wrapped) + const [attachmentFiles, setAttachmentFiles] = useState<(FileUIPart & { id: string })[]>([]); + const fileInputRef = useRef(null); + // oxlint-disable-next-line eslint(no-empty-function) + const openRef = useRef<() => void>(() => {}); + + const add = useCallback((files: File[] | FileList) => { + const incoming = [...files]; + if (incoming.length === 0) { + return; + } + + setAttachmentFiles((prev) => [ + ...prev, + ...incoming.map((file) => ({ + filename: file.name, + id: nanoid(), + mediaType: file.type, + type: "file" as const, + url: URL.createObjectURL(file), + })), + ]); + }, []); + + const remove = useCallback((id: string) => { + setAttachmentFiles((prev) => { + const found = prev.find((f) => f.id === id); + if (found?.url) { + URL.revokeObjectURL(found.url); + } + return prev.filter((f) => f.id !== id); + }); + }, []); + + const clear = useCallback(() => { + setAttachmentFiles((prev) => { + for (const f of prev) { + if (f.url) { + URL.revokeObjectURL(f.url); + } + } + return []; + }); + }, []); + + // Keep a ref to attachments for cleanup on unmount (avoids stale closure) + const attachmentsRef = useRef(attachmentFiles); + + useEffect(() => { + attachmentsRef.current = attachmentFiles; + }, [attachmentFiles]); + + // Cleanup blob URLs on unmount to prevent memory leaks + useEffect( + () => () => { + for (const f of attachmentsRef.current) { + if (f.url) { + URL.revokeObjectURL(f.url); + } + } + }, + [], + ); + + const openFileDialog = useCallback(() => { + openRef.current?.(); + }, []); + + const attachments = useMemo( + () => ({ + add, + clear, + fileInputRef, + files: attachmentFiles, + openFileDialog, + remove, + }), + [attachmentFiles, add, remove, clear, openFileDialog], + ); + + const __registerFileInput = useCallback( + (ref: RefObject, open: () => void) => { + fileInputRef.current = ref.current; + openRef.current = open; + }, + [], + ); + + const controller = useMemo( + () => ({ + __registerFileInput, + attachments, + textInput: { + clear: clearInput, + setInput: setTextInput, + value: textInput, + }, + }), + [textInput, clearInput, attachments, __registerFileInput], + ); + + return ( + + + {children} + + + ); +}; + +// ============================================================================ +// Component Context & Hooks +// ============================================================================ + +const LocalAttachmentsContext = createContext(null); + +export const usePromptInputAttachments = () => { + // Prefer local context (inside PromptInput) as it has validation, fall back to provider + const provider = useOptionalProviderAttachments(); + const local = useContext(LocalAttachmentsContext); + const context = local ?? provider; + if (!context) { + throw new Error( + "usePromptInputAttachments must be used within a PromptInput or PromptInputProvider", + ); + } + return context; +}; + +// ============================================================================ +// Referenced Sources (Local to PromptInput) +// ============================================================================ + +export interface ReferencedSourcesContext { + sources: (SourceDocumentUIPart & { id: string })[]; + add: (sources: SourceDocumentUIPart[] | SourceDocumentUIPart) => void; + remove: (id: string) => void; + clear: () => void; +} + +export const LocalReferencedSourcesContext = createContext(null); + +export const usePromptInputReferencedSources = () => { + const ctx = useContext(LocalReferencedSourcesContext); + if (!ctx) { + throw new Error( + "usePromptInputReferencedSources must be used within a LocalReferencedSourcesContext.Provider", + ); + } + return ctx; +}; + +export type PromptInputActionAddAttachmentsProps = ComponentProps & { + label?: string; +}; + +export const PromptInputActionAddAttachments = ({ + label = "Add photos or files", + ...props +}: PromptInputActionAddAttachmentsProps) => { + const attachments = usePromptInputAttachments(); + + const handleSelect = useCallback( + (e: Event) => { + e.preventDefault(); + attachments.openFileDialog(); + }, + [attachments], + ); + + return ( + + {label} + + ); +}; + +export type PromptInputActionAddScreenshotProps = ComponentProps & { + label?: string; +}; + +export const PromptInputActionAddScreenshot = ({ + label = "Take screenshot", + onSelect, + ...props +}: PromptInputActionAddScreenshotProps) => { + const attachments = usePromptInputAttachments(); + + const handleSelect = useCallback( + async (event: Event) => { + onSelect?.(event); + if (event.defaultPrevented) { + return; + } + + try { + const screenshot = await captureScreenshot(); + if (screenshot) { + attachments.add([screenshot]); + } + } catch (error) { + if ( + error instanceof DOMException && + (error.name === "NotAllowedError" || error.name === "AbortError") + ) { + return; + } + throw error; + } + }, + [onSelect, attachments], + ); + + return ( + + + {label} + + ); +}; + +export interface PromptInputMessage { + text: string; + files: FileUIPart[]; +} + +export type PromptInputProps = Omit, "onSubmit" | "onError"> & { + // e.g., "image/*" or leave undefined for any + accept?: string; + multiple?: boolean; + // When true, accepts drops anywhere on document. Default false (opt-in). + globalDrop?: boolean; + // Render a hidden input with given name and keep it in sync for native form posts. Default false. + syncHiddenInput?: boolean; + // Minimal constraints + maxFiles?: number; + // bytes + maxFileSize?: number; + onError?: (err: { code: "max_files" | "max_file_size" | "accept"; message: string }) => void; + onSubmit: ( + message: PromptInputMessage, + event: FormEvent, + ) => void | Promise; +}; + +export const PromptInput = ({ + className, + accept, + multiple, + globalDrop, + syncHiddenInput, + maxFiles, + maxFileSize, + onError, + onSubmit, + children, + ...props +}: PromptInputProps) => { + // Try to use a provider controller if present + const controller = useOptionalPromptInputController(); + const usingProvider = !!controller; + + // Refs + const inputRef = useRef(null); + const formRef = useRef(null); + + // ----- Local attachments (only used when no provider) + const [items, setItems] = useState<(FileUIPart & { id: string })[]>([]); + const files = usingProvider ? controller.attachments.files : items; + + // ----- Local referenced sources (always local to PromptInput) + const [referencedSources, setReferencedSources] = useState< + (SourceDocumentUIPart & { id: string })[] + >([]); + + // Keep a ref to files for cleanup on unmount (avoids stale closure) + const filesRef = useRef(files); + + useEffect(() => { + filesRef.current = files; + }, [files]); + + const openFileDialogLocal = useCallback(() => { + inputRef.current?.click(); + }, []); + + const matchesAccept = useCallback( + (f: File) => { + if (!accept || accept.trim() === "") { + return true; + } + + const patterns = accept + .split(",") + .map((s) => s.trim()) + .filter(Boolean); + + return patterns.some((pattern) => { + if (pattern.endsWith("/*")) { + // e.g: image/* -> image/ + const prefix = pattern.slice(0, -1); + return f.type.startsWith(prefix); + } + return f.type === pattern; + }); + }, + [accept], + ); + + const addLocal = useCallback( + (fileList: File[] | FileList) => { + const incoming = [...fileList]; + const accepted = incoming.filter((f) => matchesAccept(f)); + if (incoming.length && accepted.length === 0) { + onError?.({ + code: "accept", + message: "No files match the accepted types.", + }); + return; + } + const withinSize = (f: File) => (maxFileSize ? f.size <= maxFileSize : true); + const sized = accepted.filter(withinSize); + if (accepted.length > 0 && sized.length === 0) { + onError?.({ + code: "max_file_size", + message: "All files exceed the maximum size.", + }); + return; + } + + setItems((prev) => { + const capacity = + typeof maxFiles === "number" ? Math.max(0, maxFiles - prev.length) : undefined; + const capped = typeof capacity === "number" ? sized.slice(0, capacity) : sized; + if (typeof capacity === "number" && sized.length > capacity) { + onError?.({ + code: "max_files", + message: "Too many files. Some were not added.", + }); + } + const next: (FileUIPart & { id: string })[] = []; + for (const file of capped) { + next.push({ + filename: file.name, + id: nanoid(), + mediaType: file.type, + type: "file", + url: URL.createObjectURL(file), + }); + } + return [...prev, ...next]; + }); + }, + [matchesAccept, maxFiles, maxFileSize, onError], + ); + + const removeLocal = useCallback( + (id: string) => + setItems((prev) => { + const found = prev.find((file) => file.id === id); + if (found?.url) { + URL.revokeObjectURL(found.url); + } + return prev.filter((file) => file.id !== id); + }), + [], + ); + + // Wrapper that validates files before calling provider's add + const addWithProviderValidation = useCallback( + (fileList: File[] | FileList) => { + const incoming = [...fileList]; + const accepted = incoming.filter((f) => matchesAccept(f)); + if (incoming.length && accepted.length === 0) { + onError?.({ + code: "accept", + message: "No files match the accepted types.", + }); + return; + } + const withinSize = (f: File) => (maxFileSize ? f.size <= maxFileSize : true); + const sized = accepted.filter(withinSize); + if (accepted.length > 0 && sized.length === 0) { + onError?.({ + code: "max_file_size", + message: "All files exceed the maximum size.", + }); + return; + } + + const currentCount = files.length; + const capacity = + typeof maxFiles === "number" ? Math.max(0, maxFiles - currentCount) : undefined; + const capped = typeof capacity === "number" ? sized.slice(0, capacity) : sized; + if (typeof capacity === "number" && sized.length > capacity) { + onError?.({ + code: "max_files", + message: "Too many files. Some were not added.", + }); + } + + if (capped.length > 0) { + controller?.attachments.add(capped); + } + }, + [matchesAccept, maxFileSize, maxFiles, onError, files.length, controller], + ); + + const clearAttachments = useCallback( + () => + usingProvider + ? controller?.attachments.clear() + : setItems((prev) => { + for (const file of prev) { + if (file.url) { + URL.revokeObjectURL(file.url); + } + } + return []; + }), + [usingProvider, controller], + ); + + const clearReferencedSources = useCallback(() => setReferencedSources([]), []); + + const add = usingProvider ? addWithProviderValidation : addLocal; + const remove = usingProvider ? controller.attachments.remove : removeLocal; + const openFileDialog = usingProvider + ? controller.attachments.openFileDialog + : openFileDialogLocal; + + const clear = useCallback(() => { + clearAttachments(); + clearReferencedSources(); + }, [clearAttachments, clearReferencedSources]); + + // Let provider know about our hidden file input so external menus can call openFileDialog() + useEffect(() => { + if (!usingProvider) { + return; + } + controller.__registerFileInput(inputRef, () => inputRef.current?.click()); + }, [usingProvider, controller]); + + // Note: File input cannot be programmatically set for security reasons + // The syncHiddenInput prop is no longer functional + useEffect(() => { + if (syncHiddenInput && inputRef.current && files.length === 0) { + inputRef.current.value = ""; + } + }, [files, syncHiddenInput]); + + // Attach drop handlers on nearest form and document (opt-in) + useEffect(() => { + const form = formRef.current; + if (!form) { + return; + } + if (globalDrop) { + // when global drop is on, let the document-level handler own drops + return; + } + + const onDragOver = (e: DragEvent) => { + if (e.dataTransfer?.types?.includes("Files")) { + e.preventDefault(); + } + }; + const onDrop = (e: DragEvent) => { + if (e.dataTransfer?.types?.includes("Files")) { + e.preventDefault(); + } + if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) { + add(e.dataTransfer.files); + } + }; + form.addEventListener("dragover", onDragOver); + form.addEventListener("drop", onDrop); + return () => { + form.removeEventListener("dragover", onDragOver); + form.removeEventListener("drop", onDrop); + }; + }, [add, globalDrop]); + + useEffect(() => { + if (!globalDrop) { + return; + } + + const onDragOver = (e: DragEvent) => { + if (e.dataTransfer?.types?.includes("Files")) { + e.preventDefault(); + } + }; + const onDrop = (e: DragEvent) => { + if (e.dataTransfer?.types?.includes("Files")) { + e.preventDefault(); + } + if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) { + add(e.dataTransfer.files); + } + }; + document.addEventListener("dragover", onDragOver); + document.addEventListener("drop", onDrop); + return () => { + document.removeEventListener("dragover", onDragOver); + document.removeEventListener("drop", onDrop); + }; + }, [add, globalDrop]); + + useEffect( + () => () => { + if (!usingProvider) { + for (const f of filesRef.current) { + if (f.url) { + URL.revokeObjectURL(f.url); + } + } + } + }, + [usingProvider], + ); + + const handleChange: ChangeEventHandler = useCallback( + (event) => { + if (event.currentTarget.files) { + add(event.currentTarget.files); + } + // Reset input value to allow selecting files that were previously removed + event.currentTarget.value = ""; + }, + [add], + ); + + const attachmentsCtx = useMemo( + () => ({ + add, + clear: clearAttachments, + fileInputRef: inputRef, + files: files.map((item) => ({ ...item, id: item.id })), + openFileDialog, + remove, + }), + [files, add, remove, clearAttachments, openFileDialog], + ); + + const refsCtx = useMemo( + () => ({ + add: (incoming: SourceDocumentUIPart[] | SourceDocumentUIPart) => { + const array = Array.isArray(incoming) ? incoming : [incoming]; + setReferencedSources((prev) => [...prev, ...array.map((s) => ({ ...s, id: nanoid() }))]); + }, + clear: clearReferencedSources, + remove: (id: string) => { + setReferencedSources((prev) => prev.filter((s) => s.id !== id)); + }, + sources: referencedSources, + }), + [referencedSources, clearReferencedSources], + ); + + const handleSubmit: FormEventHandler = useCallback( + async (event) => { + event.preventDefault(); + + const form = event.currentTarget; + const text = usingProvider + ? controller.textInput.value + : (() => { + const formData = new FormData(form); + return (formData.get("message") as string) || ""; + })(); + + // Reset form immediately after capturing text to avoid race condition + // where user input during async blob conversion would be lost + if (!usingProvider) { + form.reset(); + } + + try { + // Convert blob URLs to data URLs asynchronously + const convertedFiles: FileUIPart[] = await Promise.all( + files.map(async ({ id: _id, ...item }) => { + if (item.url?.startsWith("blob:")) { + const dataUrl = await convertBlobUrlToDataUrl(item.url); + // If conversion failed, keep the original blob URL + return { + ...item, + url: dataUrl ?? item.url, + }; + } + return item; + }), + ); + + const result = onSubmit({ files: convertedFiles, text }, event); + + // Handle both sync and async onSubmit + if (result instanceof Promise) { + try { + await result; + clear(); + if (usingProvider) { + controller.textInput.clear(); + } + } catch { + // Don't clear on error - user may want to retry + } + } else { + // Sync function completed without throwing, clear inputs + clear(); + if (usingProvider) { + controller.textInput.clear(); + } + } + } catch { + // Don't clear on error - user may want to retry + } + }, + [usingProvider, controller, files, onSubmit, clear], + ); + + // Render with or without local provider + const inner = ( + <> + +
+ {children} +
+ + ); + + const withReferencedSources = ( + + {inner} + + ); + + // Always provide LocalAttachmentsContext so children get validated add function + return ( + + {withReferencedSources} + + ); +}; + +export type PromptInputBodyProps = HTMLAttributes; + +export const PromptInputBody = ({ className, ...props }: PromptInputBodyProps) => ( +
+); + +export type PromptInputTextareaProps = ComponentProps; + +export const PromptInputTextarea = ({ + onChange, + onKeyDown, + className, + placeholder = "What would you like to know?", + ...props +}: PromptInputTextareaProps) => { + const controller = useOptionalPromptInputController(); + const attachments = usePromptInputAttachments(); + const [isComposing, setIsComposing] = useState(false); + + const handleKeyDown: KeyboardEventHandler = useCallback( + (e) => { + // Call the external onKeyDown handler first + onKeyDown?.(e); + + // If the external handler prevented default, don't run internal logic + if (e.defaultPrevented) { + return; + } + + if (e.key === "Enter") { + if (isComposing || e.nativeEvent.isComposing) { + return; + } + if (e.shiftKey) { + return; + } + e.preventDefault(); + + // Check if the submit button is disabled before submitting + const { form } = e.currentTarget; + const submitButton = form?.querySelector( + 'button[type="submit"]', + ) as HTMLButtonElement | null; + if (submitButton?.disabled) { + return; + } + + form?.requestSubmit(); + } + + // Remove last attachment when Backspace is pressed and textarea is empty + if (e.key === "Backspace" && e.currentTarget.value === "" && attachments.files.length > 0) { + e.preventDefault(); + const lastAttachment = attachments.files.at(-1); + if (lastAttachment) { + attachments.remove(lastAttachment.id); + } + } + }, + [onKeyDown, isComposing, attachments], + ); + + const handlePaste: ClipboardEventHandler = useCallback( + (event) => { + const items = event.clipboardData?.items; + + if (!items) { + return; + } + + const files: File[] = []; + + for (const item of items) { + if (item.kind === "file") { + const file = item.getAsFile(); + if (file) { + files.push(file); + } + } + } + + if (files.length > 0) { + event.preventDefault(); + attachments.add(files); + } + }, + [attachments], + ); + + const handleCompositionEnd = useCallback(() => setIsComposing(false), []); + const handleCompositionStart = useCallback(() => setIsComposing(true), []); + + const controlledProps = controller + ? { + onChange: (e: ChangeEvent) => { + controller.textInput.setInput(e.currentTarget.value); + onChange?.(e); + }, + value: controller.textInput.value, + } + : { + onChange, + }; + + return ( + + ); +}; + +export type PromptInputHeaderProps = Omit, "align">; + +export const PromptInputHeader = ({ className, ...props }: PromptInputHeaderProps) => ( + +); + +export type PromptInputFooterProps = Omit, "align">; + +export const PromptInputFooter = ({ className, ...props }: PromptInputFooterProps) => ( + +); + +export type PromptInputToolsProps = HTMLAttributes; + +export const PromptInputTools = ({ className, ...props }: PromptInputToolsProps) => ( +
+); + +export type PromptInputButtonTooltip = + | string + | { + content: ReactNode; + shortcut?: string; + side?: ComponentProps["side"]; + }; + +export type PromptInputButtonProps = ComponentProps & { + tooltip?: PromptInputButtonTooltip; +}; + +export const PromptInputButton = ({ + variant = "ghost", + className, + size, + tooltip, + ...props +}: PromptInputButtonProps) => { + const newSize = size ?? (Children.count(props.children) > 1 ? "sm" : "icon-sm"); + + const button = ( + + ); + + if (!tooltip) { + return button; + } + + const tooltipContent = typeof tooltip === "string" ? tooltip : tooltip.content; + const shortcut = typeof tooltip === "string" ? undefined : tooltip.shortcut; + const side = typeof tooltip === "string" ? "top" : (tooltip.side ?? "top"); + + return ( + + {button} + + {tooltipContent} + {shortcut && {shortcut}} + + + ); +}; + +export type PromptInputActionMenuProps = ComponentProps; +export const PromptInputActionMenu = (props: PromptInputActionMenuProps) => ( + +); + +export type PromptInputActionMenuTriggerProps = PromptInputButtonProps; + +export const PromptInputActionMenuTrigger = ({ + className, + children, + ...props +}: PromptInputActionMenuTriggerProps) => ( + + + {children ?? } + + +); + +export type PromptInputActionMenuContentProps = ComponentProps; +export const PromptInputActionMenuContent = ({ + className, + ...props +}: PromptInputActionMenuContentProps) => ( + +); + +export type PromptInputActionMenuItemProps = ComponentProps; +export const PromptInputActionMenuItem = ({ + className, + ...props +}: PromptInputActionMenuItemProps) => ( + +); + +// Note: Actions that perform side-effects (like opening a file dialog) +// are provided in opt-in modules (e.g., prompt-input-attachments). + +export type PromptInputSubmitProps = ComponentProps & { + status?: ChatStatus; + onStop?: () => void; +}; + +export const PromptInputSubmit = ({ + className, + variant = "default", + size = "icon-sm", + status, + onStop, + onClick, + children, + ...props +}: PromptInputSubmitProps) => { + const isGenerating = status === "submitted" || status === "streaming"; + + let Icon = ; + + if (status === "submitted") { + Icon = ; + } else if (status === "streaming") { + Icon = ; + } else if (status === "error") { + Icon = ; + } + + const handleClick = useCallback( + (e: React.MouseEvent) => { + if (isGenerating && onStop) { + e.preventDefault(); + onStop(); + return; + } + onClick?.(e); + }, + [isGenerating, onStop, onClick], + ); + + return ( + + {children ?? Icon} + + ); +}; + +export type PromptInputSelectProps = ComponentProps; + +export const PromptInputSelect = (props: PromptInputSelectProps) => keys.setKey(e.target.value)} - className="h-8 text-xs" - placeholder={g.keyPlaceholder} - /> -
- - keys.setSaved(c === "indeterminate" ? false : c) - } - /> - -
-
-
- ); - })} - -
-
- - Tavily (Web Search) - {chat.tavilyKeys.isSaved && ( - - )} -
- chat.tavilyKeys.setKey(e.target.value)} - className="h-8 text-xs" - placeholder="tvly-..." - /> -
- - chat.tavilyKeys.setSaved(c === "indeterminate" ? false : c) - } - /> - -
-
-
- - +
@@ -896,19 +808,29 @@ export function AIChatPanel({ Describe what you want or ask to modify the tool

- - {PROMPT_PILLS.map((pill) => ( - - {pill.label} - - ))} - + {chat.recentSessions.length > 0 && ( +
+

+ Recent sessions +

+
+ {chat.recentSessions.map((session) => ( + + ))} +
+
+ )} ) : ( chat.messages.map((msg, i) => ( @@ -924,6 +846,9 @@ export function AIChatPanel({ setEditingValue={setEditingValue} onResend={chat.resendFromIndex} onRetry={() => chat.resendFromIndex(i - 1, chat.messages[i - 1]?.content ?? "")} + pendingApproval={chat.pendingApproval} + onConfirmPatch={chat.confirmPatch} + onRejectPatch={chat.rejectPatch} /> )) )} @@ -931,127 +856,126 @@ export function AIChatPanel({ -
-
-