project-graph-mcp 2.3.2 → 2.4.0
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 +3 -2
- package/src/analysis/analysis-cache.ctx +9 -0
- package/src/analysis/analysis-cache.js +1 -1
- package/src/analysis/complexity.ctx +6 -0
- package/src/analysis/complexity.js +1 -1
- package/src/analysis/custom-rules.ctx +14 -0
- package/src/analysis/custom-rules.js +1 -1
- package/src/analysis/db-analysis.ctx +7 -0
- package/src/analysis/db-analysis.js +1 -1
- package/src/analysis/dead-code.ctx +6 -0
- package/src/analysis/dead-code.js +1 -1
- package/src/analysis/full-analysis.ctx +9 -0
- package/src/analysis/full-analysis.js +1 -1
- package/src/analysis/jsdoc-checker.ctx +10 -0
- package/src/analysis/jsdoc-checker.js +1 -1
- package/src/analysis/jsdoc-generator.ctx +9 -0
- package/src/analysis/jsdoc-generator.js +1 -1
- package/src/analysis/large-files.ctx +6 -0
- package/src/analysis/large-files.js +1 -1
- package/src/analysis/outdated-patterns.ctx +7 -0
- package/src/analysis/outdated-patterns.js +1 -1
- package/src/analysis/similar-functions.ctx +6 -0
- package/src/analysis/similar-functions.js +1 -1
- package/src/analysis/test-annotations.ctx +11 -0
- package/src/analysis/test-annotations.js +1 -1
- package/src/analysis/type-checker.ctx +6 -0
- package/src/analysis/type-checker.js +1 -1
- package/src/analysis/undocumented.ctx +8 -0
- package/src/analysis/undocumented.js +1 -1
- package/src/cli/cli-handlers.ctx +7 -0
- package/src/cli/cli-handlers.js +1 -1
- package/src/cli/cli.ctx +6 -0
- package/src/cli/cli.js +1 -1
- package/src/compact/ai-context.ctx +6 -0
- package/src/compact/ai-context.js +1 -1
- package/src/compact/compact-migrate.ctx +8 -0
- package/src/compact/compact-migrate.js +1 -1
- package/src/compact/compact.ctx +11 -0
- package/src/compact/compact.js +1 -1
- package/src/compact/compress.ctx +7 -0
- package/src/compact/compress.js +1 -1
- package/src/compact/ctx-resolver.ctx +2 -0
- package/src/compact/ctx-resolver.js +1 -1
- package/src/compact/ctx-to-jsdoc.ctx +11 -0
- package/src/compact/ctx-to-jsdoc.js +1 -1
- package/src/compact/doc-dialect.ctx +11 -0
- package/src/compact/doc-dialect.js +2 -2
- package/src/compact/expand.ctx +14 -0
- package/src/compact/expand.js +1 -1
- package/src/compact/framework-references.ctx +7 -0
- package/src/compact/framework-references.js +1 -1
- package/src/compact/instructions.ctx +6 -0
- package/src/compact/instructions.js +1 -1
- package/src/compact/jsdoc-builder.ctx +4 -0
- package/src/compact/jsdoc-builder.js +1 -1
- package/src/compact/mode-config.ctx +8 -0
- package/src/compact/mode-config.js +1 -1
- package/src/compact/split-declarations.ctx +6 -0
- package/src/compact/split-declarations.js +1 -1
- package/src/compact/validate-pipeline.ctx +12 -0
- package/src/compact/validate-pipeline.js +1 -1
- package/src/core/event-bus.ctx +9 -0
- package/src/core/event-bus.js +1 -1
- package/src/core/file-walker.ctx +1 -0
- package/src/core/file-walker.js +1 -1
- package/src/core/filters.ctx +12 -0
- package/src/core/filters.js +1 -1
- package/src/core/graph-builder.ctx +7 -0
- package/src/core/graph-builder.js +1 -1
- package/src/core/parser.ctx +12 -0
- package/src/core/parser.js +1 -1
- package/src/core/utils.ctx +1 -0
- package/src/core/utils.js +1 -1
- package/src/core/workspace.ctx +7 -0
- package/src/core/workspace.js +1 -1
- package/src/lang/lang-go.ctx +8 -0
- package/src/lang/lang-go.js +1 -1
- package/src/lang/lang-python.ctx +5 -0
- package/src/lang/lang-python.js +1 -1
- package/src/lang/lang-sql.ctx +10 -0
- package/src/lang/lang-sql.js +1 -1
- package/src/lang/lang-typescript.ctx +6 -0
- package/src/lang/lang-typescript.js +1 -1
- package/src/lang/lang-utils.ctx +5 -0
- package/src/lang/lang-utils.js +1 -1
- package/src/mcp/mcp-server.ctx +6 -0
- package/src/mcp/mcp-server.js +1 -1
- package/src/mcp/tool-defs.ctx +2 -0
- package/src/mcp/tool-defs.js +1 -1
- package/src/mcp/tools.ctx +13 -0
- package/src/mcp/tools.js +1 -1
- package/src/network/backend-lifecycle.ctx +10 -0
- package/src/network/backend-lifecycle.js +1 -1
- package/src/network/backend.ctx +5 -0
- package/src/network/backend.js +1 -1
- package/src/network/local-gateway.ctx +9 -0
- package/src/network/local-gateway.js +1 -1
- package/src/network/mdns.ctx +6 -0
- package/src/network/mdns.js +1 -1
- package/src/network/server.ctx +2 -0
- package/src/network/server.js +2 -2
- package/src/network/web-server.ctx +17 -0
- package/src/network/web-server.js +2 -2
- package/web/follow-controller.js +94 -25
- package/web/panels/dep-graph.js +207 -21
- package/project-graph-mcp-2.3.0.tgz +0 -0
- package/vendor/symbiote-node/CHANGELOG.md +0 -31
- package/vendor/symbiote-node/LICENSE +0 -21
- package/vendor/symbiote-node/README.md +0 -206
- package/vendor/symbiote-node/canvas/AutoLayout.js +0 -725
- package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.css.js +0 -73
- package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.js +0 -93
- package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.tpl.js +0 -9
- package/vendor/symbiote-node/canvas/CanvasConnectionRenderer.js +0 -962
- package/vendor/symbiote-node/canvas/ConnectionRenderer.js +0 -1468
- package/vendor/symbiote-node/canvas/FlowSimulator.js +0 -323
- package/vendor/symbiote-node/canvas/ForceLayout.js +0 -189
- package/vendor/symbiote-node/canvas/ForceWorker.js +0 -1325
- package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.css.js +0 -97
- package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.js +0 -176
- package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.tpl.js +0 -12
- package/vendor/symbiote-node/canvas/LODManager.js +0 -88
- package/vendor/symbiote-node/canvas/Minimap/Minimap.css.js +0 -71
- package/vendor/symbiote-node/canvas/Minimap/Minimap.js +0 -207
- package/vendor/symbiote-node/canvas/Minimap/Minimap.tpl.js +0 -9
- package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.css.js +0 -261
- package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.js +0 -1840
- package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.tpl.js +0 -22
- package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.css.js +0 -97
- package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.js +0 -132
- package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.tpl.js +0 -21
- package/vendor/symbiote-node/canvas/NodeViewManager.js +0 -584
- package/vendor/symbiote-node/canvas/PinExpansion.js +0 -131
- package/vendor/symbiote-node/canvas/PseudoConnection.js +0 -80
- package/vendor/symbiote-node/canvas/SubgraphManager.js +0 -201
- package/vendor/symbiote-node/canvas/SubgraphRouter.js +0 -443
- package/vendor/symbiote-node/canvas/ViewportActions.js +0 -446
- package/vendor/symbiote-node/core/Connection.js +0 -45
- package/vendor/symbiote-node/core/Editor.js +0 -451
- package/vendor/symbiote-node/core/Frame.js +0 -31
- package/vendor/symbiote-node/core/GraphMermaid.js +0 -348
- package/vendor/symbiote-node/core/GraphText.js +0 -210
- package/vendor/symbiote-node/core/Node.js +0 -143
- package/vendor/symbiote-node/core/Portal.js +0 -104
- package/vendor/symbiote-node/core/Socket.js +0 -185
- package/vendor/symbiote-node/core/SubgraphNode.js +0 -125
- package/vendor/symbiote-node/engine/AgentUICommands.js +0 -100
- package/vendor/symbiote-node/engine/Executor.js +0 -371
- package/vendor/symbiote-node/engine/Graph.js +0 -314
- package/vendor/symbiote-node/engine/GraphServer.js +0 -353
- package/vendor/symbiote-node/engine/HandlerLoader.js +0 -145
- package/vendor/symbiote-node/engine/History.js +0 -83
- package/vendor/symbiote-node/engine/Lifecycle.js +0 -118
- package/vendor/symbiote-node/engine/Persistence.js +0 -84
- package/vendor/symbiote-node/engine/Registry.js +0 -264
- package/vendor/symbiote-node/engine/SocketTypes.js +0 -79
- package/vendor/symbiote-node/engine/cli.js +0 -404
- package/vendor/symbiote-node/engine/index.js +0 -56
- package/vendor/symbiote-node/engine/nanoid.js +0 -28
- package/vendor/symbiote-node/engine/package.json +0 -26
- package/vendor/symbiote-node/engine/packs/ai/beat-detect.handler.js +0 -215
- package/vendor/symbiote-node/engine/packs/ai/content-adapt.handler.js +0 -238
- package/vendor/symbiote-node/engine/packs/ai/face-detect.handler.js +0 -287
- package/vendor/symbiote-node/engine/packs/ai/grok-generate.handler.js +0 -565
- package/vendor/symbiote-node/engine/packs/ai/kling-lipsync.handler.js +0 -414
- package/vendor/symbiote-node/engine/packs/ai/lesson-generate.handler.js +0 -343
- package/vendor/symbiote-node/engine/packs/ai/opencode.handler.js +0 -164
- package/vendor/symbiote-node/engine/packs/ai/replicate-lipsync.handler.js +0 -341
- package/vendor/symbiote-node/engine/packs/ai/tts.handler.js +0 -241
- package/vendor/symbiote-node/engine/packs/ai/whisper.handler.js +0 -191
- package/vendor/symbiote-node/engine/packs/data/db-query.handler.js +0 -67
- package/vendor/symbiote-node/engine/packs/data/news-accumulate.handler.js +0 -281
- package/vendor/symbiote-node/engine/packs/data/personas.handler.js +0 -160
- package/vendor/symbiote-node/engine/packs/data/prompt-loader.handler.js +0 -193
- package/vendor/symbiote-node/engine/packs/data/roles.handler.js +0 -216
- package/vendor/symbiote-node/engine/packs/data/rss-feed.handler.js +0 -244
- package/vendor/symbiote-node/engine/packs/debug/inject.handler.js +0 -52
- package/vendor/symbiote-node/engine/packs/flow/agent.handler.js +0 -73
- package/vendor/symbiote-node/engine/packs/flow/if.handler.js +0 -107
- package/vendor/symbiote-node/engine/packs/flow/loop.handler.js +0 -58
- package/vendor/symbiote-node/engine/packs/flow/merge.handler.js +0 -60
- package/vendor/symbiote-node/engine/packs/flow/retry.handler.js +0 -65
- package/vendor/symbiote-node/engine/packs/flow/switch.handler.js +0 -64
- package/vendor/symbiote-node/engine/packs/flow/wait-all.handler.js +0 -39
- package/vendor/symbiote-node/engine/packs/io/http-request.handler.js +0 -82
- package/vendor/symbiote-node/engine/packs/io/read-file.handler.js +0 -60
- package/vendor/symbiote-node/engine/packs/io/write-file.handler.js +0 -63
- package/vendor/symbiote-node/engine/packs/transform/anchor-match.handler.js +0 -494
- package/vendor/symbiote-node/engine/packs/transform/effects-skeleton.handler.js +0 -417
- package/vendor/symbiote-node/engine/packs/transform/json-parse.handler.js +0 -43
- package/vendor/symbiote-node/engine/packs/transform/lipsync-select.handler.js +0 -339
- package/vendor/symbiote-node/engine/packs/transform/riopla-adapt.handler.js +0 -432
- package/vendor/symbiote-node/engine/packs/transform/set.handler.js +0 -57
- package/vendor/symbiote-node/engine/packs/transform/template-builder.handler.js +0 -134
- package/vendor/symbiote-node/engine/packs/transform/template.handler.js +0 -79
- package/vendor/symbiote-node/engine/packs/transform/timeline-build.handler.js +0 -399
- package/vendor/symbiote-node/engine/packs/util/delay.handler.js +0 -39
- package/vendor/symbiote-node/engine/packs/util/log.handler.js +0 -44
- package/vendor/symbiote-node/engine/packs/video-pack.js +0 -323
- package/vendor/symbiote-node/index.js +0 -103
- package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.css.js +0 -361
- package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.js +0 -332
- package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.tpl.js +0 -96
- package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.css.js +0 -104
- package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.js +0 -133
- package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.tpl.js +0 -33
- package/vendor/symbiote-node/interactions/ConnectFlow.js +0 -307
- package/vendor/symbiote-node/interactions/Drag.js +0 -102
- package/vendor/symbiote-node/interactions/Selector.js +0 -132
- package/vendor/symbiote-node/interactions/SnapGrid.js +0 -65
- package/vendor/symbiote-node/interactions/Zoom.js +0 -140
- package/vendor/symbiote-node/layout/ActionZone/ActionZone.css.js +0 -88
- package/vendor/symbiote-node/layout/ActionZone/ActionZone.js +0 -254
- package/vendor/symbiote-node/layout/ActionZone/ActionZone.tpl.js +0 -11
- package/vendor/symbiote-node/layout/Layout/Layout.css.js +0 -88
- package/vendor/symbiote-node/layout/Layout/Layout.js +0 -622
- package/vendor/symbiote-node/layout/Layout/Layout.tpl.js +0 -25
- package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.css.js +0 -293
- package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.js +0 -467
- package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.tpl.js +0 -33
- package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.css.js +0 -46
- package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.js +0 -102
- package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.tpl.js +0 -6
- package/vendor/symbiote-node/layout/LayoutRouter/LayoutRouter.js +0 -156
- package/vendor/symbiote-node/layout/LayoutRouter/routerSync.js +0 -250
- package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.css.js +0 -379
- package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.js +0 -263
- package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.tpl.js +0 -20
- package/vendor/symbiote-node/layout/LayoutSidebar/SidebarSection.js +0 -183
- package/vendor/symbiote-node/layout/LayoutTree.js +0 -246
- package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.css.js +0 -43
- package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.js +0 -89
- package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.tpl.js +0 -14
- package/vendor/symbiote-node/layout/index.js +0 -16
- package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.css.js +0 -61
- package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.js +0 -79
- package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.tpl.js +0 -19
- package/vendor/symbiote-node/node/CtrlItem/CtrlItem.css.js +0 -41
- package/vendor/symbiote-node/node/CtrlItem/CtrlItem.js +0 -24
- package/vendor/symbiote-node/node/CtrlItem/CtrlItem.tpl.js +0 -16
- package/vendor/symbiote-node/node/GraphFrame/GraphFrame.css.js +0 -65
- package/vendor/symbiote-node/node/GraphFrame/GraphFrame.js +0 -29
- package/vendor/symbiote-node/node/GraphFrame/GraphFrame.tpl.js +0 -13
- package/vendor/symbiote-node/node/GraphNode/GraphNode.css.js +0 -683
- package/vendor/symbiote-node/node/GraphNode/GraphNode.js +0 -92
- package/vendor/symbiote-node/node/GraphNode/GraphNode.tpl.js +0 -17
- package/vendor/symbiote-node/node/NodeSocket/NodeSocket.js +0 -25
- package/vendor/symbiote-node/node/NodeSocket/NodeSocket.tpl.js +0 -7
- package/vendor/symbiote-node/node/PortItem/PortItem.css.js +0 -90
- package/vendor/symbiote-node/node/PortItem/PortItem.js +0 -87
- package/vendor/symbiote-node/node/PortItem/PortItem.tpl.js +0 -10
- package/vendor/symbiote-node/package.json +0 -59
- package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.css.js +0 -143
- package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.js +0 -131
- package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.tpl.js +0 -16
- package/vendor/symbiote-node/plugins/History.js +0 -384
- package/vendor/symbiote-node/plugins/Readonly.js +0 -59
- package/vendor/symbiote-node/shapes/CircleShape.js +0 -80
- package/vendor/symbiote-node/shapes/CommentShape.js +0 -35
- package/vendor/symbiote-node/shapes/DiamondShape.js +0 -115
- package/vendor/symbiote-node/shapes/NodeShape.js +0 -80
- package/vendor/symbiote-node/shapes/PillShape.js +0 -91
- package/vendor/symbiote-node/shapes/RectShape.js +0 -72
- package/vendor/symbiote-node/shapes/SVGShape.js +0 -494
- package/vendor/symbiote-node/shapes/index.js +0 -53
- package/vendor/symbiote-node/themes/Palette.js +0 -32
- package/vendor/symbiote-node/themes/Skin.js +0 -113
- package/vendor/symbiote-node/themes/Theme.js +0 -84
- package/vendor/symbiote-node/themes/carbon.js +0 -137
- package/vendor/symbiote-node/themes/dark.js +0 -137
- package/vendor/symbiote-node/themes/ebook.js +0 -138
- package/vendor/symbiote-node/themes/grey.js +0 -137
- package/vendor/symbiote-node/themes/light.js +0 -137
- package/vendor/symbiote-node/themes/neon.js +0 -138
- package/vendor/symbiote-node/themes/pcb.js +0 -273
- package/vendor/symbiote-node/themes/synthwave.js +0 -137
- package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.css.js +0 -86
- package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.js +0 -128
- package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.tpl.js +0 -29
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* data/prompt-loader — Dynamic Markdown template assembly
|
|
3
|
-
*
|
|
4
|
-
* Loads and processes MD prompt templates with variable substitution
|
|
5
|
-
* and recursive file includes. Supports {{VARIABLE}} placeholders and
|
|
6
|
-
* {{file.md}} file includes.
|
|
7
|
-
*
|
|
8
|
-
* Ported from Mr-Computer/automations/argentine-spanish-bot/src/utils/prompt-loader.js
|
|
9
|
-
*
|
|
10
|
-
* @module agi-graph/packs/data/prompt-loader
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { readFile, readdir } from 'node:fs/promises';
|
|
14
|
-
import path from 'node:path';
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Process template with variables and file includes
|
|
18
|
-
* @param {string} template - Template string
|
|
19
|
-
* @param {Object} context - Variables to substitute
|
|
20
|
-
* @param {string} baseDir - Base directory for relative includes
|
|
21
|
-
* @returns {Promise<string>}
|
|
22
|
-
*/
|
|
23
|
-
async function processTemplate(template, context, baseDir) {
|
|
24
|
-
let result = template;
|
|
25
|
-
|
|
26
|
-
// Process file includes: {{file.md}} or {{path/to/file.md}}
|
|
27
|
-
const fileIncludeRegex = /\{\{([a-zA-Z0-9_\-\/\.]+\.md)\}\}/g;
|
|
28
|
-
let match;
|
|
29
|
-
|
|
30
|
-
while ((match = fileIncludeRegex.exec(result)) !== null) {
|
|
31
|
-
const filePath = match[1];
|
|
32
|
-
const fullMatch = match[0];
|
|
33
|
-
|
|
34
|
-
try {
|
|
35
|
-
let includeContent = await readFile(path.join(baseDir, filePath), 'utf-8');
|
|
36
|
-
// Recursively process included content
|
|
37
|
-
includeContent = await processTemplate(includeContent, context, path.dirname(path.join(baseDir, filePath)));
|
|
38
|
-
result = result.replace(fullMatch, includeContent);
|
|
39
|
-
} catch {
|
|
40
|
-
// Leave placeholder as is
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Process variables: {{VARIABLE_NAME}}
|
|
45
|
-
const variableRegex = /\{\{([A-Z_][A-Z0-9_]*)\}\}/g;
|
|
46
|
-
|
|
47
|
-
result = result.replace(variableRegex, (fullMatch, varName) => {
|
|
48
|
-
if (Object.hasOwn(context, varName)) {
|
|
49
|
-
const value = context[varName];
|
|
50
|
-
if (typeof value === 'string') return value;
|
|
51
|
-
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
52
|
-
if (Array.isArray(value)) return value.join('\n');
|
|
53
|
-
if (typeof value === 'object' && value !== null) return JSON.stringify(value, null, 2);
|
|
54
|
-
return String(value);
|
|
55
|
-
}
|
|
56
|
-
return fullMatch;
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
return result;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Validate prompt template — check for missing variables
|
|
64
|
-
* @param {string} template
|
|
65
|
-
* @param {Object} context
|
|
66
|
-
* @returns {Array<string>}
|
|
67
|
-
*/
|
|
68
|
-
function validatePromptTemplate(template, context) {
|
|
69
|
-
const variableRegex = /\{\{([A-Z_][A-Z0-9_]*)\}\}/g;
|
|
70
|
-
const missing = [];
|
|
71
|
-
let match;
|
|
72
|
-
while ((match = variableRegex.exec(template)) !== null) {
|
|
73
|
-
const varName = match[1];
|
|
74
|
-
if (!Object.hasOwn(context, varName)) missing.push(varName);
|
|
75
|
-
}
|
|
76
|
-
return [...new Set(missing)];
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* List available prompt templates in directory
|
|
81
|
-
* @param {string} dir
|
|
82
|
-
* @returns {Promise<Array<string>>}
|
|
83
|
-
*/
|
|
84
|
-
async function listPromptTemplates(dir) {
|
|
85
|
-
try {
|
|
86
|
-
const entries = await readdir(dir, { recursive: true });
|
|
87
|
-
return entries.filter(e => e.endsWith('.md'));
|
|
88
|
-
} catch {
|
|
89
|
-
return [];
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// ─── Handler Definition ────────────────────────────────────────────────
|
|
94
|
-
|
|
95
|
-
export default {
|
|
96
|
-
type: 'data/prompt-loader',
|
|
97
|
-
category: 'data',
|
|
98
|
-
icon: 'article',
|
|
99
|
-
|
|
100
|
-
driver: {
|
|
101
|
-
description: 'Dynamic Markdown template assembly with {{VARIABLE}} substitution and {{file.md}} includes',
|
|
102
|
-
inputs: [
|
|
103
|
-
{ name: 'template', type: 'string' },
|
|
104
|
-
],
|
|
105
|
-
outputs: [
|
|
106
|
-
{ name: 'result', type: 'any' },
|
|
107
|
-
{ name: 'error', type: 'string' },
|
|
108
|
-
],
|
|
109
|
-
params: {
|
|
110
|
-
operation: { type: 'string', default: 'load', description: 'Operation: load | load-multi | validate | list' },
|
|
111
|
-
context: { type: 'any', default: {}, description: 'Variables map for template substitution' },
|
|
112
|
-
baseDir: { type: 'string', default: '.', description: 'Base directory for file includes' },
|
|
113
|
-
// load-multi
|
|
114
|
-
templates: { type: 'any', default: null, description: 'Map of {name: path} for load-multi' },
|
|
115
|
-
// load from file
|
|
116
|
-
filePath: { type: 'string', default: null, description: 'Path to template file (alternative to template input)' },
|
|
117
|
-
},
|
|
118
|
-
},
|
|
119
|
-
|
|
120
|
-
lifecycle: {
|
|
121
|
-
validate: (inputs, params) => {
|
|
122
|
-
const op = params.operation;
|
|
123
|
-
if (op === 'list') return typeof params.baseDir === 'string';
|
|
124
|
-
if (op === 'load-multi') return typeof params.templates === 'object' && params.templates !== null;
|
|
125
|
-
if (op === 'validate') return typeof inputs.template === 'string';
|
|
126
|
-
// load
|
|
127
|
-
return typeof inputs.template === 'string' || typeof params.filePath === 'string';
|
|
128
|
-
},
|
|
129
|
-
|
|
130
|
-
cacheKey: (inputs, params) => {
|
|
131
|
-
if (params.operation === 'list') return `prompt-list:${params.baseDir}`;
|
|
132
|
-
return null; // templates change with context
|
|
133
|
-
},
|
|
134
|
-
|
|
135
|
-
execute: async (inputs, params) => {
|
|
136
|
-
const { operation, context, baseDir } = params;
|
|
137
|
-
|
|
138
|
-
try {
|
|
139
|
-
switch (operation) {
|
|
140
|
-
case 'load': {
|
|
141
|
-
let template = inputs.template;
|
|
142
|
-
let resolvedBase = baseDir;
|
|
143
|
-
|
|
144
|
-
if (!template && params.filePath) {
|
|
145
|
-
const fullPath = path.isAbsolute(params.filePath)
|
|
146
|
-
? params.filePath
|
|
147
|
-
: path.join(baseDir, params.filePath);
|
|
148
|
-
template = await readFile(fullPath, 'utf-8');
|
|
149
|
-
resolvedBase = path.dirname(fullPath);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const processed = await processTemplate(template, context, resolvedBase);
|
|
153
|
-
return { result: { content: processed, variablesUsed: Object.keys(context) } };
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
case 'load-multi': {
|
|
157
|
-
const entries = Object.entries(params.templates);
|
|
158
|
-
const results = {};
|
|
159
|
-
for (const [name, templatePath] of entries) {
|
|
160
|
-
const fullPath = path.isAbsolute(templatePath)
|
|
161
|
-
? templatePath
|
|
162
|
-
: path.join(baseDir, templatePath);
|
|
163
|
-
const raw = await readFile(fullPath, 'utf-8');
|
|
164
|
-
results[name] = await processTemplate(raw, context, path.dirname(fullPath));
|
|
165
|
-
}
|
|
166
|
-
return { result: { templates: results, count: entries.length } };
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
case 'validate': {
|
|
170
|
-
const missing = validatePromptTemplate(inputs.template, context);
|
|
171
|
-
return {
|
|
172
|
-
result: {
|
|
173
|
-
valid: missing.length === 0,
|
|
174
|
-
missing,
|
|
175
|
-
totalVariables: (inputs.template.match(/\{\{([A-Z_][A-Z0-9_]*)\}\}/g) || []).length,
|
|
176
|
-
},
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
case 'list': {
|
|
181
|
-
const templates = await listPromptTemplates(baseDir);
|
|
182
|
-
return { result: { templates, count: templates.length, baseDir } };
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
default:
|
|
186
|
-
return { error: `Unknown operation: ${operation}` };
|
|
187
|
-
}
|
|
188
|
-
} catch (err) {
|
|
189
|
-
return { error: `prompt-loader ${operation} failed: ${err.message}` };
|
|
190
|
-
}
|
|
191
|
-
},
|
|
192
|
-
},
|
|
193
|
-
};
|
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* data/roles — Instruction & Role Manager
|
|
3
|
-
*
|
|
4
|
-
* Manages AI roles/instructions from Markdown files with YAML frontmatter.
|
|
5
|
-
* Supports listing, filtering by tags, combining roles into system prompts,
|
|
6
|
-
* and scanning directories for role files.
|
|
7
|
-
*
|
|
8
|
-
* Ported from Mr-Computer/modules/razrab-bot/src/services/roles-service.js
|
|
9
|
-
*
|
|
10
|
-
* @module agi-graph/packs/data/roles
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { readFile, readdir, stat } from 'node:fs/promises';
|
|
14
|
-
import path from 'node:path';
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Parse YAML frontmatter from markdown content
|
|
18
|
-
* @param {string} content
|
|
19
|
-
* @returns {{ frontmatter: Object, body: string }}
|
|
20
|
-
*/
|
|
21
|
-
function parseFrontmatter(content) {
|
|
22
|
-
const fmRegex = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/;
|
|
23
|
-
const match = content.match(fmRegex);
|
|
24
|
-
if (!match) return { frontmatter: {}, body: content.trim() };
|
|
25
|
-
|
|
26
|
-
const fmText = match[1];
|
|
27
|
-
const body = match[2].trim();
|
|
28
|
-
const frontmatter = {};
|
|
29
|
-
|
|
30
|
-
for (const line of fmText.split('\n')) {
|
|
31
|
-
const colonIdx = line.indexOf(':');
|
|
32
|
-
if (colonIdx < 0) continue;
|
|
33
|
-
const key = line.slice(0, colonIdx).trim();
|
|
34
|
-
let value = line.slice(colonIdx + 1).trim();
|
|
35
|
-
|
|
36
|
-
// Parse arrays (simple YAML inline [a, b, c])
|
|
37
|
-
if (value.startsWith('[') && value.endsWith(']')) {
|
|
38
|
-
value = value.slice(1, -1).split(',').map(v => v.trim().replace(/^["']|["']$/g, ''));
|
|
39
|
-
}
|
|
40
|
-
frontmatter[key] = value;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return { frontmatter, body };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Scan directory for role markdown files
|
|
48
|
-
* @param {string} dirPath
|
|
49
|
-
* @param {string} prefix
|
|
50
|
-
* @returns {Promise<Map<string, Object>>}
|
|
51
|
-
*/
|
|
52
|
-
async function scanRolesDirectory(dirPath, prefix = '') {
|
|
53
|
-
const roles = new Map();
|
|
54
|
-
const tags = new Set();
|
|
55
|
-
|
|
56
|
-
try {
|
|
57
|
-
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
58
|
-
|
|
59
|
-
for (const entry of entries) {
|
|
60
|
-
const fullPath = path.join(dirPath, entry.name);
|
|
61
|
-
|
|
62
|
-
if (entry.isDirectory()) {
|
|
63
|
-
const subRoles = await scanRolesDirectory(fullPath, prefix ? `${prefix}/${entry.name}` : entry.name);
|
|
64
|
-
for (const [id, role] of subRoles) {
|
|
65
|
-
roles.set(id, role);
|
|
66
|
-
if (Array.isArray(role.tags)) role.tags.forEach(t => tags.add(t));
|
|
67
|
-
}
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (!entry.name.endsWith('.md')) continue;
|
|
72
|
-
|
|
73
|
-
const content = await readFile(fullPath, 'utf-8');
|
|
74
|
-
const { frontmatter, body } = parseFrontmatter(content);
|
|
75
|
-
|
|
76
|
-
const roleId = prefix
|
|
77
|
-
? `${prefix}/${entry.name.replace('.md', '')}`
|
|
78
|
-
: entry.name.replace('.md', '');
|
|
79
|
-
|
|
80
|
-
const role = {
|
|
81
|
-
id: roleId,
|
|
82
|
-
name: frontmatter.name || frontmatter.title || roleId,
|
|
83
|
-
tags: Array.isArray(frontmatter.tags) ? frontmatter.tags : [],
|
|
84
|
-
description: frontmatter.description || '',
|
|
85
|
-
category: frontmatter.category || (prefix || 'general'),
|
|
86
|
-
content: body,
|
|
87
|
-
path: fullPath,
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
roles.set(roleId, role);
|
|
91
|
-
role.tags.forEach(t => tags.add(t));
|
|
92
|
-
}
|
|
93
|
-
} catch {
|
|
94
|
-
// Directory not found or unreadable
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return roles;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// ─── Handler Definition ────────────────────────────────────────────────
|
|
101
|
-
|
|
102
|
-
export default {
|
|
103
|
-
type: 'data/roles',
|
|
104
|
-
category: 'data',
|
|
105
|
-
icon: 'person',
|
|
106
|
-
|
|
107
|
-
driver: {
|
|
108
|
-
description: 'Manage AI roles/instructions from Markdown files with YAML frontmatter',
|
|
109
|
-
inputs: [
|
|
110
|
-
{ name: 'rolesDir', type: 'string' },
|
|
111
|
-
],
|
|
112
|
-
outputs: [
|
|
113
|
-
{ name: 'result', type: 'any' },
|
|
114
|
-
{ name: 'error', type: 'string' },
|
|
115
|
-
],
|
|
116
|
-
params: {
|
|
117
|
-
operation: { type: 'string', default: 'list', description: 'Operation: list | get | filter-tags | combine | scan' },
|
|
118
|
-
roleId: { type: 'string', default: null, description: 'Role ID for get operation' },
|
|
119
|
-
tags: { type: 'any', default: null, description: 'Array of tags for filter-tags' },
|
|
120
|
-
matchAll: { type: 'boolean', default: true, description: 'Require ALL tags (true) or ANY tag (false)' },
|
|
121
|
-
roleIds: { type: 'any', default: null, description: 'Array of role IDs for combine operation' },
|
|
122
|
-
},
|
|
123
|
-
},
|
|
124
|
-
|
|
125
|
-
lifecycle: {
|
|
126
|
-
validate: (inputs) => {
|
|
127
|
-
return typeof inputs.rolesDir === 'string' && inputs.rolesDir.length > 0;
|
|
128
|
-
},
|
|
129
|
-
|
|
130
|
-
cacheKey: (inputs, params) => {
|
|
131
|
-
return `roles:${params.operation}:${inputs.rolesDir}:${params.roleId || ''}:${JSON.stringify(params.tags || '')}`;
|
|
132
|
-
},
|
|
133
|
-
|
|
134
|
-
execute: async (inputs, params) => {
|
|
135
|
-
const { rolesDir } = inputs;
|
|
136
|
-
const { operation } = params;
|
|
137
|
-
|
|
138
|
-
try {
|
|
139
|
-
// Scan directory for all roles
|
|
140
|
-
const roles = await scanRolesDirectory(rolesDir);
|
|
141
|
-
|
|
142
|
-
switch (operation) {
|
|
143
|
-
case 'list':
|
|
144
|
-
case 'scan': {
|
|
145
|
-
const allTags = new Set();
|
|
146
|
-
const rolesList = [];
|
|
147
|
-
for (const [id, role] of roles) {
|
|
148
|
-
rolesList.push({
|
|
149
|
-
id: role.id,
|
|
150
|
-
name: role.name,
|
|
151
|
-
tags: role.tags,
|
|
152
|
-
description: role.description,
|
|
153
|
-
category: role.category,
|
|
154
|
-
});
|
|
155
|
-
role.tags.forEach(t => allTags.add(t));
|
|
156
|
-
}
|
|
157
|
-
return {
|
|
158
|
-
result: {
|
|
159
|
-
roles: rolesList,
|
|
160
|
-
count: rolesList.length,
|
|
161
|
-
tags: [...allTags].sort(),
|
|
162
|
-
},
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
case 'get': {
|
|
167
|
-
if (!params.roleId) return { error: 'roleId is required for get operation' };
|
|
168
|
-
const role = roles.get(params.roleId);
|
|
169
|
-
if (!role) return { error: `Role not found: ${params.roleId}` };
|
|
170
|
-
return { result: { role } };
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
case 'filter-tags': {
|
|
174
|
-
if (!Array.isArray(params.tags)) return { error: 'tags array is required' };
|
|
175
|
-
const filtered = [];
|
|
176
|
-
for (const [, role] of roles) {
|
|
177
|
-
const match = params.matchAll
|
|
178
|
-
? params.tags.every(t => role.tags.includes(t))
|
|
179
|
-
: params.tags.some(t => role.tags.includes(t));
|
|
180
|
-
if (match) filtered.push(role);
|
|
181
|
-
}
|
|
182
|
-
return { result: { roles: filtered, count: filtered.length } };
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
case 'combine': {
|
|
186
|
-
if (!Array.isArray(params.roleIds)) return { error: 'roleIds array is required' };
|
|
187
|
-
const parts = [];
|
|
188
|
-
const resolved = [];
|
|
189
|
-
const missing = [];
|
|
190
|
-
for (const id of params.roleIds) {
|
|
191
|
-
const role = roles.get(id);
|
|
192
|
-
if (role) {
|
|
193
|
-
parts.push(`# ${role.name}\n\n${role.content}`);
|
|
194
|
-
resolved.push(id);
|
|
195
|
-
} else {
|
|
196
|
-
missing.push(id);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
return {
|
|
200
|
-
result: {
|
|
201
|
-
systemPrompt: parts.join('\n\n---\n\n'),
|
|
202
|
-
resolved,
|
|
203
|
-
missing,
|
|
204
|
-
},
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
default:
|
|
209
|
-
return { error: `Unknown operation: ${operation}` };
|
|
210
|
-
}
|
|
211
|
-
} catch (err) {
|
|
212
|
-
return { error: `roles ${operation} failed: ${err.message}` };
|
|
213
|
-
}
|
|
214
|
-
},
|
|
215
|
-
},
|
|
216
|
-
};
|
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* data/rss-feed — RSS Feed Fetcher
|
|
3
|
-
*
|
|
4
|
-
* Fetches and parses RSS feeds with caching, retry logic, and rate limiting.
|
|
5
|
-
* Supports multi-source aggregation with category rotation and topic categorization.
|
|
6
|
-
*
|
|
7
|
-
* Ported from Mr-Computer/automations/argentine-spanish-bot/src/services/rss-feed.js
|
|
8
|
-
*
|
|
9
|
-
* @module symbiote-node/packs/data/rss-feed */
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Simple hash for dedup
|
|
13
|
-
* @param {string} str
|
|
14
|
-
* @returns {string}
|
|
15
|
-
*/
|
|
16
|
-
function simpleHash(str) {
|
|
17
|
-
let hash = 0;
|
|
18
|
-
for (let i = 0; i < str.length; i++) {
|
|
19
|
-
const chr = str.charCodeAt(i);
|
|
20
|
-
hash = ((hash << 5) - hash) + chr;
|
|
21
|
-
hash |= 0;
|
|
22
|
-
}
|
|
23
|
-
return Math.abs(hash).toString(36);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Extract image URL from RSS item
|
|
28
|
-
* @param {Object} item
|
|
29
|
-
* @returns {string|null}
|
|
30
|
-
*/
|
|
31
|
-
function extractImageUrl(item) {
|
|
32
|
-
if (item.enclosure?.url) return item.enclosure.url;
|
|
33
|
-
if (item['media:content']?.$.url) return item['media:content'].$.url;
|
|
34
|
-
const imgMatch = (item.content || item['content:encoded'] || '').match(/<img[^>]+src=["']([^"']+)/);
|
|
35
|
-
if (imgMatch) return imgMatch[1];
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Category definitions for topic classification
|
|
40
|
-
const CATEGORIES = [
|
|
41
|
-
{ id: 'politica', name: 'Política', keywords: ['presidente', 'gobierno', 'congreso', 'diputado', 'senador', 'ley', 'decreto', 'ministerio', 'elecciones', 'votación'] },
|
|
42
|
-
{ id: 'economia', name: 'Economía', keywords: ['dólar', 'peso', 'inflación', 'bcra', 'mercado', 'bolsa', 'precio', 'sueldo', 'impuesto', 'deuda'] },
|
|
43
|
-
{ id: 'deportes', name: 'Deportes', keywords: ['gol', 'partido', 'fútbol', 'selección', 'liga', 'mundial', 'copa', 'técnico', 'jugador', 'cancha'] },
|
|
44
|
-
{ id: 'sociedad', name: 'Sociedad', keywords: ['vecinos', 'barrio', 'ciudad', 'protesta', 'educación', 'salud', 'hospital', 'seguridad', 'policía'] },
|
|
45
|
-
{ id: 'tecnologia', name: 'Tecnología', keywords: ['app', 'inteligencia artificial', 'robot', 'celular', 'internet', 'red social', 'digital', 'hacker'] },
|
|
46
|
-
{ id: 'cultura', name: 'Cultura', keywords: ['cine', 'teatro', 'música', 'festival', 'museo', 'libro', 'artista', 'exposición', 'concierto'] },
|
|
47
|
-
{ id: 'internacional', name: 'Internacional', keywords: ['eeuu', 'estados unidos', 'europa', 'china', 'rusia', 'brasil', 'guerra', 'otan', 'onu'] },
|
|
48
|
-
{ id: 'clima', name: 'Clima', keywords: ['temperatura', 'lluvia', 'viento', 'tormenta', 'calor', 'frío', 'pronóstico', 'ola de calor'] },
|
|
49
|
-
];
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Categorize a topic based on title and content
|
|
53
|
-
* @param {string} title
|
|
54
|
-
* @param {string} content
|
|
55
|
-
* @returns {{ id: string, name: string }}
|
|
56
|
-
*/
|
|
57
|
-
function categorizeTopic(title, content) {
|
|
58
|
-
const combined = `${title} ${content}`.toLowerCase();
|
|
59
|
-
let bestCategory = { id: 'general', name: 'General' };
|
|
60
|
-
let bestScore = 0;
|
|
61
|
-
|
|
62
|
-
for (const cat of CATEGORIES) {
|
|
63
|
-
let score = 0;
|
|
64
|
-
for (const kw of cat.keywords) {
|
|
65
|
-
if (combined.includes(kw)) score++;
|
|
66
|
-
}
|
|
67
|
-
if (score > bestScore) {
|
|
68
|
-
bestScore = score;
|
|
69
|
-
bestCategory = { id: cat.id, name: cat.name };
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return bestCategory;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Parse RSS XML manually (lightweight, no dependency)
|
|
78
|
-
* @param {string} xml
|
|
79
|
-
* @returns {Array<Object>}
|
|
80
|
-
*/
|
|
81
|
-
function parseRssXml(xml) {
|
|
82
|
-
const items = [];
|
|
83
|
-
const itemRegex = /<item>([\s\S]*?)<\/item>/gi;
|
|
84
|
-
let match;
|
|
85
|
-
|
|
86
|
-
while ((match = itemRegex.exec(xml)) !== null) {
|
|
87
|
-
const content = match[1];
|
|
88
|
-
const getTag = (tag) => {
|
|
89
|
-
const m = content.match(new RegExp(`<${tag}[^>]*><!\\[CDATA\\[([\\s\\S]*?)\\]\\]><\\/${tag}>|<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`, 'i'));
|
|
90
|
-
return m ? (m[1] || m[2] || '').trim() : '';
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
items.push({
|
|
94
|
-
title: getTag('title'),
|
|
95
|
-
link: getTag('link'),
|
|
96
|
-
description: getTag('description'),
|
|
97
|
-
pubDate: getTag('pubDate'),
|
|
98
|
-
content: getTag('content:encoded') || getTag('description'),
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return items;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// ─── Handler Definition ────────────────────────────────────────────────
|
|
106
|
-
|
|
107
|
-
export default {
|
|
108
|
-
type: 'data/rss-feed',
|
|
109
|
-
category: 'data',
|
|
110
|
-
icon: 'rss_feed',
|
|
111
|
-
|
|
112
|
-
driver: {
|
|
113
|
-
description: 'Fetch and parse RSS feeds with caching, categorization, and multi-source support',
|
|
114
|
-
inputs: [
|
|
115
|
-
{ name: 'url', type: 'string' },
|
|
116
|
-
],
|
|
117
|
-
outputs: [
|
|
118
|
-
{ name: 'result', type: 'any' },
|
|
119
|
-
{ name: 'error', type: 'string' },
|
|
120
|
-
],
|
|
121
|
-
params: {
|
|
122
|
-
operation: { type: 'string', default: 'fetch', description: 'Operation: fetch | fetch-multi | categorize' },
|
|
123
|
-
urls: { type: 'any', default: null, description: 'Array of URLs for fetch-multi' },
|
|
124
|
-
maxItems: { type: 'int', default: 20, description: 'Maximum items to return per feed' },
|
|
125
|
-
timeout: { type: 'int', default: 10000, description: 'Fetch timeout in ms' },
|
|
126
|
-
// categorize
|
|
127
|
-
items: { type: 'any', default: null, description: 'Array of {title, content} for categorize operation' },
|
|
128
|
-
},
|
|
129
|
-
},
|
|
130
|
-
|
|
131
|
-
lifecycle: {
|
|
132
|
-
validate: (inputs, params) => {
|
|
133
|
-
const op = params.operation;
|
|
134
|
-
if (op === 'categorize') return Array.isArray(params.items);
|
|
135
|
-
if (op === 'fetch-multi') return Array.isArray(params.urls) && params.urls.length > 0;
|
|
136
|
-
return typeof inputs.url === 'string' && inputs.url.startsWith('http');
|
|
137
|
-
},
|
|
138
|
-
|
|
139
|
-
cacheKey: (inputs, params) => {
|
|
140
|
-
if (params.operation === 'categorize') return null;
|
|
141
|
-
const url = params.operation === 'fetch-multi'
|
|
142
|
-
? params.urls.join(',')
|
|
143
|
-
: inputs.url;
|
|
144
|
-
return `rss:${params.operation}:${simpleHash(url)}`;
|
|
145
|
-
},
|
|
146
|
-
|
|
147
|
-
execute: async (inputs, params) => {
|
|
148
|
-
const { operation, maxItems, timeout } = params;
|
|
149
|
-
|
|
150
|
-
try {
|
|
151
|
-
switch (operation) {
|
|
152
|
-
case 'fetch': {
|
|
153
|
-
const response = await fetch(inputs.url, {
|
|
154
|
-
signal: AbortSignal.timeout(timeout),
|
|
155
|
-
headers: { 'User-Agent': 'symbiote-node/rss-feed/1.0' }, });
|
|
156
|
-
if (!response.ok) return { error: `HTTP ${response.status}: ${response.statusText}` };
|
|
157
|
-
|
|
158
|
-
const xml = await response.text();
|
|
159
|
-
const rawItems = parseRssXml(xml);
|
|
160
|
-
|
|
161
|
-
const items = rawItems.slice(0, maxItems).map(item => ({
|
|
162
|
-
id: simpleHash(item.title + item.link),
|
|
163
|
-
title: item.title,
|
|
164
|
-
link: item.link,
|
|
165
|
-
description: item.description,
|
|
166
|
-
pubDate: item.pubDate,
|
|
167
|
-
image: extractImageUrl(item),
|
|
168
|
-
category: categorizeTopic(item.title, item.description),
|
|
169
|
-
}));
|
|
170
|
-
|
|
171
|
-
return { result: { items, count: items.length, source: inputs.url } };
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
case 'fetch-multi': {
|
|
175
|
-
const allItems = [];
|
|
176
|
-
const errors = [];
|
|
177
|
-
|
|
178
|
-
for (const url of params.urls) {
|
|
179
|
-
try {
|
|
180
|
-
const response = await fetch(url, {
|
|
181
|
-
signal: AbortSignal.timeout(timeout),
|
|
182
|
-
headers: { 'User-Agent': 'symbiote-node/rss-feed/1.0' }, });
|
|
183
|
-
if (!response.ok) {
|
|
184
|
-
errors.push({ url, error: `HTTP ${response.status}` });
|
|
185
|
-
continue;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const xml = await response.text();
|
|
189
|
-
const rawItems = parseRssXml(xml);
|
|
190
|
-
|
|
191
|
-
for (const item of rawItems.slice(0, maxItems)) {
|
|
192
|
-
allItems.push({
|
|
193
|
-
id: simpleHash(item.title + item.link),
|
|
194
|
-
title: item.title,
|
|
195
|
-
link: item.link,
|
|
196
|
-
description: item.description,
|
|
197
|
-
pubDate: item.pubDate,
|
|
198
|
-
image: extractImageUrl(item),
|
|
199
|
-
category: categorizeTopic(item.title, item.description),
|
|
200
|
-
source: url,
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
} catch (err) {
|
|
204
|
-
errors.push({ url, error: err.message });
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Deduplicate by ID
|
|
209
|
-
const seen = new Set();
|
|
210
|
-
const unique = allItems.filter(item => {
|
|
211
|
-
if (seen.has(item.id)) return false;
|
|
212
|
-
seen.add(item.id);
|
|
213
|
-
return true;
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
return { result: { items: unique, count: unique.length, sources: params.urls.length, errors } };
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
case 'categorize': {
|
|
220
|
-
const categorized = params.items.map(item => ({
|
|
221
|
-
...item,
|
|
222
|
-
category: categorizeTopic(item.title || '', item.content || item.description || ''),
|
|
223
|
-
}));
|
|
224
|
-
|
|
225
|
-
// Group by category
|
|
226
|
-
const grouped = {};
|
|
227
|
-
for (const item of categorized) {
|
|
228
|
-
const key = item.category.id;
|
|
229
|
-
if (!grouped[key]) grouped[key] = { category: item.category, items: [] };
|
|
230
|
-
grouped[key].items.push(item);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
return { result: { categorized, grouped, count: categorized.length } };
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
default:
|
|
237
|
-
return { error: `Unknown operation: ${operation}` };
|
|
238
|
-
}
|
|
239
|
-
} catch (err) {
|
|
240
|
-
return { error: `rss-feed ${operation} failed: ${err.message}` };
|
|
241
|
-
}
|
|
242
|
-
},
|
|
243
|
-
},
|
|
244
|
-
};
|