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.
Files changed (54) hide show
  1. package/package.json +1 -1
  2. package/vendor/symbiote-node/engine/AgentUICommands.js +100 -0
  3. package/vendor/symbiote-node/engine/Executor.js +371 -0
  4. package/vendor/symbiote-node/engine/Graph.js +314 -0
  5. package/vendor/symbiote-node/engine/GraphServer.js +353 -0
  6. package/vendor/symbiote-node/engine/HandlerLoader.js +145 -0
  7. package/vendor/symbiote-node/engine/History.js +83 -0
  8. package/vendor/symbiote-node/engine/Lifecycle.js +118 -0
  9. package/vendor/symbiote-node/engine/Persistence.js +84 -0
  10. package/vendor/symbiote-node/engine/Registry.js +264 -0
  11. package/vendor/symbiote-node/engine/SocketTypes.js +79 -0
  12. package/vendor/symbiote-node/engine/cli.js +404 -0
  13. package/vendor/symbiote-node/engine/index.js +56 -0
  14. package/vendor/symbiote-node/engine/nanoid.js +28 -0
  15. package/vendor/symbiote-node/engine/package.json +26 -0
  16. package/vendor/symbiote-node/engine/packs/ai/beat-detect.handler.js +215 -0
  17. package/vendor/symbiote-node/engine/packs/ai/content-adapt.handler.js +238 -0
  18. package/vendor/symbiote-node/engine/packs/ai/face-detect.handler.js +287 -0
  19. package/vendor/symbiote-node/engine/packs/ai/grok-generate.handler.js +565 -0
  20. package/vendor/symbiote-node/engine/packs/ai/kling-lipsync.handler.js +414 -0
  21. package/vendor/symbiote-node/engine/packs/ai/lesson-generate.handler.js +343 -0
  22. package/vendor/symbiote-node/engine/packs/ai/opencode.handler.js +164 -0
  23. package/vendor/symbiote-node/engine/packs/ai/replicate-lipsync.handler.js +341 -0
  24. package/vendor/symbiote-node/engine/packs/ai/tts.handler.js +241 -0
  25. package/vendor/symbiote-node/engine/packs/ai/whisper.handler.js +191 -0
  26. package/vendor/symbiote-node/engine/packs/data/db-query.handler.js +67 -0
  27. package/vendor/symbiote-node/engine/packs/data/news-accumulate.handler.js +281 -0
  28. package/vendor/symbiote-node/engine/packs/data/personas.handler.js +160 -0
  29. package/vendor/symbiote-node/engine/packs/data/prompt-loader.handler.js +193 -0
  30. package/vendor/symbiote-node/engine/packs/data/roles.handler.js +216 -0
  31. package/vendor/symbiote-node/engine/packs/data/rss-feed.handler.js +244 -0
  32. package/vendor/symbiote-node/engine/packs/debug/inject.handler.js +52 -0
  33. package/vendor/symbiote-node/engine/packs/flow/agent.handler.js +73 -0
  34. package/vendor/symbiote-node/engine/packs/flow/if.handler.js +107 -0
  35. package/vendor/symbiote-node/engine/packs/flow/loop.handler.js +58 -0
  36. package/vendor/symbiote-node/engine/packs/flow/merge.handler.js +60 -0
  37. package/vendor/symbiote-node/engine/packs/flow/retry.handler.js +65 -0
  38. package/vendor/symbiote-node/engine/packs/flow/switch.handler.js +64 -0
  39. package/vendor/symbiote-node/engine/packs/flow/wait-all.handler.js +39 -0
  40. package/vendor/symbiote-node/engine/packs/io/http-request.handler.js +82 -0
  41. package/vendor/symbiote-node/engine/packs/io/read-file.handler.js +60 -0
  42. package/vendor/symbiote-node/engine/packs/io/write-file.handler.js +63 -0
  43. package/vendor/symbiote-node/engine/packs/transform/anchor-match.handler.js +494 -0
  44. package/vendor/symbiote-node/engine/packs/transform/effects-skeleton.handler.js +417 -0
  45. package/vendor/symbiote-node/engine/packs/transform/json-parse.handler.js +43 -0
  46. package/vendor/symbiote-node/engine/packs/transform/lipsync-select.handler.js +339 -0
  47. package/vendor/symbiote-node/engine/packs/transform/riopla-adapt.handler.js +432 -0
  48. package/vendor/symbiote-node/engine/packs/transform/set.handler.js +57 -0
  49. package/vendor/symbiote-node/engine/packs/transform/template-builder.handler.js +134 -0
  50. package/vendor/symbiote-node/engine/packs/transform/template.handler.js +79 -0
  51. package/vendor/symbiote-node/engine/packs/transform/timeline-build.handler.js +399 -0
  52. package/vendor/symbiote-node/engine/packs/util/delay.handler.js +39 -0
  53. package/vendor/symbiote-node/engine/packs/util/log.handler.js +44 -0
  54. package/vendor/symbiote-node/engine/packs/video-pack.js +323 -0
