@dainprotocol/service-sdk 2.0.85 → 2.0.87

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.
@@ -32,21 +32,6 @@ function debugWarn(...args) {
32
32
  console.warn(...args);
33
33
  }
34
34
  }
35
- function getFirstForwardedValue(value) {
36
- if (!value)
37
- return undefined;
38
- const first = value.split(",")[0]?.trim();
39
- return first || undefined;
40
- }
41
- function normalizeForwardedPrefix(prefix) {
42
- if (!prefix)
43
- return undefined;
44
- const trimmed = prefix.trim();
45
- if (!trimmed || trimmed === "/")
46
- return undefined;
47
- const normalized = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
48
- return normalized.replace(/\/+$/, "");
49
- }
50
35
  /**
51
36
  * Safely parse JSON body from request, returning empty object on failure.
52
37
  */
@@ -78,30 +63,23 @@ function requireScope(requiredScope) {
78
63
  // Helper to sign and stream SSE events
79
64
  function signedStreamSSE(c, privateKey, config, handler) {
80
65
  return (0, streaming_1.streamSSE)(c, async (stream) => {
81
- const requestSignal = c.req.raw?.signal;
82
- const isAborted = () => stream.aborted || stream.closed || requestSignal?.aborted === true;
83
66
  const signedStream = {
84
67
  writeSSE: async (event) => {
85
- if (isAborted())
86
- return;
87
68
  const timestamp = Date.now().toString();
88
69
  const message = `${event.data}:${timestamp}`;
89
70
  const messageHash = (0, sha256_1.sha256)(message);
90
71
  const signatureBytes = ed25519_1.ed25519.sign(messageHash, privateKey);
91
72
  const signature = (0, utils_1.bytesToHex)(signatureBytes);
92
73
  // Fast path for non-critical events (progress/UI updates)
93
- const isCriticalEvent = event.event === 'result' ||
94
- event.event === 'process-created' ||
95
- event.event === 'datasource-update';
74
+ const isCriticalEvent = event.event === 'result' || event.event === 'process-created';
96
75
  const dataWithSignature = isCriticalEvent
97
76
  ? JSON.stringify({
98
77
  data: JSON.parse(event.data),
99
78
  _signature: { signature, timestamp, agentId: config.identity.agentId, orgId: config.identity.orgId, address: config.identity.publicKey }
100
79
  })
101
80
  : `{"data":${event.data},"_signature":{"signature":"${signature}","timestamp":"${timestamp}","agentId":"${config.identity.agentId}","orgId":"${config.identity.orgId}","address":"${config.identity.publicKey}"}}`;
102
- await stream.writeSSE({ event: event.event, data: dataWithSignature, id: event.id });
103
- },
104
- isAborted,
81
+ return stream.writeSSE({ event: event.event, data: dataWithSignature, id: event.id });
82
+ }
105
83
  };
106
84
  await handler(signedStream);
107
85
  });
@@ -185,6 +163,7 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
185
163
  c.req.path.startsWith("/getSkills") ||
186
164
  c.req.path.startsWith("/getWebhookTriggers") ||
187
165
  c.req.path.startsWith("/metadata") ||
166
+ c.req.path.startsWith("/recommendations") ||
188
167
  c.req.path.startsWith("/ping") ||
189
168
  isWebhookPath ||
190
169
  isHITLPath) {
@@ -198,9 +177,8 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
198
177
  debugLog(`[Auth Middleware] JWT present: ${!!jwtToken}, API Key present: ${!!apiKey}`);
199
178
  if (jwtToken) {
200
179
  // JWT Authentication (Users)
201
- // Auto-detect JWT audience from forwarded headers (works with localhost, path-based tunnels, production)
202
- const host = getFirstForwardedValue(c.req.header("x-forwarded-host")) ||
203
- getFirstForwardedValue(c.req.header("host"));
180
+ // Auto-detect JWT audience from Host header (works with localhost, tunnels, production)
181
+ const host = c.req.header("x-forwarded-host") || c.req.header("host");
204
182
  if (!host) {
205
183
  throw new http_exception_1.HTTPException(400, { message: "Unable to determine service URL. Host header is required." });
206
184
  }
@@ -208,33 +186,22 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
208
186
  if (host.includes(' ') || host.includes('\n') || host.includes('\r')) {
209
187
  throw new http_exception_1.HTTPException(400, { message: "Invalid Host header format" });
210
188
  }
211
- const xForwardedProto = getFirstForwardedValue(c.req.header("x-forwarded-proto"));
212
- const protocol = xForwardedProto === "http" || xForwardedProto === "https"
213
- ? xForwardedProto
214
- : (host.includes("localhost") || host.startsWith("127.") ? "http" : "https");
215
- const forwardedPrefix = normalizeForwardedPrefix(getFirstForwardedValue(c.req.header("x-forwarded-prefix")));
216
- const jwtAudiences = [`${protocol}://${host}`];
217
- if (forwardedPrefix) {
218
- jwtAudiences.push(`${protocol}://${host}${forwardedPrefix}`);
219
- }
189
+ const xForwardedProto = c.req.header("x-forwarded-proto");
190
+ const protocol = xForwardedProto || (host.includes("localhost") ? "http" : "https");
191
+ const jwtAudience = `${protocol}://${host}`;
220
192
  const publicKeyOrUrl = config.jwtPublicKey || config.dainIdUrl || "https://id.dain.org";
221
193
  const issuer = config.jwtIssuer || "dainid-oauth";
222
194
  const result = await (0, auth_1.verifyJWT)(jwtToken, publicKeyOrUrl, {
223
195
  issuer: issuer,
224
- audience: jwtAudiences,
196
+ audience: jwtAudience,
225
197
  });
226
198
  if (!result.valid) {
227
199
  throw new http_exception_1.HTTPException(401, { message: `JWT verification failed: ${result.error}` });
228
200
  }
229
201
  // Defense in depth: double-check audience claim
230
- const tokenAudiences = Array.isArray(result.payload?.aud)
231
- ? result.payload.aud
232
- : [result.payload?.aud];
233
- const expectedAudienceSet = new Set(jwtAudiences);
234
- const audienceMatched = tokenAudiences.some((aud) => aud && expectedAudienceSet.has(aud));
235
- if (!audienceMatched) {
202
+ if (result.payload.aud !== jwtAudience) {
236
203
  throw new http_exception_1.HTTPException(403, {
237
- message: `JWT audience mismatch. Expected one of: ${jwtAudiences.join(", ")}, Got: ${tokenAudiences.filter(Boolean).join(", ")}`
204
+ message: `JWT audience mismatch. Expected: ${jwtAudience}, Got: ${result.payload.aud}`
238
205
  });
239
206
  }
240
207
  c.set('authMethod', 'jwt');
@@ -312,49 +279,9 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
312
279
  app.get("/metadata", (c) => {
313
280
  // Compute service-level capability: does ANY tool support user actions (HITL)?
314
281
  const supportsUserActions = tools.some((tool) => tool.supportsUserActions === true);
315
- const requestedContract = c.req.header("x-butterfly-contract") || c.req.header("X-Butterfly-Contract");
316
- const sdkMajor = Number.parseInt(String(package_json_1.default.version).split(".")[0] || "0", 10);
317
- const contractVersion = Number.isFinite(sdkMajor) && sdkMajor > 0 ? `${sdkMajor}.0.0` : "0.0.0";
318
- const capabilities = {
319
- tools: true,
320
- contexts: true,
321
- widgets: true,
322
- datasources: true,
323
- streamingTools: true,
324
- streamingDatasources: true,
325
- datasourcePolicy: true,
326
- widgetPolicy: true,
327
- toolSafety: true,
328
- };
329
- const compatibility = (() => {
330
- if (!requestedContract)
331
- return undefined;
332
- const requestedMajor = Number.parseInt(String(requestedContract).split(".")[0] || "0", 10);
333
- if (!Number.isFinite(requestedMajor) || requestedMajor <= 0) {
334
- return {
335
- requested: requestedContract,
336
- contractVersion,
337
- ok: false,
338
- reason: "Invalid requested contract version",
339
- };
340
- }
341
- const ok = requestedMajor === sdkMajor;
342
- return ok
343
- ? { requested: requestedContract, contractVersion, ok: true }
344
- : {
345
- requested: requestedContract,
346
- contractVersion,
347
- ok: false,
348
- reason: `Requested major ${requestedMajor} does not match service major ${sdkMajor}`,
349
- };
350
- })();
351
282
  return c.json({
352
283
  ...metadata,
353
284
  supportsUserActions,
354
- dainSdkVersion: package_json_1.default.version,
355
- contractVersion,
356
- capabilities,
357
- ...(compatibility ? { compatibility } : {}),
358
285
  });
359
286
  });
360
287
  // Tools list endpoint
@@ -373,10 +300,6 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
373
300
  outputSchema,
374
301
  interface: tool.interface,
375
302
  suggestConfirmation: tool.suggestConfirmation,
376
- sideEffectClass: tool.sideEffectClass,
377
- supportsParallel: tool.supportsParallel,
378
- idempotencyScope: tool.idempotencyScope,
379
- maxConcurrencyHint: tool.maxConcurrencyHint,
380
303
  supportsUserActions: tool.supportsUserActions,
381
304
  };
382
305
  });
