@dainprotocol/service-sdk 2.0.90 → 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");
@@ -33,46 +32,6 @@ function debugWarn(...args) {
33
32
  console.warn(...args);
34
33
  }
35
34
  }
36
- /**
37
- * Extract a user-safe error message. If `err` is a ZodError (or `err.message`
38
- * looks like a raw Zod v4 issue-array JSON `"[{...}]"`), re-summarize as
39
- * `"<path>: <msg>; <path>: <msg>"` so it never lands verbatim in chat text.
40
- *
41
- * Backstop for the tool-output validation leak: the 2026-04-17 whitelist
42
- * regression surfaced `[{"code":"invalid_type","path":["markets"],...}]` in
43
- * user-visible chat because the four SSE/non-SSE error exits below all do
44
- * `text: \`Error: ${error.message}\``. This helper neutralizes that class
45
- * of bug for ANY ZodError thrown anywhere in the tool-exec pipeline, not
46
- * just the one we already fixed at the source in core.ts.
47
- */
48
- function sanitizeErrorMessage(err) {
49
- if (err instanceof zod_1.z.ZodError) {
50
- const summary = err.issues
51
- .map((i) => `${i.path.join(".") || "root"}: ${i.message}`)
52
- .join("; ");
53
- return summary || "Validation failed";
54
- }
55
- const raw = err?.message;
56
- if (typeof raw !== "string" || raw.length === 0)
57
- return "Unknown error";
58
- const trimmed = raw.trimStart();
59
- if (trimmed.startsWith("[{") || trimmed.startsWith("[ {")) {
60
- try {
61
- const parsed = JSON.parse(trimmed);
62
- if (Array.isArray(parsed)) {
63
- const summary = parsed
64
- .map((i) => `${Array.isArray(i.path) ? i.path.join(".") || "root" : "root"}: ${i.message ?? "invalid"}`)
65
- .join("; ");
66
- if (summary)
67
- return summary;
68
- }
69
- }
70
- catch {
71
- /* fall through, return raw message */
72
- }
73
- }
74
- return raw;
75
- }
76
35
  function getFirstForwardedValue(value) {
77
36
  if (!value)
78
37
  return undefined;
@@ -119,30 +78,23 @@ function requireScope(requiredScope) {
119
78
  // Helper to sign and stream SSE events
120
79
  function signedStreamSSE(c, privateKey, config, handler) {
121
80
  return (0, streaming_1.streamSSE)(c, async (stream) => {
122
- const requestSignal = c.req.raw?.signal;
123
- const isAborted = () => stream.aborted || stream.closed || requestSignal?.aborted === true;
124
81
  const signedStream = {
125
82
  writeSSE: async (event) => {
126
- if (isAborted())
127
- return;
128
83
  const timestamp = Date.now().toString();
129
84
  const message = `${event.data}:${timestamp}`;
130
85
  const messageHash = (0, sha256_1.sha256)(message);
131
86
  const signatureBytes = ed25519_1.ed25519.sign(messageHash, privateKey);
132
87
  const signature = (0, utils_1.bytesToHex)(signatureBytes);
133
88
  // Fast path for non-critical events (progress/UI updates)
134
- const isCriticalEvent = event.event === 'result' ||
135
- event.event === 'process-created' ||
136
- event.event === 'datasource-update';
89
+ const isCriticalEvent = event.event === 'result' || event.event === 'process-created';
137
90
  const dataWithSignature = isCriticalEvent
138
91
  ? JSON.stringify({
139
92
  data: JSON.parse(event.data),
140
93
  _signature: { signature, timestamp, agentId: config.identity.agentId, orgId: config.identity.orgId, address: config.identity.publicKey }
141
94
  })
142
95
  : `{"data":${event.data},"_signature":{"signature":"${signature}","timestamp":"${timestamp}","agentId":"${config.identity.agentId}","orgId":"${config.identity.orgId}","address":"${config.identity.publicKey}"}}`;
143
- await stream.writeSSE({ event: event.event, data: dataWithSignature, id: event.id });
144
- },
145
- isAborted,
96
+ return stream.writeSSE({ event: event.event, data: dataWithSignature, id: event.id });
97
+ }
146
98
  };
147
99
  await handler(signedStream);
148
100
  });
@@ -222,7 +174,11 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
222
174
  const isHITLPath = hitlPath && c.req.path === hitlPath;
223
175
  if (c.req.path.startsWith("/oauth2/callback/") ||
224
176
  c.req.path.startsWith("/addons") ||
177
+ c.req.path.startsWith("/getAllToolsAsJsonSchema") ||
178
+ c.req.path.startsWith("/getSkills") ||
179
+ c.req.path.startsWith("/getWebhookTriggers") ||
225
180
  c.req.path.startsWith("/metadata") ||
181
+ c.req.path.startsWith("/recommendations") ||
226
182
  c.req.path.startsWith("/ping") ||
227
183
  isWebhookPath ||
228
184
  isHITLPath) {
@@ -344,73 +300,15 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
344
300
  debugLog("[getAgentInfo] API Key - Returning agent info:", agentInfo);
345
301
  return agentInfo;
346
302
  }
347
- async function resolveHealthStatus() {
348
- if (!config.healthCheck)
349
- return true;
350
- try {
351
- return await config.healthCheck();
352
- }
353
- catch {
354
- return false;
355
- }
356
- }
357
303
  // Setup default ping route
358
- app.get("/ping", async (c) => {
359
- const healthy = await resolveHealthStatus();
360
- return c.json({
361
- message: healthy ? "pong" : "unhealthy",
362
- platform: "DAIN",
363
- service_version: metadata.version,
364
- dain_sdk_version: package_json_1.default.version,
365
- }, healthy ? 200 : 503);
366
- });
304
+ app.get("/ping", (c) => c.json({ message: "pong", platform: "DAIN", service_version: metadata.version, dain_sdk_version: package_json_1.default.version }));
367
305
  // Metadata endpoint - includes computed supportsUserActions from tools
368
306
  app.get("/metadata", (c) => {
369
307
  // Compute service-level capability: does ANY tool support user actions (HITL)?
370
308
  const supportsUserActions = tools.some((tool) => tool.supportsUserActions === true);
371
- const requestedContract = c.req.header("x-butterfly-contract") || c.req.header("X-Butterfly-Contract");
372
- const sdkMajor = Number.parseInt(String(package_json_1.default.version).split(".")[0] || "0", 10);
373
- const contractVersion = Number.isFinite(sdkMajor) && sdkMajor > 0 ? `${sdkMajor}.0.0` : "0.0.0";
374
- const capabilities = {
375
- tools: true,
376
- contexts: true,
377
- widgets: true,
378
- datasources: true,
379
- streamingTools: true,
380
- streamingDatasources: true,
381
- datasourcePolicy: true,
382
- widgetPolicy: true,
383
- toolSafety: true,
384
- };
385
- const compatibility = (() => {
386
- if (!requestedContract)
387
- return undefined;
388
- const requestedMajor = Number.parseInt(String(requestedContract).split(".")[0] || "0", 10);
389
- if (!Number.isFinite(requestedMajor) || requestedMajor <= 0) {
390
- return {
391
- requested: requestedContract,
392
- contractVersion,
393
- ok: false,
394
- reason: "Invalid requested contract version",
395
- };
396
- }
397
- const ok = requestedMajor === sdkMajor;
398
- return ok
399
- ? { requested: requestedContract, contractVersion, ok: true }
400
- : {
401
- requested: requestedContract,
402
- contractVersion,
403
- ok: false,
404
- reason: `Requested major ${requestedMajor} does not match service major ${sdkMajor}`,
405
- };
406
- })();
407
309
  return c.json({
408
310
  ...metadata,
409
311
  supportsUserActions,
410
- dainSdkVersion: package_json_1.default.version,
411
- contractVersion,
412
- capabilities,
413
- ...(compatibility ? { compatibility } : {}),
414
312
  });
415
313
  });
416
314
  // Tools list endpoint
@@ -429,10 +327,6 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
429
327
  outputSchema,
430
328
  interface: tool.interface,
431
329
  suggestConfirmation: tool.suggestConfirmation,
432
- sideEffectClass: tool.sideEffectClass,
433
- supportsParallel: tool.supportsParallel,
434
- idempotencyScope: tool.idempotencyScope,
435
- maxConcurrencyHint: tool.maxConcurrencyHint,
436
330
  supportsUserActions: tool.supportsUserActions,
437
331
  };
438
332
  });
