Running executor generate against an instance writes one self-contained TypeScript file: a dependency-free Proxy-based runtime client plus full input/output types for every visible tool, so client.github.org.main.issues.create({ title }) is typed end to end and executes through the server's /api/executions endpoint (auth, policies, approvals included). New pieces: - tools.export SDK surface + GET /tools/export: the bulk schema-bearing catalog read (schemas + trimmed shared $defs, grouped per connection, policy-filtered), one request regardless of catalog size. - compileToolChunkTypeScript: compiles many tools per compiler pass. Per-tool passes pay fixed overhead thousands of times and one whole-catalog pass is super-linear (30s+ at 10k tools); 200-tool chunks generate a 10k-tool catalog in ~400ms. - generateToolProxySource: assembles the generated file (type-only namespaces per connection, ExecutorTools path tree, embedded runtime). - executor generate CLI command with --output/--integration/--connection/ --include-static, reusing the server-target and auth machinery. Tested down to the wire: generated source is typechecked with the real compiler (valid calls accept, invalid reject), the transpiled runtime is imported and run against a fake fetch (completed/paused/injection paths), and a scale test ingests a 10,000-operation OpenAPI spec through addSpec and proves export + generate + strict typecheck hold up with regression tripwires on time.
Showing the changes to apps/cli/src (1 of 14 files). Show all
Unified · Split
| @@ -69,6 +69,8 @@ import type { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js"; · 68 unmodified lines | |||
| 69 | 69 | ||
| 70 | import { ExecutorApi, checkForUpdate } from "@executor-js/api"; | 70 | import { ExecutorApi, checkForUpdate } from "@executor-js/api"; |
| 71 | import { | 71 | import { |
| 72 | ConnectionName, | ||
| 73 | IntegrationSlug, | ||
| 72 | getExecutorServerAuthorizationHeader, | 74 | getExecutorServerAuthorizationHeader, |
| 73 | normalizeExecutorServerConnection, | 75 | normalizeExecutorServerConnection, |
| 74 | normalizeExecutorServerOrigin, | 76 | normalizeExecutorServerOrigin, |
| @@ -77,6 +79,7 @@ import { · 2 unmodified lines | |||
| 77 | type ExecutorServerConnection, | 79 | type ExecutorServerConnection, |
| 78 | type ExecutorServerConnectionInput, | 80 | type ExecutorServerConnectionInput, |
| 79 | } from "@executor-js/sdk/shared"; | 81 | } from "@executor-js/sdk/shared"; |
| 82 | import { generateToolProxySource } from "@executor-js/sdk/core"; | ||
| 80 | import { | 83 | import { |
| 81 | decodeAccessTokenClaims, | 84 | decodeAccessTokenClaims, |
| 82 | discoverCliLogin, | 85 | discoverCliLogin, |
| @@ -2710,6 +2713,105 @@ const mcpCommand = Command.make( · 2627 unmodified lines | |||
| 2710 | }), | 2713 | }), |
| 2711 | ).pipe(Command.withDescription("Start an MCP server over stdio")); | 2714 | ).pipe(Command.withDescription("Start an MCP server over stdio")); |
| 2712 | 2715 | ||
| 2716 | // --------------------------------------------------------------------------- | ||
| 2717 | // Generate — emit a typed TypeScript client for the instance's tool catalog | ||
| 2718 | // --------------------------------------------------------------------------- | ||
| 2719 | |||
| 2720 | const generateCommand = Command.make( | ||
| 2721 | "generate", | ||
| 2722 | { | ||
| 2723 | output: Options.string("output").pipe( | ||
| 2724 | Options.withAlias("o"), | ||
| 2725 | Options.withDefault("executor.gen.ts"), | ||
| 2726 | Options.withDescription("Path of the generated TypeScript file."), | ||
| 2727 | ), | ||
| 2728 | integration: Options.string("integration").pipe( | ||
| 2729 | Options.optional, | ||
| 2730 | Options.withDescription("Only generate tools from this integration slug."), | ||
| 2731 | ), | ||
| 2732 | connection: Options.string("connection").pipe( | ||
| 2733 | Options.optional, | ||
| 2734 | Options.withDescription("Only generate tools from this connection name."), | ||
| 2735 | ), | ||
| 2736 | includeStatic: Options.boolean("include-static").pipe( | ||
| 2737 | Options.withDefault(false), | ||
| 2738 | Options.withDescription( | ||
| 2739 | "Include Executor's built-in static tools (executor.*, search, describe.*).", | ||
| 2740 | ), | ||
| 2741 | ), | ||
| 2742 | baseUrl: serverBaseUrl, | ||
| 2743 | server: serverProfile, | ||
| 2744 | scope, | ||
| 2745 | }, | ||
| 2746 | ({ output, integration, connection, includeStatic, baseUrl, server, scope }) => | ||
| 2747 | Effect.gen(function* () { | ||
| 2748 | applyScope(scope); | ||
| 2749 | const target = serverTargetFromOptions({ baseUrl, server }); | ||
| 2750 | const serverConnection = yield* resolveExecutorServerConnection(target); | ||
| 2751 | const client = yield* makeApiClient(serverConnection); | ||
| 2752 | |||
| 2753 | const catalog = yield* client.tools | ||
| 2754 | .export({ | ||
| 2755 | query: { | ||
| 2756 | ...(Option.isSome(integration) | ||
| 2757 | ? { integration: IntegrationSlug.make(integration.value) } | ||
| 2758 | : {}), | ||
| 2759 | ...(Option.isSome(connection) | ||
| 2760 | ? { connection: ConnectionName.make(connection.value) } | ||
| 2761 | : {}), | ||
| 2762 | }, | ||
| 2763 | }) | ||
| 2764 | .pipe(Effect.mapError(toError)); | ||
| 2765 | |||
| 2766 | const filtered = includeStatic | ||
| 2767 | ? catalog | ||
| 2768 | : { | ||
| 2769 | connections: catalog.connections | ||
| 2770 | .map((entry) => ({ | ||
| 2771 | ...entry, | ||
| 2772 | tools: entry.tools.filter((tool) => tool.static !== true), | ||
| 2773 | })) | ||
| 2774 | .filter((entry) => entry.tools.length > 0), | ||
| 2775 | }; | ||
| 2776 | |||
| 2777 | const generated = yield* Effect.try({ | ||
| 2778 | try: () => generateToolProxySource(filtered), | ||
| 2779 | catch: (cause) => | ||
| 2780 | cause instanceof Error | ||
| 2781 | ? cause | ||
| 2782 | : new Error(`Failed to generate TypeScript source: ${String(cause)}`), | ||
| 2783 | }); | ||
| 2784 | |||
| 2785 | if (generated.toolCount === 0) { | ||
| 2786 | return yield* Effect.fail( | ||
| 2787 | new Error( | ||
| 2788 | [ | ||
| 2789 | "No tools matched, nothing to generate.", | ||
| 2790 | `Server: ${serverConnection.origin}`, | ||
| 2791 | `Check filters, or add an integration first: ${cliPrefix} web`, | ||
| 2792 | ].join("\n"), | ||
| 2793 | ), | ||
| 2794 | ); | ||
| 2795 | } | ||
| 2796 | |||
| 2797 | const fs = yield* FileSystem.FileSystem; | ||
| 2798 | const path = yield* PlatformPath.Path; | ||
| 2799 | const outputPath = path.isAbsolute(output) ? output : path.join(process.cwd(), output); | ||
| 2800 | yield* fs.makeDirectory(path.dirname(outputPath), { recursive: true }); | ||
| 2801 | yield* fs.writeFileString(outputPath, generated.source); | ||
| 2802 | |||
| 2803 | console.log( | ||
| 2804 | `Generated ${outputPath} (${generated.toolCount} tools, ${generated.connectionCount} connections).`, | ||
| 2805 | ); | ||
| 2806 | console.log(""); | ||
| 2807 | console.log("Use it:"); | ||
| 2808 | console.log(` import { createExecutorClient } from "./${path.basename(output, ".ts")}";`); | ||
| 2809 | console.log(` const executor = createExecutorClient();`); | ||
| 2810 | }).pipe(Effect.mapError(toError)), | ||
| 2811 | ).pipe( | ||
| 2812 | Command.withDescription("Generate a typed TypeScript client for this instance's tool catalog"), | ||
| 2813 | ); | ||
| 2814 | |||
| 2713 | // --------------------------------------------------------------------------- | 2815 | // --------------------------------------------------------------------------- |
| 2714 | // Service — register the daemon with the OS so it survives app-quit + restart | 2816 | // Service — register the daemon with the OS so it survives app-quit + restart |
| 2715 | // --------------------------------------------------------------------------- | 2817 | // --------------------------------------------------------------------------- |
| @@ -3112,6 +3214,7 @@ const root = Command.make("executor").pipe( · 396 unmodified lines | |||
| 3112 | daemonCommand, | 3214 | daemonCommand, |
| 3113 | serviceCommand, | 3215 | serviceCommand, |
| 3114 | mcpCommand, | 3216 | mcpCommand, |
| 3217 | generateCommand, | ||
| 3115 | openCommand, | 3218 | openCommand, |
| 3116 | docsCommand, | 3219 | docsCommand, |
| 3117 | ] as const), | 3220 | ] as const), |