@dainprotocol/service-sdk 2.0.89 → 2.0.91

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,7 +9,6 @@ const cors_1 = require("hono/cors");
9
9
  const http_exception_1 = require("hono/http-exception");
10
10
  const auth_1 = require("./auth");
11
11
  const schemaStructure_1 = require("../lib/schemaStructure");
12
- const toolDiscovery_1 = require("../lib/toolDiscovery");
13
12
  const oauth2_1 = require("./oauth2");
14
13
  const bs58_1 = tslib_1.__importDefault(require("bs58"));
15
14
  const processes_1 = require("./processes");
@@ -79,30 +78,23 @@ function requireScope(requiredScope) {
79
78
  // Helper to sign and stream SSE events
80
79
  function signedStreamSSE(c, privateKey, config, handler) {
81
80
  return (0, streaming_1.streamSSE)(c, async (stream) => {
82
- const requestSignal = c.req.raw?.signal;
83
- const isAborted = () => stream.aborted || stream.closed || requestSignal?.aborted === true;
84
81
  const signedStream = {
85
82
  writeSSE: async (event) => {
86
- if (isAborted())
87
- return;
88
83
  const timestamp = Date.now().toString();
89
84
  const message = `${event.data}:${timestamp}`;
90
85
  const messageHash = (0, sha256_1.sha256)(message);
91
86
  const signatureBytes = ed25519_1.ed25519.sign(messageHash, privateKey);
92
87
  const signature = (0, utils_1.bytesToHex)(signatureBytes);
93
88
  // Fast path for non-critical events (progress/UI updates)
94
- const isCriticalEvent = event.event === 'result' ||
95
- event.event === 'process-created' ||
96
- event.event === 'datasource-update';
89
+ const isCriticalEvent = event.event === 'result' || event.event === 'process-created';
97
90
  const dataWithSignature = isCriticalEvent
98
91
  ? JSON.stringify({
99
92
  data: JSON.parse(event.data),
100
93
  _signature: { signature, timestamp, agentId: config.identity.agentId, orgId: config.identity.orgId, address: config.identity.publicKey }
101
94
  })
102
95
  : `{"data":${event.data},"_signature":{"signature":"${signature}","timestamp":"${timestamp}","agentId":"${config.identity.agentId}","orgId":"${config.identity.orgId}","address":"${config.identity.publicKey}"}}`;
103
- await stream.writeSSE({ event: event.event, data: dataWithSignature, id: event.id });
104
- },
105
- isAborted,
96
+ return stream.writeSSE({ event: event.event, data: dataWithSignature, id: event.id });
97
+ }
106
98
  };
107
99
  await handler(signedStream);
108
100
  });
@@ -182,7 +174,11 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
182
174
  const isHITLPath = hitlPath && c.req.path === hitlPath;
183
175
  if (c.req.path.startsWith("/oauth2/callback/") ||
184
176
  c.req.path.startsWith("/addons") ||
177
+ c.req.path.startsWith("/getAllToolsAsJsonSchema") ||
178
+ c.req.path.startsWith("/getSkills") ||
179
+ c.req.path.startsWith("/getWebhookTriggers") ||
185
180
  c.req.path.startsWith("/metadata") ||
181
+ c.req.path.startsWith("/recommendations") ||
186
182
  c.req.path.startsWith("/ping") ||
187
183
  isWebhookPath ||
188
184
  isHITLPath) {
@@ -304,73 +300,15 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
304
300
  debugLog("[getAgentInfo] API Key - Returning agent info:", agentInfo);
305
301
  return agentInfo;
306
302
  }
307
- async function resolveHealthStatus() {
308
- if (!config.healthCheck)
309
- return true;
310
- try {
311
- return await config.healthCheck();
312
- }
313
- catch {
314
- return false;
315
- }
316
- }
317
303
  // Setup default ping route
318
- app.get("/ping", async (c) => {
319
- const healthy = await resolveHealthStatus();
320
- return c.json({
321
- message: healthy ? "pong" : "unhealthy",
322
- platform: "DAIN",
323
- service_version: metadata.version,
324
- dain_sdk_version: package_json_1.default.version,
325
- }, healthy ? 200 : 503);
326
- });
304
+ app.get("/ping", (c) => c.json({ message: "pong", platform: "DAIN", service_version: metadata.version, dain_sdk_version: package_json_1.default.version }));
327
305
  // Metadata endpoint - includes computed supportsUserActions from tools
328
306
  app.get("/metadata", (c) => {
329
307
  // Compute service-level capability: does ANY tool support user actions (HITL)?
330
308
  const supportsUserActions = tools.some((tool) => tool.supportsUserActions === true);
331
- const requestedContract = c.req.header("x-butterfly-contract") || c.req.header("X-Butterfly-Contract");
332
- const sdkMajor = Number.parseInt(String(package_json_1.default.version).split(".")[0] || "0", 10);
333
- const contractVersion = Number.isFinite(sdkMajor) && sdkMajor > 0 ? `${sdkMajor}.0.0` : "0.0.0";
334
- const capabilities = {
335
- tools: true,
336
- contexts: true,
337
- widgets: true,
338
- datasources: true,
339
- streamingTools: true,
340
- streamingDatasources: true,
341
- datasourcePolicy: true,
342
- widgetPolicy: true,
343
- toolSafety: true,
344
- };
345
- const compatibility = (() => {
346
- if (!requestedContract)
347
- return undefined;
348
- const requestedMajor = Number.parseInt(String(requestedContract).split(".")[0] || "0", 10);
349
- if (!Number.isFinite(requestedMajor) || requestedMajor <= 0) {
350
- return {
351
- requested: requestedContract,
352
- contractVersion,
353
- ok: false,
354
- reason: "Invalid requested contract version",
355
- };
356
- }
357
- const ok = requestedMajor === sdkMajor;
358
- return ok
359
- ? { requested: requestedContract, contractVersion, ok: true }
360
- : {
361
- requested: requestedContract,
362
- contractVersion,
363
- ok: false,
364
- reason: `Requested major ${requestedMajor} does not match service major ${sdkMajor}`,
365
- };
366
- })();
367
309
  return c.json({
368
310
  ...metadata,
369
311
  supportsUserActions,
370
- dainSdkVersion: package_json_1.default.version,
371
- contractVersion,
372
- capabilities,
373
- ...(compatibility ? { compatibility } : {}),
374
312
  });
375
313
  });
376
314
  // Tools list endpoint
@@ -389,10 +327,6 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
389
327
  outputSchema,
390
328
  interface: tool.interface,
391
329
  suggestConfirmation: tool.suggestConfirmation,
392
- sideEffectClass: tool.sideEffectClass,
393
- supportsParallel: tool.supportsParallel,
394
- idempotencyScope: tool.idempotencyScope,
395
- maxConcurrencyHint: tool.maxConcurrencyHint,
396
330
  supportsUserActions: tool.supportsUserActions,
397
331
  };