@@ -533,26 +427,14 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
533
427
  oauth2Client,
534
428
  app
535
429
  });
536
- const response = {
430
+ return {
537
431
  id: widget.id,
538
432
  name: widget.name,
539
433
  description: widget.description,
540
434
  icon: widget.icon,
541
435
  size: widget.size || "sm",
542
- refreshIntervalMs: widget.refreshIntervalMs,
543
436
  ...widgetData
544
437
  };
545
- if (!("freshness" in response)) {
546
- response.freshness = {
547
- freshAt: Date.now(),
548
- transport: "poll",
549
- requestedPolicy: {
550
- refreshIntervalMs: widget.refreshIntervalMs,
551
- },
552
- scope: "account",
553
- };
554
- }
555
- return response;
556
438
  }));
557
439
  const validWidgets = widgetsFull.filter(w => w !== null);
558
440
  const processedResponse = await processPluginsForResponse(validWidgets, body, { extraData: { plugins: processedPluginData.plugins } });
@@ -593,19 +475,8 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
593
475
  description: widget.description,
594
476
  icon: widget.icon,
595
477
  size: widget.size || "sm",
596
- refreshIntervalMs: widget.refreshIntervalMs,
597
478
  ...widgetData
598
479
  };
599
- if (!("freshness" in response)) {
600
- response.freshness = {
601
- freshAt: Date.now(),
602
- transport: "poll",
603
- requestedPolicy: {
604
- refreshIntervalMs: widget.refreshIntervalMs,
605
- },
606
- scope: "account",
607
- };
608
- }
609
480
  const processedResponse = await processPluginsForResponse(response, body, { extraData: { plugins: processedPluginData.plugins } });
