opalserve 0.1.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/.env.example +19 -0
- package/AGENTS.md +23 -0
- package/README.md +109 -0
- package/config/servers.example.yaml +67 -0
- package/config/servers.yaml +2 -0
- package/dist/cli/discover.d.ts +3 -0
- package/dist/cli/discover.d.ts.map +1 -0
- package/dist/cli/discover.js +160 -0
- package/dist/cli/discover.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +32 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/connectors/base.d.ts +49 -0
- package/dist/connectors/base.d.ts.map +1 -0
- package/dist/connectors/base.js +45 -0
- package/dist/connectors/base.js.map +1 -0
- package/dist/connectors/custom.d.ts +19 -0
- package/dist/connectors/custom.d.ts.map +1 -0
- package/dist/connectors/custom.js +129 -0
- package/dist/connectors/custom.js.map +1 -0
- package/dist/connectors/github.d.ts +18 -0
- package/dist/connectors/github.d.ts.map +1 -0
- package/dist/connectors/github.js +188 -0
- package/dist/connectors/github.js.map +1 -0
- package/dist/connectors/google-drive.d.ts +18 -0
- package/dist/connectors/google-drive.d.ts.map +1 -0
- package/dist/connectors/google-drive.js +209 -0
- package/dist/connectors/google-drive.js.map +1 -0
- package/dist/connectors/index.d.ts +11 -0
- package/dist/connectors/index.d.ts.map +1 -0
- package/dist/connectors/index.js +76 -0
- package/dist/connectors/index.js.map +1 -0
- package/dist/connectors/postgres.d.ts +18 -0
- package/dist/connectors/postgres.d.ts.map +1 -0
- package/dist/connectors/postgres.js +140 -0
- package/dist/connectors/postgres.js.map +1 -0
- package/dist/connectors/slack.d.ts +18 -0
- package/dist/connectors/slack.d.ts.map +1 -0
- package/dist/connectors/slack.js +181 -0
- package/dist/connectors/slack.js.map +1 -0
- package/dist/core/auth.d.ts +26 -0
- package/dist/core/auth.d.ts.map +1 -0
- package/dist/core/auth.js +81 -0
- package/dist/core/auth.js.map +1 -0
- package/dist/core/registry.d.ts +33 -0
- package/dist/core/registry.d.ts.map +1 -0
- package/dist/core/registry.js +237 -0
- package/dist/core/registry.js.map +1 -0
- package/dist/core/tokenizer.d.ts +16 -0
- package/dist/core/tokenizer.d.ts.map +1 -0
- package/dist/core/tokenizer.js +29 -0
- package/dist/core/tokenizer.js.map +1 -0
- package/dist/governance/audit.d.ts +27 -0
- package/dist/governance/audit.d.ts.map +1 -0
- package/dist/governance/audit.js +149 -0
- package/dist/governance/audit.js.map +1 -0
- package/dist/governance/index.d.ts +5 -0
- package/dist/governance/index.d.ts.map +1 -0
- package/dist/governance/index.js +5 -0
- package/dist/governance/index.js.map +1 -0
- package/dist/governance/policy.d.ts +20 -0
- package/dist/governance/policy.d.ts.map +1 -0
- package/dist/governance/policy.js +162 -0
- package/dist/governance/policy.js.map +1 -0
- package/dist/governance/rate-limiter.d.ts +20 -0
- package/dist/governance/rate-limiter.d.ts.map +1 -0
- package/dist/governance/rate-limiter.js +73 -0
- package/dist/governance/rate-limiter.js.map +1 -0
- package/dist/governance/types.d.ts +246 -0
- package/dist/governance/types.d.ts.map +1 -0
- package/dist/governance/types.js +72 -0
- package/dist/governance/types.js.map +1 -0
- package/dist/identity/access-control.d.ts +15 -0
- package/dist/identity/access-control.d.ts.map +1 -0
- package/dist/identity/access-control.js +81 -0
- package/dist/identity/access-control.js.map +1 -0
- package/dist/identity/index.d.ts +4 -0
- package/dist/identity/index.d.ts.map +1 -0
- package/dist/identity/index.js +4 -0
- package/dist/identity/index.js.map +1 -0
- package/dist/identity/manager.d.ts +29 -0
- package/dist/identity/manager.d.ts.map +1 -0
- package/dist/identity/manager.js +167 -0
- package/dist/identity/manager.js.map +1 -0
- package/dist/identity/types.d.ts +237 -0
- package/dist/identity/types.d.ts.map +1 -0
- package/dist/identity/types.js +80 -0
- package/dist/identity/types.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/registry/server.d.ts +14 -0
- package/dist/registry/server.d.ts.map +1 -0
- package/dist/registry/server.js +173 -0
- package/dist/registry/server.js.map +1 -0
- package/dist/types/index.d.ts +639 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +76 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/config.d.ts +29 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +47 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/index.d.ts +7 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +44 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/workflow/engine.d.ts +18 -0
- package/dist/workflow/engine.d.ts.map +1 -0
- package/dist/workflow/engine.js +155 -0
- package/dist/workflow/engine.js.map +1 -0
- package/dist/workflow/index.d.ts +4 -0
- package/dist/workflow/index.d.ts.map +1 -0
- package/dist/workflow/index.js +4 -0
- package/dist/workflow/index.js.map +1 -0
- package/dist/workflow/templates.d.ts +4 -0
- package/dist/workflow/templates.d.ts.map +1 -0
- package/dist/workflow/templates.js +218 -0
- package/dist/workflow/templates.js.map +1 -0
- package/dist/workflow/types.d.ts +255 -0
- package/dist/workflow/types.d.ts.map +1 -0
- package/dist/workflow/types.js +48 -0
- package/dist/workflow/types.js.map +1 -0
- package/eslint.config.js +25 -0
- package/package.json +78 -0
- package/src/cli/discover.ts +223 -0
- package/src/cli/index.ts +40 -0
- package/src/connectors/base.ts +75 -0
- package/src/connectors/custom.ts +139 -0
- package/src/connectors/github.ts +195 -0
- package/src/connectors/google-drive.ts +217 -0
- package/src/connectors/index.ts +86 -0
- package/src/connectors/postgres.ts +148 -0
- package/src/connectors/slack.ts +188 -0
- package/src/core/auth.ts +109 -0
- package/src/core/registry.ts +301 -0
- package/src/core/tokenizer.ts +40 -0
- package/src/governance/audit.ts +182 -0
- package/src/governance/index.ts +4 -0
- package/src/governance/policy.ts +187 -0
- package/src/governance/rate-limiter.ts +95 -0
- package/src/governance/types.ts +100 -0
- package/src/identity/access-control.ts +119 -0
- package/src/identity/index.ts +3 -0
- package/src/identity/manager.ts +207 -0
- package/src/identity/types.ts +91 -0
- package/src/index.ts +16 -0
- package/src/registry/server.ts +195 -0
- package/src/types/index.ts +128 -0
- package/src/utils/config.ts +78 -0
- package/src/utils/index.ts +47 -0
- package/src/workflow/engine.ts +187 -0
- package/src/workflow/index.ts +3 -0
- package/src/workflow/templates.ts +220 -0
- package/src/workflow/types.ts +89 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'fs';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import YAML from 'yaml';
|
|
4
|
+
|
|
5
|
+
export interface Config {
|
|
6
|
+
registry: {
|
|
7
|
+
port: number;
|
|
8
|
+
host: string;
|
|
9
|
+
contextTokenBudget: number;
|
|
10
|
+
maxToolsPerResponse: number;
|
|
11
|
+
};
|
|
12
|
+
auth: {
|
|
13
|
+
enabled: boolean;
|
|
14
|
+
jwtSecret: string;
|
|
15
|
+
apiKey?: string;
|
|
16
|
+
};
|
|
17
|
+
servers: ServerConfig[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ServerConfig {
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
type: string;
|
|
24
|
+
enabled: boolean;
|
|
25
|
+
description: string;
|
|
26
|
+
endpoint?: string;
|
|
27
|
+
auth: {
|
|
28
|
+
type: 'none' | 'api-key' | 'oauth' | 'jwt' | 'basic';
|
|
29
|
+
envVar?: string;
|
|
30
|
+
};
|
|
31
|
+
tags: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function loadConfig(path?: string): Config {
|
|
35
|
+
const configPath = path || resolve(process.cwd(), 'config/servers.yaml');
|
|
36
|
+
|
|
37
|
+
if (!existsSync(configPath)) {
|
|
38
|
+
return getDefaultConfig();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const content = readFileSync(configPath, 'utf-8');
|
|
43
|
+
const parsed = YAML.parse(content);
|
|
44
|
+
return {
|
|
45
|
+
registry: {
|
|
46
|
+
port: parseInt(process.env.PORT || '3000', 10),
|
|
47
|
+
host: process.env.HOST || 'localhost',
|
|
48
|
+
contextTokenBudget: parseInt(process.env.CONTEXT_TOKEN_BUDGET || '128000', 10),
|
|
49
|
+
maxToolsPerResponse: parseInt(process.env.MAX_TOOLS_PER_RESPONSE || '50', 10),
|
|
50
|
+
},
|
|
51
|
+
auth: {
|
|
52
|
+
enabled: process.env.ENABLE_AUTH === 'true',
|
|
53
|
+
jwtSecret: process.env.JWT_SECRET || 'dev-secret',
|
|
54
|
+
apiKey: process.env.API_KEY,
|
|
55
|
+
},
|
|
56
|
+
servers: parsed.servers || [],
|
|
57
|
+
};
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error('Failed to load config:', error);
|
|
60
|
+
return getDefaultConfig();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function getDefaultConfig(): Config {
|
|
65
|
+
return {
|
|
66
|
+
registry: {
|
|
67
|
+
port: 3000,
|
|
68
|
+
host: 'localhost',
|
|
69
|
+
contextTokenBudget: 128000,
|
|
70
|
+
maxToolsPerResponse: 50,
|
|
71
|
+
},
|
|
72
|
+
auth: {
|
|
73
|
+
enabled: false,
|
|
74
|
+
jwtSecret: 'dev-secret',
|
|
75
|
+
},
|
|
76
|
+
servers: [],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export function formatBytes(bytes: number): string {
|
|
2
|
+
if (bytes === 0) return '0 B';
|
|
3
|
+
const k = 1024;
|
|
4
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
5
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
6
|
+
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function formatDuration(ms: number): string {
|
|
10
|
+
if (ms < 1000) return `${ms}ms`;
|
|
11
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
12
|
+
return `${(ms / 60000).toFixed(1)}m`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function truncate(str: string, length: number): string {
|
|
16
|
+
if (str.length <= length) return str;
|
|
17
|
+
return str.slice(0, length - 3) + '...';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function slugify(text: string): string {
|
|
21
|
+
return text
|
|
22
|
+
.toLowerCase()
|
|
23
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
24
|
+
.replace(/(^-|-$)/g, '');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function debounce<T extends (...args: unknown[]) => void>(
|
|
28
|
+
fn: T,
|
|
29
|
+
delay: number
|
|
30
|
+
): (...args: Parameters<T>) => void {
|
|
31
|
+
let timeoutId: ReturnType<typeof setTimeout>;
|
|
32
|
+
return (...args: Parameters<T>) => {
|
|
33
|
+
clearTimeout(timeoutId);
|
|
34
|
+
timeoutId = setTimeout(() => fn(...args), delay);
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function groupBy<T>(array: T[], key: keyof T): Record<string, T[]> {
|
|
39
|
+
return array.reduce((result, item) => {
|
|
40
|
+
const groupKey = String(item[key]);
|
|
41
|
+
if (!result[groupKey]) {
|
|
42
|
+
result[groupKey] = [];
|
|
43
|
+
}
|
|
44
|
+
result[groupKey].push(item);
|
|
45
|
+
return result;
|
|
46
|
+
}, {} as Record<string, T[]>);
|
|
47
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import type { Workflow, Node, Edge, WorkflowExecution, ExecutionLog } from './types.js';
|
|
2
|
+
|
|
3
|
+
export class WorkflowEngine {
|
|
4
|
+
private workflows: Map<string, Workflow> = new Map();
|
|
5
|
+
private executions: Map<string, WorkflowExecution> = new Map();
|
|
6
|
+
|
|
7
|
+
registerWorkflow(workflow: Workflow): void {
|
|
8
|
+
this.validateWorkflow(workflow);
|
|
9
|
+
this.workflows.set(workflow.id, workflow);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
getWorkflow(id: string): Workflow | undefined {
|
|
13
|
+
return this.workflows.get(id);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
getAllWorkflows(): Workflow[] {
|
|
17
|
+
return Array.from(this.workflows.values());
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
deleteWorkflow(id: string): boolean {
|
|
21
|
+
return this.workflows.delete(id);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async execute(workflowId: string, input: Record<string, unknown>): Promise<WorkflowExecution> {
|
|
25
|
+
const workflow = this.workflows.get(workflowId);
|
|
26
|
+
if (!workflow) {
|
|
27
|
+
throw new Error(`Workflow ${workflowId} not found`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const executionId = `${workflowId}-${Date.now()}`;
|
|
31
|
+
const execution: WorkflowExecution = {
|
|
32
|
+
id: executionId,
|
|
33
|
+
workflowId,
|
|
34
|
+
status: 'running',
|
|
35
|
+
startedAt: new Date().toISOString(),
|
|
36
|
+
completedAt: null,
|
|
37
|
+
context: { ...workflow.variables, ...input },
|
|
38
|
+
logs: [],
|
|
39
|
+
error: null,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
this.executions.set(executionId, execution);
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const result = await this.executeNodes(workflow, execution);
|
|
46
|
+
execution.status = 'completed';
|
|
47
|
+
execution.completedAt = new Date().toISOString();
|
|
48
|
+
execution.context = { ...execution.context, ...result };
|
|
49
|
+
} catch (error) {
|
|
50
|
+
execution.status = 'failed';
|
|
51
|
+
execution.error = error instanceof Error ? error.message : String(error);
|
|
52
|
+
execution.completedAt = new Date().toISOString();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
this.updateWorkflowLastRun(workflowId);
|
|
56
|
+
return execution;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private async executeNodes(workflow: Workflow, execution: WorkflowExecution): Promise<Record<string, unknown>> {
|
|
60
|
+
const { nodes, edges } = workflow;
|
|
61
|
+
const startNodes = nodes.filter(n => n.type === 'trigger');
|
|
62
|
+
const context: Record<string, unknown> = { ...execution.context };
|
|
63
|
+
|
|
64
|
+
const visited = new Set<string>();
|
|
65
|
+
const queue = [...startNodes];
|
|
66
|
+
|
|
67
|
+
while (queue.length > 0) {
|
|
68
|
+
const node = queue.shift()!;
|
|
69
|
+
if (visited.has(node.id)) continue;
|
|
70
|
+
visited.add(node.id);
|
|
71
|
+
|
|
72
|
+
this.log(execution, node, 'start', 'Executing node');
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const result = await this.executeNode(node, context, edges);
|
|
76
|
+
Object.assign(context, result);
|
|
77
|
+
this.log(execution, node, 'end', 'Node completed', result);
|
|
78
|
+
|
|
79
|
+
const outgoingEdges = edges.filter(e => e.source === node.id);
|
|
80
|
+
for (const edge of outgoingEdges) {
|
|
81
|
+
const nextNode = nodes.find(n => n.id === edge.target);
|
|
82
|
+
if (nextNode && !visited.has(nextNode.id)) {
|
|
83
|
+
queue.push(nextNode);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
} catch (error) {
|
|
87
|
+
this.log(execution, node, 'error', String(error));
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return context;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private async executeNode(
|
|
96
|
+
node: Node,
|
|
97
|
+
context: Record<string, unknown>,
|
|
98
|
+
_edges: Edge[]
|
|
99
|
+
): Promise<Record<string, unknown>> {
|
|
100
|
+
switch (node.type) {
|
|
101
|
+
case 'trigger':
|
|
102
|
+
return { trigger: node.name, timestamp: new Date().toISOString() };
|
|
103
|
+
|
|
104
|
+
case 'tool':
|
|
105
|
+
return { [`tool_${node.id}_result`]: { executed: true, nodeId: node.id } };
|
|
106
|
+
|
|
107
|
+
case 'condition':
|
|
108
|
+
const conditionValue = context[node.config.field as string];
|
|
109
|
+
const expectedValue = node.config.value;
|
|
110
|
+
const result = node.config.operator === 'equals'
|
|
111
|
+
? conditionValue === expectedValue
|
|
112
|
+
: conditionValue !== expectedValue;
|
|
113
|
+
return { [`condition_${node.id}_result`]: result };
|
|
114
|
+
|
|
115
|
+
case 'transform':
|
|
116
|
+
const input = context[node.config.input as string] as unknown[];
|
|
117
|
+
if (Array.isArray(input)) {
|
|
118
|
+
const transformed = input.map((item, index) => ({
|
|
119
|
+
...(item as Record<string, unknown>),
|
|
120
|
+
_index: index,
|
|
121
|
+
_transformed: true,
|
|
122
|
+
}));
|
|
123
|
+
return { [node.config.output as string]: transformed };
|
|
124
|
+
}
|
|
125
|
+
return { [node.config.output as string]: input };
|
|
126
|
+
|
|
127
|
+
case 'loop':
|
|
128
|
+
return { loop_count: 0 };
|
|
129
|
+
|
|
130
|
+
case 'output':
|
|
131
|
+
return { output: context };
|
|
132
|
+
|
|
133
|
+
default:
|
|
134
|
+
return {};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private log(execution: WorkflowExecution, node: Node, status: ExecutionLog['status'], message: string, data?: Record<string, unknown>): void {
|
|
139
|
+
execution.logs.push({
|
|
140
|
+
timestamp: new Date().toISOString(),
|
|
141
|
+
nodeId: node.id,
|
|
142
|
+
nodeName: node.name,
|
|
143
|
+
status,
|
|
144
|
+
message,
|
|
145
|
+
data: data || null,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private validateWorkflow(workflow: Workflow): void {
|
|
150
|
+
if (workflow.nodes.length === 0) {
|
|
151
|
+
throw new Error('Workflow must have at least one node');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const hasTrigger = workflow.nodes.some(n => n.type === 'trigger');
|
|
155
|
+
if (!hasTrigger) {
|
|
156
|
+
throw new Error('Workflow must have at least one trigger node');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const nodeIds = new Set(workflow.nodes.map(n => n.id));
|
|
160
|
+
for (const edge of workflow.edges) {
|
|
161
|
+
if (!nodeIds.has(edge.source)) {
|
|
162
|
+
throw new Error(`Edge references non-existent source node: ${edge.source}`);
|
|
163
|
+
}
|
|
164
|
+
if (!nodeIds.has(edge.target)) {
|
|
165
|
+
throw new Error(`Edge references non-existent target node: ${edge.target}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private updateWorkflowLastRun(workflowId: string): void {
|
|
171
|
+
const workflow = this.workflows.get(workflowId);
|
|
172
|
+
if (workflow) {
|
|
173
|
+
workflow.lastRunAt = new Date().toISOString();
|
|
174
|
+
this.workflows.set(workflowId, workflow);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
getExecution(id: string): WorkflowExecution | undefined {
|
|
179
|
+
return this.executions.get(id);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
getWorkflowExecutions(workflowId: string): WorkflowExecution[] {
|
|
183
|
+
return Array.from(this.executions.values())
|
|
184
|
+
.filter(e => e.workflowId === workflowId)
|
|
185
|
+
.sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import type { WorkflowTemplate, Workflow } from './types.js';
|
|
2
|
+
|
|
3
|
+
export const WORKFLOW_TEMPLATES: WorkflowTemplate[] = [
|
|
4
|
+
{
|
|
5
|
+
id: 'github-issue-to-slack',
|
|
6
|
+
name: 'GitHub Issue → Slack Notification',
|
|
7
|
+
description: 'When a new GitHub issue is created, notify a Slack channel',
|
|
8
|
+
category: 'notifications',
|
|
9
|
+
tags: ['github', 'slack', 'automation'],
|
|
10
|
+
nodes: [
|
|
11
|
+
{
|
|
12
|
+
id: 'trigger-1',
|
|
13
|
+
type: 'trigger',
|
|
14
|
+
name: 'GitHub Issue Created',
|
|
15
|
+
description: 'Triggered when a new issue is created',
|
|
16
|
+
position: { x: 100, y: 100 },
|
|
17
|
+
config: { event: 'issues.opened' },
|
|
18
|
+
inputs: [],
|
|
19
|
+
outputs: ['issue-data'],
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: 'tool-1',
|
|
23
|
+
type: 'tool',
|
|
24
|
+
name: 'Format Message',
|
|
25
|
+
description: 'Format the issue as a Slack message',
|
|
26
|
+
position: { x: 300, y: 100 },
|
|
27
|
+
toolId: 'custom:custom_transform_data',
|
|
28
|
+
config: { operation: 'map', expression: 'issue => `New Issue: ${issue.title}\\n${issue.body}`' },
|
|
29
|
+
inputs: ['issue-data'],
|
|
30
|
+
outputs: ['formatted-message'],
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'tool-2',
|
|
34
|
+
type: 'tool',
|
|
35
|
+
name: 'Post to Slack',
|
|
36
|
+
description: 'Post the formatted message to Slack',
|
|
37
|
+
position: { x: 500, y: 100 },
|
|
38
|
+
toolId: 'slack:slack_post_message',
|
|
39
|
+
config: { channel: '#engineering', text: '{{formatted-message}}' },
|
|
40
|
+
inputs: ['formatted-message'],
|
|
41
|
+
outputs: ['notification-result'],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: 'output-1',
|
|
45
|
+
type: 'output',
|
|
46
|
+
name: 'Complete',
|
|
47
|
+
position: { x: 700, y: 100 },
|
|
48
|
+
config: {},
|
|
49
|
+
inputs: ['notification-result'],
|
|
50
|
+
outputs: [],
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
edges: [
|
|
54
|
+
{ id: 'e1', source: 'trigger-1', target: 'tool-1' },
|
|
55
|
+
{ id: 'e2', source: 'tool-1', target: 'tool-2' },
|
|
56
|
+
{ id: 'e3', source: 'tool-2', target: 'output-1' },
|
|
57
|
+
],
|
|
58
|
+
variables: {
|
|
59
|
+
slack_channel: '#engineering',
|
|
60
|
+
github_repo: 'owner/repo',
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'database-report',
|
|
65
|
+
name: 'Daily Database Report',
|
|
66
|
+
description: 'Query database and send daily metrics report',
|
|
67
|
+
category: 'reporting',
|
|
68
|
+
tags: ['postgres', 'email', 'scheduled'],
|
|
69
|
+
nodes: [
|
|
70
|
+
{
|
|
71
|
+
id: 'trigger-1',
|
|
72
|
+
type: 'trigger',
|
|
73
|
+
name: 'Daily Schedule',
|
|
74
|
+
description: 'Runs daily at 9 AM',
|
|
75
|
+
position: { x: 100, y: 100 },
|
|
76
|
+
config: { schedule: '0 9 * * *' },
|
|
77
|
+
inputs: [],
|
|
78
|
+
outputs: ['schedule-trigger'],
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: 'tool-1',
|
|
82
|
+
type: 'tool',
|
|
83
|
+
name: 'Query Metrics',
|
|
84
|
+
description: 'Run database query for metrics',
|
|
85
|
+
position: { x: 300, y: 100 },
|
|
86
|
+
toolId: 'postgres:postgres_query',
|
|
87
|
+
config: { sql: 'SELECT COUNT(*) FROM users' },
|
|
88
|
+
inputs: [],
|
|
89
|
+
outputs: ['metrics-data'],
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: 'tool-2',
|
|
93
|
+
type: 'tool',
|
|
94
|
+
name: 'Aggregate Metrics',
|
|
95
|
+
description: 'Calculate totals and averages',
|
|
96
|
+
position: { x: 500, y: 100 },
|
|
97
|
+
toolId: 'custom:custom_aggregate_metrics',
|
|
98
|
+
config: { metrics: ['count', 'sum', 'avg'] },
|
|
99
|
+
inputs: ['metrics-data'],
|
|
100
|
+
outputs: ['aggregated'],
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
id: 'tool-3',
|
|
104
|
+
type: 'tool',
|
|
105
|
+
name: 'Send Email Report',
|
|
106
|
+
description: 'Email the report to stakeholders',
|
|
107
|
+
position: { x: 700, y: 100 },
|
|
108
|
+
toolId: 'custom:custom_send_email',
|
|
109
|
+
config: { subject: 'Daily Report' },
|
|
110
|
+
inputs: ['aggregated'],
|
|
111
|
+
outputs: ['email-result'],
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: 'output-1',
|
|
115
|
+
type: 'output',
|
|
116
|
+
name: 'Complete',
|
|
117
|
+
position: { x: 900, y: 100 },
|
|
118
|
+
config: {},
|
|
119
|
+
inputs: ['email-result'],
|
|
120
|
+
outputs: [],
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
edges: [
|
|
124
|
+
{ id: 'e1', source: 'trigger-1', target: 'tool-1' },
|
|
125
|
+
{ id: 'e2', source: 'tool-1', target: 'tool-2' },
|
|
126
|
+
{ id: 'e3', source: 'tool-2', target: 'tool-3' },
|
|
127
|
+
{ id: 'e4', source: 'tool-3', target: 'output-1' },
|
|
128
|
+
],
|
|
129
|
+
variables: {
|
|
130
|
+
recipients: ['team@example.com'],
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: 'file-sync',
|
|
135
|
+
name: 'Google Drive File Sync',
|
|
136
|
+
description: 'Monitor a Drive folder and notify on new files',
|
|
137
|
+
category: 'file-management',
|
|
138
|
+
tags: ['google-drive', 'slack', 'monitoring'],
|
|
139
|
+
nodes: [
|
|
140
|
+
{
|
|
141
|
+
id: 'trigger-1',
|
|
142
|
+
type: 'trigger',
|
|
143
|
+
name: 'Folder Monitor',
|
|
144
|
+
description: 'Check for new files every hour',
|
|
145
|
+
position: { x: 100, y: 100 },
|
|
146
|
+
config: { interval: '1h', folderId: 'root' },
|
|
147
|
+
inputs: [],
|
|
148
|
+
outputs: ['folder-state'],
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
id: 'tool-1',
|
|
152
|
+
type: 'tool',
|
|
153
|
+
name: 'List Files',
|
|
154
|
+
description: 'List files in monitored folder',
|
|
155
|
+
position: { x: 300, y: 100 },
|
|
156
|
+
toolId: 'google-drive:drive_list_files',
|
|
157
|
+
config: { folderId: '{{folderId}}' },
|
|
158
|
+
inputs: ['folder-state'],
|
|
159
|
+
outputs: ['files'],
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
id: 'condition-1',
|
|
163
|
+
type: 'condition',
|
|
164
|
+
name: 'New Files?',
|
|
165
|
+
description: 'Check if there are new files',
|
|
166
|
+
position: { x: 500, y: 100 },
|
|
167
|
+
config: { field: 'files.length', operator: 'greater_than', value: 0 },
|
|
168
|
+
inputs: ['files'],
|
|
169
|
+
outputs: ['has-files'],
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
id: 'tool-2',
|
|
173
|
+
type: 'tool',
|
|
174
|
+
name: 'Notify New Files',
|
|
175
|
+
description: 'Post file list to Slack',
|
|
176
|
+
position: { x: 700, y: 50 },
|
|
177
|
+
toolId: 'slack:slack_post_message',
|
|
178
|
+
config: { channel: '#files' },
|
|
179
|
+
inputs: ['has-files'],
|
|
180
|
+
outputs: ['notification'],
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
id: 'output-1',
|
|
184
|
+
type: 'output',
|
|
185
|
+
name: 'End (No Files)',
|
|
186
|
+
position: { x: 700, y: 150 },
|
|
187
|
+
config: {},
|
|
188
|
+
inputs: ['has-files'],
|
|
189
|
+
outputs: [],
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
edges: [
|
|
193
|
+
{ id: 'e1', source: 'trigger-1', target: 'tool-1' },
|
|
194
|
+
{ id: 'e2', source: 'tool-1', target: 'condition-1' },
|
|
195
|
+
{ id: 'e3', source: 'condition-1', target: 'tool-2', label: 'Yes' },
|
|
196
|
+
{ id: 'e4', source: 'condition-1', target: 'output-1', label: 'No' },
|
|
197
|
+
],
|
|
198
|
+
variables: {
|
|
199
|
+
folderId: '',
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
export function createWorkflowFromTemplate(template: WorkflowTemplate, overrides?: Partial<Workflow>): Workflow {
|
|
205
|
+
const now = new Date().toISOString();
|
|
206
|
+
return {
|
|
207
|
+
id: `workflow-${Date.now()}`,
|
|
208
|
+
name: overrides?.name || template.name,
|
|
209
|
+
description: overrides?.description || template.description,
|
|
210
|
+
version: '1.0.0',
|
|
211
|
+
nodes: template.nodes.map(n => ({ ...n })),
|
|
212
|
+
edges: template.edges.map(e => ({ ...e })),
|
|
213
|
+
variables: { ...template.variables, ...overrides?.variables },
|
|
214
|
+
tags: [...template.tags, ...(overrides?.tags || [])],
|
|
215
|
+
status: 'draft',
|
|
216
|
+
createdAt: now,
|
|
217
|
+
updatedAt: now,
|
|
218
|
+
...overrides,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const NodeTypeSchema = z.enum([
|
|
4
|
+
'trigger',
|
|
5
|
+
'tool',
|
|
6
|
+
'condition',
|
|
7
|
+
'transform',
|
|
8
|
+
'loop',
|
|
9
|
+
'output',
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
export const NodeSchema = z.object({
|
|
13
|
+
id: z.string(),
|
|
14
|
+
type: NodeTypeSchema,
|
|
15
|
+
name: z.string(),
|
|
16
|
+
description: z.string().optional(),
|
|
17
|
+
position: z.object({
|
|
18
|
+
x: z.number(),
|
|
19
|
+
y: z.number(),
|
|
20
|
+
}),
|
|
21
|
+
config: z.record(z.any()).default({}),
|
|
22
|
+
toolId: z.string().optional(),
|
|
23
|
+
inputs: z.array(z.string()).default([]),
|
|
24
|
+
outputs: z.array(z.string()).default([]),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export type NodeType = z.infer<typeof NodeTypeSchema>;
|
|
28
|
+
export type Node = z.infer<typeof NodeSchema>;
|
|
29
|
+
|
|
30
|
+
export const EdgeSchema = z.object({
|
|
31
|
+
id: z.string(),
|
|
32
|
+
source: z.string(),
|
|
33
|
+
sourceHandle: z.string().optional(),
|
|
34
|
+
target: z.string(),
|
|
35
|
+
targetHandle: z.string().optional(),
|
|
36
|
+
label: z.string().optional(),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export type Edge = z.infer<typeof EdgeSchema>;
|
|
40
|
+
|
|
41
|
+
export const WorkflowSchema = z.object({
|
|
42
|
+
id: z.string(),
|
|
43
|
+
name: z.string(),
|
|
44
|
+
description: z.string().optional(),
|
|
45
|
+
version: z.string().default('1.0.0'),
|
|
46
|
+
nodes: z.array(NodeSchema).default([]),
|
|
47
|
+
edges: z.array(EdgeSchema).default([]),
|
|
48
|
+
variables: z.record(z.any()).default({}),
|
|
49
|
+
owner: z.string().optional(),
|
|
50
|
+
team: z.string().optional(),
|
|
51
|
+
tags: z.array(z.string()).default([]),
|
|
52
|
+
status: z.enum(['draft', 'active', 'paused', 'archived']).default('draft'),
|
|
53
|
+
createdAt: z.string(),
|
|
54
|
+
updatedAt: z.string(),
|
|
55
|
+
lastRunAt: z.string().optional(),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
export type Workflow = z.infer<typeof WorkflowSchema>;
|
|
59
|
+
|
|
60
|
+
export interface WorkflowExecution {
|
|
61
|
+
id: string;
|
|
62
|
+
workflowId: string;
|
|
63
|
+
status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
|
|
64
|
+
startedAt: string;
|
|
65
|
+
completedAt: string | null;
|
|
66
|
+
context: Record<string, unknown>;
|
|
67
|
+
logs: ExecutionLog[];
|
|
68
|
+
error: string | null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface ExecutionLog {
|
|
72
|
+
timestamp: string;
|
|
73
|
+
nodeId: string;
|
|
74
|
+
nodeName: string;
|
|
75
|
+
status: 'start' | 'end' | 'error' | 'info';
|
|
76
|
+
message: string;
|
|
77
|
+
data: Record<string, unknown> | null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface WorkflowTemplate {
|
|
81
|
+
id: string;
|
|
82
|
+
name: string;
|
|
83
|
+
description: string;
|
|
84
|
+
category: string;
|
|
85
|
+
nodes: Node[];
|
|
86
|
+
edges: Edge[];
|
|
87
|
+
variables: Record<string, unknown>;
|
|
88
|
+
tags: string[];
|
|
89
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"rootDir": "./src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": true,
|
|
15
|
+
"sourceMap": true,
|
|
16
|
+
"resolveJsonModule": true,
|
|
17
|
+
"allowSyntheticDefaultImports": true,
|
|
18
|
+
"noImplicitAny": true,
|
|
19
|
+
"noImplicitReturns": true,
|
|
20
|
+
"noUnusedLocals": true,
|
|
21
|
+
"noUnusedParameters": true
|
|
22
|
+
},
|
|
23
|
+
"include": ["src/**/*"],
|
|
24
|
+
"exclude": ["node_modules", "dist"]
|
|
25
|
+
}
|