didev 1.0.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 (112) hide show
  1. package/README.md +378 -0
  2. package/dist/agents/analyst.d.ts +21 -0
  3. package/dist/agents/analyst.d.ts.map +1 -0
  4. package/dist/agents/analyst.js +69 -0
  5. package/dist/agents/analyst.js.map +1 -0
  6. package/dist/agents/architect.d.ts +21 -0
  7. package/dist/agents/architect.d.ts.map +1 -0
  8. package/dist/agents/architect.js +85 -0
  9. package/dist/agents/architect.js.map +1 -0
  10. package/dist/agents/base-agent.d.ts +56 -0
  11. package/dist/agents/base-agent.d.ts.map +1 -0
  12. package/dist/agents/base-agent.js +263 -0
  13. package/dist/agents/base-agent.js.map +1 -0
  14. package/dist/agents/developer.d.ts +21 -0
  15. package/dist/agents/developer.d.ts.map +1 -0
  16. package/dist/agents/developer.js +87 -0
  17. package/dist/agents/developer.js.map +1 -0
  18. package/dist/agents/orchestrator.d.ts +23 -0
  19. package/dist/agents/orchestrator.d.ts.map +1 -0
  20. package/dist/agents/orchestrator.js +287 -0
  21. package/dist/agents/orchestrator.js.map +1 -0
  22. package/dist/agents/reviewer.d.ts +15 -0
  23. package/dist/agents/reviewer.d.ts.map +1 -0
  24. package/dist/agents/reviewer.js +65 -0
  25. package/dist/agents/reviewer.js.map +1 -0
  26. package/dist/agents/tester.d.ts +15 -0
  27. package/dist/agents/tester.d.ts.map +1 -0
  28. package/dist/agents/tester.js +64 -0
  29. package/dist/agents/tester.js.map +1 -0
  30. package/dist/bmad/method.d.ts +6 -0
  31. package/dist/bmad/method.d.ts.map +1 -0
  32. package/dist/bmad/method.js +221 -0
  33. package/dist/bmad/method.js.map +1 -0
  34. package/dist/cli/commands/agent.d.ts +10 -0
  35. package/dist/cli/commands/agent.d.ts.map +1 -0
  36. package/dist/cli/commands/agent.js +28 -0
  37. package/dist/cli/commands/agent.js.map +1 -0
  38. package/dist/cli/commands/chat.d.ts +6 -0
  39. package/dist/cli/commands/chat.d.ts.map +1 -0
  40. package/dist/cli/commands/chat.js +556 -0
  41. package/dist/cli/commands/chat.js.map +1 -0
  42. package/dist/cli/commands/config.d.ts +3 -0
  43. package/dist/cli/commands/config.d.ts.map +1 -0
  44. package/dist/cli/commands/config.js +65 -0
  45. package/dist/cli/commands/config.js.map +1 -0
  46. package/dist/cli/commands/init.d.ts +8 -0
  47. package/dist/cli/commands/init.d.ts.map +1 -0
  48. package/dist/cli/commands/init.js +204 -0
  49. package/dist/cli/commands/init.js.map +1 -0
  50. package/dist/cli/commands/mcp.d.ts +5 -0
  51. package/dist/cli/commands/mcp.d.ts.map +1 -0
  52. package/dist/cli/commands/mcp.js +836 -0
  53. package/dist/cli/commands/mcp.js.map +1 -0
  54. package/dist/cli/commands/refactor.d.ts +8 -0
  55. package/dist/cli/commands/refactor.d.ts.map +1 -0
  56. package/dist/cli/commands/refactor.js +161 -0
  57. package/dist/cli/commands/refactor.js.map +1 -0
  58. package/dist/cli/commands/review.d.ts +9 -0
  59. package/dist/cli/commands/review.d.ts.map +1 -0
  60. package/dist/cli/commands/review.js +138 -0
  61. package/dist/cli/commands/review.js.map +1 -0
  62. package/dist/core/api.d.ts +73 -0
  63. package/dist/core/api.d.ts.map +1 -0
  64. package/dist/core/api.js +206 -0
  65. package/dist/core/api.js.map +1 -0
  66. package/dist/core/config.d.ts +42 -0
  67. package/dist/core/config.d.ts.map +1 -0
  68. package/dist/core/config.js +180 -0
  69. package/dist/core/config.js.map +1 -0
  70. package/dist/core/context.d.ts +33 -0
  71. package/dist/core/context.d.ts.map +1 -0
  72. package/dist/core/context.js +235 -0
  73. package/dist/core/context.js.map +1 -0
  74. package/dist/core/file-manager.d.ts +20 -0
  75. package/dist/core/file-manager.d.ts.map +1 -0
  76. package/dist/core/file-manager.js +133 -0
  77. package/dist/core/file-manager.js.map +1 -0
  78. package/dist/core/mcp.d.ts +31 -0
  79. package/dist/core/mcp.d.ts.map +1 -0
  80. package/dist/core/mcp.js +112 -0
  81. package/dist/core/mcp.js.map +1 -0
  82. package/dist/core/session.d.ts +16 -0
  83. package/dist/core/session.d.ts.map +1 -0
  84. package/dist/core/session.js +60 -0
  85. package/dist/core/session.js.map +1 -0
  86. package/dist/index.d.ts +3 -0
  87. package/dist/index.d.ts.map +1 -0
  88. package/dist/index.js +237 -0
  89. package/dist/index.js.map +1 -0
  90. package/dist/utils/banner.d.ts +2 -0
  91. package/dist/utils/banner.d.ts.map +1 -0
  92. package/dist/utils/banner.js +50 -0
  93. package/dist/utils/banner.js.map +1 -0
  94. package/dist/utils/git.d.ts +9 -0
  95. package/dist/utils/git.d.ts.map +1 -0
  96. package/dist/utils/git.js +49 -0
  97. package/dist/utils/git.js.map +1 -0
  98. package/dist/utils/logger.d.ts +42 -0
  99. package/dist/utils/logger.d.ts.map +1 -0
  100. package/dist/utils/logger.js +98 -0
  101. package/dist/utils/logger.js.map +1 -0
  102. package/dist/utils/resilience.d.ts +17 -0
  103. package/dist/utils/resilience.d.ts.map +1 -0
  104. package/dist/utils/resilience.js +41 -0
  105. package/dist/utils/resilience.js.map +1 -0
  106. package/dist/utils/token-counter.d.ts +7 -0
  107. package/dist/utils/token-counter.d.ts.map +1 -0
  108. package/dist/utils/token-counter.js +20 -0
  109. package/dist/utils/token-counter.js.map +1 -0
  110. package/package.json +62 -0
  111. package/scripts/postinstall.mjs +54 -0
  112. package/scripts/setup-path.sh +42 -0