610
481
  return c.json(processedResponse);
611
482
  });
@@ -635,12 +506,6 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
635
506
  name: datasource.name,
636
507
  description: datasource.description,
637
508
  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
509
  inputSchema: (0, schemaStructure_1.getDetailedSchemaStructure)(datasource.input),
645
510
  };
646
511
  }
@@ -678,32 +543,12 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
678
543
  plugins: pluginsData,
679
544
  oauth2Client
680
545
  });
681
- const requestedPolicy = {
682
- refreshIntervalMs: datasource.refreshIntervalMs,
683
- maxStalenessMs: datasource.maxStalenessMs,
684
- transport: datasource.transport,
685
- priority: datasource.priority,
686
- scope: datasource.scope,
687
- dataClass: datasource.dataClass,
688
- };
689
546
  const response = {
690
547
  id: datasource.id,
691
548
  name: datasource.name,
692
549
  description: datasource.description,
693
550
  type: datasource.type,
694
- refreshIntervalMs: datasource.refreshIntervalMs,
695
- maxStalenessMs: datasource.maxStalenessMs,
696
- transport: datasource.transport,
697
- priority: datasource.priority,
698
- scope: datasource.scope,
699
- dataClass: datasource.dataClass,
700
551
  data,
701
- freshness: {
702
- freshAt: Date.now(),
703
- transport: datasource.transport || "poll",
704
- requestedPolicy,
705
- scope: datasource.scope,
706
- },
707
552
  };
708
553
  const processedResponse = await processPluginsForResponse(response, { plugins: pluginsData }, { extraData: { plugins: pluginsData } });
709
554
  return c.json(processedResponse);
@@ -721,149 +566,6 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
721
566
  throw error;
722
567
  }
723
568
  });
