@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
@@ -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
+ }