@@ -0,0 +1,79 @@
1
+ /**
2
+ * transform/template — String template interpolation
3
+ *
4
+ * Replaces {{variable}} placeholders in template string with values from data object.
5
+ * Supports nested access via dot notation: {{user.name}}.
6
+ *
7
+ * @module symbiote-node/packs/transform/template
8
+ */
9
+
10
+ export default {
11
+ type: 'transform/template',
12
+ category: 'transform',
13
+ icon: 'text_snippet',
14
+
15
+ driver: {
16
+ description: 'Template interpolation — replace {{var}} with data values',
17
+ inputs: [
18
+ { name: 'template', type: 'string' },
19
+ { name: 'data', type: 'any' },
20
+ ],
21
+ outputs: [
22
+ { name: 'result', type: 'string' },
23
+ { name: 'data', type: 'any' },
24
+ ],
25
+ params: {
26
+ template: { type: 'textarea', default: '', description: 'Message template ({{var}} syntax)' },
27
+ replyMarkup: { type: 'textarea', default: '', description: 'Inline keyboard JSON (Telegram reply_markup)' },
28
+ },
29
+ },
30
+
31
+ lifecycle: {
32
+ // No validate: template comes from params.template or inputs.template
33
+ // Execute handles both cases
34
+
35
+ cacheKey: (inputs) =>
36
+ `tpl:${inputs.template}:${JSON.stringify(inputs.data)}`,
37
+
38
+ execute: async (inputs, params) => {
39
+ const template = params?.template || inputs.template;
40
+ const { data } = inputs;
41
+
42
+ const result = template.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
43
+ const trimmed = key.trim();
44
+ // Support dot notation: {{user.name}}
45
+ const value = trimmed.split('.').reduce((obj, k) => {
46
+ if (obj === null || obj === undefined) return undefined;
47
+ return obj[k];
48
+ }, data);
49
+
50
+ if (value === undefined) {
51
+ console.warn(`[template] ⚠️ Missing variable "${trimmed}" in data keys: [${data ? Object.keys(data).join(', ') : 'NO DATA'}]`);
52
+ return match;
53
+ }
54
+ if (typeof value === 'object') return JSON.stringify(value);
55
+ return String(value);
56
+ });
57
+
58
+ // Output rendered text in both formats:
59
+ // - result: raw string (for chaining)
60
+ // - data: full context with text field (for telegram/chat)
61
+ const outputField = params?.outputField || 'text';
62
+ const outputData = { ...(typeof data === 'object' ? data : {}), [outputField]: result };
63
+
64
+ // Attach inline keyboard if configured
65
+ if (params?.replyMarkup) {
66
+ try {
67
+ outputData.reply_markup = JSON.parse(params.replyMarkup);
68
+ } catch (e) {
69
+ console.warn('[template] ⚠️ Invalid replyMarkup JSON:', e.message);
70
+ }
71
+ }
72
+
73
+ return {
74
+ result,
75
+ data: outputData,
76
+ };
77
+ },
78
+ },
79
+ };
@@ -0,0 +1,399 @@
1
+ /**
2
+ * transform/timeline-build — Whisper + beats → timeline segments
3
+ *
4
+ * Combines word timestamps from ai/whisper with beat data from ai/beat-detect
5
+ * to produce a continuous timeline of segments with 100% coverage.
6
+ *
7
+ * Core logic:
8
+ * 1. Build phrases from whisper words (punctuation-based splitting)
9
+ * 2. Fill gaps between phrases with beat-snapped segments
10
+ * 3. Enforce minimum/maximum segment duration (merge/split)
11
+ * 4. Calculate coverage statistics
12
+ *
13
+ * Simplified port of TimelineGenerator from
14
+ * Mr-Computer/modules/ai-music-video/src/services/timeline-generator.js
15
+ *
16
+ * @module agi-graph/packs/transform/timeline-build
17
+ */
18
+
19
+ export default {
20
+ type: 'transform/timeline-build',
21
+ category: 'transform',
22
+ icon: 'view_timeline',
23
+
24
+ driver: {
25
+ description: 'Whisper words + beat data → timeline segments with 100% coverage',
26
+ inputs: [
27
+ { name: 'whisperData', type: 'any' },
28
+ { name: 'beatData', type: 'any' },
29
+ ],
30
+ outputs: [
31
+ { name: 'segments', type: 'any' },
32
+ { name: 'stats', type: 'any' },
33
+ { name: 'error', type: 'string' },
34
+ ],
35
+ params: {
36
+ minSegmentDuration: { type: 'number', default: 1.8, description: 'Min segment duration (seconds)' },
37
+ maxSegmentDuration: { type: 'number', default: 5.0, description: 'Max segment duration (seconds)' },
38
+ shortMergeThreshold: { type: 'number', default: 1.2, description: 'Merge lyrics segments shorter than this' },
39
+ gapType: { type: 'string', default: 'beat', description: 'Type label for gap-fill segments' },
40
+ },
41
+ },
42
+
43
+ lifecycle: {
44
+ validate: (inputs) => {
45
+ if (!inputs.whisperData) return false;
46
+ return true;
47
+ },
48
+
49
+ cacheKey: (inputs, params) => {
50
+ const wd = inputs.whisperData;
51
+ const bd = inputs.beatData;
52
+ return `timeline:${wd.duration || 0}:${bd?.tempo || 0}:${params.minSegmentDuration}`;
53
+ },
54
+
55
+ execute: async (inputs, params) => {
56
+ try {
57
+ const { whisperData, beatData } = inputs;
58
+ const words = whisperData.words || [];
59
+ const duration = whisperData.duration || beatData?.duration || 0;
60
+ const beats = beatData?.beats || [];
61
+
62
+ if (words.length === 0) {
63
+ return { segments: null, stats: null, error: 'No whisper words provided' };
64
+ }
65
+
66
+ // Step 1: Build phrases from words
67
+ let segments = buildPhrases(words);
68
+
69
+ // Step 2: Fill gaps with beat-snapped segments
70
+ segments = fillGaps(segments, beats, duration, params);
71
+
72
+ // Step 3: Remove overlaps
73
+ segments = removeOverlaps(segments);
74
+
75
+ // Step 4: Merge short segments
76
+ segments = mergeShort(segments, params.shortMergeThreshold || 1.2);
77
+
78
+ // Step 5: Enforce min duration
79
+ segments = enforceMinDuration(segments, params.minSegmentDuration || 1.8);
80
+
81
+ // Step 6: Cap max duration
82
+ segments = capMaxDuration(segments, params.maxSegmentDuration || 5.0);
83
+
84
+ // Sort final
85
+ segments.sort((a, b) => a.start - b.start);
86
+
87
+ // Stats
88
+ const stats = calculateStats(segments, duration);
89
+
90
+ return { segments, stats, error: null };
91
+ } catch (err) {
92
+ return { segments: null, stats: null, error: err.message };
93
+ }
94
+ },
95
+ },
96
+ };
97
+
98
+ /**
99
+ * Build phrases from whisper words using punctuation-based splitting
100
+ * @param {Array<{word: string, start: number, end: number}>} words
101
+ * @returns {Array<{start: number, end: number, text: string, type: string, wordCount: number}>}
102
+ */
103
+ function buildPhrases(words) {
104
+ const phrases = [];
105
+ let current = null;
106
+
107
+ for (const w of words) {
108
+ if (!current) {
109
+ current = {
110
+ start: w.start,
111
+ end: w.end,
112
+ words: [w.word],
113
+ type: 'lyrics',
114
+ };
115
+ } else {
116
+ current.end = w.end;
117
+ current.words.push(w.word);
118
+ }
119
+
120
+ // Split on sentence endings, commas, or long pauses
121
+ const endsWithPunct = /[.!?;]$/.test(w.word);
122
+ const nextWord = words[words.indexOf(w) + 1];
123
+ const hasGap = nextWord && (nextWord.start - w.end > 0.8);
124
+
125
+ if (endsWithPunct || hasGap || current.words.length >= 12) {
126
+ phrases.push({
127
+ start: current.start,
128
+ end: current.end,
129
+ text: current.words.join(' '),
130
+ type: 'lyrics',
131
+ wordCount: current.words.length,
132
+ });
133
+ current = null;
134
+ }
135
+ }
136
+
137
+ // Close last phrase
138
+ if (current && current.words.length > 0) {
139
+ phrases.push({
140
+ start: current.start,
141
+ end: current.end,
142
+ text: current.words.join(' '),
143
+ type: 'lyrics',
144
+ wordCount: current.words.length,
145
+ });
146
+ }
147
+
148
+ return phrases;
149
+ }
150
+
151
+ /**
152
+ * Snap a time to the nearest beat
153
+ * @param {number} time - Seconds
154
+ * @param {number[]} beats - Beat timestamps
155
+ * @returns {number} Snapped time
156
+ */
157
+ function snapToBeat(time, beats) {
158
+ if (!beats || beats.length === 0) return time;
159
+
160
+ let closest = beats[0];
161
+ let minDist = Math.abs(beats[0] - time);
162
+
163
+ for (const beat of beats) {
164
+ const dist = Math.abs(beat - time);
165
+ if (dist < minDist) {
166
+ minDist = dist;
167
+ closest = beat;
168
+ }
169
+ if (beat > time + minDist) break;
170
+ }
171
+
172
+ // Only snap if within 0.3s of a beat
173
+ return minDist < 0.3 ? closest : time;
174
+ }
175
+
176
+ /**
177
+ * Fill gaps between segments with beat-snapped segments
178
+ * @param {Array} segments
179
+ * @param {number[]} beats
180
+ * @param {number} duration
181
+ * @param {Object} params
182
+ * @returns {Array}
183
+ */
184
+ function fillGaps(segments, beats, duration, params) {
185
+ if (segments.length === 0) return segments;
186
+
187
+ const result = [];
188
+ const gapType = params.gapType || 'beat';
189
+
190
+ // Gap at start?
191
+ if (segments[0].start > 0.1) {
192
+ result.push({
193
+ start: 0,
194
+ end: snapToBeat(segments[0].start, beats),
195
+ text: '',
196
+ type: gapType,
197
+ wordCount: 0,
198
+ });
199
+ }
200
+
201
+ for (let i = 0; i < segments.length; i++) {
202
+ result.push(segments[i]);
203
+
204
+ // Gap to next segment?
205
+ const next = segments[i + 1];
206
+ if (next) {
207
+ const gapStart = segments[i].end;
208
+ const gapEnd = next.start;
209
+ const gapSize = gapEnd - gapStart;
210
+
211
+ if (gapSize > 0.2) {
212
+ result.push({
213
+ start: snapToBeat(gapStart, beats),
214
+ end: snapToBeat(gapEnd, beats),
215
+ text: '',
216
+ type: gapType,
217
+ wordCount: 0,
218
+ });
219
+ }
220
+ }
221
+ }
222
+
223
+ // Gap at end?
224
+ const lastEnd = segments[segments.length - 1].end;
225
+ if (duration > 0 && duration - lastEnd > 0.2) {
226
+ result.push({
227
+ start: snapToBeat(lastEnd, beats),
228
+ end: duration,
229
+ text: '',
230
+ type: gapType,
231
+ wordCount: 0,
232
+ });
233
+ }
234
+
235
+ return result;
236
+ }
237
+
238
+ /**
239
+ * Remove overlapping segments (trim shorter one)
240
+ * @param {Array} segments
241
+ * @returns {Array}
242
+ */
243
+ function removeOverlaps(segments) {
244
+ if (segments.length < 2) return segments;
245
+
246
+ segments.sort((a, b) => a.start - b.start);
247
+
248
+ for (let i = 1; i < segments.length; i++) {
249
+ const prev = segments[i - 1];
250
+ const curr = segments[i];
251
+
252
+ if (curr.start < prev.end) {
253
+ // Overlap: trim the gap segment, or split at midpoint
254
+ if (prev.type !== 'lyrics' && curr.type === 'lyrics') {
255
+ prev.end = curr.start;
256
+ } else if (prev.type === 'lyrics' && curr.type !== 'lyrics') {
257
+ curr.start = prev.end;
258
+ } else {
259
+ const mid = (prev.end + curr.start) / 2;
260
+ prev.end = mid;
261
+ curr.start = mid;
262
+ }
263
+ }
264
+ }
265
+
266
+ // Remove zero/negative duration segments
267
+ return segments.filter(s => s.end - s.start > 0.05);
268
+ }
269
+
270
+ /**
271
+ * Merge short lyrics segments into neighbors
272
+ * @param {Array} segments
273
+ * @param {number} threshold
274
+ * @returns {Array}
275
+ */
276
+ function mergeShort(segments, threshold) {
277
+ if (segments.length < 2) return segments;
278
+
279
+ const result = [segments[0]];
280
+
281
+ for (let i = 1; i < segments.length; i++) {
282
+ const curr = segments[i];
283
+ const prev = result[result.length - 1];
284
+ const currDuration = curr.end - curr.start;
285
+
286
+ // Merge short lyrics into previous
287
+ if (curr.type === 'lyrics' && currDuration < threshold && prev.type === 'lyrics') {
288
+ prev.end = curr.end;
289
+ prev.text = prev.text + ' ' + curr.text;
290
+ prev.wordCount = (prev.wordCount || 0) + (curr.wordCount || 0);
291
+ } else {
292
+ result.push(curr);
293
+ }
294
+ }
295
+
296
+ return result;
297
+ }
298
+
299
+ /**
300
+ * Enforce minimum duration by merging
301
+ * @param {Array} segments
302
+ * @param {number} minDuration
303
+ * @returns {Array}
304
+ */
305
+ function enforceMinDuration(segments, minDuration) {
306
+ if (segments.length < 2) return segments;
307
+
308
+ const result = [segments[0]];
309
+
310
+ for (let i = 1; i < segments.length; i++) {
311
+ const prev = result[result.length - 1];
312
+ const prevDuration = prev.end - prev.start;
313
+
314
+ if (prevDuration < minDuration) {
315
+ // Extend previous to absorb current
316
+ prev.end = segments[i].end;
317
+ if (segments[i].text) {
318
+ prev.text = (prev.text ? prev.text + ' ' : '') + segments[i].text;
319
+ }
320
+ if (segments[i].type === 'lyrics') prev.type = 'lyrics';
321
+ } else {
322
+ result.push(segments[i]);
323
+ }
324
+ }
325
+
326
+ return result;
327
+ }
328
+
329
+ /**
330
+ * Cap segments at max duration by splitting evenly
331
+ * @param {Array} segments
332
+ * @param {number} maxDuration
333
+ * @returns {Array}
334
+ */
335
+ function capMaxDuration(segments, maxDuration) {
336
+ const result = [];
337
+
338
+ for (const seg of segments) {
339
+ const duration = seg.end - seg.start;
340
+
341
+ if (duration <= maxDuration) {
342
+ result.push(seg);
343
+ continue;
344
+ }
345
+
346
+ // Split evenly
347
+ const parts = Math.ceil(duration / maxDuration);
348
+ const partDuration = duration / parts;
349
+
350
+ for (let i = 0; i < parts; i++) {
351
+ result.push({
352
+ ...seg,
353
+ start: seg.start + i * partDuration,
354
+ end: seg.start + (i + 1) * partDuration,
355
+ text: i === 0 ? seg.text : '',
356
+ _splitPart: i + 1,
357
+ _splitTotal: parts,
358
+ });
359
+ }
360
+ }
361
+
362
+ return result;
363
+ }
364
+
365
+ /**
366
+ * Calculate timeline coverage statistics
367
+ * @param {Array} segments
368
+ * @param {number} audioDuration
369
+ * @returns {Object}
370
+ */
371
+ function calculateStats(segments, audioDuration) {
372
+ const totalSegments = segments.length;
373
+ const lyricsSegments = segments.filter(s => s.type === 'lyrics').length;
374
+ const gapSegments = totalSegments - lyricsSegments;
375
+
376
+ const coveredDuration = segments.reduce((sum, s) => sum + (s.end - s.start), 0);
377
+ const lyricsDuration = segments
378
+ .filter(s => s.type === 'lyrics')
379
+ .reduce((sum, s) => sum + (s.end - s.start), 0);
380
+
381
+ const coverage = audioDuration > 0
382
+ ? Math.round((coveredDuration / audioDuration) * 100)
383
+ : 0;
384
+
385
+ const avgDuration = totalSegments > 0
386
+ ? Math.round((coveredDuration / totalSegments) * 100) / 100
387
+ : 0;
388
+
389
+ return {
390
+ totalSegments,
391
+ lyricsSegments,
392
+ gapSegments,
393
+ coveredDuration: Math.round(coveredDuration * 100) / 100,
394
+ audioDuration: Math.round(audioDuration * 100) / 100,
395
+ coverage,
396
+ lyricsDuration: Math.round(lyricsDuration * 100) / 100,
397
+ avgDuration,
398
+ };
399
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * util/delay — Pause pipeline execution
3
+ *
4
+ * Waits for specified milliseconds, then passes input value through.
5
+ * Useful for rate limiting, animation timing, and API cooldowns.
6
+ *
7
+ * @module agi-graph/packs/util/delay
8
+ */
9
+
10
+ export default {
11
+ type: 'util/delay',
12
+ category: 'util',
13
+ icon: 'hourglass_empty',
14
+
15
+ driver: {
16
+ description: 'Pause execution for N milliseconds, pass value through',
17
+ inputs: [
18
+ { name: 'value', type: 'any' },
19
+ ],
20
+ outputs: [
21
+ { name: 'value', type: 'any' },
22
+ ],
23
+ params: {
24
+ ms: { type: 'int', default: 1000, description: 'Delay in milliseconds' },
25
+ },
26
+ },
27
+
28
+ lifecycle: {
29
+ validate: () => true,
30
+
31
+ // Never cache delays
32
+ cacheKey: null,
33
+
34
+ execute: async (inputs, params) => {
35
+ await new Promise(resolve => setTimeout(resolve, params.ms || 1000));
36
+ return { value: inputs.value };
37
+ },
38
+ },
39
+ };
@@ -0,0 +1,44 @@
1
+ /**
2
+ * util/log — Console logger passthrough
3
+ *
4
+ * Logs input value to console and passes it through unchanged.
5
+ * Useful for debugging pipelines.
6
+ *
7
+ * @module symbiote-node/packs/util/log */
8
+
9
+ export default {
10
+ type: 'util/log',
11
+ category: 'util',
12
+ icon: 'terminal',
13
+
14
+ driver: {
15
+ description: 'Log value to console and pass through',
16
+ inputs: [
17
+ { name: 'value', type: 'any' },
18
+ ],
19
+ outputs: [
20
+ { name: 'value', type: 'any' },
21
+ ],
22
+ params: {
23
+ label: { type: 'string', default: '', description: 'Log label prefix' },
24
+ level: { type: 'string', default: 'info', description: 'Log level: log | info | warn | error' },
25
+ },
26
+ },
27
+
28
+ lifecycle: {
29
+ validate: () => true,
30
+ cacheKey: null,
31
+
32
+ execute: async (inputs, params) => {
33
+ const label = params.label ? `[${params.label}]` : '[symbiote-node]'; const method = params.level || 'info';
34
+
35
+ const logFn = console[method] || console.log;
36
+ logFn(label, typeof inputs.value === 'object'
37
+ ? JSON.stringify(inputs.value, null, 2)
38
+ : inputs.value
39
+ );
40
+
41
+ return { value: inputs.value };
42
+ },
43
+ },
44
+ };