@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
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert Atlassian Document Format (ADF) to plain text
|
|
3
|
+
* Used to convert Jira ticket descriptions from ADF JSON format to readable text
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Convert ADF document to plain text
|
|
8
|
+
* @param {Object|string} adf - Atlassian Document Format object or string
|
|
9
|
+
* @returns {string} Plain text representation
|
|
10
|
+
*/
|
|
11
|
+
export function adfToText(adf) {
|
|
12
|
+
if (!adf || typeof adf !== 'object') {
|
|
13
|
+
return String(adf || '');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// If it's ADF format, extract text recursively
|
|
17
|
+
if (adf.type === 'doc' && Array.isArray(adf.content)) {
|
|
18
|
+
return adf.content.map(node => adfNodeToText(node)).join('\n\n');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Fallback to JSON stringify
|
|
22
|
+
return JSON.stringify(adf, null, 2);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Convert a single ADF node to plain text
|
|
27
|
+
* @param {Object} node - ADF node
|
|
28
|
+
* @returns {string} Plain text representation of the node
|
|
29
|
+
*/
|
|
30
|
+
function adfNodeToText(node) {
|
|
31
|
+
if (!node) return '';
|
|
32
|
+
|
|
33
|
+
if (node.type === 'text') {
|
|
34
|
+
return node.text || '';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (node.type === 'paragraph' && Array.isArray(node.content)) {
|
|
38
|
+
return node.content.map(adfNodeToText).join('');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (node.type === 'heading' && Array.isArray(node.content)) {
|
|
42
|
+
return node.content.map(adfNodeToText).join('');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (node.type === 'bulletList' && Array.isArray(node.content)) {
|
|
46
|
+
return node.content.map(item => `• ${ adfNodeToText(item)}`).join('\n');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (node.type === 'orderedList' && Array.isArray(node.content)) {
|
|
50
|
+
return node.content.map((item, i) => `${i + 1}. ${ adfNodeToText(item)}`).join('\n');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (node.type === 'listItem' && Array.isArray(node.content)) {
|
|
54
|
+
return node.content.map(adfNodeToText).join('');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (node.type === 'codeBlock' && Array.isArray(node.content)) {
|
|
58
|
+
const code = node.content.map(adfNodeToText).join('');
|
|
59
|
+
const lang = node.attrs?.language || '';
|
|
60
|
+
return `\`\`\`${lang}\n${code}\n\`\`\``;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (Array.isArray(node.content)) {
|
|
64
|
+
return node.content.map(adfNodeToText).join('');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return '';
|
|
68
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as acorn from 'acorn';
|
|
2
|
+
import * as walk from 'acorn-walk';
|
|
3
|
+
|
|
4
|
+
export function hasAgentCall(code) {
|
|
5
|
+
if (!code || typeof code !== 'string') return false;
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
const wrappedCode = code.trimStart().startsWith('async')
|
|
9
|
+
? `const __fn = ${code}`
|
|
10
|
+
: code;
|
|
11
|
+
|
|
12
|
+
const ast = acorn.parse(wrappedCode, {
|
|
13
|
+
ecmaVersion: 'latest',
|
|
14
|
+
sourceType: 'module',
|
|
15
|
+
allowAwaitOutsideModules: true
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
let found = false;
|
|
19
|
+
walk.simple(ast, {
|
|
20
|
+
CallExpression(node) {
|
|
21
|
+
if (found) return;
|
|
22
|
+
const callee = node.callee;
|
|
23
|
+
if (callee.type === 'Identifier' && callee.name === 'invokeAgent') {
|
|
24
|
+
found = true;
|
|
25
|
+
}
|
|
26
|
+
if (callee.type === 'MemberExpression' &&
|
|
27
|
+
callee.property.type === 'Identifier' &&
|
|
28
|
+
callee.property.name === 'invokeAgent') {
|
|
29
|
+
found = true;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
return found;
|
|
34
|
+
} catch {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
|
|
6
|
+
export async function patchCursorAgentForCI() {
|
|
7
|
+
const versionsDir = join(homedir(), '.local/share/cursor-agent/versions');
|
|
8
|
+
|
|
9
|
+
if (!existsSync(versionsDir)) {
|
|
10
|
+
throw new Error(`cursor-agent not found at ${versionsDir}. Please install cursor-agent first.`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
console.log('🔧 Patching cursor-agent for CI/CD...\n');
|
|
14
|
+
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
const pythonScript = join(__dirname, '../../scripts/patch-cursor-mcp.py');
|
|
17
|
+
|
|
18
|
+
if (!existsSync(pythonScript)) {
|
|
19
|
+
reject(new Error('Patch script not found'));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const patch = spawn('python3', [pythonScript], {
|
|
24
|
+
stdio: 'inherit',
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
patch.on('close', (code) => {
|
|
28
|
+
if (code === 0) {
|
|
29
|
+
resolve({ success: true });
|
|
30
|
+
} else {
|
|
31
|
+
reject(new Error(`Patch failed with code ${code}`));
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
patch.on('error', (error) => {
|
|
36
|
+
reject(error);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function checkCursorAgentPatched() {
|
|
42
|
+
const versionsDir = join(homedir(), '.local/share/cursor-agent/versions');
|
|
43
|
+
|
|
44
|
+
if (!existsSync(versionsDir)) {
|
|
45
|
+
return { patched: false, installed: false };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const versions = require('fs').readdirSync(versionsDir);
|
|
50
|
+
if (versions.length === 0) {
|
|
51
|
+
return { patched: false, installed: false };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const latestVersion = versions.sort().reverse()[0];
|
|
55
|
+
const indexFile = join(versionsDir, latestVersion, 'index.js');
|
|
56
|
+
|
|
57
|
+
if (!existsSync(indexFile)) {
|
|
58
|
+
return { patched: false, installed: false };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const content = readFileSync(indexFile, 'utf-8');
|
|
62
|
+
const patched = content.includes('AUTO-APPROVE MCP TOOLS FOR CI/CD');
|
|
63
|
+
|
|
64
|
+
return { patched, installed: true, path: indexFile };
|
|
65
|
+
} catch (error) {
|
|
66
|
+
return { patched: false, installed: false, error: error.message };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function getApprovalKeys(projectPath) {
|
|
71
|
+
console.log('🔑 Getting MCP approval keys...\n');
|
|
72
|
+
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
const getKeys = spawn('cursor-agent', ['mcp', 'list'], {
|
|
75
|
+
cwd: projectPath,
|
|
76
|
+
stdio: 'pipe',
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
let output = '';
|
|
80
|
+
|
|
81
|
+
getKeys.stdout.on('data', (data) => {
|
|
82
|
+
output += data.toString();
|
|
83
|
+
process.stdout.write(data);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
getKeys.stderr.on('data', (data) => {
|
|
87
|
+
process.stderr.write(data);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
getKeys.on('close', (code) => {
|
|
91
|
+
if (code === 0) {
|
|
92
|
+
const keys = parseApprovalKeys(output);
|
|
93
|
+
resolve({ success: true, keys, output });
|
|
94
|
+
} else {
|
|
95
|
+
reject(new Error(`Failed to get approval keys (exit code ${code})`));
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
getKeys.on('error', (error) => {
|
|
100
|
+
reject(error);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function parseApprovalKeys(output) {
|
|
106
|
+
const keys = {};
|
|
107
|
+
const regex = /🔑 APPROVAL KEY:\s+(\S+)\s+=>\s+(\S+)/g;
|
|
108
|
+
let match;
|
|
109
|
+
|
|
110
|
+
while ((match = regex.exec(output)) !== null) {
|
|
111
|
+
keys[match[1]] = match[2];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return keys;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function saveApprovalKeys(projectPath, keys) {
|
|
118
|
+
const cursorDir = join(projectPath, '.cursor/projects');
|
|
119
|
+
const approvalsFile = join(cursorDir, 'mcp-approvals.json');
|
|
120
|
+
|
|
121
|
+
writeFileSync(approvalsFile, JSON.stringify(keys, null, 2));
|
|
122
|
+
console.log(`\n✅ Saved approval keys to: ${approvalsFile}\n`);
|
|
123
|
+
}
|
|
124
|
+
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor Agent Utilities
|
|
3
|
+
* Check if cursor-agent CLI is installed
|
|
4
|
+
*/
|
|
5
|
+
import { exec } from 'child_process';
|
|
6
|
+
import { promisify } from 'util';
|
|
7
|
+
import { existsSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { homedir } from 'os';
|
|
10
|
+
|
|
11
|
+
const execAsync = promisify(exec);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Find the cursor-agent binary path
|
|
15
|
+
* Checks PATH first, then common installation locations
|
|
16
|
+
*/
|
|
17
|
+
export async function findCursorAgentPath() {
|
|
18
|
+
// First try via PATH (standard check)
|
|
19
|
+
try {
|
|
20
|
+
await execAsync('cursor-agent --version');
|
|
21
|
+
return 'cursor-agent'; // Found in PATH
|
|
22
|
+
} catch (_error) {
|
|
23
|
+
// PATH check failed, try common installation locations
|
|
24
|
+
const commonPaths = [
|
|
25
|
+
join(homedir(), '.local', 'bin', 'cursor-agent'),
|
|
26
|
+
join(homedir(), '.cursor', 'bin', 'cursor-agent'),
|
|
27
|
+
join(homedir(), '.cursor-agent', 'bin', 'cursor-agent')
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
// Check if binary exists in common locations
|
|
31
|
+
for (const binaryPath of commonPaths) {
|
|
32
|
+
if (existsSync(binaryPath)) {
|
|
33
|
+
// Try running it directly
|
|
34
|
+
try {
|
|
35
|
+
await execAsync(`"${binaryPath}" --version`);
|
|
36
|
+
return binaryPath; // Found in common location
|
|
37
|
+
} catch (_e) {
|
|
38
|
+
// Binary exists but might not be executable, continue checking
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return null; // Not found
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function checkCursorAgentInstalled() {
|
|
48
|
+
const path = await findCursorAgentPath();
|
|
49
|
+
return path !== null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getCursorAgentInstallInstructions() {
|
|
53
|
+
return `
|
|
54
|
+
❌ cursor-agent CLI not found!
|
|
55
|
+
|
|
56
|
+
To use the Cursor agent, install it from:
|
|
57
|
+
📦 https://github.com/getcursor/cursor-agent
|
|
58
|
+
|
|
59
|
+
Installation:
|
|
60
|
+
curl https://cursor.com/install -fsS | bash
|
|
61
|
+
|
|
62
|
+
After installation:
|
|
63
|
+
1. Add ~/.local/bin to your PATH:
|
|
64
|
+
For zsh: echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc && source ~/.zshrc
|
|
65
|
+
For bash: echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc && source ~/.bashrc
|
|
66
|
+
|
|
67
|
+
2. Or restart your terminal/shell
|
|
68
|
+
|
|
69
|
+
Then retry your command.
|
|
70
|
+
`;
|
|
71
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zibby Logger - Structured logging with levels
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* import { logger } from './utils/logger.js';
|
|
6
|
+
* logger.debug('Starting process...');
|
|
7
|
+
* logger.info('Test completed');
|
|
8
|
+
* logger.warn('API rate limit approaching');
|
|
9
|
+
* logger.error('Failed to connect', { error });
|
|
10
|
+
*
|
|
11
|
+
* Configuration:
|
|
12
|
+
* LOG_LEVEL=debug - Show debug, info, warn, error
|
|
13
|
+
* LOG_LEVEL=info - Show info, warn, error
|
|
14
|
+
* LOG_LEVEL=warn - Show warn, error only (default)
|
|
15
|
+
* LOG_LEVEL=error - Show error only
|
|
16
|
+
* LOG_LEVEL=silent - No logs
|
|
17
|
+
*
|
|
18
|
+
* CLI Flags:
|
|
19
|
+
* --verbose - Sets LOG_LEVEL=info (show progress)
|
|
20
|
+
* --debug - Sets LOG_LEVEL=debug (show everything)
|
|
21
|
+
*
|
|
22
|
+
* Best Practice:
|
|
23
|
+
* - Use console.log() for user-facing messages (always shown)
|
|
24
|
+
* - Use logger.debug() for diagnostics (--debug only)
|
|
25
|
+
* - Use logger.info() for progress/status (--verbose)
|
|
26
|
+
* - Use logger.warn() for warnings (always shown)
|
|
27
|
+
* - Use logger.error() for errors (always shown)
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import chalk from 'chalk';
|
|
31
|
+
|
|
32
|
+
const LOG_LEVELS = {
|
|
33
|
+
debug: 0,
|
|
34
|
+
info: 1,
|
|
35
|
+
warn: 2,
|
|
36
|
+
error: 3,
|
|
37
|
+
silent: 4
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
class Logger {
|
|
41
|
+
constructor() {
|
|
42
|
+
this._level = this._getLogLevel();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
_getLogLevel() {
|
|
46
|
+
if (process.env.ZIBBY_DEBUG === 'true') {
|
|
47
|
+
return LOG_LEVELS.debug;
|
|
48
|
+
}
|
|
49
|
+
if (process.env.ZIBBY_VERBOSE === 'true') {
|
|
50
|
+
return LOG_LEVELS.info;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const envLevel = process.env.LOG_LEVEL?.toLowerCase();
|
|
54
|
+
if (envLevel && envLevel in LOG_LEVELS) {
|
|
55
|
+
return LOG_LEVELS[envLevel];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return LOG_LEVELS.info;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
_shouldLog(level) {
|
|
62
|
+
return LOG_LEVELS[level] >= this._level;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
_formatMessage(level, message, meta = {}) {
|
|
66
|
+
const _timestamp = new Date().toISOString();
|
|
67
|
+
const prefix = this._getPrefix(level);
|
|
68
|
+
|
|
69
|
+
let output = `${prefix} ${message}`;
|
|
70
|
+
|
|
71
|
+
// Add metadata if present
|
|
72
|
+
if (Object.keys(meta).length > 0) {
|
|
73
|
+
output += chalk.dim(` ${JSON.stringify(meta)}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return output;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
_getPrefix(level) {
|
|
80
|
+
const prefixes = {
|
|
81
|
+
debug: chalk.gray('[DEBUG]'),
|
|
82
|
+
info: chalk.cyan('[INFO]'),
|
|
83
|
+
warn: chalk.yellow('[WARN]'),
|
|
84
|
+
error: chalk.red('❌ [ERROR]')
|
|
85
|
+
};
|
|
86
|
+
return prefixes[level] || '';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Debug - verbose diagnostic information
|
|
91
|
+
* Only shown with --debug or LOG_LEVEL=debug
|
|
92
|
+
*/
|
|
93
|
+
debug(message, meta) {
|
|
94
|
+
if (!this._shouldLog('debug')) return;
|
|
95
|
+
console.log(this._formatMessage('debug', message, meta));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Info - general informational messages
|
|
100
|
+
* Shown with --verbose, --debug, or LOG_LEVEL=info
|
|
101
|
+
*/
|
|
102
|
+
info(message, meta) {
|
|
103
|
+
if (!this._shouldLog('info')) return;
|
|
104
|
+
console.log(this._formatMessage('info', message, meta));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Warn - warning messages (potential issues)
|
|
109
|
+
* Always shown unless LOG_LEVEL=error or silent
|
|
110
|
+
*/
|
|
111
|
+
warn(message, meta) {
|
|
112
|
+
if (!this._shouldLog('warn')) return;
|
|
113
|
+
console.warn(this._formatMessage('warn', message, meta));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Error - error messages (failures)
|
|
118
|
+
* Always shown unless LOG_LEVEL=silent
|
|
119
|
+
*/
|
|
120
|
+
error(message, meta) {
|
|
121
|
+
if (!this._shouldLog('error')) return;
|
|
122
|
+
console.error(this._formatMessage('error', message, meta));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Set log level programmatically
|
|
127
|
+
*/
|
|
128
|
+
setLevel(level) {
|
|
129
|
+
if (level in LOG_LEVELS) {
|
|
130
|
+
this._level = LOG_LEVELS[level];
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get current log level name
|
|
136
|
+
*/
|
|
137
|
+
getLevel() {
|
|
138
|
+
return Object.keys(LOG_LEVELS).find(key => LOG_LEVELS[key] === this._level);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export const logger = new Logger();
|
|
143
|
+
|
|
144
|
+
export { Logger, LOG_LEVELS };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import { getSkill } from '../framework/skill-registry.js';
|
|
5
|
+
|
|
6
|
+
const MCP_CONFIG_PATH = join(homedir(), '.cursor', 'mcp.json');
|
|
7
|
+
|
|
8
|
+
export function collectRequiredTools(nodeConfigs) {
|
|
9
|
+
const toolSet = new Set();
|
|
10
|
+
if (!nodeConfigs || typeof nodeConfigs !== 'object') return toolSet;
|
|
11
|
+
|
|
12
|
+
for (const nodeId of Object.keys(nodeConfigs)) {
|
|
13
|
+
const config = nodeConfigs[nodeId];
|
|
14
|
+
if (Array.isArray(config.tools)) {
|
|
15
|
+
for (const tool of config.tools) {
|
|
16
|
+
toolSet.add(tool);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return toolSet;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function writeMcpConfig(nodeConfigs) {
|
|
24
|
+
const requiredTools = collectRequiredTools(nodeConfigs);
|
|
25
|
+
if (requiredTools.size === 0) {
|
|
26
|
+
console.log('[MCP Config] No tools requested — skipping MCP config write');
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let mcpConfig = { mcpServers: {} };
|
|
31
|
+
try {
|
|
32
|
+
if (existsSync(MCP_CONFIG_PATH)) {
|
|
33
|
+
mcpConfig = JSON.parse(readFileSync(MCP_CONFIG_PATH, 'utf-8'));
|
|
34
|
+
if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
|
|
35
|
+
}
|
|
36
|
+
} catch (err) {
|
|
37
|
+
console.warn(`[MCP Config] Could not read existing config, starting fresh: ${err.message}`);
|
|
38
|
+
mcpConfig = { mcpServers: {} };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let addedCount = 0;
|
|
42
|
+
for (const toolId of requiredTools) {
|
|
43
|
+
const skill = getSkill(toolId);
|
|
44
|
+
if (!skill) {
|
|
45
|
+
console.log(`[MCP Config] Unknown skill "${toolId}" — skipping`);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (mcpConfig.mcpServers[skill.serverName]) {
|
|
50
|
+
console.log(`[MCP Config] Server "${skill.serverName}" already configured — skipping`);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const envVars = {};
|
|
55
|
+
let missingEnv = false;
|
|
56
|
+
for (const key of (skill.envKeys || [])) {
|
|
57
|
+
const value = process.env[key];
|
|
58
|
+
if (!value) {
|
|
59
|
+
console.warn(`[MCP Config] Missing env var ${key} for skill "${toolId}" — skipping server`);
|
|
60
|
+
missingEnv = true;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
envVars[key] = value;
|
|
64
|
+
}
|
|
65
|
+
if (missingEnv) continue;
|
|
66
|
+
|
|
67
|
+
const resolvedArgs = (skill.args || []).map(a => a);
|
|
68
|
+
if (skill.command === 'node' && resolvedArgs.length > 0 && !existsSync(resolvedArgs[0])) {
|
|
69
|
+
console.warn(`[MCP Config] Binary not found at ${resolvedArgs[0]} for "${toolId}" — skipping server`);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
mcpConfig.mcpServers[skill.serverName] = {
|
|
74
|
+
command: skill.command,
|
|
75
|
+
args: resolvedArgs,
|
|
76
|
+
env: envVars,
|
|
77
|
+
description: skill.description,
|
|
78
|
+
};
|
|
79
|
+
addedCount++;
|
|
80
|
+
console.log(`[MCP Config] Added "${skill.serverName}" server`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (addedCount === 0) {
|
|
84
|
+
console.log('[MCP Config] No new MCP servers to add');
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const configDir = join(homedir(), '.cursor');
|
|
89
|
+
if (!existsSync(configDir)) {
|
|
90
|
+
mkdirSync(configDir, { recursive: true });
|
|
91
|
+
}
|
|
92
|
+
writeFileSync(MCP_CONFIG_PATH, JSON.stringify(mcpConfig, null, 2), 'utf-8');
|
|
93
|
+
console.log(`[MCP Config] Wrote ${MCP_CONFIG_PATH} with ${Object.keys(mcpConfig.mcpServers).length} server(s)`);
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function getToolDescriptions(toolIds) {
|
|
98
|
+
if (!Array.isArray(toolIds) || toolIds.length === 0) return '';
|
|
99
|
+
|
|
100
|
+
const sections = [];
|
|
101
|
+
for (const toolId of toolIds) {
|
|
102
|
+
const skill = getSkill(toolId);
|
|
103
|
+
if (!skill) continue;
|
|
104
|
+
|
|
105
|
+
const toolList = (skill.tools || [])
|
|
106
|
+
.map(t => `- ${t.name}: ${t.description}`)
|
|
107
|
+
.join('\n');
|
|
108
|
+
|
|
109
|
+
sections.push(`### ${skill.description}\n${toolList}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (sections.length === 0) return '';
|
|
113
|
+
|
|
114
|
+
return `\n\nAVAILABLE MCP TOOLS:\nYou have access to the following MCP tools. Use them when your task requires interacting with these services.\n\n${sections.join('\n\n')}`;
|
|
115
|
+
}
|