724
- // Stream datasource updates over SSE. This is primarily used for "fresh" UIs
725
- // (positions/orders) where clients want stream-first updates with a poll fallback.
726
- //
727
- // Note: This does not require services to implement a separate streaming backend.
728
- // The server re-runs the datasource handler on an interval and streams results.
729
- app.post("/datasources/:datasourceId/stream", async (c) => {
730
- const datasource = datasources.find((ds) => ds.id === c.req.param("datasourceId"));
731
- if (!datasource) {
732
- throw new http_exception_1.HTTPException(404, { message: "Datasource not found" });
733
- }
734
- const agentInfo = await getAgentInfo(c);
735
- const rawBody = await safeParseBody(c);
736
- const hasWrappedParams = rawBody &&
737
- typeof rawBody === "object" &&
738
- !Array.isArray(rawBody) &&
739
- "params" in rawBody;
740
- const requestedIntervalMsRaw = hasWrappedParams ? rawBody.intervalMs : undefined;
741
- const requestParamsRaw = hasWrappedParams ? rawBody.params : rawBody;
742
- let params = await processPluginsForRequest(requestParamsRaw && typeof requestParamsRaw === "object" ? requestParamsRaw : {}, agentInfo);
743
- const pluginsData = (params.plugins && typeof params.plugins === "object"
744
- ? params.plugins
745
- : {});
746
- delete params.plugins;
747
- let parsedParams;
748
- try {
749
- parsedParams = datasource.input.parse(params);
750
- }
751
- catch (error) {
752
- if (error instanceof zod_1.z.ZodError) {
753
- const missingParams = error.issues
754
- .map((issue) => issue.path.join("."))
755
- .join(", ");
756
- return c.json({
757
- error: `Missing or invalid parameters: ${missingParams}`,
758
- code: "INVALID_PARAMS"
759
- }, 400);
760
- }
761
- throw error;
762
- }
763
- const pluginRequestContext = hasWrappedParams
764
- ? {
765
- params: parsedParams,
766
- intervalMs: requestedIntervalMsRaw,
767
- plugins: pluginsData,
768
- }
769
- : {
770
- ...(typeof parsedParams === "object" && parsedParams !== null
771
- ? parsedParams
772
- : {}),
773
- plugins: pluginsData,
774
- };
775
- const oauth2Client = app.oauth2?.getClient();
776
- const requestedPolicy = {
777
- refreshIntervalMs: datasource.refreshIntervalMs,
778
- maxStalenessMs: datasource.maxStalenessMs,
779
- transport: datasource.transport,
780
- priority: datasource.priority,
781
- scope: datasource.scope,
782
- dataClass: datasource.dataClass,
783
- };
784
- const requestedIntervalMs = typeof requestedIntervalMsRaw === "number" && Number.isFinite(requestedIntervalMsRaw) && requestedIntervalMsRaw > 0
785
- ? requestedIntervalMsRaw
786
- : null;
787
- const baseIntervalMs = requestedIntervalMs ??
788
- (typeof datasource.refreshIntervalMs === "number" && datasource.refreshIntervalMs > 0
789
- ? datasource.refreshIntervalMs
790
- : 15_000);
791
- const intervalMs = Math.max(1_000, Math.floor(baseIntervalMs));
792
- const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
793
- debugLog(`[SSE] Datasource ${datasource.id} stream start (${intervalMs}ms interval)`);
794
- return signedStreamSSE(c, privateKey, config, async (stream) => {
795
- let eventId = 0;
796
- const emitUpdate = async () => {
797
- const data = await datasource.getDatasource(agentInfo, parsedParams, {
798
- plugins: pluginsData,
799
- oauth2Client,
800
- });
801
- const response = {
802
- id: datasource.id,
803
- name: datasource.name,
804
- description: datasource.description,
805
- type: datasource.type,
806
- refreshIntervalMs: datasource.refreshIntervalMs,
807
- maxStalenessMs: datasource.maxStalenessMs,
808
- transport: datasource.transport,
809
- priority: datasource.priority,
810
- scope: datasource.scope,
811
- dataClass: datasource.dataClass,
812
- data,
813
- freshness: {
814
- freshAt: Date.now(),
815
- transport: "stream",
816
- requestedPolicy,
817
- scope: datasource.scope,
818
- },
819
- };
820
- const processedResponse = await processPluginsForResponse(response, pluginRequestContext, { extraData: { plugins: pluginsData } });
821
- await stream.writeSSE({
822
- event: "datasource-update",
823
- data: JSON.stringify(processedResponse),
824
- id: String(eventId++),
825
- });
826
- };
827
- // Send initial snapshot immediately.
828
- try {
829
- await emitUpdate();
830
- }
831
- catch (error) {
832
- console.error(`[SSE] Datasource ${datasource.id} initial update failed:`, error);
833
- if (!stream.isAborted()) {
834
- await stream.writeSSE({
835
- event: "error",
836
- data: JSON.stringify({ message: error?.message || "Datasource stream error" }),
837
- });
838
- }
839
- return;
840
- }
841
- // Continue sending updates until client disconnects.
842
- // Hono exposes the underlying Request via c.req.raw, which includes an AbortSignal.
843
- const signal = c.req.raw?.signal;
844
- while (!stream.isAborted() && !signal?.aborted) {
845
- await sleep(intervalMs);
846
- if (stream.isAborted() || signal?.aborted)
847
- break;
848
- try {
849
- await emitUpdate();
850
- }
851
- catch (error) {
852
- if (stream.isAborted() || signal?.aborted)
853
- break;
854
- console.error(`[SSE] Datasource ${datasource.id} update failed:`, error);
855
- // Keep the stream alive; clients should rely on poll fallback if needed.
856
- if (!stream.isAborted()) {
857
- await stream.writeSSE({
858
- event: "error",
859
- data: JSON.stringify({ message: error?.message || "Datasource stream error" }),
860
- });
861
- }
862
- }
863
- }
864
- debugLog(`[SSE] Datasource ${datasource.id} stream end`);
865
- });
866
- });
867
569
  function mapAgentInfo(agent) {
868
570
  return {
869
571
  id: agent.id,
@@ -940,23 +642,13 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
940
642
  outputSchema,
941
643
  interface: tool.interface,
942
644
  suggestConfirmation: tool.suggestConfirmation,
943
- sideEffectClass: tool.sideEffectClass,
944
- supportsParallel: tool.supportsParallel,
945
- idempotencyScope: tool.idempotencyScope,
946
- maxConcurrencyHint: tool.maxConcurrencyHint,
947
645
  supportsUserActions: tool.supportsUserActions,
948
646
  };
949
647
  }