@@ -0,0 +1,836 @@
1
+ import chalk from 'chalk';
2
+ import inquirer from 'inquirer';
3
+ import boxen from 'boxen';
4
+ import yaml from 'js-yaml';
5
+ import { execFile } from 'child_process';
6
+ import { promisify } from 'util';
7
+ import { mkdir, writeFile, access, readFile } from 'fs/promises';
8
+ import { join } from 'path';
9
+ import { homedir } from 'os';
10
+ import { loadConfig } from '../../core/config.js';
11
+ import { McpManager } from '../../core/mcp.js';
12
+ import { logger } from '../../utils/logger.js';
13
+ import { getApiKey } from '../../core/config.js';
14
+ import { initClient } from '../../core/api.js';
15
+ const execFileAsync = promisify(execFile);
16
+ function parseSourceUrl(input) {
17
+ const trimmed = input.trim();
18
+ // npm: explicit prefix or plain package name without slashes/dots
19
+ if (trimmed.startsWith('npm:')) {
20
+ return { type: 'npm', raw: trimmed.replace(/^npm:/, '') };
21
+ }
22
+ // GitHub
23
+ const gh = trimmed.match(/github\.com\/([^/]+)\/([^/?#]+)(?:\/tree\/([^/]+)(?:\/(.+?))?)?[/?#]?$/i);
24
+ if (gh) {
25
+ return {
26
+ type: 'github',
27
+ owner: gh[1],
28
+ repo: gh[2].replace(/\.git$/, ''),
29
+ branch: gh[3] ?? 'main',
30
+ subpath: gh[4]?.replace(/\/$/, '') ?? '',
31
+ raw: trimmed,
32
+ };
33
+ }
34
+ // GitLab
35
+ const gl = trimmed.match(/gitlab\.com\/([^/]+)\/([^/?#]+)(?:\/-\/tree\/([^/]+)(?:\/(.+?))?)?[/?#]?$/i);
36
+ if (gl) {
37
+ return {
38
+ type: 'gitlab',
39
+ owner: gl[1],
40
+ repo: gl[2].replace(/\.git$/, ''),
41
+ branch: gl[3] ?? 'main',
42
+ subpath: gl[4]?.replace(/\/$/, '') ?? '',
43
+ raw: trimmed,
44
+ };
45
+ }
46
+ // Bitbucket
47
+ const bb = trimmed.match(/bitbucket\.org\/([^/]+)\/([^/?#]+)(?:\/src\/([^/]+)(?:\/(.+?))?)?[/?#]?$/i);
48
+ if (bb) {
49
+ return {
50
+ type: 'bitbucket',
51
+ owner: bb[1],
52
+ repo: bb[2].replace(/\.git$/, ''),
53
+ branch: bb[3] ?? 'main',
54
+ subpath: bb[4]?.replace(/\/$/, '') ?? '',
55
+ raw: trimmed,
56
+ };
57
+ }
58
+ if (trimmed.startsWith('./') || trimmed.startsWith('/')) {
59
+ return { type: 'local', raw: trimmed };
60
+ }
61
+ // Fallback: treat as npm package name
62
+ return { type: 'npm', raw: trimmed };
63
+ }
64
+ // ─── File / HTTP helpers ───────────────────────────────────────────────────
65
+ async function fileExists(p) {
66
+ try {
67
+ await access(p);
68
+ return true;
69
+ }
70
+ catch {
71
+ return false;
72
+ }
73
+ }
74
+ async function fetchText(url) {
75
+ try {
76
+ const res = await fetch(url);
77
+ if (!res.ok)
78
+ return null;
79
+ return res.text();
80
+ }
81
+ catch {
82
+ return null;
83
+ }
84
+ }
85
+ async function fetchJson(url) {
86
+ const text = await fetchText(url);
87
+ if (!text)
88
+ return null;
89
+ try {
90
+ return JSON.parse(text);
91
+ }
92
+ catch {
93
+ return null;
94
+ }
95
+ }
96
+ // ─── Env var detection ─────────────────────────────────────────────────────
97
+ const SENSITIVE_PATTERNS = /key|token|secret|password|pass|credential|auth|api_key/i;
98
+ const COMMON_NOISE = new Set(['PATH', 'HOME', 'USER', 'SHELL', 'NODE', 'NPM', 'TRUE', 'FALSE', 'NULL', 'NODE_ENV']);
99
+ function extractEnvVars(readme, packageJson) {
100
+ const vars = new Map();
101
+ const add = (name, desc = '', example) => {
102
+ if (COMMON_NOISE.has(name) || name.length < 3)
103
+ return;
104
+ if (!vars.has(name)) {
105
+ vars.set(name, {
106
+ name,
107
+ description: desc,
108
+ required: true,
109
+ example,
110
+ sensitive: SENSITIVE_PATTERNS.test(name),
111
+ });
112
+ }
113
+ else if (desc && !vars.get(name).description) {
114
+ vars.get(name).description = desc;
115
+ }
116
+ };
117
+ // Code blocks: KEY=value
118
+ const codeBlocks = readme.matchAll(/```[^\n]*\n([\s\S]*?)```/g);
119
+ for (const [, block] of codeBlocks) {
120
+ for (const [, name, example] of block.matchAll(/^([A-Z][A-Z0-9_]{2,})\s*=\s*(.+)$/gm)) {
121
+ add(name, '', example.trim().replace(/["']/g, ''));
122
+ }
123
+ for (const [, name, example] of block.matchAll(/"([A-Z][A-Z0-9_]{2,})"\s*:\s*"([^"]+)"/g)) {
124
+ add(name, '', example);
125
+ }
126
+ }
127
+ // process.env references
128
+ for (const [, name] of readme.matchAll(/process\.env\.([A-Z][A-Z0-9_]{2,})/g)) {
129
+ add(name);
130
+ }
131
+ // ${VAR} patterns
132
+ for (const [, name] of readme.matchAll(/\$\{?([A-Z][A-Z0-9_]{2,})\}?/g)) {
133
+ add(name);
134
+ }
135
+ // Markdown list: "- VAR_NAME: description"
136
+ for (const [, name, desc] of readme.matchAll(/[-*]\s+`?([A-Z][A-Z0-9_]{2,})`?\s*[:\-–]\s*([^\n]+)/g)) {
137
+ add(name, desc.trim());
138
+ }
139
+ return [...vars.values()];
140
+ }
141
+ // ─── AI enrichment (optional, only if API key available) ──────────────────
142
+ async function enrichWithAI(readme, rawVars, apiKey, model) {
143
+ if (!apiKey || rawVars.length === 0)
144
+ return rawVars;
145
+ try {
146
+ const client = initClient({ apiKey, model });
147
+ const varNames = rawVars.map(v => v.name).join(', ');
148
+ const res = await client.chat([
149
+ {
150
+ role: 'system',
151
+ content: 'You are analyzing MCP server documentation to extract setup requirements. Be concise.',
152
+ },
153
+ {
154
+ role: 'user',
155
+ content: `README excerpt:\n\`\`\`\n${readme.slice(0, 3000)}\n\`\`\`\n\nDetected env vars: ${varNames}\n\nFor each env var provide a one-line description and whether it is required or optional.\nRespond ONLY as JSON array: [{"name":"VAR","description":"...","required":true}]`,
156
+ },
157
+ ], model, { temperature: 0 });
158
+ const match = res.content.match(/\[[\s\S]+\]/);
159
+ if (!match)
160
+ return rawVars;
161
+ const enriched = JSON.parse(match[0]);
162
+ return rawVars.map(v => {
163
+ const found = enriched.find(e => e.name === v.name);
164
+ return found ? { ...v, description: found.description, required: found.required } : v;
165
+ });
166
+ }
167
+ catch {
168
+ return rawVars;
169
+ }
170
+ }
171
+ // ─── Git clone + local analysis ────────────────────────────────────────────
172
+ function buildCloneUrl(parsed, creds) {
173
+ if (!creds) {
174
+ switch (parsed.type) {
175
+ case 'github': return `https://github.com/${parsed.owner}/${parsed.repo}.git`;
176
+ case 'gitlab': return `https://gitlab.com/${parsed.owner}/${parsed.repo}.git`;
177
+ case 'bitbucket': return `https://bitbucket.org/${parsed.owner}/${parsed.repo}.git`;
178
+ default: throw new Error(`Cannot clone from type: ${parsed.type}`);
179
+ }
180
+ }
181
+ // Authenticated URLs
182
+ switch (parsed.type) {
183
+ case 'github':
184
+ return `https://${creds.token}@github.com/${parsed.owner}/${parsed.repo}.git`;
185
+ case 'gitlab':
186
+ return `https://oauth2:${creds.token}@gitlab.com/${parsed.owner}/${parsed.repo}.git`;
187
+ case 'bitbucket':
188
+ return `https://${creds.username}:${creds.password}@bitbucket.org/${parsed.owner}/${parsed.repo}.git`;
189
+ default:
190
+ throw new Error(`Cannot clone from type: ${parsed.type}`);
191
+ }
192
+ }
193
+ const AUTH_ERROR_RE = /authentication failed|could not read username|repository not found|403|401|access denied|permission denied/i;
194
+ async function promptGitCredentials(parsed) {
195
+ logger.newline();
196
+ logger.warn(`Access denied — "${parsed.owner}/${parsed.repo}" appears to be a private repository.`);
197
+ logger.newline();
198
+ if (parsed.type === 'bitbucket') {
199
+ const { username, password } = await inquirer.prompt([
200
+ { type: 'input', name: 'username', message: 'Bitbucket username:', validate: (v) => !!v.trim() || 'Required' },
201
+ { type: 'password', name: 'password', message: 'Bitbucket App Password:', validate: (v) => !!v.trim() || 'Required' },
202
+ ]);
203
+ return { username, password };
204
+ }
205
+ const providerName = parsed.type === 'gitlab' ? 'GitLab' : 'GitHub';
206
+ const tokenHint = parsed.type === 'gitlab'
207
+ ? '(Settings → Access Tokens → read_repository)'
208
+ : '(github.com/settings/tokens — repo scope)';
209
+ const { token } = await inquirer.prompt([{
210
+ type: 'password',
211
+ name: 'token',
212
+ message: `${providerName} Personal Access Token ${chalk.gray(tokenHint)}:`,
213
+ validate: (v) => !!v.trim() || 'Required',
214
+ }]);
215
+ return { token };
216
+ }
217
+ async function cloneWithAuthRetry(parsed, targetDir) {
218
+ const cloneUrl = buildCloneUrl(parsed);
219
+ const shellOpts = { shell: true };
220
+ try {
221
+ await execFileAsync('git', ['clone', '--depth', '1', '--quiet', cloneUrl, targetDir], shellOpts);
222
+ return {};
223
+ }
224
+ catch (e) {
225
+ const err = e;
226
+ const msg = err.message + (err.stderr ?? '');
227
+ if (!AUTH_ERROR_RE.test(msg))
228
+ throw e;
229
+ // Prompt for credentials and retry
230
+ const creds = await promptGitCredentials(parsed);
231
+ const authUrl = buildCloneUrl(parsed, creds);
232
+ const retrySpinner = logger.spinner('Retrying with credentials...').start();
233
+ try {
234
+ await execFileAsync('git', ['clone', '--depth', '1', '--quiet', authUrl, targetDir], shellOpts);
235
+ retrySpinner.succeed('Authenticated successfully');
236
+ return { creds };
237
+ }
238
+ catch (e2) {
239
+ retrySpinner.fail(`Authentication failed: ${e2.message}`);
240
+ throw e2;
241
+ }
242
+ }
243
+ }
244
+ async function cloneAndAnalyze(parsed, apiKey, model) {
245
+ const mcpDir = join(homedir(), '.didev', 'mcp');
246
+ const repoName = parsed.repo;
247
+ const targetDir = join(mcpDir, repoName);
248
+ await mkdir(mcpDir, { recursive: true });
249
+ // Clone or pull
250
+ const cloneSpinner = logger.spinner('Cloning repository...').start();
251
+ let savedCreds;
252
+ try {
253
+ if (await fileExists(join(targetDir, '.git'))) {
254
+ await execFileAsync('git', ['-C', targetDir, 'pull', '--ff-only', '--quiet'], { shell: true });
255
+ cloneSpinner.succeed(`Repository updated: ${chalk.dim(targetDir)}`);
256
+ }
257
+ else {
258
+ cloneSpinner.stop();
259
+ const result = await cloneWithAuthRetry(parsed, targetDir);
260
+ savedCreds = result.creds;
261
+ logger.success(`Cloned to ${chalk.dim(targetDir)}`);
262
+ // Save git credentials to global ~/.didev/.env so future pulls work
263
+ if (savedCreds) {
264
+ const globalEnvPath = join(homedir(), '.didev', '.env');
265
+ await mkdir(join(homedir(), '.didev'), { recursive: true });
266
+ let existing = '';
267
+ try {
268
+ existing = await readFile(globalEnvPath, 'utf-8');
269
+ }
270
+ catch { }
271
+ const lines = existing.split('\n').filter(Boolean);
272
+ const varName = `DIDEV_GIT_${parsed.type.toUpperCase()}_TOKEN`;
273
+ const value = savedCreds.token ?? (savedCreds.username ? `${savedCreds.username}:${savedCreds.password}` : '');
274
+ if (value) {
275
+ const idx = lines.findIndex(l => l.startsWith(`${varName}=`));
276
+ if (idx >= 0)
277
+ lines[idx] = `${varName}=${value}`;
278
+ else
279
+ lines.push(`${varName}=${value}`);
280
+ await writeFile(globalEnvPath, lines.join('\n') + '\n', 'utf-8');
281
+ logger.dim(`Git credentials saved to ${globalEnvPath}`);
282
+ }
283
+ }
284
+ }
285
+ }
286
+ catch (e) {
287
+ cloneSpinner.fail(`Clone failed: ${e.message}`);
288
+ throw e;
289
+ }
290
+ // Detect language
291
+ let language = 'unknown';
292
+ let pkg = null;
293
+ try {
294
+ pkg = JSON.parse(await readFile(join(targetDir, 'package.json'), 'utf-8'));
295
+ language = 'node';
296
+ }
297
+ catch { }
298
+ if (language === 'unknown') {
299
+ for (const f of ['requirements.txt', 'pyproject.toml', 'setup.py']) {
300
+ if (await fileExists(join(targetDir, f))) {
301
+ language = 'python';
302
+ break;
303
+ }
304
+ }
305
+ }
306
+ // Install & build (shell:true so npm.cmd / pip are found on Windows)
307
+ const shellOpts = { cwd: targetDir, shell: true };
308
+ if (language === 'node') {
309
+ const installSpinner = logger.spinner('Installing dependencies...').start();
310
+ try {
311
+ await execFileAsync('npm', ['install', '--prefer-offline', '--silent'], shellOpts);
312
+ installSpinner.succeed('Dependencies installed');
313
+ }
314
+ catch (e) {
315
+ installSpinner.warn(`npm install: ${e.message}`);
316
+ }
317
+ const scripts = pkg?.scripts;
318
+ if (scripts?.build) {
319
+ const buildSpinner = logger.spinner('Building...').start();
320
+ try {
321
+ await execFileAsync('npm', ['run', 'build'], shellOpts);
322
+ buildSpinner.succeed('Build complete');
323
+ }
324
+ catch (e) {
325
+ buildSpinner.warn(`Build: ${e.message}`);
326
+ }
327
+ }
328
+ }
329
+ else if (language === 'python') {
330
+ if (await fileExists(join(targetDir, 'requirements.txt'))) {
331
+ const installSpinner = logger.spinner('Installing Python dependencies...').start();
332
+ try {
333
+ await execFileAsync('pip', ['install', '-r', 'requirements.txt', '-q'], shellOpts);
334
+ installSpinner.succeed('Python dependencies installed');
335
+ }
336
+ catch (e) {
337
+ installSpinner.warn(`pip install: ${e.message}`);
338
+ }
339
+ }
340
+ }
341
+ // Find entry point
342
+ let entryPoint = '';
343
+ let command = 'node';
344
+ const cmdArgs = [];
345
+ if (language === 'node' && pkg) {
346
+ const bin = pkg['bin'];
347
+ if (bin && typeof bin === 'object' && bin !== null) {
348
+ const first = Object.values(bin)[0];
349
+ if (first)
350
+ entryPoint = join(targetDir, first);
351
+ }
352
+ else if (typeof bin === 'string') {
353
+ entryPoint = join(targetDir, bin);
354
+ }
355
+ else if (typeof pkg['main'] === 'string') {
356
+ entryPoint = join(targetDir, pkg['main']);
357
+ }
358
+ if (!entryPoint) {
359
+ for (const candidate of ['dist/index.js', 'build/index.js', 'index.js', 'src/index.js', 'dist/main.js', 'build/main.js']) {
360
+ if (await fileExists(join(targetDir, candidate))) {
361
+ entryPoint = join(targetDir, candidate);
362
+ break;
363
+ }
364
+ }
365
+ }
366
+ command = 'node';
367
+ if (entryPoint)
368
+ cmdArgs.push(entryPoint);
369
+ }
370
+ else if (language === 'python') {
371
+ for (const candidate of ['main.py', 'server.py', 'app.py', 'src/main.py', '__main__.py']) {
372
+ if (await fileExists(join(targetDir, candidate))) {
373
+ entryPoint = join(targetDir, candidate);
374
+ break;
375
+ }
376
+ }
377
+ command = 'python';
378
+ if (entryPoint)
379
+ cmdArgs.push(entryPoint);
380
+ }
381
+ // Read README for env var detection
382
+ let readme = '';
383
+ try {
384
+ readme = await readFile(join(targetDir, 'README.md'), 'utf-8');
385
+ }
386
+ catch { }
387
+ let envVars = extractEnvVars(readme, pkg ?? {});
388
+ if (envVars.length > 0 && apiKey) {
389
+ const enrichSpinner = logger.spinner('Analyzing requirements with AI...').start();
390
+ envVars = await enrichWithAI(readme, envVars, apiKey, model);
391
+ enrichSpinner.stop();
392
+ }
393
+ const shortName = repoName.replace(/^mcp-server-/, '').replace(/^mcp-/, '');
394
+ return {
395
+ name: shortName,
396
+ description: typeof pkg?.description === 'string' ? pkg.description : `MCP server from ${parsed.owner}/${parsed.repo}`,
397
+ cloneDir: targetDir,
398
+ runCommand: { command, args: cmdArgs },
399
+ envVars,
400
+ requiresAuth: envVars.some(v => v.required),
401
+ readme: readme.slice(0, 1000),
402
+ installNote: !entryPoint
403
+ ? `⚠ Could not detect entry point. Check ${targetDir} and set command/args in .didev/config.yaml manually.`
404
+ : undefined,
405
+ };
406
+ }
407
+ // ─── npm package analysis ──────────────────────────────────────────────────
408
+ async function analyzeNpmPackage(pkgName, apiKey, model) {
409
+ const spinner = logger.spinner(`Fetching npm info for ${chalk.cyan(pkgName)}...`).start();
410
+ // npm registry URL — scoped packages need @scope%2Fpkg format
411
+ const registryUrl = pkgName.startsWith('@')
412
+ ? `https://registry.npmjs.org/${pkgName.replace('/', '%2F')}/latest`
413
+ : `https://registry.npmjs.org/${pkgName}/latest`;
414
+ const data = await fetchJson(registryUrl);
415
+ const readme = data?.readme ?? '';
416
+ const description = data?.description ?? pkgName;
417
+ if (data) {
418
+ spinner.succeed(`Found: ${chalk.gray(description)}`);
419
+ }
420
+ else {
421
+ spinner.warn(`Package "${pkgName}" not found on npm — will try npx anyway`);
422
+ }
423
+ let envVars = extractEnvVars(readme, {});
424
+ if (envVars.length > 0 && apiKey) {
425
+ const enrichSpinner = logger.spinner('Analyzing requirements with AI...').start();
426
+ envVars = await enrichWithAI(readme, envVars, apiKey, model);
427
+ enrichSpinner.stop();
428
+ }
429
+ const shortName = pkgName.replace(/^@[^/]+\/server-/, '').replace(/^mcp-server-/, '').replace(/^mcp-/, '');
430
+ return {
431
+ name: shortName,
432
+ description,
433
+ npmPackage: pkgName,
434
+ runCommand: { command: 'npx', args: ['-y', pkgName] },
435
+ envVars,
436
+ requiresAuth: envVars.some(v => v.required),
437
+ readme: readme.slice(0, 1000),
438
+ };
439
+ }
440
+ // ─── Main analysis router ──────────────────────────────────────────────────
441
+ async function analyzePackage(parsed, apiKey, model) {
442
+ if (parsed.type === 'npm') {
443
+ return analyzeNpmPackage(parsed.raw, apiKey, model);
444
+ }
445
+ if (parsed.type === 'local') {
446
+ // Treat local path as a pre-cloned repo — analyze in place
447
+ return analyzeLocalDir(parsed.raw, apiKey, model);
448
+ }
449
+ // GitHub / GitLab / Bitbucket → clone and analyze
450
+ return cloneAndAnalyze(parsed, apiKey, model);
451
+ }
452
+ async function analyzeLocalDir(dir, apiKey, model) {
453
+ let pkg = null;
454
+ try {
455
+ pkg = JSON.parse(await readFile(join(dir, 'package.json'), 'utf-8'));
456
+ }
457
+ catch { }
458
+ let readme = '';
459
+ try {
460
+ readme = await readFile(join(dir, 'README.md'), 'utf-8');
461
+ }
462
+ catch { }
463
+ let envVars = extractEnvVars(readme, pkg ?? {});
464
+ if (envVars.length > 0 && apiKey) {
465
+ envVars = await enrichWithAI(readme, envVars, apiKey, model);
466
+ }
467
+ let entryPoint = '';
468
+ if (pkg) {
469
+ const bin = pkg['bin'];
470
+ if (typeof bin === 'object' && bin !== null) {
471
+ const first = Object.values(bin)[0];
472
+ if (first)
473
+ entryPoint = join(dir, first);
474
+ }
475
+ else if (typeof bin === 'string') {
476
+ entryPoint = join(dir, bin);
477
+ }
478
+ else if (typeof pkg['main'] === 'string') {
479
+ entryPoint = join(dir, pkg['main']);
480
+ }
481
+ }
482
+ if (!entryPoint) {
483
+ for (const c of ['dist/index.js', 'index.js', 'src/index.js', 'main.py', 'server.py']) {
484
+ if (await fileExists(join(dir, c))) {
485
+ entryPoint = join(dir, c);
486
+ break;
487
+ }
488
+ }
489
+ }
490
+ const command = entryPoint.endsWith('.py') ? 'python' : 'node';
491
+ return {
492
+ name: dir.split('/').pop() ?? 'local-server',
493
+ description: typeof pkg?.description === 'string' ? pkg.description : `Local MCP server at ${dir}`,
494
+ cloneDir: dir,
495
+ runCommand: { command, args: entryPoint ? [entryPoint] : [] },
496
+ envVars,
497
+ requiresAuth: envVars.some(v => v.required),
498
+ readme: readme.slice(0, 1000),
499
+ };
500
+ }
501
+ // ─── Interactive env var collection ───────────────────────────────────────
502
+ async function promptForEnvVars(vars) {
503
+ if (vars.length === 0)
504
+ return {};
505
+ logger.newline();
506
+ logger.bold('Required configuration:');
507
+ logger.newline();
508
+ const collected = {};
509
+ for (const v of vars) {
510
+ const label = v.required ? chalk.red('*') : chalk.gray('?');
511
+ const desc = v.description ? chalk.gray(` — ${v.description}`) : '';
512
+ const example = v.example ? chalk.gray(` (e.g. ${v.example})`) : '';
513
+ const { value } = await inquirer.prompt([{
514
+ type: v.sensitive ? 'password' : 'input',
515
+ name: 'value',
516
+ message: `${label} ${chalk.bold(v.name)}${desc}${example}:`,
517
+ validate: (val) => {
518
+ if (v.required && !val.trim())
519
+ return `${v.name} is required`;
520
+ return true;
521
+ },
522
+ }]);
523
+ if (value.trim()) {
524
+ collected[v.name] = value.trim();
525
+ }
526
+ }
527
+ return collected;
528
+ }
529
+ // ─── Save helpers ──────────────────────────────────────────────────────────
530
+ async function appendServer(server) {
531
+ const config = await loadConfig();
532
+ const servers = config.mcp?.servers ?? [];
533
+ const idx = servers.findIndex(s => s.name === server.name);
534
+ if (idx >= 0)
535
+ servers[idx] = server;
536
+ else
537
+ servers.push(server);
538
+ config.mcp = { servers };
539
+ const configPath = join(process.cwd(), '.didev', 'config.yaml');
540
+ await mkdir(join(process.cwd(), '.didev'), { recursive: true });
541
+ const toSave = JSON.parse(JSON.stringify(config));
542
+ const api = toSave['api'];
543
+ if (api?.['apiKey'] && typeof api['apiKey'] === 'string' && !api['apiKey'].includes('${')) {
544
+ api['apiKey'] = '${DEEPSEEK_API_KEY}';
545
+ }
546
+ await writeFile(configPath, yaml.dump(toSave, { indent: 2 }), 'utf-8');
547
+ }
548
+ // ─── Public commands ───────────────────────────────────────────────────────
549
+ export async function runMcpAdd(sourceUrl) {
550
+ const config = await loadConfig();
551
+ const apiKey = await getApiKey(config);
552
+ const model = config.api.model;
553
+ logger.banner('didev mcp add', sourceUrl ? 'Installing from URL' : 'Add MCP Server');
554
+ let analysis;
555
+ if (sourceUrl) {
556
+ // ── URL / git mode ───────────────────────────────────────────────────────
557
+ let parsed;
558
+ try {
559
+ parsed = parseSourceUrl(sourceUrl);
560
+ }
561
+ catch (e) {
562
+ logger.error(e.message);
563
+ process.exit(1);
564
+ }
565
+ const typeLabel = parsed.type === 'npm'
566
+ ? 'npm package'
567
+ : `${parsed.type}${parsed.owner ? ` · ${parsed.owner}/${parsed.repo}` : ''}`;
568
+ logger.info(`Source: ${chalk.cyan(sourceUrl)}`);
569
+ logger.dim(`Type: ${typeLabel}`);
570
+ logger.newline();
571
+ analysis = await analyzePackage(parsed, apiKey, model);
572
+ }
573
+ else {
574
+ // ── Wizard mode ──────────────────────────────────────────────────────────
575
+ const { choice } = await inquirer.prompt([{
576
+ type: 'list',
577
+ name: 'choice',
578
+ message: 'How do you want to add a server?',
579
+ choices: [
580
+ { name: 'From GitHub / GitLab / Bitbucket URL (clones & builds)', value: 'url' },
581
+ { name: 'From npm package name (runs via npx)', value: 'npm' },
582
+ { name: 'Choose from popular servers', value: 'popular' },
583
+ ],
584
+ }]);
585
+ if (choice === 'url' || choice === 'npm') {
586
+ const { input } = await inquirer.prompt([{
587
+ type: 'input',
588
+ name: 'input',
589
+ message: choice === 'url' ? 'Paste Git URL:' : 'Package name (e.g. @scope/mcp-server):',
590
+ validate: (v) => v.trim().length > 3 || 'Required',
591
+ }]);
592
+ return runMcpAdd(input.trim());
593
+ }
594
+ return runMcpAddWizard(config, apiKey, model);
595
+ }
596
+ // ── Show analysis result ─────────────────────────────────────────────────
597
+ const runCmd = [analysis.runCommand.command, ...analysis.runCommand.args.map(a => a.length > 60 ? `...${a.slice(-50)}` : a // truncate long absolute paths for display
598
+ )].join(' ');
599
+ const authStatus = analysis.requiresAuth
600
+ ? chalk.yellow(`⚠ Requires ${analysis.envVars.filter(v => v.required).length} env var(s)`)
601
+ : chalk.green('✓ No auth required — installs automatically');
602
+ const locationLine = analysis.cloneDir
603
+ ? `\n${chalk.bold('Location:')} ${chalk.dim(analysis.cloneDir)}`
604
+ : '';
605
+ console.log(boxen(chalk.bold(analysis.name) +
606
+ (analysis.description ? `\n${chalk.gray(analysis.description)}` : '') +
607
+ `\n\n${authStatus}` +
608
+ locationLine +
609
+ `\n\n${chalk.bold('Run:')} ${chalk.cyan(runCmd)}` +
610
+ (analysis.envVars.length > 0
611
+ ? `\n${chalk.bold('Vars:')} ${analysis.envVars.map(v => v.required ? chalk.red(v.name) : chalk.gray(v.name)).join(', ')}`
612
+ : '') +
613
+ (analysis.installNote ? `\n\n${chalk.yellow(analysis.installNote)}` : ''), { padding: 1, borderColor: analysis.requiresAuth ? 'yellow' : 'green', borderStyle: 'round' }));
614
+ // ── Warn if entry point not found ────────────────────────────────────────
615
+ if (analysis.cloneDir && analysis.runCommand.args.length === 0) {
616
+ logger.warn('Entry point not detected automatically.');
617
+ logger.dim(`Edit .didev/config.yaml after saving to set the correct command.`);
618
+ logger.newline();
619
+ }
620
+ // ── Confirm server name ──────────────────────────────────────────────────
621
+ const { serverName } = await inquirer.prompt([{
622
+ type: 'input',
623
+ name: 'serverName',
624
+ message: 'Server name in didev config:',
625
+ default: analysis.name,
626
+ validate: (v) => /^[a-z0-9_-]+$/.test(v) || 'Lowercase letters, numbers, - or _',
627
+ }]);
628
+ // ── Collect env vars ─────────────────────────────────────────────────────
629
+ let collectedEnv = {};
630
+ if (analysis.requiresAuth) {
631
+ collectedEnv = await promptForEnvVars(analysis.envVars.filter(v => v.required));
632
+ const optional = analysis.envVars.filter(v => !v.required);
633
+ if (optional.length > 0) {
634
+ const { configureOptional } = await inquirer.prompt([{
635
+ type: 'confirm',
636
+ name: 'configureOptional',
637
+ message: `Configure ${optional.length} optional var(s)?`,
638
+ default: false,
639
+ }]);
640
+ if (configureOptional) {
641
+ const optEnv = await promptForEnvVars(optional);
642
+ Object.assign(collectedEnv, optEnv);
643
+ }
644
+ }
645
+ }
646
+ // ── Build server config ───────────────────────────────────────────────────
647
+ const serverConfig = {
648
+ name: serverName,
649
+ command: analysis.runCommand.command,
650
+ args: analysis.runCommand.args,
651
+ ...(Object.keys(collectedEnv).length > 0 ? {
652
+ env: Object.fromEntries(Object.keys(collectedEnv).map(k => [k, `\${${k}}`])),
653
+ } : {}),
654
+ };
655
+ // ── Save env values to .didev/.env (gitignored) ──────────────────────────
656
+ if (Object.keys(collectedEnv).length > 0) {
657
+ const envPath = join(process.cwd(), '.didev', '.env');
658
+ let existing = '';
659
+ try {
660
+ existing = await readFile(envPath, 'utf-8');
661
+ }
662
+ catch { }
663
+ const lines = existing.split('\n').filter(Boolean);
664
+ for (const [k, v] of Object.entries(collectedEnv)) {
665
+ const idx = lines.findIndex(l => l.startsWith(`${k}=`));
666
+ if (idx >= 0)
667
+ lines[idx] = `${k}=${v}`;
668
+ else
669
+ lines.push(`${k}=${v}`);
670
+ }
671
+ await writeFile(envPath, lines.join('\n') + '\n', 'utf-8');
672
+ logger.info('Credentials saved to .didev/.env (gitignored)');
673
+ for (const [k, v] of Object.entries(collectedEnv)) {
674
+ process.env[k] = v;
675
+ }
676
+ }
677
+ // ── Test connection ───────────────────────────────────────────────────────
678
+ if (analysis.runCommand.args.length > 0 || analysis.npmPackage) {
679
+ const testSpinner = logger.spinner(`Testing connection to "${serverName}"...`).start();
680
+ const mgr = new McpManager();
681
+ let testOk = false;
682
+ try {
683
+ await mgr.connectOne(serverConfig);
684
+ const summary = mgr.summary().find(s => s.server === serverName);
685
+ const toolCount = summary?.tools.length ?? 0;
686
+ testOk = true;
687
+ testSpinner.succeed(chalk.green(`Connected — ${toolCount} tool(s) available`));
688
+ if (summary)
689
+ summary.tools.forEach(t => logger.dim(` • ${t}`));
690
+ }
691
+ catch (e) {
692
+ testSpinner.fail(chalk.red(`Connection failed: ${e.message}`));
693
+ const { saveAnyway } = await inquirer.prompt([{
694
+ type: 'confirm',
695
+ name: 'saveAnyway',
696
+ message: 'Save config anyway?',
697
+ default: true,
698
+ }]);
699
+ if (!saveAnyway) {
700
+ await mgr.disconnectAll();
701
+ return;
702
+ }
703
+ }
704
+ finally {
705
+ await mgr.disconnectAll();
706
+ }
707
+ // ── Save to config ──────────────────────────────────────────────────────
708
+ await appendServer(serverConfig);
709
+ logger.newline();
710
+ logger.success(`"${serverName}" added to .didev/config.yaml`);
711
+ if (testOk)
712
+ logger.dim('Now available in: didev chat · didev agent');
713
+ }
714
+ else {
715
+ // No entry point found — save anyway with a note
716
+ await appendServer(serverConfig);
717
+ logger.newline();
718
+ logger.success(`"${serverName}" added to .didev/config.yaml`);
719
+ logger.warn('Entry point not set — edit config before using this server.');
720
+ }
721
+ }
722
+ // Wizard for popular npm-based servers
723
+ async function runMcpAddWizard(config, apiKey, model) {
724
+ const POPULAR = [
725
+ { label: 'Filesystem — read/write local files', config: { name: 'filesystem', command: 'npx', args: ['-y', '@modelcontextprotocol/server-filesystem', '.'] } },
726
+ { label: 'GitHub — repos, issues, PRs', config: { name: 'github', command: 'npx', args: ['-y', '@modelcontextprotocol/server-github'], env: { GITHUB_PERSONAL_ACCESS_TOKEN: '${GITHUB_TOKEN}' } } },
727
+ { label: 'Brave Search — web search', config: { name: 'brave-search', command: 'npx', args: ['-y', '@modelcontextprotocol/server-brave-search'], env: { BRAVE_API_KEY: '${BRAVE_API_KEY}' } } },
728
+ { label: 'PostgreSQL — query your DB', config: { name: 'postgres', command: 'npx', args: ['-y', '@modelcontextprotocol/server-postgres', 'postgresql://localhost/mydb'] } },
729
+ { label: 'SQLite — local database', config: { name: 'sqlite', command: 'npx', args: ['-y', '@modelcontextprotocol/server-sqlite', './db.sqlite'] } },
730
+ { label: 'Puppeteer — browser automation', config: { name: 'puppeteer', command: 'npx', args: ['-y', '@modelcontextprotocol/server-puppeteer'] } },
731
+ { label: 'Memory — persistent knowledge graph', config: { name: 'memory', command: 'npx', args: ['-y', '@modelcontextprotocol/server-memory'] } },
732
+ { label: 'Slack — channels and messages', config: { name: 'slack', command: 'npx', args: ['-y', '@modelcontextprotocol/server-slack'], env: { SLACK_BOT_TOKEN: '${SLACK_BOT_TOKEN}', SLACK_TEAM_ID: '${SLACK_TEAM_ID}' } } },
733
+ ];
734
+ const { idx } = await inquirer.prompt([{
735
+ type: 'list',
736
+ name: 'idx',
737
+ message: 'Choose a server:',
738
+ choices: POPULAR.map((s, i) => ({ name: s.label, value: i })),
739
+ pageSize: 10,
740
+ }]);
741
+ const chosen = POPULAR[idx];
742
+ await appendServer(chosen.config);
743
+ logger.success(`"${chosen.config.name}" added. Test with: didev mcp test ${chosen.config.name}`);
744
+ }
745
+ // ─── Other commands ────────────────────────────────────────────────────────
746
+ export async function runMcpList() {
747
+ const config = await loadConfig();
748
+ const servers = config.mcp?.servers ?? [];
749
+ logger.banner('didev mcp', 'MCP Server Management');
750
+ if (servers.length === 0) {
751
+ logger.warn('No MCP servers configured.');
752
+ logger.dim(`Add one: ${chalk.cyan('didev mcp add https://github.com/owner/repo.git')}`);
753
+ logger.dim(`Or pick from list: ${chalk.cyan('didev mcp add')}`);
754
+ return;
755
+ }
756
+ logger.bold(`Configured servers (${servers.length}):`);
757
+ logger.newline();
758
+ for (const srv of servers) {
759
+ const status = srv.enabled === false ? chalk.gray('[disabled]') : chalk.green('[enabled]');
760
+ const cmd = [srv.command, ...(srv.args ?? [])].join(' ');
761
+ console.log(` ${status} ${chalk.bold(srv.name)}`);
762
+ logger.dim(` ${cmd.length > 80 ? cmd.slice(0, 77) + '...' : cmd}`);
763
+ if (srv.env && Object.keys(srv.env).length > 0) {
764
+ logger.dim(` env: ${chalk.yellow(Object.keys(srv.env).join(', '))}`);
765
+ }
766
+ logger.newline();
767
+ }
768
+ logger.dim(`Test: ${chalk.cyan('didev mcp test')}`);
769
+ logger.dim(`Add: ${chalk.cyan('didev mcp add <git-url>')}`);
770
+ }
771
+ export async function runMcpTest(serverName) {
772
+ const config = await loadConfig();
773
+ // Load .didev/.env
774
+ try {
775
+ const envText = await readFile(join(process.cwd(), '.didev', '.env'), 'utf-8');
776
+ for (const line of envText.split('\n')) {
777
+ const eq = line.indexOf('=');
778
+ if (eq > 0)
779
+ process.env[line.slice(0, eq).trim()] = line.slice(eq + 1).trim();
780
+ }
781
+ }
782
+ catch { }
783
+ const servers = (config.mcp?.servers ?? []).filter(s => s.enabled !== false && (!serverName || s.name === serverName));
784
+ if (servers.length === 0) {
785
+ logger.warn(serverName ? `Server "${serverName}" not found.` : 'No enabled servers configured.');
786
+ return;
787
+ }
788
+ logger.banner('didev mcp test', 'Testing MCP connections');
789
+ for (const srv of servers) {
790
+ const spinner = logger.spinner(`Connecting to "${srv.name}"...`).start();
791
+ const mgr = new McpManager();
792
+ try {
793
+ await mgr.connectOne(srv);
794
+ const summary = mgr.summary().find(s => s.server === srv.name);
795
+ const tools = summary?.tools ?? [];
796
+ spinner.succeed(`${chalk.bold(srv.name)} — ${chalk.green(tools.length + ' tools')}`);
797
+ tools.forEach(t => logger.dim(` • ${t}`));
798
+ }
799
+ catch (e) {
800
+ spinner.fail(`${chalk.bold(srv.name)} — ${chalk.red(e.message)}`);
801
+ }
802
+ finally {
803
+ await mgr.disconnectAll();
804
+ }
805
+ logger.newline();
806
+ }
807
+ }
808
+ export async function runMcpRemove(serverName) {
809
+ const config = await loadConfig();
810
+ const servers = config.mcp?.servers ?? [];
811
+ const idx = servers.findIndex(s => s.name === serverName);
812
+ if (idx === -1) {
813
+ logger.error(`Server "${serverName}" not found.`);
814
+ return;
815
+ }
816
+ const { confirm } = await inquirer.prompt([{
817
+ type: 'confirm',
818
+ name: 'confirm',
819
+ message: `Remove "${serverName}"?`,
820
+ default: false,
821
+ }]);
822
+ if (!confirm) {
823
+ logger.info('Cancelled.');
824
+ return;
825
+ }
826
+ servers.splice(idx, 1);
827
+ config.mcp = { servers };
828
+ const configPath = join(process.cwd(), '.didev', 'config.yaml');
829
+ const toSave = JSON.parse(JSON.stringify(config));
830
+ const api = toSave['api'];
831
+ if (api?.['apiKey'] && !String(api['apiKey']).includes('${'))
832
+ api['apiKey'] = '${DEEPSEEK_API_KEY}';
833
+ await writeFile(configPath, yaml.dump(toSave, { indent: 2 }), 'utf-8');
834
+ logger.success(`"${serverName}" removed.`);
835
+ }
836
+ //# sourceMappingURL=mcp.js.map