yo-bug 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 (140) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +160 -0
  3. package/bin/cli.ts +17 -0
  4. package/bin/install.ts +34 -0
  5. package/bin/mcp.ts +2 -0
  6. package/dist/bin/cli.d.ts +3 -0
  7. package/dist/bin/cli.d.ts.map +1 -0
  8. package/dist/bin/cli.js +19 -0
  9. package/dist/bin/cli.js.map +1 -0
  10. package/dist/bin/install.d.ts +3 -0
  11. package/dist/bin/install.d.ts.map +1 -0
  12. package/dist/bin/install.js +28 -0
  13. package/dist/bin/install.js.map +1 -0
  14. package/dist/bin/mcp.d.ts +3 -0
  15. package/dist/bin/mcp.d.ts.map +1 -0
  16. package/dist/bin/mcp.js +3 -0
  17. package/dist/bin/mcp.js.map +1 -0
  18. package/dist/src/detect/dev-server.d.ts +11 -0
  19. package/dist/src/detect/dev-server.d.ts.map +1 -0
  20. package/dist/src/detect/dev-server.js +92 -0
  21. package/dist/src/detect/dev-server.js.map +1 -0
  22. package/dist/src/detect/spawn-dev.d.ts +4 -0
  23. package/dist/src/detect/spawn-dev.d.ts.map +1 -0
  24. package/dist/src/detect/spawn-dev.js +37 -0
  25. package/dist/src/detect/spawn-dev.js.map +1 -0
  26. package/dist/src/install/detect-ai-tool.d.ts +7 -0
  27. package/dist/src/install/detect-ai-tool.d.ts.map +1 -0
  28. package/dist/src/install/detect-ai-tool.js +38 -0
  29. package/dist/src/install/detect-ai-tool.js.map +1 -0
  30. package/dist/src/install/write-mcp-config.d.ts +6 -0
  31. package/dist/src/install/write-mcp-config.d.ts.map +1 -0
  32. package/dist/src/install/write-mcp-config.js +47 -0
  33. package/dist/src/install/write-mcp-config.js.map +1 -0
  34. package/dist/src/mcp/index.d.ts +2 -0
  35. package/dist/src/mcp/index.d.ts.map +1 -0
  36. package/dist/src/mcp/index.js +263 -0
  37. package/dist/src/mcp/index.js.map +1 -0
  38. package/dist/src/mcp/tools/checklist.d.ts +22 -0
  39. package/dist/src/mcp/tools/checklist.d.ts.map +1 -0
  40. package/dist/src/mcp/tools/checklist.js +58 -0
  41. package/dist/src/mcp/tools/checklist.js.map +1 -0
  42. package/dist/src/mcp/tools/get-feedback.d.ts +13 -0
  43. package/dist/src/mcp/tools/get-feedback.d.ts.map +1 -0
  44. package/dist/src/mcp/tools/get-feedback.js +27 -0
  45. package/dist/src/mcp/tools/get-feedback.js.map +1 -0
  46. package/dist/src/mcp/tools/list-feedbacks.d.ts +20 -0
  47. package/dist/src/mcp/tools/list-feedbacks.d.ts.map +1 -0
  48. package/dist/src/mcp/tools/list-feedbacks.js +29 -0
  49. package/dist/src/mcp/tools/list-feedbacks.js.map +1 -0
  50. package/dist/src/mcp/tools/resolve-feedback.d.ts +8 -0
  51. package/dist/src/mcp/tools/resolve-feedback.d.ts.map +1 -0
  52. package/dist/src/mcp/tools/resolve-feedback.js +28 -0
  53. package/dist/src/mcp/tools/resolve-feedback.js.map +1 -0
  54. package/dist/src/mcp/tools/start-session.d.ts +11 -0
  55. package/dist/src/mcp/tools/start-session.d.ts.map +1 -0
  56. package/dist/src/mcp/tools/start-session.js +56 -0
  57. package/dist/src/mcp/tools/start-session.js.map +1 -0
  58. package/dist/src/mcp/tools/stop-session.d.ts +6 -0
  59. package/dist/src/mcp/tools/stop-session.d.ts.map +1 -0
  60. package/dist/src/mcp/tools/stop-session.js +80 -0
  61. package/dist/src/mcp/tools/stop-session.js.map +1 -0
  62. package/dist/src/mcp/tools/test-history.d.ts +33 -0
  63. package/dist/src/mcp/tools/test-history.d.ts.map +1 -0
  64. package/dist/src/mcp/tools/test-history.js +66 -0
  65. package/dist/src/mcp/tools/test-history.js.map +1 -0
  66. package/dist/src/server/feedback-api.d.ts +4 -0
  67. package/dist/src/server/feedback-api.d.ts.map +1 -0
  68. package/dist/src/server/feedback-api.js +152 -0
  69. package/dist/src/server/feedback-api.js.map +1 -0
  70. package/dist/src/server/html-injector.d.ts +10 -0
  71. package/dist/src/server/html-injector.d.ts.map +1 -0
  72. package/dist/src/server/html-injector.js +34 -0
  73. package/dist/src/server/html-injector.js.map +1 -0
  74. package/dist/src/server/proxy-server.d.ts +8 -0
  75. package/dist/src/server/proxy-server.d.ts.map +1 -0
  76. package/dist/src/server/proxy-server.js +87 -0
  77. package/dist/src/server/proxy-server.js.map +1 -0
  78. package/dist/src/server/sdk-serve.d.ts +3 -0
  79. package/dist/src/server/sdk-serve.d.ts.map +1 -0
  80. package/dist/src/server/sdk-serve.js +20 -0
  81. package/dist/src/server/sdk-serve.js.map +1 -0
  82. package/dist/src/storage/store.d.ts +21 -0
  83. package/dist/src/storage/store.d.ts.map +1 -0
  84. package/dist/src/storage/store.js +138 -0
  85. package/dist/src/storage/store.js.map +1 -0
  86. package/dist/src/storage/types.d.ts +74 -0
  87. package/dist/src/storage/types.d.ts.map +1 -0
  88. package/dist/src/storage/types.js +2 -0
  89. package/dist/src/storage/types.js.map +1 -0
  90. package/dist/vibe-feedback.js +399 -0
  91. package/package.json +67 -0
  92. package/src/client/annotation-mode/canvas-editor.ts +178 -0
  93. package/src/client/annotation-mode/history.ts +32 -0
  94. package/src/client/annotation-mode/region-selector.ts +123 -0
  95. package/src/client/annotation-mode/screenshot.ts +17 -0
  96. package/src/client/annotation-mode/toolbar.ts +139 -0
  97. package/src/client/annotation-mode/tools/arrow.ts +57 -0
  98. package/src/client/annotation-mode/tools/base-tool.ts +25 -0
  99. package/src/client/annotation-mode/tools/circle.ts +37 -0
  100. package/src/client/annotation-mode/tools/freehand.ts +23 -0
  101. package/src/client/annotation-mode/tools/rect.ts +32 -0
  102. package/src/client/annotation-mode/tools/text.ts +93 -0
  103. package/src/client/api/client.ts +48 -0
  104. package/src/client/capture/action-recorder.ts +157 -0
  105. package/src/client/capture/console-interceptor.ts +65 -0
  106. package/src/client/capture/context-buffer.ts +23 -0
  107. package/src/client/capture/error-interceptor.ts +52 -0
  108. package/src/client/capture/network-interceptor.ts +143 -0
  109. package/src/client/core/event-bus.ts +20 -0
  110. package/src/client/core/i18n.ts +83 -0
  111. package/src/client/core/sdk.ts +373 -0
  112. package/src/client/core/shadow-host.ts +27 -0
  113. package/src/client/element-mode/highlighter.ts +79 -0
  114. package/src/client/element-mode/inspector.ts +73 -0
  115. package/src/client/element-mode/selector.ts +22 -0
  116. package/src/client/index.ts +10 -0
  117. package/src/client/styles/sdk.css.ts +222 -0
  118. package/src/client/ui/checklist-panel.ts +279 -0
  119. package/src/client/ui/feedback-panel.ts +149 -0
  120. package/src/client/ui/floating-button.ts +103 -0
  121. package/src/client/ui/toast.ts +17 -0
  122. package/src/client/ui/verify-panel.ts +111 -0
  123. package/src/detect/dev-server.ts +110 -0
  124. package/src/detect/spawn-dev.ts +50 -0
  125. package/src/install/detect-ai-tool.ts +49 -0
  126. package/src/install/write-mcp-config.ts +49 -0
  127. package/src/mcp/index.ts +327 -0
  128. package/src/mcp/tools/checklist.ts +61 -0
  129. package/src/mcp/tools/get-feedback.ts +34 -0
  130. package/src/mcp/tools/list-feedbacks.ts +37 -0
  131. package/src/mcp/tools/resolve-feedback.ts +34 -0
  132. package/src/mcp/tools/start-session.ts +65 -0
  133. package/src/mcp/tools/stop-session.ts +93 -0
  134. package/src/mcp/tools/test-history.ts +97 -0
  135. package/src/server/feedback-api.ts +164 -0
  136. package/src/server/html-injector.ts +41 -0
  137. package/src/server/proxy-server.ts +107 -0
  138. package/src/server/sdk-serve.ts +24 -0
  139. package/src/storage/store.ts +172 -0
  140. package/src/storage/types.ts +68 -0