950
648
  app.get("/getAllToolsAsJsonSchema", (c) => {
951
- const toolInfo = tools.map(mapToolToJsonSchema);
952
- const reccomendedPrompts = toolboxes.map((toolbox) => toolbox.recommendedPrompt);
953
649
  return c.json({
954
- tools: toolInfo,
955
- reccomendedPrompts,
956
- schemaVersion: (0, toolDiscovery_1.computeToolDiscoverySchemaVersion)({
957
- tools: toolInfo,
958
- reccomendedPrompts,
959
- }),
650
+ tools: tools.map(mapToolToJsonSchema),
651
+ reccomendedPrompts: toolboxes.map((toolbox) => toolbox.recommendedPrompt),
960
652
  });
961
653
  });
962
654
  app.post("/getAllToolsAsJsonSchema", async (c) => {
@@ -965,14 +657,9 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
965
657
  const processedPluginData = await processPluginsForRequest(body, agentInfo);
966
658
  const toolInfo = tools.map(mapToolToJsonSchema);
967
659
  debugLog(`[getAllToolsAsJsonSchema POST] Returning ${toolInfo.length} tools`);
968
- const reccomendedPrompts = toolboxes.map((toolbox) => toolbox.recommendedPrompt);
969
660
  const response = {
970
661
  tools: toolInfo,
971
- reccomendedPrompts,
972
- schemaVersion: (0, toolDiscovery_1.computeToolDiscoverySchemaVersion)({
973
- tools: toolInfo,
974
- reccomendedPrompts,
975
- }),
662
+ reccomendedPrompts: toolboxes.map((toolbox) => toolbox.recommendedPrompt),
976
663
  };
977
664
  const processedResponse = await processPluginsForResponse(response, body, { extraData: { plugins: processedPluginData.plugins } });
978
665
  return c.json(processedResponse);
@@ -1159,10 +846,6 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
1159
846
  outputSchema: (0, schemaStructure_1.getDetailedSchemaStructure)(tool.output),
1160
847
  interface: tool.interface,
1161
848
  suggestConfirmation: tool.suggestConfirmation,
1162
- sideEffectClass: tool.sideEffectClass,
1163
- supportsParallel: tool.supportsParallel,
1164
- idempotencyScope: tool.idempotencyScope,
1165
- maxConcurrencyHint: tool.maxConcurrencyHint,
1166
849
  };
1167
850
  return c.json(toolDetails);
1168
851
  }
@@ -1190,11 +873,6 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
1190
873
  pricing: tool.pricing,
1191
874
  inputSchema: (0, schemaStructure_1.getDetailedSchemaStructure)(tool.input),
1192
875
  outputSchema: (0, schemaStructure_1.getDetailedSchemaStructure)(tool.output),
1193
- suggestConfirmation: tool.suggestConfirmation,
1194
- sideEffectClass: tool.sideEffectClass,
1195
- supportsParallel: tool.supportsParallel,
1196
- idempotencyScope: tool.idempotencyScope,
1197
- maxConcurrencyHint: tool.maxConcurrencyHint,
1198
876
  }
