History

wip ui

Rhys Sullivan committed 8 months ago

Commit 2d7f7801 parent 4b6e5d781 files · +6620 −566Browse files

Unified · Split

81 files
  1. apps/executor/package.json +1 −0
  2. apps/executor/src/cli/main.ts +2 −2
  3. apps/executor/src/server/index.ts +0 −138
  4. apps/executor/src/server/server.test.ts +1 −1
  5. apps/web/package.json +36 −0
  6. apps/web/src/components/code-block.tsx +96 −0
  7. apps/web/src/components/document-panel.tsx +44 −0
  8. apps/web/src/components/icons.tsx +109 −0
  9. apps/web/src/components/loadable.tsx +50 −0
  10. apps/web/src/components/shell.tsx +232 −0
  11. apps/web/src/components/ui/badge.tsx +43 −0
  12. apps/web/src/components/ui/button.tsx +34 −0
  13. apps/web/src/dev.ts +29 −0
  14. apps/web/src/frontend.tsx +10 −0
  15. apps/web/src/globals.css +107 −0
  16. apps/web/src/index.html +29 −0
  17. apps/web/src/lib/utils.ts +6 −0
  18. apps/web/src/main.tsx +100 −0
  19. apps/web/src/server.ts +13 −0
  20. apps/web/src/views/home.tsx +181 −0
  21. apps/web/src/views/source-detail.tsx +604 −0
  22. apps/web/tsconfig.json +17 −0
  23. apps/web/vite.config.ts +29 −0
  24. bun.lock +453 −2
  25. package.json +2 −0
  26. packages/ai-sdk-adapter/src/example-dynamic.ts +4 −4
  27. packages/codemode-core/src/discovery.ts +4 −6
  28. packages/codemode-core/src/index.test.ts +4 −5
  29. packages/codemode-core/src/index.ts +1 −0
  30. packages/codemode-core/src/schema-types.ts +125 −0
  31. packages/codemode-core/src/system-tools.ts +8 −6
  32. packages/codemode-core/src/tool-map.ts +18 −47
  33. packages/codemode-core/src/types.ts +12 −6
  34. packages/codemode-openapi/openapi-extractor-rs/.gitignore +1 −0
  35. packages/codemode-openapi/openapi-extractor-rs/Cargo.lock +445 −0
  36. packages/codemode-openapi/openapi-extractor-rs/Cargo.toml +17 −0
  37. packages/codemode-openapi/openapi-extractor-rs/build-wasm.sh +20 −0
  38. packages/codemode-openapi/openapi-extractor-rs/src/lib.rs +1018 −0
  39. packages/codemode-openapi/openapi-extractor-rs/src/main.rs +58 −0
  40. packages/codemode-openapi/package.json +1 −0
  41. packages/codemode-openapi/src/index.ts +20 −0
  42. packages/codemode-openapi/src/openapi-definitions.ts +254 −0
  43. packages/codemode-openapi/src/openapi-extraction.real-specs.test.ts +118 −0
  44. packages/codemode-openapi/src/openapi-extractor-wasm/openapi_extractor_bg.wasm +0 −0
  45. packages/codemode-openapi/src/openapi-schema-refs.ts +135 −0
  46. packages/codemode-openapi/src/openapi-tool-presentation.ts +106 −0
  47. packages/codemode-openapi/src/openapi-tools.test.ts +59 −0
  48. packages/codemode-openapi/src/openapi-tools.ts +107 −29
  49. packages/codemode-openapi/src/openapi-types.ts +43 −2
  50. packages/control-plane/drizzle/20260307134000_openapi_tool_identity/migration.sql +37 −0
  51. packages/control-plane/drizzle/20260307152000_source_document_text/migration.sql +1 −0
  52. packages/control-plane/package.json +3 −1
  53. packages/control-plane/src/api/sources/api.ts +33 −0
  54. packages/control-plane/src/api/sources/http.ts +37 −0
  55. packages/control-plane/src/client.ts +2 −2
  56. packages/control-plane/src/persistence/control-plane-rows.test.ts +1 −0
  57. packages/control-plane/src/persistence/repos/sources-repo.ts +1 −0
  58. packages/control-plane/src/persistence/schema.ts +9 −0
  59. packages/control-plane/src/runtime/control-plane-runtime.test.ts +2 −0
  60. packages/control-plane/src/runtime/executor-tools.ts +2 −2
  61. packages/control-plane/src/runtime/index.ts +2 −1
  62. packages/control-plane/src/runtime/source-auth-service.ts +14 −7
  63. packages/control-plane/src/runtime/source-definitions.ts +1 −0
  64. packages/control-plane/src/runtime/source-inspection.ts +520 −0
  65. packages/control-plane/src/runtime/sources-operations.ts +4 −2
  66. packages/control-plane/src/runtime/tool-artifacts.ts +28 −80
  67. packages/control-plane/src/runtime/workspace-execution-environment.test.ts +101 −94
  68. packages/control-plane/src/runtime/workspace-execution-environment.ts +249 −125
  69. packages/control-plane/src/schema/index.ts +1 −0
  70. packages/control-plane/src/schema/models/source-inspection.ts +85 −0
  71. packages/control-plane/src/schema/models/source.ts +3 −0
  72. packages/control-plane/src/schema/models/tool-artifact.ts +9 −0
  73. packages/kitchen-sink/src/source-runtime.test.ts +8 −4
  74. packages/react/package.json +24 −0
  75. packages/react/src/index.ts +325 −0
  76. packages/react/tsconfig.json +17 −0
  77. packages/server/package.json +20 −0
  78. packages/server/src/config.ts +7 −1
  79. packages/server/src/index.ts +237 −0
  80. packages/server/tsconfig.json +15 −0
  81. turbo.json +17 −0