@@ -477,26 +400,14 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
477
400
  oauth2Client,
478
401
  app
479
402
  });
480
- const response = {
403
+ return {
481
404
  id: widget.id,
482
405
  name: widget.name,
483
406
  description: widget.description,
484
407
  icon: widget.icon,
485
408
  size: widget.size || "sm",
486
- refreshIntervalMs: widget.refreshIntervalMs,
487
409
  ...widgetData
488
410
  };
489
- if (!("freshness" in response)) {
490
- response.freshness = {
491
- freshAt: Date.now(),
492
- transport: "poll",
493
- requestedPolicy: {
494
- refreshIntervalMs: widget.refreshIntervalMs,
495
- },
496
- scope: "account",
497
- };
498
- }
499
- return response;
500
411
  }));
501
412
  const validWidgets = widgetsFull.filter(w => w !== null);
502
413
  const processedResponse = await processPluginsForResponse(validWidgets, body, { extraData: { plugins: processedPluginData.plugins } });
@@ -537,19 +448,8 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
537
448
  description: widget.description,
538
449
  icon: widget.icon,
539
450
  size: widget.size || "sm",
540
- refreshIntervalMs: widget.refreshIntervalMs,
541
451
  ...widgetData
542
452
  };
543
- if (!("freshness" in response)) {
544
- response.freshness = {
545
- freshAt: Date.now(),
546
- transport: "poll",
547
- requestedPolicy: {
548
- refreshIntervalMs: widget.refreshIntervalMs,
549
- },
550
- scope: "account",
551
- };
552
- }
553
453
  const processedResponse = await processPluginsForResponse(response, body, { extraData: { plugins: processedPluginData.plugins } });
