@zibby/core 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.
Files changed (93) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +147 -0
  3. package/package.json +94 -0
  4. package/src/agents/base.js +361 -0
  5. package/src/constants.js +47 -0
  6. package/src/enrichment/base.js +49 -0
  7. package/src/enrichment/enrichers/accessibility-enricher.js +197 -0
  8. package/src/enrichment/enrichers/dom-enricher.js +171 -0
  9. package/src/enrichment/enrichers/page-state-enricher.js +129 -0
  10. package/src/enrichment/enrichers/position-enricher.js +67 -0
  11. package/src/enrichment/index.js +96 -0
  12. package/src/enrichment/mcp-integration.js +149 -0
  13. package/src/enrichment/mcp-ref-enricher.js +78 -0
  14. package/src/enrichment/pipeline.js +192 -0
  15. package/src/enrichment/trace-text-enricher.js +115 -0
  16. package/src/framework/AGENTS.md +98 -0
  17. package/src/framework/agents/base.js +72 -0
  18. package/src/framework/agents/claude-strategy.js +278 -0
  19. package/src/framework/agents/cursor-strategy.js +459 -0
  20. package/src/framework/agents/index.js +105 -0
  21. package/src/framework/agents/utils/cursor-output-formatter.js +67 -0
  22. package/src/framework/agents/utils/openai-proxy-formatter.js +249 -0
  23. package/src/framework/code-generator.js +301 -0
  24. package/src/framework/constants.js +33 -0
  25. package/src/framework/context-loader.js +101 -0
  26. package/src/framework/function-bridge.js +78 -0
  27. package/src/framework/function-skill-registry.js +20 -0
  28. package/src/framework/graph-compiler.js +342 -0
  29. package/src/framework/graph.js +610 -0
  30. package/src/framework/index.js +28 -0
  31. package/src/framework/node-registry.js +163 -0
  32. package/src/framework/node.js +259 -0
  33. package/src/framework/output-parser.js +71 -0
  34. package/src/framework/skill-registry.js +55 -0
  35. package/src/framework/state-utils.js +52 -0
  36. package/src/framework/state.js +67 -0
  37. package/src/framework/tool-resolver.js +65 -0
  38. package/src/index.js +342 -0
  39. package/src/runtime/generation/base.js +46 -0
  40. package/src/runtime/generation/index.js +70 -0
  41. package/src/runtime/generation/mcp-ref-strategy.js +197 -0
  42. package/src/runtime/generation/stable-id-strategy.js +170 -0
  43. package/src/runtime/stable-id-runtime.js +248 -0
  44. package/src/runtime/verification/base.js +44 -0
  45. package/src/runtime/verification/index.js +67 -0
  46. package/src/runtime/verification/playwright-json-strategy.js +119 -0
  47. package/src/runtime/zibby-runtime.js +299 -0
  48. package/src/sync/index.js +2 -0
  49. package/src/sync/uploader.js +29 -0
  50. package/src/tools/run-playwright-test.js +158 -0
  51. package/src/utils/adf-converter.js +68 -0
  52. package/src/utils/ast-utils.js +37 -0
  53. package/src/utils/ci-setup.js +124 -0
  54. package/src/utils/cursor-utils.js +71 -0
  55. package/src/utils/logger.js +144 -0
  56. package/src/utils/mcp-config-writer.js +115 -0
  57. package/src/utils/node-schema-parser.js +522 -0
  58. package/src/utils/post-process-events.js +55 -0
  59. package/src/utils/result-handler.js +102 -0
  60. package/src/utils/ripple-effect.js +84 -0
  61. package/src/utils/selector-generator.js +239 -0
  62. package/src/utils/streaming-parser.js +387 -0
  63. package/src/utils/test-post-processor.js +211 -0
  64. package/src/utils/timeline.js +217 -0
  65. package/src/utils/trace-parser.js +325 -0
  66. package/src/utils/video-organizer.js +91 -0
  67. package/templates/browser-test-automation/README.md +114 -0
  68. package/templates/browser-test-automation/graph.js +54 -0
  69. package/templates/browser-test-automation/nodes/execute-live.js +250 -0
  70. package/templates/browser-test-automation/nodes/generate-script.js +77 -0
  71. package/templates/browser-test-automation/nodes/index.js +3 -0
  72. package/templates/browser-test-automation/nodes/preflight.js +59 -0
  73. package/templates/browser-test-automation/nodes/utils.js +154 -0
  74. package/templates/browser-test-automation/result-handler.js +286 -0
  75. package/templates/code-analysis/graph.js +72 -0
  76. package/templates/code-analysis/index.js +18 -0
  77. package/templates/code-analysis/nodes/analyze-ticket-node.js +204 -0
  78. package/templates/code-analysis/nodes/create-pr-node.js +175 -0
  79. package/templates/code-analysis/nodes/finalize-node.js +118 -0
  80. package/templates/code-analysis/nodes/generate-code-node.js +425 -0
  81. package/templates/code-analysis/nodes/generate-test-cases-node.js +376 -0
  82. package/templates/code-analysis/nodes/services/prMetaService.js +86 -0
  83. package/templates/code-analysis/nodes/setup-node.js +142 -0
  84. package/templates/code-analysis/prompts/analyze-ticket.md +181 -0
  85. package/templates/code-analysis/prompts/generate-code.md +33 -0
  86. package/templates/code-analysis/prompts/generate-test-cases.md +110 -0
  87. package/templates/code-analysis/state.js +40 -0
  88. package/templates/code-implementation/graph.js +35 -0
  89. package/templates/code-implementation/index.js +7 -0
  90. package/templates/code-implementation/state.js +14 -0
  91. package/templates/global-setup.js +56 -0
  92. package/templates/index.js +94 -0
  93. package/templates/register-nodes.js +24 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zibby
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,147 @@
1
+ # @zibby/test-automation-core
2
+
3
+ Core test automation engine with multi-agent support, DAG-based workflow execution, and extensible skill system.
4
+
5
+ ## Features
6
+
7
+ - **Multi-Agent Support** — Cursor (via cursor-agent CLI) and Claude (via Anthropic API)
8
+ - **Workflow Engine** — DAG-based graph compiler with typed state, conditional branching, and lifecycle hooks
9
+ - **Skill System** — Extensible registry that maps capabilities to MCP servers
10
+ - **Node Registry** — Register custom workflow nodes with Zod schemas and prompts
11
+ - **Code Generator** — Compile workflow graphs into standalone executable scripts
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install @zibby/test-automation-core
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ### Functional Style (recommended)
22
+
23
+ ```javascript
24
+ import { workflow, z } from '@zibby/test-automation-core';
25
+ import '@zibby/skills';
26
+
27
+ const agent = workflow((graph) => {
28
+ graph.addNode('plan', {
29
+ name: 'plan',
30
+ prompt: (state) => `Analyze: "${state.input}". Return JSON.`,
31
+ outputSchema: z.object({ action: z.string() }),
32
+ });
33
+ graph.addNode('execute', {
34
+ name: 'execute',
35
+ skills: ['browser'],
36
+ prompt: (state) => `Execute: ${state.plan.action}`,
37
+ outputSchema: z.object({ result: z.string() }),
38
+ });
39
+ graph.setEntryPoint('plan');
40
+ graph.addEdge('plan', 'execute');
41
+ graph.addEdge('execute', 'END');
42
+ });
43
+
44
+ await agent.run('Navigate to example.com and verify the title');
45
+ ```
46
+
47
+ ### Class-Based Style
48
+
49
+ ```javascript
50
+ import { WorkflowAgent, WorkflowGraph, z } from '@zibby/test-automation-core';
51
+
52
+ class MyAgent extends WorkflowAgent {
53
+ buildGraph() {
54
+ const graph = new WorkflowGraph();
55
+ graph.addNode('plan', { /* ... */ });
56
+ graph.addNode('execute', { /* ... */ });
57
+ graph.setEntryPoint('plan');
58
+ graph.addEdge('plan', 'execute');
59
+ graph.addEdge('execute', 'END');
60
+ return graph;
61
+ }
62
+ }
63
+
64
+ const agent = new MyAgent();
65
+ await agent.run('Do something');
66
+ ```
67
+
68
+ ### Custom Nodes
69
+
70
+ Define workflow nodes with Zod output schemas:
71
+
72
+ ```javascript
73
+ import { z } from '@zibby/test-automation-core';
74
+
75
+ export const myNode = {
76
+ name: 'analyze',
77
+ skills: ['browser'],
78
+ prompt: (state) => `Analyze ${state.input} and extract key information`,
79
+ outputSchema: z.object({
80
+ title: z.string(),
81
+ links: z.array(z.string()),
82
+ }),
83
+ };
84
+ ```
85
+
86
+ ### Skill Registry
87
+
88
+ ```javascript
89
+ import { registerSkill, getSkill, listSkillIds } from '@zibby/test-automation-core';
90
+
91
+ // Register a custom MCP skill
92
+ registerSkill({
93
+ id: 'my-tool',
94
+ serverName: 'my-mcp-server',
95
+ command: 'npx',
96
+ args: ['my-mcp-package'],
97
+ allowedTools: ['do_thing'],
98
+ resolve: () => ({ command: 'npx', args: ['my-mcp-package'] }),
99
+ });
100
+
101
+ // Query the registry
102
+ const skill = getSkill('browser');
103
+ const allIds = listSkillIds();
104
+ ```
105
+
106
+ ## Architecture
107
+
108
+ ```
109
+ @zibby/test-automation-core
110
+ ├── src/
111
+ │ ├── framework/ # Workflow engine
112
+ │ │ ├── graph.js # WorkflowGraph (DAG builder)
113
+ │ │ ├── graph-compiler.js # Compiles graphs to execution plans
114
+ │ │ ├── node.js # Node execution with agent invocation
115
+ │ │ ├── node-registry.js # Register/lookup custom nodes
116
+ │ │ ├── skill-registry.js # Skill → MCP server mapping
117
+ │ │ ├── tool-resolver.js # Resolves skills to MCP configs
118
+ │ │ ├── code-generator.js # Generate standalone scripts
119
+ │ │ ├── state.js # Typed workflow state
120
+ │ │ └── agents/ # Agent strategies
121
+ │ │ ├── cursor-strategy.js
122
+ │ │ └── claude-strategy.js
123
+ │ ├── runtime/ # Browser runtime utilities
124
+ │ ├── utils/ # Helpers (logger, selectors, parsers)
125
+ │ └── index.js # Public API
126
+ ├── templates/ # Built-in workflow templates
127
+ │ ├── browser-test-automation/
128
+ │ └── code-analysis/
129
+ └── scripts/ # Setup scripts (Playwright MCP, CI)
130
+ ```
131
+
132
+ ## Environment Variables
133
+
134
+ ```bash
135
+ # Cursor agent (CI/CD only — local uses stored credentials)
136
+ CURSOR_API_KEY=your-cursor-token
137
+
138
+ # Claude agent
139
+ ANTHROPIC_API_KEY=sk-ant-...
140
+
141
+ # Cloud sync (optional)
142
+ ZIBBY_API_KEY=zby_xxx
143
+ ```
144
+
145
+ ## License
146
+
147
+ MIT
package/package.json ADDED
@@ -0,0 +1,94 @@
1
+ {
2
+ "name": "@zibby/core",
3
+ "version": "0.1.0",
4
+ "description": "Core test automation engine with multi-agent and multi-MCP support",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js",
9
+ "./sync": "./src/sync/index.js",
10
+ "./framework/graph.js": "./src/framework/graph.js",
11
+ "./framework/state.js": "./src/framework/state.js",
12
+ "./framework/node.js": "./src/framework/node.js",
13
+ "./framework/graph-compiler.js": "./src/framework/graph-compiler.js",
14
+ "./framework/node-registry.js": "./src/framework/node-registry.js",
15
+ "./framework/skill-registry.js": "./src/framework/skill-registry.js",
16
+ "./framework/tool-resolver.js": "./src/framework/tool-resolver.js",
17
+ "./framework/function-bridge.js": "./src/framework/function-bridge.js",
18
+ "./framework/function-skill-registry.js": "./src/framework/function-skill-registry.js",
19
+ "./framework/code-generator.js": "./src/framework/code-generator.js",
20
+ "./utils/ast-utils.js": "./src/utils/ast-utils.js",
21
+ "./utils/mcp-config-writer.js": "./src/utils/mcp-config-writer.js",
22
+ "./utils/node-schema-parser.js": "./src/utils/node-schema-parser.js",
23
+ "./templates/register-nodes.js": "./templates/register-nodes.js",
24
+ "./templates": "./templates/index.js",
25
+ "./templates/*": "./templates/*"
26
+ },
27
+ "scripts": {
28
+ "test": "vitest run",
29
+ "test:watch": "vitest",
30
+ "test:state-schema": "vitest run src/framework/__tests__/state-schema.test.js",
31
+ "test:state-schema:e2e": "node src/framework/__tests__/state-schema.e2e.test.js",
32
+ "export:workflows": "node scripts/export-default-workflows.js",
33
+ "lint": "eslint .",
34
+ "lint:fix": "eslint --fix .",
35
+ "prepublishOnly": "npm run lint && npm test"
36
+ },
37
+ "keywords": [
38
+ "testing",
39
+ "automation",
40
+ "playwright",
41
+ "ai",
42
+ "mcp"
43
+ ],
44
+ "author": "Zibby",
45
+ "license": "MIT",
46
+ "homepage": "https://zibby.app",
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "https://github.com/ZibbyHQ/zibby-agent"
50
+ },
51
+ "bugs": {
52
+ "url": "https://github.com/ZibbyHQ/zibby-agent/issues"
53
+ },
54
+ "files": [
55
+ "src/",
56
+ "!src/**/__tests__/",
57
+ "!src/**/*.test.js",
58
+ "!src/**/*.spec.js",
59
+ "templates/",
60
+ "!templates/**/__tests__/",
61
+ "!templates/**/*.test.js",
62
+ "!templates/**/*.spec.js",
63
+ "README.md",
64
+ "LICENSE"
65
+ ],
66
+ "engines": {
67
+ "node": ">=18.0.0"
68
+ },
69
+ "dependencies": {
70
+ "@anthropic-ai/claude-agent-sdk": "^0.2.50",
71
+ "@anthropic-ai/sdk": "^0.71.2",
72
+ "@modelcontextprotocol/sdk": "^1.27.1",
73
+ "@playwright/mcp": "^0.0.54",
74
+ "acorn": "^8.15.0",
75
+ "acorn-walk": "^8.3.4",
76
+ "axios": "^1.13.3",
77
+ "chalk": "^5.3.0",
78
+ "dotenv": "^16.4.0",
79
+ "handlebars": "^4.7.8",
80
+ "zod": "^3.23.0",
81
+ "zod-to-json-schema": "^3.25.1"
82
+ },
83
+ "optionalDependencies": {
84
+ "@zibby/mcp-browser": "*"
85
+ },
86
+ "peerDependencies": {
87
+ "@playwright/test": ">=1.49.0",
88
+ "playwright": ">=1.49.0"
89
+ },
90
+ "devDependencies": {
91
+ "@playwright/test": "^1.49.0",
92
+ "vitest": "^4.0.18"
93
+ }
94
+ }
@@ -0,0 +1,361 @@
1
+ import { spawn } from 'child_process';
2
+ import { mkdirSync, existsSync, writeFileSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { ContextLoader } from '../framework/context-loader.js';
5
+ import { StreamingParser } from '../utils/streaming-parser.js';
6
+ import { findCursorAgentPath } from '../utils/cursor-utils.js';
7
+ import { DEFAULT_OUTPUT_BASE, SESSIONS_DIR, SESSION_INFO_FILE } from '../framework/constants.js';
8
+ import { WorkflowGraph } from '../framework/graph.js';
9
+
10
+ export class WorkflowAgent {
11
+ constructor(config = {}) {
12
+ this.config = config;
13
+ this.adapter = null;
14
+ this.paths = config.paths || { specs: 'test-specs', generated: 'tests' };
15
+ this.agentCommand = config.agentCommand || 'cursor-agent';
16
+ this.buildArgs = config.buildArgs || ((prompt, useStreaming = true) => {
17
+ const args = [
18
+ '-p',
19
+ prompt,
20
+ '--approve-mcps',
21
+ '--force'
22
+ ];
23
+
24
+ // Only use streaming for non-structured output
25
+ if (useStreaming) {
26
+ args.push('--output-format', 'stream-json');
27
+ args.push('--stream-partial-output');
28
+ }
29
+
30
+ return args;
31
+ });
32
+ }
33
+
34
+ /**
35
+ * Extract JSON result from NDJSON streaming output
36
+ * Uses StreamingParser for clean, reliable extraction
37
+ */
38
+ static extractJsonFromStream(rawOutput) {
39
+ return StreamingParser.extractResult(rawOutput);
40
+ }
41
+
42
+ async initialize(adapter) {
43
+ this.adapter = adapter;
44
+ if (adapter && !adapter.isConnected()) {
45
+ await adapter.connect();
46
+ }
47
+ }
48
+
49
+ buildGraph() {
50
+ throw new Error('buildGraph() must be implemented by subclass');
51
+ }
52
+
53
+ async onComplete(_result) {
54
+ // Override in subclass for post-execution processing
55
+ // (e.g., save artifacts, enrich events, rename videos)
56
+ }
57
+
58
+ async run(input, options = {}) {
59
+ const graph = this.buildGraph();
60
+ const initialState = typeof input === 'object' && !Array.isArray(input)
61
+ ? { input, ...input, ...options }
62
+ : { input, ...options };
63
+ const result = await graph.run(this, initialState);
64
+ return result;
65
+ }
66
+
67
+ async executeNode(nodeConfig, state) {
68
+ const { prompt, outputSchema, model } = nodeConfig;
69
+ const promptText = typeof prompt === 'function' ? prompt(state) : prompt;
70
+
71
+ console.log(`\n📝 Prompt:\n${promptText}\n`);
72
+
73
+ const output = await this.executePrompt(promptText, state.cwd, 300000, model);
74
+
75
+ console.log(`\n📤 Raw Output:\n${output}\n`);
76
+
77
+ let parsedOutput = null;
78
+ if (outputSchema) {
79
+ try {
80
+ // Use the static helper to extract JSON from streaming output
81
+ parsedOutput = WorkflowAgent.extractJsonFromStream(output);
82
+
83
+ if (!parsedOutput) {
84
+ throw new Error('No valid result JSON found in output');
85
+ }
86
+
87
+ console.log(`\n✅ Parsed Output:\n${JSON.stringify(parsedOutput, null, 2)}\n`);
88
+ } catch (err) {
89
+ console.warn(`⚠️ Failed to parse output as JSON: ${err.message}`);
90
+ }
91
+ }
92
+
93
+ return { success: true, output: parsedOutput, raw: output };
94
+ }
95
+
96
+ async cleanup() {
97
+ if (this.adapter && this.adapter.isConnected()) {
98
+ await this.adapter.disconnect();
99
+ }
100
+ }
101
+
102
+ async executePrompt(prompt, cwd = process.cwd(), timeoutMs = 300000, model = null, useStreaming = true) {
103
+ // Resolve cursor-agent path if needed (handles cases where PATH isn't set)
104
+ let agentCommand = this.agentCommand;
105
+ if (agentCommand === 'cursor-agent') {
106
+ const resolvedPath = await findCursorAgentPath();
107
+ if (resolvedPath) {
108
+ agentCommand = resolvedPath;
109
+ }
110
+ // If not found, keep original command - error will be clearer from spawn
111
+ }
112
+
113
+ return new Promise((resolve, reject) => {
114
+ let output = '';
115
+ const args = this.buildArgs(prompt, useStreaming);
116
+
117
+ if (model) {
118
+ args.push('--model', model);
119
+ }
120
+
121
+ const env = { ...process.env };
122
+
123
+ const agent = spawn(agentCommand, args, {
124
+ cwd,
125
+ env,
126
+ stdio: ['inherit', 'pipe', 'inherit'], // Let stderr pass through for typewriter effect
127
+ shell: false,
128
+ detached: false,
129
+ });
130
+
131
+ const timeout = setTimeout(() => {
132
+ console.log(`\n⏱️ Timeout reached (${timeoutMs / 1000}s) - killing agent...`);
133
+ agent.kill('SIGTERM');
134
+ // Force kill if still running after 2 seconds
135
+ setTimeout(() => {
136
+ if (!agent.killed) {
137
+ console.log('⚠️ Forcing kill (SIGKILL)...');
138
+ agent.kill('SIGKILL');
139
+ }
140
+ }, 2000);
141
+ reject(new Error(`Agent timed out after ${timeoutMs / 1000}s. The agent may be stuck or waiting for user input.`));
142
+ }, timeoutMs);
143
+
144
+ let lastOutputLength = 0;
145
+ let lastOutputTime = Date.now();
146
+ const heartbeat = setInterval(() => {
147
+ if (output.length > lastOutputLength) {
148
+ // Output is coming in, no need to show heartbeat
149
+ lastOutputLength = output.length;
150
+ lastOutputTime = Date.now();
151
+ } else {
152
+ // Only show "thinking" message if no output for 10 seconds
153
+ const timeSinceLastOutput = Date.now() - lastOutputTime;
154
+ if (timeSinceLastOutput > 10000) {
155
+ console.log(`⏳ AI agent is thinking... (${Math.floor(timeSinceLastOutput / 1000)}s, ${output.length} bytes so far)`);
156
+ }
157
+ }
158
+ }, 10000);
159
+
160
+ // Handle Ctrl+C gracefully
161
+ const cleanup = () => {
162
+ clearTimeout(timeout);
163
+ clearInterval(heartbeat);
164
+ if (agent && !agent.killed) {
165
+ agent.kill('SIGTERM');
166
+ // Force kill if not dead after 2 seconds
167
+ setTimeout(() => {
168
+ if (!agent.killed) {
169
+ agent.kill('SIGKILL');
170
+ }
171
+ }, 2000);
172
+ }
173
+ };
174
+
175
+ const sigintHandler = () => {
176
+ console.log('\n\n🛑 Interrupted by user (Ctrl+C)');
177
+ cleanup();
178
+ // Don't reset terminal - it's too aggressive
179
+ reject(new Error('Interrupted by user'));
180
+ };
181
+
182
+ process.on('SIGINT', sigintHandler);
183
+
184
+ // Use StreamingParser for clean, reliable parsing
185
+ const parser = new StreamingParser();
186
+
187
+ // Handle stdout - cursor-agent outputs NDJSON protocol messages
188
+ agent.stdout.on('data', (data) => {
189
+ const chunk = data.toString();
190
+ output += chunk; // Accumulate raw output
191
+
192
+ const displayText = parser.processChunk(chunk);
193
+
194
+ if (displayText) {
195
+ // Write the extracted text immediately (no buffering)
196
+ // This creates the typewriter effect as cursor-agent streams
197
+ process.stdout.write(displayText, 'utf8', () => {
198
+ // Flush immediately after write
199
+ if (process.stdout.isTTY) {
200
+ process.stdout._flush && process.stdout._flush();
201
+ }
202
+ });
203
+ lastOutputTime = Date.now();
204
+ lastOutputLength += displayText.length;
205
+ }
206
+ });
207
+
208
+ agent.on('close', (code) => {
209
+ process.off('SIGINT', sigintHandler);
210
+ clearTimeout(timeout);
211
+ clearInterval(heartbeat);
212
+
213
+ // Flush any remaining buffer
214
+ const remainingText = parser.flush();
215
+ if (remainingText) {
216
+ process.stdout.write(remainingText);
217
+ }
218
+
219
+ // Get the complete output (for JSON extraction, includes protocol)
220
+ output = parser.getRawText();
221
+
222
+ // Also store the extracted result for direct access
223
+ const extractedResult = parser.getResult();
224
+
225
+ if (code === 0) {
226
+ // Return both raw output and extracted result
227
+ resolve({ raw: output, extracted: extractedResult });
228
+ } else {
229
+ reject(new Error(`Agent exited with code ${code}`));
230
+ }
231
+ });
232
+
233
+ agent.on('error', (error) => {
234
+ process.off('SIGINT', sigintHandler);
235
+ clearTimeout(timeout);
236
+ clearInterval(heartbeat);
237
+ reject(new Error(`Failed to spawn agent: ${error.message}`));
238
+ });
239
+ });
240
+ }
241
+
242
+ /**
243
+ * Run a single node (for testing/debugging)
244
+ * This is a framework feature, not part of workflow definition
245
+ */
246
+ async runSingleNode(nodeName, nodeMap, initialState) {
247
+ const nodeConfig = nodeMap[nodeName];
248
+ if (!nodeConfig) {
249
+ throw new Error(`Unknown node: ${nodeName}. Available nodes: ${Object.keys(nodeMap).join(', ')}`);
250
+ }
251
+
252
+ const { cwd } = initialState;
253
+ if (!cwd) {
254
+ throw new Error('cwd is required for single node execution');
255
+ }
256
+
257
+ // Reuse existing session if provided (--session flag), otherwise create new
258
+ let sessionPath = initialState.sessionPath;
259
+ let sessionTimestamp = initialState.sessionTimestamp;
260
+ const config = initialState.config || {};
261
+
262
+ if (!sessionPath) {
263
+ // Generate CI-aware session ID
264
+ const ciSessionId = process.env.CI_JOB_ID ||
265
+ process.env.GITHUB_RUN_ID ||
266
+ process.env.CIRCLE_WORKFLOW_ID ||
267
+ process.env.BUILD_ID;
268
+
269
+ const baseId = ciSessionId || Date.now().toString();
270
+ const prefix = config.paths?.sessionPrefix;
271
+ const sessionId = prefix ? `${prefix}_${baseId}` : baseId;
272
+
273
+ sessionTimestamp = sessionTimestamp || Date.now();
274
+
275
+ // Use configurable output path
276
+ const outputBase = config.paths?.output || DEFAULT_OUTPUT_BASE;
277
+ sessionPath = join(cwd, outputBase, SESSIONS_DIR, sessionId);
278
+
279
+ if (!existsSync(sessionPath)) {
280
+ mkdirSync(sessionPath, { recursive: true });
281
+ }
282
+ }
283
+
284
+ // Write session info to output directory
285
+ const outputBase = config.paths?.output || DEFAULT_OUTPUT_BASE;
286
+ const sessionInfoPath = join(cwd, outputBase, SESSION_INFO_FILE);
287
+ mkdirSync(join(cwd, outputBase), { recursive: true });
288
+ writeFileSync(sessionInfoPath, JSON.stringify({
289
+ sessionPath,
290
+ sessionTimestamp: sessionTimestamp || sessionPath.split('/').pop(),
291
+ currentNode: nodeName,
292
+ createdAt: new Date().toISOString()
293
+ }), 'utf-8');
294
+
295
+ console.log(`\n${'='.repeat(80)}`);
296
+ console.log(`🎯 SINGLE NODE EXECUTION: ${nodeName}`);
297
+ console.log(`📁 Session: ${sessionPath.split('/').pop()}${initialState.sessionPath ? ' (reusing)' : ''}`);
298
+ console.log(`${'='.repeat(80)}\n`);
299
+
300
+ const context = await ContextLoader.loadContext(
301
+ initialState.specPath || '',
302
+ cwd,
303
+ initialState.contextConfig || {}
304
+ );
305
+
306
+ // Use Node class for consistent behavior with workflow execution
307
+ const { Node } = await import('../framework/node.js');
308
+ const { WorkflowState } = await import('../framework/state.js');
309
+
310
+ const state = new WorkflowState({
311
+ ...initialState,
312
+ sessionPath,
313
+ sessionTimestamp,
314
+ context
315
+ });
316
+
317
+ const node = new Node(nodeConfig);
318
+ const result = await node.execute(this, state);
319
+
320
+ console.log(`\n${'='.repeat(80)}`);
321
+ console.log(`✅ Node ${nodeName} completed`);
322
+ console.log(`${'='.repeat(80)}\n`);
323
+
324
+ return { success: true, output: result.output, outputPath: initialState.outputPath, state: result };
325
+ }
326
+
327
+ calculateOutputPath(specPath) {
328
+ const { specs, generated } = this.paths;
329
+ if (!specPath) return `${generated}/generated-test.spec.js`;
330
+
331
+ const relativePath = specPath.replace(new RegExp(`^${specs}/`), '').replace(/\.[^.]+$/, '.spec.js');
332
+ return `${generated}/${relativePath}`.replace(/\/+/g, '/');
333
+ }
334
+ }
335
+
336
+ /**
337
+ * Functional workflow factory — no class boilerplate needed.
338
+ *
339
+ * const agent = workflow((graph) => {
340
+ * graph.addNode('plan', { prompt: ..., outputSchema: ... });
341
+ * graph.addNode('execute', { skills: ['calculator'], prompt: ... });
342
+ * graph.setEntryPoint('plan');
343
+ * graph.addEdge('plan', 'execute');
344
+ * graph.addEdge('execute', 'END');
345
+ * });
346
+ *
347
+ * await agent.run("What is 15 plus 27?");
348
+ */
349
+ export function workflow(builder, options = {}) {
350
+ const agent = new WorkflowAgent(options);
351
+ agent.buildGraph = function () {
352
+ const graph = new WorkflowGraph();
353
+ builder(graph);
354
+ return graph;
355
+ };
356
+ if (options.onComplete) {
357
+ agent.onComplete = options.onComplete;
358
+ }
359
+ return agent;
360
+ }
361
+
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Zibby Framework Constants
3
+ * Single source of truth for all configuration values
4
+ */
5
+
6
+ export const DEFAULT_MODELS = {
7
+ CLAUDE: 'claude-sonnet-4-6',
8
+ CURSOR: 'auto',
9
+ OPENAI_POSTPROCESSING: 'gpt-4o-mini'
10
+ };
11
+
12
+ export const AGENT_TYPES = {
13
+ CLAUDE: 'claude',
14
+ CURSOR: 'cursor'
15
+ };
16
+
17
+ export const LOG_LEVELS = {
18
+ DEBUG: 'debug',
19
+ INFO: 'info',
20
+ WARN: 'warn',
21
+ ERROR: 'error',
22
+ SILENT: 'silent'
23
+ };
24
+
25
+ export const CLAUDE_MODEL_MAP = {
26
+ 'auto': 'claude-sonnet-4-6',
27
+ // 4.6 aliases
28
+ 'sonnet-4.6': 'claude-sonnet-4-6',
29
+ 'sonnet-4-6': 'claude-sonnet-4-6',
30
+ 'opus-4.6': 'claude-opus-4-6',
31
+ 'opus-4-6': 'claude-opus-4-6',
32
+ // 4.5 aliases
33
+ 'sonnet-4.5': 'claude-sonnet-4-5-20250929',
34
+ 'sonnet-4-5': 'claude-sonnet-4-5-20250929',
35
+ 'opus-4.5': 'claude-opus-4-20250514',
36
+ 'opus-4-5': 'claude-opus-4-20250514',
37
+ // Direct API IDs (pass-through)
38
+ 'claude-sonnet-4-6': 'claude-sonnet-4-6',
39
+ 'claude-opus-4-6': 'claude-opus-4-6',
40
+ 'claude-sonnet-4-5-20250929': 'claude-sonnet-4-5-20250929',
41
+ 'claude-opus-4-20250514': 'claude-opus-4-20250514',
42
+ };
43
+
44
+ export const TIMEOUTS = {
45
+ CURSOR_AGENT_DEFAULT: 20 * 60 * 1000,
46
+ OPENAI_REQUEST: 30000
47
+ };