Unified · Split
| @@ -101,6 +101,48 @@ export declare const internal: FilterApi< · 100 unmodified lines | ||
| 101 | 101 | >; |
| 102 | 102 | |
| 103 | 103 | export declare const components: { |
| 104 | + actionCache: { | |
| 105 | + crons: { | |
| 106 | + purge: FunctionReference< | |
| 107 | + "mutation", | |
| 108 | + "internal", | |
| 109 | + { expiresAt?: number }, | |
| 110 | + null | |
| 111 | + >; | |
| 112 | + }; | |
| 113 | + lib: { | |
| 114 | + get: FunctionReference< | |
| 115 | + "query", | |
| 116 | + "internal", | |
| 117 | + { args: any; name: string; ttl: number | null }, | |
| 118 | + { kind: "hit"; value: any } | { expiredEntry?: string; kind: "miss" } | |
| 119 | + >; | |
| 120 | + put: FunctionReference< | |
| 121 | + "mutation", | |
| 122 | + "internal", | |
| 123 | + { | |
| 124 | + args: any; | |
| 125 | + expiredEntry?: string; | |
| 126 | + name: string; | |
| 127 | + ttl: number | null; | |
| 128 | + value: any; | |
| 129 | + }, | |
| 130 | + { cacheHit: boolean; deletedExpiredEntry: boolean } | |
| 131 | + >; | |
| 132 | + remove: FunctionReference< | |
| 133 | + "mutation", | |
| 134 | + "internal", | |
| 135 | + { args: any; name: string }, | |
| 136 | + null | |
| 137 | + >; | |
| 138 | + removeAll: FunctionReference< | |
| 139 | + "mutation", | |
| 140 | + "internal", | |
| 141 | + { batchSize?: number; before?: number; name?: string }, | |
| 142 | + null | |
| 143 | + >; | |
| 144 | + }; | |
| 145 | + }; | |
| 104 | 146 | workflow: { |
| 105 | 147 | event: { |
| 106 | 148 | create: FunctionReference< |
| @@ -122,7 +122,7 @@ export const makeConvexControlPlaneService = ( · 121 unmodified lines | ||
| 122 | 122 | listWorkspaceTools: (workspaceId) => |
| 123 | 123 | Effect.tryPromise({ |
| 124 | 124 | try: () => |
| 125 | − ctx.runQuery(api.controlPlane.listWorkspaceTools, { | |
| 125 | + ctx.runAction(api.controlPlane.listWorkspaceTools, { | |
| 126 | 126 | workspaceId, |
| 127 | 127 | }), |
| 128 | 128 | catch: (cause) => toSourceStoreError("controlPlane.listWorkspaceTools", cause), |
| @@ -130,7 +130,7 @@ export const makeConvexControlPlaneService = ( · 1 unmodified line | ||
| 130 | 130 | listSourceTools: (input) => |
| 131 | 131 | Effect.tryPromise({ |
| 132 | 132 | try: () => |
| 133 | − ctx.runQuery(api.controlPlane.listSourceTools, { | |
| 133 | + ctx.runAction(api.controlPlane.listSourceTools, { | |
| 134 | 134 | workspaceId: input.workspaceId, |
| 135 | 135 | sourceId: input.sourceId, |
| 136 | 136 | }), |
| @@ -1,12 +1,18 @@ | ||
| 1 | +import { ActionCache } from "@convex-dev/action-cache"; | |
| 1 | 2 | import { v } from "convex/values"; |
| 2 | −import { internal } from "../_generated/api"; | |
| 3 | +import { components, internal } from "../_generated/api"; | |
| 3 | 4 | |
| 4 | −import { action, internalMutation, internalQuery } from "../_generated/server"; | |
| 5 | +import { action, internalAction, internalMutation, internalQuery } from "../_generated/server"; | |
| 5 | 6 | |
| 6 | 7 | const defaultSearchLimit = 50; |
| 7 | 8 | const maxSearchLimit = 5_000; |
| 8 | 9 | const writeBatchSize = 500; |
| 9 | 10 | const runtimeInternal = internal as any; |
| 11 | +const listWorkspaceNamespacesActionCache = new ActionCache((components as any).actionCache, { | |
| 12 | + action: runtimeInternal.control_plane.tool_registry.listWorkspaceNamespacesUncached, | |
| 13 | + name: "workspace_namespaces_v1", | |
| 14 | + ttl: 15_000, | |
| 15 | +}); | |
| 10 | 16 | |
| 11 | 17 | const stripConvexSystemFields = ( |
| 12 | 18 | value: Record<string, unknown>, |
| @@ -1252,41 +1258,97 @@ export const listWorkspaceToolsByNormalizedPath = internalQuery({ · 1239 unmodified lines | ||
| 1252 | 1258 | }, |
| 1253 | 1259 | }); |
| 1254 | 1260 | |
| 1255 | −export const listWorkspaceNamespaces = internalQuery({ | |
| 1261 | +export const listWorkspaceToolIndexPage = internalQuery({ | |
| 1256 | 1262 | args: { |
| 1257 | 1263 | workspaceId: v.string(), |
| 1258 | − limit: v.optional(v.number()), | |
| 1264 | + cursor: v.optional(v.union(v.string(), v.null())), | |
| 1265 | + pageSize: v.optional(v.number()), | |
| 1259 | 1266 | }, |
| 1260 | 1267 | handler: async (ctx, args) => { |
| 1261 | − const rows = await buildWorkspaceToolRows(ctx, { | |
| 1262 | − workspaceId: args.workspaceId, | |
| 1263 | − includeDisabled: false, | |
| 1264 | − }); | |
| 1268 | + const pageSize = Math.max(1, Math.min(500, Math.floor(args.pageSize ?? 200))); | |
| 1269 | + const page = await ctx.db | |
| 1270 | + .query("workspaceToolIndex") | |
| 1271 | + .withIndex("by_workspaceId", (q) => q.eq("workspaceId", args.workspaceId)) | |
| 1272 | + .paginate({ | |
| 1273 | + cursor: args.cursor ?? null, | |
| 1274 | + numItems: pageSize, | |
| 1275 | + }); | |
| 1265 | 1276 | |
| 1277 | + const rows = page.page | |
| 1278 | + .filter((row) => row.status === "active") | |
| 1279 | + .map((row) => stripConvexSystemFields(row as unknown as Record<string, unknown>)); | |
| 1280 | + | |
| 1281 | + return { | |
| 1282 | + rows, | |
| 1283 | + continueCursor: page.continueCursor, | |
| 1284 | + isDone: page.isDone, | |
| 1285 | + }; | |
| 1286 | + }, | |
| 1287 | +}); | |
| 1288 | + | |
| 1289 | +export const listWorkspaceNamespacesUncached = internalAction({ | |
| 1290 | + args: { | |
| 1291 | + workspaceId: v.string(), | |
| 1292 | + limit: v.optional(v.number()), | |
| 1293 | + }, | |
| 1294 | + handler: async (ctx, args) => { | |
| 1266 | 1295 | const grouped = new Map< |
| 1267 | 1296 | string, |
| 1268 | 1297 | { |
| 1269 | 1298 | sourceName: string; |
| 1270 | 1299 | sourceId: string; |
| 1271 | 1300 | sourceKind: string; |
| 1301 | + toolCount: number; | |
| 1272 | 1302 | paths: Array<string>; |
| 1273 | 1303 | } |
| 1274 | 1304 | >(); |
| 1275 | 1305 | |
| 1276 | − for (const row of rows) { | |
| 1277 | − const namespace = String(row.namespace); | |
| 1278 | − const existing = grouped.get(namespace); | |
| 1279 | − if (existing) { | |
| 1280 | − existing.paths.push(String(row.path)); | |
| 1281 | − continue; | |
| 1306 | + let cursor: string | null = null; | |
| 1307 | + do { | |
| 1308 | + const page: { | |
| 1309 | + rows: Array<Record<string, unknown>>; | |
| 1310 | + continueCursor: string | null; | |
| 1311 | + isDone: boolean; | |
| 1312 | + } = await ctx.runQuery(runtimeInternal.control_plane.tool_registry.listWorkspaceToolIndexPage, { | |
| 1313 | + workspaceId: args.workspaceId, | |
| 1314 | + cursor, | |
| 1315 | + pageSize: 250, | |
| 1316 | + }); | |
| 1317 | + | |
| 1318 | + for (const row of page.rows) { | |
| 1319 | + const namespace = String(row.namespace); | |
| 1320 | + const path = String(row.path); | |
| 1321 | + const existing = grouped.get(namespace); | |
| 1322 | + if (existing) { | |
| 1323 | + existing.toolCount += 1; | |
| 1324 | + | |
| 1325 | + if (existing.paths.length < 3) { | |
| 1326 | + existing.paths.push(path); | |
| 1327 | + existing.paths.sort((left, right) => left.localeCompare(right)); | |
| 1328 | + } else if (path.localeCompare(existing.paths[existing.paths.length - 1] ?? path) < 0) { | |
| 1329 | + existing.paths[existing.paths.length - 1] = path; | |
| 1330 | + existing.paths.sort((left, right) => left.localeCompare(right)); | |
| 1331 | + } | |
| 1332 | + | |
| 1333 | + continue; | |
| 1334 | + } | |
| 1335 | + | |
| 1336 | + grouped.set(namespace, { | |
| 1337 | + sourceName: String(row.sourceName), | |
| 1338 | + sourceId: String(row.sourceId), | |
| 1339 | + sourceKind: String(row.sourceKind), | |
| 1340 | + toolCount: 1, | |
| 1341 | + paths: [path], | |
| 1342 | + }); | |
| 1282 | 1343 | } |
| 1283 | 1344 | |
| 1284 | − grouped.set(namespace, { | |
| 1285 | − sourceName: String(row.sourceName), | |
| 1286 | − sourceId: String(row.sourceId), | |
| 1287 | − sourceKind: String(row.sourceKind), | |
| 1288 | − paths: [String(row.path)], | |
| 1289 | − }); | |
| 1345 | + cursor = page.isDone ? null : (page.continueCursor as string | null); | |
| 1346 | + } while (cursor); | |
| 1347 | + | |
| 1348 | + const limit = normalizeLimit(args.limit); | |
| 1349 | + | |
| 1350 | + if (limit <= 0 || grouped.size === 0) { | |
| 1351 | + return []; | |
| 1290 | 1352 | } |
| 1291 | 1353 | |
| 1292 | 1354 | const namespaces = [...grouped.entries()] |
| @@ -1295,11 +1357,23 @@ export const listWorkspaceNamespaces = internalQuery({ · 2 unmodified lines | ||
| 1295 | 1357 | source: value.sourceName, |
| 1296 | 1358 | sourceId: value.sourceId, |
| 1297 | 1359 | sourceKind: value.sourceKind, |
| 1298 | − toolCount: value.paths.length, | |
| 1299 | − samplePaths: value.paths.sort((left, right) => left.localeCompare(right)).slice(0, 3), | |
| 1360 | + toolCount: value.toolCount, | |
| 1361 | + samplePaths: value.paths, | |
| 1300 | 1362 | })) |
| 1301 | 1363 | .sort((left, right) => left.namespace.localeCompare(right.namespace)); |
| 1302 | 1364 | |
| 1303 | − return namespaces.slice(0, normalizeLimit(args.limit)); | |
| 1365 | + return namespaces.slice(0, limit); | |
| 1304 | 1366 | }, |
| 1305 | 1367 | }); |
| 1368 | + | |
| 1369 | +export const listWorkspaceNamespaces = internalAction({ | |
| 1370 | + args: { | |
| 1371 | + workspaceId: v.string(), | |
| 1372 | + limit: v.optional(v.number()), | |
| 1373 | + }, | |
| 1374 | + handler: async (ctx, args) => | |
| 1375 | + listWorkspaceNamespacesActionCache.fetch(ctx, { | |
| 1376 | + workspaceId: args.workspaceId, | |
| 1377 | + limit: args.limit, | |
| 1378 | + }), | |
| 1379 | +}); | |
| @@ -6,10 +6,12 @@ import { · 5 unmodified lines | ||
| 6 | 6 | } from "@executor-v2/schema"; |
| 7 | 7 | import { v } from "convex/values"; |
| 8 | 8 | import * as Schema from "effect/Schema"; |
| 9 | +import { internal } from "../_generated/api"; | |
| 9 | 10 | |
| 10 | −import { query } from "../_generated/server"; | |
| 11 | +import { action, internalQuery, query } from "../_generated/server"; | |
| 11 | 12 | |
| 12 | 13 | const decodeSource = Schema.decodeUnknownSync(SourceSchema); |
| 14 | +const runtimeInternal = internal as any; | |
| 13 | 15 | |
| 14 | 16 | const stripConvexSystemFields = ( |
| 15 | 17 | value: Record<string, unknown>, |
| @@ -163,7 +165,7 @@ const resolveToolRefHintTableJson = async ( · 147 unmodified lines | ||
| 163 | 165 | |
| 164 | 166 | const row = await ctx.db |
| 165 | 167 | .query("artifactSchemaRefs") |
| 166 | − .withIndex("by_artifactId_refKey", (q) => | |
| 168 | + .withIndex("by_artifactId_refKey", (q: any) => | |
| 167 | 169 | q.eq("artifactId", artifactId).eq("refKey", refKey), |
| 168 | 170 | ) |
| 169 | 171 | .unique(); |
| @@ -215,6 +217,28 @@ const toSummary = ( · 45 unmodified lines | ||
| 215 | 217 | }; |
| 216 | 218 | }; |
| 217 | 219 | |
| 220 | +const summaryFromIndexRow = (row: Record<string, unknown>): SourceToolSummary => { | |
| 221 | + const method = asNullableString(row.method)?.toLowerCase() ?? null; | |
| 222 | + const sourceKind = asNullableString(row.sourceKind); | |
| 223 | + const normalizedSourceKind: Source["kind"] = | |
| 224 | + sourceKind === "graphql" || sourceKind === "mcp" || sourceKind === "internal" | |
| 225 | + ? sourceKind | |
| 226 | + : "openapi"; | |
| 227 | + | |
| 228 | + return { | |
| 229 | + sourceId: (asNullableString(row.sourceId) ?? "") as Source["id"], | |
| 230 | + sourceName: asNullableString(row.sourceName) ?? "", | |
| 231 | + sourceKind: normalizedSourceKind, | |
| 232 | + toolId: asNullableString(row.toolId) ?? "unknown", | |
| 233 | + name: asNullableString(row.name) ?? "Unnamed Tool", | |
| 234 | + description: asNullableString(row.description), | |
| 235 | + method: isMethod(method) ? method : "post", | |
| 236 | + path: asNullableString(row.path) ?? "", | |
| 237 | + operationPath: asNullableString(row.operationPath ?? null), | |
| 238 | + operationHash: asNullableString(row.operationHash) ?? "", | |
| 239 | + }; | |
| 240 | +}; | |
| 241 | + | |
| 218 | 242 | const sortTools = (tools: ReadonlyArray<SourceToolSummary>): Array<SourceToolSummary> => |
| 219 | 243 | [...tools].sort((left, right) => { |
| 220 | 244 | const leftSource = left.sourceName.toLowerCase(); |
| @@ -234,91 +258,97 @@ const sortTools = (tools: ReadonlyArray<SourceToolSummary>): Array<SourceToolSum · 13 unmodified lines | ||
| 234 | 258 | return left.toolId.localeCompare(right.toolId); |
| 235 | 259 | }); |
| 236 | 260 | |
| 237 | −export const listWorkspaceTools = query({ | |
| 261 | +const workspaceToolsPageSize = 250; | |
| 262 | + | |
| 263 | +export const listWorkspaceToolSummaryPage = internalQuery({ | |
| 238 | 264 | args: { |
| 239 | 265 | workspaceId: v.string(), |
| 266 | + sourceId: v.optional(v.string()), | |
| 267 | + cursor: v.optional(v.union(v.string(), v.null())), | |
| 268 | + pageSize: v.optional(v.number()), | |
| 240 | 269 | }, |
| 241 | − handler: async (ctx, args): Promise<Array<SourceToolSummary>> => { | |
| 242 | − const sourceRows = await ctx.db | |
| 243 | − .query("sources") | |
| 244 | − .withIndex("by_workspaceId", (q) => q.eq("workspaceId", args.workspaceId)) | |
| 245 | − .collect(); | |
| 246 | − | |
| 247 | − const sources = sourceRows | |
| 248 | − .map((row) => toSource(row as unknown as Record<string, unknown>)) | |
| 249 | − .filter((source) => source.enabled); | |
| 250 | − const bindings = await ctx.db | |
| 251 | − .query("sourceArtifactBindings") | |
| 252 | − .withIndex("by_workspaceId", (q) => q.eq("workspaceId", args.workspaceId)) | |
| 253 | − .collect(); | |
| 254 | − const bindingBySourceId = new Map<string, string>( | |
| 255 | − bindings.map((row) => [String(row.sourceId), String(row.artifactId)] as const), | |
| 256 | − ); | |
| 257 | − | |
| 258 | − const summaries: Array<SourceToolSummary> = []; | |
| 270 | + handler: async (ctx, args) => { | |
| 271 | + const pageSize = Math.max(1, Math.min(500, Math.floor(args.pageSize ?? workspaceToolsPageSize))); | |
| 272 | + const paginationOptions = { | |
| 273 | + cursor: args.cursor ?? null, | |
| 274 | + numItems: pageSize, | |
| 275 | + }; | |
| 259 | 276 | |
| 260 | − for (const source of sources) { | |
| 261 | − const artifactId = bindingBySourceId.get(source.id); | |
| 262 | − if (!artifactId) { | |
| 263 | − continue; | |
| 264 | − } | |
| 277 | + const page = args.sourceId | |
| 278 | + ? await ctx.db | |
| 279 | + .query("workspaceToolIndex") | |
| 280 | + .withIndex("by_workspaceId_sourceId", (q: any) => | |
| 281 | + q.eq("workspaceId", args.workspaceId).eq("sourceId", args.sourceId), | |
| 282 | + ) | |
| 283 | + .paginate(paginationOptions) | |
| 284 | + : await ctx.db | |
| 285 | + .query("workspaceToolIndex") | |
| 286 | + .withIndex("by_workspaceId", (q: any) => q.eq("workspaceId", args.workspaceId)) | |
| 287 | + .paginate(paginationOptions); | |
| 288 | + | |
| 289 | + const summaries = page.page | |
| 290 | + .filter((row) => row.status === "active") | |
| 291 | + .map((row) => summaryFromIndexRow(stripConvexSystemFields(row as unknown as Record<string, unknown>))); | |
| 265 | 292 | |
| 266 | − const artifactTools = await ctx.db | |
| 267 | − .query("artifactTools") | |
| 268 | − .withIndex("by_artifactId", (q) => q.eq("artifactId", artifactId)) | |
| 269 | − .collect(); | |
| 293 | + return { | |
| 294 | + summaries, | |
| 295 | + continueCursor: page.continueCursor, | |
| 296 | + isDone: page.isDone, | |
| 297 | + }; | |
| 298 | + }, | |
| 299 | +}); | |
| 270 | 300 | |
| 271 | − for (const artifactTool of artifactTools) { | |
| 272 | − const row = stripConvexSystemFields(artifactTool as unknown as Record<string, unknown>); | |
| 273 | − summaries.push(toSummary(source, row, asNullableString(row.metadataJson))); | |
| 274 | − } | |
| 275 | − } | |
| 301 | +export const listWorkspaceTools = action({ | |
| 302 | + args: { | |
| 303 | + workspaceId: v.string(), | |
| 304 | + }, | |
| 305 | + handler: async (ctx, args): Promise<Array<SourceToolSummary>> => { | |
| 306 | + const summaries: Array<SourceToolSummary> = []; | |
| 307 | + let cursor: string | null = null; | |
| 308 | + | |
| 309 | + do { | |
| 310 | + const page: { | |
| 311 | + summaries: Array<SourceToolSummary>; | |
| 312 | + continueCursor: string | null; | |
| 313 | + isDone: boolean; | |
| 314 | + } = await ctx.runQuery(runtimeInternal.control_plane.tools.listWorkspaceToolSummaryPage, { | |
| 315 | + workspaceId: args.workspaceId, | |
| 316 | + cursor, | |
| 317 | + pageSize: workspaceToolsPageSize, | |
| 318 | + }); | |
| 319 | + | |
| 320 | + summaries.push(...page.summaries); | |
| 321 | + cursor = page.isDone ? null : page.continueCursor; | |
| 322 | + } while (cursor); | |
| 276 | 323 | |
| 277 | 324 | return sortTools(summaries); |
| 278 | 325 | }, |
| 279 | 326 | }); |
| 280 | 327 | |
| 281 | −export const listSourceTools = query({ | |
| 328 | +export const listSourceTools = action({ | |
| 282 | 329 | args: { |
| 283 | 330 | workspaceId: v.string(), |
| 284 | 331 | sourceId: v.string(), |
| 285 | 332 | }, |
| 286 | 333 | handler: async (ctx, args): Promise<Array<SourceToolSummary>> => { |
| 287 | − const sourceRow = await ctx.db | |
| 288 | − .query("sources") | |
| 289 | − .withIndex("by_domainId", (q) => q.eq("id", args.sourceId)) | |
| 290 | − .unique(); | |
| 291 | − | |
| 292 | − if (!sourceRow || sourceRow.workspaceId !== args.workspaceId) { | |
| 293 | − return []; | |
| 294 | − } | |
| 295 | − | |
| 296 | − const source = toSource(sourceRow as unknown as Record<string, unknown>); | |
| 297 | − if (!source.enabled) { | |
| 298 | − return []; | |
| 299 | − } | |
| 300 | − | |
| 301 | − const binding = await ctx.db | |
| 302 | − .query("sourceArtifactBindings") | |
| 303 | − .withIndex("by_workspaceId_sourceId", (q) => | |
| 304 | − q.eq("workspaceId", args.workspaceId).eq("sourceId", args.sourceId), | |
| 305 | − ) | |
| 306 | − .unique(); | |
| 307 | − if (!binding) { | |
| 308 | − return []; | |
| 309 | − } | |
| 310 | − | |
| 311 | − const artifactTools = await ctx.db | |
| 312 | − .query("artifactTools") | |
| 313 | − .withIndex("by_artifactId", (q) => q.eq("artifactId", binding.artifactId)) | |
| 314 | − .collect(); | |
| 315 | − | |
| 316 | 334 | const summaries: Array<SourceToolSummary> = []; |
| 317 | − | |
| 318 | − for (const artifactTool of artifactTools) { | |
| 319 | − const row = stripConvexSystemFields(artifactTool as unknown as Record<string, unknown>); | |
| 320 | − summaries.push(toSummary(source, row, asNullableString(row.metadataJson))); | |
| 321 | − } | |
| 335 | + let cursor: string | null = null; | |
| 336 | + | |
| 337 | + do { | |
| 338 | + const page: { | |
| 339 | + summaries: Array<SourceToolSummary>; | |
| 340 | + continueCursor: string | null; | |
| 341 | + isDone: boolean; | |
| 342 | + } = await ctx.runQuery(runtimeInternal.control_plane.tools.listWorkspaceToolSummaryPage, { | |
| 343 | + workspaceId: args.workspaceId, | |
| 344 | + sourceId: args.sourceId, | |
| 345 | + cursor, | |
| 346 | + pageSize: workspaceToolsPageSize, | |
| 347 | + }); | |
| 348 | + | |
| 349 | + summaries.push(...page.summaries); | |
| 350 | + cursor = page.isDone ? null : page.continueCursor; | |
| 351 | + } while (cursor); | |
| 322 | 352 | |
| 323 | 353 | return sortTools(summaries); |
| 324 | 354 | }, |
| @@ -341,44 +371,43 @@ export const getToolDetail = query({ · 16 unmodified lines | ||
| 341 | 371 | } |
| 342 | 372 | |
| 343 | 373 | const source = toSource(sourceRow as unknown as Record<string, unknown>); |
| 344 | − const binding = await ctx.db | |
| 345 | − .query("sourceArtifactBindings") | |
| 346 | − .withIndex("by_workspaceId_sourceId", (q) => | |
| 347 | − q.eq("workspaceId", args.workspaceId).eq("sourceId", args.sourceId), | |
| 374 | + const indexedRows = await ctx.db | |
| 375 | + .query("workspaceToolIndex") | |
| 376 | + .withIndex("by_workspaceId_operationHash", (q: any) => | |
| 377 | + q.eq("workspaceId", args.workspaceId).eq("operationHash", args.operationHash), | |
| 348 | 378 | ) |
| 349 | − .unique(); | |
| 350 | − if (!binding) { | |
| 379 | + .collect(); | |
| 380 | + const indexedRow = indexedRows.find((row) => row.sourceId === args.sourceId) ?? null; | |
| 381 | + if (!indexedRow) { | |
| 351 | 382 | return null; |
| 352 | 383 | } |
| 353 | 384 | |
| 354 | − const artifactRows = await ctx.db | |
| 385 | + const artifactTool = await ctx.db | |
| 355 | 386 | .query("artifactTools") |
| 356 | − .withIndex("by_artifactId", (q) => q.eq("artifactId", binding.artifactId)) | |
| 357 | − .collect(); | |
| 358 | − | |
| 359 | − const artifactTool = artifactRows.find((row) => row.operationHash === args.operationHash); | |
| 387 | + .withIndex("by_artifactId_toolId", (q: any) => | |
| 388 | + q.eq("artifactId", indexedRow.artifactId).eq("toolId", indexedRow.toolId), | |
| 389 | + ) | |
| 390 | + .unique(); | |
| 360 | 391 | if (!artifactTool) { |
| 361 | 392 | return null; |
| 362 | 393 | } |
| 363 | 394 | |
| 364 | 395 | const artifact = await ctx.db |
| 365 | 396 | .query("artifacts") |
| 366 | − .withIndex("by_domainId", (q) => q.eq("id", binding.artifactId)) | |
| 397 | + .withIndex("by_domainId", (q) => q.eq("id", indexedRow.artifactId)) | |
| 367 | 398 | .unique(); |
| 368 | 399 | |
| 369 | 400 | const resolvedRefHintTableJson = source.kind === "openapi" |
| 370 | 401 | ? await resolveToolRefHintTableJson( |
| 371 | 402 | ctx, |
| 372 | − binding.artifactId, | |
| 403 | + indexedRow.artifactId, | |
| 373 | 404 | artifactTool.inputSchemaJson ?? null, |
| 374 | 405 | artifactTool.outputSchemaJson ?? null, |
| 375 | 406 | ) |
| 376 | 407 | : null; |
| 377 | 408 | |
| 378 | − const summary = toSummary( | |
| 379 | − source, | |
| 380 | − stripConvexSystemFields(artifactTool as unknown as Record<string, unknown>), | |
| 381 | − artifactTool.metadataJson ?? null, | |
| 409 | + const summary = summaryFromIndexRow( | |
| 410 | + stripConvexSystemFields(indexedRow as unknown as Record<string, unknown>), | |
| 382 | 411 | ); |
| 383 | 412 | |
| 384 | 413 | return { |
| @@ -1,7 +1,9 @@ | ||
| 1 | +import actionCache from "@convex-dev/action-cache/convex.config"; | |
| 1 | 2 | import workflow from "@convex-dev/workflow/convex.config.js"; |
| 2 | 3 | import { defineApp } from "convex/server"; |
| 3 | 4 | |
| 4 | 5 | const app = defineApp(); |
| 6 | +app.use(actionCache); | |
| 5 | 7 | app.use(workflow); |
| 6 | 8 | |
| 7 | 9 | export default app; |
| @@ -21,6 +21,7 @@ const setup = () => · 20 unmodified lines | ||
| 21 | 21 | "./control_plane/credentials.ts": () => import("./control_plane/credentials"), |
| 22 | 22 | "./control_plane/sources.ts": () => import("./control_plane/sources"), |
| 23 | 23 | "./control_plane/tool_registry.ts": () => import("./control_plane/tool_registry"), |
| 24 | + "./control_plane/tools.ts": () => import("./control_plane/tools"), | |
| 24 | 25 | "./control_plane/openapi_ingest.ts": () => import("./control_plane/openapi_ingest"), |
| 25 | 26 | "./workflow.ts": () => import("./workflow"), |
| 26 | 27 | "./controlPlane.ts": () => import("./controlPlane"), |
| @@ -650,7 +651,7 @@ describe("Convex executor and control-plane", () => { · 623 unmodified lines | ||
| 650 | 651 | ); |
| 651 | 652 | |
| 652 | 653 | const workspaceTools = (yield* Effect.tryPromise(() => |
| 653 | − t.query(api.controlPlane.listWorkspaceTools, { | |
| 654 | + t.action(api.controlPlane.listWorkspaceTools, { | |
| 654 | 655 | workspaceId: "ws_1", |
| 655 | 656 | }), |
| 656 | 657 | )) as Array<{ |
| @@ -661,7 +662,7 @@ describe("Convex executor and control-plane", () => { · 4 unmodified lines | ||
| 661 | 662 | expect(workspaceTools[0]?.sourceId).toBe("src_1"); |
| 662 | 663 | |
| 663 | 664 | const sourceTools = (yield* Effect.tryPromise(() => |
| 664 | − t.query(api.controlPlane.listSourceTools, { | |
| 665 | + t.action(api.controlPlane.listSourceTools, { | |
| 665 | 666 | workspaceId: "ws_1", |
| 666 | 667 | sourceId: "src_1", |
| 667 | 668 | }), |
| @@ -788,7 +789,7 @@ describe("Convex executor and control-plane", () => { · 120 unmodified lines | ||
| 788 | 789 | ); |
| 789 | 790 | |
| 790 | 791 | const graphqlSourceTools = (yield* Effect.tryPromise(() => |
| 791 | − t.query(api.controlPlane.listSourceTools, { | |
| 792 | + t.action(api.controlPlane.listSourceTools, { | |
| 792 | 793 | workspaceId: "ws_1", |
| 793 | 794 | sourceId: "src_graphql_1", |
| 794 | 795 | }), |
| @@ -803,7 +804,7 @@ describe("Convex executor and control-plane", () => { · 8 unmodified lines | ||
| 803 | 804 | expect(graphqlSourceTools[0]?.method).toBe("post"); |
| 804 | 805 | |
| 805 | 806 | const workspaceToolsWithGraphql = (yield* Effect.tryPromise(() => |
| 806 | − t.query(api.controlPlane.listWorkspaceTools, { | |
| 807 | + t.action(api.controlPlane.listWorkspaceTools, { | |
| 807 | 808 | workspaceId: "ws_1", |
| 808 | 809 | }), |
| 809 | 810 | )) as Array<{ |
| @@ -906,7 +907,7 @@ describe("Convex executor and control-plane", () => { · 96 unmodified lines | ||
| 906 | 907 | ); |
| 907 | 908 | |
| 908 | 909 | const mcpSourceTools = (yield* Effect.tryPromise(() => |
| 909 | − t.query(api.controlPlane.listSourceTools, { | |
| 910 | + t.action(api.controlPlane.listSourceTools, { | |
| 910 | 911 | workspaceId: "ws_1", |
| 911 | 912 | sourceId: "src_mcp_1", |
| 912 | 913 | }), |
| @@ -920,7 +921,7 @@ describe("Convex executor and control-plane", () => { · 7 unmodified lines | ||
| 920 | 921 | expect(mcpSourceTools[0]?.path).toContain("deepwiki.mcp.search_docs"); |
| 921 | 922 | |
| 922 | 923 | const workspaceToolsWithMcp = (yield* Effect.tryPromise(() => |
| 923 | − t.query(api.controlPlane.listWorkspaceTools, { | |
| 924 | + t.action(api.controlPlane.listWorkspaceTools, { | |
| 924 | 925 | workspaceId: "ws_1", |
| 925 | 926 | }), |
| 926 | 927 | )) as Array<{ |
| @@ -930,7 +931,7 @@ describe("Convex executor and control-plane", () => { · 3 unmodified lines | ||
| 930 | 931 | expect(workspaceToolsWithMcp.some((tool) => tool.sourceId === "src_mcp_1")).toBe(true); |
| 931 | 932 | |
| 932 | 933 | const sourceToolsFromWrongWorkspace = (yield* Effect.tryPromise(() => |
| 933 | − t.query(api.controlPlane.listSourceTools, { | |
| 934 | + t.action(api.controlPlane.listSourceTools, { | |
| 934 | 935 | workspaceId: "ws_2", |
| 935 | 936 | sourceId: "src_1", |
| 936 | 937 | }), |
| @@ -1168,7 +1169,7 @@ describe("Convex executor and control-plane", () => { · 231 unmodified lines | ||
| 1168 | 1169 | expect(sources[0]?.sourceHash).toBeTypeOf("string"); |
| 1169 | 1170 | |
| 1170 | 1171 | const sourceTools = (yield* Effect.tryPromise(() => |
| 1171 | − t.query(api.controlPlane.listSourceTools, { | |
| 1172 | + t.action(api.controlPlane.listSourceTools, { | |
| 1172 | 1173 | workspaceId: "ws_cred", |
| 1173 | 1174 | sourceId: "src_graphql_cred_1", |
| 1174 | 1175 | }), |
| @@ -1361,7 +1362,7 @@ describe("Convex executor and control-plane", () => { · 186 unmodified lines | ||
| 1361 | 1362 | expect(sources[0]?.sourceHash).toBeTypeOf("string"); |
| 1362 | 1363 | |
| 1363 | 1364 | const sourceTools = (yield* Effect.tryPromise(() => |
| 1364 | − t.query(api.controlPlane.listSourceTools, { | |
| 1365 | + t.action(api.controlPlane.listSourceTools, { | |
| 1365 | 1366 | workspaceId: "ws_mcp_ingest", |
| 1366 | 1367 | sourceId: "src_mcp_ingest_1", |
| 1367 | 1368 | }), |
| @@ -1729,7 +1729,7 @@ export const createConvexSourceToolRegistry = ( · 1728 unmodified lines | ||
| 1729 | 1729 | |
| 1730 | 1730 | const rows = yield* Effect.tryPromise({ |
| 1731 | 1731 | try: async () => |
| 1732 | − (await ctx.runQuery(runtimeInternal.control_plane.tool_registry.listWorkspaceNamespaces, { | |
| 1732 | + (await ctx.runAction(runtimeInternal.control_plane.tool_registry.listWorkspaceNamespaces, { | |
| 1733 | 1733 | workspaceId, |
| 1734 | 1734 | limit, |
| 1735 | 1735 | })) as Array<{ |
| @@ -9,8 +9,8 @@ · 8 unmodified lines | ||
| 9 | 9 | "convex:codegen": "bunx convex codegen" |
| 10 | 10 | }, |
| 11 | 11 | "dependencies": { |
| 12 | + "@convex-dev/action-cache": "^0.3.0", | |
| 12 | 13 | "@convex-dev/workflow": "^0.3.4", |
| 13 | − "@modelcontextprotocol/sdk": "^1.27.1", | |
| 14 | 14 | "@executor-v2/confect": "workspace:*", |
| 15 | 15 | "@executor-v2/domain": "workspace:*", |
| 16 | 16 | "@executor-v2/engine": "workspace:*", |
| @@ -20,6 +20,7 @@ · 3 unmodified lines | ||
| 20 | 20 | "@executor-v2/runtime-cloudflare-worker-loader": "workspace:*", |
| 21 | 21 | "@executor-v2/schema": "workspace:*", |
| 22 | 22 | "@executor-v2/sdk": "workspace:*", |
| 23 | + "@modelcontextprotocol/sdk": "^1.27.1", | |
| 23 | 24 | "convex": "catalog:", |
| 24 | 25 | "effect": "catalog:", |
| 25 | 26 | "jose": "^6.1.0" |
| @@ -84,6 +84,14 @@ const catalogTemplates: ReadonlyArray<CatalogTemplate> = [ · 83 unmodified lines | ||
| 84 | 84 | type: "mcp", |
| 85 | 85 | endpoint: "https://mcp.neon.tech/mcp", |
| 86 | 86 | }, |
| 87 | + { | |
| 88 | + id: "axiom-mcp", | |
| 89 | + name: "Axiom MCP", | |
| 90 | + summary: "Query, analyze, and monitor your Axiom datasets and logs via MCP.", | |
| 91 | + providerName: "Axiom", | |
| 92 | + type: "mcp", | |
| 93 | + endpoint: "https://mcp.axiom.co/mcp", | |
| 94 | + }, | |
| 87 | 95 | { |
| 88 | 96 | id: "neon-openapi", |
| 89 | 97 | name: "Neon API", |
| @@ -170,6 +178,46 @@ const catalogTemplates: ReadonlyArray<CatalogTemplate> = [ · 80 unmodified lines | ||
| 170 | 178 | type: "openapi", |
| 171 | 179 | endpoint: "https://developer.atlassian.com/cloud/jira/platform/swagger-v3.v3.json", |
| 172 | 180 | }, |
| 181 | + { | |
| 182 | + id: "kubernetes-api", | |
| 183 | + name: "Kubernetes API", | |
| 184 | + summary: "Manage pods, deployments, services, and cluster resources.", | |
| 185 | + providerName: "Kubernetes", | |
| 186 | + type: "openapi", | |
| 187 | + endpoint: | |
| 188 | + "https://raw.githubusercontent.com/kubernetes/kubernetes/master/api/openapi-spec/swagger.json", | |
| 189 | + faviconUrl: "https://kubernetes.io", | |
| 190 | + }, | |
| 191 | + { | |
| 192 | + id: "square-api", | |
| 193 | + name: "Square API", | |
| 194 | + summary: "Process payments, manage inventory, customers, and merchant operations.", | |
| 195 | + providerName: "Square", | |
| 196 | + type: "openapi", | |
| 197 | + endpoint: | |
| 198 | + "https://raw.githubusercontent.com/square/connect-api-specification/master/api.json", | |
| 199 | + faviconUrl: "https://squareup.com", | |
| 200 | + }, | |
| 201 | + { | |
| 202 | + id: "twilio-api", | |
| 203 | + name: "Twilio API", | |
| 204 | + summary: "Send SMS, make calls, and manage communication workflows.", | |
| 205 | + providerName: "Twilio", | |
| 206 | + type: "openapi", | |
| 207 | + endpoint: | |
| 208 | + "https://raw.githubusercontent.com/twilio/twilio-oai/main/spec/json/twilio_api_v2010.json", | |
| 209 | + faviconUrl: "https://twilio.com", | |
| 210 | + }, | |
| 211 | + { | |
| 212 | + id: "box-api", | |
| 213 | + name: "Box Platform API", | |
| 214 | + summary: "Manage files, folders, collaborations, and enterprise content workflows.", | |
| 215 | + providerName: "Box", | |
| 216 | + type: "openapi", | |
| 217 | + endpoint: | |
| 218 | + "https://raw.githubusercontent.com/box/box-openapi/main/openapi.json", | |
| 219 | + faviconUrl: "https://box.com", | |
| 220 | + }, | |
| 173 | 221 | ]; |
| 174 | 222 | |
| 175 | 223 | // --------------------------------------------------------------------------- |
| @@ -35,6 +35,7 @@ · 34 unmodified lines | ||
| 35 | 35 | "apps/convex": { |
| 36 | 36 | "name": "@executor-v2/app-convex", |
| 37 | 37 | "dependencies": { |
| 38 | + "@convex-dev/action-cache": "^0.3.0", | |
| 38 | 39 | "@convex-dev/workflow": "^0.3.4", |
| 39 | 40 | "@executor-v2/confect": "workspace:*", |
| 40 | 41 | "@executor-v2/domain": "workspace:*", |
| @@ -375,6 +376,8 @@ · 334 unmodified lines | ||
| 375 | 376 | |
| 376 | 377 | "@cloudflare/workers-types": ["@cloudflare/workers-types@4.20260305.0", "", {}, "sha512-sCgPFnQ03SVpC2OVW8wysONLZW/A8hlp9Mq2ckG/h1oId4kr9NawA6vUiOmOjCWRn2hIohejBYVQ+Vu20rCdKA=="], |
| 377 | 378 | |
| 379 | + "@convex-dev/action-cache": ["@convex-dev/action-cache@0.3.0", "", { "peerDependencies": { "convex": "^1.24.8" } }, "sha512-B3GsOgwPv81whdu1mHaMKBwbIYHhNXsv3Fr5oMjynIOrRy1laGLUo9zRWDKWJgtKXfVnn8X/uxYD9RyWmPIbPQ=="], | |
| 380 | + | |
| 378 | 381 | "@convex-dev/workflow": ["@convex-dev/workflow@0.3.4", "", { "dependencies": { "async-channel": "^0.2.0" }, "peerDependencies": { "@convex-dev/workpool": "^0.3.0", "convex": "^1.24.8", "convex-helpers": "^0.1.99" } }, "sha512-uX9qhEbgkgt+touvgLnyKztussT3m5q4c2XH/iBNQxzk8TlL29F1+UeoCxXA6WibWEpOQLeuFA6/QMVP4tEX1w=="], |
| 379 | 382 | |
| 380 | 383 | "@convex-dev/workpool": ["@convex-dev/workpool@0.3.2", "", { "peerDependencies": { "convex": "^1.31.7", "convex-helpers": "^0.1.94" } }, "sha512-jNPBHLCmZQXaztbBntZgIoM8JhKDmGwaIezZS/zPmX74PzON0Fgysgd9u+NOjJ04Dx2pN8ppBH9ElpsKxCOBpA=="], |
| @@ -53,6 +53,19 @@ export const HARD_CODED_CATALOG_ITEMS: CatalogCollectionItem[] = [ · 52 unmodified lines | ||
| 53 | 53 | rank: 0.2, |
| 54 | 54 | addedAt: "2026-02-20", |
| 55 | 55 | }, |
| 56 | + { | |
| 57 | + id: "axiom-mcp", | |
| 58 | + name: "Axiom MCP", | |
| 59 | + summary: "Query, analyze, and monitor your Axiom datasets and logs via MCP.", | |
| 60 | + specUrl: "https://mcp.axiom.co/mcp", | |
| 61 | + sourceType: "mcp", | |
| 62 | + originUrl: "https://axiom.co", | |
| 63 | + providerName: "Axiom", | |
| 64 | + categories: "observability", | |
| 65 | + version: "latest", | |
| 66 | + rank: 0.25, | |
| 67 | + addedAt: "2026-03-01", | |
| 68 | + }, | |
| 56 | 69 | { |
| 57 | 70 | id: "neon-openapi", |
| 58 | 71 | name: "Neon API", |
| @@ -175,4 +188,52 @@ export const HARD_CODED_CATALOG_ITEMS: CatalogCollectionItem[] = [ · 116 unmodified lines | ||
| 175 | 188 | rank: 8, |
| 176 | 189 | addedAt: "2025-11-15", |
| 177 | 190 | }, |
| 191 | + { | |
| 192 | + id: "kubernetes-api", | |
| 193 | + name: "Kubernetes API", | |
| 194 | + summary: "Manage pods, deployments, services, and cluster resources.", | |
| 195 | + specUrl: "https://raw.githubusercontent.com/kubernetes/kubernetes/master/api/openapi-spec/swagger.json", | |
| 196 | + originUrl: "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/", | |
| 197 | + providerName: "Kubernetes", | |
| 198 | + categories: "infrastructure", | |
| 199 | + version: "latest", | |
| 200 | + rank: 9, | |
| 201 | + addedAt: "2026-03-01", | |
| 202 | + }, | |
| 203 | + { | |
| 204 | + id: "square-api", | |
| 205 | + name: "Square API", | |
| 206 | + summary: "Process payments, manage inventory, customers, and merchant operations.", | |
| 207 | + specUrl: "https://raw.githubusercontent.com/square/connect-api-specification/master/api.json", | |
| 208 | + originUrl: "https://developer.squareup.com/reference/square", | |
| 209 | + providerName: "Square", | |
| 210 | + categories: "payments", | |
| 211 | + version: "latest", | |
| 212 | + rank: 10, | |
| 213 | + addedAt: "2026-03-01", | |
| 214 | + }, | |
| 215 | + { | |
| 216 | + id: "twilio-api", | |
| 217 | + name: "Twilio API", | |
| 218 | + summary: "Send SMS, make calls, and manage communication workflows.", | |
| 219 | + specUrl: "https://raw.githubusercontent.com/twilio/twilio-oai/main/spec/json/twilio_api_v2010.json", | |
| 220 | + originUrl: "https://www.twilio.com/docs/usage/api", | |
| 221 | + providerName: "Twilio", | |
| 222 | + categories: "communications", | |
| 223 | + version: "v2010", | |
| 224 | + rank: 11, | |
| 225 | + addedAt: "2026-03-01", | |
| 226 | + }, | |
| 227 | + { | |
| 228 | + id: "box-api", | |
| 229 | + name: "Box Platform API", | |
| 230 | + summary: "Manage files, folders, collaborations, and enterprise content workflows.", | |
| 231 | + specUrl: "https://raw.githubusercontent.com/box/box-openapi/main/openapi.json", | |
| 232 | + originUrl: "https://developer.box.com/reference/", | |
| 233 | + providerName: "Box", | |
| 234 | + categories: "developer-tools", | |
| 235 | + version: "latest", | |
| 236 | + rank: 12, | |
| 237 | + addedAt: "2026-03-01", | |
| 238 | + }, | |
| 178 | 239 | ]; |