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 (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 | 70 | import { ExecutorApi, checkForUpdate } from "@executor-js/api"; |
| 71 | 71 | import { |
| 72 | + ConnectionName, | |
| 73 | + IntegrationSlug, | |
| 72 | 74 | getExecutorServerAuthorizationHeader, |
| 73 | 75 | normalizeExecutorServerConnection, |
| 74 | 76 | normalizeExecutorServerOrigin, |
| @@ -77,6 +79,7 @@ import { · 2 unmodified lines | ||
| 77 | 79 | type ExecutorServerConnection, |
| 78 | 80 | type ExecutorServerConnectionInput, |
| 79 | 81 | } from "@executor-js/sdk/shared"; |
| 82 | +import { generateToolProxySource } from "@executor-js/sdk/core"; | |
| 80 | 83 | import { |
| 81 | 84 | decodeAccessTokenClaims, |
| 82 | 85 | discoverCliLogin, |
| @@ -2710,6 +2713,105 @@ const mcpCommand = Command.make( · 2627 unmodified lines | ||
| 2710 | 2713 | }), |
| 2711 | 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 | 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 | 3214 | daemonCommand, |
| 3113 | 3215 | serviceCommand, |
| 3114 | 3216 | mcpCommand, |
| 3217 | + generateCommand, | |
| 3115 | 3218 | openCommand, |
| 3116 | 3219 | docsCommand, |
| 3117 | 3220 | ] as const), |