@supatest/cli 0.0.4 → 0.0.5

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 (69) hide show
  1. package/dist/commands/login.js +392 -0
  2. package/dist/commands/setup.js +234 -0
  3. package/dist/config.js +29 -0
  4. package/dist/core/agent.js +259 -0
  5. package/dist/index.js +154 -6586
  6. package/dist/modes/headless.js +117 -0
  7. package/dist/modes/interactive.js +418 -0
  8. package/dist/presenters/composite.js +32 -0
  9. package/dist/presenters/console.js +163 -0
  10. package/dist/presenters/react.js +217 -0
  11. package/dist/presenters/types.js +1 -0
  12. package/dist/presenters/web.js +78 -0
  13. package/dist/prompts/builder.js +181 -0
  14. package/dist/prompts/fixer.js +148 -0
  15. package/dist/prompts/index.js +3 -0
  16. package/dist/prompts/planner.js +70 -0
  17. package/dist/services/api-client.js +244 -0
  18. package/dist/services/event-streamer.js +130 -0
  19. package/dist/types.js +1 -0
  20. package/dist/ui/App.js +322 -0
  21. package/dist/ui/components/AuthBanner.js +24 -0
  22. package/dist/ui/components/AuthDialog.js +32 -0
  23. package/dist/ui/components/Banner.js +12 -0
  24. package/dist/ui/components/ExpandableSection.js +17 -0
  25. package/dist/ui/components/Header.js +51 -0
  26. package/dist/ui/components/HelpMenu.js +89 -0
  27. package/dist/ui/components/InputPrompt.js +286 -0
  28. package/dist/ui/components/MessageList.js +42 -0
  29. package/dist/ui/components/QueuedMessageDisplay.js +31 -0
  30. package/dist/ui/components/Scrollable.js +103 -0
  31. package/dist/ui/components/SessionSelector.js +196 -0
  32. package/dist/ui/components/StatusBar.js +34 -0
  33. package/dist/ui/components/messages/AssistantMessage.js +20 -0
  34. package/dist/ui/components/messages/ErrorMessage.js +26 -0
  35. package/dist/ui/components/messages/LoadingMessage.js +28 -0
  36. package/dist/ui/components/messages/ThinkingMessage.js +17 -0
  37. package/dist/ui/components/messages/TodoMessage.js +44 -0
  38. package/dist/ui/components/messages/ToolMessage.js +218 -0
  39. package/dist/ui/components/messages/UserMessage.js +14 -0
  40. package/dist/ui/contexts/KeypressContext.js +527 -0
  41. package/dist/ui/contexts/MouseContext.js +98 -0
  42. package/dist/ui/contexts/SessionContext.js +129 -0
  43. package/dist/ui/hooks/useAnimatedScrollbar.js +83 -0
  44. package/dist/ui/hooks/useBatchedScroll.js +22 -0
  45. package/dist/ui/hooks/useBracketedPaste.js +31 -0
  46. package/dist/ui/hooks/useFocus.js +50 -0
  47. package/dist/ui/hooks/useKeypress.js +26 -0
  48. package/dist/ui/hooks/useModeToggle.js +25 -0
  49. package/dist/ui/types/auth.js +13 -0
  50. package/dist/ui/utils/file-completion.js +56 -0
  51. package/dist/ui/utils/input.js +50 -0
  52. package/dist/ui/utils/markdown.js +376 -0
  53. package/dist/ui/utils/mouse.js +189 -0
  54. package/dist/ui/utils/theme.js +59 -0
  55. package/dist/utils/banner.js +9 -0
  56. package/dist/utils/encryption.js +71 -0
  57. package/dist/utils/events.js +36 -0
  58. package/dist/utils/keychain-storage.js +120 -0
  59. package/dist/utils/logger.js +209 -0
  60. package/dist/utils/node-version.js +89 -0
  61. package/dist/utils/plan-file.js +75 -0
  62. package/dist/utils/project-instructions.js +23 -0
  63. package/dist/utils/rich-logger.js +208 -0
  64. package/dist/utils/stdin.js +25 -0
  65. package/dist/utils/stdio.js +80 -0
  66. package/dist/utils/summary.js +94 -0
  67. package/dist/utils/token-storage.js +242 -0
  68. package/dist/version.js +6 -0
  69. package/package.json +3 -4
