@zerowidth/workbench-sdk 2.0.0 → 2.1.1

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.
@@ -0,0 +1,46 @@
1
+ {
2
+ "display_name": "Firecrawl Search",
3
+ "tagline": "Search the web with Firecrawl",
4
+ "description": "Search the web using the Firecrawl Search API - returns clean search results with titles, links, and snippets",
5
+ "category": "third-party",
6
+ "provider": "firecrawl",
7
+ "needs_key_from": ["firecrawl"],
8
+ "is_plugin": true,
9
+ "inputs": [
10
+ {
11
+ "name": "query",
12
+ "display_name": "Search Query",
13
+ "type": "string",
14
+ "description": "The search query string",
15
+ "required": true
16
+ },
17
+ {
18
+ "name": "limit",
19
+ "display_name": "Number of Results",
20
+ "type": "number",
21
+ "description": "Maximum number of search results to return (1-100)",
22
+ "default": 5
23
+ }
24
+ ],
25
+ "outputs": [
26
+ {
27
+ "name": "items",
28
+ "display_name": "Search Results",
29
+ "type": "array of objects",
30
+ "description": "Array of cleaned search result items with title, link, displayLink, snippet, and position"
31
+ },
32
+ {
33
+ "name": "total_results",
34
+ "display_name": "Total Results",
35
+ "type": "number",
36
+ "description": "Number of search results returned"
37
+ },
38
+ {
39
+ "name": "warning",
40
+ "display_name": "Warning",
41
+ "type": "string",
42
+ "description": "Any warning message returned by the Firecrawl API"
43
+ }
44
+ ],
45
+ "settings": []
46
+ }
@@ -0,0 +1,54 @@
1
+ export default async ({inputs, settings, config, nodeConfig}) => {
2
+ // Get Firecrawl integration from engine
3
+ const firecrawl = config.integrations?.firecrawl;
4
+ if (!firecrawl) {
5
+ throw new Error("Firecrawl integration not found");
6
+ }
7
+
8
+ // Build parameters
9
+ const params = {
10
+ query: inputs.query
11
+ };
12
+
13
+ // Clamp limit into Firecrawl's accepted 1-100 range
14
+ if (inputs.limit !== null && inputs.limit !== undefined && inputs.limit !== '') {
15
+ const limit = Number(inputs.limit);
16
+ if (!Number.isNaN(limit)) {
17
+ params.limit = Math.max(1, Math.min(100, Math.floor(limit)));
18
+ }
19
+ }
20
+
21
+ // Make API request - let errors bubble up to be caught by error manager
22
+ const response = await firecrawl.search(params);
23
+
24
+ // Firecrawl v2 keys results by source ({ web: [...], news: [...], images: [...] });
25
+ // tolerate a flat array (v1-style envelope) as well.
26
+ const data = response.data || {};
27
+ const rawResults = Array.isArray(data) ? data : (data.web || []);
28
+
29
+ // Clean up the items to match the shared search-result shape
30
+ // (title / link / displayLink / snippet) used by search nodes.
31
+ const items = rawResults.map((result, index) => {
32
+ let displayLink = null;
33
+ if (result.url) {
34
+ try {
35
+ displayLink = new URL(result.url).hostname;
36
+ } catch {
37
+ displayLink = null;
38
+ }
39
+ }
40
+ return {
41
+ title: result.title || null,
42
+ link: result.url || null,
43
+ displayLink: displayLink,
44
+ snippet: result.description || null,
45
+ position: result.position !== undefined && result.position !== null ? result.position : index + 1
46
+ };
47
+ });
48
+
49
+ return {
50
+ items: items,
51
+ total_results: items.length,
52
+ warning: response.warning || null
53
+ };
54
+ };
@@ -0,0 +1,9 @@
1
+ [
2
+ {
3
+ "inputs": {
4
+ "query": "zerowidth ai"
5
+ },
6
+ "expectedError": "Firecrawl integration not found",
7
+ "description": "Missing integration test - node throws when no firecrawl key is configured"
8
+ }
9
+ ]
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "display_name": "Search the Internet",
3
3
  "tagline": "Search the internet",
4
- "description": "Search the web using Google Custom Search - returns clean search results with titles, links, and snippets",
4
+ "description": "Search the web using Firecrawl Search - returns clean search results with titles, links, and snippets",
5
5
  "category": "third-party",
6
- "provider": "google",
7
- "needs_key_from": ["google_custom_search"],
6
+ "provider": "firecrawl",
7
+ "needs_key_from": ["firecrawl"],
8
8
  "is_macro": true,
9
9
  "is_plugin": true,