1199
877
  : {
1200
878
  id: toolId,
@@ -1220,13 +898,7 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
1220
898
  }
1221
899
  });
1222
900
  // Health check endpoint
1223
- app.get("/health", async (c) => {
1224
- const healthy = await resolveHealthStatus();
1225
- return c.json({
1226
- status: healthy ? "healthy" : "unhealthy",
1227
- timestamp: new Date().toISOString(),
1228
- }, healthy ? 200 : 503);
1229
- });
901
+ app.get("/health", (c) => c.json({ status: "healthy", timestamp: new Date().toISOString() }));
1230
902
  // Setup custom routes if provided
1231
903
  if (config.routes) {
1232
904
  config.routes(app);
@@ -1510,231 +1182,64 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
1510
1182
  });
1511
1183
  // Toolboxes list endpoint
1512
1184
  app.get("/toolboxes", (c) => c.json(toolboxes));
1513
- function normalizeRecommendationText(value) {
1514
- return value.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
1515
- }
1516
- function tokenizeRecommendationQuery(query) {
1517
- const normalized = normalizeRecommendationText(query);
1518
- if (!normalized)
1519
- return [];
1520
- return Array.from(new Set(normalized.split(/\s+/).filter((token) => token.length >= 2)));
1521
- }
1522
- function buildRecommendationIndex() {
1523
- const index = new Map();
1185
+ // Recommendations endpoint - returns cards for AI selection
1186
+ app.get("/recommendations", (c) => {
1187
+ const cards = [];
1524
1188
  for (const tool of tools) {
1525
- if (!tool.recommendations?.length)
1526
- continue;
1527
- for (const card of tool.recommendations) {
1528
- const scopedCardId = `${tool.id}:${card.id}`;
1529
- if (index.has(scopedCardId)) {
1530
- 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
+ });
1531
1200
  }
1532
- const isDynamic = !!card.inputSchema;
1533
- const inputSchema = isDynamic ? (0, schemaStructure_1.zodToJsonSchema)(card.inputSchema) : undefined;
1534
- const searchText = normalizeRecommendationText([tool.id, tool.name, card.id, ...card.tags].join(" "));
1535
- index.set(scopedCardId, {
1536
- cardId: scopedCardId,
1537
- localCardId: card.id,
1538
- toolId: tool.id,
1539
- toolName: tool.name,
1540
- tags: card.tags,
1541
- inputSchema,
1542
- ui: isDynamic ? undefined : card.ui,
1543
- isDynamic,
1544
- card,
1545
- searchText,
1546
- });
1547
- }
1548
- }
1549
- return index;
1550
- }
1551
- function scoreRecommendationCard(card, tokens) {
1552
- if (tokens.length === 0)
1553
- return 0;
1554
- let score = 0;
1555
- const tagSet = new Set(card.tags.map((tag) => normalizeRecommendationText(tag)));
1556
- const scopedId = normalizeRecommendationText(card.cardId);
1557
- const toolName = normalizeRecommendationText(card.toolName);
1558
- for (const token of tokens) {
1559
- if (scopedId === token || card.localCardId.toLowerCase() === token) {
1560
- score += 6;
1561
- continue;
1562
- }
1563
- if (tagSet.has(token)) {
1564
- score += 4;
1565
- }
1566
- if (toolName.includes(token)) {
1567
- score += 3;
1568
- }
1569
- if (card.searchText.includes(token)) {
1570
- score += 1;
1571
1201
  }
1572
1202
  }
1573
- return score;
1574
- }
1575
- const recommendationCardsById = buildRecommendationIndex();
1576
- const recommendationSearchSchema = zod_1.z.object({
1577
- query: zod_1.z.string().optional(),
1578
- limit: zod_1.z.number().int().min(1).max(50).optional().default(12),
1579
- toolIds: zod_1.z.array(zod_1.z.string()).optional(),
1580
- includeStaticUI: zod_1.z.boolean().optional().default(false),
1203
+ return c.json({ cards, count: cards.length });
1581
1204
  });
