project-graph-mcp 2.3.0 → 2.3.2

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 (66) hide show
  1. package/package.json +1 -3
  2. package/project-graph-mcp-2.3.0.tgz +0 -0
  3. package/src/network/web-server.js +1 -1
  4. package/vendor/symbiote-node/engine/AgentUICommands.js +100 -0
  5. package/vendor/symbiote-node/engine/Executor.js +371 -0
  6. package/vendor/symbiote-node/engine/Graph.js +314 -0
  7. package/vendor/symbiote-node/engine/GraphServer.js +353 -0
  8. package/vendor/symbiote-node/engine/HandlerLoader.js +145 -0
  9. package/vendor/symbiote-node/engine/History.js +83 -0
  10. package/vendor/symbiote-node/engine/Lifecycle.js +118 -0
  11. package/vendor/symbiote-node/engine/Persistence.js +84 -0
  12. package/vendor/symbiote-node/engine/Registry.js +264 -0
  13. package/vendor/symbiote-node/engine/SocketTypes.js +79 -0
  14. package/vendor/symbiote-node/engine/cli.js +404 -0
  15. package/vendor/symbiote-node/engine/index.js +56 -0
  16. package/vendor/symbiote-node/engine/nanoid.js +28 -0
  17. package/vendor/symbiote-node/engine/package.json +26 -0
  18. package/vendor/symbiote-node/engine/packs/ai/beat-detect.handler.js +215 -0
  19. package/vendor/symbiote-node/engine/packs/ai/content-adapt.handler.js +238 -0
  20. package/vendor/symbiote-node/engine/packs/ai/face-detect.handler.js +287 -0
  21. package/vendor/symbiote-node/engine/packs/ai/grok-generate.handler.js +565 -0
  22. package/vendor/symbiote-node/engine/packs/ai/kling-lipsync.handler.js +414 -0
  23. package/vendor/symbiote-node/engine/packs/ai/lesson-generate.handler.js +343 -0
  24. package/vendor/symbiote-node/engine/packs/ai/opencode.handler.js +164 -0
  25. package/vendor/symbiote-node/engine/packs/ai/replicate-lipsync.handler.js +341 -0
  26. package/vendor/symbiote-node/engine/packs/ai/tts.handler.js +241 -0
  27. package/vendor/symbiote-node/engine/packs/ai/whisper.handler.js +191 -0
  28. package/vendor/symbiote-node/engine/packs/data/db-query.handler.js +67 -0
  29. package/vendor/symbiote-node/engine/packs/data/news-accumulate.handler.js +281 -0
  30. package/vendor/symbiote-node/engine/packs/data/personas.handler.js +160 -0
  31. package/vendor/symbiote-node/engine/packs/data/prompt-loader.handler.js +193 -0
  32. package/vendor/symbiote-node/engine/packs/data/roles.handler.js +216 -0
  33. package/vendor/symbiote-node/engine/packs/data/rss-feed.handler.js +244 -0
  34. package/vendor/symbiote-node/engine/packs/debug/inject.handler.js +52 -0
  35. package/vendor/symbiote-node/engine/packs/flow/agent.handler.js +73 -0
  36. package/vendor/symbiote-node/engine/packs/flow/if.handler.js +107 -0
  37. package/vendor/symbiote-node/engine/packs/flow/loop.handler.js +58 -0
  38. package/vendor/symbiote-node/engine/packs/flow/merge.handler.js +60 -0
  39. package/vendor/symbiote-node/engine/packs/flow/retry.handler.js +65 -0
  40. package/vendor/symbiote-node/engine/packs/flow/switch.handler.js +64 -0
  41. package/vendor/symbiote-node/engine/packs/flow/wait-all.handler.js +39 -0
  42. package/vendor/symbiote-node/engine/packs/io/http-request.handler.js +82 -0
  43. package/vendor/symbiote-node/engine/packs/io/read-file.handler.js +60 -0
  44. package/vendor/symbiote-node/engine/packs/io/write-file.handler.js +63 -0
  45. package/vendor/symbiote-node/engine/packs/transform/anchor-match.handler.js +494 -0
  46. package/vendor/symbiote-node/engine/packs/transform/effects-skeleton.handler.js +417 -0
  47. package/vendor/symbiote-node/engine/packs/transform/json-parse.handler.js +43 -0
  48. package/vendor/symbiote-node/engine/packs/transform/lipsync-select.handler.js +339 -0
  49. package/vendor/symbiote-node/engine/packs/transform/riopla-adapt.handler.js +432 -0
  50. package/vendor/symbiote-node/engine/packs/transform/set.handler.js +57 -0
  51. package/vendor/symbiote-node/engine/packs/transform/template-builder.handler.js +134 -0
  52. package/vendor/symbiote-node/engine/packs/transform/template.handler.js +79 -0
  53. package/vendor/symbiote-node/engine/packs/transform/timeline-build.handler.js +399 -0
  54. package/vendor/symbiote-node/engine/packs/util/delay.handler.js +39 -0
  55. package/vendor/symbiote-node/engine/packs/util/log.handler.js +44 -0
  56. package/vendor/symbiote-node/engine/packs/video-pack.js +323 -0
  57. package/vendor/symbiote-node/package.json +2 -2
  58. package/web/app.js +6 -3
  59. package/web/components/canvas-graph.js +50 -11
  60. package/web/components/code-block.js +1 -1
  61. package/web/components/event-feed/MiniGraphWidget.js +105 -15
  62. package/web/components/follow-ribbon.js +134 -0
  63. package/web/follow-controller.js +241 -0
  64. package/web/panels/code-viewer.js +1 -1
  65. package/web/panels/dep-graph.js +21 -42
  66. package/web/style.css +6 -0
