opc-agent 2.1.0 → 3.0.1

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 (144) hide show
  1. package/README.md +603 -545
  2. package/dist/channels/voice.d.ts +59 -0
  3. package/dist/channels/voice.js +351 -1
  4. package/dist/cli.js +172 -1
  5. package/dist/core/agent.d.ts +4 -0
  6. package/dist/core/agent.js +35 -0
  7. package/dist/core/collaboration.d.ts +89 -0
  8. package/dist/core/collaboration.js +201 -0
  9. package/dist/deploy/index.d.ts +40 -0
  10. package/dist/deploy/index.js +261 -0
  11. package/dist/index.d.ts +7 -1
  12. package/dist/index.js +47 -3
  13. package/dist/mcp/servers/calculator-mcp.d.ts +3 -0
  14. package/dist/mcp/servers/calculator-mcp.js +65 -0
  15. package/dist/mcp/servers/crypto-mcp.d.ts +3 -0
  16. package/dist/mcp/servers/crypto-mcp.js +108 -0
  17. package/dist/mcp/servers/database-mcp.d.ts +3 -0
  18. package/dist/mcp/servers/database-mcp.js +73 -0
  19. package/dist/mcp/servers/datetime-mcp.d.ts +3 -0
  20. package/dist/mcp/servers/datetime-mcp.js +71 -0
  21. package/dist/mcp/servers/filesystem.d.ts +3 -0
  22. package/dist/mcp/servers/filesystem.js +101 -0
  23. package/dist/mcp/servers/github-mcp.d.ts +3 -0
  24. package/dist/mcp/servers/github-mcp.js +60 -0
  25. package/dist/mcp/servers/index.d.ts +21 -0
  26. package/dist/mcp/servers/index.js +50 -0
  27. package/dist/mcp/servers/json-mcp.d.ts +3 -0
  28. package/dist/mcp/servers/json-mcp.js +126 -0
  29. package/dist/mcp/servers/memory-mcp.d.ts +3 -0
  30. package/dist/mcp/servers/memory-mcp.js +60 -0
  31. package/dist/mcp/servers/regex-mcp.d.ts +3 -0
  32. package/dist/mcp/servers/regex-mcp.js +56 -0
  33. package/dist/mcp/servers/web-mcp.d.ts +3 -0
  34. package/dist/mcp/servers/web-mcp.js +51 -0
  35. package/dist/schema/oad.d.ts +292 -12
  36. package/dist/schema/oad.js +12 -1
  37. package/dist/security/guardrails.d.ts +50 -0
  38. package/dist/security/guardrails.js +197 -0
  39. package/dist/studio/server.d.ts +31 -1
  40. package/dist/studio/server.js +154 -3
  41. package/dist/studio-ui/index.html +1278 -662
  42. package/dist/tools/integrations/calendar.d.ts +3 -0
  43. package/dist/tools/integrations/calendar.js +73 -0
  44. package/dist/tools/integrations/code-exec.d.ts +3 -0
  45. package/dist/tools/integrations/code-exec.js +42 -0
  46. package/dist/tools/integrations/csv-analyzer.d.ts +3 -0
  47. package/dist/tools/integrations/csv-analyzer.js +142 -0
  48. package/dist/tools/integrations/database.d.ts +3 -0
  49. package/dist/tools/integrations/database.js +44 -0
  50. package/dist/tools/integrations/email-send.d.ts +3 -0
  51. package/dist/tools/integrations/email-send.js +104 -0
  52. package/dist/tools/integrations/git-tool.d.ts +3 -0
  53. package/dist/tools/integrations/git-tool.js +49 -0
  54. package/dist/tools/integrations/github-tool.d.ts +3 -0
  55. package/dist/tools/integrations/github-tool.js +77 -0
  56. package/dist/tools/integrations/image-gen.d.ts +3 -0
  57. package/dist/tools/integrations/image-gen.js +58 -0
  58. package/dist/tools/integrations/index.d.ts +30 -0
  59. package/dist/tools/integrations/index.js +107 -0
  60. package/dist/tools/integrations/jira.d.ts +3 -0
  61. package/dist/tools/integrations/jira.js +85 -0
  62. package/dist/tools/integrations/notion.d.ts +3 -0
  63. package/dist/tools/integrations/notion.js +71 -0
  64. package/dist/tools/integrations/npm-tool.d.ts +3 -0
  65. package/dist/tools/integrations/npm-tool.js +49 -0
  66. package/dist/tools/integrations/pdf-reader.d.ts +3 -0
  67. package/dist/tools/integrations/pdf-reader.js +91 -0
  68. package/dist/tools/integrations/slack.d.ts +3 -0
  69. package/dist/tools/integrations/slack.js +67 -0
  70. package/dist/tools/integrations/summarizer.d.ts +3 -0
  71. package/dist/tools/integrations/summarizer.js +49 -0
  72. package/dist/tools/integrations/translator.d.ts +3 -0
  73. package/dist/tools/integrations/translator.js +48 -0
  74. package/dist/tools/integrations/trello.d.ts +3 -0
  75. package/dist/tools/integrations/trello.js +60 -0
  76. package/dist/tools/integrations/vector-search.d.ts +3 -0
  77. package/dist/tools/integrations/vector-search.js +44 -0
  78. package/dist/tools/integrations/web-scraper.d.ts +3 -0
  79. package/dist/tools/integrations/web-scraper.js +48 -0
  80. package/dist/tools/integrations/web-search.d.ts +3 -0
  81. package/dist/tools/integrations/web-search.js +60 -0
  82. package/dist/tools/integrations/webhook.d.ts +3 -0
  83. package/dist/tools/integrations/webhook.js +39 -0
  84. package/dist/ui/components.d.ts +10 -0
  85. package/dist/ui/components.js +123 -0
  86. package/package.json +3 -3
  87. package/src/channels/voice.ts +365 -0
  88. package/src/cli.ts +176 -2
  89. package/src/core/agent.ts +38 -0
  90. package/src/core/collaboration.ts +275 -0
  91. package/src/deploy/index.ts +255 -0
  92. package/src/index.ts +21 -1
  93. package/src/mcp/servers/calculator-mcp.ts +65 -0
  94. package/src/mcp/servers/crypto-mcp.ts +73 -0
  95. package/src/mcp/servers/database-mcp.ts +72 -0
  96. package/src/mcp/servers/datetime-mcp.ts +69 -0
  97. package/src/mcp/servers/filesystem.ts +66 -0
  98. package/src/mcp/servers/github-mcp.ts +58 -0
  99. package/src/mcp/servers/index.ts +63 -0
  100. package/src/mcp/servers/json-mcp.ts +102 -0
  101. package/src/mcp/servers/memory-mcp.ts +56 -0
  102. package/src/mcp/servers/regex-mcp.ts +53 -0
  103. package/src/mcp/servers/web-mcp.ts +49 -0
  104. package/src/schema/oad.ts +13 -0
  105. package/src/security/guardrails.ts +248 -0
  106. package/src/studio/server.ts +166 -4
  107. package/src/studio-ui/index.html +1278 -662
  108. package/src/tools/integrations/calendar.ts +73 -0
  109. package/src/tools/integrations/code-exec.ts +39 -0
  110. package/src/tools/integrations/csv-analyzer.ts +92 -0
  111. package/src/tools/integrations/database.ts +44 -0
  112. package/src/tools/integrations/email-send.ts +76 -0
  113. package/src/tools/integrations/git-tool.ts +42 -0
  114. package/src/tools/integrations/github-tool.ts +76 -0
  115. package/src/tools/integrations/image-gen.ts +56 -0
  116. package/src/tools/integrations/index.ts +92 -0
  117. package/src/tools/integrations/jira.ts +83 -0
  118. package/src/tools/integrations/notion.ts +71 -0
  119. package/src/tools/integrations/npm-tool.ts +48 -0
  120. package/src/tools/integrations/pdf-reader.ts +58 -0
  121. package/src/tools/integrations/slack.ts +65 -0
  122. package/src/tools/integrations/summarizer.ts +49 -0
  123. package/src/tools/integrations/translator.ts +48 -0
  124. package/src/tools/integrations/trello.ts +60 -0
  125. package/src/tools/integrations/vector-search.ts +42 -0
  126. package/src/tools/integrations/web-scraper.ts +47 -0
  127. package/src/tools/integrations/web-search.ts +58 -0
  128. package/src/tools/integrations/webhook.ts +38 -0
  129. package/src/ui/components.ts +127 -0
  130. package/tests/brain-seed-extended.test.ts +490 -0
  131. package/tests/collaboration.test.ts +319 -0
  132. package/tests/deploy-and-dag.test.ts +196 -0
  133. package/tests/guardrails.test.ts +177 -0
  134. package/tests/integrations.test.ts +249 -0
  135. package/tests/mcp-servers.test.ts +260 -0
  136. package/tests/voice-enhanced.test.ts +169 -0
  137. package/dist/dtv/data.d.ts +0 -18
  138. package/dist/dtv/data.js +0 -25
  139. package/dist/dtv/trust.d.ts +0 -19
  140. package/dist/dtv/trust.js +0 -40
  141. package/dist/dtv/value.d.ts +0 -23
  142. package/dist/dtv/value.js +0 -38
  143. package/dist/marketplace/index.d.ts +0 -34
  144. package/dist/marketplace/index.js +0 -202