398
332
  });
@@ -493,26 +427,14 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
493
427
  oauth2Client,
494
428
  app
495
429
  });
496
- const response = {
430
+ return {
497
431
  id: widget.id,
498
432
  name: widget.name,
499
433
  description: widget.description,
500
434
  icon: widget.icon,
501
435
  size: widget.size || "sm",
502
- refreshIntervalMs: widget.refreshIntervalMs,
503
436
  ...widgetData
504
437
  };
505
- if (!("freshness" in response)) {
506
- response.freshness = {
507
- freshAt: Date.now(),
508
- transport: "poll",
509
- requestedPolicy: {
510
- refreshIntervalMs: widget.refreshIntervalMs,
511
- },
512
- scope: "account",
513
- };
514
- }
515
- return response;
516
438
  }));
517
439
  const validWidgets = widgetsFull.filter(w => w !== null);
518
440
  const processedResponse = await processPluginsForResponse(validWidgets, body, { extraData: { plugins: processedPluginData.plugins } });
@@ -553,19 +475,8 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
553
475
  description: widget.description,
554
476
  icon: widget.icon,
555
477
  size: widget.size || "sm",
556
- refreshIntervalMs: widget.refreshIntervalMs,
557
478
  ...widgetData
558
479
  };
559
- if (!("freshness" in response)) {
560
- response.freshness = {
561
- freshAt: Date.now(),
562
- transport: "poll",
563
- requestedPolicy: {
564
- refreshIntervalMs: widget.refreshIntervalMs,
565
- },
566
- scope: "account",
567
- };
568
- }
569
480
  const processedResponse = await processPluginsForResponse(response, body, { extraData: { plugins: processedPluginData.plugins } });
