mirror of
https://github.com/cline/cline.git
synced 2026-09-03 12:14:00 +08:00
Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fc3d986d05 | |||
| 520ddb83bb | |||
| d96c5d4b40 | |||
| e6cbae0edc | |||
| 76bd0926e6 | |||
| 819f9ce00d | |||
| fd8cecddd5 | |||
| a502bd8653 | |||
| d48d5ee74d | |||
| cd320ea01f | |||
| 60485277c4 | |||
| 0a8f1ef248 | |||
| 4b2619daf7 | |||
| 913cf4b74d | |||
| 8e5be3f648 | |||
| 6c519ff6e5 | |||
| c61f9a9394 | |||
| fa53f301a4 | |||
| c36e375af5 | |||
| 94b02bf052 | |||
| f10b6f39be | |||
| 42e6a24d0f | |||
| 452733c3fd | |||
| 7fadcfaa3f | |||
| 9989225b69 | |||
| 8a73f63189 | |||
| 31e8c85f0a | |||
| 5b9916866d | |||
| f001e735f8 | |||
| 32893ee343 | |||
| 0d4e47e5c3 | |||
| c1a43482e7 | |||
| ea65383e16 | |||
| b97d1487a7 | |||
| 091cf945e4 | |||
| df4f551ba7 | |||
| 88da4ddf89 | |||
| 93eb607e6d | |||
| 810b5b78f5 | |||
| 38ea422f6a | |||
| a7a35c0138 |
+1
-1
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: create-pull-request
|
||||
description: Create a GitHub pull request following project conventions. Use when the user asks to create a PR, submit changes for review, or open a pull request. Handles commit analysis, branch management, and PR creation using the gh CLI tool.
|
||||
description: Create a GitHub pull request following project conventions. Use when the user asks to create a PR, submit changes for review, or open a pull request. Handles commit analysis, branch management, PR template usage, and PR creation using the gh CLI tool.
|
||||
---
|
||||
|
||||
# Create Pull Request
|
||||
@@ -1,8 +0,0 @@
|
||||
# Changesets
|
||||
|
||||
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
||||
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
||||
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
|
||||
|
||||
We have a quick list of common questions to get you started engaging with this project in
|
||||
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@3.0.5/schema.json",
|
||||
"changelog": "@changesets/cli/changelog",
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
"access": "restricted",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": []
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"cline": patch
|
||||
---
|
||||
|
||||
Add Additional Markdown Formatting in CLI
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
"cline": patch
|
||||
---
|
||||
|
||||
fix: resolve "Could not find the file context" error in Explain Changes comment replies
|
||||
|
||||
When clicking a line to start a discussion in the Explain Changes diff view, replies would
|
||||
intermittently fail with "Error: Could not find the file context". This happened because
|
||||
the reply handler and the `onCommentStart` callback were using a strict `absolutePath`-only
|
||||
match to look up files in `changedFiles`, while the VS Code comment controller may return
|
||||
paths in different formats (relative vs. absolute, different separators on Windows, etc.).
|
||||
|
||||
Fixed by adding a `relativePath` fallback in both lookup sites, making them consistent with
|
||||
the already-correct logic in `streamAIExplanationComments`.
|
||||
|
||||
Fixes #9382
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
"cline": patch
|
||||
---
|
||||
|
||||
fix: clear all OCA secrets on auth refresh failure to prevent re-auth loop
|
||||
|
||||
When OCA (Oracle Code Assist) token refresh fails with 400 invalid_grant or 401,
|
||||
the stale secrets were not fully cleared from storage. The `clearAuth()` method
|
||||
only cleared `ocaApiKey` and `ocaRefreshToken`, leaving legacy secrets
|
||||
`ocaAccessToken` and `ocaTokenSet` (set by older Cline versions) in VS Code's
|
||||
secret storage. These stale secrets caused every subsequent re-auth attempt to
|
||||
fail in a loop, requiring manual SQLite deletion to recover.
|
||||
|
||||
Fix:
|
||||
- Added `ocaAccessToken` and `ocaTokenSet` to `SecretKeys` in `state-keys.ts`
|
||||
- Updated `OcaAuthProvider.clearAuth()` to clear all 4 OCA secrets
|
||||
|
||||
Fixes #9567
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix OpenAI-compatible `gpt-oss` native tool mode so file editing works reliably:
|
||||
|
||||
- Enable `apply_patch` for `gpt-oss` models when using native GPT-5 prompt variants.
|
||||
- Add regression tests covering model family selection and tool availability.
|
||||
- Add a smoke-test scenario for OpenAI-compatible `gpt-oss` file editing and improve the smoke runner for per-scenario auth/env requirements.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"cline": patch
|
||||
---
|
||||
|
||||
Use JSON_SCHEMA for yaml.load to prevent unsafe deserialization from untrusted sources
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"cline": patch
|
||||
---
|
||||
|
||||
Add automatic retries (up to 3 attempts) for smoke test CI jobs to reduce flaky failures
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"cline": patch
|
||||
---
|
||||
|
||||
add focus ring on action buttons
|
||||
@@ -1,26 +0,0 @@
|
||||
changesDir: .changes
|
||||
unreleasedDir: unreleased
|
||||
headerPath: header.tpl.md
|
||||
changelogPath: CHANGELOG.md
|
||||
versionExt: md
|
||||
versionFormat: '## {{.Version}} - {{.Time.Format "2006-01-02"}}'
|
||||
kindFormat: "### {{.Kind}}"
|
||||
changeFormat: "* {{.Body}}"
|
||||
kinds:
|
||||
- label: Added
|
||||
auto: minor
|
||||
- label: Changed
|
||||
auto: major
|
||||
- label: Deprecated
|
||||
auto: minor
|
||||
- label: Removed
|
||||
auto: major
|
||||
- label: Fixed
|
||||
auto: patch
|
||||
- label: Security
|
||||
auto: patch
|
||||
newlines:
|
||||
afterChangelogHeader: 1
|
||||
beforeChangelogVersion: 1
|
||||
endOfVersion: 1
|
||||
envPrefix: CHANGIE_
|
||||
@@ -14,7 +14,7 @@ This file is the secret sauce for working effectively in this codebase. It captu
|
||||
|
||||
## Miscellaneous
|
||||
- This is a VS Code extension—check `package.json` for available scripts before trying to verify builds (e.g., `npm run compile`, not `npm run build`).
|
||||
- When creating PRs, if the change is user-facing and significant enough to warrant a changelog entry, run `npm run changeset` and create a patch changeset. Never create minor or major version bumps. Skip changesets for trivial fixes, internal refactors, or minor UI tweaks that users wouldn't notice.
|
||||
- When creating PRs, contributors should not create changelog-entry files. Maintainers handle release versioning and changelog curation during the release process.
|
||||
- When adding new feature flags, see this PR as a reference https://github.com/cline/cline/pull/7566
|
||||
- Additional instructions about making requests: @.clinerules/network.md
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ Review and address all comments on the current branch's PR.
|
||||
- Inline comments: `gh api repos/{owner}/{repo}/pulls/{pr_number}/comments`
|
||||
- General comments: `gh pr view {pr_number} --json comments,reviews`
|
||||
|
||||
4. Present a summary of all comments with your recommendation for each (apply, skip, or respond). Ignore bot noise (changeset-bot, CI status, etc.).
|
||||
4. Present a summary of all comments with your recommendation for each (apply, skip, or respond). Ignore bot noise (release automation, CI status, etc.).
|
||||
|
||||
5. **Wait for my approval** before proceeding.
|
||||
|
||||
|
||||
@@ -1,549 +0,0 @@
|
||||
The goal of this workflow is to take a changeset for a release of Cline, an autonomous coding agent extension that plugs right into your IDE, and write the updated announcement component, and the updated changelog.
|
||||
|
||||
|
||||
For reference, here are some examples of how we converted previous changesets to announcement components / changelogs.
|
||||
|
||||
|
||||
- 3.14
|
||||
<changeset>
|
||||
This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and publish to npm yourself or setup this action to publish automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.
|
||||
|
||||
Releases
|
||||
claude-dev@3.14.0
|
||||
Minor Changes
|
||||
77c9863: create clinerules folder if its currently a file and creating new rule
|
||||
0ffb7dd: disabling shift hint for now & improving tooltip behavior
|
||||
79b76fd: Add support for custom model ID in AWS Bedrock provider, enabling use of Application Inference Profile.
|
||||
eb6e481: Full support for LaTeX rendering
|
||||
df37f29: Add support for custom API request timeout. Previously, timeouts were hardcoded to 30 seconds for providers like Ollama or 15 seconds for OpenRouter and Cline. Now users can set a custom timeout value in milliseconds through the settings interface.
|
||||
e4d26be: allow cursorrules and windsurfrules
|
||||
c5de50f: Fix Handle @withRetry() SyntaxError when running extension locally issue
|
||||
61d2f42: enabled pricing calculation for gemini and vertex + more robust caching & cache tracking for gemini & vertex
|
||||
aed152b: add truncation notice when truncating manually
|
||||
2fe2405: Migrate Cline Tools Section to new docs
|
||||
19cc8bc: Add a timeout setting for the terminal connection, allowing users to adjust this if they are having timeout issues
|
||||
03d4410: Added copy button to code blocks.
|
||||
c78fe23: addressed race condition in terminal command usage
|
||||
91e222f: add checkpoints after more messages
|
||||
14230e7: add newrule slash command
|
||||
1c7d33a: Add remote config with posthog allowing for disabling new features until they're reading, making for a better developer experience.
|
||||
4196c14: add cache ui for open router and cline provider
|
||||
d97424f: showing expanded task by default
|
||||
5294e78: Refactor to not pass a message for showing the MCP View from the servers modal
|
||||
70cc437: Fix Windows path issue: Correct handling of import.meta.url to avoid leading slash in pathname
|
||||
4b697d8: Migrate the addRemoteServer to protobus
|
||||
Patch Changes
|
||||
c63d9a1: updated drag and drop text to say "drop" instead of "drag"
|
||||
459adf0: Add markdown copy to chat
|
||||
74ec823: Minor UX improvement to drag and drop ux
|
||||
b0961f4: Remove linear pull request action
|
||||
e9ce384: searchCommits protobus migration
|
||||
5802b68: createRuleFile protobus migration
|
||||
df7f9fc: Add dependsOn to more blocks in the tasks.json
|
||||
41ae732: Fix for git commit mentions in repos with no git commits
|
||||
7e78445: Adding args to allow Cursor to open workspaces (for checkpoint testing/development)
|
||||
bdfda6f: feat(bedrock): Introduce Amazon Nova Premier
|
||||
65243ad: Introduce UI library for future UI development
|
||||
4565e06: checkIsImageURL migrated to protobus
|
||||
5a8e9d8: protobus migration for openImage
|
||||
deeda6e: Lowering Gemini cache TTL time
|
||||
db0b022: Adding UI to show openrouter balance next to provider
|
||||
4650ffa: deleteRuleFile protobus migration
|
||||
d4bd755: fix cost calculation
|
||||
</changeset>
|
||||
|
||||
<changelog>
|
||||
## [3.14.0]
|
||||
|
||||
- Add UI to show openrouter balance next to provider
|
||||
- Add support for custom model ID in AWS Bedrock provider, enabling use of Application Inference Profile (Thanks @clicube!)
|
||||
- Add more robust caching & cache tracking for gemini & vertex providers
|
||||
- Add support for LaTeX rendering
|
||||
- Add support for custom API request timeout. Timeouts were 15-30s, but can now be configured via settings for OpenRouter/Cline & Ollama (Thanks @WingsDrafterwork!)
|
||||
- Add truncation notice when truncating manually
|
||||
- Add a timeout setting for the terminal connection, allowing users to set a time to wait for terminal startup
|
||||
- Add copy button to code blocks
|
||||
- Add copy button to markdown blocks (Thanks @weshoke!)
|
||||
- Add checkpoints to more messages
|
||||
- Add slash command to create a new rules file (/newrule)
|
||||
- Add cache ui for open router and cline provider
|
||||
- Add Amazon Nova Premier model to Bedrock (Thanks @watany!)
|
||||
- Add support for cursorrules and windsurfrules
|
||||
- Add support for batch history deletion (Thanks @danix800!)
|
||||
- Improve Drag & Drop experience
|
||||
- Create clinerules folder creating new rule if it's needed
|
||||
- Enable pricing calculation for gemini and vertex providers
|
||||
- Refactor message handling to not show the MCP View of the server modal
|
||||
- Migrate the addRemoteServer to protobus (Thanks @DaveFres!)
|
||||
- Update task header to be expanded by default
|
||||
- Update Gemini cache TTL time to 15 minutes
|
||||
- Fix race condition in terminal command usage
|
||||
- Fix to correctly handle `import.meta.url`, avoiding leading slash in pathname for Windows (Thanks @DaveFres!)
|
||||
- Fix @withRetry() decoration syntax error when running extension locally (Thanks @DaveFres!)
|
||||
- Fix for git commit mentions in repos with no git commits
|
||||
- Fix cost calculation (Thanks @BarreiroT!)
|
||||
</changelog>
|
||||
|
||||
|
||||
<announcement-component>
|
||||
const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
|
||||
const minorVersion = version.split(".").slice(0, 2).join(".") // 2.0.0 -> 2.0
|
||||
return (
|
||||
<div style={containerStyle}>
|
||||
<VSCodeButton appearance="icon" onClick={hideAnnouncement} style={closeIconStyle}>
|
||||
<span className="codicon codicon-close"></span>
|
||||
</VSCodeButton>
|
||||
<h3 style={h3TitleStyle}>
|
||||
🎉{" "}New in v{minorVersion}
|
||||
</h3>
|
||||
<ul style={ulStyle}>
|
||||
<li>
|
||||
<b>Gemini prompt caching:</b> Gemini and Vertex providers now support prompt caching and price tracking for
|
||||
Gemini models.
|
||||
</li>
|
||||
<li>
|
||||
<b>Copy Buttons:</b> Buttons were added to Markdown and Code blocks that allow you to copy their contents
|
||||
easily.
|
||||
</li>
|
||||
<li>
|
||||
<b>/newrule command:</b> New slash command to have cline write your .clinerules for you based on your
|
||||
workflow.
|
||||
</li>
|
||||
<li>
|
||||
<b>Drag and drop improvements:</b> Don't forget to hold shift while dragging files!
|
||||
</li>
|
||||
<li>Added more checkpoints across the task, allowing you to restore from more than just file changes.</li>
|
||||
<li>Added support for rendering LaTeX in message responses. (Try asking Cline to show the quadratic formula)</li>
|
||||
</ul>
|
||||
<Accordion isCompact className="pl-0">
|
||||
<AccordionItem
|
||||
key="1"
|
||||
aria-label="Previous Updates"
|
||||
title="Previous Updates:"
|
||||
classNames={{
|
||||
trigger: "bg-transparent border-0 pl-0 pb-0 w-fit",
|
||||
title: "font-bold text-(--vscode-foreground)",
|
||||
indicator:
|
||||
"text-(--vscode-foreground) mb-0.5 -rotate-180 data-[open=true]:-rotate-90 rtl:rotate-0 rtl:data-[open=true]:-rotate-90",
|
||||
}}>
|
||||
<ul style={ulStyle}>
|
||||
<li>
|
||||
<b>Global Cline Rules:</b> store multiple rules files in Documents/Cline/Rules to share between
|
||||
projects.
|
||||
</li>
|
||||
<li>
|
||||
<b>Cline Rules Popup:</b> New button in the chat area to view workspace and global cline rules files
|
||||
to plug and play specific rules for the task
|
||||
</li>
|
||||
<li>
|
||||
<b>Slash Commands:</b> Type <code>/</code> in chat to see the list of quick actions, like starting a
|
||||
new task (more coming soon!)
|
||||
</li>
|
||||
<li>
|
||||
<b>Edit Messages:</b> You can now edit a message you sent previously by clicking on it. Optionally
|
||||
restore your project when the message was sent!
|
||||
</li>
|
||||
</ul>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
|
||||
{/*
|
||||
// Leave this here for an example of how to structure the announcement
|
||||
<ul style={{ margin: "0 0 8px", paddingLeft: "12px" }}>
|
||||
<li>
|
||||
OpenRouter now supports prompt caching! They also have much higher rate limits than other providers,
|
||||
so I recommend trying them out.
|
||||
<br />
|
||||
{!apiConfiguration?.openRouterApiKey && (
|
||||
<VSCodeButtonLink
|
||||
href={getOpenRouterAuthUrl(vscodeUriScheme)}
|
||||
style={{
|
||||
transform: "scale(0.85)",
|
||||
transformOrigin: "left center",
|
||||
margin: "4px -30px 2px 0",
|
||||
}}>
|
||||
Get OpenRouter API Key
|
||||
</VSCodeButtonLink>
|
||||
)}
|
||||
{apiConfiguration?.openRouterApiKey && apiConfiguration?.apiProvider !== "openrouter" && (
|
||||
<VSCodeButton
|
||||
onClick={() => {
|
||||
vscode.postMessage({
|
||||
type: "apiConfiguration",
|
||||
apiConfiguration: { ...apiConfiguration, apiProvider: "openrouter" },
|
||||
})
|
||||
}}
|
||||
style={{
|
||||
transform: "scale(0.85)",
|
||||
transformOrigin: "left center",
|
||||
margin: "4px -30px 2px 0",
|
||||
}}>
|
||||
Switch to OpenRouter
|
||||
</VSCodeButton>
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
<b>Edit Cline's changes before accepting!</b> When he creates or edits a file, you can modify his
|
||||
changes directly in the right side of the diff view (+ hover over the 'Revert Block' arrow button in
|
||||
the center to undo "<code>{"// rest of code here"}</code>" shenanigans)
|
||||
</li>
|
||||
<li>
|
||||
New <code>search_files</code> tool that lets Cline perform regex searches in your project, letting
|
||||
him refactor code, address TODOs and FIXMEs, remove dead code, and more!
|
||||
</li>
|
||||
<li>
|
||||
When Cline runs commands, you can now type directly in the terminal (+ support for Python
|
||||
environments)
|
||||
</li>
|
||||
</ul>*/}
|
||||
<div style={hrStyle} />
|
||||
<p style={linkContainerStyle}>
|
||||
Join us on{" "}
|
||||
<VSCodeLink style={linkStyle} href="https://x.com/cline">
|
||||
X,
|
||||
</VSCodeLink>{" "}
|
||||
<VSCodeLink style={linkStyle} href="https://discord.gg/cline">
|
||||
discord,
|
||||
</VSCodeLink>{" "}
|
||||
or{" "}
|
||||
<VSCodeLink style={linkStyle} href="https://www.reddit.com/r/cline/">
|
||||
r/cline
|
||||
</VSCodeLink>
|
||||
for more updates!
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</announcement-component>
|
||||
|
||||
- 3.13
|
||||
|
||||
<changeset>
|
||||
Minor Changes
|
||||
2964388: Added copy button to MermaidBlock component
|
||||
75143a7: Add the ability to fetch from global cline rules files
|
||||
Patch Changes
|
||||
a0252e7: convert inline style to tailwind css of file SettingsView.tsx
|
||||
ab59bd9: Add stream options back to xai provider
|
||||
7276f50: Icons to indicate an action is occuring outside of the users workspace
|
||||
0b19ba6: update to NEW model
|
||||
</changeset>
|
||||
|
||||
<changelog>
|
||||
## [3.13.0]
|
||||
|
||||
- Add Cline rules popover under the chat field, allowing you to easily add, enable & disable workspace level or global rule files
|
||||
- Add new slash command menu letting you type “/“ to do quick actions like creating new tasks
|
||||
- Add ability to edit past messages, with options to restore your workspace back to that point
|
||||
- Allow sending a message when selecting an option provided by the question or plan tool
|
||||
- Add command to jump to Cline's chat input
|
||||
- Add support for OpenAI o3 & 4o-mini (Thanks @PeterDaveHello and @arafatkatze!)
|
||||
- Add baseURL option for Google Gemini provider (Thanks @owengo and @olivierhub!)
|
||||
- Add support for Azure's DeepSeek model. (Thanks @yt3trees!)
|
||||
- Add ability for models that support it to receive image responses from MCP servers (Thanks @rikaaa0928!)
|
||||
- Improve search and replace diff editing by making it more flexible with models that fail to follow structured output instructions. (Thanks @chi-cat!)
|
||||
- Add detection of Ctrl+C termination in terminal, improving output reading issues
|
||||
- Fix issue where some commands with large output would cause UI to freeze
|
||||
- Fix token usage tracking issues with vertex provider (Thanks @mzsima!)
|
||||
- Fix issue with xAI reasoning content not being parsed (Thanks @mrubens!)
|
||||
</changelog>
|
||||
|
||||
<announcement-component>
|
||||
const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
|
||||
const minorVersion = version.split(".").slice(0, 2).join(".") // 2.0.0 -> 2.0
|
||||
return (
|
||||
<div style={containerStyle}>
|
||||
<VSCodeButton appearance="icon" onClick={hideAnnouncement} style={closeIconStyle}>
|
||||
<span className="codicon codicon-close"></span>
|
||||
</VSCodeButton>
|
||||
<h3 style={h3TitleStyle}>
|
||||
🎉{" "}New in v{minorVersion}
|
||||
</h3>
|
||||
<ul style={ulStyle}>
|
||||
<li>
|
||||
<b>Global Cline Rules:</b> store multiple rules files in Documents/Cline/Rules to share between projects.
|
||||
</li>
|
||||
<li>
|
||||
<b>Cline Rules Popup:</b> New button in the chat area to view workspace and global cline rules files to plug
|
||||
and play specific rules for the task
|
||||
</li>
|
||||
<li>
|
||||
<b>Slash Commands:</b> Type <code>/</code> in chat to see the list of quick actions, like starting a new task
|
||||
(more coming soon!)
|
||||
</li>
|
||||
<li>
|
||||
<b>Edit Messages:</b> You can now edit a message you sent previously by clicking on it. Optionally restore
|
||||
your project when the message was sent!
|
||||
</li>
|
||||
</ul>
|
||||
<h4 style={{ margin: "5px 0 5px" }}>Previous Updates:</h4>
|
||||
<ul style={ulStyle}>
|
||||
<li>
|
||||
<b>Model Favorites:</b> You can now mark your favorite models when using Cline & OpenRouter providers for
|
||||
quick access!
|
||||
</li>
|
||||
<li>
|
||||
<b>Faster Diff Editing:</b> Improved animation performance for large files, plus a new indicator in chat
|
||||
showing the number of edits Cline makes.
|
||||
</li>
|
||||
<li>
|
||||
<b>New Auto-Approve Options:</b> Turn off Cline's ability to read and edit files outside your workspace.
|
||||
</li>
|
||||
</ul>
|
||||
{/*
|
||||
// Leave this here for an example of how to structure the announcement
|
||||
<ul style={{ margin: "0 0 8px", paddingLeft: "12px" }}>
|
||||
<li>
|
||||
OpenRouter now supports prompt caching! They also have much higher rate limits than other providers,
|
||||
so I recommend trying them out.
|
||||
<br />
|
||||
{!apiConfiguration?.openRouterApiKey && (
|
||||
<VSCodeButtonLink
|
||||
href={getOpenRouterAuthUrl(vscodeUriScheme)}
|
||||
style={{
|
||||
transform: "scale(0.85)",
|
||||
transformOrigin: "left center",
|
||||
margin: "4px -30px 2px 0",
|
||||
}}>
|
||||
Get OpenRouter API Key
|
||||
</VSCodeButtonLink>
|
||||
)}
|
||||
{apiConfiguration?.openRouterApiKey && apiConfiguration?.apiProvider !== "openrouter" && (
|
||||
<VSCodeButton
|
||||
onClick={() => {
|
||||
vscode.postMessage({
|
||||
type: "apiConfiguration",
|
||||
apiConfiguration: { ...apiConfiguration, apiProvider: "openrouter" },
|
||||
})
|
||||
}}
|
||||
style={{
|
||||
transform: "scale(0.85)",
|
||||
transformOrigin: "left center",
|
||||
margin: "4px -30px 2px 0",
|
||||
}}>
|
||||
Switch to OpenRouter
|
||||
</VSCodeButton>
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
<b>Edit Cline's changes before accepting!</b> When he creates or edits a file, you can modify his
|
||||
changes directly in the right side of the diff view (+ hover over the 'Revert Block' arrow button in
|
||||
the center to undo "<code>{"// rest of code here"}</code>" shenanigans)
|
||||
</li>
|
||||
<li>
|
||||
New <code>search_files</code> tool that lets Cline perform regex searches in your project, letting
|
||||
him refactor code, address TODOs and FIXMEs, remove dead code, and more!
|
||||
</li>
|
||||
<li>
|
||||
When Cline runs commands, you can now type directly in the terminal (+ support for Python
|
||||
environments)
|
||||
</li>
|
||||
</ul>*/}
|
||||
<div style={hrStyle} />
|
||||
<p style={linkContainerStyle}>
|
||||
Join us on{" "}
|
||||
<VSCodeLink style={linkStyle} href="https://x.com/cline">
|
||||
X,
|
||||
</VSCodeLink>{" "}
|
||||
<VSCodeLink style={linkStyle} href="https://discord.gg/cline">
|
||||
discord,
|
||||
</VSCodeLink>{" "}
|
||||
or{" "}
|
||||
<VSCodeLink style={linkStyle} href="https://www.reddit.com/r/cline/">
|
||||
r/cline
|
||||
</VSCodeLink>
|
||||
for more updates!
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</announcement-component>
|
||||
|
||||
|
||||
We have a changeset PR that automatically generated as new unreleased PRs are merged into main, the PR is always called "Changeset version bump" and the author is github-actions.
|
||||
|
||||
The Changeset PR description looks something like this:
|
||||
|
||||
<changeset-pr-description>
|
||||
This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and publish to npm yourself or [setup this action to publish automatically](https://github.com/changesets/action#with-publishing). If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.
|
||||
|
||||
|
||||
# Releases
|
||||
## claude-dev@3.16.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- c6e8b04: Recent task list is now collapsible, allowing users to hide their recent tasks (e.g. when sharing their screen).
|
||||
- aabe4ae: Add detection for new users to display special components
|
||||
- 6c18d51: adds global endpoint for vertex ai users
|
||||
- 080ed7c: Add Tailwind CSS IntelliSense to the the recommended extensions list
|
||||
- 5147e28: new workflow feature
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- c0b3c69: fix eternal loading states when the last message is a checkpoint
|
||||
- 570ece3: selectImages protos migration
|
||||
- 8d8452e: askResponse protobus migration
|
||||
- cd1ff2a: Finishing the migration of Vscode Advanced settings to Settings Webview
|
||||
</changeset-pr-description>
|
||||
|
||||
The changeset pr is ALWAYS on the following branch: `changeset-release/main`.
|
||||
|
||||
I have the `gh` command line tool set up and authenticated, so you have everything you need.
|
||||
|
||||
The first step is to get the full diff from the changeset PR to look at the changes that were automatically made to the `CHANGELOG.md` file. By default it will automatically add a new section to the changelog.md file with the new version. The problem with the automatically generated section is that it just takes the text that the developers threw into their changeset files for each corresponding PR, and they can be pretty vague and bad. Additionally there's some stuff that is totally irrelevant for the end user, like minor refactoring changes. So I manually typically go in and update this section to be a proper changelog that will show up in our patchnotes. You can look at how the rest of the file is done because those are all good examples of us updating this to use good language for the end user. We usually put new features up top (and the most exciting flagship features at the very top), and then bug fixes/improvements at the bottom. Having some basic organization to the ordering of the bullet points by content is nice. But use common sense.
|
||||
|
||||
To handle this process effectively, do the following:
|
||||
|
||||
For each of the automatically generated bullet points in the Changelog.md, you should
|
||||
1. Take the commit hash at the start of the bullet point, and use the `gh` command line tool find the PR that it was associated with.
|
||||
2. Use the `gh` command to get the PR title/description/discussion to understand the context surrounding the PR.
|
||||
3. Use the `gh` command line tool to get the full PR diff to fully understand the changes made in the code.
|
||||
4. Synthesize that knowledge to determine (a) whether or not this change is relevant to end users and (b) what the text & ordering of the line should be.
|
||||
5. Update the `CHANGELOG.md` accordingly
|
||||
|
||||
Do this for every single item in the list from the autogenerated bullet points. We want to be diligent and have a full understanding of every feature so we can make the best changelog ever!
|
||||
|
||||
Here are some principles for good changelogs from keepchangelog.com, a handy guide:
|
||||
|
||||
<keepachangelog-pinciples-for-good-changelogs>
|
||||
### Guiding Principles
|
||||
- Changelogs are for humans, not machines.
|
||||
- There should be an entry for every single version.
|
||||
- The same types of changes should be grouped.
|
||||
- The latest version comes first.
|
||||
|
||||
### Bullet points in the changelog should follow these principles:
|
||||
- Types of changes
|
||||
- Added for new features.
|
||||
- Changed for changes in existing functionality.
|
||||
- Deprecated for soon-to-be removed features.
|
||||
- Removed for now removed features.
|
||||
- Fixed for any bug fixes.
|
||||
- Security in case of vulnerabilities.
|
||||
</keepachangelog-pinciples-for-good-changelogs>
|
||||
|
||||
Lastly, when developers make a PR, they typically make a changeset. And they have 3 options when making the changeset:
|
||||
|
||||
1. Patch
|
||||
2. Minor
|
||||
3. Major
|
||||
|
||||
Sometimes they label something as minor when really it should just be a patch. Or vice versa. Because of this, the automatic version bump may be incorrect. So when starting out this workflow, you should use the <ask_followup_question> tool to confirm with me whether or not this should be a patch bump (show the old version number and what the proposed new version number would be) or a minor bump. Part of the release process is making sure the version in package.json that is automatically changed actually corresponds with what we decided the bump should actually be based on the features. ALL these modifications happen in the `changeset-release/main` branch btw.
|
||||
|
||||
<important_note>
|
||||
Before doing any of this, make sure you check out the `changeset-release/main` and pull the most recent up to date changes. Then perform all this work in that branch.
|
||||
|
||||
New announcement banners should ONLY be made for minor version bumps or higher. That's another reason why double checking if the changelog warrants the bump is important.
|
||||
|
||||
Also, SUPER important: For any external contributors that aren't part of the cline github organization, we always want to add a (Thanks @username!) at the end of the changelog to attribute them properly. We're an open source project and it's ethical to do this.
|
||||
</important_note>
|
||||
|
||||
Once the changelog looks good, and the version number looks good, we gotta double check that the version number in the changelog has the brackets around it. And as a final step, double check the package.json version number matches the latest number in the changelog. And as the ultimate final step we run `npm run install:all` to make sure the package version number permiates through the lock file.
|
||||
|
||||
|
||||
<detailed_sequence_of_steps>
|
||||
# Cline Release Process - Detailed Sequence of Steps
|
||||
|
||||
## Before Starting
|
||||
1. First, examine the changeset PR without checking it out:
|
||||
```bash
|
||||
gh pr view changeset-release/main
|
||||
```
|
||||
|
||||
2. View the PR diff to see the auto-generated CHANGELOG.md changes:
|
||||
```bash
|
||||
gh pr diff changeset-release/main > changeset-diff.txt
|
||||
cat changeset-diff.txt | grep -A 50 "CHANGELOG.md"
|
||||
```
|
||||
|
||||
## Initial Setup
|
||||
3. Once you're ready to start, checkout and update the changeset release branch:
|
||||
```bash
|
||||
git checkout changeset-release/main
|
||||
git pull origin changeset-release/main
|
||||
```
|
||||
|
||||
## Analyzing Each Change
|
||||
4. For each commit hash in the auto-generated changelog entries:
|
||||
|
||||
a. Find the PR number associated with a commit hash:
|
||||
```bash
|
||||
gh pr list --search "<commit-hash>" --state merged
|
||||
```
|
||||
|
||||
b. Get PR details for better context:
|
||||
```bash
|
||||
gh pr view <PR-number>
|
||||
```
|
||||
|
||||
c. Check if the contributor is external to determine if attribution is needed:
|
||||
```bash
|
||||
# Extract username from PR
|
||||
USERNAME=$(gh pr view <PR-number> --json author --jq .author.login)
|
||||
|
||||
# Check if user is a member of the Cline organization
|
||||
# this command is a bit finnicky, but it 100% works.
|
||||
# if you see a `Error executing command: The command ran successfully, but we couldn't capture its output. Please proceed accordingly.` error, just retry it until you actually get the output
|
||||
# don't make any assumptions, just retry the command to actually get the output and determine if they're external or not.
|
||||
# no output means they are an external contributor, otherwise if there is output they are an internal contributor (part of our github org)
|
||||
gh api "orgs/cline/members" --jq "map(.login)" | grep -i "pashpashpash"
|
||||
```
|
||||
|
||||
d. View the full PR diff to understand code changes:
|
||||
```bash
|
||||
gh pr diff <PR-number> > pr-diff-<PR-number>.txt
|
||||
cat pr-diff-<PR-number>.txt
|
||||
```
|
||||
|
||||
## Updating the Changelog
|
||||
5. Based on PR analysis, update the CHANGELOG.md with user-friendly descriptions:
|
||||
- Use the `<replace_in_file>` tool to edit the CHANGELOG.md file
|
||||
- Group by feature type (Added, Changed, Fixed)
|
||||
- Put most exciting features at the top
|
||||
- Move bug fixes and small improvements to the bottom
|
||||
- Use clear, end-user focused language
|
||||
- For external contributors, add attribution at the end of the relevant entry: `(Thanks @username!)`
|
||||
|
||||
## Version Number Verification
|
||||
6. Confirm the version bump is appropriate:
|
||||
- Check package.json to verify the auto-generated version number:
|
||||
```bash
|
||||
cat package.json | grep "\"version\""
|
||||
```
|
||||
- If the feature set doesn't warrant a minor bump, use the `<replace_in_file>` tool to modify package.json
|
||||
|
||||
7. Ensure the version in CHANGELOG.md has brackets around it:
|
||||
```
|
||||
## [3.16.0]
|
||||
```
|
||||
|
||||
## Creating the Announcement (for minor/major versions only)
|
||||
8. If this is a minor version bump, create/update the announcement component:
|
||||
- Use the `<replace_in_file>` tool to edit the src/views/components/announcement.tsx file
|
||||
- Update the highlights based on key features
|
||||
- Move previous version highlights to the "Previous Updates" section
|
||||
- Use the previous announcement components as reference for structure
|
||||
|
||||
## Finalizing the Release
|
||||
9. Update dependencies with the new version number:
|
||||
```bash
|
||||
npm run install:all
|
||||
```
|
||||
|
||||
10. Commit your changes:
|
||||
```bash
|
||||
git add CHANGELOG.md package.json package-lock.json src/views/components/announcement.tsx
|
||||
git commit -m "Update CHANGELOG.md and announcement for version 3.16.0"
|
||||
```
|
||||
|
||||
11. Push your changes to the changeset branch:
|
||||
```bash
|
||||
git push origin changeset-release/main
|
||||
```
|
||||
|
||||
12. Check that your changes pushed successfully:
|
||||
```bash
|
||||
git status
|
||||
```
|
||||
</detailed_sequence_of_steps>
|
||||
@@ -89,16 +89,9 @@ On the main branch, create a commit that updates:
|
||||
|
||||
2. **package.json** - Update the version field to the new version
|
||||
|
||||
3. **Delete changesets** for the commits being included in the hotfix. This prevents the changeset bot from including duplicate entries in the next regular release.
|
||||
3. No changelog-entry file cleanup is needed. Contributors do not create changelog-entry files in this repo.
|
||||
|
||||
Find and delete the changeset files associated with the selected commits:
|
||||
```bash
|
||||
ls .changeset/
|
||||
```
|
||||
|
||||
Each changeset file in `.changeset/` corresponds to a PR. Read them to identify which ones belong to the commits you're hotfixing, then delete those files.
|
||||
|
||||
**Skip running `npm run install:all`** - the automation handles outdated lockfiles.
|
||||
**Skip running `npm run install:all`** - release automation handles lockfile consistency as needed.
|
||||
|
||||
Commit with message format: `v{VERSION} Release Notes (hotfix)`
|
||||
|
||||
@@ -107,7 +100,7 @@ In the commit body, mention:
|
||||
- List the cherry-picked commits that will be included
|
||||
|
||||
```bash
|
||||
git add CHANGELOG.md package.json .changeset/
|
||||
git add CHANGELOG.md package.json
|
||||
git commit -m "v3.40.1 Release Notes (hotfix)
|
||||
|
||||
Hotfix release including:
|
||||
|
||||
@@ -347,8 +347,6 @@ A few notes:
|
||||
So until the settings page is update, and this is added to settings in a way that's clean and doesn't confuse new users, I don't think we can merge this. Please bear with us.
|
||||
</request_changes_comment>
|
||||
<request_changes_comment>
|
||||
Also, don't forget to add a changeset since this fixes a user-facing bug.
|
||||
|
||||
The architectural change is solid - moving the focus logic to the command handlers makes sense. Just don't want to introduce subtle timing issues by removing those timeouts.
|
||||
</request_changes_comment>
|
||||
</example_comments_that_i_have_written_before>
|
||||
|
||||
@@ -1,232 +1,64 @@
|
||||
# Release
|
||||
|
||||
Prepare and publish a release from the open changeset PR.
|
||||
Prepare and publish a release directly from `main`.
|
||||
|
||||
## Overview
|
||||
|
||||
This workflow helps you:
|
||||
1. Find and checkout the open changeset PR
|
||||
2. Clean up the changelog (fix version format, wordsmith entries)
|
||||
3. Push changes back to the PR branch
|
||||
4. Merge with proper commit message format
|
||||
5. Tag and push the release (after verifying the commit)
|
||||
6. Trigger the publish workflow
|
||||
7. Update GitHub release notes
|
||||
8. Provide final summary with Slack announcement
|
||||
1. Select/confirm the target version
|
||||
2. Curate `CHANGELOG.md` entries manually for end users
|
||||
3. Ensure `package.json` version matches the changelog
|
||||
4. Create and push a release commit + tag
|
||||
5. Trigger publish workflow
|
||||
6. Update GitHub release notes and share a summary
|
||||
|
||||
## Step 1: Find the Changeset PR
|
||||
## Process
|
||||
|
||||
Look for the open changeset PR:
|
||||
|
||||
```bash
|
||||
gh pr list --search "Changeset version bump" --state open --json number,title,headRefName,url
|
||||
```
|
||||
|
||||
If no PR is found, inform the user there's no changeset PR ready. They may need to:
|
||||
- Merge PRs with changesets to main first
|
||||
- Manually trigger the Changeset Converter workflow at: https://github.com/cline/cline/actions/workflows/changeset-converter.yml
|
||||
|
||||
## Step 2: Gather PR Information
|
||||
|
||||
Get the PR details:
|
||||
|
||||
```bash
|
||||
PR_NUMBER=<number from step 1>
|
||||
gh pr view $PR_NUMBER --json body,files,headRefName
|
||||
```
|
||||
|
||||
Checkout the PR branch:
|
||||
|
||||
```bash
|
||||
git fetch origin changeset-release/main
|
||||
git checkout changeset-release/main
|
||||
```
|
||||
|
||||
If the branch has diverged from remote, reset to the remote version:
|
||||
|
||||
```bash
|
||||
git reset --hard origin/changeset-release/main
|
||||
```
|
||||
|
||||
## Step 3: Analyze the Changes
|
||||
|
||||
Read the current CHANGELOG.md to see what the automation generated:
|
||||
|
||||
```bash
|
||||
head -50 CHANGELOG.md
|
||||
```
|
||||
|
||||
Get the version from package.json:
|
||||
|
||||
```bash
|
||||
cat package.json | grep '"version"'
|
||||
```
|
||||
|
||||
**Present to the user:**
|
||||
- The version number that will be released
|
||||
- The raw changelog entries from the changeset PR
|
||||
- Whether this is a patch, minor, or major release
|
||||
|
||||
## Step 4: Clean Up the Changelog
|
||||
|
||||
The changelog needs these fixes:
|
||||
|
||||
1. **Add brackets to version number**: Change `## 3.44.1` to `## [3.44.1]`
|
||||
|
||||
2. **No category headers**: Don't use `### Added`, `### Fixed`, etc. Just a flat list of bullet points.
|
||||
|
||||
3. **Order entries from most important to least important**:
|
||||
- Lead with major new features or significant fixes users care about
|
||||
- End with minor fixes or internal changes
|
||||
|
||||
4. **Write user-friendly descriptions**:
|
||||
- This is for end users, not developers—explain what changed in plain language
|
||||
- Remove commit hashes from the beginning of lines (the automation adds these)
|
||||
- Look at the actual commit diffs (`git show <hash>`) and PRs to understand what changed
|
||||
- Write colorful descriptions that explain the value and impact, not just technical details
|
||||
- Consolidate related changes into single entries when appropriate
|
||||
|
||||
**Ask the user** to review the proposed changelog changes before applying them. Show them:
|
||||
- Current (raw) changelog section
|
||||
- Proposed (cleaned) changelog section
|
||||
|
||||
Once approved, apply the changes to CHANGELOG.md.
|
||||
|
||||
## Step 5: Commit and Push Changes
|
||||
|
||||
After making changelog edits:
|
||||
|
||||
```bash
|
||||
git add CHANGELOG.md
|
||||
git commit -m "Clean up changelog formatting"
|
||||
git push origin changeset-release/main
|
||||
```
|
||||
|
||||
## Step 6: Merge the PR
|
||||
|
||||
**Ask the user to confirm** they're ready to merge.
|
||||
|
||||
Merge the PR with the proper commit message format:
|
||||
|
||||
```bash
|
||||
VERSION=<version from package.json>
|
||||
gh pr merge $PR_NUMBER --squash --subject "v${VERSION} Release Notes" --body ""
|
||||
```
|
||||
|
||||
**If merge is blocked by branch protection:**
|
||||
- Users with admin privileges can add the `--admin` flag to bypass
|
||||
- Users without admin privileges need to get the PR approved through normal review first before merging
|
||||
|
||||
## Step 7: Tag the Release
|
||||
|
||||
After the merge completes, checkout main and pull:
|
||||
### 1) Sync and determine version
|
||||
|
||||
```bash
|
||||
git checkout main
|
||||
git pull origin main
|
||||
cat package.json | grep '"version"'
|
||||
```
|
||||
|
||||
**IMPORTANT: Verify the latest commit is the release commit before tagging:**
|
||||
Confirm the release version with the maintainer (patch/minor/major).
|
||||
|
||||
### 2) Curate changelog and version
|
||||
|
||||
- Edit `CHANGELOG.md` for the target version using human-friendly release notes.
|
||||
- Ensure version headers use bracket format, e.g. `## [3.66.1]`.
|
||||
- Update `package.json` version to the same value.
|
||||
|
||||
### 3) Commit and tag
|
||||
|
||||
```bash
|
||||
git log -1 --oneline
|
||||
git add CHANGELOG.md package.json package-lock.json
|
||||
git commit -m "v<version> Release Notes"
|
||||
git push origin main
|
||||
git tag v<version>
|
||||
git push origin v<version>
|
||||
```
|
||||
|
||||
Confirm the commit message matches `v{VERSION} Release Notes` (e.g., `v3.44.1 Release Notes`). Do NOT blindly tag HEAD without verification.
|
||||
### 4) Trigger publish workflow
|
||||
|
||||
Once verified, tag and push:
|
||||
Tell the maintainer to run:
|
||||
https://github.com/cline/cline/actions/workflows/publish.yml
|
||||
|
||||
Use `v<version>` as the release tag.
|
||||
|
||||
### 5) Update GitHub release notes
|
||||
|
||||
After publish completes:
|
||||
|
||||
```bash
|
||||
VERSION=<version>
|
||||
git tag v${VERSION}
|
||||
git push origin v${VERSION}
|
||||
gh release view v<version> --json body --jq '.body'
|
||||
gh release edit v<version> --notes "<final curated release notes>"
|
||||
```
|
||||
|
||||
## Step 8: Trigger Publish Workflow
|
||||
### 6) Final summary
|
||||
|
||||
**Copy the tag to clipboard** so the user can easily paste it into the GitHub Actions workflow:
|
||||
|
||||
```bash
|
||||
echo -n "v{VERSION}" | pbcopy
|
||||
```
|
||||
|
||||
**Tell the user to trigger the publish workflow:**
|
||||
1. Go to: https://github.com/cline/cline/actions/workflows/publish.yml
|
||||
2. Select **"release"** for release-type
|
||||
3. Paste **`v{VERSION}`** as the tag (already in clipboard)
|
||||
|
||||
**Wait for the user** to confirm the publish workflow has completed before proceeding.
|
||||
|
||||
## Step 9: Update GitHub Release Notes
|
||||
|
||||
Once the user confirms the publish workflow is done, fetch the auto-generated release content:
|
||||
|
||||
```bash
|
||||
VERSION=<version>
|
||||
gh release view v${VERSION} --json body --jq '.body'
|
||||
```
|
||||
|
||||
The auto-generated release has:
|
||||
- `## What's Changed` - PR list (we'll replace this with our changelog)
|
||||
- `## New Contributors` - First-time contributors (keep this if present)
|
||||
- `**Full Changelog**` - Comparison link (keep this)
|
||||
|
||||
Build the new release body:
|
||||
1. Start with `## What's Changed` header
|
||||
2. Add our changelog content (from CHANGELOG.md for this version)
|
||||
3. Keep the `## New Contributors` section if it exists
|
||||
4. Keep the `**Full Changelog**` link
|
||||
|
||||
Update the release:
|
||||
|
||||
```bash
|
||||
gh release edit v${VERSION} --notes "<new body content>"
|
||||
```
|
||||
|
||||
Verify the release was updated:
|
||||
|
||||
```bash
|
||||
gh release view v${VERSION}
|
||||
```
|
||||
|
||||
## Step 10: Final Summary
|
||||
|
||||
**Copy a Slack announcement message to clipboard** (include the full changelog, not just highlights):
|
||||
|
||||
```bash
|
||||
echo "VS Code v{VERSION} Released
|
||||
|
||||
- Changelog entry 1
|
||||
- Changelog entry 2
|
||||
- Changelog entry 3" | pbcopy
|
||||
```
|
||||
|
||||
**Present a final summary:**
|
||||
- Version released: v{VERSION}
|
||||
- PR merged: #{PR_NUMBER}
|
||||
- Tag pushed: v{VERSION}
|
||||
- Release: https://github.com/cline/cline/releases/tag/v{VERSION}
|
||||
- Slack message copied to clipboard
|
||||
|
||||
**Final reminder:**
|
||||
Post the Slack message to announce the release
|
||||
|
||||
## Handling Edge Cases
|
||||
|
||||
### No changesets found
|
||||
If the changeset PR body shows no changes, inform the user they need to merge PRs with changesets first.
|
||||
|
||||
### Merge conflicts
|
||||
If there are conflicts on the changeset branch, help the user resolve them:
|
||||
```bash
|
||||
git fetch origin main
|
||||
git rebase origin/main
|
||||
# resolve conflicts
|
||||
git push origin changeset-release/main --force-with-lease
|
||||
```
|
||||
|
||||
### User wants to add more changes
|
||||
If the user wants to include additional PRs before releasing:
|
||||
1. Ask them to merge those PRs to main first
|
||||
2. The changeset automation will update the PR automatically
|
||||
3. Re-run this workflow after the PR is updated
|
||||
Provide:
|
||||
- Released version/tag
|
||||
- Link to release page
|
||||
- Summary of top end-user changes
|
||||
|
||||
@@ -60,7 +60,6 @@ We're not looking for exhaustive documentation - just evidence that you've thoug
|
||||
|
||||
- [ ] Changes are limited to a single feature, bugfix or chore (split larger changes into separate PRs)
|
||||
- [ ] Tests are passing (`npm test`) and code is formatted and linted (`npm run format && npm run lint`)
|
||||
- [ ] I have created a changeset using `npm run changeset` (required for user-facing changes)
|
||||
- [ ] I have reviewed [contributor guidelines](https://github.com/cline/cline/blob/main/CONTRIBUTING.md)
|
||||
|
||||
### Screenshots
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
"""
|
||||
This script updates a specific version's release notes section in CHANGELOG.md with new content
|
||||
or reformats existing content.
|
||||
|
||||
The script:
|
||||
1. Takes a version number, changelog path, and optionally new content as input from environment variables
|
||||
2. Finds the section in the changelog for the specified version
|
||||
3. Either:
|
||||
a) Replaces the content with new content if provided, or
|
||||
b) Reformats existing content by:
|
||||
- Removing the first two lines of the changeset format
|
||||
- Ensuring version numbers are wrapped in square brackets
|
||||
4. Writes the updated changelog back to the file
|
||||
|
||||
Environment Variables:
|
||||
CHANGELOG_PATH: Path to the changelog file (defaults to 'CHANGELOG.md')
|
||||
VERSION: The version number to update/format
|
||||
PREV_VERSION: The previous version number (used to locate section boundaries)
|
||||
NEW_CONTENT: Optional new content to insert for this version
|
||||
"""
|
||||
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
|
||||
CHANGELOG_PATH = os.environ.get("CHANGELOG_PATH", "CHANGELOG.md")
|
||||
VERSION = os.environ['VERSION']
|
||||
PREV_VERSION = os.environ.get("PREV_VERSION", "")
|
||||
NEW_CONTENT = os.environ.get("NEW_CONTENT", "")
|
||||
|
||||
def overwrite_changelog_section(changelog_text: str, new_content: str):
|
||||
# Find the section for the specified version
|
||||
version_pattern = f"## {VERSION}\n"
|
||||
unformmatted_prev_version_pattern = f"## {PREV_VERSION}\n"
|
||||
prev_version_pattern = f"## [{PREV_VERSION}]\n"
|
||||
print(f"latest version: {VERSION}")
|
||||
print(f"prev_version: {PREV_VERSION}")
|
||||
|
||||
notes_start_index = changelog_text.find(version_pattern) + len(version_pattern)
|
||||
notes_end_index = changelog_text.find(prev_version_pattern, notes_start_index) if PREV_VERSION and (prev_version_pattern in changelog_text or unformmatted_prev_version_pattern in changelog_text) else len(changelog_text)
|
||||
|
||||
if new_content:
|
||||
return changelog_text[:notes_start_index] + f"{new_content}\n" + changelog_text[notes_end_index:]
|
||||
else:
|
||||
changeset_lines = changelog_text[notes_start_index:notes_end_index].split("\n")
|
||||
filtered_lines = []
|
||||
for line in changeset_lines:
|
||||
# If the previous line is a changeset format
|
||||
if len(filtered_lines) > 1 and filtered_lines[-1].startswith("### "):
|
||||
# Remove the last two lines from the filted_lines
|
||||
filtered_lines.pop()
|
||||
filtered_lines.pop()
|
||||
else:
|
||||
filtered_lines.append(line.strip())
|
||||
|
||||
# Prepend a new line to the first line of filtered_lines
|
||||
if filtered_lines:
|
||||
filtered_lines[0] = "\n" + filtered_lines[0]
|
||||
|
||||
# Print filted_lines wiht a "\n" at the end of each line
|
||||
for line in filtered_lines:
|
||||
print(line.strip())
|
||||
|
||||
parsed_lines = "\n".join(line for line in filtered_lines)
|
||||
updated_changelog = changelog_text[:notes_start_index] + parsed_lines + changelog_text[notes_end_index:]
|
||||
return updated_changelog
|
||||
|
||||
with open(CHANGELOG_PATH, 'r') as f:
|
||||
changelog_content = f.read()
|
||||
|
||||
new_changelog = overwrite_changelog_section(changelog_content, NEW_CONTENT)
|
||||
# print("----------------------------------------------------------------------------------")
|
||||
# print(new_changelog)
|
||||
# print("----------------------------------------------------------------------------------")
|
||||
# Write back to CHANGELOG.md
|
||||
with open(CHANGELOG_PATH, 'w') as f:
|
||||
f.write(new_changelog)
|
||||
|
||||
print(f"{CHANGELOG_PATH} updated successfully!")
|
||||
@@ -1,113 +0,0 @@
|
||||
name: Changeset Converter
|
||||
run-name: Changeset Conversion
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
|
||||
env:
|
||||
REPO_PATH: ${{ github.repository }}
|
||||
GIT_REF: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || 'main' }}
|
||||
NODE_VERSION: 20.18.1
|
||||
|
||||
jobs:
|
||||
# Job 1: Create version bump PR when changesets are merged to main
|
||||
changeset-pr-version-bump:
|
||||
if: |
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(
|
||||
github.event_name == 'pull_request' &&
|
||||
github.event.pull_request.merged == true &&
|
||||
github.event.pull_request.base.ref == 'main' &&
|
||||
github.actor != 'github-actions'
|
||||
)
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Check user for team affiliation
|
||||
id: team_check
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
uses: morfien101/actions-authorized-user@4a3cfbf0bcb3cafe4a71710a278920c5d94bb38b
|
||||
with:
|
||||
username: ${{ github.actor }}
|
||||
org: ${{ github.repository_owner }}
|
||||
team: "deployer"
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Check if user is authorized
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |
|
||||
if [ "${{ steps.team_check.outputs.authorized }}" != "true" ]; then
|
||||
echo "User is not authorized to run this workflow."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Git Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ env.GIT_REF }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: "npm"
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
|
||||
# Check if there are any new changesets to process
|
||||
- name: Check for changesets
|
||||
id: check-changesets
|
||||
run: |
|
||||
NEW_CHANGESETS=$(find .changeset -name "*.md" ! -name "README.md" | wc -l | tr -d ' ')
|
||||
echo "Changesets diff with previous version: $NEW_CHANGESETS"
|
||||
echo "new_changesets=$NEW_CHANGESETS" >> $GITHUB_OUTPUT
|
||||
|
||||
# Create version bump PR using changesets/action if there are new changesets
|
||||
- name: Create Changeset Pull Request
|
||||
if: steps.check-changesets.outputs.new_changesets != '0'
|
||||
uses: changesets/action@v1
|
||||
with:
|
||||
commit: "changeset version bump"
|
||||
title: "Changeset version bump"
|
||||
version: npm run version-packages # This performs the changeset version bump
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Get current and previous versions to edit changelog entry
|
||||
- name: Get version
|
||||
id: get_version
|
||||
run: |
|
||||
VERSION=$(git show HEAD:package.json | jq -r '.version')
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
PREV_VERSION=$(git show origin/main:package.json | jq -r '.version')
|
||||
echo "prev_version=$PREV_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "version=$VERSION"
|
||||
echo "prev_version=$PREV_VERSION"
|
||||
|
||||
# Update CHANGELOG.md with proper format
|
||||
- name: Update Changelog Format
|
||||
env:
|
||||
VERSION: ${{ steps.get_version.outputs.version }}
|
||||
PREV_VERSION: ${{ steps.get_version.outputs.prev_version }}
|
||||
run: python .github/scripts/overwrite_changeset_changelog.py
|
||||
|
||||
# Commit and push changelog updates
|
||||
- name: Push Changelog updates to Pull Request
|
||||
run: |
|
||||
git config user.name "github-actions"
|
||||
git config user.email github-actions@github.com
|
||||
echo "Running git add and commit..."
|
||||
git add CHANGELOG.md
|
||||
git commit -m "Updating CHANGELOG.md format"
|
||||
git status
|
||||
echo "--------------------------------------------------------------------------------"
|
||||
echo "Pushing to remote..."
|
||||
echo "--------------------------------------------------------------------------------"
|
||||
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
git push origin $CURRENT_BRANCH
|
||||
@@ -55,7 +55,22 @@ jobs:
|
||||
CLINE_API_KEY: ${{ secrets.CLINE_API_KEY }}
|
||||
run: |
|
||||
cline auth -p cline -k "$CLINE_API_KEY" -m "anthropic/claude-sonnet-4.5"
|
||||
npx tsx evals/smoke-tests/run-smoke-tests.ts --trials 1 --parallel
|
||||
max_attempts=3
|
||||
for attempt in $(seq 1 $max_attempts); do
|
||||
echo "::group::Attempt $attempt of $max_attempts"
|
||||
if npx tsx evals/smoke-tests/run-smoke-tests.ts --trials 1 --parallel; then
|
||||
echo "::endgroup::"
|
||||
echo "Smoke tests passed on attempt $attempt"
|
||||
exit 0
|
||||
fi
|
||||
echo "::endgroup::"
|
||||
if [ $attempt -lt $max_attempts ]; then
|
||||
echo "::warning::Smoke tests failed on attempt $attempt, retrying..."
|
||||
sleep 10
|
||||
fi
|
||||
done
|
||||
echo "::error::Smoke tests failed after $max_attempts attempts"
|
||||
exit 1
|
||||
|
||||
- name: Generate summary
|
||||
if: always()
|
||||
|
||||
@@ -11,8 +11,13 @@ on:
|
||||
options:
|
||||
- pre-release
|
||||
- release
|
||||
auto_create_tag_from_main:
|
||||
description: "Auto-create and push the provided tag from the tested main commit (recommended)"
|
||||
required: true
|
||||
default: true
|
||||
type: boolean
|
||||
tag:
|
||||
description: "Enter existing tag to publish (e.g., v3.1.2)"
|
||||
description: "Tag to publish (required in both modes, e.g., v3.1.2)"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
@@ -35,10 +40,69 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.tag }}
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: Resolve Release Tag
|
||||
id: resolve_tag
|
||||
run: |
|
||||
TAG="${{ github.event.inputs.tag }}"
|
||||
AUTO_CREATE="${{ github.event.inputs.auto_create_tag_from_main }}"
|
||||
TESTED_SHA="${{ github.sha }}"
|
||||
WORKFLOW_REF="${{ github.ref }}"
|
||||
|
||||
if [[ -z "$TAG" ]]; then
|
||||
echo "Error: tag input is required"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.]+)?$ ]]; then
|
||||
echo "Error: tag must match vX.Y.Z (optionally with -suffix or .suffix)"
|
||||
exit 1
|
||||
fi
|
||||
TAG_REF="refs/tags/$TAG"
|
||||
|
||||
git fetch origin main --tags
|
||||
|
||||
if [[ "$AUTO_CREATE" == "true" ]]; then
|
||||
if [[ "$WORKFLOW_REF" != "refs/heads/main" ]]; then
|
||||
echo "Error: auto-create mode requires dispatching from main (current ref: $WORKFLOW_REF)"
|
||||
exit 1
|
||||
fi
|
||||
echo "Auto-create enabled. Using tested workflow SHA: $TESTED_SHA"
|
||||
|
||||
if ! git merge-base --is-ancestor "$TESTED_SHA" origin/main; then
|
||||
echo "Error: tested SHA $TESTED_SHA is not on origin/main"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if git show-ref --verify --quiet "$TAG_REF"; then
|
||||
TAG_SHA=$(git rev-list -n 1 "$TAG_REF^{commit}")
|
||||
if [[ "$TAG_SHA" != "$TESTED_SHA" ]]; then
|
||||
echo "Error: tag '$TAG' already exists at $TAG_SHA, not at tested SHA ($TESTED_SHA)"
|
||||
exit 1
|
||||
fi
|
||||
echo "Tag '$TAG' already exists at tested SHA. Continuing."
|
||||
else
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git tag "$TAG" "$TESTED_SHA"
|
||||
git push origin "$TAG_REF"
|
||||
echo "Created and pushed tag '$TAG' from tested SHA $TESTED_SHA."
|
||||
fi
|
||||
else
|
||||
if ! git show-ref --verify --quiet "$TAG_REF"; then
|
||||
echo "Error: tag '$TAG' does not exist in the repository"
|
||||
exit 1
|
||||
fi
|
||||
echo "Using existing tag '$TAG'."
|
||||
fi
|
||||
|
||||
git checkout --detach "$TAG_REF^{commit}"
|
||||
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
||||
echo "resolved_sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
@@ -59,20 +123,15 @@ jobs:
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Validate Tag
|
||||
id: validate_tag
|
||||
- name: Verify Tag Matches Package Version
|
||||
run: |
|
||||
TAG="${{ github.event.inputs.tag }}"
|
||||
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
||||
echo "Using existing tag: $TAG"
|
||||
|
||||
# Verify the tag exists
|
||||
if ! git rev-parse "$TAG" >/dev/null 2>&1; then
|
||||
echo "Error: Tag '$TAG' does not exist in the repository"
|
||||
TAG="${{ steps.resolve_tag.outputs.tag }}"
|
||||
VERSION="v${{ steps.get_version.outputs.version }}"
|
||||
if [[ "$TAG" != "$VERSION" ]]; then
|
||||
echo "Error: tag '$TAG' does not match package version '$VERSION'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Tag '$TAG' validated successfully"
|
||||
echo "Tag and package version match: $TAG"
|
||||
|
||||
- name: Package and Publish Extension
|
||||
env:
|
||||
@@ -103,7 +162,7 @@ jobs:
|
||||
- name: Get Previous Tag
|
||||
id: prev_tag
|
||||
run: |
|
||||
CURRENT_TAG="${{ steps.validate_tag.outputs.tag }}"
|
||||
CURRENT_TAG="${{ steps.resolve_tag.outputs.tag }}"
|
||||
PREV_TAG=$(git describe --tags --abbrev=0 "$CURRENT_TAG^" 2>/dev/null || echo "")
|
||||
echo "prev_tag=$PREV_TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
@@ -119,12 +178,12 @@ jobs:
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
tag_name: ${{ steps.validate_tag.outputs.tag }}
|
||||
tag_name: ${{ steps.resolve_tag.outputs.tag }}
|
||||
files: "*.vsix"
|
||||
body: |
|
||||
${{ steps.changelog.outputs.content }}
|
||||
|
||||
**Full Changelog**: https://github.com/${{ github.repository }}/compare/${{ steps.prev_tag.outputs.prev_tag }}...${{ steps.validate_tag.outputs.tag }}
|
||||
**Full Changelog**: https://github.com/${{ github.repository }}/compare/${{ steps.prev_tag.outputs.prev_tag }}...${{ steps.resolve_tag.outputs.tag }}
|
||||
prerelease: ${{ github.event.inputs.release-type == 'pre-release' }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -35,11 +35,9 @@ cli/**
|
||||
eslint-rules/
|
||||
old_docs/
|
||||
evals/
|
||||
.changie.yaml
|
||||
.codespellrc
|
||||
.mocharc.json
|
||||
buf.yaml
|
||||
.changeset/
|
||||
.clinerules/
|
||||
|
||||
# Ignore all webview-ui files except the build directory (https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/frameworks/hello-world-react-cra/.vscodeignore)
|
||||
|
||||
@@ -1,5 +1,89 @@
|
||||
# Changelog
|
||||
|
||||
## [3.69.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Add `User-Agent` header to requests sent to the Cline backend
|
||||
- Add default auto-tag workflow for publish release flow
|
||||
- Show Cline SDK docs on the Cline page
|
||||
|
||||
### Fixed
|
||||
|
||||
- Retry nested git restore and prevent silent `.git_disabled` leftovers in checkpoints
|
||||
- Prevent Chinese filename escaping in diff view
|
||||
- Trigger auto-compaction on OpenRouter context overflow errors
|
||||
- Restore GPT-OSS native file editing on OpenAI-compatible models
|
||||
|
||||
### Changed
|
||||
|
||||
- Update Cline SDK docs
|
||||
- Improve hooks support for Windows PowerShell
|
||||
|
||||
## [3.68.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Add dynamic Cline provider model fetching from Cline endpoint
|
||||
- Add additional Markdown formatting in CLI
|
||||
- Add focus indicator on action buttons in extension
|
||||
|
||||
### Fixed
|
||||
|
||||
- Clear all OCA secrets on auth refresh failure to prevent re-auth loops
|
||||
- Resolve "Could not find the file context" error in Explain Changes
|
||||
- Use `JSON_SCHEMA` for `yaml.load` to prevent unsafe deserialization
|
||||
- Fetch model info from API in CLI headless auth for Cline and Vercel providers
|
||||
- Generate commit message from staged changes only when staging exists
|
||||
- Update stale `maxTokens` values for Claude 3.7+ models across Anthropic, Bedrock, Vertex, and SAP AI Core
|
||||
- Use `model.info.maxTokens` for OpenRouter instead of hardcoded `8192`
|
||||
|
||||
### Changed
|
||||
|
||||
- Increase timeout for a flaky test to reduce short-term test instability
|
||||
|
||||
## [3.67.1]
|
||||
|
||||
### Added
|
||||
|
||||
- Added Cline SDK API interface for programmatic access to Cline features and tools, enabling integration into custom applications.
|
||||
- Added Codex 5.3 model support
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix OpenAI Codex by setting `store` to `false`
|
||||
- Use `isLocatedInPath()` instead of string matching for path containment checks
|
||||
|
||||
## [3.67.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Add support for skills and optional modelId in subagent configuration
|
||||
- Add AgentConfigLoader for file-based agent configs
|
||||
- Add Responses API support for OpenAI native provider
|
||||
- Preconnect websocket to reduce response latency
|
||||
- Fetch featured models from backend with local fallback
|
||||
- Add /q command to quit CLI
|
||||
- Add MCP enterprise configuration details
|
||||
- Pull Cline's recommended models from internal endpoint
|
||||
- Add dynamic flag to adjust banner cache duration
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix reasoning delta crash on usage-only stream chunks
|
||||
- Fix OpenAI tool ID transformation restricted to native provider only
|
||||
- Fix auth check for ACP mode
|
||||
- Fix CLI yolo mode to not persist yolo setting to disk
|
||||
- Fix inline focus-chain slider within its feature row
|
||||
- Fix Gemini 3.1 Pro compatibility
|
||||
- Fix Cline auth with ACP flag
|
||||
|
||||
### Changed
|
||||
|
||||
- Move PR skill to .agents/skills
|
||||
- SambaNova provider: update models list
|
||||
- Remove changeset-converter GitHub Action and npm run changeset
|
||||
|
||||
## [3.66.0]
|
||||
|
||||
### Added
|
||||
|
||||
+6
-25
@@ -57,25 +57,11 @@ We also welcome contributions to our [documentation](https://github.com/cline/cl
|
||||
|
||||
### Creating a Pull Request
|
||||
|
||||
1. Before creating a PR, generate a changeset entry:
|
||||
```bash
|
||||
npm run changeset
|
||||
```
|
||||
This will prompt you for:
|
||||
- Type of change (major, minor, patch)
|
||||
- `major` → breaking changes (1.0.0 → 2.0.0)
|
||||
- `minor` → new features (1.0.0 → 1.1.0)
|
||||
- `patch` → bug fixes (1.0.0 → 1.0.1)
|
||||
- Description of your changes
|
||||
1. Commit your changes.
|
||||
|
||||
2. Commit your changes and the generated `.changeset` file
|
||||
|
||||
3. Push your branch and create a PR on GitHub. Our CI will:
|
||||
2. Push your branch and create a PR on GitHub. Our CI will:
|
||||
- Run tests and checks
|
||||
- Changesetbot will create a comment showing the version impact
|
||||
- When merged to main, changesetbot will create a Version Packages PR
|
||||
- When the Version Packages PR is merged, a new release will be published
|
||||
4. Testing
|
||||
3. Testing
|
||||
- Run `npm run test` to run tests locally.
|
||||
- Before submitting PR, run `npm run format:fix` to format your code
|
||||
|
||||
@@ -192,15 +178,10 @@ Anyone can contribute code to Cline, but we ask that you follow these guidelines
|
||||
- Temporary workspaces with test fixtures
|
||||
- Video recording for failed tests
|
||||
|
||||
4. **Version Management with Changesets**
|
||||
4. **Versioning & Changelog Notes**
|
||||
|
||||
- Create a changeset for any user-facing changes using `npm run changeset`
|
||||
- Choose the appropriate version bump:
|
||||
- `major` for breaking changes (1.0.0 → 2.0.0)
|
||||
- `minor` for new features (1.0.0 → 1.1.0)
|
||||
- `patch` for bug fixes (1.0.0 → 1.0.1)
|
||||
- Write clear, descriptive changeset messages that explain the impact
|
||||
- Documentation-only changes don't require changesets
|
||||
- Contributors do not need to create changelog-entry files as part of PRs.
|
||||
- Maintainers handle release versioning and changelog curation during the release process.
|
||||
|
||||
5. **Commit Guidelines**
|
||||
|
||||
|
||||
+50
-1
@@ -1,6 +1,55 @@
|
||||
# cline
|
||||
|
||||
## 2.4.2
|
||||
## [2.5.2]
|
||||
|
||||
### Added
|
||||
|
||||
- Added Windows PowerShell support for hooks (execution, resolution, and management), improving hook behavior on Windows for CLI and shared core workflows.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Restored GPT-OSS native file editing for OpenAI-compatible models used through shared core tooling.
|
||||
- Improved OpenRouter context overflow error handling so auto-compaction triggers correctly for wrapped 400 errors.
|
||||
- Hardened checkpoint recovery by retrying nested git restore and preventing silent `.git_disabled` leftovers.
|
||||
- Added a User-Agent header for requests to the Cline back-end to improve request handling consistency.
|
||||
|
||||
## [2.5.1]
|
||||
|
||||
### Added
|
||||
|
||||
- Expanded CLI markdown rendering support (headings, lists, blockquotes, fenced code blocks, links, and nested lists).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed CLI headless auth provider model metadata loading for Cline and Vercel AI Gateway by fetching model info from API with cache fallback.
|
||||
- Increased flaky CLI import test timeout on Windows CI to reduce intermittent test failures.
|
||||
|
||||
## [2.5.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Added Cline SDK API interface for programmatic access to Cline features and tools, enabling integration into custom applications.
|
||||
- Added Codex 5.3 model support
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix OpenAI Codex by setting `store` to `false`
|
||||
- Use `isLocatedInPath()` instead of string matching for path containment checks
|
||||
|
||||
## [2.4.3]
|
||||
|
||||
### Added
|
||||
|
||||
- Add /q command to quit CLI
|
||||
- Fetch featured models from backend with local fallback
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix auth check for ACP mode
|
||||
- Fix Cline auth with ACP flag
|
||||
- Fix yolo mode to not persist yolo setting to disk
|
||||
|
||||
## [2.4.2]
|
||||
|
||||
### Added
|
||||
|
||||
|
||||
+41
-10
@@ -208,8 +208,8 @@ if (production) {
|
||||
buildEnvVars["process.env.IS_DEV"] = "false"
|
||||
}
|
||||
|
||||
const config: esbuild.BuildOptions = {
|
||||
entryPoints: [path.join(__dirname, "src", "index.ts")],
|
||||
// Shared build options
|
||||
const sharedOptions: Partial<esbuild.BuildOptions> = {
|
||||
bundle: true,
|
||||
minify: production,
|
||||
sourcemap: !production,
|
||||
@@ -221,7 +221,6 @@ const config: esbuild.BuildOptions = {
|
||||
sourcesContent: false,
|
||||
platform: "node",
|
||||
target: "node20",
|
||||
outfile: path.join(__dirname, "dist", "cli.mjs"),
|
||||
// These modules need to load files from the module directory at runtime
|
||||
external: [
|
||||
"@grpc/reflection",
|
||||
@@ -237,6 +236,13 @@ const config: esbuild.BuildOptions = {
|
||||
"@vscode/ripgrep", // Uses __dirname to locate the binary
|
||||
],
|
||||
supported: { "top-level-await": true },
|
||||
}
|
||||
|
||||
// CLI executable configuration
|
||||
const cliConfig: esbuild.BuildOptions = {
|
||||
...sharedOptions,
|
||||
entryPoints: [path.join(__dirname, "src", "index.ts")],
|
||||
outfile: path.join(__dirname, "dist", "cli.mjs"),
|
||||
banner: {
|
||||
js: `#!/usr/bin/env node
|
||||
// Suppress all Node.js warnings (deprecation, experimental, etc.)
|
||||
@@ -250,19 +256,44 @@ const __dirname = _dirname(__filename);`,
|
||||
},
|
||||
}
|
||||
|
||||
// Library configuration for programmatic use
|
||||
const libConfig: esbuild.BuildOptions = {
|
||||
...sharedOptions,
|
||||
entryPoints: [path.join(__dirname, "src", "exports.ts")],
|
||||
outfile: path.join(__dirname, "dist", "lib.mjs"),
|
||||
banner: {
|
||||
js: `// Cline Library - Programmatic API
|
||||
import { createRequire as _createRequire } from 'module';
|
||||
import { fileURLToPath as _fileURLToPath } from 'url';
|
||||
import { dirname as _dirname } from 'path';
|
||||
const require = _createRequire(import.meta.url);
|
||||
const __filename = _fileURLToPath(import.meta.url);
|
||||
const __dirname = _dirname(__filename);`,
|
||||
},
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const ctx = await esbuild.context(config)
|
||||
if (watch) {
|
||||
// In watch mode, only watch the CLI (primary use case for development)
|
||||
const ctx = await esbuild.context(cliConfig)
|
||||
await ctx.watch()
|
||||
console.log("[cli] Watching for changes...")
|
||||
} else {
|
||||
await ctx.rebuild()
|
||||
await ctx.dispose()
|
||||
// Build both CLI and library
|
||||
console.log("[cli esbuild] Building CLI executable...")
|
||||
const cliCtx = await esbuild.context(cliConfig)
|
||||
await cliCtx.rebuild()
|
||||
await cliCtx.dispose()
|
||||
|
||||
// Make the output executable
|
||||
const outfile = path.join(__dirname, "dist", "cli.mjs")
|
||||
if (fs.existsSync(outfile)) {
|
||||
fs.chmodSync(outfile, "755")
|
||||
console.log("[cli esbuild] Building library bundle...")
|
||||
const libCtx = await esbuild.context(libConfig)
|
||||
await libCtx.rebuild()
|
||||
await libCtx.dispose()
|
||||
|
||||
// Make the CLI output executable
|
||||
const cliOutfile = path.join(__dirname, "dist", "cli.mjs")
|
||||
if (fs.existsSync(cliOutfile)) {
|
||||
fs.chmodSync(cliOutfile, "755")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+15
-5
@@ -1,11 +1,18 @@
|
||||
{
|
||||
"name": "cline",
|
||||
"version": "2.4.2",
|
||||
"version": "2.5.2",
|
||||
"description": "Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more",
|
||||
"main": "dist/cli.mjs",
|
||||
"main": "dist/lib.mjs",
|
||||
"types": "dist/lib.d.ts",
|
||||
"bin": {
|
||||
"cline": "./dist/cli.mjs"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/lib.mjs",
|
||||
"types": "./dist/lib.d.ts"
|
||||
}
|
||||
},
|
||||
"os": [
|
||||
"darwin",
|
||||
"linux",
|
||||
@@ -23,8 +30,9 @@
|
||||
"scripts": {
|
||||
"package:brew": "npx tsx ./scripts/update-brew-formula.mts",
|
||||
"package": "npm pack --pack-destination ./dist",
|
||||
"build": "npm run typecheck && npx tsx esbuild.mts",
|
||||
"build:production": "npm run typecheck && npx tsx esbuild.mts --production",
|
||||
"build": "npm run typecheck && npx tsx esbuild.mts && npm run build:types",
|
||||
"build:production": "npm run typecheck && npx tsx esbuild.mts --production && npm run build:types",
|
||||
"build:types": "(npx tsc -p tsconfig.lib.json || true) && cp dist/types/cli/src/exports.d.ts dist/lib.d.ts && mkdir -p dist/agent && cp dist/types/cli/src/agent/ClineAgent.d.ts dist/types/cli/src/agent/ClineSessionEmitter.d.ts dist/types/cli/src/agent/public-types.d.ts dist/agent/ && rm -rf dist/types",
|
||||
"watch": "npx tsx esbuild.mts --watch",
|
||||
"dev": "IS_DEV=true && npm run link && npm run watch ; npm run unlink",
|
||||
"clean": "rimraf dist",
|
||||
@@ -62,6 +70,7 @@
|
||||
"url": "https://github.com/cline/cline/issues"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/marked": "^5.0.2",
|
||||
"@types/node": "20.x",
|
||||
"@types/prompts": "^2.4.9",
|
||||
"@types/react": "^19.2.9",
|
||||
@@ -81,8 +90,9 @@
|
||||
"ink": "npm:@jrichman/ink@6.4.7",
|
||||
"ink-picture": "^1.3.3",
|
||||
"ink-spinner": "^5.0.0",
|
||||
"ora": "^8.0.1",
|
||||
"marked": "^17.0.3",
|
||||
"nanoid": "^5.1.6",
|
||||
"ora": "^8.0.1",
|
||||
"pino": "^10.0.0",
|
||||
"pino-roll": "^4.0.0",
|
||||
"prompts": "^2.4.2",
|
||||
|
||||
@@ -108,11 +108,7 @@ class ACPDiffServiceClient implements DiffServiceClientInterface {
|
||||
class ACPEnvServiceClient implements EnvServiceClientInterface {
|
||||
private readonly version: string
|
||||
|
||||
constructor(
|
||||
_clientCapabilities: acp.ClientCapabilities | undefined,
|
||||
_sessionIdResolver: SessionIdResolver,
|
||||
version: string = "1.0.0",
|
||||
) {
|
||||
constructor(_clientCapabilities: acp.ClientCapabilities | undefined, _sessionIdResolver: SessionIdResolver, version: string) {
|
||||
this.version = version
|
||||
}
|
||||
|
||||
@@ -402,7 +398,7 @@ export class ACPHostBridgeClientProvider implements HostBridgeClientProvider {
|
||||
clientCapabilities: acp.ClientCapabilities | undefined,
|
||||
sessionIdResolver: SessionIdResolver,
|
||||
cwdResolver: CwdResolver,
|
||||
version: string = "1.0.0",
|
||||
version: string,
|
||||
) {
|
||||
this.workspaceClient = new ACPWorkspaceServiceClient(clientCapabilities, sessionIdResolver, cwdResolver)
|
||||
this.envClient = new ACPEnvServiceClient(clientCapabilities, sessionIdResolver, version)
|
||||
|
||||
+5
-21
@@ -15,7 +15,7 @@
|
||||
import type * as acp from "@agentclientprotocol/sdk"
|
||||
import { Logger } from "@/shared/services/Logger.js"
|
||||
import { ClineAgent } from "../agent/ClineAgent.js"
|
||||
import type { AcpAgentOptions, SessionUpdateType } from "../agent/types.js"
|
||||
import { type AcpAgentOptions, type SessionUpdateType } from "../agent/types.js"
|
||||
|
||||
/**
|
||||
* ACP Agent wrapper that bridges stdio connection to ClineAgent.
|
||||
@@ -39,37 +39,21 @@ export class AcpAgent implements acp.Agent {
|
||||
this.clineAgent = new ClineAgent(options)
|
||||
|
||||
// Wire up the permission handler to use the connection
|
||||
this.clineAgent.setPermissionHandler(async (request, resolve) => {
|
||||
this.clineAgent.setPermissionHandler(async (request) => {
|
||||
try {
|
||||
Logger.debug("[AcpAgent] Forwarding permission request to connection")
|
||||
const response = await this.connection.requestPermission({
|
||||
sessionId: this.getCurrentSessionId() ?? "",
|
||||
return await this.connection.requestPermission({
|
||||
sessionId: request.sessionId,
|
||||
toolCall: request.toolCall,
|
||||
options: request.options,
|
||||
})
|
||||
resolve(response)
|
||||
} catch (error) {
|
||||
Logger.debug("[AcpAgent] Error requesting permission:", error)
|
||||
resolve({ outcome: "rejected" as unknown as acp.RequestPermissionOutcome })
|
||||
return { outcome: { outcome: "cancelled" } }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current active session ID from the ClineAgent.
|
||||
*/
|
||||
private getCurrentSessionId(): string | undefined {
|
||||
// Find the session that's currently processing
|
||||
for (const [sessionId, session] of this.clineAgent.sessions) {
|
||||
if (session.controller?.task) {
|
||||
return sessionId
|
||||
}
|
||||
}
|
||||
// Fall back to the first session if none is actively processing
|
||||
const firstSession = this.clineAgent.sessions.keys().next()
|
||||
return firstSession.done ? undefined : firstSession.value
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to session events and forward them to the connection.
|
||||
*/
|
||||
|
||||
@@ -15,22 +15,18 @@
|
||||
|
||||
import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { version as CLI_VERSION } from "../../../package.json"
|
||||
import { AcpAgent } from "./AcpAgent.js"
|
||||
import { nodeToWebReadable, nodeToWebWritable } from "./streamUtils.js"
|
||||
|
||||
// Re-export classes for programmatic use
|
||||
export { ClineAgent } from "../agent/ClineAgent.js"
|
||||
export { ClineSessionEmitter } from "../agent/ClineSessionEmitter.js"
|
||||
// Re-export types
|
||||
export type {
|
||||
AcpAgentOptions,
|
||||
AcpSessionState,
|
||||
ClineAcpSession,
|
||||
ClineAgentOptions,
|
||||
ClineSessionEvents,
|
||||
PermissionHandler,
|
||||
PermissionResolver,
|
||||
} from "../agent/types.js"
|
||||
export { AcpAgent } from "./AcpAgent.js"
|
||||
|
||||
@@ -99,7 +95,6 @@ export async function runAcpMode(options: AcpModeOptions = {}): Promise<void> {
|
||||
|
||||
new AgentSideConnection((conn) => {
|
||||
agent = new AcpAgent(conn, {
|
||||
version: CLI_VERSION,
|
||||
debug: Boolean(options.verbose),
|
||||
})
|
||||
return agent
|
||||
|
||||
+46
-31
@@ -54,16 +54,19 @@ import { AuthService } from "@/services/auth/AuthService.js"
|
||||
import { Logger } from "@/shared/services/Logger.js"
|
||||
import type { Mode } from "@/shared/storage/types"
|
||||
import { openExternal } from "@/utils/env"
|
||||
import { version as AGENT_VERSION } from "../../package.json"
|
||||
import { ACPDiffViewProvider } from "../acp/ACPDiffViewProvider.js"
|
||||
import { ACPHostBridgeClientProvider } from "../acp/ACPHostBridgeClientProvider.js"
|
||||
import { AcpTerminalManager } from "../acp/AcpTerminalManager.js"
|
||||
import { isAuthConfigured } from "../index.js"
|
||||
import { isAuthConfigured } from "../utils/auth"
|
||||
import { fetchOpenRouterModels, usesOpenRouterModels } from "../utils/openrouter-models"
|
||||
import { CliContextResult, initializeCliContext } from "../vscode-context.js"
|
||||
import { ClineSessionEmitter } from "./ClineSessionEmitter.js"
|
||||
import { translateMessage } from "./messageTranslator.js"
|
||||
import { handlePermissionResponse } from "./permissionHandler.js"
|
||||
import type { AcpSessionState, ClineAcpSession, ClineAgentOptions, PermissionHandler } from "./types.js"
|
||||
import type { ClineAcpSession, ClineAgentOptions, PermissionHandler } from "./public-types.js"
|
||||
import { AcpSessionStatus } from "./public-types.js"
|
||||
import { type AcpSessionState } from "./types.js"
|
||||
|
||||
// Map providers to their static model lists and defaults (copied from ModelPicker.tsx)
|
||||
const providerModels: Record<string, { models: Record<string, unknown>; defaultId: string }> = {
|
||||
@@ -104,7 +107,12 @@ function getModelList(provider: string): string[] {
|
||||
export class ClineAgent implements acp.Agent {
|
||||
private readonly options: ClineAgentOptions
|
||||
private readonly ctx: CliContextResult
|
||||
readonly sessions: Map<string, ClineAcpSession> = new Map()
|
||||
|
||||
/** Map of active sessions by session ID */
|
||||
public readonly sessions: Map<string, ClineAcpSession> = new Map()
|
||||
|
||||
/** WeakMap to associate ClineAcpSession with its Controller without exposing it to consumers */
|
||||
readonly #sessionControllers = new WeakMap<ClineAcpSession, Controller>()
|
||||
|
||||
/** Runtime state for active sessions */
|
||||
private readonly sessionStates: Map<string, AcpSessionState> = new Map()
|
||||
@@ -132,7 +140,7 @@ export class ClineAgent implements acp.Agent {
|
||||
|
||||
constructor(options: ClineAgentOptions) {
|
||||
this.options = options
|
||||
this.ctx = initializeCliContext()
|
||||
this.ctx = initializeCliContext({ clineDir: options.clineDir })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -194,7 +202,7 @@ export class ClineAgent implements acp.Agent {
|
||||
},
|
||||
agentInfo: {
|
||||
name: "cline",
|
||||
version: this.options.version,
|
||||
version: AGENT_VERSION,
|
||||
},
|
||||
authMethods: [
|
||||
{
|
||||
@@ -226,7 +234,7 @@ export class ClineAgent implements acp.Agent {
|
||||
clientCapabilities,
|
||||
() => this.currentActiveSessionId,
|
||||
() => this.sessions.get(this.currentActiveSessionId ?? "")?.cwd ?? process.cwd(),
|
||||
this.options.version,
|
||||
AGENT_VERSION,
|
||||
)
|
||||
|
||||
HostProvider.initialize(
|
||||
@@ -289,16 +297,16 @@ export class ClineAgent implements acp.Agent {
|
||||
mcpServers: params.mcpServers ?? [],
|
||||
createdAt: Date.now(),
|
||||
lastActivityAt: Date.now(),
|
||||
controller,
|
||||
}
|
||||
|
||||
this.#sessionControllers.set(session, controller)
|
||||
|
||||
this.sessions.set(sessionId, session)
|
||||
|
||||
// Initialize session state
|
||||
const sessionState: AcpSessionState = {
|
||||
sessionId,
|
||||
isProcessing: false,
|
||||
cancelled: false,
|
||||
status: AcpSessionStatus.Idle,
|
||||
pendingToolCalls: new Map(),
|
||||
}
|
||||
|
||||
@@ -435,11 +443,11 @@ export class ClineAgent implements acp.Agent {
|
||||
*
|
||||
* The prompt flow:
|
||||
* 1. Extract content from the ACP prompt (text, images, files)
|
||||
* 2. Set up state broadcasting (subscribe to controller updates)
|
||||
* 3. Initialize or continue task with Controller
|
||||
* 2. Set up internal cline state subsription
|
||||
* 3. Initialize or continue cline task
|
||||
* 4. Translate ClineMessages to ACP SessionUpdates
|
||||
* 5. Handle permission requests for tools/commands
|
||||
* 6. Return when task completes, is cancelled, or needs user input
|
||||
* 6. Return when cline task completes, is cancelled, or needs user input
|
||||
*/
|
||||
async prompt(params: acp.PromptRequest): Promise<acp.PromptResponse> {
|
||||
const session = this.sessions.get(params.sessionId)
|
||||
@@ -449,11 +457,11 @@ export class ClineAgent implements acp.Agent {
|
||||
throw new Error(`Session not found: ${params.sessionId}`)
|
||||
}
|
||||
|
||||
if (sessionState.isProcessing) {
|
||||
if (sessionState.status === AcpSessionStatus.Processing) {
|
||||
throw new Error(`Session ${params.sessionId} is already processing a prompt`)
|
||||
}
|
||||
|
||||
const controller = session.controller
|
||||
const controller = this.#sessionControllers.get(session)
|
||||
if (!controller) {
|
||||
throw new Error("Controller not initialized for session. This is a bug in the ACP agent setup.")
|
||||
}
|
||||
@@ -464,8 +472,7 @@ export class ClineAgent implements acp.Agent {
|
||||
})
|
||||
|
||||
// Mark session as processing and set as current active session
|
||||
sessionState.isProcessing = true
|
||||
sessionState.cancelled = false
|
||||
sessionState.status = AcpSessionStatus.Processing
|
||||
session.lastActivityAt = Date.now()
|
||||
this.currentActiveSessionId = params.sessionId
|
||||
|
||||
@@ -586,7 +593,7 @@ export class ClineAgent implements acp.Agent {
|
||||
Logger.debug("[ClineAgent] Error during cleanup:", error)
|
||||
}
|
||||
}
|
||||
sessionState.isProcessing = false
|
||||
sessionState.status = AcpSessionStatus.Idle
|
||||
}
|
||||
}
|
||||
|
||||
@@ -648,7 +655,13 @@ export class ClineAgent implements acp.Agent {
|
||||
permissionRequest: Omit<acp.RequestPermissionRequest, "sessionId">,
|
||||
): Promise<void> {
|
||||
const session = this.sessions.get(sessionId)
|
||||
const controller = session?.controller
|
||||
|
||||
if (!session) {
|
||||
Logger.debug("[ClineAgent] No session found for permission request")
|
||||
return
|
||||
}
|
||||
|
||||
const controller = this.#sessionControllers.get(session)
|
||||
|
||||
if (!controller?.task) {
|
||||
Logger.debug("[ClineAgent] No active task for permission request")
|
||||
@@ -829,7 +842,7 @@ export class ClineAgent implements acp.Agent {
|
||||
|
||||
await this.emitSessionUpdate(sessionId, {
|
||||
sessionUpdate,
|
||||
content: { type: "text", text: needsNewline ? "\n" + textDelta : textDelta },
|
||||
content: { type: "text", text: needsNewline ? `\n${textDelta}` : textDelta },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -882,18 +895,22 @@ export class ClineAgent implements acp.Agent {
|
||||
*/
|
||||
async cancel(params: acp.CancelNotification): Promise<void> {
|
||||
const session = this.sessions.get(params.sessionId)
|
||||
if (!session) {
|
||||
Logger.debug("[ClineAgent] cancel called for non-existent session:", params.sessionId)
|
||||
return
|
||||
}
|
||||
const sessionState = this.sessionStates.get(params.sessionId)
|
||||
|
||||
Logger.debug("[ClineAgent] cancel called:", {
|
||||
sessionId: params.sessionId,
|
||||
isProcessing: sessionState?.isProcessing,
|
||||
status: sessionState?.status,
|
||||
})
|
||||
|
||||
if (sessionState) {
|
||||
sessionState.cancelled = true
|
||||
sessionState.status = AcpSessionStatus.Cancelled
|
||||
|
||||
// If we have an active controller task, cancel it
|
||||
const controller = session?.controller
|
||||
const controller = this.#sessionControllers.get(session)
|
||||
if (controller?.task) {
|
||||
try {
|
||||
await controller.cancelTask()
|
||||
@@ -934,7 +951,7 @@ export class ClineAgent implements acp.Agent {
|
||||
session.lastActivityAt = Date.now()
|
||||
|
||||
// Update Controller mode if active
|
||||
const controller = session.controller
|
||||
const controller = this.#sessionControllers.get(session)
|
||||
if (controller) {
|
||||
controller.stateManager.setGlobalState("mode", session.mode)
|
||||
|
||||
@@ -1065,7 +1082,7 @@ export class ClineAgent implements acp.Agent {
|
||||
* @returns The permission response from the client
|
||||
*/
|
||||
protected async requestPermission(
|
||||
_sessionId: string,
|
||||
sessionId: string,
|
||||
toolCall: acp.ToolCallUpdate,
|
||||
options: acp.PermissionOption[],
|
||||
): Promise<acp.RequestPermissionResponse> {
|
||||
@@ -1080,17 +1097,15 @@ export class ClineAgent implements acp.Agent {
|
||||
return { outcome: "rejected" as unknown as acp.RequestPermissionOutcome }
|
||||
}
|
||||
|
||||
// Use the permission handler callback pattern
|
||||
return new Promise<acp.RequestPermissionResponse>((resolve) => {
|
||||
this.permissionHandler!({ toolCall, options }, resolve)
|
||||
})
|
||||
return await this.permissionHandler({ sessionId, toolCall, options })
|
||||
}
|
||||
|
||||
async shutdown(): Promise<void> {
|
||||
for (const [sessionId, session] of this.sessions) {
|
||||
await session.controller?.task?.abortTask()
|
||||
await session.controller?.stateManager.flushPendingState()
|
||||
await session.controller?.dispose()
|
||||
const controller = this.#sessionControllers.get(session)
|
||||
await controller?.task?.abortTask()
|
||||
await controller?.stateManager.flushPendingState()
|
||||
await controller?.dispose()
|
||||
this.sessions.delete(sessionId)
|
||||
this.sessionStates.delete(sessionId)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
import { EventEmitter } from "events"
|
||||
import type { ClineSessionEvents } from "./types.js"
|
||||
import type { ClineSessionEvents } from "./public-types.js"
|
||||
|
||||
/**
|
||||
* Type-safe EventEmitter for ClineAgent session events.
|
||||
|
||||
@@ -12,6 +12,7 @@ import type { ClineMessage } from "@shared/ExtensionMessage"
|
||||
import { beforeEach, describe, expect, it } from "vitest"
|
||||
import { createSessionState, translateMessage, translateMessages } from "./messageTranslator"
|
||||
import type { AcpSessionState } from "./types"
|
||||
import { AcpSessionStatus } from "./types"
|
||||
|
||||
// =============================================================================
|
||||
// Test Helpers
|
||||
@@ -175,8 +176,7 @@ describe("createSessionState", () => {
|
||||
const state = createSessionState("my-session-123")
|
||||
|
||||
expect(state.sessionId).toBe("my-session-123")
|
||||
expect(state.isProcessing).toBe(false)
|
||||
expect(state.cancelled).toBe(false)
|
||||
expect(state.status).toBe(AcpSessionStatus.Idle)
|
||||
expect(state.pendingToolCalls).toBeInstanceOf(Map)
|
||||
expect(state.pendingToolCalls.size).toBe(0)
|
||||
expect(state.currentToolCallId).toBeUndefined()
|
||||
@@ -187,11 +187,11 @@ describe("createSessionState", () => {
|
||||
const state2 = createSessionState("session-2")
|
||||
|
||||
// Modify state1
|
||||
state1.isProcessing = true
|
||||
state1.status = AcpSessionStatus.Processing
|
||||
state1.pendingToolCalls.set("tool-1", {} as acp.ToolCall)
|
||||
|
||||
// state2 should be unaffected
|
||||
expect(state2.isProcessing).toBe(false)
|
||||
expect(state2.status).toBe(AcpSessionStatus.Idle)
|
||||
expect(state2.pendingToolCalls.size).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import type * as acp from "@agentclientprotocol/sdk"
|
||||
import type { ClineMessage, ClineSayBrowserAction, ClineSayTool } from "@shared/ExtensionMessage"
|
||||
import type { AcpSessionState, TranslatedMessage } from "./types.js"
|
||||
import { AcpSessionStatus } from "./types.js"
|
||||
|
||||
/**
|
||||
* Maps Cline tool types to ACP ToolKind values.
|
||||
@@ -1019,8 +1020,7 @@ export function translateMessages(messages: ClineMessage[], sessionState: AcpSes
|
||||
export function createSessionState(sessionId: string): AcpSessionState {
|
||||
return {
|
||||
sessionId,
|
||||
isProcessing: false,
|
||||
cancelled: false,
|
||||
status: AcpSessionStatus.Idle,
|
||||
pendingToolCalls: new Map(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
/**
|
||||
* Public types for the Cline library API.
|
||||
*
|
||||
* This file contains types that are safe to export to library consumers.
|
||||
* It must NOT import any internal types (Controller, StateManager, etc.)
|
||||
* to keep the generated declaration files clean.
|
||||
*
|
||||
* Internal-only extensions of these types live in ./types.ts.
|
||||
*/
|
||||
|
||||
import type * as acp from "@agentclientprotocol/sdk"
|
||||
|
||||
// ============================================================
|
||||
// Session Update Type Utilities
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Different types of updates that can be sent during session processing.
|
||||
*
|
||||
* These updates provide real-time feedback about the agent's progress.
|
||||
*
|
||||
* See protocol docs: [Agent Reports Output](https://agentclientprotocol.com/protocol/prompt-turn#3-agent-reports-output)
|
||||
*/
|
||||
export type SessionUpdateType = acp.SessionUpdate["sessionUpdate"]
|
||||
|
||||
/**
|
||||
* Different types of update payloads that can be sent during session processing.
|
||||
*
|
||||
* Each update type has a corresponding payload structure defined in the ACP SessionUpdate union.
|
||||
*/
|
||||
export type SessionUpdatePayload<T extends SessionUpdateType> = Omit<
|
||||
Extract<acp.SessionUpdate, { sessionUpdate: T }>,
|
||||
"sessionUpdate"
|
||||
>
|
||||
|
||||
// ============================================================
|
||||
// Permission Handler Callback Types
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Handler function for permission requests.
|
||||
* Called when the agent needs permission for a tool call.
|
||||
* The handler should present the request to the user and call resolve() with their response.
|
||||
*/
|
||||
export type PermissionHandler = (request: acp.RequestPermissionRequest) => Promise<acp.RequestPermissionResponse>
|
||||
|
||||
// ============================================================
|
||||
// Session Event Emitter Types
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Maps ACP SessionUpdate types to their event listener signatures.
|
||||
* Uses the sessionUpdate discriminator to derive event names and payload types.
|
||||
*/
|
||||
export type ClineSessionEvents = {
|
||||
[K in SessionUpdateType]: (payload: SessionUpdatePayload<K>) => void
|
||||
} & {
|
||||
/** Error event for session-level errors (not part of ACP SessionUpdate) */
|
||||
error: (error: Error) => void
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// ClineAgent Options
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Options for creating a ClineAgent instance.
|
||||
*/
|
||||
export interface ClineAgentOptions {
|
||||
/** Whether debug logging is enabled */
|
||||
debug?: boolean
|
||||
/** Cline Config Directory (defaults to ~/.cline) */
|
||||
clineDir?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for creating an ACP agent instance.
|
||||
*/
|
||||
export interface AcpAgentOptions {
|
||||
/** Whether debug logging is enabled */
|
||||
debug?: boolean
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Session Types
|
||||
// ============================================================
|
||||
export type SessionID = string
|
||||
|
||||
/**
|
||||
* Extended session data stored by Cline for ACP sessions.
|
||||
*/
|
||||
export interface ClineAcpSession {
|
||||
/** Unique session ID */
|
||||
sessionId: SessionID
|
||||
/** Working directory for the session */
|
||||
cwd: string
|
||||
/** Current mode (plan/act) */
|
||||
mode: "plan" | "act"
|
||||
/** MCP servers passed from the client */
|
||||
mcpServers: acp.McpServer[]
|
||||
/** Timestamp when session was created */
|
||||
createdAt: number
|
||||
/** Timestamp of last activity */
|
||||
lastActivityAt: number
|
||||
/** Whether this session was loaded from history (needs resume on first prompt) */
|
||||
isLoadedFromHistory?: boolean
|
||||
/** Model ID override for plan mode (format: "provider/modelId") */
|
||||
planModeModelId?: string
|
||||
/** Model ID override for act mode (format: "provider/modelId") */
|
||||
actModeModelId?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Lifecycle status of an ACP session.
|
||||
*
|
||||
* Represents the state machine:
|
||||
* Idle → Processing → Idle (normal completion)
|
||||
* Idle → Processing → Cancelled (cancellation, then back to Idle on next prompt)
|
||||
*/
|
||||
export enum AcpSessionStatus {
|
||||
/** Session is idle, waiting for a prompt */
|
||||
Idle = "idle",
|
||||
/** Session is actively processing a prompt */
|
||||
Processing = "processing",
|
||||
/** Session processing was cancelled */
|
||||
Cancelled = "cancelled",
|
||||
}
|
||||
|
||||
/**
|
||||
* State tracking for an active ACP session within Cline.
|
||||
*/
|
||||
export interface AcpSessionState {
|
||||
/** Session ID */
|
||||
sessionId: SessionID
|
||||
/** Current lifecycle status of the session */
|
||||
status: AcpSessionStatus
|
||||
/** Current tool call ID being executed (if any) */
|
||||
currentToolCallId?: string
|
||||
/** Accumulated tool calls for permission batching */
|
||||
pendingToolCalls: Map<string, acp.ToolCall>
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Agent Capabilities
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Cline-specific agent capabilities extending the ACP base capabilities.
|
||||
*/
|
||||
export interface ClineAgentCapabilities {
|
||||
/** Support for loading sessions from disk */
|
||||
loadSession: boolean
|
||||
/** Prompt capabilities for the agent */
|
||||
promptCapabilities: {
|
||||
/** Support for image inputs */
|
||||
image: boolean
|
||||
/** Support for audio inputs */
|
||||
audio: boolean
|
||||
/** Support for embedded context (file resources) */
|
||||
embeddedContext: boolean
|
||||
}
|
||||
/** MCP server passthrough capabilities */
|
||||
mcpCapabilities: {
|
||||
/** Support for HTTP MCP servers */
|
||||
http: boolean
|
||||
/** Support for SSE MCP servers */
|
||||
sse: boolean
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cline agent info for ACP initialization response.
|
||||
*/
|
||||
export interface ClineAgentInfo {
|
||||
name: "cline"
|
||||
title: "Cline"
|
||||
version: string
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Permission Options
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Permission option as presented to the ACP client.
|
||||
*/
|
||||
export interface ClinePermissionOption {
|
||||
kind: acp.PermissionOptionKind
|
||||
name: string
|
||||
optionId: string
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Message Translation
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Result of translating a Cline message to ACP session update(s).
|
||||
* A single Cline message may produce multiple ACP updates.
|
||||
*/
|
||||
export interface TranslatedMessage {
|
||||
/** The session updates to send */
|
||||
updates: acp.SessionUpdate[]
|
||||
/** Whether this message requires a permission request */
|
||||
requiresPermission?: boolean
|
||||
/** Permission request details if required */
|
||||
permissionRequest?: Omit<acp.RequestPermissionRequest, "sessionId">
|
||||
/** The toolCallId that was created/used (for tracking across streaming updates) */
|
||||
toolCallId?: string
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Re-exported ACP Types
|
||||
// ============================================================
|
||||
|
||||
export type {
|
||||
Agent,
|
||||
AgentSideConnection,
|
||||
AudioContent,
|
||||
CancelNotification,
|
||||
ClientCapabilities,
|
||||
ContentBlock,
|
||||
ImageContent,
|
||||
InitializeRequest,
|
||||
InitializeResponse,
|
||||
LoadSessionRequest,
|
||||
LoadSessionResponse,
|
||||
McpServer,
|
||||
ModelInfo,
|
||||
NewSessionRequest,
|
||||
NewSessionResponse,
|
||||
PermissionOption,
|
||||
PermissionOptionKind,
|
||||
PromptRequest,
|
||||
PromptResponse,
|
||||
RequestPermissionRequest,
|
||||
RequestPermissionResponse,
|
||||
SessionConfigOption,
|
||||
SessionModelState,
|
||||
SessionNotification,
|
||||
SessionUpdate,
|
||||
SetSessionConfigOptionRequest,
|
||||
SetSessionConfigOptionResponse,
|
||||
SetSessionModelRequest,
|
||||
SetSessionModelResponse,
|
||||
SetSessionModeRequest,
|
||||
SetSessionModeResponse,
|
||||
StopReason,
|
||||
TextContent,
|
||||
ToolCall,
|
||||
ToolCallStatus,
|
||||
ToolCallUpdate,
|
||||
ToolKind,
|
||||
} from "@agentclientprotocol/sdk"
|
||||
+20
-199
@@ -1,76 +1,13 @@
|
||||
/**
|
||||
* Custom types and extensions for ACP integration with Cline CLI.
|
||||
* Internal types for ACP integration with Cline CLI.
|
||||
*
|
||||
* This file extends the base ACP types with Cline-specific functionality.
|
||||
* This file re-exports all public types from ./public-types.ts and adds
|
||||
* internal-only Types that reference core modules (Controller, etc.).
|
||||
*
|
||||
* Library consumers should never import from this file directly — they
|
||||
* get the public types via the library entrypoint (exports.ts).
|
||||
*/
|
||||
|
||||
import type * as acp from "@agentclientprotocol/sdk"
|
||||
import type { Controller } from "@/core/controller"
|
||||
|
||||
// ============================================================
|
||||
// Session Update Type Utilities
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Extract the sessionUpdate discriminator value from a SessionUpdate variant.
|
||||
*/
|
||||
export type SessionUpdateType = acp.SessionUpdate["sessionUpdate"]
|
||||
|
||||
/**
|
||||
* Extract the payload type for a given sessionUpdate discriminator value.
|
||||
* This removes the `sessionUpdate` discriminator field from the type.
|
||||
*/
|
||||
export type SessionUpdatePayload<T extends SessionUpdateType> = Omit<
|
||||
Extract<acp.SessionUpdate, { sessionUpdate: T }>,
|
||||
"sessionUpdate"
|
||||
>
|
||||
|
||||
// ============================================================
|
||||
// Permission Handler Callback Types
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Callback to resolve a permission request with the user's response.
|
||||
*/
|
||||
export type PermissionResolver = (response: acp.RequestPermissionResponse) => void
|
||||
|
||||
/**
|
||||
* Handler function for permission requests.
|
||||
* Called when the agent needs permission for a tool call.
|
||||
* The handler should present the request to the user and call resolve() with their response.
|
||||
*/
|
||||
export type PermissionHandler = (request: Omit<acp.RequestPermissionRequest, "sessionId">, resolve: PermissionResolver) => void
|
||||
|
||||
// ============================================================
|
||||
// Session Event Emitter Types
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Maps ACP SessionUpdate types to their event listener signatures.
|
||||
* Uses the sessionUpdate discriminator to derive event names and payload types.
|
||||
*/
|
||||
export type ClineSessionEvents = {
|
||||
[K in SessionUpdateType]: (payload: SessionUpdatePayload<K>) => void
|
||||
} & {
|
||||
/** Error event for session-level errors (not part of ACP SessionUpdate) */
|
||||
error: (error: Error) => void
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// ClineAgent Options (decoupled from connection)
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Options for creating a ClineAgent instance (decoupled from connection).
|
||||
*/
|
||||
export interface ClineAgentOptions {
|
||||
/** CLI version string */
|
||||
version: string
|
||||
/** Whether debug logging is enabled */
|
||||
debug?: boolean
|
||||
}
|
||||
|
||||
// Re-export common ACP types for convenience
|
||||
export type {
|
||||
Agent,
|
||||
AgentSideConnection,
|
||||
@@ -114,134 +51,18 @@ export type {
|
||||
WriteTextFileResponse,
|
||||
} from "@agentclientprotocol/sdk"
|
||||
|
||||
/**
|
||||
* Cline-specific agent capabilities extending the ACP base capabilities.
|
||||
*/
|
||||
export interface ClineAgentCapabilities {
|
||||
/** Support for loading sessions from disk */
|
||||
loadSession: boolean
|
||||
/** Prompt capabilities for the agent */
|
||||
promptCapabilities: {
|
||||
/** Support for image inputs */
|
||||
image: boolean
|
||||
/** Support for audio inputs */
|
||||
audio: boolean
|
||||
/** Support for embedded context (file resources) */
|
||||
embeddedContext: boolean
|
||||
}
|
||||
/** MCP server passthrough capabilities */
|
||||
mcpCapabilities: {
|
||||
/** Support for HTTP MCP servers */
|
||||
http: boolean
|
||||
/** Support for SSE MCP servers */
|
||||
sse: boolean
|
||||
}
|
||||
}
|
||||
export type {
|
||||
AcpAgentOptions,
|
||||
AcpSessionState,
|
||||
ClineAgentCapabilities,
|
||||
ClineAgentInfo,
|
||||
ClineAgentOptions,
|
||||
ClinePermissionOption,
|
||||
ClineSessionEvents,
|
||||
PermissionHandler,
|
||||
SessionUpdatePayload,
|
||||
SessionUpdateType,
|
||||
TranslatedMessage,
|
||||
} from "./public-types.js"
|
||||
|
||||
/**
|
||||
* Cline agent info for ACP initialization response.
|
||||
*/
|
||||
export interface ClineAgentInfo {
|
||||
name: "cline"
|
||||
title: "Cline"
|
||||
version: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Extended session data stored by Cline for ACP sessions.
|
||||
* Maps to Cline's task history structure.
|
||||
*/
|
||||
export interface ClineAcpSession {
|
||||
/** Unique session/task ID */
|
||||
sessionId: string
|
||||
/** Working directory for the session */
|
||||
cwd: string
|
||||
/** Current mode (plan/act) */
|
||||
mode: "plan" | "act"
|
||||
/** MCP servers passed from the client */
|
||||
mcpServers: acp.McpServer[]
|
||||
/** Timestamp when session was created */
|
||||
createdAt: number
|
||||
/** Timestamp of last activity */
|
||||
lastActivityAt: number
|
||||
/** Whether this session was loaded from history (needs resume on first prompt) */
|
||||
isLoadedFromHistory?: boolean
|
||||
/** Controller instance for this session (manages task execution) */
|
||||
controller?: Controller
|
||||
/** Model ID override for plan mode (format: "provider/modelId") */
|
||||
planModeModelId?: string
|
||||
/** Model ID override for act mode (format: "provider/modelId") */
|
||||
actModeModelId?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission option as presented to the ACP client.
|
||||
*/
|
||||
export interface ClinePermissionOption {
|
||||
kind: acp.PermissionOptionKind
|
||||
name: string
|
||||
optionId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapping of Cline message types to their ACP session update equivalents.
|
||||
*/
|
||||
export type ClineToAcpUpdateMapping = {
|
||||
/** Text messages from the agent */
|
||||
text: "agent_message_chunk"
|
||||
/** Reasoning/thinking from the agent */
|
||||
reasoning: "agent_thought_chunk"
|
||||
/** Markdown content from the agent */
|
||||
markdown: "agent_message_chunk"
|
||||
/** Tool execution */
|
||||
tool: "tool_call"
|
||||
/** Command execution */
|
||||
command: "tool_call"
|
||||
/** Command output */
|
||||
command_output: "tool_call_update"
|
||||
/** Task completion */
|
||||
completion_result: "end_turn"
|
||||
/** Error messages */
|
||||
error: "tool_call_update" | "error"
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for creating an ACP agent instance.
|
||||
*/
|
||||
export interface AcpAgentOptions {
|
||||
/** CLI version string */
|
||||
version: string
|
||||
/** Whether debug logging is enabled */
|
||||
debug?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of translating a Cline message to ACP session update(s).
|
||||
* A single Cline message may produce multiple ACP updates.
|
||||
*/
|
||||
export interface TranslatedMessage {
|
||||
/** The session updates to send */
|
||||
updates: acp.SessionUpdate[]
|
||||
/** Whether this message requires a permission request */
|
||||
requiresPermission?: boolean
|
||||
/** Permission request details if required */
|
||||
permissionRequest?: Omit<acp.RequestPermissionRequest, "sessionId">
|
||||
/** The toolCallId that was created/used (for tracking across streaming updates) */
|
||||
toolCallId?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* State tracking for an active ACP session within Cline.
|
||||
*/
|
||||
export interface AcpSessionState {
|
||||
/** Session ID */
|
||||
sessionId: string
|
||||
/** Whether the session is currently processing a prompt */
|
||||
isProcessing: boolean
|
||||
/** Current tool call ID being executed (if any) */
|
||||
currentToolCallId?: string
|
||||
/** Whether the session has been cancelled */
|
||||
cancelled: boolean
|
||||
/** Accumulated tool calls for permission batching */
|
||||
pendingToolCalls: Map<string, acp.ToolCall>
|
||||
}
|
||||
export { AcpSessionStatus } from "./public-types.js"
|
||||
|
||||
@@ -15,6 +15,7 @@ import { StringRequest } from "@/shared/proto/cline/common"
|
||||
import { openExternal } from "@/utils/env"
|
||||
import { COLORS } from "../constants/colors"
|
||||
import { useStdinContext } from "../context/StdinContext"
|
||||
import { useClineFeaturedModels } from "../hooks/useClineFeaturedModels"
|
||||
import { useOcaAuth } from "../hooks/useOcaAuth"
|
||||
import { useScrollableList } from "../hooks/useScrollableList"
|
||||
import { type DetectedSources, detectImportSources, type ImportSource } from "../utils/import-configs"
|
||||
@@ -172,6 +173,7 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
const [providerSearch, setProviderSearch] = useState("")
|
||||
const [providerIndex, setProviderIndex] = useState(0)
|
||||
const [clineModelIndex, setClineModelIndex] = useState(0)
|
||||
const featuredModels = useClineFeaturedModels()
|
||||
const [importSources, setImportSources] = useState<DetectedSources>({ codex: false, opencode: false })
|
||||
const [importSource, setImportSource] = useState<ImportSource | null>(null)
|
||||
const [bedrockConfig, setBedrockConfig] = useState<BedrockConfig | null>(null)
|
||||
@@ -767,7 +769,7 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
<Box flexDirection="column">
|
||||
<Text color="white">Choose a model</Text>
|
||||
<Text> </Text>
|
||||
<FeaturedModelPicker selectedIndex={clineModelIndex} />
|
||||
<FeaturedModelPicker featuredModels={featuredModels} selectedIndex={clineModelIndex} />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -869,17 +871,17 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
setProviderSearch((prev) => prev + input)
|
||||
}
|
||||
} else if (step === "cline_model") {
|
||||
const maxIndex = getFeaturedModelMaxIndex()
|
||||
const maxIndex = getFeaturedModelMaxIndex(featuredModels)
|
||||
|
||||
if (key.upArrow) {
|
||||
setClineModelIndex((prev) => (prev > 0 ? prev - 1 : maxIndex))
|
||||
} else if (key.downArrow) {
|
||||
setClineModelIndex((prev) => (prev < maxIndex ? prev + 1 : 0))
|
||||
} else if (key.return) {
|
||||
if (isBrowseAllSelected(clineModelIndex)) {
|
||||
if (isBrowseAllSelected(clineModelIndex, featuredModels)) {
|
||||
setStep("modelid")
|
||||
} else {
|
||||
const selectedModel = getFeaturedModelAtIndex(clineModelIndex)
|
||||
const selectedModel = getFeaturedModelAtIndex(clineModelIndex, featuredModels)
|
||||
if (selectedModel) {
|
||||
handleClineModelSelect(selectedModel.id)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { ClineMessage } from "@shared/ExtensionMessage"
|
||||
import { render } from "ink-testing-library"
|
||||
import React from "react"
|
||||
import { describe, expect, it, vi } from "vitest"
|
||||
import { ChatMessage } from "./ChatMessage"
|
||||
|
||||
vi.mock("../hooks/useTerminalSize", () => ({
|
||||
useTerminalSize: () => ({
|
||||
columns: 120,
|
||||
rows: 40,
|
||||
resizeKey: 0,
|
||||
}),
|
||||
}))
|
||||
|
||||
describe("ChatMessage markdown rendering", () => {
|
||||
it("renders basic markdown elements correctly with appropriate styling", () => {
|
||||
const message: ClineMessage = {
|
||||
ts: Date.now(),
|
||||
type: "say",
|
||||
say: "text",
|
||||
text: "# Heading 1\n\nThis is a **bold** and *italic* text with `inline code`.\n\n- List item 1\n- List item 2\n\n> Blockquote\n\n```javascript\nconst x = 1;\n```",
|
||||
}
|
||||
|
||||
const { lastFrame } = render(React.createElement(ChatMessage, { message, mode: "act" }))
|
||||
const frame = lastFrame() || ""
|
||||
|
||||
// Check for heading (bold)
|
||||
// \x1B[1m is the ANSI escape code for bold
|
||||
expect(frame).toMatch(/\x1B\[1mHeading 1\x1B\[22m/)
|
||||
|
||||
// Check for bold text
|
||||
expect(frame).toMatch(/\x1B\[1mbold\x1B\[22m/)
|
||||
|
||||
// Check for italic text
|
||||
// \x1B[3m is the ANSI escape code for italic
|
||||
expect(frame).toMatch(/\x1B\[3mitalic\x1B\[23m/)
|
||||
|
||||
// Check for inline code (no special styling in the current implementation, just text)
|
||||
expect(frame).toContain("inline code")
|
||||
|
||||
// Check for list items (gray bullet)
|
||||
// \x1B[90m is the ANSI escape code for gray
|
||||
expect(frame).toMatch(/\x1B\[90m• \x1B\[39mList item 1/)
|
||||
expect(frame).toMatch(/\x1B\[90m• \x1B\[39mList item 2/)
|
||||
|
||||
// Check for blockquote (gray pipe)
|
||||
expect(frame).toMatch(/\x1B\[90m│ \x1B\[39mBlockquote/)
|
||||
|
||||
// Check for code block (cyan text)
|
||||
// \x1B[36m is the ANSI escape code for cyan
|
||||
expect(frame).toMatch(/\x1B\[36mconst x = 1;\x1B\[39m/)
|
||||
})
|
||||
})
|
||||
@@ -11,6 +11,7 @@ import { COMMAND_OUTPUT_STRING } from "@shared/combineCommandSequences"
|
||||
import type { ClineAskUseMcpServer, ClineMessage } from "@shared/ExtensionMessage"
|
||||
import { Box, Text } from "ink"
|
||||
import Spinner from "ink-spinner"
|
||||
import { lexer, type Token, type Tokens } from "marked"
|
||||
import React from "react"
|
||||
import { COLORS } from "../constants/colors"
|
||||
import { useTerminalSize } from "../hooks/useTerminalSize"
|
||||
@@ -20,13 +21,10 @@ import { DiffView } from "./DiffView"
|
||||
import { SubagentMessage } from "./SubagentMessage"
|
||||
|
||||
/**
|
||||
* Add "(Tab)" hint after "Act mode" mentions.
|
||||
* Add "(Tab)" hint after "Act mode" mentions in plain text.
|
||||
* Case-insensitive, avoids double-adding if already present.
|
||||
* Matches just "Act mode" without requiring "to " prefix because markdown
|
||||
* processing may split "toggle to **Act mode**" into separate text chunks.
|
||||
*/
|
||||
function addActModeHint(text: string, keyPrefix: string): React.ReactNode[] {
|
||||
// Match "Act mode" in various capitalizations, but not if already followed by (Tab)
|
||||
const actModeRegex = /\bact\s+mode\b(?!\s*\(tab\))/gi
|
||||
const parts = text.split(actModeRegex)
|
||||
const matches = text.match(actModeRegex)
|
||||
@@ -37,9 +35,7 @@ function addActModeHint(text: string, keyPrefix: string): React.ReactNode[] {
|
||||
|
||||
const nodes: React.ReactNode[] = []
|
||||
parts.forEach((part, i) => {
|
||||
if (part) {
|
||||
nodes.push(part)
|
||||
}
|
||||
if (part) nodes.push(part)
|
||||
if (matches[i]) {
|
||||
nodes.push(
|
||||
<React.Fragment key={`${keyPrefix}-act-mode-${i}`}>
|
||||
@@ -49,72 +45,146 @@ function addActModeHint(text: string, keyPrefix: string): React.ReactNode[] {
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
/**
|
||||
* Render inline markdown: **bold**, *italic*, `code`
|
||||
* Also adds "(Tab)" hints after "Act mode" mentions.
|
||||
* Returns array of React nodes with appropriate styling
|
||||
* Render an array of marked tokens as Ink React nodes.
|
||||
* This is the entry point for recursive rendering — each token may
|
||||
* contain child tokens (e.g. a paragraph contains inline tokens,
|
||||
* a list contains items, etc.).
|
||||
*/
|
||||
function renderInlineMarkdown(text: string): React.ReactNode[] {
|
||||
const nodes: React.ReactNode[] = []
|
||||
let hintCallIndex = 0
|
||||
const addHintedText = (value: string) => addActModeHint(value, `hint-${hintCallIndex++}`)
|
||||
// Match **bold**, *italic*, or `code` - order matters (** before *)
|
||||
const regex = /(\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`)/g
|
||||
let lastIndex = 0
|
||||
let match
|
||||
|
||||
while ((match = regex.exec(text)) !== null) {
|
||||
// Add text before match (with Act Mode hint processing)
|
||||
if (match.index > lastIndex) {
|
||||
const beforeText = text.slice(lastIndex, match.index)
|
||||
nodes.push(...addHintedText(beforeText))
|
||||
}
|
||||
|
||||
const fullMatch = match[0]
|
||||
const key = `md-${match.index}`
|
||||
|
||||
if (fullMatch.startsWith("**") && fullMatch.endsWith("**")) {
|
||||
// Bold - also process for Act Mode hints inside bold text
|
||||
const boldContent = fullMatch.slice(2, -2)
|
||||
const hintedContent = addHintedText(boldContent)
|
||||
nodes.push(
|
||||
<Text bold key={key}>
|
||||
{hintedContent}
|
||||
</Text>,
|
||||
)
|
||||
} else if (fullMatch.startsWith("*") && fullMatch.endsWith("*")) {
|
||||
// Italic
|
||||
nodes.push(
|
||||
<Text italic key={key}>
|
||||
{fullMatch.slice(1, -1)}
|
||||
</Text>,
|
||||
)
|
||||
} else if (fullMatch.startsWith("`") && fullMatch.endsWith("`")) {
|
||||
// Inline code
|
||||
nodes.push(<Text key={key}>{fullMatch.slice(1, -1)}</Text>)
|
||||
}
|
||||
|
||||
lastIndex = regex.lastIndex
|
||||
}
|
||||
|
||||
// Add remaining text (with Act Mode hint processing)
|
||||
if (lastIndex < text.length) {
|
||||
nodes.push(...addHintedText(text.slice(lastIndex)))
|
||||
}
|
||||
|
||||
return nodes.length > 0 ? nodes : addHintedText(text)
|
||||
function renderTokens(tokens: Token[], color?: string): React.ReactNode[] {
|
||||
return tokens.map((token, i) => renderToken(token, i, color))
|
||||
}
|
||||
|
||||
/**
|
||||
* Render text with inline markdown support
|
||||
* Render a single marked token (block or inline) as an Ink React node.
|
||||
* Handles both block-level tokens (heading, paragraph, list, code, etc.)
|
||||
* and inline tokens (strong, em, codespan, link, text).
|
||||
*/
|
||||
function renderToken(token: Token, key: number, color?: string): React.ReactNode {
|
||||
switch (token.type) {
|
||||
// --- Block tokens ---
|
||||
|
||||
case "heading": {
|
||||
const { depth, tokens } = token as Tokens.Heading
|
||||
return (
|
||||
<Box key={key} marginY={depth === 1 ? 1 : 0}>
|
||||
<Text bold color={color}>
|
||||
{renderTokens(tokens, color)}
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
case "paragraph":
|
||||
return (
|
||||
<Text color={color} key={key}>
|
||||
{renderTokens((token as Tokens.Paragraph).tokens, color)}
|
||||
</Text>
|
||||
)
|
||||
|
||||
case "code":
|
||||
return (
|
||||
<Box flexDirection="column" key={key} marginY={1}>
|
||||
{(token as Tokens.Code).text.split("\n").map((line, i) => (
|
||||
<Text color="cyan" key={i}>
|
||||
{line || " "}
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
)
|
||||
|
||||
case "list": {
|
||||
const { ordered, start, items } = token as Tokens.List
|
||||
return (
|
||||
<Box flexDirection="column" key={key}>
|
||||
{items.map((item, i) => (
|
||||
<Box flexDirection="row" key={i}>
|
||||
<Text color="gray">{ordered ? `${Number(start ?? 1) + i}. ` : "• "}</Text>
|
||||
<Box flexDirection="column" flexGrow={1}>
|
||||
{renderTokens(item.tokens, color)}
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
case "blockquote":
|
||||
return (
|
||||
<Box flexDirection="row" key={key}>
|
||||
<Text color="gray">│ </Text>
|
||||
<Box flexDirection="column">{renderTokens((token as Tokens.Blockquote).tokens, color)}</Box>
|
||||
</Box>
|
||||
)
|
||||
|
||||
case "space":
|
||||
return <Text key={key}> </Text>
|
||||
|
||||
// --- Inline tokens ---
|
||||
|
||||
case "strong":
|
||||
return (
|
||||
<Text bold color={color} key={key}>
|
||||
{renderTokens((token as Tokens.Strong).tokens, color)}
|
||||
</Text>
|
||||
)
|
||||
|
||||
case "em":
|
||||
return (
|
||||
<Text color={color} italic key={key}>
|
||||
{renderTokens((token as Tokens.Em).tokens, color)}
|
||||
</Text>
|
||||
)
|
||||
|
||||
case "codespan":
|
||||
return <Text key={key}>{(token as Tokens.Codespan).text}</Text>
|
||||
|
||||
case "link": {
|
||||
const { text, href } = token as Tokens.Link
|
||||
return (
|
||||
<Text color={color} key={key}>
|
||||
{text && text !== href ? `${text} (${href})` : href}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
case "text": {
|
||||
const { text, tokens } = token as Tokens.Text
|
||||
if (tokens?.length) {
|
||||
return (
|
||||
<Text color={color} key={key}>
|
||||
{renderTokens(tokens, color)}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Text color={color} key={key}>
|
||||
{addActModeHint(text, `${key}`)}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
// Fallback for any unhandled token type
|
||||
default:
|
||||
return "raw" in token ? (
|
||||
<Text color={color} key={key}>
|
||||
{(token as { raw: string }).raw}
|
||||
</Text>
|
||||
) : null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a markdown string as Ink components.
|
||||
* Uses marked's lexer to parse markdown into tokens, then renders
|
||||
* each token to the appropriate Ink component.
|
||||
*/
|
||||
const MarkdownText: React.FC<{ children: string; color?: string }> = ({ children, color }) => {
|
||||
const nodes = renderInlineMarkdown(children)
|
||||
return <Text color={color}>{nodes}</Text>
|
||||
const tokens = lexer(children)
|
||||
return <Box flexDirection="column">{renderTokens(tokens, color)}</Box>
|
||||
}
|
||||
|
||||
interface ChatMessageProps {
|
||||
|
||||
@@ -7,13 +7,14 @@
|
||||
import { Box, Text } from "ink"
|
||||
import React from "react"
|
||||
import { COLORS } from "../constants/colors"
|
||||
import { type FeaturedModel, getAllFeaturedModels } from "../constants/featured-models"
|
||||
import type { FeaturedModel } from "../constants/featured-models"
|
||||
|
||||
interface FeaturedModelPickerProps {
|
||||
selectedIndex: number
|
||||
title?: string
|
||||
showBrowseAll?: boolean
|
||||
helpText?: string
|
||||
featuredModels: FeaturedModel[]
|
||||
}
|
||||
|
||||
export const FeaturedModelPicker: React.FC<FeaturedModelPickerProps> = ({
|
||||
@@ -21,8 +22,9 @@ export const FeaturedModelPicker: React.FC<FeaturedModelPickerProps> = ({
|
||||
title,
|
||||
showBrowseAll = true,
|
||||
helpText = "Arrows to navigate, Enter to select",
|
||||
featuredModels,
|
||||
}) => {
|
||||
const featuredModels = getAllFeaturedModels()
|
||||
const models = featuredModels
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
@@ -35,7 +37,7 @@ export const FeaturedModelPicker: React.FC<FeaturedModelPickerProps> = ({
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{featuredModels.map((model, i) => {
|
||||
{models.map((model, i) => {
|
||||
const isSelected = i === selectedIndex
|
||||
|
||||
return (
|
||||
@@ -64,8 +66,8 @@ export const FeaturedModelPicker: React.FC<FeaturedModelPickerProps> = ({
|
||||
|
||||
{showBrowseAll && (
|
||||
<Box>
|
||||
<Text color={selectedIndex === featuredModels.length ? COLORS.primaryBlue : "white"}>
|
||||
{selectedIndex === featuredModels.length ? "❯ " : " "}
|
||||
<Text color={selectedIndex === models.length ? COLORS.primaryBlue : "white"}>
|
||||
{selectedIndex === models.length ? "❯ " : " "}
|
||||
Browse all models...
|
||||
</Text>
|
||||
</Box>
|
||||
@@ -81,24 +83,21 @@ export const FeaturedModelPicker: React.FC<FeaturedModelPickerProps> = ({
|
||||
* Get the maximum valid index for the featured model picker
|
||||
* (includes "Browse all" option if showBrowseAll is true)
|
||||
*/
|
||||
export function getFeaturedModelMaxIndex(showBrowseAll = true): number {
|
||||
const featuredModels = getAllFeaturedModels()
|
||||
export function getFeaturedModelMaxIndex(featuredModels: FeaturedModel[], showBrowseAll = true): number {
|
||||
return showBrowseAll ? featuredModels.length : featuredModels.length - 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the selected index is the "Browse all" option
|
||||
*/
|
||||
export function isBrowseAllSelected(selectedIndex: number): boolean {
|
||||
const featuredModels = getAllFeaturedModels()
|
||||
export function isBrowseAllSelected(selectedIndex: number, featuredModels: FeaturedModel[]): boolean {
|
||||
return selectedIndex === featuredModels.length
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the featured model at the given index, or null if "Browse all" is selected
|
||||
*/
|
||||
export function getFeaturedModelAtIndex(index: number): FeaturedModel | null {
|
||||
const featuredModels = getAllFeaturedModels()
|
||||
export function getFeaturedModelAtIndex(index: number, featuredModels: FeaturedModel[]): FeaturedModel | null {
|
||||
if (index >= 0 && index < featuredModels.length) {
|
||||
return featuredModels[index]
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import { supportsReasoningEffortForModel } from "@/utils/model-utils"
|
||||
import { version as CLI_VERSION } from "../../package.json"
|
||||
import { COLORS } from "../constants/colors"
|
||||
import { useStdinContext } from "../context/StdinContext"
|
||||
import { useClineFeaturedModels } from "../hooks/useClineFeaturedModels"
|
||||
import { useOcaAuth } from "../hooks/useOcaAuth"
|
||||
import { isMouseEscapeSequence } from "../utils/input"
|
||||
import { applyBedrockConfig, applyProviderConfig } from "../utils/provider-config"
|
||||
@@ -161,6 +162,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
|
||||
)
|
||||
const [isPickingFeaturedModel, setIsPickingFeaturedModel] = useState(initialMode === "featured-models")
|
||||
const [featuredModelIndex, setFeaturedModelIndex] = useState(0)
|
||||
const featuredModels = useClineFeaturedModels()
|
||||
const [isPickingProvider, setIsPickingProvider] = useState(false)
|
||||
const [isPickingLanguage, setIsPickingLanguage] = useState(false)
|
||||
const [isEnteringApiKey, setIsEnteringApiKey] = useState(false)
|
||||
@@ -1292,7 +1294,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
|
||||
|
||||
// Featured model picker mode (Cline provider)
|
||||
if (isPickingFeaturedModel) {
|
||||
const maxIndex = getFeaturedModelMaxIndex()
|
||||
const maxIndex = getFeaturedModelMaxIndex(featuredModels)
|
||||
|
||||
if (key.escape) {
|
||||
setIsPickingFeaturedModel(false)
|
||||
@@ -1306,12 +1308,12 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
|
||||
} else if (key.downArrow) {
|
||||
setFeaturedModelIndex((prev) => (prev < maxIndex ? prev + 1 : 0))
|
||||
} else if (key.return) {
|
||||
if (isBrowseAllSelected(featuredModelIndex)) {
|
||||
if (isBrowseAllSelected(featuredModelIndex, featuredModels)) {
|
||||
// Switch to full ModelPicker
|
||||
setIsPickingFeaturedModel(false)
|
||||
setIsPickingModel(true)
|
||||
} else {
|
||||
const selectedModel = getFeaturedModelAtIndex(featuredModelIndex)
|
||||
const selectedModel = getFeaturedModelAtIndex(featuredModelIndex, featuredModels)
|
||||
if (selectedModel && pickingModelKey) {
|
||||
handleModelSelect(selectedModel.id)
|
||||
setIsPickingFeaturedModel(false)
|
||||
@@ -1522,6 +1524,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
|
||||
const label = pickingModelKey === "actModelId" ? "Model ID (Act)" : "Model ID (Plan)"
|
||||
return (
|
||||
<FeaturedModelPicker
|
||||
featuredModels={featuredModels}
|
||||
helpText="Arrows to navigate, Enter to select, Esc to cancel"
|
||||
selectedIndex={featuredModelIndex}
|
||||
title={`Select: ${label}`}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { getAllFeaturedModels } from "./featured-models"
|
||||
import { getAllFeaturedModels, mapRecommendedModelsToFeaturedModels } from "./featured-models"
|
||||
|
||||
describe("featured models", () => {
|
||||
it("includes display names for all featured models", () => {
|
||||
@@ -9,4 +9,15 @@ describe("featured models", () => {
|
||||
expect(model.name).toBeTruthy()
|
||||
}
|
||||
})
|
||||
|
||||
it("fills free model metadata from fallback when upstream payload is sparse", () => {
|
||||
const models = mapRecommendedModelsToFeaturedModels({
|
||||
recommended: [],
|
||||
free: [{ id: "trinity-large-preview:free", name: "trinity-large-preview:free", description: "", tags: [] }],
|
||||
})
|
||||
|
||||
expect(models.free[0]?.name).toBe("Arcee AI Trinity Large Preview")
|
||||
expect(models.free[0]?.description).toBe("Arcee AI's advanced large preview model in the Trinity series")
|
||||
expect(models.free[0]?.labels).toContain("FREE")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Featured models shown in the Cline model picker during onboarding
|
||||
* These are curated models that work well with Cline
|
||||
*/
|
||||
import { CLINE_RECOMMENDED_MODELS_FALLBACK } from "@shared/cline/recommended-models"
|
||||
|
||||
export interface FeaturedModel {
|
||||
id: string
|
||||
@@ -10,61 +11,81 @@ export interface FeaturedModel {
|
||||
labels: string[]
|
||||
}
|
||||
|
||||
export const FEATURED_MODELS: { recommended: FeaturedModel[]; free: FeaturedModel[] } = {
|
||||
recommended: [
|
||||
{
|
||||
id: "google/gemini-3.1-pro-preview",
|
||||
name: "Gemini 3.1 Pro Preview",
|
||||
description: "Latest Gemini release with 1m ctx window and strong coding performance",
|
||||
labels: ["NEW"],
|
||||
},
|
||||
{
|
||||
id: "anthropic/claude-sonnet-4.6",
|
||||
name: "Claude Sonnet 4.6",
|
||||
description: "Latest Sonnet release with strong coding and agent performance",
|
||||
labels: ["NEW"],
|
||||
},
|
||||
{
|
||||
id: "anthropic/claude-opus-4.6",
|
||||
name: "Claude Opus 4.6",
|
||||
description: "Most intelligent model for agents and coding",
|
||||
labels: ["BEST"],
|
||||
},
|
||||
{
|
||||
id: "openai/gpt-5.2-codex",
|
||||
name: "GPT 5.2 Codex",
|
||||
description: "OpenAI's latest with strong coding abilities",
|
||||
labels: ["HOT"],
|
||||
},
|
||||
],
|
||||
free: [
|
||||
{
|
||||
id: "minimax/minimax-m2.5",
|
||||
name: "MiniMax M2.5",
|
||||
description: "MiniMax-M2.5 is a lightweight, state-of-the-art LLM optimized for coding and agentic workflows",
|
||||
labels: ["FREE"],
|
||||
},
|
||||
{
|
||||
id: "z-ai/glm-5",
|
||||
name: "Z-AI GLM5",
|
||||
description: "Z.AI's latest GLM 5 model with strong coding and agent performance",
|
||||
labels: ["FREE"],
|
||||
},
|
||||
{
|
||||
id: "kwaipilot/kat-coder-pro",
|
||||
name: "KAT Coder Pro",
|
||||
description: "KwaiKAT's most advanced agentic coding model in the KAT-Coder series",
|
||||
labels: ["FREE"],
|
||||
},
|
||||
{
|
||||
id: "arcee-ai/trinity-large-preview:free",
|
||||
name: "Trinity Large Preview",
|
||||
description: "Arcee AI's advanced large preview model in the Trinity series",
|
||||
labels: ["FREE"],
|
||||
},
|
||||
],
|
||||
type RecommendedModelLike = {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
tags: string[]
|
||||
}
|
||||
|
||||
export function getAllFeaturedModels(): FeaturedModel[] {
|
||||
return [...FEATURED_MODELS.recommended, ...FEATURED_MODELS.free]
|
||||
export interface FeaturedModelsByTier {
|
||||
recommended: FeaturedModel[]
|
||||
free: FeaturedModel[]
|
||||
}
|
||||
|
||||
interface RecommendedModelsByTier {
|
||||
recommended: RecommendedModelLike[]
|
||||
free: RecommendedModelLike[]
|
||||
}
|
||||
|
||||
function toFeaturedModel(model: RecommendedModelLike): FeaturedModel {
|
||||
return {
|
||||
id: model.id,
|
||||
name: model.name,
|
||||
description: model.description,
|
||||
labels: model.tags,
|
||||
}
|
||||
}
|
||||
|
||||
function getModelIdSuffix(id: string): string {
|
||||
const lastSlashIndex = id.lastIndexOf("/")
|
||||
return lastSlashIndex >= 0 ? id.slice(lastSlashIndex + 1) : id
|
||||
}
|
||||
|
||||
function findFallbackFeaturedModelById(models: FeaturedModel[], id: string): FeaturedModel | undefined {
|
||||
const idSuffix = getModelIdSuffix(id)
|
||||
return models.find((model) => model.id === id || getModelIdSuffix(model.id) === idSuffix)
|
||||
}
|
||||
|
||||
function mapRecommendedModelToFeaturedModelWithFallback(
|
||||
model: RecommendedModelLike,
|
||||
fallbackModels: FeaturedModel[],
|
||||
defaultLabels: string[] = [],
|
||||
): FeaturedModel {
|
||||
const fallbackModel = findFallbackFeaturedModelById(fallbackModels, model.id)
|
||||
const upstreamNameLooksLikeFallback = model.name === model.id || model.name.trim().length === 0
|
||||
const name = upstreamNameLooksLikeFallback ? (fallbackModel?.name ?? model.name) : model.name
|
||||
const description = model.description.trim().length > 0 ? model.description : (fallbackModel?.description ?? "")
|
||||
const labels = model.tags.length > 0 ? model.tags : (fallbackModel?.labels ?? defaultLabels)
|
||||
|
||||
return {
|
||||
id: model.id,
|
||||
name,
|
||||
description,
|
||||
labels,
|
||||
}
|
||||
}
|
||||
|
||||
export const FEATURED_MODELS: FeaturedModelsByTier = {
|
||||
recommended: CLINE_RECOMMENDED_MODELS_FALLBACK.recommended.map(toFeaturedModel),
|
||||
free: CLINE_RECOMMENDED_MODELS_FALLBACK.free.map(toFeaturedModel),
|
||||
}
|
||||
|
||||
export function getAllFeaturedModels(modelsByTier: FeaturedModelsByTier = FEATURED_MODELS): FeaturedModel[] {
|
||||
return [...modelsByTier.recommended, ...modelsByTier.free]
|
||||
}
|
||||
|
||||
export function mapRecommendedModelsToFeaturedModels(data: RecommendedModelsByTier): FeaturedModelsByTier {
|
||||
return {
|
||||
recommended: data.recommended.map((model) =>
|
||||
mapRecommendedModelToFeaturedModelWithFallback(model, FEATURED_MODELS.recommended),
|
||||
),
|
||||
free: data.free.map((model) => mapRecommendedModelToFeaturedModelWithFallback(model, FEATURED_MODELS.free, ["FREE"])),
|
||||
}
|
||||
}
|
||||
|
||||
export function withFeaturedModelFallback(modelsByTier: FeaturedModelsByTier): FeaturedModelsByTier {
|
||||
const recommended = modelsByTier.recommended.length > 0 ? modelsByTier.recommended : FEATURED_MODELS.recommended
|
||||
const free = modelsByTier.free.length > 0 ? modelsByTier.free : FEATURED_MODELS.free
|
||||
return { recommended, free }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Cline Library Exports
|
||||
*
|
||||
* This file exports the public API for programmatic use of Cline.
|
||||
* Use these classes and types to embed Cline into your applications.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { ClineAgent } from "cline"
|
||||
*
|
||||
* const agent = new ClineAgent()
|
||||
* await agent.initialize({ clientCapabilities: {} })
|
||||
* const session = await agent.newSession({ cwd: process.cwd() })
|
||||
* ```
|
||||
* @module cline
|
||||
*/
|
||||
|
||||
export { ClineAgent } from "./agent/ClineAgent.js"
|
||||
export { ClineSessionEmitter } from "./agent/ClineSessionEmitter.js"
|
||||
export type {
|
||||
AcpAgentOptions,
|
||||
AcpSessionState,
|
||||
AcpSessionStatus,
|
||||
Agent,
|
||||
AgentSideConnection,
|
||||
AudioContent,
|
||||
CancelNotification,
|
||||
ClientCapabilities,
|
||||
ClineAcpSession,
|
||||
ClineAgentCapabilities,
|
||||
ClineAgentInfo,
|
||||
ClineAgentOptions,
|
||||
ClinePermissionOption,
|
||||
ClineSessionEvents,
|
||||
ContentBlock,
|
||||
ImageContent,
|
||||
InitializeRequest,
|
||||
InitializeResponse,
|
||||
LoadSessionRequest,
|
||||
LoadSessionResponse,
|
||||
McpServer,
|
||||
ModelInfo,
|
||||
NewSessionRequest,
|
||||
NewSessionResponse,
|
||||
PermissionHandler,
|
||||
PermissionOption,
|
||||
PermissionOptionKind,
|
||||
PromptRequest,
|
||||
PromptResponse,
|
||||
RequestPermissionRequest,
|
||||
RequestPermissionResponse,
|
||||
SessionConfigOption,
|
||||
SessionModelState,
|
||||
SessionNotification,
|
||||
SessionUpdate,
|
||||
SessionUpdatePayload,
|
||||
SessionUpdateType,
|
||||
SetSessionConfigOptionRequest,
|
||||
SetSessionConfigOptionResponse,
|
||||
SetSessionModelRequest,
|
||||
SetSessionModelResponse,
|
||||
SetSessionModeRequest,
|
||||
SetSessionModeResponse,
|
||||
StopReason,
|
||||
TextContent,
|
||||
ToolCall,
|
||||
ToolCallStatus,
|
||||
ToolCallUpdate,
|
||||
ToolKind,
|
||||
TranslatedMessage,
|
||||
} from "./agent/public-types.js"
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import { refreshClineRecommendedModels } from "@/core/controller/models/refreshClineRecommendedModels"
|
||||
import {
|
||||
type FeaturedModel,
|
||||
getAllFeaturedModels,
|
||||
mapRecommendedModelsToFeaturedModels,
|
||||
withFeaturedModelFallback,
|
||||
} from "../constants/featured-models"
|
||||
|
||||
export function useClineFeaturedModels(): FeaturedModel[] {
|
||||
const [featuredModels, setFeaturedModels] = useState<FeaturedModel[]>(() => getAllFeaturedModels())
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
void (async () => {
|
||||
try {
|
||||
const recommendedModels = await refreshClineRecommendedModels()
|
||||
const mappedModels = mapRecommendedModelsToFeaturedModels(recommendedModels)
|
||||
const modelsWithFallback = withFeaturedModelFallback(mappedModels)
|
||||
if (!cancelled) {
|
||||
setFeaturedModels(getAllFeaturedModels(modelsWithFallback))
|
||||
}
|
||||
} catch {
|
||||
// Keep local fallback models on error.
|
||||
}
|
||||
})()
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [])
|
||||
|
||||
return featuredModels
|
||||
}
|
||||
+7
-64
@@ -20,7 +20,7 @@ import { PostHogClientProvider } from "@/services/telemetry/providers/posthog/Po
|
||||
import { HistoryItem } from "@/shared/HistoryItem"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { Session } from "@/shared/services/Session"
|
||||
import { getProviderModelIdKey, ProviderToApiKeyMap } from "@/shared/storage"
|
||||
import { getProviderModelIdKey } from "@/shared/storage"
|
||||
import { isOpenaiReasoningEffort, OPENAI_REASONING_EFFORT_OPTIONS, type OpenaiReasoningEffort } from "@/shared/storage/types"
|
||||
import { version as CLI_VERSION } from "../package.json"
|
||||
import { runAcpMode } from "./acp/index.js"
|
||||
@@ -29,7 +29,8 @@ import { checkRawModeSupport } from "./context/StdinContext"
|
||||
import { createCliHostBridgeProvider } from "./controllers"
|
||||
import { CliCommentReviewController } from "./controllers/CliCommentReviewController"
|
||||
import { CliWebviewProvider } from "./controllers/CliWebviewProvider"
|
||||
import { restoreConsole } from "./utils/console"
|
||||
import { isAuthConfigured } from "./utils/auth"
|
||||
import { restoreConsole, suppressConsoleUnlessVerbose } from "./utils/console"
|
||||
import { printInfo, printWarning } from "./utils/display"
|
||||
import { selectOutputMode } from "./utils/mode-selection"
|
||||
import { parseImagesFromInput, processImagePaths } from "./utils/parser"
|
||||
@@ -42,6 +43,10 @@ import { autoUpdateOnStartup, checkForUpdates } from "./utils/update"
|
||||
import { initializeCliContext } from "./vscode-context"
|
||||
import { CLI_LOG_FILE, shutdownEvent, window } from "./vscode-shim"
|
||||
|
||||
// CLI-only behavior: suppress console output unless verbose mode is enabled.
|
||||
// Kept explicit here so importing the library bundle does not mutate global console methods.
|
||||
suppressConsoleUnlessVerbose()
|
||||
|
||||
/**
|
||||
* Common options shared between runTask and resumeTask
|
||||
*/
|
||||
@@ -790,68 +795,6 @@ devCommand
|
||||
await openExternal(CLI_LOG_FILE)
|
||||
})
|
||||
|
||||
/**
|
||||
* Check if the user has completed onboarding (has any provider configured).
|
||||
*
|
||||
* Uses `welcomeViewCompleted` as the single source of truth, matching the VS Code extension's approach.
|
||||
* If `welcomeViewCompleted` is undefined (first run), checks if ANY provider has credentials
|
||||
* and sets the flag accordingly.
|
||||
*/
|
||||
export async function isAuthConfigured(): Promise<boolean> {
|
||||
const stateManager = StateManager.get()
|
||||
|
||||
// Check welcomeViewCompleted first - this is the single source of truth
|
||||
const welcomeViewCompleted = stateManager.getGlobalStateKey("welcomeViewCompleted")
|
||||
if (welcomeViewCompleted !== undefined) {
|
||||
return welcomeViewCompleted
|
||||
}
|
||||
|
||||
// welcomeViewCompleted is undefined - run migration logic to check if ANY provider has credentials
|
||||
// This mirrors the extension's migrateWelcomeViewCompleted behavior
|
||||
const hasAnyAuth = await checkAnyProviderConfigured()
|
||||
|
||||
// Set welcomeViewCompleted based on what we found
|
||||
stateManager.setGlobalState("welcomeViewCompleted", hasAnyAuth)
|
||||
await stateManager.flushPendingState()
|
||||
|
||||
return hasAnyAuth
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if ANY provider has valid credentials configured.
|
||||
* Used for migration when welcomeViewCompleted is undefined.
|
||||
*/
|
||||
async function checkAnyProviderConfigured(): Promise<boolean> {
|
||||
const stateManager = StateManager.get()
|
||||
const config = stateManager.getApiConfiguration() as Record<string, unknown>
|
||||
|
||||
// Check Cline account (stored as "cline:clineAccountId" in secrets, loaded into config)
|
||||
if (config["clineApiKey"] || config["cline:clineAccountId"]) return true
|
||||
|
||||
// Check OpenAI Codex OAuth (stored in SECRETS_KEYS, loaded into config)
|
||||
if (config["openai-codex-oauth-credentials"]) return true
|
||||
|
||||
// Check all BYO provider API keys (loaded into config from secrets)
|
||||
for (const [provider, keyField] of Object.entries(ProviderToApiKeyMap)) {
|
||||
// Skip cline - already checked above with the correct key
|
||||
if (provider === "cline") continue
|
||||
|
||||
const fields = Array.isArray(keyField) ? keyField : [keyField]
|
||||
for (const field of fields) {
|
||||
if (config[field]) return true
|
||||
}
|
||||
}
|
||||
|
||||
// Check provider-specific settings that indicate configuration
|
||||
// (for providers that don't require API keys like Bedrock with IAM, Ollama, LM Studio)
|
||||
if (config.awsRegion) return true
|
||||
if (config.vertexProjectId) return true
|
||||
if (config.ollamaBaseUrl) return true
|
||||
if (config.lmStudioBaseUrl) return true
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that a task exists in history
|
||||
* @returns The task history item if found, null otherwise
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { describe, expect, it } from "vitest"
|
||||
|
||||
describe("library import side effects", () => {
|
||||
it("importing library exports must not mutate console.log", async () => {
|
||||
const originalConsoleLog = console.log
|
||||
await import("./exports")
|
||||
expect(console.log).toBe(originalConsoleLog)
|
||||
}, 30000)
|
||||
})
|
||||
@@ -0,0 +1,64 @@
|
||||
import { StateManager } from "@/core/storage/StateManager"
|
||||
import { ProviderToApiKeyMap } from "@/shared/storage"
|
||||
|
||||
/**
|
||||
* Check if the user has completed onboarding (has any provider configured).
|
||||
*
|
||||
* Uses `welcomeViewCompleted` as the single source of truth, matching the VS Code extension's approach.
|
||||
* If `welcomeViewCompleted` is undefined (first run), checks if ANY provider has credentials
|
||||
* and sets the flag accordingly.
|
||||
*/
|
||||
export async function isAuthConfigured(): Promise<boolean> {
|
||||
const stateManager = StateManager.get()
|
||||
|
||||
// Check welcomeViewCompleted first - this is the single source of truth
|
||||
const welcomeViewCompleted = stateManager.getGlobalStateKey("welcomeViewCompleted")
|
||||
if (welcomeViewCompleted !== undefined) {
|
||||
return welcomeViewCompleted
|
||||
}
|
||||
|
||||
// welcomeViewCompleted is undefined - run migration logic to check if ANY provider has credentials
|
||||
// This mirrors the extension's migrateWelcomeViewCompleted behavior
|
||||
const hasAnyAuth = await checkAnyProviderConfigured()
|
||||
|
||||
// Set welcomeViewCompleted based on what we found
|
||||
stateManager.setGlobalState("welcomeViewCompleted", hasAnyAuth)
|
||||
await stateManager.flushPendingState()
|
||||
|
||||
return hasAnyAuth
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if ANY provider has valid credentials configured.
|
||||
* Used for migration when welcomeViewCompleted is undefined.
|
||||
*/
|
||||
export async function checkAnyProviderConfigured(): Promise<boolean> {
|
||||
const stateManager = StateManager.get()
|
||||
const config = stateManager.getApiConfiguration() as Record<string, unknown>
|
||||
|
||||
// Check Cline account (stored as "cline:clineAccountId" in secrets, loaded into config)
|
||||
if (config["clineApiKey"] || config["cline:clineAccountId"]) return true
|
||||
|
||||
// Check OpenAI Codex OAuth (stored in SECRETS_KEYS, loaded into config)
|
||||
if (config["openai-codex-oauth-credentials"]) return true
|
||||
|
||||
// Check all BYO provider API keys (loaded into config from secrets)
|
||||
for (const [provider, keyField] of Object.entries(ProviderToApiKeyMap)) {
|
||||
// Skip cline - already checked above with the correct key
|
||||
if (provider === "cline") continue
|
||||
|
||||
const fields = Array.isArray(keyField) ? keyField : [keyField]
|
||||
for (const field of fields) {
|
||||
if (config[field]) return true
|
||||
}
|
||||
}
|
||||
|
||||
// Check provider-specific settings that indicate configuration
|
||||
// (for providers that don't require API keys like Bedrock with IAM, Ollama, LM Studio)
|
||||
if (config.awsRegion) return true
|
||||
if (config.vertexProjectId) return true
|
||||
if (config.ollamaBaseUrl) return true
|
||||
if (config.lmStudioBaseUrl) return true
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -12,11 +12,19 @@ export const originalConsoleWarn = console.warn.bind(console)
|
||||
export const originalConsoleInfo = console.info.bind(console)
|
||||
export const originalConsoleDebug = console.debug.bind(console)
|
||||
|
||||
// Check for verbose flag early (before commander parses)
|
||||
const isVerbose = process.argv.includes("-v") || process.argv.includes("--verbose")
|
||||
/**
|
||||
* Suppress console output unless verbose mode is enabled.
|
||||
*
|
||||
* This is intentionally opt-in and should only be called by the CLI entrypoint.
|
||||
* Library consumers should not have their global console methods mutated as a
|
||||
* side effect of importing the library bundle.
|
||||
*/
|
||||
export function suppressConsoleUnlessVerbose(argv: string[] = process.argv) {
|
||||
const isVerbose = argv.includes("-v") || argv.includes("--verbose")
|
||||
if (isVerbose) {
|
||||
return
|
||||
}
|
||||
|
||||
// Suppress console output unless verbose mode
|
||||
if (!isVerbose) {
|
||||
console.log = () => {}
|
||||
console.warn = () => {}
|
||||
console.error = () => {}
|
||||
|
||||
@@ -7,6 +7,8 @@ import type { ApiProvider } from "@shared/api"
|
||||
import { getProviderModelIdKey, ProviderToApiKeyMap } from "@shared/storage"
|
||||
import { buildApiHandler } from "@/core/api"
|
||||
import type { Controller } from "@/core/controller"
|
||||
import { refreshOpenRouterModels } from "@/core/controller/models/refreshOpenRouterModels"
|
||||
import { refreshVercelAiGatewayModels } from "@/core/controller/models/refreshVercelAiGatewayModels"
|
||||
import { StateManager } from "@/core/storage/StateManager"
|
||||
import type { BedrockConfig } from "../components/BedrockSetup"
|
||||
import { getDefaultModelId } from "../components/ModelPicker"
|
||||
@@ -40,14 +42,22 @@ export async function applyProviderConfig(options: ApplyProviderConfigOptions):
|
||||
if (actModelKey) config[actModelKey] = finalModelId
|
||||
if (planModelKey) config[planModelKey] = finalModelId
|
||||
|
||||
// For cline/openrouter, also set model info (required for getModel() to return correct model)
|
||||
// Fetch model info from the provider API (not just disk cache) so headless
|
||||
// CLI auth gets correct maxTokens, thinkingConfig, etc.
|
||||
if ((providerId === "cline" || providerId === "openrouter") && controller) {
|
||||
const openRouterModels = await controller.readOpenRouterModels()
|
||||
const openRouterModels = await refreshOpenRouterModels(controller)
|
||||
const modelInfo = openRouterModels?.[finalModelId]
|
||||
if (modelInfo) {
|
||||
stateManager.setGlobalState("actModeOpenRouterModelInfo", modelInfo)
|
||||
stateManager.setGlobalState("planModeOpenRouterModelInfo", modelInfo)
|
||||
}
|
||||
} else if (providerId === "vercel-ai-gateway" && controller) {
|
||||
const vercelModels = await refreshVercelAiGatewayModels(controller)
|
||||
const modelInfo = vercelModels?.[finalModelId]
|
||||
if (modelInfo) {
|
||||
stateManager.setGlobalState("actModeVercelAiGatewayModelInfo", modelInfo)
|
||||
stateManager.setGlobalState("planModeVercelAiGatewayModelInfo", modelInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"declarationMap": false,
|
||||
"noCheck": true,
|
||||
"noResolve": true,
|
||||
"outDir": "dist/types"
|
||||
},
|
||||
"include": [
|
||||
"src/exports.ts",
|
||||
"src/agent/public-types.ts",
|
||||
"src/agent/ClineAgent.ts",
|
||||
"src/agent/ClineSessionEmitter.ts",
|
||||
"src/agent/types.ts",
|
||||
"src/agent/messageTranslator.ts",
|
||||
"src/agent/permissionHandler.ts"
|
||||
]
|
||||
}
|
||||
+18
-1
@@ -5,11 +5,28 @@ export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: "node",
|
||||
include: ["src/**/*.test.{ts,tsx}", "tests/**/*.test.{ts,tsx}"],
|
||||
coverage: {
|
||||
reporter: ["text", "json", "html"],
|
||||
exclude: ["node_modules/", "dist/"],
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
extends: true,
|
||||
test: {
|
||||
name: "unit",
|
||||
include: ["src/**/*.test.{ts,tsx}", "tests/**/*.test.{ts,tsx}"],
|
||||
exclude: ["src/**/*.markdown.test.tsx"],
|
||||
},
|
||||
},
|
||||
{
|
||||
extends: true,
|
||||
test: {
|
||||
name: "markdown",
|
||||
include: ["src/**/*.markdown.test.tsx"],
|
||||
env: { FORCE_COLOR: "3" },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||
@@ -1,535 +0,0 @@
|
||||
---
|
||||
title: "CVE Vulnerability Scanner"
|
||||
description: "Automatically scan dependencies for CVEs and get AI-powered security reports using Cline CLI in GitHub Actions."
|
||||
---
|
||||
|
||||
Turn noisy dependency audit output into actionable, prioritized security intelligence. This sample uses Cline CLI in GitHub Actions to scan for CVEs automatically — on every PR, on a weekly schedule, or on-demand — and post clear, prioritized reports with exact fix commands.
|
||||
|
||||
<Note>
|
||||
**New to Cline CLI?** This sample assumes you understand Cline CLI basics and have completed the [Installation Guide](https://docs.cline.bot/cline-cli/installation). Start with the [GitHub RCA sample](./github-issue-rca) if you're looking for something simpler.
|
||||
</Note>
|
||||
|
||||
## What It Does
|
||||
|
||||
| Trigger | What happens |
|
||||
|---------|-------------|
|
||||
| **PR opened** (dependency files changed) | Scans for CVEs, posts analysis as a PR comment |
|
||||
| **Weekly schedule** (Monday 9am UTC) | Scans for newly disclosed CVEs, creates a GitHub Issue |
|
||||
| **Manual trigger** | Scan with custom severity filter and optional auto-fix |
|
||||
|
||||
For each vulnerability found, Cline provides:
|
||||
- **Plain-English impact** — what an attacker could actually do
|
||||
- **Exploitability assessment** — is this theoretical or actively exploited?
|
||||
- **Exact fix commands** — copy-paste remediation
|
||||
- **Auto-fix safety** — which fixes are safe to apply without breaking changes
|
||||
|
||||
## Quick Start — Local Usage
|
||||
|
||||
Before setting up CI/CD, try it locally:
|
||||
|
||||
```bash
|
||||
# Download the script
|
||||
curl -O https://raw.githubusercontent.com/cline/cline/main/src/samples/cli/cve-scan/scan-cves.sh
|
||||
chmod +x scan-cves.sh
|
||||
|
||||
# Run it (auto-detects npm/yarn/pnpm/pip)
|
||||
./scan-cves.sh
|
||||
```
|
||||
|
||||
Or skip the script and pipe directly:
|
||||
|
||||
```bash
|
||||
npm audit --json | cline --yolo "Analyze these CVEs. For each: explain impact, assess exploitability, give exact fix commands. Prioritize by severity."
|
||||
```
|
||||
|
||||
<Tip>
|
||||
The `--yolo` flag (or `-y` for short) runs Cline in fully autonomous mode — it executes commands without waiting for approval. This is what makes piping and CI/CD workflows possible.
|
||||
</Tip>
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Cline CLI** installed and authenticated ([Installation Guide](https://docs.cline.bot/cline-cli/installation))
|
||||
- **GitHub repository** with Actions enabled
|
||||
- **API provider account** (Anthropic, OpenRouter, etc.) with API key added as a repository secret
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Copy the Workflow File
|
||||
|
||||
```bash
|
||||
mkdir -p .github/workflows
|
||||
curl -o .github/workflows/cline-cve-scan.yml \
|
||||
https://raw.githubusercontent.com/cline/cline/main/src/samples/cli/cve-scan/cline-cve-scan.yml
|
||||
```
|
||||
|
||||
<Accordion title="Click to view the complete cline-cve-scan.yml workflow">
|
||||
```yaml
|
||||
name: Cline CVE Scanner
|
||||
|
||||
on:
|
||||
# Weekly scheduled scan — catches new CVEs in existing dependencies
|
||||
schedule:
|
||||
- cron: "0 9 * * 1" # Every Monday at 9am UTC
|
||||
|
||||
# PR scan — catch vulnerable dependencies before they merge
|
||||
pull_request:
|
||||
types: [opened, synchronize, ready_for_review]
|
||||
paths:
|
||||
- "package.json"
|
||||
- "package-lock.json"
|
||||
- "yarn.lock"
|
||||
- "pnpm-lock.yaml"
|
||||
- "requirements.txt"
|
||||
- "Pipfile.lock"
|
||||
- "pyproject.toml"
|
||||
|
||||
# Manual trigger with options
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
severity:
|
||||
description: "Minimum severity to report"
|
||||
required: false
|
||||
default: "all"
|
||||
type: choice
|
||||
options:
|
||||
- all
|
||||
- low
|
||||
- medium
|
||||
- high
|
||||
- critical
|
||||
auto_fix:
|
||||
description: "Attempt safe auto-fixes"
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
concurrency:
|
||||
group: cve-scan-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
cve-scan:
|
||||
if: |
|
||||
(github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
|
||||
github.event_name == 'schedule' ||
|
||||
github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "npm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Install Cline CLI
|
||||
run: npm install -g cline
|
||||
|
||||
- name: Configure Cline Authentication
|
||||
run: |
|
||||
cline auth --provider anthropic \
|
||||
--apikey "${{ secrets.ANTHROPIC_API_KEY }}" \
|
||||
--modelid claude-sonnet-4-5-20250929
|
||||
|
||||
- name: Determine scan parameters
|
||||
id: params
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||
echo "severity=${{ inputs.severity }}" >> $GITHUB_OUTPUT
|
||||
echo "auto_fix=${{ inputs.auto_fix }}" >> $GITHUB_OUTPUT
|
||||
elif [ "${{ github.event_name }}" == "pull_request" ]; then
|
||||
echo "severity=high" >> $GITHUB_OUTPUT
|
||||
echo "auto_fix=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "severity=all" >> $GITHUB_OUTPUT
|
||||
echo "auto_fix=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
if [ "${{ github.event_name }}" == "pull_request" ]; then
|
||||
echo "output=pr-comment" >> $GITHUB_OUTPUT
|
||||
echo "pr_number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "output=github-issue" >> $GITHUB_OUTPUT
|
||||
echo "pr_number=" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Download CVE scan script
|
||||
run: |
|
||||
curl -sL https://raw.githubusercontent.com/${{ github.repository }}/main/scan-cves.sh -o scan-cves.sh \
|
||||
|| cp src/samples/cli/cve-scan/scan-cves.sh scan-cves.sh 2>/dev/null \
|
||||
|| true
|
||||
chmod +x scan-cves.sh
|
||||
|
||||
- name: Run CVE scan with Cline
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
CLINE_COMMAND_PERMISSIONS: |
|
||||
{
|
||||
"allow": [
|
||||
"npm audit *",
|
||||
"yarn audit *",
|
||||
"pnpm audit *",
|
||||
"pip-audit *",
|
||||
"gh issue create *",
|
||||
"gh issue list *",
|
||||
"gh pr comment *",
|
||||
"cat *",
|
||||
"echo *"
|
||||
],
|
||||
"deny": [
|
||||
"rm *",
|
||||
"sudo *",
|
||||
"npm install *",
|
||||
"npm publish *"
|
||||
]
|
||||
}
|
||||
run: |
|
||||
PR_FLAG=""
|
||||
if [ -n "${{ steps.params.outputs.pr_number }}" ]; then
|
||||
PR_FLAG="--pr ${{ steps.params.outputs.pr_number }}"
|
||||
fi
|
||||
|
||||
AUTO_FIX_FLAG=""
|
||||
if [ "${{ steps.params.outputs.auto_fix }}" == "true" ]; then
|
||||
AUTO_FIX_FLAG="--auto-fix"
|
||||
fi
|
||||
|
||||
./scan-cves.sh \
|
||||
--scanner npm \
|
||||
--output ${{ steps.params.outputs.output }} \
|
||||
--severity ${{ steps.params.outputs.severity }} \
|
||||
$PR_FLAG \
|
||||
$AUTO_FIX_FLAG
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
### 2. Add the Scan Script
|
||||
|
||||
Add `scan-cves.sh` to your repository root (or wherever the workflow downloads it from):
|
||||
|
||||
```bash
|
||||
curl -O https://raw.githubusercontent.com/cline/cline/main/src/samples/cli/cve-scan/scan-cves.sh
|
||||
chmod +x scan-cves.sh
|
||||
```
|
||||
|
||||
<Accordion title="Click to view scan-cves.sh (simplified — see source for full version)">
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# scan-cves.sh — CVE vulnerability scanner powered by Cline CLI
|
||||
#
|
||||
# Usage:
|
||||
# ./scan-cves.sh # Auto-detect scanner, stdout
|
||||
# ./scan-cves.sh --output github-issue # Post as GitHub Issue
|
||||
# ./scan-cves.sh --output pr-comment --pr 42 # Post as PR comment
|
||||
# ./scan-cves.sh --scanner npm --severity critical # Filter by severity
|
||||
# cat audit.json | ./scan-cves.sh --scanner custom # Custom scanner input
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCANNER=""
|
||||
OUTPUT="stdout"
|
||||
SEVERITY="all"
|
||||
PR_NUMBER=""
|
||||
REPO="${GITHUB_REPOSITORY:-}"
|
||||
AUTO_FIX="false"
|
||||
CLINE_EXTRA_FLAGS=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--scanner) SCANNER="$2"; shift 2 ;;
|
||||
--output) OUTPUT="$2"; shift 2 ;;
|
||||
--severity) SEVERITY="$2"; shift 2 ;;
|
||||
--pr) PR_NUMBER="$2"; shift 2 ;;
|
||||
--repo) REPO="$2"; shift 2 ;;
|
||||
--auto-fix) AUTO_FIX="true"; shift ;;
|
||||
--config) CLINE_EXTRA_FLAGS="$CLINE_EXTRA_FLAGS --config $2"; shift 2 ;;
|
||||
--model) CLINE_EXTRA_FLAGS="$CLINE_EXTRA_FLAGS -m $2"; shift 2 ;;
|
||||
-h|--help) echo "Usage: scan-cves.sh [--scanner npm|yarn|pnpm|pip|custom] [--output stdout|github-issue|pr-comment|file] [--severity all|critical|high|medium|low] [--pr N] [--repo owner/repo] [--auto-fix] [--config path] [--model id]"; exit 0 ;;
|
||||
*) echo "Unknown option: $1"; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Auto-detect scanner from lockfiles
|
||||
if [[ -z "$SCANNER" ]]; then
|
||||
if [[ -f "package-lock.json" ]]; then SCANNER="npm"
|
||||
elif [[ -f "yarn.lock" ]]; then SCANNER="yarn"
|
||||
elif [[ -f "pnpm-lock.yaml" ]]; then SCANNER="pnpm"
|
||||
elif [[ -f "requirements.txt" ]] || [[ -f "Pipfile.lock" ]]; then SCANNER="pip"
|
||||
else echo "Error: Could not detect package manager." >&2; exit 1; fi
|
||||
echo "Auto-detected scanner: $SCANNER" >&2
|
||||
fi
|
||||
|
||||
# Run the scan
|
||||
case "$SCANNER" in
|
||||
npm) SCAN_OUTPUT=$(npm audit --json 2>/dev/null || true) ;;
|
||||
yarn) SCAN_OUTPUT=$(yarn audit --json 2>/dev/null || true) ;;
|
||||
pnpm) SCAN_OUTPUT=$(pnpm audit --json 2>/dev/null || true) ;;
|
||||
pip) SCAN_OUTPUT=$(pip-audit --format json 2>/dev/null || true) ;;
|
||||
custom) SCAN_OUTPUT=$(cat) ;;
|
||||
*) echo "Unknown scanner: $SCANNER" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
if [[ -z "$SCAN_OUTPUT" ]]; then echo "✅ No vulnerabilities found!" >&2; exit 0; fi
|
||||
|
||||
# Build the security analyst prompt
|
||||
PROMPT='You are a senior security analyst. Analyze these vulnerability scan results.
|
||||
For EACH vulnerability: provide CVE ID, severity, affected package with versions,
|
||||
plain-English impact, exploitability assessment, exact fix commands, and auto-fix safety.
|
||||
Format as markdown with sections: 🔴 Critical, 🟠 High, 🟡 Medium, 🔵 Low,
|
||||
Summary & Recommended Actions, Risk Assessment.
|
||||
Omit empty severity sections. Flag actively exploited CVEs with ⚠️.'
|
||||
|
||||
if [[ "$SEVERITY" != "all" ]]; then
|
||||
PROMPT="$PROMPT Focus ONLY on $SEVERITY severity or higher."
|
||||
fi
|
||||
|
||||
# Run Cline analysis
|
||||
echo "Analyzing vulnerabilities with Cline..." >&2
|
||||
REPORT=$(echo "$SCAN_OUTPUT" | cline -y $CLINE_EXTRA_FLAGS "$PROMPT" 2>/dev/null)
|
||||
|
||||
# Output results
|
||||
case "$OUTPUT" in
|
||||
stdout) echo "$REPORT" ;;
|
||||
github-issue) gh issue create --repo "$REPO" --title "🔒 CVE Report — $(date +%Y-%m-%d)" --body "$REPORT" --label "security,automated" ;;
|
||||
pr-comment) gh pr comment "$PR_NUMBER" --repo "$REPO" --body "$REPORT" ;;
|
||||
file) echo "$REPORT" > "cve-report-$(date +%Y%m%d-%H%M%S).md" ;;
|
||||
esac
|
||||
```
|
||||
|
||||
The [full source script](https://github.com/cline/cline/blob/main/src/samples/cli/cve-scan/scan-cves.sh) includes additional features: a `--help` usage guide, `detect_scanner()` and `run_scan()` helper functions, a detailed heredoc security prompt with auto-fix instructions, and JSON output extraction via `jq`.
|
||||
</Accordion>
|
||||
|
||||
### 3. Configure Secrets
|
||||
|
||||
1. Go to your repository **Settings** → **Secrets and variables** → **Actions**
|
||||
2. Add a **New repository secret**:
|
||||
- **Name:** `ANTHROPIC_API_KEY` (or match the provider in your workflow)
|
||||
- **Value:** Your API key
|
||||
|
||||
### 4. Commit and Push
|
||||
|
||||
```bash
|
||||
git add .github/workflows/cline-cve-scan.yml scan-cves.sh
|
||||
git commit -m "Add Cline CVE scanner workflow"
|
||||
git push
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Automatic Triggers
|
||||
|
||||
Once set up, the scanner runs automatically:
|
||||
|
||||
- **Weekly (Monday 9am UTC):** Creates a GitHub Issue with a full vulnerability report
|
||||
- **On PR:** Posts a comment on PRs that modify dependency files (only high+ severity)
|
||||
|
||||
### Manual Trigger
|
||||
|
||||
Go to **Actions** → **Cline CVE Scanner** → **Run workflow** to trigger a scan with custom options:
|
||||
- Choose minimum severity level
|
||||
- Optionally enable auto-fix for safe updates
|
||||
|
||||
### Local Usage
|
||||
|
||||
```bash
|
||||
# Basic scan (auto-detects package manager)
|
||||
./scan-cves.sh
|
||||
|
||||
# Save to file
|
||||
./scan-cves.sh --output file
|
||||
|
||||
# Only critical CVEs
|
||||
./scan-cves.sh --severity critical
|
||||
|
||||
# Post as GitHub Issue
|
||||
./scan-cves.sh --output github-issue --repo myorg/myrepo
|
||||
|
||||
# Use a specific model
|
||||
./scan-cves.sh --model claude-opus-4-5-20251101
|
||||
|
||||
# Pipe from any scanner (Trivy, Snyk, Grype, etc.)
|
||||
trivy fs --format json . | ./scan-cves.sh --scanner custom
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
### Architecture
|
||||
|
||||
The scanner follows a three-layer design that keeps each concern separate and extensible:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Layer 1: Scanner Adapter (pluggable) │
|
||||
│ npm audit | yarn audit | pip-audit | custom │
|
||||
└────────────────────┬────────────────────────────┘
|
||||
│ JSON vulnerability data
|
||||
┌────────────────────▼────────────────────────────┐
|
||||
│ Layer 2: Cline Security Analyst (reusable) │
|
||||
│ AI-powered analysis via cline --yolo │
|
||||
└────────────────────┬────────────────────────────┘
|
||||
│ Markdown report
|
||||
┌────────────────────▼────────────────────────────┐
|
||||
│ Layer 3: Output Adapter (pluggable) │
|
||||
│ stdout | GitHub Issue | PR comment | file │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Layer 1 (Scanner)** runs the appropriate audit command and produces JSON. You can swap scanners without touching the analysis logic.
|
||||
|
||||
**Layer 2 (Cline)** receives the raw vulnerability JSON and produces a prioritized, human-readable report. The security analyst prompt is self-contained and could be extracted into a Prompts Library entry.
|
||||
|
||||
**Layer 3 (Output)** delivers the report to its destination. Adding a new output target (e.g., Slack webhook) requires only a few lines in the output case statement.
|
||||
|
||||
### The Security Analyst Prompt
|
||||
|
||||
The core prompt instructs Cline to act as a senior security analyst. For each CVE, it provides:
|
||||
|
||||
1. **CVE ID & Severity** with color-coded sections
|
||||
2. **Impact Assessment** in plain English (not just "RCE" — the actual attack vector)
|
||||
3. **Exploitability** — is this a real-world risk or theoretical?
|
||||
4. **Exact Fix** — copy-paste commands specific to your package manager
|
||||
5. **Auto-fix Safety** — whether a simple version bump is safe
|
||||
|
||||
This prompt is **reusable** — it works with any JSON vulnerability data, not just npm audit. It could be published to the Cline Prompts Library for broader use.
|
||||
|
||||
### Security: Command Permissions
|
||||
|
||||
The workflow uses `CLINE_COMMAND_PERMISSIONS` to restrict Cline to safe, read-only operations:
|
||||
|
||||
```json
|
||||
{
|
||||
"allow": ["npm audit *", "gh issue create *", "gh pr comment *"],
|
||||
"deny": ["rm *", "sudo *", "npm install *", "npm publish *"]
|
||||
}
|
||||
```
|
||||
|
||||
This ensures Cline can scan and report, but cannot modify your codebase or install packages — even in YOLO mode.
|
||||
|
||||
## Customization
|
||||
|
||||
### Different Package Managers
|
||||
|
||||
The script auto-detects from lockfiles, or you can specify explicitly:
|
||||
|
||||
```bash
|
||||
./scan-cves.sh --scanner yarn
|
||||
./scan-cves.sh --scanner pnpm
|
||||
./scan-cves.sh --scanner pip
|
||||
```
|
||||
|
||||
### Model Orchestration
|
||||
|
||||
Combine with [Model Orchestration](./model-orchestration) patterns for cost optimization:
|
||||
|
||||
```bash
|
||||
# Cheap model for weekly triage
|
||||
./scan-cves.sh --config ~/.cline-haiku --severity all
|
||||
|
||||
# Expensive model only for critical CVEs
|
||||
./scan-cves.sh --config ~/.cline-opus --severity critical
|
||||
```
|
||||
|
||||
### Custom Scanners
|
||||
|
||||
Pipe output from any scanner that produces JSON:
|
||||
|
||||
```bash
|
||||
# Trivy (container/filesystem scanner)
|
||||
trivy fs --format json . | ./scan-cves.sh --scanner custom
|
||||
|
||||
# Snyk
|
||||
snyk test --json | ./scan-cves.sh --scanner custom
|
||||
|
||||
# Grype
|
||||
grype dir:. -o json | ./scan-cves.sh --scanner custom
|
||||
```
|
||||
|
||||
### Slack Notifications
|
||||
|
||||
Extend the output adapter by piping stdout to a Slack webhook:
|
||||
|
||||
```bash
|
||||
REPORT=$(./scan-cves.sh)
|
||||
curl -X POST -H 'Content-type: application/json' \
|
||||
--data "{\"text\": \"$REPORT\"}" \
|
||||
"$SLACK_WEBHOOK_URL"
|
||||
```
|
||||
|
||||
## Sample Output
|
||||
|
||||
Here's an example of a Cline-generated CVE report:
|
||||
|
||||
```markdown
|
||||
# 🔒 CVE Vulnerability Report
|
||||
|
||||
**Scan Date:** 2026-02-11
|
||||
**Scanner:** npm
|
||||
**Total Vulnerabilities:** 4
|
||||
|
||||
## 🔴 Critical Vulnerabilities (1)
|
||||
|
||||
### CVE-2022-24999: qs
|
||||
- **Severity:** Critical
|
||||
- **Package:** `qs@6.7.0` → fix in `qs@6.11.0`
|
||||
- **Impact:** Prototype pollution via crafted query strings. An attacker can inject
|
||||
properties into Object.prototype, which in Express.js apps can lead to remote code
|
||||
execution or denial of service.
|
||||
- **Exploitability:** ⚠️ ACTIVELY EXPLOITED — public exploits available, any Express
|
||||
app using query parsing is vulnerable.
|
||||
- **Fix:** `npm install qs@6.11.0`
|
||||
- **Auto-fix safe:** Yes
|
||||
|
||||
## 🟠 High Vulnerabilities (2)
|
||||
|
||||
### CVE-2023-28155: jsonwebtoken
|
||||
- **Severity:** High
|
||||
- **Package:** `jsonwebtoken@8.5.1` → fix in `jsonwebtoken@9.0.0`
|
||||
- **Impact:** Insecure default algorithm allows an attacker to forge tokens if the
|
||||
server doesn't explicitly set the algorithm. Could lead to authentication bypass.
|
||||
- **Exploitability:** Medium — requires the server to not specify algorithms explicitly.
|
||||
- **Fix:** `npm install jsonwebtoken@9.0.0`
|
||||
- **Auto-fix safe:** No (major version bump, verify API compatibility)
|
||||
|
||||
### CVE-2023-45857: axios
|
||||
- **Severity:** High
|
||||
- **Package:** `axios@0.21.1` → fix in `axios@1.6.0`
|
||||
- **Impact:** SSRF vulnerability allows specially crafted requests to access internal
|
||||
services. An attacker controlling request URLs could probe internal infrastructure.
|
||||
- **Exploitability:** Medium — requires user-controlled URL input.
|
||||
- **Fix:** `npm install axios@1.6.0`
|
||||
- **Auto-fix safe:** No (major version bump)
|
||||
|
||||
## 📋 Summary & Recommended Actions
|
||||
|
||||
1. **Immediate:** Update qs to 6.11.0 — critical, actively exploited, safe auto-fix
|
||||
2. **This sprint:** Update jsonwebtoken to 9.0.0 and axios to 1.6.0 (test for breaking changes)
|
||||
3. **Safe auto-fix command:** `npm audit fix`
|
||||
|
||||
## 📊 Risk Assessment
|
||||
|
||||
This project has 1 critical and 2 high severity vulnerabilities. The critical qs
|
||||
vulnerability is actively exploited and should be fixed immediately — it's a safe
|
||||
auto-fix with no breaking changes. The jsonwebtoken and axios updates are major
|
||||
version bumps that require testing but should be scheduled for the current sprint.
|
||||
Overall dependency hygiene needs improvement — consider running automated CVE scans
|
||||
weekly to catch issues earlier.
|
||||
```
|
||||
|
||||
## Related Samples
|
||||
|
||||
- **[GitHub PR Review](./github-pr-review)** — Automated code review on PRs
|
||||
- **[GitHub Integration](./github-integration)** — Respond to issues with @cline
|
||||
- **[Model Orchestration](./model-orchestration)** — Multi-model workflows for cost optimization
|
||||
@@ -47,14 +47,6 @@ This section provides sample implementations that demonstrate various Cline CLI
|
||||
>
|
||||
Automatically review Pull Requests with AI. Configures Cline in GitHub Actions to analyze diffs, check for security issues, and post detailed reviews with inline code suggestions.
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="CVE Vulnerability Scanner (Actions)"
|
||||
icon="shield-halved"
|
||||
href="/cline-cli/samples/cve-scan"
|
||||
>
|
||||
Automatically scan dependencies for CVEs and get AI-powered security reports. Runs on PRs, weekly schedules, or on-demand. Supports npm, yarn, pnpm, pip, and custom scanners like Trivy and Snyk.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Additional Resources
|
||||
|
||||
@@ -0,0 +1,675 @@
|
||||
---
|
||||
title: "Cline SDK"
|
||||
description: "Embed Cline as a programmable coding agent in your Node.js applications using an ACP-compatible TypeScript API."
|
||||
---
|
||||
|
||||
# Cline SDK
|
||||
|
||||
The Cline SDK lets you embed Cline as a programmable coding agent in your Node.js applications. It exposes the same capabilities as the Cline CLI and VS Code extension — file editing, command execution, browser use, MCP servers — through a TypeScript API that conforms to the [Agent Client Protocol (ACP)](https://agentclientprotocol.com/protocol/schema).
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install cline
|
||||
```
|
||||
|
||||
If you want direct ACP type imports as well:
|
||||
|
||||
```bash
|
||||
npm install @agentclientprotocol/sdk
|
||||
```
|
||||
|
||||
Requires Node.js 20+.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```typescript
|
||||
import { ClineAgent } from "cline";
|
||||
|
||||
const CLINE_DIR = "/Users/username/.cline";
|
||||
const agent = new ClineAgent({ clineDir: CLINE_DIR });
|
||||
|
||||
// 1. Initialize — negotiates capabilities
|
||||
const initializeResponse = await agent.initialize({
|
||||
protocolVersion: 1,
|
||||
// these are the capabilities that the client (you) supports
|
||||
// The cline agent may or may not use them, but it needs to know about them to make informed decisions about what tools to use.
|
||||
clientCapabilities: {
|
||||
fs: { readTextFile: true, writeTextFile: true },
|
||||
terminal: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { agentInfo, authMethods } = initializeResponse;
|
||||
console.log("Agent info:", agentInfo); // contains things like agent name and version
|
||||
console.log("Auth methods:", authMethods); // contains a list of supported authentication methods. More auth methods coming soon
|
||||
|
||||
// 2. Authenticate if needed
|
||||
// If you skip this step, ClineAgent will look in CLINE_DIR for any existing credentials and authenticate with those
|
||||
await agent.authenticate({ methodId: "cline-oauth" });
|
||||
|
||||
// 3. Create a session.
|
||||
// A session represents a conversation or task with the agent. You can have multiple sessions for different tasks or conversations.
|
||||
const { sessionId } = await agent.newSession({
|
||||
cwd: process.cwd(),
|
||||
mcpServers: [], // mcpServers field not supported yet, but exposed here to maintain conformance with acp protocol
|
||||
});
|
||||
|
||||
// 4. Agent updates are sent via events. You can subscribe to these events to get real-time updates on the agent's progress, tool calls, and more.
|
||||
const emitter = agent.emitterForSession(sessionId);
|
||||
|
||||
emitter.on("agent_message_chunk", (payload) => {
|
||||
process.stdout.write(
|
||||
payload.content.type === "text"
|
||||
? payload.content.text
|
||||
: `[${payload.content.type}]`,
|
||||
);
|
||||
});
|
||||
emitter.on("agent_thought_chunk", (payload) => {
|
||||
process.stdout.write(
|
||||
payload.content.type === "text"
|
||||
? payload.content.text
|
||||
: `[${payload.content.type}]`,
|
||||
);
|
||||
});
|
||||
emitter.on("tool_call", (payload) => {
|
||||
console.log(`[tool] ${payload.title}`);
|
||||
});
|
||||
emitter.on("error", (err) => {
|
||||
console.error("[session error]", err);
|
||||
});
|
||||
|
||||
// 5. Send a prompt and wait for completion
|
||||
const { stopReason } = await agent.prompt({
|
||||
sessionId,
|
||||
prompt: [{ type: "text", text: "Create a hello world Express server" }],
|
||||
});
|
||||
|
||||
console.log("Done:", stopReason);
|
||||
|
||||
// 6. Clean up
|
||||
await agent.shutdown();
|
||||
|
||||
```
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Agent Lifecycle
|
||||
|
||||
The SDK follows the ACP lifecycle:
|
||||
|
||||
```
|
||||
initialize() → authenticate() → newSession() → prompt() ⇄ events → shutdown()
|
||||
```
|
||||
|
||||
| Step | Method | Purpose |
|
||||
|------|--------|---------|
|
||||
| Init | `initialize()` | Exchange protocol version and capabilities |
|
||||
| Auth | `authenticate()` | OAuth flow for Cline or OpenAI Codex accounts. Optional step if cline config directory already has credentials |
|
||||
| Session | `newSession()` | Create an isolated conversation context |
|
||||
| Prompt | `prompt()` | Send user messages; blocks until the turn ends |
|
||||
| Cancel | `cancel()` | Abort an in-progress prompt turn |
|
||||
| Mode | `setSessionMode()` | Switch between `"plan"` and `"act"` modes |
|
||||
| Model | `unstable_setSessionModel()` | Change the backing LLM (experimental) |
|
||||
| Shutdown | `shutdown()` | Abort all tasks, flush state, release resources |
|
||||
|
||||
### Sessions
|
||||
|
||||
A session is an independent conversation with its own task history and working directory. You can run multiple sessions concurrently.
|
||||
|
||||
```typescript
|
||||
const { sessionId, modes, models } = await agent.newSession({
|
||||
cwd: "/path/to/project",
|
||||
mcpServers: [], // mcpServers field not supported yet, but exposed here to maintain conformance with acp protocol
|
||||
})
|
||||
```
|
||||
|
||||
The response includes:
|
||||
- `sessionId` — use this in all subsequent calls
|
||||
- `modes` — available modes (`plan`, `act`) and the current mode
|
||||
- `models` — available models and the current model ID
|
||||
|
||||
Access session metadata via the read-only `sessions` map:
|
||||
|
||||
```typescript
|
||||
const session = agent.sessions.get(sessionId)
|
||||
// { sessionId, cwd, mode, mcpServers, createdAt, lastActivityAt, ... }
|
||||
```
|
||||
|
||||
### Prompting
|
||||
|
||||
`prompt()` sends a user message and blocks until the agent finishes its turn. While the prompt is processing, the agent streams output via session events.
|
||||
|
||||
```typescript
|
||||
const response = await agent.prompt({
|
||||
sessionId,
|
||||
prompt: [
|
||||
{ type: "text", text: "Refactor the auth module to use JWT" },
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
The prompt array accepts multiple content blocks:
|
||||
|
||||
```typescript
|
||||
// Text + image + file context
|
||||
await agent.prompt({
|
||||
sessionId,
|
||||
prompt: [
|
||||
{ type: "text", text: "What's in this screenshot?" },
|
||||
{ type: "image", data: base64ImageData, mimeType: "image/png" },
|
||||
{
|
||||
type: "resource",
|
||||
resource: {
|
||||
uri: "file:///path/to/relevant-file.ts",
|
||||
mimeType: "text/plain",
|
||||
text: fileContents,
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
#### Content Block Types
|
||||
|
||||
| Type | Fields | Description |
|
||||
|------|--------|-------------|
|
||||
| `TextContent` | `{ type: "text", text: string }` | Plain text message |
|
||||
| `ImageContent` | `{ type: "image", mimeType: string, data: string }` | Base64-encoded image |
|
||||
| `EmbeddedResource` | `{ type: "resource", resource: { uri: string, mimeType?: string, text?: string, blob?: string } }` | File or resource context |
|
||||
|
||||
#### Stop Reasons
|
||||
|
||||
`prompt()` resolves with a `stopReason`:
|
||||
|
||||
| Value | Meaning |
|
||||
|-------|---------|
|
||||
| `"end_turn"` | Agent finished normally (completed task or waiting for user input) |
|
||||
| `"error"` | An error occurred |
|
||||
|
||||
### Streaming Events
|
||||
|
||||
Subscribe to real-time output via `ClineSessionEmitter`. Each session has its own emitter.
|
||||
|
||||
```typescript
|
||||
const emitter = agent.emitterForSession(sessionId)
|
||||
```
|
||||
|
||||
#### Event Types
|
||||
|
||||
All events correspond to [ACP `SessionUpdate` types](https://agentclientprotocol.com/protocol/schema#SessionUpdate):
|
||||
|
||||
| Event | Payload | Description |
|
||||
|-------|---------|-------------|
|
||||
| `agent_message_chunk` | `{ content: ContentBlock }` | Streamed text from the agent |
|
||||
| `agent_thought_chunk` | `{ content: ContentBlock }` | Internal reasoning / chain-of-thought |
|
||||
| `tool_call` | `ToolCall` | New tool invocation (file edit, command, etc.) |
|
||||
| `tool_call_update` | `ToolCallUpdate` | Progress/result update for an existing tool call |
|
||||
| `plan` | `{ entries: PlanEntry[] }` | Agent's execution plan |
|
||||
| `available_commands_update` | `{ availableCommands: AvailableCommand[] }` | Slash commands the agent supports |
|
||||
| `current_mode_update` | `{ currentModeId: string }` | Mode changed (plan/act) |
|
||||
| `user_message_chunk` | `{ content: ContentBlock }` | User message chunks (for multi-turn) |
|
||||
| `config_option_update` | `{ configOptions: SessionConfigOption[] }` | Configuration changed |
|
||||
| `session_info_update` | Session metadata | Session metadata changed |
|
||||
| `error` | `Error` | Session-level error (not an ACP update) |
|
||||
|
||||
```typescript
|
||||
emitter.on("agent_message_chunk", (payload) => {
|
||||
// payload.content is a ContentBlock — usually { type: "text", text: "..." }
|
||||
process.stdout.write(payload.content.text)
|
||||
})
|
||||
|
||||
emitter.on("agent_thought_chunk", (payload) => {
|
||||
console.log("[thinking]", payload.content.text)
|
||||
})
|
||||
|
||||
emitter.on("tool_call", (payload) => {
|
||||
console.log(`[${payload.kind}] ${payload.title} (${payload.status})`)
|
||||
})
|
||||
|
||||
emitter.on("tool_call_update", (payload) => {
|
||||
console.log(` → ${payload.toolCallId}: ${payload.status}`)
|
||||
})
|
||||
|
||||
emitter.on("error", (err) => {
|
||||
console.error("Session error:", err)
|
||||
})
|
||||
```
|
||||
|
||||
The emitter supports `on`, `once`, `off`, and `removeAllListeners`.
|
||||
|
||||
### Permission Handling
|
||||
|
||||
When the agent wants to execute a tool (edit a file, run a command, etc.), it requests permission. You **must** set a permission handler or all tool calls will be auto-rejected.
|
||||
|
||||
```typescript
|
||||
agent.setPermissionHandler(async (request) => {
|
||||
// request.toolCall — details about what the agent wants to do
|
||||
// request.options — available choices (allow_once, reject_once, etc.)
|
||||
|
||||
console.log(`Permission requested: ${request.toolCall.title}`)
|
||||
console.log("Options:", request.options.map(o => `${o.optionId} (${o.kind})`))
|
||||
|
||||
// Auto-approve everything:
|
||||
const allowOption = request.options.find(o => o.kind.includes("allow"))
|
||||
if (allowOption) {
|
||||
return { outcome: { outcome: "selected", optionId: allowOption.optionId } }
|
||||
} else {
|
||||
return { outcome: { outcome: "rejected" } }
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
#### Permission Options
|
||||
|
||||
Each permission request includes an array of `PermissionOption` objects:
|
||||
|
||||
| `kind` | Meaning |
|
||||
|--------|---------|
|
||||
| `allow_once` | Approve this single operation |
|
||||
| `allow_always` | Approve and remember for future operations |
|
||||
| `reject_once` | Deny this single operation |
|
||||
| `reject_always` | Deny and remember for future operations |
|
||||
|
||||
**Important:** If no permission handler is set, all tool calls are rejected for safety.
|
||||
|
||||
### Modes
|
||||
|
||||
Cline supports two modes:
|
||||
|
||||
- **`plan`** — The agent gathers information and creates a plan without executing actions
|
||||
- **`act`** — The agent executes actions (file edits, commands, etc.)
|
||||
|
||||
```typescript
|
||||
// Switch to plan mode
|
||||
await agent.setSessionMode({ sessionId, modeId: "plan" })
|
||||
|
||||
// Switch back to act mode
|
||||
await agent.setSessionMode({ sessionId, modeId: "act" })
|
||||
```
|
||||
|
||||
The current mode is returned in `newSession()`
|
||||
|
||||
### Model Selection
|
||||
|
||||
Change the backing model with `unstable_setSessionModel()`. The model ID format is `"provider/modelId"`.
|
||||
|
||||
```typescript
|
||||
await agent.unstable_setSessionModel({
|
||||
sessionId,
|
||||
modelId: "anthropic/claude-sonnet-4-20250514",
|
||||
})
|
||||
```
|
||||
|
||||
This sets the model for both plan and act modes. Available providers include `anthropic`, `openai-native`, `gemini`, `bedrock`, `deepseek`, `mistral`, `groq`, `xai`, and others. Model Ids can be found in the NewSessionResponse object after calling `agent.newSession(..)`
|
||||
|
||||
> **Note:** This API is experimental and may change.
|
||||
|
||||
### Authentication
|
||||
|
||||
The SDK supports two OAuth flows:
|
||||
|
||||
```typescript
|
||||
// Cline account (uses browser OAuth)
|
||||
await agent.authenticate({ methodId: "cline-oauth" })
|
||||
|
||||
// OpenAI Codex / ChatGPT subscription
|
||||
await agent.authenticate({ methodId: "openai-codex-oauth" })
|
||||
```
|
||||
|
||||
Both methods open a browser window for the OAuth flow and block until authentication completes (5-minute timeout for Cline OAuth).
|
||||
|
||||
For BYO (bring-your-own) API key providers, configure the key through the cline config directory before creating a session. The `authenticate()` call is not needed for BYO providers. We plan to support more auth providers in the near future.
|
||||
|
||||
### Cancellation
|
||||
|
||||
Cancel an in-progress prompt turn:
|
||||
|
||||
```typescript
|
||||
await agent.cancel({ sessionId })
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Constructor
|
||||
|
||||
```typescript
|
||||
new ClineAgent(options: ClineAgentOptions)
|
||||
```
|
||||
|
||||
```typescript
|
||||
interface ClineAgentOptions {
|
||||
/** Enable debug logging (default: false) */
|
||||
debug?: boolean
|
||||
/** Custom Cline config directory (default: ~/.cline) */
|
||||
clineDir?: string
|
||||
}
|
||||
```
|
||||
|
||||
The `clineDir` option lets you isolate configuration and task history per-application:
|
||||
|
||||
```typescript
|
||||
const agent = new ClineAgent({
|
||||
clineDir: "/tmp/my-app-cline",
|
||||
})
|
||||
```
|
||||
|
||||
### Methods
|
||||
|
||||
#### `initialize(params): Promise<InitializeResponse>`
|
||||
|
||||
Initialize the agent and negotiate protocol capabilities.
|
||||
|
||||
```typescript
|
||||
const response = await agent.initialize({
|
||||
clientCapabilities: {},
|
||||
protocolVersion: 1,
|
||||
})
|
||||
|
||||
// Response includes:
|
||||
{
|
||||
protocolVersion: "0.9.0",
|
||||
agentCapabilities: {
|
||||
loadSession: true,
|
||||
promptCapabilities: { image: true, audio: false, embeddedContext: true },
|
||||
mcpCapabilities: { http: true, sse: false }
|
||||
},
|
||||
agentInfo: { name: "cline", version: "2.2.3" },
|
||||
authMethods: [
|
||||
{ id: "cline-oauth", name: "Sign in with Cline", description: "..." },
|
||||
{ id: "openai-codex-oauth", name: "Sign in with ChatGPT", description: "..." }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### `newSession(params): Promise<NewSessionResponse>`
|
||||
|
||||
Create a new conversation session.
|
||||
|
||||
```typescript
|
||||
const session = await agent.newSession({
|
||||
cwd: "/path/to/project",
|
||||
mcpServers: [
|
||||
{
|
||||
type: "stdio",
|
||||
name: "filesystem",
|
||||
command: "npx",
|
||||
args: ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/dir"],
|
||||
env: {},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// Response includes:
|
||||
{
|
||||
sessionId: "uuid-string",
|
||||
modes: {
|
||||
availableModes: [
|
||||
{ id: "plan", name: "Plan", description: "Gather information and create a detailed plan" },
|
||||
{ id: "act", name: "Act", description: "Execute actions to accomplish the task" }
|
||||
],
|
||||
currentModeId: "act"
|
||||
},
|
||||
models: {
|
||||
currentModelId: "anthropic/claude-sonnet-4-5-20241022",
|
||||
availableModels: [{ modelId: "anthropic/claude-3-5-sonnet-20241022", name: "..." }]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **Note:** `newSession()` may throw an auth-required error if credentials are not configured yet.
|
||||
|
||||
#### `prompt(params): Promise<PromptResponse>`
|
||||
|
||||
Send a user prompt to the agent. This is the main method for interacting with Cline. Blocks until the agent finishes its turn.
|
||||
|
||||
```typescript
|
||||
const response = await agent.prompt({
|
||||
sessionId: session.sessionId,
|
||||
prompt: [
|
||||
{ type: "text", text: "Create a function that adds two numbers" },
|
||||
],
|
||||
})
|
||||
|
||||
// Response: { stopReason: "end_turn" | "max_tokens" | "cancelled" | "error" }
|
||||
```
|
||||
|
||||
#### `cancel(params): Promise<void>`
|
||||
|
||||
Cancel an ongoing prompt operation.
|
||||
|
||||
```typescript
|
||||
await agent.cancel({ sessionId: session.sessionId })
|
||||
```
|
||||
|
||||
#### `setSessionMode(params): Promise<SetSessionModeResponse>`
|
||||
|
||||
Switch between plan and act modes.
|
||||
|
||||
```typescript
|
||||
await agent.setSessionMode({ sessionId, modeId: "plan" })
|
||||
```
|
||||
|
||||
#### `unstable_setSessionModel(params): Promise<SetSessionModelResponse>`
|
||||
|
||||
Change the model for the session. Model ID format depends on the inference provider. See NewSessionResponse object to get modelIds.
|
||||
|
||||
```typescript
|
||||
await agent.unstable_setSessionModel({
|
||||
sessionId,
|
||||
modelId: "anthropic/claude-sonnet-4-20250514",
|
||||
})
|
||||
```
|
||||
|
||||
#### `authenticate(params): Promise<AuthenticateResponse>`
|
||||
|
||||
Authenticate with a provider. Opens a browser window for OAuth flow.
|
||||
|
||||
```typescript
|
||||
await agent.authenticate({ methodId: "cline-oauth" })
|
||||
```
|
||||
|
||||
Current methodIds we support:
|
||||
|
||||
| methodId | Description |
|
||||
| -------------------- | ----------------------------- |
|
||||
| `cline-oauth` | use cline inference provider |
|
||||
| `openai-codex-oauth` | use your chatgpt subscription |
|
||||
| more coming soon!... | |
|
||||
|
||||
#### `shutdown(): Promise<void>`
|
||||
|
||||
Clean up all resources. Call this when done.
|
||||
|
||||
```typescript
|
||||
await agent.shutdown()
|
||||
```
|
||||
|
||||
#### `setPermissionHandler(handler)`
|
||||
|
||||
Set a callback to handle tool permission requests.
|
||||
|
||||
```typescript
|
||||
agent.setPermissionHandler((request, resolve) => {
|
||||
resolve({ outcome: { outcome: "selected", optionId: "allow_once" } })
|
||||
})
|
||||
```
|
||||
|
||||
#### `emitterForSession(sessionId): ClineSessionEmitter`
|
||||
|
||||
Get the typed event emitter for a session.
|
||||
|
||||
```typescript
|
||||
const emitter = agent.emitterForSession(session.sessionId)
|
||||
```
|
||||
|
||||
#### `sessions` (read-only Map)
|
||||
|
||||
Access active sessions:
|
||||
|
||||
```typescript
|
||||
for (const [sessionId, session] of agent.sessions) {
|
||||
console.log(sessionId, session.cwd, session.mode)
|
||||
}
|
||||
```
|
||||
|
||||
## Full Example: Auto-Approve Agent
|
||||
|
||||
```typescript
|
||||
import { ClineAgent } from "cline";
|
||||
|
||||
async function runTask(taskPrompt: string, cwd: string) {
|
||||
const agent = new ClineAgent({ clineDir: "/Users/maxpaulus/.cline" });
|
||||
|
||||
await agent.initialize({
|
||||
protocolVersion: 1,
|
||||
clientCapabilities: {},
|
||||
});
|
||||
|
||||
const { sessionId } = await agent.newSession({ cwd, mcpServers: [] });
|
||||
|
||||
// Auto-approve all tool calls
|
||||
agent.setPermissionHandler(async (request) => {
|
||||
const allow = request.options.find((o) => o.kind === "allow_once");
|
||||
return {
|
||||
outcome: allow
|
||||
? { outcome: "selected", optionId: allow.optionId }
|
||||
: { outcome: "cancelled" },
|
||||
};
|
||||
});
|
||||
|
||||
// Collect output
|
||||
const output: string[] = [];
|
||||
const emitter = agent.emitterForSession(sessionId);
|
||||
|
||||
emitter.on("agent_message_chunk", (p) => {
|
||||
if (p.content.type === "text") output.push(p.content.text);
|
||||
});
|
||||
|
||||
emitter.on("tool_call", (p) => {
|
||||
console.log(`[tool] ${p.title}`);
|
||||
});
|
||||
|
||||
const { stopReason } = await agent.prompt({
|
||||
sessionId,
|
||||
prompt: [{ type: "text", text: taskPrompt }],
|
||||
});
|
||||
|
||||
console.log("\n--- Agent Output ---");
|
||||
console.log(output.join(""));
|
||||
console.log(`\nStop reason: ${stopReason}`);
|
||||
|
||||
await agent.shutdown();
|
||||
}
|
||||
|
||||
runTask("Create a README.md for this project", process.cwd());
|
||||
```
|
||||
|
||||
## Full Example: Interactive Permission Flow
|
||||
|
||||
```typescript
|
||||
import { ClineAgent, type PermissionHandler } from "cline";
|
||||
import * as readline from "readline";
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
const ask = (q: string) => new Promise<string>((res) => rl.question(q, res));
|
||||
|
||||
const interactivePermissions: PermissionHandler = async (request) => {
|
||||
console.log(`\n⚠️ Permission: ${request.toolCall.title}`);
|
||||
|
||||
for (const [i, opt] of request.options.entries()) {
|
||||
console.log(` ${i + 1}. [${opt.kind}] ${opt.name}`);
|
||||
}
|
||||
|
||||
const choice = await ask("Choose (number): ");
|
||||
const idx = parseInt(choice, 10) - 1;
|
||||
const selected = request.options[idx];
|
||||
|
||||
if (selected) {
|
||||
return {
|
||||
outcome: { outcome: "selected", optionId: selected.optionId },
|
||||
};
|
||||
} else {
|
||||
return { outcome: { outcome: "cancelled" } };
|
||||
}
|
||||
};
|
||||
|
||||
async function main() {
|
||||
const agent = new ClineAgent({});
|
||||
await agent.initialize({ protocolVersion: 1, clientCapabilities: {} });
|
||||
|
||||
const { sessionId } = await agent.newSession({
|
||||
cwd: process.cwd(),
|
||||
mcpServers: [],
|
||||
});
|
||||
|
||||
agent.setPermissionHandler(interactivePermissions);
|
||||
|
||||
const emitter = agent.emitterForSession(sessionId);
|
||||
emitter.on("agent_message_chunk", (p) => {
|
||||
if (p.content.type === "text") process.stdout.write(p.content.text);
|
||||
});
|
||||
|
||||
// Multi-turn conversation
|
||||
while (true) {
|
||||
const userInput = await ask("\n> ");
|
||||
if (userInput === "exit") break;
|
||||
|
||||
const { stopReason } = await agent.prompt({
|
||||
sessionId,
|
||||
prompt: [{ type: "text", text: userInput }],
|
||||
});
|
||||
|
||||
console.log(`\n[${stopReason}]`);
|
||||
}
|
||||
|
||||
await agent.shutdown();
|
||||
rl.close();
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
```
|
||||
|
||||
## Exported Types
|
||||
|
||||
All types are re-exported from the `cline` package. Key types:
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `ClineAgent` | Main agent class |
|
||||
| `ClineSessionEmitter` | Typed event emitter for session events |
|
||||
| `ClineAgentOptions` | Constructor options |
|
||||
| `ClineAcpSession` | Session metadata (read-only) |
|
||||
| `ClineSessionEvents` | Event name → handler signature map |
|
||||
| `PermissionHandler` | `(request, resolve) => void` callback |
|
||||
| `PermissionResolver` | `(response) => void` callback |
|
||||
| `SessionUpdate` | Union of all session update types |
|
||||
| `SessionUpdateType` | Discriminator values (`"agent_message_chunk"`, `"tool_call"`, etc.) |
|
||||
| `ToolCall` | Tool call details (id, title, kind, status, content) |
|
||||
| `ToolCallUpdate` | Partial update to an existing tool call |
|
||||
| `ToolCallStatus` | `"pending" \| "in_progress" \| "completed" \| "failed"` |
|
||||
| `ToolKind` | `"read" \| "edit" \| "delete" \| "execute" \| "search" \| ...` |
|
||||
| `StopReason` | `"end_turn" \| "cancelled" \| "error" \| "max_tokens" \| ...` |
|
||||
| `ContentBlock` | `TextContent \| ImageContent \| AudioContent \| ...` |
|
||||
| `McpServer` | MCP server configuration (stdio, http) |
|
||||
| `PromptRequest` / `PromptResponse` | Prompt call types |
|
||||
| `NewSessionRequest` / `NewSessionResponse` | Session creation types |
|
||||
| `InitializeRequest` / `InitializeResponse` | Initialization types |
|
||||
|
||||
See the [ACP Schema](https://agentclientprotocol.com/protocol/schema) for the full type definitions.
|
||||
|
||||
## Relationship to ACP
|
||||
|
||||
The Cline SDK implements the [Agent Client Protocol](https://agentclientprotocol.com) `Agent` interface. The key difference from a standard ACP stdio agent is that the SDK uses an **event emitter pattern** instead of a transport connection:
|
||||
|
||||
| ACP Stdio (via `AcpAgent`) | SDK (via `ClineAgent`) |
|
||||
|-----------------------------|------------------------|
|
||||
| Session updates sent over JSON-RPC stdio | Session updates emitted via `ClineSessionEmitter` |
|
||||
| Permissions requested via `connection.requestPermission()` | Permissions requested via `setPermissionHandler()` callback |
|
||||
| Single process, single connection | Embeddable, multiple concurrent sessions |
|
||||
|
||||
If you need stdio-based ACP communication (e.g., for IDE integration), use the `cline` CLI binary directly. The SDK is for embedding Cline in your own Node.js processes.
|
||||
@@ -143,16 +143,35 @@ echo '{"cancel":false}'
|
||||
|
||||
<Steps>
|
||||
<Step title="Create the hook file">
|
||||
Save the script above as `~/Documents/Cline/Hooks/file-logger` (macOS/Linux) or create it through the Hooks UI.
|
||||
Save the script above as `~/Documents/Cline/Hooks/file-logger` or create it through the Hooks UI.
|
||||
</Step>
|
||||
<Step title="Make it executable">
|
||||
Run `chmod +x ~/Documents/Cline/Hooks/file-logger` in your terminal.
|
||||
On macOS/Linux, run `chmod +x ~/Documents/Cline/Hooks/file-logger`.
|
||||
</Step>
|
||||
<Step title="Enable it">
|
||||
<Step title="Enable it (macOS/Linux only)">
|
||||
In Cline's Hooks tab, find "file-logger" under PreToolUse hooks and toggle it on.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<Note>
|
||||
On Windows, hooks are executed with PowerShell and run whenever the hook file exists. In this
|
||||
foundation PR, hook enable/disable toggling is not yet supported on Windows.
|
||||
</Note>
|
||||
|
||||
<Note>
|
||||
Coming next: JSON-backed hook enabled/disabled state across platforms, so toggle behavior is
|
||||
consistent on Windows, macOS, and Linux.
|
||||
</Note>
|
||||
|
||||
<Note>
|
||||
Hook filenames are platform-specific:
|
||||
|
||||
- **Windows**: only `HookName.ps1` is supported (PowerShell script files)
|
||||
- **macOS/Linux**: only extensionless `HookName` is supported (executable files like bash scripts or binaries)
|
||||
|
||||
Wrong-platform naming is ignored by hook discovery.
|
||||
</Note>
|
||||
|
||||
### Test It
|
||||
|
||||
Ask Cline to read any file in your project: "What's in package.json?"
|
||||
@@ -444,14 +463,17 @@ cline config set hooks-enabled=true
|
||||
```
|
||||
|
||||
<Note>
|
||||
CLI hooks are only supported on macOS and Linux.
|
||||
Windows hooks require PowerShell (`powershell.exe`) available on your PATH.
|
||||
</Note>
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Hook not running?**
|
||||
- Check that the file is executable (`chmod +x hookname`)
|
||||
- Verify the hook is enabled (toggle is on in the Hooks tab)
|
||||
- On macOS/Linux, check that the file is executable (`chmod +x hookname`)
|
||||
- On Windows, ensure PowerShell is available (`powershell -NoProfile -Command "$PSVersionTable.PSVersion"`)
|
||||
- On Windows, ensure the hook file is named `<HookName>.ps1` (for example `PreToolUse.ps1`)
|
||||
- On macOS/Linux, ensure the hook file uses extensionless `<HookName>` naming (for example `PreToolUse`)
|
||||
- On macOS/Linux, verify the hook is enabled (toggle is on in the Hooks tab)
|
||||
- Check that Hooks are enabled globally in Settings
|
||||
|
||||
**Hook output not parsed?**
|
||||
|
||||
+3
-2
@@ -110,13 +110,13 @@
|
||||
"cline-cli/samples/github-issue-rca",
|
||||
"cline-cli/samples/github-integration",
|
||||
"cline-cli/samples/github-pr-review",
|
||||
"cline-cli/samples/cve-scan",
|
||||
"cline-cli/samples/model-orchestration",
|
||||
"cline-cli/samples/worktree-workflows"
|
||||
]
|
||||
},
|
||||
"cline-cli/configuration",
|
||||
"cline-cli/acp-editor-integrations",
|
||||
"cline-sdk/overview",
|
||||
"cline-cli/cli-reference"
|
||||
]
|
||||
},
|
||||
@@ -287,7 +287,8 @@
|
||||
{
|
||||
"group": "Control Other Cline Features",
|
||||
"pages": [
|
||||
"enterprise-solutions/configuration/infrastructure-configuration/control-other-cline-features/yolo-mode"
|
||||
"enterprise-solutions/configuration/infrastructure-configuration/control-other-cline-features/yolo-mode",
|
||||
"enterprise-solutions/configuration/infrastructure-configuration/control-other-cline-features/mcp-marketplace"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
+298
@@ -0,0 +1,298 @@
|
||||
---
|
||||
title: "MCP Marketplace"
|
||||
sidebarTitle: "MCP Marketplace"
|
||||
description: "Enterprise controls for MCP Marketplace access, server allowlisting, and remote MCP server management"
|
||||
---
|
||||
|
||||
The MCP Marketplace lets developers discover and install MCP servers that extend Cline's capabilities. For Enterprise administrators, this page covers how to control marketplace access, restrict which servers are available, and push pre-configured MCP servers to your organization.
|
||||
|
||||
<Note>
|
||||
For complete details about the MCP Marketplace and how developers use it, see [MCP Made Easy](/mcp/mcp-marketplace).
|
||||
</Note>
|
||||
|
||||
## Overview
|
||||
|
||||
Enterprise administrators have four configuration options to govern MCP server usage across their organization:
|
||||
|
||||
| Setting | Purpose |
|
||||
|---------|---------|
|
||||
| `mcpMarketplaceEnabled` | Enable or disable the MCP Marketplace entirely |
|
||||
| `allowedMCPServers` | Restrict the marketplace to only approved MCP servers |
|
||||
| `remoteMCPServers` | Push pre-configured remote MCP servers to all users |
|
||||
| `blockPersonalRemoteMCPServers` | Prevent users from adding their own remote MCP servers |
|
||||
|
||||
These settings are applied through your organization's [remote configuration](/enterprise-solutions/configuration/remote-configuration/overview) and take effect immediately for all team members.
|
||||
|
||||
## Disabling the MCP Marketplace
|
||||
|
||||
To completely disable the MCP Marketplace for your organization, set `mcpMarketplaceEnabled` to `false`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpMarketplaceEnabled": false
|
||||
}
|
||||
```
|
||||
|
||||
When `mcpMarketplaceEnabled` is set to `false`:
|
||||
- The MCP Marketplace tab is hidden from all users
|
||||
- Users cannot browse or install MCP servers from the marketplace
|
||||
- Locally configured MCP servers are blocked
|
||||
- Enterprise policy takes precedence over individual preferences
|
||||
|
||||
When `mcpMarketplaceEnabled` is set to `true` or omitted:
|
||||
- Users can freely browse and install MCP servers from the marketplace
|
||||
- No organizational restrictions apply to marketplace access
|
||||
|
||||
<Warning>
|
||||
Disabling the marketplace entirely also blocks locally configured MCP servers. If you want to allow specific servers while restricting others, use the allowlist approach described below instead.
|
||||
</Warning>
|
||||
|
||||
## Restricting the Marketplace to Approved Servers
|
||||
|
||||
Rather than disabling the marketplace entirely, you can restrict it to a curated list of approved MCP servers using the `allowedMCPServers` setting. This is the recommended approach for most enterprises — it lets developers benefit from MCP while ensuring only vetted servers are available.
|
||||
|
||||
### Configuration
|
||||
|
||||
Add an `allowedMCPServers` array to your remote configuration. Each entry requires an `id` field set to the server's GitHub repository path:
|
||||
|
||||
```json
|
||||
{
|
||||
"allowedMCPServers": [
|
||||
{ "id": "github.com/modelcontextprotocol/server-filesystem" },
|
||||
{ "id": "github.com/modelcontextprotocol/server-github" },
|
||||
{ "id": "github.com/your-org/internal-mcp-server" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### How It Works
|
||||
|
||||
When `allowedMCPServers` is configured:
|
||||
- The marketplace catalog is filtered to show **only** the servers in your allowlist
|
||||
- Users can browse, view details, and install any server on the list
|
||||
- Servers not on the list are completely hidden from the marketplace
|
||||
- The allowlist applies to all team members in the organization
|
||||
|
||||
When `allowedMCPServers` is omitted or `undefined`:
|
||||
- The full marketplace catalog is available with no restrictions
|
||||
|
||||
When `allowedMCPServers` is set to an empty array (`[]`):
|
||||
- The marketplace shows no servers — effectively disabling installation while keeping the UI visible
|
||||
|
||||
### Finding Server IDs
|
||||
|
||||
The `id` for each allowed server is its GitHub repository path (without the `https://` prefix). For example:
|
||||
|
||||
| Server | ID |
|
||||
|--------|----|
|
||||
| Filesystem | `github.com/modelcontextprotocol/server-filesystem` |
|
||||
| GitHub | `github.com/modelcontextprotocol/server-github` |
|
||||
| Custom internal server | `github.com/your-org/your-mcp-server` |
|
||||
|
||||
You can find the correct ID by checking the `githubUrl` field of any server in the [MCP Marketplace](/mcp/mcp-marketplace) and removing the `https://` prefix.
|
||||
|
||||
## Pushing Pre-Configured Remote MCP Servers
|
||||
|
||||
Use `remoteMCPServers` to push MCP servers directly to all users without requiring them to install anything from the marketplace. This is ideal for internal MCP servers or third-party servers that need specific configuration.
|
||||
|
||||
### Configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"remoteMCPServers": [
|
||||
{
|
||||
"name": "Internal Code Search",
|
||||
"url": "https://mcp.internal.yourcompany.com/code-search",
|
||||
"alwaysEnabled": true,
|
||||
"headers": {
|
||||
"Authorization": "Bearer ${AUTH_TOKEN}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Documentation Server",
|
||||
"url": "https://mcp.internal.yourcompany.com/docs",
|
||||
"alwaysEnabled": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Remote Server Options
|
||||
|
||||
Each remote MCP server entry supports the following fields:
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `name` | string | Yes | Display name for the server |
|
||||
| `url` | string | Yes | The URL endpoint of the MCP server |
|
||||
| `alwaysEnabled` | boolean | No | When `true`, users cannot disable this server |
|
||||
| `headers` | object | No | Custom HTTP headers for authentication |
|
||||
|
||||
### Always-Enabled Servers
|
||||
|
||||
When `alwaysEnabled` is set to `true`:
|
||||
- The server is automatically active for all users
|
||||
- Users cannot toggle the server off
|
||||
- The server appears in the user's MCP configuration but the disable control is locked
|
||||
- This is useful for compliance, security, or internal tooling servers that must always be available
|
||||
|
||||
## Blocking Personal Remote MCP Servers
|
||||
|
||||
To prevent users from adding their own remote MCP servers, set `blockPersonalRemoteMCPServers` to `true`:
|
||||
|
||||
```json
|
||||
{
|
||||
"blockPersonalRemoteMCPServers": true
|
||||
}
|
||||
```
|
||||
|
||||
When `blockPersonalRemoteMCPServers` is `true`:
|
||||
- Users cannot add or configure remote MCP servers on their own
|
||||
- Only servers defined in the organization's `remoteMCPServers` configuration are available
|
||||
- This ensures all remote MCP connections go through approved, organization-managed endpoints
|
||||
|
||||
When `blockPersonalRemoteMCPServers` is `false` or omitted:
|
||||
- Users can freely add their own remote MCP server connections
|
||||
|
||||
## Combined Configuration Examples
|
||||
|
||||
### Locked-Down Environment
|
||||
|
||||
For organizations that need strict control over all MCP server access:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpMarketplaceEnabled": true,
|
||||
"allowedMCPServers": [
|
||||
{ "id": "github.com/modelcontextprotocol/server-filesystem" },
|
||||
{ "id": "github.com/modelcontextprotocol/server-github" }
|
||||
],
|
||||
"remoteMCPServers": [
|
||||
{
|
||||
"name": "Internal API Gateway",
|
||||
"url": "https://mcp.internal.yourcompany.com/gateway",
|
||||
"alwaysEnabled": true,
|
||||
"headers": {
|
||||
"X-Api-Key": "org-managed-key"
|
||||
}
|
||||
}
|
||||
],
|
||||
"blockPersonalRemoteMCPServers": true
|
||||
}
|
||||
```
|
||||
|
||||
This configuration:
|
||||
- Allows the marketplace but limits it to two approved servers
|
||||
- Pushes an always-enabled internal MCP server to all users
|
||||
- Blocks users from adding their own remote MCP servers
|
||||
|
||||
### Open Environment with Internal Servers
|
||||
|
||||
For organizations that want flexibility with internal server access:
|
||||
|
||||
```json
|
||||
{
|
||||
"remoteMCPServers": [
|
||||
{
|
||||
"name": "Company Knowledge Base",
|
||||
"url": "https://mcp.yourcompany.com/kb",
|
||||
"alwaysEnabled": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
This configuration:
|
||||
- Leaves the full marketplace open (no `allowedMCPServers` restriction)
|
||||
- Ensures all developers have access to the company knowledge base
|
||||
- Allows users to add their own remote MCP servers
|
||||
|
||||
### Marketplace Disabled with Internal Servers Only
|
||||
|
||||
For organizations that want to fully manage the MCP experience:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpMarketplaceEnabled": false,
|
||||
"remoteMCPServers": [
|
||||
{
|
||||
"name": "Approved Code Assistant",
|
||||
"url": "https://mcp.internal.yourcompany.com/code-assist",
|
||||
"alwaysEnabled": true
|
||||
},
|
||||
{
|
||||
"name": "Internal Docs Search",
|
||||
"url": "https://mcp.internal.yourcompany.com/docs",
|
||||
"alwaysEnabled": true
|
||||
}
|
||||
],
|
||||
"blockPersonalRemoteMCPServers": true
|
||||
}
|
||||
```
|
||||
|
||||
This configuration:
|
||||
- Disables the marketplace completely
|
||||
- Provides only organization-managed MCP servers
|
||||
- Prevents users from adding any additional remote servers
|
||||
|
||||
## Enterprise Policy Recommendations
|
||||
|
||||
### Recommended Approach
|
||||
|
||||
Most organizations should **use the allowlist** (`allowedMCPServers`) rather than disabling the marketplace entirely. This gives developers access to useful tools while ensuring security review of each server.
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Security Review Process" icon="shield">
|
||||
Before adding an MCP server to your allowlist:
|
||||
- Review the server's source code on GitHub
|
||||
- Evaluate the server's permissions and data access patterns
|
||||
- Check for active maintenance and security practices
|
||||
- Assess whether the server's data handling meets your compliance requirements
|
||||
- Test the server in a sandbox environment before approving
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Internal MCP Servers" icon="building">
|
||||
For internal tooling, use `remoteMCPServers` with `alwaysEnabled: true`:
|
||||
- Connect Cline to internal APIs, databases, and knowledge bases
|
||||
- Ensure consistent access across all developers
|
||||
- Manage authentication centrally through custom headers
|
||||
- Use `blockPersonalRemoteMCPServers` to prevent shadow IT
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Compliance Considerations" icon="clipboard-check">
|
||||
MCP servers can access external APIs and process data:
|
||||
- Audit which servers handle sensitive data
|
||||
- Ensure servers comply with your data residency requirements
|
||||
- Document approved servers in your security policies
|
||||
- Regularly review and update your allowlist
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
### Recommendations by Organization Size
|
||||
|
||||
#### Small Teams (5–20 developers)
|
||||
- **Marketplace:** Open or lightly restricted with an allowlist
|
||||
- **Remote Servers:** Push internal servers as needed
|
||||
- **Personal Servers:** Allow with guidance
|
||||
- **Review Cadence:** Quarterly allowlist review
|
||||
|
||||
#### Medium Organizations (20–100 developers)
|
||||
- **Marketplace:** Restricted to an approved allowlist
|
||||
- **Remote Servers:** Push internal servers with `alwaysEnabled`
|
||||
- **Personal Servers:** Consider blocking (`blockPersonalRemoteMCPServers: true`)
|
||||
- **Review Cadence:** Monthly allowlist review
|
||||
|
||||
#### Large Enterprises (100+ developers)
|
||||
- **Marketplace:** Strictly restricted to a vetted allowlist
|
||||
- **Remote Servers:** All MCP access through organization-managed servers
|
||||
- **Personal Servers:** Blocked (`blockPersonalRemoteMCPServers: true`)
|
||||
- **Review Cadence:** Formal approval process for new servers with security review
|
||||
|
||||
## Support & Questions
|
||||
|
||||
For help configuring MCP Marketplace policies:
|
||||
- Review [Remote Configuration Overview](/enterprise-solutions/configuration/remote-configuration/overview)
|
||||
- See [MCP Made Easy](/mcp/mcp-marketplace) for marketplace functionality details
|
||||
- See [MCP Overview](/mcp/mcp-overview) for general MCP concepts
|
||||
- Contact your Enterprise support representative
|
||||
- Join our [Discord](https://discord.gg/cline) for community discussion
|
||||
@@ -9,7 +9,21 @@ Cline Enterprise integrates with your identity provider (IdP) via **WorkOS AuthK
|
||||
|
||||
This page describes, at a high level, how SSO is set up for Cline Enterprise using WorkOS AuthKit.
|
||||
|
||||
If you haven’t completed initial onboarding, start with [Onboarding](/enterprise-solutions/onboarding).
|
||||
If you haven't completed initial onboarding, start with [Onboarding](/enterprise-solutions/onboarding).
|
||||
|
||||
### Video Walkthrough
|
||||
|
||||
<Frame>
|
||||
<iframe
|
||||
src="https://www.youtube.com/embed/QC7mzXLjIH8"
|
||||
title="SSO Setup with WorkOS"
|
||||
width="100%"
|
||||
style={{ aspectRatio: "16/9" }}
|
||||
frameBorder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
## Where setup happens
|
||||
SSO setup spans two places:
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
---
|
||||
title: "Dictation (Deprecated)"
|
||||
description: "Voice input feature has been removed from Cline"
|
||||
---
|
||||
|
||||
# Dictation Feature Removed
|
||||
|
||||
The dictation (voice-to-text) feature has been removed from Cline as of this release.
|
||||
|
||||
## What Happened?
|
||||
|
||||
The voice input feature that allowed you to speak to Cline instead of typing has been discontinued and is no longer available in the extension.
|
||||
|
||||
## Alternative Workflows
|
||||
|
||||
While the built-in dictation feature is no longer available, you can still work efficiently with Cline using these approaches:
|
||||
|
||||
### 1. System-Level Voice Input
|
||||
|
||||
Both macOS and Windows offer built-in dictation features that work across all applications:
|
||||
|
||||
- **macOS**: Press `Fn` twice (or `Fn Fn`) to activate dictation in any text field
|
||||
- **Windows**: Press `Windows + H` to open voice typing
|
||||
- **Linux**: Various desktop environments offer voice input through accessibility features
|
||||
|
||||
These system-level tools will work in Cline's chat input just like any other text field.
|
||||
|
||||
### 2. Copy-Paste from Voice Notes
|
||||
|
||||
If you prefer to think out loud:
|
||||
1. Use your phone's voice recorder or a voice memo app
|
||||
2. Transcribe using your preferred tool (many phones have built-in transcription)
|
||||
3. Copy and paste the transcribed text into Cline
|
||||
|
||||
### 3. Third-Party Transcription Tools
|
||||
|
||||
Many standalone transcription tools can be used alongside Cline:
|
||||
- Browser-based transcription services
|
||||
- Desktop transcription applications
|
||||
- AI-powered note-taking apps with transcription features
|
||||
|
||||
## Why Was It Removed?
|
||||
|
||||
The dictation feature was removed to streamline Cline's core functionality and focus development efforts on the primary AI assistance capabilities.
|
||||
|
||||
## Questions?
|
||||
|
||||
If you have questions about this change or need help setting up alternative voice input methods, please reach out through Cline's support channels.
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: "MiniMax"
|
||||
description: "Learn how to configure and use MiniMax models with Cline. Access MiniMax-M2 series models with large context windows and prompt caching."
|
||||
description: "Learn how to configure and use MiniMax models with Cline. Access MiniMax-M2 series models with large context windows, prompt caching, and reasoning support."
|
||||
---
|
||||
|
||||
MiniMax provides AI models with large context windows and competitive pricing, featuring the MiniMax-M2 series.
|
||||
@@ -18,9 +18,10 @@ MiniMax provides AI models with large context windows and competitive pricing, f
|
||||
|
||||
Cline supports the following MiniMax models:
|
||||
|
||||
- `MiniMax-M2.1` (Default) - Latest model with 192K context and prompt caching ($0.30/$1.20 per 1M tokens)
|
||||
- `MiniMax-M2.5` (Default) - Latest model with 192K context, prompt caching, and reasoning/thinking support ($0.30/$1.20 per 1M tokens)
|
||||
- `MiniMax-M2.1` - Previous generation with 192K context and prompt caching ($0.30/$1.20 per 1M tokens)
|
||||
- `MiniMax-M2.1-lightning` - Fast variant with higher output pricing ($0.30/$2.40 per 1M tokens)
|
||||
- `MiniMax-M2` - Previous generation with 192K context ($0.30/$1.20 per 1M tokens)
|
||||
- `MiniMax-M2` - Earlier generation with 192K context ($0.30/$1.20 per 1M tokens)
|
||||
|
||||
### Configuration in Cline
|
||||
|
||||
@@ -32,5 +33,6 @@ Cline supports the following MiniMax models:
|
||||
### Tips and Notes
|
||||
|
||||
- **Large Context:** All models support 192K token context windows.
|
||||
- **Prompt Caching:** M2.1 models support prompt caching for reduced costs on repeated queries.
|
||||
- **Reasoning Support:** M2.5 supports extended thinking/reasoning for complex tasks.
|
||||
- **Prompt Caching:** M2.5 and M2.1 models support prompt caching for reduced costs on repeated queries.
|
||||
- **Pricing:** Check the [MiniMax pricing page](https://www.minimax.io/platform/document/pricing) for current rates.
|
||||
|
||||
@@ -177,4 +177,22 @@ describe("FailureClassifier", () => {
|
||||
expect(failures).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe("YAML Safety (JSON_SCHEMA)", () => {
|
||||
it("rejects patterns file with custom YAML tags", () => {
|
||||
// Write a temp YAML file with a !!js/function tag
|
||||
const fs = require("fs")
|
||||
const path = require("path")
|
||||
const os = require("os")
|
||||
const tmpFile = path.join(os.tmpdir(), "unsafe-patterns.yaml")
|
||||
fs.writeFileSync(
|
||||
tmpFile,
|
||||
`version: "1.0"\npatterns:\n - name: !!js/function 'function(){ return "pwned" }'\n`,
|
||||
)
|
||||
|
||||
expect(() => new FailureClassifier(tmpFile)).toThrow()
|
||||
|
||||
fs.unlinkSync(tmpFile)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -43,7 +43,7 @@ export class FailureClassifier {
|
||||
|
||||
private loadPatternsFromYaml(filePath: string): FailurePatternsConfig {
|
||||
const content = fs.readFileSync(filePath, "utf-8")
|
||||
const config = yaml.load(content) as FailurePatternsConfig
|
||||
const config = yaml.load(content, { schema: yaml.JSON_SCHEMA }) as FailurePatternsConfig
|
||||
|
||||
if (!config.version || !config.patterns) {
|
||||
throw new Error("Invalid patterns YAML: missing version or patterns")
|
||||
|
||||
@@ -11,15 +11,16 @@
|
||||
* npx tsx evals/smoke-tests/run-smoke-tests.ts [options]
|
||||
*
|
||||
* Options:
|
||||
* --provider <name> Run tests for a specific provider (default: all configured)
|
||||
* --trials <n> Number of trials per test (default: 3)
|
||||
* --scenario <name> Run a specific scenario (default: all)
|
||||
* --model <id> Override model for all scenarios
|
||||
* --output <file> Write JSON results to file
|
||||
*/
|
||||
|
||||
import { execSync, spawn } from "child_process"
|
||||
import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
import * as dotenv from "dotenv"
|
||||
import { MetricsCalculator } from "../analysis/src/metrics"
|
||||
|
||||
// Default provider and model for smoke tests
|
||||
@@ -43,24 +44,23 @@ function checkClineCli(): boolean {
|
||||
// Use user's existing Cline config (already has auth configured)
|
||||
// For CI, this would be set up by the auth step before tests run
|
||||
const CLINE_CONFIG_DIR = path.join(process.env.HOME || "", ".cline")
|
||||
const configuredAuthCache = new Set<string>()
|
||||
|
||||
// Configure authentication using CLINE_API_KEY environment variable
|
||||
// Returns success if auth is configured, error message otherwise
|
||||
function configureAuth(): { ok: boolean; error?: string } {
|
||||
const apiKey = process.env.CLINE_API_KEY
|
||||
if (!apiKey) {
|
||||
return {
|
||||
ok: false,
|
||||
error: "CLINE_API_KEY environment variable not set",
|
||||
}
|
||||
}
|
||||
|
||||
function configureAuth(options: { provider: string; apiKey: string; modelId: string; baseUrl?: string }): {
|
||||
ok: boolean
|
||||
error?: string
|
||||
} {
|
||||
// Ensure config directory exists
|
||||
fs.mkdirSync(CLINE_CONFIG_DIR, { recursive: true })
|
||||
|
||||
try {
|
||||
// Run quick auth setup (non-interactive when all flags provided)
|
||||
execSync(`cline auth --config "${CLINE_CONFIG_DIR}" -p ${DEFAULT_PROVIDER} -k "${apiKey}" -m "${DEFAULT_MODEL}"`, {
|
||||
const args = [`cline auth --config "${CLINE_CONFIG_DIR}"`, `-p "${options.provider}"`, `-k "${options.apiKey}"`, `-m "${options.modelId}"`]
|
||||
if (options.baseUrl) {
|
||||
args.push(`-b "${options.baseUrl}"`)
|
||||
}
|
||||
|
||||
execSync(args.join(" "), {
|
||||
encoding: "utf-8",
|
||||
timeout: 10000,
|
||||
stdio: "pipe",
|
||||
@@ -74,6 +74,22 @@ function configureAuth(): { ok: boolean; error?: string } {
|
||||
}
|
||||
}
|
||||
|
||||
function loadEnvFiles(): void {
|
||||
const repoRoot = path.resolve(__dirname, "..", "..")
|
||||
const envFiles = [path.join(repoRoot, ".env"), path.join(repoRoot, ".env.local")]
|
||||
for (const envPath of envFiles) {
|
||||
if (fs.existsSync(envPath)) {
|
||||
dotenv.config({ path: envPath, override: false })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface ScenarioAuthConfig {
|
||||
apiKeyEnv?: string
|
||||
baseUrlEnv?: string
|
||||
modelId?: string
|
||||
}
|
||||
|
||||
// Smoke test scenario definition
|
||||
interface SmokeScenario {
|
||||
id: string
|
||||
@@ -85,6 +101,9 @@ interface SmokeScenario {
|
||||
expectedContent?: { file: string; contains: string }[] // Content checks
|
||||
timeout: number // Seconds
|
||||
models?: string[] // Optional: override default models for this scenario
|
||||
provider?: string // Provider override (defaults to DEFAULT_PROVIDER)
|
||||
requiredEnv?: string[] // Env vars required for this scenario to run
|
||||
auth?: ScenarioAuthConfig // Optional auth overrides for provider-specific scenarios
|
||||
}
|
||||
|
||||
// Load scenarios from disk
|
||||
@@ -108,6 +127,54 @@ function loadScenarios(scenariosDir: string): SmokeScenario[] {
|
||||
return scenarios
|
||||
}
|
||||
|
||||
function getMissingEnvVars(requiredEnv: string[] | undefined): string[] {
|
||||
if (!requiredEnv || requiredEnv.length === 0) {
|
||||
return []
|
||||
}
|
||||
return requiredEnv.filter((key) => !process.env[key])
|
||||
}
|
||||
|
||||
function getScenarioProvider(scenario: SmokeScenario): string {
|
||||
return scenario.provider || DEFAULT_PROVIDER
|
||||
}
|
||||
|
||||
function ensureScenarioAuth(scenario: SmokeScenario, modelId: string): { ok: boolean; error?: string } {
|
||||
const provider = getScenarioProvider(scenario)
|
||||
const authModelId = scenario.auth?.modelId || modelId
|
||||
const apiKeyEnv = scenario.auth?.apiKeyEnv || (provider === DEFAULT_PROVIDER ? "CLINE_API_KEY" : undefined)
|
||||
const apiKey = apiKeyEnv ? process.env[apiKeyEnv] : undefined
|
||||
const baseUrl = scenario.auth?.baseUrlEnv ? process.env[scenario.auth.baseUrlEnv] : undefined
|
||||
const authCacheKey = `${provider}|${authModelId}|${baseUrl || ""}|${apiKeyEnv || ""}`
|
||||
|
||||
if (apiKey) {
|
||||
if (configuredAuthCache.has(authCacheKey)) {
|
||||
return { ok: true }
|
||||
}
|
||||
const result = configureAuth({ provider, apiKey, modelId: authModelId, baseUrl })
|
||||
if (result.ok) {
|
||||
configuredAuthCache.add(authCacheKey)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// For default provider, local developers can rely on existing ~/.cline auth.
|
||||
if (provider === DEFAULT_PROVIDER) {
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
if (!apiKeyEnv) {
|
||||
return {
|
||||
ok: false,
|
||||
error: `Provider '${provider}' requires auth.apiKeyEnv in scenario config or preconfigured credentials`,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
error: `Missing required auth env var '${apiKeyEnv}' for provider '${provider}'`,
|
||||
}
|
||||
}
|
||||
|
||||
// Run a single trial
|
||||
interface TrialResult {
|
||||
passed: boolean
|
||||
@@ -318,6 +385,8 @@ interface SmokeTestReport {
|
||||
|
||||
// Main execution
|
||||
async function main() {
|
||||
loadEnvFiles()
|
||||
|
||||
const args = process.argv.slice(2)
|
||||
|
||||
// Parse arguments
|
||||
@@ -357,24 +426,6 @@ async function main() {
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Configure authentication if CLINE_API_KEY is set
|
||||
// Otherwise use existing auth from ~/.cline
|
||||
if (process.env.CLINE_API_KEY) {
|
||||
console.log("Configuring authentication from CLINE_API_KEY...")
|
||||
const authResult = configureAuth()
|
||||
if (!authResult.ok) {
|
||||
console.error("")
|
||||
console.error("ERROR: Authentication failed")
|
||||
console.error(` ${authResult.error}`)
|
||||
console.error("")
|
||||
process.exit(1)
|
||||
}
|
||||
console.log("Authentication configured")
|
||||
} else {
|
||||
console.log("Using existing authentication from ~/.cline")
|
||||
}
|
||||
console.log("")
|
||||
|
||||
// Load scenarios
|
||||
const scenariosDir = path.join(__dirname, "scenarios")
|
||||
let scenarios = loadScenarios(scenariosDir)
|
||||
@@ -392,12 +443,40 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
const skippedByEnv: Array<{ id: string; missingEnv: string[] }> = []
|
||||
if (selectedScenario) {
|
||||
const missingEnv = getMissingEnvVars(scenarios[0].requiredEnv)
|
||||
if (missingEnv.length > 0) {
|
||||
console.error(`Scenario '${selectedScenario}' missing required env: ${missingEnv.join(", ")}`)
|
||||
process.exit(1)
|
||||
}
|
||||
} else {
|
||||
scenarios = scenarios.filter((scenario) => {
|
||||
const missingEnv = getMissingEnvVars(scenario.requiredEnv)
|
||||
if (missingEnv.length > 0) {
|
||||
skippedByEnv.push({ id: scenario.id, missingEnv })
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// Filter models
|
||||
let models = MODELS
|
||||
if (selectedModel) {
|
||||
models = [selectedModel]
|
||||
}
|
||||
|
||||
if (scenarios.length === 0) {
|
||||
console.error("No runnable scenarios after env filtering")
|
||||
if (skippedByEnv.length > 0) {
|
||||
for (const skipped of skippedByEnv) {
|
||||
console.error(` - ${skipped.id}: missing ${skipped.missingEnv.join(", ")}`)
|
||||
}
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Create results directory with timestamp
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-")
|
||||
const resultsBaseDir = path.join(__dirname, "results")
|
||||
@@ -406,11 +485,17 @@ async function main() {
|
||||
|
||||
// Models are now always explicit
|
||||
const resolvedModels = models
|
||||
const providersInRun = [...new Set(scenarios.map((scenario) => getScenarioProvider(scenario)))]
|
||||
|
||||
console.log(`Running ${scenarios.length} scenarios × ${models.length} models × ${trials} trials`)
|
||||
console.log(`Provider: ${DEFAULT_PROVIDER}`)
|
||||
console.log(`Providers: ${providersInRun.join(", ")}`)
|
||||
console.log(`Models: ${resolvedModels.join(", ")}`)
|
||||
console.log(`Scenarios: ${scenarios.map((s) => s.id).join(", ")}`)
|
||||
if (skippedByEnv.length > 0) {
|
||||
console.log(
|
||||
`Skipped by env: ${skippedByEnv.map((skipped) => `${skipped.id} (missing: ${skipped.missingEnv.join(", ")})`).join("; ")}`,
|
||||
)
|
||||
}
|
||||
console.log(`Results: ${resultsDir}`)
|
||||
console.log(`Parallel: ${parallel ? `yes (limit: ${parallelLimit})` : "no"}`)
|
||||
console.log("")
|
||||
@@ -420,7 +505,7 @@ async function main() {
|
||||
|
||||
// Build list of all scenario+model combinations
|
||||
interface TestJob {
|
||||
scenario: Scenario
|
||||
scenario: SmokeScenario
|
||||
modelId: string
|
||||
}
|
||||
const jobs: TestJob[] = []
|
||||
@@ -437,6 +522,26 @@ async function main() {
|
||||
const logDir = path.join(resultsDir, scenario.id, modelId)
|
||||
fs.mkdirSync(logDir, { recursive: true })
|
||||
|
||||
const authResult = ensureScenarioAuth(scenario, modelId)
|
||||
if (!authResult.ok) {
|
||||
const trialResults = Array.from({ length: trials }, () => ({
|
||||
passed: false,
|
||||
error: authResult.error || "Scenario authentication failed",
|
||||
durationMs: 0,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
}))
|
||||
return {
|
||||
scenarioId: scenario.id,
|
||||
scenarioName: scenario.name,
|
||||
model: modelId,
|
||||
modelId,
|
||||
trials: trialResults,
|
||||
metrics: metricsCalc.calculateTaskMetrics(trialResults.map((t) => t.passed)),
|
||||
status: metricsCalc.getTaskStatus(trialResults.map((t) => t.passed)),
|
||||
}
|
||||
}
|
||||
|
||||
const trialResults: TrialResult[] = []
|
||||
const trialWorkdirs: string[] = []
|
||||
|
||||
@@ -504,58 +609,23 @@ async function main() {
|
||||
// Sequential execution
|
||||
for (const job of jobs) {
|
||||
console.log(`\n[${job.scenario.id}] ${job.scenario.name} (${job.modelId})`)
|
||||
const logDir = path.join(resultsDir, job.scenario.id, job.modelId)
|
||||
fs.mkdirSync(logDir, { recursive: true })
|
||||
|
||||
const trialResults: TrialResult[] = []
|
||||
const trialWorkdirs: string[] = []
|
||||
|
||||
for (let t = 0; t < trials; t++) {
|
||||
const trialWorkdir = path.join(logDir, `workspace-trial-${t + 1}`)
|
||||
trialWorkdirs.push(trialWorkdir)
|
||||
process.stdout.write(` Trial ${t + 1}/${trials}... `)
|
||||
const result = await runTrial(job.scenario, job.modelId, trialWorkdir)
|
||||
trialResults.push(result)
|
||||
console.log(result.passed ? "✓ PASS" : `✗ FAIL: ${result.error}`)
|
||||
}
|
||||
|
||||
trialResults.forEach((result, t) => {
|
||||
const trialNum = t + 1
|
||||
const logContent =
|
||||
`# Trial ${trialNum}\n` +
|
||||
`Status: ${result.passed ? "PASS" : "FAIL"}\n` +
|
||||
`Duration: ${result.durationMs}ms\n` +
|
||||
(result.error ? `Error: ${result.error}\n` : "") +
|
||||
`\n## STDOUT\n${result.stdout || "(empty)"}\n` +
|
||||
`\n## STDERR\n${result.stderr || "(empty)"}\n`
|
||||
fs.writeFileSync(path.join(logDir, `trial-${trialNum}.log`), logContent)
|
||||
})
|
||||
|
||||
const trialBools = trialResults.map((t) => t.passed)
|
||||
const metrics = metricsCalc.calculateTaskMetrics(trialBools)
|
||||
const status = metricsCalc.getTaskStatus(trialBools)
|
||||
|
||||
results.push({
|
||||
scenarioId: job.scenario.id,
|
||||
scenarioName: job.scenario.name,
|
||||
model: job.modelId,
|
||||
modelId: job.modelId,
|
||||
trials: trialResults,
|
||||
metrics,
|
||||
status,
|
||||
const result = await runJob(job)
|
||||
result.trials.forEach((trial, index) => {
|
||||
console.log(` Trial ${index + 1}/${trials}... ${trial.passed ? "✓ PASS" : `✗ FAIL: ${trial.error}`}`)
|
||||
})
|
||||
results.push(result)
|
||||
|
||||
// Display pass@k where k = actual trials (pass@3 is meaningless with fewer trials)
|
||||
const passMetric = trials >= 3 ? metrics.passAt3 : metrics.passAt1
|
||||
const passMetric = trials >= 3 ? result.metrics.passAt3 : result.metrics.passAt1
|
||||
const passLabel = trials >= 3 ? "pass@3" : "pass@1"
|
||||
console.log(` Result: ${status.toUpperCase()} | ${passLabel}: ${(passMetric * 100).toFixed(0)}%`)
|
||||
console.log(` Result: ${result.status.toUpperCase()} | ${passLabel}: ${(passMetric * 100).toFixed(0)}%`)
|
||||
}
|
||||
}
|
||||
|
||||
// Generate report
|
||||
const report: SmokeTestReport = {
|
||||
timestamp: new Date().toISOString(),
|
||||
provider: DEFAULT_PROVIDER,
|
||||
provider: providersInRun.join(","),
|
||||
models: resolvedModels,
|
||||
scenarios: scenarios.map((s) => s.id),
|
||||
trialsPerTest: trials,
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "Edit file with gpt-oss via OpenAI-compatible",
|
||||
"description": "Reproduces openai-compatible gpt-oss file editing reliability when native tool calling is enabled",
|
||||
"prompt": "Edit the file config.txt and change the line 'debug = false' to 'debug = true'. Prefer direct file-edit tools instead of shell command workarounds.",
|
||||
"provider": "openai",
|
||||
"models": [
|
||||
"gpt-oss-120b"
|
||||
],
|
||||
"requiredEnv": [
|
||||
"OPENAI_COMPAT_API_KEY",
|
||||
"OPENAI_COMPAT_BASE_URL"
|
||||
],
|
||||
"auth": {
|
||||
"apiKeyEnv": "OPENAI_COMPAT_API_KEY",
|
||||
"baseUrlEnv": "OPENAI_COMPAT_BASE_URL",
|
||||
"modelId": "gpt-oss-120b"
|
||||
},
|
||||
"expectedFiles": [
|
||||
"config.txt"
|
||||
],
|
||||
"expectedContent": [
|
||||
{
|
||||
"file": "config.txt",
|
||||
"contains": "debug = true"
|
||||
}
|
||||
],
|
||||
"timeout": 180
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
# Application Configuration
|
||||
name = MyApp
|
||||
version = 1.0.0
|
||||
|
||||
# Debug settings
|
||||
debug = false
|
||||
log_level = info
|
||||
|
||||
# Server settings
|
||||
host = localhost
|
||||
port = 8080
|
||||
@@ -56,15 +56,10 @@ Cline에 기여하는 것에 관심을 가져주셔서 감사합니다! 버그
|
||||
- 변경사항이 기존 테스트에 영향을 미치는 경우 해당 테스트를 업데이트해 주세요
|
||||
- 적절한 경우 단위 테스트와 통합 테스트를 모두 포함해 주세요
|
||||
|
||||
4. **Changesets를 활용한 버전 관리**
|
||||
4. **버전/릴리스 노트 관리**
|
||||
|
||||
- 사용자에게 영향을 미치는 변경 사항이 있는 경우, `npm run changeset`을 실행하여 changeset을 생성해 주세요
|
||||
- 적절한 버전 증가 옵션을 선택하세요:
|
||||
- `major` 호환되지 않는 변경 (1.0.0 → 2.0.0)
|
||||
- `minor` 새로운 기능 추가 (1.0.0 → 1.1.0)
|
||||
- `patch` 버그 수정 (1.0.0 → 1.0.1)
|
||||
- 영향을 설명하는 명확한 변경사항 메시지를 작성해 주세요
|
||||
- 문서 변경만 있는 경우 changeset이 필요하지 않습니다
|
||||
- 기여자는 PR에서 changelog-entry 파일을 만들 필요가 없습니다.
|
||||
- 릴리스 버전 관리와 CHANGELOG 정리는 메인테이너가 릴리스 과정에서 수행합니다.
|
||||
|
||||
5. **커밋 가이드라인**
|
||||
|
||||
|
||||
Generated
+24
-733
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "claude-dev",
|
||||
"version": "3.66.0",
|
||||
"version": "3.69.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-dev",
|
||||
"version": "3.66.0",
|
||||
"version": "3.69.0",
|
||||
"license": "Apache-2.0",
|
||||
"workspaces": [
|
||||
".",
|
||||
@@ -112,7 +112,6 @@
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.3.14",
|
||||
"@bufbuild/buf": "^1.54.0",
|
||||
"@changesets/cli": "^2.27.12",
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@types/chai": "^5.0.1",
|
||||
"@types/clone-deep": "^4.0.4",
|
||||
@@ -162,7 +161,7 @@
|
||||
},
|
||||
"cli": {
|
||||
"name": "cline",
|
||||
"version": "2.4.1",
|
||||
"version": "2.5.0",
|
||||
"cpu": [
|
||||
"x64",
|
||||
"arm64"
|
||||
@@ -182,6 +181,7 @@
|
||||
"ink": "npm:@jrichman/ink@6.4.7",
|
||||
"ink-picture": "^1.3.3",
|
||||
"ink-spinner": "^5.0.0",
|
||||
"marked": "^17.0.3",
|
||||
"nanoid": "^5.1.6",
|
||||
"ora": "^8.0.1",
|
||||
"pino": "^10.0.0",
|
||||
@@ -193,6 +193,7 @@
|
||||
"cline": "dist/cli.mjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/marked": "^5.0.2",
|
||||
"@types/node": "20.x",
|
||||
"@types/prompts": "^2.4.9",
|
||||
"@types/react": "^19.2.9",
|
||||
@@ -1782,16 +1783,6 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
|
||||
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
|
||||
@@ -2193,249 +2184,6 @@
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@changesets/apply-release-plan": {
|
||||
"version": "7.0.14",
|
||||
"resolved": "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-7.0.14.tgz",
|
||||
"integrity": "sha512-ddBvf9PHdy2YY0OUiEl3TV78mH9sckndJR14QAt87KLEbIov81XO0q0QAmvooBxXlqRRP8I9B7XOzZwQG7JkWA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@changesets/config": "^3.1.2",
|
||||
"@changesets/get-version-range-type": "^0.4.0",
|
||||
"@changesets/git": "^3.0.4",
|
||||
"@changesets/should-skip-package": "^0.1.2",
|
||||
"@changesets/types": "^6.1.0",
|
||||
"@manypkg/get-packages": "^1.1.3",
|
||||
"detect-indent": "^6.0.0",
|
||||
"fs-extra": "^7.0.1",
|
||||
"lodash.startcase": "^4.4.0",
|
||||
"outdent": "^0.5.0",
|
||||
"prettier": "^2.7.1",
|
||||
"resolve-from": "^5.0.0",
|
||||
"semver": "^7.5.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@changesets/assemble-release-plan": {
|
||||
"version": "6.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@changesets/assemble-release-plan/-/assemble-release-plan-6.0.9.tgz",
|
||||
"integrity": "sha512-tPgeeqCHIwNo8sypKlS3gOPmsS3wP0zHt67JDuL20P4QcXiw/O4Hl7oXiuLnP9yg+rXLQ2sScdV1Kkzde61iSQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@changesets/errors": "^0.2.0",
|
||||
"@changesets/get-dependents-graph": "^2.1.3",
|
||||
"@changesets/should-skip-package": "^0.1.2",
|
||||
"@changesets/types": "^6.1.0",
|
||||
"@manypkg/get-packages": "^1.1.3",
|
||||
"semver": "^7.5.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@changesets/changelog-git": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@changesets/changelog-git/-/changelog-git-0.2.1.tgz",
|
||||
"integrity": "sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@changesets/types": "^6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@changesets/cli": {
|
||||
"version": "2.29.8",
|
||||
"resolved": "https://registry.npmjs.org/@changesets/cli/-/cli-2.29.8.tgz",
|
||||
"integrity": "sha512-1weuGZpP63YWUYjay/E84qqwcnt5yJMM0tep10Up7Q5cS/DGe2IZ0Uj3HNMxGhCINZuR7aO9WBMdKnPit5ZDPA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@changesets/apply-release-plan": "^7.0.14",
|
||||
"@changesets/assemble-release-plan": "^6.0.9",
|
||||
"@changesets/changelog-git": "^0.2.1",
|
||||
"@changesets/config": "^3.1.2",
|
||||
"@changesets/errors": "^0.2.0",
|
||||
"@changesets/get-dependents-graph": "^2.1.3",
|
||||
"@changesets/get-release-plan": "^4.0.14",
|
||||
"@changesets/git": "^3.0.4",
|
||||
"@changesets/logger": "^0.1.1",
|
||||
"@changesets/pre": "^2.0.2",
|
||||
"@changesets/read": "^0.6.6",
|
||||
"@changesets/should-skip-package": "^0.1.2",
|
||||
"@changesets/types": "^6.1.0",
|
||||
"@changesets/write": "^0.4.0",
|
||||
"@inquirer/external-editor": "^1.0.2",
|
||||
"@manypkg/get-packages": "^1.1.3",
|
||||
"ansi-colors": "^4.1.3",
|
||||
"ci-info": "^3.7.0",
|
||||
"enquirer": "^2.4.1",
|
||||
"fs-extra": "^7.0.1",
|
||||
"mri": "^1.2.0",
|
||||
"p-limit": "^2.2.0",
|
||||
"package-manager-detector": "^0.2.0",
|
||||
"picocolors": "^1.1.0",
|
||||
"resolve-from": "^5.0.0",
|
||||
"semver": "^7.5.3",
|
||||
"spawndamnit": "^3.0.1",
|
||||
"term-size": "^2.1.0"
|
||||
},
|
||||
"bin": {
|
||||
"changeset": "bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/@changesets/config": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@changesets/config/-/config-3.1.2.tgz",
|
||||
"integrity": "sha512-CYiRhA4bWKemdYi/uwImjPxqWNpqGPNbEBdX1BdONALFIDK7MCUj6FPkzD+z9gJcvDFUQJn9aDVf4UG7OT6Kog==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@changesets/errors": "^0.2.0",
|
||||
"@changesets/get-dependents-graph": "^2.1.3",
|
||||
"@changesets/logger": "^0.1.1",
|
||||
"@changesets/types": "^6.1.0",
|
||||
"@manypkg/get-packages": "^1.1.3",
|
||||
"fs-extra": "^7.0.1",
|
||||
"micromatch": "^4.0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@changesets/errors": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@changesets/errors/-/errors-0.2.0.tgz",
|
||||
"integrity": "sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"extendable-error": "^0.1.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@changesets/get-dependents-graph": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@changesets/get-dependents-graph/-/get-dependents-graph-2.1.3.tgz",
|
||||
"integrity": "sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@changesets/types": "^6.1.0",
|
||||
"@manypkg/get-packages": "^1.1.3",
|
||||
"picocolors": "^1.1.0",
|
||||
"semver": "^7.5.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@changesets/get-release-plan": {
|
||||
"version": "4.0.14",
|
||||
"resolved": "https://registry.npmjs.org/@changesets/get-release-plan/-/get-release-plan-4.0.14.tgz",
|
||||
"integrity": "sha512-yjZMHpUHgl4Xl5gRlolVuxDkm4HgSJqT93Ri1Uz8kGrQb+5iJ8dkXJ20M2j/Y4iV5QzS2c5SeTxVSKX+2eMI0g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@changesets/assemble-release-plan": "^6.0.9",
|
||||
"@changesets/config": "^3.1.2",
|
||||
"@changesets/pre": "^2.0.2",
|
||||
"@changesets/read": "^0.6.6",
|
||||
"@changesets/types": "^6.1.0",
|
||||
"@manypkg/get-packages": "^1.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@changesets/get-version-range-type": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@changesets/get-version-range-type/-/get-version-range-type-0.4.0.tgz",
|
||||
"integrity": "sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@changesets/git": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@changesets/git/-/git-3.0.4.tgz",
|
||||
"integrity": "sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@changesets/errors": "^0.2.0",
|
||||
"@manypkg/get-packages": "^1.1.3",
|
||||
"is-subdir": "^1.1.1",
|
||||
"micromatch": "^4.0.8",
|
||||
"spawndamnit": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@changesets/logger": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@changesets/logger/-/logger-0.1.1.tgz",
|
||||
"integrity": "sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"picocolors": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@changesets/parse": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@changesets/parse/-/parse-0.4.2.tgz",
|
||||
"integrity": "sha512-Uo5MC5mfg4OM0jU3up66fmSn6/NE9INK+8/Vn/7sMVcdWg46zfbvvUSjD9EMonVqPi9fbrJH9SXHn48Tr1f2yA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@changesets/types": "^6.1.0",
|
||||
"js-yaml": "^4.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@changesets/pre": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@changesets/pre/-/pre-2.0.2.tgz",
|
||||
"integrity": "sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@changesets/errors": "^0.2.0",
|
||||
"@changesets/types": "^6.1.0",
|
||||
"@manypkg/get-packages": "^1.1.3",
|
||||
"fs-extra": "^7.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@changesets/read": {
|
||||
"version": "0.6.6",
|
||||
"resolved": "https://registry.npmjs.org/@changesets/read/-/read-0.6.6.tgz",
|
||||
"integrity": "sha512-P5QaN9hJSQQKJShzzpBT13FzOSPyHbqdoIBUd2DJdgvnECCyO6LmAOWSV+O8se2TaZJVwSXjL+v9yhb+a9JeJg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@changesets/git": "^3.0.4",
|
||||
"@changesets/logger": "^0.1.1",
|
||||
"@changesets/parse": "^0.4.2",
|
||||
"@changesets/types": "^6.1.0",
|
||||
"fs-extra": "^7.0.1",
|
||||
"p-filter": "^2.1.0",
|
||||
"picocolors": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@changesets/should-skip-package": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@changesets/should-skip-package/-/should-skip-package-0.1.2.tgz",
|
||||
"integrity": "sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@changesets/types": "^6.1.0",
|
||||
"@manypkg/get-packages": "^1.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@changesets/types": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@changesets/types/-/types-6.1.0.tgz",
|
||||
"integrity": "sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@changesets/write": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@changesets/write/-/write-0.4.0.tgz",
|
||||
"integrity": "sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@changesets/types": "^6.1.0",
|
||||
"fs-extra": "^7.0.1",
|
||||
"human-id": "^4.1.1",
|
||||
"prettier": "^2.7.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@colors/colors": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz",
|
||||
@@ -3665,45 +3413,6 @@
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/external-editor": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz",
|
||||
"integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chardet": "^2.1.1",
|
||||
"iconv-lite": "^0.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/external-editor/node_modules/iconv-lite": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
|
||||
"integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/balanced-match": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
|
||||
@@ -3877,119 +3586,6 @@
|
||||
"integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@manypkg/find-root": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@manypkg/find-root/-/find-root-1.1.0.tgz",
|
||||
"integrity": "sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"@types/node": "^12.7.1",
|
||||
"find-up": "^4.1.0",
|
||||
"fs-extra": "^8.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@manypkg/find-root/node_modules/@types/node": {
|
||||
"version": "12.20.55",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz",
|
||||
"integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@manypkg/find-root/node_modules/fs-extra": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
|
||||
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^4.0.0",
|
||||
"universalify": "^0.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6 <7 || >=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@manypkg/get-packages": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@manypkg/get-packages/-/get-packages-1.1.3.tgz",
|
||||
"integrity": "sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"@changesets/types": "^4.0.1",
|
||||
"@manypkg/find-root": "^1.1.0",
|
||||
"fs-extra": "^8.1.0",
|
||||
"globby": "^11.0.0",
|
||||
"read-yaml-file": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@manypkg/get-packages/node_modules/@changesets/types": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@changesets/types/-/types-4.1.0.tgz",
|
||||
"integrity": "sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@manypkg/get-packages/node_modules/fs-extra": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
|
||||
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^4.0.0",
|
||||
"universalify": "^0.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6 <7 || >=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@manypkg/get-packages/node_modules/globby": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
|
||||
"integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"array-union": "^2.1.0",
|
||||
"dir-glob": "^3.0.1",
|
||||
"fast-glob": "^3.2.9",
|
||||
"ignore": "^5.2.0",
|
||||
"merge2": "^1.4.1",
|
||||
"slash": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@manypkg/get-packages/node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/@manypkg/get-packages/node_modules/slash": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
||||
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/node-pre-gyp": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-2.0.3.tgz",
|
||||
@@ -7771,6 +7367,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/marked": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/marked/-/marked-5.0.2.tgz",
|
||||
"integrity": "sha512-OucS4KMHhFzhz27KxmWg7J+kIYqyqoW5kdIEI319hqARQQUTqhao3M/F+uFnDXD0Rg72iDDZxZNxq5gvctmLlg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/mocha": {
|
||||
"version": "10.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz",
|
||||
@@ -9159,16 +8762,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/array-union": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
|
||||
"integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/arraybuffer.prototype.slice": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz",
|
||||
@@ -9487,19 +9080,6 @@
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/better-path-resolve": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/better-path-resolve/-/better-path-resolve-1.0.0.tgz",
|
||||
"integrity": "sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-windows": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/better-sqlite3": {
|
||||
"version": "12.6.2",
|
||||
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.6.2.tgz",
|
||||
@@ -10266,22 +9846,6 @@
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"node_modules/ci-info": {
|
||||
"version": "3.9.0",
|
||||
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
|
||||
"integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/sibiraj-s"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cjs-module-lexer": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz",
|
||||
@@ -11126,16 +10690,6 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-indent": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz",
|
||||
"integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
@@ -11167,29 +10721,6 @@
|
||||
"integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/dir-glob": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
|
||||
"integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-type": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/dir-glob/node_modules/path-type": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
|
||||
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-serializer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||
@@ -11456,43 +10987,6 @@
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/enquirer": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz",
|
||||
"integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-colors": "^4.1.1",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/enquirer/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/enquirer/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
@@ -12230,13 +11724,6 @@
|
||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/extendable-error": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/extendable-error/-/extendable-error-0.1.7.tgz",
|
||||
"integrity": "sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/extract-zip": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
|
||||
@@ -12752,21 +12239,6 @@
|
||||
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fs-extra": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz",
|
||||
"integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.1.2",
|
||||
"jsonfile": "^4.0.0",
|
||||
"universalify": "^0.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6 <7 || >=8"
|
||||
}
|
||||
},
|
||||
"node_modules/fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
@@ -13577,16 +13049,6 @@
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/human-id": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/human-id/-/human-id-4.1.3.tgz",
|
||||
"integrity": "sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"human-id": "dist/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/human-signals": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz",
|
||||
@@ -14609,19 +14071,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-subdir": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/is-subdir/-/is-subdir-1.2.0.tgz",
|
||||
"integrity": "sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"better-path-resolve": "1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/is-symbol": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz",
|
||||
@@ -15122,16 +14571,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jsonfile": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
|
||||
"integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optionalDependencies": {
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonwebtoken": {
|
||||
"version": "9.0.3",
|
||||
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
|
||||
@@ -15883,13 +15322,6 @@
|
||||
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.startcase": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz",
|
||||
"integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.truncate": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz",
|
||||
@@ -16233,6 +15665,18 @@
|
||||
"markdown-it": "bin/markdown-it.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "17.0.3",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-17.0.3.tgz",
|
||||
"integrity": "sha512-jt1v2ObpyOKR8p4XaUJVk3YWRJ5n+i4+rjQopxvV32rSndTJXvIzuUdWWIy/1pFQMkQmvTXawzDNqOH/CUmx6A==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/marky": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/marky/-/marky-1.3.0.tgz",
|
||||
@@ -16831,16 +16275,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mri": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
|
||||
"integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
@@ -17964,13 +17398,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/outdent": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/outdent/-/outdent-0.5.0.tgz",
|
||||
"integrity": "sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/own-keys": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
|
||||
@@ -17989,29 +17416,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/p-filter": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-filter/-/p-filter-2.1.0.tgz",
|
||||
"integrity": "sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-map": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/p-filter/node_modules/p-map": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz",
|
||||
"integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/p-limit": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||
@@ -18160,16 +17564,6 @@
|
||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
||||
"license": "BlueOak-1.0.0"
|
||||
},
|
||||
"node_modules/package-manager-detector": {
|
||||
"version": "0.2.11",
|
||||
"resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.11.tgz",
|
||||
"integrity": "sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"quansync": "^0.2.7"
|
||||
}
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||
@@ -18679,22 +18073,6 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "2.8.8",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
|
||||
"integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"prettier": "bin-prettier.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/pretty-ms": {
|
||||
"version": "9.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz",
|
||||
@@ -18960,23 +18338,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/quansync": {
|
||||
"version": "0.2.11",
|
||||
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
|
||||
"integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/sxzz"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/queue-microtask": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
@@ -19156,42 +18517,6 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/read-yaml-file": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/read-yaml-file/-/read-yaml-file-1.1.0.tgz",
|
||||
"integrity": "sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.1.5",
|
||||
"js-yaml": "^3.6.1",
|
||||
"pify": "^4.0.1",
|
||||
"strip-bom": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/read-yaml-file/node_modules/pify": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
|
||||
"integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/read-yaml-file/node_modules/strip-bom": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
|
||||
"integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
|
||||
@@ -20584,17 +19909,6 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/spawndamnit": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/spawndamnit/-/spawndamnit-3.0.1.tgz",
|
||||
"integrity": "sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==",
|
||||
"dev": true,
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.5",
|
||||
"signal-exit": "^4.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/spdx-correct": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz",
|
||||
@@ -21246,19 +20560,6 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/term-size": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz",
|
||||
"integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/terminal-link": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-4.0.0.tgz",
|
||||
@@ -21914,16 +21215,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/universalify": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
|
||||
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
|
||||
+1
-4
@@ -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.66.0",
|
||||
"version": "3.69.0",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"workspaces": [
|
||||
".",
|
||||
@@ -437,8 +437,6 @@
|
||||
"publish:marketplace:prerelease": "vsce publish --allow-package-secrets sendgrid --pre-release && ovsx publish --pre-release",
|
||||
"publish:marketplace:nightly": "node ./scripts/publish-nightly.mjs",
|
||||
"prepare": "npx husky",
|
||||
"changeset": "changeset",
|
||||
"version-packages": "changeset version",
|
||||
"docs": "cd docs && npm run dev",
|
||||
"docs:check-links": "cd docs && npm run check",
|
||||
"docs:rename-file": "cd docs && npm run rename",
|
||||
@@ -462,7 +460,6 @@
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.3.14",
|
||||
"@bufbuild/buf": "^1.54.0",
|
||||
"@changesets/cli": "^2.27.12",
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@types/chai": "^5.0.1",
|
||||
"@types/clone-deep": "^4.0.4",
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
|
||||
import "cline/common.proto";
|
||||
|
||||
option go_package = "github.com/cline/grpc-go/cline";
|
||||
option java_multiple_files = true;
|
||||
option java_package = "bot.cline.proto";
|
||||
|
||||
service DictationService {
|
||||
rpc startRecording(EmptyRequest) returns (RecordingResult);
|
||||
rpc stopRecording(EmptyRequest) returns (RecordedAudio);
|
||||
rpc cancelRecording(EmptyRequest) returns (RecordingResult);
|
||||
rpc getRecordingStatus(EmptyRequest) returns (RecordingStatus);
|
||||
rpc transcribeAudio(TranscribeAudioRequest) returns (Transcription);
|
||||
}
|
||||
|
||||
message TranscribeAudioRequest {
|
||||
string audio_base64 = 2;
|
||||
string language = 3;
|
||||
}
|
||||
|
||||
message RecordingResult {
|
||||
bool success = 1;
|
||||
string error = 2;
|
||||
}
|
||||
|
||||
message RecordedAudio {
|
||||
bool success = 1;
|
||||
string audio_base64 = 2;
|
||||
string error = 3;
|
||||
}
|
||||
|
||||
message RecordingStatus {
|
||||
bool is_recording = 1;
|
||||
double duration_seconds = 2;
|
||||
string error = 3;
|
||||
}
|
||||
|
||||
message Transcription {
|
||||
string text = 1;
|
||||
string error = 2;
|
||||
}
|
||||
@@ -21,6 +21,8 @@ service ModelsService {
|
||||
rpc refreshOpenRouterModelsRpc(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
// Refreshes and returns recommended and free Cline models
|
||||
rpc refreshClineRecommendedModelsRpc(EmptyRequest) returns (ClineRecommendedModelsResponse);
|
||||
// Refreshes and returns Cline provider models
|
||||
rpc refreshClineModelsRpc(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
// Refreshes and returns Hugging Face models
|
||||
rpc refreshHuggingFaceModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
// Refreshes and returns OpenAI models
|
||||
@@ -283,6 +285,8 @@ message ModelsApiOptions {
|
||||
optional OcaModelInfo plan_mode_oca_model_info = 132;
|
||||
optional string plan_mode_aihubmix_model_id = 133;
|
||||
optional OpenAiCompatibleModelInfo plan_mode_aihubmix_model_info = 134;
|
||||
optional string plan_mode_cline_model_id = 135;
|
||||
optional OpenRouterModelInfo plan_mode_cline_model_info = 136;
|
||||
|
||||
// Act mode configurations
|
||||
optional ApiProvider act_mode_api_provider = 200;
|
||||
@@ -320,6 +324,8 @@ message ModelsApiOptions {
|
||||
optional OcaModelInfo act_mode_oca_model_info = 232;
|
||||
optional string act_mode_aihubmix_model_id = 233;
|
||||
optional OpenAiCompatibleModelInfo act_mode_aihubmix_model_info = 234;
|
||||
optional string act_mode_cline_model_id = 235;
|
||||
optional OpenRouterModelInfo act_mode_cline_model_info = 236;
|
||||
}
|
||||
|
||||
// Request for updating API configuration (legacy - uses combined configuration)
|
||||
@@ -634,6 +640,8 @@ message ModelsApiConfiguration {
|
||||
optional OpenAiCompatibleModelInfo plan_mode_aihubmix_model_info = 137;
|
||||
optional string plan_mode_nous_research_model_id = 138;
|
||||
optional string gemini_plan_mode_thinking_level = 139;
|
||||
optional string plan_mode_cline_model_id = 140;
|
||||
optional OpenRouterModelInfo plan_mode_cline_model_info = 141;
|
||||
|
||||
// Act mode configurations
|
||||
optional ApiProvider act_mode_api_provider = 200;
|
||||
@@ -676,4 +684,6 @@ message ModelsApiConfiguration {
|
||||
optional OpenAiCompatibleModelInfo act_mode_aihubmix_model_info = 237;
|
||||
optional string act_mode_nous_research_model_id = 238;
|
||||
optional string gemini_act_mode_thinking_level = 239;
|
||||
optional string act_mode_cline_model_id = 240;
|
||||
optional OpenRouterModelInfo act_mode_cline_model_info = 241;
|
||||
}
|
||||
|
||||
@@ -255,7 +255,6 @@ message Settings {
|
||||
optional bool cline_web_tools_enabled = 144;
|
||||
optional string preferred_language = 145;
|
||||
optional PlanActMode mode = 147;
|
||||
optional DictationSettings dictation_settings = 148;
|
||||
optional FocusChainSettings focus_chain_settings = 149;
|
||||
optional string custom_prompt = 150;
|
||||
optional bool subagents_enabled = 153;
|
||||
@@ -280,13 +279,12 @@ message Settings {
|
||||
optional bool auto_approve_all_toggled = 174;
|
||||
optional bool double_check_completion_enabled = 176;
|
||||
map<string, string> open_ai_headers = 177;
|
||||
optional string plan_mode_cline_model_id = 178;
|
||||
optional OpenRouterModelInfo plan_mode_cline_model_info = 179;
|
||||
optional string act_mode_cline_model_id = 180;
|
||||
optional OpenRouterModelInfo act_mode_cline_model_info = 181;
|
||||
}
|
||||
|
||||
message DictationSettings {
|
||||
bool feature_enabled = 1;
|
||||
bool dictation_enabled = 2;
|
||||
string dictation_language = 3;
|
||||
}
|
||||
message State {
|
||||
string state_json = 1;
|
||||
}
|
||||
@@ -387,6 +385,7 @@ message UpdateTaskSettingsRequest {
|
||||
// Message for updating settings
|
||||
message UpdateSettingsRequest {
|
||||
reserved 15; // was openai_reasoning_effort (moved to mode-scoped reasoning effort)
|
||||
reserved 23; // was dictation_settings (dictation removed)
|
||||
reserved 26; // was hooks_enabled (removed - now always enabled on macOS/Linux)
|
||||
reserved 38; // was skills_enabled (removed - now always enabled)
|
||||
|
||||
@@ -410,7 +409,6 @@ message UpdateSettingsRequest {
|
||||
optional BrowserSettingsUpdate browser_settings = 20;
|
||||
optional string default_terminal_profile = 21;
|
||||
optional bool yolo_mode_toggled = 22;
|
||||
optional DictationSettings dictation_settings = 23;
|
||||
optional bool multi_root_enabled = 25;
|
||||
optional string vscode_terminal_execution_mode = 27;
|
||||
optional int32 max_consecutive_mistakes = 28;
|
||||
|
||||
@@ -82,7 +82,6 @@ function inferProtoType(typeText, fieldName) {
|
||||
// Other types - order matters for substring matching
|
||||
["AutoApprovalSettings", "AutoApprovalSettings"],
|
||||
["BrowserSettings", "BrowserSettings"],
|
||||
["DictationSettings", "DictationSettings"],
|
||||
["FocusChainSettings", "FocusChainSettings"],
|
||||
["OpenaiReasoningEffort", "OpenaiReasoningEffort"],
|
||||
["PlanActMode", "PlanActMode"],
|
||||
|
||||
@@ -11,7 +11,6 @@ import { HookProcessRegistry } from "./core/hooks/HookProcessRegistry"
|
||||
import { StateManager } from "./core/storage/StateManager"
|
||||
import { AgentConfigLoader } from "./core/task/tools/subagent/AgentConfigLoader"
|
||||
import { ExtensionRegistryInfo } from "./registry"
|
||||
import { audioRecordingService } from "./services/dictation/AudioRecordingService"
|
||||
import { ErrorService } from "./services/error"
|
||||
import { featureFlagsService } from "./services/feature-flags"
|
||||
import { getDistinctId } from "./services/logging/distinctId"
|
||||
@@ -152,9 +151,6 @@ async function checkWorktreeAutoOpen(stateManager: StateManager): Promise<void>
|
||||
* Performs cleanup when Cline is deactivated that is common to all platforms.
|
||||
*/
|
||||
export async function tearDown(): Promise<void> {
|
||||
// Clean up audio recording service to ensure no orphaned processes
|
||||
audioRecordingService.cleanup()
|
||||
|
||||
AgentConfigLoader.getInstance()?.dispose()
|
||||
PostHogClientProvider.getInstance().dispose()
|
||||
telemetryService.dispose()
|
||||
|
||||
+12
-3
@@ -253,7 +253,13 @@ function createHandlerForProvider(
|
||||
vsCodeLmModelSelector:
|
||||
mode === "plan" ? options.planModeVsCodeLmModelSelector : options.actModeVsCodeLmModelSelector,
|
||||
})
|
||||
case "cline":
|
||||
case "cline": {
|
||||
const clineModelId =
|
||||
(mode === "plan" ? options.planModeClineModelId : options.actModeClineModelId) ||
|
||||
(mode === "plan" ? options.planModeOpenRouterModelId : options.actModeOpenRouterModelId)
|
||||
const clineModelInfo =
|
||||
(mode === "plan" ? options.planModeClineModelInfo : options.actModeClineModelInfo) ||
|
||||
(mode === "plan" ? options.planModeOpenRouterModelInfo : options.actModeOpenRouterModelInfo)
|
||||
return new ClineHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
clineAccountId: options.clineAccountId,
|
||||
@@ -263,9 +269,10 @@ function createHandlerForProvider(
|
||||
thinkingBudgetTokens:
|
||||
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
|
||||
openRouterProviderSorting: options.openRouterProviderSorting,
|
||||
openRouterModelId: mode === "plan" ? options.planModeOpenRouterModelId : options.actModeOpenRouterModelId,
|
||||
openRouterModelInfo: mode === "plan" ? options.planModeOpenRouterModelInfo : options.actModeOpenRouterModelInfo,
|
||||
openRouterModelId: clineModelId,
|
||||
openRouterModelInfo: clineModelInfo,
|
||||
})
|
||||
}
|
||||
case "litellm":
|
||||
return new LiteLlmHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
@@ -428,6 +435,8 @@ function createHandlerForProvider(
|
||||
minimaxApiKey: options.minimaxApiKey,
|
||||
minimaxApiLine: options.minimaxApiLine,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
thinkingBudgetTokens:
|
||||
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
|
||||
})
|
||||
case "hicap":
|
||||
return new HicapHandler({
|
||||
|
||||
@@ -56,18 +56,22 @@ export class MinimaxHandler implements ApiHandler {
|
||||
// Tools are available only when native tools are enabled
|
||||
const nativeToolsOn = tools?.length && tools?.length > 0
|
||||
|
||||
const budget_tokens = this.options.thinkingBudgetTokens || 0
|
||||
const reasoningOn = (model.info.supportsReasoning ?? false) && budget_tokens !== 0
|
||||
|
||||
// MiniMax M2 uses Anthropic API format
|
||||
// Note: According to MiniMax docs, some Anthropic parameters like 'thinking' are ignored
|
||||
// but we'll include the standard Anthropic streaming pattern for consistency
|
||||
const stream: AnthropicStream<Anthropic.RawMessageStreamEvent> = await client.messages.create({
|
||||
model: model.id,
|
||||
max_tokens: model.info.maxTokens || 8192,
|
||||
temperature: 1.0, // MiniMax recommends 1.0, range is (0.0, 1.0]
|
||||
system: [{ text: systemPrompt, type: "text" }],
|
||||
messages,
|
||||
stream: true,
|
||||
tools: nativeToolsOn ? (tools as AnthropicTool[]) : undefined,
|
||||
tool_choice: nativeToolsOn ? { type: "any" } : undefined,
|
||||
thinking: reasoningOn ? { type: "enabled", budget_tokens: budget_tokens } : undefined,
|
||||
// "Thinking isn't compatible with temperature, top_p, or top_k modifications"
|
||||
temperature: reasoningOn ? undefined : 1.0, // MiniMax recommends 1.0, range is (0.0, 1.0]
|
||||
// NOTE: Forcing tool use when tools are provided will result in error when thinking is also enabled.
|
||||
tool_choice: nativeToolsOn && !reasoningOn ? { type: "any" } : undefined,
|
||||
})
|
||||
|
||||
const lastStartedToolCall = { id: "", name: "", arguments: "" }
|
||||
|
||||
@@ -164,7 +164,7 @@ export class OpenAiCodexHandler implements ApiHandler {
|
||||
model: model.id,
|
||||
input: formattedInput,
|
||||
stream: true,
|
||||
store: !previousResponseId,
|
||||
store: false,
|
||||
instructions: systemPrompt,
|
||||
...(previousResponseId ? { previous_response_id: previousResponseId } : {}),
|
||||
...(includeReasoning ? { include: ["reasoning.encrypted_content"] } : {}),
|
||||
|
||||
@@ -80,6 +80,7 @@ export async function createOpenRouterStream(
|
||||
case "minimax/minimax-m2":
|
||||
case "minimax/minimax-m2.1":
|
||||
case "minimax/minimax-m2.1-lightning":
|
||||
case "minimax/minimax-m2.5":
|
||||
openAiMessages[0] = {
|
||||
role: "system",
|
||||
content: [
|
||||
@@ -115,37 +116,7 @@ export async function createOpenRouterStream(
|
||||
break
|
||||
}
|
||||
|
||||
// Not sure how openrouter defaults max tokens when no value is provided, but the anthropic api requires this value and since they offer both 4096 and 8192 variants, we should ensure 8192.
|
||||
// (models usually default to max tokens allowed)
|
||||
let maxTokens: number | undefined
|
||||
switch (model.id) {
|
||||
case "anthropic/claude-opus-4.6":
|
||||
case "anthropic/claude-haiku-4.5":
|
||||
case "anthropic/claude-4.5-haiku":
|
||||
case "anthropic/claude-sonnet-4.6":
|
||||
case "anthropic/claude-4.6-sonnet":
|
||||
case "anthropic/claude-sonnet-4.5":
|
||||
case "anthropic/claude-4.5-sonnet":
|
||||
case "anthropic/claude-sonnet-4":
|
||||
case "anthropic/claude-opus-4.5":
|
||||
case "anthropic/claude-opus-4.1":
|
||||
case "anthropic/claude-opus-4":
|
||||
case "anthropic/claude-3.7-sonnet":
|
||||
case "anthropic/claude-3.7-sonnet:beta":
|
||||
case "anthropic/claude-3.7-sonnet:thinking":
|
||||
case "anthropic/claude-3-7-sonnet":
|
||||
case "anthropic/claude-3-7-sonnet:beta":
|
||||
case "anthropic/claude-3.5-sonnet":
|
||||
case "anthropic/claude-3.5-sonnet:beta":
|
||||
case "anthropic/claude-3.5-sonnet-20240620":
|
||||
case "anthropic/claude-3.5-sonnet-20240620:beta":
|
||||
case "anthropic/claude-3-5-haiku":
|
||||
case "anthropic/claude-3-5-haiku:beta":
|
||||
case "anthropic/claude-3-5-haiku-20241022":
|
||||
case "anthropic/claude-3-5-haiku-20241022:beta":
|
||||
maxTokens = 8_192
|
||||
break
|
||||
}
|
||||
const maxTokens = model.info.maxTokens || undefined
|
||||
|
||||
let temperature: number | undefined = 0
|
||||
let topP: number | undefined
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { expect } from "chai"
|
||||
import { checkContextWindowExceededError } from "../context-error-handling"
|
||||
|
||||
describe("checkContextWindowExceededError", () => {
|
||||
it("detects OpenRouter context errors using structured status", () => {
|
||||
const error = Object.assign(
|
||||
new Error(
|
||||
"This endpoint's maximum context length is 204800 tokens. However, you requested about 244027 tokens.",
|
||||
),
|
||||
{
|
||||
status: 400,
|
||||
},
|
||||
)
|
||||
|
||||
expect(checkContextWindowExceededError(error)).to.equal(true)
|
||||
})
|
||||
|
||||
it("detects OpenRouter JSON-encoded status + context length errors", () => {
|
||||
const error = new Error(
|
||||
'OpenRouter Mid-Stream Error: {"status":400,"message":"This endpoint\'s maximum context length is 200000 tokens"}',
|
||||
)
|
||||
|
||||
expect(checkContextWindowExceededError(error)).to.equal(true)
|
||||
})
|
||||
|
||||
it("does not classify unrelated 400 errors as context window failures", () => {
|
||||
const error = new Error("OpenRouter API Error 400: Invalid API key")
|
||||
|
||||
expect(checkContextWindowExceededError(error)).to.equal(false)
|
||||
})
|
||||
})
|
||||
@@ -13,11 +13,15 @@ export function checkContextWindowExceededError(error: unknown): boolean {
|
||||
|
||||
function checkIsOpenRouterContextWindowError(error: any): boolean {
|
||||
try {
|
||||
// OpenRouter errors can reach us in two shapes:
|
||||
// 1) Direct chunk.error path wrapped as Error with status/code attached.
|
||||
// 2) Mid-stream finish_reason="error" path where JSON is stringified into message.
|
||||
// So we check structured status first, then JSON-encoded status/code in message text.
|
||||
const status = error?.status ?? error?.code ?? error?.error?.status ?? error?.response?.status
|
||||
const message: string = String(error?.message || error?.error?.message || "")
|
||||
|
||||
// There seems to be an issue where the true status code is embedded only in the message itself
|
||||
const statusFromMessage = message.match(/"code":\s*(\d+)/)?.[1]
|
||||
// Handle JSON-encoded errors where status/code is embedded in the message string.
|
||||
const statusFromMessage = message.match(/"code":\s*(\d+)/)?.[1] ?? message.match(/"status":\s*(\d+)/)?.[1]
|
||||
const finalStatus = statusFromMessage || status
|
||||
|
||||
// Known OpenAI/OpenRouter-style signal (code 400 and message includes "context length")
|
||||
|
||||
@@ -27,4 +27,32 @@ describe("parseYamlFrontmatter", () => {
|
||||
expect(result.body).to.equal(input)
|
||||
expect(result.parseError).to.be.a("string")
|
||||
})
|
||||
|
||||
it("rejects YAML custom tags (security: prevents unsafe deserialization)", () => {
|
||||
// !!js/function is the classic RCE vector in js-yaml v3.
|
||||
// With JSON_SCHEMA, any custom tag should be rejected.
|
||||
const input = `---\nfoo: !!js/function 'function(){ return 1 }'\n---\nBody`
|
||||
const result = parseYamlFrontmatter(input)
|
||||
expect(result.hadFrontmatter).to.equal(true)
|
||||
expect(result.data).to.deep.equal({})
|
||||
expect(result.body).to.equal(input)
|
||||
expect(result.parseError).to.be.a("string")
|
||||
})
|
||||
|
||||
it("rejects !!python/object YAML tag", () => {
|
||||
const input = `---\nfoo: !!python/object:os.system 'echo pwned'\n---\nBody`
|
||||
const result = parseYamlFrontmatter(input)
|
||||
expect(result.hadFrontmatter).to.equal(true)
|
||||
expect(result.data).to.deep.equal({})
|
||||
expect(result.parseError).to.be.a("string")
|
||||
})
|
||||
|
||||
it("parses JSON-compatible YAML values correctly", () => {
|
||||
const input = `---\ncount: 42\nenabled: true\ntags:\n - "a"\n - "b"\n---\nContent`
|
||||
const result = parseYamlFrontmatter(input)
|
||||
expect(result.hadFrontmatter).to.equal(true)
|
||||
expect(result.parseError).to.equal(undefined)
|
||||
expect(result.data).to.deep.equal({ count: 42, enabled: true, tags: ["a", "b"] })
|
||||
expect(result.body.trim()).to.equal("Content")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -44,7 +44,7 @@ export function parseYamlFrontmatter(markdown: string): FrontmatterParseResult {
|
||||
|
||||
const [, yamlContent, body] = match
|
||||
try {
|
||||
const data = (yaml.load(yamlContent) as Record<string, unknown>) || {}
|
||||
const data = (yaml.load(yamlContent, { schema: yaml.JSON_SCHEMA }) as Record<string, unknown>) || {}
|
||||
return { data, body, hadFrontmatter: true }
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
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 ".."
|
||||
|
||||
/**
|
||||
* Cancels audio recording without saving or transcribing the audio
|
||||
* @param controller The controller instance
|
||||
* @returns RecordingResult indicating success or failure
|
||||
*/
|
||||
export const cancelRecording = async (controller: Controller): Promise<RecordingResult> => {
|
||||
const taskId = controller.task?.taskId
|
||||
const recordingStatus = audioRecordingService.getRecordingStatus()
|
||||
const recordingDuration = recordingStatus.durationSeconds * 1000 // Convert to milliseconds
|
||||
let errorMessage = ""
|
||||
let isSuccess = true
|
||||
try {
|
||||
const result = await audioRecordingService.cancelRecording()
|
||||
isSuccess = !!result?.success
|
||||
errorMessage = result?.error ?? ""
|
||||
} catch (error) {
|
||||
Logger.error("Error canceling recording:", error)
|
||||
isSuccess = false
|
||||
errorMessage = error instanceof Error ? error.message : "Unknown error occurred"
|
||||
}
|
||||
|
||||
telemetryService.captureVoiceRecordingStopped(taskId, recordingDuration, false, process.platform)
|
||||
return RecordingResult.create({
|
||||
success: isSuccess,
|
||||
error: errorMessage ?? "",
|
||||
})
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { RecordingStatus } from "@shared/proto/cline/dictation"
|
||||
import { audioRecordingService } from "@/services/dictation/AudioRecordingService"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
|
||||
/**
|
||||
* Gets the current recording status
|
||||
* @returns RecordingStatus with current status
|
||||
*/
|
||||
export const getRecordingStatus = async (): Promise<RecordingStatus> => {
|
||||
try {
|
||||
const status = audioRecordingService.getRecordingStatus()
|
||||
|
||||
return RecordingStatus.create({
|
||||
isRecording: status.isRecording,
|
||||
durationSeconds: status.durationSeconds,
|
||||
error: status.error ?? "",
|
||||
})
|
||||
} catch (error) {
|
||||
Logger.error("Error getting recording status:", error)
|
||||
return RecordingStatus.create({
|
||||
isRecording: false,
|
||||
durationSeconds: 0,
|
||||
error: error instanceof Error ? error.message : "Unknown error occurred",
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
import { RecordingResult } from "@shared/proto/cline/dictation"
|
||||
import * as os from "os"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { audioRecordingService } from "@/services/dictation/AudioRecordingService"
|
||||
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 ".."
|
||||
|
||||
/**
|
||||
* Handles the installation of missing dependencies with Cline
|
||||
*/
|
||||
async function handleInstallWithCline(
|
||||
controller: Controller,
|
||||
dependencyName: string,
|
||||
installCommand: string,
|
||||
platform: string,
|
||||
): Promise<void> {
|
||||
const platformName = platform === "darwin" ? "macOS" : platform === "win32" ? "Windows" : "Linux"
|
||||
const installTask = `Please install ${dependencyName} for voice recording on ${platformName}.\n\nRun this command:\n\`\`\`bash\n${installCommand}\n\`\`\`\n\nThis will enable voice recording functionality in Cline.`
|
||||
|
||||
// Clear any existing task and start the installation task
|
||||
await controller.clearTask()
|
||||
await controller.postStateToWebview()
|
||||
await controller.initTask(installTask)
|
||||
|
||||
Logger.log(`[handleInstallWithCline] Started task to install ${dependencyName}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles copying the installation command to clipboard
|
||||
*/
|
||||
async function handleCopyCommand(installCommand: string): Promise<void> {
|
||||
await HostProvider.env.clipboardWriteText({ value: installCommand })
|
||||
await HostProvider.window.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: `Installation command copied to clipboard: ${installCommand}`,
|
||||
options: { items: [] },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles missing dependency notification and user action
|
||||
*/
|
||||
async function handleMissingDependency(
|
||||
controller: Controller,
|
||||
platform: string,
|
||||
config: (typeof AUDIO_PROGRAM_CONFIG)[keyof typeof AUDIO_PROGRAM_CONFIG],
|
||||
): Promise<void> {
|
||||
const installWithCline = "Install with Cline"
|
||||
const installManually = "Copy Command"
|
||||
const dismiss = "Dismiss"
|
||||
|
||||
const action = await HostProvider.window.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: `${config.dependencyName} is required for voice recording. ${config.installDescription}`,
|
||||
options: { items: [installWithCline, installManually, dismiss] },
|
||||
})
|
||||
|
||||
if (action.selectedOption === installWithCline) {
|
||||
await handleInstallWithCline(controller, config.dependencyName, config.installCommand, platform)
|
||||
} else if (action.selectedOption === installManually) {
|
||||
await handleCopyCommand(config.installCommand)
|
||||
}
|
||||
// If dismiss, do nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles sign-in errors for dictation
|
||||
*/
|
||||
async function handleSignInError(controller: Controller, errorMessage: string): Promise<void> {
|
||||
const signInAction = "Sign in to Cline"
|
||||
const action = await HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Voice recording error: ${errorMessage}`,
|
||||
options: { items: [signInAction] },
|
||||
})
|
||||
|
||||
if (action.selectedOption === signInAction) {
|
||||
await controller.authService.createAuthRequest()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a generic error message
|
||||
*/
|
||||
async function showGenericError(errorMessage: string): Promise<void> {
|
||||
await HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Voice recording error: ${errorMessage}`,
|
||||
options: { items: [] },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the recording error is due to missing dependencies
|
||||
*/
|
||||
function isMissingDependencyError(
|
||||
error: string | undefined,
|
||||
config: (typeof AUDIO_PROGRAM_CONFIG)[keyof typeof AUDIO_PROGRAM_CONFIG] | undefined,
|
||||
): boolean {
|
||||
return !!(error && config && error.includes(config.error))
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts audio recording using the Extension Host
|
||||
* @param controller The controller instance
|
||||
* @returns RecordingResult with success status
|
||||
*/
|
||||
export const startRecording = async (controller: Controller): Promise<RecordingResult> => {
|
||||
const taskId = controller.task?.taskId
|
||||
|
||||
try {
|
||||
// Verify user authentication
|
||||
const userInfo = controller.authService.getInfo()
|
||||
if (!userInfo?.user?.uid) {
|
||||
throw new Error("Please sign in to your Cline Account to use Dictation.")
|
||||
}
|
||||
|
||||
// Attempt to start recording
|
||||
const result = await audioRecordingService.startRecording()
|
||||
|
||||
// Handle successful recording start
|
||||
if (result.success) {
|
||||
telemetryService.captureVoiceRecordingStarted(taskId, process.platform)
|
||||
return RecordingResult.create({
|
||||
success: true,
|
||||
error: "",
|
||||
})
|
||||
}
|
||||
|
||||
// Check if the error is due to missing dependencies
|
||||
const platform = os.platform() as keyof typeof AUDIO_PROGRAM_CONFIG
|
||||
const config = AUDIO_PROGRAM_CONFIG[platform]
|
||||
|
||||
if (isMissingDependencyError(result.error, config)) {
|
||||
// Don't await - show dialog asynchronously so frontend gets immediate response
|
||||
handleMissingDependency(controller, platform, config)
|
||||
}
|
||||
|
||||
return RecordingResult.create({
|
||||
success: false,
|
||||
error: result.error || "",
|
||||
})
|
||||
} catch (error) {
|
||||
Logger.error("Error starting recording:", error)
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"
|
||||
|
||||
// Handle different error types
|
||||
if (errorMessage.includes("sign in")) {
|
||||
// Don't await - show dialog asynchronously so frontend gets immediate response
|
||||
handleSignInError(controller, errorMessage)
|
||||
} else {
|
||||
// Don't await - show dialog asynchronously so frontend gets immediate response
|
||||
showGenericError(errorMessage)
|
||||
}
|
||||
|
||||
return RecordingResult.create({
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
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 ".."
|
||||
|
||||
/**
|
||||
* Stops audio recording and returns the recorded audio
|
||||
* @param controller The controller instance
|
||||
* @returns RecordedAudio with audio data
|
||||
*/
|
||||
export const stopRecording = async (controller: Controller): Promise<RecordedAudio> => {
|
||||
const taskId = controller.task?.taskId
|
||||
const recordingStatus = audioRecordingService.getRecordingStatus()
|
||||
const recordingDuration = recordingStatus.durationSeconds * 1000 // Convert to milliseconds
|
||||
|
||||
try {
|
||||
const result = await audioRecordingService.stopRecording()
|
||||
|
||||
telemetryService.captureVoiceRecordingStopped(taskId, recordingDuration, result.success, process.platform)
|
||||
|
||||
return RecordedAudio.create({
|
||||
success: result.success,
|
||||
audioBase64: result.audioBase64 ?? "",
|
||||
error: result.error ?? "",
|
||||
})
|
||||
} catch (error) {
|
||||
Logger.error("Error stopping recording:", error)
|
||||
|
||||
telemetryService.captureVoiceRecordingStopped(taskId, recordingDuration, false, process.platform)
|
||||
|
||||
return RecordedAudio.create({
|
||||
success: false,
|
||||
audioBase64: "",
|
||||
error: error instanceof Error ? error.message : "Unknown error occurred",
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
import { TranscribeAudioRequest, Transcription } from "@shared/proto/cline/dictation"
|
||||
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 ".."
|
||||
|
||||
/**
|
||||
* Transcribes audio using Cline transcription service
|
||||
* @param controller The controller instance
|
||||
* @param request TranscribeAudioRequest containing base64 audio data
|
||||
* @returns Transcription with transcribed text or error
|
||||
*/
|
||||
export const transcribeAudio = async (controller: Controller, request: TranscribeAudioRequest): Promise<Transcription> => {
|
||||
const taskId = controller.task?.taskId
|
||||
const startTime = Date.now()
|
||||
|
||||
// Capture telemetry for transcription start
|
||||
telemetryService.captureVoiceTranscriptionStarted(taskId, request.language ?? "en")
|
||||
|
||||
try {
|
||||
// Transcribe the audio
|
||||
const result = await getVoiceTranscriptionService().transcribeAudio(request.audioBase64, request.language ?? "en")
|
||||
const durationMs = Date.now() - startTime
|
||||
|
||||
if (result.error) {
|
||||
let errorType = "api_error"
|
||||
if (result.error.includes("Authentication failed")) {
|
||||
errorType = "invalid_jwt_token"
|
||||
} else if (result.error.includes("Insufficient credits")) {
|
||||
errorType = "insufficient_credits"
|
||||
} else if (result.error.includes("Invalid audio format")) {
|
||||
errorType = "invalid_audio_format"
|
||||
} else if (result.error.includes("No internet connection")) {
|
||||
errorType = "no_internet"
|
||||
} else if (result.error.includes("Cannot connect")) {
|
||||
errorType = "connection_error"
|
||||
} else if (result.error.includes("Connection timed out")) {
|
||||
errorType = "timeout_error"
|
||||
} else if (result.error.includes("Network error")) {
|
||||
errorType = "network_error"
|
||||
}
|
||||
|
||||
telemetryService.captureVoiceTranscriptionError(taskId, errorType, result.error, durationMs)
|
||||
|
||||
// Use the error message directly from the service as it's already user-friendly
|
||||
const errorMessage = result.error
|
||||
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: errorMessage,
|
||||
})
|
||||
} else if (result.text) {
|
||||
telemetryService.captureVoiceTranscriptionCompleted(taskId, result.text.length, durationMs, request.language ?? "en")
|
||||
}
|
||||
|
||||
return Transcription.create({
|
||||
text: result.text ?? "",
|
||||
error: result.error ?? "",
|
||||
})
|
||||
} catch (error) {
|
||||
Logger.error("Error transcribing audio:", error)
|
||||
const durationMs = Date.now() - startTime
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"
|
||||
|
||||
telemetryService.captureVoiceTranscriptionError(taskId, "unexpected_error", errorMessage, durationMs)
|
||||
|
||||
return Transcription.create({
|
||||
text: "",
|
||||
error: errorMessage,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,8 @@ export async function createHook(
|
||||
// Ensure directory exists
|
||||
await fs.mkdir(hooksDir, { recursive: true })
|
||||
|
||||
const hookPath = path.join(hooksDir, hookName)
|
||||
const hookFileName = process.platform === "win32" ? `${hookName}.ps1` : hookName
|
||||
const hookPath = path.join(hooksDir, hookFileName)
|
||||
|
||||
// Check if already exists
|
||||
try {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { DeleteHookRequest, DeleteHookResponse } from "@shared/proto/cline/file"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { HookDiscoveryCache } from "../../hooks/HookDiscoveryCache"
|
||||
import { resolveHooksDirectory } from "../../hooks/utils"
|
||||
import { resolveExistingHookPath, resolveHooksDirectory } from "../../hooks/utils"
|
||||
import { Controller } from ".."
|
||||
import { refreshHooks } from "./refreshHooks"
|
||||
|
||||
@@ -15,14 +14,11 @@ export async function deleteHook(
|
||||
|
||||
// Determine hook path
|
||||
const hooksDir = await resolveHooksDirectory(isGlobal, workspaceName, globalHooksDirOverride)
|
||||
|
||||
const hookPath = path.join(hooksDir, hookName)
|
||||
const hookPath = await resolveExistingHookPath(hooksDir, hookName)
|
||||
|
||||
// Verify hook exists before attempting deletion
|
||||
try {
|
||||
await fs.stat(hookPath)
|
||||
} catch {
|
||||
throw new Error(`Hook ${hookName} does not exist at ${hookPath}`)
|
||||
if (!hookPath) {
|
||||
throw new Error(`Hook ${hookName} does not exist in ${hooksDir}`)
|
||||
}
|
||||
|
||||
// Delete the hook file
|
||||
|
||||
@@ -3,7 +3,7 @@ import fs from "fs/promises"
|
||||
import os from "os"
|
||||
import path from "path"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { VALID_HOOK_TYPES } from "../../hooks/utils"
|
||||
import { resolveExistingHookPath, VALID_HOOK_TYPES } from "../../hooks/utils"
|
||||
import { Controller } from ".."
|
||||
|
||||
export async function refreshHooks(
|
||||
@@ -17,20 +17,15 @@ export async function refreshHooks(
|
||||
// Collect global hooks
|
||||
const globalHooks: HookInfo[] = []
|
||||
for (const hookName of VALID_HOOK_TYPES) {
|
||||
const hookPath = path.join(globalHooksDir, hookName)
|
||||
try {
|
||||
const stat = await fs.stat(hookPath)
|
||||
if (stat.isFile()) {
|
||||
globalHooks.push(
|
||||
HookInfo.create({
|
||||
name: hookName,
|
||||
enabled: await isExecutable(hookPath),
|
||||
absolutePath: hookPath,
|
||||
}),
|
||||
)
|
||||
}
|
||||
} catch {
|
||||
// File doesn't exist, skip
|
||||
const hookPath = await resolveExistingHookPath(globalHooksDir, hookName)
|
||||
if (hookPath) {
|
||||
globalHooks.push(
|
||||
HookInfo.create({
|
||||
name: hookName,
|
||||
enabled: await isExecutable(hookPath),
|
||||
absolutePath: hookPath,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,20 +38,15 @@ export async function refreshHooks(
|
||||
const hooks: HookInfo[] = []
|
||||
|
||||
for (const hookName of VALID_HOOK_TYPES) {
|
||||
const hookPath = path.join(workspaceHooksDir, hookName)
|
||||
try {
|
||||
const stat = await fs.stat(hookPath)
|
||||
if (stat.isFile()) {
|
||||
hooks.push(
|
||||
HookInfo.create({
|
||||
name: hookName,
|
||||
enabled: await isExecutable(hookPath),
|
||||
absolutePath: hookPath,
|
||||
}),
|
||||
)
|
||||
}
|
||||
} catch {
|
||||
// File doesn't exist, skip
|
||||
const hookPath = await resolveExistingHookPath(workspaceHooksDir, hookName)
|
||||
if (hookPath) {
|
||||
hooks.push(
|
||||
HookInfo.create({
|
||||
name: hookName,
|
||||
enabled: await isExecutable(hookPath),
|
||||
absolutePath: hookPath,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +71,8 @@ export async function refreshHooks(
|
||||
async function isExecutable(filePath: string): Promise<boolean> {
|
||||
if (process.platform === "win32") {
|
||||
// On Windows, files are "enabled" if they exist
|
||||
// TODO(PR-9552 follow-up): Replace this temporary file-exists behavior
|
||||
// with JSON-backed cross-platform hook enablement state.
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { ToggleHookRequest, ToggleHookResponse } from "@shared/proto/cline/file"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { HookDiscoveryCache } from "../../hooks/HookDiscoveryCache"
|
||||
import { resolveHooksDirectory } from "../../hooks/utils"
|
||||
import { resolveExistingHookPath, resolveHooksDirectory } from "../../hooks/utils"
|
||||
import { Controller } from ".."
|
||||
import { refreshHooks } from "./refreshHooks"
|
||||
|
||||
@@ -15,20 +14,21 @@ export async function toggleHook(
|
||||
|
||||
// Determine hook path
|
||||
const hooksDir = await resolveHooksDirectory(isGlobal, workspaceName, globalHooksDirOverride)
|
||||
|
||||
const hookPath = path.join(hooksDir, hookName)
|
||||
const hookPath = await resolveExistingHookPath(hooksDir, hookName)
|
||||
|
||||
// Verify hook exists
|
||||
try {
|
||||
await fs.stat(hookPath)
|
||||
} catch {
|
||||
throw new Error(`Hook ${hookName} does not exist at ${hookPath}`)
|
||||
if (!hookPath) {
|
||||
throw new Error(`Hook ${hookName} does not exist in ${hooksDir}`)
|
||||
}
|
||||
|
||||
// On Windows, we can't use chmod, so we just return the current state
|
||||
// without modifying the file. The frontend will disable the toggle.
|
||||
// TODO(PR-9552 follow-up): Replace this temporary behavior with a
|
||||
// JSON-backed cross-platform enabled/disabled hook state.
|
||||
if (process.platform !== "win32") {
|
||||
// Toggle executable bit (Unix-like systems only)
|
||||
// TODO(PR-9552 follow-up): Revisit chmod-driven enablement semantics
|
||||
// once cross-platform JSON-backed state is implemented.
|
||||
await fs.chmod(hookPath, enabled ? 0o755 : 0o644)
|
||||
}
|
||||
|
||||
|
||||
@@ -851,7 +851,6 @@ export class Controller {
|
||||
const autoApprovalSettings = this.stateManager.getGlobalSettingsKey("autoApprovalSettings")
|
||||
const browserSettings = this.stateManager.getGlobalSettingsKey("browserSettings")
|
||||
const focusChainSettings = this.stateManager.getGlobalSettingsKey("focusChainSettings")
|
||||
const dictationSettings = this.stateManager.getGlobalSettingsKey("dictationSettings")
|
||||
const preferredLanguage = this.stateManager.getGlobalSettingsKey("preferredLanguage")
|
||||
const mode = this.stateManager.getGlobalSettingsKey("mode")
|
||||
const strictPlanModeEnabled = this.stateManager.getGlobalSettingsKey("strictPlanModeEnabled")
|
||||
@@ -919,12 +918,6 @@ export class Controller {
|
||||
const { openAiCodexOAuthManager } = await import("@/integrations/openai-codex/oauth")
|
||||
const openAiCodexIsAuthenticated = await openAiCodexOAuthManager.isAuthenticated()
|
||||
|
||||
// Set feature flag in dictation settings based on platform
|
||||
const updatedDictationSettings = {
|
||||
...dictationSettings,
|
||||
featureEnabled: process.platform === "darwin" || process.platform === "linux", // Enable dictation on macOS and Linux
|
||||
}
|
||||
|
||||
return {
|
||||
version,
|
||||
apiConfiguration,
|
||||
@@ -935,7 +928,6 @@ export class Controller {
|
||||
autoApprovalSettings,
|
||||
browserSettings,
|
||||
focusChainSettings,
|
||||
dictationSettings: updatedDictationSettings,
|
||||
preferredLanguage,
|
||||
mode,
|
||||
strictPlanModeEnabled,
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import * as disk from "@core/storage/disk"
|
||||
import axios from "axios"
|
||||
import { expect } from "chai"
|
||||
import fs from "fs/promises"
|
||||
import { afterEach, beforeEach, describe, it } from "mocha"
|
||||
import sinon from "sinon"
|
||||
import { ClineEnv, Environment } from "@/config"
|
||||
import { getFeatureFlagsService } from "@/services/feature-flags"
|
||||
import { CLINE_RECOMMENDED_MODELS_FALLBACK } from "@/shared/cline/recommended-models"
|
||||
import { FeatureFlag } from "@/shared/services/feature-flags/feature-flags"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { refreshClineRecommendedModels, resetClineRecommendedModelsCacheForTests } from "../refreshClineRecommendedModels"
|
||||
|
||||
describe("refreshClineRecommendedModels", () => {
|
||||
let sandbox: sinon.SinonSandbox
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
resetClineRecommendedModelsCacheForTests()
|
||||
sandbox.stub(Logger, "log")
|
||||
sandbox.stub(Logger, "error")
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
resetClineRecommendedModelsCacheForTests()
|
||||
sandbox.restore()
|
||||
})
|
||||
|
||||
it("returns hardcoded models and skips upstream fetch when rollout flag is off", async () => {
|
||||
sandbox.stub(getFeatureFlagsService(), "getBooleanFlagEnabled").returns(false)
|
||||
const axiosGetStub = sandbox.stub(axios, "get")
|
||||
|
||||
const result = await refreshClineRecommendedModels()
|
||||
|
||||
expect(result).to.deep.equal(CLINE_RECOMMENDED_MODELS_FALLBACK)
|
||||
expect(axiosGetStub.called).to.equal(false)
|
||||
})
|
||||
|
||||
it("fetches from upstream when rollout flag is on", async () => {
|
||||
sandbox.stub(getFeatureFlagsService(), "getBooleanFlagEnabled").callsFake((flag) => {
|
||||
return flag === FeatureFlag.CLINE_RECOMMENDED_MODELS_UPSTREAM
|
||||
})
|
||||
sandbox.stub(ClineEnv, "config").returns({
|
||||
environment: Environment.production,
|
||||
appBaseUrl: "https://app.cline-mock.bot",
|
||||
apiBaseUrl: "https://api.cline-mock.bot",
|
||||
mcpBaseUrl: "https://api.cline-mock.bot/v1/mcp",
|
||||
})
|
||||
sandbox.stub(disk, "ensureCacheDirectoryExists").resolves("/tmp")
|
||||
sandbox.stub(fs, "writeFile").resolves()
|
||||
const axiosGetStub = sandbox.stub(axios, "get").resolves({
|
||||
data: {
|
||||
recommended: [{ id: "anthropic/claude-sonnet-4.6", description: "Remote recommended", tags: ["NEW"] }],
|
||||
free: [{ id: "z-ai/glm-5", description: "Remote free" }],
|
||||
},
|
||||
})
|
||||
|
||||
const result = await refreshClineRecommendedModels()
|
||||
|
||||
expect(axiosGetStub.calledOnce).to.equal(true)
|
||||
expect(result).to.deep.equal({
|
||||
recommended: [
|
||||
{
|
||||
id: "anthropic/claude-sonnet-4.6",
|
||||
name: "anthropic/claude-sonnet-4.6",
|
||||
description: "Remote recommended",
|
||||
tags: ["NEW"],
|
||||
},
|
||||
],
|
||||
free: [
|
||||
{
|
||||
id: "z-ai/glm-5",
|
||||
name: "z-ai/glm-5",
|
||||
description: "Remote free",
|
||||
tags: [],
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it("uses hardcoded models when rollout flag is turned off after upstream cache is populated", async () => {
|
||||
const flagStub = sandbox.stub(getFeatureFlagsService(), "getBooleanFlagEnabled")
|
||||
flagStub.onFirstCall().returns(true)
|
||||
flagStub.onSecondCall().returns(false)
|
||||
sandbox.stub(ClineEnv, "config").returns({
|
||||
environment: Environment.production,
|
||||
appBaseUrl: "https://app.cline-mock.bot",
|
||||
apiBaseUrl: "https://api.cline-mock.bot",
|
||||
mcpBaseUrl: "https://api.cline-mock.bot/v1/mcp",
|
||||
})
|
||||
sandbox.stub(disk, "ensureCacheDirectoryExists").resolves("/tmp")
|
||||
sandbox.stub(fs, "writeFile").resolves()
|
||||
const axiosGetStub = sandbox.stub(axios, "get").resolves({
|
||||
data: {
|
||||
recommended: [{ id: "google/gemini-3.1-pro-preview", description: "Remote recommended", tags: ["NEW"] }],
|
||||
free: [{ id: "minimax/minimax-m2.5", description: "Remote free", tags: ["FREE"] }],
|
||||
},
|
||||
})
|
||||
|
||||
const firstResult = await refreshClineRecommendedModels()
|
||||
const secondResult = await refreshClineRecommendedModels()
|
||||
|
||||
expect(axiosGetStub.calledOnce).to.equal(true)
|
||||
expect(firstResult).to.not.deep.equal(CLINE_RECOMMENDED_MODELS_FALLBACK)
|
||||
expect(secondResult).to.deep.equal(CLINE_RECOMMENDED_MODELS_FALLBACK)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,321 @@
|
||||
import { ensureCacheDirectoryExists, GlobalFileNames } from "@core/storage/disk"
|
||||
import type { ModelInfo } from "@shared/api"
|
||||
import { fileExistsAtPath } from "@utils/fs"
|
||||
import axios from "axios"
|
||||
import cloneDeep from "clone-deep"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { ClineEnv } from "@/config"
|
||||
import { StateManager } from "@/core/storage/StateManager"
|
||||
import { featureFlagsService } from "@/services/feature-flags"
|
||||
import {
|
||||
ANTHROPIC_MAX_THINKING_BUDGET,
|
||||
CLAUDE_OPUS_1M_TIERS,
|
||||
CLAUDE_SONNET_1M_TIERS,
|
||||
openRouterClaudeOpus461mModelId,
|
||||
openRouterClaudeSonnet41mModelId,
|
||||
openRouterClaudeSonnet451mModelId,
|
||||
openRouterClaudeSonnet461mModelId,
|
||||
} from "@/shared/api"
|
||||
import { getAxiosSettings } from "@/shared/net"
|
||||
import { FeatureFlag } from "@/shared/services/feature-flags/feature-flags"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import type { Controller } from ".."
|
||||
import { refreshOpenRouterModels } from "./refreshOpenRouterModels"
|
||||
|
||||
type ClineSupportedParams =
|
||||
| "frequency_penalty"
|
||||
| "include_reasoning"
|
||||
| "logit_bias"
|
||||
| "logprobs"
|
||||
| "max_tokens"
|
||||
| "min_p"
|
||||
| "presence_penalty"
|
||||
| "reasoning"
|
||||
| "repetition_penalty"
|
||||
| "response_format"
|
||||
| "seed"
|
||||
| "stop"
|
||||
| "temperature"
|
||||
| "tool_choice"
|
||||
| "tools"
|
||||
| "top_k"
|
||||
| "top_logprobs"
|
||||
| "top_p"
|
||||
|
||||
/**
|
||||
* The raw model information returned by the Cline API to list models
|
||||
*/
|
||||
interface ClineRawModelInfo {
|
||||
id: string
|
||||
name: string
|
||||
description: string | null
|
||||
context_length: number | null
|
||||
top_provider: {
|
||||
max_completion_tokens: number | null
|
||||
context_length: number | null
|
||||
is_moderated: boolean | null
|
||||
} | null
|
||||
architecture: {
|
||||
modality: string | string[]
|
||||
input_modalities?: string[]
|
||||
output_modalities?: string[]
|
||||
tokenizer?: string
|
||||
instruct_type?: string
|
||||
} | null
|
||||
pricing: {
|
||||
prompt: string
|
||||
completion: string
|
||||
request?: string
|
||||
image?: string
|
||||
audio?: string
|
||||
web_search?: string
|
||||
internal_reasoning?: string
|
||||
input_cache_read?: string
|
||||
input_cache_write?: string
|
||||
} | null
|
||||
supports_global_endpoint?: boolean | null
|
||||
tiers?: any[] | null
|
||||
supported_parameters?: ClineSupportedParams[] | null
|
||||
}
|
||||
|
||||
// Track pending refresh promise to prevent duplicate concurrent fetches
|
||||
let pendingRefresh: Promise<Record<string, ModelInfo>> | null = null
|
||||
|
||||
async function fetchRawClineModels(): Promise<ClineRawModelInfo[]> {
|
||||
const apiBaseUrl = ClineEnv.config().apiBaseUrl
|
||||
const response = await axios.get(`${apiBaseUrl}/api/v1/ai/cline/models`, getAxiosSettings())
|
||||
|
||||
if (!Array.isArray(response.data?.data)) {
|
||||
throw new Error("Invalid response data when fetching Cline models")
|
||||
}
|
||||
|
||||
Logger.log("Cline models source: Cline API")
|
||||
return response.data.data as ClineRawModelInfo[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Core function: Refreshes the Cline models and returns application types
|
||||
* @param controller The controller instance
|
||||
* @returns Record of model ID to ModelInfo (application types)
|
||||
*/
|
||||
export async function refreshClineModels(controller: Controller): Promise<Record<string, ModelInfo>> {
|
||||
const shouldUseClineEndpointSource = featureFlagsService.getBooleanFlagEnabled(FeatureFlag.EXTENSION_CLINE_MODELS_ENDPOINT)
|
||||
if (!shouldUseClineEndpointSource) {
|
||||
return refreshOpenRouterModels(controller)
|
||||
}
|
||||
|
||||
// Check in-memory cache first
|
||||
const cache = StateManager.get().getModelsCache("cline")
|
||||
if (cache) {
|
||||
return cache
|
||||
}
|
||||
|
||||
// If a fetch is already in progress, return the same promise
|
||||
if (pendingRefresh) {
|
||||
return pendingRefresh
|
||||
}
|
||||
|
||||
// Start new fetch and track the promise
|
||||
pendingRefresh = (async () => {
|
||||
try {
|
||||
return await fetchAndCacheClineModels()
|
||||
} finally {
|
||||
// Clear pending promise when done (success or error)
|
||||
pendingRefresh = null
|
||||
}
|
||||
})()
|
||||
|
||||
return pendingRefresh
|
||||
}
|
||||
|
||||
async function fetchAndCacheClineModels(): Promise<Record<string, ModelInfo>> {
|
||||
const clineModelsFilePath = path.join(await ensureCacheDirectoryExists(), GlobalFileNames.clineModels)
|
||||
|
||||
let models: Record<string, ModelInfo> = {}
|
||||
try {
|
||||
const rawModels = await fetchRawClineModels()
|
||||
const parsePrice = (price: any) => {
|
||||
if (price === undefined || price === null || price === "") {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const parsedPrice = Number.parseFloat(String(price))
|
||||
return Number.isNaN(parsedPrice) ? undefined : parsedPrice * 1_000_000
|
||||
}
|
||||
for (const rawModel of rawModels) {
|
||||
const supportThinking = rawModel.supported_parameters?.some((p) => p === "include_reasoning" || p === "reasoning")
|
||||
|
||||
// Handle modality which can be a string or array
|
||||
const modality = rawModel.architecture?.modality
|
||||
const supportsImages = Array.isArray(modality)
|
||||
? modality.includes("image")
|
||||
: typeof modality === "string" && modality.includes("image")
|
||||
|
||||
const modelInfo: ModelInfo = {
|
||||
name: rawModel.name,
|
||||
maxTokens: rawModel.top_provider?.max_completion_tokens ?? 0,
|
||||
contextWindow: rawModel.context_length ?? 0,
|
||||
supportsImages,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: parsePrice(rawModel.pricing?.prompt) ?? 0,
|
||||
outputPrice: parsePrice(rawModel.pricing?.completion) ?? 0,
|
||||
cacheWritesPrice: parsePrice(rawModel.pricing?.input_cache_write),
|
||||
cacheReadsPrice: parsePrice(rawModel.pricing?.input_cache_read),
|
||||
description: rawModel.description ?? "",
|
||||
// If thinking is supported, set maxBudget with a default value as a placeholder
|
||||
// to ensure it has a valid thinkingConfig that lets the application know thinking is supported.
|
||||
thinkingConfig: supportThinking ? { maxBudget: ANTHROPIC_MAX_THINKING_BUDGET } : undefined,
|
||||
supportsGlobalEndpoint: rawModel.supports_global_endpoint ?? undefined,
|
||||
tiers: rawModel.tiers ?? undefined,
|
||||
}
|
||||
|
||||
// Apply model-specific overrides for known models
|
||||
switch (rawModel.id) {
|
||||
case "anthropic/claude-sonnet-4.6":
|
||||
case "anthropic/claude-4.6-sonnet":
|
||||
case "anthropic/claude-sonnet-4.5":
|
||||
case "anthropic/claude-4.5-sonnet":
|
||||
case "anthropic/claude-sonnet-4":
|
||||
modelInfo.contextWindow = 200_000
|
||||
modelInfo.supportsPromptCache = true
|
||||
modelInfo.cacheWritesPrice = 3.75
|
||||
modelInfo.cacheReadsPrice = 0.3
|
||||
break
|
||||
case "anthropic/claude-3-7-sonnet":
|
||||
case "anthropic/claude-3.7-sonnet":
|
||||
case "anthropic/claude-3.5-sonnet":
|
||||
modelInfo.supportsPromptCache = true
|
||||
modelInfo.cacheWritesPrice = 3.75
|
||||
modelInfo.cacheReadsPrice = 0.3
|
||||
break
|
||||
case "anthropic/claude-opus-4.6":
|
||||
modelInfo.contextWindow = 200_000
|
||||
modelInfo.supportsPromptCache = true
|
||||
modelInfo.cacheWritesPrice = 6.25
|
||||
modelInfo.cacheReadsPrice = 0.5
|
||||
break
|
||||
case "anthropic/claude-opus-4.5":
|
||||
modelInfo.supportsPromptCache = true
|
||||
modelInfo.cacheWritesPrice = 6.25
|
||||
modelInfo.cacheReadsPrice = 0.5
|
||||
break
|
||||
case "anthropic/claude-opus-4.1":
|
||||
case "anthropic/claude-opus-4":
|
||||
modelInfo.supportsPromptCache = true
|
||||
modelInfo.cacheWritesPrice = 18.75
|
||||
modelInfo.cacheReadsPrice = 1.5
|
||||
break
|
||||
case "anthropic/claude-haiku-4.5":
|
||||
case "anthropic/claude-4.5-haiku":
|
||||
case "anthropic/claude-3-5-haiku":
|
||||
case "anthropic/claude-3.5-haiku":
|
||||
modelInfo.supportsPromptCache = true
|
||||
modelInfo.cacheWritesPrice = 1.25
|
||||
modelInfo.cacheReadsPrice = 0.1
|
||||
break
|
||||
case "deepseek/deepseek-chat":
|
||||
modelInfo.supportsPromptCache = true
|
||||
modelInfo.inputPrice = 0
|
||||
modelInfo.cacheWritesPrice = 0.14
|
||||
modelInfo.cacheReadsPrice = 0.014
|
||||
break
|
||||
case "openai/gpt-5":
|
||||
case "openai/gpt-5-chat":
|
||||
case "openai/gpt-5-mini":
|
||||
case "openai/gpt-5-nano":
|
||||
modelInfo.maxTokens = 8_192
|
||||
modelInfo.contextWindow = 272_000
|
||||
break
|
||||
default:
|
||||
// Check for cache pricing from the API response
|
||||
if (rawModel.id.startsWith("openai/") || rawModel.id.startsWith("google/")) {
|
||||
const cacheReadPrice = parsePrice(rawModel.pricing?.input_cache_read)
|
||||
modelInfo.cacheReadsPrice = cacheReadPrice
|
||||
if (cacheReadPrice !== undefined) {
|
||||
modelInfo.supportsPromptCache = true
|
||||
modelInfo.cacheWritesPrice = parsePrice(rawModel.pricing?.input_cache_write)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
models[rawModel.id] = modelInfo
|
||||
|
||||
// Add custom :1m model variant for Sonnet models
|
||||
if (
|
||||
rawModel.id === "anthropic/claude-sonnet-4" ||
|
||||
rawModel.id === "anthropic/claude-sonnet-4.5" ||
|
||||
rawModel.id === "anthropic/claude-sonnet-4.6" ||
|
||||
rawModel.id === "anthropic/claude-4.6-sonnet"
|
||||
) {
|
||||
const claudeSonnet1mModelInfo = cloneDeep(modelInfo)
|
||||
claudeSonnet1mModelInfo.contextWindow = 1_000_000
|
||||
claudeSonnet1mModelInfo.tiers = CLAUDE_SONNET_1M_TIERS
|
||||
|
||||
if (rawModel.id === "anthropic/claude-sonnet-4") {
|
||||
models[openRouterClaudeSonnet41mModelId] = claudeSonnet1mModelInfo
|
||||
}
|
||||
if (rawModel.id === "anthropic/claude-sonnet-4.5") {
|
||||
models[openRouterClaudeSonnet451mModelId] = claudeSonnet1mModelInfo
|
||||
}
|
||||
if (rawModel.id === "anthropic/claude-sonnet-4.6" || rawModel.id === "anthropic/claude-4.6-sonnet") {
|
||||
models[openRouterClaudeSonnet461mModelId] = claudeSonnet1mModelInfo
|
||||
}
|
||||
}
|
||||
|
||||
// Add custom :1m model variant for Opus 4.6
|
||||
if (rawModel.id === "anthropic/claude-opus-4.6") {
|
||||
const claudeOpus1mModelInfo = cloneDeep(modelInfo)
|
||||
claudeOpus1mModelInfo.contextWindow = 1_000_000
|
||||
claudeOpus1mModelInfo.tiers = CLAUDE_OPUS_1M_TIERS
|
||||
models[openRouterClaudeOpus461mModelId] = claudeOpus1mModelInfo
|
||||
}
|
||||
}
|
||||
if (Object.keys(models).length === 0) {
|
||||
throw new Error("No Cline models returned from API")
|
||||
}
|
||||
// Save models and cache them in memory
|
||||
await fs.writeFile(clineModelsFilePath, JSON.stringify(models))
|
||||
Logger.log("Cline models fetched and saved")
|
||||
} catch (error) {
|
||||
Logger.error("Error fetching Cline models:", error)
|
||||
|
||||
// If we failed to fetch models, try to read cached models from disk
|
||||
try {
|
||||
const fileExists = await fileExistsAtPath(clineModelsFilePath)
|
||||
if (fileExists) {
|
||||
const fileContents = await fs.readFile(clineModelsFilePath, "utf8")
|
||||
models = JSON.parse(fileContents)
|
||||
Logger.log("Loaded Cline models from cache")
|
||||
}
|
||||
} catch (cacheError) {
|
||||
Logger.error("Error reading Cline models from cache:", cacheError)
|
||||
}
|
||||
}
|
||||
|
||||
// Avoid poisoning in-memory cache with an empty model map after transient failures.
|
||||
if (Object.keys(models).length > 0) {
|
||||
StateManager.get().setModelsCache("cline", models)
|
||||
}
|
||||
|
||||
return models
|
||||
}
|
||||
|
||||
/**
|
||||
* Read cached Cline models from disk
|
||||
* @returns The cached models or undefined if not found
|
||||
*/
|
||||
export async function readClineModelsFromCache(): Promise<Record<string, ModelInfo> | undefined> {
|
||||
try {
|
||||
const clineModelsFilePath = path.join(await ensureCacheDirectoryExists(), GlobalFileNames.clineModels)
|
||||
const fileExists = await fileExistsAtPath(clineModelsFilePath)
|
||||
if (fileExists) {
|
||||
const fileContents = await fs.readFile(clineModelsFilePath, "utf8")
|
||||
return JSON.parse(fileContents)
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error("Error reading Cline models from cache:", error)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { OpenRouterCompatibleModelInfo } from "@shared/proto/cline/models"
|
||||
import { toProtobufModels } from "../../../shared/proto-conversions/models/typeConversion"
|
||||
import type { Controller } from "../index"
|
||||
import { refreshClineModels } from "./refreshClineModels"
|
||||
|
||||
/**
|
||||
* Refreshes Cline models and returns protobuf types for gRPC
|
||||
* @param controller The controller instance
|
||||
* @param request Empty request (unused but required for gRPC signature)
|
||||
* @returns OpenRouterCompatibleModelInfo with protobuf types (reusing the same proto type)
|
||||
*/
|
||||
export async function refreshClineModelsRpc(
|
||||
controller: Controller,
|
||||
_request: EmptyRequest,
|
||||
): Promise<OpenRouterCompatibleModelInfo> {
|
||||
const models = await refreshClineModels(controller)
|
||||
return OpenRouterCompatibleModelInfo.create({
|
||||
models: toProtobufModels(models),
|
||||
})
|
||||
}
|
||||
@@ -3,9 +3,11 @@ import axios from "axios"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { ClineEnv } from "@/config"
|
||||
import { featureFlagsService } from "@/services/feature-flags"
|
||||
import { CLINE_RECOMMENDED_MODELS_FALLBACK } from "@/shared/cline/recommended-models"
|
||||
import { getAxiosSettings } from "@/shared/net"
|
||||
import { FeatureFlag } from "@/shared/services/feature-flags/feature-flags"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import type { Controller } from ".."
|
||||
|
||||
export interface ClineRecommendedModelData {
|
||||
id: string
|
||||
@@ -24,6 +26,14 @@ const RECOMMENDED_MODELS_CACHE_TTL_MS = 60 * 60 * 1000
|
||||
let pendingRefresh: Promise<ClineRecommendedModelsData> | null = null
|
||||
let inMemoryCache: { data: ClineRecommendedModelsData; timestamp: number } | null = null
|
||||
|
||||
function getHardcodedRecommendedModels(): ClineRecommendedModelsData {
|
||||
return CLINE_RECOMMENDED_MODELS_FALLBACK
|
||||
}
|
||||
|
||||
function useUpstreamRecommendedModels(): boolean {
|
||||
return featureFlagsService.getBooleanFlagEnabled(FeatureFlag.CLINE_RECOMMENDED_MODELS_UPSTREAM)
|
||||
}
|
||||
|
||||
function normalizeRecommendedModel(raw: unknown): ClineRecommendedModelData | null {
|
||||
if (!raw || typeof raw !== "object") {
|
||||
return null
|
||||
@@ -69,7 +79,11 @@ function normalizeRecommendedModelsResponse(raw: unknown): ClineRecommendedModel
|
||||
return { recommended, free }
|
||||
}
|
||||
|
||||
export async function refreshClineRecommendedModels(_controller: Controller): Promise<ClineRecommendedModelsData> {
|
||||
export async function refreshClineRecommendedModels(): Promise<ClineRecommendedModelsData> {
|
||||
if (!useUpstreamRecommendedModels()) {
|
||||
return getHardcodedRecommendedModels()
|
||||
}
|
||||
|
||||
if (inMemoryCache && Date.now() - inMemoryCache.timestamp <= RECOMMENDED_MODELS_CACHE_TTL_MS) {
|
||||
return inMemoryCache.data
|
||||
}
|
||||
@@ -89,6 +103,11 @@ export async function refreshClineRecommendedModels(_controller: Controller): Pr
|
||||
return pendingRefresh
|
||||
}
|
||||
|
||||
export function resetClineRecommendedModelsCacheForTests(): void {
|
||||
pendingRefresh = null
|
||||
inMemoryCache = null
|
||||
}
|
||||
|
||||
async function fetchAndCacheClineRecommendedModels(): Promise<ClineRecommendedModelsData> {
|
||||
const clineRecommendedModelsFilePath = path.join(await ensureCacheDirectoryExists(), GlobalFileNames.clineRecommendedModels)
|
||||
let result: ClineRecommendedModelsData = { recommended: [], free: [] }
|
||||
|
||||
@@ -4,10 +4,10 @@ import type { Controller } from "../index"
|
||||
import { refreshClineRecommendedModels } from "./refreshClineRecommendedModels"
|
||||
|
||||
export async function refreshClineRecommendedModelsRpc(
|
||||
controller: Controller,
|
||||
_controller: Controller,
|
||||
_request: EmptyRequest,
|
||||
): Promise<ClineRecommendedModelsResponse> {
|
||||
const models = await refreshClineRecommendedModels(controller)
|
||||
const models = await refreshClineRecommendedModels()
|
||||
return ClineRecommendedModelsResponse.create({
|
||||
recommended: models.recommended.map((model) =>
|
||||
ClineRecommendedModel.create({
|
||||
|
||||
@@ -47,6 +47,9 @@ export async function updateApiConfigurationProto(
|
||||
planModeOpenRouterModelInfo: protoApiConfiguration.planModeOpenRouterModelInfo
|
||||
? fromProtobufModelInfo(protoApiConfiguration.planModeOpenRouterModelInfo)
|
||||
: undefined,
|
||||
planModeClineModelInfo: protoApiConfiguration.planModeClineModelInfo
|
||||
? fromProtobufModelInfo(protoApiConfiguration.planModeClineModelInfo)
|
||||
: undefined,
|
||||
planModeOpenAiModelInfo: protoApiConfiguration.planModeOpenAiModelInfo
|
||||
? fromProtobufOpenAiCompatibleModelInfo(protoApiConfiguration.planModeOpenAiModelInfo)
|
||||
: undefined,
|
||||
@@ -82,6 +85,9 @@ export async function updateApiConfigurationProto(
|
||||
actModeOpenRouterModelInfo: protoApiConfiguration.actModeOpenRouterModelInfo
|
||||
? fromProtobufModelInfo(protoApiConfiguration.actModeOpenRouterModelInfo)
|
||||
: undefined,
|
||||
actModeClineModelInfo: protoApiConfiguration.actModeClineModelInfo
|
||||
? fromProtobufModelInfo(protoApiConfiguration.actModeClineModelInfo)
|
||||
: undefined,
|
||||
actModeOpenAiModelInfo: protoApiConfiguration.actModeOpenAiModelInfo
|
||||
? fromProtobufOpenAiCompatibleModelInfo(protoApiConfiguration.actModeOpenAiModelInfo)
|
||||
: undefined,
|
||||
|
||||
@@ -170,15 +170,6 @@ export async function updateSettings(controller: Controller, request: UpdateSett
|
||||
}
|
||||
}
|
||||
|
||||
if (request.dictationSettings !== undefined) {
|
||||
// Convert from protobuf format (snake_case) to TypeScript format (camelCase)
|
||||
const dictationSettings = {
|
||||
featureEnabled: request.dictationSettings.featureEnabled ?? true,
|
||||
dictationEnabled: request.dictationSettings.dictationEnabled ?? true,
|
||||
dictationLanguage: request.dictationSettings.dictationLanguage ?? "en",
|
||||
}
|
||||
controller.stateManager.setGlobalState("dictationSettings", dictationSettings)
|
||||
}
|
||||
// Update auto-condense setting
|
||||
if (request.useAutoCondense !== undefined) {
|
||||
if (controller.task) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user