apps/executor/package.json+1−0
@@ -17,6 +17,7 @@ · 16 unmodified lines
1717 "@effect/platform-node": "catalog:",
1818 "@executor-v3/codemode-core": "workspace:*",
1919 "@executor-v3/control-plane": "workspace:*",
20+ "@executor-v3/server": "workspace:*",
2021 "@executor-v3/runtime-local-inproc": "workspace:*",
2122 "effect": "catalog:"
2223 },
apps/executor/src/cli/main.ts+2−2
@@ -32,12 +32,12 @@ import { · 31 unmodified lines
3232 DEFAULT_SERVER_PORT,
3333 SERVER_POLL_INTERVAL_MS,
3434 SERVER_START_TIMEOUT_MS,
35−} from "../server/config";
35+ runLocalExecutorServer,
36+} from "@executor-v3/server";
3637 import {
3738 seedDemoMcpSourceInWorkspace,
3839 seedGithubOpenApiSourceInWorkspace,
3940 } from "./dev";
40−import { runLocalExecutorServer } from "../server";
4141
4242 const toError = (cause: unknown): Error =>
4343 cause instanceof Error ? cause : new Error(String(cause));
apps/executor/src/server/index.ts+0−138
@@ -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− );
apps/executor/src/server/server.test.ts+1−1
@@ -31,7 +31,7 @@ import { · 30 unmodified lines
3131 seedDemoMcpSourceInWorkspace,
3232 seedGithubOpenApiSourceInWorkspace,
3333 } from "../cli/dev";
34−import { createLocalExecutorServer } from ".";
34+import { createLocalExecutorServer } from "@executor-v3/server";
3535
3636 const executionResolver: ResolveExecutionEnvironment = () =>
3737 Effect.succeed({
apps/web/package.json+36−0
@@ -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+}
apps/web/src/components/code-block.tsx+96−0
@@ -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+}
apps/web/src/components/document-panel.tsx+44−0
@@ -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+}
apps/web/src/components/icons.tsx+109−0
@@ -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+}
apps/web/src/components/loadable.tsx+50−0
@@ -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+}
apps/web/src/components/shell.tsx+232−0
@@ -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+}
apps/web/src/components/ui/badge.tsx+43−0
@@ -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+}
apps/web/src/components/ui/button.tsx+34−0
@@ -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+}
apps/web/src/dev.ts+29−0
@@ -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+};
apps/web/src/frontend.tsx+10−0
@@ -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 />);
apps/web/src/globals.css+107−0
@@ -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+}
apps/web/src/index.html+29−0
@@ -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>
apps/web/src/lib/utils.ts+6−0
@@ -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+}
apps/web/src/main.tsx+100−0
@@ -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+}
apps/web/src/server.ts+13−0
@@ -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+});
apps/web/src/views/home.tsx+181−0
@@ -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+ &rarr;
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+}
apps/web/src/views/source-detail.tsx+604−0
@@ -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>&rarr;</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 &rarr;
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+}
apps/web/tsconfig.json+17−0
@@ -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+}
apps/web/vite.config.ts+29−0
@@ -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+});
bun.lock+453−2
@@ -6,6 +6,7 @@ · 5 unmodified lines
66 "name": "executor-v3",
77 "devDependencies": {
88 "knip": "^5.85.0",
9+ "turbo": "^2.5.6",
910 "typescript": "^5.9.3",
1011 },
1112 },
@@ -21,6 +22,7 @@ · 9 unmodified lines
2122 "@executor-v3/codemode-core": "workspace:*",
2223 "@executor-v3/control-plane": "workspace:*",
2324 "@executor-v3/runtime-local-inproc": "workspace:*",
25+ "@executor-v3/server": "workspace:*",
2426 "effect": "catalog:",
2527 },
2628 "devDependencies": {
@@ -44,6 +46,34 @@ · 17 unmodified lines
4446 "bun-types": "catalog:",
4547 },
4648 },
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+ },
4777 "packages/ai-sdk-adapter": {
4878 "name": "@executor-v3/ai-sdk-adapter",
4979 "dependencies": {
@@ -91,12 +121,12 @@ · 41 unmodified lines
91121 "packages/codemode-openapi": {
92122 "name": "@executor-v3/codemode-openapi",
93123 "dependencies": {
124+ "@effect/platform": "catalog:",
94125 "@executor-v3/codemode-core": "workspace:*",
95126 "effect": "catalog:",
96127 "yaml": "^2.8.1",
97128 },
98129 "devDependencies": {
99− "@effect/platform": "catalog:",
100130 "@effect/platform-node": "^0.104.1",
101131 "@effect/vitest": "catalog:",
102132 "@types/node": "catalog:",
@@ -135,6 +165,7 @@ · 32 unmodified lines
135165 "@executor-v3/ai-sdk-adapter": "workspace:*",
136166 "@executor-v3/codemode-core": "workspace:*",
137167 "@executor-v3/codemode-openapi": "workspace:*",
168+ "@executor-v3/control-plane": "workspace:*",
138169 "@executor-v3/runtime-local-inproc": "workspace:*",
139170 "effect": "catalog:",
140171 },
@@ -145,6 +176,22 @@ · 4 unmodified lines
145176 "vitest": "catalog:",
146177 },
147178 },
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+ },
148195 "packages/runtime-local-inproc": {
149196 "name": "@executor-v3/runtime-local-inproc",
150197 "dependencies": {
@@ -156,6 +203,18 @@ · 5 unmodified lines
156203 "bun-types": "catalog:",
157204 },
158205 },
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+ },
159218 },
160219 "catalog": {
161220 "@effect/cli": "^0.73.2",
@@ -175,6 +234,8 @@ · 13 unmodified lines
175234
176235 "@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=="],
177236
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+
178239 "@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=="],
179240
180241 "@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
209270
210271 "@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=="],
211272
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+
212311 "@drizzle-team/brocli": ["@drizzle-team/brocli@0.11.0", "", {}, "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg=="],
213312
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+
214317 "@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=="],
215318
216319 "@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
311414
312415 "@executor-v3/mcp-elicitation-demo": ["@executor-v3/mcp-elicitation-demo@workspace:apps/mcp-elicitation-demo"],
313416
417+ "@executor-v3/react": ["@executor-v3/react@workspace:packages/react"],
418+
314419 "@executor-v3/runtime-local-inproc": ["@executor-v3/runtime-local-inproc@workspace:packages/runtime-local-inproc"],
315420
421+ "@executor-v3/server": ["@executor-v3/server@workspace:packages/server"],
422+
423+ "@executor-v3/web": ["@executor-v3/web@workspace:apps/web"],
424+
316425 "@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="],
317426
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+
318435 "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
319436
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+
320439 "@js-joda/core": ["@js-joda/core@5.7.0", "", {}, "sha512-WBu4ULVVxySLLzK1Ppq+OdfP+adRS4ntmDQT915rzDJ++i95gc2jZkM5B6LWEAwN3lGXpfie3yPABozdD3K3Vg=="],
321440
322441 "@js-temporal/polyfill": ["@js-temporal/polyfill@0.5.1", "", { "dependencies": { "jsbi": "^4.3.0" } }, "sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ=="],
323442
443+ "@medv/finder": ["@medv/finder@4.0.2", "", {}, "sha512-RraNY9SCcx4KZV0Dh6BEW6XEW2swkqYca74pkFFRw6hHItSHiy+O/xMnpbofjYbzXj0tSpBGthUF1hHTsr3vIQ=="],
444+
324445 "@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=="],
325446
326447 "@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
413534
414535 "@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.6", "", { "os": "win32", "cpu": "x64" }, "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw=="],
415536
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+
416541 "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="],
417542
418543 "@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
463588
464589 "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="],
465590
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+
466607 "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
467608
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+
468649 "@tediousjs/connection-string": ["@tediousjs/connection-string@0.5.0", "", {}, "sha512-7qSgZbincDDDFyRweCIEvZULFAw5iz/DeunhvuxpL31nfntX3P4Yd4HkHBRg9H8CdqY1e5WFN1PZIz/REL9MVQ=="],
469650
470651 "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
471652
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+
472661 "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
473662
474663 "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
475664
476665 "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
477666
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+
478671 "@types/mssql": ["@types/mssql@9.1.9", "", { "dependencies": { "@types/node": "*", "tarn": "^3.0.1", "tedious": "*" } }, "sha512-P0nCgw6vzY23UxZMnbI4N7fnLGANt4LI4yvxze1paPj+LuN28cFv5EI+QidP8udnId/BKhkcRhm/BleNsjK65A=="],
479672
480673 "@types/node": ["@types/node@24.11.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw=="],
481674
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+
482681 "@types/readable-stream": ["@types/readable-stream@4.0.23", "", { "dependencies": { "@types/node": "*" } }, "sha512-wwXrtQvbMHxCbBgjHaMGEmImFTQxxpfMOR/ZoQnXxB1woqkUbdLGFDgauo00Py9IudiaqSeiBiulSV9i6XIPig=="],
483682
683+ "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
684+
484685 "@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=="],
485686
687+ "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
688+
486689 "@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="],
487690
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+
488693 "@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=="],
489694
490695 "@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
511716
512717 "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
513718
719+ "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
720+
514721 "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
515722
516723 "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
517724
725+ "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
726+
518727 "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
519728
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+
520733 "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=="],
521734
522735 "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=="],
523736
737+ "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
738+
524739 "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
525740
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+
526743 "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
527744
528745 "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
@@ -539,24 +756,50 @@ · 10 unmodified lines
539756
540757 "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
541758
759+ "caniuse-lite": ["caniuse-lite@1.0.30001777", "", {}, "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ=="],
760+
761+ "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
762+
542763 "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=="],
543764
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+
544771 "check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="],
545772
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+
546783 "commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
547784
548785 "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="],
549786
550787 "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
551788
789+ "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
790+
552791 "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
553792
793+ "cookie-es": ["cookie-es@2.0.0", "", {}, "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg=="],
794+
554795 "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
555796
556797 "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
557798
558799 "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
559800
801+ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
802+
560803 "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
561804
562805 "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="],
@@ -569,8 +812,12 @@ · 6 unmodified lines
569812
570813 "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
571814
815+ "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
816+
572817 "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
573818
819+ "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
820+
574821 "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=="],
575822
576823 "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
583830
584831 "effect": ["effect@3.19.19", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-Yc8U/SVXo2dHnaP7zNBlAo83h/nzSJpi7vph6Hzyl4ulgMBIgPmz3UzOjb9sBgpFE00gC0iETR244sfXDNLHRg=="],
585832
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+
586837 "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
587838
839+ "enhanced-resolve": ["enhanced-resolve@5.20.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ=="],
840+
588841 "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
589842
590843 "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
@@ -595,6 +848,8 @@ · 4 unmodified lines
595848
596849 "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=="],
597850
851+ "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
852+
598853 "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
599854
600855 "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
@@ -647,6 +902,10 @@ · 46 unmodified lines
647902
648903 "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
649904
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+
650909 "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=="],
651910
652911 "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
655914
656915 "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
657916
917+ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
918+
658919 "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
659920
660921 "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
661922
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+
662927 "hono": ["hono@4.12.5", "", {}, "sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg=="],
663928
929+ "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
930+
664931 "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=="],
665932
666933 "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
671938
672939 "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
673940
941+ "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
942+
674943 "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
675944
676945 "ini": ["ini@4.1.3", "", {}, "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg=="],
@@ -687,12 +956,18 @@ · 10 unmodified lines
687956
688957 "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=="],
689958
959+ "is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="],
960+
690961 "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
691962
692963 "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
693964
965+ "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
966+
694967 "is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="],
695968
969+ "isbot": ["isbot@5.1.35", "", {}, "sha512-waFfC72ZNfwLLuJ2iLaoVaqcNo+CAaLR7xCpAn0Y5WfGzkNHv7ZN39Vbi1y+kb+Zs46XHOX3tZNExroFUPX+Kg=="],
970+
696971 "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
697972
698973 "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
@@ -707,22 +982,54 @@ · 8 unmodified lines
707982
708983 "jsbi": ["jsbi@4.3.2", "", {}, "sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew=="],
709984
985+ "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
986+
710987 "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
711988
712989 "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
713990
714991 "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
715992
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+
716997 "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=="],
717998
718999 "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=="],
7191000
7201001 "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=="],
7211002
1003+ "kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
1004+
7221005 "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=="],
7231006
7241007 "kubernetes-types": ["kubernetes-types@1.30.0", "", {}, "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q=="],
7251008
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+
7261033 "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="],
7271034
7281035 "lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="],
@@ -737,18 +1044,34 @@ · 8 unmodified lines
7371044
7381045 "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="],
7391046
1047+ "log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="],
1048+
7401049 "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="],
7411050
1051+ "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
1052+
7421053 "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
7431054
7441055 "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
7451056
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+
7461059 "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
7471060
7481061 "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
7491062
7501063 "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
7511064
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+
7521075 "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
7531076
7541077 "mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="],
@@ -757,6 +1080,10 @@ · 2 unmodified lines
7571080
7581081 "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
7591082
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+
7601087 "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
7611088
7621089 "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
@@ -779,6 +1106,8 @@ · 16 unmodified lines
7791106
7801107 "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=="],
7811108
1109+ "node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="],
1110+
7821111 "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
7831112
7841113 "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
@@ -787,8 +1116,16 @@ · 2 unmodified lines
7871116
7881117 "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
7891118
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+
7901125 "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=="],
7911126
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+
7921129 "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=="],
7931130
7941131 "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
@@ -813,6 +1150,10 @@ · 18 unmodified lines
8131150
8141151 "process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="],
8151152
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+
8161157 "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=="],
8171158
8181159 "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="],
@@ -825,10 +1166,26 @@ · 6 unmodified lines
8251166
8261167 "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=="],
8271168
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+
8281177 "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=="],
8291178
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+
8301185 "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
8311186
1187+ "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="],
1188+
8321189 "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
8331190
8341191 "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
@@ -845,10 +1202,16 @@ · 10 unmodified lines
8451202
8461203 "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
8471204
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=="],
8491208
8501209 "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=="],
8511210
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+
8521215 "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=="],
8531216
8541217 "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
@@ -857,6 +1220,8 @@ · 2 unmodified lines
8571220
8581221 "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
8591222
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+
8601225 "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=="],
8611226
8621227 "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
8671232
8681233 "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
8691234
1235+ "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
1236+
1237+ "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
1238+
8701239 "smol-toml": ["smol-toml@1.6.0", "", {}, "sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw=="],
8711240
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+
8721243 "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
8731244
1245+ "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
1246+
8741247 "sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="],
8751248
8761249 "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
@@ -879,16 +1252,34 @@ · 2 unmodified lines
8791252
8801253 "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
8811254
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+
8821259 "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
8831260
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+
8841265 "strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="],
8851266
8861267 "strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="],
8871268
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+
8881275 "tarn": ["tarn@3.0.2", "", {}, "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ=="],
8891276
8901277 "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=="],
8911278
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+
8921283 "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
8931284
8941285 "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
@@ -907,8 +1298,26 @@ · 12 unmodified lines
9071298
9081299 "toml": ["toml@3.0.0", "", {}, "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="],
9091300
1301+ "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
1302+
9101303 "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
9111304
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+
9121321 "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=="],
9131322
9141323 "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
@@ -917,12 +1326,30 @@ · 2 unmodified lines
9171326
9181327 "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
9191328
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+
9201339 "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
9211340
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+
9221345 "uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="],
9231346
9241347 "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
9251348
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+
9261353 "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=="],
9271354
9281355 "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
9411368
9421369 "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
9431370
1371+ "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
1372+
9441373 "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="],
9451374
9461375 "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
9471376
9481377 "zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="],
9491378
1379+ "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
1380+
9501381 "@azure/msal-node/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
9511382
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+
9521403 "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
9531404
9541405 "tedious/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
package.json+2−0
@@ -19,12 +19,14 @@ · 18 unmodified lines
1919 "zod": "^4.3.6"
2020 },
2121 "scripts": {
22+ "dev": "turbo run dev --filter=@executor-v3/web",
2223 "executor": "./apps/executor/bin/executor",
2324 "typecheck": "bun run --filter='*' typecheck",
2425 "knip": "knip"
2526 },
2627 "devDependencies": {
2728 "knip": "^5.85.0",
29+ "turbo": "^2.5.6",
2830 "typescript": "^5.9.3"
2931 }
3032 }
packages/ai-sdk-adapter/src/example-dynamic.ts+4−4
@@ -21,8 +21,8 @@ const catalog: ToolCatalog = { · 20 unmodified lines
2121 path: asToolPath("source.src_api.github.issues.list"),
2222 sourceKey: "source.src_api",
2323 description: "Hydrated metadata for selected path",
24− inputHint: "object",
25− outputHint: "object",
24+ inputType: "object",
25+ outputType: "object",
2626 },
2727 ].filter((tool) => !namespace || tool.path.startsWith(`source.${namespace}.`)));
2828 },
@@ -31,8 +31,8 @@ const catalog: ToolCatalog = { · 2 unmodified lines
3131 path,
3232 sourceKey: "source.src_api",
3333 description: "Hydrated metadata for selected path",
34− inputHint: "object",
35− outputHint: "object",
34+ inputType: "object",
35+ outputType: "object",
3636 });
3737 },
3838 searchTools({ limit }) {
packages/codemode-core/src/discovery.ts+4−6
@@ -24,8 +24,8 @@ const searchableTextForTool = (tool: ToolDescriptor): string => · 23 unmodified lines
2424 tool.path,
2525 tool.sourceKey,
2626 tool.description ?? "",
27− tool.inputHint ?? "",
28− tool.outputHint ?? "",
27+ tool.inputType ?? "",
28+ tool.outputType ?? "",
2929 ]
3030 .join(" ")
3131 .toLowerCase();
@@ -44,7 +44,6 @@ const projectDescriptor = (input: { · 12 unmodified lines
4444 ...descriptor,
4545 inputSchemaJson: undefined,
4646 outputSchemaJson: undefined,
47− refHintKeys: undefined,
4847 };
4948 };
5049
@@ -229,13 +228,12 @@ export function createDiscoveryPrimitivesFromToolCatalog(input: { · 178 unmodified lines
229228 score: hit.score,
230229 description: descriptor.description,
231230 interaction: descriptor.interaction ?? "auto",
232− inputHint: descriptor.inputHint,
233− outputHint: descriptor.outputHint,
231+ inputType: descriptor.inputType,
232+ outputType: descriptor.outputType,
234233 ...(includeSchemas
235234 ? {
236235 inputSchemaJson: descriptor.inputSchemaJson,
237236 outputSchemaJson: descriptor.outputSchemaJson,
238− refHintKeys: descriptor.refHintKeys,
239237 }
240238 : {}),
241239 };
packages/codemode-core/src/index.test.ts+4−5
@@ -287,16 +287,16 @@ describe("codemode-core", () => { · 286 unmodified lines
287287 path: asToolPath("source.docs.search"),
288288 sourceKey: "source.docs",
289289 description: "Search docs",
290− inputHint: "object",
291− outputHint: "object",
290+ inputType: "object",
291+ outputType: "object",
292292 },
293293 "source.issues.create": {
294294 path: asToolPath("source.issues.create"),
295295 sourceKey: "source.issues",
296296 description: "Create issue",
297297 interaction: "required",
298− inputHint: "object",
299− outputHint: "object",
298+ inputType: "object",
299+ outputType: "object",
300300 },
301301 };
302302
@@ -327,7 +327,6 @@ describe("codemode-core", () => { · 24 unmodified lines
327327 ...descriptor,
328328 inputSchemaJson: includeSchemas ? descriptor.inputSchemaJson : undefined,
329329 outputSchemaJson: includeSchemas ? descriptor.outputSchemaJson : undefined,
330− refHintKeys: includeSchemas ? descriptor.refHintKeys : undefined,
331330 })),
332331 ),
333332 getToolByPath: ({ path }) =>
packages/codemode-core/src/index.ts+1−0
@@ -1,5 +1,6 @@
11 export * from "./types";
22 export * from "./json-schema";
3+export * from "./schema-types";
34 export * from "./tool-map";
45 export * from "./discovery";
56 export * from "./system-tools";
packages/codemode-core/src/schema-types.ts+125−0
@@ -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+};
packages/codemode-core/src/system-tools.ts+8−6
@@ -47,11 +47,14 @@ const catalogToolsOutputSchema = Schema.standardSchemaV1( · 46 unmodified lines
4747 sourceKey: Schema.String,
4848 description: Schema.optional(Schema.String),
4949 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),
5252 inputSchemaJson: Schema.optional(Schema.String),
5353 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),
5558 }),
5659 ),
5760 }),
@@ -81,11 +84,10 @@ const discoverResultItemSchema = Schema.Struct({ · 23 unmodified lines
8184 score: Schema.Number,
8285 description: Schema.optional(Schema.String),
8386 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),
8689 inputSchemaJson: Schema.optional(Schema.String),
8790 outputSchemaJson: Schema.optional(Schema.String),
88− refHintKeys: Schema.optional(Schema.Array(Schema.String)),
8991 });
9092
9193 const discoverOutputSchema = Schema.standardSchemaV1(
packages/codemode-core/src/tool-map.ts+18−47
@@ -2,6 +2,7 @@ import type { StandardSchemaV1 } from "@standard-schema/spec"; · 1 unmodified line
22 import * as Data from "effect/Data";
33 import * as Effect from "effect/Effect";
44 import * as JSONSchema from "effect/JSONSchema";
5+import { typeSignatureFromSchemaJson } from "./schema-types";
56
67 import type {
78 ElicitationRequest,
@@ -355,50 +356,11 @@ const stringifySchema = (value: unknown): string | undefined => { · 347 unmodified lines
355356 }
356357 };
357358
358−const inferHintFromSchemaJson = (
359+const inferTypeFromSchemaJson = (
359360 schemaJson: string | undefined,
360361 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);
402364
403365 export function createToolsFromRecord(input: {
404366 tools: Record<string, ExecutableTool>;
@@ -470,13 +432,22 @@ export function toolDescriptorsFromTools(input: { · 65 unmodified lines
470432 description: definition.description,
471433 interaction: metadata?.interaction,
472434 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"),
477439 inputSchemaJson,
478440 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+ : {}),
480451 } satisfies ToolDescriptor;
481452 });
482453 }
packages/codemode-core/src/types.ts+12−6
@@ -77,12 +77,15 @@ export type OnElicitation = ( · 76 unmodified lines
7777 export type ToolMetadata = {
7878 interaction?: "auto" | "required";
7979 elicitation?: ElicitationRequest;
80− inputHint?: string;
81− outputHint?: string;
80+ inputType?: string;
81+ outputType?: string;
8282 inputSchemaJson?: string;
8383 outputSchemaJson?: string;
84− refHintKeys?: readonly string[];
84+ exampleInputJson?: string;
85+ exampleOutputJson?: string;
8586 sourceKey?: string;
87+ providerKind?: string;
88+ providerDataJson?: string;
8689 };
8790
8891 export type ToolExecutionContext = {
@@ -127,11 +130,14 @@ export type ToolDescriptor = { · 38 unmodified lines
127130 description?: string;
128131 interaction?: "auto" | "required";
129132 elicitation?: ElicitationRequest;
130− inputHint?: string;
131− outputHint?: string;
133+ inputType?: string;
134+ outputType?: string;
132135 inputSchemaJson?: string;
133136 outputSchemaJson?: string;
134− refHintKeys?: readonly string[];
137+ exampleInputJson?: string;
138+ exampleOutputJson?: string;
139+ providerKind?: string;
140+ providerDataJson?: string;
135141 };
136142
137143 export type ToolNamespace = {
packages/codemode-openapi/openapi-extractor-rs/.gitignore+1−0
@@ -0,0 +1 @@
1+target/
packages/codemode-openapi/openapi-extractor-rs/Cargo.lock+445−0
@@ -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"
packages/codemode-openapi/openapi-extractor-rs/Cargo.toml+17−0
@@ -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"
packages/codemode-openapi/openapi-extractor-rs/build-wasm.sh+20−0
@@ -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"
packages/codemode-openapi/openapi-extractor-rs/src/lib.rs+1018−0
@@ -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+}
packages/codemode-openapi/openapi-extractor-rs/src/main.rs+58−0
@@ -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+}
packages/codemode-openapi/package.json+1−0
@@ -9,6 +9,7 @@ · 8 unmodified lines
99 "src"
1010 ],
1111 "scripts": {
12+ "extractor:build": "./openapi-extractor-rs/build-wasm.sh",
1213 "typecheck": "bunx tsc --noEmit -p tsconfig.json",
1314 "test": "vitest run",
1415 "test:watch": "vitest"
packages/codemode-openapi/src/index.ts+20−0
@@ -6,11 +6,19 @@ export { · 5 unmodified lines
66 OpenApiExtractionError,
77 extractOpenApiManifest,
88 } 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";
916 export {
1017 OpenApiToolInvocationError,
1118 createOpenApiToolsFromManifest,
1219 createOpenApiToolsFromSpec,
1320 } from "./openapi-tools";
21+export { resolveSchemaJsonWithRefHints, resolveTypingSchemasWithRefHints } from "./openapi-schema-refs";
1422 export {
1523 OPEN_API_HTTP_METHODS,
1624 OPEN_API_PARAMETER_LOCATIONS,
@@ -22,6 +30,11 @@ export { · 5 unmodified lines
2230 OpenApiToolManifestSchema,
2331 OpenApiToolParameterSchema,
2432 OpenApiToolRequestBodySchema,
33+ OpenApiExampleSchema,
34+ OpenApiParameterDocumentationSchema,
35+ OpenApiRequestBodyDocumentationSchema,
36+ OpenApiResponseDocumentationSchema,
37+ OpenApiToolDocumentationSchema,
2538 type DiscoveryTypingPayload,
2639 type OpenApiExtractedTool,
2740 type OpenApiHttpMethod,
@@ -35,3 +48,10 @@ export { · 7 unmodified lines
3548 type OpenApiToolParameter,
3649 type OpenApiToolRequestBody,
3750 } from "./openapi-types";
51+export type {
52+ OpenApiExample,
53+ OpenApiParameterDocumentation,
54+ OpenApiRequestBodyDocumentation,
55+ OpenApiResponseDocumentation,
56+ OpenApiToolDocumentation,
57+} from "./openapi-types";
packages/codemode-openapi/src/openapi-definitions.ts+254−0
@@ -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+ });
packages/codemode-openapi/src/openapi-extraction.real-specs.test.ts+118−0
@@ -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+});
packages/codemode-openapi/src/openapi-extractor-wasm/openapi_extractor_bg.wasm+0−0