570
481
  return c.json(processedResponse);
571
482
  });
@@ -595,12 +506,6 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
595
506
  name: datasource.name,
596
507
  description: datasource.description,
597
508
  type: datasource.type,
598
- refreshIntervalMs: datasource.refreshIntervalMs,
599
- maxStalenessMs: datasource.maxStalenessMs,
600
- transport: datasource.transport,
601
- priority: datasource.priority,
602
- scope: datasource.scope,
603
- dataClass: datasource.dataClass,
604
509
  inputSchema: (0, schemaStructure_1.getDetailedSchemaStructure)(datasource.input),
605
510
  };
606
511
  }
@@ -638,32 +543,12 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
638
543
  plugins: pluginsData,
639
544
  oauth2Client
640
545
  });
641
- const requestedPolicy = {
642
- refreshIntervalMs: datasource.refreshIntervalMs,
643
- maxStalenessMs: datasource.maxStalenessMs,
644
- transport: datasource.transport,
645
- priority: datasource.priority,
646
- scope: datasource.scope,
647
- dataClass: datasource.dataClass,
648
- };
649
546
  const response = {
650
547
  id: datasource.id,
651
548
  name: datasource.name,
652
549
  description: datasource.description,
653
550
  type: datasource.type,
654
- refreshIntervalMs: datasource.refreshIntervalMs,
655
- maxStalenessMs: datasource.maxStalenessMs,
656
- transport: datasource.transport,
657
- priority: datasource.priority,
658
- scope: datasource.scope,
659
- dataClass: datasource.dataClass,
660
551
  data,
661
- freshness: {
662
- freshAt: Date.now(),
663
- transport: datasource.transport || "poll",
664
- requestedPolicy,
665
- scope: datasource.scope,
666
- },
667
552
  };
668
553
  const processedResponse = await processPluginsForResponse(response, { plugins: pluginsData }, { extraData: { plugins: pluginsData } });
669
554
  return c.json(processedResponse);
@@ -681,149 +566,6 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
681
566
  throw error;
682
567
  }
683
568
  });
