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,97 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+
4
+ const HISTORY_DIR = path.join(
5
+ process.env.HOME || process.env.USERPROFILE || '.',
6
+ '.yo-bug',
7
+ 'test-history'
8
+ );
9
+
10
+ interface TestRecord {
11
+ module: string;
12
+ title: string;
13
+ items: any[];
14
+ results: { passed: number; failed: number; total: number };
15
+ failedItems: string[];
16
+ timestamp: string;
17
+ }
18
+
19
+ export async function saveTestRecord(args: {
20
+ module: string;
21
+ title: string;
22
+ items: any[];
23
+ results: { passed: number; failed: number; total: number };
24
+ failedItems: string[];
25
+ }): Promise<{ text: string }> {
26
+ await fs.mkdir(HISTORY_DIR, { recursive: true });
27
+
28
+ const record: TestRecord = {
29
+ ...args,
30
+ timestamp: new Date().toISOString(),
31
+ };
32
+
33
+ // Load existing records for this module
34
+ const filePath = path.join(HISTORY_DIR, `${sanitize(args.module)}.json`);
35
+ let records: TestRecord[] = [];
36
+ try {
37
+ const data = await fs.readFile(filePath, 'utf-8');
38
+ records = JSON.parse(data);
39
+ } catch {}
40
+
41
+ records.push(record);
42
+ // Keep last 20 records per module
43
+ if (records.length > 20) records = records.slice(-20);
44
+
45
+ await fs.writeFile(filePath, JSON.stringify(records, null, 2), 'utf-8');
46
+
47
+ return {
48
+ text: `测试记录已保存。模块「${args.module}」累计 ${records.length} 次测试记录。`,
49
+ };
50
+ }
51
+
52
+ export async function getTestHistory(args: {
53
+ module: string;
54
+ }): Promise<{ text: string; records: TestRecord[] }> {
55
+ const filePath = path.join(HISTORY_DIR, `${sanitize(args.module)}.json`);
56
+ let records: TestRecord[] = [];
57
+ try {
58
+ const data = await fs.readFile(filePath, 'utf-8');
59
+ records = JSON.parse(data);
60
+ } catch {
61
+ return { text: `模块「${args.module}」没有历史测试记录。`, records: [] };
62
+ }
63
+
64
+ const lines = [`模块「${args.module}」历史测试记录(${records.length} 次):\n`];
65
+
66
+ for (const r of records.slice(-5)) { // Show last 5
67
+ lines.push(`[${r.timestamp.slice(0, 10)}] ${r.title}`);
68
+ lines.push(` 结果: ${r.results.passed}/${r.results.total} 通过`);
69
+ if (r.failedItems.length > 0) {
70
+ lines.push(` 失败项: ${r.failedItems.join('; ')}`);
71
+ }
72
+ }
73
+
74
+ // Aggregate: which test scenarios fail most often?
75
+ const failCounts: Record<string, number> = {};
76
+ for (const r of records) {
77
+ for (const f of r.failedItems) {
78
+ failCounts[f] = (failCounts[f] || 0) + 1;
79
+ }
80
+ }
81
+ const frequentFails = Object.entries(failCounts)
82
+ .sort((a, b) => b[1] - a[1])
83
+ .slice(0, 5);
84
+
85
+ if (frequentFails.length > 0) {
86
+ lines.push('\n高频失败场景(生成清单时请重点覆盖):');
87
+ for (const [scenario, count] of frequentFails) {
88
+ lines.push(` ${count}x 失败: ${scenario}`);
89
+ }
90
+ }
91
+
92
+ return { text: lines.join('\n'), records };
93
+ }
94
+
95
+ function sanitize(name: string): string {
96
+ return name.replace(/[^a-zA-Z0-9\u4e00-\u9fff_-]/g, '_').slice(0, 50);
97
+ }
@@ -0,0 +1,164 @@
1
+ import { Router } from 'express';
2
+ import multer from 'multer';
3
+ import { FeedbackStore } from '../storage/store.js';
4
+
5
+ const upload = multer({ storage: multer.memoryStorage() });
6
+
7
+ export function createFeedbackRouter(store: FeedbackStore): Router {
8
+ const router = Router();
9
+
10
+ // Submit feedback
11
+ router.post('/api/feedback', upload.single('screenshot'), async (req, res) => {
12
+ try {
13
+ let feedbackData: any;
14
+
15
+ if (req.body.feedback) {
16
+ // multipart form: feedback JSON string + screenshot file
17
+ feedbackData = JSON.parse(req.body.feedback);
18
+ } else {
19
+ // plain JSON body
20
+ feedbackData = req.body;
21
+ }
22
+
23
+ // Sanitize: only allow known fields, strip anything dangerous
24
+ const sanitized = {
25
+ mode: feedbackData.mode,
26
+ problemType: feedbackData.problemType,
27
+ description: String(feedbackData.description || '').slice(0, 2000),
28
+ element: feedbackData.element,
29
+ pageUrl: String(feedbackData.pageUrl || '').slice(0, 500),
30
+ pageTitle: String(feedbackData.pageTitle || '').slice(0, 200),
31
+ viewport: feedbackData.viewport,
32
+ userAgent: String(feedbackData.userAgent || '').slice(0, 300),
33
+ timestamp: feedbackData.timestamp,
34
+ consoleErrors: Array.isArray(feedbackData.consoleErrors) ? feedbackData.consoleErrors.slice(0, 50) : [],
35
+ networkErrors: Array.isArray(feedbackData.networkErrors) ? feedbackData.networkErrors.slice(0, 50) : [],
36
+ unhandledErrors: Array.isArray(feedbackData.unhandledErrors) ? feedbackData.unhandledErrors.slice(0, 50) : [],
37
+ actionSteps: Array.isArray(feedbackData.actionSteps) ? feedbackData.actionSteps.slice(0, 100) : [],
38
+ hasScreenshot: false,
39
+ };
40
+
41
+ const screenshot = req.file?.buffer;
42
+ const item = await store.create(sanitized, screenshot);
43
+
44
+ res.json({ id: item.id, createdAt: item.createdAt });
45
+ } catch (err: any) {
46
+ res.status(400).json({ error: err.message });
47
+ }
48
+ });
49
+
50
+ // List feedbacks
51
+ router.get('/api/feedback', async (req, res) => {
52
+ try {
53
+ const result = await store.list({
54
+ limit: Number(req.query.limit) || 20,
55
+ status: (req.query.status as string) || 'open',
56
+ type: req.query.type as string,
57
+ });
58
+ res.json(result);
59
+ } catch (err: any) {
60
+ res.status(500).json({ error: err.message });
61
+ }
62
+ });
63
+
64
+ // Get single feedback
65
+ router.get('/api/feedback/:id', async (req, res) => {
66
+ try {
67
+ const item = await store.getById(req.params.id);
68
+ if (!item) {
69
+ return res.status(404).json({ error: 'Not found' });
70
+ }
71
+ res.json(item);
72
+ } catch (err: any) {
73
+ res.status(500).json({ error: err.message });
74
+ }
75
+ });
76
+
77
+ // Get screenshot
78
+ router.get('/api/feedback/:id/screenshot', async (req, res) => {
79
+ try {
80
+ const screenshot = await store.getScreenshot(req.params.id);
81
+ if (!screenshot) {
82
+ return res.status(404).json({ error: 'No screenshot' });
83
+ }
84
+ res.setHeader('Content-Type', 'image/png');
85
+ res.end(screenshot);
86
+ } catch (err: any) {
87
+ res.status(500).json({ error: err.message });
88
+ }
89
+ });
90
+
91
+ // --- Checklist API (AI → Browser → AI) ---
92
+ let checklist: { items: any[]; title: string; createdAt: string } = {
93
+ items: [],
94
+ title: '',
95
+ createdAt: '',
96
+ };
97
+
98
+ // AI pushes checklist (supports both string[] and structured items)
99
+ router.post('/api/checklist', (req, res) => {
100
+ const { title, items } = req.body;
101
+ checklist = {
102
+ title: title || 'Test Checklist',
103
+ items: (items || []).map((item: any, i: number) => ({
104
+ id: i,
105
+ step: typeof item === 'string' ? item : (item.step || item.text || ''),
106
+ expect: typeof item === 'string' ? '' : (item.expect || ''),
107
+ priority: (typeof item === 'object' && item.priority) || 'normal',
108
+ dimension: (typeof item === 'object' && item.dimension) || '',
109
+ status: 'pending',
110
+ feedback: '',
111
+ })),
112
+ createdAt: new Date().toISOString(),
113
+ };
114
+ res.json({ ok: true, count: checklist.items.length });
115
+ });
116
+
117
+ // Browser polls checklist
118
+ router.get('/api/checklist', (_req, res) => {
119
+ res.json(checklist);
120
+ });
121
+
122
+ // Browser updates a checklist item
123
+ router.patch('/api/checklist/:itemId', (req, res) => {
124
+ const id = parseInt(req.params.itemId);
125
+ const item = checklist.items.find((i: any) => i.id === id);
126
+ if (!item) return res.status(404).json({ error: 'Not found' });
127
+ if (req.body.status) item.status = req.body.status;
128
+ if (req.body.feedback !== undefined) item.feedback = req.body.feedback;
129
+ res.json(item);
130
+ });
131
+
132
+ // --- Verification API (AI → Browser → AI) ---
133
+ let pendingVerifications: any[] = [];
134
+
135
+ // AI pushes verification request
136
+ router.post('/api/verify', (req, res) => {
137
+ pendingVerifications.push(req.body);
138
+ res.json({ ok: true });
139
+ });
140
+
141
+ // Browser polls for pending verifications
142
+ router.get('/api/verify', (_req, res) => {
143
+ res.json({ items: pendingVerifications });
144
+ });
145
+
146
+ // Browser confirms/rejects verification
147
+ router.patch('/api/verify/:feedbackId', async (req, res) => {
148
+ const { feedbackId } = req.params;
149
+ const { confirmed } = req.body; // true = fix works, false = still broken
150
+ const newStatus = confirmed ? 'resolved' : 'open';
151
+ await store.updateStatus(feedbackId, newStatus);
152
+ pendingVerifications = pendingVerifications.filter(v => v.feedbackId !== feedbackId);
153
+ res.json({ ok: true, status: newStatus });
154
+ });
155
+
156
+ // Reset in-memory state
157
+ router.post('/api/reset', (_req, res) => {
158
+ checklist = { items: [], title: '', createdAt: '' };
159
+ pendingVerifications = [];
160
+ res.json({ ok: true });
161
+ });
162
+
163
+ return router;
164
+ }
@@ -0,0 +1,41 @@
1
+ import type { IncomingMessage, ServerResponse } from 'http';
2
+
3
+ const INJECT_SCRIPT = `<script src="/vibe-feedback.js"></script>`;
4
+
5
+ /**
6
+ * Middleware that intercepts HTML responses from the proxy
7
+ * and injects the vibe-feedback SDK script before </body>.
8
+ */
9
+ export function createHtmlInjector(proxyRes: IncomingMessage, res: ServerResponse) {
10
+ const contentType = proxyRes.headers['content-type'] || '';
11
+ if (!contentType.includes('text/html')) {
12
+ return null; // Not HTML, don't intercept
13
+ }
14
+
15
+ // Copy all response headers except content-length (we'll modify the body)
16
+ res.writeHead(proxyRes.statusCode || 200, Object.fromEntries(
17
+ Object.entries(proxyRes.headers).filter(([key]) => key !== 'content-length')
18
+ ));
19
+
20
+ const chunks: Buffer[] = [];
21
+
22
+ return {
23
+ onData(chunk: Buffer) {
24
+ chunks.push(chunk);
25
+ },
26
+ onEnd() {
27
+ let html = Buffer.concat(chunks).toString('utf-8');
28
+
29
+ // Inject before </body> or at end if no </body>
30
+ if (html.includes('</body>')) {
31
+ html = html.replace('</body>', `${INJECT_SCRIPT}\n</body>`);
32
+ } else if (html.includes('</html>')) {
33
+ html = html.replace('</html>', `${INJECT_SCRIPT}\n</html>`);
34
+ } else {
35
+ html += `\n${INJECT_SCRIPT}`;
36
+ }
37
+
38
+ res.end(html);
39
+ },
40
+ };
41
+ }
@@ -0,0 +1,107 @@
1
+ import express from 'express';
2
+ import cors from 'cors';
3
+ import httpProxy from 'http-proxy';
4
+ import type { Server } from 'http';
5
+ import { FeedbackStore } from '../storage/store.js';
6
+ import { createFeedbackRouter } from './feedback-api.js';
7
+ import { createSdkRouter } from './sdk-serve.js';
8
+ import { createHtmlInjector } from './html-injector.js';
9
+
10
+ const PROXY_PORT = 3695;
11
+
12
+ let server: Server | null = null;
13
+ let proxy: httpProxy | null = null;
14
+
15
+ export async function startProxyServer(
16
+ targetPort: number,
17
+ store: FeedbackStore
18
+ ): Promise<{ proxyUrl: string; targetUrl: string }> {
19
+ if (server) {
20
+ throw new Error('Proxy server is already running');
21
+ }
22
+
23
+ const targetUrl = `http://localhost:${targetPort}`;
24
+ const proxyUrl = `http://localhost:${PROXY_PORT}`;
25
+
26
+ const app = express();
27
+
28
+ // CORS for SDK to submit feedback
29
+ app.use(cors());
30
+
31
+ // Parse JSON for feedback API
32
+ app.use(express.json({ limit: '10mb' }));
33
+
34
+ // Serve SDK JS file
35
+ app.use(createSdkRouter());
36
+
37
+ // Feedback API routes
38
+ app.use(createFeedbackRouter(store));
39
+
40
+ // Create proxy
41
+ proxy = httpProxy.createProxyServer({
42
+ target: targetUrl,
43
+ ws: true, // WebSocket support for HMR
44
+ changeOrigin: true,
45
+ });
46
+
47
+ proxy.on('error', (_err, _req, res) => {
48
+ if (res && 'writeHead' in res) {
49
+ (res as any).writeHead?.(502, { 'Content-Type': 'text/plain' });
50
+ (res as any).end?.('Dev server not responding');
51
+ }
52
+ });
53
+
54
+ // Intercept proxy responses to inject SDK into HTML
55
+ proxy.on('proxyRes', (proxyRes, _req, res) => {
56
+ const injector = createHtmlInjector(proxyRes, res as any);
57
+ if (injector) {
58
+ // HTML response — buffer and inject
59
+ proxyRes.on('data', (chunk) => injector.onData(chunk));
60
+ proxyRes.on('end', () => injector.onEnd());
61
+ } else {
62
+ // Non-HTML — pipe through directly
63
+ proxyRes.pipe(res as any);
64
+ }
65
+ });
66
+
67
+ // All other requests → proxy to dev server
68
+ app.all('*', (req, res) => {
69
+ proxy!.web(req, res, {
70
+ selfHandleResponse: true, // We handle response in proxyRes event
71
+ });
72
+ });
73
+
74
+ // Start server
75
+ await new Promise<void>((resolve, reject) => {
76
+ server = app.listen(PROXY_PORT, () => resolve());
77
+ server!.on('error', (err: NodeJS.ErrnoException) => {
78
+ if (err.code === 'EADDRINUSE') {
79
+ reject(new Error(`Port ${PROXY_PORT} is already in use. Stop the other process or restart.`));
80
+ } else {
81
+ reject(err);
82
+ }
83
+ });
84
+ });
85
+
86
+ // Handle WebSocket upgrade for HMR
87
+ server!.on('upgrade', (req: any, socket: any, head: any) => {
88
+ proxy!.ws(req, socket, head);
89
+ });
90
+
91
+ return { proxyUrl, targetUrl };
92
+ }
93
+
94
+ export function stopProxyServer(): void {
95
+ if (proxy) {
96
+ proxy.close();
97
+ proxy = null;
98
+ }
99
+ if (server) {
100
+ server.close();
101
+ server = null;
102
+ }
103
+ }
104
+
105
+ export function isProxyRunning(): boolean {
106
+ return server !== null;
107
+ }
@@ -0,0 +1,24 @@
1
+ import { Router } from 'express';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+
6
+ export function createSdkRouter(): Router {
7
+ const router = Router();
8
+
9
+ router.get('/vibe-feedback.js', (_req, res) => {
10
+ // Resolve SDK path relative to this file's location
11
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
+ const sdkPath = path.resolve(__dirname, '../../dist/vibe-feedback.js');
13
+
14
+ if (!fs.existsSync(sdkPath)) {
15
+ return res.status(404).send('// SDK not built yet');
16
+ }
17
+
18
+ res.setHeader('Content-Type', 'application/javascript');
19
+ res.setHeader('Cache-Control', 'no-cache');
20
+ fs.createReadStream(sdkPath).pipe(res);
21
+ });
22
+
23
+ return router;
24
+ }
@@ -0,0 +1,172 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { v4 as uuidv4 } from 'uuid';
4
+ import type { FeedbackItem } from './types.js';
5
+
6
+ const ID_PATTERN = /^[a-f0-9\-]{8,36}$/;
7
+
8
+ function validateId(id: string): boolean {
9
+ return ID_PATTERN.test(id) && !id.includes('.') && !id.includes('/') && !id.includes('\\');
10
+ }
11
+
12
+ const STORAGE_DIR = path.join(
13
+ process.env.HOME || process.env.USERPROFILE || '.',
14
+ '.yo-bug',
15
+ 'feedback'
16
+ );
17
+
18
+ export class FeedbackStore {
19
+ private indexPath: string;
20
+
21
+ constructor() {
22
+ this.indexPath = path.join(STORAGE_DIR, 'index.json');
23
+ }
24
+
25
+ async init(): Promise<void> {
26
+ await fs.mkdir(STORAGE_DIR, { recursive: true });
27
+ try {
28
+ await fs.access(this.indexPath);
29
+ } catch {
30
+ await fs.writeFile(this.indexPath, '[]', 'utf-8');
31
+ }
32
+ }
33
+
34
+ async create(
35
+ data: Omit<FeedbackItem, 'id' | 'createdAt' | 'status'>,
36
+ screenshot?: Buffer
37
+ ): Promise<FeedbackItem> {
38
+ const id = uuidv4().slice(0, 12);
39
+ const item: FeedbackItem = {
40
+ ...data,
41
+ id,
42
+ createdAt: new Date().toISOString(),
43
+ status: 'open',
44
+ hasScreenshot: !!screenshot,
45
+ };
46
+
47
+ // Write feedback JSON
48
+ await fs.writeFile(
49
+ path.join(STORAGE_DIR, `${id}.json`),
50
+ JSON.stringify(item, null, 2),
51
+ 'utf-8'
52
+ );
53
+
54
+ // Write screenshot if provided
55
+ if (screenshot) {
56
+ await fs.writeFile(path.join(STORAGE_DIR, `${id}.png`), screenshot);
57
+ }
58
+
59
+ // Update index (append-only line to avoid race conditions)
60
+ const indexEntry = JSON.stringify({
61
+ id: item.id,
62
+ mode: item.mode,
63
+ problemType: item.problemType,
64
+ description: item.description,
65
+ pageUrl: item.pageUrl,
66
+ status: item.status,
67
+ createdAt: item.createdAt,
68
+ hasScreenshot: item.hasScreenshot,
69
+ });
70
+ await fs.appendFile(
71
+ path.join(STORAGE_DIR, 'index.ndjson'),
72
+ indexEntry + '\n',
73
+ 'utf-8'
74
+ );
75
+ // Also update the JSON index for backward compat
76
+ const index = await this.readIndex();
77
+ index.push(JSON.parse(indexEntry));
78
+ await fs.writeFile(this.indexPath, JSON.stringify(index, null, 2), 'utf-8');
79
+
80
+ return item;
81
+ }
82
+
83
+ async list(options?: {
84
+ limit?: number;
85
+ status?: string;
86
+ type?: string;
87
+ }): Promise<{ items: FeedbackItem[]; total: number }> {
88
+ const index = await this.readIndex();
89
+ let filtered = index;
90
+
91
+ if (options?.status && options.status !== 'all') {
92
+ filtered = filtered.filter((i: any) => i.status === options.status);
93
+ }
94
+ if (options?.type) {
95
+ filtered = filtered.filter((i: any) => i.problemType === options.type);
96
+ }
97
+
98
+ const total = filtered.length;
99
+ const limit = options?.limit || 20;
100
+ const items = filtered.slice(-limit).reverse(); // Most recent first
101
+
102
+ // Load full items
103
+ const fullItems: FeedbackItem[] = [];
104
+ for (const entry of items) {
105
+ const item = await this.getById(entry.id);
106
+ if (item) fullItems.push(item);
107
+ }
108
+
109
+ return { items: fullItems, total };
110
+ }
111
+
112
+ async getById(id: string): Promise<FeedbackItem | null> {
113
+ if (!validateId(id)) return null;
114
+ try {
115
+ const data = await fs.readFile(
116
+ path.join(STORAGE_DIR, `${id}.json`),
117
+ 'utf-8'
118
+ );
119
+ return JSON.parse(data);
120
+ } catch {
121
+ return null;
122
+ }
123
+ }
124
+
125
+ async getScreenshot(id: string): Promise<Buffer | null> {
126
+ if (!validateId(id)) return null;
127
+ try {
128
+ return await fs.readFile(path.join(STORAGE_DIR, `${id}.png`));
129
+ } catch {
130
+ return null;
131
+ }
132
+ }
133
+
134
+ async updateStatus(id: string, status: 'open' | 'verify' | 'resolved'): Promise<void> {
135
+ if (!validateId(id)) return;
136
+ const item = await this.getById(id);
137
+ if (!item) return;
138
+
139
+ item.status = status;
140
+ await fs.writeFile(
141
+ path.join(STORAGE_DIR, `${id}.json`),
142
+ JSON.stringify(item, null, 2),
143
+ 'utf-8'
144
+ );
145
+
146
+ // Update index
147
+ const index = await this.readIndex();
148
+ const entry = index.find((i: any) => i.id === id);
149
+ if (entry) {
150
+ entry.status = status;
151
+ await fs.writeFile(this.indexPath, JSON.stringify(index, null, 2), 'utf-8');
152
+ }
153
+ }
154
+
155
+ async clear(): Promise<void> {
156
+ const index = await this.readIndex();
157
+ for (const entry of index) {
158
+ try { await fs.unlink(path.join(STORAGE_DIR, `${entry.id}.json`)); } catch {}
159
+ try { await fs.unlink(path.join(STORAGE_DIR, `${entry.id}.png`)); } catch {}
160
+ }
161
+ await fs.writeFile(this.indexPath, '[]', 'utf-8');
162
+ }
163
+
164
+ private async readIndex(): Promise<any[]> {
165
+ try {
166
+ const data = await fs.readFile(this.indexPath, 'utf-8');
167
+ return JSON.parse(data);
168
+ } catch {
169
+ return [];
170
+ }
171
+ }
172
+ }
@@ -0,0 +1,68 @@
1
+ export interface ElementInfo {
2
+ selector: string;
3
+ tagName: string;
4
+ textContent: string;
5
+ rect: { x: number; y: number; width: number; height: number };
6
+ computedStyles: Record<string, string>;
7
+ attributes: Record<string, string>;
8
+ componentName: string | null;
9
+ componentFile: string | null;
10
+ }
11
+
12
+ export interface ConsoleEntry {
13
+ type: 'error' | 'warn';
14
+ message: string;
15
+ stack?: string;
16
+ timestamp: number;
17
+ }
18
+
19
+ export interface NetworkEntry {
20
+ type: 'fetch' | 'xhr';
21
+ method: string;
22
+ url: string;
23
+ status: number;
24
+ statusText: string;
25
+ duration: number;
26
+ timestamp: number;
27
+ error?: string;
28
+ }
29
+
30
+ export interface ErrorEntry {
31
+ type: 'unhandled-error' | 'unhandled-rejection';
32
+ message: string;
33
+ stack?: string;
34
+ timestamp: number;
35
+ }
36
+
37
+ export interface ActionStep {
38
+ type: 'click' | 'input' | 'navigate' | 'scroll' | 'keypress';
39
+ timestamp: number;
40
+ target?: string;
41
+ tagName?: string;
42
+ text?: string;
43
+ url?: string;
44
+ position?: { x: number; y: number };
45
+ elapsed?: number;
46
+ }
47
+
48
+ export type ProblemType = 'bug' | 'ui-issue' | 'performance' | 'feature-request' | 'other';
49
+
50
+ export interface FeedbackItem {
51
+ id: string;
52
+ mode: 'element' | 'annotation';
53
+ problemType: ProblemType;
54
+ description: string;
55
+ element?: ElementInfo;
56
+ hasScreenshot: boolean;
57
+ pageUrl: string;
58
+ pageTitle: string;
59
+ viewport: { width: number; height: number };
60
+ userAgent: string;
61
+ timestamp: number;
62
+ createdAt: string;
63
+ consoleErrors: ConsoleEntry[];
64
+ networkErrors: NetworkEntry[];
65
+ unhandledErrors: ErrorEntry[];
66
+ actionSteps: ActionStep[];
67
+ status: 'open' | 'verify' | 'resolved';
68
+ }