554
454
  return c.json(processedResponse);
555
455
  });
@@ -579,12 +479,6 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
579
479
  name: datasource.name,
580
480
  description: datasource.description,
581
481
  type: datasource.type,
582
- refreshIntervalMs: datasource.refreshIntervalMs,
583
- maxStalenessMs: datasource.maxStalenessMs,
584
- transport: datasource.transport,
585
- priority: datasource.priority,
586
- scope: datasource.scope,
587
- dataClass: datasource.dataClass,
588
482
  inputSchema: (0, schemaStructure_1.getDetailedSchemaStructure)(datasource.input),
589
483
  };
590
484
  }
@@ -622,32 +516,12 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
622
516
  plugins: pluginsData,
623
517
  oauth2Client
624
518
  });
625
- const requestedPolicy = {
626
- refreshIntervalMs: datasource.refreshIntervalMs,
627
- maxStalenessMs: datasource.maxStalenessMs,
628
- transport: datasource.transport,
629
- priority: datasource.priority,
630
- scope: datasource.scope,
631
- dataClass: datasource.dataClass,
632
- };
633
519
  const response = {
634
520
  id: datasource.id,
635
521
  name: datasource.name,
636
522
  description: datasource.description,
637
523
  type: datasource.type,
638
- refreshIntervalMs: datasource.refreshIntervalMs,
639
- maxStalenessMs: datasource.maxStalenessMs,
640
- transport: datasource.transport,
641
- priority: datasource.priority,
642
- scope: datasource.scope,
643
- dataClass: datasource.dataClass,
644
524
  data,
645
- freshness: {
646
- freshAt: Date.now(),
647
- transport: datasource.transport || "poll",
648
- requestedPolicy,
649
- scope: datasource.scope,
650
- },
651
525
  };
652
526
  const processedResponse = await processPluginsForResponse(response, { plugins: pluginsData }, { extraData: { plugins: pluginsData } });
653
527
  return c.json(processedResponse);
@@ -665,149 +539,6 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
665
539
  throw error;
