@zerowidth/workbench-sdk 2.0.0-alpha.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.
Files changed (42) hide show
  1. package/nodes/combine-document-chunks/combine-document-chunks.config.json +8 -0
  2. package/nodes/combine-document-chunks/combine-document-chunks.process.js +9 -2
  3. package/nodes/describe-tables/describe-tables.config.json +59 -0
  4. package/nodes/describe-tables/describe-tables.process.js +40 -0
  5. package/nodes/expand-chunk-context/expand-chunk-context.config.json +87 -0
  6. package/nodes/expand-chunk-context/expand-chunk-context.process.js +34 -0
  7. package/nodes/find-entity-path/find-entity-path.config.json +73 -0
  8. package/nodes/find-entity-path/find-entity-path.process.js +51 -0
  9. package/nodes/firecrawl-search/firecrawl-search.config.json +46 -0
  10. package/nodes/firecrawl-search/firecrawl-search.process.js +54 -0
  11. package/nodes/firecrawl-search/firecrawl-search.tests.json +9 -0
  12. package/nodes/get-chunk-by-index/get-chunk-by-index.config.json +8 -0
  13. package/nodes/get-chunk-by-index/get-chunk-by-index.process.js +9 -2
  14. package/nodes/get-entity-neighbors/get-entity-neighbors.config.json +81 -0
  15. package/nodes/get-entity-neighbors/get-entity-neighbors.process.js +51 -0
  16. package/nodes/keyword-search/keyword-search.config.json +67 -0
  17. package/nodes/keyword-search/keyword-search.process.js +45 -0
  18. package/nodes/knowledge-base/knowledge-base.config.json +32 -0
  19. package/nodes/knowledge-base/knowledge-base.process.js +15 -0
  20. package/nodes/list-documents/list-documents.config.json +60 -0
  21. package/nodes/list-documents/list-documents.process.js +25 -0
  22. package/nodes/list-entities/list-entities.config.json +74 -0
  23. package/nodes/list-entities/list-entities.process.js +25 -0
  24. package/nodes/query-knowledge-base/query-knowledge-base.config.json +14 -7
  25. package/nodes/query-knowledge-base/query-knowledge-base.process.js +25 -5
  26. package/nodes/read-chunks/read-chunks.config.json +79 -0
  27. package/nodes/read-chunks/read-chunks.process.js +49 -0
  28. package/nodes/remote-mcp-tool/remote-mcp-tool.config.json +1 -0
  29. package/nodes/search-internet/search-internet.config.json +7 -7
  30. package/nodes/semantic-search/semantic-search.config.json +9 -1
  31. package/nodes/semantic-search/semantic-search.process.js +41 -18
  32. package/nodes/tool/tool.config.json +1 -0
  33. package/package.json +12 -7
  34. package/src/index.js +81 -11
  35. package/src/integrations/firecrawl.js +71 -0
  36. package/src/integrations/knowledge-base-interface.js +49 -0
  37. package/src/integrations/sqlite.js +535 -83
  38. package/src/types/knowledge_base.json +9 -0
  39. package/src/utilities/loaders.js +67 -11
  40. package/src/utilities/typers.js +15 -17
  41. package/src/utilities/validators.js +16 -1
  42. package/types/knowledge_base.json +9 -0
@@ -25,6 +25,14 @@
25
25
  "type": "string",
26
26
  "description": "String to use as separator between chunks",
27
27
  "default": "\n\n---\n\n"
28
+ },
29
+ {
30
+ "name": "knowledge_base",
31
+ "display_name": "Knowledge Base",
32
+ "type": "knowledge_base",
33
+ "description": "The knowledge base, from a Knowledge Base node. If omitted, falls back to the flow's default knowledge base.",
34
+ "required": false,
35
+ "default": null
28
36
  }
29
37
  ],