1582
- app.post("/recommendations/search", async (c) => {
1583
- const agentInfo = await getAgentInfo(c);
1584
- const body = await safeParseBody(c);
1585
- const processedPluginData = await processPluginsForRequest(body, agentInfo);
1586
- const requestData = { ...processedPluginData };
1587
- delete requestData.plugins;
1588
- const parsed = recommendationSearchSchema.safeParse(requestData);
1589
- if (!parsed.success) {
1590
- return c.json({
1591
- error: "Invalid recommendation search request",
1592
- details: parsed.error.format(),
1593
- }, 400);
1594
- }
1595
- const { query, limit, toolIds, includeStaticUI, } = parsed.data;
1596
- const toolIdSet = toolIds?.length ? new Set(toolIds) : null;
1597
- const filteredCards = Array.from(recommendationCardsById.values()).filter((card) => !toolIdSet || toolIdSet.has(card.toolId));
1598
- const tokens = tokenizeRecommendationQuery(query ?? "");
1599
- const ranked = filteredCards
1600
- .map((card, index) => ({
1601
- ...card,
1602
- score: scoreRecommendationCard(card, tokens),
1603
- index,
1604
- }))
1605
- .sort((a, b) => {
1606
- if (tokens.length === 0)
1607
- return a.index - b.index;
1608
- if (b.score !== a.score)
1609
- return b.score - a.score;
1610
- if (a.toolName !== b.toolName)
1611
- return a.toolName.localeCompare(b.toolName);
1612
- return a.localCardId.localeCompare(b.localCardId);
1613
- })
1614
- .slice(0, limit);
1615
- const cards = ranked.map((card) => ({
1616
- cardId: card.cardId,
1617
- localCardId: card.localCardId,
1618
- toolId: card.toolId,
1619
- toolName: card.toolName,
1620
- tags: card.tags,
1621
- score: card.score,
1622
- inputSchema: card.inputSchema,
1623
- ui: includeStaticUI && !card.isDynamic ? card.ui : undefined,
1624
- isDynamic: card.isDynamic,
1625
- }));
1626
- const response = {
1627
- cards,
1628
- count: cards.length,
1629
- query,
1630
- };
1631
- const processedResponse = await processPluginsForResponse(response, body, { extraData: { plugins: processedPluginData.plugins } });
1632
- return c.json(processedResponse);
1633
- });
1634
- const recommendationRenderSchema = zod_1.z.object({
1635
- cards: zod_1.z
1636
- .array(zod_1.z.object({
1637
- cardId: zod_1.z.string().min(1),
1638
- params: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional(),
1639
- context: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional(),
1640
- }))
1641
- .min(1)
1642
- .max(8),
1643
- continueOnError: zod_1.z.boolean().optional().default(true),
1644
- });
1645
- app.post("/recommendations/render", async (c) => {
1646
- const agentInfo = await getAgentInfo(c);
1647
- const body = await safeParseBody(c);
1648
- const processedPluginData = await processPluginsForRequest(body, agentInfo);
1649
- const requestData = { ...processedPluginData };
1650
- delete requestData.plugins;
1651
- const parsed = recommendationRenderSchema.safeParse(requestData);
1652
- if (!parsed.success) {
1653
- return c.json({
1654
- error: "Invalid recommendation render request",
1655
- details: parsed.error.format(),
1656
- }, 400);
1657
- }
1658
- const { cards: requestedCards, continueOnError } = parsed.data;
1659
- const renderedCards = [];
1660
- const renderErrors = [];
1661
- for (const requested of requestedCards) {
1662
- const card = recommendationCardsById.get(requested.cardId);
1663
- if (!card) {
1664
- renderErrors.push({
1665
- cardId: requested.cardId,
1666
- code: "not_found",
1667
- message: `Card not found: ${requested.cardId}`,
1668
- });
1669
- if (!continueOnError)
1670
- break;
1671
- continue;
1672
- }
1673
- if (!card.isDynamic) {
1674
- renderedCards.push({
1675
- cardId: card.cardId,
1676
- localCardId: card.localCardId,
1677
- toolId: card.toolId,
1678
- toolName: card.toolName,
1679
- tags: card.tags,
1680
- ui: card.card.ui,
1681
- });
1682
- continue;
1683
- }
1684
- const paramsInput = requested.params ?? {};
1685
- const parseResult = card.card.inputSchema.safeParse(paramsInput);
1686
- if (!parseResult.success) {
1687
- renderErrors.push({
1688
- cardId: card.cardId,
1689
- code: "invalid_params",
1690
- message: "Invalid parameters",
1691
- details: parseResult.error.format(),
1692
- });
1693
- if (!continueOnError)
1694
- break;
1695
- continue;
1696
- }
1697
- if (typeof card.card.ui !== "function") {
1698
- renderErrors.push({
1699
- cardId: card.cardId,
1700
- code: "render_failed",
1701
- message: `Card "${card.cardId}" is dynamic but has no UI generator function`,
1702
- });
1703
- if (!continueOnError)
1704
- break;
1705
- continue;
1706
- }
1707
- try {
1708
- const ui = await card.card.ui(parseResult.data, requested.context ?? {});
1709
- renderedCards.push({
1710
- cardId: card.cardId,
1711
- localCardId: card.localCardId,
1712
- toolId: card.toolId,
1713
- toolName: card.toolName,
1714
- tags: card.tags,
1715
- ui,
1716
- params: parseResult.data,
1717
- });
1718
- }
1719
- catch (error) {
1720
- renderErrors.push({
1721
- cardId: card.cardId,
1722
- code: "render_failed",
1723
- message: error instanceof Error ? error.message : "Failed to render recommendation card",
1724
- });
1725
- if (!continueOnError)
1726
- 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;
1727
1219
  }
1728
1220
  }
1729
- const response = {
1730
- cards: renderedCards,
1731
- errors: renderErrors,
1732
- renderedCount: renderedCards.length,
1733
- requestedCount: requestedCards.length,
1734
- };
1735
- const processedResponse = await processPluginsForResponse(response, body, { extraData: { plugins: processedPluginData.plugins } });
1736
- const statusCode = !continueOnError && renderErrors.length > 0 ? 400 : 200;
1737
- 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
+ }
1738
1243
  });