Binary file not shown.

packages/codemode-openapi/src/openapi-schema-refs.ts+135−0
@@ -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+});
packages/codemode-openapi/src/openapi-tool-presentation.ts+106−0
@@ -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+};
packages/codemode-openapi/src/openapi-tools.test.ts+59−0
@@ -277,6 +277,8 @@ describe("openapi-tools", () => { · 276 unmodified lines
277277
278278 const resolvedGet = resolveToolDefinition(tools["source.demo.repos.getRepo"]!);
279279 expect(resolvedGet.metadata?.sourceKey).toBe("api.demo");
280+ expect(resolvedGet.metadata?.providerKind).toBe("openapi");
281+ expect(resolvedGet.metadata?.providerDataJson).toContain('"group":"repos"');
280282 expect(resolvedGet.metadata?.inputSchemaJson).toBeDefined();
281283 }),
282284 );
@@ -303,6 +305,7 @@ describe("openapi-tools", () => { · 20 unmodified lines
303305 );
304306
305307 expect(result).toEqual({
308+ ok: true,
306309 status: 200,
307310 headers: expect.any(Object),
308311 body: {
@@ -337,6 +340,7 @@ describe("openapi-tools", () => { · 28 unmodified lines
337340 );
338341
339342 expect(getResult).toEqual({
343+ ok: true,
340344 status: 200,
341345 headers: expect.any(Object),
342346 body: {
@@ -354,6 +358,7 @@ describe("openapi-tools", () => { · 11 unmodified lines
354358 );
355359
356360 expect(postResult).toEqual({
361+ ok: true,
357362 status: 201,
358363 headers: expect.any(Object),
359364 body: {
@@ -433,6 +438,7 @@ describe("openapi-tools", () => { · 73 unmodified lines
433438 );
434439
435440 expect(result).toEqual({
441+ ok: true,
436442 status: 200,
437443 headers: expect.any(Object),
438444 body: {
@@ -482,6 +488,7 @@ describe("openapi-tools", () => { · 43 unmodified lines
482488 );
483489
484490 expect(result).toEqual({
491+ ok: true,
485492 status: 200,
486493 headers: expect.any(Object),
487494 body: {
@@ -493,4 +500,56 @@ describe("openapi-tools", () => { · 5 unmodified lines
493500 expect(server.requests[0]?.path).toBe("/repos/octocat/hello-world");
494501 }),
495502 );
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+ );
496555 });
packages/codemode-openapi/src/openapi-tools.ts+107−29
@@ -14,6 +14,11 @@ import { · 13 unmodified lines
1414 unknownInputSchema,
1515 } from "@executor-v3/codemode-core";
1616
17+import {
18+ compileOpenApiToolDefinitions,
19+ type OpenApiToolDefinition,
20+} from "./openapi-definitions";
21+import { buildOpenApiToolPresentation } from "./openapi-tool-presentation";
1722 import {
1823 extractOpenApiManifest,
1924 type OpenApiExtractionError,
@@ -35,6 +40,44 @@ export class OpenApiToolInvocationError extends Data.TaggedError( · 15 unmodified lines
3540 details: string | null;
3641 }> {}
3742
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+
3881 const isRecord = (value: unknown): value is Record<string, unknown> =>
3982 typeof value === "object" && value !== null && !Array.isArray(value);
4083
@@ -201,13 +244,35 @@ const normalizeHttpUrl = (value: string): string => { · 160 unmodified lines
201244 }
202245 };
203246
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+
204269 const decodeHttpClientResponseBody = (
205270 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
209274 >
210− ? A
275+ ? Value
211276 : never,
212277 ): Effect.Effect<unknown, Error, never> => {
213278 const contentType = response.headers["content-type"]?.toLowerCase() ?? "";
@@ -242,8 +307,7 @@ const inputSchemaFromTypingJson = (inputSchemaJson: string | undefined) => { · 28 unmodified lines
242307 }
243308 };
244309
245−
246−type CreateOpenApiToolsFromManifestInput = {
310+export type CreateOpenApiToolsFromManifestInput = {
247311 manifest: OpenApiToolManifest;
248312 baseUrl: string;
249313 namespace?: string;
@@ -365,32 +429,38 @@ const buildFetchRequest = (input: { · 115 unmodified lines
365429 };
366430 };
367431
432+const createToolPath = (namespace: string | undefined, definition: OpenApiToolDefinition): string =>
433+ namespace ? `${namespace}.${definition.toolId}` : definition.toolId;
434+
368435 export const createOpenApiToolsFromManifest = (
369436 input: CreateOpenApiToolsFromManifestInput,
370437 ): ToolMap => {
371438 const baseUrl = normalizeHttpUrl(input.baseUrl);
372439 const sourceKey = input.sourceKey ?? "openapi.generated";
373− const namespace = input.namespace;
374440 const defaultHeaders = input.defaultHeaders ?? {};
375441 const credentialHeaders = input.credentialHeaders ?? {};
376442 const httpClientLayer = input.httpClientLayer ?? FetchHttpClient.layer;
377443
444+ const definitions = compileOpenApiToolDefinitions(input.manifest);
378445 const result: ToolMap = {};
379446
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+ });
384453
385− const description = extracted.description ?? `${extracted.method.toUpperCase()} ${extracted.path}`;
386454 result[toolPath] = toTool({
387455 tool: {
388− description,
389− inputSchema: inputSchemaFromTypingJson(extracted.typing?.inputSchemaJson),
456+ description: definition.description,
457+ inputSchema: inputSchemaFromTypingJson(
458+ presentation.inputSchemaJson ?? definition.typing?.inputSchemaJson,
459+ ),
390460 execute: async (args: unknown) => {
391461 const decodedArgs = asToolArgs(args);
392462 const request = buildFetchRequest({
393− payload: extracted.invocation,
463+ payload: definition.invocation,
394464 args: decodedArgs,
395465 baseUrl,
396466 defaultHeaders,
@@ -422,8 +492,9 @@ export const createOpenApiToolsFromManifest = ( · 25 unmodified lines
422492 const body = yield* decodeHttpClientResponseBody(response);
423493
424494 return {
495+ ok: response.status >= 200 && response.status < 300,
425496 status: response.status,
426− headers: { ...response.headers },
497+ headers: sanitizeResponseHeaders(response.headers),
427498 body,
428499 };
429500 }).pipe(
@@ -434,9 +505,14 @@ export const createOpenApiToolsFromManifest = ( · 4 unmodified lines
434505 },
435506 metadata: {
436507 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,
440516 },
441517 });
442518 }
@@ -454,20 +530,22 @@ export const createOpenApiToolsFromSpec = (input: { · 11 unmodified lines
454530 credentialHeaders?: Readonly<Record<string, string>>;
455531 httpClientLayer?: Layer.Layer<HttpClient.HttpClient, never, never>;
456532 }): Effect.Effect<
457− { manifest: OpenApiToolManifest; tools: ToolMap },
533+ { manifest: OpenApiToolManifest; definitions: Array<OpenApiToolDefinition>; tools: ToolMap },
458534 OpenApiExtractionError
459535 > =>
460536 Effect.map(
461537 extractOpenApiManifest(input.sourceName, input.openApiSpec),
462538 (manifest: OpenApiToolManifest) => ({
463− manifest,
464− tools: createOpenApiToolsFromManifest({
465539 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+ }),
472550 }),
473− }));
551+ );
packages/codemode-openapi/src/openapi-types.ts+43−2
@@ -13,7 +13,6 @@ export type OpenApiJsonObject = { · 12 unmodified lines
1313 [key: string]: OpenApiJsonValue;
1414 };
1515
16−// Accepts Effect-generated OpenAPI specs and generic JSON-like OpenAPI objects.
1716 export type OpenApiSpecInput = string | OpenAPISpec | OpenApiJsonObject;
1817
1918 export const OPEN_API_HTTP_METHODS = [
@@ -64,8 +63,44 @@ export const DiscoveryTypingPayloadSchema = Schema.Struct({ · 44 unmodified lines
6463 refHintKeys: Schema.optional(Schema.Array(Schema.String)),
6564 });
6665
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+
67100 export const OpenApiExtractedToolSchema = Schema.Struct({
68101 toolId: Schema.String,
102+ operationId: Schema.optional(Schema.String),
103+ tags: Schema.Array(Schema.String),
69104 name: Schema.String,
70105 description: Schema.NullOr(Schema.String),
71106 method: OpenApiHttpMethodSchema,
@@ -73,10 +108,11 @@ export const OpenApiExtractedToolSchema = Schema.Struct({ · 1 unmodified line
73108 invocation: OpenApiInvocationPayloadSchema,
74109 operationHash: Schema.String,
75110 typing: Schema.optional(DiscoveryTypingPayloadSchema),
111+ documentation: Schema.optional(OpenApiToolDocumentationSchema),
76112 });
77113
78114 export const OpenApiToolManifestSchema = Schema.Struct({
79− version: Schema.Literal(1),
115+ version: Schema.Literal(2),
80116 sourceHash: Schema.String,
81117 tools: Schema.Array(OpenApiExtractedToolSchema),
82118 refHintTable: Schema.optional(
@@ -93,5 +129,10 @@ export type OpenApiToolParameter = typeof OpenApiToolParameterSchema.Type; · 10 unmodified lines
93129 export type OpenApiToolRequestBody = typeof OpenApiToolRequestBodySchema.Type;
94130 export type OpenApiInvocationPayload = typeof OpenApiInvocationPayloadSchema.Type;
95131 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;
96137 export type OpenApiExtractedTool = typeof OpenApiExtractedToolSchema.Type;
97138 export type OpenApiToolManifest = typeof OpenApiToolManifestSchema.Type;
packages/control-plane/drizzle/20260307134000_openapi_tool_identity/migration.sql+37−0
@@ -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+);
packages/control-plane/drizzle/20260307152000_source_document_text/migration.sql+1−0
@@ -0,0 +1 @@
1+ALTER TABLE "sources" ADD COLUMN "source_document_text" text;
packages/control-plane/package.json+3−1
@@ -3,7 +3,9 @@ · 2 unmodified lines
33 "private": true,
44 "type": "module",
55 "exports": {
6− ".": "./src/index.ts"
6+ ".": "./src/index.ts",
7+ "./client": "./src/client.ts",
8+ "./schema": "./src/schema/index.ts"
79 },
810 "imports": {
911 "#schema": "./src/schema/index.ts",
packages/control-plane/src/api/sources/api.ts+33−0
@@ -3,6 +3,10 @@ import { · 2 unmodified lines
33 ExecutionInteractionIdSchema,
44 SourceAuthSchema,
55 SourceIdSchema,
6+ SourceInspectionDiscoverPayloadSchema,
7+ SourceInspectionDiscoverResultSchema,
8+ SourceInspectionSchema,
9+ SourceInspectionToolDetailSchema,
610 SourceKindSchema,
711 SourceSchema,
812 SourceStatusSchema,
@@ -72,6 +76,7 @@ export type UpdateSourcePayload = typeof UpdateSourcePayloadSchema.Type; · 63 unmodified lines
7276
7377 const workspaceIdParam = HttpApiSchema.param("workspaceId", WorkspaceIdSchema);
7478 const sourceIdParam = HttpApiSchema.param("sourceId", SourceIdSchema);
79+const toolPathParam = HttpApiSchema.param("toolPath", Schema.String);
7580
7681 const CredentialPageUrlParamsSchema = Schema.Struct({
7782 interactionId: ExecutionInteractionIdSchema,
@@ -163,4 +168,32 @@ export class SourcesApi extends HttpApiGroup.make("sources") · 85 unmodified lines
163168 .addError(ControlPlaneNotFoundError)
164169 .addError(ControlPlaneStorageError),
165170 )
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+ )
166199 .prefix("/v1") {}
packages/control-plane/src/api/sources/http.ts+37−0
@@ -17,6 +17,11 @@ import { · 16 unmodified lines
1717 removeSource,
1818 updateSource,
1919 } from "../../runtime/sources-operations";
20+import {
21+ discoverSourceInspectionTools,
22+ getSourceInspection,
23+ getSourceInspectionToolDetail,
24+} from "../../runtime/source-inspection";
2025 import {
2126 completeSourceCredentialSetup,
2227 getSourceCredentialInteraction,
@@ -512,6 +517,38 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 489 unmodified lines
512517 ),
513518 ),
514519 )
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+ )
515552 .handle("update", ({ path, payload }) =>
516553 withWorkspaceRequestActor("sources.update", path.workspaceId, () =>
517554 withPolicy(requireWriteSources(path.workspaceId))(
packages/control-plane/src/client.ts+2−2
@@ -6,8 +6,8 @@ import { · 5 unmodified lines
66 } from "@effect/platform";
77 import * as Effect from "effect/Effect";
88
9−import { ControlPlaneApi } from "./api";
10−import { ControlPlaneAuthHeaders } from "./runtime";
9+import { ControlPlaneApi } from "./api/api";
10+import { ControlPlaneAuthHeaders } from "./runtime/actor-resolver";
1111
1212 export const createControlPlaneClient = (input: {
1313 baseUrl: string;
packages/control-plane/src/persistence/control-plane-rows.test.ts+1−0
@@ -70,6 +70,7 @@ describe("control-plane-persistence-drizzle", () => { · 69 unmodified lines
7070 authHeaderName: null,
7171 authPrefix: null,
7272 sourceHash: null,
73+ sourceDocumentText: null,
7374 lastError: null,
7475 createdAt: now,
7576 updatedAt: now,
packages/control-plane/src/persistence/repos/sources-repo.ts+1−0
@@ -35,6 +35,7 @@ const toSourceUpdateSet = ( · 34 unmodified lines
3535 if (patch.authHeaderName !== undefined) set.authHeaderName = patch.authHeaderName;
3636 if (patch.authPrefix !== undefined) set.authPrefix = patch.authPrefix;
3737 if (patch.sourceHash !== undefined) set.sourceHash = patch.sourceHash;
38+ if (patch.sourceDocumentText !== undefined) set.sourceDocumentText = patch.sourceDocumentText;
3839 if (patch.lastError !== undefined) set.lastError = patch.lastError;
3940 if (patch.updatedAt !== undefined) set.updatedAt = patch.updatedAt;
4041
packages/control-plane/src/persistence/schema.ts+9−0
@@ -135,6 +135,7 @@ export const sourcesTable = pgTable( · 134 unmodified lines
135135 authHeaderName: text("auth_header_name"),
136136 authPrefix: text("auth_prefix"),
137137 sourceHash: text("source_hash"),
138+ sourceDocumentText: text("source_document_text"),
138139 lastError: text("last_error"),
139140 createdAt: bigint("created_at", { mode: "number" }).notNull(),
140141 updatedAt: bigint("updated_at", { mode: "number" }).notNull(),
@@ -205,6 +206,9 @@ export const toolArtifactsTable = pgTable( · 64 unmodified lines
205206 openApiMethod: text("openapi_method"),
206207 openApiPathTemplate: text("openapi_path_template"),
207208 openApiOperationHash: text("openapi_operation_hash"),
209+ openApiRawToolId: text("openapi_raw_tool_id"),
210+ openApiOperationId: text("openapi_operation_id"),
211+ openApiTagsJson: text("openapi_tags_json"),
208212 openApiRequestBodyRequired: boolean("openapi_request_body_required"),
209213 createdAt: bigint("created_at", { mode: "number" }).notNull(),
210214 updatedAt: bigint("updated_at", { mode: "number" }).notNull(),
@@ -236,6 +240,9 @@ export const toolArtifactsTable = pgTable( · 25 unmodified lines
236240 and ${table.openApiMethod} is null
237241 and ${table.openApiPathTemplate} is null
238242 and ${table.openApiOperationHash} is null
243+ and ${table.openApiRawToolId} is null
244+ and ${table.openApiOperationId} is null
245+ and ${table.openApiTagsJson} is null
239246 and ${table.openApiRequestBodyRequired} is null
240247 )`,
241248 ),
@@ -247,6 +254,8 @@ export const toolArtifactsTable = pgTable( · 5 unmodified lines
247254 and ${table.openApiMethod} in ('get', 'put', 'post', 'delete', 'patch', 'head', 'options', 'trace')
248255 and ${table.openApiPathTemplate} is not null
249256 and ${table.openApiOperationHash} is not null
257+ and ${table.openApiRawToolId} is not null
258+ and ${table.openApiTagsJson} is not null
250259 )`,
251260 ),
252261 ],
packages/control-plane/src/runtime/control-plane-runtime.test.ts+2−0
@@ -159,6 +159,7 @@ describe("control-plane-runtime", () => { · 158 unmodified lines
159159 authHeaderName: null,
160160 authPrefix: null,
161161 sourceHash: null,
162+ sourceDocumentText: null,
162163 lastError: null,
163164 createdAt: now,
164165 updatedAt: now,
@@ -307,6 +308,7 @@ describe("control-plane-runtime", () => { · 142 unmodified lines
307308 authHeaderName: null,
308309 authPrefix: null,
309310 sourceHash: null,
311+ sourceDocumentText: null,
310312 lastError: null,
311313 createdAt: now,
312314 updatedAt: now,
packages/control-plane/src/runtime/executor-tools.ts+2−2
@@ -373,8 +373,8 @@ export const createExecutorToolMap = (input: { · 372 unmodified lines
373373 },
374374 },
375375 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,
378378 inputSchemaJson: EXECUTOR_SOURCES_ADD_INPUT_SCHEMA_JSON,
379379 outputSchemaJson: EXECUTOR_SOURCES_ADD_OUTPUT_SCHEMA_JSON,
380380 sourceKey: "executor",
packages/control-plane/src/runtime/index.ts+2−1
@@ -57,6 +57,7 @@ export * from "./schema-type-signature"; · 56 unmodified lines
5757 export * from "./source-auth-service";
5858 export * from "./store";
5959 export * from "./workspace-execution-environment";
60+export * from "./source-inspection";
6061
6162 export type RuntimeControlPlaneOptions = {
6263 actorResolver?: ControlPlaneActorResolverShape;
@@ -114,7 +115,7 @@ const createRuntimeLayerFromContext = ( · 51 unmodified lines
114115 | RuntimeExecutionResolverService
115116 >;
116117
117− return Layer.succeedContext(runtimeContext);
118+ return Layer.succeedContext(runtimeContext) as RuntimeControlPlaneLayer;
118119 };
119120
120121 export const createRuntimeControlPlaneLayer = (
packages/control-plane/src/runtime/source-auth-service.ts+14−7
@@ -165,7 +165,7 @@ const persistSource = (rows: SqlControlPlaneRows, source: Source) => · 164 unmodified lines
165165 if (Option.isNone(existing)) {
166166 yield* rows.sources.insert(sourceRecord);
167167 } else {
168− const { id: _id, workspaceId: _workspaceId, createdAt: _createdAt, ...patch } = sourceRecord;
168+ const { id: _id, workspaceId: _workspaceId, createdAt: _createdAt, sourceDocumentText: _sourceDocumentText, ...patch } = sourceRecord;
169169 yield* rows.sources.update(source.workspaceId, source.id, patch);
170170 }
171171
@@ -431,12 +431,19 @@ const updateSourceStatus = (rows: SqlControlPlaneRows, source: Source, input: { · 259 unmodified lines
431431 lastError?: string | null;
432432 auth?: Source["auth"];
433433 }) =>
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+ });
440447 });
441448
442449 const upsertSecretMaterial = (rows: SqlControlPlaneRows, input: {
packages/control-plane/src/runtime/source-definitions.ts+1−0
@@ -255,6 +255,7 @@ export const splitSourceForStorage = (input: { · 254 unmodified lines
255255 input.source.auth.kind === "none" ? null : input.source.auth.headerName,
256256 authPrefix: input.source.auth.kind === "none" ? null : input.source.auth.prefix,
257257 sourceHash: input.source.sourceHash,
258+ sourceDocumentText: null,
258259 lastError: input.source.lastError,
259260 createdAt: input.source.createdAt,
260261 updatedAt: input.source.updatedAt,
packages/control-plane/src/runtime/source-inspection.ts+520−0
@@ -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+ );
packages/control-plane/src/runtime/sources-operations.ts+4−2
@@ -87,10 +87,11 @@ const syncArtifactsForSource = (input: { · 86 unmodified lines
8787 const { sourceRecord } = splitSourceForStorage({
8888 source: erroredSource,
8989 });
90+ const { sourceDocumentText: _sourceDocumentText, ...sourcePatch } = sourceRecord;
9091 yield* mapPersistenceError(
9192 input.operation.child("source_error"),
9293 input.store.sources.update(input.source.workspaceId, input.source.id, {
93− ...sourceRecord,
94+ ...sourcePatch,
9495 updatedAt: erroredSource.updatedAt,
9596 }),
9697 );
@@ -264,11 +265,12 @@ export const updateSource = (input: { · 167 unmodified lines
264265 const { sourceRecord, credentialBinding } = splitSourceForStorage({
265266 source: updatedSource,
266267 });
268+ const { sourceDocumentText: _sourceDocumentText, ...sourcePatch } = sourceRecord;
267269
268270 const stored = yield* mapPersistenceError(
269271 sourceOps.update.child("source"),
270272 store.sources.update(input.workspaceId, input.sourceId, {
271− ...sourceRecord,
273+ ...sourcePatch,
272274 updatedAt: updatedSource.updatedAt,
273275 }),
274276 );
packages/control-plane/src/runtime/tool-artifacts.ts+28−80
@@ -8,10 +8,7 @@ import { · 7 unmodified lines
88 discoverMcpToolsFromConnector,
99 type McpToolManifestEntry,
1010 } 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";
1512 import type { SqlControlPlaneRows } from "#persistence";
1613 import {
1714 type SecretRef,
@@ -22,6 +19,7 @@ import { · 4 unmodified lines
2219 type StoredToolArtifactRequestBodyContentTypeRecord,
2320 } from "#schema";
2421 import * as Effect from "effect/Effect";
22+import * as Option from "effect/Option";
2523
2624 export type ResolveSourceSecretMaterial = (
2725 ref: SecretRef,
@@ -98,6 +96,9 @@ const toMcpToolArtifactRecord = (input: { · 70 unmodified lines
9896 openApiMethod: null,
9997 openApiPathTemplate: null,
10098 openApiOperationHash: null,
99+ openApiRawToolId: null,
100+ openApiOperationId: null,
101+ openApiTagsJson: null,
101102 openApiRequestBodyRequired: null,
102103 createdAt: input.now,
103104 updatedAt: input.now,
@@ -105,71 +106,6 @@ const toMcpToolArtifactRecord = (input: { · 1 unmodified line
105106 };
106107 };
107108
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−};
173109
174110 const shouldIndexSource = (source: Source): boolean =>
175111 source.enabled
@@ -358,18 +294,30 @@ const indexOpenApiSourceToolArtifacts = (input: { · 182 unmodified lines
358294 );
359295
360296 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)),
371308 ),
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(
373321 Effect.mapError((cause) =>
374322 cause instanceof Error ? cause : new Error(String(cause)),
375323 ),
packages/control-plane/src/runtime/workspace-execution-environment.test.ts+101−94
@@ -25,6 +25,7 @@ import * as Effect from "effect/Effect"; · 24 unmodified lines
2525 import * as Exit from "effect/Exit";
2626 import * as Layer from "effect/Layer";
2727 import * as Scope from "effect/Scope";
28+import * as Option from "effect/Option";
2829 import { Schema } from "effect";
2930 import { z } from "zod/v4";
3031
@@ -372,6 +373,7 @@ const persistConnectedEchoTool = (input: { · 341 unmodified lines
372373 authHeaderName: null,
373374 authPrefix: null,
374375 sourceHash: null,
376+ sourceDocumentText: null,
375377 lastError: null,
376378 createdAt: now,
377379 updatedAt: now,
@@ -406,6 +408,9 @@ const persistConnectedEchoTool = (input: { · 28 unmodified lines
406408 openApiMethod: null,
407409 openApiPathTemplate: null,
408410 openApiOperationHash: null,
411+ openApiRawToolId: null,
412+ openApiOperationId: null,
413+ openApiTagsJson: null,
409414 openApiRequestBodyRequired: null,
410415 createdAt: now,
411416 updatedAt: now,
@@ -414,13 +419,89 @@ const persistConnectedEchoTool = (input: { · 2 unmodified lines
414419 });
415420 });
416421
417−const persistConnectedGithubSearchArtifacts = (input: {
422+const persistConnectedGithubOpenApiSource = (input: {
418423 persistence: SqlControlPlanePersistence;
419424 workspaceId: WorkspaceId;
420425 sourceId: SourceId;
421426 }) =>
422427 Effect.gen(function* () {
423428 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+ });
424505
425506 yield* input.persistence.rows.sources.insert({
426507 id: input.sourceId,
@@ -440,89 +521,11 @@ const persistConnectedGithubSearchArtifacts = (input: { · 13 unmodified lines
440521 authHeaderName: null,
441522 authPrefix: null,
442523 sourceHash: null,
524+ sourceDocumentText: openApiDocumentText,
443525 lastError: null,
444526 createdAt: now,
445527 updatedAt: now,
446528 });
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− });
526529 });
527530
528531 const makeResolver = (persistence: SqlControlPlanePersistence) =>
@@ -597,7 +600,7 @@ describe("workspace-execution-environment", () => { · 68 unmodified lines
597600 const persistence = yield* makePersistence;
598601 const workspaceId = WorkspaceIdSchema.make("ws_github_discovery");
599602 const sourceId = SourceIdSchema.make("src_github_discovery");
600− yield* persistConnectedGithubSearchArtifacts({
603+ yield* persistConnectedGithubOpenApiSource({
601604 persistence,
602605 workspaceId,
603606 sourceId,
@@ -613,7 +616,7 @@ describe("workspace-execution-environment", () => { · 9 unmodified lines
613616 const firstQuery = (yield* environment.toolInvoker.invoke({
614617 path: "discover",
615618 args: {
616− query: "get authenticated user",
619+ query: "current authenticated user profile",
617620 limit: 5,
618621 },
619622 })) as {
@@ -621,8 +624,8 @@ describe("workspace-execution-environment", () => { · 1 unmodified line
621624 results: Array<{ path: string }>;
622625 };
623626
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");
626629
627630 const secondQuery = (yield* environment.toolInvoker.invoke({
628631 path: "discover",
@@ -635,8 +638,8 @@ describe("workspace-execution-environment", () => { · 6 unmodified lines
635638 results: Array<{ path: string }>;
636639 };
637640
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");
640643 }),
641644 );
642645
@@ -735,6 +738,7 @@ describe("workspace-execution-environment", () => { · 92 unmodified lines
735738 runId: "exec_add_openapi",
736739 },
737740 })) as {
741+ id: SourceId;
738742 kind: string;
739743 status: string;
740744 auth: {
@@ -758,11 +762,14 @@ describe("workspace-execution-environment", () => { · 17 unmodified lines
758762 encodeURIComponent("exec_add_openapi:executor.sources.add:"),
759763 );
760764
761− const storedArtifacts = yield* persistence.rows.toolArtifacts.listByWorkspaceId(
765+ const storedSource = yield* persistence.rows.sources.getByWorkspaceAndId(
762766 workspaceId,
767+ added.id,
763768 );
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+ }
766773
767774 const freshEnvironment = yield* resolveEnvironment({
768775 workspaceId,
@@ -780,10 +787,10 @@ describe("workspace-execution-environment", () => { · 11 unmodified lines
780787 bestPath: string | null;
781788 };
782789
783− expect(discovered.bestPath).toBe(repoToolPath);
790+ expect(discovered.bestPath).toBe("github.repos.getRepo");
784791
785792 const invoked = (yield* freshEnvironment.toolInvoker.invoke({
786− path: repoToolPath,
793+ path: "github.repos.getRepo",
787794 args: {
788795 owner: "vercel",
789796 repo: "ai",
@@ -825,7 +832,7 @@ describe("workspace-execution-environment", () => { · 35 unmodified lines
825832 })) as {
826833 path: string;
827834 description?: string;
828− inputHint?: string;
835+ inputType?: string;
829836 inputSchemaJson?: string;
830837 } | null;
831838
@@ -833,8 +840,8 @@ describe("workspace-execution-environment", () => { · 1 unmodified line
833840 expect(described?.description).toContain("Source add input shapes:");
834841 expect(described?.description).toContain("specUrl");
835842 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");
838845
839846 const inputSchema = described?.inputSchemaJson
840847 ? JSON.parse(described.inputSchemaJson) as Record<string, unknown>
packages/control-plane/src/runtime/workspace-execution-environment.ts+249−125
@@ -8,13 +8,19 @@ import { · 7 unmodified lines
88 type ToolInvoker,
99 type ToolNamespace,
1010 type ToolPath,
11+ typeSignatureFromSchemaJson,
1112 } from "@executor-v3/codemode-core";
1213 import {
1314 createSdkMcpConnector,
1415 createMcpToolsFromManifest,
1516 } from "@executor-v3/codemode-mcp";
1617 import {
18+ buildOpenApiToolPresentation,
19+ compileOpenApiToolDefinitions,
1720 createOpenApiToolsFromManifest,
21+ extractOpenApiManifest,
22+ type OpenApiToolDefinition,
23+ type OpenApiToolManifest,
1824 } from "@executor-v3/codemode-openapi";
1925 import { makeInProcessExecutor } from "@executor-v3/runtime-local-inproc";
2026 import {
@@ -35,7 +41,7 @@ import type { · 14 unmodified lines
3541 ResolveExecutionEnvironment,
3642 } from "./execution-state";
3743 import { createExecutorToolMap } from "./executor-tools";
38−import { projectSourceFromStorage } from "./source-definitions";
44+import { projectSourceFromStorage, projectSourcesFromStorage } from "./source-definitions";
3945 import {
4046 RuntimeSourceAuthServiceTag,
4147 createDbBackedSecretMaterialResolver,
@@ -58,6 +64,7 @@ const tokenize = (value: string): string[] => · 16 unmodified lines
5864 .split(/[^a-z0-9]+/)
5965 .filter(Boolean);
6066
67+
6168 const LOW_SIGNAL_QUERY_TOKENS = new Set([
6269 "a",
6370 "an",
@@ -207,22 +214,153 @@ const scoreArtifact = ( · 143 unmodified lines
207214 return score;
208215 };
209216
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: {
211223 artifact: StoredToolArtifactRecord;
212224 includeSchemas: boolean;
213− refHintKeys?: readonly string[];
214225 }): ToolDescriptor => ({
215226 path: asToolPath(input.artifact.path),
216227 sourceKey: input.artifact.sourceId,
217228 description: input.artifact.description ?? input.artifact.title ?? undefined,
218229 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+ ),
221240 inputSchemaJson: input.includeSchemas ? input.artifact.inputSchemaJson ?? undefined : undefined,
222241 outputSchemaJson: input.includeSchemas ? input.artifact.outputSchemaJson ?? undefined : undefined,
223− refHintKeys: input.includeSchemas ? input.refHintKeys : undefined,
242+ ...(input.artifact.providerKind ? { providerKind: input.artifact.providerKind } : {}),
224243 });
225244
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+
226364 const loadSourceById = (input: {
227365 rows: SqlControlPlaneRows;
228366 workspaceId: Source["workspaceId"];
@@ -265,7 +403,7 @@ const createWorkspaceToolCatalog = (input: { · 36 unmodified lines
265403 }): ToolCatalog => ({
266404 listNamespaces: ({ limit }) =>
267405 Effect.gen(function* () {
268− const [persisted, executor] = yield* Effect.all([
406+ const [persisted, openApiTools, executor] = yield* Effect.all([
269407 input.rows.toolArtifacts.listNamespacesByWorkspaceId(input.workspaceId, {
270408 limit,
271409 }).pipe(
@@ -273,6 +411,11 @@ const createWorkspaceToolCatalog = (input: { · 1 unmodified line
273411 cause instanceof Error ? cause : new Error(String(cause)),
274412 ),
275413 ),
414+ loadOpenApiWorkspaceTools({
415+ rows: input.rows,
416+ workspaceId: input.workspaceId,
417+ includeSchemas: false,
418+ }),
276419 input.executorCatalog.listNamespaces({ limit }),
277420 ]);
278421
@@ -280,6 +423,13 @@ const createWorkspaceToolCatalog = (input: { · 1 unmodified line
280423 for (const namespace of persisted) {
281424 merged.set(namespace.namespace, namespace);
282425 }
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+ }
283433 for (const namespace of executor) {
284434 const existing = merged.get(namespace.namespace);
285435 merged.set(namespace.namespace, {
@@ -299,7 +449,7 @@ const createWorkspaceToolCatalog = (input: { · 13 unmodified lines
299449
300450 listTools: ({ namespace, query, limit, includeSchemas = false }) =>
301451 Effect.gen(function* () {
302− const [persisted, executor] = yield* Effect.all([
452+ const [persisted, openApiTools, executor] = yield* Effect.all([
303453 namespace?.startsWith("executor")
304454 ? Effect.succeed([] as readonly StoredToolArtifactRecord[])
305455 : input.rows.toolArtifacts.listByWorkspaceId(input.workspaceId, {
@@ -311,6 +461,23 @@ const createWorkspaceToolCatalog = (input: { · 5 unmodified lines
311461 cause instanceof Error ? cause : new Error(String(cause)),
312462 ),
313463 ),
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+ ),
314481 input.executorCatalog.listTools({
315482 ...(namespace !== undefined ? { namespace } : {}),
316483 ...(query !== undefined ? { query } : {}),
@@ -319,35 +486,18 @@ const createWorkspaceToolCatalog = (input: { · 2 unmodified lines
319486 }),
320487 ]);
321488
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+ );
349495
350− return [...persistedDescriptors, ...executor]
496+ return [
497+ ...persistedDescriptors,
498+ ...openApiTools.map((tool) => tool.descriptor),
499+ ...executor,
500+ ]
351501 .sort((left, right) => left.path.localeCompare(right.path))
352502 .slice(0, limit);
353503 }),
@@ -362,6 +512,16 @@ const createWorkspaceToolCatalog = (input: { · 8 unmodified lines
362512 return executor;
363513 }
364514
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+
365525 const artifact = yield* input.rows.toolArtifacts.getByWorkspaceAndPath(
366526 input.workspaceId,
367527 path,
@@ -375,29 +535,16 @@ const createWorkspaceToolCatalog = (input: { · 7 unmodified lines
375535 return null;
376536 }
377537
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({
391539 artifact: artifact.value,
392540 includeSchemas,
393− refHintKeys,
394541 });
395542 }),
396543
397544 searchTools: ({ query, namespace, limit }) =>
398545 Effect.gen(function* () {
399546 const queryTokens = tokenize(query);
400− const [persisted, executor] = yield* Effect.all([
547+ const [persisted, openApiTools, executor] = yield* Effect.all([
401548 namespace?.startsWith("executor")
402549 ? Effect.succeed([] as readonly StoredToolArtifactRecord[])
403550 : input.rows.toolArtifacts.searchByWorkspaceId(input.workspaceId, {
@@ -408,6 +555,15 @@ const createWorkspaceToolCatalog = (input: { · 4 unmodified lines
408555 cause instanceof Error ? cause : new Error(String(cause)),
409556 ),
410557 ),
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+ ),
411567 input.executorCatalog.searchTools({
412568 query,
413569 ...(namespace !== undefined ? { namespace } : {}),
@@ -422,7 +578,17 @@ const createWorkspaceToolCatalog = (input: { · 8 unmodified lines
422578 }))
423579 .filter((hit) => hit.score > 0);
424580
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]
426592 .sort((left, right) =>
427593 right.score - left.score || left.path.localeCompare(right.path),
428594 )
@@ -470,6 +636,37 @@ const createWorkspaceToolInvoker = (input: { · 41 unmodified lines
470636 context?: Record<string, unknown>;
471637 }) =>
472638 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+
473670 const artifactOption = yield* input.rows.toolArtifacts
474671 .getByWorkspaceAndPath(input.workspaceId, invocation.path)
475672 .pipe(
@@ -535,79 +732,6 @@ const createWorkspaceToolInvoker = (input: { · 59 unmodified lines
535732 });
536733 }
537734
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−
611735 return yield* Effect.fail(
612736 new Error(`Unsupported stored tool provider for ${invocation.path}`),
613737 );
packages/control-plane/src/schema/index.ts+1−0
@@ -11,3 +11,4 @@ export * from "./models/source-auth-session"; · 10 unmodified lines
1111 export * from "./models/policy";
1212 export * from "./models/local-installation";
1313 export * from "./models/execution";
14+export * from "./models/source-inspection";
packages/control-plane/src/schema/models/source-inspection.ts+85−0
@@ -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;
packages/control-plane/src/schema/models/source.ts+3−0
@@ -95,6 +95,7 @@ export const StoredSourceRecordSchema = Schema.transform( · 94 unmodified lines
9595 authHeaderName: Schema.NullOr(Schema.String),
9696 authPrefix: Schema.NullOr(Schema.String),
9797 sourceHash: Schema.NullOr(Schema.String),
98+ sourceDocumentText: Schema.NullOr(Schema.String),
9899 lastError: Schema.NullOr(Schema.String),
99100 createdAt: TimestampMsSchema,
100101 updatedAt: TimestampMsSchema,
@@ -119,6 +120,7 @@ export const StoredSourceRecordSchema = Schema.transform( · 18 unmodified lines
119120 authHeaderName: row.authHeaderName,
120121 authPrefix: row.authPrefix,
121122 sourceHash: row.sourceHash,
123+ sourceDocumentText: row.sourceDocumentText,
122124 lastError: row.lastError,
123125 createdAt: row.createdAt,
124126 updatedAt: row.updatedAt,
@@ -141,6 +143,7 @@ export const StoredSourceRecordSchema = Schema.transform( · 16 unmodified lines
141143 authHeaderName: source.authHeaderName,
142144 authPrefix: source.authPrefix,
143145 sourceHash: source.sourceHash,
146+ sourceDocumentText: source.sourceDocumentText,
144147 lastError: source.lastError,
145148 createdAt: source.createdAt,
146149 updatedAt: source.updatedAt,
packages/control-plane/src/schema/models/tool-artifact.ts+9−0
@@ -56,6 +56,9 @@ export const StoredToolArtifactRecordSchema = Schema.Struct({ · 55 unmodified lines
5656 ),
5757 openApiPathTemplate: Schema.NullOr(Schema.String),
5858 openApiOperationHash: Schema.NullOr(Schema.String),
59+ openApiRawToolId: Schema.NullOr(Schema.String),
60+ openApiOperationId: Schema.NullOr(Schema.String),
61+ openApiTagsJson: Schema.NullOr(Schema.String),
5962 openApiRequestBodyRequired: Schema.NullOr(Schema.Boolean),
6063 createdAt: TimestampMsSchema,
6164 updatedAt: TimestampMsSchema,
@@ -84,6 +87,9 @@ export const ToolArtifactStorageSchema = Schema.transform( · 22 unmodified lines
8487 openApiMethod: row.openApiMethod,
8588 openApiPathTemplate: row.openApiPathTemplate,
8689 openApiOperationHash: row.openApiOperationHash,
90+ openApiRawToolId: row.openApiRawToolId,
91+ openApiOperationId: row.openApiOperationId,
92+ openApiTagsJson: row.openApiTagsJson,
8793 openApiRequestBodyRequired: row.openApiRequestBodyRequired,
8894 createdAt: row.createdAt,
8995 updatedAt: row.updatedAt,
@@ -104,6 +110,9 @@ export const ToolArtifactStorageSchema = Schema.transform( · 14 unmodified lines
104110 openApiMethod: record.openApiMethod,
105111 openApiPathTemplate: record.openApiPathTemplate,
106112 openApiOperationHash: record.openApiOperationHash,
113+ openApiRawToolId: record.openApiRawToolId,
114+ openApiOperationId: record.openApiOperationId,
115+ openApiTagsJson: record.openApiTagsJson,
107116 openApiRequestBodyRequired: record.openApiRequestBodyRequired,
108117 createdAt: record.createdAt,
109118 updatedAt: record.updatedAt,
packages/kitchen-sink/src/source-runtime.test.ts+8−4
@@ -67,8 +67,8 @@ const toDescriptor = ( · 66 unmodified lines
6767 sourceKey: artifact.sourceId,
6868 description: artifact.description ?? artifact.title ?? undefined,
6969 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,
7272 inputSchemaJson: includeSchemas ? artifact.inputSchemaJson ?? undefined : undefined,
7373 outputSchemaJson: includeSchemas ? artifact.outputSchemaJson ?? undefined : undefined,
7474 });
@@ -527,6 +527,9 @@ const openApiArtifact = (input: { · 452 unmodified lines
527527 openApiMethod: input.method,
528528 openApiPathTemplate: input.pathTemplate,
529529 openApiOperationHash: input.operationHash ?? input.toolId,
530+ openApiRawToolId: input.toolId,
531+ openApiOperationId: input.toolId,
532+ openApiTagsJson: null,
530533 openApiRequestBodyRequired: null,
531534 createdAt: 0,
532535 updatedAt: 0,
@@ -805,13 +808,14 @@ describe("source runtime", () => { · 272 unmodified lines
805808 score: expect.any(Number),
806809 description: "GET /repos/{owner}/{repo}",
807810 interaction: "auto",
808− inputHint: "object { owner, repo }",
809− outputHint: "unknown",
811+ inputType: "{ owner: string, repo: string }",
812+ outputType: "unknown",
810813 },
811814 ],
812815 total: 1,
813816 },
814817 result: {
818+ ok: true,
815819 status: 200,
816820 headers: expect.any(Object),
817821 body: {
packages/react/package.json+24−0
@@ -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+}
packages/react/src/index.ts+325−0
@@ -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+};
packages/react/tsconfig.json+17−0
@@ -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+}
packages/server/package.json+20−0
@@ -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+}
packages/server/src/config.tsrenamed from apps/executor/src/server/config.ts+7−1
@@ -4,6 +4,12 @@ import { join } from "node:path"; · 3 unmodified lines
44 export const DEFAULT_SERVER_PORT = 8788;
55 export const DEFAULT_SERVER_HOST = "127.0.0.1";
66 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+);
814 export const SERVER_START_TIMEOUT_MS = 5_000;
915 export const SERVER_POLL_INTERVAL_MS = 100;
packages/server/src/index.ts+237−0
@@ -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+ );
packages/server/tsconfig.json+15−0
@@ -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+}
turbo.json+17−0
@@ -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+}