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,23 @@
1
+ import { BaseTool } from './base-tool.js';
2
+
3
+ export class FreehandTool extends BaseTool {
4
+ onMouseDown(x: number, y: number): void {
5
+ this.drawing = true;
6
+ this.ctx.beginPath();
7
+ this.ctx.moveTo(x, y);
8
+ this.ctx.strokeStyle = this.color;
9
+ this.ctx.lineWidth = this.lineWidth;
10
+ this.ctx.lineCap = 'round';
11
+ this.ctx.lineJoin = 'round';
12
+ }
13
+
14
+ onMouseMove(x: number, y: number): void {
15
+ if (!this.drawing) return;
16
+ this.ctx.lineTo(x, y);
17
+ this.ctx.stroke();
18
+ }
19
+
20
+ onMouseUp(_x: number, _y: number): void {
21
+ this.drawing = false;
22
+ }
23
+ }
@@ -0,0 +1,32 @@
1
+ import { BaseTool } from './base-tool.js';
2
+
3
+ export class RectTool extends BaseTool {
4
+ private startX = 0;
5
+ private startY = 0;
6
+ private snapshot: ImageData | null = null;
7
+
8
+ onMouseDown(x: number, y: number): void {
9
+ this.drawing = true;
10
+ this.startX = x;
11
+ this.startY = y;
12
+ this.snapshot = this.ctx.getImageData(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
13
+ }
14
+
15
+ onMouseMove(x: number, y: number): void {
16
+ if (!this.drawing || !this.snapshot) return;
17
+ this.ctx.putImageData(this.snapshot, 0, 0);
18
+ this.ctx.strokeStyle = this.color;
19
+ this.ctx.lineWidth = this.lineWidth;
20
+ this.ctx.strokeRect(
21
+ this.startX, this.startY,
22
+ x - this.startX, y - this.startY
23
+ );
24
+ }
25
+
26
+ onMouseUp(x: number, y: number): void {
27
+ if (!this.drawing) return;
28
+ this.onMouseMove(x, y);
29
+ this.drawing = false;
30
+ this.snapshot = null;
31
+ }
32
+ }
@@ -0,0 +1,93 @@
1
+ import { BaseTool } from './base-tool.js';
2
+
3
+ export class TextTool extends BaseTool {
4
+ private input: HTMLInputElement | null = null;
5
+ private container: HTMLElement;
6
+
7
+ constructor(
8
+ ctx: CanvasRenderingContext2D,
9
+ color: string,
10
+ lineWidth: number,
11
+ container: HTMLElement
12
+ ) {
13
+ super(ctx, color, lineWidth);
14
+ this.container = container;
15
+ }
16
+
17
+ onMouseDown(x: number, y: number, cssX?: number, cssY?: number): void {
18
+ this.removeInput();
19
+
20
+ // Use CSS coordinates for input positioning, canvas coordinates for drawing
21
+ const posX = cssX ?? x;
22
+ const posY = cssY ?? y;
23
+
24
+ this.input = document.createElement('input');
25
+ this.input.type = 'text';
26
+ this.input.placeholder = 'Type here...';
27
+ this.input.style.cssText = `
28
+ position: absolute;
29
+ left: ${posX}px;
30
+ top: ${posY - 12}px;
31
+ min-width: 120px;
32
+ background: rgba(0,0,0,0.8);
33
+ border: 2px solid ${this.color};
34
+ color: ${this.color};
35
+ font-size: 16px;
36
+ padding: 4px 8px;
37
+ border-radius: 4px;
38
+ outline: none;
39
+ z-index: 10;
40
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
41
+ `;
42
+
43
+ // Prevent canvas editor from receiving keyboard events
44
+ this.input.addEventListener('keydown', (e) => {
45
+ e.stopPropagation();
46
+ if (e.key === 'Enter') {
47
+ this.commitText(x, y);
48
+ } else if (e.key === 'Escape') {
49
+ this.removeInput();
50
+ }
51
+ });
52
+
53
+ this.input.addEventListener('blur', () => {
54
+ this.commitText(x, y);
55
+ });
56
+
57
+ this.container.appendChild(this.input);
58
+ // Focus after a tick to ensure it's in the DOM
59
+ requestAnimationFrame(() => this.input?.focus());
60
+ }
61
+
62
+ onMouseMove(): void {}
63
+ onMouseUp(): void {}
64
+
65
+ private commitText(canvasX: number, canvasY: number): void {
66
+ if (!this.input) return;
67
+ const text = this.input.value.trim();
68
+ if (text) {
69
+ const fontSize = Math.max(18, this.lineWidth * 6);
70
+ this.ctx.font = `bold ${fontSize}px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif`;
71
+ this.ctx.fillStyle = this.color;
72
+ // Draw shadow for readability
73
+ this.ctx.shadowColor = 'rgba(0,0,0,0.5)';
74
+ this.ctx.shadowBlur = 3;
75
+ this.ctx.shadowOffsetX = 1;
76
+ this.ctx.shadowOffsetY = 1;
77
+ this.ctx.fillText(text, canvasX, canvasY);
78
+ // Reset shadow
79
+ this.ctx.shadowColor = 'transparent';
80
+ this.ctx.shadowBlur = 0;
81
+ this.ctx.shadowOffsetX = 0;
82
+ this.ctx.shadowOffsetY = 0;
83
+ }
84
+ this.removeInput();
85
+ }
86
+
87
+ private removeInput(): void {
88
+ if (this.input) {
89
+ this.input.remove();
90
+ this.input = null;
91
+ }
92
+ }
93
+ }
@@ -0,0 +1,48 @@
1
+ export interface FeedbackPayload {
2
+ mode: 'element' | 'annotation';
3
+ problemType: string;
4
+ description: string;
5
+ element?: any;
6
+ pageUrl: string;
7
+ pageTitle: string;
8
+ viewport: { width: number; height: number };
9
+ userAgent: string;
10
+ timestamp: number;
11
+ consoleErrors: any[];
12
+ networkErrors: any[];
13
+ unhandledErrors: any[];
14
+ actionSteps?: any[];
15
+ }
16
+
17
+ export class ApiClient {
18
+ private baseUrl: string;
19
+
20
+ constructor() {
21
+ // SDK is served from the proxy, so use same origin
22
+ this.baseUrl = window.location.origin;
23
+ }
24
+
25
+ async submitFeedback(
26
+ payload: FeedbackPayload,
27
+ screenshotDataUrl?: string
28
+ ): Promise<{ id: string }> {
29
+ const formData = new FormData();
30
+ formData.append('feedback', JSON.stringify(payload));
31
+
32
+ if (screenshotDataUrl) {
33
+ const blob = await (await fetch(screenshotDataUrl)).blob();
34
+ formData.append('screenshot', blob, 'annotation.png');
35
+ }
36
+
37
+ const res = await fetch(`${this.baseUrl}/api/feedback`, {
38
+ method: 'POST',
39
+ body: formData,
40
+ });
41
+
42
+ if (!res.ok) {
43
+ throw new Error(`Submit failed: ${res.status}`);
44
+ }
45
+ return res.json();
46
+ }
47
+
48
+ }
@@ -0,0 +1,157 @@
1
+ export interface ActionStep {
2
+ type: 'click' | 'input' | 'navigate' | 'scroll' | 'keypress';
3
+ timestamp: number;
4
+ target?: string; // CSS selector
5
+ tagName?: string;
6
+ text?: string; // button text / input value / key
7
+ url?: string; // for navigate
8
+ position?: { x: number; y: number };
9
+ elapsed?: number; // ms since last step
10
+ }
11
+
12
+ export class ActionRecorder {
13
+ private steps: ActionStep[] = [];
14
+ private recording = false;
15
+ private lastTimestamp = 0;
16
+ private maxSteps = 100;
17
+
18
+ // Bound handlers
19
+ private clickHandler: (e: MouseEvent) => void;
20
+ private inputHandler: (e: Event) => void;
21
+ private keyHandler: (e: KeyboardEvent) => void;
22
+ private navHandler: () => void;
23
+
24
+ constructor() {
25
+ this.clickHandler = (e) => this.onClick(e);
26
+ this.inputHandler = (e) => this.onInput(e);
27
+ this.keyHandler = (e) => this.onKeypress(e);
28
+ this.navHandler = () => this.onNavigate();
29
+ }
30
+
31
+ start(): void {
32
+ if (this.recording) return;
33
+ this.recording = true;
34
+ this.steps = [];
35
+ this.lastTimestamp = Date.now();
36
+
37
+ document.addEventListener('click', this.clickHandler, true);
38
+ document.addEventListener('change', this.inputHandler, true);
39
+ document.addEventListener('keydown', this.keyHandler, true);
40
+ window.addEventListener('popstate', this.navHandler);
41
+ window.addEventListener('hashchange', this.navHandler);
42
+ }
43
+
44
+ stop(): ActionStep[] {
45
+ this.recording = false;
46
+ document.removeEventListener('click', this.clickHandler, true);
47
+ document.removeEventListener('change', this.inputHandler, true);
48
+ document.removeEventListener('keydown', this.keyHandler, true);
49
+ window.removeEventListener('popstate', this.navHandler);
50
+ window.removeEventListener('hashchange', this.navHandler);
51
+ return this.getSteps();
52
+ }
53
+
54
+ getSteps(): ActionStep[] {
55
+ return [...this.steps];
56
+ }
57
+
58
+ isRecording(): boolean {
59
+ return this.recording;
60
+ }
61
+
62
+ private addStep(step: ActionStep): void {
63
+ const now = Date.now();
64
+ step.elapsed = now - this.lastTimestamp;
65
+ this.lastTimestamp = now;
66
+ this.steps.push(step);
67
+ if (this.steps.length > this.maxSteps) this.steps.shift();
68
+ }
69
+
70
+ private onClick(e: MouseEvent): void {
71
+ const el = e.target as HTMLElement;
72
+ if (!el || this.isSDKElement(el)) return;
73
+
74
+ this.addStep({
75
+ type: 'click',
76
+ timestamp: Date.now(),
77
+ target: briefSelector(el),
78
+ tagName: el.tagName.toLowerCase(),
79
+ text: getElementText(el),
80
+ position: { x: Math.round(e.clientX), y: Math.round(e.clientY) },
81
+ });
82
+ }
83
+
84
+ private onInput(e: Event): void {
85
+ const el = e.target as HTMLInputElement;
86
+ if (!el || this.isSDKElement(el)) return;
87
+
88
+ const value = el.type === 'password' ? '••••' : (el.value || '').slice(0, 50);
89
+ this.addStep({
90
+ type: 'input',
91
+ timestamp: Date.now(),
92
+ target: briefSelector(el),
93
+ tagName: el.tagName.toLowerCase(),
94
+ text: value,
95
+ });
96
+ }
97
+
98
+ private onKeypress(e: KeyboardEvent): void {
99
+ // Only record special keys, not every character
100
+ if (!['Enter', 'Escape', 'Tab', 'Backspace', 'Delete'].includes(e.key) &&
101
+ !e.ctrlKey && !e.metaKey && !e.altKey) return;
102
+
103
+ const el = e.target as HTMLElement;
104
+ if (this.isSDKElement(el)) return;
105
+
106
+ const combo = [
107
+ e.ctrlKey ? 'Ctrl' : '',
108
+ e.metaKey ? 'Cmd' : '',
109
+ e.altKey ? 'Alt' : '',
110
+ e.shiftKey ? 'Shift' : '',
111
+ e.key,
112
+ ].filter(Boolean).join('+');
113
+
114
+ this.addStep({
115
+ type: 'keypress',
116
+ timestamp: Date.now(),
117
+ target: briefSelector(el),
118
+ text: combo,
119
+ });
120
+ }
121
+
122
+ private onNavigate(): void {
123
+ this.addStep({
124
+ type: 'navigate',
125
+ timestamp: Date.now(),
126
+ url: window.location.href,
127
+ });
128
+ }
129
+
130
+ private isSDKElement(el: Element): boolean {
131
+ let current: Element | null = el;
132
+ while (current) {
133
+ if (current.id === 'vibe-feedback-host') return true;
134
+ current = current.parentElement;
135
+ }
136
+ return false;
137
+ }
138
+ }
139
+
140
+ function briefSelector(el: Element): string {
141
+ if (el.id) return `#${el.id}`;
142
+ const testId = el.getAttribute('data-testid');
143
+ if (testId) return `[data-testid="${testId}"]`;
144
+ const cls = el.className && typeof el.className === 'string'
145
+ ? '.' + el.className.trim().split(/\s+/).slice(0, 2).join('.')
146
+ : '';
147
+ return `${el.tagName.toLowerCase()}${cls}`;
148
+ }
149
+
150
+ function getElementText(el: HTMLElement): string {
151
+ // Get meaningful text: button label, link text, placeholder, etc.
152
+ const text = el.textContent?.trim() ||
153
+ el.getAttribute('aria-label') ||
154
+ el.getAttribute('placeholder') ||
155
+ el.getAttribute('title') || '';
156
+ return text.slice(0, 60);
157
+ }
@@ -0,0 +1,65 @@
1
+ import { ContextBuffer } from './context-buffer.js';
2
+
3
+ export interface ConsoleEntry {
4
+ type: 'error' | 'warn';
5
+ message: string;
6
+ stack?: string;
7
+ timestamp: number;
8
+ }
9
+
10
+ export class ConsoleInterceptor {
11
+ private origError!: typeof console.error;
12
+ private origWarn!: typeof console.warn;
13
+ private buffer: ContextBuffer<ConsoleEntry>;
14
+ private installed = false;
15
+
16
+ constructor(buffer: ContextBuffer<ConsoleEntry>) {
17
+ this.buffer = buffer;
18
+ }
19
+
20
+ install(): void {
21
+ if (this.installed) return;
22
+ this.installed = true;
23
+
24
+ this.origError = console.error;
25
+ this.origWarn = console.warn;
26
+
27
+ console.error = (...args: any[]) => {
28
+ this.buffer.push({
29
+ type: 'error',
30
+ message: args.map(serialize).join(' '),
31
+ stack: new Error().stack,
32
+ timestamp: Date.now(),
33
+ });
34
+ this.origError.apply(console, args);
35
+ };
36
+
37
+ console.warn = (...args: any[]) => {
38
+ this.buffer.push({
39
+ type: 'warn',
40
+ message: args.map(serialize).join(' '),
41
+ timestamp: Date.now(),
42
+ });
43
+ this.origWarn.apply(console, args);
44
+ };
45
+ }
46
+
47
+ uninstall(): void {
48
+ if (!this.installed) return;
49
+ console.error = this.origError;
50
+ console.warn = this.origWarn;
51
+ this.installed = false;
52
+ }
53
+ }
54
+
55
+ function serialize(arg: any): string {
56
+ if (arg instanceof Error) return `${arg.name}: ${arg.message}\n${arg.stack}`;
57
+ if (typeof arg === 'object') {
58
+ try {
59
+ return JSON.stringify(arg, null, 0).slice(0, 500);
60
+ } catch {
61
+ return String(arg);
62
+ }
63
+ }
64
+ return String(arg);
65
+ }
@@ -0,0 +1,23 @@
1
+ export class ContextBuffer<T> {
2
+ private items: T[] = [];
3
+ private maxSize: number;
4
+
5
+ constructor(maxSize = 50) {
6
+ this.maxSize = maxSize;
7
+ }
8
+
9
+ push(item: T): void {
10
+ this.items.push(item);
11
+ if (this.items.length > this.maxSize) {
12
+ this.items.shift();
13
+ }
14
+ }
15
+
16
+ getAll(): T[] {
17
+ return [...this.items];
18
+ }
19
+
20
+ clear(): void {
21
+ this.items = [];
22
+ }
23
+ }
@@ -0,0 +1,52 @@
1
+ import { ContextBuffer } from './context-buffer.js';
2
+
3
+ export interface ErrorEntry {
4
+ type: 'unhandled-error' | 'unhandled-rejection';
5
+ message: string;
6
+ stack?: string;
7
+ timestamp: number;
8
+ }
9
+
10
+ export class ErrorInterceptor {
11
+ private buffer: ContextBuffer<ErrorEntry>;
12
+ private errorHandler!: (e: ErrorEvent) => void;
13
+ private rejectionHandler!: (e: PromiseRejectionEvent) => void;
14
+ private installed = false;
15
+
16
+ constructor(buffer: ContextBuffer<ErrorEntry>) {
17
+ this.buffer = buffer;
18
+ }
19
+
20
+ install(): void {
21
+ if (this.installed) return;
22
+ this.installed = true;
23
+
24
+ this.errorHandler = (e: ErrorEvent) => {
25
+ this.buffer.push({
26
+ type: 'unhandled-error',
27
+ message: e.message,
28
+ stack: e.error?.stack,
29
+ timestamp: Date.now(),
30
+ });
31
+ };
32
+
33
+ this.rejectionHandler = (e: PromiseRejectionEvent) => {
34
+ this.buffer.push({
35
+ type: 'unhandled-rejection',
36
+ message: String(e.reason),
37
+ stack: e.reason?.stack,
38
+ timestamp: Date.now(),
39
+ });
40
+ };
41
+
42
+ window.addEventListener('error', this.errorHandler);
43
+ window.addEventListener('unhandledrejection', this.rejectionHandler);
44
+ }
45
+
46
+ uninstall(): void {
47
+ if (!this.installed) return;
48
+ window.removeEventListener('error', this.errorHandler);
49
+ window.removeEventListener('unhandledrejection', this.rejectionHandler);
50
+ this.installed = false;
51
+ }
52
+ }
@@ -0,0 +1,143 @@
1
+ import { ContextBuffer } from './context-buffer.js';
2
+
3
+ export interface NetworkEntry {
4
+ type: 'fetch' | 'xhr';
5
+ method: string;
6
+ url: string;
7
+ status: number;
8
+ statusText: string;
9
+ duration: number;
10
+ timestamp: number;
11
+ error?: string;
12
+ }
13
+
14
+ /** Check if a URL belongs to the vibe-feedback SDK itself */
15
+ function isVibeFeedbackUrl(url: string): boolean {
16
+ try {
17
+ const u = new URL(url, window.location.origin);
18
+ return u.pathname === '/api/feedback' ||
19
+ u.pathname.startsWith('/api/feedback/') ||
20
+ u.pathname === '/api/checklist' ||
21
+ u.pathname.startsWith('/api/checklist/') ||
22
+ u.pathname === '/api/inspection' ||
23
+ u.pathname === '/vibe-feedback.js';
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
28
+
29
+ export class NetworkInterceptor {
30
+ private origFetch!: typeof window.fetch;
31
+ private origXHROpen!: typeof XMLHttpRequest.prototype.open;
32
+ private origXHRSend!: typeof XMLHttpRequest.prototype.send;
33
+ private buffer: ContextBuffer<NetworkEntry>;
34
+ private installed = false;
35
+
36
+ constructor(buffer: ContextBuffer<NetworkEntry>) {
37
+ this.buffer = buffer;
38
+ }
39
+
40
+ install(): void {
41
+ if (this.installed) return;
42
+ this.installed = true;
43
+ this.patchFetch();
44
+ this.patchXHR();
45
+ }
46
+
47
+ uninstall(): void {
48
+ if (!this.installed) return;
49
+ window.fetch = this.origFetch;
50
+ XMLHttpRequest.prototype.open = this.origXHROpen;
51
+ XMLHttpRequest.prototype.send = this.origXHRSend;
52
+ this.installed = false;
53
+ }
54
+
55
+ private patchFetch(): void {
56
+ this.origFetch = window.fetch;
57
+ const buffer = this.buffer;
58
+ const origFetch = this.origFetch;
59
+
60
+ window.fetch = async function (input, init?) {
61
+ const url =
62
+ typeof input === 'string'
63
+ ? input
64
+ : input instanceof URL
65
+ ? input.href
66
+ : input instanceof Request
67
+ ? input.url
68
+ : String(input);
69
+ const method = init?.method || 'GET';
70
+
71
+ // Skip our own requests
72
+ if (isVibeFeedbackUrl(url)) {
73
+ return origFetch.call(window, input, init);
74
+ }
75
+
76
+ const start = Date.now();
77
+ try {
78
+ const response = await origFetch.call(window, input, init);
79
+ if (!response.ok) {
80
+ buffer.push({
81
+ type: 'fetch',
82
+ method,
83
+ url: url.slice(0, 200),
84
+ status: response.status,
85
+ statusText: response.statusText,
86
+ duration: Date.now() - start,
87
+ timestamp: Date.now(),
88
+ });
89
+ }
90
+ return response;
91
+ } catch (err) {
92
+ buffer.push({
93
+ type: 'fetch',
94
+ method,
95
+ url: url.slice(0, 200),
96
+ status: 0,
97
+ statusText: 'Network Error',
98
+ duration: Date.now() - start,
99
+ timestamp: Date.now(),
100
+ error: String(err),
101
+ });
102
+ throw err;
103
+ }
104
+ };
105
+ }
106
+
107
+ private patchXHR(): void {
108
+ this.origXHROpen = XMLHttpRequest.prototype.open;
109
+ this.origXHRSend = XMLHttpRequest.prototype.send;
110
+ const buffer = this.buffer;
111
+ const origOpen = this.origXHROpen;
112
+ const origSend = this.origXHRSend;
113
+
114
+ XMLHttpRequest.prototype.open = function (method: string, url: string | URL, ...rest: any[]) {
115
+ (this as any).__vf_method = method;
116
+ (this as any).__vf_url = typeof url === 'string' ? url : url.href;
117
+ return origOpen.apply(this, [method, url, ...rest] as any);
118
+ };
119
+
120
+ XMLHttpRequest.prototype.send = function (body?: any) {
121
+ const url = (this as any).__vf_url || '';
122
+ if (isVibeFeedbackUrl(url)) {
123
+ return origSend.call(this, body);
124
+ }
125
+
126
+ const start = Date.now();
127
+ this.addEventListener('loadend', () => {
128
+ if (this.status >= 400 || this.status === 0) {
129
+ buffer.push({
130
+ type: 'xhr',
131
+ method: (this as any).__vf_method || 'GET',
132
+ url: url.slice(0, 200),
133
+ status: this.status,
134
+ statusText: this.statusText,
135
+ duration: Date.now() - start,
136
+ timestamp: Date.now(),
137
+ });
138
+ }
139
+ });
140
+ return origSend.call(this, body);
141
+ };
142
+ }
143
+ }
@@ -0,0 +1,20 @@
1
+ type Handler = (...args: any[]) => void;
2
+
3
+ export class EventBus {
4
+ private handlers = new Map<string, Set<Handler>>();
5
+
6
+ on(event: string, handler: Handler): void {
7
+ if (!this.handlers.has(event)) {
8
+ this.handlers.set(event, new Set());
9
+ }
10
+ this.handlers.get(event)!.add(handler);
11
+ }
12
+
13
+ off(event: string, handler: Handler): void {
14
+ this.handlers.get(event)?.delete(handler);
15
+ }
16
+
17
+ emit(event: string, ...args: any[]): void {
18
+ this.handlers.get(event)?.forEach((h) => h(...args));
19
+ }
20
+ }