@@ -0,0 +1,103 @@
1
+ import { t } from '../core/i18n.js';
2
+
3
+ const SEEN_KEY = 'yo-bug-hint-seen';
4
+
5
+ export class FloatingButton {
6
+ private el: HTMLButtonElement;
7
+ private recDot: HTMLDivElement;
8
+ private active = false;
9
+
10
+ constructor(shadowRoot: ShadowRoot, private onClick: () => void) {
11
+ this.el = document.createElement('button');
12
+ this.el.className = 'vf-fab';
13
+ this.el.innerHTML = '👁'; // eye
14
+ this.el.title = 'yo-bug';
15
+ this.el.addEventListener('click', () => {
16
+ this.dismissHint();
17
+ this.onClick();
18
+ });
19
+
20
+ // Recording indicator dot
21
+ this.recDot = document.createElement('div');
22
+ this.recDot.style.cssText = `
23
+ position: absolute; top: 2px; right: 2px;
24
+ width: 10px; height: 10px; border-radius: 50%;
25
+ background: #ef4444; border: 2px solid #1e293b;
26
+ animation: vf-pulse 1.5s infinite;
27
+ `;
28
+
29
+ const style = document.createElement('style');
30
+ style.textContent = `
31
+ @keyframes vf-pulse {
32
+ 0%, 100% { opacity: 1; }
33
+ 50% { opacity: 0.4; }
34
+ }
35
+ `;
36
+ shadowRoot.appendChild(style);
37
+
38
+ this.el.style.position = 'relative';
39
+ this.el.appendChild(this.recDot);
40
+ shadowRoot.appendChild(this.el);
41
+
42
+ // First-time hint bubble
43
+ this.showHintIfNeeded(shadowRoot);
44
+ }
45
+
46
+ setActive(active: boolean): void {
47
+ this.active = active;
48
+ this.el.classList.toggle('active', active);
49
+ this.el.innerHTML = active ? '✕' : '👁';
50
+ this.el.appendChild(this.recDot);
51
+ }
52
+
53
+ isActive(): boolean {
54
+ return this.active;
55
+ }
56
+
57
+ destroy(): void {
58
+ this.el.remove();
59
+ }
60
+
61
+ private hint: HTMLDivElement | null = null;
62
+
63
+ private showHintIfNeeded(shadowRoot: ShadowRoot): void {
64
+ try {
65
+ if (localStorage.getItem(SEEN_KEY)) return;
66
+ } catch {}
67
+
68
+ this.hint = document.createElement('div');
69
+ this.hint.style.cssText = `
70
+ position: fixed; bottom: 82px; right: 20px;
71
+ background: #1e293b; color: #e2e8f0;
72
+ padding: 10px 16px; border-radius: 10px;
73
+ font-size: 13px; pointer-events: auto;
74
+ box-shadow: 0 4px 16px rgba(0,0,0,0.35);
75
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
76
+ animation: vf-slide-in 0.3s ease-out;
77
+ max-width: 200px; line-height: 1.5;
78
+ `;
79
+ this.hint.textContent = t('hint.firstTime');
80
+
81
+ // Arrow pointing down to the button
82
+ const arrow = document.createElement('div');
83
+ arrow.style.cssText = `
84
+ position: absolute; bottom: -6px; right: 22px;
85
+ width: 12px; height: 12px; background: #1e293b;
86
+ transform: rotate(45deg);
87
+ `;
88
+ this.hint.appendChild(arrow);
89
+
90
+ shadowRoot.appendChild(this.hint);
91
+
92
+ // Auto-dismiss after 5s
93
+ setTimeout(() => this.dismissHint(), 5000);
94
+ }
95
+
96
+ private dismissHint(): void {
97
+ if (this.hint) {
98
+ this.hint.remove();
99
+ this.hint = null;
100
+ try { localStorage.setItem(SEEN_KEY, '1'); } catch {}
101
+ }
102
+ }
103
+ }
@@ -0,0 +1,17 @@
1
+ export function showToast(
2
+ shadowRoot: ShadowRoot,
3
+ message: string,
4
+ isError = false,
5
+ duration = 3000
6
+ ): void {
7
+ const el = document.createElement('div');
8
+ el.className = `vf-toast${isError ? ' error' : ''}`;
9
+ el.textContent = message;
10
+ shadowRoot.appendChild(el);
11
+
12
+ setTimeout(() => {
13
+ el.style.opacity = '0';
14
+ el.style.transition = 'opacity 0.3s';
15
+ setTimeout(() => el.remove(), 300);
16
+ }, duration);
17
+ }
@@ -0,0 +1,111 @@
1
+ import { t } from '../core/i18n.js';
2
+
3
+ export class VerifyPanel {
4
+ private container: HTMLDivElement;
5
+ private pollTimer: ReturnType<typeof setInterval> | null = null;
6
+ private baseUrl: string;
7
+ private shownIds = new Set<string>();
8
+
9
+ constructor(private shadowRoot: ShadowRoot) {
10
+ this.baseUrl = window.location.origin;
11
+ this.container = document.createElement('div');
12
+ this.container.style.cssText = `
13
+ position: fixed; bottom: 84px; left: 16px; width: 300px;
14
+ display: flex; flex-direction: column; gap: 8px;
15
+ pointer-events: none; z-index: 2147483647;
16
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
17
+ `;
18
+ shadowRoot.appendChild(this.container);
19
+ }
20
+
21
+ startPolling(): void {
22
+ this.pollTimer = setInterval(() => this.poll(), 3000);
23
+ }
24
+
25
+ stopPolling(): void {
26
+ if (this.pollTimer) {
27
+ clearInterval(this.pollTimer);
28
+ this.pollTimer = null;
29
+ }
30
+ }
31
+
32
+ private async poll(): Promise<void> {
33
+ try {
34
+ const res = await fetch(`${this.baseUrl}/api/verify`);
35
+ const data = await res.json();
36
+ for (const item of data.items || []) {
37
+ if (!this.shownIds.has(item.feedbackId)) {
38
+ this.shownIds.add(item.feedbackId);
39
+ this.showVerifyCard(item);
40
+ }
41
+ }
42
+ } catch {}
43
+ }
44
+
45
+ private showVerifyCard(item: any): void {
46
+ const card = document.createElement('div');
47
+ card.style.cssText = `
48
+ background: #1e293b; border-radius: 10px; padding: 14px 16px;
49
+ box-shadow: 0 4px 20px rgba(0,0,0,0.35); pointer-events: auto;
50
+ color: #e2e8f0; font-size: 13px;
51
+ border-left: 3px solid #f59e0b;
52
+ animation: vf-slide-in 0.3s ease-out;
53
+ `;
54
+
55
+ const title = document.createElement('div');
56
+ title.style.cssText = `font-weight:600;color:#f59e0b;margin-bottom:6px;font-size:12px;`;
57
+ title.textContent = t('verify.title');
58
+ card.appendChild(title);
59
+
60
+ const desc = document.createElement('div');
61
+ desc.style.cssText = `margin-bottom:10px;line-height:1.4;`;
62
+ desc.textContent = item.description || `${item.problemType} on ${item.element || 'page'}`;
63
+ card.appendChild(desc);
64
+
65
+ const btnRow = document.createElement('div');
66
+ btnRow.style.cssText = `display:flex;gap:8px;`;
67
+
68
+ const passBtn = document.createElement('button');
69
+ passBtn.style.cssText = `
70
+ flex:1; padding:6px; border:none; border-radius:6px;
71
+ background:#22c55e; color:white; font-size:12px; cursor:pointer; font-weight:500;
72
+ `;
73
+ passBtn.textContent = '\u2713 ' + t('verify.fixed');
74
+ passBtn.addEventListener('click', () => {
75
+ this.respond(item.feedbackId, true);
76
+ card.remove();
77
+ });
78
+
79
+ const failBtn = document.createElement('button');
80
+ failBtn.style.cssText = `
81
+ flex:1; padding:6px; border:none; border-radius:6px;
82
+ background:#ef4444; color:white; font-size:12px; cursor:pointer; font-weight:500;
83
+ `;
84
+ failBtn.textContent = '\u2717 ' + t('verify.broken');
85
+ failBtn.addEventListener('click', () => {
86
+ this.respond(item.feedbackId, false);
87
+ card.remove();
88
+ });
89
+
90
+ btnRow.appendChild(passBtn);
91
+ btnRow.appendChild(failBtn);
92
+ card.appendChild(btnRow);
93
+
94
+ this.container.appendChild(card);
95
+ }
96
+
97
+ private async respond(feedbackId: string, confirmed: boolean): Promise<void> {
98
+ try {
99
+ await fetch(`${this.baseUrl}/api/verify/${feedbackId}`, {
100
+ method: 'PATCH',
101
+ headers: { 'Content-Type': 'application/json' },
102
+ body: JSON.stringify({ confirmed }),
103
+ });
104
+ } catch {}
105
+ }
106
+
107
+ destroy(): void {
108
+ this.stopPolling();
109
+ this.container.remove();
110
+ }
111
+ }
@@ -0,0 +1,110 @@
1
+ import fs from 'fs/promises';
2
+ import net from 'net';
3
+ import path from 'path';
4
+
5
+ interface DevServerInfo {
6
+ framework: string;
7
+ port: number;
8
+ command: string;
9
+ isRunning: boolean;
10
+ }
11
+
12
+ const FRAMEWORK_PORTS: Record<string, { port: number; keywords: string[] }> = {
13
+ vite: { port: 5173, keywords: ['vite'] },
14
+ next: { port: 3000, keywords: ['next'] },
15
+ 'create-react-app': { port: 3000, keywords: ['react-scripts'] },
16
+ webpack: { port: 8080, keywords: ['webpack serve', 'webpack-dev-server'] },
17
+ nuxt: { port: 3000, keywords: ['nuxt'] },
18
+ angular: { port: 4200, keywords: ['ng serve'] },
19
+ svelte: { port: 5173, keywords: ['svelte-kit', 'vite'] },
20
+ astro: { port: 4321, keywords: ['astro'] },
21
+ };
22
+
23
+ export async function detectDevServer(
24
+ cwd: string,
25
+ manualPort?: number
26
+ ): Promise<DevServerInfo> {
27
+ // Read package.json
28
+ let devCommand = '';
29
+ let framework = 'unknown';
30
+ let port = manualPort || 0;
31
+
32
+ try {
33
+ const pkgPath = path.join(cwd, 'package.json');
34
+ const pkg = JSON.parse(await fs.readFile(pkgPath, 'utf-8'));
35
+ const scripts = pkg.scripts || {};
36
+
37
+ // Check "dev" or "start" scripts
38
+ devCommand = scripts.dev || scripts.start || '';
39
+
40
+ // Match framework
41
+ for (const [name, info] of Object.entries(FRAMEWORK_PORTS)) {
42
+ if (info.keywords.some((kw) => devCommand.includes(kw))) {
43
+ framework = name;
44
+ if (!manualPort) port = info.port;
45
+ break;
46
+ }
47
+ }
48
+
49
+ // Also check dependencies for framework detection
50
+ if (framework === 'unknown') {
51
+ const allDeps = {
52
+ ...pkg.dependencies,
53
+ ...pkg.devDependencies,
54
+ };
55
+ for (const [name, info] of Object.entries(FRAMEWORK_PORTS)) {
56
+ if (info.keywords.some((kw) => kw in allDeps)) {
57
+ framework = name;
58
+ if (!manualPort) port = info.port;
59
+ break;
60
+ }
61
+ }
62
+ }
63
+ } catch {
64
+ // No package.json found
65
+ }
66
+
67
+ // Default port
68
+ if (!port) port = 3000;
69
+
70
+ // Check if port is already in use
71
+ const isRunning = await isPortInUse(port);
72
+
73
+ return {
74
+ framework,
75
+ port,
76
+ command: devCommand,
77
+ isRunning,
78
+ };
79
+ }
80
+
81
+ export function isPortInUse(port: number): Promise<boolean> {
82
+ return new Promise((resolve) => {
83
+ const socket = new net.Socket();
84
+ socket.setTimeout(1000);
85
+ socket.on('connect', () => {
86
+ socket.destroy();
87
+ resolve(true);
88
+ });
89
+ socket.on('timeout', () => {
90
+ socket.destroy();
91
+ resolve(false);
92
+ });
93
+ socket.on('error', () => {
94
+ resolve(false);
95
+ });
96
+ socket.connect(port, '127.0.0.1');
97
+ });
98
+ }
99
+
100
+ export async function waitForPort(
101
+ port: number,
102
+ timeout = 30000
103
+ ): Promise<boolean> {
104
+ const start = Date.now();
105
+ while (Date.now() - start < timeout) {
106
+ if (await isPortInUse(port)) return true;
107
+ await new Promise((r) => setTimeout(r, 500));
108
+ }
109
+ return false;
110
+ }
@@ -0,0 +1,50 @@
1
+ import { spawn, ChildProcess } from 'child_process';
2
+ import { waitForPort } from './dev-server.js';
3
+
4
+ let devProcess: ChildProcess | null = null;
5
+
6
+ export async function spawnDevServer(
7
+ cwd: string,
8
+ command: string,
9
+ port: number
10
+ ): Promise<boolean> {
11
+ const isWindows = process.platform === 'win32';
12
+ const npmCmd = isWindows ? 'npm.cmd' : 'npm';
13
+
14
+ // Use the detected script name (e.g., "dev" or "start")
15
+ const scriptName = command.includes('start') ? 'start' : 'dev';
16
+
17
+ devProcess = spawn(npmCmd, ['run', scriptName], {
18
+ cwd,
19
+ shell: isWindows,
20
+ stdio: 'pipe', // Don't inherit — we're an MCP server using stdio
21
+ env: { ...process.env },
22
+ });
23
+
24
+ devProcess.stdout?.on('data', () => {
25
+ // Silently consume output — MCP server uses stdio
26
+ });
27
+
28
+ devProcess.stderr?.on('data', () => {
29
+ // Silently consume stderr
30
+ });
31
+
32
+ devProcess.on('error', () => {
33
+ // Silently handle — MCP server uses stdio, can't write to stderr
34
+ });
35
+
36
+ // Wait for the port to become available
37
+ const ready = await waitForPort(port, 30000);
38
+ return ready;
39
+ }
40
+
41
+ export function killDevServer(): void {
42
+ if (devProcess) {
43
+ devProcess.kill();
44
+ devProcess = null;
45
+ }
46
+ }
47
+
48
+ export function isDevServerSpawned(): boolean {
49
+ return devProcess !== null && !devProcess.killed;
50
+ }
@@ -0,0 +1,49 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+
4
+ export interface AITool {
5
+ name: string;
6
+ configPath: string;
7
+ detected: boolean;
8
+ }
9
+
10
+ const HOME = process.env.HOME || process.env.USERPROFILE || '';
11
+
12
+ export async function detectAITools(): Promise<AITool[]> {
13
+ const tools: AITool[] = [];
14
+
15
+ // Claude Code
16
+ const claudeDir = path.join(HOME, '.claude');
17
+ tools.push({
18
+ name: 'Claude Code',
19
+ configPath: path.join(claudeDir, 'settings.json'),
20
+ detected: await dirExists(claudeDir),
21
+ });
22
+
23
+ // Cursor
24
+ const cursorDir = path.join(HOME, '.cursor');
25
+ tools.push({
26
+ name: 'Cursor',
27
+ configPath: path.join(cursorDir, 'mcp.json'),
28
+ detected: await dirExists(cursorDir),
29
+ });
30
+
31
+ // Windsurf
32
+ const windsurfDir = path.join(HOME, '.windsurf');
33
+ tools.push({
34
+ name: 'Windsurf',
35
+ configPath: path.join(windsurfDir, 'mcp.json'),
36
+ detected: await dirExists(windsurfDir),
37
+ });
38
+
39
+ return tools;
40
+ }
41
+
42
+ async function dirExists(dir: string): Promise<boolean> {
43
+ try {
44
+ const stat = await fs.stat(dir);
45
+ return stat.isDirectory();
46
+ } catch {
47
+ return false;
48
+ }
49
+ }
@@ -0,0 +1,49 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import type { AITool } from './detect-ai-tool.js';
4
+
5
+ const MCP_SERVER_CONFIG = {
6
+ command: 'npx',
7
+ args: ['yo-bug', 'mcp'],
8
+ };
9
+
10
+ export async function writeMcpConfig(tool: AITool): Promise<{ success: boolean; message: string }> {
11
+ try {
12
+ // Ensure directory exists
13
+ await fs.mkdir(path.dirname(tool.configPath), { recursive: true });
14
+
15
+ // Read existing config
16
+ let config: any = {};
17
+ try {
18
+ const content = await fs.readFile(tool.configPath, 'utf-8');
19
+ config = JSON.parse(content);
20
+ } catch {
21
+ // File doesn't exist or invalid JSON — start fresh
22
+ }
23
+
24
+ // Check if already configured
25
+ if (tool.name === 'Claude Code') {
26
+ config.mcpServers = config.mcpServers || {};
27
+ if (config.mcpServers['yo-bug']) {
28
+ return { success: true, message: `${tool.name}: 已配置,跳过。` };
29
+ }
30
+ config.mcpServers['yo-bug'] = {
31
+ type: 'stdio',
32
+ ...MCP_SERVER_CONFIG,
33
+ };
34
+ } else {
35
+ // Cursor / Windsurf format
36
+ config.mcpServers = config.mcpServers || {};
37
+ if (config.mcpServers['yo-bug']) {
38
+ return { success: true, message: `${tool.name}: 已配置,跳过。` };
39
+ }
40
+ config.mcpServers['yo-bug'] = MCP_SERVER_CONFIG;
41
+ }
42
+
43
+ // Write config
44
+ await fs.writeFile(tool.configPath, JSON.stringify(config, null, 2), 'utf-8');
45
+ return { success: true, message: `${tool.name}: MCP 配置已写入 ${tool.configPath}` };
46
+ } catch (err: any) {
47
+ return { success: false, message: `${tool.name}: 写入失败 — ${err.message}` };
48
+ }
49
+ }