@@ -0,0 +1,49 @@
1
+ import type { MCPServerConfig } from '../../protocols/mcp/types';
2
+
3
+ export function createWebServer(): MCPServerConfig {
4
+ return {
5
+ name: 'web',
6
+ version: '1.0.0',
7
+ tools: [
8
+ {
9
+ name: 'web_fetch',
10
+ description: 'Fetch a URL and return its content',
11
+ inputSchema: { type: 'object', properties: { url: { type: 'string' }, method: { type: 'string', default: 'GET' }, headers: { type: 'object' }, body: { type: 'string' } }, required: ['url'] },
12
+ handler: async (args: { url: string; method?: string; headers?: Record<string, string>; body?: string }) => {
13
+ const res = await fetch(args.url, { method: args.method || 'GET', headers: args.headers, body: args.body });
14
+ const contentType = res.headers.get('content-type') || '';
15
+ const text = await res.text();
16
+ return { status: res.status, contentType, body: text.slice(0, 50000), truncated: text.length > 50000 };
17
+ },
18
+ },
19
+ {
20
+ name: 'web_extract_text',
21
+ description: 'Fetch a URL and extract readable text (strips HTML tags)',
22
+ inputSchema: { type: 'object', properties: { url: { type: 'string' } }, required: ['url'] },
23
+ handler: async (args: { url: string }) => {
24
+ const res = await fetch(args.url);
25
+ const html = await res.text();
26
+ const text = html.replace(/<script[\s\S]*?<\/script>/gi, '').replace(/<style[\s\S]*?<\/style>/gi, '').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
27
+ return { text: text.slice(0, 30000), truncated: text.length > 30000 };
28
+ },
29
+ },
30
+ {
31
+ name: 'web_search',
32
+ description: 'Search the web (simulated — returns search URL for manual use)',
33
+ inputSchema: { type: 'object', properties: { query: { type: 'string' }, engine: { type: 'string', enum: ['google', 'bing', 'duckduckgo'], default: 'duckduckgo' } }, required: ['query'] },
34
+ handler: async (args: { query: string; engine?: string }) => {
35
+ const engines: Record<string, string> = {
36
+ google: `https://www.google.com/search?q=${encodeURIComponent(args.query)}`,
37
+ bing: `https://www.bing.com/search?q=${encodeURIComponent(args.query)}`,
38
+ duckduckgo: `https://html.duckduckgo.com/html/?q=${encodeURIComponent(args.query)}`,
39
+ };
40
+ const url = engines[args.engine || 'duckduckgo'];
41
+ const res = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0 opc-mcp/1.0' } });
42
+ const html = await res.text();
43
+ const text = html.replace(/<script[\s\S]*?<\/script>/gi, '').replace(/<style[\s\S]*?<\/style>/gi, '').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
44
+ return { query: args.query, engine: args.engine || 'duckduckgo', results: text.slice(0, 20000) };
45
+ },
46
+ },
47
+ ],
48
+ };
49
+ }
package/src/schema/oad.ts CHANGED
@@ -167,6 +167,18 @@ export const ProtocolsSchema = z.object({
167
167
  mcp: MCPServeSchema.optional(),
168
168
  });