666
540
  }
667
541
  });
668
- // Stream datasource updates over SSE. This is primarily used for "fresh" UIs
669
- // (positions/orders) where clients want stream-first updates with a poll fallback.
670
- //
671
- // Note: This does not require services to implement a separate streaming backend.
672
- // The server re-runs the datasource handler on an interval and streams results.
673
- app.post("/datasources/:datasourceId/stream", async (c) => {
674
- const datasource = datasources.find((ds) => ds.id === c.req.param("datasourceId"));
675
- if (!datasource) {
676
- throw new http_exception_1.HTTPException(404, { message: "Datasource not found" });
677
- }
678
- const agentInfo = await getAgentInfo(c);
679
- const rawBody = await safeParseBody(c);
680
- const hasWrappedParams = rawBody &&
681
- typeof rawBody === "object" &&
682
- !Array.isArray(rawBody) &&
683
- "params" in rawBody;
684
- const requestedIntervalMsRaw = hasWrappedParams ? rawBody.intervalMs : undefined;
685
- const requestParamsRaw = hasWrappedParams ? rawBody.params : rawBody;
686
- let params = await processPluginsForRequest(requestParamsRaw && typeof requestParamsRaw === "object" ? requestParamsRaw : {}, agentInfo);
687
- const pluginsData = (params.plugins && typeof params.plugins === "object"
688
- ? params.plugins
689
- : {});
690
- delete params.plugins;
691
- let parsedParams;
692
- try {
693
- parsedParams = datasource.input.parse(params);
694
- }
695
- catch (error) {
696
- if (error instanceof zod_1.z.ZodError) {
697
- const missingParams = error.issues
698
- .map((issue) => issue.path.join("."))
699
- .join(", ");
700
- return c.json({
701
- error: `Missing or invalid parameters: ${missingParams}`,
702
- code: "INVALID_PARAMS"
703
- }, 400);
704
- }
705
- throw error;
706
- }
707
- const pluginRequestContext = hasWrappedParams
708
- ? {
709
- params: parsedParams,
710
- intervalMs: requestedIntervalMsRaw,
711
- plugins: pluginsData,
712
- }
713
- : {
714
- ...(typeof parsedParams === "object" && parsedParams !== null
715
- ? parsedParams
716
- : {}),
717
- plugins: pluginsData,
718
- };
719
- const oauth2Client = app.oauth2?.getClient();
720
- const requestedPolicy = {
721
- refreshIntervalMs: datasource.refreshIntervalMs,
722
- maxStalenessMs: datasource.maxStalenessMs,
723
- transport: datasource.transport,
724
- priority: datasource.priority,
725
- scope: datasource.scope,
726
- dataClass: datasource.dataClass,
727
- };
728
- const requestedIntervalMs = typeof requestedIntervalMsRaw === "number" && Number.isFinite(requestedIntervalMsRaw) && requestedIntervalMsRaw > 0
729
- ? requestedIntervalMsRaw
730
- : null;
731
- const baseIntervalMs = requestedIntervalMs ??
732
- (typeof datasource.refreshIntervalMs === "number" && datasource.refreshIntervalMs > 0
733
- ? datasource.refreshIntervalMs
734
- : 15_000);
735
- const intervalMs = Math.max(1_000, Math.floor(baseIntervalMs));
736
- const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
737
- debugLog(`[SSE] Datasource ${datasource.id} stream start (${intervalMs}ms interval)`);
738
- return signedStreamSSE(c, privateKey, config, async (stream) => {
739
- let eventId = 0;
740
- const emitUpdate = async () => {
741
- const data = await datasource.getDatasource(agentInfo, parsedParams, {
742
- plugins: pluginsData,
743
- oauth2Client,
744
- });
745
- const response = {
746
- id: datasource.id,
747
- name: datasource.name,
748
- description: datasource.description,
749
- type: datasource.type,
750
- refreshIntervalMs: datasource.refreshIntervalMs,
751
- maxStalenessMs: datasource.maxStalenessMs,
752
- transport: datasource.transport,
753
- priority: datasource.priority,
754
- scope: datasource.scope,
755
- dataClass: datasource.dataClass,
756
- data,
757
- freshness: {
758
- freshAt: Date.now(),
759
- transport: "stream",
760
- requestedPolicy,
761
- scope: datasource.scope,
762
- },
763
- };
764
- const processedResponse = await processPluginsForResponse(response, pluginRequestContext, { extraData: { plugins: pluginsData } });
765
- await stream.writeSSE({
766
- event: "datasource-update",
767
- data: JSON.stringify(processedResponse),
768
- id: String(eventId++),
769
- });
770
- };
771
- // Send initial snapshot immediately.
772
- try {
773
- await emitUpdate();
774
- }
775
- catch (error) {
776
- console.error(`[SSE] Datasource ${datasource.id} initial update failed:`, error);
777
- if (!stream.isAborted()) {
778
- await stream.writeSSE({
779
- event: "error",
780
- data: JSON.stringify({ message: error?.message || "Datasource stream error" }),
781
- });
782
- }
783
- return;
784
- }
785
- // Continue sending updates until client disconnects.
786
- // Hono exposes the underlying Request via c.req.raw, which includes an AbortSignal.
787
- const signal = c.req.raw?.signal;
788
- while (!stream.isAborted() && !signal?.aborted) {
789
- await sleep(intervalMs);
790
- if (stream.isAborted() || signal?.aborted)
791
- break;
792
- try {
793
- await emitUpdate();
794
- }
795
- catch (error) {
796
- if (stream.isAborted() || signal?.aborted)
797
- break;
798
- console.error(`[SSE] Datasource ${datasource.id} update failed:`, error);
799
- // Keep the stream alive; clients should rely on poll fallback if needed.
800
- if (!stream.isAborted()) {
801
- await stream.writeSSE({
802
- event: "error",
803
- data: JSON.stringify({ message: error?.message || "Datasource stream error" }),
804
- });
805
- }
806
- }
807
- }
808
- debugLog(`[SSE] Datasource ${datasource.id} stream end`);
809
- });
810
- });
811
542
  function mapAgentInfo(agent) {
812
543
  return {
813
544
  id: agent.id,
@@ -884,10 +615,6 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
884
615
  outputSchema,
885
616
  interface: tool.interface,
886
617
  suggestConfirmation: tool.suggestConfirmation,
887
- sideEffectClass: tool.sideEffectClass,
888
- supportsParallel: tool.supportsParallel,
889
- idempotencyScope: tool.idempotencyScope,
890
- maxConcurrencyHint: tool.maxConcurrencyHint,
891
618
  supportsUserActions: tool.supportsUserActions,
892
619
  };
893
620
  }