684
- // Stream datasource updates over SSE. This is primarily used for "fresh" UIs
685
- // (positions/orders) where clients want stream-first updates with a poll fallback.
686
- //
687
- // Note: This does not require services to implement a separate streaming backend.
688
- // The server re-runs the datasource handler on an interval and streams results.
689
- app.post("/datasources/:datasourceId/stream", async (c) => {
690
- const datasource = datasources.find((ds) => ds.id === c.req.param("datasourceId"));
691
- if (!datasource) {
692
- throw new http_exception_1.HTTPException(404, { message: "Datasource not found" });
693
- }
694
- const agentInfo = await getAgentInfo(c);
695
- const rawBody = await safeParseBody(c);
696
- const hasWrappedParams = rawBody &&
697
- typeof rawBody === "object" &&
698
- !Array.isArray(rawBody) &&
699
- "params" in rawBody;
700
- const requestedIntervalMsRaw = hasWrappedParams ? rawBody.intervalMs : undefined;
701
- const requestParamsRaw = hasWrappedParams ? rawBody.params : rawBody;
702
- let params = await processPluginsForRequest(requestParamsRaw && typeof requestParamsRaw === "object" ? requestParamsRaw : {}, agentInfo);
703
- const pluginsData = (params.plugins && typeof params.plugins === "object"
704
- ? params.plugins
705
- : {});
706
- delete params.plugins;
707
- let parsedParams;
708
- try {
709
- parsedParams = datasource.input.parse(params);
710
- }
711
- catch (error) {
712
- if (error instanceof zod_1.z.ZodError) {
713
- const missingParams = error.issues
714
- .map((issue) => issue.path.join("."))
715
- .join(", ");
716
- return c.json({
717
- error: `Missing or invalid parameters: ${missingParams}`,
718
- code: "INVALID_PARAMS"
719
- }, 400);
720
- }
721
- throw error;
722
- }
723
- const pluginRequestContext = hasWrappedParams
724
- ? {
725
- params: parsedParams,
726
- intervalMs: requestedIntervalMsRaw,
727
- plugins: pluginsData,
728
- }
729
- : {
730
- ...(typeof parsedParams === "object" && parsedParams !== null
731
- ? parsedParams
732
- : {}),
733
- plugins: pluginsData,
734
- };
735
- const oauth2Client = app.oauth2?.getClient();
736
- const requestedPolicy = {
737
- refreshIntervalMs: datasource.refreshIntervalMs,
738
- maxStalenessMs: datasource.maxStalenessMs,
739
- transport: datasource.transport,
740
- priority: datasource.priority,
741
- scope: datasource.scope,
742
- dataClass: datasource.dataClass,
743
- };
744
- const requestedIntervalMs = typeof requestedIntervalMsRaw === "number" && Number.isFinite(requestedIntervalMsRaw) && requestedIntervalMsRaw > 0
745
- ? requestedIntervalMsRaw
746
- : null;
747
- const baseIntervalMs = requestedIntervalMs ??
748
- (typeof datasource.refreshIntervalMs === "number" && datasource.refreshIntervalMs > 0
749
- ? datasource.refreshIntervalMs
750
- : 15_000);
751
- const intervalMs = Math.max(1_000, Math.floor(baseIntervalMs));
752
- const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
753
- debugLog(`[SSE] Datasource ${datasource.id} stream start (${intervalMs}ms interval)`);
754
- return signedStreamSSE(c, privateKey, config, async (stream) => {
755
- let eventId = 0;
756
- const emitUpdate = async () => {
757
- const data = await datasource.getDatasource(agentInfo, parsedParams, {
758
- plugins: pluginsData,
759
- oauth2Client,
760
- });
761
- const response = {
762
- id: datasource.id,
763
- name: datasource.name,
764
- description: datasource.description,
765
- type: datasource.type,
766
- refreshIntervalMs: datasource.refreshIntervalMs,
767
- maxStalenessMs: datasource.maxStalenessMs,
768
- transport: datasource.transport,
769
- priority: datasource.priority,
770
- scope: datasource.scope,
771
- dataClass: datasource.dataClass,
772
- data,
773
- freshness: {
774
- freshAt: Date.now(),
775
- transport: "stream",
776
- requestedPolicy,
777
- scope: datasource.scope,
778
- },
779
- };
780
- const processedResponse = await processPluginsForResponse(response, pluginRequestContext, { extraData: { plugins: pluginsData } });
781
- await stream.writeSSE({
782
- event: "datasource-update",
783
- data: JSON.stringify(processedResponse),
784
- id: String(eventId++),
785
- });
786
- };
787
- // Send initial snapshot immediately.
788
- try {
789
- await emitUpdate();
790
- }
791
- catch (error) {
792
- console.error(`[SSE] Datasource ${datasource.id} initial update failed:`, error);
793
- if (!stream.isAborted()) {
794
- await stream.writeSSE({
795
- event: "error",
796
- data: JSON.stringify({ message: error?.message || "Datasource stream error" }),
797
- });
798
- }
799
- return;
800
- }
801
- // Continue sending updates until client disconnects.
802
- // Hono exposes the underlying Request via c.req.raw, which includes an AbortSignal.
803
- const signal = c.req.raw?.signal;
804
- while (!stream.isAborted() && !signal?.aborted) {
805
- await sleep(intervalMs);
806
- if (stream.isAborted() || signal?.aborted)
807
- break;
808
- try {
809
- await emitUpdate();
810
- }
811
- catch (error) {
812
- if (stream.isAborted() || signal?.aborted)
813
- break;
814
- console.error(`[SSE] Datasource ${datasource.id} update failed:`, error);
815
- // Keep the stream alive; clients should rely on poll fallback if needed.
816
- if (!stream.isAborted()) {
817
- await stream.writeSSE({
818
- event: "error",
819
- data: JSON.stringify({ message: error?.message || "Datasource stream error" }),
820
- });
821
- }
822
- }
823
- }
824
- debugLog(`[SSE] Datasource ${datasource.id} stream end`);
825
- });
826
- });
827
569
  function mapAgentInfo(agent) {
828
570
  return {
829
571
  id: agent.id,
@@ -900,23 +642,13 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
900
642
  outputSchema,
901
643
  interface: tool.interface,
902
644
  suggestConfirmation: tool.suggestConfirmation,
903
- sideEffectClass: tool.sideEffectClass,
904
- supportsParallel: tool.supportsParallel,
905
- idempotencyScope: tool.idempotencyScope,
906
- maxConcurrencyHint: tool.maxConcurrencyHint,
907
645
  supportsUserActions: tool.supportsUserActions,
908
646
  };
909
647
  }