169
169
 
170
+ export const GuardrailRuleSchema = z.object({
171
+ name: z.string(),
172
+ type: z.enum(['regex', 'keyword', 'llm', 'custom']),
173
+ action: z.enum(['block', 'warn', 'redact', 'log']),
174
+ config: z.record(z.any()).optional(),
175
+ });
176
+
177
+ export const GuardrailsSchema = z.object({
178
+ input: z.array(GuardrailRuleSchema).optional(),
179
+ output: z.array(GuardrailRuleSchema).optional(),
180
+ });
181
+
170
182
  export const SpecSchema = z.object({
171
183
  provider: ProviderSchema.optional(),
172
184
  model: z.string().default('deepseek-chat'),
@@ -187,6 +199,7 @@ export const SpecSchema = z.object({
187
199
  telemetry: TelemetrySchema.optional(),
188
200
  protocols: ProtocolsSchema.optional(),
189
201
  plugins: z.array(PluginRefSchema).optional(),
202
+ guardrails: GuardrailsSchema.optional(),
190
203
  });
191
204
 
192
205
  export const OADSchema = z.object({
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Guardrails Module - v2.1.0
3
+ * Input/output guardrails for LLM safety: PII, toxicity, injection, compliance.
4
+ */
5
+
6
+ // ── Types ───────────────────────────────────────────────────
7
+
8
+ export interface GuardrailConfig {
9
+ input?: GuardrailRule[];
10
+ output?: GuardrailRule[];
11
+ }
12
+
13
+ export interface GuardrailRule {
14
+ name: string;
15
+ type: 'regex' | 'keyword' | 'llm' | 'custom';
16
+ action: 'block' | 'warn' | 'redact' | 'log';
17
+ config?: Record<string, any>;
18
+ }
19
+
20
+ export interface GuardrailResult {
21
+ passed: boolean;
22
+ blocked: boolean;
23
+ warned: boolean;
24
+ redacted: boolean;
25
+ message?: string;
26
+ redactedText?: string;
27
+ violations: GuardrailViolation[];
28
+ }
29
+
30
+ export interface GuardrailViolation {
31
+ rule: string;
32
+ action: string;
33
+ detail: string;
34
+ }
35
+
36
+ // ── Built-in Patterns ───────────────────────────────────────
37
+
38
+ const PII_PATTERNS: Record<string, RegExp> = {
39
+ email: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
40
+ phone: /(\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}/g,
41
+ ssn: /\b\d{3}-\d{2}-\d{4}\b/g,
42
+ creditCard: /\b(?:\d{4}[-\s]?){3}\d{4}\b/g,
43
+ ipAddress: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g,
44
+ };
45
+
46
+ const INJECTION_PATTERNS = [
47
+ /ignore\s+(all\s+)?previous\s+instructions/i,
48
+ /ignore\s+(all\s+)?above\s+instructions/i,
49
+ /system\s*prompt\s*:/i,
50
+ /you\s+are\s+now\s+(?:a|an|the)\s+/i,
51
+ /act\s+as\s+(?:a|an)\s+/i,
52
+ /pretend\s+(?:you(?:'re|\s+are)\s+)?/i,
53
+ /new\s+instruction[s]?\s*:/i,
54
+ /override\s+(?:your\s+)?(?:instructions|rules|guidelines)/i,
55
+ /jailbreak/i,
56
+ /DAN\s+mode/i,
57
+ ];
58
+
59
+ const TOXICITY_KEYWORDS = [
60
+ 'kill yourself', 'kys', 'go die', 'hate you',
61
+ 'stupid idiot', 'worthless', 'piece of shit',
62
+ ];
63
+
64
+ const COMPLIANCE_PATTERNS = [
65
+ { pattern: /(?:you\s+should\s+)?(?:buy|sell|invest\s+in)\s+(?:stocks?|crypto|bitcoin)/i, label: 'financial advice' },
66
+ { pattern: /(?:you\s+(?:have|probably\s+have)|diagnos(?:e|is))\s+(?:\w+\s+){0,3}(?:disease|syndrome|disorder|cancer)/i, label: 'medical diagnosis' },
67
+ { pattern: /(?:legal(?:ly)?|sue|lawsuit|court)\s+(?:you\s+should|advice)/i, label: 'legal advice' },
68
+ ];
69
+
70
+ // ── Rule Executors ──────────────────────────────────────────
71
+
72
+ function checkPII(text: string, action: string): { violations: GuardrailViolation[]; redactedText: string } {
73
+ const violations: GuardrailViolation[] = [];
74
+ let redacted = text;
75
+ for (const [type, pattern] of Object.entries(PII_PATTERNS)) {
76
+ const cloned = new RegExp(pattern.source, pattern.flags);
77
+ const matches = text.match(cloned);
78
+ if (matches) {
79
+ violations.push({ rule: 'pii-detector', action, detail: `Found ${type}: ${matches.length} match(es)` });
80
+ redacted = redacted.replace(cloned, '[REDACTED]');
81
+ }
82
+ }
83
+ return { violations, redactedText: redacted };
84
+ }
85
+
86
+ function checkInjection(text: string): GuardrailViolation[] {
87
+ const violations: GuardrailViolation[] = [];
88
+ for (const pattern of INJECTION_PATTERNS) {
89
+ if (pattern.test(text)) {
90
+ violations.push({ rule: 'prompt-injection', action: 'block', detail: `Matched pattern: ${pattern.source}` });
91
+ break; // one is enough
92
+ }
93
+ }
94
+ return violations;
95
+ }
96
+
97
+ function checkToxicity(text: string, extraKeywords?: string[]): GuardrailViolation[] {
98
+ const lower = text.toLowerCase();
99
+ const keywords = [...TOXICITY_KEYWORDS, ...(extraKeywords ?? [])];
100
+ for (const kw of keywords) {
101
+ if (lower.includes(kw.toLowerCase())) {
102
+ return [{ rule: 'toxicity', action: 'block', detail: `Matched keyword: "${kw}"` }];
103
+ }
104
+ }
105
+ return [];
106
+ }
107
+
108
+ function checkTopicRestriction(text: string, config?: Record<string, any>): GuardrailViolation[] {
109
+ const denyTopics: string[] = config?.denyTopics ?? [];
110
+ const lower = text.toLowerCase();
111
+ for (const topic of denyTopics) {
112
+ if (lower.includes(topic.toLowerCase())) {
113
+ return [{ rule: 'topic-restrictor', action: 'block', detail: `Blocked topic: "${topic}"` }];
114
+ }
115
+ }
116
+ return [];
117
+ }
118
+
119
+ function checkLength(text: string, config?: Record<string, any>): GuardrailViolation[] {
120
+ const maxChars = config?.maxChars ?? 10000;
121
+ if (text.length > maxChars) {
122
+ return [{ rule: 'length-limit', action: 'warn', detail: `Response length ${text.length} exceeds max ${maxChars}` }];
123
+ }
124
+ return [];
125
+ }
126
+
127
+ function checkLanguage(text: string, config?: Record<string, any>): GuardrailViolation[] {
128
+ const allowed: string[] = config?.allowedLanguages ?? [];
129
+ if (allowed.length === 0) return [];
130
+ // Simple heuristic: check if text contains CJK characters
131
+ const hasCJK = /[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff]/.test(text);
132
+ const hasLatin = /[a-zA-Z]{3,}/.test(text);
133
+ if (allowed.includes('en') && !allowed.includes('zh') && hasCJK && !hasLatin) {
134
+ return [{ rule: 'language-filter', action: 'block', detail: 'Non-allowed language detected' }];
135
+ }
136
+ if (allowed.includes('zh') && !allowed.includes('en') && hasLatin && !hasCJK) {
137
+ return [{ rule: 'language-filter', action: 'block', detail: 'Non-allowed language detected' }];
138
+ }
139
+ return [];
140
+ }
141
+
142
+ function checkCompliance(text: string): GuardrailViolation[] {
143
+ for (const { pattern, label } of COMPLIANCE_PATTERNS) {
144
+ if (pattern.test(text)) {
145
+ return [{ rule: 'compliance-filter', action: 'block', detail: `Potential ${label} detected` }];
146
+ }
147
+ }
148
+ return [];
149
+ }
150
+
151
+ // ── Guardrail Manager ───────────────────────────────────────
152
+
153
+ export class GuardrailManager {
154
+ private config: GuardrailConfig;
155
+
156
+ constructor(config: GuardrailConfig) {
157
+ this.config = config;
158
+ }
159
+
160
+ async checkInput(message: string): Promise<GuardrailResult> {
161
+ return this.runRules(message, this.config.input ?? []);
162
+ }
163
+
164
+ async checkOutput(response: string): Promise<GuardrailResult> {
165
+ return this.runRules(response, this.config.output ?? []);
166
+ }
167
+
168
+ private async runRules(text: string, rules: GuardrailRule[]): Promise<GuardrailResult> {
169
+ const allViolations: GuardrailViolation[] = [];
170
+ let blocked = false;
171
+ let warned = false;
172
+ let redacted = false;
173
+ let redactedText = text;
174
+ let blockMessage = '';
175
+
176
+ for (const rule of rules) {
177
+ let violations: GuardrailViolation[] = [];
178
+
179
+ switch (rule.name) {
180
+ case 'pii-detector': {
181
+ const result = checkPII(text, rule.action);
182
+ violations = result.violations;
183
+ if (violations.length > 0 && rule.action === 'redact') {
184
+ redacted = true;
185
+ redactedText = result.redactedText;
186
+ }
187
+ break;
188
+ }
189
+ case 'prompt-injection':
190
+ violations = checkInjection(text);
191
+ break;
192
+ case 'toxicity':
193
+ violations = checkToxicity(text, rule.config?.keywords);
194
+ break;
195
+ case 'topic-restrictor':
196
+ violations = checkTopicRestriction(text, rule.config);
197
+ break;
198
+ case 'length-limit':
199
+ violations = checkLength(text, rule.config);
200
+ break;
201
+ case 'language-filter':
202
+ violations = checkLanguage(text, rule.config);
203
+ break;
204
+ case 'compliance-filter':
205
+ violations = checkCompliance(text);
206
+ break;
207
+ default:
208
+ // Unknown rule — skip
209
+ break;
210
+ }
211
+
212
+ if (violations.length > 0) {
213
+ // Override action from rule config
214
+ violations = violations.map(v => ({ ...v, action: rule.action }));
215
+ allViolations.push(...violations);
216
+
217
+ if (rule.action === 'block') {
218
+ blocked = true;
219
+ blockMessage = `Message blocked by ${rule.name}: ${violations[0].detail}`;
220
+ } else if (rule.action === 'warn') {
221
+ warned = true;
222
+ }
223
+ }
224
+ }
225
+
226
+ return {
227
+ passed: allViolations.length === 0,
228
+ blocked,
229
+ warned,
230
+ redacted,
231
+ message: blocked ? blockMessage : undefined,
232
+ redactedText: redacted ? redactedText : undefined,
233
+ violations: allViolations,
234
+ };
235
+ }
236
+ }
237
+
238
+ // ── Factory from OAD config ─────────────────────────────────
239
+
240
+ export function createGuardrailsFromConfig(config: {
241
+ input?: Array<{ name: string; type: string; action: string; config?: any }>;
242
+ output?: Array<{ name: string; type: string; action: string; config?: any }>;
243
+ }): GuardrailManager {
244
+ return new GuardrailManager({
245
+ input: config.input as GuardrailRule[],
246
+ output: config.output as GuardrailRule[],
247
+ });
248
+ }
@@ -1,9 +1,35 @@
1
1
  import { createServer, IncomingMessage, ServerResponse, request as httpRequest } from 'http';
2
- import { readFileSync, existsSync } from 'fs';
2
+ import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'fs';
3
3
  import { join, extname } from 'path';
4
4
  import * as net from 'net';
5
5
  import { Tracer } from '../telemetry';
6
6
 
7
+ export interface WorkflowNode {
8
+ id: string;
9
+ type: 'agent' | 'tool' | 'condition' | 'loop' | 'parallel' | 'input' | 'output';
10
+ name: string;
11
+ x: number;
12
+ y: number;
13
+ config: Record<string, any>;
14
+ }
15
+
16
+ export interface WorkflowEdge {
17
+ id: string;
18
+ from: string;
19
+ to: string;
20
+ fromPort: string;
21
+ toPort: string;
22
+ }
23
+
24
+ export interface WorkflowDefinition {
25
+ id: string;
26
+ name: string;
27
+ nodes: WorkflowNode[];
28
+ edges: WorkflowEdge[];
29
+ created: string;
30
+ updated: string;
31
+ }
32
+
7
33
  interface StudioConfig {
8
34
  port: number;
9
35
  agentDir: string;
@@ -100,6 +126,29 @@ class StudioServer {
100
126
  try {
101
127
  let data: any;
102
128
 
129
+ // Dynamic workflow routes (parameterized)
130
+ if (route.match(/^workflows\/[^/]+\/run$/) && req.method === 'POST') {
131
+ const wfId = route.split('/')[1];
132
+ data = await this.runWorkflow(wfId);
133
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
134
+ res.end(JSON.stringify(data));
135
+ return;
136
+ }
137
+ if (route.match(/^workflows\/[^/]+$/) && req.method === 'GET') {
138
+ const wfId = route.split('/')[1];
139
+ data = this.getWorkflowById(wfId);
140
+ res.writeHead(data.error ? 404 : 200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
141
+ res.end(JSON.stringify(data));
142
+ return;
143
+ }
144
+ if (route.match(/^workflows\/[^/]+$/) && req.method === 'DELETE') {
145
+ const wfId = route.split('/')[1];
146
+ data = this.deleteWorkflow(wfId);
147
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
148
+ res.end(JSON.stringify(data));
149
+ return;
150
+ }
151
+
103
152
  switch (route) {
104
153
  case 'modules':
105
154
  data = await this.getModulesStatus();
@@ -130,7 +179,12 @@ class StudioServer {
130
179
  data = await this.getTools();
131
180
  break;
132
181
  case 'workflows/list':
133
- data = await this.getWorkflows();
182
+ data = this.listWorkflows();
183
+ break;
184
+ case 'workflows':
185
+ if (req.method === 'POST') data = await this.saveWorkflow(req);
186
+ else if (req.method === 'GET') data = this.listWorkflows();
187
+ else { res.writeHead(405); res.end(); return; }
134
188
  break;
135
189
  case 'jobs/list':
136
190
  data = await this.getJobs();
@@ -188,6 +242,14 @@ class StudioServer {
188
242
  case 'telemetry/metrics':
189
243
  data = this.tracer ? this.tracer.getMetrics() : [];
190
244
  break;
245
+ case 'playground/chat':
246
+ if (req.method === 'POST') {
247
+ return this.handlePlaygroundChat(req, res);
248
+ }
249
+ res.writeHead(405); res.end(); return;
250
+ case 'playground/models':
251
+ data = { models: ['gpt-4o', 'gpt-4o-mini', 'claude-sonnet-4', 'claude-haiku', 'gemini-2.0-flash', 'deepseek-v3'] };
252
+ break;
191
253
  default:
192
254
  res.writeHead(404, { 'Content-Type': 'application/json' });
193
255
  res.end(JSON.stringify({ error: 'Not found' }));
@@ -333,9 +395,84 @@ class StudioServer {
333
395
  }
334
396
  }
335
397
 
336
- private async getWorkflows() {
398
+ private getWorkflowsDir(): string {
399
+ const dir = join(this.config.agentDir, '.opc', 'workflows');
400
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
401
+ return dir;
402
+ }
403
+
404
+ private listWorkflows(): { workflows: WorkflowDefinition[] } {
405
+ const dir = this.getWorkflowsDir();
406
+ const files = require('fs').readdirSync(dir).filter((f: string) => f.endsWith('.json'));
407
+ const workflows = files.map((f: string) => {
408
+ try { return JSON.parse(readFileSync(join(dir, f), 'utf-8')); } catch { return null; }
409
+ }).filter(Boolean);
410
+ // Also include OAD-defined workflows
337
411
  const oad = this.loadOAD();
338
- return { workflows: oad?.spec?.workflows || [] };
412
+ const oadWorkflows = (oad?.spec?.workflows || []).map((w: any, i: number) => ({
413
+ id: `oad-${i}`,
414
+ name: w.name || `Workflow ${i + 1}`,
415
+ nodes: [],
416
+ edges: [],
417
+ steps: w.steps,
418
+ source: 'oad',
419
+ }));
420
+ return { workflows: [...workflows, ...oadWorkflows] };
421
+ }
422
+
423
+ private getWorkflowById(id: string): WorkflowDefinition | { error: string } {
424
+ const filePath = join(this.getWorkflowsDir(), `${id}.json`);
425
+ if (!existsSync(filePath)) return { error: 'Workflow not found' };
426
+ return JSON.parse(readFileSync(filePath, 'utf-8'));
427
+ }
428
+
429
+ private async saveWorkflow(req: IncomingMessage): Promise<{ success: boolean; id: string }> {
430
+ const body = await this.readBody(req);
431
+ const workflow = JSON.parse(body) as WorkflowDefinition;
432
+ if (!workflow.id) workflow.id = `wf-${Date.now()}`;
433
+ workflow.updated = new Date().toISOString();
434
+ if (!workflow.created) workflow.created = workflow.updated;
435
+ const filePath = join(this.getWorkflowsDir(), `${workflow.id}.json`);
436
+ writeFileSync(filePath, JSON.stringify(workflow, null, 2));
437
+ return { success: true, id: workflow.id };
438
+ }
439
+
440
+ private deleteWorkflow(id: string): { success: boolean } {
441
+ const filePath = join(this.getWorkflowsDir(), `${id}.json`);
442
+ if (existsSync(filePath)) require('fs').unlinkSync(filePath);
443
+ return { success: true };
444
+ }
445
+
446
+ private async runWorkflow(id: string): Promise<any> {
447
+ const wf = this.getWorkflowById(id);
448
+ if ('error' in wf) return wf;
449
+ // Basic topological execution simulation
450
+ const results: Record<string, any> = {};
451
+ const sorted = this.topoSort(wf.nodes, wf.edges);
452
+ for (const node of sorted) {
453
+ results[node.id] = { type: node.type, name: node.name, status: 'completed', output: `[simulated output for ${node.name}]` };
454
+ }
455
+ return { workflowId: id, status: 'completed', results };
456
+ }
457
+
458
+ private topoSort(nodes: WorkflowNode[], edges: WorkflowEdge[]): WorkflowNode[] {
459
+ const nodeMap = new Map(nodes.map(n => [n.id, n]));
460
+ const inDegree = new Map<string, number>();
461
+ const adj = new Map<string, string[]>();
462
+ for (const n of nodes) { inDegree.set(n.id, 0); adj.set(n.id, []); }
463
+ for (const e of edges) { adj.get(e.from)?.push(e.to); inDegree.set(e.to, (inDegree.get(e.to) || 0) + 1); }
464
+ const queue = nodes.filter(n => (inDegree.get(n.id) || 0) === 0);
465
+ const result: WorkflowNode[] = [];
466
+ while (queue.length > 0) {
467
+ const node = queue.shift()!;
468
+ result.push(node);
469
+ for (const next of (adj.get(node.id) || [])) {
470
+ const d = (inDegree.get(next) || 1) - 1;
471
+ inDegree.set(next, d);
472
+ if (d === 0) queue.push(nodeMap.get(next)!);
473
+ }
474
+ }
475
+ return result;
339
476
  }
340
477
 
341
478
  private async getJobs() {
@@ -616,6 +753,31 @@ class StudioServer {
616
753
  res.end(content);
617
754
  }
618
755
 
756
+ private async handlePlaygroundChat(req: IncomingMessage, res: ServerResponse): Promise<void> {
757
+ const body = JSON.parse(await this.readBody(req));
758
+ const { messages = [], model = 'gpt-4o', temperature = 0.7, systemPrompt } = body;
759
+
760
+ res.writeHead(200, {
761
+ 'Content-Type': 'text/event-stream',
762
+ 'Cache-Control': 'no-cache',
763
+ 'Connection': 'keep-alive',
764
+ 'Access-Control-Allow-Origin': '*',
765
+ });
766
+
767
+ // Simulated streaming response for playground demo
768
+ const allMsgs = systemPrompt ? [{ role: 'system', content: systemPrompt }, ...messages] : messages;
769
+ const lastMsg = allMsgs[allMsgs.length - 1]?.content || '';
770
+ const response = `This is a playground demo response to: "${lastMsg}"\n\nModel: ${model}, Temperature: ${temperature}\nMessages in context: ${allMsgs.length}`;
771
+
772
+ const words = response.split(' ');
773
+ for (let i = 0; i < words.length; i++) {
774
+ const chunk = (i === 0 ? '' : ' ') + words[i];
775
+ res.write(`data: ${JSON.stringify({ content: chunk })}\n\n`);
776
+ }
777
+ res.write('data: [DONE]\n\n');
778
+ res.end();
779
+ }
780
+
619
781
  private readBody(req: IncomingMessage): Promise<string> {
620
782
  return new Promise((resolve, reject) => {
621
783
  let body = '';