1739
1244
  // Process request with plugins
1740
1245
  async function processPluginsForRequest(request, agentInfo) {
@@ -1915,7 +1420,6 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
1915
1420
  }
1916
1421
  catch (error) {
1917
1422
  console.error(`Error in stream for ${tool.id}:`, error);
1918
- const safeMsg = sanitizeErrorMessage(error);
1919
1423
  // Send an error result rather than letting the stream end without a response
1920
1424
  try {
1921
1425
  if (withContext) {
@@ -1923,8 +1427,8 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
1923
1427
  event: 'result',
1924
1428
  data: JSON.stringify({
1925
1429
  toolResult: {
1926
- error: safeMsg,
1927
- text: `Error: ${safeMsg}`,
1430
+ error: error.message || "Unknown error",
1431
+ text: `Error: ${error.message || "Unknown error"}`,
1928
1432
  data: null,
1929
1433
  ui: null
1930
1434
  },
@@ -1937,8 +1441,8 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
1937
1441
  await stream.writeSSE({
1938
1442
  event: 'result',
1939
1443
  data: JSON.stringify({
1940
- error: safeMsg,
1941
- text: `Error: ${safeMsg}`,
1444
+ error: error.message || "Unknown error",
1445
+ text: `Error: ${error.message || "Unknown error"}`,
1942
1446
  data: null,
1943
1447
  ui: null
1944
1448
  }),
@@ -2030,14 +1534,13 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
2030
1534
  }
2031
1535
  catch (error) {
2032
1536
  console.error(`Error executing tool ${tool.id} (non-streaming):`, error);
2033
- const safeMsg = sanitizeErrorMessage(error);
2034
1537
  // Return formatted error response to match streaming behavior
2035
1538
  let errorResponse;
2036
1539
  if (withContext) {
2037
1540
  errorResponse = {
2038
1541
  toolResult: {
2039
- error: safeMsg,
2040
- text: `Error: ${safeMsg}`,
1542
+ error: error.message || "Unknown error",
1543
+ text: `Error: ${error.message || "Unknown error"}`,
2041
1544
  data: null,
2042
1545
  ui: null
2043
1546
  },
@@ -2046,8 +1549,8 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
2046
1549
  }
2047
1550
  else {
2048
1551
  errorResponse = {
2049
- error: safeMsg,
2050
- text: `Error: ${safeMsg}`,
1552
+ error: error.message || "Unknown error",
1553
+ text: `Error: ${error.message || "Unknown error"}`,
2051
1554
  data: null,
2052
1555
  ui: null
2053
1556
  };