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/src (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
6969
7070 import { ExecutorApi, checkForUpdate } from "@executor-js/api";
7171 import {
72+ ConnectionName,
73+ IntegrationSlug,
7274 getExecutorServerAuthorizationHeader,
7375 normalizeExecutorServerConnection,
7476 normalizeExecutorServerOrigin,
@@ -77,6 +79,7 @@ import { · 2 unmodified lines
7779 type ExecutorServerConnection,
7880 type ExecutorServerConnectionInput,
7981 } from "@executor-js/sdk/shared";
82+import { generateToolProxySource } from "@executor-js/sdk/core";
8083 import {
8184 decodeAccessTokenClaims,
8285 discoverCliLogin,
@@ -2710,6 +2713,105 @@ const mcpCommand = Command.make( · 2627 unmodified lines
27102713 }),
27112714 ).pipe(Command.withDescription("Start an MCP server over stdio"));
27122715
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+
27132815 // ---------------------------------------------------------------------------
27142816 // Service — register the daemon with the OS so it survives app-quit + restart
27152817 // ---------------------------------------------------------------------------
@@ -3112,6 +3214,7 @@ const root = Command.make("executor").pipe( · 396 unmodified lines
31123214 daemonCommand,
31133215 serviceCommand,
31143216 mcpCommand,
3217+ generateCommand,
31153218 openCommand,
31163219 docsCommand,
31173220 ] as const),