@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.
- package/LICENSE +21 -0
- package/README.md +147 -0
- package/package.json +94 -0
- package/src/agents/base.js +361 -0
- package/src/constants.js +47 -0
- package/src/enrichment/base.js +49 -0
- package/src/enrichment/enrichers/accessibility-enricher.js +197 -0
- package/src/enrichment/enrichers/dom-enricher.js +171 -0
- package/src/enrichment/enrichers/page-state-enricher.js +129 -0
- package/src/enrichment/enrichers/position-enricher.js +67 -0
- package/src/enrichment/index.js +96 -0
- package/src/enrichment/mcp-integration.js +149 -0
- package/src/enrichment/mcp-ref-enricher.js +78 -0
- package/src/enrichment/pipeline.js +192 -0
- package/src/enrichment/trace-text-enricher.js +115 -0
- package/src/framework/AGENTS.md +98 -0
- package/src/framework/agents/base.js +72 -0
- package/src/framework/agents/claude-strategy.js +278 -0
- package/src/framework/agents/cursor-strategy.js +459 -0
- package/src/framework/agents/index.js +105 -0
- package/src/framework/agents/utils/cursor-output-formatter.js +67 -0
- package/src/framework/agents/utils/openai-proxy-formatter.js +249 -0
- package/src/framework/code-generator.js +301 -0
- package/src/framework/constants.js +33 -0
- package/src/framework/context-loader.js +101 -0
- package/src/framework/function-bridge.js +78 -0
- package/src/framework/function-skill-registry.js +20 -0
- package/src/framework/graph-compiler.js +342 -0
- package/src/framework/graph.js +610 -0
- package/src/framework/index.js +28 -0
- package/src/framework/node-registry.js +163 -0
- package/src/framework/node.js +259 -0
- package/src/framework/output-parser.js +71 -0
- package/src/framework/skill-registry.js +55 -0
- package/src/framework/state-utils.js +52 -0
- package/src/framework/state.js +67 -0
- package/src/framework/tool-resolver.js +65 -0
- package/src/index.js +342 -0
- package/src/runtime/generation/base.js +46 -0
- package/src/runtime/generation/index.js +70 -0
- package/src/runtime/generation/mcp-ref-strategy.js +197 -0
- package/src/runtime/generation/stable-id-strategy.js +170 -0
- package/src/runtime/stable-id-runtime.js +248 -0
- package/src/runtime/verification/base.js +44 -0
- package/src/runtime/verification/index.js +67 -0
- package/src/runtime/verification/playwright-json-strategy.js +119 -0
- package/src/runtime/zibby-runtime.js +299 -0
- package/src/sync/index.js +2 -0
- package/src/sync/uploader.js +29 -0
- package/src/tools/run-playwright-test.js +158 -0
- package/src/utils/adf-converter.js +68 -0
- package/src/utils/ast-utils.js +37 -0
- package/src/utils/ci-setup.js +124 -0
- package/src/utils/cursor-utils.js +71 -0
- package/src/utils/logger.js +144 -0
- package/src/utils/mcp-config-writer.js +115 -0
- package/src/utils/node-schema-parser.js +522 -0
- package/src/utils/post-process-events.js +55 -0
- package/src/utils/result-handler.js +102 -0
- package/src/utils/ripple-effect.js +84 -0
- package/src/utils/selector-generator.js +239 -0
- package/src/utils/streaming-parser.js +387 -0
- package/src/utils/test-post-processor.js +211 -0
- package/src/utils/timeline.js +217 -0
- package/src/utils/trace-parser.js +325 -0
- package/src/utils/video-organizer.js +91 -0
- package/templates/browser-test-automation/README.md +114 -0
- package/templates/browser-test-automation/graph.js +54 -0
- package/templates/browser-test-automation/nodes/execute-live.js +250 -0
- package/templates/browser-test-automation/nodes/generate-script.js +77 -0
- package/templates/browser-test-automation/nodes/index.js +3 -0
- package/templates/browser-test-automation/nodes/preflight.js +59 -0
- package/templates/browser-test-automation/nodes/utils.js +154 -0
- package/templates/browser-test-automation/result-handler.js +286 -0
- package/templates/code-analysis/graph.js +72 -0
- package/templates/code-analysis/index.js +18 -0
- package/templates/code-analysis/nodes/analyze-ticket-node.js +204 -0
- package/templates/code-analysis/nodes/create-pr-node.js +175 -0
- package/templates/code-analysis/nodes/finalize-node.js +118 -0
- package/templates/code-analysis/nodes/generate-code-node.js +425 -0
- package/templates/code-analysis/nodes/generate-test-cases-node.js +376 -0
- package/templates/code-analysis/nodes/services/prMetaService.js +86 -0
- package/templates/code-analysis/nodes/setup-node.js +142 -0
- package/templates/code-analysis/prompts/analyze-ticket.md +181 -0
- package/templates/code-analysis/prompts/generate-code.md +33 -0
- package/templates/code-analysis/prompts/generate-test-cases.md +110 -0
- package/templates/code-analysis/state.js +40 -0
- package/templates/code-implementation/graph.js +35 -0
- package/templates/code-implementation/index.js +7 -0
- package/templates/code-implementation/state.js +14 -0
- package/templates/global-setup.js +56 -0
- package/templates/index.js +94 -0
- 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
|
+
|
package/src/constants.js
ADDED
|
@@ -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
|
+
};
|