History

Add executor generate: typed TypeScript client for the tool catalog

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.

Rhys Sullivan committed 4 months ago

Commit dabe51f1 parent e39dbcf14 files · +1733 −5Browse files

Showing the changes to apps/cli (1 of 14 files). Show all

Unified · Split

apps/cli/src/main.ts+103−0
@@ -69,6 +69,8 @@ import type { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js"; · 68 unmodified lines
69 69
70import { ExecutorApi, checkForUpdate } from "@executor-js/api";70import { ExecutorApi, checkForUpdate } from "@executor-js/api";
71import {71import {
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";
82import { generateToolProxySource } from "@executor-js/sdk/core";
80import {83import {
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
2720const 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 + restart2816// 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),