project-graph-mcp 2.3.1 → 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.
- package/package.json +1 -1
- package/vendor/symbiote-node/engine/AgentUICommands.js +100 -0
- package/vendor/symbiote-node/engine/Executor.js +371 -0
- package/vendor/symbiote-node/engine/Graph.js +314 -0
- package/vendor/symbiote-node/engine/GraphServer.js +353 -0
- package/vendor/symbiote-node/engine/HandlerLoader.js +145 -0
- package/vendor/symbiote-node/engine/History.js +83 -0
- package/vendor/symbiote-node/engine/Lifecycle.js +118 -0
- package/vendor/symbiote-node/engine/Persistence.js +84 -0
- package/vendor/symbiote-node/engine/Registry.js +264 -0
- package/vendor/symbiote-node/engine/SocketTypes.js +79 -0
- package/vendor/symbiote-node/engine/cli.js +404 -0
- package/vendor/symbiote-node/engine/index.js +56 -0
- package/vendor/symbiote-node/engine/nanoid.js +28 -0
- package/vendor/symbiote-node/engine/package.json +26 -0
- package/vendor/symbiote-node/engine/packs/ai/beat-detect.handler.js +215 -0
- package/vendor/symbiote-node/engine/packs/ai/content-adapt.handler.js +238 -0
- package/vendor/symbiote-node/engine/packs/ai/face-detect.handler.js +287 -0
- package/vendor/symbiote-node/engine/packs/ai/grok-generate.handler.js +565 -0
- package/vendor/symbiote-node/engine/packs/ai/kling-lipsync.handler.js +414 -0
- package/vendor/symbiote-node/engine/packs/ai/lesson-generate.handler.js +343 -0
- package/vendor/symbiote-node/engine/packs/ai/opencode.handler.js +164 -0
- package/vendor/symbiote-node/engine/packs/ai/replicate-lipsync.handler.js +341 -0
- package/vendor/symbiote-node/engine/packs/ai/tts.handler.js +241 -0
- package/vendor/symbiote-node/engine/packs/ai/whisper.handler.js +191 -0
- package/vendor/symbiote-node/engine/packs/data/db-query.handler.js +67 -0
- package/vendor/symbiote-node/engine/packs/data/news-accumulate.handler.js +281 -0
- package/vendor/symbiote-node/engine/packs/data/personas.handler.js +160 -0
- package/vendor/symbiote-node/engine/packs/data/prompt-loader.handler.js +193 -0
- package/vendor/symbiote-node/engine/packs/data/roles.handler.js +216 -0
- package/vendor/symbiote-node/engine/packs/data/rss-feed.handler.js +244 -0
- package/vendor/symbiote-node/engine/packs/debug/inject.handler.js +52 -0
- package/vendor/symbiote-node/engine/packs/flow/agent.handler.js +73 -0
- package/vendor/symbiote-node/engine/packs/flow/if.handler.js +107 -0
- package/vendor/symbiote-node/engine/packs/flow/loop.handler.js +58 -0
- package/vendor/symbiote-node/engine/packs/flow/merge.handler.js +60 -0
- package/vendor/symbiote-node/engine/packs/flow/retry.handler.js +65 -0
- package/vendor/symbiote-node/engine/packs/flow/switch.handler.js +64 -0
- package/vendor/symbiote-node/engine/packs/flow/wait-all.handler.js +39 -0
- package/vendor/symbiote-node/engine/packs/io/http-request.handler.js +82 -0
- package/vendor/symbiote-node/engine/packs/io/read-file.handler.js +60 -0
- package/vendor/symbiote-node/engine/packs/io/write-file.handler.js +63 -0
- package/vendor/symbiote-node/engine/packs/transform/anchor-match.handler.js +494 -0
- package/vendor/symbiote-node/engine/packs/transform/effects-skeleton.handler.js +417 -0
- package/vendor/symbiote-node/engine/packs/transform/json-parse.handler.js +43 -0
- package/vendor/symbiote-node/engine/packs/transform/lipsync-select.handler.js +339 -0
- package/vendor/symbiote-node/engine/packs/transform/riopla-adapt.handler.js +432 -0
- package/vendor/symbiote-node/engine/packs/transform/set.handler.js +57 -0
- package/vendor/symbiote-node/engine/packs/transform/template-builder.handler.js +134 -0
- package/vendor/symbiote-node/engine/packs/transform/template.handler.js +79 -0
- package/vendor/symbiote-node/engine/packs/transform/timeline-build.handler.js +399 -0
- package/vendor/symbiote-node/engine/packs/util/delay.handler.js +39 -0
- package/vendor/symbiote-node/engine/packs/util/log.handler.js +44 -0
- package/vendor/symbiote-node/engine/packs/video-pack.js +323 -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();
|