History

Refactor AddSourceDialog and useAddSourceFormState for improved credential handling

- Replaced the custom source configuration logic with a new `saveSourceWithCredentials` function in `AddSourceDialog`. - Updated `useAddSourceFormState` to manage authentication state revisions more effectively. - Removed unused functions and streamlined error handling related to credential input. - Enhanced user feedback on source addition and updates with clearer success messages.

Rhys Sullivan committed 8 months ago

Commit 2c3cc0e1 parent 910fd0a19 files · +185 −448Browse files

Unified · Split

19 files
  1. executor/apps/web/src/components/tools/add-source-dialog.tsx +23 −69
  2. executor/apps/web/src/components/tools/add-source-submit.ts +129 −0
  3. executor/apps/web/src/components/tools/use-add-source-form-state.ts +19 −14
  4. executor/packages/convex/auth/db_queries.ts +1 −13
  5. executor/packages/convex/auth/identity.ts +1 −1
  6. executor/packages/convex/database/mappers.ts +1 −1
  7. executor/packages/convex/task/events.ts +1 −1
  8. executor/packages/convex/task/finish.ts +1 −1
  9. executor/packages/convex/task/status.ts +1 −1
  10. executor/packages/core/src/declaration-generation.ts +0 −195
  11. executor/packages/core/src/events.ts +0 −43
  12. executor/packages/core/src/runtimes/runtime-catalog.ts +1 −1
  13. executor/packages/core/src/runtimes/transpile.ts +1 −1
  14. executor/packages/core/src/tool-discovery/indexing.ts +1 −1
  15. executor/packages/core/src/tool-source-execution.ts +3 −3
  16. executor/packages/core/src/tool-source-loaders/openapi-loader.ts +1 −1
  17. executor/packages/core/src/tool-sources.ts +0 −46
  18. executor/packages/core/src/type-hints.ts +1 −1
  19. executor/packages/core/src/typechecker-ts-utils.ts +0 −55