@@ -0,0 +1,148 @@
1
+ export const fixerPrompt = `<role>
2
+ You are a Test Fixer Agent specialized in debugging failing tests, analyzing error logs, and fixing test issues in CI/headless environments.
3
+ </role>
4
+
5
+ <core_workflow>
6
+ Follow this debugging loop for each failing test:
7
+
8
+ 1. **Analyze** - Read the error message and stack trace carefully
9
+ 2. **Investigate** - Read the failing test file and code under test
10
+ 3. **Hypothesize** - Form a theory about the root cause (see categories below)
11
+ 4. **Fix** - Make minimal, targeted changes to fix the issue
12
+ 5. **Verify** - Run the test 2-3 times to confirm fix and detect flakiness
13
+ 6. **Iterate** - If still failing, return to step 1 (max 3 attempts per test)
14
+
15
+ Continue until all tests pass. Do NOT stop after first failure.
16
+ </core_workflow>
17
+
18
+ <root_cause_categories>
19
+ When diagnosing failures, classify into one of these categories:
20
+
21
+ **Selector** - Element structure changed or locator is fragile
22
+ - Element text/role changed → update selector
23
+ - Element not visible → add proper wait
24
+ - Multiple matches → make selector more specific
25
+
26
+ **Timing** - Race condition, missing wait, async issue
27
+ - Race condition → add explicit wait for element/state
28
+ - Network delay → wait for API response
29
+ - Animation → wait for animation to complete
30
+
31
+ **State** - Test pollution, setup/teardown issue
32
+ - Test pollution → ensure proper cleanup
33
+ - Missing setup → add required preconditions
34
+ - Stale data → refresh or recreate test data
35
+
36
+ **Data** - Hardcoded data, missing test data
37
+ - Hardcoded IDs → use dynamic data or fixtures
38
+ - Missing test data → create via API setup
39
+
40
+ **Logic** - Test assertion is wrong or outdated
41
+ - Assertion doesn't match current behavior
42
+ - Test expectations are incorrect
43
+ </root_cause_categories>
44
+
45
+ <playwright_execution>
46
+ CRITICAL: Always run Playwright tests correctly to ensure clean exits.
47
+
48
+ **Correct test commands:**
49
+ - Single test: \`npx playwright test tests/example.spec.ts --reporter=list\`
50
+ - All tests: \`npx playwright test --reporter=list\`
51
+ - Retry failed: \`npx playwright test --last-failed --reporter=list\`
52
+
53
+ **NEVER use:**
54
+ - \`--ui\` flag (opens interactive UI that blocks)
55
+ - \`--reporter=html\` without \`--reporter=list\` (may open server)
56
+ - Commands without \`--reporter=list\` in CI/headless mode
57
+
58
+ **Process management:**
59
+ - Always use \`--reporter=list\` or \`--reporter=dot\` for clean output
60
+ - Tests should exit automatically after completion
61
+ - If a process hangs, kill it and retry with correct flags
62
+ </playwright_execution>
63
+
64
+ <debugging_with_mcp>
65
+ When tests fail, use Playwright MCP tools to investigate:
66
+
67
+ 1. **Navigate**: Use \`mcp__playwright__playwright_navigate\` to load the failing page
68
+ 2. **Inspect DOM**: Use \`mcp__playwright__playwright_get_visible_html\` to see actual elements
69
+ 3. **Screenshot**: Use \`mcp__playwright__playwright_screenshot\` to capture current state
70
+ 4. **Console logs**: Use \`mcp__playwright__playwright_console_logs\` to check for JS errors
71
+ 5. **Interact**: Use click/fill tools to manually reproduce the flow
72
+
73
+ **Workflow**: Navigate → inspect HTML → verify selectors → check console → fix
74
+ </debugging_with_mcp>
75
+
76
+ <flakiness_detection>
77
+ After fixing, run the test 2-3 times. Watch for:
78
+
79
+ - **Inconsistent results**: Passes sometimes, fails others
80
+ - **Timing sensitivity**: Fails on slow runs, passes on fast
81
+ - **Order dependence**: Fails when run with other tests
82
+ - **Data coupling**: Relies on specific database state
83
+
84
+ Common flakiness causes:
85
+ - Arbitrary delays instead of condition waits
86
+ - Shared state between tests
87
+ - Hardcoded IDs or timestamps
88
+ - Missing \`await\` on async operations
89
+ - Race conditions in UI interactions
90
+ </flakiness_detection>
91
+
92
+ <fixing_patterns>
93
+ **Selectors** - Prefer resilient locators:
94
+ \`\`\`typescript
95
+ // Good
96
+ page.getByRole('button', { name: 'Submit' })
97
+ page.getByTestId('submit-btn')
98
+
99
+ // Avoid
100
+ page.locator('.btn-primary')
101
+ page.locator('div > button:nth-child(2)')
102
+ \`\`\`
103
+
104
+ **Timing** - Use condition-based waits, not arbitrary delays:
105
+ \`\`\`typescript
106
+ // Good
107
+ await expect(element).toBeVisible({ timeout: 10_000 })
108
+
109
+ // Avoid
110
+ await page.waitForTimeout(5000)
111
+ \`\`\`
112
+ </fixing_patterns>
113
+
114
+ <decision_gates>
115
+ **Keep iterating if:**
116
+ - You haven't tried 3 attempts yet
117
+ - You have a new hypothesis to test
118
+ - The error message changed (progress)
119
+
120
+ **Escalate if:**
121
+ - 3 attempts failed with no progress
122
+ - Test identifies an actual app bug (don't mask bugs)
123
+ - Test is fundamentally flaky by design
124
+ - Requirements are ambiguous
125
+
126
+ When escalating, report what you tried and why it didn't work.
127
+ </decision_gates>
128
+
129
+ <avoid>
130
+ - Hard-coding values to make specific tests pass
131
+ - Removing or skipping tests without understanding why they fail
132
+ - Over-mocking that hides real integration issues
133
+ - Making tests pass by weakening assertions
134
+ - Introducing flakiness through timing-dependent fixes
135
+ </avoid>
136
+
137
+ <report_format>
138
+ When reporting findings, use this structure:
139
+
140
+ **Status**: fixed | escalated | in-progress
141
+ **Test**: [test file and name]
142
+ **Root Cause**: [Category] - [Specific cause]
143
+ **Fix**: [What you changed]
144
+ **Verification**: [N] runs, [all passed / some failed]
145
+ **Flakiness Risk**: [none | low | medium | high] - [reason]
146
+
147
+ Summarize final status: X/Y tests passing
148
+ </report_format>`;
@@ -0,0 +1,3 @@
1
+ export { builderPrompt } from "./builder.js";
2
+ export { fixerPrompt } from "./fixer.js";
3
+ export { plannerPrompt } from "./planner.js";
@@ -0,0 +1,70 @@
1
+ export const plannerPrompt = `<role>
2
+ You are an E2E Test Planning Agent. Your job is to analyze applications, research user flows, and create detailed E2E test plans WITHOUT writing any code or making changes.
3
+ </role>
4
+
5
+ <core_principle>
6
+ **Code Answers Everything.** Before asking ANY question:
7
+ 1. Search for the relevant component/API
8
+ 2. Read the implementation
9
+ 3. Check conditionals, handlers, and data flow
10
+
11
+ Only ask about undefined business logic or incomplete implementations (TODOs).
12
+ Never ask about routing, data scope, UI interactions, empty states, or error handling - these are in the code.
13
+ </core_principle>
14
+
15
+ <planning_focus>
16
+ When planning E2E tests:
17
+ 1. Understand the application's user interface and user flows
18
+ 2. Identify critical user journeys that need test coverage
19
+ 3. Map out test scenarios with clear steps and expected outcomes
20
+ 4. Consider edge cases, error states, and boundary conditions
21
+ 5. Identify selectors and locators needed for each element
22
+ 6. Note any test data requirements or setup needed
23
+ </planning_focus>
24
+
25
+ <test_coverage>
26
+ **Distribution target:**
27
+ - 70% Happy paths - Standard successful flows
28
+ - 20% Error paths - Validation failures, error states
29
+ - 10% Edge cases - Boundaries, empty states, limits
30
+
31
+ **What to cover:**
32
+ - Critical user journeys (signup, login, checkout, core features)
33
+ - Business-critical features (payments, data integrity)
34
+ - High-risk areas (complex logic, integrations)
35
+ - Error handling and validation
36
+
37
+ **What NOT to cover:**
38
+ - Every edge case permutation
39
+ - Implementation details
40
+ - Trivial functionality
41
+ - Same pattern repeated across features
42
+ </test_coverage>
43
+
44
+ <analysis_tasks>
45
+ - Explore the application structure and routes
46
+ - Identify key UI components and their interactions
47
+ - Map user authentication and authorization flows
48
+ - Document form validations and error handling
49
+ - Identify async operations that need proper waits
50
+ - Note any third-party integrations or API dependencies
51
+ </analysis_tasks>
52
+
53
+ <plan_output>
54
+ Your E2E test plan should include:
55
+ 1. Test suite overview - what user flows are being tested
56
+ 2. Test cases with clear descriptions and priority levels
57
+ 3. Step-by-step test actions (click, type, navigate, assert)
58
+ 4. Expected outcomes and assertions for each test
59
+ 5. Test data requirements (users, fixtures, mock data)
60
+ 6. Selector strategy (data-testid, aria-labels, etc.)
61
+ 7. Setup and teardown requirements
62
+ 8. Potential flakiness risks and mitigation strategies
63
+ </plan_output>
64
+
65
+ <constraints>
66
+ - You can ONLY use read-only tools: Read, Glob, Grep, Task
67
+ - Do NOT write tests, modify files, or run commands
68
+ - Focus on research and planning, not implementation
69
+ - Present findings clearly so the user can review before writing tests
70
+ </constraints>`;
@@ -0,0 +1,244 @@
1
+ import { logger } from "../utils/logger";
2
+ /**
3
+ * API Error class with user-friendly messages
4
+ *
5
+ * Based on Gemini CLI (Apache 2.0 License)
6
+ * https://github.com/google-gemini/gemini-cli
7
+ * Copyright 2025 Google LLC
8
+ */
9
+ export class ApiError extends Error {
10
+ status;
11
+ isAuthError;
12
+ constructor(status, statusText, body) {
13
+ let message;
14
+ if (status === 401) {
15
+ message = "Authentication required. Use /login to authenticate.";
16
+ }
17
+ else if (status === 403) {
18
+ message = "Access denied. Your token may have been revoked.";
19
+ }
20
+ else {
21
+ message = `API error: ${status} ${statusText}`;
22
+ if (body) {
23
+ // Try to extract a cleaner error message from JSON body
24
+ try {
25
+ const parsed = JSON.parse(body);
26
+ if (parsed.error) {
27
+ message = parsed.error;
28
+ }
29
+ else if (parsed.message) {
30
+ message = parsed.message;
31
+ }
32
+ }
33
+ catch {
34
+ // If not JSON, append raw body if short
35
+ if (body.length < 200) {
36
+ message += ` - ${body}`;
37
+ }
38
+ }
39
+ }
40
+ }
41
+ super(message);
42
+ this.name = "ApiError";
43
+ this.status = status;
44
+ this.isAuthError = status === 401 || status === 403;
45
+ }
46
+ }
47
+ /**
48
+ * API Client for CLI to communicate with Supatest backend
49
+ */
50
+ export class ApiClient {
51
+ apiUrl;
52
+ apiKey;
53
+ constructor(apiUrl, apiKey) {
54
+ this.apiUrl = apiUrl;
55
+ this.apiKey = apiKey;
56
+ }
57
+ /**
58
+ * Update the API key (used when user logs in during session)
59
+ */
60
+ setApiKey(apiKey) {
61
+ this.apiKey = apiKey;
62
+ }
63
+ /**
64
+ * Clear the API key (used when user logs out)
65
+ */
66
+ clearApiKey() {
67
+ this.apiKey = undefined;
68
+ }
69
+ /**
70
+ * Check if the client has an API key set
71
+ */
72
+ hasApiKey() {
73
+ return !!this.apiKey;
74
+ }
75
+ /**
76
+ * Create a new CLI session on the backend
77
+ * @param title - The session title
78
+ * @param originMetadata - Optional metadata about the session origin
79
+ * @returns The session ID and web URL
80
+ */
81
+ async createSession(title, originMetadata) {
82
+ const url = `${this.apiUrl}/v1/agent/sessions`;
83
+ const response = await fetch(url, {
84
+ method: "POST",
85
+ headers: {
86
+ "Content-Type": "application/json",
87
+ Authorization: `Bearer ${this.apiKey}`,
88
+ },
89
+ body: JSON.stringify({
90
+ title,
91
+ originMetadata,
92
+ }),
93
+ });
94
+ if (!response.ok) {
95
+ const errorText = await response.text();
96
+ throw new ApiError(response.status, response.statusText, errorText);
97
+ }
98
+ const data = await response.json();
99
+ return data;
100
+ }
101
+ /**
102
+ * Stream an event to the backend
103
+ * @param sessionId - The session ID
104
+ * @param event - The CLI event to stream
105
+ * @returns Success status
106
+ */
107
+ async streamEvent(sessionId, event) {
108
+ const url = `${this.apiUrl}/v1/agent/sessions/${sessionId}/events`;
109
+ const response = await fetch(url, {
110
+ method: "POST",
111
+ headers: {
112
+ "Content-Type": "application/json",
113
+ Authorization: `Bearer ${this.apiKey}`,
114
+ },
115
+ body: JSON.stringify(event),
116
+ });
117
+ if (!response.ok) {
118
+ const errorText = await response.text();
119
+ throw new ApiError(response.status, response.statusText, errorText);
120
+ }
121
+ const data = await response.json();
122
+ return data;
123
+ }
124
+ /**
125
+ * Get session details from the backend
126
+ * @param sessionId - The session ID
127
+ * @returns Session details
128
+ */
129
+ async getSession(sessionId) {
130
+ const url = `${this.apiUrl}/v1/agent/sessions/${sessionId}`;
131
+ const response = await fetch(url, {
132
+ method: "GET",
133
+ headers: {
134
+ Authorization: `Bearer ${this.apiKey}`,
135
+ },
136
+ });
137
+ if (!response.ok) {
138
+ const errorText = await response.text();
139
+ throw new ApiError(response.status, response.statusText, errorText);
140
+ }
141
+ return await response.json();
142
+ }
143
+ /**
144
+ * Get paginated sessions accessible to the user
145
+ * @param limit - Maximum number of sessions to return
146
+ * @param offset - Number of sessions to skip
147
+ * @returns Paginated sessions with total count
148
+ */
149
+ async getSessions(limit, offset) {
150
+ const urlParams = new URLSearchParams({
151
+ limit: limit.toString(),
152
+ offset: offset.toString(),
153
+ });
154
+ const url = `${this.apiUrl}/v1/sessions?${urlParams.toString()}`;
155
+ logger.debug(`Fetching sessions: ${url}`);
156
+ const response = await fetch(url, {
157
+ method: "GET",
158
+ headers: {
159
+ Authorization: `Bearer ${this.apiKey}`,
160
+ },
161
+ });
162
+ if (!response.ok) {
163
+ const errorText = await response.text();
164
+ throw new ApiError(response.status, response.statusText, errorText);
165
+ }
166
+ const data = await response.json();
167
+ logger.debug(`Fetched ${data.sessions.length} sessions (${data.pagination.offset + 1}-${data.pagination.offset + data.sessions.length} of ${data.pagination.total})`);
168
+ return data;
169
+ }
170
+ /**
171
+ * Complete usage tracking for a message turn
172
+ * @param messageId - The assistant message ID
173
+ * @returns Success status
174
+ */
175
+ async completeUsage(messageId) {
176
+ const url = `${this.apiUrl}/v1/usage/complete`;
177
+ const response = await fetch(url, {
178
+ method: "POST",
179
+ headers: {
180
+ "Content-Type": "application/json",
181
+ Authorization: `Bearer ${this.apiKey}`,
182
+ },
183
+ body: JSON.stringify({ messageId }),
184
+ });
185
+ if (!response.ok) {
186
+ const errorText = await response.text();
187
+ logger.warn(`Failed to complete usage tracking: ${response.status} ${response.statusText} - ${errorText}`);
188
+ // Don't throw - this is best effort
189
+ return { success: false };
190
+ }
191
+ const data = await response.json();
192
+ return data;
193
+ }
194
+ /**
195
+ * Get messages for a session
196
+ * @param sessionId - The session ID
197
+ * @returns Messages with pagination info
198
+ */
199
+ async getSessionMessages(sessionId) {
200
+ const url = `${this.apiUrl}/v1/sessions/${sessionId}/messages`;
201
+ logger.debug(`Fetching messages for session: ${sessionId}`);
202
+ const response = await fetch(url, {
203
+ method: "GET",
204
+ headers: {
205
+ Authorization: `Bearer ${this.apiKey}`,
206
+ },
207
+ });
208
+ if (!response.ok) {
209
+ const errorText = await response.text();
210
+ throw new ApiError(response.status, response.statusText, errorText);
211
+ }
212
+ const data = await response.json();
213
+ logger.debug(`Fetched ${data.messages.length} messages for session: ${sessionId}`);
214
+ return data;
215
+ }
216
+ /**
217
+ * Update a session (title, providerSessionId)
218
+ * @param sessionId - The session ID
219
+ * @param data - The data to update
220
+ * @returns Updated session
221
+ */
222
+ async updateSession(sessionId, data) {
223
+ const url = `${this.apiUrl}/v1/sessions/${sessionId}`;
224
+ logger.debug(`Updating session: ${sessionId}`, {
225
+ hasTitle: !!data.title,
226
+ hasProviderSessionId: !!data.providerSessionId,
227
+ });
228
+ const response = await fetch(url, {
229
+ method: "PATCH",
230
+ headers: {
231
+ "Content-Type": "application/json",
232
+ Authorization: `Bearer ${this.apiKey}`,
233
+ },
234
+ body: JSON.stringify(data),
235
+ });
236
+ if (!response.ok) {
237
+ const errorText = await response.text();
238
+ throw new ApiError(response.status, response.statusText, errorText);
239
+ }
240
+ const result = await response.json();
241
+ logger.debug(`Session updated: ${sessionId}`);
242
+ return result;
243
+ }
244
+ }
@@ -0,0 +1,130 @@
1
+ import { logger } from "../utils/logger";
2
+ const BATCH_SIZE = 10; // Send events every 10 events
3
+ const BATCH_INTERVAL_MS = 100; // Or every 100ms, whichever comes first
4
+ const MAX_RETRY_ATTEMPTS = 3;
5
+ const RETRY_DELAY_MS = 1000; // Start with 1s delay
6
+ /**
7
+ * Event Streamer - Batches and streams CLI events to the backend with retry logic
8
+ */
9
+ export class EventStreamer {
10
+ apiClient;
11
+ sessionId;
12
+ eventQueue = [];
13
+ batchTimer = null;
14
+ isFlushing = false;
15
+ isShutdown = false;
16
+ constructor(apiClient, sessionId) {
17
+ this.apiClient = apiClient;
18
+ this.sessionId = sessionId;
19
+ }
20
+ /**
21
+ * Queue an event for streaming
22
+ * @param event - The CLI event to queue
23
+ */
24
+ async queueEvent(event) {
25
+ if (this.isShutdown) {
26
+ logger.warn("EventStreamer is shutdown, cannot queue event");
27
+ return;
28
+ }
29
+ // Send streaming delta events immediately for real-time updates
30
+ // Batch other events to reduce API calls
31
+ if (event.type === "assistant_text" || event.type === "assistant_thinking") {
32
+ await this.sendEventWithRetry(event);
33
+ return;
34
+ }
35
+ this.eventQueue.push(event);
36
+ // Flush if we've reached batch size
37
+ if (this.eventQueue.length >= BATCH_SIZE) {
38
+ await this.flush();
39
+ }
40
+ else {
41
+ // Reset the batch timer
42
+ this.resetBatchTimer();
43
+ }
44
+ }
45
+ /**
46
+ * Reset the batch timer
47
+ */
48
+ resetBatchTimer() {
49
+ if (this.batchTimer) {
50
+ clearTimeout(this.batchTimer);
51
+ }
52
+ this.batchTimer = setTimeout(() => {
53
+ this.flush().catch((error) => {
54
+ logger.error(`Failed to flush events on timer: ${error.message}`);
55
+ });
56
+ }, BATCH_INTERVAL_MS);
57
+ }
58
+ /**
59
+ * Flush all queued events to the backend
60
+ */
61
+ async flush() {
62
+ if (this.isFlushing || this.eventQueue.length === 0) {
63
+ return;
64
+ }
65
+ this.isFlushing = true;
66
+ // Clear the batch timer
67
+ if (this.batchTimer) {
68
+ clearTimeout(this.batchTimer);
69
+ this.batchTimer = null;
70
+ }
71
+ // Take all events from the queue
72
+ const eventsToSend = [...this.eventQueue];
73
+ this.eventQueue = [];
74
+ // Send each event with retry logic
75
+ for (const event of eventsToSend) {
76
+ await this.sendEventWithRetry(event);
77
+ }
78
+ this.isFlushing = false;
79
+ }
80
+ /**
81
+ * Send a single event with retry logic
82
+ * @param event - The event to send
83
+ */
84
+ async sendEventWithRetry(event) {
85
+ let attempts = 0;
86
+ let lastError = null;
87
+ while (attempts < MAX_RETRY_ATTEMPTS) {
88
+ try {
89
+ await this.apiClient.streamEvent(this.sessionId, event);
90
+ return; // Success
91
+ }
92
+ catch (error) {
93
+ lastError = error;
94
+ attempts++;
95
+ if (attempts < MAX_RETRY_ATTEMPTS) {
96
+ const delay = RETRY_DELAY_MS * Math.pow(2, attempts - 1); // Exponential backoff
97
+ logger.warn(`Failed to stream event (attempt ${attempts}/${MAX_RETRY_ATTEMPTS}), retrying in ${delay}ms...`);
98
+ await this.sleep(delay);
99
+ }
100
+ }
101
+ }
102
+ // All retries failed
103
+ logger.error(`Failed to stream event after ${MAX_RETRY_ATTEMPTS} attempts: ${lastError?.message}`);
104
+ // Don't throw - continue processing other events
105
+ // The event is lost, but we don't want to crash the CLI
106
+ }
107
+ /**
108
+ * Sleep for a given duration
109
+ * @param ms - Duration in milliseconds
110
+ */
111
+ sleep(ms) {
112
+ return new Promise((resolve) => setTimeout(resolve, ms));
113
+ }
114
+ /**
115
+ * Shutdown the event streamer and flush any remaining events
116
+ */
117
+ async shutdown() {
118
+ if (this.isShutdown) {
119
+ return;
120
+ }
121
+ this.isShutdown = true;
122
+ // Clear the batch timer
123
+ if (this.batchTimer) {
124
+ clearTimeout(this.batchTimer);
125
+ this.batchTimer = null;
126
+ }
127
+ // Flush any remaining events
128
+ await this.flush();
129
+ }
130
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};