@@ -1092,10 +819,6 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
1092
819
  outputSchema: (0, schemaStructure_1.getDetailedSchemaStructure)(tool.output),
1093
820
  interface: tool.interface,
1094
821
  suggestConfirmation: tool.suggestConfirmation,
1095
- sideEffectClass: tool.sideEffectClass,
1096
- supportsParallel: tool.supportsParallel,
1097
- idempotencyScope: tool.idempotencyScope,
1098
- maxConcurrencyHint: tool.maxConcurrencyHint,
1099
822
  };
1100
823
  return c.json(toolDetails);
1101
824
  }
@@ -1123,11 +846,6 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
1123
846
  pricing: tool.pricing,
1124
847
  inputSchema: (0, schemaStructure_1.getDetailedSchemaStructure)(tool.input),
1125
848
  outputSchema: (0, schemaStructure_1.getDetailedSchemaStructure)(tool.output),
1126
- suggestConfirmation: tool.suggestConfirmation,
1127
- sideEffectClass: tool.sideEffectClass,
1128
- supportsParallel: tool.supportsParallel,
1129
- idempotencyScope: tool.idempotencyScope,
1130
- maxConcurrencyHint: tool.maxConcurrencyHint,
1131
849
  }
1132
850
  : {
1133
851
  id: toolId,
@@ -1206,11 +924,31 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
1206
924
  }
1207
925
  try {
1208
926
  await app.oauth2.handleCallback(code, state);
1209
- return c.html("<html><body><script>window.opener.postMessage({ type: 'oauth2-success', provider: '" + provider + "' }, '*');window.close();</script><h1>Authentication successful! You can close this window.</h1></body></html>");
927
+ return c.html(`
928
+ <html>
929
+ <body>
930
+ <script>
931
+ window.opener.postMessage({ type: 'oauth2-success', provider: '${provider}' }, '*');
932
+ window.close();
933
+ </script>
934
+ <h1>Authentication successful! You can close this window.</h1>
935
+ </body>
936
+ </html>
937
+ `);
1210
938
  }
