@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.
- package/dist/commands/login.js +392 -0
- package/dist/commands/setup.js +234 -0
- package/dist/config.js +29 -0
- package/dist/core/agent.js +259 -0
- package/dist/index.js +154 -6586
- package/dist/modes/headless.js +117 -0
- package/dist/modes/interactive.js +418 -0
- package/dist/presenters/composite.js +32 -0
- package/dist/presenters/console.js +163 -0
- package/dist/presenters/react.js +217 -0
- package/dist/presenters/types.js +1 -0
- package/dist/presenters/web.js +78 -0
- package/dist/prompts/builder.js +181 -0
- package/dist/prompts/fixer.js +148 -0
- package/dist/prompts/index.js +3 -0
- package/dist/prompts/planner.js +70 -0
- package/dist/services/api-client.js +244 -0
- package/dist/services/event-streamer.js +130 -0
- package/dist/types.js +1 -0
- package/dist/ui/App.js +322 -0
- package/dist/ui/components/AuthBanner.js +24 -0
- package/dist/ui/components/AuthDialog.js +32 -0
- package/dist/ui/components/Banner.js +12 -0
- package/dist/ui/components/ExpandableSection.js +17 -0
- package/dist/ui/components/Header.js +51 -0
- package/dist/ui/components/HelpMenu.js +89 -0
- package/dist/ui/components/InputPrompt.js +286 -0
- package/dist/ui/components/MessageList.js +42 -0
- package/dist/ui/components/QueuedMessageDisplay.js +31 -0
- package/dist/ui/components/Scrollable.js +103 -0
- package/dist/ui/components/SessionSelector.js +196 -0
- package/dist/ui/components/StatusBar.js +34 -0
- package/dist/ui/components/messages/AssistantMessage.js +20 -0
- package/dist/ui/components/messages/ErrorMessage.js +26 -0
- package/dist/ui/components/messages/LoadingMessage.js +28 -0
- package/dist/ui/components/messages/ThinkingMessage.js +17 -0
- package/dist/ui/components/messages/TodoMessage.js +44 -0
- package/dist/ui/components/messages/ToolMessage.js +218 -0
- package/dist/ui/components/messages/UserMessage.js +14 -0
- package/dist/ui/contexts/KeypressContext.js +527 -0
- package/dist/ui/contexts/MouseContext.js +98 -0
- package/dist/ui/contexts/SessionContext.js +129 -0
- package/dist/ui/hooks/useAnimatedScrollbar.js +83 -0
- package/dist/ui/hooks/useBatchedScroll.js +22 -0
- package/dist/ui/hooks/useBracketedPaste.js +31 -0
- package/dist/ui/hooks/useFocus.js +50 -0
- package/dist/ui/hooks/useKeypress.js +26 -0
- package/dist/ui/hooks/useModeToggle.js +25 -0
- package/dist/ui/types/auth.js +13 -0
- package/dist/ui/utils/file-completion.js +56 -0
- package/dist/ui/utils/input.js +50 -0
- package/dist/ui/utils/markdown.js +376 -0
- package/dist/ui/utils/mouse.js +189 -0
- package/dist/ui/utils/theme.js +59 -0
- package/dist/utils/banner.js +9 -0
- package/dist/utils/encryption.js +71 -0
- package/dist/utils/events.js +36 -0
- package/dist/utils/keychain-storage.js +120 -0
- package/dist/utils/logger.js +209 -0
- package/dist/utils/node-version.js +89 -0
- package/dist/utils/plan-file.js +75 -0
- package/dist/utils/project-instructions.js +23 -0
- package/dist/utils/rich-logger.js +208 -0
- package/dist/utils/stdin.js +25 -0
- package/dist/utils/stdio.js +80 -0
- package/dist/utils/summary.js +94 -0
- package/dist/utils/token-storage.js +242 -0
- package/dist/version.js +6 -0
- 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,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 {};
|