Unified · Split
| @@ -17,6 +17,7 @@ · 16 unmodified lines | ||
| 17 | 17 | "@effect/platform-node": "catalog:", |
| 18 | 18 | "@executor-v3/codemode-core": "workspace:*", |
| 19 | 19 | "@executor-v3/control-plane": "workspace:*", |
| 20 | + "@executor-v3/server": "workspace:*", | |
| 20 | 21 | "@executor-v3/runtime-local-inproc": "workspace:*", |
| 21 | 22 | "effect": "catalog:" |
| 22 | 23 | }, |
| @@ -32,12 +32,12 @@ import { · 31 unmodified lines | ||
| 32 | 32 | DEFAULT_SERVER_PORT, |
| 33 | 33 | SERVER_POLL_INTERVAL_MS, |
| 34 | 34 | SERVER_START_TIMEOUT_MS, |
| 35 | −} from "../server/config"; | |
| 35 | + runLocalExecutorServer, | |
| 36 | +} from "@executor-v3/server"; | |
| 36 | 37 | import { |
| 37 | 38 | seedDemoMcpSourceInWorkspace, |
| 38 | 39 | seedGithubOpenApiSourceInWorkspace, |
| 39 | 40 | } from "./dev"; |
| 40 | −import { runLocalExecutorServer } from "../server"; | |
| 41 | 41 | |
| 42 | 42 | const toError = (cause: unknown): Error => |
| 43 | 43 | cause instanceof Error ? cause : new Error(String(cause)); |
| @@ -1,138 +0,0 @@ | ||
| 1 | −import { mkdir } from "node:fs/promises"; | |
| 2 | −import { createServer } from "node:http"; | |
| 3 | −import { dirname } from "node:path"; | |
| 4 | − | |
| 5 | −import { HttpApiBuilder, HttpServer } from "@effect/platform"; | |
| 6 | −import { NodeHttpServer } from "@effect/platform-node"; | |
| 7 | −import { | |
| 8 | − createControlPlaneApiLayer, | |
| 9 | − createSqlControlPlaneRuntime, | |
| 10 | − type ResolveExecutionEnvironment, | |
| 11 | − type ResolveSecretMaterial, | |
| 12 | − type SqlControlPlaneRuntime, | |
| 13 | −} from "@executor-v3/control-plane"; | |
| 14 | −import * as Context from "effect/Context"; | |
| 15 | −import * as Effect from "effect/Effect"; | |
| 16 | −import * as Layer from "effect/Layer"; | |
| 17 | −import type * as Scope from "effect/Scope"; | |
| 18 | − | |
| 19 | −import { | |
| 20 | − DEFAULT_LOCAL_DATA_DIR, | |
| 21 | − DEFAULT_SERVER_HOST, | |
| 22 | − DEFAULT_SERVER_PORT, | |
| 23 | −} from "./config"; | |
| 24 | − | |
| 25 | −type LocalExecutorServer = { | |
| 26 | − readonly runtime: SqlControlPlaneRuntime; | |
| 27 | − readonly port: number; | |
| 28 | − readonly host: string; | |
| 29 | − readonly baseUrl: string; | |
| 30 | −}; | |
| 31 | − | |
| 32 | −type StartLocalExecutorServerOptions = { | |
| 33 | − readonly port?: number; | |
| 34 | − readonly host?: string; | |
| 35 | − readonly localDataDir?: string; | |
| 36 | − readonly executionResolver?: ResolveExecutionEnvironment; | |
| 37 | − readonly resolveSecretMaterial?: ResolveSecretMaterial; | |
| 38 | −}; | |
| 39 | − | |
| 40 | −const disposeRuntime = (runtime: SqlControlPlaneRuntime) => | |
| 41 | − Effect.promise(() => runtime.close()).pipe(Effect.orDie); | |
| 42 | − | |
| 43 | −const createControlPlaneServerLayer = (input: { | |
| 44 | − runtime: SqlControlPlaneRuntime; | |
| 45 | − host: string; | |
| 46 | − port: number; | |
| 47 | −}) => { | |
| 48 | − const apiLayer = createControlPlaneApiLayer(input.runtime.runtimeLayer); | |
| 49 | − | |
| 50 | − return HttpApiBuilder.serve().pipe( | |
| 51 | − Layer.provide(apiLayer), | |
| 52 | − Layer.provideMerge( | |
| 53 | − NodeHttpServer.layer(createServer, { | |
| 54 | − host: input.host, | |
| 55 | − port: input.port, | |
| 56 | − }), | |
| 57 | − ), | |
| 58 | − ); | |
| 59 | −}; | |
| 60 | − | |
| 61 | −export const createLocalExecutorServer = ( | |
| 62 | − options: StartLocalExecutorServerOptions = {}, | |
| 63 | −): Effect.Effect<LocalExecutorServer, Error, Scope.Scope> => | |
| 64 | − Effect.gen(function* () { | |
| 65 | − const host = options.host ?? DEFAULT_SERVER_HOST; | |
| 66 | − const port = options.port ?? DEFAULT_SERVER_PORT; | |
| 67 | − const localDataDir = options.localDataDir ?? DEFAULT_LOCAL_DATA_DIR; | |
| 68 | − | |
| 69 | − if (localDataDir !== ":memory:") { | |
| 70 | − yield* Effect.tryPromise({ | |
| 71 | − try: () => mkdir(dirname(localDataDir), { recursive: true }), | |
| 72 | − catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), | |
| 73 | − }); | |
| 74 | − } | |
| 75 | − | |
| 76 | − let baseUrlRef: string | undefined; | |
| 77 | − | |
| 78 | − const runtime = yield* Effect.acquireRelease( | |
| 79 | − createSqlControlPlaneRuntime({ | |
| 80 | − localDataDir, | |
| 81 | − executionResolver: options.executionResolver, | |
| 82 | − resolveSecretMaterial: options.resolveSecretMaterial, | |
| 83 | − getLocalServerBaseUrl: () => baseUrlRef, | |
| 84 | − }).pipe( | |
| 85 | − Effect.mapError((cause) => | |
| 86 | − cause instanceof Error ? cause : new Error(String(cause)), | |
| 87 | − ), | |
| 88 | − ), | |
| 89 | − disposeRuntime, | |
| 90 | − ); | |
| 91 | − | |
| 92 | − const serverContext = yield* Layer.build( | |
| 93 | − createControlPlaneServerLayer({ | |
| 94 | − runtime, | |
| 95 | − host, | |
| 96 | − port, | |
| 97 | − }), | |
| 98 | − ).pipe( | |
| 99 | − Effect.mapError((cause) => | |
| 100 | − cause instanceof Error ? cause : new Error(String(cause)), | |
| 101 | − ), | |
| 102 | − ); | |
| 103 | − | |
| 104 | − const server = Context.get(serverContext, HttpServer.HttpServer); | |
| 105 | − if (server.address._tag !== "TcpAddress") { | |
| 106 | − return yield* Effect.fail(new Error("Local executor server did not bind to a TCP address")); | |
| 107 | − } | |
| 108 | − | |
| 109 | − baseUrlRef = `http://${server.address.hostname}:${server.address.port}`; | |
| 110 | − | |
| 111 | − return { | |
| 112 | − runtime, | |
| 113 | − host: server.address.hostname, | |
| 114 | − port: server.address.port, | |
| 115 | − baseUrl: baseUrlRef, | |
| 116 | − } satisfies LocalExecutorServer; | |
| 117 | − }); | |
| 118 | − | |
| 119 | −export const runLocalExecutorServer = ( | |
| 120 | − options: StartLocalExecutorServerOptions = {}, | |
| 121 | −): Effect.Effect<void, Error, never> => | |
| 122 | − Effect.scoped( | |
| 123 | − Effect.gen(function* () { | |
| 124 | − const server = yield* createLocalExecutorServer(options); | |
| 125 | − console.error(`executor server listening on ${server.baseUrl}`); | |
| 126 | − | |
| 127 | − yield* Effect.async<void, never>((resume) => { | |
| 128 | − const shutdown = () => resume(Effect.void); | |
| 129 | − process.once("SIGINT", shutdown); | |
| 130 | − process.once("SIGTERM", shutdown); | |
| 131 | − | |
| 132 | − return Effect.sync(() => { | |
| 133 | − process.off("SIGINT", shutdown); | |
| 134 | − process.off("SIGTERM", shutdown); | |
| 135 | − }); | |
| 136 | − }); | |
| 137 | − }), | |
| 138 | − ); | |
| @@ -31,7 +31,7 @@ import { · 30 unmodified lines | ||
| 31 | 31 | seedDemoMcpSourceInWorkspace, |
| 32 | 32 | seedGithubOpenApiSourceInWorkspace, |
| 33 | 33 | } from "../cli/dev"; |
| 34 | −import { createLocalExecutorServer } from "."; | |
| 34 | +import { createLocalExecutorServer } from "@executor-v3/server"; | |
| 35 | 35 | |
| 36 | 36 | const executionResolver: ResolveExecutionEnvironment = () => |
| 37 | 37 | Effect.succeed({ |
| @@ -0,0 +1,36 @@ | ||
| 1 | +{ | |
| 2 | + "name": "@executor-v3/web", | |
| 3 | + "private": true, | |
| 4 | + "type": "module", | |
| 5 | + "scripts": { | |
| 6 | + "dev": "bunx --bun vite dev", | |
| 7 | + "build": "bunx vite build --config vite.config.ts", | |
| 8 | + "typecheck": "bunx tsc --noEmit -p tsconfig.json", | |
| 9 | + "start": "NODE_ENV=production bun src/server.ts" | |
| 10 | + }, | |
| 11 | + "dependencies": { | |
| 12 | + "@executor-v3/react": "workspace:*", | |
| 13 | + "@executor-v3/server": "workspace:*", | |
| 14 | + "@tanstack/react-router": "^1.132.7", | |
| 15 | + "class-variance-authority": "^0.7.1", | |
| 16 | + "clsx": "^2.1.1", | |
| 17 | + "effect": "catalog:", | |
| 18 | + "react": "^19.2.0", | |
| 19 | + "react-dom": "^19.2.0", | |
| 20 | + "react-grab": "^0.1.26", | |
| 21 | + "scheduler": "^0.27.0", | |
| 22 | + "shiki": "^4.0.1", | |
| 23 | + "tailwind-merge": "^3.5.0" | |
| 24 | + }, | |
| 25 | + "devDependencies": { | |
| 26 | + "@hono/vite-dev-server": "^0.25.0", | |
| 27 | + "@tailwindcss/vite": "^4.2.1", | |
| 28 | + "@types/react": "^19.2.10", | |
| 29 | + "@types/react-dom": "^19.2.3", | |
| 30 | + "@vitejs/plugin-react": "^5.1.4", | |
| 31 | + "bun-types": "catalog:", | |
| 32 | + "tailwindcss": "^4.2.1", | |
| 33 | + "tw-animate-css": "^1.4.0", | |
| 34 | + "vite": "^7.3.1" | |
| 35 | + } | |
| 36 | +} | |
| @@ -0,0 +1,96 @@ | ||
| 1 | +import { useEffect, useState, useRef, useCallback } from "react"; | |
| 2 | +import { codeToHtml, type BundledLanguage } from "shiki"; | |
| 3 | +import { cn } from "../lib/utils"; | |
| 4 | +import { IconCopy, IconCheck } from "./icons"; | |
| 5 | + | |
| 6 | +// Lazily loaded, cached across renders | |
| 7 | +const highlightCache = new Map<string, string>(); | |
| 8 | + | |
| 9 | +function cacheKey(code: string, lang: string) { | |
| 10 | + return `${lang}::${code.length}::${code.slice(0, 64)}`; | |
| 11 | +} | |
| 12 | + | |
| 13 | +function detectLanguage(code: string, hint?: string): BundledLanguage { | |
| 14 | + if (hint) return hint as BundledLanguage; | |
| 15 | + const trimmed = code.trimStart(); | |
| 16 | + if (trimmed.startsWith("{") || trimmed.startsWith("[")) return "json"; | |
| 17 | + if (trimmed.startsWith("<")) return "xml"; | |
| 18 | + if (trimmed.startsWith("---")) return "yaml"; | |
| 19 | + return "json"; | |
| 20 | +} | |
| 21 | + | |
| 22 | +export function CodeBlock(props: { | |
| 23 | + code: string; | |
| 24 | + lang?: string; | |
| 25 | + className?: string; | |
| 26 | +}) { | |
| 27 | + const { code, lang: langHint, className } = props; | |
| 28 | + const [html, setHtml] = useState<string | null>(null); | |
| 29 | + const [copied, setCopied] = useState(false); | |
| 30 | + const mountedRef = useRef(true); | |
| 31 | + | |
| 32 | + const language = detectLanguage(code, langHint); | |
| 33 | + const key = cacheKey(code, language); | |
| 34 | + | |
| 35 | + useEffect(() => { | |
| 36 | + mountedRef.current = true; | |
| 37 | + return () => { | |
| 38 | + mountedRef.current = false; | |
| 39 | + }; | |
| 40 | + }, []); | |
| 41 | + | |
| 42 | + useEffect(() => { | |
| 43 | + const cached = highlightCache.get(key); | |
| 44 | + if (cached) { | |
| 45 | + setHtml(cached); | |
| 46 | + return; | |
| 47 | + } | |
| 48 | + | |
| 49 | + let cancelled = false; | |
| 50 | + codeToHtml(code, { | |
| 51 | + lang: language, | |
| 52 | + theme: "vitesse-dark", | |
| 53 | + }).then((result) => { | |
| 54 | + if (cancelled) return; | |
| 55 | + highlightCache.set(key, result); | |
| 56 | + if (mountedRef.current) setHtml(result); | |
| 57 | + }); | |
| 58 | + | |
| 59 | + return () => { | |
| 60 | + cancelled = true; | |
| 61 | + }; | |
| 62 | + }, [code, key, language]); | |
| 63 | + | |
| 64 | + const handleCopy = useCallback(() => { | |
| 65 | + void navigator.clipboard.writeText(code).then(() => { | |
| 66 | + setCopied(true); | |
| 67 | + setTimeout(() => setCopied(false), 1500); | |
| 68 | + }); | |
| 69 | + }, [code]); | |
| 70 | + | |
| 71 | + return ( | |
| 72 | + <div className={cn("group relative overflow-auto", className)}> | |
| 73 | + {/* Copy button */} | |
| 74 | + <button | |
| 75 | + type="button" | |
| 76 | + onClick={handleCopy} | |
| 77 | + className="absolute right-2 top-2 z-10 rounded-md border border-border bg-card/90 p-1.5 text-muted-foreground/40 opacity-0 backdrop-blur-sm transition-all hover:text-foreground group-hover:opacity-100" | |
| 78 | + title="Copy to clipboard" | |
| 79 | + > | |
| 80 | + {copied ? <IconCheck className="size-3" /> : <IconCopy className="size-3" />} | |
| 81 | + </button> | |
| 82 | + | |
| 83 | + {html ? ( | |
| 84 | + <div | |
| 85 | + className="shiki-container text-[12px] leading-relaxed [&_pre]:!bg-transparent [&_pre]:p-3 [&_code]:font-mono" | |
| 86 | + dangerouslySetInnerHTML={{ __html: html }} | |
| 87 | + /> | |
| 88 | + ) : ( | |
| 89 | + // Fallback while shiki loads | |
| 90 | + <pre className="p-3 font-mono text-[12px] leading-relaxed text-foreground/60"> | |
| 91 | + {code} | |
| 92 | + </pre> | |
| 93 | + )} | |
| 94 | + </div> | |
| 95 | + ); | |
| 96 | +} | |
| @@ -0,0 +1,44 @@ | ||
| 1 | +import { useMemo } from "react"; | |
| 2 | +import { cn } from "../lib/utils"; | |
| 3 | +import { CodeBlock } from "./code-block"; | |
| 4 | + | |
| 5 | +function prettyJson(raw: string): string { | |
| 6 | + try { | |
| 7 | + return JSON.stringify(JSON.parse(raw), null, 2); | |
| 8 | + } catch { | |
| 9 | + return raw; | |
| 10 | + } | |
| 11 | +} | |
| 12 | + | |
| 13 | +export function DocumentPanel(props: { | |
| 14 | + title: string; | |
| 15 | + body: string | null; | |
| 16 | + lang?: string; | |
| 17 | + empty: string; | |
| 18 | + compact?: boolean; | |
| 19 | +}) { | |
| 20 | + const formatted = useMemo( | |
| 21 | + () => (props.body ? prettyJson(props.body) : null), | |
| 22 | + [props.body], | |
| 23 | + ); | |
| 24 | + | |
| 25 | + return ( | |
| 26 | + <section | |
| 27 | + className={cn( | |
| 28 | + "overflow-hidden rounded-lg border border-border bg-card/60", | |
| 29 | + props.compact && "min-h-48", | |
| 30 | + )} | |
| 31 | + > | |
| 32 | + <div className="border-b border-border px-3 py-2 text-[11px] font-medium uppercase tracking-wider text-muted-foreground/70"> | |
| 33 | + {props.title} | |
| 34 | + </div> | |
| 35 | + {formatted ? ( | |
| 36 | + <CodeBlock code={formatted} lang={props.lang} className="max-h-[32rem]" /> | |
| 37 | + ) : ( | |
| 38 | + <div className="flex items-center justify-center p-6 text-[13px] text-muted-foreground/40"> | |
| 39 | + {props.empty} | |
| 40 | + </div> | |
| 41 | + )} | |
| 42 | + </section> | |
| 43 | + ); | |
| 44 | +} | |
| @@ -0,0 +1,109 @@ | ||
| 1 | +import type { SVGProps } from "react"; | |
| 2 | +import { cn } from "../lib/utils"; | |
| 3 | + | |
| 4 | +type IconProps = SVGProps<SVGSVGElement> & { className?: string }; | |
| 5 | + | |
| 6 | +export function IconSources({ className, ...props }: IconProps) { | |
| 7 | + return ( | |
| 8 | + <svg viewBox="0 0 16 16" fill="none" className={cn("size-4", className)} {...props}> | |
| 9 | + <rect x="2" y="2" width="5" height="5" rx="1" stroke="currentColor" strokeWidth="1.3" /> | |
| 10 | + <rect x="9" y="2" width="5" height="5" rx="1" stroke="currentColor" strokeWidth="1.3" /> | |
| 11 | + <rect x="2" y="9" width="5" height="5" rx="1" stroke="currentColor" strokeWidth="1.3" /> | |
| 12 | + <rect x="9" y="9" width="5" height="5" rx="1" stroke="currentColor" strokeWidth="1.3" /> | |
| 13 | + </svg> | |
| 14 | + ); | |
| 15 | +} | |
| 16 | + | |
| 17 | +export function IconTool({ className, ...props }: IconProps) { | |
| 18 | + return ( | |
| 19 | + <svg viewBox="0 0 16 16" fill="none" className={cn("size-4", className)} {...props}> | |
| 20 | + <path d="M4 8h8M8 4v8" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" /> | |
| 21 | + </svg> | |
| 22 | + ); | |
| 23 | +} | |
| 24 | + | |
| 25 | +export function IconSearch({ className, ...props }: IconProps) { | |
| 26 | + return ( | |
| 27 | + <svg viewBox="0 0 16 16" fill="none" className={cn("size-4", className)} {...props}> | |
| 28 | + <circle cx="7" cy="7" r="4.5" stroke="currentColor" strokeWidth="1.3" /> | |
| 29 | + <path d="M10.5 10.5L14 14" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" /> | |
| 30 | + </svg> | |
| 31 | + ); | |
| 32 | +} | |
| 33 | + | |
| 34 | +export function IconChevron({ className, ...props }: IconProps) { | |
| 35 | + return ( | |
| 36 | + <svg viewBox="0 0 16 16" fill="none" className={cn("size-3", className)} {...props}> | |
| 37 | + <path d="M6 4l4 4-4 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> | |
| 38 | + </svg> | |
| 39 | + ); | |
| 40 | +} | |
| 41 | + | |
| 42 | +export function IconFolder({ className, ...props }: IconProps) { | |
| 43 | + return ( | |
| 44 | + <svg viewBox="0 0 16 16" fill="none" className={cn("size-3", className)} {...props}> | |
| 45 | + <path d="M2 4h5l2 2h5v7H2V4z" stroke="currentColor" strokeWidth="1.2" strokeLinejoin="round" /> | |
| 46 | + </svg> | |
| 47 | + ); | |
| 48 | +} | |
| 49 | + | |
| 50 | +export function IconDocument({ className, ...props }: IconProps) { | |
| 51 | + return ( | |
| 52 | + <svg viewBox="0 0 16 16" fill="none" className={cn("size-4", className)} {...props}> | |
| 53 | + <path d="M4 2h5l3 3v9H4V2z" stroke="currentColor" strokeWidth="1.3" strokeLinejoin="round" /> | |
| 54 | + <path d="M6 8h4M6 10.5h3" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" /> | |
| 55 | + </svg> | |
| 56 | + ); | |
| 57 | +} | |
| 58 | + | |
| 59 | +export function IconDiscover({ className, ...props }: IconProps) { | |
| 60 | + return ( | |
| 61 | + <svg viewBox="0 0 16 16" fill="none" className={cn("size-4", className)} {...props}> | |
| 62 | + <circle cx="8" cy="8" r="6" stroke="currentColor" strokeWidth="1.3" /> | |
| 63 | + <path d="M6.5 5.5l4.5 2-2 4.5-4.5-2z" stroke="currentColor" strokeWidth="1.2" strokeLinejoin="round" /> | |
| 64 | + </svg> | |
| 65 | + ); | |
| 66 | +} | |
| 67 | + | |
| 68 | +export function IconCopy({ className, ...props }: IconProps) { | |
| 69 | + return ( | |
| 70 | + <svg viewBox="0 0 16 16" fill="none" className={cn("size-3.5", className)} {...props}> | |
| 71 | + <rect x="5" y="5" width="8" height="8" rx="1" stroke="currentColor" strokeWidth="1.2" /> | |
| 72 | + <path d="M11 3H4a1 1 0 00-1 1v7" stroke="currentColor" strokeWidth="1.2" /> | |
| 73 | + </svg> | |
| 74 | + ); | |
| 75 | +} | |
| 76 | + | |
| 77 | +export function IconCheck({ className, ...props }: IconProps) { | |
| 78 | + return ( | |
| 79 | + <svg viewBox="0 0 16 16" fill="none" className={cn("size-3.5", className)} {...props}> | |
| 80 | + <path d="M4 8.5l3 3 5-6" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" strokeLinejoin="round" /> | |
| 81 | + </svg> | |
| 82 | + ); | |
| 83 | +} | |
| 84 | + | |
| 85 | +export function IconClose({ className, ...props }: IconProps) { | |
| 86 | + return ( | |
| 87 | + <svg viewBox="0 0 16 16" fill="none" className={cn("size-3", className)} {...props}> | |
| 88 | + <path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" /> | |
| 89 | + </svg> | |
| 90 | + ); | |
| 91 | +} | |
| 92 | + | |
| 93 | +export function IconEmpty({ className, ...props }: IconProps) { | |
| 94 | + return ( | |
| 95 | + <svg viewBox="0 0 48 48" fill="none" className={cn("size-12", className)} {...props}> | |
| 96 | + <rect x="6" y="6" width="36" height="36" rx="4" stroke="currentColor" strokeWidth="1.5" /> | |
| 97 | + <path d="M16 18h16M16 24h12M16 30h8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" /> | |
| 98 | + </svg> | |
| 99 | + ); | |
| 100 | +} | |
| 101 | + | |
| 102 | +export function IconSpinner({ className, ...props }: IconProps) { | |
| 103 | + return ( | |
| 104 | + <svg viewBox="0 0 16 16" fill="none" className={cn("size-4 animate-spin", className)} {...props}> | |
| 105 | + <circle cx="8" cy="8" r="6" stroke="currentColor" strokeWidth="1.5" opacity="0.2" /> | |
| 106 | + <path d="M14 8a6 6 0 00-6-6" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" /> | |
| 107 | + </svg> | |
| 108 | + ); | |
| 109 | +} | |
| @@ -0,0 +1,50 @@ | ||
| 1 | +import type { ReactNode } from "react"; | |
| 2 | +import type { Loadable } from "@executor-v3/react"; | |
| 3 | +import { IconSpinner, IconEmpty } from "./icons"; | |
| 4 | + | |
| 5 | +export function LoadableBlock<T>(props: { | |
| 6 | + loadable: Loadable<T>; | |
| 7 | + loading?: string; | |
| 8 | + children: (data: T) => ReactNode; | |
| 9 | +}) { | |
| 10 | + if (props.loadable.status === "loading") { | |
| 11 | + return ( | |
| 12 | + <div className="flex h-full min-h-48 items-center justify-center"> | |
| 13 | + <div className="flex items-center gap-3 text-sm text-muted-foreground"> | |
| 14 | + <IconSpinner /> | |
| 15 | + <span>{props.loading ?? "Loading..."}</span> | |
| 16 | + </div> | |
| 17 | + </div> | |
| 18 | + ); | |
| 19 | + } | |
| 20 | + | |
| 21 | + if (props.loadable.status === "error") { | |
| 22 | + return ( | |
| 23 | + <div className="flex h-full min-h-48 items-center justify-center"> | |
| 24 | + <div className="rounded-lg border border-destructive/25 bg-destructive/5 px-4 py-3 text-sm text-destructive"> | |
| 25 | + {props.loadable.error.message} | |
| 26 | + </div> | |
| 27 | + </div> | |
| 28 | + ); | |
| 29 | + } | |
| 30 | + | |
| 31 | + return <>{props.children(props.loadable.data)}</>; | |
| 32 | +} | |
| 33 | + | |
| 34 | +export function EmptyState(props: { | |
| 35 | + title: string; | |
| 36 | + description?: string; | |
| 37 | + className?: string; | |
| 38 | +}) { | |
| 39 | + return ( | |
| 40 | + <div className={`flex h-full items-center justify-center ${props.className ?? ""}`}> | |
| 41 | + <div className="text-center"> | |
| 42 | + <IconEmpty className="mx-auto mb-3 text-muted-foreground/20" /> | |
| 43 | + <p className="text-[13px] text-muted-foreground/60">{props.title}</p> | |
| 44 | + {props.description && ( | |
| 45 | + <p className="mt-1 text-[11px] text-muted-foreground/40">{props.description}</p> | |
| 46 | + )} | |
| 47 | + </div> | |
| 48 | + </div> | |
| 49 | + ); | |
| 50 | +} | |
| @@ -0,0 +1,232 @@ | ||
| 1 | +import { useState, useMemo } from "react"; | |
| 2 | +import { Link, Outlet, useMatchRoute } from "@tanstack/react-router"; | |
| 3 | +import { useSources, type Source } from "@executor-v3/react"; | |
| 4 | +import { cn } from "../lib/utils"; | |
| 5 | +import { IconSources, IconChevron } from "./icons"; | |
| 6 | +import { LoadableBlock } from "./loadable"; | |
| 7 | + | |
| 8 | +// ── Kind labels + ordering ─────────────────────────────────────────────── | |
| 9 | + | |
| 10 | +const kindMeta: Record<string, { label: string; order: number }> = { | |
| 11 | + mcp: { label: "MCP", order: 0 }, | |
| 12 | + openapi: { label: "OpenAPI", order: 1 }, | |
| 13 | + graphql: { label: "GraphQL", order: 2 }, | |
| 14 | + internal: { label: "Internal", order: 3 }, | |
| 15 | +}; | |
| 16 | + | |
| 17 | +function groupByKind(sources: ReadonlyArray<Source>) { | |
| 18 | + const groups = new Map<string, Source[]>(); | |
| 19 | + for (const s of sources) { | |
| 20 | + const list = groups.get(s.kind) ?? []; | |
| 21 | + list.push(s); | |
| 22 | + groups.set(s.kind, list); | |
| 23 | + } | |
| 24 | + return [...groups.entries()].sort( | |
| 25 | + (a, b) => (kindMeta[a[0]]?.order ?? 99) - (kindMeta[b[0]]?.order ?? 99), | |
| 26 | + ); | |
| 27 | +} | |
| 28 | + | |
| 29 | +// ── Status dot color ───────────────────────────────────────────────────── | |
| 30 | + | |
| 31 | +const statusColor: Record<string, string> = { | |
| 32 | + connected: "bg-primary", | |
| 33 | + probing: "bg-amber-400", | |
| 34 | + draft: "bg-muted-foreground/30", | |
| 35 | + auth_required: "bg-amber-500", | |
| 36 | + error: "bg-destructive", | |
| 37 | +}; | |
| 38 | + | |
| 39 | +// ── AppShell ───────────────────────────────────────────────────────────── | |
| 40 | + | |
| 41 | +export function AppShell() { | |
| 42 | + const sources = useSources(); | |
| 43 | + const matchRoute = useMatchRoute(); | |
| 44 | + const isHome = matchRoute({ to: "/" }); | |
| 45 | + | |
| 46 | + return ( | |
| 47 | + <div className="flex h-screen overflow-hidden"> | |
| 48 | + {/* Sidebar */} | |
| 49 | + <aside className="flex w-52 shrink-0 flex-col border-r border-sidebar-border bg-sidebar lg:w-56"> | |
| 50 | + {/* Brand */} | |
| 51 | + <div className="flex h-12 shrink-0 items-center border-b border-sidebar-border px-4"> | |
| 52 | + <Link to="/" className="flex items-center gap-1.5"> | |
| 53 | + <span className="font-display text-base tracking-tight text-foreground"> | |
| 54 | + executor | |
| 55 | + </span> | |
| 56 | + <span className="rounded bg-primary/15 px-1.5 py-0.5 text-[10px] font-medium text-primary"> | |
| 57 | + v3 | |
| 58 | + </span> | |
| 59 | + </Link> | |
| 60 | + </div> | |
| 61 | + | |
| 62 | + {/* Main nav */} | |
| 63 | + <nav className="flex flex-1 flex-col p-2 overflow-y-auto"> | |
| 64 | + <NavItem to="/" label="Dashboard" active={!!isHome} /> | |
| 65 | + | |
| 66 | + {/* Sources tree */} | |
| 67 | + <div className="mt-5 mb-1 px-2.5 text-[10px] font-medium uppercase tracking-widest text-muted-foreground/50"> | |
| 68 | + Sources | |
| 69 | + </div> | |
| 70 | + <LoadableBlock loadable={sources} loading="Loading..."> | |
| 71 | + {(items) => | |
| 72 | + items.length === 0 ? ( | |
| 73 | + <div className="px-2.5 py-2 text-[11px] leading-relaxed text-muted-foreground/40"> | |
| 74 | + No sources yet | |
| 75 | + </div> | |
| 76 | + ) : ( | |
| 77 | + <SourceTree sources={items} matchRoute={matchRoute} /> | |
| 78 | + ) | |
| 79 | + } | |
| 80 | + </LoadableBlock> | |
| 81 | + </nav> | |
| 82 | + | |
| 83 | + {/* Footer */} | |
| 84 | + <div className="shrink-0 border-t border-sidebar-border px-4 py-2.5"> | |
| 85 | + <div className="text-[10px] text-muted-foreground/30"> | |
| 86 | + Local instance | |
| 87 | + </div> | |
| 88 | + </div> | |
| 89 | + </aside> | |
| 90 | + | |
| 91 | + {/* Main content */} | |
| 92 | + <main className="flex flex-1 flex-col min-w-0 overflow-hidden"> | |
| 93 | + <Outlet /> | |
| 94 | + </main> | |
| 95 | + </div> | |
| 96 | + ); | |
| 97 | +} | |
| 98 | + | |
| 99 | +// ── SourceTree ─────────────────────────────────────────────────────────── | |
| 100 | + | |
| 101 | +function SourceTree(props: { | |
| 102 | + sources: ReadonlyArray<Source>; | |
| 103 | + matchRoute: ReturnType<typeof useMatchRoute>; | |
| 104 | +}) { | |
| 105 | + const groups = useMemo(() => groupByKind(props.sources), [props.sources]); | |
| 106 | + | |
| 107 | + // If there's only one kind, skip the group headers | |
| 108 | + if (groups.length === 1) { | |
| 109 | + return ( | |
| 110 | + <div className="flex flex-col gap-px"> | |
| 111 | + {groups[0]![1].map((source) => ( | |
| 112 | + <SourceItem key={source.id} source={source} matchRoute={props.matchRoute} depth={0} /> | |
| 113 | + ))} | |
| 114 | + </div> | |
| 115 | + ); | |
| 116 | + } | |
| 117 | + | |
| 118 | + return ( | |
| 119 | + <div className="flex flex-col gap-0.5"> | |
| 120 | + {groups.map(([kind, sources]) => ( | |
| 121 | + <KindGroup key={kind} kind={kind} sources={sources} matchRoute={props.matchRoute} /> | |
| 122 | + ))} | |
| 123 | + </div> | |
| 124 | + ); | |
| 125 | +} | |
| 126 | + | |
| 127 | +// ── KindGroup (collapsible) ────────────────────────────────────────────── | |
| 128 | + | |
| 129 | +function KindGroup(props: { | |
| 130 | + kind: string; | |
| 131 | + sources: Source[]; | |
| 132 | + matchRoute: ReturnType<typeof useMatchRoute>; | |
| 133 | +}) { | |
| 134 | + // Auto-expand if any source in this group is active | |
| 135 | + const hasActive = props.sources.some((s) => | |
| 136 | + props.matchRoute({ to: "/sources/$sourceId", params: { sourceId: s.id }, fuzzy: true }), | |
| 137 | + ); | |
| 138 | + const [open, setOpen] = useState(true); | |
| 139 | + | |
| 140 | + return ( | |
| 141 | + <div> | |
| 142 | + {/* Group header */} | |
| 143 | + <button | |
| 144 | + type="button" | |
| 145 | + onClick={() => setOpen((o) => !o)} | |
| 146 | + className="group flex w-full items-center gap-1.5 rounded-md px-2 py-1 text-[11px] font-medium text-muted-foreground/60 transition-colors hover:text-muted-foreground" | |
| 147 | + > | |
| 148 | + <IconChevron | |
| 149 | + className={cn( | |
| 150 | + "size-2.5 shrink-0 text-muted-foreground/30 transition-transform duration-150", | |
| 151 | + open && "rotate-90", | |
| 152 | + )} | |
| 153 | + /> | |
| 154 | + <span className="uppercase tracking-wider">{kindMeta[props.kind]?.label ?? props.kind}</span> | |
| 155 | + <span className="ml-auto tabular-nums text-[10px] text-muted-foreground/30"> | |
| 156 | + {props.sources.length} | |
| 157 | + </span> | |
| 158 | + </button> | |
| 159 | + | |
| 160 | + {/* Children */} | |
| 161 | + {open && ( | |
| 162 | + <div className="flex flex-col gap-px mt-px"> | |
| 163 | + {props.sources.map((source) => ( | |
| 164 | + <SourceItem key={source.id} source={source} matchRoute={props.matchRoute} depth={1} /> | |
| 165 | + ))} | |
| 166 | + </div> | |
| 167 | + )} | |
| 168 | + </div> | |
| 169 | + ); | |
| 170 | +} | |
| 171 | + | |
| 172 | +// ── SourceItem ─────────────────────────────────────────────────────────── | |
| 173 | + | |
| 174 | +function SourceItem(props: { | |
| 175 | + source: Source; | |
| 176 | + matchRoute: ReturnType<typeof useMatchRoute>; | |
| 177 | + depth: number; | |
| 178 | +}) { | |
| 179 | + const { source, matchRoute, depth } = props; | |
| 180 | + const active = matchRoute({ | |
| 181 | + to: "/sources/$sourceId", | |
| 182 | + params: { sourceId: source.id }, | |
| 183 | + fuzzy: true, | |
| 184 | + }); | |
| 185 | + | |
| 186 | + return ( | |
| 187 | + <Link | |
| 188 | + to="/sources/$sourceId" | |
| 189 | + params={{ sourceId: source.id }} | |
| 190 | + search={{ tab: "model" }} | |
| 191 | + className={cn( | |
| 192 | + "group flex items-center gap-2 rounded-md py-1.5 text-[12px] transition-colors", | |
| 193 | + depth === 0 ? "px-2.5" : "pl-6 pr-2.5", | |
| 194 | + active | |
| 195 | + ? "bg-sidebar-active text-foreground font-medium" | |
| 196 | + : "text-sidebar-foreground hover:bg-sidebar-active/60 hover:text-foreground", | |
| 197 | + )} | |
| 198 | + > | |
| 199 | + {/* Tree connector line for nested items */} | |
| 200 | + {depth > 0 && ( | |
| 201 | + <span className="absolute left-[18px] h-full w-px bg-sidebar-border" aria-hidden /> | |
| 202 | + )} | |
| 203 | + | |
| 204 | + <IconSources className="size-3 shrink-0 text-muted-foreground/40" /> | |
| 205 | + <span className="flex-1 truncate">{source.name}</span> | |
| 206 | + | |
| 207 | + {/* Status dot + tooltip */} | |
| 208 | + <span | |
| 209 | + className={cn("size-1.5 shrink-0 rounded-full", statusColor[source.status] ?? "bg-muted-foreground/30")} | |
| 210 | + title={source.status} | |
| 211 | + /> | |
| 212 | + </Link> | |
| 213 | + ); | |
| 214 | +} | |
| 215 | + | |
| 216 | +// ── NavItem ────────────────────────────────────────────────────────────── | |
| 217 | + | |
| 218 | +function NavItem(props: { to: string; label: string; active: boolean }) { | |
| 219 | + return ( | |
| 220 | + <Link | |
| 221 | + to={props.to} | |
| 222 | + className={cn( | |
| 223 | + "flex items-center gap-2.5 rounded-md px-2.5 py-1.5 text-[13px] transition-colors", | |
| 224 | + props.active | |
| 225 | + ? "bg-sidebar-active text-foreground font-medium" | |
| 226 | + : "text-sidebar-foreground hover:bg-sidebar-active/60 hover:text-foreground", | |
| 227 | + )} | |
| 228 | + > | |
| 229 | + {props.label} | |
| 230 | + </Link> | |
| 231 | + ); | |
| 232 | +} | |
| @@ -0,0 +1,43 @@ | ||
| 1 | +import type { HTMLAttributes } from "react"; | |
| 2 | +import { cva, type VariantProps } from "class-variance-authority"; | |
| 3 | +import { cn } from "../../lib/utils"; | |
| 4 | + | |
| 5 | +const badgeVariants = cva( | |
| 6 | + "inline-flex items-center rounded-full border px-2 py-0.5 text-[10px] font-medium tracking-wide uppercase", | |
| 7 | + { | |
| 8 | + variants: { | |
| 9 | + variant: { | |
| 10 | + default: "border-transparent bg-primary/15 text-primary", | |
| 11 | + secondary: "border-transparent bg-secondary text-secondary-foreground", | |
| 12 | + outline: "border-border text-muted-foreground", | |
| 13 | + muted: "border-transparent bg-muted text-muted-foreground", | |
| 14 | + get: "border-[var(--method-get)]/20 bg-[var(--method-get)]/10 text-[var(--method-get)]", | |
| 15 | + post: "border-[var(--method-post)]/20 bg-[var(--method-post)]/10 text-[var(--method-post)]", | |
| 16 | + put: "border-[var(--method-put)]/20 bg-[var(--method-put)]/10 text-[var(--method-put)]", | |
| 17 | + delete: "border-[var(--method-delete)]/20 bg-[var(--method-delete)]/10 text-[var(--method-delete)]", | |
| 18 | + destructive: "border-destructive/20 bg-destructive/10 text-destructive", | |
| 19 | + }, | |
| 20 | + }, | |
| 21 | + defaultVariants: { | |
| 22 | + variant: "default", | |
| 23 | + }, | |
| 24 | + }, | |
| 25 | +); | |
| 26 | + | |
| 27 | +export type BadgeProps = HTMLAttributes<HTMLSpanElement> & VariantProps<typeof badgeVariants>; | |
| 28 | + | |
| 29 | +export function Badge({ className, variant, ...props }: BadgeProps) { | |
| 30 | + return <span className={cn(badgeVariants({ variant }), className)} {...props} />; | |
| 31 | +} | |
| 32 | + | |
| 33 | +export function MethodBadge({ method }: { method: string }) { | |
| 34 | + const variant = ({ | |
| 35 | + GET: "get", | |
| 36 | + POST: "post", | |
| 37 | + PUT: "put", | |
| 38 | + PATCH: "put", | |
| 39 | + DELETE: "delete", | |
| 40 | + } as const)[method.toUpperCase()] ?? "outline"; | |
| 41 | + | |
| 42 | + return <Badge variant={variant}>{method}</Badge>; | |
| 43 | +} | |
| @@ -0,0 +1,34 @@ | ||
| 1 | +import type { ButtonHTMLAttributes } from "react"; | |
| 2 | +import { cva, type VariantProps } from "class-variance-authority"; | |
| 3 | +import { cn } from "../../lib/utils"; | |
| 4 | + | |
| 5 | +const buttonVariants = cva( | |
| 6 | + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-offset-1 focus-visible:ring-offset-background cursor-pointer", | |
| 7 | + { | |
| 8 | + variants: { | |
| 9 | + variant: { | |
| 10 | + default: "bg-primary text-primary-foreground hover:bg-primary/90", | |
| 11 | + secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", | |
| 12 | + outline: "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground", | |
| 13 | + ghost: "hover:bg-accent hover:text-accent-foreground", | |
| 14 | + destructive: "bg-destructive text-white hover:bg-destructive/90", | |
| 15 | + }, | |
| 16 | + size: { | |
| 17 | + default: "h-9 px-4 py-2", | |
| 18 | + sm: "h-7 rounded-md px-2.5 text-xs", | |
| 19 | + lg: "h-10 rounded-md px-6", | |
| 20 | + icon: "h-8 w-8", | |
| 21 | + }, | |
| 22 | + }, | |
| 23 | + defaultVariants: { | |
| 24 | + variant: "default", | |
| 25 | + size: "default", | |
| 26 | + }, | |
| 27 | + }, | |
| 28 | +); | |
| 29 | + | |
| 30 | +type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & VariantProps<typeof buttonVariants>; | |
| 31 | + | |
| 32 | +export function Button({ className, variant, size, ...props }: ButtonProps) { | |
| 33 | + return <button className={cn(buttonVariants({ variant, size }), className)} {...props} />; | |
| 34 | +} | |
| @@ -0,0 +1,29 @@ | ||
| 1 | +/** | |
| 2 | + * Dev entry point for @hono/vite-dev-server. | |
| 3 | + * | |
| 4 | + * Exports `{ fetch }` so Vite can forward API requests to the executor | |
| 5 | + * control-plane handler. Everything else (frontend assets, HMR) is | |
| 6 | + * handled by Vite itself. | |
| 7 | + */ | |
| 8 | +import * as Effect from "effect/Effect"; | |
| 9 | +import * as Scope from "effect/Scope"; | |
| 10 | +import { createLocalExecutorRequestHandler } from "@executor-v3/server"; | |
| 11 | + | |
| 12 | +// Create a long-lived scope that stays open for the lifetime of the process. | |
| 13 | +const handlerPromise = Effect.runPromise( | |
| 14 | + Effect.gen(function* () { | |
| 15 | + const scope = yield* Scope.make(); | |
| 16 | + const handler = yield* createLocalExecutorRequestHandler().pipe( | |
| 17 | + Effect.provideService(Scope.Scope, scope), | |
| 18 | + ); | |
| 19 | + handler.setBaseUrl("http://127.0.0.1:8788"); | |
| 20 | + return handler; | |
| 21 | + }), | |
| 22 | +); | |
| 23 | + | |
| 24 | +export default { | |
| 25 | + async fetch(request: Request) { | |
| 26 | + const handler = await handlerPromise; | |
| 27 | + return handler.handleApiRequest(request); | |
| 28 | + }, | |
| 29 | +}; | |
| @@ -0,0 +1,10 @@ | ||
| 1 | +import { createRoot } from "react-dom/client"; | |
| 2 | +import { App } from "./main"; | |
| 3 | + | |
| 4 | +const rootElement = document.getElementById("root"); | |
| 5 | + | |
| 6 | +if (!rootElement) { | |
| 7 | + throw new Error("Missing #root element"); | |
| 8 | +} | |
| 9 | + | |
| 10 | +createRoot(rootElement).render(<App />); | |
| @@ -0,0 +1,107 @@ | ||
| 1 | +@import "tailwindcss"; | |
| 2 | +@import "tw-animate-css"; | |
| 3 | + | |
| 4 | +@custom-variant dark (&:is(.dark *)); | |
| 5 | + | |
| 6 | +@theme inline { | |
| 7 | + --font-sans: "DM Sans", ui-sans-serif, system-ui, sans-serif; | |
| 8 | + --font-mono: "JetBrains Mono", "IBM Plex Mono", ui-monospace, monospace; | |
| 9 | + --font-display: "Instrument Serif", Georgia, serif; | |
| 10 | + | |
| 11 | + --color-background: var(--background); | |
| 12 | + --color-foreground: var(--foreground); | |
| 13 | + --color-card: var(--card); | |
| 14 | + --color-card-foreground: var(--card-foreground); | |
| 15 | + --color-popover: var(--popover); | |
| 16 | + --color-popover-foreground: var(--popover-foreground); | |
| 17 | + --color-primary: var(--primary); | |
| 18 | + --color-primary-foreground: var(--primary-foreground); | |
| 19 | + --color-secondary: var(--secondary); | |
| 20 | + --color-secondary-foreground: var(--secondary-foreground); | |
| 21 | + --color-muted: var(--muted); | |
| 22 | + --color-muted-foreground: var(--muted-foreground); | |
| 23 | + --color-accent: var(--accent); | |
| 24 | + --color-accent-foreground: var(--accent-foreground); | |
| 25 | + --color-destructive: var(--destructive); | |
| 26 | + --color-border: var(--border); | |
| 27 | + --color-input: var(--input); | |
| 28 | + --color-ring: var(--ring); | |
| 29 | + | |
| 30 | + --color-sidebar: var(--sidebar); | |
| 31 | + --color-sidebar-foreground: var(--sidebar-foreground); | |
| 32 | + --color-sidebar-border: var(--sidebar-border); | |
| 33 | + --color-sidebar-active: var(--sidebar-active); | |
| 34 | +} | |
| 35 | + | |
| 36 | +/* | |
| 37 | + * Dark-first. An opinionated dark palette: | |
| 38 | + * charcoal base, warm stone accents, teal primary, amber warning. | |
| 39 | + * No light mode — this is a dev tool. | |
| 40 | + */ | |
| 41 | +:root { | |
| 42 | + --background: oklch(0.115 0.005 260); | |
| 43 | + --foreground: oklch(0.90 0.008 250); | |
| 44 | + --card: oklch(0.145 0.005 260); | |
| 45 | + --card-foreground: oklch(0.90 0.008 250); | |
| 46 | + --popover: oklch(0.145 0.005 260); | |
| 47 | + --popover-foreground: oklch(0.90 0.008 250); | |
| 48 | + --primary: oklch(0.72 0.15 175); | |
| 49 | + --primary-foreground: oklch(0.12 0.01 175); | |
| 50 | + --secondary: oklch(0.195 0.007 260); | |
| 51 | + --secondary-foreground: oklch(0.78 0.008 250); | |
| 52 | + --muted: oklch(0.175 0.005 260); | |
| 53 | + --muted-foreground: oklch(0.56 0.01 250); | |
| 54 | + --accent: oklch(0.195 0.008 260); | |
| 55 | + --accent-foreground: oklch(0.90 0.008 250); | |
| 56 | + --destructive: oklch(0.62 0.22 25); | |
| 57 | + --border: oklch(0.235 0.007 260); | |
| 58 | + --input: oklch(0.195 0.007 260); | |
| 59 | + --ring: oklch(0.72 0.15 175); | |
| 60 | + | |
| 61 | + --sidebar: oklch(0.105 0.005 260); | |
| 62 | + --sidebar-foreground: oklch(0.72 0.008 250); | |
| 63 | + --sidebar-border: oklch(0.20 0.006 260); | |
| 64 | + --sidebar-active: oklch(0.18 0.008 260); | |
| 65 | + | |
| 66 | + /* Method colors */ | |
| 67 | + --method-get: oklch(0.75 0.16 155); | |
| 68 | + --method-post: oklch(0.72 0.14 230); | |
| 69 | + --method-put: oklch(0.78 0.14 85); | |
| 70 | + --method-delete: oklch(0.70 0.18 25); | |
| 71 | +} | |
| 72 | + | |
| 73 | +@layer base { | |
| 74 | + * { | |
| 75 | + @apply border-border; | |
| 76 | + } | |
| 77 | + | |
| 78 | + html, | |
| 79 | + body, | |
| 80 | + #root { | |
| 81 | + min-height: 100%; | |
| 82 | + height: 100%; | |
| 83 | + } | |
| 84 | + | |
| 85 | + body { | |
| 86 | + @apply bg-background font-sans text-foreground antialiased; | |
| 87 | + } | |
| 88 | + | |
| 89 | + /* Scrollbar styling */ | |
| 90 | + ::-webkit-scrollbar { | |
| 91 | + width: 6px; | |
| 92 | + height: 6px; | |
| 93 | + } | |
| 94 | + | |
| 95 | + ::-webkit-scrollbar-track { | |
| 96 | + background: transparent; | |
| 97 | + } | |
| 98 | + | |
| 99 | + ::-webkit-scrollbar-thumb { | |
| 100 | + background: oklch(0.30 0.005 260); | |
| 101 | + border-radius: 3px; | |
| 102 | + } | |
| 103 | + | |
| 104 | + ::-webkit-scrollbar-thumb:hover { | |
| 105 | + background: oklch(0.38 0.005 260); | |
| 106 | + } | |
| 107 | +} | |
| @@ -0,0 +1,29 @@ | ||
| 1 | +<!doctype html> | |
| 2 | +<html lang="en"> | |
| 3 | + <head> | |
| 4 | + <meta charset="UTF-8" /> | |
| 5 | + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| 6 | + <title>Executor</title> | |
| 7 | + <meta | |
| 8 | + name="description" | |
| 9 | + content="Executor v3 — manage sources, inspect tools, and monitor executions." | |
| 10 | + /> | |
| 11 | + <link rel="preconnect" href="https://fonts.googleapis.com" /> | |
| 12 | + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> | |
| 13 | + <script type="module"> | |
| 14 | + // first npm i react-grab | |
| 15 | + // then in head: | |
| 16 | + if (import.meta.env.DEV) { | |
| 17 | + import("react-grab"); | |
| 18 | + } | |
| 19 | + </script> | |
| 20 | + <link | |
| 21 | + href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400&family=Instrument+Serif&family=JetBrains+Mono:wght@400;500&display=swap" | |
| 22 | + rel="stylesheet" | |
| 23 | + /> | |
| 24 | + </head> | |
| 25 | + <body> | |
| 26 | + <div id="root"></div> | |
| 27 | + <script type="module" src="./frontend.tsx"></script> | |
| 28 | + </body> | |
| 29 | +</html> | |
| @@ -0,0 +1,6 @@ | ||
| 1 | +import { clsx, type ClassValue } from "clsx"; | |
| 2 | +import { twMerge } from "tailwind-merge"; | |
| 3 | + | |
| 4 | +export function cn(...inputs: ReadonlyArray<ClassValue>): string { | |
| 5 | + return twMerge(clsx(inputs)); | |
| 6 | +} | |
| @@ -0,0 +1,100 @@ | ||
| 1 | +import * as React from "react"; | |
| 2 | +import { | |
| 3 | + RouterProvider, | |
| 4 | + createRootRoute, | |
| 5 | + createRoute, | |
| 6 | + createRouter, | |
| 7 | + useNavigate, | |
| 8 | +} from "@tanstack/react-router"; | |
| 9 | +import { ExecutorReactProvider } from "@executor-v3/react"; | |
| 10 | + | |
| 11 | +import "./globals.css"; | |
| 12 | + | |
| 13 | +import { AppShell } from "./components/shell"; | |
| 14 | +import { HomePage } from "./views/home"; | |
| 15 | +import { SourceDetailPage } from "./views/source-detail"; | |
| 16 | + | |
| 17 | +// --------------------------------------------------------------------------- | |
| 18 | +// Route search schema | |
| 19 | +// --------------------------------------------------------------------------- | |
| 20 | + | |
| 21 | +type SourceRouteSearch = { | |
| 22 | + tab: "model" | "discover" | "manifest" | "definitions" | "raw"; | |
| 23 | + tool?: string; | |
| 24 | + query?: string; | |
| 25 | +}; | |
| 26 | + | |
| 27 | +const sourceTabs = ["model", "discover", "manifest", "definitions", "raw"] as const; | |
| 28 | + | |
| 29 | +// --------------------------------------------------------------------------- | |
| 30 | +// Routes | |
| 31 | +// --------------------------------------------------------------------------- | |
| 32 | + | |
| 33 | +const rootRoute = createRootRoute({ | |
| 34 | + component: AppShell, | |
| 35 | +}); | |
| 36 | + | |
| 37 | +const homeRoute = createRoute({ | |
| 38 | + getParentRoute: () => rootRoute, | |
| 39 | + path: "/", | |
| 40 | + component: HomePage, | |
| 41 | +}); | |
| 42 | + | |
| 43 | +const sourceRoute = createRoute({ | |
| 44 | + getParentRoute: () => rootRoute, | |
| 45 | + path: "/sources/$sourceId", | |
| 46 | + validateSearch: (search: Record<string, unknown>): SourceRouteSearch => ({ | |
| 47 | + tab: | |
| 48 | + typeof search.tab === "string" && sourceTabs.includes(search.tab as SourceRouteSearch["tab"]) | |
| 49 | + ? (search.tab as SourceRouteSearch["tab"]) | |
| 50 | + : "model", | |
| 51 | + tool: typeof search.tool === "string" && search.tool.length > 0 ? search.tool : undefined, | |
| 52 | + query: typeof search.query === "string" ? search.query : undefined, | |
| 53 | + }), | |
| 54 | + component: SourceDetailPageWrapper, | |
| 55 | +}); | |
| 56 | + | |
| 57 | +function SourceDetailPageWrapper() { | |
| 58 | + const { sourceId } = sourceRoute.useParams(); | |
| 59 | + const search = sourceRoute.useSearch(); | |
| 60 | + const navigate = useNavigate({ from: sourceRoute.fullPath }); | |
| 61 | + | |
| 62 | + return ( | |
| 63 | + <SourceDetailPage | |
| 64 | + sourceId={sourceId} | |
| 65 | + search={search} | |
| 66 | + navigate={navigate as any} | |
| 67 | + /> | |
| 68 | + ); | |
| 69 | +} | |
| 70 | + | |
| 71 | +// --------------------------------------------------------------------------- | |
| 72 | +// Router | |
| 73 | +// --------------------------------------------------------------------------- | |
| 74 | + | |
| 75 | +const routeTree = rootRoute.addChildren([homeRoute, sourceRoute]); | |
| 76 | + | |
| 77 | +const router = createRouter({ | |
| 78 | + routeTree, | |
| 79 | + defaultPreload: "intent", | |
| 80 | +}); | |
| 81 | + | |
| 82 | +declare module "@tanstack/react-router" { | |
| 83 | + interface Register { | |
| 84 | + router: typeof router; | |
| 85 | + } | |
| 86 | +} | |
| 87 | + | |
| 88 | +// --------------------------------------------------------------------------- | |
| 89 | +// App | |
| 90 | +// --------------------------------------------------------------------------- | |
| 91 | + | |
| 92 | +export function App() { | |
| 93 | + return ( | |
| 94 | + <React.StrictMode> | |
| 95 | + <ExecutorReactProvider> | |
| 96 | + <RouterProvider router={router} /> | |
| 97 | + </ExecutorReactProvider> | |
| 98 | + </React.StrictMode> | |
| 99 | + ); | |
| 100 | +} | |
| @@ -0,0 +1,13 @@ | ||
| 1 | +/** | |
| 2 | + * Production server. Serves the API + Vite-built static assets. | |
| 3 | + * | |
| 4 | + * In development, use `bun run dev` which starts Vite with | |
| 5 | + * @hono/vite-dev-server embedding the API via dev.ts. | |
| 6 | + */ | |
| 7 | +import { runLocalExecutorServer } from "@executor-v3/server"; | |
| 8 | + | |
| 9 | +await runLocalExecutorServer({ | |
| 10 | + ui: { | |
| 11 | + assetsDir: new URL("../dist", import.meta.url).pathname, | |
| 12 | + }, | |
| 13 | +}); | |
| @@ -0,0 +1,181 @@ | ||
| 1 | +import { Link } from "@tanstack/react-router"; | |
| 2 | +import { useSources, type Source } from "@executor-v3/react"; | |
| 3 | +import { LoadableBlock } from "../components/loadable"; | |
| 4 | +import { Badge } from "../components/ui/badge"; | |
| 5 | +import { IconSources, IconDiscover } from "../components/icons"; | |
| 6 | +import { CodeBlock } from "../components/code-block"; | |
| 7 | + | |
| 8 | +export function HomePage() { | |
| 9 | + const sources = useSources(); | |
| 10 | + | |
| 11 | + return ( | |
| 12 | + <div className="flex-1 overflow-y-auto"> | |
| 13 | + <div className="mx-auto max-w-5xl px-6 py-8 lg:px-10 lg:py-12"> | |
| 14 | + {/* Hero */} | |
| 15 | + <div className="mb-10"> | |
| 16 | + <h1 className="font-display text-3xl tracking-tight text-foreground lg:text-4xl"> | |
| 17 | + Dashboard | |
| 18 | + </h1> | |
| 19 | + <p className="mt-2 max-w-lg text-[15px] leading-relaxed text-muted-foreground"> | |
| 20 | + Manage sources, inspect tool surfaces, and monitor executions. | |
| 21 | + </p> | |
| 22 | + </div> | |
| 23 | + | |
| 24 | + {/* Quick stats row */} | |
| 25 | + <LoadableBlock loadable={sources} loading="Loading..."> | |
| 26 | + {(items) => ( | |
| 27 | + <div className="grid gap-3 sm:grid-cols-3 mb-8"> | |
| 28 | + <QuickStat label="Connected sources" value={String(items.length)} /> | |
| 29 | + <QuickStat | |
| 30 | + label="Total tools" | |
| 31 | + value={items.length > 0 ? `${items.length} sources` : "0"} | |
| 32 | + sub="Inspect a source to see tools" | |
| 33 | + /> | |
| 34 | + <QuickStat label="Executions" value="--" sub="Coming soon" /> | |
| 35 | + </div> | |
| 36 | + )} | |
| 37 | + </LoadableBlock> | |
| 38 | + | |
| 39 | + {/* Two-column dashboard grid */} | |
| 40 | + <div className="grid gap-6 xl:grid-cols-2"> | |
| 41 | + {/* Sources card */} | |
| 42 | + <section className="rounded-xl border border-border bg-card"> | |
| 43 | + <div className="flex items-center justify-between border-b border-border px-5 py-3.5"> | |
| 44 | + <div className="flex items-center gap-2.5"> | |
| 45 | + <div className="flex size-7 items-center justify-center rounded-lg bg-primary/10 text-primary"> | |
| 46 | + <IconSources className="size-3.5" /> | |
| 47 | + </div> | |
| 48 | + <h2 className="text-sm font-semibold text-foreground">Sources</h2> | |
| 49 | + </div> | |
| 50 | + <span className="text-[11px] text-muted-foreground/50"> | |
| 51 | + Connected tool providers | |
| 52 | + </span> | |
| 53 | + </div> | |
| 54 | + <div className="p-3"> | |
| 55 | + <LoadableBlock loadable={sources} loading="Loading sources..."> | |
| 56 | + {(items) => | |
| 57 | + items.length === 0 ? ( | |
| 58 | + <div className="flex min-h-40 items-center justify-center rounded-lg border border-dashed border-border"> | |
| 59 | + <div className="text-center px-4"> | |
| 60 | + <p className="text-[13px] font-medium text-foreground/70">No sources connected</p> | |
| 61 | + <p className="mt-1 text-[12px] text-muted-foreground/50"> | |
| 62 | + Run{" "} | |
| 63 | + <code className="rounded bg-muted px-1.5 py-0.5 font-mono text-[11px] text-primary"> | |
| 64 | + executor dev seed-github | |
| 65 | + </code>{" "} | |
| 66 | + to get started | |
| 67 | + </p> | |
| 68 | + </div> | |
| 69 | + </div> | |
| 70 | + ) : ( | |
| 71 | + <div className="space-y-1"> | |
| 72 | + {items.map((source) => ( | |
| 73 | + <SourceRow key={source.id} source={source} /> | |
| 74 | + ))} | |
| 75 | + </div> | |
| 76 | + ) | |
| 77 | + } | |
| 78 | + </LoadableBlock> | |
| 79 | + </div> | |
| 80 | + </section> | |
| 81 | + | |
| 82 | + {/* Getting started / MCP setup card */} | |
| 83 | + <section className="rounded-xl border border-border bg-card"> | |
| 84 | + <div className="flex items-center justify-between border-b border-border px-5 py-3.5"> | |
| 85 | + <div className="flex items-center gap-2.5"> | |
| 86 | + <div className="flex size-7 items-center justify-center rounded-lg bg-accent text-accent-foreground"> | |
| 87 | + <IconDiscover className="size-3.5" /> | |
| 88 | + </div> | |
| 89 | + <h2 className="text-sm font-semibold text-foreground">Quick start</h2> | |
| 90 | + </div> | |
| 91 | + </div> | |
| 92 | + <div className="p-5 space-y-4"> | |
| 93 | + <div> | |
| 94 | + <h3 className="text-[13px] font-medium text-foreground">Connect to an agent</h3> | |
| 95 | + <p className="mt-1 text-[12px] leading-relaxed text-muted-foreground"> | |
| 96 | + Point your MCP-compatible client at this executor instance to give agents access to all connected tool sources. | |
| 97 | + </p> | |
| 98 | + </div> | |
| 99 | + <div className="rounded-lg border border-border bg-muted/30 overflow-hidden"> | |
| 100 | + <CodeBlock | |
| 101 | + lang="json" | |
| 102 | + code={JSON.stringify({ | |
| 103 | + mcpServers: { | |
| 104 | + executor: { | |
| 105 | + url: `${typeof window !== "undefined" ? window.location.origin : "http://127.0.0.1:8788"}/v1/mcp`, | |
| 106 | + }, | |
| 107 | + }, | |
| 108 | + }, null, 2)} | |
| 109 | + /> | |
| 110 | + </div> | |
| 111 | + <div className="space-y-2"> | |
| 112 | + <StepItem number={1} text="Add a source via CLI or API" /> | |
| 113 | + <StepItem number={2} text="Point your agent at the MCP endpoint above" /> | |
| 114 | + <StepItem number={3} text="The agent discovers and calls tools through executor" /> | |
| 115 | + </div> | |
| 116 | + </div> | |
| 117 | + </section> | |
| 118 | + </div> | |
| 119 | + </div> | |
| 120 | + </div> | |
| 121 | + ); | |
| 122 | +} | |
| 123 | + | |
| 124 | +function SourceRow({ source }: { source: Source }) { | |
| 125 | + return ( | |
| 126 | + <Link | |
| 127 | + to="/sources/$sourceId" | |
| 128 | + params={{ sourceId: source.id }} | |
| 129 | + search={{ tab: "model" }} | |
| 130 | + className="group flex items-center gap-3 rounded-lg px-3 py-2.5 transition-colors hover:bg-accent/50" | |
| 131 | + > | |
| 132 | + <div className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground"> | |
| 133 | + <IconSources className="size-3.5" /> | |
| 134 | + </div> | |
| 135 | + <div className="flex-1 min-w-0"> | |
| 136 | + <div className="flex items-center gap-2"> | |
| 137 | + <span className="truncate text-[13px] font-medium text-foreground group-hover:text-primary transition-colors"> | |
| 138 | + {source.name} | |
| 139 | + </span> | |
| 140 | + <Badge variant="outline" className="text-[9px]">{source.kind}</Badge> | |
| 141 | + </div> | |
| 142 | + <p className="truncate text-[11px] text-muted-foreground/50 font-mono mt-0.5"> | |
| 143 | + {source.endpoint} | |
| 144 | + </p> | |
| 145 | + </div> | |
| 146 | + <Badge variant={source.status === "connected" ? "default" : "muted"} className="shrink-0"> | |
| 147 | + {source.status} | |
| 148 | + </Badge> | |
| 149 | + <span className="text-[11px] text-muted-foreground/30 opacity-0 group-hover:opacity-100 transition-opacity"> | |
| 150 | + → | |
| 151 | + </span> | |
| 152 | + </Link> | |
| 153 | + ); | |
| 154 | +} | |
| 155 | + | |
| 156 | +function QuickStat(props: { label: string; value: string; sub?: string }) { | |
| 157 | + return ( | |
| 158 | + <div className="rounded-xl border border-border bg-card px-4 py-3.5"> | |
| 159 | + <div className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground/50"> | |
| 160 | + {props.label} | |
| 161 | + </div> | |
| 162 | + <div className="mt-1 text-xl font-semibold tracking-tight text-foreground"> | |
| 163 | + {props.value} | |
| 164 | + </div> | |
| 165 | + {props.sub && ( | |
| 166 | + <div className="mt-0.5 text-[11px] text-muted-foreground/40">{props.sub}</div> | |
| 167 | + )} | |
| 168 | + </div> | |
| 169 | + ); | |
| 170 | +} | |
| 171 | + | |
| 172 | +function StepItem(props: { number: number; text: string }) { | |
| 173 | + return ( | |
| 174 | + <div className="flex items-start gap-3"> | |
| 175 | + <span className="flex size-5 shrink-0 items-center justify-center rounded-full bg-primary/10 text-[10px] font-semibold text-primary"> | |
| 176 | + {props.number} | |
| 177 | + </span> | |
| 178 | + <span className="text-[12px] leading-relaxed text-muted-foreground">{props.text}</span> | |
| 179 | + </div> | |
| 180 | + ); | |
| 181 | +} | |
| @@ -0,0 +1,604 @@ | ||
| 1 | +import { useState, useEffect, useCallback, useRef } from "react"; | |
| 2 | +import { useNavigate } from "@tanstack/react-router"; | |
| 3 | +import { | |
| 4 | + useSourceInspection, | |
| 5 | + useSourceToolDetail, | |
| 6 | + useSourceDiscovery, | |
| 7 | + type Loadable, | |
| 8 | + type SourceInspection, | |
| 9 | + type SourceInspectionToolDetail, | |
| 10 | + type SourceInspectionDiscoverResult, | |
| 11 | +} from "@executor-v3/react"; | |
| 12 | +import { cn } from "../lib/utils"; | |
| 13 | +import { Badge, MethodBadge } from "../components/ui/badge"; | |
| 14 | +import { Button } from "../components/ui/button"; | |
| 15 | +import { LoadableBlock, EmptyState } from "../components/loadable"; | |
| 16 | +import { DocumentPanel } from "../components/document-panel"; | |
| 17 | +import { | |
| 18 | + IconSearch, | |
| 19 | + IconChevron, | |
| 20 | + IconTool, | |
| 21 | + IconCopy, | |
| 22 | + IconCheck, | |
| 23 | + IconClose, | |
| 24 | + IconEmpty, | |
| 25 | +} from "../components/icons"; | |
| 26 | + | |
| 27 | +// --------------------------------------------------------------------------- | |
| 28 | +// Types | |
| 29 | +// --------------------------------------------------------------------------- | |
| 30 | + | |
| 31 | +type SourceRouteSearch = { | |
| 32 | + tab: "model" | "discover" | "manifest" | "definitions" | "raw"; | |
| 33 | + tool?: string; | |
| 34 | + query?: string; | |
| 35 | +}; | |
| 36 | + | |
| 37 | +const sourceTabs: Array<{ id: SourceRouteSearch["tab"]; label: string }> = [ | |
| 38 | + { id: "model", label: "Model" }, | |
| 39 | + { id: "discover", label: "Discover" }, | |
| 40 | + { id: "manifest", label: "Manifest" }, | |
| 41 | + { id: "definitions", label: "Definitions" }, | |
| 42 | + { id: "raw", label: "Raw" }, | |
| 43 | +]; | |
| 44 | + | |
| 45 | +// --------------------------------------------------------------------------- | |
| 46 | +// SourceDetailPage (main export) | |
| 47 | +// --------------------------------------------------------------------------- | |
| 48 | + | |
| 49 | +export function SourceDetailPage(props: { | |
| 50 | + sourceId: string; | |
| 51 | + search: SourceRouteSearch; | |
| 52 | + navigate: (opts: { search: (prev: SourceRouteSearch) => SourceRouteSearch; replace?: boolean }) => void; | |
| 53 | +}) { | |
| 54 | + const { sourceId, search, navigate } = props; | |
| 55 | + const inspection = useSourceInspection(sourceId); | |
| 56 | + | |
| 57 | + const selectedToolPath = | |
| 58 | + search.tool | |
| 59 | + ?? (inspection.status === "ready" ? inspection.data.tools[0]?.path : undefined); | |
| 60 | + | |
| 61 | + const toolDetail = useSourceToolDetail( | |
| 62 | + sourceId, | |
| 63 | + search.tab === "model" ? selectedToolPath ?? null : null, | |
| 64 | + ); | |
| 65 | + | |
| 66 | + const discovery = useSourceDiscovery({ | |
| 67 | + sourceId, | |
| 68 | + query: search.query ?? "", | |
| 69 | + limit: 12, | |
| 70 | + }); | |
| 71 | + | |
| 72 | + // Auto-select first tool | |
| 73 | + useEffect(() => { | |
| 74 | + if (search.tab !== "model" || search.tool || inspection.status !== "ready") return; | |
| 75 | + const firstTool = inspection.data.tools[0]?.path; | |
| 76 | + if (!firstTool) return; | |
| 77 | + void navigate({ search: (prev) => ({ ...prev, tool: firstTool }), replace: true }); | |
| 78 | + }, [inspection, navigate, search.tab, search.tool]); | |
| 79 | + | |
| 80 | + return ( | |
| 81 | + <LoadableBlock loadable={inspection} loading="Resolving inspection bundle..."> | |
| 82 | + {(bundle) => { | |
| 83 | + const selectedTool = | |
| 84 | + bundle.tools.find((t) => t.path === selectedToolPath) ?? bundle.tools[0] ?? null; | |
| 85 | + | |
| 86 | + return ( | |
| 87 | + <div className="flex h-full flex-col overflow-hidden"> | |
| 88 | + {/* Header bar */} | |
| 89 | + <div className="flex shrink-0 items-center justify-between border-b border-border bg-background/95 backdrop-blur-sm px-4 h-12"> | |
| 90 | + <div className="flex items-center gap-3 min-w-0"> | |
| 91 | + <h2 className="truncate text-sm font-semibold text-foreground"> | |
| 92 | + {bundle.source.name} | |
| 93 | + </h2> | |
| 94 | + <Badge variant="outline">{bundle.pipelineKind}</Badge> | |
| 95 | + <span className="hidden text-[11px] text-muted-foreground/50 font-mono sm:block"> | |
| 96 | + {bundle.namespace} | |
| 97 | + </span> | |
| 98 | + </div> | |
| 99 | + <div className="flex items-center gap-1"> | |
| 100 | + {sourceTabs.map((tab) => ( | |
| 101 | + <button | |
| 102 | + key={tab.id} | |
| 103 | + type="button" | |
| 104 | + onClick={() => void navigate({ search: (prev) => ({ ...prev, tab: tab.id }) })} | |
| 105 | + className={cn( | |
| 106 | + "rounded-md px-2.5 py-1 text-[12px] font-medium transition-colors", | |
| 107 | + tab.id === search.tab | |
| 108 | + ? "bg-accent text-foreground" | |
| 109 | + : "text-muted-foreground hover:text-foreground hover:bg-accent/50", | |
| 110 | + )} | |
| 111 | + > | |
| 112 | + {tab.label} | |
| 113 | + </button> | |
| 114 | + ))} | |
| 115 | + </div> | |
| 116 | + </div> | |
| 117 | + | |
| 118 | + {/* Tab content */} | |
| 119 | + <div className="flex flex-1 min-h-0 overflow-hidden"> | |
| 120 | + {search.tab === "model" && ( | |
| 121 | + <ModelView | |
| 122 | + bundle={bundle} | |
| 123 | + detail={toolDetail} | |
| 124 | + selectedToolPath={selectedTool?.path ?? null} | |
| 125 | + onSelectTool={(toolPath) => | |
| 126 | + void navigate({ search: (prev) => ({ ...prev, tool: toolPath, tab: "model" }) }) | |
| 127 | + } | |
| 128 | + /> | |
| 129 | + )} | |
| 130 | + {search.tab === "discover" && ( | |
| 131 | + <DiscoveryView | |
| 132 | + bundle={bundle} | |
| 133 | + discovery={discovery} | |
| 134 | + initialQuery={search.query ?? ""} | |
| 135 | + onSubmitQuery={(query) => | |
| 136 | + void navigate({ search: (prev) => ({ ...prev, query, tab: "discover" }) }) | |
| 137 | + } | |
| 138 | + onOpenTool={(toolPath) => | |
| 139 | + void navigate({ search: (prev) => ({ ...prev, tab: "model", tool: toolPath }) }) | |
| 140 | + } | |
| 141 | + /> | |
| 142 | + )} | |
| 143 | + {search.tab === "manifest" && ( | |
| 144 | + <div className="flex-1 overflow-y-auto p-4"> | |
| 145 | + <DocumentPanel title="Extracted manifest" body={bundle.manifestJson} empty="No manifest snapshot available." /> | |
| 146 | + </div> | |
| 147 | + )} | |
| 148 | + {search.tab === "definitions" && ( | |
| 149 | + <div className="flex-1 overflow-y-auto p-4"> | |
| 150 | + <DocumentPanel title="Compiled tool definitions" body={bundle.definitionsJson} empty="No compiled definitions snapshot available." /> | |
| 151 | + </div> | |
| 152 | + )} | |
| 153 | + {search.tab === "raw" && ( | |
| 154 | + <div className="flex-1 overflow-y-auto p-4"> | |
| 155 | + <DocumentPanel title="Raw source document" body={bundle.rawDocumentText} empty="No raw source document stored." /> | |
| 156 | + </div> | |
| 157 | + )} | |
| 158 | + </div> | |
| 159 | + </div> | |
| 160 | + ); | |
| 161 | + }} | |
| 162 | + </LoadableBlock> | |
| 163 | + ); | |
| 164 | +} | |
| 165 | + | |
| 166 | +// --------------------------------------------------------------------------- | |
| 167 | +// ModelView — full-screen two-panel: tool list + tool detail | |
| 168 | +// --------------------------------------------------------------------------- | |
| 169 | + | |
| 170 | +function ModelView(props: { | |
| 171 | + bundle: SourceInspection; | |
| 172 | + detail: Loadable<SourceInspectionToolDetail | null>; | |
| 173 | + selectedToolPath: string | null; | |
| 174 | + onSelectTool: (toolPath: string) => void; | |
| 175 | +}) { | |
| 176 | + const [search, setSearch] = useState(""); | |
| 177 | + const searchRef = useRef<HTMLInputElement>(null); | |
| 178 | + const terms = search.trim().toLowerCase().split(/\s+/).filter(Boolean); | |
| 179 | + | |
| 180 | + const filteredTools = props.bundle.tools.filter((tool) => { | |
| 181 | + if (terms.length === 0) return true; | |
| 182 | + const corpus = [tool.path, tool.description ?? "", tool.title ?? "", tool.method ?? ""] | |
| 183 | + .join(" ") | |
| 184 | + .toLowerCase(); | |
| 185 | + return terms.every((t) => corpus.includes(t)); | |
| 186 | + }); | |
| 187 | + | |
| 188 | + // Keyboard shortcut: / to focus search | |
| 189 | + useEffect(() => { | |
| 190 | + const handler = (e: KeyboardEvent) => { | |
| 191 | + if (e.key === "/" && document.activeElement?.tagName !== "INPUT") { | |
| 192 | + e.preventDefault(); | |
| 193 | + searchRef.current?.focus(); | |
| 194 | + } | |
| 195 | + if (e.key === "Escape") { | |
| 196 | + searchRef.current?.blur(); | |
| 197 | + if (search.length > 0) setSearch(""); | |
| 198 | + } | |
| 199 | + }; | |
| 200 | + document.addEventListener("keydown", handler); | |
| 201 | + return () => document.removeEventListener("keydown", handler); | |
| 202 | + }, [search]); | |
| 203 | + | |
| 204 | + return ( | |
| 205 | + <> | |
| 206 | + {/* Left panel: tool tree */} | |
| 207 | + <div className="flex w-72 shrink-0 flex-col border-r border-border bg-card/30 lg:w-80 xl:w-[22rem]"> | |
| 208 | + {/* Search */} | |
| 209 | + <div className="shrink-0 border-b border-border px-3 py-2"> | |
| 210 | + <div className="relative"> | |
| 211 | + <IconSearch className="absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground/40" /> | |
| 212 | + <input | |
| 213 | + ref={searchRef} | |
| 214 | + value={search} | |
| 215 | + onChange={(e) => setSearch(e.target.value)} | |
| 216 | + placeholder={`Search ${props.bundle.toolCount} tools...`} | |
| 217 | + className="h-8 w-full rounded-md border border-input bg-background pl-8 pr-8 text-[13px] outline-none transition-colors placeholder:text-muted-foreground/40 focus:border-ring focus:ring-1 focus:ring-ring/30" | |
| 218 | + /> | |
| 219 | + {search.length > 0 ? ( | |
| 220 | + <button | |
| 221 | + type="button" | |
| 222 | + onClick={() => setSearch("")} | |
| 223 | + className="absolute right-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-muted-foreground/40 hover:text-foreground" | |
| 224 | + > | |
| 225 | + <IconClose /> | |
| 226 | + </button> | |
| 227 | + ) : ( | |
| 228 | + <kbd className="absolute right-2.5 top-1/2 -translate-y-1/2 rounded border border-border bg-muted px-1 py-px text-[10px] text-muted-foreground/50"> | |
| 229 | + / | |
| 230 | + </kbd> | |
| 231 | + )} | |
| 232 | + </div> | |
| 233 | + </div> | |
| 234 | + | |
| 235 | + {/* Tool header */} | |
| 236 | + <div className="flex shrink-0 items-center justify-between border-b border-border px-3 py-2"> | |
| 237 | + <span className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground/60"> | |
| 238 | + Tools | |
| 239 | + </span> | |
| 240 | + <span className="text-[11px] tabular-nums text-muted-foreground/40"> | |
| 241 | + {filteredTools.length !== props.bundle.toolCount | |
| 242 | + ? `${filteredTools.length} / ${props.bundle.toolCount}` | |
| 243 | + : props.bundle.toolCount} | |
| 244 | + </span> | |
| 245 | + </div> | |
| 246 | + | |
| 247 | + {/* Tool list */} | |
| 248 | + <div className="flex-1 overflow-y-auto"> | |
| 249 | + {filteredTools.length === 0 ? ( | |
| 250 | + <div className="p-4 text-center text-[13px] text-muted-foreground/50"> | |
| 251 | + {terms.length > 0 ? "No tools match your search" : "No tools available"} | |
| 252 | + </div> | |
| 253 | + ) : ( | |
| 254 | + <div className="p-1.5 space-y-px"> | |
| 255 | + {filteredTools.map((tool) => ( | |
| 256 | + <ToolListItem | |
| 257 | + key={tool.path} | |
| 258 | + tool={tool} | |
| 259 | + active={tool.path === props.selectedToolPath} | |
| 260 | + onSelect={() => props.onSelectTool(tool.path)} | |
| 261 | + search={search} | |
| 262 | + /> | |
| 263 | + ))} | |
| 264 | + </div> | |
| 265 | + )} | |
| 266 | + </div> | |
| 267 | + </div> | |
| 268 | + | |
| 269 | + {/* Right panel: tool detail */} | |
| 270 | + <div className="flex flex-1 flex-col min-w-0 overflow-hidden"> | |
| 271 | + <LoadableBlock loadable={props.detail} loading="Loading tool detail..."> | |
| 272 | + {(detail) => | |
| 273 | + detail ? ( | |
| 274 | + <ToolDetailPanel detail={detail} /> | |
| 275 | + ) : ( | |
| 276 | + <EmptyState | |
| 277 | + title={props.bundle.toolCount > 0 ? "Select a tool to view its details" : "No tools available"} | |
| 278 | + description={props.bundle.toolCount > 0 ? "Browse the tree on the left or press / to search" : undefined} | |
| 279 | + /> | |
| 280 | + ) | |
| 281 | + } | |
| 282 | + </LoadableBlock> | |
| 283 | + </div> | |
| 284 | + </> | |
| 285 | + ); | |
| 286 | +} | |
| 287 | + | |
| 288 | +// --------------------------------------------------------------------------- | |
| 289 | +// ToolListItem | |
| 290 | +// --------------------------------------------------------------------------- | |
| 291 | + | |
| 292 | +function ToolListItem(props: { | |
| 293 | + tool: SourceInspection["tools"][number]; | |
| 294 | + active: boolean; | |
| 295 | + onSelect: () => void; | |
| 296 | + search: string; | |
| 297 | +}) { | |
| 298 | + const ref = useRef<HTMLButtonElement>(null); | |
| 299 | + | |
| 300 | + useEffect(() => { | |
| 301 | + if (props.active && ref.current) { | |
| 302 | + ref.current.scrollIntoView({ block: "nearest" }); | |
| 303 | + } | |
| 304 | + }, [props.active]); | |
| 305 | + | |
| 306 | + return ( | |
| 307 | + <button | |
| 308 | + ref={ref} | |
| 309 | + type="button" | |
| 310 | + onClick={props.onSelect} | |
| 311 | + className={cn( | |
| 312 | + "group flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-left transition-colors", | |
| 313 | + props.active | |
| 314 | + ? "bg-primary/10 text-foreground border-l-2 border-l-primary -ml-px" | |
| 315 | + : "hover:bg-accent/50 text-foreground/70 hover:text-foreground", | |
| 316 | + )} | |
| 317 | + > | |
| 318 | + <IconTool className="size-3 shrink-0 text-muted-foreground/40" /> | |
| 319 | + <span className="flex-1 truncate font-mono text-[12px]"> | |
| 320 | + {highlightMatch(props.tool.path, props.search)} | |
| 321 | + </span> | |
| 322 | + {props.tool.method && <MethodBadge method={props.tool.method} />} | |
| 323 | + </button> | |
| 324 | + ); | |
| 325 | +} | |
| 326 | + | |
| 327 | +// --------------------------------------------------------------------------- | |
| 328 | +// ToolDetailPanel | |
| 329 | +// --------------------------------------------------------------------------- | |
| 330 | + | |
| 331 | +function ToolDetailPanel(props: { detail: SourceInspectionToolDetail }) { | |
| 332 | + const { detail } = props; | |
| 333 | + const [copiedField, setCopiedField] = useState<string | null>(null); | |
| 334 | + | |
| 335 | + const copy = useCallback((text: string, field: string) => { | |
| 336 | + void navigator.clipboard.writeText(text).then(() => { | |
| 337 | + setCopiedField(field); | |
| 338 | + setTimeout(() => setCopiedField(null), 1500); | |
| 339 | + }); | |
| 340 | + }, []); | |
| 341 | + | |
| 342 | + return ( | |
| 343 | + <div className="flex h-full flex-col overflow-hidden"> | |
| 344 | + {/* Sticky header */} | |
| 345 | + <div className="shrink-0 border-b border-border bg-background/95 backdrop-blur-sm"> | |
| 346 | + <div className="flex items-start gap-3 px-5 py-3.5"> | |
| 347 | + <div className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary"> | |
| 348 | + <IconTool className="size-4" /> | |
| 349 | + </div> | |
| 350 | + <div className="min-w-0 flex-1"> | |
| 351 | + <div className="flex items-center gap-2"> | |
| 352 | + <h3 className="truncate text-sm font-semibold text-foreground"> | |
| 353 | + {detail.summary.path} | |
| 354 | + </h3> | |
| 355 | + <CopyButton text={detail.summary.path} field="path" copiedField={copiedField} onCopy={copy} /> | |
| 356 | + </div> | |
| 357 | + <div className="mt-1 flex flex-wrap items-center gap-1.5"> | |
| 358 | + <Badge variant="outline">{detail.summary.providerKind}</Badge> | |
| 359 | + {detail.summary.operationId && ( | |
| 360 | + <span className="font-mono text-[11px] text-muted-foreground"> | |
| 361 | + {detail.summary.operationId} | |
| 362 | + </span> | |
| 363 | + )} | |
| 364 | + </div> | |
| 365 | + </div> | |
| 366 | + </div> | |
| 367 | + </div> | |
| 368 | + | |
| 369 | + {/* Scrollable body */} | |
| 370 | + <div className="flex-1 overflow-y-auto"> | |
| 371 | + <div className="px-5 py-4 space-y-5"> | |
| 372 | + {/* Summary cards */} | |
| 373 | + <div className="grid grid-cols-2 gap-2 lg:grid-cols-4"> | |
| 374 | + <MetricCard label="Input type" value={detail.summary.inputType ?? "unknown"} /> | |
| 375 | + <MetricCard label="Output type" value={detail.summary.outputType ?? "unknown"} /> | |
| 376 | + <MetricCard label="Tool ID" value={detail.summary.toolId} mono /> | |
| 377 | + <MetricCard label="Path template" value={detail.summary.pathTemplate ?? "n/a"} mono /> | |
| 378 | + </div> | |
| 379 | + | |
| 380 | + {/* Schema panels */} | |
| 381 | + <div className="grid grid-cols-1 gap-2 xl:grid-cols-2"> | |
| 382 | + <DocumentPanel title="Input schema" body={detail.inputSchemaJson} empty="No input schema." compact /> | |
| 383 | + <DocumentPanel title="Output schema" body={detail.outputSchemaJson} empty="No output schema." compact /> | |
| 384 | + <DocumentPanel title="Example input" body={detail.exampleInputJson} empty="No example input." compact /> | |
| 385 | + <DocumentPanel title="Example output" body={detail.exampleOutputJson} empty="No example output." compact /> | |
| 386 | + <DocumentPanel title="Provider detail" body={detail.providerDataJson} empty="No provider detail." compact /> | |
| 387 | + <DocumentPanel title="Definition" body={detail.definitionJson} empty="No compiled definition." compact /> | |
| 388 | + </div> | |
| 389 | + </div> | |
| 390 | + </div> | |
| 391 | + </div> | |
| 392 | + ); | |
| 393 | +} | |
| 394 | + | |
| 395 | +// --------------------------------------------------------------------------- | |
| 396 | +// DiscoveryView | |
| 397 | +// --------------------------------------------------------------------------- | |
| 398 | + | |
| 399 | +function DiscoveryView(props: { | |
| 400 | + bundle: SourceInspection; | |
| 401 | + discovery: Loadable<SourceInspectionDiscoverResult>; | |
| 402 | + initialQuery: string; | |
| 403 | + onSubmitQuery: (query: string) => void; | |
| 404 | + onOpenTool: (toolPath: string) => void; | |
| 405 | +}) { | |
| 406 | + const [draftQuery, setDraftQuery] = useState(props.initialQuery); | |
| 407 | + | |
| 408 | + useEffect(() => { | |
| 409 | + setDraftQuery(props.initialQuery); | |
| 410 | + }, [props.initialQuery]); | |
| 411 | + | |
| 412 | + return ( | |
| 413 | + <div className="flex flex-1 flex-col overflow-hidden"> | |
| 414 | + {/* Search bar */} | |
| 415 | + <div className="shrink-0 border-b border-border px-4 py-3"> | |
| 416 | + <form | |
| 417 | + className="flex items-center gap-2" | |
| 418 | + onSubmit={(e) => { | |
| 419 | + e.preventDefault(); | |
| 420 | + props.onSubmitQuery(draftQuery.trim()); | |
| 421 | + }} | |
| 422 | + > | |
| 423 | + <input | |
| 424 | + value={draftQuery} | |
| 425 | + onChange={(e) => setDraftQuery(e.target.value)} | |
| 426 | + placeholder="Search repos, issues, webhooks..." | |
| 427 | + className="h-9 flex-1 rounded-md border border-input bg-background px-3 text-sm outline-none transition-colors placeholder:text-muted-foreground/40 focus:border-ring focus:ring-1 focus:ring-ring/30" | |
| 428 | + /> | |
| 429 | + <Button type="submit" size="sm">Run</Button> | |
| 430 | + </form> | |
| 431 | + </div> | |
| 432 | + | |
| 433 | + {/* Results */} | |
| 434 | + <div className="flex flex-1 overflow-hidden"> | |
| 435 | + {/* Sidebar stats */} | |
| 436 | + <aside className="hidden w-56 shrink-0 border-r border-border p-3 space-y-2 lg:block"> | |
| 437 | + <MetricCard label="Source" value={props.bundle.source.name} /> | |
| 438 | + <MetricCard label="Namespace" value={props.bundle.namespace} /> | |
| 439 | + <MetricCard label="Tool count" value={String(props.bundle.toolCount)} /> | |
| 440 | + </aside> | |
| 441 | + | |
| 442 | + {/* Results list */} | |
| 443 | + <div className="flex-1 overflow-y-auto p-3"> | |
| 444 | + <LoadableBlock loadable={props.discovery} loading="Scoring candidate tools..."> | |
| 445 | + {(result) => | |
| 446 | + result.query.length === 0 ? ( | |
| 447 | + <EmptyState | |
| 448 | + title="Enter a query" | |
| 449 | + description="Search terms live in the URL so you can deep-link ranking investigations." | |
| 450 | + /> | |
| 451 | + ) : result.results.length === 0 ? ( | |
| 452 | + <EmptyState | |
| 453 | + title="No ranked matches" | |
| 454 | + description="Try a provider noun, operation verb, tag, or path fragment." | |
| 455 | + /> | |
| 456 | + ) : ( | |
| 457 | + <div className="space-y-2"> | |
| 458 | + {result.results.map((item, index) => ( | |
| 459 | + <article | |
| 460 | + key={item.path} | |
| 461 | + className="rounded-lg border border-border bg-card/60 p-3 space-y-2" | |
| 462 | + > | |
| 463 | + <div className="flex items-start justify-between gap-2"> | |
| 464 | + <div className="flex items-center gap-2 min-w-0"> | |
| 465 | + <span className="shrink-0 rounded bg-muted px-1.5 py-0.5 text-[10px] font-mono tabular-nums text-muted-foreground"> | |
| 466 | + #{index + 1} | |
| 467 | + </span> | |
| 468 | + <h4 className="truncate font-mono text-[13px] font-medium text-foreground"> | |
| 469 | + {item.path} | |
| 470 | + </h4> | |
| 471 | + </div> | |
| 472 | + <span className="shrink-0 font-mono text-sm font-semibold text-primary"> | |
| 473 | + {item.score.toFixed(2)} | |
| 474 | + </span> | |
| 475 | + </div> | |
| 476 | + <p className="text-[12px] text-muted-foreground leading-relaxed"> | |
| 477 | + {item.description ?? "No description"} | |
| 478 | + </p> | |
| 479 | + <div className="flex items-center gap-2 text-[11px] text-muted-foreground/60"> | |
| 480 | + <span>{item.inputType ?? "unknown input"}</span> | |
| 481 | + <span>→</span> | |
| 482 | + <span>{item.outputType ?? "unknown output"}</span> | |
| 483 | + </div> | |
| 484 | + <div className="flex flex-wrap gap-1"> | |
| 485 | + {item.reasons.map((reason) => ( | |
| 486 | + <span | |
| 487 | + key={reason} | |
| 488 | + className="rounded-full bg-primary/10 px-2 py-0.5 text-[10px] font-medium text-primary" | |
| 489 | + > | |
| 490 | + {reason} | |
| 491 | + </span> | |
| 492 | + ))} | |
| 493 | + </div> | |
| 494 | + <Button | |
| 495 | + variant="ghost" | |
| 496 | + size="sm" | |
| 497 | + className="text-[11px]" | |
| 498 | + onClick={() => props.onOpenTool(item.path)} | |
| 499 | + > | |
| 500 | + Open tool detail → | |
| 501 | + </Button> | |
| 502 | + </article> | |
| 503 | + ))} | |
| 504 | + </div> | |
| 505 | + ) | |
| 506 | + } | |
| 507 | + </LoadableBlock> | |
| 508 | + </div> | |
| 509 | + </div> | |
| 510 | + </div> | |
| 511 | + ); | |
| 512 | +} | |
| 513 | + | |
| 514 | +// --------------------------------------------------------------------------- | |
| 515 | +// Shared sub-components | |
| 516 | +// --------------------------------------------------------------------------- | |
| 517 | + | |
| 518 | +function MetricCard(props: { label: string; value: string; mono?: boolean }) { | |
| 519 | + return ( | |
| 520 | + <div className="rounded-lg border border-border bg-card/60 px-3 py-2"> | |
| 521 | + <div className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground/60"> | |
| 522 | + {props.label} | |
| 523 | + </div> | |
| 524 | + <div className={cn( | |
| 525 | + "mt-0.5 truncate text-[13px] text-foreground/85", | |
| 526 | + props.mono && "font-mono text-[12px]", | |
| 527 | + )}> | |
| 528 | + {props.value} | |
| 529 | + </div> | |
| 530 | + </div> | |
| 531 | + ); | |
| 532 | +} | |
| 533 | + | |
| 534 | +function CopyButton(props: { | |
| 535 | + text: string; | |
| 536 | + field: string; | |
| 537 | + copiedField: string | null; | |
| 538 | + onCopy: (text: string, field: string) => void; | |
| 539 | +}) { | |
| 540 | + return ( | |
| 541 | + <button | |
| 542 | + type="button" | |
| 543 | + onClick={() => props.onCopy(props.text, props.field)} | |
| 544 | + className="shrink-0 rounded p-1 text-muted-foreground/30 transition-colors hover:text-muted-foreground" | |
| 545 | + title={`Copy ${props.field}`} | |
| 546 | + > | |
| 547 | + {props.copiedField === props.field ? <IconCheck /> : <IconCopy />} | |
| 548 | + </button> | |
| 549 | + ); | |
| 550 | +} | |
| 551 | + | |
| 552 | +function highlightMatch(text: string, search: string) { | |
| 553 | + if (!search.trim()) return text; | |
| 554 | + const terms = search.trim().toLowerCase().split(/\s+/); | |
| 555 | + const lowerText = text.toLowerCase(); | |
| 556 | + const ranges: Array<[number, number]> = []; | |
| 557 | + | |
| 558 | + for (const term of terms) { | |
| 559 | + let idx = 0; | |
| 560 | + while (idx < lowerText.length) { | |
| 561 | + const found = lowerText.indexOf(term, idx); | |
| 562 | + if (found === -1) break; | |
| 563 | + ranges.push([found, found + term.length]); | |
| 564 | + idx = found + 1; | |
| 565 | + } | |
| 566 | + } | |
| 567 | + | |
| 568 | + if (ranges.length === 0) return text; | |
| 569 | + | |
| 570 | + ranges.sort((a, b) => a[0] - b[0]); | |
| 571 | + const merged: Array<[number, number]> = [ranges[0]!]; | |
| 572 | + for (let i = 1; i < ranges.length; i++) { | |
| 573 | + const last = merged[merged.length - 1]!; | |
| 574 | + const current = ranges[i]!; | |
| 575 | + if (current[0] <= last[1]) { | |
| 576 | + last[1] = Math.max(last[1], current[1]); | |
| 577 | + } else { | |
| 578 | + merged.push(current); | |
| 579 | + } | |
| 580 | + } | |
| 581 | + | |
| 582 | + const parts: Array<{ text: string; hl: boolean }> = []; | |
| 583 | + let cursor = 0; | |
| 584 | + for (const [start, end] of merged) { | |
| 585 | + if (cursor < start) parts.push({ text: text.slice(cursor, start), hl: false }); | |
| 586 | + parts.push({ text: text.slice(start, end), hl: true }); | |
| 587 | + cursor = end; | |
| 588 | + } | |
| 589 | + if (cursor < text.length) parts.push({ text: text.slice(cursor), hl: false }); | |
| 590 | + | |
| 591 | + return ( | |
| 592 | + <> | |
| 593 | + {parts.map((part, i) => | |
| 594 | + part.hl ? ( | |
| 595 | + <mark key={i} className="rounded-sm bg-primary/20 text-foreground px-px"> | |
| 596 | + {part.text} | |
| 597 | + </mark> | |
| 598 | + ) : ( | |
| 599 | + <span key={i}>{part.text}</span> | |
| 600 | + ), | |
| 601 | + )} | |
| 602 | + </> | |
| 603 | + ); | |
| 604 | +} | |
| @@ -0,0 +1,17 @@ | ||
| 1 | +{ | |
| 2 | + "compilerOptions": { | |
| 3 | + "target": "ES2022", | |
| 4 | + "module": "ESNext", | |
| 5 | + "moduleResolution": "Bundler", | |
| 6 | + "strict": true, | |
| 7 | + "skipLibCheck": true, | |
| 8 | + "noEmit": true, | |
| 9 | + "lib": ["ES2022", "DOM", "DOM.Iterable"], | |
| 10 | + "jsx": "react-jsx", | |
| 11 | + "types": ["bun-types"] | |
| 12 | + }, | |
| 13 | + "include": [ | |
| 14 | + "src/**/*.ts", | |
| 15 | + "src/**/*.tsx" | |
| 16 | + ] | |
| 17 | +} | |
| @@ -0,0 +1,29 @@ | ||
| 1 | +import { defineConfig } from "vite"; | |
| 2 | +import react from "@vitejs/plugin-react"; | |
| 3 | +import tailwindcss from "@tailwindcss/vite"; | |
| 4 | +import devServer from "@hono/vite-dev-server"; | |
| 5 | + | |
| 6 | +export default defineConfig({ | |
| 7 | + root: "src", | |
| 8 | + plugins: [ | |
| 9 | + tailwindcss(), | |
| 10 | + react(), | |
| 11 | + devServer({ | |
| 12 | + entry: "src/dev.ts", | |
| 13 | + exclude: [ | |
| 14 | + // Only let /v1 requests reach the API handler | |
| 15 | + /^\/(?!v1(\/|$))/, | |
| 16 | + /^\/(src|node_modules|@vite|@id|@react-refresh)/, | |
| 17 | + /\.(css|ts|tsx|js|jsx|svg|png|jpg|gif|ico|woff2?|json|map)(\?.*)?$/, | |
| 18 | + ], | |
| 19 | + injectClientScript: false, | |
| 20 | + }), | |
| 21 | + ], | |
| 22 | + build: { | |
| 23 | + outDir: "../dist", | |
| 24 | + emptyOutDir: true, | |
| 25 | + }, | |
| 26 | + server: { | |
| 27 | + port: 8788, | |
| 28 | + }, | |
| 29 | +}); | |
| @@ -6,6 +6,7 @@ · 5 unmodified lines | ||
| 6 | 6 | "name": "executor-v3", |
| 7 | 7 | "devDependencies": { |
| 8 | 8 | "knip": "^5.85.0", |
| 9 | + "turbo": "^2.5.6", | |
| 9 | 10 | "typescript": "^5.9.3", |
| 10 | 11 | }, |
| 11 | 12 | }, |
| @@ -21,6 +22,7 @@ · 9 unmodified lines | ||
| 21 | 22 | "@executor-v3/codemode-core": "workspace:*", |
| 22 | 23 | "@executor-v3/control-plane": "workspace:*", |
| 23 | 24 | "@executor-v3/runtime-local-inproc": "workspace:*", |
| 25 | + "@executor-v3/server": "workspace:*", | |
| 24 | 26 | "effect": "catalog:", |
| 25 | 27 | }, |
| 26 | 28 | "devDependencies": { |
| @@ -44,6 +46,34 @@ · 17 unmodified lines | ||
| 44 | 46 | "bun-types": "catalog:", |
| 45 | 47 | }, |
| 46 | 48 | }, |
| 49 | + "apps/web": { | |
| 50 | + "name": "@executor-v3/web", | |
| 51 | + "dependencies": { | |
| 52 | + "@executor-v3/react": "workspace:*", | |
| 53 | + "@executor-v3/server": "workspace:*", | |
| 54 | + "@tanstack/react-router": "^1.132.7", | |
| 55 | + "class-variance-authority": "^0.7.1", | |
| 56 | + "clsx": "^2.1.1", | |
| 57 | + "effect": "catalog:", | |
| 58 | + "react": "^19.2.0", | |
| 59 | + "react-dom": "^19.2.0", | |
| 60 | + "react-grab": "^0.1.26", | |
| 61 | + "scheduler": "^0.27.0", | |
| 62 | + "shiki": "^4.0.1", | |
| 63 | + "tailwind-merge": "^3.5.0", | |
| 64 | + }, | |
| 65 | + "devDependencies": { | |
| 66 | + "@hono/vite-dev-server": "^0.25.0", | |
| 67 | + "@tailwindcss/vite": "^4.2.1", | |
| 68 | + "@types/react": "^19.2.10", | |
| 69 | + "@types/react-dom": "^19.2.3", | |
| 70 | + "@vitejs/plugin-react": "^5.1.4", | |
| 71 | + "bun-types": "catalog:", | |
| 72 | + "tailwindcss": "^4.2.1", | |
| 73 | + "tw-animate-css": "^1.4.0", | |
| 74 | + "vite": "^7.3.1", | |
| 75 | + }, | |
| 76 | + }, | |
| 47 | 77 | "packages/ai-sdk-adapter": { |
| 48 | 78 | "name": "@executor-v3/ai-sdk-adapter", |
| 49 | 79 | "dependencies": { |
| @@ -91,12 +121,12 @@ · 41 unmodified lines | ||
| 91 | 121 | "packages/codemode-openapi": { |
| 92 | 122 | "name": "@executor-v3/codemode-openapi", |
| 93 | 123 | "dependencies": { |
| 124 | + "@effect/platform": "catalog:", | |
| 94 | 125 | "@executor-v3/codemode-core": "workspace:*", |
| 95 | 126 | "effect": "catalog:", |
| 96 | 127 | "yaml": "^2.8.1", |
| 97 | 128 | }, |
| 98 | 129 | "devDependencies": { |
| 99 | − "@effect/platform": "catalog:", | |
| 100 | 130 | "@effect/platform-node": "^0.104.1", |
| 101 | 131 | "@effect/vitest": "catalog:", |
| 102 | 132 | "@types/node": "catalog:", |
| @@ -135,6 +165,7 @@ · 32 unmodified lines | ||
| 135 | 165 | "@executor-v3/ai-sdk-adapter": "workspace:*", |
| 136 | 166 | "@executor-v3/codemode-core": "workspace:*", |
| 137 | 167 | "@executor-v3/codemode-openapi": "workspace:*", |
| 168 | + "@executor-v3/control-plane": "workspace:*", | |
| 138 | 169 | "@executor-v3/runtime-local-inproc": "workspace:*", |
| 139 | 170 | "effect": "catalog:", |
| 140 | 171 | }, |
| @@ -145,6 +176,22 @@ · 4 unmodified lines | ||
| 145 | 176 | "vitest": "catalog:", |
| 146 | 177 | }, |
| 147 | 178 | }, |
| 179 | + "packages/react": { | |
| 180 | + "name": "@executor-v3/react", | |
| 181 | + "dependencies": { | |
| 182 | + "@effect-atom/atom": "0.5.3", | |
| 183 | + "@effect-atom/atom-react": "0.5.0", | |
| 184 | + "@executor-v3/control-plane": "workspace:*", | |
| 185 | + "effect": "catalog:", | |
| 186 | + }, | |
| 187 | + "devDependencies": { | |
| 188 | + "@types/react": "^19.2.10", | |
| 189 | + "bun-types": "catalog:", | |
| 190 | + }, | |
| 191 | + "peerDependencies": { | |
| 192 | + "react": ">=18 <20", | |
| 193 | + }, | |
| 194 | + }, | |
| 148 | 195 | "packages/runtime-local-inproc": { |
| 149 | 196 | "name": "@executor-v3/runtime-local-inproc", |
| 150 | 197 | "dependencies": { |
| @@ -156,6 +203,18 @@ · 5 unmodified lines | ||
| 156 | 203 | "bun-types": "catalog:", |
| 157 | 204 | }, |
| 158 | 205 | }, |
| 206 | + "packages/server": { | |
| 207 | + "name": "@executor-v3/server", | |
| 208 | + "dependencies": { | |
| 209 | + "@effect/platform": "catalog:", | |
| 210 | + "@executor-v3/control-plane": "workspace:*", | |
| 211 | + "effect": "catalog:", | |
| 212 | + }, | |
| 213 | + "devDependencies": { | |
| 214 | + "@types/node": "catalog:", | |
| 215 | + "bun-types": "catalog:", | |
| 216 | + }, | |
| 217 | + }, | |
| 159 | 218 | }, |
| 160 | 219 | "catalog": { |
| 161 | 220 | "@effect/cli": "^0.73.2", |
| @@ -175,6 +234,8 @@ · 13 unmodified lines | ||
| 175 | 234 | |
| 176 | 235 | "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.19", "", { "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-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg=="], |
| 177 | 236 | |
| 237 | + "@antfu/ni": ["@antfu/ni@0.23.2", "", { "bin": { "na": "bin/na.mjs", "ni": "bin/ni.mjs", "nr": "bin/nr.mjs", "nu": "bin/nu.mjs", "nci": "bin/nci.mjs", "nlx": "bin/nlx.mjs", "nun": "bin/nun.mjs" } }, "sha512-FSEVWXvwroExDXUu8qV6Wqp2X3D1nJ0Li4LFymCyvCVrm7I3lNfG0zZWSWvGU1RE7891eTnFTyh31L3igOwNKQ=="], | |
| 238 | + | |
| 178 | 239 | "@azure-rest/core-client": ["@azure-rest/core-client@2.5.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0", "@azure/core-tracing": "^1.3.0", "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-EHaOXW0RYDKS5CFffnixdyRPak5ytiCtU7uXDcP/uiY+A6jFRwNGzzJBiznkCzvi5EYpY+YWinieqHb0oY916A=="], |
| 179 | 240 | |
| 180 | 241 | "@azure/abort-controller": ["@azure/abort-controller@2.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA=="], |
| @@ -209,8 +270,50 @@ · 28 unmodified lines | ||
| 209 | 270 | |
| 210 | 271 | "@azure/msal-node": ["@azure/msal-node@3.8.8", "", { "dependencies": { "@azure/msal-common": "15.15.0", "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" } }, "sha512-+f1VrJH1iI517t4zgmuhqORja0bL6LDQXfBqkjuMmfTYXTQQnh1EvwwxO3UbKLT05N0obF72SRHFrC1RBDv5Gg=="], |
| 211 | 272 | |
| 273 | + "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], | |
| 274 | + | |
| 275 | + "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], | |
| 276 | + | |
| 277 | + "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], | |
| 278 | + | |
| 279 | + "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], | |
| 280 | + | |
| 281 | + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], | |
| 282 | + | |
| 283 | + "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], | |
| 284 | + | |
| 285 | + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], | |
| 286 | + | |
| 287 | + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], | |
| 288 | + | |
| 289 | + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], | |
| 290 | + | |
| 291 | + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], | |
| 292 | + | |
| 293 | + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], | |
| 294 | + | |
| 295 | + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], | |
| 296 | + | |
| 297 | + "@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="], | |
| 298 | + | |
| 299 | + "@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], | |
| 300 | + | |
| 301 | + "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], | |
| 302 | + | |
| 303 | + "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], | |
| 304 | + | |
| 305 | + "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], | |
| 306 | + | |
| 307 | + "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], | |
| 308 | + | |
| 309 | + "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], | |
| 310 | + | |
| 212 | 311 | "@drizzle-team/brocli": ["@drizzle-team/brocli@0.11.0", "", {}, "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg=="], |
| 213 | 312 | |
| 313 | + "@effect-atom/atom": ["@effect-atom/atom@0.5.3", "", { "peerDependencies": { "@effect/experimental": "^0.58.0", "@effect/platform": "^0.94.2", "@effect/rpc": "^0.73.0", "effect": "^3.19.15" } }, "sha512-TRZv/i+YT3TtnN0oFORJqXdxSs1fc7lrJlH+1xZvDFyjC9hgoVnrcKbeZsDFmr6r0wYRqVo7U3IftxiQNjpNZA=="], | |
| 314 | + | |
| 315 | + "@effect-atom/atom-react": ["@effect-atom/atom-react@0.5.0", "", { "dependencies": { "@effect-atom/atom": "^0.5.0" }, "peerDependencies": { "effect": "^3.19", "react": ">=18 <20", "scheduler": "*" } }, "sha512-aFfjWi4rEJCqfM12Oi36/EKaDm/W6n4/N6yM5vL0t/QozKhJhK05rQL/GY4XMxlH2eqkQ4ih8jBQa3Yyp0Fiqw=="], | |
| 316 | + | |
| 214 | 317 | "@effect/cli": ["@effect/cli@0.73.2", "", { "dependencies": { "ini": "^4.1.3", "toml": "^3.0.0", "yaml": "^2.5.0" }, "peerDependencies": { "@effect/platform": "^0.94.3", "@effect/printer": "^0.47.0", "@effect/printer-ansi": "^0.47.0", "effect": "^3.19.16" } }, "sha512-K8IJo81+qa1LU8dhxcDU4QO/bIjL/dPd3zUOSCpLiuUNz8Y3/T+WNs3GqIXEhMfCFMSlRZERN0YgmtRlEZUREA=="], |
| 215 | 318 | |
| 216 | 319 | "@effect/cluster": ["@effect/cluster@0.56.4", "", { "dependencies": { "kubernetes-types": "^1.30.0" }, "peerDependencies": { "@effect/platform": "^0.94.5", "@effect/rpc": "^0.73.1", "@effect/sql": "^0.49.0", "@effect/workflow": "^0.16.0", "effect": "^3.19.17" } }, "sha512-7Je5/JlbZOlsSxsbKjr97dJed2cNGWsb+TLNgMcr5mRDbcWlFOTUGvsrisEJV6waosYLIg+2omPdvnvRoYKdhA=="], |
| @@ -311,16 +414,34 @@ · 94 unmodified lines | ||
| 311 | 414 | |
| 312 | 415 | "@executor-v3/mcp-elicitation-demo": ["@executor-v3/mcp-elicitation-demo@workspace:apps/mcp-elicitation-demo"], |
| 313 | 416 | |
| 417 | + "@executor-v3/react": ["@executor-v3/react@workspace:packages/react"], | |
| 418 | + | |
| 314 | 419 | "@executor-v3/runtime-local-inproc": ["@executor-v3/runtime-local-inproc@workspace:packages/runtime-local-inproc"], |
| 315 | 420 | |
| 421 | + "@executor-v3/server": ["@executor-v3/server@workspace:packages/server"], | |
| 422 | + | |
| 423 | + "@executor-v3/web": ["@executor-v3/web@workspace:apps/web"], | |
| 424 | + | |
| 316 | 425 | "@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="], |
| 317 | 426 | |
| 427 | + "@hono/vite-dev-server": ["@hono/vite-dev-server@0.25.0", "", { "dependencies": { "@hono/node-server": "^1.14.2", "minimatch": "^9.0.3" }, "peerDependencies": { "hono": "*", "miniflare": "*", "wrangler": "*" }, "optionalPeers": ["miniflare", "wrangler"] }, "sha512-4j5rs7yUTOObU2/yCEHeWbf3C8m95SD9lhdUUE6CJ462Emp9vS94yiza2dsA8+ly5pdaIQW0cd4nhNVcwYL5tA=="], | |
| 428 | + | |
| 429 | + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], | |
| 430 | + | |
| 431 | + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], | |
| 432 | + | |
| 433 | + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], | |
| 434 | + | |
| 318 | 435 | "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], |
| 319 | 436 | |
| 437 | + "@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=="], | |
| 438 | + | |
| 320 | 439 | "@js-joda/core": ["@js-joda/core@5.7.0", "", {}, "sha512-WBu4ULVVxySLLzK1Ppq+OdfP+adRS4ntmDQT915rzDJ++i95gc2jZkM5B6LWEAwN3lGXpfie3yPABozdD3K3Vg=="], |
| 321 | 440 | |
| 322 | 441 | "@js-temporal/polyfill": ["@js-temporal/polyfill@0.5.1", "", { "dependencies": { "jsbi": "^4.3.0" } }, "sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ=="], |
| 323 | 442 | |
| 443 | + "@medv/finder": ["@medv/finder@4.0.2", "", {}, "sha512-RraNY9SCcx4KZV0Dh6BEW6XEW2swkqYca74pkFFRw6hHItSHiy+O/xMnpbofjYbzXj0tSpBGthUF1hHTsr3vIQ=="], | |
| 444 | + | |
| 324 | 445 | "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.27.1", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA=="], |
| 325 | 446 | |
| 326 | 447 | "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw=="], |
| @@ -413,6 +534,10 @@ · 86 unmodified lines | ||
| 413 | 534 | |
| 414 | 535 | "@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.6", "", { "os": "win32", "cpu": "x64" }, "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw=="], |
| 415 | 536 | |
| 537 | + "@react-grab/cli": ["@react-grab/cli@0.1.26", "", { "dependencies": { "@antfu/ni": "^0.23.0", "commander": "^14.0.0", "ignore": "^7.0.5", "jsonc-parser": "^3.3.1", "ora": "^8.2.0", "picocolors": "^1.1.1", "prompts": "^2.4.2", "smol-toml": "^1.6.0" }, "bin": { "react-grab": "dist/cli.js" } }, "sha512-nNOBdrJ2K7jji6L/X9ARCpU1o/CQ33KYZUtW7vAtrfDM2hyvAIA224ps+Syl97FxhS7pO8b59Bnhc7YLG6h3nA=="], | |
| 538 | + | |
| 539 | + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="], | |
| 540 | + | |
| 416 | 541 | "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="], |
| 417 | 542 | |
| 418 | 543 | "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="], |
| @@ -463,28 +588,108 @@ · 44 unmodified lines | ||
| 463 | 588 | |
| 464 | 589 | "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="], |
| 465 | 590 | |
| 591 | + "@shikijs/core": ["@shikijs/core@4.0.1", "", { "dependencies": { "@shikijs/primitive": "4.0.1", "@shikijs/types": "4.0.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-vWvqi9JNgz1dRL9Nvog5wtx7RuNkf7MEPl2mU/cyUUxJeH1CAr3t+81h8zO8zs7DK6cKLMoU9TvukWIDjP4Lzg=="], | |
| 592 | + | |
| 593 | + "@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.0.1", "", { "dependencies": { "@shikijs/types": "4.0.1", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-DJK9NiwtGYqMuKCRO4Ip0FKNDQpmaiS+K5bFjJ7DWFn4zHueDWgaUG8kAofkrnXF6zPPYYQY7J5FYVW9MbZyBg=="], | |
| 594 | + | |
| 595 | + "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.0.1", "", { "dependencies": { "@shikijs/types": "4.0.1", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-oCWdCTDch3J8Kc0OZJ98KuUPC02O1VqIE3W/e2uvrHqTxYRR21RGEJMtchrgrxhsoJJCzmIciKsqG+q/yD+Cxg=="], | |
| 596 | + | |
| 597 | + "@shikijs/langs": ["@shikijs/langs@4.0.1", "", { "dependencies": { "@shikijs/types": "4.0.1" } }, "sha512-v/mluaybWdnGJR4GqAR6zh8qAZohW9k+cGYT28Y7M8+jLbC0l4yG085O1A+WkseHTn+awd+P3UBymb2+MXFc8w=="], | |
| 598 | + | |
| 599 | + "@shikijs/primitive": ["@shikijs/primitive@4.0.1", "", { "dependencies": { "@shikijs/types": "4.0.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-ns0hHZc5eWZuvuIEJz2pTx3Qecz0aRVYumVQJ8JgWY2tq/dH8WxdcVM49Fc2NsHEILNIT6vfdW9MF26RANWiTA=="], | |
| 600 | + | |
| 601 | + "@shikijs/themes": ["@shikijs/themes@4.0.1", "", { "dependencies": { "@shikijs/types": "4.0.1" } }, "sha512-FW41C/D6j/yKQkzVdjrRPiJCtgeDaYRJFEyCKFCINuRJRj9WcmubhP4KQHPZ4+9eT87jruSrYPyoblNRyDFzvA=="], | |
| 602 | + | |
| 603 | + "@shikijs/types": ["@shikijs/types@4.0.1", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-EaygPEn57+jJ76mw+nTLvIpJMAcMPokFbrF8lufsZP7Ukk+ToJYEcswN1G0e49nUZAq7aCQtoeW219A8HK1ZOw=="], | |
| 604 | + | |
| 605 | + "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], | |
| 606 | + | |
| 466 | 607 | "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], |
| 467 | 608 | |
| 609 | + "@tailwindcss/node": ["@tailwindcss/node@4.2.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.1" } }, "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg=="], | |
| 610 | + | |
| 611 | + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.1", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.1", "@tailwindcss/oxide-darwin-arm64": "4.2.1", "@tailwindcss/oxide-darwin-x64": "4.2.1", "@tailwindcss/oxide-freebsd-x64": "4.2.1", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", "@tailwindcss/oxide-linux-x64-musl": "4.2.1", "@tailwindcss/oxide-wasm32-wasi": "4.2.1", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" } }, "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw=="], | |
| 612 | + | |
| 613 | + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.1", "", { "os": "android", "cpu": "arm64" }, "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg=="], | |
| 614 | + | |
| 615 | + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw=="], | |
| 616 | + | |
| 617 | + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw=="], | |
| 618 | + | |
| 619 | + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA=="], | |
| 620 | + | |
| 621 | + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw=="], | |
| 622 | + | |
| 623 | + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ=="], | |
| 624 | + | |
| 625 | + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ=="], | |
| 626 | + | |
| 627 | + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g=="], | |
| 628 | + | |
| 629 | + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g=="], | |
| 630 | + | |
| 631 | + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.1", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q=="], | |
| 632 | + | |
| 633 | + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA=="], | |
| 634 | + | |
| 635 | + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.1", "", { "os": "win32", "cpu": "x64" }, "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ=="], | |
| 636 | + | |
| 637 | + "@tailwindcss/vite": ["@tailwindcss/vite@4.2.1", "", { "dependencies": { "@tailwindcss/node": "4.2.1", "@tailwindcss/oxide": "4.2.1", "tailwindcss": "4.2.1" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w=="], | |
| 638 | + | |
| 639 | + "@tanstack/history": ["@tanstack/history@1.161.4", "", {}, "sha512-Kp/WSt411ZWYvgXy6uiv5RmhHrz9cAml05AQPrtdAp7eUqvIDbMGPnML25OKbzR3RJ1q4wgENxDTvlGPa9+Mww=="], | |
| 640 | + | |
| 641 | + "@tanstack/react-router": ["@tanstack/react-router@1.166.2", "", { "dependencies": { "@tanstack/history": "1.161.4", "@tanstack/react-store": "^0.9.1", "@tanstack/router-core": "1.166.2", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-pKhUtrvVLlhjWhsHkJSuIzh1J4LcP+8ErbIqRLORX9Js8dUFMKoT0+8oFpi+P8QRpuhm/7rzjYiWfcyTsqQZtA=="], | |
| 642 | + | |
| 643 | + "@tanstack/react-store": ["@tanstack/react-store@0.9.2", "", { "dependencies": { "@tanstack/store": "0.9.2", "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-Vt5usJE5sHG/cMechQfmwvwne6ktGCELe89Lmvoxe3LKRoFrhPa8OCKWs0NliG8HTJElEIj7PLtaBQIcux5pAQ=="], | |
| 644 | + | |
| 645 | + "@tanstack/router-core": ["@tanstack/router-core@1.166.2", "", { "dependencies": { "@tanstack/history": "1.161.4", "@tanstack/store": "^0.9.1", "cookie-es": "^2.0.0", "seroval": "^1.4.2", "seroval-plugins": "^1.4.2", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-zn3NhENOAX9ToQiX077UV2OH3aJKOvV2ZMNZZxZ3gDG3i3WqL8NfWfEgetEAfMN37/Mnt90PpotYgf7IyuoKqQ=="], | |
| 646 | + | |
| 647 | + "@tanstack/store": ["@tanstack/store@0.9.2", "", {}, "sha512-K013lUJEFJK2ofFQ/hZKJUmCnpcV00ebLyOyFOWQvyQHUOZp/iYO84BM6aOGiV81JzwbX0APTVmW8YI7yiG5oA=="], | |
| 648 | + | |
| 468 | 649 | "@tediousjs/connection-string": ["@tediousjs/connection-string@0.5.0", "", {}, "sha512-7qSgZbincDDDFyRweCIEvZULFAw5iz/DeunhvuxpL31nfntX3P4Yd4HkHBRg9H8CdqY1e5WFN1PZIz/REL9MVQ=="], |
| 469 | 650 | |
| 470 | 651 | "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], |
| 471 | 652 | |
| 653 | + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], | |
| 654 | + | |
| 655 | + "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], | |
| 656 | + | |
| 657 | + "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], | |
| 658 | + | |
| 659 | + "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], | |
| 660 | + | |
| 472 | 661 | "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], |
| 473 | 662 | |
| 474 | 663 | "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], |
| 475 | 664 | |
| 476 | 665 | "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], |
| 477 | 666 | |
| 667 | + "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], | |
| 668 | + | |
| 669 | + "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], | |
| 670 | + | |
| 478 | 671 | "@types/mssql": ["@types/mssql@9.1.9", "", { "dependencies": { "@types/node": "*", "tarn": "^3.0.1", "tedious": "*" } }, "sha512-P0nCgw6vzY23UxZMnbI4N7fnLGANt4LI4yvxze1paPj+LuN28cFv5EI+QidP8udnId/BKhkcRhm/BleNsjK65A=="], |
| 479 | 672 | |
| 480 | 673 | "@types/node": ["@types/node@24.11.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw=="], |
| 481 | 674 | |
| 675 | + "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], | |
| 676 | + | |
| 677 | + "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], | |
| 678 | + | |
| 679 | + "@types/react-reconciler": ["@types/react-reconciler@0.28.9", "", { "peerDependencies": { "@types/react": "*" } }, "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg=="], | |
| 680 | + | |
| 482 | 681 | "@types/readable-stream": ["@types/readable-stream@4.0.23", "", { "dependencies": { "@types/node": "*" } }, "sha512-wwXrtQvbMHxCbBgjHaMGEmImFTQxxpfMOR/ZoQnXxB1woqkUbdLGFDgauo00Py9IudiaqSeiBiulSV9i6XIPig=="], |
| 483 | 682 | |
| 683 | + "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], | |
| 684 | + | |
| 484 | 685 | "@typespec/ts-http-runtime": ["@typespec/ts-http-runtime@0.3.3", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" } }, "sha512-91fp6CAAJSRtH5ja95T1FHSKa8aPW9/Zw6cta81jlZTUw/+Vq8jM/AfF/14h2b71wwR84JUTW/3Y8QPhDAawFA=="], |
| 485 | 686 | |
| 687 | + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], | |
| 688 | + | |
| 486 | 689 | "@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="], |
| 487 | 690 | |
| 691 | + "@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.4", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA=="], | |
| 692 | + | |
| 488 | 693 | "@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], |
| 489 | 694 | |
| 490 | 695 | "@vitest/mocker": ["@vitest/mocker@3.2.4", "", { "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ=="], |
| @@ -511,18 +716,30 @@ · 20 unmodified lines | ||
| 511 | 716 | |
| 512 | 717 | "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], |
| 513 | 718 | |
| 719 | + "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], | |
| 720 | + | |
| 514 | 721 | "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], |
| 515 | 722 | |
| 516 | 723 | "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], |
| 517 | 724 | |
| 725 | + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], | |
| 726 | + | |
| 518 | 727 | "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], |
| 519 | 728 | |
| 729 | + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.0", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA=="], | |
| 730 | + | |
| 731 | + "bippy": ["bippy@0.5.32", "", { "dependencies": { "@types/react-reconciler": "^0.28.9" }, "peerDependencies": { "react": ">=17.0.1" } }, "sha512-yt1mC8eReTxjfg41YBZdN4PvsDwHFWxltoiQX0Q+Htlbf41aSniopb7ECZits01HwNAvXEh69RGk/ImlswDTEw=="], | |
| 732 | + | |
| 520 | 733 | "bl": ["bl@6.1.6", "", { "dependencies": { "@types/readable-stream": "^4.0.0", "buffer": "^6.0.3", "inherits": "^2.0.4", "readable-stream": "^4.2.0" } }, "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg=="], |
| 521 | 734 | |
| 522 | 735 | "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], |
| 523 | 736 | |
| 737 | + "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], | |
| 738 | + | |
| 524 | 739 | "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], |
| 525 | 740 | |
| 741 | + "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], | |
| 742 | + | |
| 526 | 743 | "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], |
| 527 | 744 | |
| 528 | 745 | "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], |
| @@ -539,24 +756,50 @@ · 10 unmodified lines | ||
| 539 | 756 | |
| 540 | 757 | "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], |
| 541 | 758 | |
| 759 | + "caniuse-lite": ["caniuse-lite@1.0.30001777", "", {}, "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ=="], | |
| 760 | + | |
| 761 | + "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], | |
| 762 | + | |
| 542 | 763 | "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], |
| 543 | 764 | |
| 765 | + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], | |
| 766 | + | |
| 767 | + "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], | |
| 768 | + | |
| 769 | + "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], | |
| 770 | + | |
| 544 | 771 | "check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="], |
| 545 | 772 | |
| 773 | + "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], | |
| 774 | + | |
| 775 | + "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], | |
| 776 | + | |
| 777 | + "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], | |
| 778 | + | |
| 779 | + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], | |
| 780 | + | |
| 781 | + "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], | |
| 782 | + | |
| 546 | 783 | "commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], |
| 547 | 784 | |
| 548 | 785 | "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], |
| 549 | 786 | |
| 550 | 787 | "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], |
| 551 | 788 | |
| 789 | + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], | |
| 790 | + | |
| 552 | 791 | "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], |
| 553 | 792 | |
| 793 | + "cookie-es": ["cookie-es@2.0.0", "", {}, "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg=="], | |
| 794 | + | |
| 554 | 795 | "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], |
| 555 | 796 | |
| 556 | 797 | "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], |
| 557 | 798 | |
| 558 | 799 | "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], |
| 559 | 800 | |
| 801 | + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], | |
| 802 | + | |
| 560 | 803 | "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], |
| 561 | 804 | |
| 562 | 805 | "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], |
| @@ -569,8 +812,12 @@ · 6 unmodified lines | ||
| 569 | 812 | |
| 570 | 813 | "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], |
| 571 | 814 | |
| 815 | + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], | |
| 816 | + | |
| 572 | 817 | "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], |
| 573 | 818 | |
| 819 | + "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], | |
| 820 | + | |
| 574 | 821 | "drizzle-kit": ["drizzle-kit@1.0.0-beta.15-859cf75", "", { "dependencies": { "@drizzle-team/brocli": "^0.11.0", "@js-temporal/polyfill": "^0.5.1", "esbuild": "^0.25.10", "jiti": "^2.6.1" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-Y36s1XQGVb1PgU3aRNgufp1K3D2VkIifu8kv4Ubsmxi+Dq+N7KMklnpp7Knu/XC4FZi2MHPPG3v3o097r0/TcQ=="], |
| 575 | 822 | |
| 576 | 823 | "drizzle-orm": ["drizzle-orm@1.0.0-beta.15-859cf75", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@effect/sql": "^0.48.5", "@effect/sql-pg": "^0.49.7", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@sinclair/typebox": ">=0.34.8", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "arktype": ">=2.0.0", "better-sqlite3": ">=9.3.0", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5", "typebox": ">=1.0.0", "valibot": ">=1.0.0-beta.7", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@effect/sql", "@effect/sql-pg", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@sinclair/typebox", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "arktype", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "mysql2", "pg", "postgres", "sql.js", "sqlite3", "typebox", "valibot", "zod"] }, "sha512-dGVb2Q70H2AV6513hkOXR3Ud0FeGXLdugVq3YehoqkGIVTJrkuo0gRnCcW/dfI00O07t3T4HSh4clF/D/o/IsQ=="], |
| @@ -583,8 +830,14 @@ · 6 unmodified lines | ||
| 583 | 830 | |
| 584 | 831 | "effect": ["effect@3.19.19", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-Yc8U/SVXo2dHnaP7zNBlAo83h/nzSJpi7vph6Hzyl4ulgMBIgPmz3UzOjb9sBgpFE00gC0iETR244sfXDNLHRg=="], |
| 585 | 832 | |
| 833 | + "electron-to-chromium": ["electron-to-chromium@1.5.307", "", {}, "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg=="], | |
| 834 | + | |
| 835 | + "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], | |
| 836 | + | |
| 586 | 837 | "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], |
| 587 | 838 | |
| 839 | + "enhanced-resolve": ["enhanced-resolve@5.20.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ=="], | |
| 840 | + | |
| 588 | 841 | "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], |
| 589 | 842 | |
| 590 | 843 | "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], |
| @@ -595,6 +848,8 @@ · 4 unmodified lines | ||
| 595 | 848 | |
| 596 | 849 | "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], |
| 597 | 850 | |
| 851 | + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], | |
| 852 | + | |
| 598 | 853 | "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], |
| 599 | 854 | |
| 600 | 855 | "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], |
| @@ -647,6 +902,10 @@ · 46 unmodified lines | ||
| 647 | 902 | |
| 648 | 903 | "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], |
| 649 | 904 | |
| 905 | + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], | |
| 906 | + | |
| 907 | + "get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="], | |
| 908 | + | |
| 650 | 909 | "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], |
| 651 | 910 | |
| 652 | 911 | "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], |
| @@ -655,12 +914,20 @@ · 2 unmodified lines | ||
| 655 | 914 | |
| 656 | 915 | "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], |
| 657 | 916 | |
| 917 | + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], | |
| 918 | + | |
| 658 | 919 | "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], |
| 659 | 920 | |
| 660 | 921 | "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], |
| 661 | 922 | |
| 923 | + "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], | |
| 924 | + | |
| 925 | + "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], | |
| 926 | + | |
| 662 | 927 | "hono": ["hono@4.12.5", "", {}, "sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg=="], |
| 663 | 928 | |
| 929 | + "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], | |
| 930 | + | |
| 664 | 931 | "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], |
| 665 | 932 | |
| 666 | 933 | "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], |
| @@ -671,6 +938,8 @@ · 4 unmodified lines | ||
| 671 | 938 | |
| 672 | 939 | "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], |
| 673 | 940 | |
| 941 | + "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], | |
| 942 | + | |
| 674 | 943 | "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], |
| 675 | 944 | |
| 676 | 945 | "ini": ["ini@4.1.3", "", {}, "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg=="], |
| @@ -687,12 +956,18 @@ · 10 unmodified lines | ||
| 687 | 956 | |
| 688 | 957 | "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], |
| 689 | 958 | |
| 959 | + "is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="], | |
| 960 | + | |
| 690 | 961 | "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], |
| 691 | 962 | |
| 692 | 963 | "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], |
| 693 | 964 | |
| 965 | + "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], | |
| 966 | + | |
| 694 | 967 | "is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="], |
| 695 | 968 | |
| 969 | + "isbot": ["isbot@5.1.35", "", {}, "sha512-waFfC72ZNfwLLuJ2iLaoVaqcNo+CAaLR7xCpAn0Y5WfGzkNHv7ZN39Vbi1y+kb+Zs46XHOX3tZNExroFUPX+Kg=="], | |
| 970 | + | |
| 696 | 971 | "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], |
| 697 | 972 | |
| 698 | 973 | "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], |
| @@ -707,22 +982,54 @@ · 8 unmodified lines | ||
| 707 | 982 | |
| 708 | 983 | "jsbi": ["jsbi@4.3.2", "", {}, "sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew=="], |
| 709 | 984 | |
| 985 | + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], | |
| 986 | + | |
| 710 | 987 | "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], |
| 711 | 988 | |
| 712 | 989 | "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], |
| 713 | 990 | |
| 714 | 991 | "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], |
| 715 | 992 | |
| 993 | + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], | |
| 994 | + | |
| 995 | + "jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="], | |
| 996 | + | |
| 716 | 997 | "jsonwebtoken": ["jsonwebtoken@9.0.3", "", { "dependencies": { "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g=="], |
| 717 | 998 | |
| 718 | 999 | "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], |
| 719 | 1000 | |
| 720 | 1001 | "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="], |
| 721 | 1002 | |
| 1003 | + "kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], | |
| 1004 | + | |
| 722 | 1005 | "knip": ["knip@5.85.0", "", { "dependencies": { "@nodelib/fs.walk": "^1.2.3", "fast-glob": "^3.3.3", "formatly": "^0.3.0", "jiti": "^2.6.0", "js-yaml": "^4.1.1", "minimist": "^1.2.8", "oxc-resolver": "^11.15.0", "picocolors": "^1.1.1", "picomatch": "^4.0.1", "smol-toml": "^1.5.2", "strip-json-comments": "5.0.3", "zod": "^4.1.11" }, "peerDependencies": { "@types/node": ">=18", "typescript": ">=5.0.4 <7" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-V2kyON+DZiYdNNdY6GALseiNCwX7dYdpz9Pv85AUn69Gk0UKCts+glOKWfe5KmaMByRjM9q17Mzj/KinTVOyxg=="], |
| 723 | 1006 | |
| 724 | 1007 | "kubernetes-types": ["kubernetes-types@1.30.0", "", {}, "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q=="], |
| 725 | 1008 | |
| 1009 | + "lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="], | |
| 1010 | + | |
| 1011 | + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="], | |
| 1012 | + | |
| 1013 | + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.31.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg=="], | |
| 1014 | + | |
| 1015 | + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.31.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA=="], | |
| 1016 | + | |
| 1017 | + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.31.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A=="], | |
| 1018 | + | |
| 1019 | + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.31.1", "", { "os": "linux", "cpu": "arm" }, "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g=="], | |
| 1020 | + | |
| 1021 | + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg=="], | |
| 1022 | + | |
| 1023 | + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg=="], | |
| 1024 | + | |
| 1025 | + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA=="], | |
| 1026 | + | |
| 1027 | + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA=="], | |
| 1028 | + | |
| 1029 | + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.31.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w=="], | |
| 1030 | + | |
| 1031 | + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="], | |
| 1032 | + | |
| 726 | 1033 | "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="], |
| 727 | 1034 | |
| 728 | 1035 | "lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="], |
| @@ -737,18 +1044,34 @@ · 8 unmodified lines | ||
| 737 | 1044 | |
| 738 | 1045 | "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="], |
| 739 | 1046 | |
| 1047 | + "log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="], | |
| 1048 | + | |
| 740 | 1049 | "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], |
| 741 | 1050 | |
| 1051 | + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], | |
| 1052 | + | |
| 742 | 1053 | "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], |
| 743 | 1054 | |
| 744 | 1055 | "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], |
| 745 | 1056 | |
| 1057 | + "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], | |
| 1058 | + | |
| 746 | 1059 | "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], |
| 747 | 1060 | |
| 748 | 1061 | "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], |
| 749 | 1062 | |
| 750 | 1063 | "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], |
| 751 | 1064 | |
| 1065 | + "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], | |
| 1066 | + | |
| 1067 | + "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], | |
| 1068 | + | |
| 1069 | + "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], | |
| 1070 | + | |
| 1071 | + "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], | |
| 1072 | + | |
| 1073 | + "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], | |
| 1074 | + | |
| 752 | 1075 | "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], |
| 753 | 1076 | |
| 754 | 1077 | "mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="], |
| @@ -757,6 +1080,10 @@ · 2 unmodified lines | ||
| 757 | 1080 | |
| 758 | 1081 | "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], |
| 759 | 1082 | |
| 1083 | + "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], | |
| 1084 | + | |
| 1085 | + "minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], | |
| 1086 | + | |
| 760 | 1087 | "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], |
| 761 | 1088 | |
| 762 | 1089 | "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], |
| @@ -779,6 +1106,8 @@ · 16 unmodified lines | ||
| 779 | 1106 | |
| 780 | 1107 | "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="], |
| 781 | 1108 | |
| 1109 | + "node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="], | |
| 1110 | + | |
| 782 | 1111 | "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], |
| 783 | 1112 | |
| 784 | 1113 | "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], |
| @@ -787,8 +1116,16 @@ · 2 unmodified lines | ||
| 787 | 1116 | |
| 788 | 1117 | "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], |
| 789 | 1118 | |
| 1119 | + "onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], | |
| 1120 | + | |
| 1121 | + "oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="], | |
| 1122 | + | |
| 1123 | + "oniguruma-to-es": ["oniguruma-to-es@4.3.4", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA=="], | |
| 1124 | + | |
| 790 | 1125 | "open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], |
| 791 | 1126 | |
| 1127 | + "ora": ["ora@8.2.0", "", { "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^5.0.0", "cli-spinners": "^2.9.2", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.0.0", "log-symbols": "^6.0.0", "stdin-discarder": "^0.2.2", "string-width": "^7.2.0", "strip-ansi": "^7.1.0" } }, "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw=="], | |
| 1128 | + | |
| 792 | 1129 | "oxc-resolver": ["oxc-resolver@11.19.1", "", { "optionalDependencies": { "@oxc-resolver/binding-android-arm-eabi": "11.19.1", "@oxc-resolver/binding-android-arm64": "11.19.1", "@oxc-resolver/binding-darwin-arm64": "11.19.1", "@oxc-resolver/binding-darwin-x64": "11.19.1", "@oxc-resolver/binding-freebsd-x64": "11.19.1", "@oxc-resolver/binding-linux-arm-gnueabihf": "11.19.1", "@oxc-resolver/binding-linux-arm-musleabihf": "11.19.1", "@oxc-resolver/binding-linux-arm64-gnu": "11.19.1", "@oxc-resolver/binding-linux-arm64-musl": "11.19.1", "@oxc-resolver/binding-linux-ppc64-gnu": "11.19.1", "@oxc-resolver/binding-linux-riscv64-gnu": "11.19.1", "@oxc-resolver/binding-linux-riscv64-musl": "11.19.1", "@oxc-resolver/binding-linux-s390x-gnu": "11.19.1", "@oxc-resolver/binding-linux-x64-gnu": "11.19.1", "@oxc-resolver/binding-linux-x64-musl": "11.19.1", "@oxc-resolver/binding-openharmony-arm64": "11.19.1", "@oxc-resolver/binding-wasm32-wasi": "11.19.1", "@oxc-resolver/binding-win32-arm64-msvc": "11.19.1", "@oxc-resolver/binding-win32-ia32-msvc": "11.19.1", "@oxc-resolver/binding-win32-x64-msvc": "11.19.1" } }, "sha512-qE/CIg/spwrTBFt5aKmwe3ifeDdLfA2NESN30E42X/lII5ClF8V7Wt6WIJhcGZjp0/Q+nQ+9vgxGk//xZNX2hg=="], |
| 793 | 1130 | |
| 794 | 1131 | "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], |
| @@ -813,6 +1150,10 @@ · 18 unmodified lines | ||
| 813 | 1150 | |
| 814 | 1151 | "process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="], |
| 815 | 1152 | |
| 1153 | + "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], | |
| 1154 | + | |
| 1155 | + "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], | |
| 1156 | + | |
| 816 | 1157 | "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], |
| 817 | 1158 | |
| 818 | 1159 | "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], |
| @@ -825,10 +1166,26 @@ · 6 unmodified lines | ||
| 825 | 1166 | |
| 826 | 1167 | "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], |
| 827 | 1168 | |
| 1169 | + "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], | |
| 1170 | + | |
| 1171 | + "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], | |
| 1172 | + | |
| 1173 | + "react-grab": ["react-grab@0.1.26", "", { "dependencies": { "@medv/finder": "^4.0.2", "@react-grab/cli": "0.1.26", "bippy": "^0.5.32", "solid-js": "^1.9.10" }, "peerDependencies": { "react": ">=17.0.0" }, "optionalPeers": ["react"], "bin": { "react-grab": "bin/cli.js" } }, "sha512-ubpS3jvxuO47J3/GGYYXVQ0L/EfIojpLg8a+2Ii+8doGJmnZw0nw2Ez1cGpNaZeSHbmY4+8qADWoIFOMWD008A=="], | |
| 1174 | + | |
| 1175 | + "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], | |
| 1176 | + | |
| 828 | 1177 | "readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], |
| 829 | 1178 | |
| 1179 | + "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], | |
| 1180 | + | |
| 1181 | + "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], | |
| 1182 | + | |
| 1183 | + "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="], | |
| 1184 | + | |
| 830 | 1185 | "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], |
| 831 | 1186 | |
| 1187 | + "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], | |
| 1188 | + | |
| 832 | 1189 | "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], |
| 833 | 1190 | |
| 834 | 1191 | "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], |
| @@ -845,10 +1202,16 @@ · 10 unmodified lines | ||
| 845 | 1202 | |
| 846 | 1203 | "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], |
| 847 | 1204 | |
| 848 | − "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], | |
| 1205 | + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], | |
| 1206 | + | |
| 1207 | + "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], | |
| 849 | 1208 | |
| 850 | 1209 | "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], |
| 851 | 1210 | |
| 1211 | + "seroval": ["seroval@1.5.0", "", {}, "sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw=="], | |
| 1212 | + | |
| 1213 | + "seroval-plugins": ["seroval-plugins@1.5.0", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-EAHqADIQondwRZIdeW2I636zgsODzoBDwb3PT/+7TLDWyw1Dy/Xv7iGUIEXXav7usHDE9HVhOU61irI3EnyyHA=="], | |
| 1214 | + | |
| 852 | 1215 | "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], |
| 853 | 1216 | |
| 854 | 1217 | "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], |
| @@ -857,6 +1220,8 @@ · 2 unmodified lines | ||
| 857 | 1220 | |
| 858 | 1221 | "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], |
| 859 | 1222 | |
| 1223 | + "shiki": ["shiki@4.0.1", "", { "dependencies": { "@shikijs/core": "4.0.1", "@shikijs/engine-javascript": "4.0.1", "@shikijs/engine-oniguruma": "4.0.1", "@shikijs/langs": "4.0.1", "@shikijs/themes": "4.0.1", "@shikijs/types": "4.0.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-EkAEhDTN5WhpoQFXFw79OHIrSAfHhlImeCdSyg4u4XvrpxKEmdo/9x/HWSowujAnUrFsGOwWiE58a6GVentMnQ=="], | |
| 1224 | + | |
| 860 | 1225 | "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], |
| 861 | 1226 | |
| 862 | 1227 | "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], |
| @@ -867,10 +1232,18 @@ · 4 unmodified lines | ||
| 867 | 1232 | |
| 868 | 1233 | "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], |
| 869 | 1234 | |
| 1235 | + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], | |
| 1236 | + | |
| 1237 | + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], | |
| 1238 | + | |
| 870 | 1239 | "smol-toml": ["smol-toml@1.6.0", "", {}, "sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw=="], |
| 871 | 1240 | |
| 1241 | + "solid-js": ["solid-js@1.9.11", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", "seroval-plugins": "~1.5.0" } }, "sha512-WEJtcc5mkh/BnHA6Yrg4whlF8g6QwpmXXRg4P2ztPmcKeHHlH4+djYecBLhSpecZY2RRECXYUwIc/C2r3yzQ4Q=="], | |
| 1242 | + | |
| 872 | 1243 | "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], |
| 873 | 1244 | |
| 1245 | + "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], | |
| 1246 | + | |
| 874 | 1247 | "sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], |
| 875 | 1248 | |
| 876 | 1249 | "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], |
| @@ -879,16 +1252,34 @@ · 2 unmodified lines | ||
| 879 | 1252 | |
| 880 | 1253 | "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], |
| 881 | 1254 | |
| 1255 | + "stdin-discarder": ["stdin-discarder@0.2.2", "", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="], | |
| 1256 | + | |
| 1257 | + "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], | |
| 1258 | + | |
| 882 | 1259 | "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], |
| 883 | 1260 | |
| 1261 | + "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], | |
| 1262 | + | |
| 1263 | + "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], | |
| 1264 | + | |
| 884 | 1265 | "strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="], |
| 885 | 1266 | |
| 886 | 1267 | "strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="], |
| 887 | 1268 | |
| 1269 | + "tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="], | |
| 1270 | + | |
| 1271 | + "tailwindcss": ["tailwindcss@4.2.1", "", {}, "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw=="], | |
| 1272 | + | |
| 1273 | + "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], | |
| 1274 | + | |
| 888 | 1275 | "tarn": ["tarn@3.0.2", "", {}, "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ=="], |
| 889 | 1276 | |
| 890 | 1277 | "tedious": ["tedious@18.6.2", "", { "dependencies": { "@azure/core-auth": "^1.7.2", "@azure/identity": "^4.2.1", "@azure/keyvault-keys": "^4.4.0", "@js-joda/core": "^5.6.1", "@types/node": ">=18", "bl": "^6.0.11", "iconv-lite": "^0.6.3", "js-md4": "^0.3.2", "native-duplexpair": "^1.0.0", "sprintf-js": "^1.1.3" } }, "sha512-g7jC56o3MzLkE3lHkaFe2ZdOVFBahq5bsB60/M4NYUbocw/MCrS89IOEQUFr+ba6pb8ZHczZ/VqCyYeYq0xBAg=="], |
| 891 | 1278 | |
| 1279 | + "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], | |
| 1280 | + | |
| 1281 | + "tiny-warning": ["tiny-warning@1.0.3", "", {}, "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="], | |
| 1282 | + | |
| 892 | 1283 | "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], |
| 893 | 1284 | |
| 894 | 1285 | "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], |
| @@ -907,8 +1298,26 @@ · 12 unmodified lines | ||
| 907 | 1298 | |
| 908 | 1299 | "toml": ["toml@3.0.0", "", {}, "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="], |
| 909 | 1300 | |
| 1301 | + "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], | |
| 1302 | + | |
| 910 | 1303 | "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], |
| 911 | 1304 | |
| 1305 | + "turbo": ["turbo@2.8.14", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.14", "turbo-darwin-arm64": "2.8.14", "turbo-linux-64": "2.8.14", "turbo-linux-arm64": "2.8.14", "turbo-windows-64": "2.8.14", "turbo-windows-arm64": "2.8.14" }, "bin": { "turbo": "bin/turbo" } }, "sha512-UCTxeMNYT1cKaHiIFdLCQ7ulI+jw5i5uOnJOrRXsgUD7G3+OjlUjwVd7JfeVt2McWSVGjYA3EVW/v1FSsJ5DtA=="], | |
| 1306 | + | |
| 1307 | + "turbo-darwin-64": ["turbo-darwin-64@2.8.14", "", { "os": "darwin", "cpu": "x64" }, "sha512-9sFi7n2lLfEsGWi5OEoA/eTtQU2BPKtzSYKqufMtDeRmqMT9vKjbv9gJCRkllSVE9BOXA0qXC3diyX8V8rKIKw=="], | |
| 1308 | + | |
| 1309 | + "turbo-darwin-arm64": ["turbo-darwin-arm64@2.8.14", "", { "os": "darwin", "cpu": "arm64" }, "sha512-aS4yJuy6A1PCLws+PJpZP0qCURG8Y5iVx13z/WAbKyeDTY6W6PiGgcEllSaeLGxyn++382ztN/EZH85n2zZ6VQ=="], | |
| 1310 | + | |
| 1311 | + "turbo-linux-64": ["turbo-linux-64@2.8.14", "", { "os": "linux", "cpu": "x64" }, "sha512-XC6wPUDJkakjhNLaS0NrHDMiujRVjH+naEAwvKLArgqRaFkNxjmyNDRM4eu3soMMFmjym6NTxYaF74rvET+Orw=="], | |
| 1312 | + | |
| 1313 | + "turbo-linux-arm64": ["turbo-linux-arm64@2.8.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-ChfE7isyVNjZrVSPDwcfqcHLG/FuIBbOFxnt1FM8vSuBGzHAs8AlTdwFNIxlEMJfZ8Ad9mdMxdmsCUPIWiQ6cg=="], | |
| 1314 | + | |
| 1315 | + "turbo-windows-64": ["turbo-windows-64@2.8.14", "", { "os": "win32", "cpu": "x64" }, "sha512-FTbIeQL1ycLFW2t9uQNMy+bRSzi3Xhwun/e7ZhFBdM+U0VZxxrtfYEBM9CHOejlfqomk6Jh7aRz0sJoqYn39Hg=="], | |
| 1316 | + | |
| 1317 | + "turbo-windows-arm64": ["turbo-windows-arm64@2.8.14", "", { "os": "win32", "cpu": "arm64" }, "sha512-KgZX12cTyhY030qS7ieT8zRkhZZE2VWJasDFVUSVVn17nR7IShpv68/7j5UqJNeRLIGF1XPK0phsP5V5yw3how=="], | |
| 1318 | + | |
| 1319 | + "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], | |
| 1320 | + | |
| 912 | 1321 | "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], |
| 913 | 1322 | |
| 914 | 1323 | "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], |
| @@ -917,12 +1326,30 @@ · 2 unmodified lines | ||
| 917 | 1326 | |
| 918 | 1327 | "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], |
| 919 | 1328 | |
| 1329 | + "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], | |
| 1330 | + | |
| 1331 | + "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], | |
| 1332 | + | |
| 1333 | + "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], | |
| 1334 | + | |
| 1335 | + "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="], | |
| 1336 | + | |
| 1337 | + "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], | |
| 1338 | + | |
| 920 | 1339 | "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], |
| 921 | 1340 | |
| 1341 | + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], | |
| 1342 | + | |
| 1343 | + "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], | |
| 1344 | + | |
| 922 | 1345 | "uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], |
| 923 | 1346 | |
| 924 | 1347 | "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], |
| 925 | 1348 | |
| 1349 | + "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], | |
| 1350 | + | |
| 1351 | + "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], | |
| 1352 | + | |
| 926 | 1353 | "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], |
| 927 | 1354 | |
| 928 | 1355 | "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], |
| @@ -941,14 +1368,38 @@ · 12 unmodified lines | ||
| 941 | 1368 | |
| 942 | 1369 | "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], |
| 943 | 1370 | |
| 1371 | + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], | |
| 1372 | + | |
| 944 | 1373 | "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], |
| 945 | 1374 | |
| 946 | 1375 | "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], |
| 947 | 1376 | |
| 948 | 1377 | "zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], |
| 949 | 1378 | |
| 1379 | + "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], | |
| 1380 | + | |
| 950 | 1381 | "@azure/msal-node/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], |
| 951 | 1382 | |
| 1383 | + "@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], | |
| 1384 | + | |
| 1385 | + "@react-grab/cli/commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], | |
| 1386 | + | |
| 1387 | + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], | |
| 1388 | + | |
| 1389 | + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], | |
| 1390 | + | |
| 1391 | + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], | |
| 1392 | + | |
| 1393 | + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], | |
| 1394 | + | |
| 1395 | + "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], | |
| 1396 | + | |
| 1397 | + "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], | |
| 1398 | + | |
| 1399 | + "jsonwebtoken/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], | |
| 1400 | + | |
| 1401 | + "log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="], | |
| 1402 | + | |
| 952 | 1403 | "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], |
| 953 | 1404 | |
| 954 | 1405 | "tedious/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], |
| @@ -19,12 +19,14 @@ · 18 unmodified lines | ||
| 19 | 19 | "zod": "^4.3.6" |
| 20 | 20 | }, |
| 21 | 21 | "scripts": { |
| 22 | + "dev": "turbo run dev --filter=@executor-v3/web", | |
| 22 | 23 | "executor": "./apps/executor/bin/executor", |
| 23 | 24 | "typecheck": "bun run --filter='*' typecheck", |
| 24 | 25 | "knip": "knip" |
| 25 | 26 | }, |
| 26 | 27 | "devDependencies": { |
| 27 | 28 | "knip": "^5.85.0", |
| 29 | + "turbo": "^2.5.6", | |
| 28 | 30 | "typescript": "^5.9.3" |
| 29 | 31 | } |
| 30 | 32 | } |
| @@ -21,8 +21,8 @@ const catalog: ToolCatalog = { · 20 unmodified lines | ||
| 21 | 21 | path: asToolPath("source.src_api.github.issues.list"), |
| 22 | 22 | sourceKey: "source.src_api", |
| 23 | 23 | description: "Hydrated metadata for selected path", |
| 24 | − inputHint: "object", | |
| 25 | − outputHint: "object", | |
| 24 | + inputType: "object", | |
| 25 | + outputType: "object", | |
| 26 | 26 | }, |
| 27 | 27 | ].filter((tool) => !namespace || tool.path.startsWith(`source.${namespace}.`))); |
| 28 | 28 | }, |
| @@ -31,8 +31,8 @@ const catalog: ToolCatalog = { · 2 unmodified lines | ||
| 31 | 31 | path, |
| 32 | 32 | sourceKey: "source.src_api", |
| 33 | 33 | description: "Hydrated metadata for selected path", |
| 34 | − inputHint: "object", | |
| 35 | − outputHint: "object", | |
| 34 | + inputType: "object", | |
| 35 | + outputType: "object", | |
| 36 | 36 | }); |
| 37 | 37 | }, |
| 38 | 38 | searchTools({ limit }) { |
| @@ -24,8 +24,8 @@ const searchableTextForTool = (tool: ToolDescriptor): string => · 23 unmodified lines | ||
| 24 | 24 | tool.path, |
| 25 | 25 | tool.sourceKey, |
| 26 | 26 | tool.description ?? "", |
| 27 | − tool.inputHint ?? "", | |
| 28 | − tool.outputHint ?? "", | |
| 27 | + tool.inputType ?? "", | |
| 28 | + tool.outputType ?? "", | |
| 29 | 29 | ] |
| 30 | 30 | .join(" ") |
| 31 | 31 | .toLowerCase(); |
| @@ -44,7 +44,6 @@ const projectDescriptor = (input: { · 12 unmodified lines | ||
| 44 | 44 | ...descriptor, |
| 45 | 45 | inputSchemaJson: undefined, |
| 46 | 46 | outputSchemaJson: undefined, |
| 47 | − refHintKeys: undefined, | |
| 48 | 47 | }; |
| 49 | 48 | }; |
| 50 | 49 | |
| @@ -229,13 +228,12 @@ export function createDiscoveryPrimitivesFromToolCatalog(input: { · 178 unmodified lines | ||
| 229 | 228 | score: hit.score, |
| 230 | 229 | description: descriptor.description, |
| 231 | 230 | interaction: descriptor.interaction ?? "auto", |
| 232 | − inputHint: descriptor.inputHint, | |
| 233 | − outputHint: descriptor.outputHint, | |
| 231 | + inputType: descriptor.inputType, | |
| 232 | + outputType: descriptor.outputType, | |
| 234 | 233 | ...(includeSchemas |
| 235 | 234 | ? { |
| 236 | 235 | inputSchemaJson: descriptor.inputSchemaJson, |
| 237 | 236 | outputSchemaJson: descriptor.outputSchemaJson, |
| 238 | − refHintKeys: descriptor.refHintKeys, | |
| 239 | 237 | } |
| 240 | 238 | : {}), |
| 241 | 239 | }; |
| @@ -287,16 +287,16 @@ describe("codemode-core", () => { · 286 unmodified lines | ||
| 287 | 287 | path: asToolPath("source.docs.search"), |
| 288 | 288 | sourceKey: "source.docs", |
| 289 | 289 | description: "Search docs", |
| 290 | − inputHint: "object", | |
| 291 | − outputHint: "object", | |
| 290 | + inputType: "object", | |
| 291 | + outputType: "object", | |
| 292 | 292 | }, |
| 293 | 293 | "source.issues.create": { |
| 294 | 294 | path: asToolPath("source.issues.create"), |
| 295 | 295 | sourceKey: "source.issues", |
| 296 | 296 | description: "Create issue", |
| 297 | 297 | interaction: "required", |
| 298 | − inputHint: "object", | |
| 299 | − outputHint: "object", | |
| 298 | + inputType: "object", | |
| 299 | + outputType: "object", | |
| 300 | 300 | }, |
| 301 | 301 | }; |
| 302 | 302 | |
| @@ -327,7 +327,6 @@ describe("codemode-core", () => { · 24 unmodified lines | ||
| 327 | 327 | ...descriptor, |
| 328 | 328 | inputSchemaJson: includeSchemas ? descriptor.inputSchemaJson : undefined, |
| 329 | 329 | outputSchemaJson: includeSchemas ? descriptor.outputSchemaJson : undefined, |
| 330 | − refHintKeys: includeSchemas ? descriptor.refHintKeys : undefined, | |
| 331 | 330 | })), |
| 332 | 331 | ), |
| 333 | 332 | getToolByPath: ({ path }) => |
| @@ -1,5 +1,6 @@ | ||
| 1 | 1 | export * from "./types"; |
| 2 | 2 | export * from "./json-schema"; |
| 3 | +export * from "./schema-types"; | |
| 3 | 4 | export * from "./tool-map"; |
| 4 | 5 | export * from "./discovery"; |
| 5 | 6 | export * from "./system-tools"; |
| @@ -0,0 +1,125 @@ | ||
| 1 | +type JsonSchemaRecord = Record<string, unknown>; | |
| 2 | + | |
| 3 | +const asRecord = (value: unknown): JsonSchemaRecord => | |
| 4 | + typeof value === "object" && value !== null && !Array.isArray(value) | |
| 5 | + ? (value as JsonSchemaRecord) | |
| 6 | + : {}; | |
| 7 | + | |
| 8 | +const asStringArray = (value: unknown): Array<string> => | |
| 9 | + Array.isArray(value) | |
| 10 | + ? value.filter((item): item is string => typeof item === "string") | |
| 11 | + : []; | |
| 12 | + | |
| 13 | +const truncate = (value: string, maxLength: number): string => | |
| 14 | + value.length <= maxLength | |
| 15 | + ? value | |
| 16 | + : `${value.slice(0, Math.max(0, maxLength - 4))} ...`; | |
| 17 | + | |
| 18 | +const propertyLabel = ( | |
| 19 | + name: string, | |
| 20 | + schema: JsonSchemaRecord, | |
| 21 | + optional: boolean, | |
| 22 | + maxLength: number, | |
| 23 | +): string => | |
| 24 | + `${name}${optional ? "?" : ""}: ${schemaToTypeSignature(schema, maxLength)}`; | |
| 25 | + | |
| 26 | +const compositeLabel = ( | |
| 27 | + key: "oneOf" | "anyOf" | "allOf", | |
| 28 | + schema: JsonSchemaRecord, | |
| 29 | + maxLength: number, | |
| 30 | +): string | null => { | |
| 31 | + const items = Array.isArray(schema[key]) ? schema[key].map(asRecord) : []; | |
| 32 | + if (items.length === 0) { | |
| 33 | + return null; | |
| 34 | + } | |
| 35 | + | |
| 36 | + const labels = items | |
| 37 | + .map((item) => schemaToTypeSignature(item, maxLength)) | |
| 38 | + .filter((label) => label.length > 0); | |
| 39 | + | |
| 40 | + if (labels.length === 0) { | |
| 41 | + return null; | |
| 42 | + } | |
| 43 | + | |
| 44 | + return labels.join(key === "allOf" ? " & " : " | "); | |
| 45 | +}; | |
| 46 | + | |
| 47 | +export const schemaToTypeSignature = ( | |
| 48 | + input: unknown, | |
| 49 | + maxLength: number = 220, | |
| 50 | +): string => { | |
| 51 | + const schema = asRecord(input); | |
| 52 | + | |
| 53 | + if (typeof schema.$ref === "string") { | |
| 54 | + const ref = schema.$ref.trim(); | |
| 55 | + return ref.length > 0 ? ref.split("/").at(-1) ?? ref : "unknown"; | |
| 56 | + } | |
| 57 | + | |
| 58 | + if ("const" in schema) { | |
| 59 | + return JSON.stringify(schema.const); | |
| 60 | + } | |
| 61 | + | |
| 62 | + const enumValues = Array.isArray(schema.enum) ? schema.enum : []; | |
| 63 | + if (enumValues.length > 0) { | |
| 64 | + return truncate( | |
| 65 | + enumValues.map((value) => JSON.stringify(value)).join(" | "), | |
| 66 | + maxLength, | |
| 67 | + ); | |
| 68 | + } | |
| 69 | + | |
| 70 | + const composite = | |
| 71 | + compositeLabel("oneOf", schema, maxLength) | |
| 72 | + ?? compositeLabel("anyOf", schema, maxLength) | |
| 73 | + ?? compositeLabel("allOf", schema, maxLength); | |
| 74 | + if (composite) { | |
| 75 | + return truncate(composite, maxLength); | |
| 76 | + } | |
| 77 | + | |
| 78 | + if (schema.type === "array") { | |
| 79 | + const itemLabel = schema.items | |
| 80 | + ? schemaToTypeSignature(schema.items, maxLength) | |
| 81 | + : "unknown"; | |
| 82 | + return truncate(`${itemLabel}[]`, maxLength); | |
| 83 | + } | |
| 84 | + | |
| 85 | + if (schema.type === "object" || schema.properties) { | |
| 86 | + const properties = asRecord(schema.properties); | |
| 87 | + const keys = Object.keys(properties); | |
| 88 | + if (keys.length === 0) { | |
| 89 | + return schema.additionalProperties ? "Record<string, unknown>" : "object"; | |
| 90 | + } | |
| 91 | + | |
| 92 | + const required = new Set(asStringArray(schema.required)); | |
| 93 | + const parts = keys.map((key) => | |
| 94 | + propertyLabel(key, asRecord(properties[key]), !required.has(key), maxLength) | |
| 95 | + ); | |
| 96 | + | |
| 97 | + return truncate(`{ ${parts.join(", ")} }`, maxLength); | |
| 98 | + } | |
| 99 | + | |
| 100 | + if (Array.isArray(schema.type)) { | |
| 101 | + return truncate(schema.type.join(" | "), maxLength); | |
| 102 | + } | |
| 103 | + | |
| 104 | + if (typeof schema.type === "string") { | |
| 105 | + return schema.type; | |
| 106 | + } | |
| 107 | + | |
| 108 | + return "unknown"; | |
| 109 | +}; | |
| 110 | + | |
| 111 | +export const typeSignatureFromSchemaJson = ( | |
| 112 | + schemaJson: string | undefined, | |
| 113 | + fallback: string, | |
| 114 | + maxLength: number = 220, | |
| 115 | +): string => { | |
| 116 | + if (!schemaJson) { | |
| 117 | + return fallback; | |
| 118 | + } | |
| 119 | + | |
| 120 | + try { | |
| 121 | + return schemaToTypeSignature(JSON.parse(schemaJson) as unknown, maxLength); | |
| 122 | + } catch { | |
| 123 | + return fallback; | |
| 124 | + } | |
| 125 | +}; | |
| @@ -47,11 +47,14 @@ const catalogToolsOutputSchema = Schema.standardSchemaV1( · 46 unmodified lines | ||
| 47 | 47 | sourceKey: Schema.String, |
| 48 | 48 | description: Schema.optional(Schema.String), |
| 49 | 49 | interaction: Schema.optional(Schema.String), |
| 50 | − inputHint: Schema.optional(Schema.String), | |
| 51 | − outputHint: Schema.optional(Schema.String), | |
| 50 | + inputType: Schema.optional(Schema.String), | |
| 51 | + outputType: Schema.optional(Schema.String), | |
| 52 | 52 | inputSchemaJson: Schema.optional(Schema.String), |
| 53 | 53 | outputSchemaJson: Schema.optional(Schema.String), |
| 54 | − refHintKeys: Schema.optional(Schema.Array(Schema.String)), | |
| 54 | + exampleInputJson: Schema.optional(Schema.String), | |
| 55 | + exampleOutputJson: Schema.optional(Schema.String), | |
| 56 | + providerKind: Schema.optional(Schema.String), | |
| 57 | + providerDataJson: Schema.optional(Schema.String), | |
| 55 | 58 | }), |
| 56 | 59 | ), |
| 57 | 60 | }), |
| @@ -81,11 +84,10 @@ const discoverResultItemSchema = Schema.Struct({ · 23 unmodified lines | ||
| 81 | 84 | score: Schema.Number, |
| 82 | 85 | description: Schema.optional(Schema.String), |
| 83 | 86 | interaction: Schema.optional(Schema.String), |
| 84 | − inputHint: Schema.optional(Schema.String), | |
| 85 | − outputHint: Schema.optional(Schema.String), | |
| 87 | + inputType: Schema.optional(Schema.String), | |
| 88 | + outputType: Schema.optional(Schema.String), | |
| 86 | 89 | inputSchemaJson: Schema.optional(Schema.String), |
| 87 | 90 | outputSchemaJson: Schema.optional(Schema.String), |
| 88 | − refHintKeys: Schema.optional(Schema.Array(Schema.String)), | |
| 89 | 91 | }); |
| 90 | 92 | |
| 91 | 93 | const discoverOutputSchema = Schema.standardSchemaV1( |
| @@ -2,6 +2,7 @@ import type { StandardSchemaV1 } from "@standard-schema/spec"; · 1 unmodified line | ||
| 2 | 2 | import * as Data from "effect/Data"; |
| 3 | 3 | import * as Effect from "effect/Effect"; |
| 4 | 4 | import * as JSONSchema from "effect/JSONSchema"; |
| 5 | +import { typeSignatureFromSchemaJson } from "./schema-types"; | |
| 5 | 6 | |
| 6 | 7 | import type { |
| 7 | 8 | ElicitationRequest, |
| @@ -355,50 +356,11 @@ const stringifySchema = (value: unknown): string | undefined => { · 347 unmodified lines | ||
| 355 | 356 | } |
| 356 | 357 | }; |
| 357 | 358 | |
| 358 | −const inferHintFromSchemaJson = ( | |
| 359 | +const inferTypeFromSchemaJson = ( | |
| 359 | 360 | schemaJson: string | undefined, |
| 360 | 361 | fallback: string, |
| 361 | −): string => { | |
| 362 | − if (!schemaJson) { | |
| 363 | − return fallback; | |
| 364 | − } | |
| 365 | − | |
| 366 | − try { | |
| 367 | − const parsed = JSON.parse(schemaJson) as Record<string, unknown>; | |
| 368 | − const title = typeof parsed.title === "string" ? parsed.title.trim() : ""; | |
| 369 | − if (title.length > 0) { | |
| 370 | − return title; | |
| 371 | − } | |
| 372 | − | |
| 373 | − if (parsed.type === "object") { | |
| 374 | − const properties = | |
| 375 | − parsed.properties | |
| 376 | − && typeof parsed.properties === "object" | |
| 377 | − && !Array.isArray(parsed.properties) | |
| 378 | − ? Object.keys(parsed.properties as Record<string, unknown>) | |
| 379 | − : []; | |
| 380 | − if (properties.length > 0) { | |
| 381 | − const shown = properties.slice(0, 3).join(", "); | |
| 382 | − return properties.length <= 3 | |
| 383 | − ? `object { ${shown} }` | |
| 384 | − : `object { ${shown}, ... }`; | |
| 385 | − } | |
| 386 | − return "object"; | |
| 387 | − } | |
| 388 | − | |
| 389 | − if (parsed.type === "array") { | |
| 390 | − return "array"; | |
| 391 | − } | |
| 392 | − | |
| 393 | − if (typeof parsed.type === "string") { | |
| 394 | − return parsed.type; | |
| 395 | − } | |
| 396 | − } catch { | |
| 397 | − // Ignore malformed schema and fall back. | |
| 398 | − } | |
| 399 | − | |
| 400 | − return fallback; | |
| 401 | −}; | |
| 362 | + maxLength: number = 240, | |
| 363 | +): string => typeSignatureFromSchemaJson(schemaJson, fallback, maxLength); | |
| 402 | 364 | |
| 403 | 365 | export function createToolsFromRecord(input: { |
| 404 | 366 | tools: Record<string, ExecutableTool>; |
| @@ -470,13 +432,22 @@ export function toolDescriptorsFromTools(input: { · 65 unmodified lines | ||
| 470 | 432 | description: definition.description, |
| 471 | 433 | interaction: metadata?.interaction, |
| 472 | 434 | elicitation: metadata?.elicitation, |
| 473 | − inputHint: | |
| 474 | − metadata?.inputHint ?? inferHintFromSchemaJson(inputSchemaJson, "input"), | |
| 475 | − outputHint: | |
| 476 | − metadata?.outputHint ?? inferHintFromSchemaJson(outputSchemaJson, "output"), | |
| 435 | + inputType: | |
| 436 | + metadata?.inputType ?? inferTypeFromSchemaJson(inputSchemaJson, "unknown"), | |
| 437 | + outputType: | |
| 438 | + metadata?.outputType ?? inferTypeFromSchemaJson(outputSchemaJson, "unknown"), | |
| 477 | 439 | inputSchemaJson, |
| 478 | 440 | outputSchemaJson, |
| 479 | − refHintKeys: metadata?.refHintKeys, | |
| 441 | + ...(metadata?.exampleInputJson | |
| 442 | + ? { exampleInputJson: metadata.exampleInputJson } | |
| 443 | + : {}), | |
| 444 | + ...(metadata?.exampleOutputJson | |
| 445 | + ? { exampleOutputJson: metadata.exampleOutputJson } | |
| 446 | + : {}), | |
| 447 | + ...(metadata?.providerKind ? { providerKind: metadata.providerKind } : {}), | |
| 448 | + ...(metadata?.providerDataJson | |
| 449 | + ? { providerDataJson: metadata.providerDataJson } | |
| 450 | + : {}), | |
| 480 | 451 | } satisfies ToolDescriptor; |
| 481 | 452 | }); |
| 482 | 453 | } |
| @@ -77,12 +77,15 @@ export type OnElicitation = ( · 76 unmodified lines | ||
| 77 | 77 | export type ToolMetadata = { |
| 78 | 78 | interaction?: "auto" | "required"; |
| 79 | 79 | elicitation?: ElicitationRequest; |
| 80 | − inputHint?: string; | |
| 81 | − outputHint?: string; | |
| 80 | + inputType?: string; | |
| 81 | + outputType?: string; | |
| 82 | 82 | inputSchemaJson?: string; |
| 83 | 83 | outputSchemaJson?: string; |
| 84 | − refHintKeys?: readonly string[]; | |
| 84 | + exampleInputJson?: string; | |
| 85 | + exampleOutputJson?: string; | |
| 85 | 86 | sourceKey?: string; |
| 87 | + providerKind?: string; | |
| 88 | + providerDataJson?: string; | |
| 86 | 89 | }; |
| 87 | 90 | |
| 88 | 91 | export type ToolExecutionContext = { |
| @@ -127,11 +130,14 @@ export type ToolDescriptor = { · 38 unmodified lines | ||
| 127 | 130 | description?: string; |
| 128 | 131 | interaction?: "auto" | "required"; |
| 129 | 132 | elicitation?: ElicitationRequest; |
| 130 | − inputHint?: string; | |
| 131 | − outputHint?: string; | |
| 133 | + inputType?: string; | |
| 134 | + outputType?: string; | |
| 132 | 135 | inputSchemaJson?: string; |
| 133 | 136 | outputSchemaJson?: string; |
| 134 | − refHintKeys?: readonly string[]; | |
| 137 | + exampleInputJson?: string; | |
| 138 | + exampleOutputJson?: string; | |
| 139 | + providerKind?: string; | |
| 140 | + providerDataJson?: string; | |
| 135 | 141 | }; |
| 136 | 142 | |
| 137 | 143 | export type ToolNamespace = { |
| @@ -0,0 +1 @@ | ||
| 1 | +target/ | |
| @@ -0,0 +1,445 @@ | ||
| 1 | +# This file is automatically @generated by Cargo. | |
| 2 | +# It is not intended for manual editing. | |
| 3 | +version = 4 | |
| 4 | + | |
| 5 | +[[package]] | |
| 6 | +name = "anstream" | |
| 7 | +version = "0.6.21" | |
| 8 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 9 | +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" | |
| 10 | +dependencies = [ | |
| 11 | + "anstyle", | |
| 12 | + "anstyle-parse", | |
| 13 | + "anstyle-query", | |
| 14 | + "anstyle-wincon", | |
| 15 | + "colorchoice", | |
| 16 | + "is_terminal_polyfill", | |
| 17 | + "utf8parse", | |
| 18 | +] | |
| 19 | + | |
| 20 | +[[package]] | |
| 21 | +name = "anstyle" | |
| 22 | +version = "1.0.13" | |
| 23 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 24 | +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" | |
| 25 | + | |
| 26 | +[[package]] | |
| 27 | +name = "anstyle-parse" | |
| 28 | +version = "0.2.7" | |
| 29 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 30 | +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" | |
| 31 | +dependencies = [ | |
| 32 | + "utf8parse", | |
| 33 | +] | |
| 34 | + | |
| 35 | +[[package]] | |
| 36 | +name = "anstyle-query" | |
| 37 | +version = "1.1.5" | |
| 38 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 39 | +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" | |
| 40 | +dependencies = [ | |
| 41 | + "windows-sys", | |
| 42 | +] | |
| 43 | + | |
| 44 | +[[package]] | |
| 45 | +name = "anstyle-wincon" | |
| 46 | +version = "3.0.11" | |
| 47 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 48 | +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" | |
| 49 | +dependencies = [ | |
| 50 | + "anstyle", | |
| 51 | + "once_cell_polyfill", | |
| 52 | + "windows-sys", | |
| 53 | +] | |
| 54 | + | |
| 55 | +[[package]] | |
| 56 | +name = "block-buffer" | |
| 57 | +version = "0.10.4" | |
| 58 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 59 | +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" | |
| 60 | +dependencies = [ | |
| 61 | + "generic-array", | |
| 62 | +] | |
| 63 | + | |
| 64 | +[[package]] | |
| 65 | +name = "bumpalo" | |
| 66 | +version = "3.20.2" | |
| 67 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 68 | +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" | |
| 69 | + | |
| 70 | +[[package]] | |
| 71 | +name = "cfg-if" | |
| 72 | +version = "1.0.4" | |
| 73 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 74 | +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" | |
| 75 | + | |
| 76 | +[[package]] | |
| 77 | +name = "clap" | |
| 78 | +version = "4.5.60" | |
| 79 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 80 | +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" | |
| 81 | +dependencies = [ | |
| 82 | + "clap_builder", | |
| 83 | + "clap_derive", | |
| 84 | +] | |
| 85 | + | |
| 86 | +[[package]] | |
| 87 | +name = "clap_builder" | |
| 88 | +version = "4.5.60" | |
| 89 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 90 | +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" | |
| 91 | +dependencies = [ | |
| 92 | + "anstream", | |
| 93 | + "anstyle", | |
| 94 | + "clap_lex", | |
| 95 | + "strsim", | |
| 96 | +] | |
| 97 | + | |
| 98 | +[[package]] | |
| 99 | +name = "clap_derive" | |
| 100 | +version = "4.5.55" | |
| 101 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 102 | +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" | |
| 103 | +dependencies = [ | |
| 104 | + "heck", | |
| 105 | + "proc-macro2", | |
| 106 | + "quote", | |
| 107 | + "syn", | |
| 108 | +] | |
| 109 | + | |
| 110 | +[[package]] | |
| 111 | +name = "clap_lex" | |
| 112 | +version = "1.0.0" | |
| 113 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 114 | +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" | |
| 115 | + | |
| 116 | +[[package]] | |
| 117 | +name = "colorchoice" | |
| 118 | +version = "1.0.4" | |
| 119 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 120 | +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" | |
| 121 | + | |
| 122 | +[[package]] | |
| 123 | +name = "cpufeatures" | |
| 124 | +version = "0.2.17" | |
| 125 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 126 | +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" | |
| 127 | +dependencies = [ | |
| 128 | + "libc", | |
| 129 | +] | |
| 130 | + | |
| 131 | +[[package]] | |
| 132 | +name = "crypto-common" | |
| 133 | +version = "0.1.7" | |
| 134 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 135 | +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" | |
| 136 | +dependencies = [ | |
| 137 | + "generic-array", | |
| 138 | + "typenum", | |
| 139 | +] | |
| 140 | + | |
| 141 | +[[package]] | |
| 142 | +name = "digest" | |
| 143 | +version = "0.10.7" | |
| 144 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 145 | +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" | |
| 146 | +dependencies = [ | |
| 147 | + "block-buffer", | |
| 148 | + "crypto-common", | |
| 149 | +] | |
| 150 | + | |
| 151 | +[[package]] | |
| 152 | +name = "equivalent" | |
| 153 | +version = "1.0.2" | |
| 154 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 155 | +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" | |
| 156 | + | |
| 157 | +[[package]] | |
| 158 | +name = "generic-array" | |
| 159 | +version = "0.14.7" | |
| 160 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 161 | +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" | |
| 162 | +dependencies = [ | |
| 163 | + "typenum", | |
| 164 | + "version_check", | |
| 165 | +] | |
| 166 | + | |
| 167 | +[[package]] | |
| 168 | +name = "hashbrown" | |
| 169 | +version = "0.16.1" | |
| 170 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 171 | +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" | |
| 172 | + | |
| 173 | +[[package]] | |
| 174 | +name = "heck" | |
| 175 | +version = "0.5.0" | |
| 176 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 177 | +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" | |
| 178 | + | |
| 179 | +[[package]] | |
| 180 | +name = "indexmap" | |
| 181 | +version = "2.13.0" | |
| 182 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 183 | +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" | |
| 184 | +dependencies = [ | |
| 185 | + "equivalent", | |
| 186 | + "hashbrown", | |
| 187 | +] | |
| 188 | + | |
| 189 | +[[package]] | |
| 190 | +name = "is_terminal_polyfill" | |
| 191 | +version = "1.70.2" | |
| 192 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 193 | +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" | |
| 194 | + | |
| 195 | +[[package]] | |
| 196 | +name = "itoa" | |
| 197 | +version = "1.0.17" | |
| 198 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 199 | +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" | |
| 200 | + | |
| 201 | +[[package]] | |
| 202 | +name = "libc" | |
| 203 | +version = "0.2.182" | |
| 204 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 205 | +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" | |
| 206 | + | |
| 207 | +[[package]] | |
| 208 | +name = "memchr" | |
| 209 | +version = "2.8.0" | |
| 210 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 211 | +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" | |
| 212 | + | |
| 213 | +[[package]] | |
| 214 | +name = "once_cell" | |
| 215 | +version = "1.21.3" | |
| 216 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 217 | +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" | |
| 218 | + | |
| 219 | +[[package]] | |
| 220 | +name = "once_cell_polyfill" | |
| 221 | +version = "1.70.2" | |
| 222 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 223 | +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" | |
| 224 | + | |
| 225 | +[[package]] | |
| 226 | +name = "openapi-extractor-rs" | |
| 227 | +version = "0.1.0" | |
| 228 | +dependencies = [ | |
| 229 | + "clap", | |
| 230 | + "serde", | |
| 231 | + "serde_json", | |
| 232 | + "serde_yaml", | |
| 233 | + "sha2", | |
| 234 | + "wasm-bindgen", | |
| 235 | +] | |
| 236 | + | |
| 237 | +[[package]] | |
| 238 | +name = "proc-macro2" | |
| 239 | +version = "1.0.106" | |
| 240 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 241 | +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" | |
| 242 | +dependencies = [ | |
| 243 | + "unicode-ident", | |
| 244 | +] | |
| 245 | + | |
| 246 | +[[package]] | |
| 247 | +name = "quote" | |
| 248 | +version = "1.0.44" | |
| 249 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 250 | +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" | |
| 251 | +dependencies = [ | |
| 252 | + "proc-macro2", | |
| 253 | +] | |
| 254 | + | |
| 255 | +[[package]] | |
| 256 | +name = "rustversion" | |
| 257 | +version = "1.0.22" | |
| 258 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 259 | +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" | |
| 260 | + | |
| 261 | +[[package]] | |
| 262 | +name = "ryu" | |
| 263 | +version = "1.0.23" | |
| 264 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 265 | +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" | |
| 266 | + | |
| 267 | +[[package]] | |
| 268 | +name = "serde" | |
| 269 | +version = "1.0.228" | |
| 270 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 271 | +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" | |
| 272 | +dependencies = [ | |
| 273 | + "serde_core", | |
| 274 | + "serde_derive", | |
| 275 | +] | |
| 276 | + | |
| 277 | +[[package]] | |
| 278 | +name = "serde_core" | |
| 279 | +version = "1.0.228" | |
| 280 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 281 | +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" | |
| 282 | +dependencies = [ | |
| 283 | + "serde_derive", | |
| 284 | +] | |
| 285 | + | |
| 286 | +[[package]] | |
| 287 | +name = "serde_derive" | |
| 288 | +version = "1.0.228" | |
| 289 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 290 | +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" | |
| 291 | +dependencies = [ | |
| 292 | + "proc-macro2", | |
| 293 | + "quote", | |
| 294 | + "syn", | |
| 295 | +] | |
| 296 | + | |
| 297 | +[[package]] | |
| 298 | +name = "serde_json" | |
| 299 | +version = "1.0.149" | |
| 300 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 301 | +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" | |
| 302 | +dependencies = [ | |
| 303 | + "itoa", | |
| 304 | + "memchr", | |
| 305 | + "serde", | |
| 306 | + "serde_core", | |
| 307 | + "zmij", | |
| 308 | +] | |
| 309 | + | |
| 310 | +[[package]] | |
| 311 | +name = "serde_yaml" | |
| 312 | +version = "0.9.34+deprecated" | |
| 313 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 314 | +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" | |
| 315 | +dependencies = [ | |
| 316 | + "indexmap", | |
| 317 | + "itoa", | |
| 318 | + "ryu", | |
| 319 | + "serde", | |
| 320 | + "unsafe-libyaml", | |
| 321 | +] | |
| 322 | + | |
| 323 | +[[package]] | |
| 324 | +name = "sha2" | |
| 325 | +version = "0.10.9" | |
| 326 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 327 | +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" | |
| 328 | +dependencies = [ | |
| 329 | + "cfg-if", | |
| 330 | + "cpufeatures", | |
| 331 | + "digest", | |
| 332 | +] | |
| 333 | + | |
| 334 | +[[package]] | |
| 335 | +name = "strsim" | |
| 336 | +version = "0.11.1" | |
| 337 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 338 | +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" | |
| 339 | + | |
| 340 | +[[package]] | |
| 341 | +name = "syn" | |
| 342 | +version = "2.0.117" | |
| 343 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 344 | +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" | |
| 345 | +dependencies = [ | |
| 346 | + "proc-macro2", | |
| 347 | + "quote", | |
| 348 | + "unicode-ident", | |
| 349 | +] | |
| 350 | + | |
| 351 | +[[package]] | |
| 352 | +name = "typenum" | |
| 353 | +version = "1.19.0" | |
| 354 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 355 | +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" | |
| 356 | + | |
| 357 | +[[package]] | |
| 358 | +name = "unicode-ident" | |
| 359 | +version = "1.0.24" | |
| 360 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 361 | +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" | |
| 362 | + | |
| 363 | +[[package]] | |
| 364 | +name = "unsafe-libyaml" | |
| 365 | +version = "0.2.11" | |
| 366 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 367 | +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" | |
| 368 | + | |
| 369 | +[[package]] | |
| 370 | +name = "utf8parse" | |
| 371 | +version = "0.2.2" | |
| 372 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 373 | +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" | |
| 374 | + | |
| 375 | +[[package]] | |
| 376 | +name = "version_check" | |
| 377 | +version = "0.9.5" | |
| 378 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 379 | +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" | |
| 380 | + | |
| 381 | +[[package]] | |
| 382 | +name = "wasm-bindgen" | |
| 383 | +version = "0.2.114" | |
| 384 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 385 | +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" | |
| 386 | +dependencies = [ | |
| 387 | + "cfg-if", | |
| 388 | + "once_cell", | |
| 389 | + "rustversion", | |
| 390 | + "wasm-bindgen-macro", | |
| 391 | + "wasm-bindgen-shared", | |
| 392 | +] | |
| 393 | + | |
| 394 | +[[package]] | |
| 395 | +name = "wasm-bindgen-macro" | |
| 396 | +version = "0.2.114" | |
| 397 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 398 | +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" | |
| 399 | +dependencies = [ | |
| 400 | + "quote", | |
| 401 | + "wasm-bindgen-macro-support", | |
| 402 | +] | |
| 403 | + | |
| 404 | +[[package]] | |
| 405 | +name = "wasm-bindgen-macro-support" | |
| 406 | +version = "0.2.114" | |
| 407 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 408 | +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" | |
| 409 | +dependencies = [ | |
| 410 | + "bumpalo", | |
| 411 | + "proc-macro2", | |
| 412 | + "quote", | |
| 413 | + "syn", | |
| 414 | + "wasm-bindgen-shared", | |
| 415 | +] | |
| 416 | + | |
| 417 | +[[package]] | |
| 418 | +name = "wasm-bindgen-shared" | |
| 419 | +version = "0.2.114" | |
| 420 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 421 | +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" | |
| 422 | +dependencies = [ | |
| 423 | + "unicode-ident", | |
| 424 | +] | |
| 425 | + | |
| 426 | +[[package]] | |
| 427 | +name = "windows-link" | |
| 428 | +version = "0.2.1" | |
| 429 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 430 | +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" | |
| 431 | + | |
| 432 | +[[package]] | |
| 433 | +name = "windows-sys" | |
| 434 | +version = "0.61.2" | |
| 435 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 436 | +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" | |
| 437 | +dependencies = [ | |
| 438 | + "windows-link", | |
| 439 | +] | |
| 440 | + | |
| 441 | +[[package]] | |
| 442 | +name = "zmij" | |
| 443 | +version = "1.0.21" | |
| 444 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 445 | +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" | |
| @@ -0,0 +1,17 @@ | ||
| 1 | +[package] | |
| 2 | +name = "openapi-extractor-rs" | |
| 3 | +version = "0.1.0" | |
| 4 | +edition = "2024" | |
| 5 | + | |
| 6 | +[lib] | |
| 7 | +crate-type = ["cdylib", "rlib"] | |
| 8 | + | |
| 9 | +[dependencies] | |
| 10 | +clap = { version = "4.5.40", features = ["derive"] } | |
| 11 | +serde = { version = "1.0.228", features = ["derive"] } | |
| 12 | +serde_json = "1.0.145" | |
| 13 | +serde_yaml = "0.9.34" | |
| 14 | +sha2 = "0.10.9" | |
| 15 | + | |
| 16 | +[target.'cfg(target_arch = "wasm32")'.dependencies] | |
| 17 | +wasm-bindgen = "0.2.114" | |
| @@ -0,0 +1,20 @@ | ||
| 1 | +#!/usr/bin/env bash | |
| 2 | +set -euo pipefail | |
| 3 | + | |
| 4 | +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" | |
| 5 | +CRATE_DIR="$ROOT_DIR/openapi-extractor-rs" | |
| 6 | +OUT_DIR="$ROOT_DIR/src/openapi-extractor-wasm" | |
| 7 | + | |
| 8 | +cd "$CRATE_DIR" | |
| 9 | + | |
| 10 | +rustup target add wasm32-unknown-unknown >/dev/null | |
| 11 | + | |
| 12 | +cargo build --release --target wasm32-unknown-unknown | |
| 13 | + | |
| 14 | +wasm-bindgen \ | |
| 15 | + --target web \ | |
| 16 | + --out-dir "$OUT_DIR" \ | |
| 17 | + --out-name openapi_extractor \ | |
| 18 | + "$CRATE_DIR/target/wasm32-unknown-unknown/release/openapi_extractor_rs.wasm" | |
| 19 | + | |
| 20 | +rm -rf "$CRATE_DIR/target" | |
| @@ -0,0 +1,1018 @@ | ||
| 1 | +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque}; | |
| 2 | + | |
| 3 | +use serde::Serialize; | |
| 4 | +use serde_json::{Map, Value, json}; | |
| 5 | +use sha2::{Digest, Sha256}; | |
| 6 | + | |
| 7 | +#[cfg(target_arch = "wasm32")] | |
| 8 | +use wasm_bindgen::prelude::*; | |
| 9 | + | |
| 10 | +const OPEN_API_HTTP_METHODS: [&str; 8] = [ | |
| 11 | + "get", | |
| 12 | + "put", | |
| 13 | + "post", | |
| 14 | + "delete", | |
| 15 | + "patch", | |
| 16 | + "head", | |
| 17 | + "options", | |
| 18 | + "trace", | |
| 19 | +]; | |
| 20 | + | |
| 21 | +const OPEN_API_PARAMETER_LOCATIONS: [&str; 4] = ["path", "query", "header", "cookie"]; | |
| 22 | + | |
| 23 | +#[derive(Serialize, Clone)] | |
| 24 | +struct OpenApiToolParameter { | |
| 25 | + name: String, | |
| 26 | + location: String, | |
| 27 | + required: bool, | |
| 28 | +} | |
| 29 | + | |
| 30 | +#[derive(Serialize, Clone)] | |
| 31 | +#[serde(rename_all = "camelCase")] | |
| 32 | +struct OpenApiToolRequestBody { | |
| 33 | + required: bool, | |
| 34 | + content_types: Vec<String>, | |
| 35 | +} | |
| 36 | + | |
| 37 | +#[derive(Serialize, Clone)] | |
| 38 | +#[serde(rename_all = "camelCase")] | |
| 39 | +struct OpenApiInvocationPayload { | |
| 40 | + method: String, | |
| 41 | + path_template: String, | |
| 42 | + parameters: Vec<OpenApiToolParameter>, | |
| 43 | + request_body: Option<OpenApiToolRequestBody>, | |
| 44 | +} | |
| 45 | + | |
| 46 | +#[derive(Serialize, Clone)] | |
| 47 | +#[serde(rename_all = "camelCase")] | |
| 48 | +struct DiscoveryTypingPayload { | |
| 49 | + #[serde(skip_serializing_if = "Option::is_none")] | |
| 50 | + input_schema_json: Option<String>, | |
| 51 | + #[serde(skip_serializing_if = "Option::is_none")] | |
| 52 | + output_schema_json: Option<String>, | |
| 53 | + #[serde(skip_serializing_if = "Option::is_none")] | |
| 54 | + ref_hint_keys: Option<Vec<String>>, | |
| 55 | +} | |
| 56 | + | |
| 57 | +#[derive(Serialize, Clone)] | |
| 58 | +#[serde(rename_all = "camelCase")] | |
| 59 | +struct OpenApiExample { | |
| 60 | + value_json: String, | |
| 61 | + #[serde(skip_serializing_if = "Option::is_none")] | |
| 62 | + media_type: Option<String>, | |
| 63 | + #[serde(skip_serializing_if = "Option::is_none")] | |
| 64 | + label: Option<String>, | |
| 65 | +} | |
| 66 | + | |
| 67 | +#[derive(Serialize, Clone)] | |
| 68 | +#[serde(rename_all = "camelCase")] | |
| 69 | +struct OpenApiParameterDocumentation { | |
| 70 | + name: String, | |
| 71 | + location: String, | |
| 72 | + required: bool, | |
| 73 | + #[serde(skip_serializing_if = "Option::is_none")] | |
| 74 | + description: Option<String>, | |
| 75 | + #[serde(skip_serializing_if = "Option::is_none")] | |
| 76 | + examples: Option<Vec<OpenApiExample>>, | |
| 77 | +} | |
| 78 | + | |
| 79 | +#[derive(Serialize, Clone)] | |
| 80 | +#[serde(rename_all = "camelCase")] | |
| 81 | +struct OpenApiRequestBodyDocumentation { | |
| 82 | + #[serde(skip_serializing_if = "Option::is_none")] | |
| 83 | + description: Option<String>, | |
| 84 | + #[serde(skip_serializing_if = "Option::is_none")] | |
| 85 | + examples: Option<Vec<OpenApiExample>>, | |
| 86 | +} | |
| 87 | + | |
| 88 | +#[derive(Serialize, Clone)] | |
| 89 | +#[serde(rename_all = "camelCase")] | |
| 90 | +struct OpenApiResponseDocumentation { | |
| 91 | + status_code: String, | |
| 92 | + #[serde(skip_serializing_if = "Option::is_none")] | |
| 93 | + description: Option<String>, | |
| 94 | + content_types: Vec<String>, | |
| 95 | + #[serde(skip_serializing_if = "Option::is_none")] | |
| 96 | + examples: Option<Vec<OpenApiExample>>, | |
| 97 | +} | |
| 98 | + | |
| 99 | +#[derive(Serialize, Clone)] | |
| 100 | +#[serde(rename_all = "camelCase")] | |
| 101 | +struct OpenApiToolDocumentation { | |
| 102 | + #[serde(skip_serializing_if = "Option::is_none")] | |
| 103 | + summary: Option<String>, | |
| 104 | + #[serde(skip_serializing_if = "Option::is_none")] | |
| 105 | + deprecated: Option<bool>, | |
| 106 | + parameters: Vec<OpenApiParameterDocumentation>, | |
| 107 | + #[serde(skip_serializing_if = "Option::is_none")] | |
| 108 | + request_body: Option<OpenApiRequestBodyDocumentation>, | |
| 109 | + #[serde(skip_serializing_if = "Option::is_none")] | |
| 110 | + response: Option<OpenApiResponseDocumentation>, | |
| 111 | +} | |
| 112 | + | |
| 113 | +#[derive(Serialize, Clone)] | |
| 114 | +#[serde(rename_all = "camelCase")] | |
| 115 | +struct OpenApiExtractedTool { | |
| 116 | + tool_id: String, | |
| 117 | + #[serde(skip_serializing_if = "Option::is_none")] | |
| 118 | + operation_id: Option<String>, | |
| 119 | + tags: Vec<String>, | |
| 120 | + name: String, | |
| 121 | + description: Option<String>, | |
| 122 | + method: String, | |
| 123 | + path: String, | |
| 124 | + invocation: OpenApiInvocationPayload, | |
| 125 | + operation_hash: String, | |
| 126 | + #[serde(skip_serializing_if = "Option::is_none")] | |
| 127 | + typing: Option<DiscoveryTypingPayload>, | |
| 128 | + #[serde(skip_serializing_if = "Option::is_none")] | |
| 129 | + documentation: Option<OpenApiToolDocumentation>, | |
| 130 | +} | |
| 131 | + | |
| 132 | +#[derive(Serialize)] | |
| 133 | +#[serde(rename_all = "camelCase")] | |
| 134 | +struct OpenApiToolManifest { | |
| 135 | + version: u8, | |
| 136 | + source_hash: String, | |
| 137 | + tools: Vec<OpenApiExtractedTool>, | |
| 138 | + #[serde(skip_serializing_if = "Option::is_none")] | |
| 139 | + ref_hint_table: Option<BTreeMap<String, String>>, | |
| 140 | +} | |
| 141 | + | |
| 142 | +pub fn extract_manifest_json( | |
| 143 | + source_name: &str, | |
| 144 | + openapi_document_text: &str, | |
| 145 | + pretty: bool, | |
| 146 | +) -> Result<String, String> { | |
| 147 | + let spec = parse_openapi_document(openapi_document_text)?; | |
| 148 | + let manifest = extract_openapi_manifest(source_name, &spec)?; | |
| 149 | + | |
| 150 | + if pretty { | |
| 151 | + serde_json::to_string_pretty(&manifest).map_err(|error| format!("serialize manifest: {error}")) | |
| 152 | + } else { | |
| 153 | + serde_json::to_string(&manifest).map_err(|error| format!("serialize manifest: {error}")) | |
| 154 | + } | |
| 155 | +} | |
| 156 | + | |
| 157 | +#[cfg(target_arch = "wasm32")] | |
| 158 | +#[wasm_bindgen] | |
| 159 | +pub fn extract_manifest_json_wasm( | |
| 160 | + source_name: &str, | |
| 161 | + openapi_document_text: &str, | |
| 162 | +) -> Result<String, JsValue> { | |
| 163 | + extract_manifest_json(source_name, openapi_document_text, false) | |
| 164 | + .map_err(|error| JsValue::from_str(&error)) | |
| 165 | +} | |
| 166 | + | |
| 167 | +fn parse_openapi_document(input: &str) -> Result<Value, String> { | |
| 168 | + let text = input.trim(); | |
| 169 | + if text.is_empty() { | |
| 170 | + return Err("OpenAPI document is empty".to_string()); | |
| 171 | + } | |
| 172 | + | |
| 173 | + match serde_json::from_str::<Value>(text) { | |
| 174 | + Ok(value) => Ok(value), | |
| 175 | + Err(_) => serde_yaml::from_str::<Value>(text) | |
| 176 | + .map_err(|error| format!("Unable to parse OpenAPI document as JSON or YAML: {error}")), | |
| 177 | + } | |
| 178 | +} | |
| 179 | + | |
| 180 | +fn as_object(value: &Value) -> Option<&Map<String, Value>> { | |
| 181 | + value.as_object() | |
| 182 | +} | |
| 183 | + | |
| 184 | +fn as_trimmed_string(value: Option<&Value>) -> Option<String> { | |
| 185 | + let candidate = value?.as_str()?.trim(); | |
| 186 | + if candidate.is_empty() { | |
| 187 | + return None; | |
| 188 | + } | |
| 189 | + | |
| 190 | + Some(candidate.to_string()) | |
| 191 | +} | |
| 192 | + | |
| 193 | +fn response_status_rank(status_code: &str) -> i32 { | |
| 194 | + if status_code.len() == 3 | |
| 195 | + && status_code.starts_with('2') | |
| 196 | + && status_code.chars().all(|character| character.is_ascii_digit()) | |
| 197 | + { | |
| 198 | + return 0; | |
| 199 | + } | |
| 200 | + | |
| 201 | + if status_code == "default" { | |
| 202 | + return 1; | |
| 203 | + } | |
| 204 | + | |
| 205 | + 2 | |
| 206 | +} | |
| 207 | + | |
| 208 | +fn to_stable_value(value: &Value) -> Value { | |
| 209 | + match value { | |
| 210 | + Value::Array(items) => Value::Array(items.iter().map(to_stable_value).collect()), | |
| 211 | + Value::Object(object) => { | |
| 212 | + let mut keys: Vec<&String> = object.keys().collect(); | |
| 213 | + keys.sort(); | |
| 214 | + | |
| 215 | + let mut stable = Map::new(); | |
| 216 | + for key in keys { | |
| 217 | + if let Some(item) = object.get(key) { | |
| 218 | + stable.insert(key.clone(), to_stable_value(item)); | |
| 219 | + } | |
| 220 | + } | |
| 221 | + | |
| 222 | + Value::Object(stable) | |
| 223 | + } | |
| 224 | + Value::Number(number) => { | |
| 225 | + if let Some(float_value) = number.as_f64() | |
| 226 | + && float_value.is_finite() | |
| 227 | + && float_value.fract() == 0.0 | |
| 228 | + { | |
| 229 | + if float_value >= 0.0 && float_value <= u64::MAX as f64 { | |
| 230 | + let unsigned = float_value as u64; | |
| 231 | + if unsigned as f64 == float_value { | |
| 232 | + return Value::Number(serde_json::Number::from(unsigned)); | |
| 233 | + } | |
| 234 | + } | |
| 235 | + | |
| 236 | + if float_value >= i64::MIN as f64 && float_value <= i64::MAX as f64 { | |
| 237 | + let signed = float_value as i64; | |
| 238 | + if signed as f64 == float_value { | |
| 239 | + return Value::Number(serde_json::Number::from(signed)); | |
| 240 | + } | |
| 241 | + } | |
| 242 | + } | |
| 243 | + | |
| 244 | + Value::Number(number.clone()) | |
| 245 | + } | |
| 246 | + _ => value.clone(), | |
| 247 | + } | |
| 248 | +} | |
| 249 | + | |
| 250 | +fn hash_unknown(value: &Value) -> Result<String, String> { | |
| 251 | + let stable_json = | |
| 252 | + serde_json::to_string(&to_stable_value(value)).map_err(|error| format!("hash json: {error}"))?; | |
| 253 | + | |
| 254 | + let mut hasher = Sha256::new(); | |
| 255 | + hasher.update(stable_json.as_bytes()); | |
| 256 | + Ok(format!("{:x}", hasher.finalize())) | |
| 257 | +} | |
| 258 | + | |
| 259 | +fn encode_stable_json(value: &Value) -> Result<String, String> { | |
| 260 | + serde_json::to_string(&to_stable_value(value)).map_err(|error| format!("encode stable json: {error}")) | |
| 261 | +} | |
| 262 | + | |
| 263 | +fn to_extracted_tool_parameter_from_record(record: &Map<String, Value>) -> Option<OpenApiToolParameter> { | |
| 264 | + let name = record.get("name")?.as_str()?.trim(); | |
| 265 | + | |
| 266 | + if name.is_empty() { | |
| 267 | + return None; | |
| 268 | + } | |
| 269 | + | |
| 270 | + let location = record.get("in")?.as_str()?; | |
| 271 | + if !OPEN_API_PARAMETER_LOCATIONS.contains(&location) { | |
| 272 | + return None; | |
| 273 | + } | |
| 274 | + | |
| 275 | + let required = location == "path" || record.get("required").and_then(Value::as_bool) == Some(true); | |
| 276 | + | |
| 277 | + Some(OpenApiToolParameter { | |
| 278 | + name: name.to_string(), | |
| 279 | + location: location.to_string(), | |
| 280 | + required, | |
| 281 | + }) | |
| 282 | +} | |
| 283 | + | |
| 284 | +fn to_extracted_tool_parameter(root: &Value, value: &Value) -> Option<OpenApiToolParameter> { | |
| 285 | + let record = resolve_local_reference_object(root, value)?; | |
| 286 | + to_extracted_tool_parameter_from_record(&record) | |
| 287 | +} | |
| 288 | + | |
| 289 | +fn merge_parameters(root: &Value, path_item: &Map<String, Value>, operation: &Map<String, Value>) -> Vec<OpenApiToolParameter> { | |
| 290 | + let mut merged: Vec<OpenApiToolParameter> = collect_parameter_record_by_key(root, path_item, operation) | |
| 291 | + .into_values() | |
| 292 | + .filter_map(|record| to_extracted_tool_parameter_from_record(&record)) | |
| 293 | + .collect(); | |
| 294 | + merged.sort_by(|left, right| { | |
| 295 | + left.location | |
| 296 | + .cmp(&right.location) | |
| 297 | + .then_with(|| left.name.cmp(&right.name)) | |
| 298 | + }); | |
| 299 | + merged | |
| 300 | +} | |
| 301 | + | |
| 302 | +fn extract_request_body(root: &Value, operation: &Map<String, Value>) -> Option<OpenApiToolRequestBody> { | |
| 303 | + let request_body = resolve_request_body_record(root, operation)?; | |
| 304 | + let content = request_body.get("content").and_then(as_object); | |
| 305 | + | |
| 306 | + Some(OpenApiToolRequestBody { | |
| 307 | + required: request_body.get("required").and_then(Value::as_bool) == Some(true), | |
| 308 | + content_types: collect_content_types(content), | |
| 309 | + }) | |
| 310 | +} | |
| 311 | + | |
| 312 | +fn build_invocation_metadata( | |
| 313 | + root: &Value, | |
| 314 | + method: &str, | |
| 315 | + path_value: &str, | |
| 316 | + path_item: &Map<String, Value>, | |
| 317 | + operation: &Map<String, Value>, | |
| 318 | +) -> OpenApiInvocationPayload { | |
| 319 | + OpenApiInvocationPayload { | |
| 320 | + method: method.to_string(), | |
| 321 | + path_template: path_value.to_string(), | |
| 322 | + parameters: merge_parameters(root, path_item, operation), | |
| 323 | + request_body: extract_request_body(root, operation), | |
| 324 | + } | |
| 325 | +} | |
| 326 | + | |
| 327 | +fn collect_ref_keys(value: &Value, refs: &mut BTreeSet<String>) { | |
| 328 | + match value { | |
| 329 | + Value::Array(items) => { | |
| 330 | + for item in items { | |
| 331 | + collect_ref_keys(item, refs); | |
| 332 | + } | |
| 333 | + } | |
| 334 | + Value::Object(record) => { | |
| 335 | + if let Some(reference) = record.get("$ref").and_then(Value::as_str) | |
| 336 | + && reference.starts_with("#/") | |
| 337 | + { | |
| 338 | + refs.insert(reference.to_string()); | |
| 339 | + } | |
| 340 | + | |
| 341 | + for nested in record.values() { | |
| 342 | + collect_ref_keys(nested, refs); | |
| 343 | + } | |
| 344 | + } | |
| 345 | + _ => {} | |
| 346 | + } | |
| 347 | +} | |
| 348 | + | |
| 349 | +fn resolve_json_pointer<'a>(root: &'a Value, pointer: &str) -> Option<&'a Value> { | |
| 350 | + if !pointer.starts_with("#/") { | |
| 351 | + return None; | |
| 352 | + } | |
| 353 | + | |
| 354 | + let mut current = root; | |
| 355 | + for part in pointer[2..].split('/') { | |
| 356 | + let decoded = part.replace("~1", "/").replace("~0", "~"); | |
| 357 | + let next = current.as_object()?.get(&decoded)?; | |
| 358 | + current = next; | |
| 359 | + } | |
| 360 | + | |
| 361 | + Some(current) | |
| 362 | +} | |
| 363 | + | |
| 364 | +fn resolve_local_reference_value( | |
| 365 | + root: &Value, | |
| 366 | + value: &Value, | |
| 367 | + active_refs: &HashSet<String>, | |
| 368 | +) -> Value { | |
| 369 | + let Some(record) = as_object(value) else { | |
| 370 | + return value.clone(); | |
| 371 | + }; | |
| 372 | + | |
| 373 | + let Some(reference) = record.get("$ref").and_then(Value::as_str) else { | |
| 374 | + return value.clone(); | |
| 375 | + }; | |
| 376 | + | |
| 377 | + if !reference.starts_with("#/") || active_refs.contains(reference) { | |
| 378 | + return value.clone(); | |
| 379 | + } | |
| 380 | + | |
| 381 | + let Some(resolved) = resolve_json_pointer(root, reference) else { | |
| 382 | + return value.clone(); | |
| 383 | + }; | |
| 384 | + | |
| 385 | + let mut next_active_refs = active_refs.clone(); | |
| 386 | + next_active_refs.insert(reference.to_string()); | |
| 387 | + | |
| 388 | + let mut resolved_value = resolve_local_reference_value(root, resolved, &next_active_refs); | |
| 389 | + | |
| 390 | + if let Some(resolved_record) = resolved_value.as_object_mut() { | |
| 391 | + for (key, nested) in record { | |
| 392 | + if key != "$ref" { | |
| 393 | + resolved_record.insert(key.clone(), nested.clone()); | |
| 394 | + } | |
| 395 | + } | |
| 396 | + return resolved_value; | |
| 397 | + } | |
| 398 | + | |
| 399 | + value.clone() | |
| 400 | +} | |
| 401 | + | |
| 402 | +fn resolve_local_reference_object(root: &Value, value: &Value) -> Option<Map<String, Value>> { | |
| 403 | + as_object(&resolve_local_reference_value(root, value, &HashSet::new())).cloned() | |
| 404 | +} | |
| 405 | + | |
| 406 | +fn collect_parameter_record_by_key( | |
| 407 | + root: &Value, | |
| 408 | + path_item: &Map<String, Value>, | |
| 409 | + operation: &Map<String, Value>, | |
| 410 | +) -> HashMap<String, Map<String, Value>> { | |
| 411 | + let mut records = HashMap::new(); | |
| 412 | + | |
| 413 | + let mut add_parameters = |candidate: Option<&Value>| { | |
| 414 | + let Some(items) = candidate.and_then(Value::as_array) else { | |
| 415 | + return; | |
| 416 | + }; | |
| 417 | + | |
| 418 | + for item in items { | |
| 419 | + let Some(record) = resolve_local_reference_object(root, item) else { | |
| 420 | + continue; | |
| 421 | + }; | |
| 422 | + | |
| 423 | + let Some(parameter) = to_extracted_tool_parameter_from_record(&record) else { | |
| 424 | + continue; | |
| 425 | + }; | |
| 426 | + | |
| 427 | + records.insert( | |
| 428 | + format!("{}:{}", parameter.location, parameter.name), | |
| 429 | + record, | |
| 430 | + ); | |
| 431 | + } | |
| 432 | + }; | |
| 433 | + | |
| 434 | + add_parameters(path_item.get("parameters")); | |
| 435 | + add_parameters(operation.get("parameters")); | |
| 436 | + | |
| 437 | + records | |
| 438 | +} | |
| 439 | + | |
| 440 | +fn resolve_request_body_record(root: &Value, operation: &Map<String, Value>) -> Option<Map<String, Value>> { | |
| 441 | + resolve_local_reference_object(root, operation.get("requestBody")?) | |
| 442 | +} | |
| 443 | + | |
| 444 | +fn collect_content_types(content: Option<&Map<String, Value>>) -> Vec<String> { | |
| 445 | + let mut content_types = content | |
| 446 | + .map(|object| object.keys().cloned().collect::<Vec<String>>()) | |
| 447 | + .unwrap_or_default(); | |
| 448 | + content_types.sort(); | |
| 449 | + content_types | |
| 450 | +} | |
| 451 | + | |
| 452 | +fn pick_content_entry(content: Option<&Value>) -> Option<(String, Map<String, Value>)> { | |
| 453 | + let content_record = as_object(content?)?; | |
| 454 | + | |
| 455 | + let mut keys: Vec<String> = content_record.keys().cloned().collect(); | |
| 456 | + keys.sort(); | |
| 457 | + | |
| 458 | + let mut preferred = Vec::with_capacity(keys.len() + 1); | |
| 459 | + preferred.push("application/json".to_string()); | |
| 460 | + preferred.extend(keys); | |
| 461 | + | |
| 462 | + let mut seen = HashSet::new(); | |
| 463 | + for media_type in preferred { | |
| 464 | + if !seen.insert(media_type.clone()) { | |
| 465 | + continue; | |
| 466 | + } | |
| 467 | + | |
| 468 | + let Some(media_type_value) = content_record.get(&media_type).and_then(as_object) else { | |
| 469 | + continue; | |
| 470 | + }; | |
| 471 | + | |
| 472 | + return Some((media_type, media_type_value.clone())); | |
| 473 | + } | |
| 474 | + | |
| 475 | + None | |
| 476 | +} | |
| 477 | + | |
| 478 | +fn pick_example_values(value: &Value, label: Option<String>, media_type: Option<String>) -> Result<Vec<OpenApiExample>, String> { | |
| 479 | + let mut examples = Vec::new(); | |
| 480 | + let Some(record) = as_object(value) else { | |
| 481 | + return Ok(examples); | |
| 482 | + }; | |
| 483 | + | |
| 484 | + if let Some(example_value) = record.get("example") { | |
| 485 | + examples.push(OpenApiExample { | |
| 486 | + value_json: encode_stable_json(example_value)?, | |
| 487 | + media_type: media_type.clone(), | |
| 488 | + label: label.clone(), | |
| 489 | + }); | |
| 490 | + } | |
| 491 | + | |
| 492 | + if let Some(example_values) = record.get("examples").and_then(Value::as_object) { | |
| 493 | + let mut keys: Vec<String> = example_values.keys().cloned().collect(); | |
| 494 | + keys.sort(); | |
| 495 | + | |
| 496 | + for key in keys { | |
| 497 | + let Some(example_entry) = example_values.get(&key) else { | |
| 498 | + continue; | |
| 499 | + }; | |
| 500 | + let example_record = as_object(example_entry) | |
| 501 | + .and_then(|entry| entry.get("value")) | |
| 502 | + .unwrap_or(example_entry); | |
| 503 | + | |
| 504 | + examples.push(OpenApiExample { | |
| 505 | + value_json: encode_stable_json(example_record)?, | |
| 506 | + media_type: media_type.clone(), | |
| 507 | + label: Some(key), | |
| 508 | + }); | |
| 509 | + } | |
| 510 | + } | |
| 511 | + | |
| 512 | + Ok(examples) | |
| 513 | +} | |
| 514 | + | |
| 515 | +fn extract_examples_from_schema(schema: Option<&Value>) -> Result<Vec<OpenApiExample>, String> { | |
| 516 | + let Some(schema_value) = schema else { | |
| 517 | + return Ok(Vec::new()); | |
| 518 | + }; | |
| 519 | + | |
| 520 | + pick_example_values(schema_value, None, None) | |
| 521 | +} | |
| 522 | + | |
| 523 | +fn extract_examples_from_media_type( | |
| 524 | + media_type: &str, | |
| 525 | + media_type_record: &Map<String, Value>, | |
| 526 | +) -> Result<Vec<OpenApiExample>, String> { | |
| 527 | + let direct = pick_example_values( | |
| 528 | + &Value::Object(media_type_record.clone()), | |
| 529 | + None, | |
| 530 | + Some(media_type.to_string()), | |
| 531 | + )?; | |
| 532 | + if !direct.is_empty() { | |
| 533 | + return Ok(direct); | |
| 534 | + } | |
| 535 | + | |
| 536 | + extract_examples_from_schema(media_type_record.get("schema")).map(|examples| { | |
| 537 | + examples | |
| 538 | + .into_iter() | |
| 539 | + .map(|mut example| { | |
| 540 | + example.media_type = Some(media_type.to_string()); | |
| 541 | + example | |
| 542 | + }) | |
| 543 | + .collect() | |
| 544 | + }) | |
| 545 | +} | |
| 546 | + | |
| 547 | +fn pick_preferred_response_record( | |
| 548 | + root: &Value, | |
| 549 | + operation: &Map<String, Value>, | |
| 550 | +) -> Option<(String, Map<String, Value>)> { | |
| 551 | + let responses = as_object(operation.get("responses")?)?; | |
| 552 | + let mut response_codes: Vec<String> = responses.keys().cloned().collect(); | |
| 553 | + response_codes.sort_by(|left, right| { | |
| 554 | + response_status_rank(left) | |
| 555 | + .cmp(&response_status_rank(right)) | |
| 556 | + .then_with(|| left.cmp(right)) | |
| 557 | + }); | |
| 558 | + | |
| 559 | + for response_code in response_codes { | |
| 560 | + let Some(response) = responses.get(&response_code) else { | |
| 561 | + continue; | |
| 562 | + }; | |
| 563 | + let Some(response_record) = resolve_local_reference_object(root, response) else { | |
| 564 | + continue; | |
| 565 | + }; | |
| 566 | + | |
| 567 | + return Some((response_code, response_record)); | |
| 568 | + } | |
| 569 | + | |
| 570 | + None | |
| 571 | +} | |
| 572 | + | |
| 573 | +fn pick_schema_from_content(content: Option<&Value>) -> Option<Value> { | |
| 574 | + if let Some((_media_type, media_type_value)) = pick_content_entry(content) { | |
| 575 | + if let Some(schema) = media_type_value.get("schema") { | |
| 576 | + return Some(schema.clone()); | |
| 577 | + } | |
| 578 | + } | |
| 579 | + | |
| 580 | + None | |
| 581 | +} | |
| 582 | + | |
| 583 | +fn extract_request_body_schema(root: &Value, operation: &Map<String, Value>) -> Option<Value> { | |
| 584 | + let request_body = resolve_request_body_record(root, operation)?; | |
| 585 | + pick_schema_from_content(request_body.get("content")) | |
| 586 | +} | |
| 587 | + | |
| 588 | +fn extract_response_schema(root: &Value, operation: &Map<String, Value>) -> Option<Value> { | |
| 589 | + let (_status_code, response) = pick_preferred_response_record(root, operation)?; | |
| 590 | + pick_schema_from_content(response.get("content")) | |
| 591 | +} | |
| 592 | + | |
| 593 | +fn collect_parameter_schema_by_key( | |
| 594 | + root: &Value, | |
| 595 | + path_item: &Map<String, Value>, | |
| 596 | + operation: &Map<String, Value>, | |
| 597 | +) -> HashMap<String, Value> { | |
| 598 | + let mut schemas = HashMap::new(); | |
| 599 | + | |
| 600 | + for (key, record) in collect_parameter_record_by_key(root, path_item, operation) { | |
| 601 | + let Some(schema) = record.get("schema") else { | |
| 602 | + continue; | |
| 603 | + }; | |
| 604 | + | |
| 605 | + schemas.insert(key, schema.clone()); | |
| 606 | + } | |
| 607 | + | |
| 608 | + schemas | |
| 609 | +} | |
| 610 | + | |
| 611 | +fn build_input_schema( | |
| 612 | + root: &Value, | |
| 613 | + path_item: &Map<String, Value>, | |
| 614 | + operation: &Map<String, Value>, | |
| 615 | + invocation: &OpenApiInvocationPayload, | |
| 616 | +) -> Option<Value> { | |
| 617 | + let parameter_schema_by_key = collect_parameter_schema_by_key(root, path_item, operation); | |
| 618 | + | |
| 619 | + let mut properties = Map::new(); | |
| 620 | + let mut required = BTreeSet::new(); | |
| 621 | + | |
| 622 | + for parameter in &invocation.parameters { | |
| 623 | + let key = format!("{}:{}", parameter.location, parameter.name); | |
| 624 | + let schema = parameter_schema_by_key | |
| 625 | + .get(&key) | |
| 626 | + .cloned() | |
| 627 | + .unwrap_or_else(|| json!({ "type": "string" })); | |
| 628 | + | |
| 629 | + properties.insert(parameter.name.clone(), schema); | |
| 630 | + | |
| 631 | + if parameter.required { | |
| 632 | + required.insert(parameter.name.clone()); | |
| 633 | + } | |
| 634 | + } | |
| 635 | + | |
| 636 | + let request_body_schema = extract_request_body_schema(root, operation); | |
| 637 | + if let Some(schema) = request_body_schema { | |
| 638 | + properties.insert("body".to_string(), schema); | |
| 639 | + | |
| 640 | + if invocation | |
| 641 | + .request_body | |
| 642 | + .as_ref() | |
| 643 | + .map(|request_body| request_body.required) | |
| 644 | + == Some(true) | |
| 645 | + { | |
| 646 | + required.insert("body".to_string()); | |
| 647 | + } | |
| 648 | + } | |
| 649 | + | |
| 650 | + if properties.is_empty() { | |
| 651 | + return None; | |
| 652 | + } | |
| 653 | + | |
| 654 | + let required_list: Vec<String> = required.into_iter().collect(); | |
| 655 | + | |
| 656 | + Some(json!({ | |
| 657 | + "type": "object", | |
| 658 | + "properties": properties, | |
| 659 | + "required": required_list, | |
| 660 | + "additionalProperties": false, | |
| 661 | + })) | |
| 662 | +} | |
| 663 | + | |
| 664 | +fn build_tool_typing( | |
| 665 | + root: &Value, | |
| 666 | + path_item: &Map<String, Value>, | |
| 667 | + operation: &Map<String, Value>, | |
| 668 | + invocation: &OpenApiInvocationPayload, | |
| 669 | +) -> Result<Option<DiscoveryTypingPayload>, String> { | |
| 670 | + let input_schema = build_input_schema(root, path_item, operation, invocation); | |
| 671 | + let output_schema = extract_response_schema(root, operation); | |
| 672 | + | |
| 673 | + if input_schema.is_none() && output_schema.is_none() { | |
| 674 | + return Ok(None); | |
| 675 | + } | |
| 676 | + | |
| 677 | + let mut refs = BTreeSet::new(); | |
| 678 | + if let Some(schema) = &input_schema { | |
| 679 | + collect_ref_keys(schema, &mut refs); | |
| 680 | + } | |
| 681 | + | |
| 682 | + if let Some(schema) = &output_schema { | |
| 683 | + collect_ref_keys(schema, &mut refs); | |
| 684 | + } | |
| 685 | + | |
| 686 | + let ref_hint_keys = if refs.is_empty() { | |
| 687 | + None | |
| 688 | + } else { | |
| 689 | + Some(refs.into_iter().collect()) | |
| 690 | + }; | |
| 691 | + | |
| 692 | + Ok(Some(DiscoveryTypingPayload { | |
| 693 | + input_schema_json: input_schema | |
| 694 | + .as_ref() | |
| 695 | + .map(encode_stable_json) | |
| 696 | + .transpose()?, | |
| 697 | + output_schema_json: output_schema | |
| 698 | + .as_ref() | |
| 699 | + .map(encode_stable_json) | |
| 700 | + .transpose()?, | |
| 701 | + ref_hint_keys, | |
| 702 | + })) | |
| 703 | +} | |
| 704 | + | |
| 705 | +fn build_parameter_documentation( | |
| 706 | + root: &Value, | |
| 707 | + path_item: &Map<String, Value>, | |
| 708 | + operation: &Map<String, Value>, | |
| 709 | + invocation: &OpenApiInvocationPayload, | |
| 710 | +) -> Result<Vec<OpenApiParameterDocumentation>, String> { | |
| 711 | + let records_by_key = collect_parameter_record_by_key(root, path_item, operation); | |
| 712 | + | |
| 713 | + invocation | |
| 714 | + .parameters | |
| 715 | + .iter() | |
| 716 | + .map(|parameter| { | |
| 717 | + let key = format!("{}:{}", parameter.location, parameter.name); | |
| 718 | + let record = records_by_key.get(&key); | |
| 719 | + | |
| 720 | + let mut examples = record | |
| 721 | + .map(|item| pick_example_values(&Value::Object(item.clone()), None, None)) | |
| 722 | + .transpose()? | |
| 723 | + .unwrap_or_default(); | |
| 724 | + | |
| 725 | + if examples.is_empty() { | |
| 726 | + examples = extract_examples_from_schema(record.and_then(|item| item.get("schema")))?; | |
| 727 | + } | |
| 728 | + | |
| 729 | + Ok(OpenApiParameterDocumentation { | |
| 730 | + name: parameter.name.clone(), | |
| 731 | + location: parameter.location.clone(), | |
| 732 | + required: parameter.required, | |
| 733 | + description: record.and_then(|item| as_trimmed_string(item.get("description"))), | |
| 734 | + examples: if examples.is_empty() { None } else { Some(examples) }, | |
| 735 | + }) | |
| 736 | + }) | |
| 737 | + .collect() | |
| 738 | +} | |
| 739 | + | |
| 740 | +fn build_request_body_documentation( | |
| 741 | + root: &Value, | |
| 742 | + operation: &Map<String, Value>, | |
| 743 | +) -> Result<Option<OpenApiRequestBodyDocumentation>, String> { | |
| 744 | + let Some(request_body) = resolve_request_body_record(root, operation) else { | |
| 745 | + return Ok(None); | |
| 746 | + }; | |
| 747 | + | |
| 748 | + let examples = if let Some((media_type, media_type_record)) = pick_content_entry(request_body.get("content")) { | |
| 749 | + extract_examples_from_media_type(&media_type, &media_type_record)? | |
| 750 | + } else { | |
| 751 | + Vec::new() | |
| 752 | + }; | |
| 753 | + | |
| 754 | + Ok(Some(OpenApiRequestBodyDocumentation { | |
| 755 | + description: as_trimmed_string(request_body.get("description")), | |
| 756 | + examples: if examples.is_empty() { None } else { Some(examples) }, | |
| 757 | + })) | |
| 758 | +} | |
| 759 | + | |
| 760 | +fn build_response_documentation( | |
| 761 | + root: &Value, | |
| 762 | + operation: &Map<String, Value>, | |
| 763 | +) -> Result<Option<OpenApiResponseDocumentation>, String> { | |
| 764 | + let Some((status_code, response)) = pick_preferred_response_record(root, operation) else { | |
| 765 | + return Ok(None); | |
| 766 | + }; | |
| 767 | + | |
| 768 | + let content = response.get("content").and_then(as_object); | |
| 769 | + let content_types = collect_content_types(content); | |
| 770 | + let examples = if let Some((media_type, media_type_record)) = pick_content_entry(response.get("content")) { | |
| 771 | + extract_examples_from_media_type(&media_type, &media_type_record)? | |
| 772 | + } else { | |
| 773 | + Vec::new() | |
| 774 | + }; | |
| 775 | + | |
| 776 | + Ok(Some(OpenApiResponseDocumentation { | |
| 777 | + status_code, | |
| 778 | + description: as_trimmed_string(response.get("description")), | |
| 779 | + content_types, | |
| 780 | + examples: if examples.is_empty() { None } else { Some(examples) }, | |
| 781 | + })) | |
| 782 | +} | |
| 783 | + | |
| 784 | +fn build_tool_documentation( | |
| 785 | + root: &Value, | |
| 786 | + path_item: &Map<String, Value>, | |
| 787 | + operation: &Map<String, Value>, | |
| 788 | + invocation: &OpenApiInvocationPayload, | |
| 789 | +) -> Result<Option<OpenApiToolDocumentation>, String> { | |
| 790 | + let summary = as_trimmed_string(operation.get("summary")); | |
| 791 | + let deprecated = operation.get("deprecated").and_then(Value::as_bool); | |
| 792 | + let parameters = build_parameter_documentation(root, path_item, operation, invocation)?; | |
| 793 | + let request_body = build_request_body_documentation(root, operation)?; | |
| 794 | + let response = build_response_documentation(root, operation)?; | |
| 795 | + | |
| 796 | + if summary.is_none() && deprecated.is_none() && parameters.is_empty() && request_body.is_none() && response.is_none() { | |
| 797 | + return Ok(None); | |
| 798 | + } | |
| 799 | + | |
| 800 | + Ok(Some(OpenApiToolDocumentation { | |
| 801 | + summary, | |
| 802 | + deprecated, | |
| 803 | + parameters, | |
| 804 | + request_body, | |
| 805 | + response, | |
| 806 | + })) | |
| 807 | +} | |
| 808 | + | |
| 809 | +fn build_ref_hint_table(open_api_spec: &Value, initial_ref_keys: &[String]) -> Result<BTreeMap<String, String>, String> { | |
| 810 | + let mut queue = VecDeque::new(); | |
| 811 | + let mut queued = HashSet::new(); | |
| 812 | + | |
| 813 | + for ref_key in initial_ref_keys { | |
| 814 | + if queued.insert(ref_key.clone()) { | |
| 815 | + queue.push_back(ref_key.clone()); | |
| 816 | + } | |
| 817 | + } | |
| 818 | + | |
| 819 | + let mut seen = HashSet::new(); | |
| 820 | + let mut table = BTreeMap::new(); | |
| 821 | + | |
| 822 | + while let Some(ref_key) = queue.pop_front() { | |
| 823 | + if !seen.insert(ref_key.clone()) { | |
| 824 | + continue; | |
| 825 | + } | |
| 826 | + | |
| 827 | + let Some(resolved) = resolve_json_pointer(open_api_spec, &ref_key) else { | |
| 828 | + continue; | |
| 829 | + }; | |
| 830 | + | |
| 831 | + table.insert(ref_key.clone(), encode_stable_json(resolved)?); | |
| 832 | + | |
| 833 | + let mut nested = BTreeSet::new(); | |
| 834 | + collect_ref_keys(resolved, &mut nested); | |
| 835 | + for nested_ref in nested { | |
| 836 | + if !seen.contains(&nested_ref) { | |
| 837 | + queue.push_back(nested_ref); | |
| 838 | + } | |
| 839 | + } | |
| 840 | + } | |
| 841 | + | |
| 842 | + Ok(table) | |
| 843 | +} | |
| 844 | + | |
| 845 | +fn normalize_path_for_tool_id(path_value: &str) -> String { | |
| 846 | + let trimmed = path_value.trim(); | |
| 847 | + let no_leading = trimmed.strip_prefix('/').unwrap_or(trimmed); | |
| 848 | + | |
| 849 | + let mut normalized = String::new(); | |
| 850 | + let mut last_was_underscore = false; | |
| 851 | + | |
| 852 | + for character in no_leading.chars() { | |
| 853 | + if character.is_ascii_alphanumeric() { | |
| 854 | + normalized.push(character); | |
| 855 | + last_was_underscore = false; | |
| 856 | + continue; | |
| 857 | + } | |
| 858 | + | |
| 859 | + if !last_was_underscore { | |
| 860 | + normalized.push('_'); | |
| 861 | + last_was_underscore = true; | |
| 862 | + } | |
| 863 | + } | |
| 864 | + | |
| 865 | + let without_edges = normalized.trim_matches('_').to_lowercase(); | |
| 866 | + if without_edges.is_empty() { | |
| 867 | + return "root".to_string(); | |
| 868 | + } | |
| 869 | + | |
| 870 | + without_edges | |
| 871 | +} | |
| 872 | + | |
| 873 | +fn build_operation_id(operation: &Map<String, Value>) -> Option<String> { | |
| 874 | + as_trimmed_string(operation.get("operationId")) | |
| 875 | +} | |
| 876 | + | |
| 877 | +fn build_tags(operation: &Map<String, Value>) -> Vec<String> { | |
| 878 | + operation | |
| 879 | + .get("tags") | |
| 880 | + .and_then(Value::as_array) | |
| 881 | + .map(|items| { | |
| 882 | + items | |
| 883 | + .iter() | |
| 884 | + .filter_map(|item| item.as_str().map(str::trim)) | |
| 885 | + .filter(|item| !item.is_empty()) | |
| 886 | + .map(ToOwned::to_owned) | |
| 887 | + .collect::<Vec<String>>() | |
| 888 | + }) | |
| 889 | + .unwrap_or_default() | |
| 890 | +} | |
| 891 | + | |
| 892 | + | |
| 893 | +fn build_tool_id(method: &str, path_value: &str, operation: &Map<String, Value>) -> String { | |
| 894 | + if let Some(operation_id) = build_operation_id(operation) { | |
| 895 | + return operation_id; | |
| 896 | + } | |
| 897 | + | |
| 898 | + format!("{}_{}", method, normalize_path_for_tool_id(path_value)) | |
| 899 | +} | |
| 900 | + | |
| 901 | +fn build_tool_name(method: &str, path_value: &str, operation: &Map<String, Value>) -> String { | |
| 902 | + if let Some(summary) = as_trimmed_string(operation.get("summary")) { | |
| 903 | + return summary; | |
| 904 | + } | |
| 905 | + | |
| 906 | + if let Some(operation_id) = build_operation_id(operation) { | |
| 907 | + return operation_id; | |
| 908 | + } | |
| 909 | + | |
| 910 | + format!("{} {}", method.to_uppercase(), path_value) | |
| 911 | +} | |
| 912 | + | |
| 913 | +fn build_tool_description(operation: &Map<String, Value>) -> Option<String> { | |
| 914 | + as_trimmed_string(operation.get("description")).or_else(|| as_trimmed_string(operation.get("summary"))) | |
| 915 | +} | |
| 916 | + | |
| 917 | +fn ensure_unique_tool_ids(source_name: &str, tools: &[OpenApiExtractedTool]) -> Result<(), String> { | |
| 918 | + let mut seen = HashSet::new(); | |
| 919 | + | |
| 920 | + for tool in tools { | |
| 921 | + if !seen.insert(tool.tool_id.clone()) { | |
| 922 | + return Err(format!( | |
| 923 | + "OpenAPI extraction failed for '{source_name}': duplicate toolId {} ({})", | |
| 924 | + tool.tool_id, | |
| 925 | + tool.path | |
| 926 | + )); | |
| 927 | + } | |
| 928 | + } | |
| 929 | + | |
| 930 | + Ok(()) | |
| 931 | +} | |
| 932 | + | |
| 933 | +fn extract_openapi_manifest(source_name: &str, openapi_spec: &Value) -> Result<OpenApiToolManifest, String> { | |
| 934 | + let spec_record = as_object(openapi_spec) | |
| 935 | + .ok_or_else(|| format!("OpenAPI extraction failed for '{source_name}': spec must be object"))?; | |
| 936 | + | |
| 937 | + let Some(paths) = spec_record.get("paths").and_then(as_object) else { | |
| 938 | + return Ok(OpenApiToolManifest { | |
| 939 | + version: 2, | |
| 940 | + source_hash: hash_unknown(openapi_spec)?, | |
| 941 | + tools: Vec::new(), | |
| 942 | + ref_hint_table: None, | |
| 943 | + }); | |
| 944 | + }; | |
| 945 | + | |
| 946 | + let mut path_keys: Vec<String> = paths.keys().cloned().collect(); | |
| 947 | + path_keys.sort(); | |
| 948 | + | |
| 949 | + let mut tools = Vec::new(); | |
| 950 | + | |
| 951 | + for path_value in path_keys { | |
| 952 | + let Some(path_item) = paths.get(&path_value).and_then(as_object) else { | |
| 953 | + continue; | |
| 954 | + }; | |
| 955 | + | |
| 956 | + for method in OPEN_API_HTTP_METHODS { | |
| 957 | + let Some(operation) = path_item.get(method).and_then(as_object) else { | |
| 958 | + continue; | |
| 959 | + }; | |
| 960 | + | |
| 961 | + let invocation = build_invocation_metadata(openapi_spec, method, &path_value, path_item, operation); | |
| 962 | + let typing = build_tool_typing(openapi_spec, path_item, operation, &invocation)?; | |
| 963 | + let documentation = build_tool_documentation(openapi_spec, path_item, operation, &invocation)?; | |
| 964 | + | |
| 965 | + let operation_hash_input = json!({ | |
| 966 | + "method": method, | |
| 967 | + "path": path_value, | |
| 968 | + "operation": operation, | |
| 969 | + "invocation": invocation, | |
| 970 | + }); | |
| 971 | + | |
| 972 | + tools.push(OpenApiExtractedTool { | |
| 973 | + tool_id: build_tool_id(method, &path_value, operation), | |
| 974 | + operation_id: build_operation_id(operation), | |
| 975 | + tags: build_tags(operation), | |
| 976 | + name: build_tool_name(method, &path_value, operation), | |
| 977 | + description: build_tool_description(operation), | |
| 978 | + method: method.to_string(), | |
| 979 | + path: path_value.clone(), | |
| 980 | + invocation, | |
| 981 | + operation_hash: hash_unknown(&operation_hash_input)?, | |
| 982 | + typing, | |
| 983 | + documentation, | |
| 984 | + }); | |
| 985 | + } | |
| 986 | + } | |
| 987 | + | |
| 988 | + tools.sort_by(|left, right| { | |
| 989 | + left.tool_id | |
| 990 | + .to_lowercase() | |
| 991 | + .cmp(&right.tool_id.to_lowercase()) | |
| 992 | + .then_with(|| left.tool_id.cmp(&right.tool_id)) | |
| 993 | + }); | |
| 994 | + ensure_unique_tool_ids(source_name, &tools)?; | |
| 995 | + | |
| 996 | + let direct_ref_keys: Vec<String> = tools | |
| 997 | + .iter() | |
| 998 | + .flat_map(|tool| { | |
| 999 | + tool.typing | |
| 1000 | + .as_ref() | |
| 1001 | + .and_then(|typing| typing.ref_hint_keys.clone()) | |
| 1002 | + .unwrap_or_default() | |
| 1003 | + }) | |
| 1004 | + .collect(); | |
| 1005 | + | |
| 1006 | + let ref_hint_table = build_ref_hint_table(openapi_spec, &direct_ref_keys)?; | |
| 1007 | + | |
| 1008 | + Ok(OpenApiToolManifest { | |
| 1009 | + version: 2, | |
| 1010 | + source_hash: hash_unknown(openapi_spec)?, | |
| 1011 | + tools, | |
| 1012 | + ref_hint_table: if ref_hint_table.is_empty() { | |
| 1013 | + None | |
| 1014 | + } else { | |
| 1015 | + Some(ref_hint_table) | |
| 1016 | + }, | |
| 1017 | + }) | |
| 1018 | +} | |
| @@ -0,0 +1,58 @@ | ||
| 1 | +use std::fs; | |
| 2 | +use std::io::{self, Read, Write}; | |
| 3 | + | |
| 4 | +use clap::Parser; | |
| 5 | + | |
| 6 | +#[derive(Parser, Debug)] | |
| 7 | +#[command(name = "openapi-extractor-rs")] | |
| 8 | +struct Cli { | |
| 9 | + #[arg(long)] | |
| 10 | + source_name: String, | |
| 11 | + #[arg(long)] | |
| 12 | + input: String, | |
| 13 | + #[arg(long)] | |
| 14 | + output: Option<String>, | |
| 15 | + #[arg(long, default_value_t = false)] | |
| 16 | + pretty: bool, | |
| 17 | +} | |
| 18 | + | |
| 19 | +fn main() { | |
| 20 | + if let Err(error) = run() { | |
| 21 | + eprintln!("{error}"); | |
| 22 | + std::process::exit(1); | |
| 23 | + } | |
| 24 | +} | |
| 25 | + | |
| 26 | +fn run() -> Result<(), String> { | |
| 27 | + let args = Cli::parse(); | |
| 28 | + let source_text = read_input_text(&args.input)?; | |
| 29 | + | |
| 30 | + let output_json = | |
| 31 | + openapi_extractor_rs::extract_manifest_json(&args.source_name, &source_text, args.pretty)?; | |
| 32 | + | |
| 33 | + if let Some(path) = args.output { | |
| 34 | + fs::write(path, output_json).map_err(|error| format!("write output file: {error}"))?; | |
| 35 | + } else { | |
| 36 | + let mut stdout = io::stdout().lock(); | |
| 37 | + stdout | |
| 38 | + .write_all(output_json.as_bytes()) | |
| 39 | + .map_err(|error| format!("write stdout: {error}"))?; | |
| 40 | + stdout | |
| 41 | + .write_all(b"\n") | |
| 42 | + .map_err(|error| format!("write stdout newline: {error}"))?; | |
| 43 | + } | |
| 44 | + | |
| 45 | + Ok(()) | |
| 46 | +} | |
| 47 | + | |
| 48 | +fn read_input_text(input: &str) -> Result<String, String> { | |
| 49 | + if input == "-" { | |
| 50 | + let mut buffer = String::new(); | |
| 51 | + io::stdin() | |
| 52 | + .read_to_string(&mut buffer) | |
| 53 | + .map_err(|error| format!("read stdin: {error}"))?; | |
| 54 | + return Ok(buffer); | |
| 55 | + } | |
| 56 | + | |
| 57 | + fs::read_to_string(input).map_err(|error| format!("read input file '{input}': {error}")) | |
| 58 | +} | |
| @@ -9,6 +9,7 @@ · 8 unmodified lines | ||
| 9 | 9 | "src" |
| 10 | 10 | ], |
| 11 | 11 | "scripts": { |
| 12 | + "extractor:build": "./openapi-extractor-rs/build-wasm.sh", | |
| 12 | 13 | "typecheck": "bunx tsc --noEmit -p tsconfig.json", |
| 13 | 14 | "test": "vitest run", |
| 14 | 15 | "test:watch": "vitest" |
| @@ -6,11 +6,19 @@ export { · 5 unmodified lines | ||
| 6 | 6 | OpenApiExtractionError, |
| 7 | 7 | extractOpenApiManifest, |
| 8 | 8 | } from "./openapi-extraction"; |
| 9 | +export { | |
| 10 | + compileOpenApiToolDefinitions, | |
| 11 | + openApiProviderDataJsonFromDefinition, | |
| 12 | + OpenApiToolDefinitionSchema, | |
| 13 | + type OpenApiToolDefinition, | |
| 14 | +} from "./openapi-definitions"; | |
| 15 | +export { buildOpenApiToolPresentation, type OpenApiToolPresentation } from "./openapi-tool-presentation"; | |
| 9 | 16 | export { |
| 10 | 17 | OpenApiToolInvocationError, |
| 11 | 18 | createOpenApiToolsFromManifest, |
| 12 | 19 | createOpenApiToolsFromSpec, |
| 13 | 20 | } from "./openapi-tools"; |
| 21 | +export { resolveSchemaJsonWithRefHints, resolveTypingSchemasWithRefHints } from "./openapi-schema-refs"; | |
| 14 | 22 | export { |
| 15 | 23 | OPEN_API_HTTP_METHODS, |
| 16 | 24 | OPEN_API_PARAMETER_LOCATIONS, |
| @@ -22,6 +30,11 @@ export { · 5 unmodified lines | ||
| 22 | 30 | OpenApiToolManifestSchema, |
| 23 | 31 | OpenApiToolParameterSchema, |
| 24 | 32 | OpenApiToolRequestBodySchema, |
| 33 | + OpenApiExampleSchema, | |
| 34 | + OpenApiParameterDocumentationSchema, | |
| 35 | + OpenApiRequestBodyDocumentationSchema, | |
| 36 | + OpenApiResponseDocumentationSchema, | |
| 37 | + OpenApiToolDocumentationSchema, | |
| 25 | 38 | type DiscoveryTypingPayload, |
| 26 | 39 | type OpenApiExtractedTool, |
| 27 | 40 | type OpenApiHttpMethod, |
| @@ -35,3 +48,10 @@ export { · 7 unmodified lines | ||
| 35 | 48 | type OpenApiToolParameter, |
| 36 | 49 | type OpenApiToolRequestBody, |
| 37 | 50 | } from "./openapi-types"; |
| 51 | +export type { | |
| 52 | + OpenApiExample, | |
| 53 | + OpenApiParameterDocumentation, | |
| 54 | + OpenApiRequestBodyDocumentation, | |
| 55 | + OpenApiResponseDocumentation, | |
| 56 | + OpenApiToolDocumentation, | |
| 57 | +} from "./openapi-types"; | |
| @@ -0,0 +1,254 @@ | ||
| 1 | +import { Schema } from "effect"; | |
| 2 | + | |
| 3 | +import { | |
| 4 | + DiscoveryTypingPayloadSchema, | |
| 5 | + OpenApiHttpMethodSchema, | |
| 6 | + OpenApiInvocationPayloadSchema, | |
| 7 | + OpenApiToolDocumentationSchema, | |
| 8 | + type OpenApiExtractedTool, | |
| 9 | + type OpenApiToolManifest, | |
| 10 | +} from "./openapi-types"; | |
| 11 | + | |
| 12 | +const VERSION_SEGMENT_REGEX = /^v\d+(?:[._-]\d+)?$/i; | |
| 13 | +const IGNORED_PATH_SEGMENTS = new Set(["api"]); | |
| 14 | + | |
| 15 | +const splitWords = (value: string): Array<string> => | |
| 16 | + value | |
| 17 | + .replace(/([a-z0-9])([A-Z])/g, "$1 $2") | |
| 18 | + .replace(/([A-Z]+)([A-Z][a-z0-9]+)/g, "$1 $2") | |
| 19 | + .replace(/[^a-zA-Z0-9]+/g, " ") | |
| 20 | + .trim() | |
| 21 | + .split(/\s+/) | |
| 22 | + .filter((part) => part.length > 0); | |
| 23 | + | |
| 24 | +const normalizeWord = (value: string): string => value.toLowerCase(); | |
| 25 | + | |
| 26 | +const toCamelCase = (value: string): string => { | |
| 27 | + const words = splitWords(value).map(normalizeWord); | |
| 28 | + if (words.length === 0) { | |
| 29 | + return "tool"; | |
| 30 | + } | |
| 31 | + | |
| 32 | + const [first, ...rest] = words; | |
| 33 | + return `${first}${rest.map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join("")}`; | |
| 34 | +}; | |
| 35 | + | |
| 36 | +const toPascalCase = (value: string): string => { | |
| 37 | + const camel = toCamelCase(value); | |
| 38 | + return `${camel[0]?.toUpperCase() ?? ""}${camel.slice(1)}`; | |
| 39 | +}; | |
| 40 | + | |
| 41 | +const normalizeGroupSegment = (value: string | undefined): string | null => { | |
| 42 | + const candidate = value?.trim(); | |
| 43 | + if (!candidate) { | |
| 44 | + return null; | |
| 45 | + } | |
| 46 | + | |
| 47 | + return toCamelCase(candidate); | |
| 48 | +}; | |
| 49 | + | |
| 50 | +const pathSegmentsFromTemplate = (pathTemplate: string): Array<string> => | |
| 51 | + pathTemplate | |
| 52 | + .split("/") | |
| 53 | + .map((segment) => segment.trim()) | |
| 54 | + .filter((segment) => segment.length > 0); | |
| 55 | + | |
| 56 | +const isPathParameterSegment = (segment: string): boolean => | |
| 57 | + segment.startsWith("{") && segment.endsWith("}"); | |
| 58 | + | |
| 59 | +const deriveVersionSegment = (pathTemplate: string): string | undefined => | |
| 60 | + pathSegmentsFromTemplate(pathTemplate) | |
| 61 | + .map((segment) => segment.toLowerCase()) | |
| 62 | + .find((segment) => VERSION_SEGMENT_REGEX.test(segment)); | |
| 63 | + | |
| 64 | +const derivePathGroup = (pathTemplate: string): string => { | |
| 65 | + for (const segment of pathSegmentsFromTemplate(pathTemplate)) { | |
| 66 | + const lower = segment.toLowerCase(); | |
| 67 | + if (VERSION_SEGMENT_REGEX.test(lower)) { | |
| 68 | + continue; | |
| 69 | + } | |
| 70 | + if (IGNORED_PATH_SEGMENTS.has(lower)) { | |
| 71 | + continue; | |
| 72 | + } | |
| 73 | + if (isPathParameterSegment(segment)) { | |
| 74 | + continue; | |
| 75 | + } | |
| 76 | + | |
| 77 | + return normalizeGroupSegment(segment) ?? "root"; | |
| 78 | + } | |
| 79 | + | |
| 80 | + return "root"; | |
| 81 | +}; | |
| 82 | + | |
| 83 | +const splitOperationIdSegments = (value: string): Array<string> => | |
| 84 | + value | |
| 85 | + .split(/[/.]+/) | |
| 86 | + .map((segment) => segment.trim()) | |
| 87 | + .filter((segment) => segment.length > 0); | |
| 88 | + | |
| 89 | +const deriveLeafSeed = (tool: OpenApiExtractedTool, group: string): string => { | |
| 90 | + const preferredSource = tool.operationId ?? tool.toolId; | |
| 91 | + const segments = splitOperationIdSegments(preferredSource); | |
| 92 | + if (segments.length > 1) { | |
| 93 | + const [first, ...rest] = segments; | |
| 94 | + if ((normalizeGroupSegment(first) ?? first) === group && rest.length > 0) { | |
| 95 | + return rest.join(" "); | |
| 96 | + } | |
| 97 | + } | |
| 98 | + | |
| 99 | + return preferredSource; | |
| 100 | +}; | |
| 101 | + | |
| 102 | +const fallbackLeafSeed = (tool: OpenApiExtractedTool, group: string): string => { | |
| 103 | + const relevantSegments = pathSegmentsFromTemplate(tool.path) | |
| 104 | + .filter((segment) => !VERSION_SEGMENT_REGEX.test(segment.toLowerCase())) | |
| 105 | + .filter((segment) => !IGNORED_PATH_SEGMENTS.has(segment.toLowerCase())) | |
| 106 | + .filter((segment) => !isPathParameterSegment(segment)) | |
| 107 | + .map((segment) => normalizeGroupSegment(segment) ?? segment) | |
| 108 | + .filter((segment) => segment !== group); | |
| 109 | + | |
| 110 | + const segmentSuffix = relevantSegments | |
| 111 | + .map((segment) => toPascalCase(segment)) | |
| 112 | + .join(""); | |
| 113 | + | |
| 114 | + return `${tool.method}${segmentSuffix || "Operation"}`; | |
| 115 | +}; | |
| 116 | + | |
| 117 | +const deriveLeaf = (tool: OpenApiExtractedTool, group: string): string => { | |
| 118 | + const preferred = toCamelCase(deriveLeafSeed(tool, group)); | |
| 119 | + if (preferred.length > 0 && preferred !== group) { | |
| 120 | + return preferred; | |
| 121 | + } | |
| 122 | + | |
| 123 | + return toCamelCase(fallbackLeafSeed(tool, group)); | |
| 124 | +}; | |
| 125 | + | |
| 126 | +const defaultDescription = (tool: OpenApiExtractedTool): string => | |
| 127 | + tool.description ?? `${tool.method.toUpperCase()} ${tool.path}`; | |
| 128 | + | |
| 129 | +export const OpenApiToolDefinitionSchema = Schema.Struct({ | |
| 130 | + toolId: Schema.String, | |
| 131 | + rawToolId: Schema.String, | |
| 132 | + operationId: Schema.optional(Schema.String), | |
| 133 | + name: Schema.String, | |
| 134 | + description: Schema.String, | |
| 135 | + group: Schema.String, | |
| 136 | + leaf: Schema.String, | |
| 137 | + tags: Schema.Array(Schema.String), | |
| 138 | + versionSegment: Schema.optional(Schema.String), | |
| 139 | + method: OpenApiHttpMethodSchema, | |
| 140 | + path: Schema.String, | |
| 141 | + invocation: OpenApiInvocationPayloadSchema, | |
| 142 | + operationHash: Schema.String, | |
| 143 | + typing: Schema.optional(DiscoveryTypingPayloadSchema), | |
| 144 | + documentation: Schema.optional(OpenApiToolDocumentationSchema), | |
| 145 | +}); | |
| 146 | + | |
| 147 | +export type OpenApiToolDefinition = typeof OpenApiToolDefinitionSchema.Type; | |
| 148 | + | |
| 149 | +type MutableOpenApiToolDefinition = { | |
| 150 | + -readonly [Key in keyof OpenApiToolDefinition]: OpenApiToolDefinition[Key]; | |
| 151 | +}; | |
| 152 | + | |
| 153 | +const withResolvedToolIds = ( | |
| 154 | + definitions: Array<Omit<OpenApiToolDefinition, "toolId">>, | |
| 155 | +): Array<OpenApiToolDefinition> => { | |
| 156 | + const staged: Array<MutableOpenApiToolDefinition> = definitions.map((definition) => ({ | |
| 157 | + ...definition, | |
| 158 | + toolId: `${definition.group}.${definition.leaf}`, | |
| 159 | + })); | |
| 160 | + | |
| 161 | + const applyCandidates = ( | |
| 162 | + candidates: Array<MutableOpenApiToolDefinition>, | |
| 163 | + factory: (definition: MutableOpenApiToolDefinition) => string, | |
| 164 | + ): void => { | |
| 165 | + const byToolId = new Map<string, Array<MutableOpenApiToolDefinition>>(); | |
| 166 | + for (const candidate of candidates) { | |
| 167 | + const bucket = byToolId.get(candidate.toolId) ?? []; | |
| 168 | + bucket.push(candidate); | |
| 169 | + byToolId.set(candidate.toolId, bucket); | |
| 170 | + } | |
| 171 | + | |
| 172 | + for (const bucket of byToolId.values()) { | |
| 173 | + if (bucket.length < 2) { | |
| 174 | + continue; | |
| 175 | + } | |
| 176 | + | |
| 177 | + for (const definition of bucket) { | |
| 178 | + definition.toolId = factory(definition); | |
| 179 | + } | |
| 180 | + } | |
| 181 | + }; | |
| 182 | + | |
| 183 | + applyCandidates(staged, (definition) => | |
| 184 | + definition.versionSegment | |
| 185 | + ? `${definition.group}.${definition.versionSegment}.${definition.leaf}` | |
| 186 | + : definition.toolId, | |
| 187 | + ); | |
| 188 | + | |
| 189 | + applyCandidates(staged, (definition) => { | |
| 190 | + const prefix = definition.versionSegment | |
| 191 | + ? `${definition.group}.${definition.versionSegment}` | |
| 192 | + : definition.group; | |
| 193 | + return `${prefix}.${definition.leaf}${toPascalCase(definition.method)}`; | |
| 194 | + }); | |
| 195 | + | |
| 196 | + applyCandidates(staged, (definition) => { | |
| 197 | + const prefix = definition.versionSegment | |
| 198 | + ? `${definition.group}.${definition.versionSegment}` | |
| 199 | + : definition.group; | |
| 200 | + return `${prefix}.${definition.leaf}${toPascalCase(definition.method)}${definition.operationHash.slice(0, 8)}`; | |
| 201 | + }); | |
| 202 | + | |
| 203 | + return staged; | |
| 204 | +}; | |
| 205 | + | |
| 206 | +export const compileOpenApiToolDefinitions = ( | |
| 207 | + manifest: OpenApiToolManifest, | |
| 208 | +): Array<OpenApiToolDefinition> => { | |
| 209 | + const definitions = manifest.tools.map((tool) => { | |
| 210 | + const group = normalizeGroupSegment(tool.tags[0]) ?? derivePathGroup(tool.path); | |
| 211 | + const leaf = deriveLeaf(tool, group); | |
| 212 | + | |
| 213 | + return { | |
| 214 | + rawToolId: tool.toolId, | |
| 215 | + operationId: tool.operationId, | |
| 216 | + name: tool.name, | |
| 217 | + description: defaultDescription(tool), | |
| 218 | + group, | |
| 219 | + leaf, | |
| 220 | + tags: [...tool.tags], | |
| 221 | + versionSegment: deriveVersionSegment(tool.path), | |
| 222 | + method: tool.method, | |
| 223 | + path: tool.path, | |
| 224 | + invocation: tool.invocation, | |
| 225 | + operationHash: tool.operationHash, | |
| 226 | + typing: tool.typing, | |
| 227 | + documentation: tool.documentation, | |
| 228 | + } satisfies Omit<OpenApiToolDefinition, "toolId">; | |
| 229 | + }); | |
| 230 | + | |
| 231 | + return withResolvedToolIds(definitions).sort((left, right) => | |
| 232 | + left.toolId.localeCompare(right.toolId) | |
| 233 | + || left.rawToolId.localeCompare(right.rawToolId) | |
| 234 | + || left.operationHash.localeCompare(right.operationHash), | |
| 235 | + ); | |
| 236 | +}; | |
| 237 | + | |
| 238 | +export const openApiProviderDataJsonFromDefinition = ( | |
| 239 | + definition: OpenApiToolDefinition, | |
| 240 | +): string => | |
| 241 | + JSON.stringify({ | |
| 242 | + kind: "openapi", | |
| 243 | + toolId: definition.toolId, | |
| 244 | + rawToolId: definition.rawToolId, | |
| 245 | + operationId: definition.operationId, | |
| 246 | + group: definition.group, | |
| 247 | + leaf: definition.leaf, | |
| 248 | + tags: definition.tags, | |
| 249 | + versionSegment: definition.versionSegment, | |
| 250 | + method: definition.method, | |
| 251 | + path: definition.path, | |
| 252 | + operationHash: definition.operationHash, | |
| 253 | + documentation: definition.documentation, | |
| 254 | + }); | |
| @@ -0,0 +1,118 @@ | ||
| 1 | +import { describe, expect, it } from "@effect/vitest"; | |
| 2 | +import * as Effect from "effect/Effect"; | |
| 3 | + | |
| 4 | +import { extractOpenApiManifest } from "./openapi-extraction"; | |
| 5 | +import { createOpenApiToolsFromManifest } from "./openapi-tools"; | |
| 6 | +import type { OpenApiJsonObject } from "./openapi-types"; | |
| 7 | + | |
| 8 | +const VERCEL_OPENAPI_SPEC_URL = "https://openapi.vercel.sh/"; | |
| 9 | + | |
| 10 | +const fetchVercelOpenApiSpec = Effect.tryPromise({ | |
| 11 | + try: async () => { | |
| 12 | + const response = await fetch(VERCEL_OPENAPI_SPEC_URL); | |
| 13 | + if (!response.ok) { | |
| 14 | + throw new Error(`Failed to fetch Vercel OpenAPI spec: HTTP ${response.status} ${response.statusText}`); | |
| 15 | + } | |
| 16 | + | |
| 17 | + return response.json() as Promise<unknown>; | |
| 18 | + }, | |
| 19 | + catch: (cause) => | |
| 20 | + cause instanceof Error ? cause : new Error(String(cause)), | |
| 21 | +}); | |
| 22 | + | |
| 23 | +type ParsedInputSchema = { | |
| 24 | + type?: string; | |
| 25 | + properties?: Record<string, unknown>; | |
| 26 | + required?: Array<string>; | |
| 27 | +}; | |
| 28 | + | |
| 29 | +const parseInputSchema = (inputSchemaJson: string | undefined): ParsedInputSchema | null => | |
| 30 | + inputSchemaJson ? (JSON.parse(inputSchemaJson) as ParsedInputSchema) : null; | |
| 31 | + | |
| 32 | +const expectedTools = [ | |
| 33 | + { | |
| 34 | + toolId: "createAuthToken", | |
| 35 | + method: "post", | |
| 36 | + path: "/v3/user/tokens", | |
| 37 | + tags: ["authentication"], | |
| 38 | + requiredInputs: ["body"], | |
| 39 | + expectedPath: "source.vercel.authentication.createAuthToken", | |
| 40 | + }, | |
| 41 | + { | |
| 42 | + toolId: "getProjectMembers", | |
| 43 | + method: "get", | |
| 44 | + path: "/v1/projects/{idOrName}/members", | |
| 45 | + tags: ["projectMembers"], | |
| 46 | + requiredInputs: ["idOrName"], | |
| 47 | + expectedPath: "source.vercel.projectMembers.getProjectMembers", | |
| 48 | + }, | |
| 49 | + { | |
| 50 | + toolId: "addProjectDomain", | |
| 51 | + method: "post", | |
| 52 | + path: "/v10/projects/{idOrName}/domains", | |
| 53 | + tags: ["projects"], | |
| 54 | + requiredInputs: ["body", "idOrName"], | |
| 55 | + expectedPath: "source.vercel.projects.addProjectDomain", | |
| 56 | + }, | |
| 57 | + { | |
| 58 | + toolId: "getDeploymentEvents", | |
| 59 | + method: "get", | |
| 60 | + path: "/v3/deployments/{idOrUrl}/events", | |
| 61 | + tags: ["deployments"], | |
| 62 | + requiredInputs: ["idOrUrl"], | |
| 63 | + expectedPath: "source.vercel.deployments.getDeploymentEvents", | |
| 64 | + }, | |
| 65 | + { | |
| 66 | + toolId: "create-event", | |
| 67 | + method: "post", | |
| 68 | + path: "/v1/installations/{integrationConfigurationId}/events", | |
| 69 | + tags: ["marketplace"], | |
| 70 | + requiredInputs: ["body", "integrationConfigurationId"], | |
| 71 | + expectedPath: "source.vercel.marketplace.createEvent", | |
| 72 | + }, | |
| 73 | +] as const; | |
| 74 | + | |
| 75 | +describe("openapi-extraction real specs", () => { | |
| 76 | + it.effect("extracts and derives tool paths from Vercel's live OpenAPI spec", () => | |
| 77 | + Effect.gen(function* () { | |
| 78 | + const spec = (yield* fetchVercelOpenApiSpec) as OpenApiJsonObject; | |
| 79 | + const manifest = yield* extractOpenApiManifest("vercel", spec); | |
| 80 | + | |
| 81 | + expect(manifest.version).toBe(2); | |
| 82 | + expect(manifest.tools.length).toBeGreaterThan(250); | |
| 83 | + | |
| 84 | + const selectedTools = expectedTools.map((expectedTool) => { | |
| 85 | + const tool = manifest.tools.find((candidate) => candidate.toolId === expectedTool.toolId); | |
| 86 | + | |
| 87 | + expect(tool).toBeDefined(); | |
| 88 | + expect(tool?.method).toBe(expectedTool.method); | |
| 89 | + expect(tool?.path).toBe(expectedTool.path); | |
| 90 | + expect(tool?.tags).toEqual(expectedTool.tags); | |
| 91 | + | |
| 92 | + const inputSchema = parseInputSchema(tool?.typing?.inputSchemaJson); | |
| 93 | + expect(inputSchema?.type).toBe("object"); | |
| 94 | + expect(Object.keys(inputSchema?.properties ?? {})).toEqual( | |
| 95 | + expect.arrayContaining([...expectedTool.requiredInputs]), | |
| 96 | + ); | |
| 97 | + expect(inputSchema?.required ?? []).toEqual( | |
| 98 | + expect.arrayContaining([...expectedTool.requiredInputs]), | |
| 99 | + ); | |
| 100 | + | |
| 101 | + return tool!; | |
| 102 | + }); | |
| 103 | + | |
| 104 | + const tools = createOpenApiToolsFromManifest({ | |
| 105 | + manifest: { | |
| 106 | + ...manifest, | |
| 107 | + tools: selectedTools, | |
| 108 | + }, | |
| 109 | + baseUrl: "https://api.vercel.com", | |
| 110 | + namespace: "source.vercel", | |
| 111 | + }); | |
| 112 | + | |
| 113 | + expect(Object.keys(tools).sort()).toEqual( | |
| 114 | + expectedTools.map((tool) => tool.expectedPath).sort(), | |
| 115 | + ); | |
| 116 | + }), | |
| 117 | + 120_000); | |
| 118 | +}); | |
Binary file not shown.
| @@ -0,0 +1,135 @@ | ||
| 1 | +type JsonObject = Record<string, unknown>; | |
| 2 | + | |
| 3 | +type DiscoveryTypingLike = { | |
| 4 | + inputSchemaJson?: string; | |
| 5 | + outputSchemaJson?: string; | |
| 6 | +}; | |
| 7 | + | |
| 8 | +const isJsonObject = (value: unknown): value is JsonObject => | |
| 9 | + typeof value === "object" && value !== null && !Array.isArray(value); | |
| 10 | + | |
| 11 | +const parseJson = (value: string): unknown | null => { | |
| 12 | + try { | |
| 13 | + return JSON.parse(value) as unknown; | |
| 14 | + } catch { | |
| 15 | + return null; | |
| 16 | + } | |
| 17 | +}; | |
| 18 | + | |
| 19 | +const resolveSchemaNode = ( | |
| 20 | + value: unknown, | |
| 21 | + refHintTable: Readonly<Record<string, string>>, | |
| 22 | + parsedHintCache: Map<string, unknown>, | |
| 23 | + activeRefs: ReadonlySet<string>, | |
| 24 | +): unknown => { | |
| 25 | + if (Array.isArray(value)) { | |
| 26 | + return value.map((item) => | |
| 27 | + resolveSchemaNode(item, refHintTable, parsedHintCache, activeRefs) | |
| 28 | + ); | |
| 29 | + } | |
| 30 | + | |
| 31 | + if (!isJsonObject(value)) { | |
| 32 | + return value; | |
| 33 | + } | |
| 34 | + | |
| 35 | + const ref = typeof value.$ref === "string" ? value.$ref : null; | |
| 36 | + if (ref && ref.startsWith("#/") && !activeRefs.has(ref)) { | |
| 37 | + let resolvedRefValue = parsedHintCache.get(ref); | |
| 38 | + | |
| 39 | + if (resolvedRefValue === undefined) { | |
| 40 | + const rawHint = refHintTable[ref]; | |
| 41 | + resolvedRefValue = typeof rawHint === "string" ? parseJson(rawHint) : null; | |
| 42 | + parsedHintCache.set(ref, resolvedRefValue); | |
| 43 | + } | |
| 44 | + | |
| 45 | + if (resolvedRefValue !== null && resolvedRefValue !== undefined) { | |
| 46 | + const nextActiveRefs = new Set(activeRefs); | |
| 47 | + nextActiveRefs.add(ref); | |
| 48 | + | |
| 49 | + const { $ref: _ignoredRef, ...rest } = value; | |
| 50 | + const resolvedTarget = resolveSchemaNode( | |
| 51 | + resolvedRefValue, | |
| 52 | + refHintTable, | |
| 53 | + parsedHintCache, | |
| 54 | + nextActiveRefs, | |
| 55 | + ); | |
| 56 | + | |
| 57 | + if (Object.keys(rest).length === 0) { | |
| 58 | + return resolvedTarget; | |
| 59 | + } | |
| 60 | + | |
| 61 | + const resolvedRest = resolveSchemaNode( | |
| 62 | + rest, | |
| 63 | + refHintTable, | |
| 64 | + parsedHintCache, | |
| 65 | + activeRefs, | |
| 66 | + ); | |
| 67 | + | |
| 68 | + if (isJsonObject(resolvedTarget) && isJsonObject(resolvedRest)) { | |
| 69 | + return { ...resolvedTarget, ...resolvedRest }; | |
| 70 | + } | |
| 71 | + | |
| 72 | + return resolvedTarget; | |
| 73 | + } | |
| 74 | + } | |
| 75 | + | |
| 76 | + const next: JsonObject = {}; | |
| 77 | + for (const [key, nestedValue] of Object.entries(value)) { | |
| 78 | + next[key] = resolveSchemaNode( | |
| 79 | + nestedValue, | |
| 80 | + refHintTable, | |
| 81 | + parsedHintCache, | |
| 82 | + activeRefs, | |
| 83 | + ); | |
| 84 | + } | |
| 85 | + | |
| 86 | + return next; | |
| 87 | +}; | |
| 88 | + | |
| 89 | +export const resolveSchemaJsonWithRefHints = ( | |
| 90 | + schemaJson: string | undefined, | |
| 91 | + refHintTable: Readonly<Record<string, string>> | undefined, | |
| 92 | +): string | null => { | |
| 93 | + if (!schemaJson) { | |
| 94 | + return null; | |
| 95 | + } | |
| 96 | + | |
| 97 | + const parsedSchema = parseJson(schemaJson); | |
| 98 | + if (parsedSchema === null) { | |
| 99 | + return schemaJson; | |
| 100 | + } | |
| 101 | + | |
| 102 | + if (!refHintTable || Object.keys(refHintTable).length === 0) { | |
| 103 | + return schemaJson; | |
| 104 | + } | |
| 105 | + | |
| 106 | + const resolved = resolveSchemaNode( | |
| 107 | + parsedSchema, | |
| 108 | + refHintTable, | |
| 109 | + new Map<string, unknown>(), | |
| 110 | + new Set<string>(), | |
| 111 | + ); | |
| 112 | + | |
| 113 | + try { | |
| 114 | + return JSON.stringify(resolved); | |
| 115 | + } catch { | |
| 116 | + return schemaJson; | |
| 117 | + } | |
| 118 | +}; | |
| 119 | + | |
| 120 | +export const resolveTypingSchemasWithRefHints = ( | |
| 121 | + typing: DiscoveryTypingLike | undefined, | |
| 122 | + refHintTable: Readonly<Record<string, string>> | undefined, | |
| 123 | +): { | |
| 124 | + inputSchemaJson: string | null; | |
| 125 | + outputSchemaJson: string | null; | |
| 126 | +} => ({ | |
| 127 | + inputSchemaJson: resolveSchemaJsonWithRefHints( | |
| 128 | + typing?.inputSchemaJson, | |
| 129 | + refHintTable, | |
| 130 | + ), | |
| 131 | + outputSchemaJson: resolveSchemaJsonWithRefHints( | |
| 132 | + typing?.outputSchemaJson, | |
| 133 | + refHintTable, | |
| 134 | + ), | |
| 135 | +}); | |
| @@ -0,0 +1,106 @@ | ||
| 1 | +import { typeSignatureFromSchemaJson } from "@executor-v3/codemode-core"; | |
| 2 | + | |
| 3 | +import { | |
| 4 | + resolveTypingSchemasWithRefHints, | |
| 5 | +} from "./openapi-schema-refs"; | |
| 6 | +import type { | |
| 7 | + OpenApiExample, | |
| 8 | + OpenApiToolDocumentation, | |
| 9 | + OpenApiToolManifest, | |
| 10 | +} from "./openapi-types"; | |
| 11 | +import type { OpenApiToolDefinition } from "./openapi-definitions"; | |
| 12 | + | |
| 13 | +const parseJson = (value: string): unknown | undefined => { | |
| 14 | + try { | |
| 15 | + return JSON.parse(value) as unknown; | |
| 16 | + } catch { | |
| 17 | + return undefined; | |
| 18 | + } | |
| 19 | +}; | |
| 20 | + | |
| 21 | +const firstExample = ( | |
| 22 | + examples: ReadonlyArray<OpenApiExample> | undefined, | |
| 23 | +): OpenApiExample | undefined => examples?.[0]; | |
| 24 | + | |
| 25 | +const buildExampleInputJson = ( | |
| 26 | + documentation: OpenApiToolDocumentation | undefined, | |
| 27 | +): string | undefined => { | |
| 28 | + if (!documentation) { | |
| 29 | + return undefined; | |
| 30 | + } | |
| 31 | + | |
| 32 | + const input: Record<string, unknown> = {}; | |
| 33 | + | |
| 34 | + for (const parameter of documentation.parameters) { | |
| 35 | + const example = firstExample(parameter.examples); | |
| 36 | + if (!example) { | |
| 37 | + continue; | |
| 38 | + } | |
| 39 | + | |
| 40 | + const parsedValue = parseJson(example.valueJson); | |
| 41 | + if (parsedValue !== undefined) { | |
| 42 | + input[parameter.name] = parsedValue; | |
| 43 | + } | |
| 44 | + } | |
| 45 | + | |
| 46 | + const requestBodyExample = firstExample(documentation.requestBody?.examples); | |
| 47 | + if (requestBodyExample) { | |
| 48 | + const parsedValue = parseJson(requestBodyExample.valueJson); | |
| 49 | + if (parsedValue !== undefined) { | |
| 50 | + input.body = parsedValue; | |
| 51 | + } | |
| 52 | + } | |
| 53 | + | |
| 54 | + return Object.keys(input).length > 0 ? JSON.stringify(input) : undefined; | |
| 55 | +}; | |
| 56 | + | |
| 57 | +const buildExampleOutputJson = ( | |
| 58 | + documentation: OpenApiToolDocumentation | undefined, | |
| 59 | +): string | undefined => firstExample(documentation?.response?.examples)?.valueJson; | |
| 60 | + | |
| 61 | +export type OpenApiToolPresentation = { | |
| 62 | + inputType: string; | |
| 63 | + outputType: string; | |
| 64 | + inputSchemaJson?: string; | |
| 65 | + outputSchemaJson?: string; | |
| 66 | + exampleInputJson?: string; | |
| 67 | + exampleOutputJson?: string; | |
| 68 | + providerDataJson: string; | |
| 69 | +}; | |
| 70 | + | |
| 71 | +export const buildOpenApiToolPresentation = (input: { | |
| 72 | + manifest: OpenApiToolManifest; | |
| 73 | + definition: OpenApiToolDefinition; | |
| 74 | +}): OpenApiToolPresentation => { | |
| 75 | + const resolvedSchemas = resolveTypingSchemasWithRefHints( | |
| 76 | + input.definition.typing, | |
| 77 | + input.manifest.refHintTable, | |
| 78 | + ); | |
| 79 | + const inputSchemaJson = resolvedSchemas.inputSchemaJson ?? undefined; | |
| 80 | + const outputSchemaJson = resolvedSchemas.outputSchemaJson ?? undefined; | |
| 81 | + const exampleInputJson = buildExampleInputJson(input.definition.documentation); | |
| 82 | + const exampleOutputJson = buildExampleOutputJson(input.definition.documentation); | |
| 83 | + | |
| 84 | + return { | |
| 85 | + inputType: typeSignatureFromSchemaJson(inputSchemaJson, "unknown", 320), | |
| 86 | + outputType: typeSignatureFromSchemaJson(outputSchemaJson, "unknown", 320), | |
| 87 | + ...(inputSchemaJson ? { inputSchemaJson } : {}), | |
| 88 | + ...(outputSchemaJson ? { outputSchemaJson } : {}), | |
| 89 | + ...(exampleInputJson ? { exampleInputJson } : {}), | |
| 90 | + ...(exampleOutputJson ? { exampleOutputJson } : {}), | |
| 91 | + providerDataJson: JSON.stringify({ | |
| 92 | + kind: "openapi", | |
| 93 | + toolId: input.definition.toolId, | |
| 94 | + rawToolId: input.definition.rawToolId, | |
| 95 | + operationId: input.definition.operationId, | |
| 96 | + group: input.definition.group, | |
| 97 | + leaf: input.definition.leaf, | |
| 98 | + tags: input.definition.tags, | |
| 99 | + versionSegment: input.definition.versionSegment, | |
| 100 | + method: input.definition.method, | |
| 101 | + path: input.definition.path, | |
| 102 | + operationHash: input.definition.operationHash, | |
| 103 | + documentation: input.definition.documentation, | |
| 104 | + }), | |
| 105 | + }; | |
| 106 | +}; | |
| @@ -277,6 +277,8 @@ describe("openapi-tools", () => { · 276 unmodified lines | ||
| 277 | 277 | |
| 278 | 278 | const resolvedGet = resolveToolDefinition(tools["source.demo.repos.getRepo"]!); |
| 279 | 279 | expect(resolvedGet.metadata?.sourceKey).toBe("api.demo"); |
| 280 | + expect(resolvedGet.metadata?.providerKind).toBe("openapi"); | |
| 281 | + expect(resolvedGet.metadata?.providerDataJson).toContain('"group":"repos"'); | |
| 280 | 282 | expect(resolvedGet.metadata?.inputSchemaJson).toBeDefined(); |
| 281 | 283 | }), |
| 282 | 284 | ); |
| @@ -303,6 +305,7 @@ describe("openapi-tools", () => { · 20 unmodified lines | ||
| 303 | 305 | ); |
| 304 | 306 | |
| 305 | 307 | expect(result).toEqual({ |
| 308 | + ok: true, | |
| 306 | 309 | status: 200, |
| 307 | 310 | headers: expect.any(Object), |
| 308 | 311 | body: { |
| @@ -337,6 +340,7 @@ describe("openapi-tools", () => { · 28 unmodified lines | ||
| 337 | 340 | ); |
| 338 | 341 | |
| 339 | 342 | expect(getResult).toEqual({ |
| 343 | + ok: true, | |
| 340 | 344 | status: 200, |
| 341 | 345 | headers: expect.any(Object), |
| 342 | 346 | body: { |
| @@ -354,6 +358,7 @@ describe("openapi-tools", () => { · 11 unmodified lines | ||
| 354 | 358 | ); |
| 355 | 359 | |
| 356 | 360 | expect(postResult).toEqual({ |
| 361 | + ok: true, | |
| 357 | 362 | status: 201, |
| 358 | 363 | headers: expect.any(Object), |
| 359 | 364 | body: { |
| @@ -433,6 +438,7 @@ describe("openapi-tools", () => { · 73 unmodified lines | ||
| 433 | 438 | ); |
| 434 | 439 | |
| 435 | 440 | expect(result).toEqual({ |
| 441 | + ok: true, | |
| 436 | 442 | status: 200, |
| 437 | 443 | headers: expect.any(Object), |
| 438 | 444 | body: { |
| @@ -482,6 +488,7 @@ describe("openapi-tools", () => { · 43 unmodified lines | ||
| 482 | 488 | ); |
| 483 | 489 | |
| 484 | 490 | expect(result).toEqual({ |
| 491 | + ok: true, | |
| 485 | 492 | status: 200, |
| 486 | 493 | headers: expect.any(Object), |
| 487 | 494 | body: { |
| @@ -493,4 +500,56 @@ describe("openapi-tools", () => { · 5 unmodified lines | ||
| 493 | 500 | expect(server.requests[0]?.path).toBe("/repos/octocat/hello-world"); |
| 494 | 501 | }), |
| 495 | 502 | ); |
| 503 | + | |
| 504 | + it.effect("normalizes slash-based operationIds into grouped dotted tool paths", () => | |
| 505 | + Effect.gen(function* () { | |
| 506 | + const tools = createOpenApiToolsFromManifest({ | |
| 507 | + manifest: { | |
| 508 | + version: 2, | |
| 509 | + sourceHash: "fixture", | |
| 510 | + tools: [ | |
| 511 | + { | |
| 512 | + toolId: "actions/create-workflow-dispatch", | |
| 513 | + operationId: "actions/create-workflow-dispatch", | |
| 514 | + tags: ["actions"], | |
| 515 | + name: "Create workflow dispatch", | |
| 516 | + description: null, | |
| 517 | + method: "post", | |
| 518 | + path: "/repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches", | |
| 519 | + invocation: { | |
| 520 | + method: "post", | |
| 521 | + pathTemplate: "/repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches", | |
| 522 | + parameters: [], | |
| 523 | + requestBody: null, | |
| 524 | + }, | |
| 525 | + operationHash: "hash_actions_create_workflow_dispatch", | |
| 526 | + }, | |
| 527 | + { | |
| 528 | + toolId: "users/get-authenticated", | |
| 529 | + operationId: "users/get-authenticated", | |
| 530 | + tags: ["users"], | |
| 531 | + name: "Get authenticated user", | |
| 532 | + description: null, | |
| 533 | + method: "get", | |
| 534 | + path: "/user", | |
| 535 | + invocation: { | |
| 536 | + method: "get", | |
| 537 | + pathTemplate: "/user", | |
| 538 | + parameters: [], | |
| 539 | + requestBody: null, | |
| 540 | + }, | |
| 541 | + operationHash: "hash_users_get_authenticated", | |
| 542 | + }, | |
| 543 | + ], | |
| 544 | + }, | |
| 545 | + baseUrl: "https://api.github.com", | |
| 546 | + namespace: "source.github", | |
| 547 | + }); | |
| 548 | + | |
| 549 | + expect(Object.keys(tools).sort()).toEqual([ | |
| 550 | + "source.github.actions.createWorkflowDispatch", | |
| 551 | + "source.github.users.getAuthenticated", | |
| 552 | + ]); | |
| 553 | + }), | |
| 554 | + ); | |
| 496 | 555 | }); |
| @@ -14,6 +14,11 @@ import { · 13 unmodified lines | ||
| 14 | 14 | unknownInputSchema, |
| 15 | 15 | } from "@executor-v3/codemode-core"; |
| 16 | 16 | |
| 17 | +import { | |
| 18 | + compileOpenApiToolDefinitions, | |
| 19 | + type OpenApiToolDefinition, | |
| 20 | +} from "./openapi-definitions"; | |
| 21 | +import { buildOpenApiToolPresentation } from "./openapi-tool-presentation"; | |
| 17 | 22 | import { |
| 18 | 23 | extractOpenApiManifest, |
| 19 | 24 | type OpenApiExtractionError, |
| @@ -35,6 +40,44 @@ export class OpenApiToolInvocationError extends Data.TaggedError( · 15 unmodified lines | ||
| 35 | 40 | details: string | null; |
| 36 | 41 | }> {} |
| 37 | 42 | |
| 43 | +const BLOCKED_RESPONSE_HEADER_NAMES = new Set([ | |
| 44 | + "authorization", | |
| 45 | + "authentication-info", | |
| 46 | + "cookie", | |
| 47 | + "proxy-authenticate", | |
| 48 | + "proxy-authorization", | |
| 49 | + "set-cookie", | |
| 50 | + "set-cookie2", | |
| 51 | + "www-authenticate", | |
| 52 | + "x-api-key", | |
| 53 | + "x-auth-token", | |
| 54 | + "x-csrf-token", | |
| 55 | +]); | |
| 56 | + | |
| 57 | +const NOISY_RESPONSE_HEADER_NAMES = new Set([ | |
| 58 | + "alt-svc", | |
| 59 | + "cf-ray", | |
| 60 | + "server", | |
| 61 | + "traceparent", | |
| 62 | + "tracestate", | |
| 63 | + "via", | |
| 64 | + "x-cache", | |
| 65 | + "x-cache-hits", | |
| 66 | + "x-powered-by", | |
| 67 | + "x-request-id", | |
| 68 | + "x-runtime", | |
| 69 | + "x-served-by", | |
| 70 | + "x-trace-id", | |
| 71 | +]); | |
| 72 | + | |
| 73 | +const NOISY_RESPONSE_HEADER_PREFIXES = [ | |
| 74 | + "cf-", | |
| 75 | + "trace-", | |
| 76 | + "x-amz-cf-", | |
| 77 | + "x-b3-", | |
| 78 | + "x-cloud-trace-", | |
| 79 | +]; | |
| 80 | + | |
| 38 | 81 | const isRecord = (value: unknown): value is Record<string, unknown> => |
| 39 | 82 | typeof value === "object" && value !== null && !Array.isArray(value); |
| 40 | 83 | |
| @@ -201,13 +244,35 @@ const normalizeHttpUrl = (value: string): string => { · 160 unmodified lines | ||
| 201 | 244 | } |
| 202 | 245 | }; |
| 203 | 246 | |
| 247 | +const shouldDropResponseHeader = (headerName: string): boolean => | |
| 248 | + BLOCKED_RESPONSE_HEADER_NAMES.has(headerName) | |
| 249 | + || NOISY_RESPONSE_HEADER_NAMES.has(headerName) | |
| 250 | + || NOISY_RESPONSE_HEADER_PREFIXES.some((prefix) => headerName.startsWith(prefix)); | |
| 251 | + | |
| 252 | +const sanitizeResponseHeaders = ( | |
| 253 | + headers: Readonly<Record<string, string>>, | |
| 254 | +): Record<string, string> => { | |
| 255 | + const sanitized: Record<string, string> = {}; | |
| 256 | + | |
| 257 | + for (const [rawName, rawValue] of Object.entries(headers)) { | |
| 258 | + const name = rawName.trim().toLowerCase(); | |
| 259 | + if (name.length === 0 || shouldDropResponseHeader(name)) { | |
| 260 | + continue; | |
| 261 | + } | |
| 262 | + | |
| 263 | + sanitized[name] = rawValue.length > 4000 ? `${rawValue.slice(0, 4000)}...` : rawValue; | |
| 264 | + } | |
| 265 | + | |
| 266 | + return sanitized; | |
| 267 | +}; | |
| 268 | + | |
| 204 | 269 | const decodeHttpClientResponseBody = ( |
| 205 | 270 | response: Awaited<ReturnType<HttpClient.HttpClient["execute"]>> extends Effect.Effect< |
| 206 | − infer A, | |
| 207 | − infer _E, | |
| 208 | − infer _R | |
| 271 | + infer Value, | |
| 272 | + infer _Error, | |
| 273 | + infer _Requirements | |
| 209 | 274 | > |
| 210 | − ? A | |
| 275 | + ? Value | |
| 211 | 276 | : never, |
| 212 | 277 | ): Effect.Effect<unknown, Error, never> => { |
| 213 | 278 | const contentType = response.headers["content-type"]?.toLowerCase() ?? ""; |
| @@ -242,8 +307,7 @@ const inputSchemaFromTypingJson = (inputSchemaJson: string | undefined) => { · 28 unmodified lines | ||
| 242 | 307 | } |
| 243 | 308 | }; |
| 244 | 309 | |
| 245 | − | |
| 246 | −type CreateOpenApiToolsFromManifestInput = { | |
| 310 | +export type CreateOpenApiToolsFromManifestInput = { | |
| 247 | 311 | manifest: OpenApiToolManifest; |
| 248 | 312 | baseUrl: string; |
| 249 | 313 | namespace?: string; |
| @@ -365,32 +429,38 @@ const buildFetchRequest = (input: { · 115 unmodified lines | ||
| 365 | 429 | }; |
| 366 | 430 | }; |
| 367 | 431 | |
| 432 | +const createToolPath = (namespace: string | undefined, definition: OpenApiToolDefinition): string => | |
| 433 | + namespace ? `${namespace}.${definition.toolId}` : definition.toolId; | |
| 434 | + | |
| 368 | 435 | export const createOpenApiToolsFromManifest = ( |
| 369 | 436 | input: CreateOpenApiToolsFromManifestInput, |
| 370 | 437 | ): ToolMap => { |
| 371 | 438 | const baseUrl = normalizeHttpUrl(input.baseUrl); |
| 372 | 439 | const sourceKey = input.sourceKey ?? "openapi.generated"; |
| 373 | − const namespace = input.namespace; | |
| 374 | 440 | const defaultHeaders = input.defaultHeaders ?? {}; |
| 375 | 441 | const credentialHeaders = input.credentialHeaders ?? {}; |
| 376 | 442 | const httpClientLayer = input.httpClientLayer ?? FetchHttpClient.layer; |
| 377 | 443 | |
| 444 | + const definitions = compileOpenApiToolDefinitions(input.manifest); | |
| 378 | 445 | const result: ToolMap = {}; |
| 379 | 446 | |
| 380 | − for (const extracted of input.manifest.tools) { | |
| 381 | − const toolPath = namespace | |
| 382 | − ? `${namespace}.${extracted.toolId}` | |
| 383 | − : extracted.toolId; | |
| 447 | + for (const definition of definitions) { | |
| 448 | + const toolPath = createToolPath(input.namespace, definition); | |
| 449 | + const presentation = buildOpenApiToolPresentation({ | |
| 450 | + manifest: input.manifest, | |
| 451 | + definition, | |
| 452 | + }); | |
| 384 | 453 | |
| 385 | − const description = extracted.description ?? `${extracted.method.toUpperCase()} ${extracted.path}`; | |
| 386 | 454 | result[toolPath] = toTool({ |
| 387 | 455 | tool: { |
| 388 | − description, | |
| 389 | − inputSchema: inputSchemaFromTypingJson(extracted.typing?.inputSchemaJson), | |
| 456 | + description: definition.description, | |
| 457 | + inputSchema: inputSchemaFromTypingJson( | |
| 458 | + presentation.inputSchemaJson ?? definition.typing?.inputSchemaJson, | |
| 459 | + ), | |
| 390 | 460 | execute: async (args: unknown) => { |
| 391 | 461 | const decodedArgs = asToolArgs(args); |
| 392 | 462 | const request = buildFetchRequest({ |
| 393 | − payload: extracted.invocation, | |
| 463 | + payload: definition.invocation, | |
| 394 | 464 | args: decodedArgs, |
| 395 | 465 | baseUrl, |
| 396 | 466 | defaultHeaders, |
| @@ -422,8 +492,9 @@ export const createOpenApiToolsFromManifest = ( · 25 unmodified lines | ||
| 422 | 492 | const body = yield* decodeHttpClientResponseBody(response); |
| 423 | 493 | |
| 424 | 494 | return { |
| 495 | + ok: response.status >= 200 && response.status < 300, | |
| 425 | 496 | status: response.status, |
| 426 | − headers: { ...response.headers }, | |
| 497 | + headers: sanitizeResponseHeaders(response.headers), | |
| 427 | 498 | body, |
| 428 | 499 | }; |
| 429 | 500 | }).pipe( |
| @@ -434,9 +505,14 @@ export const createOpenApiToolsFromManifest = ( · 4 unmodified lines | ||
| 434 | 505 | }, |
| 435 | 506 | metadata: { |
| 436 | 507 | sourceKey, |
| 437 | − inputSchemaJson: extracted.typing?.inputSchemaJson, | |
| 438 | − outputSchemaJson: extracted.typing?.outputSchemaJson, | |
| 439 | − refHintKeys: extracted.typing?.refHintKeys, | |
| 508 | + inputType: presentation.inputType, | |
| 509 | + outputType: presentation.outputType, | |
| 510 | + inputSchemaJson: presentation.inputSchemaJson, | |
| 511 | + outputSchemaJson: presentation.outputSchemaJson, | |
| 512 | + exampleInputJson: presentation.exampleInputJson, | |
| 513 | + exampleOutputJson: presentation.exampleOutputJson, | |
| 514 | + providerKind: "openapi", | |
| 515 | + providerDataJson: presentation.providerDataJson, | |
| 440 | 516 | }, |
| 441 | 517 | }); |
| 442 | 518 | } |
| @@ -454,20 +530,22 @@ export const createOpenApiToolsFromSpec = (input: { · 11 unmodified lines | ||
| 454 | 530 | credentialHeaders?: Readonly<Record<string, string>>; |
| 455 | 531 | httpClientLayer?: Layer.Layer<HttpClient.HttpClient, never, never>; |
| 456 | 532 | }): Effect.Effect< |
| 457 | − { manifest: OpenApiToolManifest; tools: ToolMap }, | |
| 533 | + { manifest: OpenApiToolManifest; definitions: Array<OpenApiToolDefinition>; tools: ToolMap }, | |
| 458 | 534 | OpenApiExtractionError |
| 459 | 535 | > => |
| 460 | 536 | Effect.map( |
| 461 | 537 | extractOpenApiManifest(input.sourceName, input.openApiSpec), |
| 462 | 538 | (manifest: OpenApiToolManifest) => ({ |
| 463 | − manifest, | |
| 464 | − tools: createOpenApiToolsFromManifest({ | |
| 465 | 539 | manifest, |
| 466 | − baseUrl: input.baseUrl, | |
| 467 | − namespace: input.namespace, | |
| 468 | − sourceKey: input.sourceKey, | |
| 469 | − defaultHeaders: input.defaultHeaders, | |
| 470 | − credentialHeaders: input.credentialHeaders, | |
| 471 | − httpClientLayer: input.httpClientLayer, | |
| 540 | + definitions: compileOpenApiToolDefinitions(manifest), | |
| 541 | + tools: createOpenApiToolsFromManifest({ | |
| 542 | + manifest, | |
| 543 | + baseUrl: input.baseUrl, | |
| 544 | + namespace: input.namespace, | |
| 545 | + sourceKey: input.sourceKey, | |
| 546 | + defaultHeaders: input.defaultHeaders, | |
| 547 | + credentialHeaders: input.credentialHeaders, | |
| 548 | + httpClientLayer: input.httpClientLayer, | |
| 549 | + }), | |
| 472 | 550 | }), |
| 473 | − })); | |
| 551 | + ); | |
| @@ -13,7 +13,6 @@ export type OpenApiJsonObject = { · 12 unmodified lines | ||
| 13 | 13 | [key: string]: OpenApiJsonValue; |
| 14 | 14 | }; |
| 15 | 15 | |
| 16 | −// Accepts Effect-generated OpenAPI specs and generic JSON-like OpenAPI objects. | |
| 17 | 16 | export type OpenApiSpecInput = string | OpenAPISpec | OpenApiJsonObject; |
| 18 | 17 | |
| 19 | 18 | export const OPEN_API_HTTP_METHODS = [ |
| @@ -64,8 +63,44 @@ export const DiscoveryTypingPayloadSchema = Schema.Struct({ · 44 unmodified lines | ||
| 64 | 63 | refHintKeys: Schema.optional(Schema.Array(Schema.String)), |
| 65 | 64 | }); |
| 66 | 65 | |
| 66 | +export const OpenApiExampleSchema = Schema.Struct({ | |
| 67 | + valueJson: Schema.String, | |
| 68 | + mediaType: Schema.optional(Schema.String), | |
| 69 | + label: Schema.optional(Schema.String), | |
| 70 | +}); | |
| 71 | + | |
| 72 | +export const OpenApiParameterDocumentationSchema = Schema.Struct({ | |
| 73 | + name: Schema.String, | |
| 74 | + location: OpenApiParameterLocationSchema, | |
| 75 | + required: Schema.Boolean, | |
| 76 | + description: Schema.optional(Schema.String), | |
| 77 | + examples: Schema.optional(Schema.Array(OpenApiExampleSchema)), | |
| 78 | +}); | |
| 79 | + | |
| 80 | +export const OpenApiRequestBodyDocumentationSchema = Schema.Struct({ | |
| 81 | + description: Schema.optional(Schema.String), | |
| 82 | + examples: Schema.optional(Schema.Array(OpenApiExampleSchema)), | |
| 83 | +}); | |
| 84 | + | |
| 85 | +export const OpenApiResponseDocumentationSchema = Schema.Struct({ | |
| 86 | + statusCode: Schema.String, | |
| 87 | + description: Schema.optional(Schema.String), | |
| 88 | + contentTypes: Schema.Array(Schema.String), | |
| 89 | + examples: Schema.optional(Schema.Array(OpenApiExampleSchema)), | |
| 90 | +}); | |
| 91 | + | |
| 92 | +export const OpenApiToolDocumentationSchema = Schema.Struct({ | |
| 93 | + summary: Schema.optional(Schema.String), | |
| 94 | + deprecated: Schema.optional(Schema.Boolean), | |
| 95 | + parameters: Schema.Array(OpenApiParameterDocumentationSchema), | |
| 96 | + requestBody: Schema.optional(OpenApiRequestBodyDocumentationSchema), | |
| 97 | + response: Schema.optional(OpenApiResponseDocumentationSchema), | |
| 98 | +}); | |
| 99 | + | |
| 67 | 100 | export const OpenApiExtractedToolSchema = Schema.Struct({ |
| 68 | 101 | toolId: Schema.String, |
| 102 | + operationId: Schema.optional(Schema.String), | |
| 103 | + tags: Schema.Array(Schema.String), | |
| 69 | 104 | name: Schema.String, |
| 70 | 105 | description: Schema.NullOr(Schema.String), |
| 71 | 106 | method: OpenApiHttpMethodSchema, |
| @@ -73,10 +108,11 @@ export const OpenApiExtractedToolSchema = Schema.Struct({ · 1 unmodified line | ||
| 73 | 108 | invocation: OpenApiInvocationPayloadSchema, |
| 74 | 109 | operationHash: Schema.String, |
| 75 | 110 | typing: Schema.optional(DiscoveryTypingPayloadSchema), |
| 111 | + documentation: Schema.optional(OpenApiToolDocumentationSchema), | |
| 76 | 112 | }); |
| 77 | 113 | |
| 78 | 114 | export const OpenApiToolManifestSchema = Schema.Struct({ |
| 79 | − version: Schema.Literal(1), | |
| 115 | + version: Schema.Literal(2), | |
| 80 | 116 | sourceHash: Schema.String, |
| 81 | 117 | tools: Schema.Array(OpenApiExtractedToolSchema), |
| 82 | 118 | refHintTable: Schema.optional( |
| @@ -93,5 +129,10 @@ export type OpenApiToolParameter = typeof OpenApiToolParameterSchema.Type; · 10 unmodified lines | ||
| 93 | 129 | export type OpenApiToolRequestBody = typeof OpenApiToolRequestBodySchema.Type; |
| 94 | 130 | export type OpenApiInvocationPayload = typeof OpenApiInvocationPayloadSchema.Type; |
| 95 | 131 | export type DiscoveryTypingPayload = typeof DiscoveryTypingPayloadSchema.Type; |
| 132 | +export type OpenApiExample = typeof OpenApiExampleSchema.Type; | |
| 133 | +export type OpenApiParameterDocumentation = typeof OpenApiParameterDocumentationSchema.Type; | |
| 134 | +export type OpenApiRequestBodyDocumentation = typeof OpenApiRequestBodyDocumentationSchema.Type; | |
| 135 | +export type OpenApiResponseDocumentation = typeof OpenApiResponseDocumentationSchema.Type; | |
| 136 | +export type OpenApiToolDocumentation = typeof OpenApiToolDocumentationSchema.Type; | |
| 96 | 137 | export type OpenApiExtractedTool = typeof OpenApiExtractedToolSchema.Type; |
| 97 | 138 | export type OpenApiToolManifest = typeof OpenApiToolManifestSchema.Type; |
| @@ -0,0 +1,37 @@ | ||
| 1 | +ALTER TABLE "tool_artifacts" ADD COLUMN "openapi_raw_tool_id" text;--> statement-breakpoint | |
| 2 | +ALTER TABLE "tool_artifacts" ADD COLUMN "openapi_operation_id" text;--> statement-breakpoint | |
| 3 | +ALTER TABLE "tool_artifacts" ADD COLUMN "openapi_tags_json" text;--> statement-breakpoint | |
| 4 | +UPDATE "tool_artifacts" | |
| 5 | +SET | |
| 6 | + "openapi_raw_tool_id" = "tool_id", | |
| 7 | + "openapi_operation_id" = "tool_id", | |
| 8 | + "openapi_tags_json" = '[]' | |
| 9 | +WHERE "provider_kind" = 'openapi';--> statement-breakpoint | |
| 10 | +ALTER TABLE "tool_artifacts" DROP CONSTRAINT "tool_artifacts_mcp_shape_check";--> statement-breakpoint | |
| 11 | +ALTER TABLE "tool_artifacts" DROP CONSTRAINT "tool_artifacts_openapi_shape_check";--> statement-breakpoint | |
| 12 | +ALTER TABLE "tool_artifacts" | |
| 13 | +ADD CONSTRAINT "tool_artifacts_mcp_shape_check" CHECK ( | |
| 14 | + "provider_kind" <> 'mcp' | |
| 15 | + OR ( | |
| 16 | + "mcp_tool_name" is not null | |
| 17 | + and "openapi_method" is null | |
| 18 | + and "openapi_path_template" is null | |
| 19 | + and "openapi_operation_hash" is null | |
| 20 | + and "openapi_raw_tool_id" is null | |
| 21 | + and "openapi_operation_id" is null | |
| 22 | + and "openapi_tags_json" is null | |
| 23 | + and "openapi_request_body_required" is null | |
| 24 | + ) | |
| 25 | +);--> statement-breakpoint | |
| 26 | +ALTER TABLE "tool_artifacts" | |
| 27 | +ADD CONSTRAINT "tool_artifacts_openapi_shape_check" CHECK ( | |
| 28 | + "provider_kind" <> 'openapi' | |
| 29 | + OR ( | |
| 30 | + "mcp_tool_name" is null | |
| 31 | + and "openapi_method" in ('get', 'put', 'post', 'delete', 'patch', 'head', 'options', 'trace') | |
| 32 | + and "openapi_path_template" is not null | |
| 33 | + and "openapi_operation_hash" is not null | |
| 34 | + and "openapi_raw_tool_id" is not null | |
| 35 | + and "openapi_tags_json" is not null | |
| 36 | + ) | |
| 37 | +); | |
| @@ -0,0 +1 @@ | ||
| 1 | +ALTER TABLE "sources" ADD COLUMN "source_document_text" text; | |
| @@ -3,7 +3,9 @@ · 2 unmodified lines | ||
| 3 | 3 | "private": true, |
| 4 | 4 | "type": "module", |
| 5 | 5 | "exports": { |
| 6 | − ".": "./src/index.ts" | |
| 6 | + ".": "./src/index.ts", | |
| 7 | + "./client": "./src/client.ts", | |
| 8 | + "./schema": "./src/schema/index.ts" | |
| 7 | 9 | }, |
| 8 | 10 | "imports": { |
| 9 | 11 | "#schema": "./src/schema/index.ts", |
| @@ -3,6 +3,10 @@ import { · 2 unmodified lines | ||
| 3 | 3 | ExecutionInteractionIdSchema, |
| 4 | 4 | SourceAuthSchema, |
| 5 | 5 | SourceIdSchema, |
| 6 | + SourceInspectionDiscoverPayloadSchema, | |
| 7 | + SourceInspectionDiscoverResultSchema, | |
| 8 | + SourceInspectionSchema, | |
| 9 | + SourceInspectionToolDetailSchema, | |
| 6 | 10 | SourceKindSchema, |
| 7 | 11 | SourceSchema, |
| 8 | 12 | SourceStatusSchema, |
| @@ -72,6 +76,7 @@ export type UpdateSourcePayload = typeof UpdateSourcePayloadSchema.Type; · 63 unmodified lines | ||
| 72 | 76 | |
| 73 | 77 | const workspaceIdParam = HttpApiSchema.param("workspaceId", WorkspaceIdSchema); |
| 74 | 78 | const sourceIdParam = HttpApiSchema.param("sourceId", SourceIdSchema); |
| 79 | +const toolPathParam = HttpApiSchema.param("toolPath", Schema.String); | |
| 75 | 80 | |
| 76 | 81 | const CredentialPageUrlParamsSchema = Schema.Struct({ |
| 77 | 82 | interactionId: ExecutionInteractionIdSchema, |
| @@ -163,4 +168,32 @@ export class SourcesApi extends HttpApiGroup.make("sources") · 85 unmodified lines | ||
| 163 | 168 | .addError(ControlPlaneNotFoundError) |
| 164 | 169 | .addError(ControlPlaneStorageError), |
| 165 | 170 | ) |
| 171 | + .add( | |
| 172 | + HttpApiEndpoint.get("inspection")`/workspaces/${workspaceIdParam}/sources/${sourceIdParam}/inspection` | |
| 173 | + .addSuccess(SourceInspectionSchema) | |
| 174 | + .addError(ControlPlaneBadRequestError) | |
| 175 | + .addError(ControlPlaneUnauthorizedError) | |
| 176 | + .addError(ControlPlaneForbiddenError) | |
| 177 | + .addError(ControlPlaneNotFoundError) | |
| 178 | + .addError(ControlPlaneStorageError), | |
| 179 | + ) | |
| 180 | + .add( | |
| 181 | + HttpApiEndpoint.get("inspectionTool")`/workspaces/${workspaceIdParam}/sources/${sourceIdParam}/tools/${toolPathParam}/inspection` | |
| 182 | + .addSuccess(SourceInspectionToolDetailSchema) | |
| 183 | + .addError(ControlPlaneBadRequestError) | |
| 184 | + .addError(ControlPlaneUnauthorizedError) | |
| 185 | + .addError(ControlPlaneForbiddenError) | |
| 186 | + .addError(ControlPlaneNotFoundError) | |
| 187 | + .addError(ControlPlaneStorageError), | |
| 188 | + ) | |
| 189 | + .add( | |
| 190 | + HttpApiEndpoint.post("inspectionDiscover")`/workspaces/${workspaceIdParam}/sources/${sourceIdParam}/inspection/discover` | |
| 191 | + .setPayload(SourceInspectionDiscoverPayloadSchema) | |
| 192 | + .addSuccess(SourceInspectionDiscoverResultSchema) | |
| 193 | + .addError(ControlPlaneBadRequestError) | |
| 194 | + .addError(ControlPlaneUnauthorizedError) | |
| 195 | + .addError(ControlPlaneForbiddenError) | |
| 196 | + .addError(ControlPlaneNotFoundError) | |
| 197 | + .addError(ControlPlaneStorageError), | |
| 198 | + ) | |
| 166 | 199 | .prefix("/v1") {} |
| @@ -17,6 +17,11 @@ import { · 16 unmodified lines | ||
| 17 | 17 | removeSource, |
| 18 | 18 | updateSource, |
| 19 | 19 | } from "../../runtime/sources-operations"; |
| 20 | +import { | |
| 21 | + discoverSourceInspectionTools, | |
| 22 | + getSourceInspection, | |
| 23 | + getSourceInspectionToolDetail, | |
| 24 | +} from "../../runtime/source-inspection"; | |
| 20 | 25 | import { |
| 21 | 26 | completeSourceCredentialSetup, |
| 22 | 27 | getSourceCredentialInteraction, |
| @@ -512,6 +517,38 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 489 unmodified lines | ||
| 512 | 517 | ), |
| 513 | 518 | ), |
| 514 | 519 | ) |
| 520 | + .handle("inspection", ({ path }) => | |
| 521 | + withWorkspaceRequestActor("sources.inspection", path.workspaceId, () => | |
| 522 | + withPolicy(requireReadSources(path.workspaceId))( | |
| 523 | + getSourceInspection({ | |
| 524 | + workspaceId: path.workspaceId, | |
| 525 | + sourceId: path.sourceId, | |
| 526 | + }), | |
| 527 | + ), | |
| 528 | + ), | |
| 529 | + ) | |
| 530 | + .handle("inspectionTool", ({ path }) => | |
| 531 | + withWorkspaceRequestActor("sources.inspection_tool", path.workspaceId, () => | |
| 532 | + withPolicy(requireReadSources(path.workspaceId))( | |
| 533 | + getSourceInspectionToolDetail({ | |
| 534 | + workspaceId: path.workspaceId, | |
| 535 | + sourceId: path.sourceId, | |
| 536 | + toolPath: path.toolPath, | |
| 537 | + }), | |
| 538 | + ), | |
| 539 | + ), | |
| 540 | + ) | |
| 541 | + .handle("inspectionDiscover", ({ path, payload }) => | |
| 542 | + withWorkspaceRequestActor("sources.inspection_discover", path.workspaceId, () => | |
| 543 | + withPolicy(requireReadSources(path.workspaceId))( | |
| 544 | + discoverSourceInspectionTools({ | |
| 545 | + workspaceId: path.workspaceId, | |
| 546 | + sourceId: path.sourceId, | |
| 547 | + payload, | |
| 548 | + }), | |
| 549 | + ), | |
| 550 | + ), | |
| 551 | + ) | |
| 515 | 552 | .handle("update", ({ path, payload }) => |
| 516 | 553 | withWorkspaceRequestActor("sources.update", path.workspaceId, () => |
| 517 | 554 | withPolicy(requireWriteSources(path.workspaceId))( |
| @@ -6,8 +6,8 @@ import { · 5 unmodified lines | ||
| 6 | 6 | } from "@effect/platform"; |
| 7 | 7 | import * as Effect from "effect/Effect"; |
| 8 | 8 | |
| 9 | −import { ControlPlaneApi } from "./api"; | |
| 10 | −import { ControlPlaneAuthHeaders } from "./runtime"; | |
| 9 | +import { ControlPlaneApi } from "./api/api"; | |
| 10 | +import { ControlPlaneAuthHeaders } from "./runtime/actor-resolver"; | |
| 11 | 11 | |
| 12 | 12 | export const createControlPlaneClient = (input: { |
| 13 | 13 | baseUrl: string; |
| @@ -70,6 +70,7 @@ describe("control-plane-persistence-drizzle", () => { · 69 unmodified lines | ||
| 70 | 70 | authHeaderName: null, |
| 71 | 71 | authPrefix: null, |
| 72 | 72 | sourceHash: null, |
| 73 | + sourceDocumentText: null, | |
| 73 | 74 | lastError: null, |
| 74 | 75 | createdAt: now, |
| 75 | 76 | updatedAt: now, |
| @@ -35,6 +35,7 @@ const toSourceUpdateSet = ( · 34 unmodified lines | ||
| 35 | 35 | if (patch.authHeaderName !== undefined) set.authHeaderName = patch.authHeaderName; |
| 36 | 36 | if (patch.authPrefix !== undefined) set.authPrefix = patch.authPrefix; |
| 37 | 37 | if (patch.sourceHash !== undefined) set.sourceHash = patch.sourceHash; |
| 38 | + if (patch.sourceDocumentText !== undefined) set.sourceDocumentText = patch.sourceDocumentText; | |
| 38 | 39 | if (patch.lastError !== undefined) set.lastError = patch.lastError; |
| 39 | 40 | if (patch.updatedAt !== undefined) set.updatedAt = patch.updatedAt; |
| 40 | 41 | |
| @@ -135,6 +135,7 @@ export const sourcesTable = pgTable( · 134 unmodified lines | ||
| 135 | 135 | authHeaderName: text("auth_header_name"), |
| 136 | 136 | authPrefix: text("auth_prefix"), |
| 137 | 137 | sourceHash: text("source_hash"), |
| 138 | + sourceDocumentText: text("source_document_text"), | |
| 138 | 139 | lastError: text("last_error"), |
| 139 | 140 | createdAt: bigint("created_at", { mode: "number" }).notNull(), |
| 140 | 141 | updatedAt: bigint("updated_at", { mode: "number" }).notNull(), |
| @@ -205,6 +206,9 @@ export const toolArtifactsTable = pgTable( · 64 unmodified lines | ||
| 205 | 206 | openApiMethod: text("openapi_method"), |
| 206 | 207 | openApiPathTemplate: text("openapi_path_template"), |
| 207 | 208 | openApiOperationHash: text("openapi_operation_hash"), |
| 209 | + openApiRawToolId: text("openapi_raw_tool_id"), | |
| 210 | + openApiOperationId: text("openapi_operation_id"), | |
| 211 | + openApiTagsJson: text("openapi_tags_json"), | |
| 208 | 212 | openApiRequestBodyRequired: boolean("openapi_request_body_required"), |
| 209 | 213 | createdAt: bigint("created_at", { mode: "number" }).notNull(), |
| 210 | 214 | updatedAt: bigint("updated_at", { mode: "number" }).notNull(), |
| @@ -236,6 +240,9 @@ export const toolArtifactsTable = pgTable( · 25 unmodified lines | ||
| 236 | 240 | and ${table.openApiMethod} is null |
| 237 | 241 | and ${table.openApiPathTemplate} is null |
| 238 | 242 | and ${table.openApiOperationHash} is null |
| 243 | + and ${table.openApiRawToolId} is null | |
| 244 | + and ${table.openApiOperationId} is null | |
| 245 | + and ${table.openApiTagsJson} is null | |
| 239 | 246 | and ${table.openApiRequestBodyRequired} is null |
| 240 | 247 | )`, |
| 241 | 248 | ), |
| @@ -247,6 +254,8 @@ export const toolArtifactsTable = pgTable( · 5 unmodified lines | ||
| 247 | 254 | and ${table.openApiMethod} in ('get', 'put', 'post', 'delete', 'patch', 'head', 'options', 'trace') |
| 248 | 255 | and ${table.openApiPathTemplate} is not null |
| 249 | 256 | and ${table.openApiOperationHash} is not null |
| 257 | + and ${table.openApiRawToolId} is not null | |
| 258 | + and ${table.openApiTagsJson} is not null | |
| 250 | 259 | )`, |
| 251 | 260 | ), |
| 252 | 261 | ], |
| @@ -159,6 +159,7 @@ describe("control-plane-runtime", () => { · 158 unmodified lines | ||
| 159 | 159 | authHeaderName: null, |
| 160 | 160 | authPrefix: null, |
| 161 | 161 | sourceHash: null, |
| 162 | + sourceDocumentText: null, | |
| 162 | 163 | lastError: null, |
| 163 | 164 | createdAt: now, |
| 164 | 165 | updatedAt: now, |
| @@ -307,6 +308,7 @@ describe("control-plane-runtime", () => { · 142 unmodified lines | ||
| 307 | 308 | authHeaderName: null, |
| 308 | 309 | authPrefix: null, |
| 309 | 310 | sourceHash: null, |
| 311 | + sourceDocumentText: null, | |
| 310 | 312 | lastError: null, |
| 311 | 313 | createdAt: now, |
| 312 | 314 | updatedAt: now, |
| @@ -373,8 +373,8 @@ export const createExecutorToolMap = (input: { · 372 unmodified lines | ||
| 373 | 373 | }, |
| 374 | 374 | }, |
| 375 | 375 | metadata: { |
| 376 | − inputHint: EXECUTOR_SOURCES_ADD_INPUT_HINT, | |
| 377 | − outputHint: EXECUTOR_SOURCES_ADD_OUTPUT_SIGNATURE, | |
| 376 | + inputType: EXECUTOR_SOURCES_ADD_INPUT_HINT, | |
| 377 | + outputType: EXECUTOR_SOURCES_ADD_OUTPUT_SIGNATURE, | |
| 378 | 378 | inputSchemaJson: EXECUTOR_SOURCES_ADD_INPUT_SCHEMA_JSON, |
| 379 | 379 | outputSchemaJson: EXECUTOR_SOURCES_ADD_OUTPUT_SCHEMA_JSON, |
| 380 | 380 | sourceKey: "executor", |
| @@ -57,6 +57,7 @@ export * from "./schema-type-signature"; · 56 unmodified lines | ||
| 57 | 57 | export * from "./source-auth-service"; |
| 58 | 58 | export * from "./store"; |
| 59 | 59 | export * from "./workspace-execution-environment"; |
| 60 | +export * from "./source-inspection"; | |
| 60 | 61 | |
| 61 | 62 | export type RuntimeControlPlaneOptions = { |
| 62 | 63 | actorResolver?: ControlPlaneActorResolverShape; |
| @@ -114,7 +115,7 @@ const createRuntimeLayerFromContext = ( · 51 unmodified lines | ||
| 114 | 115 | | RuntimeExecutionResolverService |
| 115 | 116 | >; |
| 116 | 117 | |
| 117 | − return Layer.succeedContext(runtimeContext); | |
| 118 | + return Layer.succeedContext(runtimeContext) as RuntimeControlPlaneLayer; | |
| 118 | 119 | }; |
| 119 | 120 | |
| 120 | 121 | export const createRuntimeControlPlaneLayer = ( |
| @@ -165,7 +165,7 @@ const persistSource = (rows: SqlControlPlaneRows, source: Source) => · 164 unmodified lines | ||
| 165 | 165 | if (Option.isNone(existing)) { |
| 166 | 166 | yield* rows.sources.insert(sourceRecord); |
| 167 | 167 | } else { |
| 168 | − const { id: _id, workspaceId: _workspaceId, createdAt: _createdAt, ...patch } = sourceRecord; | |
| 168 | + const { id: _id, workspaceId: _workspaceId, createdAt: _createdAt, sourceDocumentText: _sourceDocumentText, ...patch } = sourceRecord; | |
| 169 | 169 | yield* rows.sources.update(source.workspaceId, source.id, patch); |
| 170 | 170 | } |
| 171 | 171 | |
| @@ -431,12 +431,19 @@ const updateSourceStatus = (rows: SqlControlPlaneRows, source: Source, input: { · 259 unmodified lines | ||
| 431 | 431 | lastError?: string | null; |
| 432 | 432 | auth?: Source["auth"]; |
| 433 | 433 | }) => |
| 434 | − persistSource(rows, { | |
| 435 | − ...source, | |
| 436 | − status: input.status, | |
| 437 | − lastError: input.lastError ?? null, | |
| 438 | − auth: input.auth ?? source.auth, | |
| 439 | − updatedAt: Date.now(), | |
| 434 | + Effect.gen(function* () { | |
| 435 | + const latest = yield* loadSourceById(rows, { | |
| 436 | + workspaceId: source.workspaceId, | |
| 437 | + sourceId: source.id, | |
| 438 | + }); | |
| 439 | + | |
| 440 | + return yield* persistSource(rows, { | |
| 441 | + ...latest, | |
| 442 | + status: input.status, | |
| 443 | + lastError: input.lastError ?? null, | |
| 444 | + auth: input.auth ?? latest.auth, | |
| 445 | + updatedAt: Date.now(), | |
| 446 | + }); | |
| 440 | 447 | }); |
| 441 | 448 | |
| 442 | 449 | const upsertSecretMaterial = (rows: SqlControlPlaneRows, input: { |
| @@ -255,6 +255,7 @@ export const splitSourceForStorage = (input: { · 254 unmodified lines | ||
| 255 | 255 | input.source.auth.kind === "none" ? null : input.source.auth.headerName, |
| 256 | 256 | authPrefix: input.source.auth.kind === "none" ? null : input.source.auth.prefix, |
| 257 | 257 | sourceHash: input.source.sourceHash, |
| 258 | + sourceDocumentText: null, | |
| 258 | 259 | lastError: input.source.lastError, |
| 259 | 260 | createdAt: input.source.createdAt, |
| 260 | 261 | updatedAt: input.source.updatedAt, |
| @@ -0,0 +1,520 @@ | ||
| 1 | +import { typeSignatureFromSchemaJson } from "@executor-v3/codemode-core"; | |
| 2 | +import { | |
| 3 | + buildOpenApiToolPresentation, | |
| 4 | + compileOpenApiToolDefinitions, | |
| 5 | + extractOpenApiManifest, | |
| 6 | + type OpenApiToolDefinition, | |
| 7 | + type OpenApiToolManifest, | |
| 8 | +} from "@executor-v3/codemode-openapi"; | |
| 9 | +import type { | |
| 10 | + Source, | |
| 11 | + SourceId, | |
| 12 | + SourceInspection, | |
| 13 | + SourceInspectionDiscoverPayload, | |
| 14 | + SourceInspectionDiscoverResult, | |
| 15 | + SourceInspectionDiscoverResultItem, | |
| 16 | + SourceInspectionToolDetail, | |
| 17 | + SourceInspectionToolSummary, | |
| 18 | + StoredSourceRecord, | |
| 19 | + WorkspaceId, | |
| 20 | +} from "#schema"; | |
| 21 | +import { | |
| 22 | + ControlPlaneNotFoundError, | |
| 23 | + ControlPlaneStorageError, | |
| 24 | +} from "#api"; | |
| 25 | +import * as Effect from "effect/Effect"; | |
| 26 | +import * as Option from "effect/Option"; | |
| 27 | + | |
| 28 | +import { operationErrors } from "./operation-errors"; | |
| 29 | +import { projectSourceFromStorage } from "./source-definitions"; | |
| 30 | +import { ControlPlaneStore, type ControlPlaneStoreShape } from "./store"; | |
| 31 | +import { namespaceFromSourceName } from "./tool-artifacts"; | |
| 32 | + | |
| 33 | +const sourceInspectOps = { | |
| 34 | + bundle: operationErrors("sources.inspect.bundle"), | |
| 35 | + tool: operationErrors("sources.inspect.tool"), | |
| 36 | + discover: operationErrors("sources.inspect.discover"), | |
| 37 | +} as const; | |
| 38 | + | |
| 39 | +const asPrettyJson = (value: unknown): string => JSON.stringify(value, null, 2); | |
| 40 | + | |
| 41 | +const tokenize = (value: string): Array<string> => | |
| 42 | + value | |
| 43 | + .trim() | |
| 44 | + .toLowerCase() | |
| 45 | + .split(/[^a-z0-9]+/) | |
| 46 | + .filter(Boolean); | |
| 47 | + | |
| 48 | +const searchTextFromSummary = (summary: SourceInspectionToolSummary): string => | |
| 49 | + [ | |
| 50 | + summary.path, | |
| 51 | + summary.toolId, | |
| 52 | + summary.rawToolId ?? "", | |
| 53 | + summary.operationId ?? "", | |
| 54 | + summary.title ?? "", | |
| 55 | + summary.description ?? "", | |
| 56 | + summary.method ?? "", | |
| 57 | + summary.pathTemplate ?? "", | |
| 58 | + summary.tags.join(" "), | |
| 59 | + summary.inputType ?? "", | |
| 60 | + summary.outputType ?? "", | |
| 61 | + ] | |
| 62 | + .join(" ") | |
| 63 | + .toLowerCase(); | |
| 64 | + | |
| 65 | +type InspectionToolRecord = { | |
| 66 | + summary: SourceInspectionToolSummary; | |
| 67 | + detail: SourceInspectionToolDetail; | |
| 68 | + searchText: string; | |
| 69 | +}; | |
| 70 | + | |
| 71 | +type ResolvedSourceInspection = { | |
| 72 | + source: Source; | |
| 73 | + namespace: string; | |
| 74 | + pipelineKind: SourceInspection["pipelineKind"]; | |
| 75 | + rawDocumentText: string | null; | |
| 76 | + manifestJson: string | null; | |
| 77 | + definitionsJson: string | null; | |
| 78 | + tools: ReadonlyArray<InspectionToolRecord>; | |
| 79 | +}; | |
| 80 | + | |
| 81 | +const loadSourceRecord = (input: { | |
| 82 | + workspaceId: WorkspaceId; | |
| 83 | + sourceId: SourceId; | |
| 84 | +}) => | |
| 85 | + Effect.flatMap(ControlPlaneStore, (store) => | |
| 86 | + Effect.gen(function* () { | |
| 87 | + const sourceRecord = yield* sourceInspectOps.bundle.child("record").mapStorage( | |
| 88 | + store.sources.getByWorkspaceAndId(input.workspaceId, input.sourceId), | |
| 89 | + ); | |
| 90 | + if (Option.isNone(sourceRecord)) { | |
| 91 | + return yield* Effect.fail( | |
| 92 | + sourceInspectOps.bundle.notFound( | |
| 93 | + "Source not found", | |
| 94 | + `workspaceId=${input.workspaceId} sourceId=${input.sourceId}`, | |
| 95 | + ), | |
| 96 | + ); | |
| 97 | + } | |
| 98 | + | |
| 99 | + const credentialBinding = yield* sourceInspectOps.bundle.child("binding").mapStorage( | |
| 100 | + store.sourceCredentialBindings.getByWorkspaceAndSourceId( | |
| 101 | + input.workspaceId, | |
| 102 | + input.sourceId, | |
| 103 | + ), | |
| 104 | + ); | |
| 105 | + | |
| 106 | + const source = yield* projectSourceFromStorage({ | |
| 107 | + sourceRecord: sourceRecord.value, | |
| 108 | + credentialBinding: Option.isSome(credentialBinding) | |
| 109 | + ? credentialBinding.value | |
| 110 | + : null, | |
| 111 | + }).pipe( | |
| 112 | + Effect.mapError((cause) => | |
| 113 | + sourceInspectOps.bundle.unknownStorage( | |
| 114 | + cause, | |
| 115 | + "Failed projecting stored source", | |
| 116 | + ), | |
| 117 | + ), | |
| 118 | + ); | |
| 119 | + | |
| 120 | + return { | |
| 121 | + store, | |
| 122 | + source, | |
| 123 | + sourceRecord: sourceRecord.value, | |
| 124 | + }; | |
| 125 | + }), | |
| 126 | + ); | |
| 127 | + | |
| 128 | +const persistedToolSummaryFromArtifact = (input: { | |
| 129 | + source: Source; | |
| 130 | + artifact: { | |
| 131 | + path: string; | |
| 132 | + toolId: string; | |
| 133 | + title: string | null; | |
| 134 | + description: string | null; | |
| 135 | + providerKind: string; | |
| 136 | + openApiRawToolId: string | null; | |
| 137 | + openApiOperationId: string | null; | |
| 138 | + openApiTagsJson: string | null; | |
| 139 | + openApiMethod: SourceInspectionToolSummary["method"]; | |
| 140 | + openApiPathTemplate: string | null; | |
| 141 | + inputSchemaJson: string | null; | |
| 142 | + outputSchemaJson: string | null; | |
| 143 | + }; | |
| 144 | +}): SourceInspectionToolSummary => ({ | |
| 145 | + path: input.artifact.path, | |
| 146 | + sourceKey: input.source.id, | |
| 147 | + ...(input.artifact.title ? { title: input.artifact.title } : {}), | |
| 148 | + ...(input.artifact.description ? { description: input.artifact.description } : {}), | |
| 149 | + providerKind: input.artifact.providerKind, | |
| 150 | + toolId: input.artifact.toolId, | |
| 151 | + rawToolId: input.artifact.openApiRawToolId, | |
| 152 | + operationId: input.artifact.openApiOperationId, | |
| 153 | + group: null, | |
| 154 | + leaf: null, | |
| 155 | + tags: input.artifact.openApiTagsJson | |
| 156 | + ? ((JSON.parse(input.artifact.openApiTagsJson) as Array<string>) ?? []) | |
| 157 | + : [], | |
| 158 | + method: input.artifact.openApiMethod, | |
| 159 | + pathTemplate: input.artifact.openApiPathTemplate, | |
| 160 | + ...(input.artifact.inputSchemaJson | |
| 161 | + ? { | |
| 162 | + inputType: typeSignatureFromSchemaJson( | |
| 163 | + input.artifact.inputSchemaJson, | |
| 164 | + "unknown", | |
| 165 | + 320, | |
| 166 | + ), | |
| 167 | + } | |
| 168 | + : {}), | |
| 169 | + ...(input.artifact.outputSchemaJson | |
| 170 | + ? { | |
| 171 | + outputType: typeSignatureFromSchemaJson( | |
| 172 | + input.artifact.outputSchemaJson, | |
| 173 | + "unknown", | |
| 174 | + 320, | |
| 175 | + ), | |
| 176 | + } | |
| 177 | + : {}), | |
| 178 | +}); | |
| 179 | + | |
| 180 | +const openApiToolRecord = (input: { | |
| 181 | + source: Source; | |
| 182 | + namespace: string; | |
| 183 | + manifest: OpenApiToolManifest; | |
| 184 | + definition: OpenApiToolDefinition; | |
| 185 | +}): InspectionToolRecord => { | |
| 186 | + const presentation = buildOpenApiToolPresentation({ | |
| 187 | + manifest: input.manifest, | |
| 188 | + definition: input.definition, | |
| 189 | + }); | |
| 190 | + const path = `${input.namespace}.${input.definition.toolId}`; | |
| 191 | + const summary: SourceInspectionToolSummary = { | |
| 192 | + path, | |
| 193 | + sourceKey: input.source.id, | |
| 194 | + title: input.definition.name, | |
| 195 | + description: input.definition.description, | |
| 196 | + providerKind: "openapi", | |
| 197 | + toolId: input.definition.toolId, | |
| 198 | + rawToolId: input.definition.rawToolId, | |
| 199 | + operationId: input.definition.operationId ?? null, | |
| 200 | + group: input.definition.group, | |
| 201 | + leaf: input.definition.leaf, | |
| 202 | + tags: [...input.definition.tags], | |
| 203 | + method: input.definition.method, | |
| 204 | + pathTemplate: input.definition.path, | |
| 205 | + inputType: presentation.inputType, | |
| 206 | + outputType: presentation.outputType, | |
| 207 | + }; | |
| 208 | + | |
| 209 | + return { | |
| 210 | + summary, | |
| 211 | + detail: { | |
| 212 | + summary, | |
| 213 | + definitionJson: asPrettyJson(input.definition), | |
| 214 | + documentationJson: input.definition.documentation | |
| 215 | + ? asPrettyJson(input.definition.documentation) | |
| 216 | + : null, | |
| 217 | + providerDataJson: presentation.providerDataJson, | |
| 218 | + inputSchemaJson: presentation.inputSchemaJson ?? null, | |
| 219 | + outputSchemaJson: presentation.outputSchemaJson ?? null, | |
| 220 | + exampleInputJson: presentation.exampleInputJson ?? null, | |
| 221 | + exampleOutputJson: presentation.exampleOutputJson ?? null, | |
| 222 | + }, | |
| 223 | + searchText: searchTextFromSummary(summary), | |
| 224 | + }; | |
| 225 | +}; | |
| 226 | + | |
| 227 | +const loadPersistedInspection = (input: { | |
| 228 | + store: ControlPlaneStoreShape; | |
| 229 | + source: Source; | |
| 230 | +}): Effect.Effect<ResolvedSourceInspection, Error, never> => | |
| 231 | + Effect.gen(function* () { | |
| 232 | + const artifacts = yield* sourceInspectOps.bundle.child("artifacts").mapStorage( | |
| 233 | + input.store.toolArtifacts.listByWorkspaceId(input.source.workspaceId, { | |
| 234 | + sourceId: input.source.id, | |
| 235 | + limit: 1000, | |
| 236 | + }), | |
| 237 | + ); | |
| 238 | + const namespace = input.source.namespace ?? namespaceFromSourceName(input.source.name); | |
| 239 | + const tools = artifacts.map((artifact) => { | |
| 240 | + const summary = persistedToolSummaryFromArtifact({ | |
| 241 | + source: input.source, | |
| 242 | + artifact, | |
| 243 | + }); | |
| 244 | + return { | |
| 245 | + summary, | |
| 246 | + detail: { | |
| 247 | + summary, | |
| 248 | + definitionJson: null, | |
| 249 | + documentationJson: null, | |
| 250 | + providerDataJson: null, | |
| 251 | + inputSchemaJson: artifact.inputSchemaJson, | |
| 252 | + outputSchemaJson: artifact.outputSchemaJson, | |
| 253 | + exampleInputJson: null, | |
| 254 | + exampleOutputJson: null, | |
| 255 | + }, | |
| 256 | + searchText: searchTextFromSummary(summary), | |
| 257 | + } satisfies InspectionToolRecord; | |
| 258 | + }); | |
| 259 | + | |
| 260 | + return { | |
| 261 | + source: input.source, | |
| 262 | + namespace, | |
| 263 | + pipelineKind: "persisted", | |
| 264 | + rawDocumentText: null, | |
| 265 | + manifestJson: null, | |
| 266 | + definitionsJson: null, | |
| 267 | + tools, | |
| 268 | + } satisfies ResolvedSourceInspection; | |
| 269 | + }); | |
| 270 | + | |
| 271 | +const loadOpenApiInspection = (input: { | |
| 272 | + source: Source; | |
| 273 | + sourceRecord: StoredSourceRecord; | |
| 274 | +}): Effect.Effect<ResolvedSourceInspection, Error, never> => | |
| 275 | + Effect.gen(function* () { | |
| 276 | + const rawDocumentText = input.sourceRecord.sourceDocumentText; | |
| 277 | + if (!rawDocumentText) { | |
| 278 | + return yield* Effect.fail(new Error("Missing stored OpenAPI document")); | |
| 279 | + } | |
| 280 | + | |
| 281 | + const manifest = yield* extractOpenApiManifest( | |
| 282 | + input.source.name, | |
| 283 | + rawDocumentText, | |
| 284 | + ).pipe( | |
| 285 | + Effect.mapError((cause) => | |
| 286 | + cause instanceof Error ? cause : new Error(String(cause)), | |
| 287 | + ), | |
| 288 | + ); | |
| 289 | + const definitions = compileOpenApiToolDefinitions(manifest); | |
| 290 | + const namespace = input.source.namespace ?? namespaceFromSourceName(input.source.name); | |
| 291 | + const tools = definitions.map((definition) => | |
| 292 | + openApiToolRecord({ | |
| 293 | + source: input.source, | |
| 294 | + namespace, | |
| 295 | + manifest, | |
| 296 | + definition, | |
| 297 | + }), | |
| 298 | + ); | |
| 299 | + | |
| 300 | + return { | |
| 301 | + source: input.source, | |
| 302 | + namespace, | |
| 303 | + pipelineKind: "openapi", | |
| 304 | + rawDocumentText, | |
| 305 | + manifestJson: asPrettyJson(manifest), | |
| 306 | + definitionsJson: asPrettyJson(definitions), | |
| 307 | + tools, | |
| 308 | + } satisfies ResolvedSourceInspection; | |
| 309 | + }); | |
| 310 | + | |
| 311 | +const resolveSourceInspection = (input: { | |
| 312 | + workspaceId: WorkspaceId; | |
| 313 | + sourceId: SourceId; | |
| 314 | +}) => | |
| 315 | + Effect.gen(function* () { | |
| 316 | + const { store, source, sourceRecord } = yield* loadSourceRecord(input); | |
| 317 | + | |
| 318 | + if (source.kind === "openapi" && sourceRecord.sourceDocumentText) { | |
| 319 | + return yield* loadOpenApiInspection({ | |
| 320 | + source, | |
| 321 | + sourceRecord, | |
| 322 | + }).pipe( | |
| 323 | + Effect.catchAll(() => | |
| 324 | + loadPersistedInspection({ | |
| 325 | + store, | |
| 326 | + source, | |
| 327 | + }), | |
| 328 | + ), | |
| 329 | + ); | |
| 330 | + } | |
| 331 | + | |
| 332 | + return yield* loadPersistedInspection({ | |
| 333 | + store, | |
| 334 | + source, | |
| 335 | + }); | |
| 336 | + }); | |
| 337 | + | |
| 338 | +const scoreTool = (input: { | |
| 339 | + queryTokens: ReadonlyArray<string>; | |
| 340 | + tool: InspectionToolRecord; | |
| 341 | +}): SourceInspectionDiscoverResultItem | null => { | |
| 342 | + let score = 0; | |
| 343 | + const reasons: Array<string> = []; | |
| 344 | + const pathTokens = tokenize(input.tool.summary.path); | |
| 345 | + const titleTokens = tokenize(input.tool.summary.title ?? ""); | |
| 346 | + const descriptionTokens = tokenize(input.tool.summary.description ?? ""); | |
| 347 | + const tagTokens = input.tool.summary.tags.flatMap(tokenize); | |
| 348 | + const typeTokens = tokenize( | |
| 349 | + `${input.tool.summary.inputType ?? ""} ${input.tool.summary.outputType ?? ""}`, | |
| 350 | + ); | |
| 351 | + const methodPathTokens = tokenize( | |
| 352 | + `${input.tool.summary.method ?? ""} ${input.tool.summary.pathTemplate ?? ""}`, | |
| 353 | + ); | |
| 354 | + | |
| 355 | + for (const token of input.queryTokens) { | |
| 356 | + if (pathTokens.includes(token)) { | |
| 357 | + score += 12; | |
| 358 | + reasons.push(`path matches ${token} (+12)`); | |
| 359 | + continue; | |
| 360 | + } | |
| 361 | + if (tagTokens.includes(token)) { | |
| 362 | + score += 10; | |
| 363 | + reasons.push(`tag matches ${token} (+10)`); | |
| 364 | + continue; | |
| 365 | + } | |
| 366 | + if (titleTokens.includes(token)) { | |
| 367 | + score += 8; | |
| 368 | + reasons.push(`title matches ${token} (+8)`); | |
| 369 | + continue; | |
| 370 | + } | |
| 371 | + if (methodPathTokens.includes(token)) { | |
| 372 | + score += 6; | |
| 373 | + reasons.push(`method/path matches ${token} (+6)`); | |
| 374 | + continue; | |
| 375 | + } | |
| 376 | + if (typeTokens.includes(token)) { | |
| 377 | + score += 4; | |
| 378 | + reasons.push(`type signature matches ${token} (+4)`); | |
| 379 | + continue; | |
| 380 | + } | |
| 381 | + if (descriptionTokens.includes(token) || input.tool.searchText.includes(token)) { | |
| 382 | + score += 2; | |
| 383 | + reasons.push(`description/text matches ${token} (+2)`); | |
| 384 | + } | |
| 385 | + } | |
| 386 | + | |
| 387 | + if (score <= 0) { | |
| 388 | + return null; | |
| 389 | + } | |
| 390 | + | |
| 391 | + return { | |
| 392 | + path: input.tool.summary.path, | |
| 393 | + score, | |
| 394 | + ...(input.tool.summary.description | |
| 395 | + ? { description: input.tool.summary.description } | |
| 396 | + : {}), | |
| 397 | + ...(input.tool.summary.inputType | |
| 398 | + ? { inputType: input.tool.summary.inputType } | |
| 399 | + : {}), | |
| 400 | + ...(input.tool.summary.outputType | |
| 401 | + ? { outputType: input.tool.summary.outputType } | |
| 402 | + : {}), | |
| 403 | + reasons, | |
| 404 | + } satisfies SourceInspectionDiscoverResultItem; | |
| 405 | +}; | |
| 406 | + | |
| 407 | +const mapInspectionError = ( | |
| 408 | + operation: | |
| 409 | + | typeof sourceInspectOps.bundle | |
| 410 | + | typeof sourceInspectOps.tool | |
| 411 | + | typeof sourceInspectOps.discover, | |
| 412 | + cause: unknown, | |
| 413 | + details: string, | |
| 414 | +): ControlPlaneNotFoundError | ControlPlaneStorageError => { | |
| 415 | + if (cause instanceof ControlPlaneNotFoundError) { | |
| 416 | + return cause; | |
| 417 | + } | |
| 418 | + if (cause instanceof ControlPlaneStorageError) { | |
| 419 | + return cause; | |
| 420 | + } | |
| 421 | + | |
| 422 | + return operation.unknownStorage(cause, details); | |
| 423 | +}; | |
| 424 | + | |
| 425 | +export const getSourceInspection = (input: { | |
| 426 | + workspaceId: WorkspaceId; | |
| 427 | + sourceId: SourceId; | |
| 428 | +}) => | |
| 429 | + Effect.gen(function* () { | |
| 430 | + const inspection = yield* resolveSourceInspection(input); | |
| 431 | + | |
| 432 | + return { | |
| 433 | + source: inspection.source, | |
| 434 | + namespace: inspection.namespace, | |
| 435 | + pipelineKind: inspection.pipelineKind, | |
| 436 | + toolCount: inspection.tools.length, | |
| 437 | + rawDocumentText: inspection.rawDocumentText, | |
| 438 | + manifestJson: inspection.manifestJson, | |
| 439 | + definitionsJson: inspection.definitionsJson, | |
| 440 | + tools: inspection.tools.map((tool) => tool.summary), | |
| 441 | + } satisfies SourceInspection; | |
| 442 | + }).pipe( | |
| 443 | + Effect.mapError((cause) => | |
| 444 | + mapInspectionError( | |
| 445 | + sourceInspectOps.bundle, | |
| 446 | + cause, | |
| 447 | + "Failed building source inspection bundle", | |
| 448 | + )), | |
| 449 | + ); | |
| 450 | + | |
| 451 | +export const getSourceInspectionToolDetail = (input: { | |
| 452 | + workspaceId: WorkspaceId; | |
| 453 | + sourceId: SourceId; | |
| 454 | + toolPath: string; | |
| 455 | +}) => | |
| 456 | + Effect.gen(function* () { | |
| 457 | + const inspection = yield* resolveSourceInspection({ | |
| 458 | + workspaceId: input.workspaceId, | |
| 459 | + sourceId: input.sourceId, | |
| 460 | + }); | |
| 461 | + const tool = inspection.tools.find((candidate) => candidate.summary.path === input.toolPath); | |
| 462 | + | |
| 463 | + if (!tool) { | |
| 464 | + return yield* Effect.fail( | |
| 465 | + sourceInspectOps.tool.notFound( | |
| 466 | + "Tool not found", | |
| 467 | + `workspaceId=${input.workspaceId} sourceId=${input.sourceId} path=${input.toolPath}`, | |
| 468 | + ), | |
| 469 | + ); | |
| 470 | + } | |
| 471 | + | |
| 472 | + return tool.detail; | |
| 473 | + }).pipe( | |
| 474 | + Effect.mapError((cause) => | |
| 475 | + mapInspectionError( | |
| 476 | + sourceInspectOps.tool, | |
| 477 | + cause, | |
| 478 | + "Failed building source inspection tool detail", | |
| 479 | + )), | |
| 480 | + ); | |
| 481 | + | |
| 482 | +export const discoverSourceInspectionTools = (input: { | |
| 483 | + workspaceId: WorkspaceId; | |
| 484 | + sourceId: SourceId; | |
| 485 | + payload: SourceInspectionDiscoverPayload; | |
| 486 | +}) => | |
| 487 | + Effect.gen(function* () { | |
| 488 | + const inspection = yield* resolveSourceInspection({ | |
| 489 | + workspaceId: input.workspaceId, | |
| 490 | + sourceId: input.sourceId, | |
| 491 | + }); | |
| 492 | + const queryTokens = tokenize(input.payload.query); | |
| 493 | + const results = inspection.tools | |
| 494 | + .map((tool) => | |
| 495 | + scoreTool({ | |
| 496 | + queryTokens, | |
| 497 | + tool, | |
| 498 | + }), | |
| 499 | + ) | |
| 500 | + .filter((value): value is SourceInspectionDiscoverResultItem => value !== null) | |
| 501 | + .sort((left, right) => | |
| 502 | + right.score - left.score || left.path.localeCompare(right.path), | |
| 503 | + ) | |
| 504 | + .slice(0, input.payload.limit ?? 12); | |
| 505 | + | |
| 506 | + return { | |
| 507 | + query: input.payload.query, | |
| 508 | + queryTokens, | |
| 509 | + bestPath: results[0]?.path ?? null, | |
| 510 | + total: results.length, | |
| 511 | + results, | |
| 512 | + } satisfies SourceInspectionDiscoverResult; | |
| 513 | + }).pipe( | |
| 514 | + Effect.mapError((cause) => | |
| 515 | + mapInspectionError( | |
| 516 | + sourceInspectOps.discover, | |
| 517 | + cause, | |
| 518 | + "Failed building source inspection discovery", | |
| 519 | + )), | |
| 520 | + ); | |
| @@ -87,10 +87,11 @@ const syncArtifactsForSource = (input: { · 86 unmodified lines | ||
| 87 | 87 | const { sourceRecord } = splitSourceForStorage({ |
| 88 | 88 | source: erroredSource, |
| 89 | 89 | }); |
| 90 | + const { sourceDocumentText: _sourceDocumentText, ...sourcePatch } = sourceRecord; | |
| 90 | 91 | yield* mapPersistenceError( |
| 91 | 92 | input.operation.child("source_error"), |
| 92 | 93 | input.store.sources.update(input.source.workspaceId, input.source.id, { |
| 93 | − ...sourceRecord, | |
| 94 | + ...sourcePatch, | |
| 94 | 95 | updatedAt: erroredSource.updatedAt, |
| 95 | 96 | }), |
| 96 | 97 | ); |
| @@ -264,11 +265,12 @@ export const updateSource = (input: { · 167 unmodified lines | ||
| 264 | 265 | const { sourceRecord, credentialBinding } = splitSourceForStorage({ |
| 265 | 266 | source: updatedSource, |
| 266 | 267 | }); |
| 268 | + const { sourceDocumentText: _sourceDocumentText, ...sourcePatch } = sourceRecord; | |
| 267 | 269 | |
| 268 | 270 | const stored = yield* mapPersistenceError( |
| 269 | 271 | sourceOps.update.child("source"), |
| 270 | 272 | store.sources.update(input.workspaceId, input.sourceId, { |
| 271 | − ...sourceRecord, | |
| 273 | + ...sourcePatch, | |
| 272 | 274 | updatedAt: updatedSource.updatedAt, |
| 273 | 275 | }), |
| 274 | 276 | ); |
| @@ -8,10 +8,7 @@ import { · 7 unmodified lines | ||
| 8 | 8 | discoverMcpToolsFromConnector, |
| 9 | 9 | type McpToolManifestEntry, |
| 10 | 10 | } from "@executor-v3/codemode-mcp"; |
| 11 | −import { | |
| 12 | − extractOpenApiManifest, | |
| 13 | − type OpenApiExtractedTool, | |
| 14 | −} from "@executor-v3/codemode-openapi"; | |
| 11 | +import { extractOpenApiManifest } from "@executor-v3/codemode-openapi"; | |
| 15 | 12 | import type { SqlControlPlaneRows } from "#persistence"; |
| 16 | 13 | import { |
| 17 | 14 | type SecretRef, |
| @@ -22,6 +19,7 @@ import { · 4 unmodified lines | ||
| 22 | 19 | type StoredToolArtifactRequestBodyContentTypeRecord, |
| 23 | 20 | } from "#schema"; |
| 24 | 21 | import * as Effect from "effect/Effect"; |
| 22 | +import * as Option from "effect/Option"; | |
| 25 | 23 | |
| 26 | 24 | export type ResolveSourceSecretMaterial = ( |
| 27 | 25 | ref: SecretRef, |
| @@ -98,6 +96,9 @@ const toMcpToolArtifactRecord = (input: { · 70 unmodified lines | ||
| 98 | 96 | openApiMethod: null, |
| 99 | 97 | openApiPathTemplate: null, |
| 100 | 98 | openApiOperationHash: null, |
| 99 | + openApiRawToolId: null, | |
| 100 | + openApiOperationId: null, | |
| 101 | + openApiTagsJson: null, | |
| 101 | 102 | openApiRequestBodyRequired: null, |
| 102 | 103 | createdAt: input.now, |
| 103 | 104 | updatedAt: input.now, |
| @@ -105,71 +106,6 @@ const toMcpToolArtifactRecord = (input: { · 1 unmodified line | ||
| 105 | 106 | }; |
| 106 | 107 | }; |
| 107 | 108 | |
| 108 | −const toOpenApiToolArtifactRecord = (input: { | |
| 109 | − workspaceId: Source["workspaceId"]; | |
| 110 | − source: Source; | |
| 111 | − extracted: OpenApiExtractedTool; | |
| 112 | − now: number; | |
| 113 | −}): IndexedToolArtifactRecord => { | |
| 114 | − const sourceNamespace = input.source.namespace ?? namespaceFromSourceName(input.source.name); | |
| 115 | − const path = joinToolPath(sourceNamespace, input.extracted.toolId); | |
| 116 | − const searchNamespace = catalogNamespaceFromPath(path); | |
| 117 | − const description = | |
| 118 | − input.extracted.description | |
| 119 | − ?? `${input.extracted.method.toUpperCase()} ${input.extracted.path}`; | |
| 120 | − | |
| 121 | − return { | |
| 122 | − artifact: { | |
| 123 | − workspaceId: input.workspaceId, | |
| 124 | − path, | |
| 125 | − toolId: input.extracted.toolId, | |
| 126 | − sourceId: input.source.id, | |
| 127 | − title: input.extracted.name, | |
| 128 | − description, | |
| 129 | − searchNamespace, | |
| 130 | − searchText: normalizeSearchText( | |
| 131 | − path, | |
| 132 | − searchNamespace, | |
| 133 | − input.extracted.name, | |
| 134 | − description, | |
| 135 | − input.extracted.method.toUpperCase(), | |
| 136 | − input.extracted.path, | |
| 137 | − ), | |
| 138 | − inputSchemaJson: input.extracted.typing?.inputSchemaJson ?? null, | |
| 139 | − outputSchemaJson: input.extracted.typing?.outputSchemaJson ?? null, | |
| 140 | − providerKind: "openapi", | |
| 141 | − mcpToolName: null, | |
| 142 | − openApiMethod: input.extracted.method, | |
| 143 | − openApiPathTemplate: input.extracted.invocation.pathTemplate, | |
| 144 | − openApiOperationHash: input.extracted.operationHash, | |
| 145 | − openApiRequestBodyRequired: input.extracted.invocation.requestBody?.required ?? null, | |
| 146 | − createdAt: input.now, | |
| 147 | − updatedAt: input.now, | |
| 148 | − }, | |
| 149 | − parameters: input.extracted.invocation.parameters.map((parameter, position) => ({ | |
| 150 | − workspaceId: input.workspaceId, | |
| 151 | − path, | |
| 152 | − position, | |
| 153 | − name: parameter.name, | |
| 154 | − location: parameter.location, | |
| 155 | − required: parameter.required, | |
| 156 | − })), | |
| 157 | − requestBodyContentTypes: | |
| 158 | − input.extracted.invocation.requestBody?.contentTypes.map((contentType, position) => ({ | |
| 159 | − workspaceId: input.workspaceId, | |
| 160 | − path, | |
| 161 | − position, | |
| 162 | − contentType, | |
| 163 | − })) ?? [], | |
| 164 | − refHintKeys: | |
| 165 | − input.extracted.typing?.refHintKeys?.map((refHintKey, position) => ({ | |
| 166 | − workspaceId: input.workspaceId, | |
| 167 | − path, | |
| 168 | − position, | |
| 169 | − refHintKey, | |
| 170 | − })) ?? [], | |
| 171 | − }; | |
| 172 | −}; | |
| 173 | 109 | |
| 174 | 110 | const shouldIndexSource = (source: Source): boolean => |
| 175 | 111 | source.enabled |
| @@ -358,18 +294,30 @@ const indexOpenApiSourceToolArtifacts = (input: { · 182 unmodified lines | ||
| 358 | 294 | ); |
| 359 | 295 | |
| 360 | 296 | const now = Date.now(); |
| 361 | − yield* input.rows.toolArtifacts.replaceForSource({ | |
| 362 | − workspaceId: input.source.workspaceId, | |
| 363 | − sourceId: input.source.id, | |
| 364 | − artifacts: manifest.tools.map((extracted) => | |
| 365 | − toOpenApiToolArtifactRecord({ | |
| 366 | − workspaceId: input.source.workspaceId, | |
| 367 | − source: input.source, | |
| 368 | − extracted, | |
| 369 | − now, | |
| 370 | − }) | |
| 297 | + const updatedSource = yield* input.rows.sources.update( | |
| 298 | + input.source.workspaceId, | |
| 299 | + input.source.id, | |
| 300 | + { | |
| 301 | + sourceHash: manifest.sourceHash, | |
| 302 | + sourceDocumentText: openApiDocument, | |
| 303 | + updatedAt: now, | |
| 304 | + }, | |
| 305 | + ).pipe( | |
| 306 | + Effect.mapError((cause) => | |
| 307 | + cause instanceof Error ? cause : new Error(String(cause)), | |
| 371 | 308 | ), |
| 372 | − }).pipe( | |
| 309 | + ); | |
| 310 | + | |
| 311 | + if (Option.isNone(updatedSource)) { | |
| 312 | + return yield* Effect.fail( | |
| 313 | + new Error(`Source disappeared while storing OpenAPI document for ${input.source.id}`), | |
| 314 | + ); | |
| 315 | + } | |
| 316 | + | |
| 317 | + yield* input.rows.toolArtifacts.removeByWorkspaceAndSourceId( | |
| 318 | + input.source.workspaceId, | |
| 319 | + input.source.id, | |
| 320 | + ).pipe( | |
| 373 | 321 | Effect.mapError((cause) => |
| 374 | 322 | cause instanceof Error ? cause : new Error(String(cause)), |
| 375 | 323 | ), |
| @@ -25,6 +25,7 @@ import * as Effect from "effect/Effect"; · 24 unmodified lines | ||
| 25 | 25 | import * as Exit from "effect/Exit"; |
| 26 | 26 | import * as Layer from "effect/Layer"; |
| 27 | 27 | import * as Scope from "effect/Scope"; |
| 28 | +import * as Option from "effect/Option"; | |
| 28 | 29 | import { Schema } from "effect"; |
| 29 | 30 | import { z } from "zod/v4"; |
| 30 | 31 | |
| @@ -372,6 +373,7 @@ const persistConnectedEchoTool = (input: { · 341 unmodified lines | ||
| 372 | 373 | authHeaderName: null, |
| 373 | 374 | authPrefix: null, |
| 374 | 375 | sourceHash: null, |
| 376 | + sourceDocumentText: null, | |
| 375 | 377 | lastError: null, |
| 376 | 378 | createdAt: now, |
| 377 | 379 | updatedAt: now, |
| @@ -406,6 +408,9 @@ const persistConnectedEchoTool = (input: { · 28 unmodified lines | ||
| 406 | 408 | openApiMethod: null, |
| 407 | 409 | openApiPathTemplate: null, |
| 408 | 410 | openApiOperationHash: null, |
| 411 | + openApiRawToolId: null, | |
| 412 | + openApiOperationId: null, | |
| 413 | + openApiTagsJson: null, | |
| 409 | 414 | openApiRequestBodyRequired: null, |
| 410 | 415 | createdAt: now, |
| 411 | 416 | updatedAt: now, |
| @@ -414,13 +419,89 @@ const persistConnectedEchoTool = (input: { · 2 unmodified lines | ||
| 414 | 419 | }); |
| 415 | 420 | }); |
| 416 | 421 | |
| 417 | −const persistConnectedGithubSearchArtifacts = (input: { | |
| 422 | +const persistConnectedGithubOpenApiSource = (input: { | |
| 418 | 423 | persistence: SqlControlPlanePersistence; |
| 419 | 424 | workspaceId: WorkspaceId; |
| 420 | 425 | sourceId: SourceId; |
| 421 | 426 | }) => |
| 422 | 427 | Effect.gen(function* () { |
| 423 | 428 | const now = Date.now(); |
| 429 | + const openApiDocumentText = JSON.stringify({ | |
| 430 | + openapi: "3.0.3", | |
| 431 | + info: { | |
| 432 | + title: "GitHub", | |
| 433 | + version: "1.0.0", | |
| 434 | + }, | |
| 435 | + paths: { | |
| 436 | + "/repos/{owner}/{repo}/environments/{environment_name}/secrets/{secret_name}": { | |
| 437 | + get: { | |
| 438 | + operationId: "actions/get-environment-secret", | |
| 439 | + tags: ["actions"], | |
| 440 | + summary: "Get an environment secret", | |
| 441 | + description: | |
| 442 | + "Gets a single environment secret without revealing its encrypted value. Authenticated users must have collaborator access to a repository to create, update, or read secrets.", | |
| 443 | + parameters: [ | |
| 444 | + { name: "owner", in: "path", required: true, schema: { type: "string" } }, | |
| 445 | + { name: "repo", in: "path", required: true, schema: { type: "string" } }, | |
| 446 | + { name: "environment_name", in: "path", required: true, schema: { type: "string" } }, | |
| 447 | + { name: "secret_name", in: "path", required: true, schema: { type: "string" } }, | |
| 448 | + ], | |
| 449 | + responses: { | |
| 450 | + 200: { | |
| 451 | + description: "ok", | |
| 452 | + content: { | |
| 453 | + "application/json": { | |
| 454 | + schema: { type: "object" }, | |
| 455 | + }, | |
| 456 | + }, | |
| 457 | + }, | |
| 458 | + }, | |
| 459 | + }, | |
| 460 | + }, | |
| 461 | + "/orgs/{org}/actions/runners/{runner_id}": { | |
| 462 | + get: { | |
| 463 | + operationId: "actions/get-self-hosted-runner-for-org", | |
| 464 | + tags: ["actions"], | |
| 465 | + summary: "Get a self-hosted runner for an organization", | |
| 466 | + description: | |
| 467 | + "Gets a specific self-hosted runner configured in an organization. Authenticated users must have admin access to the organization to use this endpoint.", | |
| 468 | + parameters: [ | |
| 469 | + { name: "org", in: "path", required: true, schema: { type: "string" } }, | |
| 470 | + { name: "runner_id", in: "path", required: true, schema: { type: "integer" } }, | |
| 471 | + ], | |
| 472 | + responses: { | |
| 473 | + 200: { | |
| 474 | + description: "ok", | |
| 475 | + content: { | |
| 476 | + "application/json": { | |
| 477 | + schema: { type: "object" }, | |
| 478 | + }, | |
| 479 | + }, | |
| 480 | + }, | |
| 481 | + }, | |
| 482 | + }, | |
| 483 | + }, | |
| 484 | + "/user": { | |
| 485 | + get: { | |
| 486 | + operationId: "users/get-authenticated", | |
| 487 | + tags: ["users"], | |
| 488 | + summary: "Get the authenticated user", | |
| 489 | + description: | |
| 490 | + "Gets the authenticated user. OAuth app tokens and personal access tokens need the user scope to include private profile information.", | |
| 491 | + responses: { | |
| 492 | + 200: { | |
| 493 | + description: "ok", | |
| 494 | + content: { | |
| 495 | + "application/json": { | |
| 496 | + schema: { type: "object" }, | |
| 497 | + }, | |
| 498 | + }, | |
| 499 | + }, | |
| 500 | + }, | |
| 501 | + }, | |
| 502 | + }, | |
| 503 | + }, | |
| 504 | + }); | |
| 424 | 505 | |
| 425 | 506 | yield* input.persistence.rows.sources.insert({ |
| 426 | 507 | id: input.sourceId, |
| @@ -440,89 +521,11 @@ const persistConnectedGithubSearchArtifacts = (input: { · 13 unmodified lines | ||
| 440 | 521 | authHeaderName: null, |
| 441 | 522 | authPrefix: null, |
| 442 | 523 | sourceHash: null, |
| 524 | + sourceDocumentText: openApiDocumentText, | |
| 443 | 525 | lastError: null, |
| 444 | 526 | createdAt: now, |
| 445 | 527 | updatedAt: now, |
| 446 | 528 | }); |
| 447 | − | |
| 448 | − yield* input.persistence.rows.toolArtifacts.replaceForSource({ | |
| 449 | − workspaceId: input.workspaceId, | |
| 450 | − sourceId: input.sourceId, | |
| 451 | − artifacts: [ | |
| 452 | − { | |
| 453 | − artifact: { | |
| 454 | − workspaceId: input.workspaceId, | |
| 455 | − path: "github.actions/get-environment-secret", | |
| 456 | − toolId: "actions/get-environment-secret", | |
| 457 | − sourceId: input.sourceId, | |
| 458 | − title: "Get an environment secret", | |
| 459 | − description: | |
| 460 | − "Gets a single environment secret without revealing its encrypted value. Authenticated users must have collaborator access to a repository to create, update, or read secrets.", | |
| 461 | − searchNamespace: "github.actions", | |
| 462 | − searchText: | |
| 463 | − "github.actions/get-environment-secret github.actions get an environment secret gets a single environment secret without revealing its encrypted value authenticated users must have collaborator access to a repository to create update or read secrets GET /repos/{owner}/{repo}/environments/{environment_name}/secrets/{secret_name}", | |
| 464 | − inputSchemaJson: null, | |
| 465 | − outputSchemaJson: null, | |
| 466 | − providerKind: "openapi", | |
| 467 | − mcpToolName: null, | |
| 468 | − openApiMethod: "get", | |
| 469 | − openApiPathTemplate: "/repos/{owner}/{repo}/environments/{environment_name}/secrets/{secret_name}", | |
| 470 | − openApiOperationHash: "op_actions_get_environment_secret", | |
| 471 | − openApiRequestBodyRequired: null, | |
| 472 | − createdAt: now, | |
| 473 | − updatedAt: now, | |
| 474 | − }, | |
| 475 | − }, | |
| 476 | − { | |
| 477 | − artifact: { | |
| 478 | − workspaceId: input.workspaceId, | |
| 479 | − path: "github.actions/get-self-hosted-runner-for-org", | |
| 480 | − toolId: "actions/get-self-hosted-runner-for-org", | |
| 481 | − sourceId: input.sourceId, | |
| 482 | − title: "Get a self-hosted runner for an organization", | |
| 483 | − description: | |
| 484 | − "Gets a specific self-hosted runner configured in an organization. Authenticated users must have admin access to the organization to use this endpoint.", | |
| 485 | − searchNamespace: "github.actions", | |
| 486 | − searchText: | |
| 487 | − "github.actions/get-self-hosted-runner-for-org github.actions get a self hosted runner for an organization gets a specific self hosted runner configured in an organization authenticated users must have admin access to the organization to use this endpoint GET /orgs/{org}/actions/runners/{runner_id}", | |
| 488 | − inputSchemaJson: null, | |
| 489 | − outputSchemaJson: null, | |
| 490 | − providerKind: "openapi", | |
| 491 | − mcpToolName: null, | |
| 492 | − openApiMethod: "get", | |
| 493 | − openApiPathTemplate: "/orgs/{org}/actions/runners/{runner_id}", | |
| 494 | − openApiOperationHash: "op_actions_get_self_hosted_runner_for_org", | |
| 495 | − openApiRequestBodyRequired: null, | |
| 496 | − createdAt: now, | |
| 497 | − updatedAt: now, | |
| 498 | − }, | |
| 499 | − }, | |
| 500 | − { | |
| 501 | − artifact: { | |
| 502 | − workspaceId: input.workspaceId, | |
| 503 | − path: "github.users/get-authenticated", | |
| 504 | − toolId: "users/get-authenticated", | |
| 505 | − sourceId: input.sourceId, | |
| 506 | − title: "Get the authenticated user", | |
| 507 | − description: | |
| 508 | − "Gets the authenticated user. OAuth app tokens and personal access tokens need the user scope to include private profile information.", | |
| 509 | − searchNamespace: "github.users", | |
| 510 | − searchText: | |
| 511 | − "github.users/get-authenticated github.users get the authenticated user gets the authenticated user oauth app tokens and personal access tokens need the user scope to include private profile information GET /user", | |
| 512 | − inputSchemaJson: null, | |
| 513 | − outputSchemaJson: null, | |
| 514 | − providerKind: "openapi", | |
| 515 | − mcpToolName: null, | |
| 516 | − openApiMethod: "get", | |
| 517 | − openApiPathTemplate: "/user", | |
| 518 | − openApiOperationHash: "op_users_get_authenticated", | |
| 519 | − openApiRequestBodyRequired: null, | |
| 520 | − createdAt: now, | |
| 521 | − updatedAt: now, | |
| 522 | − }, | |
| 523 | − }, | |
| 524 | − ], | |
| 525 | − }); | |
| 526 | 529 | }); |
| 527 | 530 | |
| 528 | 531 | const makeResolver = (persistence: SqlControlPlanePersistence) => |
| @@ -597,7 +600,7 @@ describe("workspace-execution-environment", () => { · 68 unmodified lines | ||
| 597 | 600 | const persistence = yield* makePersistence; |
| 598 | 601 | const workspaceId = WorkspaceIdSchema.make("ws_github_discovery"); |
| 599 | 602 | const sourceId = SourceIdSchema.make("src_github_discovery"); |
| 600 | − yield* persistConnectedGithubSearchArtifacts({ | |
| 603 | + yield* persistConnectedGithubOpenApiSource({ | |
| 601 | 604 | persistence, |
| 602 | 605 | workspaceId, |
| 603 | 606 | sourceId, |
| @@ -613,7 +616,7 @@ describe("workspace-execution-environment", () => { · 9 unmodified lines | ||
| 613 | 616 | const firstQuery = (yield* environment.toolInvoker.invoke({ |
| 614 | 617 | path: "discover", |
| 615 | 618 | args: { |
| 616 | − query: "get authenticated user", | |
| 619 | + query: "current authenticated user profile", | |
| 617 | 620 | limit: 5, |
| 618 | 621 | }, |
| 619 | 622 | })) as { |
| @@ -621,8 +624,8 @@ describe("workspace-execution-environment", () => { · 1 unmodified line | ||
| 621 | 624 | results: Array<{ path: string }>; |
| 622 | 625 | }; |
| 623 | 626 | |
| 624 | − expect(firstQuery.bestPath).toBe("github.users/get-authenticated"); | |
| 625 | − expect(firstQuery.results[0]?.path).toBe("github.users/get-authenticated"); | |
| 627 | + expect(firstQuery.bestPath).toBe("github.users.getAuthenticated"); | |
| 628 | + expect(firstQuery.results[0]?.path).toBe("github.users.getAuthenticated"); | |
| 626 | 629 | |
| 627 | 630 | const secondQuery = (yield* environment.toolInvoker.invoke({ |
| 628 | 631 | path: "discover", |
| @@ -635,8 +638,8 @@ describe("workspace-execution-environment", () => { · 6 unmodified lines | ||
| 635 | 638 | results: Array<{ path: string }>; |
| 636 | 639 | }; |
| 637 | 640 | |
| 638 | − expect(secondQuery.bestPath).toBe("github.users/get-authenticated"); | |
| 639 | − expect(secondQuery.results[0]?.path).toBe("github.users/get-authenticated"); | |
| 641 | + expect(secondQuery.bestPath).toBe("github.users.getAuthenticated"); | |
| 642 | + expect(secondQuery.results[0]?.path).toBe("github.users.getAuthenticated"); | |
| 640 | 643 | }), |
| 641 | 644 | ); |
| 642 | 645 | |
| @@ -735,6 +738,7 @@ describe("workspace-execution-environment", () => { · 92 unmodified lines | ||
| 735 | 738 | runId: "exec_add_openapi", |
| 736 | 739 | }, |
| 737 | 740 | })) as { |
| 741 | + id: SourceId; | |
| 738 | 742 | kind: string; |
| 739 | 743 | status: string; |
| 740 | 744 | auth: { |
| @@ -758,11 +762,14 @@ describe("workspace-execution-environment", () => { · 17 unmodified lines | ||
| 758 | 762 | encodeURIComponent("exec_add_openapi:executor.sources.add:"), |
| 759 | 763 | ); |
| 760 | 764 | |
| 761 | − const storedArtifacts = yield* persistence.rows.toolArtifacts.listByWorkspaceId( | |
| 765 | + const storedSource = yield* persistence.rows.sources.getByWorkspaceAndId( | |
| 762 | 766 | workspaceId, |
| 767 | + added.id, | |
| 763 | 768 | ); |
| 764 | − expect(storedArtifacts.length).toBeGreaterThan(0); | |
| 765 | − const repoToolPath = storedArtifacts[0]!.path; | |
| 769 | + expect(Option.isSome(storedSource)).toBe(true); | |
| 770 | + if (Option.isSome(storedSource)) { | |
| 771 | + expect(storedSource.value.sourceDocumentText).toContain('"operationId":"repos.getRepo"'); | |
| 772 | + } | |
| 766 | 773 | |
| 767 | 774 | const freshEnvironment = yield* resolveEnvironment({ |
| 768 | 775 | workspaceId, |
| @@ -780,10 +787,10 @@ describe("workspace-execution-environment", () => { · 11 unmodified lines | ||
| 780 | 787 | bestPath: string | null; |
| 781 | 788 | }; |
| 782 | 789 | |
| 783 | − expect(discovered.bestPath).toBe(repoToolPath); | |
| 790 | + expect(discovered.bestPath).toBe("github.repos.getRepo"); | |
| 784 | 791 | |
| 785 | 792 | const invoked = (yield* freshEnvironment.toolInvoker.invoke({ |
| 786 | − path: repoToolPath, | |
| 793 | + path: "github.repos.getRepo", | |
| 787 | 794 | args: { |
| 788 | 795 | owner: "vercel", |
| 789 | 796 | repo: "ai", |
| @@ -825,7 +832,7 @@ describe("workspace-execution-environment", () => { · 35 unmodified lines | ||
| 825 | 832 | })) as { |
| 826 | 833 | path: string; |
| 827 | 834 | description?: string; |
| 828 | − inputHint?: string; | |
| 835 | + inputType?: string; | |
| 829 | 836 | inputSchemaJson?: string; |
| 830 | 837 | } | null; |
| 831 | 838 | |
| @@ -833,8 +840,8 @@ describe("workspace-execution-environment", () => { · 1 unmodified line | ||
| 833 | 840 | expect(described?.description).toContain("Source add input shapes:"); |
| 834 | 841 | expect(described?.description).toContain("specUrl"); |
| 835 | 842 | expect(described?.description).toContain("credential setup"); |
| 836 | − expect(described?.inputHint).toContain("endpoint"); | |
| 837 | − expect(described?.inputHint).not.toContain("auth"); | |
| 843 | + expect(described?.inputType).toContain("endpoint"); | |
| 844 | + expect(described?.inputType).not.toContain("auth"); | |
| 838 | 845 | |
| 839 | 846 | const inputSchema = described?.inputSchemaJson |
| 840 | 847 | ? JSON.parse(described.inputSchemaJson) as Record<string, unknown> |
| @@ -8,13 +8,19 @@ import { · 7 unmodified lines | ||
| 8 | 8 | type ToolInvoker, |
| 9 | 9 | type ToolNamespace, |
| 10 | 10 | type ToolPath, |
| 11 | + typeSignatureFromSchemaJson, | |
| 11 | 12 | } from "@executor-v3/codemode-core"; |
| 12 | 13 | import { |
| 13 | 14 | createSdkMcpConnector, |
| 14 | 15 | createMcpToolsFromManifest, |
| 15 | 16 | } from "@executor-v3/codemode-mcp"; |
| 16 | 17 | import { |
| 18 | + buildOpenApiToolPresentation, | |
| 19 | + compileOpenApiToolDefinitions, | |
| 17 | 20 | createOpenApiToolsFromManifest, |
| 21 | + extractOpenApiManifest, | |
| 22 | + type OpenApiToolDefinition, | |
| 23 | + type OpenApiToolManifest, | |
| 18 | 24 | } from "@executor-v3/codemode-openapi"; |
| 19 | 25 | import { makeInProcessExecutor } from "@executor-v3/runtime-local-inproc"; |
| 20 | 26 | import { |
| @@ -35,7 +41,7 @@ import type { · 14 unmodified lines | ||
| 35 | 41 | ResolveExecutionEnvironment, |
| 36 | 42 | } from "./execution-state"; |
| 37 | 43 | import { createExecutorToolMap } from "./executor-tools"; |
| 38 | −import { projectSourceFromStorage } from "./source-definitions"; | |
| 44 | +import { projectSourceFromStorage, projectSourcesFromStorage } from "./source-definitions"; | |
| 39 | 45 | import { |
| 40 | 46 | RuntimeSourceAuthServiceTag, |
| 41 | 47 | createDbBackedSecretMaterialResolver, |
| @@ -58,6 +64,7 @@ const tokenize = (value: string): string[] => · 16 unmodified lines | ||
| 58 | 64 | .split(/[^a-z0-9]+/) |
| 59 | 65 | .filter(Boolean); |
| 60 | 66 | |
| 67 | + | |
| 61 | 68 | const LOW_SIGNAL_QUERY_TOKENS = new Set([ |
| 62 | 69 | "a", |
| 63 | 70 | "an", |
| @@ -207,22 +214,153 @@ const scoreArtifact = ( · 143 unmodified lines | ||
| 207 | 214 | return score; |
| 208 | 215 | }; |
| 209 | 216 | |
| 210 | −const toDescriptor = (input: { | |
| 217 | +const catalogNamespaceFromPath = (path: string): string => { | |
| 218 | + const [first, second] = path.split("."); | |
| 219 | + return second ? `${first}.${second}` : first; | |
| 220 | +}; | |
| 221 | + | |
| 222 | +const toPersistedDescriptor = (input: { | |
| 211 | 223 | artifact: StoredToolArtifactRecord; |
| 212 | 224 | includeSchemas: boolean; |
| 213 | − refHintKeys?: readonly string[]; | |
| 214 | 225 | }): ToolDescriptor => ({ |
| 215 | 226 | path: asToolPath(input.artifact.path), |
| 216 | 227 | sourceKey: input.artifact.sourceId, |
| 217 | 228 | description: input.artifact.description ?? input.artifact.title ?? undefined, |
| 218 | 229 | interaction: "auto", |
| 219 | − inputHint: input.artifact.inputSchemaJson ? "object" : undefined, | |
| 220 | − outputHint: input.artifact.outputSchemaJson ? "output" : undefined, | |
| 230 | + inputType: typeSignatureFromSchemaJson( | |
| 231 | + input.artifact.inputSchemaJson ?? undefined, | |
| 232 | + "unknown", | |
| 233 | + 320, | |
| 234 | + ), | |
| 235 | + outputType: typeSignatureFromSchemaJson( | |
| 236 | + input.artifact.outputSchemaJson ?? undefined, | |
| 237 | + "unknown", | |
| 238 | + 320, | |
| 239 | + ), | |
| 221 | 240 | inputSchemaJson: input.includeSchemas ? input.artifact.inputSchemaJson ?? undefined : undefined, |
| 222 | 241 | outputSchemaJson: input.includeSchemas ? input.artifact.outputSchemaJson ?? undefined : undefined, |
| 223 | − refHintKeys: input.includeSchemas ? input.refHintKeys : undefined, | |
| 242 | + ...(input.artifact.providerKind ? { providerKind: input.artifact.providerKind } : {}), | |
| 224 | 243 | }); |
| 225 | 244 | |
| 245 | +type OpenApiWorkspaceTool = { | |
| 246 | + path: ToolPath; | |
| 247 | + source: Source; | |
| 248 | + manifest: OpenApiToolManifest; | |
| 249 | + definition: OpenApiToolDefinition; | |
| 250 | + descriptor: ToolDescriptor; | |
| 251 | + searchNamespace: string; | |
| 252 | + searchText: string; | |
| 253 | +}; | |
| 254 | + | |
| 255 | +const loadOpenApiWorkspaceTools = (input: { | |
| 256 | + rows: SqlControlPlaneRows; | |
| 257 | + workspaceId: Source["workspaceId"]; | |
| 258 | + includeSchemas: boolean; | |
| 259 | +}): Effect.Effect<ReadonlyArray<OpenApiWorkspaceTool>, Error, never> => | |
| 260 | + Effect.gen(function* () { | |
| 261 | + const [sourceRecords, credentialBindings] = yield* Effect.all([ | |
| 262 | + input.rows.sources.listByWorkspaceId(input.workspaceId), | |
| 263 | + input.rows.sourceCredentialBindings.listByWorkspaceId(input.workspaceId), | |
| 264 | + ]).pipe( | |
| 265 | + Effect.mapError((cause) => | |
| 266 | + cause instanceof Error ? cause : new Error(String(cause)), | |
| 267 | + ), | |
| 268 | + ); | |
| 269 | + | |
| 270 | + const documentBySourceId = new Map( | |
| 271 | + sourceRecords.map((record) => [record.id, record.sourceDocumentText]), | |
| 272 | + ); | |
| 273 | + const sources = yield* projectSourcesFromStorage({ | |
| 274 | + sourceRecords, | |
| 275 | + credentialBindings, | |
| 276 | + }); | |
| 277 | + | |
| 278 | + const connectedOpenApiSources = sources.filter((source) => | |
| 279 | + source.enabled | |
| 280 | + && source.status === "connected" | |
| 281 | + && source.kind === "openapi" | |
| 282 | + && typeof documentBySourceId.get(source.id) === "string" | |
| 283 | + && documentBySourceId.get(source.id)!.length > 0, | |
| 284 | + ); | |
| 285 | + | |
| 286 | + const toolGroups = yield* Effect.forEach( | |
| 287 | + connectedOpenApiSources, | |
| 288 | + (source) => | |
| 289 | + Effect.gen(function* () { | |
| 290 | + const openApiDocument = documentBySourceId.get(source.id)!; | |
| 291 | + const manifest = yield* extractOpenApiManifest( | |
| 292 | + source.name, | |
| 293 | + openApiDocument, | |
| 294 | + ).pipe( | |
| 295 | + Effect.mapError((cause) => | |
| 296 | + cause instanceof Error ? cause : new Error(String(cause)), | |
| 297 | + ), | |
| 298 | + ); | |
| 299 | + const definitions = compileOpenApiToolDefinitions(manifest); | |
| 300 | + const namespace = source.namespace ?? namespaceFromSourceName(source.name); | |
| 301 | + | |
| 302 | + return definitions.map((definition) => { | |
| 303 | + const presentation = buildOpenApiToolPresentation({ | |
| 304 | + manifest, | |
| 305 | + definition, | |
| 306 | + }); | |
| 307 | + const path = asToolPath( | |
| 308 | + namespace ? `${namespace}.${definition.toolId}` : definition.toolId, | |
| 309 | + ); | |
| 310 | + const searchNamespace = catalogNamespaceFromPath(path); | |
| 311 | + const searchText = [ | |
| 312 | + path, | |
| 313 | + searchNamespace, | |
| 314 | + definition.name, | |
| 315 | + definition.description, | |
| 316 | + definition.rawToolId, | |
| 317 | + definition.tags.join(" "), | |
| 318 | + definition.method.toUpperCase(), | |
| 319 | + definition.path, | |
| 320 | + ] | |
| 321 | + .filter((part): part is string => typeof part === "string" && part.length > 0) | |
| 322 | + .join(" ") | |
| 323 | + .toLowerCase(); | |
| 324 | + | |
| 325 | + return { | |
| 326 | + path, | |
| 327 | + source, | |
| 328 | + manifest, | |
| 329 | + definition, | |
| 330 | + descriptor: { | |
| 331 | + path, | |
| 332 | + sourceKey: source.id, | |
| 333 | + description: definition.description, | |
| 334 | + interaction: "auto", | |
| 335 | + inputType: presentation.inputType, | |
| 336 | + outputType: presentation.outputType, | |
| 337 | + ...(input.includeSchemas && presentation.inputSchemaJson | |
| 338 | + ? { inputSchemaJson: presentation.inputSchemaJson } | |
| 339 | + : {}), | |
| 340 | + ...(input.includeSchemas && presentation.outputSchemaJson | |
| 341 | + ? { outputSchemaJson: presentation.outputSchemaJson } | |
| 342 | + : {}), | |
| 343 | + ...(presentation.exampleInputJson | |
| 344 | + ? { exampleInputJson: presentation.exampleInputJson } | |
| 345 | + : {}), | |
| 346 | + ...(presentation.exampleOutputJson | |
| 347 | + ? { exampleOutputJson: presentation.exampleOutputJson } | |
| 348 | + : {}), | |
| 349 | + providerKind: "openapi", | |
| 350 | + providerDataJson: presentation.providerDataJson, | |
| 351 | + } satisfies ToolDescriptor, | |
| 352 | + searchNamespace, | |
| 353 | + searchText, | |
| 354 | + } satisfies OpenApiWorkspaceTool; | |
| 355 | + }); | |
| 356 | + }), | |
| 357 | + { concurrency: "unbounded" }, | |
| 358 | + ); | |
| 359 | + | |
| 360 | + return toolGroups.flat(); | |
| 361 | + }); | |
| 362 | + | |
| 363 | + | |
| 226 | 364 | const loadSourceById = (input: { |
| 227 | 365 | rows: SqlControlPlaneRows; |
| 228 | 366 | workspaceId: Source["workspaceId"]; |
| @@ -265,7 +403,7 @@ const createWorkspaceToolCatalog = (input: { · 36 unmodified lines | ||
| 265 | 403 | }): ToolCatalog => ({ |
| 266 | 404 | listNamespaces: ({ limit }) => |
| 267 | 405 | Effect.gen(function* () { |
| 268 | − const [persisted, executor] = yield* Effect.all([ | |
| 406 | + const [persisted, openApiTools, executor] = yield* Effect.all([ | |
| 269 | 407 | input.rows.toolArtifacts.listNamespacesByWorkspaceId(input.workspaceId, { |
| 270 | 408 | limit, |
| 271 | 409 | }).pipe( |
| @@ -273,6 +411,11 @@ const createWorkspaceToolCatalog = (input: { · 1 unmodified line | ||
| 273 | 411 | cause instanceof Error ? cause : new Error(String(cause)), |
| 274 | 412 | ), |
| 275 | 413 | ), |
| 414 | + loadOpenApiWorkspaceTools({ | |
| 415 | + rows: input.rows, | |
| 416 | + workspaceId: input.workspaceId, | |
| 417 | + includeSchemas: false, | |
| 418 | + }), | |
| 276 | 419 | input.executorCatalog.listNamespaces({ limit }), |
| 277 | 420 | ]); |
| 278 | 421 | |
| @@ -280,6 +423,13 @@ const createWorkspaceToolCatalog = (input: { · 1 unmodified line | ||
| 280 | 423 | for (const namespace of persisted) { |
| 281 | 424 | merged.set(namespace.namespace, namespace); |
| 282 | 425 | } |
| 426 | + for (const tool of openApiTools) { | |
| 427 | + const existing = merged.get(tool.searchNamespace); | |
| 428 | + merged.set(tool.searchNamespace, { | |
| 429 | + namespace: tool.searchNamespace, | |
| 430 | + toolCount: (existing?.toolCount ?? 0) + 1, | |
| 431 | + }); | |
| 432 | + } | |
| 283 | 433 | for (const namespace of executor) { |
| 284 | 434 | const existing = merged.get(namespace.namespace); |
| 285 | 435 | merged.set(namespace.namespace, { |
| @@ -299,7 +449,7 @@ const createWorkspaceToolCatalog = (input: { · 13 unmodified lines | ||
| 299 | 449 | |
| 300 | 450 | listTools: ({ namespace, query, limit, includeSchemas = false }) => |
| 301 | 451 | Effect.gen(function* () { |
| 302 | − const [persisted, executor] = yield* Effect.all([ | |
| 452 | + const [persisted, openApiTools, executor] = yield* Effect.all([ | |
| 303 | 453 | namespace?.startsWith("executor") |
| 304 | 454 | ? Effect.succeed([] as readonly StoredToolArtifactRecord[]) |
| 305 | 455 | : input.rows.toolArtifacts.listByWorkspaceId(input.workspaceId, { |
| @@ -311,6 +461,23 @@ const createWorkspaceToolCatalog = (input: { · 5 unmodified lines | ||
| 311 | 461 | cause instanceof Error ? cause : new Error(String(cause)), |
| 312 | 462 | ), |
| 313 | 463 | ), |
| 464 | + loadOpenApiWorkspaceTools({ | |
| 465 | + rows: input.rows, | |
| 466 | + workspaceId: input.workspaceId, | |
| 467 | + includeSchemas, | |
| 468 | + }).pipe( | |
| 469 | + Effect.map((tools) => | |
| 470 | + tools.filter((tool) => { | |
| 471 | + if (namespace && tool.searchNamespace !== namespace) { | |
| 472 | + return false; | |
| 473 | + } | |
| 474 | + if (!query) { | |
| 475 | + return true; | |
| 476 | + } | |
| 477 | + return tokenize(query).every((token) => tool.searchText.includes(token)); | |
| 478 | + }), | |
| 479 | + ), | |
| 480 | + ), | |
| 314 | 481 | input.executorCatalog.listTools({ |
| 315 | 482 | ...(namespace !== undefined ? { namespace } : {}), |
| 316 | 483 | ...(query !== undefined ? { query } : {}), |
| @@ -319,35 +486,18 @@ const createWorkspaceToolCatalog = (input: { · 2 unmodified lines | ||
| 319 | 486 | }), |
| 320 | 487 | ]); |
| 321 | 488 | |
| 322 | − const persistedDescriptors = includeSchemas | |
| 323 | − ? yield* Effect.forEach( | |
| 324 | − persisted, | |
| 325 | − (artifact) => | |
| 326 | − input.rows.toolArtifacts.listRefHintKeysByWorkspaceAndPath( | |
| 327 | − input.workspaceId, | |
| 328 | − artifact.path, | |
| 329 | − ).pipe( | |
| 330 | − Effect.map((rows) => | |
| 331 | − toDescriptor({ | |
| 332 | − artifact, | |
| 333 | − includeSchemas, | |
| 334 | − refHintKeys: rows.map((row) => row.refHintKey), | |
| 335 | − }) | |
| 336 | − ), | |
| 337 | − Effect.mapError((cause) => | |
| 338 | − cause instanceof Error ? cause : new Error(String(cause)), | |
| 339 | − ), | |
| 340 | − ), | |
| 341 | − { concurrency: "unbounded" }, | |
| 342 | − ) | |
| 343 | − : persisted.map((artifact) => | |
| 344 | − toDescriptor({ | |
| 345 | − artifact, | |
| 346 | − includeSchemas, | |
| 347 | − }) | |
| 348 | − ); | |
| 489 | + const persistedDescriptors = persisted.map((artifact) => | |
| 490 | + toPersistedDescriptor({ | |
| 491 | + artifact, | |
| 492 | + includeSchemas, | |
| 493 | + }), | |
| 494 | + ); | |
| 349 | 495 | |
| 350 | − return [...persistedDescriptors, ...executor] | |
| 496 | + return [ | |
| 497 | + ...persistedDescriptors, | |
| 498 | + ...openApiTools.map((tool) => tool.descriptor), | |
| 499 | + ...executor, | |
| 500 | + ] | |
| 351 | 501 | .sort((left, right) => left.path.localeCompare(right.path)) |
| 352 | 502 | .slice(0, limit); |
| 353 | 503 | }), |
| @@ -362,6 +512,16 @@ const createWorkspaceToolCatalog = (input: { · 8 unmodified lines | ||
| 362 | 512 | return executor; |
| 363 | 513 | } |
| 364 | 514 | |
| 515 | + const openApiTools = yield* loadOpenApiWorkspaceTools({ | |
| 516 | + rows: input.rows, | |
| 517 | + workspaceId: input.workspaceId, | |
| 518 | + includeSchemas, | |
| 519 | + }); | |
| 520 | + const openApiTool = openApiTools.find((tool) => tool.path === path); | |
| 521 | + if (openApiTool) { | |
| 522 | + return openApiTool.descriptor; | |
| 523 | + } | |
| 524 | + | |
| 365 | 525 | const artifact = yield* input.rows.toolArtifacts.getByWorkspaceAndPath( |
| 366 | 526 | input.workspaceId, |
| 367 | 527 | path, |
| @@ -375,29 +535,16 @@ const createWorkspaceToolCatalog = (input: { · 7 unmodified lines | ||
| 375 | 535 | return null; |
| 376 | 536 | } |
| 377 | 537 | |
| 378 | − const refHintKeys = includeSchemas | |
| 379 | − ? yield* input.rows.toolArtifacts.listRefHintKeysByWorkspaceAndPath( | |
| 380 | − input.workspaceId, | |
| 381 | − path, | |
| 382 | − ).pipe( | |
| 383 | − Effect.map((rows) => rows.map((row) => row.refHintKey)), | |
| 384 | − Effect.mapError((cause) => | |
| 385 | − cause instanceof Error ? cause : new Error(String(cause)), | |
| 386 | − ), | |
| 387 | − ) | |
| 388 | − : undefined; | |
| 389 | − | |
| 390 | − return toDescriptor({ | |
| 538 | + return toPersistedDescriptor({ | |
| 391 | 539 | artifact: artifact.value, |
| 392 | 540 | includeSchemas, |
| 393 | − refHintKeys, | |
| 394 | 541 | }); |
| 395 | 542 | }), |
| 396 | 543 | |
| 397 | 544 | searchTools: ({ query, namespace, limit }) => |
| 398 | 545 | Effect.gen(function* () { |
| 399 | 546 | const queryTokens = tokenize(query); |
| 400 | − const [persisted, executor] = yield* Effect.all([ | |
| 547 | + const [persisted, openApiTools, executor] = yield* Effect.all([ | |
| 401 | 548 | namespace?.startsWith("executor") |
| 402 | 549 | ? Effect.succeed([] as readonly StoredToolArtifactRecord[]) |
| 403 | 550 | : input.rows.toolArtifacts.searchByWorkspaceId(input.workspaceId, { |
| @@ -408,6 +555,15 @@ const createWorkspaceToolCatalog = (input: { · 4 unmodified lines | ||
| 408 | 555 | cause instanceof Error ? cause : new Error(String(cause)), |
| 409 | 556 | ), |
| 410 | 557 | ), |
| 558 | + loadOpenApiWorkspaceTools({ | |
| 559 | + rows: input.rows, | |
| 560 | + workspaceId: input.workspaceId, | |
| 561 | + includeSchemas: false, | |
| 562 | + }).pipe( | |
| 563 | + Effect.map((tools) => | |
| 564 | + tools.filter((tool) => !namespace || tool.searchNamespace === namespace), | |
| 565 | + ), | |
| 566 | + ), | |
| 411 | 567 | input.executorCatalog.searchTools({ |
| 412 | 568 | query, |
| 413 | 569 | ...(namespace !== undefined ? { namespace } : {}), |
| @@ -422,7 +578,17 @@ const createWorkspaceToolCatalog = (input: { · 8 unmodified lines | ||
| 422 | 578 | })) |
| 423 | 579 | .filter((hit) => hit.score > 0); |
| 424 | 580 | |
| 425 | − return [...persistedHits, ...executor] | |
| 581 | + const openApiHits: SearchHit[] = openApiTools | |
| 582 | + .map((tool) => ({ | |
| 583 | + path: tool.path, | |
| 584 | + score: tokenize(query).reduce( | |
| 585 | + (total, token) => total + (tool.searchText.includes(token) ? 1 : 0), | |
| 586 | + 0, | |
| 587 | + ), | |
| 588 | + })) | |
| 589 | + .filter((hit) => hit.score > 0); | |
| 590 | + | |
| 591 | + return [...persistedHits, ...openApiHits, ...executor] | |
| 426 | 592 | .sort((left, right) => |
| 427 | 593 | right.score - left.score || left.path.localeCompare(right.path), |
| 428 | 594 | ) |
| @@ -470,6 +636,37 @@ const createWorkspaceToolInvoker = (input: { · 41 unmodified lines | ||
| 470 | 636 | context?: Record<string, unknown>; |
| 471 | 637 | }) => |
| 472 | 638 | Effect.gen(function* () { |
| 639 | + const openApiTools = yield* loadOpenApiWorkspaceTools({ | |
| 640 | + rows: input.rows, | |
| 641 | + workspaceId: input.workspaceId, | |
| 642 | + includeSchemas: false, | |
| 643 | + }); | |
| 644 | + const openApiTool = openApiTools.find((tool) => tool.path === invocation.path); | |
| 645 | + if (openApiTool) { | |
| 646 | + const auth = yield* resolveSourceAuthMaterial({ | |
| 647 | + source: openApiTool.source, | |
| 648 | + resolveSecretMaterial: input.resolveSecretMaterial, | |
| 649 | + }); | |
| 650 | + | |
| 651 | + const tools = createOpenApiToolsFromManifest({ | |
| 652 | + manifest: openApiTool.manifest, | |
| 653 | + baseUrl: openApiTool.source.endpoint, | |
| 654 | + namespace: openApiTool.source.namespace ?? namespaceFromSourceName(openApiTool.source.name), | |
| 655 | + sourceKey: openApiTool.source.id, | |
| 656 | + defaultHeaders: openApiTool.source.defaultHeaders ?? {}, | |
| 657 | + credentialHeaders: auth.headers, | |
| 658 | + }); | |
| 659 | + | |
| 660 | + return yield* makeToolInvokerFromTools({ | |
| 661 | + tools, | |
| 662 | + onElicitation: input.onElicitation, | |
| 663 | + }).invoke({ | |
| 664 | + path: invocation.path, | |
| 665 | + args: invocation.args, | |
| 666 | + context: invocation.context, | |
| 667 | + }); | |
| 668 | + } | |
| 669 | + | |
| 473 | 670 | const artifactOption = yield* input.rows.toolArtifacts |
| 474 | 671 | .getByWorkspaceAndPath(input.workspaceId, invocation.path) |
| 475 | 672 | .pipe( |
| @@ -535,79 +732,6 @@ const createWorkspaceToolInvoker = (input: { · 59 unmodified lines | ||
| 535 | 732 | }); |
| 536 | 733 | } |
| 537 | 734 | |
| 538 | − if (artifact.providerKind === "openapi") { | |
| 539 | − const [parameters, requestBodyContentTypes, refHintKeys] = yield* Effect.all([ | |
| 540 | − input.rows.toolArtifacts.listParametersByWorkspaceAndPath( | |
| 541 | − input.workspaceId, | |
| 542 | − artifact.path, | |
| 543 | − ), | |
| 544 | − input.rows.toolArtifacts.listRequestBodyContentTypesByWorkspaceAndPath( | |
| 545 | − input.workspaceId, | |
| 546 | − artifact.path, | |
| 547 | − ), | |
| 548 | − input.rows.toolArtifacts.listRefHintKeysByWorkspaceAndPath( | |
| 549 | − input.workspaceId, | |
| 550 | − artifact.path, | |
| 551 | − ), | |
| 552 | − ]).pipe( | |
| 553 | − Effect.mapError((cause) => | |
| 554 | − cause instanceof Error ? cause : new Error(String(cause)), | |
| 555 | − ), | |
| 556 | − ); | |
| 557 | − | |
| 558 | − const tools = createOpenApiToolsFromManifest({ | |
| 559 | − manifest: { | |
| 560 | − version: 1, | |
| 561 | − sourceHash: source.sourceHash ?? "stored", | |
| 562 | − tools: [{ | |
| 563 | − toolId: storedToolIdFromArtifact(artifact), | |
| 564 | − name: artifact.title ?? storedToolIdFromArtifact(artifact), | |
| 565 | − description: artifact.description ?? null, | |
| 566 | − method: artifact.openApiMethod!, | |
| 567 | − path: artifact.openApiPathTemplate!, | |
| 568 | − invocation: { | |
| 569 | − method: artifact.openApiMethod!, | |
| 570 | − pathTemplate: artifact.openApiPathTemplate!, | |
| 571 | − parameters: parameters.map((parameter) => ({ | |
| 572 | − name: parameter.name, | |
| 573 | − location: parameter.location, | |
| 574 | − required: parameter.required, | |
| 575 | − })), | |
| 576 | − requestBody: | |
| 577 | − artifact.openApiRequestBodyRequired === null | |
| 578 | − ? null | |
| 579 | − : { | |
| 580 | − required: artifact.openApiRequestBodyRequired, | |
| 581 | − contentTypes: requestBodyContentTypes.map((row) => row.contentType), | |
| 582 | − }, | |
| 583 | − }, | |
| 584 | − operationHash: artifact.openApiOperationHash!, | |
| 585 | − typing: { | |
| 586 | − ...(artifact.inputSchemaJson ? { inputSchemaJson: artifact.inputSchemaJson } : {}), | |
| 587 | − ...(artifact.outputSchemaJson ? { outputSchemaJson: artifact.outputSchemaJson } : {}), | |
| 588 | − ...(refHintKeys.length > 0 | |
| 589 | − ? { refHintKeys: refHintKeys.map((row) => row.refHintKey) } | |
| 590 | − : {}), | |
| 591 | − }, | |
| 592 | − }], | |
| 593 | − }, | |
| 594 | − baseUrl: source.endpoint, | |
| 595 | − namespace: source.namespace ?? namespaceFromSourceName(source.name), | |
| 596 | − sourceKey: source.id, | |
| 597 | − defaultHeaders: source.defaultHeaders ?? {}, | |
| 598 | − credentialHeaders: auth.headers, | |
| 599 | − }); | |
| 600 | − | |
| 601 | − return yield* makeToolInvokerFromTools({ | |
| 602 | − tools, | |
| 603 | − onElicitation: input.onElicitation, | |
| 604 | − }).invoke({ | |
| 605 | − path: invocation.path, | |
| 606 | − args: invocation.args, | |
| 607 | − context: invocation.context, | |
| 608 | − }); | |
| 609 | − } | |
| 610 | − | |
| 611 | 735 | return yield* Effect.fail( |
| 612 | 736 | new Error(`Unsupported stored tool provider for ${invocation.path}`), |
| 613 | 737 | ); |
| @@ -11,3 +11,4 @@ export * from "./models/source-auth-session"; · 10 unmodified lines | ||
| 11 | 11 | export * from "./models/policy"; |
| 12 | 12 | export * from "./models/local-installation"; |
| 13 | 13 | export * from "./models/execution"; |
| 14 | +export * from "./models/source-inspection"; | |
| @@ -0,0 +1,85 @@ | ||
| 1 | +import { OpenApiHttpMethodSchema } from "@executor-v3/codemode-openapi"; | |
| 2 | +import { Schema } from "effect"; | |
| 3 | + | |
| 4 | +import { SourceSchema } from "./source"; | |
| 5 | + | |
| 6 | +export const SourceInspectionPipelineKindSchema = Schema.Literal( | |
| 7 | + "openapi", | |
| 8 | + "persisted", | |
| 9 | +); | |
| 10 | + | |
| 11 | +export const SourceInspectionToolSummarySchema = Schema.Struct({ | |
| 12 | + path: Schema.String, | |
| 13 | + sourceKey: Schema.String, | |
| 14 | + title: Schema.optional(Schema.String), | |
| 15 | + description: Schema.optional(Schema.String), | |
| 16 | + providerKind: Schema.String, | |
| 17 | + toolId: Schema.String, | |
| 18 | + rawToolId: Schema.NullOr(Schema.String), | |
| 19 | + operationId: Schema.NullOr(Schema.String), | |
| 20 | + group: Schema.NullOr(Schema.String), | |
| 21 | + leaf: Schema.NullOr(Schema.String), | |
| 22 | + tags: Schema.Array(Schema.String), | |
| 23 | + method: Schema.NullOr(OpenApiHttpMethodSchema), | |
| 24 | + pathTemplate: Schema.NullOr(Schema.String), | |
| 25 | + inputType: Schema.optional(Schema.String), | |
| 26 | + outputType: Schema.optional(Schema.String), | |
| 27 | +}); | |
| 28 | + | |
| 29 | +export const SourceInspectionSchema = Schema.Struct({ | |
| 30 | + source: SourceSchema, | |
| 31 | + namespace: Schema.String, | |
| 32 | + pipelineKind: SourceInspectionPipelineKindSchema, | |
| 33 | + toolCount: Schema.Number, | |
| 34 | + rawDocumentText: Schema.NullOr(Schema.String), | |
| 35 | + manifestJson: Schema.NullOr(Schema.String), | |
| 36 | + definitionsJson: Schema.NullOr(Schema.String), | |
| 37 | + tools: Schema.Array(SourceInspectionToolSummarySchema), | |
| 38 | +}); | |
| 39 | + | |
| 40 | +export const SourceInspectionToolDetailSchema = Schema.Struct({ | |
| 41 | + summary: SourceInspectionToolSummarySchema, | |
| 42 | + definitionJson: Schema.NullOr(Schema.String), | |
| 43 | + documentationJson: Schema.NullOr(Schema.String), | |
| 44 | + providerDataJson: Schema.NullOr(Schema.String), | |
| 45 | + inputSchemaJson: Schema.NullOr(Schema.String), | |
| 46 | + outputSchemaJson: Schema.NullOr(Schema.String), | |
| 47 | + exampleInputJson: Schema.NullOr(Schema.String), | |
| 48 | + exampleOutputJson: Schema.NullOr(Schema.String), | |
| 49 | +}); | |
| 50 | + | |
| 51 | +export const SourceInspectionDiscoverPayloadSchema = Schema.Struct({ | |
| 52 | + query: Schema.String, | |
| 53 | + limit: Schema.optional(Schema.Number), | |
| 54 | +}); | |
| 55 | + | |
| 56 | +export const SourceInspectionDiscoverResultItemSchema = Schema.Struct({ | |
| 57 | + path: Schema.String, | |
| 58 | + score: Schema.Number, | |
| 59 | + description: Schema.optional(Schema.String), | |
| 60 | + inputType: Schema.optional(Schema.String), | |
| 61 | + outputType: Schema.optional(Schema.String), | |
| 62 | + reasons: Schema.Array(Schema.String), | |
| 63 | +}); | |
| 64 | + | |
| 65 | +export const SourceInspectionDiscoverResultSchema = Schema.Struct({ | |
| 66 | + query: Schema.String, | |
| 67 | + queryTokens: Schema.Array(Schema.String), | |
| 68 | + bestPath: Schema.NullOr(Schema.String), | |
| 69 | + total: Schema.Number, | |
| 70 | + results: Schema.Array(SourceInspectionDiscoverResultItemSchema), | |
| 71 | +}); | |
| 72 | + | |
| 73 | +export type SourceInspectionPipelineKind = | |
| 74 | + typeof SourceInspectionPipelineKindSchema.Type; | |
| 75 | +export type SourceInspectionToolSummary = | |
| 76 | + typeof SourceInspectionToolSummarySchema.Type; | |
| 77 | +export type SourceInspection = typeof SourceInspectionSchema.Type; | |
| 78 | +export type SourceInspectionToolDetail = | |
| 79 | + typeof SourceInspectionToolDetailSchema.Type; | |
| 80 | +export type SourceInspectionDiscoverPayload = | |
| 81 | + typeof SourceInspectionDiscoverPayloadSchema.Type; | |
| 82 | +export type SourceInspectionDiscoverResultItem = | |
| 83 | + typeof SourceInspectionDiscoverResultItemSchema.Type; | |
| 84 | +export type SourceInspectionDiscoverResult = | |
| 85 | + typeof SourceInspectionDiscoverResultSchema.Type; | |
| @@ -95,6 +95,7 @@ export const StoredSourceRecordSchema = Schema.transform( · 94 unmodified lines | ||
| 95 | 95 | authHeaderName: Schema.NullOr(Schema.String), |
| 96 | 96 | authPrefix: Schema.NullOr(Schema.String), |
| 97 | 97 | sourceHash: Schema.NullOr(Schema.String), |
| 98 | + sourceDocumentText: Schema.NullOr(Schema.String), | |
| 98 | 99 | lastError: Schema.NullOr(Schema.String), |
| 99 | 100 | createdAt: TimestampMsSchema, |
| 100 | 101 | updatedAt: TimestampMsSchema, |
| @@ -119,6 +120,7 @@ export const StoredSourceRecordSchema = Schema.transform( · 18 unmodified lines | ||
| 119 | 120 | authHeaderName: row.authHeaderName, |
| 120 | 121 | authPrefix: row.authPrefix, |
| 121 | 122 | sourceHash: row.sourceHash, |
| 123 | + sourceDocumentText: row.sourceDocumentText, | |
| 122 | 124 | lastError: row.lastError, |
| 123 | 125 | createdAt: row.createdAt, |
| 124 | 126 | updatedAt: row.updatedAt, |
| @@ -141,6 +143,7 @@ export const StoredSourceRecordSchema = Schema.transform( · 16 unmodified lines | ||
| 141 | 143 | authHeaderName: source.authHeaderName, |
| 142 | 144 | authPrefix: source.authPrefix, |
| 143 | 145 | sourceHash: source.sourceHash, |
| 146 | + sourceDocumentText: source.sourceDocumentText, | |
| 144 | 147 | lastError: source.lastError, |
| 145 | 148 | createdAt: source.createdAt, |
| 146 | 149 | updatedAt: source.updatedAt, |
| @@ -56,6 +56,9 @@ export const StoredToolArtifactRecordSchema = Schema.Struct({ · 55 unmodified lines | ||
| 56 | 56 | ), |
| 57 | 57 | openApiPathTemplate: Schema.NullOr(Schema.String), |
| 58 | 58 | openApiOperationHash: Schema.NullOr(Schema.String), |
| 59 | + openApiRawToolId: Schema.NullOr(Schema.String), | |
| 60 | + openApiOperationId: Schema.NullOr(Schema.String), | |
| 61 | + openApiTagsJson: Schema.NullOr(Schema.String), | |
| 59 | 62 | openApiRequestBodyRequired: Schema.NullOr(Schema.Boolean), |
| 60 | 63 | createdAt: TimestampMsSchema, |
| 61 | 64 | updatedAt: TimestampMsSchema, |
| @@ -84,6 +87,9 @@ export const ToolArtifactStorageSchema = Schema.transform( · 22 unmodified lines | ||
| 84 | 87 | openApiMethod: row.openApiMethod, |
| 85 | 88 | openApiPathTemplate: row.openApiPathTemplate, |
| 86 | 89 | openApiOperationHash: row.openApiOperationHash, |
| 90 | + openApiRawToolId: row.openApiRawToolId, | |
| 91 | + openApiOperationId: row.openApiOperationId, | |
| 92 | + openApiTagsJson: row.openApiTagsJson, | |
| 87 | 93 | openApiRequestBodyRequired: row.openApiRequestBodyRequired, |
| 88 | 94 | createdAt: row.createdAt, |
| 89 | 95 | updatedAt: row.updatedAt, |
| @@ -104,6 +110,9 @@ export const ToolArtifactStorageSchema = Schema.transform( · 14 unmodified lines | ||
| 104 | 110 | openApiMethod: record.openApiMethod, |
| 105 | 111 | openApiPathTemplate: record.openApiPathTemplate, |
| 106 | 112 | openApiOperationHash: record.openApiOperationHash, |
| 113 | + openApiRawToolId: record.openApiRawToolId, | |
| 114 | + openApiOperationId: record.openApiOperationId, | |
| 115 | + openApiTagsJson: record.openApiTagsJson, | |
| 107 | 116 | openApiRequestBodyRequired: record.openApiRequestBodyRequired, |
| 108 | 117 | createdAt: record.createdAt, |
| 109 | 118 | updatedAt: record.updatedAt, |
| @@ -67,8 +67,8 @@ const toDescriptor = ( · 66 unmodified lines | ||
| 67 | 67 | sourceKey: artifact.sourceId, |
| 68 | 68 | description: artifact.description ?? artifact.title ?? undefined, |
| 69 | 69 | interaction: "auto" as const, |
| 70 | − inputHint: includeSchemas && artifact.inputSchemaJson ? "object" : undefined, | |
| 71 | − outputHint: includeSchemas && artifact.outputSchemaJson ? "output" : undefined, | |
| 70 | + inputType: includeSchemas && artifact.inputSchemaJson ? "object" : undefined, | |
| 71 | + outputType: includeSchemas && artifact.outputSchemaJson ? "output" : undefined, | |
| 72 | 72 | inputSchemaJson: includeSchemas ? artifact.inputSchemaJson ?? undefined : undefined, |
| 73 | 73 | outputSchemaJson: includeSchemas ? artifact.outputSchemaJson ?? undefined : undefined, |
| 74 | 74 | }); |
| @@ -527,6 +527,9 @@ const openApiArtifact = (input: { · 452 unmodified lines | ||
| 527 | 527 | openApiMethod: input.method, |
| 528 | 528 | openApiPathTemplate: input.pathTemplate, |
| 529 | 529 | openApiOperationHash: input.operationHash ?? input.toolId, |
| 530 | + openApiRawToolId: input.toolId, | |
| 531 | + openApiOperationId: input.toolId, | |
| 532 | + openApiTagsJson: null, | |
| 530 | 533 | openApiRequestBodyRequired: null, |
| 531 | 534 | createdAt: 0, |
| 532 | 535 | updatedAt: 0, |
| @@ -805,13 +808,14 @@ describe("source runtime", () => { · 272 unmodified lines | ||
| 805 | 808 | score: expect.any(Number), |
| 806 | 809 | description: "GET /repos/{owner}/{repo}", |
| 807 | 810 | interaction: "auto", |
| 808 | − inputHint: "object { owner, repo }", | |
| 809 | − outputHint: "unknown", | |
| 811 | + inputType: "{ owner: string, repo: string }", | |
| 812 | + outputType: "unknown", | |
| 810 | 813 | }, |
| 811 | 814 | ], |
| 812 | 815 | total: 1, |
| 813 | 816 | }, |
| 814 | 817 | result: { |
| 818 | + ok: true, | |
| 815 | 819 | status: 200, |
| 816 | 820 | headers: expect.any(Object), |
| 817 | 821 | body: { |
| @@ -0,0 +1,24 @@ | ||
| 1 | +{ | |
| 2 | + "name": "@executor-v3/react", | |
| 3 | + "private": true, | |
| 4 | + "type": "module", | |
| 5 | + "exports": { | |
| 6 | + ".": "./src/index.ts" | |
| 7 | + }, | |
| 8 | + "scripts": { | |
| 9 | + "typecheck": "bunx tsc --noEmit -p tsconfig.json" | |
| 10 | + }, | |
| 11 | + "dependencies": { | |
| 12 | + "@effect-atom/atom": "0.5.3", | |
| 13 | + "@effect-atom/atom-react": "0.5.0", | |
| 14 | + "@executor-v3/control-plane": "workspace:*", | |
| 15 | + "effect": "catalog:" | |
| 16 | + }, | |
| 17 | + "peerDependencies": { | |
| 18 | + "react": ">=18 <20" | |
| 19 | + }, | |
| 20 | + "devDependencies": { | |
| 21 | + "@types/react": "^19.2.10", | |
| 22 | + "bun-types": "catalog:" | |
| 23 | + } | |
| 24 | +} | |
| @@ -0,0 +1,325 @@ | ||
| 1 | +import { Atom, Result } from "@effect-atom/atom"; | |
| 2 | +import { RegistryProvider, useAtomValue } from "@effect-atom/atom-react"; | |
| 3 | +import type { | |
| 4 | + LocalInstallation, | |
| 5 | + Source, | |
| 6 | + SourceInspection, | |
| 7 | + SourceInspectionDiscoverResult, | |
| 8 | + SourceInspectionToolDetail, | |
| 9 | +} from "@executor-v3/control-plane/schema"; | |
| 10 | +import * as Cause from "effect/Cause"; | |
| 11 | +import * as Effect from "effect/Effect"; | |
| 12 | +import * as React from "react"; | |
| 13 | + | |
| 14 | +const DEFAULT_EXECUTOR_API_BASE_URL = "http://127.0.0.1:8788"; | |
| 15 | +const ACCOUNT_HEADER = "x-executor-account-id"; | |
| 16 | +const PLACEHOLDER_WORKSPACE_ID = "ws_placeholder" as Source["workspaceId"]; | |
| 17 | +const PLACEHOLDER_ACCOUNT_ID = "acc_placeholder"; | |
| 18 | +const PLACEHOLDER_SOURCE_ID = "src_placeholder" as Source["id"]; | |
| 19 | + | |
| 20 | +let apiBaseUrl = | |
| 21 | + typeof window !== "undefined" && typeof window.location?.origin === "string" | |
| 22 | + ? window.location.origin | |
| 23 | + : DEFAULT_EXECUTOR_API_BASE_URL; | |
| 24 | + | |
| 25 | +type AtomKeyPart = string | number | boolean | null | undefined; | |
| 26 | + | |
| 27 | +const encodeAtomKey = (parts: ReadonlyArray<AtomKeyPart>): string => JSON.stringify(parts); | |
| 28 | + | |
| 29 | +const decodeAtomKey = <T extends ReadonlyArray<AtomKeyPart>>(key: string): T => JSON.parse(key) as T; | |
| 30 | + | |
| 31 | +const causeMessage = (cause: Cause.Cause<unknown>): Error => | |
| 32 | + new Error(Cause.pretty(cause)); | |
| 33 | + | |
| 34 | +const requestJson = async <A>(input: { | |
| 35 | + path: string; | |
| 36 | + accountId?: string; | |
| 37 | + method?: "GET" | "POST"; | |
| 38 | + payload?: unknown; | |
| 39 | +}): Promise<A> => { | |
| 40 | + const response = await fetch(new URL(input.path, apiBaseUrl), { | |
| 41 | + method: input.method ?? "GET", | |
| 42 | + headers: { | |
| 43 | + ...(input.accountId ? { [ACCOUNT_HEADER]: input.accountId } : {}), | |
| 44 | + ...(input.payload !== undefined ? { "content-type": "application/json" } : {}), | |
| 45 | + }, | |
| 46 | + ...(input.payload !== undefined ? { body: JSON.stringify(input.payload) } : {}), | |
| 47 | + }); | |
| 48 | + | |
| 49 | + if (!response.ok) { | |
| 50 | + const text = await response.text(); | |
| 51 | + throw new Error(text || `${response.status} ${response.statusText}`); | |
| 52 | + } | |
| 53 | + | |
| 54 | + return response.json() as Promise<A>; | |
| 55 | +}; | |
| 56 | + | |
| 57 | +const localInstallationAtom = Atom.make( | |
| 58 | + Effect.promise(() => requestJson<LocalInstallation>({ path: "/v1/local/installation" })), | |
| 59 | +).pipe(Atom.keepAlive); | |
| 60 | + | |
| 61 | +const sourcesAtom = Atom.family((key: string) => { | |
| 62 | + const [enabled, workspaceId, accountId] = decodeAtomKey< | |
| 63 | + readonly [boolean, Source["workspaceId"], string] | |
| 64 | + >(key); | |
| 65 | + | |
| 66 | + return Atom.make( | |
| 67 | + enabled | |
| 68 | + ? Effect.promise(() => | |
| 69 | + requestJson<ReadonlyArray<Source>>({ | |
| 70 | + path: `/v1/workspaces/${workspaceId}/sources`, | |
| 71 | + accountId, | |
| 72 | + }), | |
| 73 | + ) | |
| 74 | + : Effect.never, | |
| 75 | + ).pipe(Atom.keepAlive); | |
| 76 | +}); | |
| 77 | + | |
| 78 | +const sourceInspectionAtom = Atom.family((key: string) => { | |
| 79 | + const [enabled, workspaceId, accountId, sourceId] = decodeAtomKey< | |
| 80 | + readonly [boolean, Source["workspaceId"], string, Source["id"]] | |
| 81 | + >(key); | |
| 82 | + | |
| 83 | + return Atom.make( | |
| 84 | + enabled | |
| 85 | + ? Effect.promise(() => | |
| 86 | + requestJson<SourceInspection>({ | |
| 87 | + path: `/v1/workspaces/${workspaceId}/sources/${sourceId}/inspection`, | |
| 88 | + accountId, | |
| 89 | + }), | |
| 90 | + ) | |
| 91 | + : Effect.never, | |
| 92 | + ).pipe(Atom.keepAlive); | |
| 93 | +}); | |
| 94 | + | |
| 95 | +const sourceInspectionToolAtom = Atom.family((key: string) => { | |
| 96 | + const [enabled, workspaceId, accountId, sourceId, toolPath] = decodeAtomKey< | |
| 97 | + readonly [boolean, Source["workspaceId"], string, Source["id"], string | null] | |
| 98 | + >(key); | |
| 99 | + | |
| 100 | + return Atom.make( | |
| 101 | + enabled && toolPath | |
| 102 | + ? Effect.promise(() => | |
| 103 | + requestJson<SourceInspectionToolDetail>({ | |
| 104 | + path: `/v1/workspaces/${workspaceId}/sources/${sourceId}/tools/${encodeURIComponent(toolPath)}/inspection`, | |
| 105 | + accountId, | |
| 106 | + }), | |
| 107 | + ) | |
| 108 | + : Effect.succeed<SourceInspectionToolDetail | null>(null), | |
| 109 | + ).pipe(Atom.keepAlive); | |
| 110 | +}); | |
| 111 | + | |
| 112 | +const sourceDiscoveryAtom = Atom.family((key: string) => { | |
| 113 | + const [enabled, workspaceId, accountId, sourceId, query, limit] = decodeAtomKey< | |
| 114 | + readonly [boolean, Source["workspaceId"], string, Source["id"], string, number | null] | |
| 115 | + >(key); | |
| 116 | + | |
| 117 | + return Atom.make( | |
| 118 | + !enabled | |
| 119 | + ? Effect.never | |
| 120 | + : query.trim().length === 0 | |
| 121 | + ? Effect.succeed<SourceInspectionDiscoverResult>({ | |
| 122 | + query: "", | |
| 123 | + queryTokens: [], | |
| 124 | + bestPath: null, | |
| 125 | + total: 0, | |
| 126 | + results: [], | |
| 127 | + }) | |
| 128 | + : Effect.promise(() => | |
| 129 | + requestJson<SourceInspectionDiscoverResult>({ | |
| 130 | + path: `/v1/workspaces/${workspaceId}/sources/${sourceId}/inspection/discover`, | |
| 131 | + accountId, | |
| 132 | + method: "POST", | |
| 133 | + payload: { | |
| 134 | + query, | |
| 135 | + ...(limit !== null ? { limit } : {}), | |
| 136 | + }, | |
| 137 | + }), | |
| 138 | + ), | |
| 139 | + ).pipe(Atom.keepAlive); | |
| 140 | +}); | |
| 141 | + | |
| 142 | +export type Loadable<T> = | |
| 143 | + | { status: "loading" } | |
| 144 | + | { status: "error"; error: Error } | |
| 145 | + | { status: "ready"; data: T }; | |
| 146 | + | |
| 147 | +type WorkspaceContext = { | |
| 148 | + installation: LocalInstallation; | |
| 149 | + workspaceId: Source["workspaceId"]; | |
| 150 | + accountId: string; | |
| 151 | +}; | |
| 152 | + | |
| 153 | +const toLoadable = <T>(result: Result.Result<T, Error>): Loadable<T> => { | |
| 154 | + if (Result.isSuccess(result)) { | |
| 155 | + return { | |
| 156 | + status: "ready", | |
| 157 | + data: result.value, | |
| 158 | + }; | |
| 159 | + } | |
| 160 | + | |
| 161 | + if (Result.isFailure(result)) { | |
| 162 | + return { | |
| 163 | + status: "error", | |
| 164 | + error: causeMessage(result.cause), | |
| 165 | + }; | |
| 166 | + } | |
| 167 | + | |
| 168 | + return { | |
| 169 | + status: "loading", | |
| 170 | + }; | |
| 171 | +}; | |
| 172 | + | |
| 173 | +const pendingLoadable = <T>(workspace: Loadable<WorkspaceContext>): Loadable<T> => { | |
| 174 | + if (workspace.status === "loading") { | |
| 175 | + return { status: "loading" }; | |
| 176 | + } | |
| 177 | + | |
| 178 | + if (workspace.status === "error") { | |
| 179 | + return { status: "error", error: workspace.error }; | |
| 180 | + } | |
| 181 | + | |
| 182 | + throw new Error("Expected workspace loadable to be pending or errored"); | |
| 183 | +}; | |
| 184 | + | |
| 185 | +const useLoadableAtom = <T>(atom: Atom.Atom<Result.Result<T, Error>>): Loadable<T> => { | |
| 186 | + const result = useAtomValue(atom); | |
| 187 | + return React.useMemo(() => toLoadable(result), [result]); | |
| 188 | +}; | |
| 189 | + | |
| 190 | +const useWorkspaceContext = (): Loadable<WorkspaceContext> => { | |
| 191 | + const installation = useLoadableAtom(localInstallationAtom); | |
| 192 | + | |
| 193 | + return React.useMemo(() => { | |
| 194 | + if (installation.status !== "ready") { | |
| 195 | + return installation; | |
| 196 | + } | |
| 197 | + | |
| 198 | + return { | |
| 199 | + status: "ready", | |
| 200 | + data: { | |
| 201 | + installation: installation.data, | |
| 202 | + workspaceId: installation.data.workspaceId, | |
| 203 | + accountId: installation.data.accountId, | |
| 204 | + }, | |
| 205 | + } satisfies Loadable<WorkspaceContext>; | |
| 206 | + }, [installation]); | |
| 207 | +}; | |
| 208 | + | |
| 209 | +const useWorkspaceRequestContext = () => { | |
| 210 | + const workspace = useWorkspaceContext(); | |
| 211 | + const enabled = workspace.status === "ready"; | |
| 212 | + | |
| 213 | + const workspaceId = enabled | |
| 214 | + ? workspace.data.workspaceId | |
| 215 | + : PLACEHOLDER_WORKSPACE_ID; | |
| 216 | + const accountId = enabled | |
| 217 | + ? workspace.data.accountId | |
| 218 | + : PLACEHOLDER_ACCOUNT_ID; | |
| 219 | + | |
| 220 | + return React.useMemo( | |
| 221 | + () => ({ | |
| 222 | + workspace, | |
| 223 | + enabled, | |
| 224 | + workspaceId, | |
| 225 | + accountId, | |
| 226 | + }), | |
| 227 | + [accountId, enabled, workspace, workspaceId], | |
| 228 | + ); | |
| 229 | +}; | |
| 230 | + | |
| 231 | +export const setExecutorApiBaseUrl = (baseUrl: string): void => { | |
| 232 | + apiBaseUrl = baseUrl; | |
| 233 | +}; | |
| 234 | + | |
| 235 | +export const ExecutorReactProvider = RegistryProvider; | |
| 236 | + | |
| 237 | +export const useLocalInstallation = (): Loadable<LocalInstallation> => | |
| 238 | + useLoadableAtom(localInstallationAtom); | |
| 239 | + | |
| 240 | +export const useSources = (): Loadable<ReadonlyArray<Source>> => { | |
| 241 | + const workspace = useWorkspaceRequestContext(); | |
| 242 | + const sources = useLoadableAtom( | |
| 243 | + sourcesAtom( | |
| 244 | + encodeAtomKey([workspace.enabled, workspace.workspaceId, workspace.accountId]), | |
| 245 | + ), | |
| 246 | + ); | |
| 247 | + | |
| 248 | + return workspace.enabled ? sources : pendingLoadable(workspace.workspace); | |
| 249 | +}; | |
| 250 | + | |
| 251 | +export const useSourceInspection = (sourceId: string): Loadable<SourceInspection> => { | |
| 252 | + const workspace = useWorkspaceRequestContext(); | |
| 253 | + const requestedSourceId = workspace.enabled | |
| 254 | + ? (sourceId as Source["id"]) | |
| 255 | + : PLACEHOLDER_SOURCE_ID; | |
| 256 | + const inspection = useLoadableAtom( | |
| 257 | + sourceInspectionAtom( | |
| 258 | + encodeAtomKey([ | |
| 259 | + workspace.enabled, | |
| 260 | + workspace.workspaceId, | |
| 261 | + workspace.accountId, | |
| 262 | + requestedSourceId, | |
| 263 | + ]), | |
| 264 | + ), | |
| 265 | + ); | |
| 266 | + | |
| 267 | + return workspace.enabled ? inspection : pendingLoadable(workspace.workspace); | |
| 268 | +}; | |
| 269 | + | |
| 270 | +export const useSourceToolDetail = ( | |
| 271 | + sourceId: string, | |
| 272 | + toolPath: string | null, | |
| 273 | +): Loadable<SourceInspectionToolDetail | null> => { | |
| 274 | + const workspace = useWorkspaceRequestContext(); | |
| 275 | + const requestedSourceId = workspace.enabled | |
| 276 | + ? (sourceId as Source["id"]) | |
| 277 | + : PLACEHOLDER_SOURCE_ID; | |
| 278 | + const detail = useLoadableAtom( | |
| 279 | + sourceInspectionToolAtom( | |
| 280 | + encodeAtomKey([ | |
| 281 | + workspace.enabled, | |
| 282 | + workspace.workspaceId, | |
| 283 | + workspace.accountId, | |
| 284 | + requestedSourceId, | |
| 285 | + toolPath, | |
| 286 | + ]), | |
| 287 | + ), | |
| 288 | + ); | |
| 289 | + | |
| 290 | + return workspace.enabled ? detail : pendingLoadable(workspace.workspace); | |
| 291 | +}; | |
| 292 | + | |
| 293 | +export const useSourceDiscovery = (input: { | |
| 294 | + sourceId: string; | |
| 295 | + query: string; | |
| 296 | + limit?: number; | |
| 297 | +}): Loadable<SourceInspectionDiscoverResult> => { | |
| 298 | + const workspace = useWorkspaceRequestContext(); | |
| 299 | + const requestedSourceId = workspace.enabled | |
| 300 | + ? (input.sourceId as Source["id"]) | |
| 301 | + : PLACEHOLDER_SOURCE_ID; | |
| 302 | + const results = useLoadableAtom( | |
| 303 | + sourceDiscoveryAtom( | |
| 304 | + encodeAtomKey([ | |
| 305 | + workspace.enabled, | |
| 306 | + workspace.workspaceId, | |
| 307 | + workspace.accountId, | |
| 308 | + requestedSourceId, | |
| 309 | + input.query, | |
| 310 | + input.limit ?? null, | |
| 311 | + ]), | |
| 312 | + ), | |
| 313 | + ); | |
| 314 | + | |
| 315 | + return workspace.enabled ? results : pendingLoadable(workspace.workspace); | |
| 316 | +}; | |
| 317 | + | |
| 318 | + | |
| 319 | +export type { | |
| 320 | + LocalInstallation, | |
| 321 | + Source, | |
| 322 | + SourceInspection, | |
| 323 | + SourceInspectionDiscoverResult, | |
| 324 | + SourceInspectionToolDetail, | |
| 325 | +}; | |
| @@ -0,0 +1,17 @@ | ||
| 1 | +{ | |
| 2 | + "compilerOptions": { | |
| 3 | + "target": "ES2022", | |
| 4 | + "module": "ESNext", | |
| 5 | + "moduleResolution": "Bundler", | |
| 6 | + "strict": true, | |
| 7 | + "skipLibCheck": true, | |
| 8 | + "noEmit": true, | |
| 9 | + "lib": ["ES2022", "DOM"], | |
| 10 | + "jsx": "react-jsx", | |
| 11 | + "types": ["bun-types"] | |
| 12 | + }, | |
| 13 | + "include": [ | |
| 14 | + "src/**/*.ts", | |
| 15 | + "src/**/*.tsx" | |
| 16 | + ] | |
| 17 | +} | |
| @@ -0,0 +1,20 @@ | ||
| 1 | +{ | |
| 2 | + "name": "@executor-v3/server", | |
| 3 | + "private": true, | |
| 4 | + "type": "module", | |
| 5 | + "exports": { | |
| 6 | + ".": "./src/index.ts" | |
| 7 | + }, | |
| 8 | + "scripts": { | |
| 9 | + "typecheck": "bunx tsc --noEmit -p tsconfig.json" | |
| 10 | + }, | |
| 11 | + "dependencies": { | |
| 12 | + "@effect/platform": "catalog:", | |
| 13 | + "@executor-v3/control-plane": "workspace:*", | |
| 14 | + "effect": "catalog:" | |
| 15 | + }, | |
| 16 | + "devDependencies": { | |
| 17 | + "@types/node": "catalog:", | |
| 18 | + "bun-types": "catalog:" | |
| 19 | + } | |
| 20 | +} | |
| @@ -4,6 +4,12 @@ import { join } from "node:path"; · 3 unmodified lines | ||
| 4 | 4 | export const DEFAULT_SERVER_PORT = 8788; |
| 5 | 5 | export const DEFAULT_SERVER_HOST = "127.0.0.1"; |
| 6 | 6 | export const DEFAULT_SERVER_BASE_URL = `http://${DEFAULT_SERVER_HOST}:${DEFAULT_SERVER_PORT}`; |
| 7 | −export const DEFAULT_LOCAL_DATA_DIR = join(homedir(), ".local", "share", "executor-v3", "control-plane"); | |
| 7 | +export const DEFAULT_LOCAL_DATA_DIR = join( | |
| 8 | + homedir(), | |
| 9 | + ".local", | |
| 10 | + "share", | |
| 11 | + "executor-v3", | |
| 12 | + "control-plane", | |
| 13 | +); | |
| 8 | 14 | export const SERVER_START_TIMEOUT_MS = 5_000; |
| 9 | 15 | export const SERVER_POLL_INTERVAL_MS = 100; |
| @@ -0,0 +1,237 @@ | ||
| 1 | +import { access, mkdir } from "node:fs/promises"; | |
| 2 | +import { dirname, extname, resolve } from "node:path"; | |
| 3 | + | |
| 4 | +import { HttpApiBuilder, HttpServer } from "@effect/platform"; | |
| 5 | +import { | |
| 6 | + createControlPlaneApiLayer, | |
| 7 | + createSqlControlPlaneRuntime, | |
| 8 | + type ResolveExecutionEnvironment, | |
| 9 | + type ResolveSecretMaterial, | |
| 10 | + type SqlControlPlaneRuntime, | |
| 11 | +} from "@executor-v3/control-plane"; | |
| 12 | +import * as Effect from "effect/Effect"; | |
| 13 | +import * as Layer from "effect/Layer"; | |
| 14 | +import type * as Scope from "effect/Scope"; | |
| 15 | + | |
| 16 | +import { | |
| 17 | + DEFAULT_LOCAL_DATA_DIR, | |
| 18 | + DEFAULT_SERVER_HOST, | |
| 19 | + DEFAULT_SERVER_PORT, | |
| 20 | +} from "./config"; | |
| 21 | + | |
| 22 | +export { | |
| 23 | + DEFAULT_LOCAL_DATA_DIR, | |
| 24 | + DEFAULT_SERVER_BASE_URL, | |
| 25 | + DEFAULT_SERVER_HOST, | |
| 26 | + DEFAULT_SERVER_PORT, | |
| 27 | + SERVER_POLL_INTERVAL_MS, | |
| 28 | + SERVER_START_TIMEOUT_MS, | |
| 29 | +} from "./config"; | |
| 30 | + | |
| 31 | +type StaticUiOptions = { | |
| 32 | + readonly assetsDir?: string; | |
| 33 | + readonly devServerUrl?: string; | |
| 34 | +}; | |
| 35 | + | |
| 36 | +export type LocalExecutorServer = { | |
| 37 | + readonly runtime: SqlControlPlaneRuntime; | |
| 38 | + readonly port: number; | |
| 39 | + readonly host: string; | |
| 40 | + readonly baseUrl: string; | |
| 41 | +}; | |
| 42 | + | |
| 43 | +export type StartLocalExecutorServerOptions = { | |
| 44 | + readonly port?: number; | |
| 45 | + readonly host?: string; | |
| 46 | + readonly localDataDir?: string; | |
| 47 | + readonly executionResolver?: ResolveExecutionEnvironment; | |
| 48 | + readonly resolveSecretMaterial?: ResolveSecretMaterial; | |
| 49 | + readonly ui?: StaticUiOptions; | |
| 50 | +}; | |
| 51 | + | |
| 52 | +export type LocalExecutorRequestHandler = { | |
| 53 | + readonly runtime: SqlControlPlaneRuntime; | |
| 54 | + readonly handleApiRequest: (request: Request) => Promise<Response>; | |
| 55 | + readonly getBaseUrl: () => string | undefined; | |
| 56 | + readonly setBaseUrl: (baseUrl: string) => void; | |
| 57 | +}; | |
| 58 | + | |
| 59 | +const disposeRuntime = (runtime: SqlControlPlaneRuntime) => | |
| 60 | + Effect.promise(() => runtime.close()).pipe(Effect.orDie); | |
| 61 | + | |
| 62 | +const createControlPlaneWebHandler = (runtime: SqlControlPlaneRuntime) => | |
| 63 | + Effect.acquireRelease( | |
| 64 | + Effect.sync(() => | |
| 65 | + HttpApiBuilder.toWebHandler( | |
| 66 | + Layer.merge( | |
| 67 | + createControlPlaneApiLayer(runtime.runtimeLayer), | |
| 68 | + HttpServer.layerContext, | |
| 69 | + ), | |
| 70 | + ), | |
| 71 | + ), | |
| 72 | + (handler) => Effect.promise(() => handler.dispose()).pipe(Effect.orDie), | |
| 73 | + ); | |
| 74 | + | |
| 75 | +const safeFilePath = (assetsDir: string, pathname: string): string | null => { | |
| 76 | + const target = resolve(assetsDir, `.${pathname}`); | |
| 77 | + const root = resolve(assetsDir); | |
| 78 | + return target.startsWith(root) ? target : null; | |
| 79 | +}; | |
| 80 | + | |
| 81 | +const fileExists = async (path: string): Promise<boolean> => { | |
| 82 | + try { | |
| 83 | + await access(path); | |
| 84 | + return true; | |
| 85 | + } catch { | |
| 86 | + return false; | |
| 87 | + } | |
| 88 | +}; | |
| 89 | + | |
| 90 | +const wantsHtml = (request: Request): boolean => | |
| 91 | + request.headers.get("accept")?.includes("text/html") ?? false; | |
| 92 | + | |
| 93 | +const serveUiAsset = async (request: Request, ui: StaticUiOptions): Promise<Response | null> => { | |
| 94 | + const url = new URL(request.url); | |
| 95 | + | |
| 96 | + if (ui.devServerUrl) { | |
| 97 | + const proxyUrl = new URL(`${url.pathname}${url.search}`, ui.devServerUrl); | |
| 98 | + return fetch(new Request(proxyUrl.toString(), request)); | |
| 99 | + } | |
| 100 | + | |
| 101 | + if (!ui.assetsDir) { | |
| 102 | + return null; | |
| 103 | + } | |
| 104 | + | |
| 105 | + const candidatePath = safeFilePath(ui.assetsDir, url.pathname); | |
| 106 | + if (candidatePath && await fileExists(candidatePath)) { | |
| 107 | + return new Response(Bun.file(candidatePath)); | |
| 108 | + } | |
| 109 | + | |
| 110 | + const shouldServeIndex = | |
| 111 | + url.pathname === "/" | |
| 112 | + || extname(url.pathname).length === 0 | |
| 113 | + || wantsHtml(request); | |
| 114 | + | |
| 115 | + if (!shouldServeIndex) { | |
| 116 | + return null; | |
| 117 | + } | |
| 118 | + | |
| 119 | + const indexPath = resolve(ui.assetsDir, "index.html"); | |
| 120 | + if (!(await fileExists(indexPath))) { | |
| 121 | + return null; | |
| 122 | + } | |
| 123 | + | |
| 124 | + return new Response(Bun.file(indexPath), { | |
| 125 | + headers: { | |
| 126 | + "content-type": "text/html; charset=utf-8", | |
| 127 | + }, | |
| 128 | + }); | |
| 129 | +}; | |
| 130 | + | |
| 131 | +const isApiRequest = (request: Request): boolean => { | |
| 132 | + const pathname = new URL(request.url).pathname; | |
| 133 | + return pathname === "/v1" || pathname.startsWith("/v1/"); | |
| 134 | +}; | |
| 135 | + | |
| 136 | +export const createLocalExecutorRequestHandler = ( | |
| 137 | + options: StartLocalExecutorServerOptions = {}, | |
| 138 | +): Effect.Effect<LocalExecutorRequestHandler, Error, Scope.Scope> => | |
| 139 | + Effect.gen(function* () { | |
| 140 | + const localDataDir = options.localDataDir ?? DEFAULT_LOCAL_DATA_DIR; | |
| 141 | + | |
| 142 | + if (localDataDir !== ":memory:") { | |
| 143 | + yield* Effect.tryPromise({ | |
| 144 | + try: () => mkdir(dirname(localDataDir), { recursive: true }), | |
| 145 | + catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), | |
| 146 | + }); | |
| 147 | + } | |
| 148 | + | |
| 149 | + let baseUrlRef: string | undefined; | |
| 150 | + | |
| 151 | + const runtime = yield* Effect.acquireRelease( | |
| 152 | + createSqlControlPlaneRuntime({ | |
| 153 | + localDataDir, | |
| 154 | + executionResolver: options.executionResolver, | |
| 155 | + resolveSecretMaterial: options.resolveSecretMaterial, | |
| 156 | + getLocalServerBaseUrl: () => baseUrlRef, | |
| 157 | + }).pipe( | |
| 158 | + Effect.mapError((cause) => | |
| 159 | + cause instanceof Error ? cause : new Error(String(cause)), | |
| 160 | + ), | |
| 161 | + ), | |
| 162 | + disposeRuntime, | |
| 163 | + ); | |
| 164 | + | |
| 165 | + const apiHandler = yield* createControlPlaneWebHandler(runtime); | |
| 166 | + | |
| 167 | + return { | |
| 168 | + runtime, | |
| 169 | + handleApiRequest: (request) => apiHandler.handler(request), | |
| 170 | + getBaseUrl: () => baseUrlRef, | |
| 171 | + setBaseUrl: (baseUrl) => { | |
| 172 | + baseUrlRef = baseUrl; | |
| 173 | + }, | |
| 174 | + } satisfies LocalExecutorRequestHandler; | |
| 175 | + }); | |
| 176 | + | |
| 177 | +export const createLocalExecutorServer = ( | |
| 178 | + options: StartLocalExecutorServerOptions = {}, | |
| 179 | +): Effect.Effect<LocalExecutorServer, Error, Scope.Scope> => | |
| 180 | + Effect.gen(function* () { | |
| 181 | + const host = options.host ?? DEFAULT_SERVER_HOST; | |
| 182 | + const port = options.port ?? DEFAULT_SERVER_PORT; | |
| 183 | + const requestHandler = yield* createLocalExecutorRequestHandler(options); | |
| 184 | + | |
| 185 | + const server = yield* Effect.acquireRelease( | |
| 186 | + Effect.sync(() => | |
| 187 | + Bun.serve({ | |
| 188 | + hostname: host, | |
| 189 | + port, | |
| 190 | + fetch: async (request) => { | |
| 191 | + if (isApiRequest(request)) { | |
| 192 | + return requestHandler.handleApiRequest(request); | |
| 193 | + } | |
| 194 | + | |
| 195 | + const uiResponse = await serveUiAsset(request, options.ui ?? {}); | |
| 196 | + if (uiResponse) { | |
| 197 | + return uiResponse; | |
| 198 | + } | |
| 199 | + | |
| 200 | + return new Response("Not Found", { status: 404 }); | |
| 201 | + }, | |
| 202 | + }), | |
| 203 | + ), | |
| 204 | + (server) => Effect.sync(() => server.stop(true)), | |
| 205 | + ); | |
| 206 | + | |
| 207 | + const resolvedUrl = new URL(server.url); | |
| 208 | + requestHandler.setBaseUrl(resolvedUrl.origin); | |
| 209 | + | |
| 210 | + return { | |
| 211 | + runtime: requestHandler.runtime, | |
| 212 | + host: resolvedUrl.hostname, | |
| 213 | + port: Number(resolvedUrl.port), | |
| 214 | + baseUrl: resolvedUrl.origin, | |
| 215 | + } satisfies LocalExecutorServer; | |
| 216 | + }); | |
| 217 | + | |
| 218 | +export const runLocalExecutorServer = ( | |
| 219 | + options: StartLocalExecutorServerOptions = {}, | |
| 220 | +): Effect.Effect<void, Error, never> => | |
| 221 | + Effect.scoped( | |
| 222 | + Effect.gen(function* () { | |
| 223 | + const server = yield* createLocalExecutorServer(options); | |
| 224 | + console.error(`executor server listening on ${server.baseUrl}`); | |
| 225 | + | |
| 226 | + yield* Effect.async<void, never>((resume) => { | |
| 227 | + const shutdown = () => resume(Effect.void); | |
| 228 | + process.once("SIGINT", shutdown); | |
| 229 | + process.once("SIGTERM", shutdown); | |
| 230 | + | |
| 231 | + return Effect.sync(() => { | |
| 232 | + process.off("SIGINT", shutdown); | |
| 233 | + process.off("SIGTERM", shutdown); | |
| 234 | + }); | |
| 235 | + }); | |
| 236 | + }), | |
| 237 | + ); | |
| @@ -0,0 +1,15 @@ | ||
| 1 | +{ | |
| 2 | + "compilerOptions": { | |
| 3 | + "target": "ES2022", | |
| 4 | + "module": "ESNext", | |
| 5 | + "moduleResolution": "Bundler", | |
| 6 | + "strict": true, | |
| 7 | + "skipLibCheck": true, | |
| 8 | + "noEmit": true, | |
| 9 | + "lib": ["ES2022", "DOM"], | |
| 10 | + "types": ["bun-types", "node"] | |
| 11 | + }, | |
| 12 | + "include": [ | |
| 13 | + "src/**/*.ts" | |
| 14 | + ] | |
| 15 | +} | |
| @@ -0,0 +1,17 @@ | ||
| 1 | +{ | |
| 2 | + "$schema": "https://turbo.build/schema.json", | |
| 3 | + "tasks": { | |
| 4 | + "build": { | |
| 5 | + "dependsOn": ["^build"], | |
| 6 | + "outputs": ["dist/**"] | |
| 7 | + }, | |
| 8 | + "typecheck": { | |
| 9 | + "dependsOn": ["^typecheck"], | |
| 10 | + "outputs": [] | |
| 11 | + }, | |
| 12 | + "dev": { | |
| 13 | + "cache": false, | |
| 14 | + "persistent": true | |
| 15 | + } | |
| 16 | + } | |
| 17 | +} | |