1211
939
  catch (error) {
1212
940
  console.error("OAuth callback error:", error);
1213
- return c.html("<html><body><script>window.opener.postMessage({ type: 'oauth2-error', provider: '" + provider + "', error: '" + error.message + "' }, '*');window.close();</script><h1>Authentication failed! You can close this window.</h1></body></html>");
941
+ return c.html(`
942
+ <html>
943
+ <body>
944
+ <script>
945
+ window.opener.postMessage({ type: 'oauth2-error', provider: '${provider}', error: '${error.message}' }, '*');
946
+ window.close();
947
+ </script>
948
+ <h1>Authentication failed! You can close this window.</h1>
949
+ </body>
950
+ </html>
951
+ `);
1214
952
  }
1215
953
  });
1216
954
  // Make oauth2Handler available to tools
@@ -1437,231 +1175,64 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
1437
1175
  });
1438
1176
  // Toolboxes list endpoint
1439
1177
  app.get("/toolboxes", (c) => c.json(toolboxes));
1440
- function normalizeRecommendationText(value) {
1441
- return value.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
1442
- }
1443
- function tokenizeRecommendationQuery(query) {
1444
- const normalized = normalizeRecommendationText(query);
1445
- if (!normalized)
1446
- return [];
1447
- return Array.from(new Set(normalized.split(/\s+/).filter((token) => token.length >= 2)));
1448
- }
1449
- function buildRecommendationIndex() {
1450
- const index = new Map();
1178
+ // Recommendations endpoint - returns cards for AI selection
1179
+ app.get("/recommendations", (c) => {
1180
+ const cards = [];
1451
1181
  for (const tool of tools) {
1452
- if (!tool.recommendations?.length)
1453
- continue;
1454
- for (const card of tool.recommendations) {
1455
- const scopedCardId = `${tool.id}:${card.id}`;
1456
- if (index.has(scopedCardId)) {
1457
- throw new Error(`Duplicate recommendation card ID "${scopedCardId}". Card IDs must be unique per tool.`);
1182
+ if (tool.recommendations?.length) {
1183
+ for (const card of tool.recommendations) {
1184
+ const hasDynamicSchema = !!card.inputSchema;
1185
+ cards.push({
1186
+ id: card.id,
1187
+ tags: card.tags,
1188
+ ui: hasDynamicSchema ? undefined : card.ui,
1189
+ inputSchema: hasDynamicSchema ? (0, schemaStructure_1.zodToJsonSchema)(card.inputSchema) : undefined,
1190
+ toolId: tool.id,
1191
+ toolName: tool.name,
1192
+ });
1458
1193
  }
1459
- const isDynamic = !!card.inputSchema;
1460
- const inputSchema = isDynamic ? (0, schemaStructure_1.zodToJsonSchema)(card.inputSchema) : undefined;
1461
- const searchText = normalizeRecommendationText([tool.id, tool.name, card.id, ...card.tags].join(" "));
1462
- index.set(scopedCardId, {
1463
- cardId: scopedCardId,
1464
- localCardId: card.id,
1465
- toolId: tool.id,
1466
- toolName: tool.name,
1467
- tags: card.tags,
1468
- inputSchema,
1469
- ui: isDynamic ? undefined : card.ui,
1470
- isDynamic,
1471
- card,
1472
- searchText,
1473
- });
1474
- }
1475
- }
1476
- return index;
1477
- }
1478
- function scoreRecommendationCard(card, tokens) {
1479
- if (tokens.length === 0)
1480
- return 0;
1481
- let score = 0;
1482
- const tagSet = new Set(card.tags.map((tag) => normalizeRecommendationText(tag)));
1483
- const scopedId = normalizeRecommendationText(card.cardId);
1484
- const toolName = normalizeRecommendationText(card.toolName);
1485
- for (const token of tokens) {
1486
- if (scopedId === token || card.localCardId.toLowerCase() === token) {
1487
- score += 6;
1488
- continue;
1489
- }
1490
- if (tagSet.has(token)) {
1491
- score += 4;
1492
- }
1493
- if (toolName.includes(token)) {
1494
- score += 3;
1495
- }
1496
- if (card.searchText.includes(token)) {
1497
- score += 1;
1498
1194
  }
1499
1195
  }
1500
- return score;
1501
- }
1502
- const recommendationCardsById = buildRecommendationIndex();
1503
- const recommendationSearchSchema = zod_1.z.object({
1504
- query: zod_1.z.string().optional(),
1505
- limit: zod_1.z.number().int().min(1).max(50).optional().default(12),
1506
- toolIds: zod_1.z.array(zod_1.z.string()).optional(),
1507
- includeStaticUI: zod_1.z.boolean().optional().default(false),
1196
+ return c.json({ cards, count: cards.length });
1508
1197
  });
1509
- app.post("/recommendations/search", async (c) => {
1510
- const agentInfo = await getAgentInfo(c);
1511
- const body = await safeParseBody(c);
1512
- const processedPluginData = await processPluginsForRequest(body, agentInfo);
1513
- const requestData = { ...processedPluginData };
1514
- delete requestData.plugins;
1515
- const parsed = recommendationSearchSchema.safeParse(requestData);
1516
- if (!parsed.success) {
1517
- return c.json({
1518
- error: "Invalid recommendation search request",
1519
- details: parsed.error.format(),
1520
- }, 400);
1521
- }
1522
- const { query, limit, toolIds, includeStaticUI, } = parsed.data;
1523
- const toolIdSet = toolIds?.length ? new Set(toolIds) : null;
1524
- const filteredCards = Array.from(recommendationCardsById.values()).filter((card) => !toolIdSet || toolIdSet.has(card.toolId));
1525
- const tokens = tokenizeRecommendationQuery(query ?? "");
1526
- const ranked = filteredCards
1527
- .map((card, index) => ({
1528
- ...card,
1529
- score: scoreRecommendationCard(card, tokens),
1530
- index,
1531
- }))
1532
- .sort((a, b) => {
1533
- if (tokens.length === 0)
1534
- return a.index - b.index;
1535
- if (b.score !== a.score)
1536
- return b.score - a.score;
1537
- if (a.toolName !== b.toolName)
1538
- return a.toolName.localeCompare(b.toolName);
1539
- return a.localCardId.localeCompare(b.localCardId);
1540
- })
1541
- .slice(0, limit);
1542
- const cards = ranked.map((card) => ({
1543
- cardId: card.cardId,
1544
- localCardId: card.localCardId,
1545
- toolId: card.toolId,
1546
- toolName: card.toolName,
1547
- tags: card.tags,
1548
- score: card.score,
1549
- inputSchema: card.inputSchema,
1550
- ui: includeStaticUI && !card.isDynamic ? card.ui : undefined,
1551
- isDynamic: card.isDynamic,
1552
- }));
1553
- const response = {
1554
- cards,
1555
- count: cards.length,
1556
- query,
1557
- };
1558
- const processedResponse = await processPluginsForResponse(response, body, { extraData: { plugins: processedPluginData.plugins } });
1559
- return c.json(processedResponse);
1560
- });
1561
- const recommendationRenderSchema = zod_1.z.object({
1562
- cards: zod_1.z
1563
- .array(zod_1.z.object({
1564
- cardId: zod_1.z.string().min(1),
1565
- params: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional(),
1566
- context: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional(),
1567
- }))
1568
- .min(1)
1569
- .max(8),
1570
- continueOnError: zod_1.z.boolean().optional().default(true),
1571
- });
1572
- app.post("/recommendations/render", async (c) => {
1573
- const agentInfo = await getAgentInfo(c);
1574
- const body = await safeParseBody(c);
1575
- const processedPluginData = await processPluginsForRequest(body, agentInfo);
1576
- const requestData = { ...processedPluginData };
1577
- delete requestData.plugins;
1578
- const parsed = recommendationRenderSchema.safeParse(requestData);
1579
- if (!parsed.success) {
1580
- return c.json({
1581
- error: "Invalid recommendation render request",
1582
- details: parsed.error.format(),
1583
- }, 400);
1584
- }
1585
- const { cards: requestedCards, continueOnError } = parsed.data;
1586
- const renderedCards = [];
1587
- const renderErrors = [];
1588
- for (const requested of requestedCards) {
1589
- const card = recommendationCardsById.get(requested.cardId);
1590
- if (!card) {
1591
- renderErrors.push({
1592
- cardId: requested.cardId,
1593
- code: "not_found",
1594
- message: `Card not found: ${requested.cardId}`,
1595
- });
1596
- if (!continueOnError)
1597
- break;
1598
- continue;
1599
- }
1600
- if (!card.isDynamic) {
1601
- renderedCards.push({
1602
- cardId: card.cardId,
1603
- localCardId: card.localCardId,
1604
- toolId: card.toolId,
1605
- toolName: card.toolName,
1606
- tags: card.tags,
1607
- ui: card.card.ui,
1608
- });
1609
- continue;
1610
- }
1611
- const paramsInput = requested.params ?? {};
1612
- const parseResult = card.card.inputSchema.safeParse(paramsInput);
1613
- if (!parseResult.success) {
1614
- renderErrors.push({
1615
- cardId: card.cardId,
1616
- code: "invalid_params",
1617
- message: "Invalid parameters",
1618
- details: parseResult.error.format(),
1619
- });
1620
- if (!continueOnError)
1621
- break;
1622
- continue;
1623
- }
1624
- if (typeof card.card.ui !== "function") {
1625
- renderErrors.push({
1626
- cardId: card.cardId,
1627
- code: "render_failed",
1628
- message: `Card "${card.cardId}" is dynamic but has no UI generator function`,
1629
- });
1630
- if (!continueOnError)
1631
- break;
1632
- continue;
1633
- }
1634
- try {
1635
- const ui = await card.card.ui(parseResult.data, requested.context ?? {});
1636
- renderedCards.push({
1637
- cardId: card.cardId,
1638
- localCardId: card.localCardId,
1639
- toolId: card.toolId,
1640
- toolName: card.toolName,
1641
- tags: card.tags,
1642
- ui,
1643
- params: parseResult.data,
1644
- });
1645
- }
1646
- catch (error) {
1647
- renderErrors.push({
1648
- cardId: card.cardId,
1649
- code: "render_failed",
1650
- message: error instanceof Error ? error.message : "Failed to render recommendation card",
1651
- });
1652
- if (!continueOnError)
1653
- break;
1198
+ // Generate recommendation card UI
1199
+ app.post("/recommendations/:id/generate", async (c) => {
1200
+ const cardId = c.req.param("id");
1201
+ const body = await c.req.json().catch(() => ({}));
1202
+ const params = body.params ?? {};
1203
+ const context = body.context ?? {};
1204
+ let foundCard;
1205
+ let foundTool;
1206
+ for (const tool of tools) {
1207
+ const card = tool.recommendations?.find((r) => r.id === cardId);
1208
+ if (card) {
1209
+ foundCard = card;
1210
+ foundTool = tool;
1211
+ break;
1654
1212
  }
1655
1213
  }
1656
- const response = {
1657
- cards: renderedCards,
1658
- errors: renderErrors,
1659
- renderedCount: renderedCards.length,
1660
- requestedCount: requestedCards.length,
1661
- };
1662
- const processedResponse = await processPluginsForResponse(response, body, { extraData: { plugins: processedPluginData.plugins } });
1663
- const statusCode = !continueOnError && renderErrors.length > 0 ? 400 : 200;
1664
- return c.json(processedResponse, statusCode);
1214
+ if (!foundCard) {
1215
+ return c.json({ error: `Card not found: ${cardId}` }, 404);
1216
+ }
1217
+ const baseResponse = { id: cardId, toolId: foundTool?.id, toolName: foundTool?.name };
1218
+ if (!foundCard.inputSchema) {
1219
+ return c.json({ ...baseResponse, ui: foundCard.ui });
1220
+ }
1221
+ const parseResult = foundCard.inputSchema.safeParse(params);
1222
+ if (!parseResult.success) {
1223
+ return c.json({ error: "Invalid parameters", details: parseResult.error.format() }, 400);
1224
+ }
1225
+ if (typeof foundCard.ui !== "function") {
1226
+ return c.json({ error: `Card ${cardId} has inputSchema but ui is not a function` }, 500);
1227
+ }
1228
+ try {
1229
+ const ui = await foundCard.ui(parseResult.data, context);
1230
+ return c.json({ ...baseResponse, ui, params: parseResult.data });
1231
+ }
1232
+ catch (e) {
1233
+ const message = e instanceof Error ? e.message : "Unknown error";
1234
+ return c.json({ error: `Failed to generate UI: ${message}` }, 500);
1235
+ }
1665
1236
  });
1666
1237
  // Process request with plugins
1667
1238
  async function processPluginsForRequest(request, agentInfo) {