910
648
  app.get("/getAllToolsAsJsonSchema", (c) => {
911
- const toolInfo = tools.map(mapToolToJsonSchema);
912
- const reccomendedPrompts = toolboxes.map((toolbox) => toolbox.recommendedPrompt);
913
649
  return c.json({
914
- tools: toolInfo,
915
- reccomendedPrompts,
916
- schemaVersion: (0, toolDiscovery_1.computeToolDiscoverySchemaVersion)({
917
- tools: toolInfo,
918
- reccomendedPrompts,
919
- }),
650
+ tools: tools.map(mapToolToJsonSchema),
651
+ reccomendedPrompts: toolboxes.map((toolbox) => toolbox.recommendedPrompt),
920
652
  });
921
653
  });
922
654
  app.post("/getAllToolsAsJsonSchema", async (c) => {
@@ -925,14 +657,9 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
925
657
  const processedPluginData = await processPluginsForRequest(body, agentInfo);
926
658
  const toolInfo = tools.map(mapToolToJsonSchema);
927
659
  debugLog(`[getAllToolsAsJsonSchema POST] Returning ${toolInfo.length} tools`);
928
- const reccomendedPrompts = toolboxes.map((toolbox) => toolbox.recommendedPrompt);
929
660
  const response = {
930
661
  tools: toolInfo,
931
- reccomendedPrompts,
932
- schemaVersion: (0, toolDiscovery_1.computeToolDiscoverySchemaVersion)({
933
- tools: toolInfo,
934
- reccomendedPrompts,
935
- }),
662
+ reccomendedPrompts: toolboxes.map((toolbox) => toolbox.recommendedPrompt),
936
663
  };
937
664
  const processedResponse = await processPluginsForResponse(response, body, { extraData: { plugins: processedPluginData.plugins } });
938
665
  return c.json(processedResponse);
@@ -1119,10 +846,6 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
1119
846
  outputSchema: (0, schemaStructure_1.getDetailedSchemaStructure)(tool.output),
1120
847
  interface: tool.interface,
1121
848
  suggestConfirmation: tool.suggestConfirmation,
1122
- sideEffectClass: tool.sideEffectClass,
1123
- supportsParallel: tool.supportsParallel,
1124
- idempotencyScope: tool.idempotencyScope,
1125
- maxConcurrencyHint: tool.maxConcurrencyHint,
1126
849
  };
1127
850
  return c.json(toolDetails);
1128
851
  }
@@ -1150,11 +873,6 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
1150
873
  pricing: tool.pricing,
1151
874
  inputSchema: (0, schemaStructure_1.getDetailedSchemaStructure)(tool.input),
1152
875
  outputSchema: (0, schemaStructure_1.getDetailedSchemaStructure)(tool.output),
1153
- suggestConfirmation: tool.suggestConfirmation,
1154
- sideEffectClass: tool.sideEffectClass,
1155
- supportsParallel: tool.supportsParallel,
1156
- idempotencyScope: tool.idempotencyScope,
1157
- maxConcurrencyHint: tool.maxConcurrencyHint,
1158
876
  }
1159
877
  : {
1160
878
  id: toolId,
@@ -1180,13 +898,7 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
1180
898
  }
1181
899
  });
1182
900
  // Health check endpoint