@@ -0,0 +1,145 @@
1
+ /**
2
+ * HandlerLoader.js - File-based node handler loader
3
+ *
4
+ * Scans directories for .handler.js files and registers them
5
+ * as node types. Supports hot reload via fs.watch.
6
+ *
7
+ * Handler file convention:
8
+ * export default {
9
+ * type: 'category/name',
10
+ * category: 'category',
11
+ * icon: 'icon_name',
12
+ * driver: { inputs, outputs, params, ... },
13
+ * lifecycle: { validate, cacheKey, execute, postProcess },
14
+ * };
15
+ *
16
+ * @module symbiote-node/HandlerLoader */
17
+
18
+ import { readdir, stat } from 'node:fs/promises';
19
+ import { join, relative, extname } from 'node:path';
20
+ import { watch } from 'node:fs';
21
+ import { pathToFileURL } from 'node:url';
22
+ import { registerNodeType } from './Registry.js';
23
+
24
+ /**
25
+ * Recursively find all .handler.js files in a directory
26
+ * @param {string} dir - Directory to scan
27
+ * @returns {Promise<string[]>} Absolute file paths
28
+ */
29
+ async function findHandlerFiles(dir) {
30
+ const results = [];
31
+ let entries;
32
+ try {
33
+ entries = await readdir(dir, { withFileTypes: true });
34
+ } catch {
35
+ return results;
36
+ }
37
+
38
+ for (const entry of entries) {
39
+ const fullPath = join(dir, entry.name);
40
+ if (entry.isDirectory()) {
41
+ const nested = await findHandlerFiles(fullPath);
42
+ results.push(...nested);
43
+ } else if (entry.name.endsWith('.handler.js')) {
44
+ results.push(fullPath);
45
+ }
46
+ }
47
+ return results;
48
+ }
49
+
50
+ /**
51
+ * Load a single handler file and register it
52
+ * @param {string} filePath - Absolute path to .handler.js file
53
+ * @returns {Promise<string|null>} Registered type name or null on error
54
+ */
55
+ async function loadHandler(filePath) {
56
+ const fileUrl = pathToFileURL(filePath).href;
57
+ // Cache-bust for hot reload
58
+ const url = `${fileUrl}?t=${Date.now()}`;
59
+
60
+ const module = await import(url);
61
+ const handler = module.default;
62
+
63
+ if (!handler?.type) {
64
+ throw new Error(`Handler file ${filePath} missing 'type' field in default export`);
65
+ }
66
+
67
+ // Build node type definition from handler
68
+ const nodeDef = {
69
+ type: handler.type,
70
+ category: handler.category || handler.type.split('/')[0],
71
+ icon: handler.icon,
72
+ driver: handler.driver || {},
73
+ };
74
+
75
+ // Attach lifecycle hooks if present
76
+ if (handler.lifecycle) {
77
+ nodeDef.lifecycle = handler.lifecycle;
78
+ }
79
+
80
+ // Attach process function if present (legacy mode)
81
+ if (handler.process) {
82
+ nodeDef.process = handler.process;
83
+ }
84
+
85
+ registerNodeType(nodeDef);
86
+ return handler.type;
87
+ }
88
+
89
+ /**
90
+ * Scan a directory for .handler.js files and register them
91
+ * @param {string} dir - Directory to scan (e.g., 'nodes/')
92
+ * @returns {Promise<string[]>} List of registered type names
93
+ */
94
+ export async function loadHandlers(dir) {
95
+ const files = await findHandlerFiles(dir);
96
+ const registered = [];
97
+
98
+ for (const file of files) {
99
+ try {
100
+ const type = await loadHandler(file);
101
+ if (type) registered.push(type);
102
+ } catch (err) {
103
+ console.error(`[symbiote-node] Failed to load handler ${relative(dir, file)}: ${err.message}`); }
104
+ }
105
+
106
+ return registered;
107
+ }
108
+
109
+ /**
110
+ * Watch a directory for new/changed .handler.js files
111
+ * Auto-registers them on change.
112
+ *
113
+ * @param {string} dir - Directory to watch
114
+ * @param {object} [options={}]
115
+ * @param {function} [options.onRegister] - Callback(type, filePath)
116
+ * @param {function} [options.onError] - Callback(filePath, error)
117
+ * @returns {{close: function}} Watcher handle
118
+ */
119
+ export function watchHandlers(dir, options = {}) {
120
+ const { onRegister, onError } = options;
121
+
122
+ const watcher = watch(dir, { recursive: true }, async (eventType, filename) => {
123
+ if (!filename?.endsWith('.handler.js')) return;
124
+
125
+ const filePath = join(dir, filename);
126
+
127
+ // Verify file exists (could be a delete event)
128
+ try {
129
+ await stat(filePath);
130
+ } catch {
131
+ return; // File deleted, ignore
132
+ }
133
+
134
+ try {
135
+ const type = await loadHandler(filePath);
136
+ if (type && onRegister) onRegister(type, filePath);
137
+ } catch (err) {
138
+ if (onError) onError(filePath, err);
139
+ else console.error(`[symbiote-node] Watch error for ${filename}: ${err.message}`); }
140
+ });
141
+
142
+ return {
143
+ close: () => watcher.close(),
144
+ };
145
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * History.js - Snapshot-based undo/redo for graphs
3
+ *
4
+ * Stores deep-cloned snapshots of nodes and connections.
5
+ * Framework-agnostic — works with any graph data.
6
+ *
7
+ * @module agi-graph/History
8
+ */
9
+
10
+ const MAX_HISTORY = 50;
11
+
12
+ export class History {
13
+
14
+ /** @type {Array<{nodes: object[], connections: object[]}>} */
15
+ _states = [];
16
+
17
+ /** @type {number} */
18
+ _index = -1;
19
+
20
+ /**
21
+ * Push a new state snapshot
22
+ * @param {object[]} nodes
23
+ * @param {object[]} connections
24
+ */
25
+ push(nodes, connections) {
26
+ this._states.length = this._index + 1;
27
+ this._states.push({
28
+ nodes: JSON.parse(JSON.stringify(nodes)),
29
+ connections: JSON.parse(JSON.stringify(connections)),
30
+ });
31
+ if (this._states.length > MAX_HISTORY) {
32
+ this._states.shift();
33
+ }
34
+ this._index = this._states.length - 1;
35
+ }
36
+
37
+ /**
38
+ * Undo — return previous state
39
+ * @returns {{nodes: object[], connections: object[]}|null}
40
+ */
41
+ undo() {
42
+ if (!this.canUndo) return null;
43
+ this._index--;
44
+ return this._clone(this._states[this._index]);
45
+ }
46
+
47
+ /**
48
+ * Redo — return next state
49
+ * @returns {{nodes: object[], connections: object[]}|null}
50
+ */
51
+ redo() {
52
+ if (!this.canRedo) return null;
53
+ this._index++;
54
+ return this._clone(this._states[this._index]);
55
+ }
56
+
57
+ /** @returns {boolean} */
58
+ get canUndo() { return this._index > 0; }
59
+
60
+ /** @returns {boolean} */
61
+ get canRedo() { return this._index < this._states.length - 1; }
62
+
63
+ /** @returns {number} */
64
+ get depth() { return this._states.length; }
65
+
66
+ /** @returns {number} */
67
+ get index() { return this._index; }
68
+
69
+ /** Clear all history */
70
+ clear() {
71
+ this._states = [];
72
+ this._index = -1;
73
+ }
74
+
75
+ /**
76
+ * @param {{nodes: object[], connections: object[]}} state
77
+ * @returns {{nodes: object[], connections: object[]}}
78
+ * @private
79
+ */
80
+ _clone(state) {
81
+ return JSON.parse(JSON.stringify(state));
82
+ }
83
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Lifecycle.js - Node lifecycle pipeline
3
+ *
4
+ * Every node can define lifecycle hooks: validate, cacheKey, execute, postProcess.
5
+ * The lifecycle runner orchestrates these steps with cache awareness.
6
+ *
7
+ * @module agi-graph/Lifecycle
8
+ */
9
+
10
+ /**
11
+ * @typedef {object} LifecycleHooks
12
+ * @property {function} [validate] - (inputs) => boolean — return false to abort
13
+ * @property {function} [cacheKey] - (inputs, params) => string — custom cache key
14
+ * @property {function} [execute] - (inputs, params) => outputs — main processing (async)
15
+ * @property {function} [postProcess] - (outputs) => outputs — transform before output
16
+ */
17
+
18
+ /**
19
+ * @typedef {object} CacheState
20
+ * @property {'auto'|'freeze'|'force'} mode - Cache behavior mode
21
+ * @property {Map<string, {key: string, outputs: object}>} store - Cache storage
22
+ * @property {string} nodeId - Current node ID
23
+ */
24
+
25
+ /**
26
+ * @typedef {object} LifecycleResult
27
+ * @property {object} outputs - Final outputs from the node
28
+ * @property {boolean} cached - Whether result came from cache
29
+ * @property {string|null} error - Error message if validation failed
30
+ * @property {string|null} cacheHash - Cache key used (for UI display)
31
+ */
32
+
33
+ /**
34
+ * Default cache key: JSON hash of inputs + params
35
+ * @param {object} inputs
36
+ * @param {object} params
37
+ * @returns {string}
38
+ */
39
+ function defaultCacheKey(inputs, params) {
40
+ return JSON.stringify({ i: inputs, p: params });
41
+ }
42
+
43
+ /**
44
+ * Run lifecycle pipeline for a node
45
+ *
46
+ * Flow:
47
+ * 1. validate(inputs) → false = abort with error
48
+ * 2. cacheKey(inputs, params) → compute hash
49
+ * 3. Check mode: freeze → return cached; force → skip; auto → check hash
50
+ * 4. execute(inputs, params) → outputs
51
+ * 5. postProcess(outputs) → final outputs
52
+ * 6. Store in cache
53
+ *
54
+ * @param {LifecycleHooks} hooks - Lifecycle hooks from driver
55
+ * @param {object} inputs - Resolved inputs from upstream
56
+ * @param {object} params - Node parameters
57
+ * @param {CacheState} cacheState - Cache state for this node
58
+ * @returns {Promise<LifecycleResult>}
59
+ */
60
+ export async function runLifecycle(hooks, inputs, params, cacheState) {
61
+ const { mode = 'auto', store, nodeId } = cacheState;
62
+
63
+ // Step 1: Validate
64
+ if (hooks.validate) {
65
+ try {
66
+ const valid = hooks.validate(inputs);
67
+ if (valid === false) {
68
+ return { outputs: null, cached: false, error: 'Validation failed', cacheHash: null };
69
+ }
70
+ } catch (err) {
71
+ return { outputs: null, cached: false, error: `Validation error: ${err.message}`, cacheHash: null };
72
+ }
73
+ }
74
+
75
+ // Step 2: Compute cache key
76
+ const cacheKeyFn = hooks.cacheKey || defaultCacheKey;
77
+ const cacheHash = cacheKeyFn(inputs, params);
78
+
79
+ // Step 3: Check cache based on mode
80
+ const cached = store.get(nodeId);
81
+
82
+ if (mode === 'freeze' && cached) {
83
+ return { outputs: cached.outputs, cached: true, error: null, cacheHash: cached.key };
84
+ }
85
+
86
+ if (mode === 'auto' && cached && cached.key === cacheHash) {
87
+ return { outputs: cached.outputs, cached: true, error: null, cacheHash };
88
+ }
89
+
90
+ // mode === 'force' → skip cache check entirely
91
+
92
+ // Step 4: Execute
93
+ const executeFn = hooks.execute;
94
+ if (!executeFn) {
95
+ return { outputs: null, cached: false, error: 'No execute hook defined', cacheHash };
96
+ }
97
+
98
+ let outputs;
99
+ try {
100
+ outputs = await executeFn(inputs, params);
101
+ } catch (err) {
102
+ return { outputs: null, cached: false, error: `Execution error: ${err.message}`, cacheHash };
103
+ }
104
+
105
+ // Step 5: PostProcess
106
+ if (hooks.postProcess) {
107
+ try {
108
+ outputs = hooks.postProcess(outputs);
109
+ } catch (err) {
110
+ return { outputs: null, cached: false, error: `PostProcess error: ${err.message}`, cacheHash };
111
+ }
112
+ }
113
+
114
+ // Step 6: Store in cache
115
+ store.set(nodeId, { key: cacheHash, outputs });
116
+
117
+ return { outputs, cached: false, error: null, cacheHash };
118
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Persistence.js - File-based graph save/load
3
+ *
4
+ * Serializes Graph to .workflow.json and loads it back.
5
+ * Works in Node.js (fs) and browser (Blob/FileReader).
6
+ *
7
+ * @module agi-graph/Persistence
8
+ */
9
+
10
+ import { Graph } from './Graph.js';
11
+
12
+ /**
13
+ * Save graph to JSON string
14
+ * @param {Graph} graph
15
+ * @param {object} [options={}]
16
+ * @param {boolean} [options.pretty=true] - Pretty-print JSON
17
+ * @param {boolean} [options.includeOutput=false] - Include _output cache in export
18
+ * @returns {string} JSON string
19
+ */
20
+ export function serialize(graph, options = {}) {
21
+ const { pretty = true, includeOutput = false } = options;
22
+ const data = graph.toJSON();
23
+
24
+ // Strip _output unless explicitly requested
25
+ if (!includeOutput) {
26
+ data.nodes = data.nodes.map(n => {
27
+ const { _output, ...rest } = n;
28
+ return rest;
29
+ });
30
+ }
31
+
32
+ return JSON.stringify(data, null, pretty ? 2 : 0);
33
+ }
34
+
35
+ /**
36
+ * Load graph from JSON string
37
+ * @param {string} json
38
+ * @returns {Graph}
39
+ */
40
+ export function deserialize(json) {
41
+ const data = JSON.parse(json);
42
+ return new Graph(data);
43
+ }
44
+
45
+ /**
46
+ * Save graph to file (Node.js)
47
+ * @param {Graph} graph
48
+ * @param {string} filePath
49
+ * @param {object} [options]
50
+ * @returns {Promise<void>}
51
+ */
52
+ export async function saveToFile(graph, filePath, options) {
53
+ const json = serialize(graph, options);
54
+ const { writeFile } = await import('node:fs/promises');
55
+ await writeFile(filePath, json, 'utf-8');
56
+ }
57
+
58
+ /**
59
+ * Load graph from file (Node.js)
60
+ * @param {string} filePath
61
+ * @returns {Promise<Graph>}
62
+ */
63
+ export async function loadFromFile(filePath) {
64
+ const { readFile } = await import('node:fs/promises');
65
+ const json = await readFile(filePath, 'utf-8');
66
+ return deserialize(json);
67
+ }
68
+
69
+ /**
70
+ * Download graph as file (Browser)
71
+ * @param {Graph} graph
72
+ * @param {string} [filename='graph.workflow.json']
73
+ * @param {object} [options]
74
+ */
75
+ export function downloadGraph(graph, filename = 'graph.workflow.json', options) {
76
+ const json = serialize(graph, options);
77
+ const blob = new Blob([json], { type: 'application/json' });
78
+ const url = URL.createObjectURL(blob);
79
+ const a = document.createElement('a');
80
+ a.href = url;
81
+ a.download = filename;
82
+ a.click();
83
+ URL.revokeObjectURL(url);
84
+ }
@@ -0,0 +1,264 @@
1
+ /**
2
+ * Registry.js - Node type and driver registry
3
+ *
4
+ * Central registry for node types, drivers, and domain packs.
5
+ * AI agents use this to discover, compose, and validate nodes.
6
+ *
7
+ * @module agi-graph/Registry
8
+ */
9
+
10
+ import { areSocketsCompatible, registerSocketTypes } from './SocketTypes.js';
11
+
12
+ /**
13
+ * @typedef {object} SocketDef
14
+ * @property {string} name - Socket name
15
+ * @property {string} type - Socket type (must be registered in SocketTypes)
16
+ * @property {boolean} [required] - Whether this input is required
17
+ * @property {string} [description] - Human-readable description for AI
18
+ */
19
+
20
+ /**
21
+ * @typedef {object} ParamDef
22
+ * @property {string} type - Parameter type (string, int, float, boolean, object, array)
23
+ * @property {*} [default] - Default value
24
+ * @property {number} [min] - Min value (for numeric types)
25
+ * @property {number} [max] - Max value (for numeric types)
26
+ * @property {Array} [enum] - Allowed values
27
+ * @property {boolean} [required] - Whether this param is required
28
+ * @property {string} [description] - Human-readable description
29
+ */
30
+
31
+ /**
32
+ * @typedef {object} Driver
33
+ * @property {string} description - What this node type does (for AI discovery)
34
+ * @property {string[]} [capabilities] - Tags for AI search
35
+ * @property {SocketDef[]} inputs - Input socket definitions
36
+ * @property {SocketDef[]} outputs - Output socket definitions
37
+ * @property {Record<string, ParamDef>} [params] - Parameter definitions
38
+ * @property {object} [dynamicOutputs] - Dynamic socket pattern definition
39
+ * @property {object} [constraints] - Requirements (secrets, SSH, etc.)
40
+ */
41
+
42
+ /**
43
+ * @typedef {object} NodeTypeDef
44
+ * @property {string} type - Unique node type identifier (e.g., 'ai/llm')
45
+ * @property {Driver} driver - Self-describing driver manifest
46
+ * @property {function} [process] - Execution function: (inputs, params) => outputs
47
+ * @property {string} [icon] - Material icon name for UI
48
+ * @property {string} [category] - Category for UI grouping
49
+ */
50
+
51
+ /** @type {Map<string, NodeTypeDef>} */
52
+ const _nodeTypes = new Map();
53
+
54
+ /** @type {Map<string, object>} */
55
+ const _packs = new Map();
56
+
57
+ /**
58
+ * Register a single node type
59
+ * @param {NodeTypeDef} def
60
+ */
61
+ export function registerNodeType(def) {
62
+ if (!def.type) throw new Error('Node type definition must have a "type" field');
63
+ if (!def.driver) throw new Error(`Node type "${def.type}" must have a "driver" field`);
64
+ _nodeTypes.set(def.type, def);
65
+ }
66
+
67
+ /**
68
+ * Register a domain pack (batch registration of node types + socket types)
69
+ * @param {object} pack
70
+ * @param {string} pack.name - Pack name
71
+ * @param {Record<string, import('./SocketTypes.js').SocketTypeDef>} [pack.socketTypes]
72
+ * @param {NodeTypeDef[]} pack.nodes - Array of node type definitions
73
+ */
74
+ export function registerPack(pack) {
75
+ if (!pack.name) throw new Error('Pack must have a "name" field');
76
+
77
+ if (pack.socketTypes) {
78
+ registerSocketTypes(pack.socketTypes);
79
+ }
80
+
81
+ for (const nodeDef of pack.nodes) {
82
+ registerNodeType(nodeDef);
83
+ }
84
+
85
+ _packs.set(pack.name, pack);
86
+ }
87
+
88
+ /**
89
+ * Get a node type definition
90
+ * @param {string} type
91
+ * @returns {NodeTypeDef|undefined}
92
+ */
93
+ export function getNodeType(type) {
94
+ return _nodeTypes.get(type);
95
+ }
96
+
97
+ /**
98
+ * List all registered node types
99
+ * @returns {Array<{type: string, driver: Driver}>}
100
+ */
101
+ export function listDrivers() {
102
+ return [..._nodeTypes.values()].map(def => ({
103
+ type: def.type,
104
+ driver: def.driver,
105
+ icon: def.icon,
106
+ category: def.category,
107
+ }));
108
+ }
109
+
110
+ /**
111
+ * Find node types compatible with a given output type
112
+ * @param {string} outputType - Socket type to match against inputs
113
+ * @returns {Array<{type: string, inputSocket: string, driver: Driver}>}
114
+ */
115
+ export function findCompatible(outputType) {
116
+ const results = [];
117
+ for (const [type, def] of _nodeTypes) {
118
+ for (const input of def.driver.inputs) {
119
+ if (areSocketsCompatible(outputType, input.type)) {
120
+ results.push({ type, inputSocket: input.name, driver: def.driver });
121
+ }
122
+ }
123
+ }
124
+ return results;
125
+ }
126
+
127
+ /**
128
+ * Find node types by capability tag
129
+ * @param {string} capability
130
+ * @returns {Array<{type: string, driver: Driver}>}
131
+ */
132
+ export function findByCapability(capability) {
133
+ return [..._nodeTypes.values()]
134
+ .filter(def => def.driver.capabilities?.includes(capability))
135
+ .map(def => ({ type: def.type, driver: def.driver }));
136
+ }
137
+
138
+ /**
139
+ * Get node menu grouped by category (for UI context menus)
140
+ * @returns {Array<{category: string, nodes: Array<{type: string, icon: string, description: string}>}>}
141
+ */
142
+ export function getNodeMenu() {
143
+ const grouped = {};
144
+ for (const [type, def] of _nodeTypes) {
145
+ const cat = def.category || type.split('/')[0];
146
+ if (!grouped[cat]) grouped[cat] = [];
147
+ grouped[cat].push({
148
+ type,
149
+ icon: def.icon,
150
+ description: def.driver.description,
151
+ });
152
+ }
153
+ return Object.entries(grouped).map(([category, nodes]) => ({ category, nodes }));
154
+ }
155
+
156
+ /**
157
+ * Register custom drivers from workflow JSON (AI-generated nodes)
158
+ * @param {Array<{type: string, driver: Driver, process: string}>} customDrivers
159
+ */
160
+ export function registerCustomDrivers(customDrivers) {
161
+ for (const cd of customDrivers) {
162
+ registerNodeType({
163
+ type: cd.type,
164
+ driver: cd.driver,
165
+ category: cd.type.split('/')[0],
166
+ // Process stored as string in JSON — needs eval (sandboxed in production)
167
+ process: cd.process ? new Function('inputs', 'params', cd.process) : null,
168
+ });
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Validate node params against driver schema
174
+ * @param {string} type - Node type
175
+ * @param {object} params - Params to validate
176
+ * @returns {{valid: boolean, errors: string[]}}
177
+ */
178
+ export function validateParams(type, params) {
179
+ const def = _nodeTypes.get(type);
180
+ if (!def) return { valid: false, errors: [`Unknown node type: ${type}`] };
181
+
182
+ const validationErrors = [];
183
+ const schema = def.driver.params || {};
184
+
185
+ for (const [key, paramDef] of Object.entries(schema)) {
186
+ const val = params[key];
187
+
188
+ if (paramDef.required && (val === undefined || val === null)) {
189
+ validationErrors.push(`Missing required param: ${key}`);
190
+ continue;
191
+ }
192
+
193
+ if (val === undefined) continue;
194
+
195
+ if (paramDef.enum && !paramDef.enum.includes(val)) {
196
+ validationErrors.push(`Param "${key}" must be one of: ${paramDef.enum.join(', ')}. Got: ${val}`);
197
+ }
198
+
199
+ if (paramDef.min !== undefined && val < paramDef.min) {
200
+ validationErrors.push(`Param "${key}" must be >= ${paramDef.min}. Got: ${val}`);
201
+ }
202
+
203
+ if (paramDef.max !== undefined && val > paramDef.max) {
204
+ validationErrors.push(`Param "${key}" must be <= ${paramDef.max}. Got: ${val}`);
205
+ }
206
+ }
207
+
208
+ return { valid: validationErrors.length === 0, errors: validationErrors };
209
+ }
210
+
211
+ /**
212
+ * Get all registered packs
213
+ * @returns {string[]}
214
+ */
215
+ export function listPacks() {
216
+ return [..._packs.keys()];
217
+ }
218
+
219
+ /**
220
+ * Clear all registrations (for testing)
221
+ */
222
+ export function clearRegistry() {
223
+ _nodeTypes.clear();
224
+ _packs.clear();
225
+ // Re-register built-in types
226
+ _registerBuiltins();
227
+ }
228
+
229
+ /**
230
+ * Register built-in node types (compound infrastructure)
231
+ * @private
232
+ */
233
+ function _registerBuiltins() {
234
+ _nodeTypes.set('compound/input', {
235
+ type: 'compound/input',
236
+ category: 'compound',
237
+ icon: 'input',
238
+ driver: {
239
+ description: 'Input bridge for compound nodes — receives data from parent graph',
240
+ capabilities: ['compound'],
241
+ inputs: [],
242
+ outputs: [{ name: 'data', type: 'any' }],
243
+ params: {},
244
+ },
245
+ process: (_inputs, params) => ({ ...params }),
246
+ });
247
+
248
+ _nodeTypes.set('compound/output', {
249
+ type: 'compound/output',
250
+ category: 'compound',
251
+ icon: 'output',
252
+ driver: {
253
+ description: 'Output bridge for compound nodes — sends data to parent graph',
254
+ capabilities: ['compound'],
255
+ inputs: [{ name: 'data', type: 'any' }],
256
+ outputs: [{ name: 'data', type: 'any' }],
257
+ params: {},
258
+ },
259
+ process: (inputs) => ({ ...inputs }),
260
+ });
261
+ }
262
+
263
+ // Register built-ins on module load
264
+ _registerBuiltins();