10
10
  "macro_flow": {
@@ -18,13 +18,13 @@
18
18
  },
19
19
  {
20
20
  "id": "search",
21
- "type": "google-custom-search"
21
+ "type": "firecrawl-search"
22
22
  },
23
23
  {
24
24
  "id": "num_results",
25
- "type": "string",
25
+ "type": "number",
26
26
  "settings": {
27
- "value": "10"
27
+ "value": 5
28
28
  }
29
29
  },
30
30
  {
@@ -42,7 +42,7 @@
42
42
  },
43
43
  {
44
44
  "from": { "node_id": "num_results", "port_name": "value" },
45
- "to": { "node_id": "search", "port_name": "num" }
45
+ "to": { "node_id": "search", "port_name": "limit" }
46
46
  },
47
47
  {
48
48
  "from": { "node_id": "search", "port_name": "items" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zerowidth/workbench-sdk",
3
- "version": "2.0.0",
3
+ "version": "2.1.1",
4
4
  "dependencies": {
5
5
  "adm-zip": "^0.5.16",
6
6
  "ajv": "^8.17.1",
package/src/index.js CHANGED
@@ -2366,10 +2366,13 @@ export default class Workbench {
2366
2366
  }
2367
2367
  }
2368
2368
  }
2369
- // Prepare the tool call message for the next LLM call
2369
+ // Prepare the tool call message for the next LLM call.
2370
+ // Preserve any text the model emitted alongside the tool calls —
2371
+ // nulling it here loses it from both the next round's context and
2372
+ // the final conversation output.
2370
2373
  toolCallMessage = {
2371
2374
  role: "assistant",
2372
- content: null,
2375
+ content: llmResult.content ?? null,
2373
2376
  tool_calls: llmResult.tool_calls
2374
2377
  };
2375
2378
  toolCallCount++;
@@ -82,6 +82,77 @@ export default class FirecrawlIntegration {
82
82
  }
83
83
  }
84
84
 
85
+ /**
86
+ * Search the web with the Firecrawl Search API
87
+ * @param {Object} params - Search parameters (query, limit, ...)
88
+ * @returns {Promise<Object>} Search response
89
+ */
90
+ async search(params = {}) {
91
+ try {
92
+ const url = `${this.options.baseURL}/search`;
93
+
94
+ // Remove any params that are null, undefined, or empty strings
95
+ Object.keys(params).forEach(key => {
96
+ if (params[key] === '') {
97
+ delete params[key];
98
+ }
99
+ if (params[key] === null) {
100
+ delete params[key];
101
+ }
102
+ if (params[key] === undefined) {
103
+ delete params[key];
104
+ }
105
+ if (Array.isArray(params[key]) && params[key].length === 0) {
106
+ delete params[key];
107
+ }
108
+ });
109
+
110
+ const requestHeaders = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.apiKey}` };
111
+ const startTime = Date.now();
112
+
113
+ const response = await axios({
114
+ url: url,
115
+ method: 'POST',
116
+ headers: requestHeaders,
117
+ data: params,
118
+ timeout: this.options.timeout
119
+ });
120
+
121
+ await emitAPICallEvent(this._engineConfig, {
122
+ timestamp: startTime, integration: 'firecrawl', nodeId: null, nodeType: null,
123
+ request: { method: 'POST', url, headers: requestHeaders, body: params },
124
+ response: { status: response.status, statusText: response.statusText },
125
+ duration: Date.now() - startTime, error: null
126
+ });
127
+
128
+ if (response.status >= 400) {
129
+ throw new Error(`Firecrawl API error: ${response.status} - ${response.data?.error || response.statusText}`);
130
+ }
131
+
132
+ return response.data;
133
+
134
+ } catch (error) {
135
+ // Note: emitAPICallEvent for error case is handled by the catch in the caller
136
+ // since errors from axios.post throw before we can capture the response
137
+ if (error.response) {
138
+ const status = error.response.status;
139
+ const statusText = error.response.statusText;
140
+ const responseData = error.response.data;
141
+
142
+ let errorMessage = `Firecrawl API Error (${status} ${statusText})`;
143
+ if (responseData?.error) {
144
+ errorMessage += `: ${responseData.error}`;
145
+ }
146
+
147
+ throw new Error(errorMessage);
148
+ } else if (error.request) {
149
+ throw new Error('Firecrawl API Error: No response received');
150
+ } else {
151
+ throw new Error(`Firecrawl API Error: ${error.message}`);
152
+ }
153
+ }
154
+ }
155
+
85
156
  /**
86
157
  * Helper function to convert comma-separated string to array
87
158
  * @param {string|Array} input - String or array input