1183
- app.get("/health", async (c) => {
1184
- const healthy = await resolveHealthStatus();
1185
- return c.json({
1186
- status: healthy ? "healthy" : "unhealthy",
1187
- timestamp: new Date().toISOString(),
1188
- }, healthy ? 200 : 503);
1189
- });
901
+ app.get("/health", (c) => c.json({ status: "healthy", timestamp: new Date().toISOString() }));
1190
902
  // Setup custom routes if provided
1191
903
  if (config.routes) {
1192
904
  config.routes(app);
@@ -1470,231 +1182,64 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
1470
1182
  });
1471
1183
  // Toolboxes list endpoint
1472
1184
  app.get("/toolboxes", (c) => c.json(toolboxes));
1473
- function normalizeRecommendationText(value) {
1474
- return value.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
1475
- }
1476
- function tokenizeRecommendationQuery(query) {
1477
- const normalized = normalizeRecommendationText(query);
1478
- if (!normalized)
1479
- return [];
1480
- return Array.from(new Set(normalized.split(/\s+/).filter((token) => token.length >= 2)));
1481
- }
1482
- function buildRecommendationIndex() {
1483
- const index = new Map();
1185
+ // Recommendations endpoint - returns cards for AI selection
1186
+ app.get("/recommendations", (c) => {
1187
+ const cards = [];
1484
1188
  for (const tool of tools) {
1485
- if (!tool.recommendations?.length)
1486
- continue;
1487
- for (const card of tool.recommendations) {
1488
- const scopedCardId = `${tool.id}:${card.id}`;
1489
- if (index.has(scopedCardId)) {
1490
- throw new Error(`Duplicate recommendation card ID "${scopedCardId}". Card IDs must be unique per tool.`);
1189
+ if (tool.recommendations?.length) {
1190
+ for (const card of tool.recommendations) {
1191
+ const hasDynamicSchema = !!card.inputSchema;
1192
+ cards.push({
1193
+ id: card.id,
1194
+ tags: card.tags,
1195
+ ui: hasDynamicSchema ? undefined : card.ui,
1196
+ inputSchema: hasDynamicSchema ? (0, schemaStructure_1.zodToJsonSchema)(card.inputSchema) : undefined,
1197
+ toolId: tool.id,
1198
+ toolName: tool.name,
1199
+ });
1491
1200
  }
1492
- const isDynamic = !!card.inputSchema;
1493
- const inputSchema = isDynamic ? (0, schemaStructure_1.zodToJsonSchema)(card.inputSchema) : undefined;
1494
- const searchText = normalizeRecommendationText([tool.id, tool.name, card.id, ...card.tags].join(" "));
1495
- index.set(scopedCardId, {
1496
- cardId: scopedCardId,
1497
- localCardId: card.id,
1498
- toolId: tool.id,
1499
- toolName: tool.name,
1500
- tags: card.tags,
1501
- inputSchema,
1502
- ui: isDynamic ? undefined : card.ui,
1503
- isDynamic,
1504
- card,
1505
- searchText,
1506
- });
1507
1201
  }
1508
1202
  }
1509
- return index;
1510
- }
1511
- function scoreRecommendationCard(card, tokens) {
1512
- if (tokens.length === 0)
1513
- return 0;
1514
- let score = 0;
1515
- const tagSet = new Set(card.tags.map((tag) => normalizeRecommendationText(tag)));
1516
- const scopedId = normalizeRecommendationText(card.cardId);
1517
- const toolName = normalizeRecommendationText(card.toolName);
1518
- for (const token of tokens) {
1519
- if (scopedId === token || card.localCardId.toLowerCase() === token) {
1520
- score += 6;
1521
- continue;
1522
- }
1523
- if (tagSet.has(token)) {
1524
- score += 4;
1525
- }
1526
- if (toolName.includes(token)) {
1527
- score += 3;
1528
- }
1529
- if (card.searchText.includes(token)) {
1530
- score += 1;
1531
- }
1532
- }
1533
- return score;
1534
- }
1535
- const recommendationCardsById = buildRecommendationIndex();
1536
- const recommendationSearchSchema = zod_1.z.object({
1537
- query: zod_1.z.string().optional(),
1538
- limit: zod_1.z.number().int().min(1).max(50).optional().default(12),
1539
- toolIds: zod_1.z.array(zod_1.z.string()).optional(),
1540
- includeStaticUI: zod_1.z.boolean().optional().default(false),
1541
- });
1542
- app.post("/recommendations/search", async (c) => {
1543
- const agentInfo = await getAgentInfo(c);
1544
- const body = await safeParseBody(c);
1545
- const processedPluginData = await processPluginsForRequest(body, agentInfo);
1546
- const requestData = { ...processedPluginData };
1547
- delete requestData.plugins;
1548
- const parsed = recommendationSearchSchema.safeParse(requestData);
1549
- if (!parsed.success) {
1550
- return c.json({
1551
- error: "Invalid recommendation search request",
1552
- details: parsed.error.format(),
1553
- }, 400);
1554
- }
1555
- const { query, limit, toolIds, includeStaticUI, } = parsed.data;
1556
- const toolIdSet = toolIds?.length ? new Set(toolIds) : null;
1557
- const filteredCards = Array.from(recommendationCardsById.values()).filter((card) => !toolIdSet || toolIdSet.has(card.toolId));
1558
- const tokens = tokenizeRecommendationQuery(query ?? "");
1559
- const ranked = filteredCards
1560
- .map((card, index) => ({
1561
- ...card,
1562
- score: scoreRecommendationCard(card, tokens),
1563
- index,
1564
- }))
1565
- .sort((a, b) => {
1566
- if (tokens.length === 0)
1567
- return a.index - b.index;
1568
- if (b.score !== a.score)
1569
- return b.score - a.score;
1570
- if (a.toolName !== b.toolName)
1571
- return a.toolName.localeCompare(b.toolName);
1572
- return a.localCardId.localeCompare(b.localCardId);
1573
- })
1574
- .slice(0, limit);
1575
- const cards = ranked.map((card) => ({
1576
- cardId: card.cardId,
1577
- localCardId: card.localCardId,
1578
- toolId: card.toolId,
1579
- toolName: card.toolName,
1580
- tags: card.tags,
1581
- score: card.score,
1582
- inputSchema: card.inputSchema,
1583
- ui: includeStaticUI && !card.isDynamic ? card.ui : undefined,
1584
- isDynamic: card.isDynamic,
1585
- }));
1586
- const response = {
1587
- cards,
1588
- count: cards.length,
1589
- query,
1590
- };
1591
- const processedResponse = await processPluginsForResponse(response, body, { extraData: { plugins: processedPluginData.plugins } });
1592
- return c.json(processedResponse);
1203
+ return c.json({ cards, count: cards.length });
1593
1204
  });
1594
- const recommendationRenderSchema = zod_1.z.object({
1595
- cards: zod_1.z
1596
- .array(zod_1.z.object({
1597
- cardId: zod_1.z.string().min(1),
1598
- params: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional(),
1599
- context: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional(),
1600
- }))
1601
- .min(1)
1602
- .max(8),
1603
- continueOnError: zod_1.z.boolean().optional().default(true),
1604
- });
1605
- app.post("/recommendations/render", async (c) => {
1606
- const agentInfo = await getAgentInfo(c);
1607
- const body = await safeParseBody(c);
1608
- const processedPluginData = await processPluginsForRequest(body, agentInfo);
1609
- const requestData = { ...processedPluginData };
1610
- delete requestData.plugins;
1611
- const parsed = recommendationRenderSchema.safeParse(requestData);
1612
- if (!parsed.success) {
1613
- return c.json({
1614
- error: "Invalid recommendation render request",
1615
- details: parsed.error.format(),
1616
- }, 400);
1617
- }
1618
- const { cards: requestedCards, continueOnError } = parsed.data;
1619
- const renderedCards = [];
1620
- const renderErrors = [];
1621
- for (const requested of requestedCards) {
1622
- const card = recommendationCardsById.get(requested.cardId);
1623
- if (!card) {
1624
- renderErrors.push({
1625
- cardId: requested.cardId,
1626
- code: "not_found",
1627
- message: `Card not found: ${requested.cardId}`,
1628
- });
1629
- if (!continueOnError)
1630
- break;
1631
- continue;
1632
- }
1633
- if (!card.isDynamic) {
1634
- renderedCards.push({
1635
- cardId: card.cardId,
1636
- localCardId: card.localCardId,
1637
- toolId: card.toolId,
1638
- toolName: card.toolName,
1639
- tags: card.tags,
1640
- ui: card.card.ui,
1641
- });
1642
- continue;
1643
- }
1644
- const paramsInput = requested.params ?? {};
1645
- const parseResult = card.card.inputSchema.safeParse(paramsInput);
1646
- if (!parseResult.success) {
1647
- renderErrors.push({
1648
- cardId: card.cardId,
1649
- code: "invalid_params",
1650
- message: "Invalid parameters",
1651
- details: parseResult.error.format(),
1652
- });
1653
- if (!continueOnError)
1654
- break;
1655
- continue;
1656
- }
1657
- if (typeof card.card.ui !== "function") {
1658
- renderErrors.push({
1659
- cardId: card.cardId,
1660
- code: "render_failed",
1661
- message: `Card "${card.cardId}" is dynamic but has no UI generator function`,
1662
- });
1663
- if (!continueOnError)
1664
- break;
1665
- continue;
1666
- }
1667
- try {
1668
- const ui = await card.card.ui(parseResult.data, requested.context ?? {});
1669
- renderedCards.push({
1670
- cardId: card.cardId,
1671
- localCardId: card.localCardId,
1672
- toolId: card.toolId,
1673
- toolName: card.toolName,
1674
- tags: card.tags,
1675
- ui,
1676
- params: parseResult.data,
1677
- });
1678
- }
1679
- catch (error) {
1680
- renderErrors.push({
1681
- cardId: card.cardId,
1682
- code: "render_failed",
1683
- message: error instanceof Error ? error.message : "Failed to render recommendation card",
1684
- });
1685
- if (!continueOnError)
1686
- break;
1205
+ // Generate recommendation card UI
1206
+ app.post("/recommendations/:id/generate", async (c) => {
1207
+ const cardId = c.req.param("id");
1208
+ const body = await c.req.json().catch(() => ({}));
1209
+ const params = body.params ?? {};
1210
+ const context = body.context ?? {};
1211
+ let foundCard;
1212
+ let foundTool;
1213
+ for (const tool of tools) {
1214
+ const card = tool.recommendations?.find((r) => r.id === cardId);
1215
+ if (card) {
1216
+ foundCard = card;
1217
+ foundTool = tool;
1218
+ break;
1687
1219
  }
1688
1220
  }
1689
- const response = {
1690
- cards: renderedCards,
1691
- errors: renderErrors,
1692
- renderedCount: renderedCards.length,
1693
- requestedCount: requestedCards.length,
1694
- };
1695
- const processedResponse = await processPluginsForResponse(response, body, { extraData: { plugins: processedPluginData.plugins } });
1696
- const statusCode = !continueOnError && renderErrors.length > 0 ? 400 : 200;
1697
- return c.json(processedResponse, statusCode);
1221
+ if (!foundCard) {
1222
+ return c.json({ error: `Card not found: ${cardId}` }, 404);
1223
+ }
1224
+ const baseResponse = { id: cardId, toolId: foundTool?.id, toolName: foundTool?.name };
1225
+ if (!foundCard.inputSchema) {
1226
+ return c.json({ ...baseResponse, ui: foundCard.ui });
1227
+ }
1228
+ const parseResult = foundCard.inputSchema.safeParse(params);
1229
+ if (!parseResult.success) {
1230
+ return c.json({ error: "Invalid parameters", details: parseResult.error.format() }, 400);
1231
+ }
1232
+ if (typeof foundCard.ui !== "function") {
1233
+ return c.json({ error: `Card ${cardId} has inputSchema but ui is not a function` }, 500);
1234
+ }
1235
+ try {
1236
+ const ui = await foundCard.ui(parseResult.data, context);
1237
+ return c.json({ ...baseResponse, ui, params: parseResult.data });
1238
+ }
1239
+ catch (e) {
1240
+ const message = e instanceof Error ? e.message : "Unknown error";
1241
+ return c.json({ error: `Failed to generate UI: ${message}` }, 500);
1242
+ }
1698
1243
  });
1699
1244
  // Process request with plugins
1700
1245
  async function processPluginsForRequest(request, agentInfo) {