mirror of
https://github.com/cline/cline.git
synced 2026-09-03 12:14:00 +08:00
Compare commits
40 Commits
bee/cli-tui
...
fixer
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d77a9a787 | |||
| 92ebfce0cb | |||
| 8a5042a8bc | |||
| 726853d679 | |||
| 0f52d80e55 | |||
| 71af56f493 | |||
| 1167b4f3a6 | |||
| e8d6370b0c | |||
| e243376a39 | |||
| 4f1be9d512 | |||
| 94d36ce719 | |||
| 5df7498f03 | |||
| c2b87252ac | |||
| be353bb3da | |||
| 0a7791de1f | |||
| 7cca102e14 | |||
| 4f591de6a9 | |||
| 60436b3ddf | |||
| df5954052d | |||
| a66a2784c3 | |||
| 47031cea25 | |||
| e29740479e | |||
| 74f607ff8e | |||
| 0edf6d777b | |||
| de630c64d4 | |||
| 3ff63562c8 | |||
| 4d6f908fbd | |||
| 6521fdcc94 | |||
| 1393eace27 | |||
| eebb99c1e3 | |||
| 9ed44f7a83 | |||
| 6a95cc5f19 | |||
| 1d0637f39c | |||
| f2c16bae7a | |||
| 204f15ce1c | |||
| 8118e11596 | |||
| f7b593df35 | |||
| 2e0358a7a1 | |||
| 0fbc10f807 | |||
| c093ca1760 |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
improve cline command permission validation logic. add cline command permission man page documentation
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"cline": patch
|
||||
---
|
||||
|
||||
Remove deprecated zai-glm-4.6 model from Cerebras provider
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Wiring-up "conditionals" for Cline Rules files.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix drag & drop files from SSH remote workspaces into chat
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Updated switch component to adhere to 3:1 contrast accessibility standard for both light and dark vscode themes
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Remove hooks feature setting and make it always enabled (except on Windows).
|
||||
@@ -16,13 +16,6 @@
|
||||
TELEMETRY_SERVICE_API_KEY=your-posthog-telemetry-api-key
|
||||
ERROR_SERVICE_API_KEY=your-posthog-error-tracking-api-key
|
||||
|
||||
# ============================================================================
|
||||
# TELEMETRY PROVIDER CONTROL
|
||||
# ============================================================================
|
||||
# Control which telemetry providers are active
|
||||
POSTHOG_TELEMETRY_ENABLED=true # Enable PostHog telemetry (default: true)
|
||||
# Set to false to disable Telemetry completely
|
||||
|
||||
# ============================================================================
|
||||
# OPENTELEMETRY (Optional - for advanced telemetry)
|
||||
# ============================================================================
|
||||
|
||||
@@ -97,7 +97,6 @@ jobs:
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
|
||||
POSTHOG_TELEMETRY_ENABLED: "true"
|
||||
run: npm run compile-standalone-npm
|
||||
|
||||
- name: Generate Protos (Second Pass - Bug Workaround)
|
||||
|
||||
@@ -138,7 +138,6 @@ jobs:
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
|
||||
POSTHOG_TELEMETRY_ENABLED: "true"
|
||||
run: npm run compile-standalone-npm
|
||||
|
||||
- name: Generate Protos (Second Pass - Bug Workaround)
|
||||
|
||||
+39
-2
@@ -1,18 +1,55 @@
|
||||
# Changelog
|
||||
|
||||
## [3.55.0]
|
||||
|
||||
- Add new model: Arcee Trinity Large Preview
|
||||
- Add new model: Moonshot Kimi K2.5
|
||||
- Add MCP prompts support - prompts from connected MCP servers now appear in slash command autocomplete as `/mcp:<server>:<prompt>`
|
||||
|
||||
## [3.54.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Native tool calls support for Ollama provider
|
||||
- Sonnet 4.5 is now the default Amazon Bedrock model id
|
||||
|
||||
### Fixed
|
||||
|
||||
- Prevent infinite retry loops when replace_in_file fails repeatedly. The system now detects repeated failures and provides better guidance to break out of retry cycles.
|
||||
- Skip diff error UI handling during streaming to prevent flickering. Error handling is deferred until streaming completes.
|
||||
- Strip notebook cell outputs when extracting text content from Jupyter notebooks, significantly reducing context size sent to the LLM.
|
||||
- Throttle diff view updates during streaming to reduce UI flickering and improve performance.
|
||||
|
||||
### Changed
|
||||
|
||||
- Removed Mistral's Devstral-2512 free from the free models list
|
||||
- Removed deprecated zai-glm-4.6 model from Cerebras provider
|
||||
|
||||
## [3.53.1]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Bug in responses API
|
||||
|
||||
## [3.53.0]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Removed grok model from free tier
|
||||
|
||||
## [3.52.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Users with ChatGPT Plus or Pro subscriptions can now use GPT-5 models directly through Cline without needing an API key. Authentication is handled via OAuth through OpenAI's authentication system.
|
||||
- Grok models are now moving out of free tier and into paid plans.
|
||||
- Introduces comprehensive Jupyter Notebook support for Cline, enabling AI-assisted editing of `.ipynb` files with full cell-level context awareness.
|
||||
- Introduces comprehensive Jupyter Notebook support for Cline, enabling AI-assisted editing of `.ipynb` files with full cell-level context awareness.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Bugs in DiffViewProvider for file editing
|
||||
- Ollama's recommended models to use correct identifiers
|
||||
|
||||
|
||||
## [3.51.0]
|
||||
|
||||
### Added
|
||||
|
||||
+23
@@ -147,6 +147,29 @@
|
||||
"src/dev/grit/vscode-api.grit"
|
||||
]
|
||||
},
|
||||
{
|
||||
// Do not use console logging directly, use the Logger service instead.
|
||||
"plugins": [
|
||||
"src/dev/grit/console-log.grit"
|
||||
],
|
||||
"includes": [
|
||||
"**",
|
||||
"!**/webview-ui/**",
|
||||
"!**/evals/**",
|
||||
"!**/standalone/**",
|
||||
"!**/e2e/**",
|
||||
"!**/test/**",
|
||||
"!**/__tests__/**",
|
||||
"!**/*.test.ts",
|
||||
"!**/*.stories.ts",
|
||||
"!src/dev/**",
|
||||
"!**/*.mjs",
|
||||
"!**/*.js",
|
||||
"!**/scripts/**",
|
||||
"!**/*.tsx",
|
||||
"!**/testing-platform/**"
|
||||
]
|
||||
},
|
||||
{
|
||||
"includes": [
|
||||
"**",
|
||||
|
||||
@@ -104,14 +104,18 @@ func (s *EnvService) Shutdown(ctx context.Context, req *cline.EmptyRequest) (*cl
|
||||
return &cline.Empty{}, nil
|
||||
}
|
||||
|
||||
func (s *EnvService) isTelemetryEnabled() bool {
|
||||
// In CLI mode, check the CLINE_TELEMETRY_DISABLED environment variable
|
||||
return os.Getenv("CLINE_TELEMETRY_DISABLED") != "true"
|
||||
}
|
||||
|
||||
// GetTelemetrySettings returns the telemetry settings for CLI mode
|
||||
func (s *EnvService) GetTelemetrySettings(ctx context.Context, req *cline.EmptyRequest) (*host.GetTelemetrySettingsResponse, error) {
|
||||
if s.verbose {
|
||||
log.Printf("GetTelemetrySettings called")
|
||||
}
|
||||
|
||||
// In CLI mode, check the POSTHOG_TELEMETRY_ENABLED environment variable
|
||||
telemetryEnabled := os.Getenv("POSTHOG_TELEMETRY_ENABLED") == "true"
|
||||
telemetryEnabled := s.isTelemetryEnabled()
|
||||
|
||||
var setting host.Setting
|
||||
if telemetryEnabled {
|
||||
@@ -133,8 +137,7 @@ func (s *EnvService) SubscribeToTelemetrySettings(req *cline.EmptyRequest, strea
|
||||
log.Printf("SubscribeToTelemetrySettings called")
|
||||
}
|
||||
|
||||
// Send initial telemetry state
|
||||
telemetryEnabled := os.Getenv("POSTHOG_TELEMETRY_ENABLED") == "true"
|
||||
telemetryEnabled := s.isTelemetryEnabled()
|
||||
|
||||
var setting host.Setting
|
||||
if telemetryEnabled {
|
||||
|
||||
+15
-1
@@ -117,7 +117,13 @@
|
||||
"features/auto-compact",
|
||||
"features/background-edit",
|
||||
"features/checkpoints",
|
||||
"features/cline-rules",
|
||||
{
|
||||
"group": "Cline Rules",
|
||||
"pages": [
|
||||
"features/cline-rules/overview",
|
||||
"features/cline-rules/conditional-rules"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Commands & Shortcuts",
|
||||
"pages": [
|
||||
@@ -424,6 +430,14 @@
|
||||
{
|
||||
"source": "/enterprise-solutions/team-management/roles-and-permissions",
|
||||
"destination": "/enterprise-solutions/team-management/managing-members"
|
||||
},
|
||||
{
|
||||
"source": "/features/cline-rules",
|
||||
"destination": "/features/cline-rules/overview"
|
||||
},
|
||||
{
|
||||
"source": "/features/conditional-rules",
|
||||
"destination": "/features/cline-rules/conditional-rules"
|
||||
}
|
||||
],
|
||||
"search": {
|
||||
|
||||
@@ -1,188 +0,0 @@
|
||||
Cline Rules allow you to provide Cline with system-level guidance. Think of them as a persistent way to include context and preferences for your projects or globally for every conversation.
|
||||
|
||||
## Creating a Rule
|
||||
|
||||
You can create a rule by clicking the `+` button in the Rules tab. This will open a new file in your IDE which you can use to write your rule.
|
||||
|
||||
<Frame>
|
||||
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/cline-rules.png" alt="Create a Rule" />
|
||||
</Frame>
|
||||
|
||||
Once you save the file:
|
||||
|
||||
- Your rule will be stored in the `.clinerules/` directory in your project (if it's a Workspace Rule)
|
||||
- Or in the Global Rules directory (if it's a Global Rule):
|
||||
|
||||
### Global Rules Directory Location
|
||||
|
||||
The location of your Global Rules directory depends on your operating system:
|
||||
|
||||
| Operating System | Default Location | Notes |
|
||||
|------------------|------------------|-------|
|
||||
| **Windows** | `Documents\Cline\Rules` | Uses system Documents folder |
|
||||
| **macOS** | `~/Documents/Cline/Rules` | Uses user Documents folder |
|
||||
| **Linux/WSL** | `~/Documents/Cline/Rules` | May fall back to `~/Cline/Rules` on some systems |
|
||||
|
||||
> **Note for Linux/WSL users**: If you don't find your global rules in `~/Documents/Cline/Rules`, check `~/Cline/Rules` as the location may vary depending on your system configuration and whether the Documents directory exists.
|
||||
|
||||
You can also have Cline create a rule for you by using the [`/newrule` slash command](/features/slash-commands/new-rule) in the chat.
|
||||
|
||||
```markdown Example Cline Rule Structure [expandable]
|
||||
# Project Guidelines
|
||||
|
||||
## Documentation Requirements
|
||||
|
||||
- Update relevant documentation in /docs when modifying features
|
||||
- Keep README.md in sync with new capabilities
|
||||
- Maintain changelog entries in CHANGELOG.md
|
||||
|
||||
## Architecture Decision Records
|
||||
|
||||
Create ADRs in /docs/adr for:
|
||||
|
||||
- Major dependency changes
|
||||
- Architectural pattern changes
|
||||
- New integration patterns
|
||||
- Database schema changes
|
||||
Follow template in /docs/adr/template.md
|
||||
|
||||
## Code Style & Patterns
|
||||
|
||||
- Generate API clients using OpenAPI Generator
|
||||
- Use TypeScript axios template
|
||||
- Place generated code in /src/generated
|
||||
- Prefer composition over inheritance
|
||||
- Use repository pattern for data access
|
||||
- Follow error handling pattern in /src/utils/errors.ts
|
||||
|
||||
## Testing Standards
|
||||
|
||||
- Unit tests required for business logic
|
||||
- Integration tests for API endpoints
|
||||
- E2E tests for critical user flows
|
||||
```
|
||||
|
||||
### Key Benefits
|
||||
|
||||
1. **Version Controlled**: The `.clinerules` file becomes part of your project's source code
|
||||
2. **Team Consistency**: Ensures consistent behavior across all team members
|
||||
3. **Project-Specific**: Rules and standards tailored to each project's needs
|
||||
4. **Institutional Knowledge**: Maintains project standards and practices in code
|
||||
|
||||
Place the `.clinerules` file in your project's root directory:
|
||||
|
||||
```
|
||||
your-project/
|
||||
├── .clinerules
|
||||
├── src/
|
||||
├── docs/
|
||||
└── ...
|
||||
```
|
||||
|
||||
Cline's system prompt, on the other hand, is not user-editable ([here's where you can find it](https://github.com/cline/cline/blob/main/src/core/prompts/system.ts)). For a broader look at prompt engineering best practices, check out [this resource](https://docs.anthropic.com/en/docs/build-with-claude/prompt-engineering/overview).
|
||||
|
||||
### AGENTS.md Standard Support
|
||||
|
||||
Cline also supports the [AGENTS.md](https://agents.md/) standard as a fallback
|
||||
(in addition to Cline Rules) by automatically detecting `AGENTS.md` files in
|
||||
your workspace root. This allows you to use the same rules file across different AI
|
||||
coding tools.
|
||||
|
||||
```
|
||||
your-project/
|
||||
├── AGENTS.md
|
||||
├── src/
|
||||
└── ...
|
||||
```
|
||||
|
||||
### Tips for Writing Effective Cline Rules
|
||||
|
||||
- Be Clear and Concise: Use simple language and avoid ambiguity.
|
||||
- Focus on Desired Outcomes: Describe the results you want, not the specific steps.
|
||||
- Test and Iterate: Experiment to find what works best for your workflow.
|
||||
|
||||
### .clinerules/ Folder System
|
||||
|
||||
```
|
||||
your-project/
|
||||
├── .clinerules/ # Folder containing active rules
|
||||
│ ├── 01-coding.md # Core coding standards
|
||||
│ ├── 02-documentation.md # Documentation requirements
|
||||
│ └── current-sprint.md # Rules specific to current work
|
||||
├── src/
|
||||
└── ...
|
||||
```
|
||||
|
||||
Cline automatically processes **all Markdown files** inside the `.clinerules/` directory, combining them into a unified set of rules. The numeric prefixes (optional) help organize files in a logical sequence.
|
||||
|
||||
#### Using a Rules Bank
|
||||
|
||||
For projects with multiple contexts or teams, maintain a rules bank directory:
|
||||
|
||||
```
|
||||
your-project/
|
||||
├── .clinerules/ # Active rules - automatically applied
|
||||
│ ├── 01-coding.md
|
||||
│ └── client-a.md
|
||||
│
|
||||
├── clinerules-bank/ # Repository of available but inactive rules
|
||||
│ ├── clients/ # Client-specific rule sets
|
||||
│ │ ├── client-a.md
|
||||
│ │ └── client-b.md
|
||||
│ ├── frameworks/ # Framework-specific rules
|
||||
│ │ ├── react.md
|
||||
│ │ └── vue.md
|
||||
│ └── project-types/ # Project type standards
|
||||
│ ├── api-service.md
|
||||
│ └── frontend-app.md
|
||||
└── ...
|
||||
```
|
||||
|
||||
#### Benefits of the Folder Approach
|
||||
|
||||
1. **Contextual Activation**: Copy only relevant rules from the bank to the active folder
|
||||
2. **Easier Maintenance**: Update individual rule files without affecting others
|
||||
3. **Team Flexibility**: Different team members can activate rules specific to their current task
|
||||
4. **Reduced Noise**: Keep the active ruleset focused and relevant
|
||||
|
||||
#### Usage Examples
|
||||
|
||||
Switch between client projects:
|
||||
|
||||
```bash
|
||||
# Switch to Client B project
|
||||
rm .clinerules/client-a.md
|
||||
cp clinerules-bank/clients/client-b.md .clinerules/
|
||||
```
|
||||
|
||||
Adapt to different tech stacks:
|
||||
|
||||
```bash
|
||||
# Frontend React project
|
||||
cp clinerules-bank/frameworks/react.md .clinerules/
|
||||
```
|
||||
|
||||
#### Implementation Tips
|
||||
|
||||
- Keep individual rule files focused on specific concerns
|
||||
- Use descriptive filenames that clearly indicate the rule's purpose
|
||||
- Consider git-ignoring the active `.clinerules/` folder while tracking the `clinerules-bank/`
|
||||
- Create team scripts to quickly activate common rule combinations
|
||||
|
||||
The folder system transforms your Cline rules from a static document into a dynamic knowledge system that adapts to your team's changing contexts and requirements.
|
||||
|
||||
### Managing Rules with the Toggleable Popover
|
||||
|
||||
To make managing both single `.clinerules` files and the folder system even easier, Cline v3.13 introduces a dedicated popover UI directly accessible from the chat interface.
|
||||
|
||||
Located conveniently under the chat input field, this popover allows you to:
|
||||
|
||||
- **Instantly See Active Rules:** View which global rules (from your user settings) and workspace rules (`.clinerules` file or folder contents) are currently active.
|
||||
- **Quickly Toggle Rules:** Enable or disable specific rule files within your workspace `.clinerules/` folder with a single click. This is perfect for activating context-specific rules (like `react-rules.md` or `memory-bank.md`) only when needed.
|
||||
- **Easily Add/Manage Rules:** Quickly create a workspace `.clinerules` file or folder if one doesn't exist, or add new rule files to an existing folder.
|
||||
|
||||
This UI significantly simplifies switching contexts and managing different sets of instructions without needing to manually edit files or configurations during a conversation.
|
||||
|
||||
<Frame>
|
||||
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(1).png" alt="Cline Logo" />
|
||||
</Frame>
|
||||
@@ -0,0 +1,267 @@
|
||||
---
|
||||
title: "Conditional Rules"
|
||||
sidebarTitle: "Conditional Rules"
|
||||
description: "Activate rules automatically based on which files you're working with"
|
||||
---
|
||||
|
||||
Conditional rules let you scope rules to specific parts of your codebase. Rules activate only when you're working with matching files, keeping your context focused and relevant.
|
||||
|
||||
For an introduction to Cline Rules, see the [Overview](/features/cline-rules/overview).
|
||||
|
||||
- **Without conditionals**: every rule loads for every request.
|
||||
- **With conditionals**, rules activate only when your current files match their defined scope.
|
||||
|
||||
For example, React component rules should appear when you're working with React components, not when you're editing Python or documentation.
|
||||
|
||||
## How It Works
|
||||
|
||||
Conditional rules use YAML frontmatter at the top of your rule files. When Cline processes a request, it gathers context from your current work (open files, visible tabs, mentioned paths, edited files), evaluates each rule's conditions, and activates matching rules.
|
||||
|
||||
<Note>
|
||||
When a conditional rule activates, you'll see a notification: **"Conditional rules applied: workspace:frontend-rules.md"**
|
||||
</Note>
|
||||
|
||||
## Writing Conditional Rules
|
||||
|
||||
Add YAML frontmatter to the top of any rule file in your `.clinerules/` directory:
|
||||
|
||||
```yaml
|
||||
---
|
||||
paths:
|
||||
- "src/components/**"
|
||||
- "src/hooks/**"
|
||||
---
|
||||
|
||||
# React Component Guidelines
|
||||
|
||||
When creating or modifying React components:
|
||||
- Use functional components with React hooks
|
||||
- Extract reusable logic into custom React hooks
|
||||
- Keep components focused on a single responsibility
|
||||
```
|
||||
|
||||
The `---` markers delimit the frontmatter. Everything after the closing `---` is your rule content.
|
||||
|
||||
### The `paths` Conditional
|
||||
|
||||
Currently, `paths` is the supported conditional. It takes an array of glob patterns:
|
||||
|
||||
```yaml
|
||||
---
|
||||
paths:
|
||||
- "src/**" # All files under src/
|
||||
- "*.config.js" # Config files in root
|
||||
- "packages/*/src/" # Monorepo package sources
|
||||
---
|
||||
```
|
||||
|
||||
**Glob pattern syntax:**
|
||||
- `*` matches any characters except `/`
|
||||
- `**` matches any characters including `/` (recursive)
|
||||
- `?` matches a single character
|
||||
- `[abc]` matches any character in the brackets
|
||||
- `{a,b}` matches either pattern
|
||||
|
||||
**Examples:**
|
||||
|
||||
| Pattern | Matches |
|
||||
|---------|---------|
|
||||
| `src/**/*.ts` | All TypeScript files under `src/` |
|
||||
| `*.md` | Markdown files in root only |
|
||||
| `**/*.test.ts` | Test files anywhere in the project |
|
||||
| `packages/{web,api}/**` | Files in web or api packages |
|
||||
| `src/components/*.tsx` | TSX files directly in components (not nested) |
|
||||
|
||||
### Behavior Details
|
||||
|
||||
**Multiple patterns**: A rule activates if any pattern matches any file in your context.
|
||||
|
||||
```yaml
|
||||
---
|
||||
paths:
|
||||
- "frontend/**"
|
||||
- "mobile/**"
|
||||
---
|
||||
# Activates when working in frontend OR mobile
|
||||
```
|
||||
|
||||
**No frontmatter**: Rules without frontmatter are always active.
|
||||
|
||||
**Empty paths array**: `paths: []` means the rule never activates. Use this to temporarily disable a rule.
|
||||
|
||||
**Invalid YAML**: If frontmatter can't be parsed, Cline fails open: the rule activates with raw content visible to help debugging.
|
||||
|
||||
## What Counts as "Current Context"
|
||||
|
||||
Cline evaluates rules based on:
|
||||
|
||||
1. **Your message**: File paths mentioned in your prompt (e.g., "update `src/App.tsx`")
|
||||
2. **Open tabs**: Files currently open in your editor
|
||||
3. **Visible files**: Files visible in your active editor panes
|
||||
4. **Edited files**: Files Cline has created, modified, or deleted during the task
|
||||
5. **Pending operations**: Files Cline is about to edit
|
||||
|
||||
Conditional rules can activate on your first message, when relevant files are open, or mid-task when Cline starts working with matching files.
|
||||
|
||||
<Tip>
|
||||
Be explicit about file paths in your prompts. "Update `src/services/user.ts`" reliably triggers path-based rules; "update the user service" may not.
|
||||
</Tip>
|
||||
|
||||
## Practical Examples
|
||||
|
||||
Copy these patterns and adapt them to your project structure.
|
||||
|
||||
### Frontend vs Backend Rules
|
||||
|
||||
Keep frontend and backend rules separate to avoid noise. Frontend rules only load when working with UI code, backend rules only load when working with API or service code.
|
||||
|
||||
```yaml
|
||||
# .clinerules/frontend.md
|
||||
---
|
||||
paths:
|
||||
- "src/components/**"
|
||||
- "src/pages/**"
|
||||
- "src/hooks/**"
|
||||
---
|
||||
|
||||
# Frontend Guidelines
|
||||
|
||||
- Use Tailwind CSS for styling
|
||||
- Prefer server components where possible
|
||||
- Keep client components small and focused
|
||||
```
|
||||
|
||||
```yaml
|
||||
# .clinerules/backend.md
|
||||
---
|
||||
paths:
|
||||
- "src/api/**"
|
||||
- "src/services/**"
|
||||
- "src/db/**"
|
||||
---
|
||||
|
||||
# Backend Guidelines
|
||||
|
||||
- Use dependency injection for services
|
||||
- All database queries go through repositories
|
||||
- Return typed errors, not thrown exceptions
|
||||
```
|
||||
|
||||
### Test File Rules
|
||||
|
||||
Enforce testing standards automatically. This rule activates only when you're writing or modifying tests, so testing guidance appears exactly when you need it.
|
||||
|
||||
```yaml
|
||||
# .clinerules/testing.md
|
||||
---
|
||||
paths:
|
||||
- "**/*.test.ts"
|
||||
- "**/*.spec.ts"
|
||||
- "**/__tests__/**"
|
||||
---
|
||||
|
||||
# Testing Standards
|
||||
|
||||
- Use descriptive test names: "should [expected behavior] when [condition]"
|
||||
- One assertion per test when possible
|
||||
- Mock external dependencies, not internal modules
|
||||
- Use factories for test data, not fixtures
|
||||
```
|
||||
|
||||
### Documentation Rules
|
||||
|
||||
Apply documentation standards only when editing docs. Prevents style rules from cluttering your context when you're writing code.
|
||||
|
||||
```yaml
|
||||
# .clinerules/docs.md
|
||||
---
|
||||
paths:
|
||||
- "docs/**"
|
||||
- "**/*.md"
|
||||
- "**/*.mdx"
|
||||
---
|
||||
|
||||
# Documentation Guidelines
|
||||
|
||||
- Use sentence case for headings
|
||||
- Include code examples for all features
|
||||
- Keep paragraphs short (3-4 sentences max)
|
||||
- Link to related documentation
|
||||
```
|
||||
|
||||
## Combining with Rule Toggles
|
||||
|
||||
Conditional rules work alongside the rule toggle UI. Toggle off a conditional rule to disable it entirely (it won't activate even if paths match). Toggle on to let it activate when conditions are met.
|
||||
|
||||
This provides two levels of control: manual toggles and automatic condition-based activation.
|
||||
|
||||
## Tips for Effective Conditional Rules
|
||||
|
||||
### Start Broad, Then Narrow
|
||||
|
||||
Begin with broader patterns and refine as you learn what works:
|
||||
|
||||
```yaml
|
||||
# Start here
|
||||
paths:
|
||||
- "src/**"
|
||||
|
||||
# Then narrow down
|
||||
paths:
|
||||
- "src/features/auth/**"
|
||||
```
|
||||
|
||||
### Use Descriptive Filenames
|
||||
|
||||
Name your rule files to indicate their scope:
|
||||
|
||||
```
|
||||
.clinerules/
|
||||
├── api-endpoints.md # Rules for API code
|
||||
├── database-models.md # Rules for DB layer
|
||||
├── react-components.md # Rules for React
|
||||
└── universal.md # No frontmatter = always active
|
||||
```
|
||||
|
||||
### Keep Universal Rules Separate
|
||||
|
||||
Put always-on rules (coding standards, project conventions) in files without frontmatter. Reserve conditional rules for context-specific guidance.
|
||||
|
||||
### Test Your Patterns
|
||||
|
||||
Not sure if a pattern matches? Create a simple test rule:
|
||||
|
||||
```yaml
|
||||
---
|
||||
paths:
|
||||
- "your/pattern/here/**"
|
||||
---
|
||||
|
||||
TEST: This rule should activate for your/pattern/here files.
|
||||
```
|
||||
|
||||
Then work with a file in that path and check if you see the activation notification.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Rule not activating:**
|
||||
- Check that file paths in your context match the glob pattern
|
||||
- Verify the rule is toggled on in the rules panel
|
||||
- Ensure YAML frontmatter has proper `---` delimiters
|
||||
|
||||
**Rule activating unexpectedly:**
|
||||
- Review glob patterns: `**` is recursive and may match more than intended
|
||||
- Check for open files that match the pattern
|
||||
- File paths mentioned in your message also count as context
|
||||
|
||||
**Frontmatter showing in output:**
|
||||
- YAML couldn't be parsed
|
||||
- Check for syntax errors (unquoted special characters, improper indentation)
|
||||
|
||||
## Related
|
||||
|
||||
- [Cline Rules Overview](/features/cline-rules/overview) - Complete rules system guide
|
||||
- [Skills](/features/skills) - Load instructions on demand with `/skill` command
|
||||
- [Workflows](/features/slash-commands/workflows/index) - Define explicit task automation
|
||||
- [@ Mentions](/features/at-mentions/overview) - Add files to context explicitly
|
||||
- [Understanding Context Management](/prompting/understanding-context-management) - How Cline manages context window
|
||||
@@ -0,0 +1,205 @@
|
||||
---
|
||||
title: "Cline Rules"
|
||||
sidebarTitle: "Overview"
|
||||
description: "Add persistent instructions and context to guide Cline's behavior"
|
||||
---
|
||||
|
||||
Cline Rules provide system-level guidance for your projects. Rules persist across conversations, ensuring consistent behavior without repeating instructions in every chat.
|
||||
|
||||
## How It Works
|
||||
|
||||
Rules are loaded when Cline starts a task. Here's what happens:
|
||||
|
||||
**Loading order**: Cline checks for rules in this sequence:
|
||||
1. `.clinerules/` folder (all `.md` files inside)
|
||||
2. Single `.clinerules` file
|
||||
3. `AGENTS.md` file
|
||||
|
||||
**Scope precedence**: Workspace rules override global rules when both define the same guidance.
|
||||
|
||||
**Multiple files**: When using a `.clinerules/` folder, all Markdown files are combined into one ruleset. Numeric prefixes (like `01-`, `02-`) control the order.
|
||||
|
||||
**Conditional activation**: Rules with YAML frontmatter activate only when you're working with matching files. See [Conditional Rules](/features/cline-rules/conditional-rules) for details.
|
||||
|
||||
## Supported Rule Files
|
||||
|
||||
Cline reads rules from multiple file formats in your workspace root, letting you share rules across different AI coding tools:
|
||||
|
||||
| File/Folder | Source | Notes |
|
||||
|-------------|--------|-------|
|
||||
| `.clinerules/` | Cline | Folder with `.md` files (recommended) |
|
||||
| `.cursor/rules/` | Cursor | Folder with `.mdc` files |
|
||||
| `.windsurf/rules` | Windsurf | Folder with multiple `md` files |
|
||||
| `AGENTS.md` | Universal | Follows [agents.md](https://agents.md/) standard, searched recursively |
|
||||
|
||||
Cline prioritizes `.clinerules` when present. Other formats load only if no `.clinerules` exists (except `AGENTS.md`, which always searches subdirectories). All rules appear in the Rules popover where you can toggle them.
|
||||
|
||||
## Creating Rules
|
||||
|
||||
Click the `+` button in the Rules tab to create a new rule. This opens a file in your editor where you write your guidance.
|
||||
|
||||
<Frame>
|
||||
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/cline-rules.png" alt="Create a Rule" />
|
||||
</Frame>
|
||||
|
||||
When you save the file, it's stored in:
|
||||
- **Workspace rules**: `.clinerules/` in your project root
|
||||
- **Global rules**: Platform-specific location (see table below)
|
||||
|
||||
You can also use the [`/newrule` slash command](/features/slash-commands/new-rule) to have Cline generate a rule based on your description.
|
||||
|
||||
### Global Rules Location
|
||||
|
||||
| Operating System | Default Location |
|
||||
|------------------|------------------|
|
||||
| **Windows** | `Documents\Cline\Rules` |
|
||||
| **macOS** | `~/Documents/Cline/Rules` |
|
||||
| **Linux/WSL** | `~/Documents/Cline/Rules` or `~/Cline/Rules` |
|
||||
|
||||
<Note>
|
||||
Linux/WSL users: Check both locations if you don't find global rules in `~/Documents/Cline/Rules`.
|
||||
</Note>
|
||||
|
||||
## Managing Rules
|
||||
|
||||
The Rules popover (below the chat input) shows active rules and lets you toggle them on or off.
|
||||
|
||||
<Frame>
|
||||
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(1).png" alt="Rules Popover" />
|
||||
</Frame>
|
||||
|
||||
The popover displays:
|
||||
- **Global rules**: From your user-level Rules directory
|
||||
- **Workspace rules**: From `.clinerules/` in your project
|
||||
|
||||
Toggle any rule to enable or disable it. Disabled rules won't load, even if they match conditions.
|
||||
|
||||
## When to Use Rules
|
||||
|
||||
Rules work best for persistent project context:
|
||||
|
||||
- **Code standards**: Formatting preferences, naming conventions, project-specific patterns
|
||||
- **Documentation requirements**: Where to add docs, what format to follow
|
||||
- **Architecture decisions**: Design patterns, dependency rules, module boundaries
|
||||
- **Team conventions**: PR processes, branch naming, commit message format
|
||||
- **Technology constraints**: Required libraries, banned APIs, version requirements
|
||||
|
||||
Rules are less effective for:
|
||||
- One-time instructions (just say it in the chat)
|
||||
- Complex multi-step workflows (use [Workflows](/features/slash-commands/workflows/index) instead)
|
||||
- Dynamic decisions that depend on runtime context
|
||||
|
||||
## Example Rule
|
||||
|
||||
```markdown
|
||||
# Backend API Guidelines
|
||||
|
||||
## Route Handlers
|
||||
|
||||
- Use async/await, not callbacks
|
||||
- Validate request bodies with Zod schemas
|
||||
- Return typed errors from `src/errors.ts`
|
||||
- All routes require authentication unless in `publicRoutes` array
|
||||
|
||||
## Database Access
|
||||
|
||||
- All queries go through repository classes in `src/repositories/`
|
||||
- Use transactions for multi-table updates
|
||||
- Never expose raw database errors to clients
|
||||
|
||||
## Testing
|
||||
|
||||
- Unit tests for business logic in `src/services/`
|
||||
- Integration tests for route handlers in `src/routes/`
|
||||
- Mock external APIs, not internal modules
|
||||
```
|
||||
|
||||
This rule provides clear, actionable guidance without explaining obvious concepts or using vague language.
|
||||
|
||||
## Using a Folder Structure
|
||||
|
||||
For projects with many rules, organize them in a `.clinerules/` folder:
|
||||
|
||||
```
|
||||
your-project/
|
||||
├── .clinerules/
|
||||
│ ├── 01-coding-standards.md
|
||||
│ ├── 02-documentation.md
|
||||
│ └── 03-testing.md
|
||||
├── src/
|
||||
└── ...
|
||||
```
|
||||
|
||||
Cline loads all Markdown files in `.clinerules/` automatically. The numeric prefixes help you control ordering, but they're optional.
|
||||
|
||||
### Organizing a Rules Bank
|
||||
|
||||
Maintain a separate folder for rules you might need but don't always want active:
|
||||
|
||||
```
|
||||
your-project/
|
||||
├── .clinerules/ # Active rules
|
||||
│ ├── 01-coding.md
|
||||
│ └── client-a.md
|
||||
│
|
||||
├── clinerules-bank/ # Available but inactive
|
||||
│ ├── clients/
|
||||
│ │ ├── client-a.md
|
||||
│ │ └── client-b.md
|
||||
│ └── frameworks/
|
||||
│ ├── react.md
|
||||
│ └── vue.md
|
||||
└── ...
|
||||
```
|
||||
|
||||
Copy files from the bank to `.clinerules/` when you need them. This keeps your active context lean while maintaining a library of reusable guidance.
|
||||
|
||||
Switch contexts with simple file operations:
|
||||
|
||||
```bash
|
||||
# Switch to Client B
|
||||
rm .clinerules/client-a.md
|
||||
cp clinerules-bank/clients/client-b.md .clinerules/
|
||||
```
|
||||
|
||||
<Tip>
|
||||
Consider git-ignoring `.clinerules/` while tracking `clinerules-bank/` so team members can activate the rules relevant to their current work.
|
||||
</Tip>
|
||||
|
||||
## Conditional Rules
|
||||
|
||||
Scope rules to specific file patterns using YAML frontmatter. This keeps React guidance out of Python code and backend rules away from frontend work.
|
||||
|
||||
```yaml
|
||||
---
|
||||
paths:
|
||||
- "src/components/**"
|
||||
- "src/hooks/**"
|
||||
---
|
||||
|
||||
# React Guidelines
|
||||
|
||||
Use functional components with hooks. Extract reusable logic into custom hooks.
|
||||
```
|
||||
|
||||
This rule activates only when working with files matching those patterns. Read the [Conditional Rules guide](/features/cline-rules/conditional-rules) for pattern syntax, behavior details, and more examples.
|
||||
|
||||
## Tips for Effective Rules
|
||||
|
||||
**Be specific**: "Use async/await for all database calls" beats "write good async code."
|
||||
|
||||
**Show patterns**: Include file paths and real examples. "Follow the error handling in `src/utils/errors.ts`" gives Cline a concrete reference.
|
||||
|
||||
**Focus on outcomes**: Describe what you want, not step-by-step instructions. Let Cline figure out how.
|
||||
|
||||
**Test and refine**: Start with core standards. Add rules when you find yourself repeating the same feedback.
|
||||
|
||||
**Use conditional rules**: Load guidance only when relevant. This keeps context efficient and reduces noise.
|
||||
|
||||
## Related
|
||||
|
||||
- [Conditional Rules](/features/cline-rules/conditional-rules) - Activate rules based on file patterns
|
||||
- [Skills](/features/skills) - Load instructions on demand with `/skill` command
|
||||
- [Workflows](/features/slash-commands/workflows/index) - Define explicit task automation
|
||||
- [New Rule Slash Command](/features/slash-commands/new-rule) - Generate rules with AI assistance
|
||||
- [Plan and Act Mode](/features/plan-and-act) - Use different rules for planning vs execution
|
||||
@@ -144,10 +144,6 @@ if (process.env.ERROR_SERVICE_API_KEY) {
|
||||
buildEnvVars["process.env.ERROR_SERVICE_API_KEY"] = JSON.stringify(process.env.ERROR_SERVICE_API_KEY)
|
||||
}
|
||||
|
||||
if (process.env.POSTHOG_TELEMETRY_ENABLED) {
|
||||
buildEnvVars["process.env.POSTHOG_TELEMETRY_ENABLED"] = JSON.stringify(process.env.POSTHOG_TELEMETRY_ENABLED)
|
||||
}
|
||||
|
||||
// OpenTelemetry configuration (injected at build time from GitHub secrets)
|
||||
// These provide production defaults that can be overridden at runtime via environment variables
|
||||
if (process.env.OTEL_TELEMETRY_ENABLED) {
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "claude-dev",
|
||||
"version": "3.52.0",
|
||||
"version": "3.55.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-dev",
|
||||
"version": "3.52.0",
|
||||
"version": "3.55.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.37.0",
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"name": "claude-dev",
|
||||
"displayName": "Cline",
|
||||
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
|
||||
"version": "3.52.0",
|
||||
"version": "3.55.0",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
|
||||
@@ -75,6 +75,19 @@ message McpResourceTemplate {
|
||||
optional string description = 4;
|
||||
}
|
||||
|
||||
message McpPromptArgument {
|
||||
string name = 1;
|
||||
optional string description = 2;
|
||||
optional bool required = 3;
|
||||
}
|
||||
|
||||
message McpPrompt {
|
||||
string name = 1;
|
||||
optional string title = 2;
|
||||
optional string description = 3;
|
||||
repeated McpPromptArgument arguments = 4;
|
||||
}
|
||||
|
||||
enum McpServerStatus {
|
||||
// Protobuf enums (in proto3) must have a zero value defined, which serves as the default if the field isn't explicitly set.
|
||||
// To align with the required nature of the TypeScript type and avoid an unnecessary UNSPECIFIED state, we map one of the existing statuses to this zero value.
|
||||
@@ -95,6 +108,7 @@ message McpServer {
|
||||
optional int32 timeout = 9;
|
||||
optional bool oauth_required = 10;
|
||||
optional string oauth_auth_status = 11;
|
||||
repeated McpPrompt prompts = 12;
|
||||
}
|
||||
|
||||
message McpServers {
|
||||
|
||||
@@ -106,11 +106,8 @@ message Secrets {
|
||||
// in src/shared/storage/state-keys.ts and use the scripts/generate-state-proto.mjs
|
||||
// script to regenerate this list.
|
||||
message Settings {
|
||||
reserved 152; // was hooks_enabled (removed - now always enabled on macOS/Linux)
|
||||
|
||||
optional string lite_llm_base_url = 1;
|
||||
optional bool lite_llm_use_prompt_cache = 2;
|
||||
map<string, string> open_ai_headers = 3;
|
||||
optional string anthropic_base_url = 4;
|
||||
optional string open_router_provider_sorting = 5;
|
||||
optional string aws_region = 6;
|
||||
@@ -279,6 +276,7 @@ message Settings {
|
||||
optional int32 open_telemetry_log_batch_timeout = 170;
|
||||
optional int32 open_telemetry_log_max_queue_size = 171;
|
||||
optional bool worktrees_enabled = 172;
|
||||
map<string, string> open_ai_headers = 173;
|
||||
}
|
||||
|
||||
message DictationSettings {
|
||||
@@ -385,7 +383,7 @@ message UpdateTaskSettingsRequest {
|
||||
|
||||
// Message for updating settings
|
||||
message UpdateSettingsRequest {
|
||||
reserved 26; // was hooks_enabled (removed - now always enabled on macOS/Linux)
|
||||
reserved 26; // was hooks_enabled (removed - now always enabled on macOS/Linux)
|
||||
|
||||
Metadata metadata = 1;
|
||||
optional ModelsApiConfiguration api_configuration = 2;
|
||||
|
||||
@@ -33,6 +33,9 @@ service EnvService {
|
||||
|
||||
// Initiates a graceful shutdown of the host bridge service.
|
||||
rpc shutdown(cline.EmptyRequest) returns (cline.Empty);
|
||||
|
||||
// Logs a debug message to the host environment's log/output console.
|
||||
rpc debugLog(cline.StringRequest) returns (cline.Empty);
|
||||
}
|
||||
|
||||
message GetHostVersionResponse {
|
||||
|
||||
@@ -0,0 +1,542 @@
|
||||
import { afterEach, beforeEach, describe, it } from "mocha"
|
||||
import "should"
|
||||
import fs from "fs/promises"
|
||||
import os from "os"
|
||||
import path from "path"
|
||||
import sinon from "sinon"
|
||||
import { ClineConfigurationError, ClineEndpoint, ClineEnv, Environment } from "../config"
|
||||
|
||||
describe("ClineEndpoint configuration", () => {
|
||||
let sandbox: sinon.SinonSandbox
|
||||
let tempDir: string
|
||||
let originalHomedir: typeof os.homedir
|
||||
|
||||
beforeEach(async () => {
|
||||
sandbox = sinon.createSandbox()
|
||||
tempDir = path.join(os.tmpdir(), `config-test-${Date.now()}-${Math.random().toString(36).slice(2)}`)
|
||||
await fs.mkdir(tempDir, { recursive: true })
|
||||
|
||||
// Create .cline directory
|
||||
await fs.mkdir(path.join(tempDir, ".cline"), { recursive: true })
|
||||
|
||||
// Stub os.homedir to return our temp directory
|
||||
originalHomedir = os.homedir
|
||||
sandbox
|
||||
.stub(os, "homedir")
|
||||
.returns(tempDir)
|
||||
|
||||
// Reset the singleton state using internal method
|
||||
;(ClineEndpoint as any)._instance = null
|
||||
;(ClineEndpoint as any)._initialized = false
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
sandbox.restore()
|
||||
// Reset singleton state
|
||||
;(ClineEndpoint as any)._instance = null
|
||||
;(ClineEndpoint as any)._initialized = false
|
||||
try {
|
||||
await fs.rm(tempDir, { recursive: true, force: true })
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
})
|
||||
|
||||
describe("valid config parsing", () => {
|
||||
it("should parse valid endpoints.json with all required fields", async () => {
|
||||
const validConfig = {
|
||||
appBaseUrl: "https://app.enterprise.com",
|
||||
apiBaseUrl: "https://api.enterprise.com",
|
||||
mcpBaseUrl: "https://mcp.enterprise.com",
|
||||
}
|
||||
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(validConfig), "utf8")
|
||||
|
||||
await ClineEndpoint.initialize()
|
||||
|
||||
const config = ClineEndpoint.config
|
||||
config.appBaseUrl.should.equal("https://app.enterprise.com")
|
||||
config.apiBaseUrl.should.equal("https://api.enterprise.com")
|
||||
config.mcpBaseUrl.should.equal("https://mcp.enterprise.com")
|
||||
config.environment.should.equal(Environment.selfHosted)
|
||||
})
|
||||
|
||||
it("should work without endpoints.json (standard mode)", async () => {
|
||||
// No endpoints.json file exists
|
||||
|
||||
await ClineEndpoint.initialize()
|
||||
|
||||
const config = ClineEndpoint.config
|
||||
config.environment.should.not.equal(Environment.selfHosted)
|
||||
// Should use production defaults
|
||||
config.appBaseUrl.should.equal("https://app.cline.bot")
|
||||
config.apiBaseUrl.should.equal("https://api.cline.bot")
|
||||
})
|
||||
|
||||
it("should accept URLs with ports", async () => {
|
||||
const validConfig = {
|
||||
appBaseUrl: "http://localhost:3000",
|
||||
apiBaseUrl: "http://localhost:7777",
|
||||
mcpBaseUrl: "http://localhost:8080/mcp",
|
||||
}
|
||||
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(validConfig), "utf8")
|
||||
|
||||
await ClineEndpoint.initialize()
|
||||
|
||||
const config = ClineEndpoint.config
|
||||
config.appBaseUrl.should.equal("http://localhost:3000")
|
||||
config.apiBaseUrl.should.equal("http://localhost:7777")
|
||||
config.mcpBaseUrl.should.equal("http://localhost:8080/mcp")
|
||||
})
|
||||
|
||||
it("should accept URLs with paths", async () => {
|
||||
const validConfig = {
|
||||
appBaseUrl: "https://proxy.enterprise.com/cline/app",
|
||||
apiBaseUrl: "https://proxy.enterprise.com/cline/api",
|
||||
mcpBaseUrl: "https://proxy.enterprise.com/cline/mcp",
|
||||
}
|
||||
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(validConfig), "utf8")
|
||||
|
||||
await ClineEndpoint.initialize()
|
||||
|
||||
const config = ClineEndpoint.config
|
||||
config.appBaseUrl.should.equal("https://proxy.enterprise.com/cline/app")
|
||||
})
|
||||
})
|
||||
|
||||
describe("invalid JSON handling", () => {
|
||||
it("should throw ClineConfigurationError for invalid JSON syntax", async () => {
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), "{ invalid json }", "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
error.message.should.containEql("Invalid JSON")
|
||||
}
|
||||
})
|
||||
|
||||
it("should throw ClineConfigurationError for truncated JSON", async () => {
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), '{"appBaseUrl": "https://test.com"', "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
error.message.should.containEql("Invalid JSON")
|
||||
}
|
||||
})
|
||||
|
||||
it("should throw ClineConfigurationError for empty file", async () => {
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), "", "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
}
|
||||
})
|
||||
|
||||
it("should throw ClineConfigurationError for non-object JSON", async () => {
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), '"just a string"', "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
error.message.should.containEql("must contain a JSON object")
|
||||
}
|
||||
})
|
||||
|
||||
it("should throw ClineConfigurationError for array JSON", async () => {
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), "[]", "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
// Arrays pass the object check but fail on required fields
|
||||
error.message.should.containEql("Missing required field")
|
||||
}
|
||||
})
|
||||
|
||||
it("should throw ClineConfigurationError for null JSON", async () => {
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), "null", "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
error.message.should.containEql("must contain a JSON object")
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("missing required fields", () => {
|
||||
it("should throw ClineConfigurationError when appBaseUrl is missing", async () => {
|
||||
const config = {
|
||||
apiBaseUrl: "https://api.enterprise.com",
|
||||
mcpBaseUrl: "https://mcp.enterprise.com",
|
||||
}
|
||||
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
error.message.should.containEql('Missing required field "appBaseUrl"')
|
||||
}
|
||||
})
|
||||
|
||||
it("should throw ClineConfigurationError when apiBaseUrl is missing", async () => {
|
||||
const config = {
|
||||
appBaseUrl: "https://app.enterprise.com",
|
||||
mcpBaseUrl: "https://mcp.enterprise.com",
|
||||
}
|
||||
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
error.message.should.containEql('Missing required field "apiBaseUrl"')
|
||||
}
|
||||
})
|
||||
|
||||
it("should throw ClineConfigurationError when mcpBaseUrl is missing", async () => {
|
||||
const config = {
|
||||
appBaseUrl: "https://app.enterprise.com",
|
||||
apiBaseUrl: "https://api.enterprise.com",
|
||||
}
|
||||
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
error.message.should.containEql('Missing required field "mcpBaseUrl"')
|
||||
}
|
||||
})
|
||||
|
||||
it("should throw ClineConfigurationError when all fields are missing", async () => {
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), "{}", "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
error.message.should.containEql("Missing required field")
|
||||
}
|
||||
})
|
||||
|
||||
it("should throw ClineConfigurationError when field is null", async () => {
|
||||
const config = {
|
||||
appBaseUrl: null,
|
||||
apiBaseUrl: "https://api.enterprise.com",
|
||||
mcpBaseUrl: "https://mcp.enterprise.com",
|
||||
}
|
||||
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
error.message.should.containEql('Missing required field "appBaseUrl"')
|
||||
}
|
||||
})
|
||||
|
||||
it("should throw ClineConfigurationError when field is empty string", async () => {
|
||||
const config = {
|
||||
appBaseUrl: "",
|
||||
apiBaseUrl: "https://api.enterprise.com",
|
||||
mcpBaseUrl: "https://mcp.enterprise.com",
|
||||
}
|
||||
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
error.message.should.containEql("cannot be empty")
|
||||
}
|
||||
})
|
||||
|
||||
it("should throw ClineConfigurationError when field is whitespace only", async () => {
|
||||
const config = {
|
||||
appBaseUrl: " ",
|
||||
apiBaseUrl: "https://api.enterprise.com",
|
||||
mcpBaseUrl: "https://mcp.enterprise.com",
|
||||
}
|
||||
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
error.message.should.containEql("cannot be empty")
|
||||
}
|
||||
})
|
||||
|
||||
it("should throw ClineConfigurationError when field is non-string", async () => {
|
||||
const config = {
|
||||
appBaseUrl: 12345,
|
||||
apiBaseUrl: "https://api.enterprise.com",
|
||||
mcpBaseUrl: "https://mcp.enterprise.com",
|
||||
}
|
||||
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
error.message.should.containEql("must be a string")
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("invalid URL detection", () => {
|
||||
it("should throw ClineConfigurationError for invalid URL format", async () => {
|
||||
const config = {
|
||||
appBaseUrl: "not-a-valid-url",
|
||||
apiBaseUrl: "https://api.enterprise.com",
|
||||
mcpBaseUrl: "https://mcp.enterprise.com",
|
||||
}
|
||||
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
error.message.should.containEql("must be a valid URL")
|
||||
}
|
||||
})
|
||||
|
||||
it("should throw ClineConfigurationError for URL without protocol", async () => {
|
||||
const config = {
|
||||
appBaseUrl: "app.enterprise.com",
|
||||
apiBaseUrl: "https://api.enterprise.com",
|
||||
mcpBaseUrl: "https://mcp.enterprise.com",
|
||||
}
|
||||
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
error.message.should.containEql("must be a valid URL")
|
||||
}
|
||||
})
|
||||
|
||||
it("should throw ClineConfigurationError for malformed URL", async () => {
|
||||
const config = {
|
||||
appBaseUrl: "https://",
|
||||
apiBaseUrl: "https://api.enterprise.com",
|
||||
mcpBaseUrl: "https://mcp.enterprise.com",
|
||||
}
|
||||
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
error.message.should.containEql("must be a valid URL")
|
||||
}
|
||||
})
|
||||
|
||||
it("should include the invalid URL value in error message", async () => {
|
||||
const invalidUrl = "definitely-not-a-url"
|
||||
const config = {
|
||||
appBaseUrl: invalidUrl,
|
||||
apiBaseUrl: "https://api.enterprise.com",
|
||||
mcpBaseUrl: "https://mcp.enterprise.com",
|
||||
}
|
||||
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
error.message.should.containEql(invalidUrl)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("environment switching blocked in self-hosted mode", () => {
|
||||
it("should throw error when trying to change environment in self-hosted mode", async () => {
|
||||
const config = {
|
||||
appBaseUrl: "https://app.enterprise.com",
|
||||
apiBaseUrl: "https://api.enterprise.com",
|
||||
mcpBaseUrl: "https://mcp.enterprise.com",
|
||||
}
|
||||
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
|
||||
|
||||
await ClineEndpoint.initialize()
|
||||
|
||||
// Verify we're in self-hosted mode
|
||||
ClineEndpoint.config.environment.should.equal(Environment.selfHosted)
|
||||
|
||||
// Try to change environment - should throw
|
||||
try {
|
||||
ClineEnv.setEnvironment("staging")
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.message.should.containEql("Cannot change environment in on-premise mode")
|
||||
}
|
||||
})
|
||||
|
||||
it("should throw error for all environment values in self-hosted mode", async () => {
|
||||
const config = {
|
||||
appBaseUrl: "https://app.enterprise.com",
|
||||
apiBaseUrl: "https://api.enterprise.com",
|
||||
mcpBaseUrl: "https://mcp.enterprise.com",
|
||||
}
|
||||
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
|
||||
|
||||
await ClineEndpoint.initialize()
|
||||
|
||||
const environments = ["staging", "local", "production", "anything"]
|
||||
for (const env of environments) {
|
||||
try {
|
||||
ClineEnv.setEnvironment(env)
|
||||
throw new Error(`Should have thrown for environment: ${env}`)
|
||||
} catch (error: any) {
|
||||
error.message.should.containEql("Cannot change environment in on-premise mode")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it("should allow environment switching in standard mode", async () => {
|
||||
// No endpoints.json file - standard mode
|
||||
|
||||
await ClineEndpoint.initialize()
|
||||
|
||||
// Verify we're NOT in self-hosted mode
|
||||
ClineEndpoint.config.environment.should.not.equal(Environment.selfHosted)
|
||||
|
||||
// Should be able to change environment
|
||||
ClineEnv.setEnvironment("staging")
|
||||
ClineEnv.getEnvironment().environment.should.equal("staging")
|
||||
|
||||
ClineEnv.setEnvironment("local")
|
||||
ClineEnv.getEnvironment().environment.should.equal("local")
|
||||
|
||||
ClineEnv.setEnvironment("production")
|
||||
ClineEnv.getEnvironment().environment.should.equal("production")
|
||||
})
|
||||
})
|
||||
|
||||
describe("self-hosted mode behavior", () => {
|
||||
it("should report selfHosted environment in self-hosted mode", async () => {
|
||||
const config = {
|
||||
appBaseUrl: "https://app.enterprise.com",
|
||||
apiBaseUrl: "https://api.enterprise.com",
|
||||
mcpBaseUrl: "https://mcp.enterprise.com",
|
||||
}
|
||||
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
|
||||
|
||||
await ClineEndpoint.initialize()
|
||||
|
||||
const envConfig = ClineEndpoint.config
|
||||
envConfig.environment.should.equal(Environment.selfHosted)
|
||||
})
|
||||
|
||||
it("should use custom endpoints from file", async () => {
|
||||
const customConfig = {
|
||||
appBaseUrl: "https://custom-app.internal",
|
||||
apiBaseUrl: "https://custom-api.internal",
|
||||
mcpBaseUrl: "https://custom-mcp.internal/v1",
|
||||
}
|
||||
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(customConfig), "utf8")
|
||||
|
||||
await ClineEndpoint.initialize()
|
||||
|
||||
const config = ClineEndpoint.config
|
||||
config.appBaseUrl.should.equal("https://custom-app.internal")
|
||||
config.apiBaseUrl.should.equal("https://custom-api.internal")
|
||||
config.mcpBaseUrl.should.equal("https://custom-mcp.internal/v1")
|
||||
})
|
||||
})
|
||||
|
||||
describe("initialization behavior", () => {
|
||||
it("should only initialize once", async () => {
|
||||
await ClineEndpoint.initialize()
|
||||
ClineEndpoint.isInitialized().should.be.true()
|
||||
|
||||
// Second initialize should be a no-op
|
||||
await ClineEndpoint.initialize()
|
||||
ClineEndpoint.isInitialized().should.be.true()
|
||||
})
|
||||
|
||||
it("should throw error when accessing config before initialization", async () => {
|
||||
// Already reset in beforeEach, so accessing should throw
|
||||
try {
|
||||
const _ = ClineEndpoint.config
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.message.should.containEql("not initialized")
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("isSelfHosted() method", () => {
|
||||
it("should return true when not initialized (safety fallback)", async () => {
|
||||
// Reset singleton state - already done in beforeEach, not initialized
|
||||
ClineEndpoint.isInitialized().should.be.false()
|
||||
ClineEndpoint.isSelfHosted().should.be.true()
|
||||
})
|
||||
|
||||
it("should return true when in self-hosted mode", async () => {
|
||||
const config = {
|
||||
appBaseUrl: "https://app.enterprise.com",
|
||||
apiBaseUrl: "https://api.enterprise.com",
|
||||
mcpBaseUrl: "https://mcp.enterprise.com",
|
||||
}
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
|
||||
await ClineEndpoint.initialize()
|
||||
|
||||
ClineEndpoint.isSelfHosted().should.be.true()
|
||||
})
|
||||
|
||||
it("should return false when in normal mode (no endpoints.json)", async () => {
|
||||
// No endpoints.json file exists
|
||||
await ClineEndpoint.initialize()
|
||||
|
||||
ClineEndpoint.isSelfHosted().should.be.false()
|
||||
})
|
||||
})
|
||||
})
|
||||
+19
-5
@@ -7,10 +7,10 @@ import {
|
||||
migrateWorkspaceToGlobalStorage,
|
||||
} from "./core/storage/state-migrations"
|
||||
import { WebviewProvider } from "./core/webview"
|
||||
import { Logger } from "./services/logging/Logger"
|
||||
import "./utils/path" // necessary to have access to String.prototype.toPosix
|
||||
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { FileContextTracker } from "./core/context/context-tracking/FileContextTracker"
|
||||
import { StateManager } from "./core/storage/StateManager"
|
||||
import { openAiCodexOAuthManager } from "./integrations/openai-codex/oauth"
|
||||
@@ -27,17 +27,29 @@ import { syncWorker } from "./shared/services/worker/sync"
|
||||
import { getBlobStoreSettingsFromEnv } from "./shared/services/worker/worker"
|
||||
import { getLatestAnnouncementId } from "./utils/announcements"
|
||||
import { arePathsEqual } from "./utils/path"
|
||||
|
||||
/**
|
||||
* Performs intialization for Cline that is common to all platforms.
|
||||
*
|
||||
* @param context
|
||||
* @returns The webview provider
|
||||
* @throws ClineConfigurationError if endpoints.json exists but is invalid
|
||||
*/
|
||||
export async function initialize(context: vscode.ExtensionContext): Promise<WebviewProvider> {
|
||||
// Configure the shared Logging class to use HostProvider's output channels and debug logger
|
||||
Logger.subscribe((msg: string) => HostProvider.get().logToChannel(msg)) // File system logging
|
||||
Logger.subscribe((msg: string) => HostProvider.env.debugLog({ value: msg })) // Host debug logging
|
||||
|
||||
// Initialize ClineEndpoint configuration first (reads ~/.cline/endpoints.json if present)
|
||||
// This must be done before any other code that calls ClineEnv.config()
|
||||
// Throws ClineConfigurationError if config file exists but is invalid
|
||||
const { ClineEndpoint } = await import("./config")
|
||||
await ClineEndpoint.initialize()
|
||||
|
||||
try {
|
||||
await StateManager.initialize(context)
|
||||
} catch (error) {
|
||||
console.error("[Controller] CRITICAL: Failed to initialize StateManager - extension may not function properly:", error)
|
||||
Logger.error("[Controller] CRITICAL: Failed to initialize StateManager - extension may not function properly:", error)
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Failed to initialize Cline's application state. Please restart the extension.",
|
||||
@@ -50,8 +62,10 @@ export async function initialize(context: vscode.ExtensionContext): Promise<Webv
|
||||
// Set the distinct ID for logging and telemetry
|
||||
await initializeDistinctId(context)
|
||||
|
||||
// Initialize PostHog client provider
|
||||
PostHogClientProvider.getInstance()
|
||||
// Initialize PostHog client provider (skip in self-hosted mode)
|
||||
if (!ClineEndpoint.isSelfHosted()) {
|
||||
PostHogClientProvider.getInstance()
|
||||
}
|
||||
|
||||
// Setup the external services
|
||||
await ErrorService.initialize()
|
||||
@@ -123,7 +137,7 @@ async function showVersionUpdateAnnouncement(context: vscode.ExtensionContext) {
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
console.error(`Error during post-update actions: ${errorMessage}, Stack trace: ${error.stack}`)
|
||||
Logger.error(`Error during post-update actions: ${errorMessage}, Stack trace: ${error.stack}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+227
-14
@@ -1,22 +1,38 @@
|
||||
export enum Environment {
|
||||
production = "production",
|
||||
staging = "staging",
|
||||
local = "local",
|
||||
}
|
||||
import * as fs from "fs/promises"
|
||||
import * as os from "os"
|
||||
import * as path from "path"
|
||||
import { Environment, type EnvironmentConfig } from "./shared/config-types"
|
||||
import { Logger } from "./shared/services/Logger"
|
||||
|
||||
export interface EnvironmentConfig {
|
||||
environment: Environment
|
||||
export { Environment, type EnvironmentConfig }
|
||||
|
||||
/**
|
||||
* Schema for the endpoints.json configuration file used in on-premise deployments.
|
||||
* All fields are required and must be valid URLs.
|
||||
*/
|
||||
interface EndpointsFileSchema {
|
||||
appBaseUrl: string
|
||||
apiBaseUrl: string
|
||||
mcpBaseUrl: string
|
||||
}
|
||||
|
||||
class ClineEndpoint {
|
||||
public static instance = new ClineEndpoint()
|
||||
public static get config() {
|
||||
return ClineEndpoint.instance.config()
|
||||
/**
|
||||
* Error thrown when the Cline configuration file exists but is invalid.
|
||||
* This error prevents Cline from starting to avoid misconfiguration in enterprise environments.
|
||||
*/
|
||||
export class ClineConfigurationError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message)
|
||||
this.name = "ClineConfigurationError"
|
||||
}
|
||||
}
|
||||
|
||||
class ClineEndpoint {
|
||||
private static _instance: ClineEndpoint | null = null
|
||||
private static _initialized = false
|
||||
|
||||
// On-premise config loaded from file (null if not on-premise)
|
||||
private onPremiseConfig: EndpointsFileSchema | null = null
|
||||
private environment: Environment = Environment.production
|
||||
|
||||
private constructor() {
|
||||
@@ -24,15 +40,189 @@ class ClineEndpoint {
|
||||
const _env = process?.env?.CLINE_ENVIRONMENT_OVERRIDE || process?.env?.CLINE_ENVIRONMENT
|
||||
if (_env && Object.values(Environment).includes(_env as Environment)) {
|
||||
this.environment = _env as Environment
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the ClineEndpoint singleton.
|
||||
* Must be called before any other methods.
|
||||
* Reads the endpoints.json file if it exists and validates its schema.
|
||||
*
|
||||
* @throws ClineConfigurationError if the endpoints.json file exists but is invalid
|
||||
*/
|
||||
public static async initialize(): Promise<void> {
|
||||
if (ClineEndpoint._initialized) {
|
||||
return
|
||||
}
|
||||
|
||||
ClineEndpoint._instance = new ClineEndpoint()
|
||||
|
||||
// Try to load on-premise config from file
|
||||
const endpointsConfig = await ClineEndpoint.loadEndpointsFile()
|
||||
if (endpointsConfig) {
|
||||
ClineEndpoint._instance.onPremiseConfig = endpointsConfig
|
||||
Logger.log("Cline running in self-hosted mode with custom endpoints")
|
||||
}
|
||||
|
||||
ClineEndpoint._initialized = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the ClineEndpoint has been initialized.
|
||||
*/
|
||||
public static isInitialized(): boolean {
|
||||
return ClineEndpoint._initialized
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if Cline is running in self-hosted/on-premise mode.
|
||||
* @returns true if in selfHosted mode, or true if not initialized (safety fallback to prevent accidental external calls)
|
||||
*/
|
||||
public static isSelfHosted(): boolean {
|
||||
// Safety fallback: if not initialized, treat as selfHosted
|
||||
// to prevent accidental external service calls before configuration is loaded
|
||||
if (!ClineEndpoint._initialized) {
|
||||
return true
|
||||
}
|
||||
return ClineEndpoint.config.environment === Environment.selfHosted
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the singleton instance.
|
||||
* @throws Error if not initialized
|
||||
*/
|
||||
public static get instance(): ClineEndpoint {
|
||||
if (!ClineEndpoint._initialized || !ClineEndpoint._instance) {
|
||||
throw new Error("ClineEndpoint not initialized. Call ClineEndpoint.initialize() first.")
|
||||
}
|
||||
return ClineEndpoint._instance
|
||||
}
|
||||
|
||||
/**
|
||||
* Static getter for convenient access to the current configuration.
|
||||
* @throws Error if not initialized
|
||||
*/
|
||||
public static get config(): EnvironmentConfig {
|
||||
return ClineEndpoint.instance.config()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the endpoints.json configuration file.
|
||||
* Located at ~/.cline/endpoints.json
|
||||
*/
|
||||
private static getEndpointsFilePath(): string {
|
||||
return path.join(os.homedir(), ".cline", "endpoints.json")
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads and validates the endpoints.json file.
|
||||
* @returns The validated endpoints config, or null if the file doesn't exist
|
||||
* @throws ClineConfigurationError if the file exists but is invalid
|
||||
*/
|
||||
private static async loadEndpointsFile(): Promise<EndpointsFileSchema | null> {
|
||||
const filePath = ClineEndpoint.getEndpointsFilePath()
|
||||
|
||||
try {
|
||||
await fs.access(filePath)
|
||||
} catch {
|
||||
// File doesn't exist - not on-premise mode
|
||||
return null
|
||||
}
|
||||
|
||||
// File exists, must be valid or we fail
|
||||
try {
|
||||
const fileContent = await fs.readFile(filePath, "utf8")
|
||||
let data: unknown
|
||||
|
||||
try {
|
||||
data = JSON.parse(fileContent)
|
||||
} catch (parseError) {
|
||||
throw new ClineConfigurationError(
|
||||
`Invalid JSON in endpoints configuration file (${filePath}): ${parseError instanceof Error ? parseError.message : String(parseError)}`,
|
||||
)
|
||||
}
|
||||
|
||||
return ClineEndpoint.validateEndpointsSchema(data, filePath)
|
||||
} catch (error) {
|
||||
if (error instanceof ClineConfigurationError) {
|
||||
throw error
|
||||
}
|
||||
throw new ClineConfigurationError(
|
||||
`Failed to read endpoints configuration file (${filePath}): ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the provided data matches the EndpointsFileSchema.
|
||||
* All fields must be present and be valid URLs.
|
||||
*
|
||||
* @param data The parsed JSON data to validate
|
||||
* @param filePath The path to the file (for error messages)
|
||||
* @returns The validated EndpointsFileSchema
|
||||
* @throws ClineConfigurationError if validation fails
|
||||
*/
|
||||
private static validateEndpointsSchema(data: unknown, filePath: string): EndpointsFileSchema {
|
||||
if (typeof data !== "object" || data === null) {
|
||||
throw new ClineConfigurationError(`Endpoints configuration file (${filePath}) must contain a JSON object`)
|
||||
}
|
||||
|
||||
const obj = data as Record<string, unknown>
|
||||
const requiredFields = ["appBaseUrl", "apiBaseUrl", "mcpBaseUrl"] as const
|
||||
const result: Partial<EndpointsFileSchema> = {}
|
||||
|
||||
for (const field of requiredFields) {
|
||||
const value = obj[field]
|
||||
|
||||
if (value === undefined || value === null) {
|
||||
throw new ClineConfigurationError(
|
||||
`Missing required field "${field}" in endpoints configuration file (${filePath})`,
|
||||
)
|
||||
}
|
||||
|
||||
if (typeof value !== "string") {
|
||||
throw new ClineConfigurationError(
|
||||
`Field "${field}" in endpoints configuration file (${filePath}) must be a string`,
|
||||
)
|
||||
}
|
||||
|
||||
if (!value.trim()) {
|
||||
throw new ClineConfigurationError(
|
||||
`Field "${field}" in endpoints configuration file (${filePath}) cannot be empty`,
|
||||
)
|
||||
}
|
||||
|
||||
// Validate URL format
|
||||
try {
|
||||
new URL(value)
|
||||
} catch {
|
||||
throw new ClineConfigurationError(
|
||||
`Field "${field}" in endpoints configuration file (${filePath}) must be a valid URL. Got: "${value}"`,
|
||||
)
|
||||
}
|
||||
|
||||
result[field] = value
|
||||
}
|
||||
|
||||
return result as EndpointsFileSchema
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current environment configuration.
|
||||
*/
|
||||
public config(): EnvironmentConfig {
|
||||
return this.getEnvironment()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current environment.
|
||||
* @throws Error if in on-premise mode (environment switching is disabled)
|
||||
*/
|
||||
public setEnvironment(env: string) {
|
||||
if (this.onPremiseConfig) {
|
||||
throw new Error("Cannot change environment in on-premise mode. Endpoints are configured via ~/.cline/endpoints.json")
|
||||
}
|
||||
|
||||
switch (env.toLowerCase()) {
|
||||
case "staging":
|
||||
this.environment = Environment.staging
|
||||
@@ -44,10 +234,24 @@ class ClineEndpoint {
|
||||
this.environment = Environment.production
|
||||
break
|
||||
}
|
||||
console.info("Cline environment updated: ", this.environment)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current environment configuration.
|
||||
* If running in on-premise mode, returns the custom endpoints.
|
||||
*/
|
||||
public getEnvironment(): EnvironmentConfig {
|
||||
// On-premise mode: use custom endpoints from file
|
||||
if (this.onPremiseConfig) {
|
||||
return {
|
||||
environment: Environment.selfHosted,
|
||||
appBaseUrl: this.onPremiseConfig.appBaseUrl,
|
||||
apiBaseUrl: this.onPremiseConfig.apiBaseUrl,
|
||||
mcpBaseUrl: this.onPremiseConfig.mcpBaseUrl,
|
||||
}
|
||||
}
|
||||
|
||||
// Standard mode: use built-in environment URLs
|
||||
switch (this.environment) {
|
||||
case Environment.staging:
|
||||
return {
|
||||
@@ -79,5 +283,14 @@ class ClineEndpoint {
|
||||
* Usage:
|
||||
* - ClineEnv.config() to get the current config.
|
||||
* - ClineEnv.setEnvironment(Environment.local) to change the environment.
|
||||
*
|
||||
* IMPORTANT: ClineEndpoint.initialize() must be called before using ClineEnv.
|
||||
*/
|
||||
export const ClineEnv = ClineEndpoint.instance
|
||||
export const ClineEnv = {
|
||||
config: () => ClineEndpoint.config,
|
||||
setEnvironment: (env: string) => ClineEndpoint.instance.setEnvironment(env),
|
||||
getEnvironment: () => ClineEndpoint.instance.getEnvironment(),
|
||||
}
|
||||
|
||||
// Export the class for initialization
|
||||
export { ClineEndpoint }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ApiConfiguration, ModelInfo, QwenApiRegions } from "@shared/api"
|
||||
import { Mode } from "@shared/storage/types"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ClineTool } from "@/shared/tools"
|
||||
import { AIhubmixHandler } from "./providers/aihubmix"
|
||||
import { AnthropicHandler } from "./providers/anthropic"
|
||||
@@ -480,7 +481,7 @@ export function buildApiHandler(configuration: ApiConfiguration, mode: Mode): Ap
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("buildApiHandler error:", error)
|
||||
Logger.error("buildApiHandler error:", error)
|
||||
}
|
||||
|
||||
return createHandlerForProvider(apiProvider, options, mode)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AskSageModelId, askSageDefaultModelId, askSageDefaultURL, askSageModels, ModelInfo } from "@shared/api"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from ".."
|
||||
import { withRetry } from "../retry"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
@@ -58,7 +59,7 @@ export class AskSageHandler implements ApiHandler {
|
||||
private apiKey: string
|
||||
|
||||
constructor(options: AskSageHandlerOptions) {
|
||||
console.log("init api url", options.asksageApiUrl, askSageDefaultURL)
|
||||
Logger.log("init api url", options.asksageApiUrl, askSageDefaultURL)
|
||||
this.options = options
|
||||
this.apiKey = options.asksageApiKey || ""
|
||||
this.apiUrl = options.asksageApiUrl || askSageDefaultURL
|
||||
@@ -164,7 +165,7 @@ export class AskSageHandler implements ApiHandler {
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
console.error("Failed to fetch AskSage usage", await response.text())
|
||||
Logger.error("Failed to fetch AskSage usage", await response.text())
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -177,7 +178,7 @@ export class AskSageHandler implements ApiHandler {
|
||||
outputTokens: 0,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching AskSage usage:", error)
|
||||
Logger.error("Error fetching AskSage usage:", error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { BedrockModelId, bedrockDefaultModelId, bedrockModels, CLAUDE_SONNET_1M_
|
||||
import { calculateApiCostOpenAI, calculateApiCostQwen } from "@utils/cost"
|
||||
import { ExtensionRegistryInfo } from "@/registry"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToR1Format } from "../transform/r1-format"
|
||||
@@ -109,9 +110,13 @@ interface ProviderChainOptions {
|
||||
profile?: string
|
||||
}
|
||||
|
||||
// a special jp inference profile was created for sonnet 4.5
|
||||
// a special jp inference profile was created for sonnet 4.5 & haiku 4.5
|
||||
// https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-support.html
|
||||
const JP_SUPPORTED_CRIS_MODELS = ["anthropic.claude-sonnet-4-5-20250929-v1:0", "anthropic.claude-sonnet-4-5-20250929-v1:0:1m"]
|
||||
const JP_SUPPORTED_CRIS_MODELS = [
|
||||
"anthropic.claude-sonnet-4-5-20250929-v1:0",
|
||||
"anthropic.claude-sonnet-4-5-20250929-v1:0:1m",
|
||||
"anthropic.claude-haiku-4-5-20251001-v1:0",
|
||||
]
|
||||
|
||||
// https://docs.anthropic.com/en/api/claude-on-amazon-bedrock
|
||||
export class AwsBedrockHandler implements ApiHandler {
|
||||
@@ -449,7 +454,7 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error parsing Deepseek response chunk:", error)
|
||||
Logger.error("Error parsing Deepseek response chunk:", error)
|
||||
// Propagate the error by yielding a text response with error information
|
||||
yield {
|
||||
type: "text",
|
||||
@@ -550,7 +555,7 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
|
||||
for await (const chunk of response.stream) {
|
||||
// Debug logging to see actual response structure
|
||||
// console.log("Bedrock chunk:", JSON.stringify(chunk, null, 2))
|
||||
// Logger.log("Bedrock chunk:", JSON.stringify(chunk, null, 2))
|
||||
|
||||
// Handle thinking response in additionalModelResponseFields (LangChain format)
|
||||
const metadata = chunk.metadata as ExtendedMetadata | undefined
|
||||
@@ -854,7 +859,7 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
// Log unsupported content types for debugging
|
||||
console.warn(`Unsupported content type: ${(item as ContentItem).type}`)
|
||||
Logger.warn(`Unsupported content type: ${(item as ContentItem).type}`)
|
||||
return null
|
||||
})
|
||||
.filter((item): item is ContentBlock => item !== null)
|
||||
@@ -912,7 +917,7 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to process image content:", error)
|
||||
Logger.error("Failed to process image content:", error)
|
||||
// Return a text content indicating the error instead of null
|
||||
// This ensures users are aware of the issue
|
||||
return {
|
||||
@@ -1111,7 +1116,7 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error with OpenAI model via Converse API:", error)
|
||||
Logger.error("Error with OpenAI model via Converse API:", error)
|
||||
|
||||
// Try to extract more detailed error information
|
||||
let errorMessage = "Failed to process OpenAI model request"
|
||||
@@ -1246,7 +1251,7 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error with Qwen model via Converse API:", error)
|
||||
Logger.error("Error with Qwen model via Converse API:", error)
|
||||
|
||||
// Try to extract more detailed error information
|
||||
let errorMessage = "Failed to process Qwen model request"
|
||||
|
||||
@@ -112,10 +112,11 @@ export class CerebrasHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
try {
|
||||
const model = this.getModel()
|
||||
const stream = await client.chat.completions.create({
|
||||
model: this.getModel().id,
|
||||
model: model.id,
|
||||
messages: cerebrasMessages,
|
||||
temperature: 0,
|
||||
temperature: model.info.temperature ?? 0,
|
||||
stream: true,
|
||||
max_tokens: CEREBRAS_DEFAULT_MAX_TOKENS,
|
||||
})
|
||||
|
||||
@@ -7,10 +7,10 @@ import { ClineEnv } from "@/config"
|
||||
import { ClineAccountService } from "@/services/account/ClineAccountService"
|
||||
import { AuthService } from "@/services/auth/AuthService"
|
||||
import { buildClineExtraHeaders } from "@/services/EnvUtils"
|
||||
import { Logger } from "@/services/logging/Logger"
|
||||
import { CLINE_ACCOUNT_AUTH_ERROR_MESSAGE } from "@/shared/ClineAccount"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch, getAxiosSettings } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { createOpenRouterStream } from "../transform/openrouter-stream"
|
||||
@@ -35,10 +35,13 @@ export class ClineHandler implements ApiHandler {
|
||||
private clineAccountService = ClineAccountService.getInstance()
|
||||
private _authService: AuthService
|
||||
private client: OpenAI | undefined
|
||||
private readonly _baseUrl = ClineEnv.config().apiBaseUrl
|
||||
lastGenerationId?: string
|
||||
private lastRequestId?: string
|
||||
|
||||
private get _baseUrl(): string {
|
||||
return ClineEnv.config().apiBaseUrl
|
||||
}
|
||||
|
||||
constructor(options: ClineHandlerOptions) {
|
||||
this.options = options
|
||||
this._authService = AuthService.getInstance()
|
||||
@@ -126,7 +129,7 @@ export class ClineHandler implements ApiHandler {
|
||||
// openrouter returns an error object instead of the openai sdk throwing an error
|
||||
if ("error" in chunk) {
|
||||
const error = chunk.error as OpenRouterErrorResponse["error"]
|
||||
console.error(`Cline API Error: ${error?.code} - ${error?.message}`)
|
||||
Logger.error(`Cline API Error: ${error?.code} - ${error?.message}`)
|
||||
// Include metadata in the error message if available
|
||||
const metadataStr = error.metadata ? `\nMetadata: ${JSON.stringify(error.metadata, null, 2)}` : ""
|
||||
throw new Error(`Cline API Error ${error.code}: ${error.message}${metadataStr}`)
|
||||
@@ -143,7 +146,7 @@ export class ClineHandler implements ApiHandler {
|
||||
const choiceWithError = choice as any
|
||||
if (choiceWithError.error) {
|
||||
const error = choiceWithError.error
|
||||
console.error(`Cline Mid-Stream Error: ${error.code || error.type || "Unknown"} - ${error.message}`)
|
||||
Logger.error(`Cline Mid-Stream Error: ${error.code || error.type || "Unknown"} - ${error.message}`)
|
||||
throw new Error(`Cline Mid-Stream Error: ${error.code || error.type || "Unknown"} - ${error.message}`)
|
||||
} else {
|
||||
throw new Error(
|
||||
@@ -199,7 +202,7 @@ export class ClineHandler implements ApiHandler {
|
||||
// @ts-ignore-next-line
|
||||
let totalCost = (chunk.usage.cost || 0) + (chunk.usage.cost_details?.upstream_inference_cost || 0)
|
||||
|
||||
if (["x-ai/grok-code-fast-1", "kwaipilot/kat-coder-pro"].includes(this.getModel().id)) {
|
||||
if (["kwaipilot/kat-coder-pro"].includes(this.getModel().id)) {
|
||||
totalCost = 0
|
||||
}
|
||||
|
||||
@@ -217,14 +220,14 @@ export class ClineHandler implements ApiHandler {
|
||||
|
||||
// Fallback to generation endpoint if usage chunk not returned
|
||||
if (!didOutputUsage) {
|
||||
console.warn("Cline API did not return usage chunk, fetching from generation endpoint")
|
||||
Logger.warn("Cline API did not return usage chunk, fetching from generation endpoint")
|
||||
const apiStreamUsage = await this.getApiStreamUsage()
|
||||
if (apiStreamUsage) {
|
||||
yield apiStreamUsage
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Cline API Error:", error)
|
||||
Logger.error("Cline API Error:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -260,7 +263,7 @@ export class ClineHandler implements ApiHandler {
|
||||
}
|
||||
} catch (error) {
|
||||
// ignore if fails
|
||||
console.error("Error fetching cline generation details:", error)
|
||||
Logger.error("Error fetching cline generation details:", error)
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
|
||||
@@ -7,7 +7,7 @@ import { fetch } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { convertToR1Format } from "../transform/r1-format"
|
||||
import { addReasoningContent } from "../transform/r1-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { getOpenAIToolParams, ToolCallProcessor } from "../transform/tool-call-processor"
|
||||
|
||||
@@ -81,14 +81,10 @@ export class DeepSeekHandler implements ApiHandler {
|
||||
|
||||
const isDeepseekReasoner = model.id.includes("deepseek-reasoner")
|
||||
|
||||
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
|
||||
if (isDeepseekReasoner) {
|
||||
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
|
||||
}
|
||||
const convertedMessages = convertToOpenAiMessages(messages)
|
||||
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = isDeepseekReasoner
|
||||
? [{ role: "system", content: systemPrompt }, ...addReasoningContent(convertedMessages, messages)]
|
||||
: [{ role: "system", content: systemPrompt }, ...convertedMessages]
|
||||
|
||||
const stream = await client.chat.completions.create({
|
||||
model: model.id,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ModelInfo } from "../../../shared/api"
|
||||
import { ApiHandler } from "../index"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
@@ -84,7 +85,7 @@ export class DifyHandler implements ApiHandler {
|
||||
this.apiKey = options.difyApiKey || ""
|
||||
this.baseUrl = options.difyBaseUrl || ""
|
||||
|
||||
console.log("[DIFY DEBUG] Constructor called with:", {
|
||||
Logger.log("[DIFY DEBUG] Constructor called with:", {
|
||||
hasApiKey: !!this.apiKey,
|
||||
baseUrl: this.baseUrl,
|
||||
})
|
||||
@@ -98,7 +99,7 @@ export class DifyHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[]): ApiStream {
|
||||
console.log("[DIFY DEBUG] createMessage called with:", {
|
||||
Logger.log("[DIFY DEBUG] createMessage called with:", {
|
||||
systemPromptLength: systemPrompt?.length || 0,
|
||||
messagesCount: messages?.length || 0,
|
||||
})
|
||||
@@ -115,8 +116,8 @@ export class DifyHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
const fullUrl = `${this.baseUrl}/chat-messages`
|
||||
console.log("[DIFY DEBUG] Making request to:", fullUrl)
|
||||
console.log("[DIFY DEBUG] Request body:", JSON.stringify(requestBody, null, 2))
|
||||
Logger.log("[DIFY DEBUG] Making request to:", fullUrl)
|
||||
Logger.log("[DIFY DEBUG] Request body:", JSON.stringify(requestBody, null, 2))
|
||||
|
||||
let response: Response
|
||||
try {
|
||||
@@ -129,21 +130,21 @@ export class DifyHandler implements ApiHandler {
|
||||
body: JSON.stringify(requestBody),
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.error("[DIFY DEBUG] Network error during fetch:", error)
|
||||
Logger.error("[DIFY DEBUG] Network error during fetch:", error)
|
||||
const cause = error.cause ? ` | Cause: ${error.cause}` : ""
|
||||
throw new Error(`Dify API network error: ${error.message}${cause}`)
|
||||
}
|
||||
|
||||
console.log("[DIFY DEBUG] Response status:", response.status)
|
||||
Logger.log("[DIFY DEBUG] Response status:", response.status)
|
||||
const headersObj: Record<string, string> = {}
|
||||
response.headers.forEach((value, key) => {
|
||||
headersObj[key] = value
|
||||
})
|
||||
console.log("[DIFY DEBUG] Response headers:", headersObj)
|
||||
Logger.log("[DIFY DEBUG] Response headers:", headersObj)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
console.error("[DIFY DEBUG] Error response:", errorText)
|
||||
Logger.error("[DIFY DEBUG] Error response:", errorText)
|
||||
throw new Error(`Dify API error: ${response.status} ${response.statusText} - ${errorText}`)
|
||||
}
|
||||
|
||||
@@ -159,14 +160,14 @@ export class DifyHandler implements ApiHandler {
|
||||
const processedEvents: string[] = []
|
||||
let lastEventTime = Date.now()
|
||||
|
||||
console.log("[DIFY DEBUG] Starting to read streaming response...")
|
||||
Logger.log("[DIFY DEBUG] Starting to read streaming response...")
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) {
|
||||
console.log("[DIFY DEBUG] Stream ended naturally")
|
||||
console.log(
|
||||
Logger.log("[DIFY DEBUG] Stream ended naturally")
|
||||
Logger.log(
|
||||
"[DIFY DEBUG] Final state - hasYieldedContent:",
|
||||
hasYieldedContent,
|
||||
"fullText length:",
|
||||
@@ -178,7 +179,7 @@ export class DifyHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
const chunk = decoder.decode(value, { stream: true })
|
||||
console.log("[DIFY DEBUG] Raw chunk received:", JSON.stringify(chunk))
|
||||
Logger.log("[DIFY DEBUG] Raw chunk received:", JSON.stringify(chunk))
|
||||
|
||||
buffer += chunk
|
||||
const lines = buffer.split("\n")
|
||||
@@ -187,41 +188,41 @@ export class DifyHandler implements ApiHandler {
|
||||
buffer = lines.pop() || ""
|
||||
|
||||
for (const line of lines) {
|
||||
console.log("[DIFY DEBUG] Processing line:", JSON.stringify(line))
|
||||
Logger.log("[DIFY DEBUG] Processing line:", JSON.stringify(line))
|
||||
|
||||
if (line.startsWith("data: ")) {
|
||||
const data = line.slice(6).trim()
|
||||
console.log("[DIFY DEBUG] Extracted data:", JSON.stringify(data))
|
||||
Logger.log("[DIFY DEBUG] Extracted data:", JSON.stringify(data))
|
||||
|
||||
if (data === "[DONE]") {
|
||||
console.log("[DIFY DEBUG] Received [DONE] signal")
|
||||
Logger.log("[DIFY DEBUG] Received [DONE] signal")
|
||||
break
|
||||
}
|
||||
|
||||
if (data === "") {
|
||||
console.log("[DIFY DEBUG] Empty data line, skipping")
|
||||
Logger.log("[DIFY DEBUG] Empty data line, skipping")
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(data)
|
||||
console.log("[DIFY DEBUG] Parsed JSON:", parsed)
|
||||
Logger.log("[DIFY DEBUG] Parsed JSON:", parsed)
|
||||
processedEvents.push(parsed.event || "unknown")
|
||||
lastEventTime = Date.now()
|
||||
|
||||
// Capture conversation_id as soon as it's available
|
||||
if (parsed.conversation_id && !this.conversationId) {
|
||||
this.conversationId = parsed.conversation_id
|
||||
console.log("[DIFY DEBUG] Captured conversation_id:", this.conversationId)
|
||||
Logger.log("[DIFY DEBUG] Captured conversation_id:", this.conversationId)
|
||||
}
|
||||
|
||||
// Handle different Dify event types based on actual Dify API
|
||||
if (parsed.event === "message") {
|
||||
console.log("[DIFY DEBUG] Message event, answer:", parsed.answer)
|
||||
Logger.log("[DIFY DEBUG] Message event, answer:", parsed.answer)
|
||||
// Dify sends the full text in each "answer" chunk, so we replace.
|
||||
if (typeof parsed.answer === "string") {
|
||||
fullText = parsed.answer
|
||||
console.log("[DIFY DEBUG] Updated fullText length:", fullText.length)
|
||||
Logger.log("[DIFY DEBUG] Updated fullText length:", fullText.length)
|
||||
yield {
|
||||
type: "text",
|
||||
text: fullText,
|
||||
@@ -229,10 +230,10 @@ export class DifyHandler implements ApiHandler {
|
||||
hasYieldedContent = true
|
||||
}
|
||||
} else if (parsed.event === "message_replace") {
|
||||
console.log("[DIFY DEBUG] Replace message event:", parsed)
|
||||
Logger.log("[DIFY DEBUG] Replace message event:", parsed)
|
||||
if (parsed.answer) {
|
||||
fullText = parsed.answer // Replace instead of append
|
||||
console.log("[DIFY DEBUG] Replaced fullText length:", fullText.length)
|
||||
Logger.log("[DIFY DEBUG] Replaced fullText length:", fullText.length)
|
||||
yield {
|
||||
type: "text",
|
||||
text: fullText,
|
||||
@@ -240,7 +241,7 @@ export class DifyHandler implements ApiHandler {
|
||||
hasYieldedContent = true
|
||||
}
|
||||
} else if (parsed.event === "message_end") {
|
||||
console.log("[DIFY DEBUG] Message end event", parsed)
|
||||
Logger.log("[DIFY DEBUG] Message end event", parsed)
|
||||
// Message completed. Yield final text if we have any.
|
||||
if (fullText) {
|
||||
yield {
|
||||
@@ -260,19 +261,19 @@ export class DifyHandler implements ApiHandler {
|
||||
}
|
||||
return // End of stream
|
||||
} else if (parsed.event === "error") {
|
||||
console.error("[DIFY DEBUG] Error event:", parsed)
|
||||
Logger.error("[DIFY DEBUG] Error event:", parsed)
|
||||
throw new Error(`Dify API error: ${parsed.message || "Unknown error"}`)
|
||||
} else if (parsed.event === "workflow_started" || parsed.event === "workflow_finished") {
|
||||
console.log("[DIFY DEBUG] Workflow event:", parsed.event)
|
||||
Logger.log("[DIFY DEBUG] Workflow event:", parsed.event)
|
||||
// These are informational events, continue processing
|
||||
} else if (parsed.event === "node_started" || parsed.event === "node_finished") {
|
||||
console.log("[DIFY DEBUG] Node event:", parsed.event, parsed.data)
|
||||
Logger.log("[DIFY DEBUG] Node event:", parsed.event, parsed.data)
|
||||
// These are informational events, continue processing
|
||||
} else if (parsed.event === "ping") {
|
||||
console.log("[DIFY DEBUG] Ping event received, keeping connection alive.")
|
||||
Logger.log("[DIFY DEBUG] Ping event received, keeping connection alive.")
|
||||
// Ping event, do nothing
|
||||
} else {
|
||||
console.log("[DIFY DEBUG] Unknown event type:", parsed.event, "Full object:", parsed)
|
||||
Logger.log("[DIFY DEBUG] Unknown event type:", parsed.event, "Full object:", parsed)
|
||||
// Try to extract text from other possible fields
|
||||
if (parsed.text) {
|
||||
fullText += parsed.text
|
||||
@@ -299,17 +300,17 @@ export class DifyHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("[DIFY DEBUG] Failed to parse JSON:", data, "Error:", e)
|
||||
Logger.warn("[DIFY DEBUG] Failed to parse JSON:", data, "Error:", e)
|
||||
}
|
||||
} else if (line.trim() !== "") {
|
||||
console.log(
|
||||
Logger.log(
|
||||
"[DIFY DEBUG] Non-data line (not starting with 'data:'), trying to parse as direct JSON:",
|
||||
JSON.stringify(line),
|
||||
)
|
||||
// Try to parse as direct JSON (fallback for non-SSE responses, though Dify uses SSE)
|
||||
try {
|
||||
const parsed = JSON.parse(line.trim())
|
||||
console.log("[DIFY DEBUG] Parsed direct JSON:", parsed)
|
||||
Logger.log("[DIFY DEBUG] Parsed direct JSON:", parsed)
|
||||
processedEvents.push(parsed.event || "direct-json")
|
||||
|
||||
// Handle the same event types as above
|
||||
@@ -330,7 +331,7 @@ export class DifyHandler implements ApiHandler {
|
||||
}
|
||||
return
|
||||
} else if (parsed.event === "error") {
|
||||
console.error("[DIFY DEBUG] Direct JSON Error event:", parsed)
|
||||
Logger.error("[DIFY DEBUG] Direct JSON Error event:", parsed)
|
||||
throw new Error(`Dify API error: ${parsed.message || "Unknown error"}`)
|
||||
} else if (parsed.answer || parsed.text || parsed.content) {
|
||||
// Fallback for any content in direct JSON
|
||||
@@ -344,7 +345,7 @@ export class DifyHandler implements ApiHandler {
|
||||
}
|
||||
} catch (e) {
|
||||
// Not JSON, continue
|
||||
console.log("[DIFY DEBUG] Line is not direct JSON, continuing")
|
||||
Logger.log("[DIFY DEBUG] Line is not direct JSON, continuing")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -359,11 +360,11 @@ export class DifyHandler implements ApiHandler {
|
||||
streamDuration: Date.now() - lastEventTime,
|
||||
conversationId: this.conversationId,
|
||||
}
|
||||
console.error("[DIFY DEBUG] No content was yielded! Diagnostic info:", diagnosticInfo)
|
||||
Logger.error("[DIFY DEBUG] No content was yielded! Diagnostic info:", diagnosticInfo)
|
||||
|
||||
// If we have any accumulated text at all, yield it as a fallback
|
||||
if (fullText.trim()) {
|
||||
console.log("[DIFY DEBUG] Yielding accumulated text as fallback:", fullText)
|
||||
Logger.log("[DIFY DEBUG] Yielding accumulated text as fallback:", fullText)
|
||||
yield {
|
||||
type: "text",
|
||||
text: fullText,
|
||||
@@ -380,7 +381,7 @@ export class DifyHandler implements ApiHandler {
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock()
|
||||
console.log("[DIFY DEBUG] Stream reader released")
|
||||
Logger.log("[DIFY DEBUG] Stream reader released")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,7 +400,7 @@ export class DifyHandler implements ApiHandler {
|
||||
|
||||
// Only prepend the system prompt if it's the very first message of a new conversation.
|
||||
if (!this.conversationId && systemPrompt) {
|
||||
console.log("[DIFY DEBUG] Prepending system prompt for new conversation.")
|
||||
Logger.log("[DIFY DEBUG] Prepending system prompt for new conversation.")
|
||||
return `${systemPrompt}\n\n---\n\n${userQuery}`
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import { GeminiModelId, geminiDefaultModelId, geminiModels, ModelInfo } from "@shared/api"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { RetriableError, withRetry } from "../retry"
|
||||
import { convertAnthropicMessageToGemini } from "../transform/gemini-format"
|
||||
@@ -332,7 +333,7 @@ export class GeminiHandler implements ApiHandler {
|
||||
throughputTokensPerSec: throughputTokensPerSecSdk,
|
||||
})
|
||||
} else {
|
||||
console.warn("GeminiHandler: ulid not available for telemetry in createMessage.")
|
||||
Logger.warn("GeminiHandler: ulid not available for telemetry in createMessage.")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -408,7 +409,7 @@ export class GeminiHandler implements ApiHandler {
|
||||
trace.cacheRead = { price: cacheReadsPrice, tokens: cacheReadTokens ?? 0, cost: cacheReadCost }
|
||||
}
|
||||
|
||||
// console.log(`[GeminiHandler] calculateCost -> ${totalCost}`, trace)
|
||||
// Logger.log(`[GeminiHandler] calculateCost -> ${totalCost}`, trace)
|
||||
return totalCost
|
||||
}
|
||||
|
||||
@@ -450,13 +451,13 @@ export class GeminiHandler implements ApiHandler {
|
||||
})
|
||||
|
||||
if (response.totalTokens === undefined) {
|
||||
console.warn("Gemini token counting returned undefined, using fallback")
|
||||
Logger.warn("Gemini token counting returned undefined, using fallback")
|
||||
return this.estimateTokens(content)
|
||||
}
|
||||
|
||||
return response.totalTokens
|
||||
} catch (error) {
|
||||
console.warn("Gemini token counting failed, using fallback", error)
|
||||
Logger.warn("Gemini token counting failed, using fallback", error)
|
||||
return this.estimateTokens(content)
|
||||
}
|
||||
}
|
||||
@@ -475,7 +476,7 @@ export class GeminiHandler implements ApiHandler {
|
||||
const jsonStr = JSON.stringify(block)
|
||||
return total + jsonStr.length
|
||||
} catch (e) {
|
||||
console.warn("Failed to stringify block for token estimation", e)
|
||||
Logger.warn("Failed to stringify block for token estimation", e)
|
||||
return total
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import OpenAI from "openai"
|
||||
import { StateManager } from "@/core/storage/StateManager"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { isAnthropicModelId } from "@/utils/model-utils"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from ".."
|
||||
import { withRetry } from "../retry"
|
||||
@@ -70,7 +71,7 @@ export async function fetchLiteLlmModelsInfo(baseUrl: string, apiKey: string): P
|
||||
const data: LiteLlmModelInfoResponse = await response.json()
|
||||
return data
|
||||
} else {
|
||||
console.error("Failed to fetch LiteLLM model info:", response.statusText)
|
||||
Logger.error("Failed to fetch LiteLLM model info:", response.statusText)
|
||||
// Try with Authorization header instead
|
||||
const retryResponse = await fetch(url, {
|
||||
method: "GET",
|
||||
@@ -84,12 +85,12 @@ export async function fetchLiteLlmModelsInfo(baseUrl: string, apiKey: string): P
|
||||
const data: LiteLlmModelInfoResponse = await retryResponse.json()
|
||||
return data
|
||||
} else {
|
||||
console.error("Failed to fetch LiteLLM model info with Authorization header:", retryResponse.statusText)
|
||||
Logger.error("Failed to fetch LiteLLM model info with Authorization header:", retryResponse.statusText)
|
||||
throw new Error(`Failed to fetch LiteLLM model info: ${retryResponse.statusText}`)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching LiteLLM model info:", error)
|
||||
Logger.error("Error fetching LiteLLM model info:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -169,7 +170,7 @@ export class LiteLlmHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Error getting LiteLLM model cost info:", error)
|
||||
Logger.warn("Error getting LiteLLM model cost info:", error)
|
||||
}
|
||||
|
||||
// Fallback to zero costs if we can't get the information
|
||||
@@ -200,7 +201,7 @@ export class LiteLlmHandler implements ApiHandler {
|
||||
|
||||
return totalCost
|
||||
} catch (error) {
|
||||
console.error("Error calculating spend:", error)
|
||||
Logger.error("Error calculating spend:", error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,11 @@ import {
|
||||
OCI_HEADER_OPC_REQUEST_ID,
|
||||
} from "@/services/auth/oca/utils/constants"
|
||||
import { createOcaHeaders } from "@/services/auth/oca/utils/utils"
|
||||
import { Logger } from "@/services/logging/Logger"
|
||||
import { OcaModelInfo } from "@/shared/api"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiFormat } from "@/shared/proto/index.cline"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ApiHandler, type CommonApiHandlerOptions } from ".."
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -134,11 +134,11 @@ export class OcaHandler implements ApiHandler {
|
||||
const data: { cost: number } = await response.json()
|
||||
return data.cost
|
||||
} else {
|
||||
console.error("Error calculating spend:", response.statusText)
|
||||
Logger.error("Error calculating spend:", response.statusText)
|
||||
return undefined
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error calculating spend:", error)
|
||||
Logger.error("Error calculating spend:", error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
@@ -313,7 +313,7 @@ export class OcaHandler implements ApiHandler {
|
||||
|
||||
// Convert ChatCompletion tools to Responses API format if provided
|
||||
const responseTools = tools
|
||||
?.filter((tool) => tool.type === "function")
|
||||
?.filter((tool) => tool?.type === "function")
|
||||
.map((tool: any) => ({
|
||||
type: "function" as const,
|
||||
name: tool.function.name,
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { type ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
|
||||
import { type Config, type Message, Ollama } from "ollama"
|
||||
import type { ChatCompletionTool } from "openai/resources/chat/completions"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import type { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOllamaMessages } from "../transform/ollama-format"
|
||||
import type { ApiStream } from "../transform/stream"
|
||||
import { ToolCallProcessor } from "../transform/tool-call-processor"
|
||||
|
||||
interface OllamaHandlerOptions extends CommonApiHandlerOptions {
|
||||
ollamaBaseUrl?: string
|
||||
@@ -50,7 +53,7 @@ export class OllamaHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
@withRetry({ retryAllErrors: true })
|
||||
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[]): ApiStream {
|
||||
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: ChatCompletionTool[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const ollamaMessages: Message[] = [{ role: "system", content: systemPrompt }, ...convertToOllamaMessages(messages)]
|
||||
|
||||
@@ -69,20 +72,44 @@ export class OllamaHandler implements ApiHandler {
|
||||
options: {
|
||||
num_ctx: Number(this.options.ollamaApiOptionsCtxNum),
|
||||
},
|
||||
tools: tools as any,
|
||||
})
|
||||
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
|
||||
// Race the API request against the timeout
|
||||
const stream = (await Promise.race([apiPromise, timeoutPromise])) as Awaited<typeof apiPromise>
|
||||
|
||||
try {
|
||||
for await (const chunk of stream) {
|
||||
if (typeof chunk.message.content === "string") {
|
||||
yield {
|
||||
type: "text",
|
||||
text: chunk.message.content,
|
||||
}
|
||||
Logger.debug("[OllamaHandler] Message Chunk" + JSON.stringify(chunk))
|
||||
|
||||
const delta = chunk.message
|
||||
|
||||
if (delta?.tool_calls) {
|
||||
Logger.debug(`[OllamaHandler] Tool Calls Detected: ${JSON.stringify(delta.tool_calls)}`)
|
||||
yield* toolCallProcessor.processToolCallDeltas(
|
||||
delta.tool_calls?.map((tc, inx) => ({
|
||||
index: inx,
|
||||
id: `ollama-tool-${inx}`,
|
||||
function: {
|
||||
name: tc.function.name,
|
||||
arguments:
|
||||
typeof tc.function.arguments === "string"
|
||||
? tc.function.arguments
|
||||
: JSON.stringify(tc.function.arguments),
|
||||
},
|
||||
type: "function",
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
if (typeof delta.content === "string") {
|
||||
yield {
|
||||
type: "text",
|
||||
text: delta.content,
|
||||
}
|
||||
}
|
||||
// Handle token usage if available
|
||||
if (chunk.eval_count !== undefined || chunk.prompt_eval_count !== undefined) {
|
||||
yield {
|
||||
@@ -93,7 +120,7 @@ export class OllamaHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
} catch (streamError: any) {
|
||||
console.error("Error processing Ollama stream:", streamError)
|
||||
Logger.error("Error processing Ollama stream:", streamError)
|
||||
throw new Error(`Ollama stream processing error: ${streamError.message || "Unknown error"}`)
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -107,7 +134,7 @@ export class OllamaHandler implements ApiHandler {
|
||||
const statusCode = error.status || error.statusCode
|
||||
const errorMessage = error.message || "Unknown error"
|
||||
|
||||
console.error(`Ollama API error (${statusCode || "unknown"}): ${errorMessage}`)
|
||||
Logger.error(`Ollama API error (${statusCode || "unknown"}): ${errorMessage}`)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
import * as os from "os"
|
||||
import { v7 as uuidv7 } from "uuid"
|
||||
import { ModelInfo, OpenAiCodexModelId, openAiCodexDefaultModelId, openAiCodexModels } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool } from "openai/resources/chat/completions"
|
||||
import {
|
||||
ModelInfo,
|
||||
OpenAiCodexModelId,
|
||||
openAiCodexDefaultModelId,
|
||||
openAiCodexModels,
|
||||
} from "@shared/api"
|
||||
import * as os from "os"
|
||||
import { v7 as uuidv7 } from "uuid"
|
||||
import { openAiCodexOAuthManager } from "@/integrations/openai-codex/oauth"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { openAiCodexOAuthManager } from "@/integrations/openai-codex/oauth"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { convertToOpenAIResponsesInput } from "../transform/openai-response-format"
|
||||
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
|
||||
@@ -53,7 +48,9 @@ export class OpenAiCodexHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
private normalizeUsage(usage: any, model: { id: string; info: ModelInfo }): ApiStreamUsageChunk | undefined {
|
||||
if (!usage) return undefined
|
||||
if (!usage) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const inputDetails = usage.input_tokens_details ?? usage.prompt_tokens_details
|
||||
|
||||
@@ -90,11 +87,7 @@ export class OpenAiCodexHandler implements ApiHandler {
|
||||
return out
|
||||
}
|
||||
|
||||
async *createMessage(
|
||||
systemPrompt: string,
|
||||
messages: ClineStorageMessage[],
|
||||
tools?: ChatCompletionTool[],
|
||||
): ApiStream {
|
||||
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: ChatCompletionTool[]): ApiStream {
|
||||
const model = this.getModel()
|
||||
|
||||
// Reset state for this request
|
||||
@@ -104,9 +97,7 @@ export class OpenAiCodexHandler implements ApiHandler {
|
||||
// Get access token from OAuth manager
|
||||
let accessToken = await openAiCodexOAuthManager.getAccessToken()
|
||||
if (!accessToken) {
|
||||
throw new Error(
|
||||
"Not authenticated with OpenAI Codex. Please sign in using the OpenAI Codex OAuth flow in settings.",
|
||||
)
|
||||
throw new Error("Not authenticated with OpenAI Codex. Please sign in using the OpenAI Codex OAuth flow in settings.")
|
||||
}
|
||||
|
||||
// Format conversation for Responses API
|
||||
@@ -166,7 +157,7 @@ export class OpenAiCodexHandler implements ApiHandler {
|
||||
// Pass through strict value from tool (MCP/custom tools have strict: false, built-in tools default to true)
|
||||
if (tools && tools.length > 0) {
|
||||
body.tools = tools
|
||||
.filter((tool: any) => tool.type === "function")
|
||||
.filter((tool: any) => tool?.type === "function")
|
||||
.map((tool: any) => ({
|
||||
type: "function",
|
||||
name: tool.function.name,
|
||||
@@ -179,11 +170,7 @@ export class OpenAiCodexHandler implements ApiHandler {
|
||||
return body
|
||||
}
|
||||
|
||||
private async *executeRequest(
|
||||
requestBody: any,
|
||||
model: { id: string; info: ModelInfo },
|
||||
accessToken: string,
|
||||
): ApiStream {
|
||||
private async *executeRequest(requestBody: any, model: { id: string; info: ModelInfo }, accessToken: string): ApiStream {
|
||||
// Create AbortController for cancellation
|
||||
this.abortController = new AbortController()
|
||||
|
||||
@@ -237,11 +224,7 @@ export class OpenAiCodexHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private async *makeCodexRequest(
|
||||
requestBody: any,
|
||||
model: { id: string; info: ModelInfo },
|
||||
accessToken: string,
|
||||
): ApiStream {
|
||||
private async *makeCodexRequest(requestBody: any, model: { id: string; info: ModelInfo }, accessToken: string): ApiStream {
|
||||
const url = `${CODEX_API_BASE_URL}/responses`
|
||||
|
||||
// Get ChatGPT account ID for organization subscriptions
|
||||
@@ -302,10 +285,7 @@ export class OpenAiCodexHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private async *handleStreamResponse(
|
||||
body: ReadableStream<Uint8Array>,
|
||||
model: { id: string; info: ModelInfo },
|
||||
): ApiStream {
|
||||
private async *handleStreamResponse(body: ReadableStream<Uint8Array>, model: { id: string; info: ModelInfo }): ApiStream {
|
||||
const reader = body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let buffer = ""
|
||||
@@ -317,7 +297,9 @@ export class OpenAiCodexHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
if (done) {
|
||||
break
|
||||
}
|
||||
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
const lines = buffer.split("\n")
|
||||
@@ -380,10 +362,7 @@ export class OpenAiCodexHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
// Handle tool/function call deltas
|
||||
if (
|
||||
event?.type === "response.tool_call_arguments.delta" ||
|
||||
event?.type === "response.function_call_arguments.delta"
|
||||
) {
|
||||
if (event?.type === "response.tool_call_arguments.delta" || event?.type === "response.function_call_arguments.delta") {
|
||||
const callId = event.call_id || event.tool_call_id || event.id || this.pendingToolCallId
|
||||
const name = event.name || event.function_name || this.pendingToolCallName
|
||||
const args = event.delta || event.arguments
|
||||
@@ -484,10 +463,7 @@ export class OpenAiCodexHandler implements ApiHandler {
|
||||
getModel(): { id: OpenAiCodexModelId; info: ModelInfo } {
|
||||
const modelId = this.options.apiModelId
|
||||
|
||||
const id =
|
||||
modelId && modelId in openAiCodexModels
|
||||
? (modelId as OpenAiCodexModelId)
|
||||
: openAiCodexDefaultModelId
|
||||
const id = modelId && modelId in openAiCodexModels ? (modelId as OpenAiCodexModelId) : openAiCodexDefaultModelId
|
||||
|
||||
const info: ModelInfo = openAiCodexModels[id]
|
||||
|
||||
|
||||
@@ -8,10 +8,10 @@ import {
|
||||
import { calculateApiCostOpenAI } from "@utils/cost"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionReasoningEffort, ChatCompletionTool } from "openai/resources/chat/completions"
|
||||
import { Logger } from "@/services/logging/Logger"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiFormat } from "@/shared/proto/cline/models"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { isGPT5ModelFamily } from "@/utils/model-utils"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
@@ -135,7 +135,7 @@ export class OpenAiNativeHandler implements ApiHandler {
|
||||
try {
|
||||
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
|
||||
} catch (error) {
|
||||
console.error("Error processing tool call delta:", error, delta.tool_calls)
|
||||
Logger.error("Error processing tool call delta:", error, delta.tool_calls)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ export class OpenAiNativeHandler implements ApiHandler {
|
||||
|
||||
// Convert ChatCompletion tools to Responses API format if provided
|
||||
const responseTools = tools
|
||||
?.filter((tool) => tool.type === "function")
|
||||
?.filter((tool) => tool?.type === "function")
|
||||
.map((tool: any) => ({
|
||||
type: "function" as const,
|
||||
name: tool.function.name,
|
||||
@@ -316,11 +316,11 @@ export class OpenAiNativeHandler implements ApiHandler {
|
||||
chunk.response?.status === "incomplete" &&
|
||||
chunk.response?.incomplete_details?.reason === "max_output_tokens"
|
||||
) {
|
||||
console.log("Ran out of tokens")
|
||||
Logger.log("Ran out of tokens")
|
||||
if (chunk.response?.output_text?.length > 0) {
|
||||
console.log("Partial output:", chunk.response.output_text)
|
||||
Logger.log("Partial output:", chunk.response.output_text)
|
||||
} else {
|
||||
console.log("Ran out of tokens during reasoning")
|
||||
Logger.log("Ran out of tokens during reasoning")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch, getAxiosSettings } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { createOpenRouterStream } from "../transform/openrouter-stream"
|
||||
@@ -80,7 +81,7 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
// Check for error field directly on chunk
|
||||
if ("error" in chunk) {
|
||||
const error = chunk.error as OpenRouterErrorResponse["error"]
|
||||
console.error(`OpenRouter API Error: ${error?.code} - ${error?.message}`)
|
||||
Logger.error(`OpenRouter API Error: ${error?.code} - ${error?.message}`)
|
||||
// Include metadata in the error message if available
|
||||
const metadataStr = error.metadata ? `\nMetadata: ${JSON.stringify(error.metadata, null, 2)}` : ""
|
||||
throw new Error(`OpenRouter API Error ${error.code}: ${error.message}${metadataStr}`)
|
||||
@@ -95,7 +96,7 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
const choiceWithError = choice as any
|
||||
if (choiceWithError.error) {
|
||||
const error = choiceWithError.error
|
||||
console.error(
|
||||
Logger.error(
|
||||
`OpenRouter Mid-Stream Error: ${error?.code || "Unknown"} - ${error?.message || "Unknown error"}`,
|
||||
)
|
||||
// Format error details
|
||||
@@ -179,7 +180,7 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
try {
|
||||
const generationIterator = this.fetchGenerationDetails(this.lastGenerationId)
|
||||
const generation = (await generationIterator.next()).value
|
||||
// console.log("OpenRouter generation details:", generation)
|
||||
// Logger.log("OpenRouter generation details:", generation)
|
||||
return {
|
||||
type: "usage",
|
||||
cacheWriteTokens: 0,
|
||||
@@ -191,7 +192,7 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
}
|
||||
} catch (error) {
|
||||
// ignore if fails
|
||||
console.error("Error fetching OpenRouter generation details:", error)
|
||||
Logger.error("Error fetching OpenRouter generation details:", error)
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
@@ -199,7 +200,7 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
|
||||
@withRetry({ maxRetries: 4, baseDelay: 250, maxDelay: 1000, retryAllErrors: true })
|
||||
async *fetchGenerationDetails(genId: string) {
|
||||
// console.log("Fetching generation details for:", genId)
|
||||
// Logger.log("Fetching generation details for:", genId)
|
||||
try {
|
||||
const response = await axios.get(`https://openrouter.ai/api/v1/generation?id=${genId}`, {
|
||||
headers: {
|
||||
@@ -211,7 +212,7 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
yield response.data?.data
|
||||
} catch (error) {
|
||||
// ignore if fails
|
||||
console.error("Error fetching OpenRouter generation details:", error)
|
||||
Logger.error("Error fetching OpenRouter generation details:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import * as os from "os"
|
||||
import * as path from "path"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -76,7 +77,7 @@ export class QwenCodeHandler implements ApiHandler {
|
||||
const credsStr = await fs.readFile(keyFile, "utf-8")
|
||||
return JSON.parse(credsStr)
|
||||
} catch (error) {
|
||||
console.error(
|
||||
Logger.error(
|
||||
`Error reading or parsing credentials file at ${getQwenCachedCredentialPath(this.options.qwenCodeOauthPath)}`,
|
||||
)
|
||||
throw new Error(`Failed to load Qwen OAuth credentials: ${error}`)
|
||||
|
||||
@@ -12,6 +12,7 @@ import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -134,7 +135,7 @@ export class QwenHandler implements ApiHandler {
|
||||
try {
|
||||
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
|
||||
} catch (error) {
|
||||
console.error("Error processing tool call delta:", error, delta.tool_calls)
|
||||
Logger.error("Error processing tool call delta:", error, delta.tool_calls)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import JSON5 from "json5"
|
||||
import OpenAI from "openai"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { getAxiosSettings } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -144,7 +145,7 @@ namespace Bedrock {
|
||||
}
|
||||
|
||||
// Log unsupported content types for debugging
|
||||
console.warn(`Unsupported content type: ${(item as ContentItem).type}`)
|
||||
Logger.warn(`Unsupported content type: ${(item as ContentItem).type}`)
|
||||
return null
|
||||
})
|
||||
.filter((item): item is BedrockContentBlock => item !== null)
|
||||
@@ -213,7 +214,7 @@ namespace Bedrock {
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to process image content:", error)
|
||||
Logger.error("Failed to process image content:", error)
|
||||
// Return a text content indicating the error instead of null
|
||||
// This ensures users are aware of the issue
|
||||
return {
|
||||
@@ -412,7 +413,7 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
url: destination.url || this.options.sapAiCoreBaseUrl!,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to create AI Core destination:", error)
|
||||
Logger.error("Failed to create AI Core destination:", error)
|
||||
throw new Error(`Unable to create AI Core destination: ${error instanceof Error ? error.message : String(error)}`)
|
||||
}
|
||||
}
|
||||
@@ -473,7 +474,7 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
})
|
||||
.filter((deployment: any) => deployment !== null)
|
||||
} catch (error) {
|
||||
console.error("Error fetching deployments:", error)
|
||||
Logger.error("Error fetching deployments:", error)
|
||||
throw new Error("Failed to fetch deployments")
|
||||
}
|
||||
}
|
||||
@@ -577,7 +578,7 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error in SAP orchestration mode:", error)
|
||||
Logger.error("Error in SAP orchestration mode:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -596,7 +597,7 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
|
||||
if (!deploymentId) {
|
||||
// Fallback to runtime deployment id fetching for users who haven't opened the SAP provider UI
|
||||
console.log(`No pre-configured deployment ID found for model ${model.id}, falling back to runtime fetching`)
|
||||
Logger.log(`No pre-configured deployment ID found for model ${model.id}, falling back to runtime fetching`)
|
||||
deploymentId = await this.getDeploymentForModel(model.id)
|
||||
}
|
||||
|
||||
@@ -800,8 +801,8 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
if (error.response) {
|
||||
// The request was made and the server responded with a status code
|
||||
// that falls out of the range of 2xx
|
||||
console.error("Error status:", error.response.status)
|
||||
console.error("Error headers:", error.response.headers)
|
||||
Logger.error("Error status:", error.response.status)
|
||||
Logger.error("Error headers:", error.response.headers)
|
||||
|
||||
// Handle error data - need to read stream if responseType was 'stream'
|
||||
let errorMessage = "Unknown error"
|
||||
@@ -830,10 +831,10 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
} else if (typeof error.response.data === "object") {
|
||||
errorMessage = JSON.stringify(error.response.data, null, 2)
|
||||
}
|
||||
console.error("Error data:", errorMessage)
|
||||
Logger.error("Error data:", errorMessage)
|
||||
} catch (e) {
|
||||
console.error("Failed to read error data:", e)
|
||||
console.error("Raw error data:", error.response.data)
|
||||
Logger.error("Failed to read error data:", e)
|
||||
Logger.error("Raw error data:", error.response.data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -846,11 +847,11 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
throw new Error(`HTTP ${error.response.status}: ${errorMessage}`)
|
||||
} else if (error.request) {
|
||||
// The request was made but no response was received
|
||||
console.error("Error request:", error.request)
|
||||
Logger.error("Error request:", error.request)
|
||||
throw new Error("No response received from server")
|
||||
} else {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
console.error("Error message:", error.message)
|
||||
Logger.error("Error message:", error.message)
|
||||
throw new Error(`Error setting up request: ${error.message}`)
|
||||
}
|
||||
}
|
||||
@@ -898,13 +899,13 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to parse JSON data:", error)
|
||||
Logger.error("Failed to parse JSON data:", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error streaming completion:", error)
|
||||
Logger.error("Error streaming completion:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -965,7 +966,7 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to parse JSON data:", error)
|
||||
Logger.error("Failed to parse JSON data:", error)
|
||||
yield {
|
||||
type: "text",
|
||||
text: `[ERROR] Failed to parse response data: ${error instanceof Error ? error.message : String(error)}`,
|
||||
@@ -975,7 +976,7 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error streaming completion:", error)
|
||||
Logger.error("Error streaming completion:", error)
|
||||
yield {
|
||||
type: "text",
|
||||
text: `[ERROR] Failed to process stream: ${error instanceof Error ? error.message : String(error)}`,
|
||||
@@ -1044,13 +1045,13 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to parse GPT JSON data:", error)
|
||||
Logger.error("Failed to parse GPT JSON data:", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error streaming GPT completion:", error)
|
||||
Logger.error("Error streaming GPT completion:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -1109,13 +1110,13 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to parse Gemini JSON data:", error)
|
||||
Logger.error("Failed to parse Gemini JSON data:", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error streaming Gemini completion:", error)
|
||||
Logger.error("Error streaming Gemini completion:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
@@ -125,11 +126,11 @@ export class VercelAIGatewayHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
if (!didOutputUsage) {
|
||||
console.warn("Vercel AI Gateway did not provide usage information in stream")
|
||||
Logger.warn("Vercel AI Gateway did not provide usage information in stream")
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Vercel AI Gateway error details:", error)
|
||||
console.error("Error stack:", error.stack)
|
||||
Logger.error("Vercel AI Gateway error details:", error)
|
||||
Logger.error("Error stack:", error.stack)
|
||||
throw new Error(`Vercel AI Gateway error: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { SELECTOR_SEPARATOR, stringifyVsCodeLmModelSelector } from "@shared/vsCo
|
||||
import { calculateApiCostAnthropic } from "@utils/cost"
|
||||
import * as vscode from "vscode"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ApiHandler, CommonApiHandlerOptions, SingleCompletionHandler } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
@@ -123,7 +124,7 @@ declare module "vscode" {
|
||||
* const systemPrompt = "You are a helpful assistant";
|
||||
* const messages = [{ role: "user", content: "Hello!" }];
|
||||
* for await (const chunk of handler.createMessage(systemPrompt, messages)) {
|
||||
* console.log(chunk);
|
||||
* Logger.log(chunk);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
@@ -147,7 +148,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
||||
this.client = null
|
||||
this.ensureCleanState()
|
||||
} catch (error) {
|
||||
console.error("Error during configuration change cleanup:", error)
|
||||
Logger.error("Error during configuration change cleanup:", error)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -279,7 +280,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
||||
|
||||
private async getClient(): Promise<vscode.LanguageModelChat> {
|
||||
if (!this.client) {
|
||||
console.debug("Cline <Language Model API>: Getting client with options:", {
|
||||
Logger.debug("Cline <Language Model API>: Getting client with options:", {
|
||||
vsCodeLmModelSelector: this.options.vsCodeLmModelSelector,
|
||||
hasOptions: !!this.options,
|
||||
selectorKeys: this.options.vsCodeLmModelSelector ? Object.keys(this.options.vsCodeLmModelSelector) : [],
|
||||
@@ -288,11 +289,11 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
||||
try {
|
||||
// Use default empty selector if none provided to get all available models
|
||||
const selector = this.options?.vsCodeLmModelSelector || {}
|
||||
console.debug("Cline <Language Model API>: Creating client with selector:", selector)
|
||||
Logger.debug("Cline <Language Model API>: Creating client with selector:", selector)
|
||||
this.client = await this.createClient(selector)
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error"
|
||||
console.error("Cline <Language Model API>: Client creation failed:", message)
|
||||
Logger.error("Cline <Language Model API>: Client creation failed:", message)
|
||||
throw new Error(`Cline <Language Model API>: Failed to create client: ${message}`)
|
||||
}
|
||||
}
|
||||
@@ -413,7 +414,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
||||
if (chunk instanceof vscode.LanguageModelTextPart) {
|
||||
// Validate text part value
|
||||
if (typeof chunk.value !== "string") {
|
||||
console.warn("Cline <Language Model API>: Invalid text part value received:", chunk.value)
|
||||
Logger.warn("Cline <Language Model API>: Invalid text part value received:", chunk.value)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -426,18 +427,18 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
||||
try {
|
||||
// Validate tool call parameters
|
||||
if (!chunk.name || typeof chunk.name !== "string") {
|
||||
console.warn("Cline <Language Model API>: Invalid tool name received:", chunk.name)
|
||||
Logger.warn("Cline <Language Model API>: Invalid tool name received:", chunk.name)
|
||||
continue
|
||||
}
|
||||
|
||||
if (!chunk.callId || typeof chunk.callId !== "string") {
|
||||
console.warn("Cline <Language Model API>: Invalid tool callId received:", chunk.callId)
|
||||
Logger.warn("Cline <Language Model API>: Invalid tool callId received:", chunk.callId)
|
||||
continue
|
||||
}
|
||||
|
||||
// Ensure input is a valid object
|
||||
if (!chunk.input || typeof chunk.input !== "object") {
|
||||
console.warn("Cline <Language Model API>: Invalid tool input received:", chunk.input)
|
||||
Logger.warn("Cline <Language Model API>: Invalid tool input received:", chunk.input)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -453,7 +454,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
||||
accumulatedText += toolCallText
|
||||
|
||||
// Log tool call for debugging
|
||||
console.debug("Cline <Language Model API>: Processing tool call:", {
|
||||
Logger.debug("Cline <Language Model API>: Processing tool call:", {
|
||||
name: chunk.name,
|
||||
callId: chunk.callId,
|
||||
inputSize: JSON.stringify(chunk.input).length,
|
||||
@@ -464,10 +465,10 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
||||
text: toolCallText,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Cline <Language Model API>: Failed to process tool call:", error)
|
||||
Logger.error("Cline <Language Model API>: Failed to process tool call:", error)
|
||||
}
|
||||
} else {
|
||||
console.warn("Cline <Language Model API>: Unknown chunk type received:", chunk)
|
||||
Logger.warn("Cline <Language Model API>: Unknown chunk type received:", chunk)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -489,7 +490,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
console.error("Cline <Language Model API>: Stream error details:", {
|
||||
Logger.error("Cline <Language Model API>: Stream error details:", {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
name: error.name,
|
||||
@@ -500,12 +501,12 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
||||
} else if (typeof error === "object" && error !== null) {
|
||||
// Handle error-like objects
|
||||
const errorDetails = JSON.stringify(error, null, 2)
|
||||
console.error("Cline <Language Model API>: Stream error object:", errorDetails)
|
||||
Logger.error("Cline <Language Model API>: Stream error object:", errorDetails)
|
||||
throw new Error(`Cline <Language Model API>: Response stream error: ${errorDetails}`)
|
||||
} else {
|
||||
// Fallback for unknown error types
|
||||
const errorMessage = String(error)
|
||||
console.error("Cline <Language Model API>: Unknown stream error:", errorMessage)
|
||||
Logger.error("Cline <Language Model API>: Unknown stream error:", errorMessage)
|
||||
throw new Error(`Cline <Language Model API>: Response stream error: ${errorMessage}`)
|
||||
}
|
||||
}
|
||||
@@ -526,7 +527,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
||||
// Log any missing properties for debugging
|
||||
for (const [prop, value] of Object.entries(requiredProps)) {
|
||||
if (!value && value !== 0) {
|
||||
console.warn(`Cline <Language Model API>: Client missing ${prop} property`)
|
||||
Logger.warn(`Cline <Language Model API>: Client missing ${prop} property`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -557,7 +558,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
||||
? stringifyVsCodeLmModelSelector(this.options.vsCodeLmModelSelector)
|
||||
: "vscode-lm"
|
||||
|
||||
console.debug("Cline <Language Model API>: No client available, using fallback model info")
|
||||
Logger.debug("Cline <Language Model API>: No client available, using fallback model info")
|
||||
|
||||
return {
|
||||
id: fallbackId,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
|
||||
interface RetryOptions {
|
||||
maxRetries?: number
|
||||
baseDelay?: number
|
||||
@@ -72,7 +74,7 @@ export function withRetry(options: RetryOptions = {}) {
|
||||
try {
|
||||
await handlerInstance.options.onRetryAttempt(attempt + 1, maxRetries, delay, error)
|
||||
} catch (e) {
|
||||
console.error("Error in onRetryAttempt callback:", e)
|
||||
Logger.error("Error in onRetryAttempt callback:", e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
|
||||
const o1SystemPrompt = (systemPrompt: string) => `
|
||||
# System Prompt
|
||||
@@ -317,7 +318,7 @@ function parseToolCall(toolName: string, content: string): ToolCall | null {
|
||||
|
||||
// Validate required parameters
|
||||
if (!validateToolInput(toolName, tool_input)) {
|
||||
console.error(`Invalid tool call for ${toolName}:`, content)
|
||||
Logger.error(`Invalid tool call for ${toolName}:`, content)
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -358,8 +359,8 @@ function validateToolInput(toolName: string, tool_input: Record<string, string>)
|
||||
// </write_to_file>`;
|
||||
//
|
||||
// const { normalText, toolCalls } = parseAIResponse(aiResponse);
|
||||
// console.log(normalText);
|
||||
// console.log(toolCalls);
|
||||
// Logger.log(normalText);
|
||||
// Logger.log(toolCalls);
|
||||
|
||||
// Convert OpenAI response to Anthropic format
|
||||
export function convertO1ResponseToAnthropicMessage(
|
||||
@@ -432,4 +433,4 @@ export function convertO1ResponseToAnthropicMessage(
|
||||
// usage: { prompt_tokens: 50, completion_tokens: 100 }
|
||||
// };
|
||||
// const anthropicMessage = convertO1ResponseToAnthropicMessage(openAICompletion);
|
||||
// console.log(anthropicMessage);
|
||||
// Logger.log(anthropicMessage);
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
ClineTextContentBlock,
|
||||
ClineUserToolResultContentBlock,
|
||||
} from "@/shared/messages/content"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
|
||||
// OpenAI API has a maximum tool call ID length of 40 characters
|
||||
const MAX_TOOL_CALL_ID_LENGTH = 40
|
||||
@@ -424,7 +425,7 @@ export function convertToAnthropicMessage(completion: OpenAI.Chat.Completions.Ch
|
||||
try {
|
||||
parsedInput = JSON.parse(toolCall.function?.arguments || "{}")
|
||||
} catch (error) {
|
||||
console.error("Failed to parse tool arguments:", error)
|
||||
Logger.error("Failed to parse tool arguments:", error)
|
||||
}
|
||||
return {
|
||||
type: "tool_use",
|
||||
@@ -439,7 +440,7 @@ export function convertToAnthropicMessage(completion: OpenAI.Chat.Completions.Ch
|
||||
return anthropicMessage
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error converting OpenAI message to Anthropic format:", error)
|
||||
Logger.error("Error converting OpenAI message to Anthropic format:", error)
|
||||
}
|
||||
|
||||
return anthropicMessage
|
||||
|
||||
@@ -1,5 +1,64 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
import { ClineAssistantThinkingBlock, ClineStorageMessage } from "@/shared/messages/content"
|
||||
|
||||
/**
|
||||
* DeepSeek Reasoner message format with reasoning_content support.
|
||||
*/
|
||||
export type DeepSeekReasonerMessage = OpenAI.Chat.ChatCompletionMessageParam & {
|
||||
reasoning_content?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds reasoning_content to OpenAI messages for DeepSeek Reasoner.
|
||||
* Per DeepSeek API: reasoning_content should be passed back during tool calling in the same turn,
|
||||
* and omitted when starting a new turn.
|
||||
*/
|
||||
export function addReasoningContent(
|
||||
openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[],
|
||||
originalMessages: ClineStorageMessage[],
|
||||
): DeepSeekReasonerMessage[] {
|
||||
// Find last user message index (start of current turn)
|
||||
// If no user message exists (lastUserIndex = -1), all messages are in the "current turn",
|
||||
// so reasoning_content will be added to all assistant messages. This is intentional.
|
||||
let lastUserIndex = -1
|
||||
for (let i = openAiMessages.length - 1; i >= 0; i--) {
|
||||
if (openAiMessages[i].role === "user") {
|
||||
lastUserIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Extract thinking content from original messages, keyed by assistant index
|
||||
const thinkingByIndex = new Map<number, string>()
|
||||
let assistantIdx = 0
|
||||
for (const msg of originalMessages) {
|
||||
if (msg.role === "assistant") {
|
||||
if (Array.isArray(msg.content)) {
|
||||
const thinking = msg.content
|
||||
.filter((p): p is ClineAssistantThinkingBlock => p.type === "thinking")
|
||||
.map((p) => p.thinking)
|
||||
.join("\n")
|
||||
if (thinking) {
|
||||
thinkingByIndex.set(assistantIdx, thinking)
|
||||
}
|
||||
}
|
||||
assistantIdx++
|
||||
}
|
||||
}
|
||||
|
||||
// Add reasoning_content only to assistant messages in current turn
|
||||
let aiIdx = 0
|
||||
return openAiMessages.map((msg, i): DeepSeekReasonerMessage => {
|
||||
if (msg.role === "assistant") {
|
||||
const thinking = thinkingByIndex.get(aiIdx++)
|
||||
if (thinking && i >= lastUserIndex) {
|
||||
return { ...msg, reasoning_content: thinking }
|
||||
}
|
||||
}
|
||||
return msg
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts Anthropic messages to OpenAI format and merges consecutive messages with the same role.
|
||||
|
||||
@@ -3,7 +3,7 @@ import type {
|
||||
ChatCompletionToolChoiceOption,
|
||||
ChatCompletionTool as OpenAITool,
|
||||
} from "openai/resources/chat/completions"
|
||||
import { Logger } from "@/services/logging/Logger"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import type { ApiStreamToolCallsChunk } from "./stream"
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import * as vscode from "vscode"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
|
||||
/**
|
||||
* Safely converts a value into a plain object.
|
||||
@@ -23,7 +24,7 @@ export function asObjectSafe(value: any): object {
|
||||
|
||||
return {}
|
||||
} catch (error) {
|
||||
console.warn("Cline <Language Model API>: Failed to parse object:", error)
|
||||
Logger.warn("Cline <Language Model API>: Failed to parse object:", error)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import OpenAI from "openai"
|
||||
import { Logger } from "@/services/logging/Logger"
|
||||
import { ModelInfo } from "@/shared/api"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
|
||||
// Type that represents the OpenAI ResponseStream with its private properties
|
||||
// The #private property issue can be resolved by using the AsyncIterable interface
|
||||
@@ -143,11 +143,11 @@ export async function* handleResponsesApiStreamResponse(
|
||||
chunk.response?.status === "incomplete" &&
|
||||
chunk.response?.incomplete_details?.reason === "max_output_tokens"
|
||||
) {
|
||||
console.log("Ran out of tokens")
|
||||
Logger.log("Ran out of tokens")
|
||||
if (chunk.response?.output_text?.length > 0) {
|
||||
console.log("Partial output:", chunk.response.output_text)
|
||||
Logger.log("Partial output:", chunk.response.output_text)
|
||||
} else {
|
||||
console.log("Ran out of tokens during reasoning")
|
||||
Logger.log("Ran out of tokens during reasoning")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ShowMessageType } from "@shared/proto/host/window"
|
||||
import { fileExistsAtPath } from "@utils/fs"
|
||||
import * as path from "path"
|
||||
import { ulid } from "ulid"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
|
||||
interface TaskReconstructionResult {
|
||||
totalTasks: number
|
||||
@@ -143,7 +144,7 @@ async function backupExistingTaskHistory(): Promise<void> {
|
||||
}
|
||||
} catch (error) {
|
||||
// Non-fatal error, just log it
|
||||
console.warn("Failed to backup existing task history:", error)
|
||||
Logger.warn("Failed to backup existing task history:", error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { fileExistsAtPath } from "@utils/fs"
|
||||
import cloneDeep from "clone-deep"
|
||||
import fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { getContextWindowInfo } from "./context-window-utils"
|
||||
|
||||
enum EditType {
|
||||
@@ -119,7 +120,7 @@ export class ContextManager {
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load context history:", error)
|
||||
Logger.error("Failed to load context history:", error)
|
||||
}
|
||||
return new Map()
|
||||
}
|
||||
@@ -139,7 +140,7 @@ export class ContextManager {
|
||||
"utf8",
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Failed to save context history:", error)
|
||||
Logger.error("Failed to save context history:", error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,7 +208,7 @@ export class ContextManager {
|
||||
maxContextWindow: contextWindow,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error parsing API request info for context telemetry:", error)
|
||||
Logger.error("Error parsing API request info for context telemetry:", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -357,7 +358,7 @@ export class ContextManager {
|
||||
// Validate and fix tool_use/tool_result pairing
|
||||
this.ensureToolResultsFollowToolUse(updatedMessages)
|
||||
|
||||
// OLD NOTE: if you try to console log these, don't forget that logging a reference to an array may not provide the same result as logging a slice() snapshot of that array at that exact moment. The following DOES in fact include the latest assistant message.
|
||||
// OLD NOTE: if you try to Logger log these, don't forget that logging a reference to an array may not provide the same result as logging a slice() snapshot of that array at that exact moment. The following DOES in fact include the latest assistant message.
|
||||
return updatedMessages
|
||||
}
|
||||
|
||||
@@ -743,7 +744,7 @@ export class ContextManager {
|
||||
return true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("applyFirstUserMessageReplacement:", error)
|
||||
Logger.error("applyFirstUserMessageReplacement:", error)
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
||||
@@ -4,6 +4,7 @@ import chokidar, { FSWatcher } from "chokidar"
|
||||
import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
import { Controller } from "@/core/controller"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { getCwd } from "@/utils/path"
|
||||
import type { FileMetadataEntry } from "./ContextTrackerTypes"
|
||||
|
||||
@@ -46,7 +47,7 @@ export class FileContextTracker {
|
||||
|
||||
const cwd = await getCwd()
|
||||
if (!cwd) {
|
||||
console.info("No workspace folder available - cannot determine current working directory")
|
||||
Logger.info("No workspace folder available - cannot determine current working directory")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -85,7 +86,7 @@ export class FileContextTracker {
|
||||
try {
|
||||
const cwd = await getCwd()
|
||||
if (!cwd) {
|
||||
console.info("No workspace folder available - cannot determine current working directory")
|
||||
Logger.info("No workspace folder available - cannot determine current working directory")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -95,7 +96,7 @@ export class FileContextTracker {
|
||||
// Set up file watcher for this file
|
||||
await this.setupFileWatcher(filePath)
|
||||
} catch (error) {
|
||||
console.error("Failed to track file operation:", error)
|
||||
Logger.error("Failed to track file operation:", error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,7 +158,7 @@ export class FileContextTracker {
|
||||
metadata.files_in_context.push(newEntry)
|
||||
await saveTaskMetadata(taskId, metadata)
|
||||
} catch (error) {
|
||||
console.error("Failed to add file to metadata:", error)
|
||||
Logger.error("Failed to add file to metadata:", error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,7 +209,7 @@ export class FileContextTracker {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error checking file context metadata:", error)
|
||||
Logger.error("Error checking file context metadata:", error)
|
||||
}
|
||||
|
||||
// Also check deleted task messages for file operations
|
||||
@@ -222,7 +223,7 @@ export class FileContextTracker {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error checking task messages:", error)
|
||||
Logger.error("Error checking task messages:", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -239,7 +240,7 @@ export class FileContextTracker {
|
||||
// are legitimate workspace state keys but don't fit the strict LocalStateKey type system
|
||||
this.controller.stateManager.setWorkspaceState(key as any, files)
|
||||
} catch (error) {
|
||||
console.error("Error storing pending file context warning:", error)
|
||||
Logger.error("Error storing pending file context warning:", error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,7 +253,7 @@ export class FileContextTracker {
|
||||
const files = this.controller.stateManager.getWorkspaceStateKey(key as any) as string[]
|
||||
return files
|
||||
} catch (error) {
|
||||
console.error("Error retrieving pending file context warning:", error)
|
||||
Logger.error("Error retrieving pending file context warning:", error)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
@@ -268,7 +269,7 @@ export class FileContextTracker {
|
||||
return files
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error retrieving pending file context warning:", error)
|
||||
Logger.error("Error retrieving pending file context warning:", error)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
@@ -301,11 +302,11 @@ export class FileContextTracker {
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime
|
||||
console.log(
|
||||
Logger.log(
|
||||
`FileContextTracker: Processed ${existingTaskIds.size} tasks, found ${pendingWarningKeys.length} pending warnings, ${orphanedPendingContextTasks.length} orphaned, deleted ${orphanedPendingContextTasks.length}, took ${duration}ms`,
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("[FileContextTracker] Error cleaning up orphaned file context warnings:", error)
|
||||
Logger.error("[FileContextTracker] Error cleaning up orphaned file context warnings:", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import * as path from "path"
|
||||
import * as sinon from "sinon"
|
||||
|
||||
import * as disk from "@/core/storage/disk"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import * as fsUtils from "@/utils/fs"
|
||||
import { discoverSkills, getAvailableSkills, getSkillContent } from "../skills"
|
||||
|
||||
@@ -29,8 +30,8 @@ describe("Skills Utility Functions", () => {
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
// Stub console.warn to avoid noise in test output
|
||||
sandbox.stub(console, "warn")
|
||||
// Stub Logger.warn to avoid noise in test output
|
||||
sandbox.stub(Logger, "warn")
|
||||
|
||||
// Stub filesystem utilities
|
||||
fileExistsStub = sandbox.stub(fsUtils, "fileExistsAtPath")
|
||||
@@ -295,7 +296,7 @@ Content`)
|
||||
const skills = await discoverSkills(TEST_CWD)
|
||||
|
||||
expect(skills).to.have.lengthOf(0)
|
||||
sinon.assert.calledWithMatch(console.warn as sinon.SinonStub, /missing required 'name' field/)
|
||||
sinon.assert.calledWithMatch(Logger.warn as sinon.SinonStub, /missing required 'name' field/)
|
||||
})
|
||||
|
||||
it("should reject skill with missing description field", async () => {
|
||||
@@ -315,7 +316,7 @@ Content`)
|
||||
const skills = await discoverSkills(TEST_CWD)
|
||||
|
||||
expect(skills).to.have.lengthOf(0)
|
||||
sinon.assert.calledWithMatch(console.warn as sinon.SinonStub, /missing required 'description' field/)
|
||||
sinon.assert.calledWithMatch(Logger.warn as sinon.SinonStub, /missing required 'description' field/)
|
||||
})
|
||||
|
||||
it("should reject skill when name doesn't match directory name", async () => {
|
||||
@@ -336,7 +337,7 @@ Content`)
|
||||
const skills = await discoverSkills(TEST_CWD)
|
||||
|
||||
expect(skills).to.have.lengthOf(0)
|
||||
sinon.assert.calledWithMatch(console.warn as sinon.SinonStub, /doesn't match directory/)
|
||||
sinon.assert.calledWithMatch(Logger.warn as sinon.SinonStub, /doesn't match directory/)
|
||||
})
|
||||
|
||||
it("should handle malformed YAML frontmatter gracefully", async () => {
|
||||
|
||||
@@ -14,6 +14,7 @@ import { fileExistsAtPath, isDirectory, readDirectory } from "@utils/fs"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { Controller } from "@/core/controller"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { parseYamlFrontmatter } from "./frontmatter"
|
||||
import { evaluateRuleConditionals, type RuleEvaluationContext } from "./rule-conditionals"
|
||||
|
||||
@@ -45,10 +46,10 @@ export const getGlobalClineRules = async (
|
||||
activatedConditionalRules.push(...rulesFilesTotal.activatedConditionalRules)
|
||||
}
|
||||
} catch {
|
||||
console.error(`Failed to read .clinerules directory at ${globalClineRulesFilePath}`)
|
||||
Logger.error(`Failed to read .clinerules directory at ${globalClineRulesFilePath}`)
|
||||
}
|
||||
} else {
|
||||
console.error(`${globalClineRulesFilePath} is not a directory`)
|
||||
Logger.error(`${globalClineRulesFilePath} is not a directory`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +106,7 @@ export const getLocalClineRules = async (
|
||||
activatedConditionalRules.push(...rulesFilesTotal.activatedConditionalRules)
|
||||
}
|
||||
} catch {
|
||||
console.error(`Failed to read .clinerules directory at ${clineRulesFilePath}`)
|
||||
Logger.error(`Failed to read .clinerules directory at ${clineRulesFilePath}`)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
@@ -137,7 +138,7 @@ export const getLocalClineRules = async (
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
console.error(`Failed to read .clinerules file at ${clineRulesFilePath}`)
|
||||
Logger.error(`Failed to read .clinerules file at ${clineRulesFilePath}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { fileExistsAtPath, isDirectory } from "@utils/fs"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { Controller } from "@/core/controller"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
|
||||
/**
|
||||
* Refreshes the toggles for windsurf, cursor, and agents rules
|
||||
@@ -75,7 +76,7 @@ export const getLocalWindsurfRules = async (cwd: string, toggles: ClineRulesTogg
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
console.error(`Failed to read .windsurfrules file at ${windsurfRulesFilePath}`)
|
||||
Logger.error(`Failed to read .windsurfrules file at ${windsurfRulesFilePath}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,7 +102,7 @@ export const getLocalCursorRules = async (cwd: string, toggles: ClineRulesToggle
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
console.error(`Failed to read .cursorrules file at ${cursorRulesFilePath}`)
|
||||
Logger.error(`Failed to read .cursorrules file at ${cursorRulesFilePath}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,7 +120,7 @@ export const getLocalCursorRules = async (cwd: string, toggles: ClineRulesToggle
|
||||
cursorRulesDirInstructions = formatResponse.cursorRulesLocalDirectoryInstructions(cwd, rulesFilesTotalContent)
|
||||
}
|
||||
} catch {
|
||||
console.error(`Failed to read .cursor/rules directory at ${cursorRulesDirPath}`)
|
||||
Logger.error(`Failed to read .cursor/rules directory at ${cursorRulesDirPath}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -149,7 +150,7 @@ async function findAgentsMdFiles(cwd: string): Promise<string[]> {
|
||||
return basename === GlobalFileNames.agentsRulesFile.toLowerCase()
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(`Failed to find agents.md files in ${cwd}:`, error)
|
||||
Logger.error(`Failed to find agents.md files in ${cwd}:`, error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
@@ -184,7 +185,7 @@ export const getLocalAgentsRules = async (cwd: string, toggles: ClineRulesToggle
|
||||
}
|
||||
return null
|
||||
} catch (error) {
|
||||
console.error(`Failed to read agents.md file at ${filePath}:`, error)
|
||||
Logger.error(`Failed to read agents.md file at ${filePath}:`, error)
|
||||
return null
|
||||
}
|
||||
}),
|
||||
@@ -194,7 +195,7 @@ export const getLocalAgentsRules = async (cwd: string, toggles: ClineRulesToggle
|
||||
return formatResponse.agentsRulesLocalFileInstructions(cwd, combinedContent)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to read agents.md files:", error)
|
||||
Logger.error("Failed to read agents.md files:", error)
|
||||
}
|
||||
|
||||
return undefined
|
||||
|
||||
@@ -5,6 +5,7 @@ import { fileExistsAtPath, isDirectory, readDirectory } from "@utils/fs"
|
||||
import fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
import { Controller } from "@/core/controller"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { parseYamlFrontmatter } from "./frontmatter"
|
||||
import { evaluateRuleConditionals, RuleEvaluationContext } from "./rule-conditionals"
|
||||
|
||||
@@ -30,7 +31,7 @@ export async function readDirectoryRecursive(
|
||||
}
|
||||
return results
|
||||
} catch (error) {
|
||||
console.error(`Error reading directory ${directoryPath}: ${error}`)
|
||||
Logger.error(`Error reading directory ${directoryPath}: ${error}`)
|
||||
return []
|
||||
}
|
||||
}
|
||||
@@ -98,7 +99,7 @@ export async function synchronizeRuleToggles(
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to synchronize rule toggles for path: ${rulesDirectoryPath}`, error)
|
||||
Logger.error(`Failed to synchronize rule toggles for path: ${rulesDirectoryPath}`, error)
|
||||
}
|
||||
|
||||
return updatedToggles
|
||||
@@ -431,7 +432,7 @@ export async function deleteRuleFile(
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
console.error(`Error deleting file: ${errorMessage}`, error)
|
||||
Logger.error(`Error deleting file: ${errorMessage}`, error)
|
||||
return {
|
||||
success: false,
|
||||
message: `Failed to delete file.`,
|
||||
|
||||
@@ -3,13 +3,14 @@ import type { SkillContent, SkillMetadata } from "@shared/skills"
|
||||
import { fileExistsAtPath, isDirectory } from "@utils/fs"
|
||||
import * as fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { parseYamlFrontmatter } from "./frontmatter"
|
||||
|
||||
/** Parse YAML frontmatter from markdown content (shared helper). */
|
||||
function parseFrontmatter(fileContent: string): { data: Record<string, unknown>; content: string } {
|
||||
const result = parseYamlFrontmatter(fileContent)
|
||||
if (result.parseError) {
|
||||
console.warn("Failed to parse YAML frontmatter:", result.parseError)
|
||||
Logger.warn("Failed to parse YAML frontmatter:", result.parseError)
|
||||
}
|
||||
return { data: result.data, content: result.body }
|
||||
}
|
||||
@@ -39,7 +40,7 @@ async function scanSkillsDirectory(dirPath: string, source: "global" | "project"
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error && "code" in error && (error as NodeJS.ErrnoException).code === "EACCES") {
|
||||
console.warn(`Permission denied reading skills directory: ${dirPath}`)
|
||||
Logger.warn(`Permission denied reading skills directory: ${dirPath}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,17 +64,17 @@ async function loadSkillMetadata(
|
||||
|
||||
// Validate required fields
|
||||
if (!frontmatter.name || typeof frontmatter.name !== "string") {
|
||||
console.warn(`Skill at ${skillDir} missing required 'name' field`)
|
||||
Logger.warn(`Skill at ${skillDir} missing required 'name' field`)
|
||||
return null
|
||||
}
|
||||
if (!frontmatter.description || typeof frontmatter.description !== "string") {
|
||||
console.warn(`Skill at ${skillDir} missing required 'description' field`)
|
||||
Logger.warn(`Skill at ${skillDir} missing required 'description' field`)
|
||||
return null
|
||||
}
|
||||
|
||||
// Name must match directory name per spec
|
||||
if (frontmatter.name !== skillName) {
|
||||
console.warn(`Skill name "${frontmatter.name}" doesn't match directory "${skillName}"`)
|
||||
Logger.warn(`Skill name "${frontmatter.name}" doesn't match directory "${skillName}"`)
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -84,7 +85,7 @@ async function loadSkillMetadata(
|
||||
source,
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Failed to load skill at ${skillDir}:`, error)
|
||||
Logger.warn(`Failed to load skill at ${skillDir}:`, error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AuthState, AuthStateChangedRequest } from "@shared/proto/cline/account"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import type { Controller } from "../index"
|
||||
|
||||
/**
|
||||
@@ -16,7 +17,7 @@ export async function authStateChanged(controller: Controller, request: AuthStat
|
||||
// Return the same user info
|
||||
return AuthState.create({ user: request.user })
|
||||
} catch (error) {
|
||||
console.error(`Failed to update auth state: ${error}`)
|
||||
Logger.error(`Failed to update auth state: ${error}`)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { GetOrganizationCreditsRequest, OrganizationCreditsData, OrganizationUsageTransaction } from "@shared/proto/cline/account"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import type { Controller } from "../index"
|
||||
|
||||
/**
|
||||
@@ -50,7 +51,7 @@ export async function getOrganizationCredits(
|
||||
) || [],
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch organization credits data: ${error}`)
|
||||
Logger.error(`Failed to fetch organization credits data: ${error}`)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { UserCreditsData } from "@shared/proto/cline/account"
|
||||
import type { EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import type { Controller } from "../index"
|
||||
|
||||
/**
|
||||
@@ -32,7 +33,7 @@ export async function getUserCredits(controller: Controller, _request: EmptyRequ
|
||||
paymentTransactions: paymentTransactions,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch user credits data: ${error}`)
|
||||
Logger.error(`Failed to fetch user credits data: ${error}`)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Empty, EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { openAiCodexOAuthManager } from "@/integrations/openai-codex/oauth"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { openExternal } from "@/utils/env"
|
||||
import { ShowMessageType } from "@shared/proto/host/window"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { openAiCodexOAuthManager } from "@/integrations/openai-codex/oauth"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { openExternal } from "@/utils/env"
|
||||
import { Controller } from ".."
|
||||
|
||||
/**
|
||||
@@ -29,7 +30,7 @@ export async function openAiCodexSignIn(controller: Controller, _: EmptyRequest)
|
||||
await controller.postStateToWebview()
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("[openAiCodexSignIn] OAuth callback failed:", error)
|
||||
Logger.error("[openAiCodexSignIn] OAuth callback failed:", error)
|
||||
openAiCodexOAuthManager.cancelAuthorizationFlow()
|
||||
// Don't show notification for timeouts (user likely just abandoned)
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
@@ -41,7 +42,7 @@ export async function openAiCodexSignIn(controller: Controller, _: EmptyRequest)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("[openAiCodexSignIn] Failed to start OAuth flow:", error)
|
||||
Logger.error("[openAiCodexSignIn] Failed to start OAuth flow:", error)
|
||||
openAiCodexOAuthManager.cancelAuthorizationFlow()
|
||||
throw error
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Empty, EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { openAiCodexOAuthManager } from "@/integrations/openai-codex/oauth"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { Controller } from ".."
|
||||
|
||||
/**
|
||||
@@ -16,7 +17,7 @@ export async function openAiCodexSignOut(controller: Controller, _: EmptyRequest
|
||||
// Update the state to reflect sign out
|
||||
await controller.postStateToWebview()
|
||||
} catch (error) {
|
||||
console.error("[openAiCodexSignOut] Failed to sign out:", error)
|
||||
Logger.error("[openAiCodexSignOut] Failed to sign out:", error)
|
||||
throw error
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { BrowserConnectionInfo } from "@shared/proto/cline/browser"
|
||||
import { EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { Controller } from "../index"
|
||||
|
||||
/**
|
||||
@@ -36,7 +37,7 @@ export async function getBrowserConnectionInfo(controller: Controller, _: EmptyR
|
||||
host: browserSettings.remoteBrowserHost || "",
|
||||
})
|
||||
} catch (error: unknown) {
|
||||
console.error("Error getting browser connection info:", error)
|
||||
Logger.error("Error getting browser connection info:", error)
|
||||
return BrowserConnectionInfo.create({
|
||||
isConnected: false,
|
||||
isRemote: false,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ChromePath } from "@shared/proto/cline/browser"
|
||||
import { EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { BrowserSession } from "../../../services/browser/BrowserSession"
|
||||
import { Controller } from "../index"
|
||||
|
||||
@@ -19,7 +20,7 @@ export async function getDetectedChromePath(controller: Controller, _: EmptyRequ
|
||||
isBundled: result.isBundled,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Error getting detected Chrome path:", error)
|
||||
Logger.error("Error getting detected Chrome path:", error)
|
||||
return ChromePath.create({
|
||||
path: "",
|
||||
isBundled: false,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Empty } from "@shared/proto/cline/common"
|
||||
import pWaitFor from "p-wait-for"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { ShowMessageType } from "@/shared/proto/index.host"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ClineCheckpointRestore } from "../../../shared/WebviewMessage"
|
||||
import { Controller } from ".."
|
||||
|
||||
@@ -14,7 +15,7 @@ export async function checkpointRestore(controller: Controller, request: Checkpo
|
||||
await pWaitFor(() => controller.task?.taskState.isInitialized === true, {
|
||||
timeout: 3_000,
|
||||
}).catch((error) => {
|
||||
console.log("Failed to init new Cline instance to restore checkpoint", error)
|
||||
Logger.log("Failed to init new Cline instance to restore checkpoint", error)
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Failed to restore checkpoint",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { CheckpointEvent, CheckpointEvent_OperationType, CheckpointSubscriptionRequest } from "@shared/proto/cline/checkpoints"
|
||||
import { Timestamp } from "@shared/proto/google/protobuf/timestamp"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { getRequestRegistry, StreamingResponseHandler } from "../grpc-handler"
|
||||
import { Controller } from "../index"
|
||||
|
||||
@@ -105,7 +106,7 @@ export async function sendCheckpointEvent(eventData: CheckpointEventData): Promi
|
||||
try {
|
||||
await responseStream(event, false) // Not the last message
|
||||
} catch (error) {
|
||||
console.error("Error sending checkpoint event:", error)
|
||||
Logger.error("Error sending checkpoint event:", error)
|
||||
subscriptions.delete(responseStream)
|
||||
if (subscriptions.size === 0) {
|
||||
activeCheckpointSubscriptions.delete(cwdHash)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { getFileMentionFromPath } from "@/core/mentions"
|
||||
import { singleFileDiagnosticsToProblemsString } from "@/integrations/diagnostics"
|
||||
import { Logger } from "@/services/logging/Logger"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { CommandContext, Empty } from "@/shared/proto/index.cline"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { Controller } from "../index"
|
||||
import { sendAddToInputEvent } from "../ui/subscribeToAddToInput"
|
||||
|
||||
@@ -39,7 +39,7 @@ export async function addToCline(controller: Controller, request: CommandContext
|
||||
await sendAddToInputEvent(input)
|
||||
}
|
||||
|
||||
console.log("addToCline", request.selectedText, filePath, request.language, notebookContext ? "with notebook context" : "")
|
||||
Logger.log("addToCline", request.selectedText, filePath, request.language)
|
||||
telemetryService.captureButtonClick("codeAction_addToChat", controller.task?.ulid)
|
||||
|
||||
return {}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { getFileMentionFromPath } from "@/core/mentions"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { Logger } from "@/services/logging/Logger"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { CommandContext, Empty } from "@/shared/proto/index.cline"
|
||||
import { ShowMessageType } from "@/shared/proto/index.host"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { Controller } from "../index"
|
||||
|
||||
export async function explainWithCline(
|
||||
|
||||
@@ -2,6 +2,7 @@ import { getFileMentionFromPath } from "@/core/mentions"
|
||||
import { singleFileDiagnosticsToProblemsString } from "@/integrations/diagnostics"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { CommandContext, Empty } from "@/shared/proto/index.cline"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { Controller } from "../index"
|
||||
|
||||
export async function fixWithCline(controller: Controller, request: CommandContext): Promise<Empty> {
|
||||
@@ -9,11 +10,11 @@ export async function fixWithCline(controller: Controller, request: CommandConte
|
||||
const fileMention = await getFileMentionFromPath(filePath)
|
||||
const problemsString = await singleFileDiagnosticsToProblemsString(filePath, request.diagnostics)
|
||||
|
||||
const taskMessage = `Fix the following code in ${fileMention}
|
||||
\`\`\`\n${request.selectedText}\n\`\`\`\n\nProblems:\n${problemsString}`
|
||||
|
||||
await controller.initTask(taskMessage)
|
||||
console.log("fixWithCline", request.selectedText, request.filePath, request.language, problemsString)
|
||||
await controller.initTask(
|
||||
`Fix the following code in ${fileMention}
|
||||
\`\`\`\n${request.selectedText}\n\`\`\`\n\nProblems:\n${problemsString}`,
|
||||
)
|
||||
Logger.log("fixWithCline", request.selectedText, request.filePath, request.language, problemsString)
|
||||
|
||||
telemetryService.captureButtonClick("codeAction_fixWithCline", controller.task?.ulid)
|
||||
return {}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { getFileMentionFromPath } from "@/core/mentions"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { Logger } from "@/services/logging/Logger"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { CommandContext, Empty } from "@/shared/proto/index.cline"
|
||||
import { ShowMessageType } from "@/shared/proto/index.host"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { Controller } from "../index"
|
||||
|
||||
export async function improveWithCline(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { RecordingResult } from "@shared/proto/cline/dictation"
|
||||
import { audioRecordingService } from "@/services/dictation/AudioRecordingService"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { Controller } from ".."
|
||||
|
||||
/**
|
||||
@@ -19,7 +20,7 @@ export const cancelRecording = async (controller: Controller): Promise<Recording
|
||||
isSuccess = !!result?.success
|
||||
errorMessage = result?.error ?? ""
|
||||
} catch (error) {
|
||||
console.error("Error canceling recording:", error)
|
||||
Logger.error("Error canceling recording:", error)
|
||||
isSuccess = false
|
||||
errorMessage = error instanceof Error ? error.message : "Unknown error occurred"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { RecordingStatus } from "@shared/proto/cline/dictation"
|
||||
import { audioRecordingService } from "@/services/dictation/AudioRecordingService"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
|
||||
/**
|
||||
* Gets the current recording status
|
||||
@@ -15,7 +16,7 @@ export const getRecordingStatus = async (): Promise<RecordingStatus> => {
|
||||
error: status.error ?? "",
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Error getting recording status:", error)
|
||||
Logger.error("Error getting recording status:", error)
|
||||
return RecordingStatus.create({
|
||||
isRecording: false,
|
||||
durationSeconds: 0,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { audioRecordingService } from "@/services/dictation/AudioRecordingServic
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { AUDIO_PROGRAM_CONFIG } from "@/shared/audioProgramConstants"
|
||||
import { ShowMessageType } from "@/shared/proto/host/window"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { Controller } from ".."
|
||||
|
||||
/**
|
||||
@@ -24,7 +25,7 @@ async function handleInstallWithCline(
|
||||
await controller.postStateToWebview()
|
||||
await controller.initTask(installTask)
|
||||
|
||||
HostProvider.get().logToChannel(`Started task to install ${dependencyName}`)
|
||||
Logger.log(`[handleInstallWithCline] Started task to install ${dependencyName}`)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -143,7 +144,7 @@ export const startRecording = async (controller: Controller): Promise<RecordingR
|
||||
error: result.error || "",
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Error starting recording:", error)
|
||||
Logger.error("Error starting recording:", error)
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"
|
||||
|
||||
// Handle different error types
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { RecordedAudio } from "@shared/proto/cline/dictation"
|
||||
import { audioRecordingService } from "@/services/dictation/AudioRecordingService"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { Controller } from ".."
|
||||
|
||||
/**
|
||||
@@ -24,7 +25,7 @@ export const stopRecording = async (controller: Controller): Promise<RecordedAud
|
||||
error: result.error ?? "",
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Error stopping recording:", error)
|
||||
Logger.error("Error stopping recording:", error)
|
||||
|
||||
telemetryService.captureVoiceRecordingStopped(taskId, recordingDuration, false, process.platform)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { HostProvider } from "@/hosts/host-provider"
|
||||
import { getVoiceTranscriptionService } from "@/services/dictation/VoiceTranscriptionService"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { ShowMessageType } from "@/shared/proto/host/window"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { Controller } from ".."
|
||||
|
||||
/**
|
||||
@@ -59,7 +60,7 @@ export const transcribeAudio = async (controller: Controller, request: Transcrib
|
||||
error: result.error ?? "",
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Error transcribing audio:", error)
|
||||
Logger.error("Error transcribing audio:", error)
|
||||
const durationMs = Date.now() - startTime
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ describe("ifFileExistsRelativePath", () => {
|
||||
let sandbox: sinon.SinonSandbox
|
||||
let mockController: Controller
|
||||
let getWorkspacePathStub: sinon.SinonStub
|
||||
let consoleErrorStub: sinon.SinonStub
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
@@ -20,9 +19,6 @@ describe("ifFileExistsRelativePath", () => {
|
||||
|
||||
// Stub getWorkspacePath utility
|
||||
getWorkspacePathStub = sandbox.stub(pathUtils, "getWorkspacePath")
|
||||
|
||||
// Stub console.error to prevent test output pollution
|
||||
consoleErrorStub = sandbox.stub(console, "error")
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
@@ -43,12 +39,11 @@ describe("ifFileExistsRelativePath", () => {
|
||||
expect(typeof result.value).to.equal("boolean")
|
||||
})
|
||||
|
||||
it("should return false and log error when no workspace path is available", async () => {
|
||||
it("should return false when no workspace path is available", async () => {
|
||||
const noWorkspaceScenarios = [null, undefined]
|
||||
|
||||
for (const workspaceValue of noWorkspaceScenarios) {
|
||||
getWorkspacePathStub.resolves(workspaceValue)
|
||||
consoleErrorStub.resetHistory()
|
||||
|
||||
const request = StringRequest.create({
|
||||
value: "src/test.ts",
|
||||
@@ -57,7 +52,6 @@ describe("ifFileExistsRelativePath", () => {
|
||||
const result = await ifFileExistsRelativePath(mockController, request)
|
||||
|
||||
expect(result).to.deep.equal(BooleanResponse.create({ value: false }))
|
||||
expect(consoleErrorStub.called).to.be.true
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { expect } from "chai"
|
||||
import { afterEach, beforeEach, describe, it } from "mocha"
|
||||
import * as path from "path"
|
||||
import * as sinon from "sinon"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { openFileRelativePath } from "../openFileRelativePath"
|
||||
|
||||
describe("openFileRelativePath", () => {
|
||||
@@ -28,7 +29,7 @@ describe("openFileRelativePath", () => {
|
||||
getWorkspacePathStub = sandbox.stub(pathUtils, "getWorkspacePath")
|
||||
|
||||
// Stub console.error to prevent test output pollution
|
||||
consoleErrorStub = sandbox.stub(console, "error")
|
||||
consoleErrorStub = sandbox.stub(Logger, "error")
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Empty, StringRequest } from "@shared/proto/cline/common"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { writeTextToClipboard } from "@/utils/env"
|
||||
import { Controller } from ".."
|
||||
|
||||
@@ -14,7 +15,7 @@ export async function copyToClipboard(_controller: Controller, request: StringRe
|
||||
await writeTextToClipboard(request.value)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error copying to clipboard:", error)
|
||||
Logger.error("Error copying to clipboard:", error)
|
||||
}
|
||||
return Empty.create()
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { RuleFile, RuleFileRequest } from "@shared/proto/cline/file"
|
||||
import { refreshWorkflowToggles } from "@/core/context/instructions/user-instructions/workflows"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { ShowMessageType } from "@/shared/proto/host/window"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { getCwd, getDesktopDir } from "@/utils/path"
|
||||
import { Controller } from ".."
|
||||
import { openFile } from "./openFile"
|
||||
@@ -24,7 +25,7 @@ export async function createRuleFile(controller: Controller, request: RuleFileRe
|
||||
!request.type ||
|
||||
typeof request.type !== "string"
|
||||
) {
|
||||
console.error("createRuleFile: Missing or invalid parameters", {
|
||||
Logger.error("createRuleFile: Missing or invalid parameters", {
|
||||
isGlobal: typeof request.isGlobal === "boolean" ? request.isGlobal : `Invalid: ${typeof request.isGlobal}`,
|
||||
filename: typeof request.filename === "string" ? request.filename : `Invalid: ${typeof request.filename}`,
|
||||
type: typeof request.type === "string" ? request.type : `Invalid: ${typeof request.type}`,
|
||||
|
||||
@@ -4,6 +4,7 @@ import path from "path"
|
||||
import { ensureSkillsDirectoryExists } from "@/core/storage/disk"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { ShowMessageType } from "@/shared/proto/host/window"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { fileExistsAtPath } from "@/utils/fs"
|
||||
import { Controller } from ".."
|
||||
import { openFile } from "./openFile"
|
||||
@@ -38,7 +39,7 @@ export async function createSkillFile(controller: Controller, request: CreateSki
|
||||
const { skillName, isGlobal } = request
|
||||
|
||||
if (!skillName || typeof skillName !== "string" || typeof isGlobal !== "boolean") {
|
||||
console.error("createSkillFile: Missing or invalid parameters", {
|
||||
Logger.error("createSkillFile: Missing or invalid parameters", {
|
||||
skillName: typeof skillName === "string" ? skillName : `Invalid: ${typeof skillName}`,
|
||||
isGlobal: typeof isGlobal === "boolean" ? isGlobal : `Invalid: ${typeof isGlobal}`,
|
||||
})
|
||||
|
||||
@@ -3,6 +3,7 @@ import { getWorkspaceBasename } from "@core/workspace"
|
||||
import { RuleFile, RuleFileRequest } from "@shared/proto/cline/file"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { ShowMessageType } from "@/shared/proto/host/window"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { Controller } from ".."
|
||||
|
||||
/**
|
||||
@@ -20,7 +21,7 @@ export async function deleteRuleFile(controller: Controller, request: RuleFileRe
|
||||
!request.type ||
|
||||
typeof request.type !== "string"
|
||||
) {
|
||||
console.error("deleteRuleFile: Missing or invalid parameters", {
|
||||
Logger.error("deleteRuleFile: Missing or invalid parameters", {
|
||||
isGlobal: typeof request.isGlobal === "boolean" ? request.isGlobal : `Invalid: ${typeof request.isGlobal}`,
|
||||
rulePath: typeof request.rulePath === "string" ? request.rulePath : `Invalid: ${typeof request.rulePath}`,
|
||||
type: typeof request.type === "string" ? request.type : `Invalid: ${typeof request.type}`,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { DeleteSkillRequest, SkillsToggles } from "@shared/proto/cline/file"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { fileExistsAtPath } from "@/utils/fs"
|
||||
import { Controller } from ".."
|
||||
|
||||
@@ -14,7 +15,7 @@ export async function deleteSkillFile(controller: Controller, request: DeleteSki
|
||||
const { skillPath, isGlobal } = request
|
||||
|
||||
if (!skillPath || typeof skillPath !== "string" || typeof isGlobal !== "boolean") {
|
||||
console.error("deleteSkillFile: Missing or invalid parameters", {
|
||||
Logger.error("deleteSkillFile: Missing or invalid parameters", {
|
||||
skillPath: typeof skillPath === "string" ? skillPath : `Invalid: ${typeof skillPath}`,
|
||||
isGlobal: typeof isGlobal === "boolean" ? isGlobal : `Invalid: ${typeof isGlobal}`,
|
||||
})
|
||||
@@ -26,7 +27,7 @@ export async function deleteSkillFile(controller: Controller, request: DeleteSki
|
||||
|
||||
// Verify the path exists
|
||||
if (!(await fileExistsAtPath(skillDir))) {
|
||||
console.warn(`deleteSkillFile: Skill directory not found: ${skillDir}`)
|
||||
Logger.warn(`deleteSkillFile: Skill directory not found: ${skillDir}`)
|
||||
// Return current toggles anyway
|
||||
const globalToggles = controller.stateManager.getGlobalSettingsKey("globalSkillsToggles") || {}
|
||||
const localToggles = controller.stateManager.getWorkspaceStateKey("localSkillsToggles") || {}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { RelativePaths, RelativePathsRequest } from "@shared/proto/cline/file"
|
||||
import * as path from "path"
|
||||
import { URI } from "vscode-uri"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { isDirectory } from "@/utils/fs"
|
||||
import { asRelativePath } from "@/utils/path"
|
||||
import { Controller } from ".."
|
||||
@@ -17,7 +18,7 @@ export async function getRelativePaths(_controller: Controller, request: Relativ
|
||||
try {
|
||||
result.push(await getRelativePath(uriString))
|
||||
} catch (error) {
|
||||
console.error(`Error calculating relative path for ${uriString}:`, error)
|
||||
Logger.error(`Error calculating relative path for ${uriString}:`, error)
|
||||
}
|
||||
}
|
||||
return RelativePaths.create({ paths: result })
|
||||
|
||||
@@ -15,7 +15,6 @@ export async function ifFileExistsRelativePath(_controller: Controller, request:
|
||||
|
||||
if (!workspacePath) {
|
||||
// If no workspace is open, return false
|
||||
console.error("Error in ifFileExistsRelativePath: No workspace path available") // TODO
|
||||
return BooleanResponse.create({ value: false })
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { workspaceResolver } from "@core/workspace"
|
||||
import { openFile as openFileIntegration } from "@integrations/misc/open-file"
|
||||
import { Empty, StringRequest } from "@shared/proto/cline/common"
|
||||
import { getWorkspacePath } from "@utils/path"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { Controller } from ".."
|
||||
|
||||
/**
|
||||
@@ -14,7 +15,7 @@ export async function openFileRelativePath(_controller: Controller, request: Str
|
||||
const workspacePath = await getWorkspacePath()
|
||||
|
||||
if (!workspacePath) {
|
||||
console.error("Error in openFileRelativePath: No workspace path available")
|
||||
Logger.error("Error in openFileRelativePath: No workspace path available")
|
||||
return Empty.create()
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { refreshExternalRulesToggles } from "@core/context/instructions/user-ins
|
||||
import { refreshWorkflowToggles } from "@core/context/instructions/user-instructions/workflows"
|
||||
import { EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { RefreshedRules } from "@shared/proto/cline/file"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { getCwd, getDesktopDir } from "@/utils/path"
|
||||
import type { Controller } from "../index"
|
||||
|
||||
@@ -32,7 +33,7 @@ export async function refreshRules(controller: Controller, _request: EmptyReques
|
||||
globalWorkflowToggles: { toggles: globalWorkflowToggles },
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to refresh rules:", error)
|
||||
Logger.error("Failed to refresh rules:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { StringRequest } from "@shared/proto/cline/common"
|
||||
import { GitCommits } from "@shared/proto/cline/file"
|
||||
import { searchCommits as searchCommitsUtil } from "@utils/git"
|
||||
import { getWorkspacePath } from "@utils/path"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { Controller } from ".."
|
||||
|
||||
/**
|
||||
@@ -21,7 +22,7 @@ export async function searchCommits(_controller: Controller, request: StringRequ
|
||||
|
||||
return GitCommits.create({ commits })
|
||||
} catch (error) {
|
||||
console.error(`Error searching commits: ${JSON.stringify(error)}`)
|
||||
Logger.error(`Error searching commits: ${JSON.stringify(error)}`)
|
||||
return GitCommits.create({ commits: [] })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { telemetryService } from "@services/telemetry"
|
||||
import { FileSearchRequest, FileSearchResults, FileSearchType } from "@shared/proto/cline/file"
|
||||
import { convertSearchResultsToProtoFileInfos } from "@shared/proto-conversions/file/search-result-conversion"
|
||||
import { getWorkspacePath } from "@utils/path"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { Controller } from ".."
|
||||
|
||||
/**
|
||||
@@ -41,8 +42,8 @@ export async function searchFiles(controller: Controller, request: FileSearchReq
|
||||
const workspacePath = await getWorkspacePath()
|
||||
|
||||
if (!workspacePath) {
|
||||
console.error("Error in searchFiles: No workspace path available")
|
||||
await telemetryService.captureMentionFailed("folder", "not_found", "No workspace path available")
|
||||
Logger.error("Error in searchFiles: No workspace path available")
|
||||
telemetryService.captureMentionFailed("folder", "not_found", "No workspace path available")
|
||||
return { results: [], mentionsRequestId: request.mentionsRequestId }
|
||||
}
|
||||
|
||||
@@ -78,7 +79,7 @@ export async function searchFiles(controller: Controller, request: FileSearchReq
|
||||
return { results: protoResults, mentionsRequestId: request.mentionsRequestId }
|
||||
} catch (error) {
|
||||
// Log the error but don't include it in the response, following the pattern in searchCommits
|
||||
console.error("Error in searchFiles:", error)
|
||||
Logger.error("Error in searchFiles:", error)
|
||||
|
||||
// Track as a search execution error with appropriate error type
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { selectFiles as selectFilesIntegration } from "@integrations/misc/process-files"
|
||||
import { BooleanRequest, StringArrays } from "@shared/proto/cline/common"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { Controller } from ".."
|
||||
|
||||
/**
|
||||
@@ -13,7 +14,7 @@ export async function selectFiles(_controller: Controller, request: BooleanReque
|
||||
const { images, files } = await selectFilesIntegration(request.value)
|
||||
return StringArrays.create({ values1: images, values2: files })
|
||||
} catch (error) {
|
||||
console.error("Error selecting images & files:", error)
|
||||
Logger.error("Error selecting images & files:", error)
|
||||
// Return empty array on error
|
||||
return StringArrays.create({ values1: [], values2: [] })
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ToggleAgentsRuleRequest } from "@shared/proto/cline/file"
|
||||
import { ClineRulesToggles } from "@shared/proto/cline/file"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import type { Controller } from "../index"
|
||||
|
||||
/**
|
||||
@@ -12,7 +13,7 @@ export async function toggleAgentsRule(controller: Controller, request: ToggleAg
|
||||
const { rulePath, enabled } = request
|
||||
|
||||
if (!rulePath || typeof enabled !== "boolean") {
|
||||
console.error("toggleAgentsRule: Missing or invalid parameters", {
|
||||
Logger.error("toggleAgentsRule: Missing or invalid parameters", {
|
||||
rulePath,
|
||||
enabled: typeof enabled === "boolean" ? enabled : `Invalid: ${typeof enabled}`,
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@ import { getWorkspaceBasename } from "@core/workspace"
|
||||
import type { ToggleClineRuleRequest } from "@shared/proto/cline/file"
|
||||
import { RuleScope, ToggleClineRules } from "@shared/proto/cline/file"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import type { Controller } from "../index"
|
||||
|
||||
/**
|
||||
@@ -14,7 +15,7 @@ export async function toggleClineRule(controller: Controller, request: ToggleCli
|
||||
const { scope, rulePath, enabled } = request
|
||||
|
||||
if (!rulePath || typeof enabled !== "boolean" || scope === undefined) {
|
||||
console.error("toggleClineRule: Missing or invalid parameters", {
|
||||
Logger.error("toggleClineRule: Missing or invalid parameters", {
|
||||
rulePath,
|
||||
scope,
|
||||
enabled: typeof enabled === "boolean" ? enabled : `Invalid: ${typeof enabled}`,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ToggleCursorRuleRequest } from "@shared/proto/cline/file"
|
||||
import { ClineRulesToggles } from "@shared/proto/cline/file"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import type { Controller } from "../index"
|
||||
|
||||
/**
|
||||
@@ -12,7 +13,7 @@ export async function toggleCursorRule(controller: Controller, request: ToggleCu
|
||||
const { rulePath, enabled } = request
|
||||
|
||||
if (!rulePath || typeof enabled !== "boolean") {
|
||||
console.error("toggleCursorRule: Missing or invalid parameters", {
|
||||
Logger.error("toggleCursorRule: Missing or invalid parameters", {
|
||||
rulePath,
|
||||
enabled: typeof enabled === "boolean" ? enabled : `Invalid: ${typeof enabled}`,
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { SkillsToggles, ToggleSkillRequest } from "@shared/proto/cline/file"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { Controller } from ".."
|
||||
|
||||
/**
|
||||
@@ -11,7 +12,7 @@ export async function toggleSkill(controller: Controller, request: ToggleSkillRe
|
||||
const { skillPath, isGlobal, enabled } = request
|
||||
|
||||
if (!skillPath || typeof enabled !== "boolean" || typeof isGlobal !== "boolean") {
|
||||
console.error("toggleSkill: Missing or invalid parameters", {
|
||||
Logger.error("toggleSkill: Missing or invalid parameters", {
|
||||
skillPath,
|
||||
isGlobal,
|
||||
enabled: typeof enabled === "boolean" ? enabled : `Invalid: ${typeof enabled}`,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ToggleWindsurfRuleRequest } from "@shared/proto/cline/file"
|
||||
import { ClineRulesToggles } from "@shared/proto/cline/file"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import type { Controller } from "../index"
|
||||
|
||||
/**
|
||||
@@ -12,7 +13,7 @@ export async function toggleWindsurfRule(controller: Controller, request: Toggle
|
||||
const { rulePath, enabled } = request
|
||||
|
||||
if (!rulePath || typeof enabled !== "boolean") {
|
||||
console.error("toggleWindsurfRule: Missing or invalid parameters", {
|
||||
Logger.error("toggleWindsurfRule: Missing or invalid parameters", {
|
||||
rulePath,
|
||||
enabled: typeof enabled === "boolean" ? enabled : `Invalid: ${typeof enabled}`,
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ClineRulesToggles, RuleScope, ToggleWorkflowRequest } from "@shared/proto/cline/file"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { Controller } from ".."
|
||||
|
||||
/**
|
||||
@@ -11,7 +12,7 @@ export async function toggleWorkflow(controller: Controller, request: ToggleWork
|
||||
const { workflowPath, enabled, scope } = request
|
||||
|
||||
if (!workflowPath || typeof enabled !== "boolean" || scope === undefined) {
|
||||
console.error("toggleWorkflow: Missing or invalid parameters", {
|
||||
Logger.error("toggleWorkflow: Missing or invalid parameters", {
|
||||
workflowPath,
|
||||
scope,
|
||||
enabled: typeof enabled === "boolean" ? enabled : `Invalid: ${typeof enabled}`,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { serviceHandlers } from "@generated/hosts/vscode/protobus-services"
|
||||
import { GrpcRecorderBuilder } from "@/core/controller/grpc-recorder/grpc-recorder.builder"
|
||||
import { GrpcRequestRegistry } from "@/core/controller/grpc-request-registry"
|
||||
import { ExtensionMessage } from "@/shared/ExtensionMessage"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { GrpcCancel, GrpcRequest } from "@/shared/WebviewMessage"
|
||||
|
||||
/**
|
||||
@@ -28,7 +29,7 @@ function withRecordingMiddleware(postMessage: PostMessageToWebview, controller:
|
||||
response.grpc_response,
|
||||
)
|
||||
} catch (e) {
|
||||
console.warn("Failed to record gRPC response:", e)
|
||||
Logger.warn("Failed to record gRPC response:", e)
|
||||
}
|
||||
}
|
||||
return postMessage(response)
|
||||
@@ -42,7 +43,7 @@ function recordRequest(request: GrpcRequest, controller: Controller): void {
|
||||
try {
|
||||
GrpcRecorderBuilder.getRecorder(controller).recordRequest(request)
|
||||
} catch (e) {
|
||||
console.warn("Failed to record gRPC request:", e)
|
||||
Logger.warn("Failed to record gRPC request:", e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +92,7 @@ async function handleUnaryRequest(
|
||||
})
|
||||
} catch (error) {
|
||||
// Send error response
|
||||
console.log("Protobus error:", error)
|
||||
Logger.log("Protobus error:", error)
|
||||
await postMessageToWebview({
|
||||
type: "grpc_response",
|
||||
grpc_response: {
|
||||
@@ -142,7 +143,7 @@ async function handleStreamingRequest(
|
||||
// The stream will be closed when the client disconnects or when the service explicitly ends it
|
||||
} catch (error) {
|
||||
// Send error response
|
||||
console.log("Protobus error:", error)
|
||||
Logger.log("Protobus error:", error)
|
||||
await postMessageToWebview({
|
||||
type: "grpc_response",
|
||||
grpc_response: {
|
||||
@@ -173,7 +174,7 @@ export async function handleGrpcRequestCancel(postMessageToWebview: PostMessageT
|
||||
},
|
||||
})
|
||||
} else {
|
||||
console.log(`[DEBUG] Request not found for cancellation: ${request.request_id}`)
|
||||
Logger.log(`[DEBUG] Request not found for cancellation: ${request.request_id}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
GrpcSessionLog,
|
||||
SessionStats,
|
||||
} from "@/core/controller/grpc-recorder/types"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
|
||||
export class GrpcRecorderNoops implements IRecorder {
|
||||
recordRequest(_request: GrpcRequest): void {}
|
||||
@@ -55,7 +56,7 @@ export class GrpcRecorder implements IRecorder {
|
||||
}
|
||||
|
||||
this.fileHandler.initialize(this.sessionLog).catch((error) => {
|
||||
console.error("Failed to initialize gRPC log file:", error)
|
||||
Logger.error("Failed to initialize gRPC log file:", error)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -118,7 +119,7 @@ export class GrpcRecorder implements IRecorder {
|
||||
const pendingRequest = this.pendingRequests.get(requestId)
|
||||
|
||||
if (!pendingRequest) {
|
||||
console.warn(`No pending request found for response with ID: ${requestId}`)
|
||||
Logger.warn(`No pending request found for response with ID: ${requestId}`)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -142,7 +143,7 @@ export class GrpcRecorder implements IRecorder {
|
||||
|
||||
this.flushLogAsync()
|
||||
|
||||
this.runHooks(entry).catch((e) => console.error("Post-record hook failed:", e))
|
||||
this.runHooks(entry).catch((e) => Logger.error("Post-record hook failed:", e))
|
||||
}
|
||||
|
||||
private async runHooks(entry: GrpcLogEntry): Promise<void> {
|
||||
@@ -181,7 +182,7 @@ export class GrpcRecorder implements IRecorder {
|
||||
public recordError(requestId: string, error: string): void {
|
||||
const pendingRequest = this.pendingRequests.get(requestId)
|
||||
if (!pendingRequest) {
|
||||
console.warn(`No pending request found for error with ID: ${requestId}`)
|
||||
Logger.warn(`No pending request found for error with ID: ${requestId}`)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -200,7 +201,7 @@ export class GrpcRecorder implements IRecorder {
|
||||
private flushLogAsync(): void {
|
||||
setImmediate(() => {
|
||||
this.fileHandler.write(this.sessionLog).catch((error) => {
|
||||
console.error("Failed to flush gRPC log:", error)
|
||||
Logger.error("Failed to flush gRPC log:", error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { StreamingResponseHandler } from "./grpc-handler"
|
||||
|
||||
/**
|
||||
@@ -54,7 +55,6 @@ export class GrpcRequestRegistry {
|
||||
timestamp: new Date(),
|
||||
responseStream,
|
||||
})
|
||||
console.log(`[DEBUG] Registered request: ${requestId}`)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,9 +69,8 @@ export class GrpcRequestRegistry {
|
||||
}
|
||||
try {
|
||||
requestInfo.cleanup()
|
||||
console.log(`[DEBUG] Cleaned up request: ${requestId}`)
|
||||
} catch (error) {
|
||||
console.error(`Error cleaning up request ${requestId}:`, error)
|
||||
Logger.error(`Error cleaning up request ${requestId}:`, error)
|
||||
}
|
||||
this.activeRequests.delete(requestId)
|
||||
return true
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user