30
38
  "outputs": [
@@ -1,7 +1,14 @@
1
1
  export default async ({inputs, settings, config, nodeConfig}) => {
2
2
  try {
3
- // Get knowledge base integration
4
- const knowledgeBase = config.integrations?.knowledgeBase || config.integrations?.sqlite;
3
+ // Resolve the KB from a knowledge_base handle (shared wiring), falling
4
+ // back to the flow-global KB. See ADR 0023.
5
+ const kbRef = inputs.knowledge_base;
6
+ const knowledgeBase =
7
+ (kbRef && kbRef.uuid
8
+ ? config.integrations?.[`knowledgeBase:${kbRef.uuid}`]
9
+ : null) ||
10
+ config.integrations?.knowledgeBase ||
11
+ config.integrations?.sqlite;
5
12
 
6
13
  if (!knowledgeBase) {
7
14
  throw new Error("Knowledge base integration not found. Make sure a knowledge database is available.");
@@ -0,0 +1,59 @@
1
+ {
2
+ "display_name": "Describe Tables",
3
+ "tagline": "See a KB's tables + columns",
4
+ "description": "List the tables in a Tabular knowledge base with their columns, types, and row counts — so a model can discover the schema before writing SQL for the Query Knowledge Base node. Wire a Knowledge Base node into knowledge_base, same as the other KB nodes. Returns [] for a docs KB.",
5
+ "category": "knowledge",
6
+ "is_constant": false,
7
+ "is_plugin": false,
8
+ "is_output": false,
9
+ "inputs": [
10
+ {
11
+ "name": "knowledge_base",
12
+ "display_name": "Knowledge Base",
13
+ "type": "knowledge_base",
14
+ "description": "The knowledge base to describe, from a Knowledge Base node. If omitted, falls back to the flow's default knowledge base.",
15
+ "required": false,
16
+ "default": null
17
+ },
18
+ {
19
+ "name": "sample_rows",
20
+ "display_name": "Sample Rows",
21
+ "type": "number",
22
+ "description": "Rows to sample per table (0 = none). Samples help an LLM write correct SQL.",
23
+ "default": 5
24
+ }
25
+ ],
26
+ "outputs": [
27
+ {
28
+ "name": "schema",
29
+ "display_name": "Schema (for LLM)",
30
+ "type": "string",
31
+ "description": "Ready-to-paste schema block: CREATE TABLE DDL + a few sample rows (as comments) per table. Feed into an LLM node's context to write SQL."
32
+ },
33
+ {
34
+ "name": "tables",
35
+ "display_name": "Tables",
36
+ "type": "array",
37
+ "description": "[{ table_name, source_name, row_count, columns: [{ name, type }], ddl, sample_rows }]"
38
+ },
39
+ {
40
+ "name": "count",
41
+ "display_name": "Table Count",
42
+ "type": "number",
43
+ "description": "Number of tables"
44
+ },
45
+ {
46
+ "name": "success",
47
+ "display_name": "Success",
48
+ "type": "boolean",
49
+ "description": "Whether it completed successfully"
50
+ },
51
+ {
52
+ "name": "error",
53
+ "display_name": "Error",
54
+ "type": "string",
55
+ "description": "Error message if it failed"
56
+ }
57
+ ],
58
+ "settings": []
59
+ }
@@ -0,0 +1,40 @@
1
+ export default async ({ inputs, config }) => {
2
+ try {
3
+ // Shared KB wiring (see semantic-search / keyword-search). ADR 0023.
4
+ const kbRef = inputs.knowledge_base;
5
+ const knowledgeBase =
6
+ (kbRef && kbRef.uuid
7
+ ? config.integrations?.[`knowledgeBase:${kbRef.uuid}`]
8
+ : null) ||
9
+ config.integrations?.knowledgeBase ||
10
+ config.integrations?.sqlite;
11
+
12
+ if (!knowledgeBase) {
13
+ throw new Error("Knowledge base integration not found. Make sure a knowledge database is available.");
14
+ }
15
+
16
+ const sampleLimit =
17
+ typeof inputs.sample_rows === "number" ? inputs.sample_rows : 5;
18
+ const tables = await knowledgeBase.listTables({ sampleLimit });
19
+
20
+ // A ready-to-paste schema block for an LLM: CREATE TABLE DDL + a few
21
+ // sample rows (as comments) per table. Drop `schema` straight into an
22
+ // LLM node's context to write SQL for the Query Knowledge Base node.
23
+ const schema = tables
24
+ .map((t) => {
25
+ const lines = [t.ddl];
26
+ if (Array.isArray(t.sample_rows) && t.sample_rows.length > 0) {
27
+ lines.push("-- sample rows:");
28
+ for (const row of t.sample_rows) {
29
+ lines.push("-- " + JSON.stringify(row));
30
+ }
31
+ }
32
+ return lines.join("\n");
33
+ })
34
+ .join("\n\n");
35
+
36
+ return { tables, schema, count: tables.length, success: true, error: null };
37
+ } catch (error) {
38
+ return { tables: [], schema: "", count: 0, success: false, error: error.message };
39
+ }
40
+ };
@@ -0,0 +1,87 @@
1
+ {
2
+ "display_name": "Expand Chunk Context",
3
+ "tagline": "Grab chunks around a hit",
4
+ "description": "Given a chunk (its document_id + chunk_index, e.g. from a search result), return the surrounding chunks — the N before and N after — plus their combined text. The classic RAG move: search finds one chunk, this widens the window so the model has the context around it.",
5
+ "category": "knowledge",
6
+ "is_constant": false,
7
+ "is_plugin": false,
8
+ "is_output": false,
9
+ "inputs": [
10
+ {
11
+ "name": "document_id",
12
+ "display_name": "Document ID",
13
+ "type": "string",
14
+ "description": "The document the anchor chunk belongs to",
15
+ "required": true
16
+ },
17
+ {
18
+ "name": "chunk_index",
19
+ "display_name": "Chunk Index",
20
+ "type": "number",
21
+ "description": "The anchor chunk's index (e.g. from a search result)",
22
+ "required": true
23
+ },
24
+ {
25
+ "name": "knowledge_base",
26
+ "display_name": "Knowledge Base",
27
+ "type": "knowledge_base",
28
+ "description": "The knowledge base, from a Knowledge Base node. If omitted, falls back to the flow's default knowledge base.",
29
+ "required": false,
30
+ "default": null
31
+ },
32
+ {
33
+ "name": "before",
34
+ "display_name": "Chunks Before",
35
+ "type": "number",
36
+ "description": "How many chunks before the anchor to include",
37
+ "default": 1
38
+ },
39
+ {
40
+ "name": "after",
41
+ "display_name": "Chunks After",
42
+ "type": "number",
43
+ "description": "How many chunks after the anchor to include",
44
+ "default": 2
45
+ },
46
+ {
47
+ "name": "separator",
48
+ "display_name": "Separator",
49
+ "type": "string",
50
+ "description": "Joins chunk contents in the combined output",
51
+ "default": "\n\n"
52
+ }
53
+ ],
54
+ "outputs": [
55
+ {
56
+ "name": "chunks",
57
+ "display_name": "Chunks",
58
+ "type": "array",
59
+ "description": "The window of chunks, ascending by index"
60
+ },
61
+ {
62
+ "name": "content",
63
+ "display_name": "Combined Content",
64
+ "type": "string",
65
+ "description": "The window's chunk contents joined by the separator"
66
+ },
67
+ {
68
+ "name": "count",
69
+ "display_name": "Count",
70
+ "type": "number",
71
+ "description": "Number of chunks in the window"
72
+ },
73
+ {
74
+ "name": "success",
75
+ "display_name": "Success",
76
+ "type": "boolean",
77
+ "description": "Whether it completed successfully"
78
+ },
79
+ {
80
+ "name": "error",
81
+ "display_name": "Error",
82
+ "type": "string",
83
+ "description": "Error message if it failed"
84
+ }
85
+ ],
86
+ "settings": []
87
+ }
@@ -0,0 +1,34 @@
1
+ export default async ({ inputs, config }) => {
2
+ try {
3
+ // Shared KB wiring (see semantic-search / keyword-search). ADR 0023.
4
+ const kbRef = inputs.knowledge_base;
5
+ const knowledgeBase =
6
+ (kbRef && kbRef.uuid
7
+ ? config.integrations?.[`knowledgeBase:${kbRef.uuid}`]
8
+ : null) ||
9
+ config.integrations?.knowledgeBase ||
10
+ config.integrations?.sqlite;
11
+
12
+ if (!knowledgeBase) {
13
+ throw new Error("Knowledge base integration not found. Make sure a knowledge database is available.");
14
+ }
15
+
16
+ const { document_id, chunk_index, before = 1, after = 2, separator = "\n\n" } = inputs;
17
+ if (!document_id || typeof document_id !== 'string') {
18
+ throw new Error("document_id is required and must be a string");
19
+ }
20
+ if (chunk_index === undefined || chunk_index === null || Number.isNaN(Number(chunk_index))) {
21
+ throw new Error("chunk_index is required and must be a number");
22
+ }
23
+
24
+ const chunks = await knowledgeBase.getChunkWindow(document_id, Number(chunk_index), {
25
+ before,
26
+ after,
27
+ });
28
+ const content = chunks.map((c) => c.content ?? '').join(separator);
29
+
30
+ return { chunks, content, count: chunks.length, success: true, error: null };
31
+ } catch (error) {
32
+ return { chunks: [], content: '', count: 0, success: false, error: error.message };
33
+ }
34
+ };
@@ -0,0 +1,73 @@
1
+ {
2
+ "display_name": "Find Entity Path",
3
+ "tagline": "Connect two graph entities",
4
+ "description": "Find the shortest chain of relations connecting two entities in a Graph knowledge base (breadth-first, direction-agnostic). Answers \"how are X and Y related?\" with the hop-by-hop path. Wire a Knowledge Base node into knowledge_base, same as the search nodes.",
5
+ "category": "knowledge",
6
+ "is_constant": false,
7
+ "is_plugin": true,
8
+ "is_output": false,
9
+ "inputs": [
10
+ {
11
+ "name": "from",
12
+ "display_name": "From Entity",
13
+ "type": "string",
14
+ "description": "The starting entity — its name (case-insensitive) or id.",
15
+ "required": true
16
+ },
17
+ {
18
+ "name": "to",
19
+ "display_name": "To Entity",
20
+ "type": "string",
21
+ "description": "The target entity — its name (case-insensitive) or id.",
22
+ "required": true
23
+ },
24
+ {
25
+ "name": "knowledge_base",
26
+ "display_name": "Knowledge Base",
27
+ "type": "knowledge_base",
28
+ "description": "The knowledge base to traverse, from a Knowledge Base node. If omitted, falls back to the flow's default knowledge base.",
29
+ "required": false,
30
+ "default": null
31
+ },
32
+ {
33
+ "name": "max_depth",
34
+ "display_name": "Max Depth",
35
+ "type": "number",
36
+ "description": "Maximum number of hops to search before giving up.",
37
+ "default": 4
38
+ }
39
+ ],
40
+ "outputs": [
41
+ {
42
+ "name": "found",
43
+ "display_name": "Found",
44
+ "type": "boolean",
45
+ "description": "Whether a path exists within max_depth"
46
+ },
47
+ {
48
+ "name": "hops",
49
+ "display_name": "Hops",
50
+ "type": "number",
51
+ "description": "Number of relations in the path (0 when from = to)"
52
+ },
53
+ {
54
+ "name": "steps",
55
+ "display_name": "Steps",
56
+ "type": "array",
57
+ "description": "The path, node by node: [{ node, via_edge? }] — via_edge names the relation that reached each node"
58
+ },
59
+ {
60
+ "name": "success",
61
+ "display_name": "Success",
62
+ "type": "boolean",
63
+ "description": "Whether the search completed successfully"
64
+ },
65
+ {
66
+ "name": "error",
67
+ "display_name": "Error",
68
+ "type": "string",
69
+ "description": "Error message if it failed (including unknown entities)"
70
+ }
71
+ ],
72
+ "settings": []
73
+ }
@@ -0,0 +1,51 @@
1
+ export default async ({ inputs, config }) => {
2
+ try {
3
+ // Shared KB wiring (see semantic-search / keyword-search): a
4
+ // knowledge_base handle names a KB by uuid, falling back to the
5
+ // flow-global KB. See ADR 0023.
6
+ const kbRef = inputs.knowledge_base;
7
+ const knowledgeBase =
8
+ (kbRef && kbRef.uuid
9
+ ? config.integrations?.[`knowledgeBase:${kbRef.uuid}`]
10
+ : null) ||
11
+ config.integrations?.knowledgeBase ||
12
+ config.integrations?.sqlite;
13
+
14
+ if (!knowledgeBase) {
15
+ throw new Error("Knowledge base integration not found. Make sure a knowledge database is available.");
16
+ }
17
+
18
+ const { from, to, max_depth = 4 } = inputs;
19
+ if (typeof from !== "string" || from.trim().length === 0) {
20
+ throw new Error("A from entity name or id is required.");
21
+ }
22
+ if (typeof to !== "string" || to.trim().length === 0) {
23
+ throw new Error("A to entity name or id is required.");
24
+ }
25
+
26
+ const result = await knowledgeBase.findPath(from.trim(), to.trim(), {
27
+ max_depth,
28
+ });
29
+
30
+ if (!result.from || !result.to) {
31
+ const missing = !result.from ? from : to;
32
+ return {
33
+ found: false,
34
+ hops: 0,
35
+ steps: [],
36
+ success: false,
37
+ error: `No entity named "${missing}" in this knowledge base. Use List Entities to see what exists.`,
38
+ };
39
+ }
40
+
41
+ return {
42
+ found: result.found,
43
+ hops: result.hops,
44
+ steps: result.steps,
45
+ success: true,
46
+ error: null,
47
+ };
48
+ } catch (error) {
49
+ return { found: false, hops: 0, steps: [], success: false, error: error.message };
50
+ }
51
+ };
@@ -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
+ ]
@@ -18,6 +18,14 @@
18
18
  "type": "number",
19
19
  "description": "The index of the chunk to retrieve (0-based)",
20
20
  "required": true
21
+ },
22
+ {
23
+ "name": "knowledge_base",
24
+ "display_name": "Knowledge Base",
25
+ "type": "knowledge_base",
26
+ "description": "The knowledge base, from a Knowledge Base node. If omitted, falls back to the flow's default knowledge base.",
27
+ "required": false,
28
+ "default": null
21
29
  }
22
30
  ],
23
31
  "outputs": [
@@ -1,7 +1,14 @@
1
1
  export default async ({inputs, settings, config, nodeConfig}) => {
2
2
  try {
3
- // Get knowledge base integration
4
- const knowledgeBase = config.integrations?.knowledgeBase || config.integrations?.sqlite;
3
+ // Resolve the KB from a knowledge_base handle (shared wiring), falling
4
+ // back to the flow-global KB. See ADR 0023.
5
+ const kbRef = inputs.knowledge_base;
6
+ const knowledgeBase =
7
+ (kbRef && kbRef.uuid
8
+ ? config.integrations?.[`knowledgeBase:${kbRef.uuid}`]
9
+ : null) ||
10
+ config.integrations?.knowledgeBase ||
11
+ config.integrations?.sqlite;
5
12
 
6
13
  if (!knowledgeBase) {
7
14
  throw new Error("Knowledge base integration not found. Make sure a knowledge database is available.");
@@ -0,0 +1,81 @@
1
+ {
2
+ "display_name": "Get Entity Neighbors",
3
+ "tagline": "Walk one hop in a graph",
4
+ "description": "Look up an entity in a Graph knowledge base (by name or id) and return every relation touching it, each with the entity on the far side — one hop of graph traversal. Filter by direction or relation type. Wire a Knowledge Base node into knowledge_base, same as the search nodes.",
5
+ "category": "knowledge",
6
+ "is_constant": false,
7
+ "is_plugin": true,
8
+ "is_output": false,
9
+ "inputs": [
10
+ {
11
+ "name": "entity",
12
+ "display_name": "Entity",
13
+ "type": "string",
14
+ "description": "The entity to look up — its name (case-insensitive) or id.",
15
+ "required": true
16
+ },
17
+ {
18
+ "name": "knowledge_base",
19
+ "display_name": "Knowledge Base",
20
+ "type": "knowledge_base",
21
+ "description": "The knowledge base to traverse, from a Knowledge Base node. If omitted, falls back to the flow's default knowledge base.",
22
+ "required": false,
23
+ "default": null
24
+ },
25
+ {
26
+ "name": "direction",
27
+ "display_name": "Direction",
28
+ "type": "string",
29
+ "description": "Which relations to include: both, out (entity is the source), or in (entity is the target).",
30
+ "default": "both",
31
+ "options": ["both", "out", "in"]
32
+ },
33
+ {
34
+ "name": "relation_type",
35
+ "display_name": "Relation Type",
36
+ "type": "string",
37
+ "description": "Optional: only relations of this type (e.g. works_at). Case-insensitive.",
38
+ "default": null
39
+ },
40
+ {
41
+ "name": "limit",
42
+ "display_name": "Limit",
43
+ "type": "number",
44
+ "description": "Maximum number of neighbors to return",
45
+ "default": 50
46
+ }
47
+ ],
48
+ "outputs": [
49
+ {
50
+ "name": "entity",
51
+ "display_name": "Entity",
52
+ "type": "object",
53
+ "description": "The resolved entity row (null if not found)"
54
+ },
55
+ {
56
+ "name": "neighbors",
57
+ "display_name": "Neighbors",
58
+ "type": "array",
59
+ "description": "Relations touching the entity: { edge_id, relation_type, description, direction, node }"
60
+ },
61
+ {
62
+ "name": "count",
63
+ "display_name": "Count",
64
+ "type": "number",
65
+ "description": "Number of neighbors returned"
66
+ },
67
+ {
68
+ "name": "success",
69
+ "display_name": "Success",
70
+ "type": "boolean",
71
+ "description": "Whether the lookup completed successfully"
72
+ },
73
+ {
74
+ "name": "error",
75
+ "display_name": "Error",
76
+ "type": "string",
77
+ "description": "Error message if it failed (including entity not found)"
78
+ }
79
+ ],
80
+ "settings": []
81
+ }
@@ -0,0 +1,51 @@
1
+ export default async ({ inputs, config }) => {
2
+ try {
3
+ // Shared KB wiring (see semantic-search / keyword-search): a
4
+ // knowledge_base handle names a KB by uuid, falling back to the
5
+ // flow-global KB. See ADR 0023.
6
+ const kbRef = inputs.knowledge_base;
7
+ const knowledgeBase =
8
+ (kbRef && kbRef.uuid
9
+ ? config.integrations?.[`knowledgeBase:${kbRef.uuid}`]
10
+ : null) ||
11
+ config.integrations?.knowledgeBase ||
12
+ config.integrations?.sqlite;
13
+
14
+ if (!knowledgeBase) {
15
+ throw new Error("Knowledge base integration not found. Make sure a knowledge database is available.");
16
+ }
17
+
18
+ const { entity, direction = "both", relation_type = null, limit = 50 } = inputs;
19
+ if (typeof entity !== "string" || entity.trim().length === 0) {
20
+ throw new Error("An entity name or id is required.");
21
+ }
22
+
23
+ const result = await knowledgeBase.getNeighbors(entity.trim(), {
24
+ direction,
25
+ relation_type,
26
+ limit,
27
+ });
28
+
29
+ if (!result.entity) {
30
+ // Soft-fail with a helpful message so an agent can recover by
31
+ // listing entities instead of dead-ending.
32
+ return {
33
+ entity: null,
34
+ neighbors: [],
35
+ count: 0,
36
+ success: false,
37
+ error: `No entity named "${entity}" in this knowledge base. Use List Entities to see what exists.`,
38
+ };
39
+ }
40
+
41
+ return {
42
+ entity: result.entity,
43
+ neighbors: result.neighbors,
44
+ count: result.neighbors.length,
45
+ success: true,
46
+ error: null,
47
+ };
48
+ } catch (error) {
49
+ return { entity: null, neighbors: [], count: 0, success: false, error: error.message };
50
+ }
51
+ };