executor/apps/web/src/components/tools/add-source-dialog.tsx+23−69
@@ -17,19 +17,15 @@ import { convexApi } from "@/lib/convex-api"; · 16 unmodified lines
1717 import type { CredentialRecord, ToolSourceRecord } from "@/lib/types";
1818 import { workspaceQueryArgs } from "@/lib/workspace-query-args";
1919 import type { CatalogCollectionItem } from "@/lib/catalog-collections";
20−import { sourceKeyForSource } from "@/lib/tools-source-helpers";
21−import {
22− createCustomSourceConfig,
23−} from "./add-source-dialog-helpers";
2420 import {
2521 CatalogViewSection,
2622 CustomViewSection,
2723 } from "./add-source-dialog-sections";
2824 import { SourceAuthPanel } from "./add-source-auth-panel";
2925 import {
30− existingCredentialMatchesAuthType,
3126 useAddSourceFormState,
3227 } from "./use-add-source-form-state";
28+import { saveSourceWithCredentials } from "./add-source-submit";
3329
3430 export function AddSourceDialog({
3531 existingSourceNames,
@@ -90,76 +86,34 @@ export function AddSourceDialog({ · 54 unmodified lines
9086
9187 setSubmitting(true);
9288 try {
93− const authConfig = form.type === "openapi" || form.type === "graphql"
94− ? form.buildAuthConfig()
95− : undefined;
96− const config = createCustomSourceConfig({
97− type: form.type,
98− endpoint: form.endpoint.trim(),
99− baseUrl: form.baseUrl,
100− auth: authConfig,
101− mcpTransport: form.mcpTransport,
102− actorId: context.actorId,
89+ const result = await saveSourceWithCredentials({
90+ context,
91+ sourceToEdit,
92+ credentialsLoading,
93+ upsertToolSource,
94+ upsertCredential,
95+ form: {
96+ name: form.name,
97+ endpoint: form.endpoint,
98+ type: form.type,
99+ baseUrl: form.baseUrl,
100+ mcpTransport: form.mcpTransport,
101+ authType: form.authType,
102+ authScope: form.authScope,
103+ existingScopedCredential: form.existingScopedCredential,
104+ buildAuthConfig: form.buildAuthConfig,
105+ hasCredentialInput: form.hasCredentialInput,
106+ buildSecretJson: form.buildSecretJson,
107+ },
103108 });
104109
105− const created = await upsertToolSource({
106− ...(sourceToEdit ? { id: sourceToEdit.id } : {}),
107− workspaceId: context.workspaceId,
108− sessionId: context.sessionId,
109− name: form.name.trim(),
110− type: form.type,
111− config,
112− }) as ToolSourceRecord;
113− let linkedCredential = false;
114−
115− if ((form.type === "openapi" || form.type === "graphql") && form.authType !== "none") {
116− const sourceKey = sourceKeyForSource(created);
117− if (!sourceKey) {
118− throw new Error("Failed to resolve source key for credentials");
119− }
120−
121− if (form.authScope === "actor" && !context.actorId) {
122− throw new Error("Actor credentials require an authenticated actor");
123− }
124−
125− const enteredCredential = form.hasCredentialInput();
126− if (!enteredCredential && credentialsLoading) {
127− throw new Error("Loading existing connections, try again in a moment");
128− }
129−
130− if (enteredCredential) {
131− const secret = form.buildSecretJson();
132− if (!secret.value) {
133− throw new Error(secret.error ?? "Credential values are required");
134− }
135−
136− await upsertCredential({
137− ...(form.existingScopedCredential ? { id: form.existingScopedCredential.id } : {}),
138− workspaceId: context.workspaceId,
139− sessionId: context.sessionId,
140− sourceKey,
141− scope: form.authScope,
142− ...(form.authScope === "actor" ? { actorId: context.actorId } : {}),
143− secretJson: secret.value,
144− });
145− linkedCredential = true;
146− } else if (form.existingScopedCredential) {
147− if (!existingCredentialMatchesAuthType(form.existingScopedCredential, form.authType)) {
148− throw new Error("Enter credentials for the selected auth type");
149− }
150− linkedCredential = true;
151− } else {
152− throw new Error("Enter credentials to finish setup");
153− }
154− }
155−
156− onSourceAdded?.(created, { connected: linkedCredential });
110+ onSourceAdded?.(result.source, { connected: result.connected });
157111 toast.success(
158112 sourceToEdit
159− ? linkedCredential
113+ ? result.connected
160114 ? `Source "${form.name.trim()}" updated with credentials`
161115 : `Source "${form.name.trim()}" updated`
162− : linkedCredential
116+ : result.connected
163117 ? `Source "${form.name.trim()}" added with credentials — loading tools…`
164118 : `Source "${form.name.trim()}" added — loading tools…`,
165119 );
executor/apps/web/src/components/tools/add-source-submit.ts+129−0
@@ -0,0 +1,129 @@
1+import { sourceKeyForSource } from "@/lib/tools-source-helpers";
2+import type {
3+ AnonymousContext,
4+ CredentialRecord,
5+ CredentialScope,
6+ SourceAuthType,
7+ ToolSourceRecord,
8+} from "@/lib/types";
9+import { createCustomSourceConfig, type SourceType } from "./add-source-dialog-helpers";
10+import { existingCredentialMatchesAuthType } from "./add-source-form-utils";
11+
12+type UpsertToolSourceFn = (args: {
13+ id?: ToolSourceRecord["id"];
14+ workspaceId: AnonymousContext["workspaceId"];
15+ sessionId: AnonymousContext["sessionId"];
16+ name: string;
17+ type: SourceType;
18+ config: Record<string, unknown>;
19+}) => Promise<unknown>;
20+
21+type UpsertCredentialFn = (args: {
22+ id?: CredentialRecord["id"];
23+ workspaceId: AnonymousContext["workspaceId"];
24+ sessionId: AnonymousContext["sessionId"];
25+ sourceKey: string;
26+ scope: CredentialScope;
27+ actorId?: AnonymousContext["actorId"];
28+ secretJson: Record<string, unknown>;
29+}) => Promise<unknown>;
30+
31+type SaveFormSnapshot = {
32+ name: string;
33+ endpoint: string;
34+ type: SourceType;
35+ baseUrl: string;
36+ mcpTransport: "auto" | "streamable-http" | "sse";
37+ authType: Exclude<SourceAuthType, "mixed">;
38+ authScope: CredentialScope;
39+ existingScopedCredential: CredentialRecord | null;
40+ buildAuthConfig: () => Record<string, unknown> | undefined;
41+ hasCredentialInput: () => boolean;
42+ buildSecretJson: () => { value?: Record<string, unknown>; error?: string };
43+};
44+
45+export async function saveSourceWithCredentials({
46+ context,
47+ sourceToEdit,
48+ form,
49+ credentialsLoading,
50+ upsertToolSource,
51+ upsertCredential,
52+}: {
53+ context: AnonymousContext;
54+ sourceToEdit?: ToolSourceRecord;
55+ form: SaveFormSnapshot;
56+ credentialsLoading: boolean;
57+ upsertToolSource: UpsertToolSourceFn;
58+ upsertCredential: UpsertCredentialFn;
59+}): Promise<{ source: ToolSourceRecord; connected: boolean }> {
60+ const authConfig = form.type === "openapi" || form.type === "graphql"
61+ ? form.buildAuthConfig()
62+ : undefined;
63+
64+ const config = createCustomSourceConfig({
65+ type: form.type,
66+ endpoint: form.endpoint.trim(),
67+ baseUrl: form.baseUrl,
68+ auth: authConfig,
69+ mcpTransport: form.mcpTransport,
70+ actorId: context.actorId,
71+ });
72+
73+ const created = await upsertToolSource({
74+ ...(sourceToEdit ? { id: sourceToEdit.id } : {}),
75+ workspaceId: context.workspaceId,
76+ sessionId: context.sessionId,
77+ name: form.name.trim(),
78+ type: form.type,
79+ config,
80+ }) as ToolSourceRecord;
81+
82+ let linkedCredential = false;
83+
84+ if ((form.type === "openapi" || form.type === "graphql") && form.authType !== "none") {
85+ const sourceKey = sourceKeyForSource(created);
86+ if (!sourceKey) {
87+ throw new Error("Failed to resolve source key for credentials");
88+ }
89+
90+ if (form.authScope === "actor" && !context.actorId) {
91+ throw new Error("Actor credentials require an authenticated actor");
92+ }
93+
94+ const enteredCredential = form.hasCredentialInput();
95+ if (!enteredCredential && credentialsLoading) {
96+ throw new Error("Loading existing connections, try again in a moment");
97+ }
98+
99+ if (enteredCredential) {
100+ const secret = form.buildSecretJson();
101+ if (!secret.value) {
102+ throw new Error(secret.error ?? "Credential values are required");
103+ }
104+
105+ await upsertCredential({
106+ ...(form.existingScopedCredential ? { id: form.existingScopedCredential.id } : {}),
107+ workspaceId: context.workspaceId,
108+ sessionId: context.sessionId,
109+ sourceKey,
110+ scope: form.authScope,
111+ ...(form.authScope === "actor" ? { actorId: context.actorId } : {}),
112+ secretJson: secret.value,
113+ });
114+ linkedCredential = true;
115+ } else if (form.existingScopedCredential) {
116+ if (!existingCredentialMatchesAuthType(form.existingScopedCredential, form.authType)) {
117+ throw new Error("Enter credentials for the selected auth type");
118+ }
119+ linkedCredential = true;
120+ } else {
121+ throw new Error("Enter credentials to finish setup");
122+ }
123+ }
124+
125+ return {
126+ source: created,
127+ connected: linkedCredential,
128+ };
129+}
executor/apps/web/src/components/tools/use-add-source-form-state.ts+19−14
@@ -35,6 +35,17 @@ function patchUi( · 34 unmodified lines
3535 setUi((current) => ({ ...current, ...patch }));
3636 }
3737
38+function patchUiWithAuthRevision(
39+ setUi: Dispatch<SetStateAction<AddSourceUiState>>,
40+ patch: Partial<AddSourceUiState> = {},
41+) {
42+ setUi((current) => ({
43+ ...current,
44+ ...patch,
45+ authRevision: current.authRevision + 1,
46+ }));
47+}
48+
3849 export function useAddSourceFormState({
3950 open,
4051 sourceToEdit,
@@ -88,7 +99,7 @@ export function useAddSourceFormState({ · 47 unmodified lines
8899 if (sourceToEdit) {
89100 const editValues = sourceToFormValues(sourceToEdit);
90101 form.reset(editValues);
91− patchUi(setUi, {
102+ patchUiWithAuthRevision(setUi, {
92103 ...createDefaultUiState("custom"),
93104 openApiBaseUrlOptions: editValues.baseUrl
94105 ? [editValues.baseUrl]
@@ -97,16 +108,12 @@ export function useAddSourceFormState({ · 2 unmodified lines
97108 return fallback ? [fallback] : [];
98109 })(),
99110 authManuallyEdited: Boolean(sourceToEdit.config.auth),
100− authRevision: ui.authRevision + 1,
101111 });
102112 return;
103113 }
104114
105115 form.reset(createDefaultFormValues());
106− patchUi(setUi, {
107− ...createDefaultUiState("catalog"),
108− authRevision: ui.authRevision + 1,
109− });
116+ patchUiWithAuthRevision(setUi, createDefaultUiState("catalog"));
110117 }, [form, open, sourceToEdit]);
111118
112119 const getTakenSourceNames = () => {
@@ -157,11 +164,10 @@ export function useAddSourceFormState({ · 44 unmodified lines
157164 endpoint: item.specUrl,
158165 baseUrl: defaultBaseUrl,
159166 });
160− patchUi(setUi, {
167+ patchUiWithAuthRevision(setUi, {
161168 view: "custom",
162169 openApiBaseUrlOptions: defaultBaseUrl ? [defaultBaseUrl] : [],
163170 authManuallyEdited: false,
164− authRevision: ui.authRevision + 1,
165171 });
166172 };
167173
@@ -180,10 +186,9 @@ export function useAddSourceFormState({ · 12 unmodified lines
180186 return;
181187 }
182188
183− patchUi(setUi, {
189+ patchUiWithAuthRevision(setUi, {
184190 openApiBaseUrlOptions: [],
185191 authManuallyEdited: false,
186− authRevision: ui.authRevision + 1,
187192 });
188193 form.setValue("authType", "none", { shouldDirty: false, shouldTouch: false });
189194 form.setValue("authScope", "workspace", { shouldDirty: false, shouldTouch: false });
@@ -194,22 +199,22 @@ export function useAddSourceFormState({ · 4 unmodified lines
194199 form.setValue("basicPassword", "", { shouldDirty: false, shouldTouch: false });
195200 };
196201
197− const bumpAuthRevision = () => patchUi(setUi, { authRevision: ui.authRevision + 1 });
202+ const bumpAuthRevision = () => patchUiWithAuthRevision(setUi);
198203
199204 const handleAuthTypeChange = (authType: Exclude<SourceAuthType, "mixed">) => {
200205 form.setValue("authType", authType, { shouldDirty: true, shouldTouch: true });
201− patchUi(setUi, { authManuallyEdited: true, authRevision: ui.authRevision + 1 });
206+ patchUiWithAuthRevision(setUi, { authManuallyEdited: true });
202207 };
203208
204209 const handleAuthScopeChange = (authScope: CredentialScope) => {
205210 form.setValue("authScope", authScope, { shouldDirty: true, shouldTouch: true });
206− patchUi(setUi, { authManuallyEdited: true, authRevision: ui.authRevision + 1 });
211+ patchUiWithAuthRevision(setUi, { authManuallyEdited: true });
207212 };
208213
209214 const handleAuthFieldChange = (field: SourceAuthPanelEditableField, value: string) => {
210215 if (field === "apiKeyHeader") {
211216 form.setValue("apiKeyHeader", value, { shouldDirty: true, shouldTouch: true });
212− patchUi(setUi, { authManuallyEdited: true, authRevision: ui.authRevision + 1 });
217+ patchUiWithAuthRevision(setUi, { authManuallyEdited: true });
213218 return;
214219 }
215220
executor/packages/convex/auth/db_queries.ts+1−13
@@ -1,5 +1,4 @@
1−import type { Doc } from "../_generated/dataModel";
2−import type { AccountId, DbCtx, OrganizationId } from "./types";
1+import type { DbCtx, OrganizationId } from "./types";
32
43 export async function getAccountByWorkosId(ctx: DbCtx, workosUserId: string) {
54 return await ctx.db
@@ -28,14 +27,3 @@ export async function getFirstWorkspaceByOrganizationId(ctx: DbCtx, organization · 22 unmodified lines
2827 .withIndex("by_organization_created", (q) => q.eq("organizationId", organizationId))
2928 .first();
3029 }
31−
32−export async function getWorkspaceMembershipByWorkspaceAndAccount(
33− ctx: DbCtx,
34− workspaceId: Doc<"workspaces">["_id"],
35− accountId: AccountId,
36−) {
37− return await ctx.db
38− .query("workspaceMembers")
39− .withIndex("by_workspace_account", (q) => q.eq("workspaceId", workspaceId).eq("accountId", accountId))
40− .unique();
41−}
executor/packages/convex/auth/identity.ts+1−1
@@ -12,7 +12,7 @@ function deriveFallbackUserLabel(workosUserId: string): string { · 11 unmodified lines
1212 return `User ${workosUserId.slice(-6)}`;
1313 }
1414
15−export function getIdentityString(identity: Record<string, unknown>, keys: string[]): string | undefined {
15+function getIdentityString(identity: Record<string, unknown>, keys: string[]): string | undefined {
1616 for (const key of keys) {
1717 const value = identity[key];
1818 if (typeof value === "string" && value.trim().length > 0) {
executor/packages/convex/database/mappers.ts+1−1
@@ -2,7 +2,7 @@ import type { Doc } from "../_generated/dataModel"; · 1 unmodified line
22 import { asRecord } from "../lib/object";
33 import { DEFAULT_TASK_TIMEOUT_MS } from "../task/constants";
44
5−export function stableStringify(value: unknown): string {
5+function stableStringify(value: unknown): string {
66 if (Array.isArray(value)) {
77 return `[${value.map((entry) => stableStringify(entry)).join(",")}]`;
88 }
executor/packages/convex/task/events.ts+1−1
@@ -2,7 +2,7 @@ import { internal } from "../_generated/api"; · 1 unmodified line
22
33 export type TaskEventName = "task" | "approval";
44
5−export interface TaskEventMutationContext {
5+interface TaskEventMutationContext {
66 runMutation: (
77 mutation: typeof internal.database.createTaskEvent,
88 args: {
executor/packages/convex/task/finish.ts+1−1
@@ -5,7 +5,7 @@ import type { TaskRecord, TaskStatus } from "../../core/src/types"; · 4 unmodified lines
55 type TerminalTaskStatus = Extract<TaskStatus, "completed" | "failed" | "timed_out" | "denied">;
66 type TaskFinishContext = Pick<ActionCtx, "runMutation"> | Pick<MutationCtx, "runMutation">;
77
8−export interface MarkTaskFinishedInput {
8+interface MarkTaskFinishedInput {
99 taskId: string;
1010 status: TerminalTaskStatus;
1111 exitCode?: number;
executor/packages/convex/task/status.ts+1−1
@@ -1,6 +1,6 @@
11 import type { TaskStatus } from "../../core/src/types";
22
3−export type TerminalTaskStatus = Extract<TaskStatus, "completed" | "failed" | "timed_out" | "denied">;
3+type TerminalTaskStatus = Extract<TaskStatus, "completed" | "failed" | "timed_out" | "denied">;
44
55 export function isTerminalTaskStatus(status: TaskStatus): status is TerminalTaskStatus {
66 return status === "completed" || status === "failed" || status === "timed_out" || status === "denied";
executor/packages/core/src/declaration-generation.ts+0−195
@@ -1,199 +1,4 @@
11 import type { ToolDescriptor } from "./types";
2−import {
3− getSourceFileParseDiagnostics,
4− getTypeScriptModule,
5− indentBlock,
6− propertyNameText,
7−} from "./typechecker-ts-utils";
8−import { OPENAPI_HELPER_TYPES } from "./openapi-helper-types";
9−
10−function isValidTypeExpression(typeExpression: string): boolean {
11− const ts = getTypeScriptModule();
12− if (!ts) {
13− return !/[\r\n`]/.test(typeExpression);
14− }
15−
16− return (() => {
17− try {
18− const sourceFile = ts.createSourceFile(
19− "_type_expr_check.ts",
20− `type __T = ${typeExpression};`,
21− ts.ScriptTarget.ESNext,
22− true,
23− ts.ScriptKind.TS,
24− );
25− return getSourceFileParseDiagnostics(sourceFile).length === 0;
26− } catch {
27− return false;
28− }
29− })();
30−}
31−
32−function safeTypeExpression(raw: string | undefined, fallback: string): string {
33− const typeExpression = raw?.trim();
34− if (!typeExpression) return fallback;
35− return isValidTypeExpression(typeExpression) ? typeExpression : fallback;
36−}
37−
38−/**
39− * Build a minimal OpenAPI .d.ts containing only selected `operations` members.
40− * Returns null when slicing is not possible and callers should use full .d.ts.
41− */
42−export function sliceOpenApiOperationsDts(
43− dts: string,
44− operationIds: Iterable<string>,
45−): string | null {
46− const ts = getTypeScriptModule();
47− if (!ts) return null;
48−
49− const wanted = new Set([...operationIds].filter((value) => value.trim().length > 0));
50− if (wanted.size === 0) return null;
51−
52− const sourceFile = ts.createSourceFile(
53− "openapi-source.d.ts",
54− dts,
55− ts.ScriptTarget.ESNext,
56− true,
57− ts.ScriptKind.TS,
58− );
59−
60− const operationsInterface = sourceFile.statements.find((statement) =>
61− ts.isInterfaceDeclaration(statement)
62− && statement.name.text === "operations",
63− );
64−
65− if (!operationsInterface || !ts.isInterfaceDeclaration(operationsInterface)) {
66− return null;
67− }
68−
69− const selectedMembers: string[] = [];
70− for (const member of operationsInterface.members) {
71− if (!ts.isPropertySignature(member) || !member.name) continue;
72− const key = propertyNameText(member.name, ts);
73− if (!key || !wanted.has(key)) continue;
74−
75− const raw = dts.slice(member.getFullStart(), member.end).trim();
76− if (raw.length > 0) {
77− selectedMembers.push(raw);
78− }
79− }
80−
81− if (selectedMembers.length === 0) return null;
82−
83− const body = selectedMembers.map((member) => indentBlock(member)).join("\n");
84− return `export interface operations {\n${body}\n}`;
85−}
86−
87−function stripExportKeywordsForTypechecker(dts: string): string {
88− return dts.replace(/\bexport\s+/g, "").trim();
89−}
90−
91−export interface GenerateToolDeclarationOptions {
92− sourceDtsBySource?: Record<string, string>;
93−}
94−
95−interface ToolTreeNode {
96− children: Map<string, ToolTreeNode>;
97− tool?: ToolDescriptor;
98−}
99−
100−function createToolTree(tools: ToolDescriptor[]): ToolTreeNode {
101− const root: ToolTreeNode = { children: new Map() };
102−
103− for (const tool of tools) {
104− const segments = tool.path.split(".");
105− let node = root;
106− for (let i = 0; i < segments.length; i += 1) {
107− const segment = segments[i];
108− if (!node.children.has(segment)) {
109− node.children.set(segment, { children: new Map() });
110− }
111− node = node.children.get(segment)!;
112− if (i === segments.length - 1) {
113− node.tool = tool;
114− }
115− }
116− }
117−
118− return root;
119−}
120−
121−function renderToolCallSignature(
122− key: string,
123− tool: ToolDescriptor,
124− dtsSources: Set<string>,
125− pad: string,
126−): string {
127− if (tool.operationId && tool.source && dtsSources.has(tool.source)) {
128− const opKey = JSON.stringify(tool.operationId);
129− const openApiInputType = `ToolInput<operations[${opKey}]>`;
130− const openApiOutputType = `ToolOutput<operations[${opKey}]>`;
131− return `${pad}${key}(input: ${openApiInputType}): Promise<${openApiOutputType}>;`;
132− }
133−
134− const strictArgsType = tool.strictArgsType?.trim();
135− const strictReturnsType = tool.strictReturnsType?.trim();
136− const effectiveArgs = strictArgsType || tool.argsType;
137− const effectiveReturns = strictReturnsType || tool.returnsType;
138− const hasArgsType = Boolean(effectiveArgs?.trim());
139− const args = safeTypeExpression(effectiveArgs, "Record<string, unknown>");
140− const returns = safeTypeExpression(effectiveReturns, "unknown");
141− const inputParam = !hasArgsType || args === "{}"
142− ? `input?: ${args}`
143− : `input: ${args}`;
144−
145− return `${pad}${key}(${inputParam}): Promise<${returns}>;`;
146−}
147−
148−function renderToolTree(
149− node: ToolTreeNode,
150− indent: number,
151− dtsSources: Set<string>,
152−): string {
153− const pad = " ".repeat(indent);
154− const lines: string[] = [];
155−
156− for (const [key, child] of node.children) {
157− if (child.tool) {
158− lines.push(renderToolCallSignature(key, child.tool, dtsSources, pad));
159− continue;
160− }
161−
162− lines.push(`${pad}${key}: {`);
163− lines.push(renderToolTree(child, indent + 1, dtsSources));
164− lines.push(`${pad}};`);
165− }
166−
167− return lines.join("\n");
168−}
169−
170−/**
171− * Build a `declare const tools: { ... }` block from flat tool descriptors.
172− */
173−export function generateToolDeclarations(
174− tools: ToolDescriptor[],
175− options?: GenerateToolDeclarationOptions,
176−): string {
177− const sourceDtsBySource = options?.sourceDtsBySource ?? {};
178− const dtsSources = new Set(Object.keys(sourceDtsBySource));
179− const root = createToolTree(tools);
180−
181− const parts: string[] = [];
182−
183− const dtsEntries = Object.entries(sourceDtsBySource)
184− .filter(([, dts]) => typeof dts === "string" && dts.trim().length > 0)
185− .sort(([a], [b]) => a.localeCompare(b));
186−
187− if (dtsEntries.length > 0) {
188− parts.push(OPENAPI_HELPER_TYPES);
189− for (const [sourceKey, dts] of dtsEntries) {
190− parts.push(`// OpenAPI types from ${sourceKey}\n${stripExportKeywordsForTypechecker(dts)}`);
191− }
192− }
193−
194− parts.push(`declare const tools: {\n${renderToolTree(root, 1, dtsSources)}\n};`);
195− return parts.join("\n");
196−}
1972
1983 /**
1994 * Generate the tool inventory text for the MCP run_code description.
executor/packages/core/src/events.ts+0−43
@@ -4,46 +4,3 @@ export interface LiveTaskEvent { · 3 unmodified lines
44 payload: unknown;
55 createdAt: number;
66 }
7−
8−type Subscriber = (event: LiveTaskEvent) => void;
9−
10−export class TaskEventHub {
11− private readonly subscribers = new Map<string, Set<Subscriber>>();
12−
13− publish(taskId: string, event: LiveTaskEvent): void {
14− const listeners = this.subscribers.get(taskId);
15− if (!listeners || listeners.size === 0) {
16− return;
17− }
18−
19− for (const listener of listeners) {
20− try {
21− listener(event);
22− } catch {
23− listeners.delete(listener);
24− }
25− }
26−
27− if (listeners.size === 0) {
28− this.subscribers.delete(taskId);
29− }
30− }
31−
32− subscribe(taskId: string, listener: Subscriber): () => void {
33− const listeners = this.subscribers.get(taskId) ?? new Set<Subscriber>();
34− listeners.add(listener);
35− this.subscribers.set(taskId, listeners);
36−
37− return () => {
38− const active = this.subscribers.get(taskId);
39− if (!active) {
40− return;
41− }
42−
43− active.delete(listener);
44− if (active.size === 0) {
45− this.subscribers.delete(taskId);
46− }
47− };
48− }
49−}
executor/packages/core/src/runtimes/runtime-catalog.ts+1−1
@@ -22,7 +22,7 @@ function isTruthyEnvValue(value: string | undefined): boolean { · 21 unmodified lines
2222 return TRUTHY_ENV_VALUES.has(value.trim().toLowerCase());
2323 }
2424
25−export function isCloudflareDynamicWorkerOnlyMode(): boolean {
25+function isCloudflareDynamicWorkerOnlyMode(): boolean {
2626 return isTruthyEnvValue(process.env[CLOUDFLARE_DYNAMIC_WORKER_ONLY_ENV_KEY]);
2727 }
2828
executor/packages/core/src/runtimes/transpile.ts+1−1
@@ -6,7 +6,7 @@ import { Result, TaggedError } from "better-result"; · 5 unmodified lines
66 // Errors
77 // ---------------------------------------------------------------------------
88
9−export class TranspileError extends TaggedError("TranspileError")<{
9+class TranspileError extends TaggedError("TranspileError")<{
1010 message: string;
1111 }>() {}
1212
executor/packages/core/src/tool-discovery/indexing.ts+1−1
@@ -55,7 +55,7 @@ function simplifyNamespaceSegment(segment: string): string { · 54 unmodified lines
5555 return collapsed.join("_");
5656 }
5757
58−export function preferredToolPath(path: string): string {
58+function preferredToolPath(path: string): string {
5959 const segments = path.split(".").filter(Boolean);
6060 if (segments.length === 0) return path;
6161
executor/packages/core/src/tool-source-execution.ts+3−3
@@ -13,7 +13,7 @@ export interface GraphqlExecutionEnvelope { · 12 unmodified lines
1313 errors: unknown[];
1414 }
1515
16−export function isMcpReconnectableError(error: unknown): boolean {
16+function isMcpReconnectableError(error: unknown): boolean {
1717 const message = error instanceof Error ? error.message : String(error);
1818 return /(socket|closed|ECONNRESET|fetch failed)/i.test(message);
1919 }
@@ -106,14 +106,14 @@ export async function executeOpenApiRequest( · 86 unmodified lines
106106 return await response.text();
107107 }
108108
109−export function hasGraphqlData(data: unknown): boolean {
109+function hasGraphqlData(data: unknown): boolean {
110110 if (data === null || data === undefined) return false;
111111 if (Array.isArray(data)) return data.length > 0;
112112 if (typeof data === "object") return Object.keys(data as Record<string, unknown>).length > 0;
113113 return true;
114114 }
115115
116−export function normalizeGraphqlEnvelope(result: { data?: unknown; errors?: unknown[] }): GraphqlExecutionEnvelope {
116+function normalizeGraphqlEnvelope(result: { data?: unknown; errors?: unknown[] }): GraphqlExecutionEnvelope {
117117 return {
118118 data: result.data ?? null,
119119 errors: Array.isArray(result.errors) ? result.errors : [],
executor/packages/core/src/tool-source-loaders/openapi-loader.ts+1−1
@@ -21,7 +21,7 @@ import type { SerializedTool } from "../tool-source-serialization"; · 20 unmodified lines
2121 const POSTMAN_SPEC_PREFIX = "postman:";
2222 const DEFAULT_POSTMAN_PROXY_URL = "https://www.postman.com/_api/ws/proxy";
2323
24−export function parsePostmanCollectionUid(spec: string): string | null {
24+function parsePostmanCollectionUid(spec: string): string | null {
2525 if (!spec.startsWith(POSTMAN_SPEC_PREFIX)) {
2626 return null;
2727 }
executor/packages/core/src/tool-sources.ts+0−46
@@ -24,52 +24,6 @@ export { prepareOpenApiSpec } from "./openapi-prepare"; · 23 unmodified lines
2424 export { parseGraphqlOperationPaths } from "./graphql-operation-paths";
2525 export { rehydrateTools, serializeTools, type SerializedTool } from "./tool-source-serialization";
2626 export { buildOpenApiToolsFromPrepared } from "./openapi-tool-builder";
27−export { parsePostmanCollectionUid } from "./tool-source-loaders/openapi-loader";
28−
29−function isRecord(value: unknown): value is Record<string, unknown> {
30− return typeof value === "object" && value !== null;
31−}
32−
33−function isNonEmptyString(value: unknown): value is string {
34− return typeof value === "string" && value.trim().length > 0;
35−}
36−
37−function isExternalToolSourceType(value: unknown): value is ExternalToolSourceConfig["type"] {
38− return value === "mcp" || value === "openapi" || value === "graphql";
39−}
40−
41−export function parseToolSourcesFromEnv(raw: string | undefined): ExternalToolSourceConfig[] {
42− if (!raw || raw.trim().length === 0) {
43− return [];
44− }
45−
46− let parsed: unknown;
47− try {
48− parsed = JSON.parse(raw) as unknown;
49− } catch (error) {
50− const message = error instanceof Error ? error.message : String(error);
51− throw new Error(`EXECUTOR_TOOL_SOURCES must be valid JSON: ${message}`);
52− }
53−
54− if (!Array.isArray(parsed)) {
55− throw new Error("EXECUTOR_TOOL_SOURCES must be a JSON array");
56− }
57−
58− for (let i = 0; i < parsed.length; i += 1) {
59− const entry = parsed[i];
60− if (!isRecord(entry)) {
61− throw new Error(`EXECUTOR_TOOL_SOURCES[${i}] must be an object`);
62− }
63− if (!isExternalToolSourceType(entry.type)) {
64− throw new Error(`EXECUTOR_TOOL_SOURCES[${i}].type must be one of: mcp, openapi, graphql`);
65− }
66− if (!isNonEmptyString(entry.name)) {
67− throw new Error(`EXECUTOR_TOOL_SOURCES[${i}].name must be a non-empty string`);
68− }
69− }
70−
71− return parsed as ExternalToolSourceConfig[];
72−}
7327
7428 export interface CompiledToolSourceArtifact {
7529 version: "v1";
executor/packages/core/src/type-hints.ts+1−1
@@ -188,7 +188,7 @@ export function extractTopLevelTypeKeys(typeHint: string): string[] { · 187 unmodified lines
188188 return keys;
189189 }
190190
191−export function extractTopLevelTypeKeysFromExpression(typeHint: string): string[] {
191+function extractTopLevelTypeKeysFromExpression(typeHint: string): string[] {
192192 const direct = extractTopLevelTypeKeys(typeHint);
193193 if (direct.length > 0) return direct;
194194
executor/packages/core/src/typechecker-ts-utils.ts+0−55
@@ -1,55 +0,0 @@
1−import { Result } from "better-result";
2−
3−export function getTypeScriptModule(): typeof import("typescript") | null {
4− const loaded = Result.try(() => require("typescript") as typeof import("typescript"));
5− return loaded.isOk() ? loaded.value : null;
6−}
7−
8−export function getSourceFileParseDiagnostics(
9− sourceFile: import("typescript").SourceFile,
10−): import("typescript").Diagnostic[] {
11− return (
12− sourceFile as { parseDiagnostics?: import("typescript").Diagnostic[] }
13− ).parseDiagnostics ?? [];
14−}
15−
16−export function unwrapExpression(
17− expression: import("typescript").Expression,
18− ts: typeof import("typescript"),
19−): import("typescript").Expression {
20− let current = expression;
21− while (
22− ts.isParenthesizedExpression(current)
23− || ts.isAsExpression(current)
24− || ts.isTypeAssertionExpression(current)
25− || ts.isNonNullExpression(current)
26− || ts.isPartiallyEmittedExpression(current)
27− || ts.isSatisfiesExpression(current)
28− ) {
29− current = current.expression;
30− }
31− return current;
32−}
33−
34−export function propertyNameText(
35− name: import("typescript").PropertyName,
36− ts: typeof import("typescript"),
37−): string | null {
38− if (ts.isIdentifier(name)) return name.text;
39− if (ts.isStringLiteral(name) || ts.isNoSubstitutionTemplateLiteral(name)) return name.text;
40− if (ts.isNumericLiteral(name)) return name.text;
41− if (ts.isComputedPropertyName(name)) {
42− const expression = unwrapExpression(name.expression, ts);
43− if (ts.isStringLiteral(expression) || ts.isNoSubstitutionTemplateLiteral(expression) || ts.isNumericLiteral(expression)) {
44− return expression.text;
45− }
46− }
47− return null;
48−}
49−
50−export function indentBlock(text: string, indent = " "): string {
51− return text
52− .split("\n")
53− .map((line) => (line.trim().length === 0 ? "" : `${indent}${line}`))
54− .join("\n");
55−}