mirror of
https://github.com/cline/cline.git
synced 2026-09-13 09:50:12 +08:00
Compare commits
29
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
396032cd3b | ||
|
|
f33ab3a872 | ||
|
|
2ca8364ffc | ||
|
|
2ef81be703 | ||
|
|
359445ae0c | ||
|
|
d9e2e9c76b | ||
|
|
d859a86a6f | ||
|
|
0b7b9c1b3d | ||
|
|
557d725690 | ||
|
|
7274d8badc | ||
|
|
d1837366c0 | ||
|
|
c380daf4a3 | ||
|
|
c564045d81 | ||
|
|
9a5e1751b2 | ||
|
|
131e25e1a1 | ||
|
|
a7ff007af9 | ||
|
|
ef27f45080 | ||
|
|
e3c6d51072 | ||
|
|
48bac25548 | ||
|
|
37f5f104f3 | ||
|
|
3577b52404 | ||
|
|
1843bc8ed0 | ||
|
|
fead00ec57 | ||
|
|
238107d21c | ||
|
|
2063a661bd | ||
|
|
ec02d5862e | ||
|
|
8452084842 | ||
|
|
a41129a5db | ||
|
|
1ea34be611 |
@@ -0,0 +1,158 @@
|
||||
---
|
||||
name: publish-ui
|
||||
description: Prepare, validate, and publish standalone @cline/ui npm releases. Use when bumping the UI package version, publishing latest or next through ui-publish.yml, checking UI release readiness, or completing the one-time npm trusted-publishing bootstrap.
|
||||
---
|
||||
|
||||
# Publish UI
|
||||
|
||||
Release `@cline/ui` independently from the Cline SDK runtime packages.
|
||||
|
||||
## Release contract
|
||||
|
||||
- Version source: `sdk/packages/ui/package.json`.
|
||||
- Workflow: `.github/workflows/ui-publish.yml`.
|
||||
- The package keeps `internal: true` only to stay out of the SDK's shared
|
||||
version/publish scripts. It is still a public npm package because
|
||||
`private: false` and `publishConfig.access: public` control npm publication.
|
||||
- `latest` is the production channel. `next` is an opt-in preview channel.
|
||||
- Use prerelease versions such as `0.2.0-next.0` for `next`; do not publish a
|
||||
version intended for `latest` under the preview tag because npm versions
|
||||
cannot be republished.
|
||||
- There is no UI Git tag, GitHub release, schedule, or Slack announcement.
|
||||
- The workflow runs only by manual dispatch. Every release attempt runs the UI
|
||||
quality checks before publishing and requires `confirm_publish=publish` from
|
||||
`main`.
|
||||
- The publish job and npm trust relationship use the protected `Publish`
|
||||
environment.
|
||||
- Every npm publication needs a new semver version; npm versions are immutable.
|
||||
- Always ask before pushing commits, triggering the publish workflow, changing
|
||||
npm trust settings, or running a local publish command.
|
||||
|
||||
## Normal release
|
||||
|
||||
1. Inspect the branch, current version, npm state, and UI changes.
|
||||
|
||||
```sh
|
||||
git status --short --branch
|
||||
node -p "require('./sdk/packages/ui/package.json').version"
|
||||
npm view @cline/ui dist-tags versions --json
|
||||
git log --oneline --no-merges -- \
|
||||
sdk/packages/ui apps/examples/desktop-app/webview/components/views/chat \
|
||||
.github/workflows/ui-publish.yml
|
||||
```
|
||||
|
||||
2. Ask for the npm channel and version together. For `latest`, ask for patch,
|
||||
minor, major, or an explicit version. For `next`, require an explicit
|
||||
prerelease version such as `0.2.0-next.0`. Do not guess. Update only
|
||||
`sdk/packages/ui/package.json` and its workspace version in `bun.lock`. Do
|
||||
not run the SDK version command.
|
||||
|
||||
3. Validate the release candidate.
|
||||
|
||||
```sh
|
||||
bun install --filter @cline/ui --filter @cline/code --frozen-lockfile
|
||||
bun -F @cline/ui typecheck
|
||||
bun -F @cline/ui test
|
||||
bun -F @cline/ui test:package
|
||||
bun -F @cline/ui build-storybook
|
||||
bun -F @cline/code test:chat-ui
|
||||
```
|
||||
|
||||
The packed-package test installs the tarball with Bun/React 19 and with
|
||||
npm/Node/React 18.
|
||||
Inspect `bun pm pack --dry-run` when the exported file set changed.
|
||||
|
||||
4. Commit the version bump separately from feature work. Ask before pushing.
|
||||
|
||||
```sh
|
||||
git add sdk/packages/ui/package.json bun.lock
|
||||
git commit -m "chore(ui): release vX.Y.Z"
|
||||
git push origin HEAD
|
||||
```
|
||||
|
||||
5. After the release commit reaches `main`, restate the selected npm tag and ask
|
||||
for explicit publish approval. Then trigger and watch the standalone
|
||||
workflow:
|
||||
|
||||
```sh
|
||||
run_url=$(gh workflow run ui-publish.yml --ref main \
|
||||
-f npm_tag=latest \
|
||||
-f confirm_publish=publish)
|
||||
test -n "$run_url"
|
||||
run_id=${run_url##*/}
|
||||
gh run watch "$run_id" --exit-status
|
||||
```
|
||||
|
||||
Use `npm_tag=next` only for a deliberate preview. Do not report success until
|
||||
the workflow succeeds and npm shows the exact version under the selected tag.
|
||||
|
||||
```sh
|
||||
npm view @cline/ui dist-tags versions --json
|
||||
```
|
||||
|
||||
## One-time npm bootstrap
|
||||
|
||||
Use this only while `npm view @cline/ui` returns `E404`. npm requires the
|
||||
package to exist before its GitHub trusted publisher can be configured.
|
||||
|
||||
1. Merge the package and `ui-publish.yml` to `main`. Start from a clean,
|
||||
reviewed `main` checkout. Verify authentication, account 2FA, and write
|
||||
access to the `@cline` npm organization. The `npm trust` command in step 4
|
||||
requires npm CLI 11.15 or newer; the automated trusted-publishing workflow
|
||||
itself enforces npm 11.5.1 or newer.
|
||||
|
||||
```sh
|
||||
npm --version
|
||||
npm whoami
|
||||
npm view @cline/ui version
|
||||
```
|
||||
|
||||
If npm is older than 11.15, ask before upgrading with
|
||||
`npm install -g npm@^11.15.0`.
|
||||
|
||||
2. Run the normal release validation in step 3 above. Then build, pack, test,
|
||||
and inspect the exact initial tarball. Record the absolute archive path
|
||||
printed by the final command.
|
||||
|
||||
```sh
|
||||
bun -F @cline/ui build
|
||||
pack_dir=$(mktemp -d)
|
||||
(cd sdk/packages/ui && bun pm pack --ignore-scripts --destination "$pack_dir" --quiet)
|
||||
tarball=$(find "$pack_dir" -maxdepth 1 -name '*.tgz' -print -quit)
|
||||
test -n "$tarball"
|
||||
bun sdk/packages/ui/scripts/smoke-package.ts "$tarball"
|
||||
tar -tzf "$tarball"
|
||||
printf 'Bootstrap archive: %s\n' "$tarball"
|
||||
```
|
||||
|
||||
3. Ask for explicit approval, then publish the initial version publicly under
|
||||
`latest`:
|
||||
|
||||
```sh
|
||||
npm publish /absolute/path/from-step-2.tgz --access public --tag latest
|
||||
```
|
||||
|
||||
4. Ask separately before configuring the standalone workflow as the trusted
|
||||
publisher:
|
||||
|
||||
```sh
|
||||
npm trust github @cline/ui \
|
||||
--repo cline/cline \
|
||||
--file ui-publish.yml \
|
||||
--env Publish \
|
||||
--allow-publish
|
||||
```
|
||||
|
||||
5. Verify both package state and trust. Every later release uses the workflow;
|
||||
do not add a long-lived npm token.
|
||||
|
||||
```sh
|
||||
npm view @cline/ui dist-tags versions --json
|
||||
npm trust list @cline/ui
|
||||
```
|
||||
|
||||
## Final report
|
||||
|
||||
Report the version and npm tag, release commit, whether anything was pushed,
|
||||
workflow URL or bootstrap result, npm verification, and tests/builds run. If
|
||||
the package still returns `E404`, state that bootstrap remains required.
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Publish UI"
|
||||
short_description: "Prepare and publish the Cline UI package"
|
||||
default_prompt: "Use $publish-ui to prepare and publish a new @cline/ui npm release."
|
||||
@@ -8,8 +8,9 @@ HTTP-controlled debugger for the VSCode extension at `src/dev/debug-harness/serv
|
||||
# Build extension first if needed (protos + esbuild):
|
||||
bun run protos && IS_DEV=true bun esbuild.mjs
|
||||
|
||||
# Launch (skip-build if already built):
|
||||
bun src/dev/debug-harness/server.ts --skip-build --auto-launch
|
||||
# Launch (skip-build if already built). Run with node, NOT bun — Playwright's
|
||||
# Electron launch times out under bun:
|
||||
node src/dev/debug-harness/server.ts --skip-build --auto-launch
|
||||
|
||||
# In another terminal:
|
||||
curl localhost:19229/api -d '{"method":"status"}'
|
||||
|
||||
@@ -16,6 +16,7 @@ This file is the secret sauce for working effectively in this codebase. It captu
|
||||
- The whole repo (including `apps/vscode`) uses **bun** for package management and task running. Emit `bun run X` / `bun install` / `bunx <bin>` / `bun file.ts`, never npm/npx. Node remains the *runtime* (VS Code's extension host and the standalone cline-core are Node), so Node-runtime tokens are legitimate and must not be "fixed" to bun — see @.clinerules/bun-and-node.md for the keep-list vs rewrite-list.
|
||||
- Avoid provider-specific string matching / hardcoded provider branches when fixing provider/config plumbing. Prefer provider metadata, shared catalog/defaults, explicit protocol/client capabilities, or centralized normalization utilities that apply by data shape rather than `providerId === "..."`. If a provider exception seems necessary, stop and explain why instead of adding ad-hoc string matching.
|
||||
- This is a VS Code extension—check `package.json` for available scripts before trying to verify builds (e.g., `bun run compile`, not `bun run build`).
|
||||
- When reading a configuration files that users may edit, use `readFileStrippingUtf8Bom`, `readFileSyncStrippingUtf8Bom`, or `stripUtf8Bom` from `@cline/shared/node`. DON'T strip byte order marks of user files handled by tools/passed to models.
|
||||
- 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
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
name: ui-publish
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
npm_tag:
|
||||
description: "npm distribution tag"
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- next
|
||||
- latest
|
||||
default: next
|
||||
confirm_publish:
|
||||
description: 'Type "publish" to publish @cline/ui to npm'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
quality:
|
||||
name: UI quality and package checks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: "1.3.13"
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24.x"
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --filter @cline/ui --filter @cline/code --frozen-lockfile
|
||||
|
||||
- name: Typecheck UI
|
||||
run: bun -F @cline/ui typecheck
|
||||
|
||||
- name: Test UI
|
||||
run: bun -F @cline/ui test
|
||||
|
||||
- name: Build Storybook
|
||||
run: bun -F @cline/ui build-storybook
|
||||
|
||||
- name: Build UI package
|
||||
run: bun -F @cline/ui build
|
||||
|
||||
- name: Test desktop chat integration
|
||||
run: bun -F @cline/code test:chat-ui
|
||||
|
||||
- name: Pack publish artifact
|
||||
id: pack
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
pack_dir="$RUNNER_TEMP/ui-npm-pack"
|
||||
mkdir -p "$pack_dir"
|
||||
cd sdk/packages/ui
|
||||
bun pm pack --ignore-scripts --destination "$pack_dir" --quiet
|
||||
archive=$(find "$pack_dir" -maxdepth 1 -name '*.tgz' -print -quit)
|
||||
test -n "$archive"
|
||||
echo "archive=$archive" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Test packed package
|
||||
env:
|
||||
UI_PACKAGE_ARCHIVE: ${{ steps.pack.outputs.archive }}
|
||||
run: bun sdk/packages/ui/scripts/smoke-package.ts "$UI_PACKAGE_ARCHIVE"
|
||||
|
||||
- name: Upload publish artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ui-npm-package
|
||||
path: ${{ runner.temp }}/ui-npm-pack/*.tgz
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
publish:
|
||||
name: Publish @cline/ui
|
||||
if: >-
|
||||
github.event_name == 'workflow_dispatch' &&
|
||||
github.repository == 'cline/cline' &&
|
||||
github.ref == 'refs/heads/main' &&
|
||||
inputs.confirm_publish == 'publish' &&
|
||||
!endsWith(github.actor, '[bot]')
|
||||
needs: quality
|
||||
runs-on: ubuntu-latest
|
||||
environment: Publish
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24.x"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Download publish artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ui-npm-package
|
||||
path: ${{ runner.temp }}/ui-npm-pack
|
||||
|
||||
- name: Verify publish tooling
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
npm_version=$(npm --version)
|
||||
echo "npm ${npm_version}"
|
||||
node -e 'const [major, minor, patch] = process.argv[1].split(".").map(Number); if (major < 11 || (major === 11 && (minor < 5 || (minor === 5 && patch < 1)))) { console.error("npm 11.5.1 or newer is required for trusted publishing"); process.exit(1); }' "$npm_version"
|
||||
|
||||
- name: Publish package
|
||||
shell: bash
|
||||
env:
|
||||
NPM_CONFIG_PROVENANCE: "true"
|
||||
NPM_TAG: ${{ inputs.npm_tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
archive=$(find "$RUNNER_TEMP/ui-npm-pack" -maxdepth 1 -name '*.tgz' -print -quit)
|
||||
if [ -z "$archive" ]; then
|
||||
echo "UI package archive was not downloaded"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
version=$(tar -xOf "$archive" package/package.json | node -e 'let input=""; process.stdin.on("data", chunk => input += chunk); process.stdin.on("end", () => process.stdout.write(JSON.parse(input).version))')
|
||||
if npm view "@cline/ui@${version}" version >/dev/null 2>&1; then
|
||||
echo "@cline/ui@${version} already exists; bump sdk/packages/ui/package.json before publishing"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
npm publish "$archive" --tag "$NPM_TAG" --access public
|
||||
echo "Published @cline/ui@${version} with npm tag '${NPM_TAG}'"
|
||||
@@ -1,5 +1,27 @@
|
||||
# Cline CLI Changelog
|
||||
|
||||
## 3.0.45
|
||||
|
||||
- Smaller install: the Claude Code and Codex providers are now optional and loaded on demand, cutting `npm i -g cline` from ~640MB to ~285MB (from SDK v0.0.65)
|
||||
- Kimi K3 is now available as a ClinePass model (from SDK v0.0.65)
|
||||
- Runs now retry once after refreshing expired OAuth credentials (from SDK v0.0.65)
|
||||
- Team runs: the spawn tool is no longer exposed to teammates, and errored teammate runs now report as failed instead of completed (from SDK v0.0.65)
|
||||
- Hub status output now includes version numbers
|
||||
- Updated the bundled model catalog (from SDK v0.0.65)
|
||||
|
||||
## 3.0.44
|
||||
|
||||
- Improved max output token handling across providers (gateway routing, OpenAI vendor, and reasoning models) (from SDK v0.0.64)
|
||||
- Frontmatter and configuration files that start with a UTF-8 byte order mark (e.g. saved by Windows editors) now parse correctly (from SDK v0.0.64)
|
||||
|
||||
## 3.0.43
|
||||
|
||||
- The CLI now automatically trusts your operating system's certificate store, so it works behind corporate proxies and TLS-inspecting firewalls without manually setting `NODE_EXTRA_CA_CERTS` (fixes "unable to get local issuer certificate" errors, including Windows intermediate CA stores)
|
||||
|
||||
## 3.0.42
|
||||
|
||||
- Fixed Ollama native API routing so context window and timeout settings work again
|
||||
|
||||
## 3.0.41
|
||||
|
||||
- Compaction now shows progress status in the TUI
|
||||
|
||||
@@ -346,9 +346,24 @@ Desktop-integrated approval mode is also supported via env wiring (`CLINE_TOOL_A
|
||||
- `CLINE_LOG_LEVEL` - Runtime log level (`trace|debug|info|warn|error|fatal|silent`, default `info`)
|
||||
- `CLINE_LOG_PATH` - Runtime log file path (default `<CLINE_DATA_DIR>/logs/cline.log`)
|
||||
- `CLINE_LOG_NAME` - Logger name embedded in runtime log records
|
||||
- `CLINE_DEBUG` - Set to `1`/`true` to print wrapper diagnostics (e.g. the CA bundle summary)
|
||||
|
||||
`--key` takes precedence over environment variables.
|
||||
|
||||
## Certificate trust
|
||||
|
||||
The CLI automatically trusts your operating system's certificate store, so it
|
||||
works behind corporate TLS-inspecting proxies and with self-signed/internal
|
||||
endpoints without any setup. On launch the `cline` wrapper harvests the OS trust
|
||||
anchors and writes them to `~/.cline/cli-node-extra-ca-certs.pem`, then points
|
||||
the runtime's `NODE_EXTRA_CA_CERTS` at that bundle. The file is regenerated when
|
||||
it changes and is safe to delete (it is rebuilt on the next run).
|
||||
|
||||
If you set `NODE_EXTRA_CA_CERTS` yourself, your certificates are **merged** into
|
||||
that bundle alongside the system store rather than replacing it. Run with
|
||||
`CLINE_DEBUG=1` to see how many OS and user CAs were loaded and where the bundle
|
||||
was written.
|
||||
|
||||
## Contributing
|
||||
|
||||
See [DEVELOPMENT.md](./DEVELOPMENT.md) for local development setup, monorepo structure, and TUI architecture. See [DISTRIBUTION.md](./DISTRIBUTION.md) for how the CLI is packaged and distributed.
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
// Auto-discovery of OS trust anchors for the Cline CLI.
|
||||
//
|
||||
// Bun does not read the OS trust store, so the 3.x CLI cannot see corporate
|
||||
// MITM / self-signed CAs out of the box. This runs in the Node `bin/cline`
|
||||
// wrapper (not Bun), reads the full OS store via tls.getCACertificates("system")
|
||||
// (Node >= 22, no --use-system-ca flag), and hands the certs to the Bun child
|
||||
// via NODE_EXTRA_CA_CERTS, which both runtimes honor. Mirrors the JetBrains
|
||||
// plugin's configureCertificates(), sourcing from the OS instead of the IDE.
|
||||
//
|
||||
// Dependency-free CommonJS with injectable modules so it is unit-testable and
|
||||
// ships verbatim in the published wrapper package.
|
||||
|
||||
const PEM_MARKER = "-----BEGIN CERTIFICATE-----";
|
||||
const CERT_BLOCK =
|
||||
/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g;
|
||||
|
||||
/**
|
||||
* Returns only the complete certificate blocks from PEM text, or null when
|
||||
* there are none. User files may also hold private keys (combined cert+key
|
||||
* PEMs) or other sections, which must never be copied into the managed
|
||||
* bundle. Files that contain nothing but certificates pass through verbatim
|
||||
* so unchanged bundles keep hash-skipping the rewrite.
|
||||
*/
|
||||
function sanitizePem(text) {
|
||||
const blocks = text.match(CERT_BLOCK) ?? [];
|
||||
if (blocks.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const rest = text.replace(CERT_BLOCK, "");
|
||||
if (/^\s*$/.test(rest)) {
|
||||
return text;
|
||||
}
|
||||
return `${blocks.join("\n")}\n`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns OS-trusted certificates as PEM strings, or [] when unavailable.
|
||||
* tls.getCACertificates("system") requires Node >= 22.
|
||||
*/
|
||||
function harvestSystemCerts(tlsModule) {
|
||||
try {
|
||||
const tls = tlsModule || require("node:tls");
|
||||
if (typeof tls.getCACertificates !== "function") {
|
||||
return [];
|
||||
}
|
||||
const certs = tls.getCACertificates("system");
|
||||
if (!Array.isArray(certs)) {
|
||||
return [];
|
||||
}
|
||||
return certs.filter(
|
||||
(cert) => typeof cert === "string" && cert.includes(PEM_MARKER),
|
||||
);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file's certificate blocks as PEM text, or null when missing,
|
||||
* unreadable, or holding no complete certificate block.
|
||||
*/
|
||||
function readUserBundle(fsModule, userPath) {
|
||||
if (!userPath) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const fs = fsModule || require("node:fs");
|
||||
const stat = fs.statSync(userPath, { throwIfNoEntry: false });
|
||||
if (!stat || !stat.isFile()) {
|
||||
return null;
|
||||
}
|
||||
// Binary DER would not have loaded in the runtime either; require PEM.
|
||||
return sanitizePem(fs.readFileSync(userPath, "utf8"));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the user's NODE_EXTRA_CA_CERTS value into PEM strings. Node treats the
|
||||
* value as a single file, but some users set an OS-path-delimited list; the
|
||||
* whole value is tried as one file first, then split.
|
||||
* The managed bundle is excluded so reading it back never re-appends its certs.
|
||||
*/
|
||||
function readUserCerts(fsModule, pathModule, value, managedPath) {
|
||||
if (!value) {
|
||||
return [];
|
||||
}
|
||||
const fs = fsModule || require("node:fs");
|
||||
const path = pathModule || require("node:path");
|
||||
const candidates = [];
|
||||
const whole = readUserBundle(fs, value);
|
||||
if (whole) {
|
||||
candidates.push({ filePath: value, pem: whole });
|
||||
} else if (value.includes(path.delimiter)) {
|
||||
for (const segment of value.split(path.delimiter)) {
|
||||
const trimmed = segment.trim();
|
||||
if (!trimmed) {
|
||||
continue;
|
||||
}
|
||||
const pem = readUserBundle(fs, trimmed);
|
||||
if (pem) {
|
||||
candidates.push({ filePath: trimmed, pem });
|
||||
}
|
||||
}
|
||||
}
|
||||
const pems = [];
|
||||
for (const candidate of candidates) {
|
||||
const isManaged =
|
||||
managedPath &&
|
||||
path.resolve(candidate.filePath) === path.resolve(managedPath);
|
||||
if (!isManaged) {
|
||||
pems.push(candidate.pem);
|
||||
}
|
||||
}
|
||||
return pems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Concatenates the user PEMs (if any) and the system certificates into one
|
||||
* bundle. A separating newline is inserted between parts so adjacent END/BEGIN
|
||||
* markers cannot fuse into one invalid line.
|
||||
*/
|
||||
function buildBundle({ systemCerts, userPems }) {
|
||||
const parts = [...(userPems ?? []), ...systemCerts];
|
||||
return parts
|
||||
.map((part) => (part.endsWith("\n") ? part : `${part}\n`))
|
||||
.join("");
|
||||
}
|
||||
|
||||
/** Counts individual PEM certificates across the given bundle strings. */
|
||||
function countCerts(pems) {
|
||||
let count = 0;
|
||||
for (const pem of pems) {
|
||||
count += pem.split(PEM_MARKER).length - 1;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
function readFileIfExists(fs, filePath) {
|
||||
try {
|
||||
return fs.readFileSync(filePath, "utf8");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveClineDir(env, os, path) {
|
||||
return env.CLINE_DIR?.trim() || path.join(os.homedir(), ".cline");
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the api-unavailable warning should print. Stamped per Node version
|
||||
* in the cline dir so the nudge shows once rather than on every command; a
|
||||
* version change (upgrade that still falls short, or downgrade) re-arms it.
|
||||
* When the stamp cannot be read or written, warn — bookkeeping failures must
|
||||
* never suppress a real diagnostic.
|
||||
*/
|
||||
function shouldWarnApiUnavailable(env, deps = {}) {
|
||||
const fs = deps.fs || require("node:fs");
|
||||
const os = deps.os || require("node:os");
|
||||
const path = deps.path || require("node:path");
|
||||
const version = deps.nodeVersion || process.versions.node;
|
||||
const dir = resolveClineDir(env, os, path);
|
||||
const stamp = path.join(dir, `.ca-api-warned-${version}`);
|
||||
try {
|
||||
if (fs.existsSync(stamp)) {
|
||||
return false;
|
||||
}
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(stamp, "", { mode: 0o600 });
|
||||
return true;
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/** Atomically writes [content] to [target]; returns true on success. */
|
||||
function writeBundle(fs, dir, target, content) {
|
||||
const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
|
||||
try {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
// Owner read/write: the bundle holds public CA material, not secrets,
|
||||
// but there is no reason to make it world-writable.
|
||||
fs.writeFileSync(tmp, content, { mode: 0o600 });
|
||||
try {
|
||||
fs.renameSync(tmp, target);
|
||||
} catch {
|
||||
// Windows can reject rename over a file a concurrent child holds open.
|
||||
fs.rmSync(target, { force: true });
|
||||
fs.renameSync(tmp, target);
|
||||
}
|
||||
return true;
|
||||
} catch {
|
||||
// Never leave a partial temp file behind (e.g. ENOSPC mid-write).
|
||||
try {
|
||||
fs.rmSync(tmp, { force: true });
|
||||
} catch {
|
||||
// Ignore: best-effort cleanup.
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Harvests OS trust anchors, merges them with any user NODE_EXTRA_CA_CERTS, and
|
||||
* points env.NODE_EXTRA_CA_CERTS at a single managed PEM bundle. Mutates `env`
|
||||
* in place. Returns an outcome the caller can log; `action` is one of
|
||||
* "unchanged" | "written" | "write-failed-reused" | "write-failed" |
|
||||
* "no-system-certs" | "api-unavailable".
|
||||
*/
|
||||
function configureNodeExtraCaCerts(env, deps = {}) {
|
||||
const fs = deps.fs || require("node:fs");
|
||||
const os = deps.os || require("node:os");
|
||||
const path = deps.path || require("node:path");
|
||||
const tls = deps.tls || require("node:tls");
|
||||
|
||||
// tls.getCACertificates("system") needs Node >= 22.15; on older Nodes the
|
||||
// harvest cannot run at all, which the caller should surface to the user.
|
||||
if (typeof tls.getCACertificates !== "function") {
|
||||
return {
|
||||
action: "api-unavailable",
|
||||
path: null,
|
||||
systemCertCount: 0,
|
||||
userCertCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const systemCerts = harvestSystemCerts(tls);
|
||||
if (systemCerts.length === 0) {
|
||||
// Nothing to add: leave any user-provided NODE_EXTRA_CA_CERTS untouched
|
||||
// and let the runtime fall back to its bundled CAs.
|
||||
return {
|
||||
action: "no-system-certs",
|
||||
path: null,
|
||||
systemCertCount: 0,
|
||||
userCertCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const managedDir = resolveClineDir(env, os, path);
|
||||
const managedPath = path.join(managedDir, "cli-node-extra-ca-certs.pem");
|
||||
const userValue = (env.NODE_EXTRA_CA_CERTS || "").trim() || null;
|
||||
const userPems = readUserCerts(fs, path, userValue, managedPath);
|
||||
const bundle = buildBundle({ systemCerts, userPems });
|
||||
const base = {
|
||||
path: managedPath,
|
||||
systemCertCount: systemCerts.length,
|
||||
userCertCount: countCerts(userPems),
|
||||
};
|
||||
|
||||
// Skip the rewrite when the bundle is already current. Avoids per-launch I/O
|
||||
// and the concurrent-rename race in the steady state.
|
||||
if (readFileIfExists(fs, managedPath) === bundle) {
|
||||
env.NODE_EXTRA_CA_CERTS = managedPath;
|
||||
return { ...base, action: "unchanged" };
|
||||
}
|
||||
|
||||
if (writeBundle(fs, managedDir, managedPath, bundle)) {
|
||||
env.NODE_EXTRA_CA_CERTS = managedPath;
|
||||
return { ...base, action: "written" };
|
||||
}
|
||||
|
||||
// Write failed: fall back to a previously-written bundle if one exists.
|
||||
if (readFileIfExists(fs, managedPath)) {
|
||||
env.NODE_EXTRA_CA_CERTS = managedPath;
|
||||
return { ...base, action: "write-failed-reused" };
|
||||
}
|
||||
return { ...base, path: null, action: "write-failed" };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
harvestSystemCerts,
|
||||
sanitizePem,
|
||||
readUserBundle,
|
||||
readUserCerts,
|
||||
buildBundle,
|
||||
countCerts,
|
||||
configureNodeExtraCaCerts,
|
||||
shouldWarnApiUnavailable,
|
||||
};
|
||||
@@ -23,6 +23,48 @@ const childEnv = {
|
||||
CLINE_WRAPPER_PATH: scriptPath,
|
||||
};
|
||||
|
||||
// Auto-discover OS trust anchors and pass them to the Bun child via
|
||||
// NODE_EXTRA_CA_CERTS. The Bun runtime does not read the OS store on its own,
|
||||
// so corporate/self-signed CAs would otherwise fail. This wrapper runs on
|
||||
// Node, which can read the full store here.
|
||||
try {
|
||||
const caCerts = require("./ca-certs.cjs");
|
||||
const outcome = caCerts.configureNodeExtraCaCerts(childEnv);
|
||||
const debug =
|
||||
process.env.CLINE_DEBUG === "1" || process.env.CLINE_DEBUG === "true";
|
||||
// Not debug-gated: on old Nodes the harvest silently doing nothing is
|
||||
// indistinguishable from a broken corporate proxy. Stamped per Node
|
||||
// version so the nudge shows once, not on every command.
|
||||
if (
|
||||
outcome &&
|
||||
outcome.action === "api-unavailable" &&
|
||||
!childEnv.NODE_EXTRA_CA_CERTS &&
|
||||
caCerts.shouldWarnApiUnavailable(childEnv)
|
||||
) {
|
||||
console.warn(
|
||||
`[cline] Node ${process.versions.node} cannot read the OS trust store (needs >= 22.15); ` +
|
||||
"corporate or self-signed CAs may fail TLS. Upgrade Node or set NODE_EXTRA_CA_CERTS.",
|
||||
);
|
||||
}
|
||||
if (debug && outcome) {
|
||||
if (outcome.action === "no-system-certs") {
|
||||
console.warn(
|
||||
"[cline] No OS trust anchors found; relying on the runtime's bundled CAs.",
|
||||
);
|
||||
} else if (outcome.action === "write-failed") {
|
||||
console.warn(
|
||||
"[cline] Could not write the managed CA bundle; relying on the runtime's bundled CAs.",
|
||||
);
|
||||
} else {
|
||||
console.warn(
|
||||
`[cline] Trust: ${outcome.systemCertCount} OS + ${outcome.userCertCount} user CAs (${outcome.action}) -> ${outcome.path}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Best effort: fall back to the runtime's default trust on any failure.
|
||||
}
|
||||
|
||||
function run(target) {
|
||||
const result = childProcess.spawnSync(target, process.argv.slice(2), {
|
||||
stdio: "inherit",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@cline/cli",
|
||||
"displayName": "cline",
|
||||
"version": "3.0.41",
|
||||
"version": "3.0.45",
|
||||
"description": "Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more",
|
||||
"type": "module",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -0,0 +1,367 @@
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { delimiter, join } from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
// The helper ships as CommonJS in the published wrapper package, so it is
|
||||
// loaded via require rather than an ESM import.
|
||||
const caCerts = require("../../bin/ca-certs.cjs") as {
|
||||
harvestSystemCerts: (tls?: unknown) => string[];
|
||||
readUserBundle: (fs: unknown, p: string | null) => string | null;
|
||||
readUserCerts: (
|
||||
fs: unknown,
|
||||
path: unknown,
|
||||
value: string | null,
|
||||
managedPath: string | null,
|
||||
) => string[];
|
||||
buildBundle: (input: {
|
||||
systemCerts: string[];
|
||||
userPems?: string[];
|
||||
}) => string;
|
||||
countCerts: (pems: string[]) => number;
|
||||
configureNodeExtraCaCerts: (
|
||||
env: Record<string, string>,
|
||||
deps?: { tls?: unknown; fs?: unknown },
|
||||
) => {
|
||||
action: string;
|
||||
path: string | null;
|
||||
systemCertCount: number;
|
||||
userCertCount: number;
|
||||
};
|
||||
shouldWarnApiUnavailable: (
|
||||
env: Record<string, string>,
|
||||
deps?: { fs?: unknown; nodeVersion?: string },
|
||||
) => boolean;
|
||||
};
|
||||
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const certSystem =
|
||||
"-----BEGIN CERTIFICATE-----\nSYSTEM\n-----END CERTIFICATE-----\n";
|
||||
const certUser = "-----BEGIN CERTIFICATE-----\nUSER\n-----END CERTIFICATE-----";
|
||||
|
||||
function fakeTls(certs: unknown) {
|
||||
return { getCACertificates: () => certs };
|
||||
}
|
||||
|
||||
describe("ca-certs", () => {
|
||||
let dir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
dir = mkdtempSync(join(tmpdir(), "cline-ca-"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe("harvestSystemCerts", () => {
|
||||
it("returns only PEM strings from the system store", () => {
|
||||
expect(
|
||||
caCerts.harvestSystemCerts(fakeTls([certSystem, "not-a-cert", 42])),
|
||||
).toEqual([certSystem]);
|
||||
});
|
||||
|
||||
it("returns [] when getCACertificates is unavailable", () => {
|
||||
expect(caCerts.harvestSystemCerts({})).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns [] when getCACertificates throws", () => {
|
||||
expect(
|
||||
caCerts.harvestSystemCerts({
|
||||
getCACertificates: () => {
|
||||
throw new Error("nope");
|
||||
},
|
||||
}),
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("readUserBundle", () => {
|
||||
it("returns PEM contents for a PEM file", () => {
|
||||
const p = join(dir, "user.pem");
|
||||
writeFileSync(p, certUser);
|
||||
expect(caCerts.readUserBundle(fs, p)).toBe(certUser);
|
||||
});
|
||||
|
||||
it("returns null for a non-PEM (DER) file", () => {
|
||||
const p = join(dir, "user.der");
|
||||
writeFileSync(p, Buffer.from([0x30, 0x82, 0x01, 0x02]));
|
||||
expect(caCerts.readUserBundle(fs, p)).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null for a missing file and for null path", () => {
|
||||
expect(caCerts.readUserBundle(fs, join(dir, "nope.pem"))).toBeNull();
|
||||
expect(caCerts.readUserBundle(fs, null)).toBeNull();
|
||||
});
|
||||
|
||||
it("strips non-certificate sections such as private keys", () => {
|
||||
// Combined cert+key files (nginx/haproxy style) are common; the key
|
||||
// must never reach the managed bundle.
|
||||
const p = join(dir, "combined.pem");
|
||||
writeFileSync(
|
||||
p,
|
||||
`${certUser}\n-----BEGIN PRIVATE KEY-----\nSECRET\n-----END PRIVATE KEY-----\n`,
|
||||
);
|
||||
const out = caCerts.readUserBundle(fs, p);
|
||||
expect(out).toContain("USER");
|
||||
expect(out).not.toContain("PRIVATE KEY");
|
||||
expect(out).not.toContain("SECRET");
|
||||
});
|
||||
|
||||
it("keeps certificates-only files verbatim", () => {
|
||||
// Byte-identical passthrough keeps the unchanged-skip hash stable.
|
||||
const p = join(dir, "clean.pem");
|
||||
writeFileSync(p, `${certUser}\n${certSystem}`);
|
||||
expect(caCerts.readUserBundle(fs, p)).toBe(`${certUser}\n${certSystem}`);
|
||||
});
|
||||
|
||||
it("returns null for a BEGIN marker without a complete block", () => {
|
||||
const p = join(dir, "truncated.pem");
|
||||
writeFileSync(p, "-----BEGIN CERTIFICATE-----\ntruncated");
|
||||
expect(caCerts.readUserBundle(fs, p)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("readUserCerts", () => {
|
||||
it("reads a single PEM file path", () => {
|
||||
const p = join(dir, "corp.pem");
|
||||
writeFileSync(p, certUser);
|
||||
expect(caCerts.readUserCerts(fs, path, p, null)).toEqual([certUser]);
|
||||
});
|
||||
|
||||
it("splits a legacy OS-path-delimited value and reads each PEM", () => {
|
||||
// Legacy footgun: NODE_EXTRA_CA_CERTS="a.pem;b.pem".
|
||||
const a = join(dir, "a.pem");
|
||||
const b = join(dir, "b.pem");
|
||||
writeFileSync(a, certUser);
|
||||
writeFileSync(b, certSystem);
|
||||
expect(
|
||||
caCerts.readUserCerts(fs, path, [a, b].join(delimiter), null),
|
||||
).toEqual([certUser, certSystem]);
|
||||
});
|
||||
|
||||
it("skips missing segments in a delimited value", () => {
|
||||
const a = join(dir, "a.pem");
|
||||
writeFileSync(a, certUser);
|
||||
const value = [a, join(dir, "missing.pem")].join(delimiter);
|
||||
expect(caCerts.readUserCerts(fs, path, value, null)).toEqual([certUser]);
|
||||
});
|
||||
|
||||
it("excludes the managed bundle from user certs", () => {
|
||||
const managed = join(dir, "cli-node-extra-ca-certs.pem");
|
||||
writeFileSync(managed, certUser);
|
||||
expect(caCerts.readUserCerts(fs, path, managed, managed)).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns [] for empty value", () => {
|
||||
expect(caCerts.readUserCerts(fs, path, null, null)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildBundle", () => {
|
||||
it("merges user PEMs before system certs", () => {
|
||||
expect(
|
||||
caCerts.buildBundle({
|
||||
systemCerts: [certSystem],
|
||||
userPems: [certUser],
|
||||
}),
|
||||
).toBe(`${certUser}\n${certSystem}`);
|
||||
});
|
||||
|
||||
it("inserts a separating newline so END/BEGIN markers do not fuse", () => {
|
||||
// certUser has no trailing newline, so this proves the boundary fix.
|
||||
const merged = caCerts.buildBundle({
|
||||
systemCerts: [certSystem],
|
||||
userPems: [certUser],
|
||||
});
|
||||
expect(merged).not.toContain(
|
||||
"-----END CERTIFICATE----------BEGIN CERTIFICATE-----",
|
||||
);
|
||||
});
|
||||
|
||||
it("handles no user PEMs", () => {
|
||||
expect(caCerts.buildBundle({ systemCerts: [certSystem] })).toBe(
|
||||
certSystem,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("configureNodeExtraCaCerts", () => {
|
||||
it("writes a managed bundle and points the env var at it", () => {
|
||||
const env: Record<string, string> = { CLINE_DIR: dir };
|
||||
const out = caCerts.configureNodeExtraCaCerts(env, {
|
||||
tls: fakeTls([certSystem]),
|
||||
});
|
||||
expect(out.action).toBe("written");
|
||||
expect(out.path).toBe(join(dir, "cli-node-extra-ca-certs.pem"));
|
||||
expect(env.NODE_EXTRA_CA_CERTS).toBe(out.path);
|
||||
expect(readFileSync(out.path as string, "utf8")).toContain("SYSTEM");
|
||||
});
|
||||
|
||||
it("merges a user-supplied NODE_EXTRA_CA_CERTS with system certs", () => {
|
||||
const userPath = join(dir, "corp.pem");
|
||||
writeFileSync(userPath, certUser);
|
||||
const env: Record<string, string> = {
|
||||
CLINE_DIR: dir,
|
||||
NODE_EXTRA_CA_CERTS: userPath,
|
||||
};
|
||||
const out = caCerts.configureNodeExtraCaCerts(env, {
|
||||
tls: fakeTls([certSystem]),
|
||||
});
|
||||
expect(out.userCertCount).toBe(1);
|
||||
const written = readFileSync(env.NODE_EXTRA_CA_CERTS, "utf8");
|
||||
expect(written).toContain("USER");
|
||||
expect(written).toContain("SYSTEM");
|
||||
});
|
||||
|
||||
it("reports unchanged and skips rewrite on the second run", () => {
|
||||
const env: Record<string, string> = { CLINE_DIR: dir };
|
||||
expect(
|
||||
caCerts.configureNodeExtraCaCerts(env, { tls: fakeTls([certSystem]) })
|
||||
.action,
|
||||
).toBe("written");
|
||||
expect(
|
||||
caCerts.configureNodeExtraCaCerts(env, { tls: fakeTls([certSystem]) })
|
||||
.action,
|
||||
).toBe("unchanged");
|
||||
});
|
||||
|
||||
it("does not re-append when the user already points at the managed bundle", () => {
|
||||
const env: Record<string, string> = { CLINE_DIR: dir };
|
||||
const first = caCerts.configureNodeExtraCaCerts(env, {
|
||||
tls: fakeTls([certSystem]),
|
||||
}).path as string;
|
||||
const env2: Record<string, string> = {
|
||||
CLINE_DIR: dir,
|
||||
NODE_EXTRA_CA_CERTS: first,
|
||||
};
|
||||
caCerts.configureNodeExtraCaCerts(env2, { tls: fakeTls([certSystem]) });
|
||||
const written = readFileSync(env2.NODE_EXTRA_CA_CERTS, "utf8");
|
||||
expect(written.match(/SYSTEM/g)?.length).toBe(1);
|
||||
});
|
||||
|
||||
it("no-ops when no system certs are available", () => {
|
||||
const env: Record<string, string> = {
|
||||
CLINE_DIR: dir,
|
||||
NODE_EXTRA_CA_CERTS: "/user/corp.pem",
|
||||
};
|
||||
const out = caCerts.configureNodeExtraCaCerts(env, { tls: fakeTls([]) });
|
||||
expect(out.action).toBe("no-system-certs");
|
||||
expect(out.path).toBeNull();
|
||||
expect(env.NODE_EXTRA_CA_CERTS).toBe("/user/corp.pem");
|
||||
});
|
||||
|
||||
it("reports api-unavailable on Nodes without getCACertificates", () => {
|
||||
const env: Record<string, string> = {
|
||||
CLINE_DIR: dir,
|
||||
NODE_EXTRA_CA_CERTS: "/user/corp.pem",
|
||||
};
|
||||
const out = caCerts.configureNodeExtraCaCerts(env, { tls: {} });
|
||||
expect(out.action).toBe("api-unavailable");
|
||||
expect(out.path).toBeNull();
|
||||
expect(env.NODE_EXTRA_CA_CERTS).toBe("/user/corp.pem");
|
||||
});
|
||||
|
||||
it("reports write-failed when the bundle cannot be written", () => {
|
||||
const realFs = require("node:fs");
|
||||
const failingFs = {
|
||||
...realFs,
|
||||
mkdirSync: () => {
|
||||
throw new Error("EACCES");
|
||||
},
|
||||
writeFileSync: () => {
|
||||
throw new Error("EACCES");
|
||||
},
|
||||
};
|
||||
const env: Record<string, string> = { CLINE_DIR: dir };
|
||||
const out = caCerts.configureNodeExtraCaCerts(env, {
|
||||
tls: fakeTls([certSystem]),
|
||||
fs: failingFs,
|
||||
});
|
||||
expect(out.action).toBe("write-failed");
|
||||
expect(out.path).toBeNull();
|
||||
expect(env.NODE_EXTRA_CA_CERTS).toBeUndefined();
|
||||
});
|
||||
|
||||
it("reuses a stale bundle when the rewrite fails", () => {
|
||||
// First run writes the bundle normally.
|
||||
const env: Record<string, string> = { CLINE_DIR: dir };
|
||||
const managedPath = caCerts.configureNodeExtraCaCerts(env, {
|
||||
tls: fakeTls([certSystem]),
|
||||
}).path as string;
|
||||
|
||||
// Second run: writes fail, but the stale bundle is still readable.
|
||||
const realFs = require("node:fs");
|
||||
const failingFs = {
|
||||
...realFs,
|
||||
mkdirSync: () => {
|
||||
throw new Error("EACCES");
|
||||
},
|
||||
writeFileSync: () => {
|
||||
throw new Error("EACCES");
|
||||
},
|
||||
};
|
||||
const env2: Record<string, string> = { CLINE_DIR: dir };
|
||||
const out = caCerts.configureNodeExtraCaCerts(env2, {
|
||||
// A different system cert forces a rewrite attempt (not "unchanged").
|
||||
tls: fakeTls([certUser]),
|
||||
fs: failingFs,
|
||||
});
|
||||
|
||||
expect(out.action).toBe("write-failed-reused");
|
||||
expect(env2.NODE_EXTRA_CA_CERTS).toBe(managedPath);
|
||||
});
|
||||
});
|
||||
|
||||
describe("countCerts", () => {
|
||||
it("counts individual certificates, not files", () => {
|
||||
// One file holding two certs must report 2, not 1.
|
||||
const twoInOne = `${certUser}\n${certSystem}`;
|
||||
expect(caCerts.countCerts([twoInOne])).toBe(2);
|
||||
expect(caCerts.countCerts([certUser, certSystem])).toBe(2);
|
||||
expect(caCerts.countCerts([])).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("shouldWarnApiUnavailable", () => {
|
||||
it("warns once per Node version, then stays quiet", () => {
|
||||
const env = { CLINE_DIR: dir };
|
||||
const deps = { nodeVersion: "22.1.0" };
|
||||
expect(caCerts.shouldWarnApiUnavailable(env, deps)).toBe(true);
|
||||
expect(caCerts.shouldWarnApiUnavailable(env, deps)).toBe(false);
|
||||
});
|
||||
|
||||
it("re-arms when the Node version changes", () => {
|
||||
const env = { CLINE_DIR: dir };
|
||||
expect(
|
||||
caCerts.shouldWarnApiUnavailable(env, { nodeVersion: "22.1.0" }),
|
||||
).toBe(true);
|
||||
expect(
|
||||
caCerts.shouldWarnApiUnavailable(env, { nodeVersion: "22.14.0" }),
|
||||
).toBe(true);
|
||||
expect(
|
||||
caCerts.shouldWarnApiUnavailable(env, { nodeVersion: "22.1.0" }),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("still warns when the stamp cannot be written", () => {
|
||||
const realFs = require("node:fs");
|
||||
const failingFs = {
|
||||
...realFs,
|
||||
mkdirSync: () => {
|
||||
throw new Error("EACCES");
|
||||
},
|
||||
writeFileSync: () => {
|
||||
throw new Error("EACCES");
|
||||
},
|
||||
};
|
||||
const env = { CLINE_DIR: dir };
|
||||
const deps = { fs: failingFs, nodeVersion: "22.1.0" };
|
||||
// Bookkeeping failure must never suppress the diagnostic.
|
||||
expect(caCerts.shouldWarnApiUnavailable(env, deps)).toBe(true);
|
||||
expect(caCerts.shouldWarnApiUnavailable(env, deps)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
||||
import { existsSync, readdirSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { basename, extname, join } from "node:path";
|
||||
import {
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
type SkillConfig,
|
||||
type WorkflowConfig,
|
||||
} from "@cline/core";
|
||||
import { readFileSyncStrippingUtf8Bom } from "@cline/shared/node";
|
||||
import { Command } from "commander";
|
||||
import { getToolCatalog } from "../runtime/tools";
|
||||
import { loadInteractiveConfigData } from "../tui/interactive-config";
|
||||
@@ -209,7 +210,7 @@ async function runAgentsConfigCommand(
|
||||
continue;
|
||||
}
|
||||
const filePath = join(directory, entry.name);
|
||||
const raw = readFileSync(filePath, "utf8");
|
||||
const raw = readFileSyncStrippingUtf8Bom(filePath);
|
||||
const frontmatterMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
||||
const frontmatter = frontmatterMatch?.[1] ?? "";
|
||||
const nameMatch = frontmatter.match(/^\s*name:\s*(.+?)\s*$/m);
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { version as cliVersion } from "../../package.json";
|
||||
import { getCliBuildInfo } from "../utils/common";
|
||||
|
||||
const {
|
||||
@@ -174,6 +175,40 @@ describe("runDoctorCommand", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("reports CLI and running hub Core versions", async () => {
|
||||
const cwd = "/workspace";
|
||||
mockReadHubDiscovery.mockResolvedValue({
|
||||
url: "ws://127.0.0.1:25463/hub",
|
||||
port: 25463,
|
||||
pid: 50174,
|
||||
coreVersion: "0.0.63",
|
||||
});
|
||||
mockProbeHubServer.mockResolvedValue({
|
||||
url: "ws://127.0.0.1:25463/hub",
|
||||
port: 25463,
|
||||
pid: 50174,
|
||||
coreVersion: "0.0.64",
|
||||
});
|
||||
mockSpawnSync.mockReturnValue({ status: 1, stdout: "" });
|
||||
|
||||
const output: string[] = [];
|
||||
const code = await runDoctorCommand(
|
||||
{ cwd, json: true },
|
||||
{
|
||||
writeln: (text) => {
|
||||
output.push(text ?? "");
|
||||
},
|
||||
writeErr: () => {},
|
||||
},
|
||||
);
|
||||
|
||||
expect(code).toBe(0);
|
||||
expect(JSON.parse(output[0] || "")).toMatchObject({
|
||||
cliVersion,
|
||||
coreVersion: "0.0.64",
|
||||
});
|
||||
});
|
||||
|
||||
it("doctor --fix clears wedged hub startup artifacts when no server is actually running", async () => {
|
||||
const cwd = mkdtempSync(path.join(os.tmpdir(), "doctor-hub-fix-"));
|
||||
tempDirs.push(cwd);
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
import { formatUptime, resolveClineBuildEnv } from "@cline/shared";
|
||||
import { Command } from "commander";
|
||||
import open from "open";
|
||||
import { version as cliVersion } from "../../package.json";
|
||||
import { isProcessRunning } from "../connectors/common";
|
||||
import {
|
||||
type ActiveConnectorRecord,
|
||||
@@ -49,6 +50,8 @@ type SpawnedProcessRecord = {
|
||||
|
||||
type DoctorStatus = {
|
||||
cwd: string;
|
||||
cliVersion: string;
|
||||
coreVersion?: string;
|
||||
hubUrl?: string;
|
||||
hubHealthy: boolean;
|
||||
hubPid?: number;
|
||||
@@ -337,6 +340,8 @@ async function collectDoctorStatus(cwd: string): Promise<DoctorStatus> {
|
||||
];
|
||||
return {
|
||||
cwd,
|
||||
cliVersion,
|
||||
coreVersion: health?.coreVersion ?? discovery?.coreVersion,
|
||||
hubUrl: current?.url,
|
||||
hubHealthy: !!health?.url,
|
||||
hubPid: current?.pid,
|
||||
@@ -419,6 +424,8 @@ export async function runDoctorCommand(
|
||||
io.writeln(JSON.stringify(before));
|
||||
return 0;
|
||||
}
|
||||
writeln(`cli version ${c.dim}${before.cliVersion}${c.reset}`);
|
||||
writeln(`core version ${c.dim}${before.coreVersion ?? "n/a"}${c.reset}`);
|
||||
writeln(`hub url ${c.dim}${before.hubUrl ?? "none"}${c.reset}`);
|
||||
writeln(
|
||||
`hub healthy ${c.dim}${before.hubHealthy ? "yes" : "no"}${before.hubPid ? ` (pid=${before.hubPid})` : ""}${c.reset}`,
|
||||
|
||||
@@ -34,6 +34,7 @@ vi.mock("@cline/core", () => ({
|
||||
stopLocalHubServerGracefully: mockStopLocalHubServerGracefully,
|
||||
}));
|
||||
|
||||
import { version as cliVersion } from "../../package.json";
|
||||
import { createHubCommand } from "./hub";
|
||||
|
||||
const originalBuildEnv = process.env.CLINE_BUILD_ENV;
|
||||
@@ -63,6 +64,7 @@ describe("createHubCommand", () => {
|
||||
port: 25463,
|
||||
pid: 50174,
|
||||
startedAt: "2026-01-01T00:00:00.000Z",
|
||||
coreVersion: "0.0.62",
|
||||
});
|
||||
|
||||
const output: string[] = [];
|
||||
@@ -88,6 +90,8 @@ describe("createHubCommand", () => {
|
||||
pid: 50174,
|
||||
startedAt: "2026-01-01T00:00:00.000Z",
|
||||
uptime: "1m 5s",
|
||||
cliVersion,
|
||||
coreVersion: "0.0.62",
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from "@cline/core";
|
||||
import { formatUptime, resolveClineBuildEnv } from "@cline/shared";
|
||||
import { Command } from "commander";
|
||||
import { version as cliVersion } from "../../package.json";
|
||||
|
||||
interface HubCommandIo {
|
||||
writeln: (text?: string) => void;
|
||||
@@ -134,6 +135,8 @@ export function createHubCommand(
|
||||
pid: health?.pid,
|
||||
startedAt: health?.startedAt,
|
||||
uptime,
|
||||
cliVersion,
|
||||
coreVersion: health?.coreVersion ?? discovery?.coreVersion,
|
||||
}),
|
||||
);
|
||||
}),
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
type UserInstructionConfigService,
|
||||
type WorkflowConfig,
|
||||
} from "@cline/core";
|
||||
import { readFileSyncStrippingUtf8Bom } from "@cline/shared/node";
|
||||
import { getToolCatalog } from "../runtime/tools";
|
||||
import {
|
||||
type InteractiveSlashCommand,
|
||||
@@ -195,7 +196,7 @@ function loadAgentConfigItems(workspaceRoot: string): InteractiveConfigItem[] {
|
||||
continue;
|
||||
}
|
||||
const filePath = join(directory, entry.name);
|
||||
const raw = readFileSync(filePath, "utf8");
|
||||
const raw = readFileSyncStrippingUtf8Bom(filePath);
|
||||
const frontmatterMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
||||
const frontmatter = frontmatterMatch?.[1] ?? "";
|
||||
const nameMatch = frontmatter.match(/^\s*name:\s*(.+?)\s*$/m);
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
resolvePluginConfigSearchPaths,
|
||||
resolveAgentConfigSearchPaths as resolveSharedAgentConfigSearchPaths,
|
||||
} from "@cline/core";
|
||||
import { readFileSyncStrippingUtf8Bom } from "@cline/shared/node";
|
||||
import { readMcpServersResponse } from "./mcp";
|
||||
import type { JsonRecord } from "./types";
|
||||
|
||||
@@ -114,7 +115,7 @@ export async function listUserInstructionConfigs(
|
||||
const ext = extname(entry.name).toLowerCase();
|
||||
if (ext !== ".yml" && ext !== ".yaml") continue;
|
||||
const filePath = join(directory, entry.name);
|
||||
const raw = readFileSync(filePath, "utf8");
|
||||
const raw = readFileSyncStrippingUtf8Bom(filePath);
|
||||
const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
||||
const fm = fmMatch?.[1] ?? "";
|
||||
const nameMatch = fm.match(/^\s*name:\s*(.+?)\s*$/m);
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build:ui": "bun -F @cline/ui build",
|
||||
"predev:web": "bun run build:ui",
|
||||
"dev:web": "next dev webview -p 3125 --turbo",
|
||||
"dev:sidecar": "bun run sidecar/index.ts",
|
||||
"dev": "tauri dev",
|
||||
"prebuild": "bun run build:ui",
|
||||
"build": "bun run bun.mts",
|
||||
"build:sidecar": "mkdir -p dist/sidecar && bun build ./sidecar/index.ts --outfile ./dist/sidecar/index.js --target bun",
|
||||
"build:sidecar:bin": "bun run scripts/build-sidecar-bin.ts",
|
||||
@@ -16,7 +19,10 @@
|
||||
"package:desktop:windows": "bun run scripts/package-desktop.ts --platform windows",
|
||||
"package:desktop:linux": "bun run scripts/package-desktop.ts --platform linux",
|
||||
"start": "next start webview",
|
||||
"pretypecheck": "bun run build:ui",
|
||||
"typecheck": "tsc -p tsconfig.dev.json --noEmit",
|
||||
"pretest:chat-ui": "bun run build:ui",
|
||||
"test:chat-ui": "vitest run webview/components/views/chat/chat-messages.test.tsx --config vitest.config.ts",
|
||||
"clean": "rm -rf webview/.next webview/out node_modules dist && (cd src-tauri && rm -rf target node_modules dist)"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -50,6 +50,7 @@ import {
|
||||
updateMcpSettingsFileSync,
|
||||
} from "@cline/core";
|
||||
import { getClineEnvironmentConfig } from "@cline/shared";
|
||||
import { readFileSyncStrippingUtf8Bom } from "@cline/shared/node";
|
||||
import {
|
||||
connectorChannelsPayload,
|
||||
startConnectorChannel,
|
||||
@@ -558,7 +559,7 @@ async function listUserInstructionConfigs(
|
||||
const ext = extname(entry.name).toLowerCase();
|
||||
if (ext !== ".yml" && ext !== ".yaml") continue;
|
||||
const filePath = join(directory, entry.name);
|
||||
const raw = readFileSync(filePath, "utf8");
|
||||
const raw = readFileSyncStrippingUtf8Bom(filePath);
|
||||
const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
||||
const fm = fmMatch?.[1] ?? "";
|
||||
const nameMatch = fm.match(/^\s*name:\s*(.+?)\s*$/m);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
@import "@cline/ui/theme/index.css";
|
||||
@import "@cline/ui/components/agent-chat.css";
|
||||
|
||||
@source "../../node_modules/streamdown/dist";
|
||||
|
||||
|
||||
@@ -1,12 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Message as AgentMessage,
|
||||
Conversation,
|
||||
ConversationContent,
|
||||
ConversationScrollButton,
|
||||
ConversationViewport,
|
||||
MessageAction,
|
||||
MessageActions,
|
||||
MessageContent,
|
||||
Reasoning,
|
||||
ReasoningContent,
|
||||
ReasoningTrigger,
|
||||
ToolActivity,
|
||||
ToolActivityCode,
|
||||
ToolActivityContent,
|
||||
ToolActivityDetails,
|
||||
ToolActivityTrigger,
|
||||
} from "@cline/ui/components/agent-chat";
|
||||
import {
|
||||
AlertCircle,
|
||||
Bot,
|
||||
BrainIcon,
|
||||
Check,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Clock3,
|
||||
Copy,
|
||||
FileEdit,
|
||||
@@ -20,15 +35,7 @@ import {
|
||||
SquareTerminalIcon,
|
||||
UndoIcon,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useId,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { memo, useCallback, useEffect, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { toast } from "@/hooks/use-toast";
|
||||
import type { ChatMessage, ChatSessionStatus } from "@/lib/chat-schema";
|
||||
@@ -86,11 +93,9 @@ type AskQuestionRequestItem = {
|
||||
};
|
||||
|
||||
const IS_DEBUG = process.env.NODE_ENV === "test";
|
||||
const STICKY_BOTTOM_THRESHOLD_PX = 24;
|
||||
const SCROLL_TO_BOTTOM_BUTTON_THRESHOLD_PX = 120;
|
||||
|
||||
function ChatMessagesImpl({
|
||||
sessionId: _sessionId,
|
||||
sessionId,
|
||||
status,
|
||||
chatTransportState = "connecting",
|
||||
isSessionSwitching = false,
|
||||
@@ -105,9 +110,6 @@ function ChatMessagesImpl({
|
||||
onRestoreCheckpoint,
|
||||
onForkSession,
|
||||
}: ChatMessagesProps) {
|
||||
const scrollAreaRef = useRef<HTMLDivElement | null>(null);
|
||||
const scrollContentRef = useRef<HTMLDivElement | null>(null);
|
||||
const shouldStickToBottomRef = useRef(true);
|
||||
const hasMessages = messages.length > 0;
|
||||
const lastErrorMessage = [...messages]
|
||||
.reverse()
|
||||
@@ -115,7 +117,6 @@ function ChatMessagesImpl({
|
||||
const shouldShowErrorBanner =
|
||||
Boolean(error) && (!lastErrorMessage || lastErrorMessage.content !== error);
|
||||
const [showSwitchTransition, setShowSwitchTransition] = useState(false);
|
||||
const [showScrollToBottom, setShowScrollToBottom] = useState(false);
|
||||
const [toolApprovalActions, setToolApprovalActions] = useState<
|
||||
Record<string, "approving" | "rejecting">
|
||||
>({});
|
||||
@@ -140,23 +141,6 @@ function ChatMessagesImpl({
|
||||
const showIdleDetails =
|
||||
!hasMessages && !isSessionSwitching && !showSwitchTransition;
|
||||
|
||||
const getViewport = useCallback(() => {
|
||||
return scrollAreaRef.current;
|
||||
}, []);
|
||||
|
||||
const scrollToBottom = useCallback(
|
||||
(behavior: ScrollBehavior = "smooth") => {
|
||||
const viewport = getViewport();
|
||||
if (!viewport) {
|
||||
return;
|
||||
}
|
||||
shouldStickToBottomRef.current = true;
|
||||
viewport.scrollTo({ top: viewport.scrollHeight, behavior });
|
||||
setShowScrollToBottom((prev) => (prev ? false : prev));
|
||||
},
|
||||
[getViewport],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSessionSwitching) {
|
||||
setShowSwitchTransition((prev) => (prev ? false : prev));
|
||||
@@ -170,50 +154,6 @@ function ChatMessagesImpl({
|
||||
};
|
||||
}, [isSessionSwitching]);
|
||||
|
||||
useEffect(() => {
|
||||
const viewport = getViewport();
|
||||
if (!viewport) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updateScrollToBottomVisibility = () => {
|
||||
const distanceFromBottom =
|
||||
viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight;
|
||||
shouldStickToBottomRef.current =
|
||||
distanceFromBottom <= STICKY_BOTTOM_THRESHOLD_PX;
|
||||
const shouldShow =
|
||||
distanceFromBottom > SCROLL_TO_BOTTOM_BUTTON_THRESHOLD_PX;
|
||||
setShowScrollToBottom((prev) =>
|
||||
prev === shouldShow ? prev : shouldShow,
|
||||
);
|
||||
};
|
||||
|
||||
updateScrollToBottomVisibility();
|
||||
viewport.addEventListener("scroll", updateScrollToBottomVisibility);
|
||||
|
||||
return () => {
|
||||
viewport.removeEventListener("scroll", updateScrollToBottomVisibility);
|
||||
};
|
||||
}, [getViewport]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!shouldStickToBottomRef.current) {
|
||||
return;
|
||||
}
|
||||
scrollToBottom("auto");
|
||||
}, [scrollToBottom]);
|
||||
|
||||
useEffect(() => {
|
||||
const content = scrollContentRef.current;
|
||||
if (!content || typeof ResizeObserver === "undefined") return;
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
if (shouldStickToBottomRef.current) scrollToBottom("auto");
|
||||
});
|
||||
resizeObserver.observe(content);
|
||||
return () => resizeObserver.disconnect();
|
||||
}, [scrollToBottom]);
|
||||
|
||||
useEffect(() => {
|
||||
const activeRequestIds = new Set(
|
||||
pendingToolApprovals.map((item) => item.requestId),
|
||||
@@ -377,17 +317,19 @@ function ChatMessagesImpl({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative h-full min-h-0 min-w-0">
|
||||
<div
|
||||
className="h-full min-h-0 min-w-0 overflow-x-hidden overflow-y-auto"
|
||||
ref={scrollAreaRef}
|
||||
<Conversation
|
||||
className="h-full min-h-0 min-w-0"
|
||||
key={sessionId ?? "new-chat"}
|
||||
>
|
||||
<ConversationViewport
|
||||
aria-label="Agent conversation"
|
||||
className="h-full min-h-0 min-w-0"
|
||||
>
|
||||
<div
|
||||
<ConversationContent
|
||||
className={cn(
|
||||
"relative mx-auto min-h-full w-full min-w-0 max-w-full overflow-x-hidden",
|
||||
showIdleDetails ? "p-0" : "px-6 py-6",
|
||||
)}
|
||||
ref={scrollContentRef}
|
||||
>
|
||||
{showIdleDetails ? null : (
|
||||
<div className="flex min-h-full w-full min-w-0 flex-col gap-2 overflow-x-hidden">
|
||||
@@ -497,21 +439,10 @@ function ChatMessagesImpl({
|
||||
{error}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{showScrollToBottom ? (
|
||||
<Button
|
||||
className="absolute bottom-4 right-4 z-20 size-9 rounded-full shadow-sm"
|
||||
onClick={() => scrollToBottom("smooth")}
|
||||
size="icon"
|
||||
type="button"
|
||||
variant="secondary"
|
||||
>
|
||||
<ChevronDown className="size-4" />
|
||||
<span className="sr-only">Scroll to bottom</span>
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</ConversationContent>
|
||||
</ConversationViewport>
|
||||
<ConversationScrollButton />
|
||||
</Conversation>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -746,8 +677,6 @@ function MessageBubble({
|
||||
isUser && Boolean(onCopyRawText || checkpoint);
|
||||
const keepUserActionsVisible = restorePending || Boolean(restoreError);
|
||||
const keepAssistantActionsVisible = forkPending || Boolean(forkError);
|
||||
const hiddenActionButtonsClassName =
|
||||
"pointer-events-none opacity-0 transition-opacity group-hover:pointer-events-auto group-hover:opacity-100 group-focus-within:pointer-events-auto group-focus-within:opacity-100";
|
||||
|
||||
if (message.role === "tool") {
|
||||
return <ToolMessageBlock message={message} />;
|
||||
@@ -760,159 +689,102 @@ function MessageBubble({
|
||||
const reasoningContent = message.reasoning?.trim() || "";
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex min-w-0",
|
||||
isUser ? "justify-end" : "w-full justify-start",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"group max-w-full min-w-0 wrap-break-word text-sm",
|
||||
isUser && "flex max-w-[85%] flex-col items-end gap-1 md:max-w-[50%]",
|
||||
!isUser && "flex flex-col items-start gap-2 overflow-hidden",
|
||||
!isUser && !isError && "text-foreground",
|
||||
isError &&
|
||||
"bg-destructive/10 border border-destructive/40 text-destructive",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"max-w-full min-w-0 space-y-2 overflow-hidden wrap-break-word",
|
||||
isUser && "rounded-sm bg-card p-2 text-foreground/80",
|
||||
)}
|
||||
>
|
||||
{reasoningContent || message.reasoningRedacted ? (
|
||||
<ReasoningBlock
|
||||
content={reasoningContent}
|
||||
redacted={message.reasoningRedacted === true}
|
||||
streaming={isStreaming}
|
||||
/>
|
||||
) : null}
|
||||
<AgentMessage from={message.role}>
|
||||
<MessageContent className="space-y-2 wrap-break-word">
|
||||
{reasoningContent || message.reasoningRedacted ? (
|
||||
<ReasoningBlock
|
||||
content={reasoningContent}
|
||||
redacted={message.reasoningRedacted === true}
|
||||
streaming={isStreaming}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<div className="my-1 min-w-0 max-w-full wrap-break-word">
|
||||
<MemoizedMarkdown
|
||||
content={displayContent || " "}
|
||||
streaming={isStreaming && message.role === "assistant"}
|
||||
/>
|
||||
</div>
|
||||
<div className="my-1 min-w-0 max-w-full wrap-break-word">
|
||||
<MemoizedMarkdown
|
||||
content={displayContent || " "}
|
||||
streaming={isStreaming && message.role === "assistant"}
|
||||
/>
|
||||
</div>
|
||||
{shouldRenderUserActions ? (
|
||||
<div className="space-y-1">
|
||||
<div className="flex h-6 items-center justify-end">
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-end gap-2",
|
||||
keepUserActionsVisible
|
||||
? "pointer-events-auto opacity-100"
|
||||
: hiddenActionButtonsClassName,
|
||||
)}
|
||||
</MessageContent>
|
||||
|
||||
{shouldRenderUserActions ? (
|
||||
<>
|
||||
<MessageActions visible={keepUserActionsVisible}>
|
||||
{onCopyRawText ? (
|
||||
<MessageAction
|
||||
label={wasCopied ? "Copied user message" : "Copy user message"}
|
||||
onClick={onCopyRawText}
|
||||
title={wasCopied ? "Copied" : "Copy message"}
|
||||
>
|
||||
{onCopyRawText ? (
|
||||
<Button
|
||||
className="h-6 px-2 text-xs text-muted-foreground hover:text-foreground"
|
||||
aria-label={
|
||||
wasCopied ? "Copied user message" : "Copy user message"
|
||||
}
|
||||
onClick={onCopyRawText}
|
||||
size="sm"
|
||||
title={wasCopied ? "Copied" : "Copy message"}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
{wasCopied ? (
|
||||
<Check className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
) : null}
|
||||
{checkpoint ? (
|
||||
<Button
|
||||
className="h-6 px-2 text-xs text-muted-foreground hover:text-foreground"
|
||||
aria-label="Restore checkpoint"
|
||||
disabled={restoreDisabled || restorePending}
|
||||
onClick={() => onRestoreCheckpoint?.(checkpoint.runCount)}
|
||||
size="sm"
|
||||
title="Restore checkpoint"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
{restorePending ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<UndoIcon className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{restoreError ? (
|
||||
<div className="text-right text-xs text-destructive">
|
||||
{restoreError}
|
||||
</div>
|
||||
{wasCopied ? (
|
||||
<Check className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</MessageAction>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{shouldRenderAssistantActions ? (
|
||||
<div className="flex h-6 items-center hidden">
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-0",
|
||||
keepAssistantActionsVisible
|
||||
? "pointer-events-auto opacity-100"
|
||||
: hiddenActionButtonsClassName,
|
||||
)}
|
||||
>
|
||||
{onCopyRawText ? (
|
||||
<Button
|
||||
className="h-6 gap-1.5 px-2 text-[11px] text-muted-foreground hover:text-foreground"
|
||||
aria-label={
|
||||
wasCopied
|
||||
? "Copied assistant message"
|
||||
: "Copy assistant message"
|
||||
}
|
||||
onClick={onCopyRawText}
|
||||
size="sm"
|
||||
title={wasCopied ? "Copied" : "Copy raw assistant output"}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
{wasCopied ? (
|
||||
<Check className="h-3 w-3" />
|
||||
) : (
|
||||
<Copy className="h-3 w-3" />
|
||||
)}
|
||||
</Button>
|
||||
) : null}
|
||||
{onForkSession ? (
|
||||
<Button
|
||||
className="h-6 gap-1.5 px-2 text-[11px] text-muted-foreground hover:text-foreground"
|
||||
aria-label="Fork session"
|
||||
disabled={forkPending}
|
||||
onClick={onForkSession}
|
||||
size="sm"
|
||||
title="Fork session - copy full message history into a new session"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
{forkPending ? (
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
) : (
|
||||
<SplitIcon className="h-3 w-3" />
|
||||
)}
|
||||
</Button>
|
||||
) : null}
|
||||
{forkError ? (
|
||||
<span className="text-[11px] text-destructive">
|
||||
{forkError}
|
||||
</span>
|
||||
) : null}
|
||||
{checkpoint ? (
|
||||
<MessageAction
|
||||
disabled={restoreDisabled || restorePending}
|
||||
label="Restore checkpoint"
|
||||
onClick={() => onRestoreCheckpoint?.(checkpoint.runCount)}
|
||||
title="Restore checkpoint"
|
||||
>
|
||||
{restorePending ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<UndoIcon className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</MessageAction>
|
||||
) : null}
|
||||
</MessageActions>
|
||||
{restoreError ? (
|
||||
<div className="text-right text-xs text-destructive">
|
||||
{restoreError}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{shouldRenderAssistantActions ? (
|
||||
<MessageActions visible={keepAssistantActionsVisible}>
|
||||
{onCopyRawText ? (
|
||||
<MessageAction
|
||||
label={
|
||||
wasCopied
|
||||
? "Copied assistant message"
|
||||
: "Copy assistant message"
|
||||
}
|
||||
onClick={onCopyRawText}
|
||||
title={wasCopied ? "Copied" : "Copy raw assistant output"}
|
||||
>
|
||||
{wasCopied ? (
|
||||
<Check className="h-3 w-3" />
|
||||
) : (
|
||||
<Copy className="h-3 w-3" />
|
||||
)}
|
||||
</MessageAction>
|
||||
) : null}
|
||||
{onForkSession ? (
|
||||
<MessageAction
|
||||
disabled={forkPending}
|
||||
label="Fork session"
|
||||
onClick={onForkSession}
|
||||
title="Fork session - copy full message history into a new session"
|
||||
>
|
||||
{forkPending ? (
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
) : (
|
||||
<SplitIcon className="h-3 w-3" />
|
||||
)}
|
||||
</MessageAction>
|
||||
) : null}
|
||||
{forkError ? (
|
||||
<span className="text-[11px] text-destructive">{forkError}</span>
|
||||
) : null}
|
||||
</MessageActions>
|
||||
) : null}
|
||||
</AgentMessage>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -925,48 +797,18 @@ function ReasoningBlock({
|
||||
redacted: boolean;
|
||||
streaming?: boolean;
|
||||
}) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const panelId = useId();
|
||||
const displayContent = content || (redacted ? "[redacted]" : "");
|
||||
if (!displayContent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="my-2">
|
||||
<Button
|
||||
aria-controls={panelId}
|
||||
aria-expanded={expanded}
|
||||
className="h-auto min-h-0 max-w-full justify-start gap-2 whitespace-normal px-0 py-1 text-left text-sm font-medium text-foreground/70 hover:bg-transparent hover:text-foreground has-[>svg]:px-0 dark:hover:bg-transparent dark:hover:text-foreground"
|
||||
onClick={() => setExpanded((current) => !current)}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<BrainIcon aria-hidden="true" className="size-4" />
|
||||
<span>{streaming ? "Thinking" : "Thought process"}</span>
|
||||
<span
|
||||
aria-live="polite"
|
||||
className="text-xs font-normal text-muted-foreground"
|
||||
>
|
||||
{streaming ? "In progress" : "Complete"}
|
||||
</span>
|
||||
<span aria-hidden="true" className="shrink-0 text-muted-foreground">
|
||||
{expanded ? (
|
||||
<ChevronDown className="size-4" />
|
||||
) : (
|
||||
<ChevronRight className="size-4" />
|
||||
)}
|
||||
</span>
|
||||
</Button>
|
||||
{expanded ? (
|
||||
<div
|
||||
className="mt-1.5 min-w-0 max-w-full rounded-lg border border-border/70 bg-muted/30 p-3 text-sm leading-relaxed text-muted-foreground"
|
||||
id={panelId}
|
||||
>
|
||||
<MemoizedMarkdown content={displayContent} streaming={streaming} />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<Reasoning isStreaming={streaming}>
|
||||
<ReasoningTrigger />
|
||||
<ReasoningContent>
|
||||
<MemoizedMarkdown content={displayContent} streaming={streaming} />
|
||||
</ReasoningContent>
|
||||
</Reasoning>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1347,8 +1189,6 @@ function buildToolSummaryFromMeta(
|
||||
}
|
||||
|
||||
function ToolMessageBlock({ message }: { message: ChatMessage }) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const panelId = useId();
|
||||
const payload = parseToolPayload(message.content);
|
||||
const toolName = message.meta?.toolName || payload?.toolName || "tool";
|
||||
const hookEventName = message.meta?.hookEventName;
|
||||
@@ -1380,94 +1220,50 @@ function ToolMessageBlock({ message }: { message: ChatMessage }) {
|
||||
const resultPreview = payload?.isError ? formatToolValue(payload.result) : "";
|
||||
const hasExpandedSections =
|
||||
details.length > 0 || Boolean(inputPreview || resultPreview);
|
||||
const summaryContent = (
|
||||
<>
|
||||
{payload?.isError ? (
|
||||
<AlertCircle className="size-4 text-destructive/80" />
|
||||
) : (
|
||||
<Icon className="size-4" />
|
||||
)}
|
||||
<span className="min-w-0 wrap-break-word">{summary.label}</span>
|
||||
{summary.diff ? (
|
||||
<span className="shrink-0 font-mono text-xs">
|
||||
<span className="text-chart-2">+{summary.diff.additions}</span>{" "}
|
||||
<span className="text-destructive">-{summary.diff.deletions}</span>
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="my-2 flex w-full min-w-0 justify-start">
|
||||
<div
|
||||
className={cn("min-w-0 max-w-full overflow-hidden rounded-xl text-sm")}
|
||||
>
|
||||
{hasExpandedSections ? (
|
||||
<Button
|
||||
aria-controls={panelId}
|
||||
aria-expanded={expanded}
|
||||
className="h-auto min-h-0 max-w-full justify-start gap-2 whitespace-normal px-0 py-1 text-left text-sm font-medium text-primary hover:bg-transparent hover:text-primary/80 has-[>svg]:px-0 dark:hover:bg-transparent dark:hover:text-primary/80"
|
||||
onClick={() => setExpanded((current) => !current)}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
{summaryContent}
|
||||
<span className="shrink-0 text-muted-foreground">
|
||||
{expanded ? (
|
||||
<ChevronDown className="size-4" />
|
||||
) : (
|
||||
<ChevronRight className="size-4" />
|
||||
)}
|
||||
</span>
|
||||
</Button>
|
||||
) : (
|
||||
<div className="flex max-w-full items-center justify-start gap-2 py-1 text-left text-sm font-medium text-primary">
|
||||
{summaryContent}
|
||||
</div>
|
||||
)}
|
||||
{expanded ? (
|
||||
<div
|
||||
className="mt-1.5 min-w-0 max-w-full overflow-x-hidden pl-8 text-sm text-muted-foreground"
|
||||
id={panelId}
|
||||
>
|
||||
{hasExpandedSections ? (
|
||||
<div className="space-y-1">
|
||||
{details.map((detail) => (
|
||||
<div
|
||||
className="wrap-break-word"
|
||||
key={`${message.id}_${detail}`}
|
||||
>
|
||||
{detail}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
{inputPreview ? (
|
||||
<div className="space-y-1">
|
||||
<div className="text-[11px] uppercase tracking-wide text-muted-foreground/80">
|
||||
Input
|
||||
</div>
|
||||
<pre className="max-h-52 max-w-full overflow-x-hidden overflow-y-auto whitespace-pre-wrap wrap-break-word rounded-md border border-border/70 bg-background/60 p-2 text-sm leading-relaxed text-foreground">
|
||||
{inputPreview}
|
||||
</pre>
|
||||
</div>
|
||||
) : null}
|
||||
{resultPreview ? (
|
||||
payload?.isError ? (
|
||||
<div className="mt-1">
|
||||
<span className="text-destructive">{resultPreview}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
<pre className="max-h-64 max-w-full overflow-x-hidden overflow-y-auto whitespace-pre-wrap wrap-break-word rounded-md border border-border/70 bg-background/60 p-2 text-sm leading-relaxed text-foreground">
|
||||
{resultPreview}
|
||||
</pre>
|
||||
</div>
|
||||
)
|
||||
) : null}
|
||||
<ToolActivity expandable={hasExpandedSections}>
|
||||
<ToolActivityTrigger
|
||||
additions={summary.diff?.additions}
|
||||
deletions={summary.diff?.deletions}
|
||||
icon={
|
||||
payload?.isError ? (
|
||||
<AlertCircle className="size-4 text-destructive/80" />
|
||||
) : (
|
||||
<Icon className="size-4" />
|
||||
)
|
||||
}
|
||||
label={summary.label}
|
||||
status={payload?.isError ? "error" : inProgress ? "running" : "success"}
|
||||
/>
|
||||
<ToolActivityContent>
|
||||
{details.length > 0 ? (
|
||||
<ToolActivityDetails>
|
||||
{details.map((detail) => (
|
||||
<div key={`${message.id}_${detail}`}>{detail}</div>
|
||||
))}
|
||||
</ToolActivityDetails>
|
||||
) : null}
|
||||
{inputPreview ? (
|
||||
<div className="space-y-1">
|
||||
<div className="text-[11px] uppercase tracking-wide text-muted-foreground/80">
|
||||
Input
|
||||
</div>
|
||||
<ToolActivityCode className="text-sm">
|
||||
{inputPreview}
|
||||
</ToolActivityCode>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{resultPreview ? (
|
||||
payload?.isError ? (
|
||||
<div className="mt-1 text-destructive">{resultPreview}</div>
|
||||
) : (
|
||||
<ToolActivityCode className="max-h-64 text-sm">
|
||||
{resultPreview}
|
||||
</ToolActivityCode>
|
||||
)
|
||||
) : null}
|
||||
</ToolActivityContent>
|
||||
</ToolActivity>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@ service TaskService {
|
||||
rpc cancelQueuedPrompt(StringRequest) returns (Empty);
|
||||
// Cancels the currently running background command
|
||||
rpc cancelBackgroundCommand(EmptyRequest) returns (Empty);
|
||||
// Detaches the running foreground terminal command ("Proceed While Running"):
|
||||
// the agent receives the partial output and a log file path for the rest.
|
||||
rpc proceedWhileRunningCommand(EmptyRequest) returns (Empty);
|
||||
// Clears the current task
|
||||
rpc clearTask(EmptyRequest) returns (Empty);
|
||||
// Gets the total size of all tasks
|
||||
|
||||
+12
@@ -56,4 +56,16 @@ describe("parseYamlFrontmatter", () => {
|
||||
expect(result.data).to.deep.equal({ count: 42, enabled: true, tags: ["a", "b"] })
|
||||
expect(result.body.trim()).to.equal("Content")
|
||||
})
|
||||
|
||||
// Regression test for https://github.com/cline/cline/issues/12151
|
||||
// A leading UTF-8 BOM (e.g. saved by Windows Notepad's "UTF-8 with BOM" encoding) must not
|
||||
// prevent frontmatter from being recognized.
|
||||
it("parses frontmatter correctly when the content has a leading UTF-8 BOM", () => {
|
||||
const input = `\uFEFF---\nname: my-skill\ndescription: A test skill\n---\n# my-skill\nThis is a test skill.`
|
||||
const result = parseYamlFrontmatter(input)
|
||||
expect(result.hadFrontmatter).to.equal(true)
|
||||
expect(result.parseError).to.equal(undefined)
|
||||
expect(result.data).to.deep.equal({ name: "my-skill", description: "A test skill" })
|
||||
expect(result.body.trim()).to.equal("# my-skill\nThis is a test skill.")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -139,6 +139,34 @@ Instructions here`)
|
||||
expect(skills[0].source).to.equal("global")
|
||||
})
|
||||
|
||||
// Regression test for https://github.com/cline/cline/issues/12151:
|
||||
// SKILL.md files saved with a UTF-8 BOM (e.g. by Windows Notepad's "UTF-8 with BOM"
|
||||
// encoding) were silently skipped because the frontmatter regex required "---" at the
|
||||
// very start of the file and never accounted for the leading \uFEFF byte sequence.
|
||||
it("should discover skills whose SKILL.md starts with a UTF-8 BOM", async () => {
|
||||
const skillDir = path.join(GLOBAL_SKILLS_DIR, "my-skill")
|
||||
const skillMdPath = path.join(skillDir, "SKILL.md")
|
||||
|
||||
fileExistsStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
|
||||
fileExistsStub.withArgs(skillMdPath).resolves(true)
|
||||
isDirectoryStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
|
||||
readdirStub.withArgs(GLOBAL_SKILLS_DIR).resolves(["my-skill"])
|
||||
statStub.withArgs(skillDir).resolves({ isDirectory: () => true })
|
||||
readFileStub.withArgs(skillMdPath, "utf-8").resolves(`\uFEFF---
|
||||
name: my-skill
|
||||
description: A test skill
|
||||
---
|
||||
# my-skill
|
||||
This is a test skill.`)
|
||||
|
||||
const skills = await discoverSkills(TEST_CWD)
|
||||
|
||||
expect(skills).to.have.lengthOf(1)
|
||||
expect(skills[0].name).to.equal("my-skill")
|
||||
expect(skills[0].description).to.equal("A test skill")
|
||||
expect(skills[0].source).to.equal("global")
|
||||
})
|
||||
|
||||
it("should discover skills from project .clinerules/skills directory", async () => {
|
||||
const projectSkillsDir = path.join(TEST_CWD, ".clinerules", "skills")
|
||||
const skillDir = path.join(projectSkillsDir, "explaining-code")
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { stripUtf8Bom } from "@cline/shared"
|
||||
import * as yaml from "js-yaml"
|
||||
|
||||
export type FrontmatterParseResult = {
|
||||
@@ -35,11 +36,16 @@ export type FrontmatterParseResult = {
|
||||
* - If no frontmatter exists, returns data={} and body=original markdown.
|
||||
*/
|
||||
export function parseYamlFrontmatter(markdown: string): FrontmatterParseResult {
|
||||
// Strip a leading UTF-8 BOM (e.g. added by Windows Notepad's "UTF-8 with BOM" encoding),
|
||||
// which Node's `utf-8` decoding does not strip on its own. Without this the frontmatter
|
||||
// regex below never matches a file that starts with "\uFEFF---" (see cline/cline#12151).
|
||||
const normalizedMarkdown = stripUtf8Bom(markdown)
|
||||
|
||||
const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/
|
||||
const match = markdown.match(frontmatterRegex)
|
||||
const match = normalizedMarkdown.match(frontmatterRegex)
|
||||
|
||||
if (!match) {
|
||||
return { data: {}, body: markdown, hadFrontmatter: false }
|
||||
return { data: {}, body: normalizedMarkdown, hadFrontmatter: false }
|
||||
}
|
||||
|
||||
const [, yamlContent, body] = match
|
||||
@@ -48,6 +54,6 @@ export function parseYamlFrontmatter(markdown: string): FrontmatterParseResult {
|
||||
return { data, body, hadFrontmatter: true }
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
return { data: {}, body: markdown, hadFrontmatter: true, parseError: message }
|
||||
return { data: {}, body: normalizedMarkdown, hadFrontmatter: true, parseError: message }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ export async function getStateToPostToWebview(controller: {
|
||||
mcpHub?: any
|
||||
backgroundCommandRunning?: boolean
|
||||
backgroundCommandTaskId?: string
|
||||
foregroundCommandRunning?: boolean
|
||||
workspaceManager?: any
|
||||
checkpointRestoreInput?: ExtensionState["checkpointRestoreInput"]
|
||||
}): Promise<ExtensionState> {
|
||||
@@ -157,6 +158,7 @@ export async function getStateToPostToWebview(controller: {
|
||||
favoritedModelIds,
|
||||
backgroundCommandRunning: controller.backgroundCommandRunning ?? false,
|
||||
backgroundCommandTaskId: controller.backgroundCommandTaskId,
|
||||
foregroundCommandRunning: controller.foregroundCommandRunning ?? false,
|
||||
workspaceRoots: controller.workspaceManager?.getRoots?.() ?? [],
|
||||
primaryRootIndex: controller.workspaceManager?.getPrimaryIndex?.() ?? 0,
|
||||
isMultiRootWorkspace: (controller.workspaceManager?.getRoots?.()?.length ?? 0) > 1,
|
||||
|
||||
@@ -239,7 +239,10 @@ export async function updateSettings(controller: Controller, request: UpdateSett
|
||||
controller.stateManager.setGlobalState("defaultTerminalProfile", request.defaultTerminalProfile)
|
||||
// Update the live terminal manager so new terminals use the new profile.
|
||||
// Existing terminals are left open — they're keyed by effective shell
|
||||
// and reused when compatible, or skipped when not.
|
||||
// and reused when compatible, or skipped when not. No session rebuild
|
||||
// is needed: the run_commands tool re-reads the profile each time a
|
||||
// model request is built, so the description and execution both pick
|
||||
// up the new shell at the next request boundary.
|
||||
controller.terminalManager?.setDefaultTerminalProfile(request.defaultTerminalProfile)
|
||||
}
|
||||
|
||||
|
||||
@@ -192,7 +192,10 @@ export async function updateSettingsCli(controller: Controller, request: UpdateS
|
||||
controller.stateManager.setGlobalState("defaultTerminalProfile", defaultTerminalProfile)
|
||||
// Update the live terminal manager so new terminals use the new profile.
|
||||
// Existing terminals are left open — they're keyed by effective shell
|
||||
// and reused when compatible, or skipped when not.
|
||||
// and reused when compatible, or skipped when not. No session rebuild
|
||||
// is needed: the run_commands tool re-reads the profile each time a
|
||||
// model request is built, so the description and execution both pick
|
||||
// up the new shell at the next request boundary.
|
||||
controller.terminalManager?.setDefaultTerminalProfile(defaultTerminalProfile)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Empty, EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { Controller } from ".."
|
||||
|
||||
/**
|
||||
* "Proceed While Running": detach the in-flight foreground terminal command(s)
|
||||
* so the agent turn continues with the partial output while the commands keep
|
||||
* running in the user's terminal, streaming further output to a log file.
|
||||
*/
|
||||
export async function proceedWhileRunningCommand(controller: Controller, _request: EmptyRequest): Promise<Empty> {
|
||||
const controllerWithProceed = controller as Controller & {
|
||||
proceedWhileRunningCommand: () => Promise<void>
|
||||
}
|
||||
await controllerWithProceed.proceedWhileRunningCommand()
|
||||
return Empty.create()
|
||||
}
|
||||
@@ -15,8 +15,9 @@ Designed to be driven from an agentic loop via `curl` commands.
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Terminal 1: Start the debug harness server
|
||||
bun src/dev/debug-harness/server.ts --auto-launch --skip-build
|
||||
# Terminal 1: Start the debug harness server.
|
||||
# Run with node, NOT bun — Playwright's Electron launch times out under bun.
|
||||
node src/dev/debug-harness/server.ts --auto-launch --skip-build
|
||||
|
||||
# Terminal 2: Interact via curl
|
||||
curl localhost:19229/api -d '{"method":"status"}'
|
||||
@@ -27,7 +28,7 @@ curl localhost:19229/api -d '{"method":"ui.screenshot"}'
|
||||
## Server Options
|
||||
|
||||
```
|
||||
bun src/dev/debug-harness/server.ts [options]
|
||||
node src/dev/debug-harness/server.ts [options]
|
||||
|
||||
Options:
|
||||
--skip-build Skip building extension/webview (use existing dist/)
|
||||
@@ -42,7 +43,7 @@ Options:
|
||||
```bash
|
||||
# This builds protos, extension (unminified+sourcemaps), webview (unminified+sourcemaps),
|
||||
# downloads VSCode, launches it, and connects CDP to the extension host.
|
||||
bun src/dev/debug-harness/server.ts --auto-launch
|
||||
node src/dev/debug-harness/server.ts --auto-launch
|
||||
```
|
||||
|
||||
## Data Isolation
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env bun
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Debug Harness Server
|
||||
@@ -10,7 +10,12 @@
|
||||
* - UI automation (click, type, screenshot) via Playwright
|
||||
*
|
||||
* Usage:
|
||||
* bun src/dev/debug-harness/server.ts [options]
|
||||
* node src/dev/debug-harness/server.ts [options]
|
||||
*
|
||||
* Run with node, not bun: Playwright's _electron.launch() never finishes
|
||||
* attaching to the debugee under bun (the Electron process starts, but the
|
||||
* launch times out), while the same launch works under node. Node >= 22.6
|
||||
* runs this file directly via type stripping.
|
||||
*
|
||||
* Options:
|
||||
* --skip-build Skip building extension/webview
|
||||
@@ -39,7 +44,6 @@ import path from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import { downloadAndUnzipVSCode, SilentReporter } from "@vscode/test-electron"
|
||||
import { _electron, type CDPSession, type ElectronApplication, type Frame, type Page } from "playwright"
|
||||
import WebSocket from "ws"
|
||||
|
||||
const __script_dir = typeof __dirname !== "undefined" ? __dirname : path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
@@ -201,19 +205,21 @@ class CdpClient {
|
||||
|
||||
async connect(wsUrl: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// The runtime's built-in WebSocket (browser-style events), so the
|
||||
// harness has no dependency on the `ws` package.
|
||||
const ws = new WebSocket(wsUrl)
|
||||
ws.on("open", () => {
|
||||
ws.addEventListener("open", () => {
|
||||
this.ws = ws
|
||||
resolve()
|
||||
})
|
||||
ws.on("error", (e: Error) => {
|
||||
if (!this.ws) reject(e)
|
||||
ws.addEventListener("error", () => {
|
||||
if (!this.ws) reject(new Error(`WebSocket connection failed: ${wsUrl}`))
|
||||
})
|
||||
ws.on("close", () => {
|
||||
ws.addEventListener("close", () => {
|
||||
this.ws = null
|
||||
})
|
||||
ws.on("message", (raw: WebSocket.Data) => {
|
||||
const msg = JSON.parse(raw.toString())
|
||||
ws.addEventListener("message", (event: MessageEvent) => {
|
||||
const msg = JSON.parse(typeof event.data === "string" ? event.data : Buffer.from(event.data).toString())
|
||||
if (msg.id !== undefined) {
|
||||
const p = this.pending.get(msg.id)
|
||||
if (p) {
|
||||
|
||||
@@ -262,10 +262,15 @@ export class VscodeTerminalManager {
|
||||
return mergePromise(process, promise)
|
||||
}
|
||||
|
||||
async getOrCreateTerminal(cwd: string): Promise<ITerminalInfo> {
|
||||
/**
|
||||
* @param profileId Terminal profile to create/match the terminal with.
|
||||
* Defaults to the current setting; callers that captured the profile
|
||||
* earlier (e.g. when the model request was built) pass it here so a
|
||||
* settings change does not switch shells under an in-flight tool call.
|
||||
*/
|
||||
async getOrCreateTerminal(cwd: string, profileId: string = this.defaultTerminalProfile): Promise<ITerminalInfo> {
|
||||
const terminals = TerminalRegistry.getAllTerminals()
|
||||
const expectedShellPath =
|
||||
this.defaultTerminalProfile !== "default" ? getShellForProfile(this.defaultTerminalProfile) : undefined
|
||||
const expectedShellPath = profileId !== "default" ? getShellForProfile(profileId) : undefined
|
||||
// Resolve effective shell for comparison (so "default" and "zsh" match on macOS)
|
||||
const effectiveExpected = VscodeTerminalManager.effectiveShellPath(expectedShellPath)
|
||||
|
||||
|
||||
@@ -694,4 +694,41 @@ describe("TerminalProcess (Integration Tests)", () => {
|
||||
;(emitSpy as sinon.SinonSpy).calledWith("line", "line 3 continued").should.be.true()
|
||||
processAny.buffer.should.equal("")
|
||||
})
|
||||
|
||||
it("detach emits continue but keeps line listeners attached and listening", () => {
|
||||
const processAny = process as any
|
||||
const continueEvents: number[] = []
|
||||
const lines: string[] = []
|
||||
process.on("continue", () => continueEvents.push(1))
|
||||
process.on("line", (line) => lines.push(line))
|
||||
|
||||
process.detach()
|
||||
continueEvents.length.should.equal(1)
|
||||
|
||||
// Unlike continue(), detach must not stop listening or drop 'line'
|
||||
// listeners: output after detach still reaches subscribers (this is
|
||||
// what streams the rest of a detached command to the log file).
|
||||
processAny.isListening.should.be.true()
|
||||
processAny.emitIfEol("after detach\n")
|
||||
lines.should.containEql("after detach")
|
||||
})
|
||||
|
||||
it("detach flushes a buffered partial line before emitting continue", () => {
|
||||
const processAny = process as any
|
||||
const events: string[] = []
|
||||
process.on("continue", () => events.push("continue"))
|
||||
process.on("line", (line) => events.push(`line:${line}`))
|
||||
|
||||
// A chunk with no trailing newline stays in the internal buffer.
|
||||
processAny.emitIfEol("partial output")
|
||||
processAny.buffer.should.equal("partial output")
|
||||
|
||||
process.detach()
|
||||
|
||||
// The partial line must reach listeners before 'continue' resolves the
|
||||
// awaited promise; otherwise it is missing from the partial output and
|
||||
// from the log's initial flush.
|
||||
events.should.eql(["line:partial output", "continue"])
|
||||
processAny.buffer.should.equal("")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -16,8 +16,8 @@ import {
|
||||
TRUNCATE_KEEP_LINES,
|
||||
} from "@/integrations/terminal/constants"
|
||||
import type { ITerminalProcess, TerminalCompletionDetails, TerminalProcessEvents } from "@/integrations/terminal/types"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import type { MarkerlessCompletionCause } from "@/services/telemetry/TelemetryService"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { Osc633EventType, Osc633Parser } from "./osc633Parser"
|
||||
import { classifyShellPrompt, getLastLine } from "./shellPromptHeuristics"
|
||||
|
||||
@@ -522,6 +522,23 @@ export class VscodeTerminalProcess extends EventEmitter<TerminalProcessEvents> i
|
||||
this.emit("continue")
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the awaited promise while the command keeps running ("Proceed
|
||||
* While Running"). Listeners stay attached and 'line' events keep
|
||||
* flowing — unlike continue() — so callers can stream the remaining
|
||||
* output until the command actually completes. Because 'completed' is
|
||||
* only emitted by the read loop when the command genuinely ends, the
|
||||
* terminal stays busy and is not eligible for reuse until then.
|
||||
*/
|
||||
detach() {
|
||||
// Flush any partial line (no trailing newline yet) so it reaches
|
||||
// listeners before the awaited promise resolves; otherwise it would be
|
||||
// dropped from both the partial output and the log capture if the
|
||||
// command exits without further newline-terminated output.
|
||||
this.emitRemainingBufferIfListening()
|
||||
this.emit("continue")
|
||||
}
|
||||
|
||||
/**
|
||||
* Get output that hasn't been retrieved yet.
|
||||
* Truncates if output is too large to prevent context window overflow.
|
||||
|
||||
@@ -63,10 +63,17 @@ export interface ITerminalProcess extends EventEmitter<TerminalProcessEvents> {
|
||||
/**
|
||||
* Continue execution without waiting for completion.
|
||||
* Stops event emission and resolves the promise.
|
||||
* This is called when user clicks "Proceed While Running".
|
||||
*/
|
||||
continue(): void
|
||||
|
||||
/**
|
||||
* Resolve the awaited promise while the command keeps running ("Proceed
|
||||
* While Running"). Unlike continue(), output listeners stay attached and
|
||||
* 'line'/'completed' events keep flowing, so callers can stream the rest
|
||||
* of the output (e.g. to a log file) until the command completes.
|
||||
*/
|
||||
detach(): void
|
||||
|
||||
/**
|
||||
* Get output that hasn't been retrieved yet.
|
||||
* @returns The unretrieved output
|
||||
|
||||
@@ -68,6 +68,7 @@ import {
|
||||
import { SdkCompactionCoordinator } from "./sdk-compaction-coordinator"
|
||||
import { SdkDiffEditCoordinator } from "./sdk-diff-edit-coordinator"
|
||||
import { SdkFollowupCoordinator } from "./sdk-followup-coordinator"
|
||||
import { SdkForegroundCommandCoordinator } from "./sdk-foreground-command-coordinator"
|
||||
import { SdkInteractionCoordinator } from "./sdk-interaction-coordinator"
|
||||
import { SdkMcpCoordinator } from "./sdk-mcp-coordinator"
|
||||
import { SdkMessageCoordinator, type SessionEventListener } from "./sdk-message-coordinator"
|
||||
@@ -204,6 +205,15 @@ export class Controller {
|
||||
// standalone (JetBrains/CLI) host run commands through the SDK's built-in tool.
|
||||
private _terminalManager?: VscodeTerminalManager
|
||||
|
||||
// Registry of in-flight foreground (VS Code terminal) command executions.
|
||||
// Owned here — not by the session — so it survives session rebuilds, which
|
||||
// recreate the tool set. Drives the "Proceed While Running" button.
|
||||
private readonly foregroundCommands = new SdkForegroundCommandCoordinator({
|
||||
onRunningChanged: () => {
|
||||
void this.postStateToWebview()
|
||||
},
|
||||
})
|
||||
|
||||
// Private state kept for stub compatibility
|
||||
private backgroundCommandRunning = false
|
||||
private backgroundCommandTaskId?: string
|
||||
@@ -330,6 +340,7 @@ export class Controller {
|
||||
},
|
||||
onDidBecomeIdle: () => this.handleSessionBecameIdle(),
|
||||
getRemoteConfigIntegration: () => this.remoteConfigCoreIntegration,
|
||||
foregroundCommands: this.foregroundCommands,
|
||||
getTerminalManager: () => {
|
||||
// Guarded by getEffectiveTerminalExecutionMode() at the read sites
|
||||
// (vscode-session-host.ts, sdk-terminal-execution-mode-coordinator.ts):
|
||||
@@ -1174,6 +1185,19 @@ export class Controller {
|
||||
stubWarn("cancelBackgroundCommand")
|
||||
}
|
||||
|
||||
/**
|
||||
* "Proceed While Running": detach every in-flight foreground terminal
|
||||
* command. Each pending run_commands call returns its partial output plus
|
||||
* the log file path the remaining output is redirected to, and the agent
|
||||
* turn continues while the commands keep running in their terminals.
|
||||
*/
|
||||
async proceedWhileRunningCommand(): Promise<void> {
|
||||
const detached = this.foregroundCommands.proceedWhileRunning()
|
||||
if (detached === 0) {
|
||||
Logger.warn("[SdkController] proceedWhileRunningCommand: No foreground command is running")
|
||||
}
|
||||
}
|
||||
|
||||
async cancelQueuedPrompt(promptId: string): Promise<void> {
|
||||
const trimmedPromptId = promptId.trim()
|
||||
if (!trimmedPromptId) {
|
||||
@@ -1868,6 +1892,7 @@ export class Controller {
|
||||
mcpHub: this.mcpHub,
|
||||
backgroundCommandRunning: this.backgroundCommandRunning,
|
||||
backgroundCommandTaskId: this.backgroundCommandTaskId,
|
||||
foregroundCommandRunning: this.foregroundCommands.isRunning,
|
||||
})
|
||||
const sdkTaskHistory = (await this.taskHistory.listHistory({ limit: 100, hydrate: false }))
|
||||
.map(sessionHistoryRecordToHistoryItem)
|
||||
|
||||
@@ -454,13 +454,35 @@ describe("SdkDiffEditCoordinator", () => {
|
||||
expect(callOrder).toEqual(["close", "apply"])
|
||||
})
|
||||
|
||||
it("applies patches without preview sessions directly", async () => {
|
||||
it("shows a brief preview around auto-approved patches", async () => {
|
||||
await writeFile("patched.ts", "line one\nline two\n")
|
||||
const patch = ["*** Begin Patch", "*** Update File: patched.ts", "@@", "-line one", "+line ONE", "*** End Patch"].join(
|
||||
"\n",
|
||||
)
|
||||
|
||||
const result = await coordinator.executeApplyPatchTool({ input: patch }, tempDir, makeContext("tc9"))
|
||||
|
||||
expect(result).toBe("fallback apply_patch result")
|
||||
expect(fallbackApplyPatch).toHaveBeenCalledOnce()
|
||||
expect(previews).toHaveLength(1)
|
||||
expect(previews[0].opened).toMatchObject({
|
||||
absolutePath: path.join(tempDir, "patched.ts"),
|
||||
leftContent: "line one\nline two\n",
|
||||
rightContent: "line ONE\nline two\n",
|
||||
})
|
||||
expect(previews[0].closed).toBe(1)
|
||||
})
|
||||
|
||||
it("applies auto-approved patches without a preview when background edit is enabled", async () => {
|
||||
backgroundEdit = true
|
||||
const result = await coordinator.executeApplyPatchTool(
|
||||
{ input: "*** Begin Patch\n*** End Patch" },
|
||||
tempDir,
|
||||
makeContext("tc9"),
|
||||
)
|
||||
|
||||
expect(result).toBe("fallback apply_patch result")
|
||||
expect(fallbackApplyPatch).toHaveBeenCalledOnce()
|
||||
expect(previews).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -122,12 +122,32 @@ export class SdkDiffEditCoordinator {
|
||||
}
|
||||
|
||||
/**
|
||||
* The `apply_patch` tool executor override: close the preview, then delegate the
|
||||
* whole patch application to the SDK's default executor.
|
||||
* The `apply_patch` tool executor override: manually-approved patches close their
|
||||
* approval preview before applying; auto-approved patches show a brief preview
|
||||
* around execution, matching the `editor` tool behavior.
|
||||
*/
|
||||
async executeApplyPatchTool(input: ApplyPatchInput, cwd: string, context: AgentToolContext): Promise<string> {
|
||||
await this.discardPreview(context.toolCallId ?? "")
|
||||
return this.fallbackApplyPatchExecutor(input, cwd, context)
|
||||
const toolCallId = context.toolCallId ?? ""
|
||||
const hadPreApprovalPreview = this.sessions.has(toolCallId)
|
||||
try {
|
||||
if (hadPreApprovalPreview) {
|
||||
await this.discardPreview(toolCallId)
|
||||
} else if (!this.options.isBackgroundEditEnabled()) {
|
||||
try {
|
||||
await this.openPatchPreview(toolCallId, input)
|
||||
} catch (error) {
|
||||
Logger.warn(`[SdkDiffEditCoordinator] Failed to show auto-approve patch preview: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
const result = await this.fallbackApplyPatchExecutor(input, cwd, context)
|
||||
if (!hadPreApprovalPreview && this.sessions.get(toolCallId)?.preview) {
|
||||
await lingerDelay(this.autoApprovePreviewLingerMs, context.signal)
|
||||
}
|
||||
return result
|
||||
} finally {
|
||||
await this.discardPreview(toolCallId)
|
||||
}
|
||||
}
|
||||
|
||||
/** Closes one preview (reject / abort / edit applied). Never throws; unknown ids are a no-op. */
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { describe, expect, it, vi } from "vitest"
|
||||
import { SdkForegroundCommandCoordinator } from "./sdk-foreground-command-coordinator"
|
||||
|
||||
describe("SdkForegroundCommandCoordinator", () => {
|
||||
it("reports isRunning while a handle is registered and notifies on changes", () => {
|
||||
const onRunningChanged = vi.fn()
|
||||
const coordinator = new SdkForegroundCommandCoordinator({ onRunningChanged })
|
||||
|
||||
expect(coordinator.isRunning).toBe(false)
|
||||
|
||||
const unregister = coordinator.register({ detach: () => {} })
|
||||
expect(coordinator.isRunning).toBe(true)
|
||||
expect(onRunningChanged).toHaveBeenCalledWith(true)
|
||||
|
||||
unregister()
|
||||
expect(coordinator.isRunning).toBe(false)
|
||||
expect(onRunningChanged).toHaveBeenCalledWith(false)
|
||||
})
|
||||
|
||||
it("only notifies on actual transitions, not per handle", () => {
|
||||
const onRunningChanged = vi.fn()
|
||||
const coordinator = new SdkForegroundCommandCoordinator({ onRunningChanged })
|
||||
|
||||
const unregister1 = coordinator.register({ detach: () => {} })
|
||||
const unregister2 = coordinator.register({ detach: () => {} })
|
||||
expect(onRunningChanged).toHaveBeenCalledTimes(1)
|
||||
|
||||
unregister1()
|
||||
expect(onRunningChanged).toHaveBeenCalledTimes(1)
|
||||
unregister2()
|
||||
expect(onRunningChanged).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it("unregister is idempotent", () => {
|
||||
const onRunningChanged = vi.fn()
|
||||
const coordinator = new SdkForegroundCommandCoordinator({ onRunningChanged })
|
||||
|
||||
const unregister = coordinator.register({ detach: () => {} })
|
||||
unregister()
|
||||
unregister()
|
||||
expect(onRunningChanged).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it("proceedWhileRunning detaches every registered handle and reports the count", () => {
|
||||
const coordinator = new SdkForegroundCommandCoordinator()
|
||||
const detach1 = vi.fn()
|
||||
const detach2 = vi.fn()
|
||||
coordinator.register({ detach: detach1 })
|
||||
coordinator.register({ detach: detach2 })
|
||||
|
||||
expect(coordinator.proceedWhileRunning()).toBe(2)
|
||||
expect(detach1).toHaveBeenCalledTimes(1)
|
||||
expect(detach2).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("proceedWhileRunning is a no-op returning 0 when nothing is running", () => {
|
||||
const coordinator = new SdkForegroundCommandCoordinator()
|
||||
expect(coordinator.proceedWhileRunning()).toBe(0)
|
||||
})
|
||||
|
||||
it("proceedWhileRunning survives a handle whose detach throws", () => {
|
||||
const coordinator = new SdkForegroundCommandCoordinator()
|
||||
const detach2 = vi.fn()
|
||||
coordinator.register({
|
||||
detach: () => {
|
||||
throw new Error("boom")
|
||||
},
|
||||
})
|
||||
coordinator.register({ detach: detach2 })
|
||||
|
||||
expect(coordinator.proceedWhileRunning()).toBe(2)
|
||||
expect(detach2).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Tracks in-flight foreground (VS Code terminal) command executions so the
|
||||
* "Proceed While Running" button can detach them: each pending tool call
|
||||
* returns with its partial output while the command keeps running in the
|
||||
* user's terminal, streaming further output to a log file.
|
||||
*
|
||||
* Owned by SdkController so it outlives session rebuilds (which recreate the
|
||||
* tool set and its reused executor closure). Handles are registered per tool
|
||||
* invocation — never on the reused executor — so parallel commands in one
|
||||
* tool call each get their own handle and log file.
|
||||
*/
|
||||
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
|
||||
export interface ForegroundCommandHandle {
|
||||
/**
|
||||
* Stop waiting for the command: flush the output captured so far to a
|
||||
* log file, keep appending until the command completes, and resolve the
|
||||
* pending tool execution with the partial output. Idempotent.
|
||||
*/
|
||||
detach(): void
|
||||
}
|
||||
|
||||
export interface SdkForegroundCommandCoordinatorOptions {
|
||||
/** Called whenever isRunning flips; used to push the flag to the webview. */
|
||||
onRunningChanged?: (running: boolean) => void
|
||||
}
|
||||
|
||||
export class SdkForegroundCommandCoordinator {
|
||||
private readonly handles = new Set<ForegroundCommandHandle>()
|
||||
|
||||
constructor(private readonly options: SdkForegroundCommandCoordinatorOptions = {}) {}
|
||||
|
||||
/** Whether any foreground command is currently awaited by a tool call. */
|
||||
get isRunning(): boolean {
|
||||
return this.handles.size > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Track one in-flight foreground execution. Returns an unregister
|
||||
* function the caller must invoke when the execution settles (completes,
|
||||
* fails, aborts, or detaches) — typically from a `finally` block.
|
||||
*/
|
||||
register(handle: ForegroundCommandHandle): () => void {
|
||||
const wasRunning = this.isRunning
|
||||
this.handles.add(handle)
|
||||
this.notifyIfChanged(wasRunning)
|
||||
return () => {
|
||||
const wasRunningBefore = this.isRunning
|
||||
if (this.handles.delete(handle)) {
|
||||
this.notifyIfChanged(wasRunningBefore)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detach every in-flight foreground command ("Proceed While Running").
|
||||
* Each pending tool execution resolves with its partial output and log
|
||||
* file path; the commands keep running in their terminals.
|
||||
*
|
||||
* @returns the number of commands detached (0 when none were running).
|
||||
*/
|
||||
proceedWhileRunning(): number {
|
||||
const handles = [...this.handles]
|
||||
for (const handle of handles) {
|
||||
try {
|
||||
handle.detach()
|
||||
} catch (error) {
|
||||
Logger.error("[ForegroundCommands] Failed to detach foreground command:", error)
|
||||
}
|
||||
}
|
||||
return handles.length
|
||||
}
|
||||
|
||||
private notifyIfChanged(wasRunning: boolean): void {
|
||||
if (this.isRunning !== wasRunning) {
|
||||
this.options.onRunningChanged?.(this.isRunning)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import type { VscodeTerminalManager } from "@/hosts/vscode/terminal/VscodeTermin
|
||||
import { McpHub } from "@/services/mcp/McpHub"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import type { ActiveSession } from "./cline-session-factory"
|
||||
import type { SdkForegroundCommandCoordinator } from "./sdk-foreground-command-coordinator"
|
||||
import { buildToolPolicies } from "./sdk-tool-policies"
|
||||
import type { SdkSessionHost } from "./session-host"
|
||||
import { VscodeSessionHost } from "./vscode-session-host"
|
||||
@@ -32,6 +33,8 @@ export interface SdkSessionLifecycleOptions {
|
||||
onSessionEvent: (event: CoreSessionEvent) => void
|
||||
/** Lazy factory for the VscodeTerminalManager (foreground terminal support). */
|
||||
getTerminalManager?: () => VscodeTerminalManager
|
||||
/** Registry of in-flight foreground executions for "Proceed While Running". */
|
||||
foregroundCommands?: SdkForegroundCommandCoordinator
|
||||
/** Returns the latest prepared remote-config integration, if remote config is active. */
|
||||
getRemoteConfigIntegration?: () => PreparedRemoteConfigCoreIntegration | undefined
|
||||
/** Shared SDK telemetry service owned by SdkController. */
|
||||
@@ -322,6 +325,7 @@ export class SdkSessionLifecycle {
|
||||
editorExecutor: this.options.editorExecutor,
|
||||
applyPatchExecutor: this.options.applyPatchExecutor,
|
||||
getTerminalManager: this.options.getTerminalManager,
|
||||
foregroundCommands: this.options.foregroundCommands,
|
||||
getRemoteConfigIntegration: this.options.getRemoteConfigIntegration,
|
||||
telemetry: this.options.telemetry,
|
||||
})
|
||||
|
||||
@@ -33,8 +33,11 @@ export class SdkTerminalExecutionModeCoordinator {
|
||||
if (previous === next) {
|
||||
return
|
||||
}
|
||||
this.requestRebuild(`Terminal execution mode changed: ${previous} -> ${next}`)
|
||||
}
|
||||
|
||||
Logger.log(`[SdkController] Terminal execution mode changed: ${previous} -> ${next}`)
|
||||
private requestRebuild(reason: string): void {
|
||||
Logger.log(`[SdkController] ${reason}`)
|
||||
|
||||
const activeSession = this.options.sessions.getActiveSession()
|
||||
if (!activeSession) {
|
||||
|
||||
@@ -1,9 +1,111 @@
|
||||
import { CommandExitError } from "@cline/core"
|
||||
import { EventEmitter } from "events"
|
||||
import { describe, expect, it } from "vitest"
|
||||
import type { TerminalCompletionDetails } from "@/integrations/terminal/types"
|
||||
import * as fs from "fs"
|
||||
import { afterEach, describe, expect, it, vi } from "vitest"
|
||||
import * as vscode from "vscode"
|
||||
import type { VscodeTerminalManager } from "@/hosts/vscode/terminal/VscodeTerminalManager"
|
||||
import { executeForeground, formatCommandForTerminal } from "./vscode-run-commands-tool"
|
||||
import type { TerminalCompletionDetails } from "@/integrations/terminal/types"
|
||||
import { SdkForegroundCommandCoordinator } from "./sdk-foreground-command-coordinator"
|
||||
import {
|
||||
createVscodeRunCommandsTool,
|
||||
executeForeground,
|
||||
formatCommandForTerminal,
|
||||
PROCEED_LOG_MAX_BYTES,
|
||||
} from "./vscode-run-commands-tool"
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
existsSync: vi.fn<(path: fs.PathLike) => boolean>(),
|
||||
getGlobalSettingsKey: vi.fn(() => "default"),
|
||||
}))
|
||||
|
||||
vi.mock("fs", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("fs")>()),
|
||||
existsSync: mocks.existsSync,
|
||||
}))
|
||||
|
||||
vi.mock("@/core/storage/StateManager", () => ({
|
||||
StateManager: {
|
||||
get: () => ({ getGlobalSettingsKey: mocks.getGlobalSettingsKey }),
|
||||
},
|
||||
}))
|
||||
|
||||
// The real telemetry proxy lazily initializes TelemetryService, which requires
|
||||
// a HostProvider that unit tests don't set up.
|
||||
vi.mock("@services/telemetry", () => ({
|
||||
TerminalUserInterventionAction: { PROCESS_WHILE_RUNNING: "process_while_running" },
|
||||
telemetryService: {
|
||||
captureTerminalUserIntervention: () => {},
|
||||
captureTerminalExecution: () => {},
|
||||
},
|
||||
}))
|
||||
|
||||
const originalPlatform = process.platform
|
||||
const originalEnv = { ...process.env }
|
||||
const originalGetConfiguration = vscode.workspace.getConfiguration
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(process, "platform", { value: originalPlatform })
|
||||
process.env = { ...originalEnv }
|
||||
vscode.workspace.getConfiguration = originalGetConfiguration
|
||||
mocks.existsSync.mockReset()
|
||||
mocks.getGlobalSettingsKey.mockReset()
|
||||
mocks.getGlobalSettingsKey.mockReturnValue("default")
|
||||
})
|
||||
|
||||
describe("createVscodeRunCommandsTool", () => {
|
||||
it("constructs a cmd tool from the stock array-valued Command Prompt profile", () => {
|
||||
Object.defineProperty(process, "platform", { value: "win32" })
|
||||
process.env.windir = "C:\\Windows"
|
||||
mocks.existsSync.mockImplementation((candidate) => candidate === "C:\\Windows\\System32\\cmd.exe")
|
||||
vscode.workspace.getConfiguration = () =>
|
||||
({
|
||||
get: (key: string) => {
|
||||
if (key === "defaultProfile.windows") {
|
||||
return "Command Prompt"
|
||||
}
|
||||
if (key === "profiles.windows") {
|
||||
return {
|
||||
"Command Prompt": {
|
||||
path: [`\${env:windir}\\Sysnative\\cmd.exe`, `\${env:windir}\\System32\\cmd.exe`],
|
||||
},
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
}) as never
|
||||
|
||||
const tool = createVscodeRunCommandsTool({
|
||||
cwd: "C:\\workspace",
|
||||
getTerminalManager: () => {
|
||||
throw new Error("Terminal manager should not be created during tool construction")
|
||||
},
|
||||
vscodeTerminalExecutionMode: "vscodeTerminal",
|
||||
})
|
||||
|
||||
expect(tool.name).toBe("run_commands")
|
||||
expect(tool.description).toContain("Commands run through cmd.exe")
|
||||
})
|
||||
|
||||
it("re-derives the description from the current profile on each read", () => {
|
||||
Object.defineProperty(process, "platform", { value: "win32" })
|
||||
mocks.existsSync.mockReturnValue(true)
|
||||
mocks.getGlobalSettingsKey.mockReturnValue("cmd")
|
||||
|
||||
const tool = createVscodeRunCommandsTool({
|
||||
cwd: "C:\\workspace",
|
||||
getTerminalManager: () => {
|
||||
throw new Error("Terminal manager should not be created during tool construction")
|
||||
},
|
||||
vscodeTerminalExecutionMode: "vscodeTerminal",
|
||||
})
|
||||
expect(tool.description).toContain("Commands run through cmd.exe")
|
||||
|
||||
// A profile change takes effect at the next description read (the
|
||||
// model-request boundary), without a session rebuild.
|
||||
mocks.getGlobalSettingsKey.mockReturnValue("powershell-7")
|
||||
expect(tool.description).toContain("Commands run through PowerShell")
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Minimal fake of the process object returned by VscodeTerminalManager.runCommand():
|
||||
@@ -12,11 +114,13 @@ import { executeForeground, formatCommandForTerminal } from "./vscode-run-comman
|
||||
*/
|
||||
function createFakeTerminalProcess(options: { lines?: string[]; completionDetails?: TerminalCompletionDetails } = {}) {
|
||||
const emitter = new EventEmitter()
|
||||
let resolvePromise!: () => void
|
||||
// Emit on a macrotask (not a microtask) so executeForeground's
|
||||
// `await terminalManager.getOrCreateTerminal(cwd)` and subsequent
|
||||
// `process.on("line", ...)` registration are guaranteed to run first,
|
||||
// matching the ordering a real terminal process provides.
|
||||
const promise = new Promise<void>((resolve) => {
|
||||
resolvePromise = resolve
|
||||
setTimeout(() => {
|
||||
for (const line of options.lines ?? []) {
|
||||
emitter.emit("line", line)
|
||||
@@ -31,6 +135,10 @@ function createFakeTerminalProcess(options: { lines?: string[]; completionDetail
|
||||
catch: promise.catch.bind(promise),
|
||||
finally: promise.finally.bind(promise),
|
||||
getCompletionDetails: () => options.completionDetails ?? {},
|
||||
detach: () => {
|
||||
emitter.emit("continue")
|
||||
resolvePromise()
|
||||
},
|
||||
})
|
||||
return fakeProcess as unknown as ReturnType<VscodeTerminalManager["runCommand"]>
|
||||
}
|
||||
@@ -42,6 +150,50 @@ function createFakeTerminalManager(process: ReturnType<VscodeTerminalManager["ru
|
||||
} as unknown as VscodeTerminalManager
|
||||
}
|
||||
|
||||
/**
|
||||
* A controllable fake terminal process for detach tests: the caller decides
|
||||
* when lines are emitted and when the command completes. Mirrors the real
|
||||
* VscodeTerminalProcess contract: detach() resolves the awaited promise while
|
||||
* 'line'/'completed' events keep flowing.
|
||||
*/
|
||||
function createControllableTerminalProcess() {
|
||||
const emitter = new EventEmitter()
|
||||
let resolvePromise!: () => void
|
||||
const promise = new Promise<void>((resolve) => {
|
||||
resolvePromise = resolve
|
||||
})
|
||||
const fakeProcess = Object.assign(emitter, {
|
||||
then: promise.then.bind(promise),
|
||||
catch: promise.catch.bind(promise),
|
||||
finally: promise.finally.bind(promise),
|
||||
getCompletionDetails: () => ({}),
|
||||
detach: () => {
|
||||
emitter.emit("continue")
|
||||
resolvePromise()
|
||||
},
|
||||
})
|
||||
return {
|
||||
process: fakeProcess as unknown as ReturnType<VscodeTerminalManager["runCommand"]>,
|
||||
emitLine: (line: string) => emitter.emit("line", line),
|
||||
complete: (details?: TerminalCompletionDetails) => {
|
||||
emitter.emit("completed", details)
|
||||
emitter.emit("continue")
|
||||
resolvePromise()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/** Poll until the predicate holds, for asserting on async log-file writes. */
|
||||
async function waitFor(predicate: () => boolean, timeoutMs = 2000): Promise<void> {
|
||||
const start = Date.now()
|
||||
while (!predicate()) {
|
||||
if (Date.now() - start > timeoutMs) {
|
||||
throw new Error("waitFor timed out")
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
}
|
||||
}
|
||||
|
||||
describe("formatCommandForTerminal", () => {
|
||||
it.each([
|
||||
{
|
||||
@@ -129,6 +281,19 @@ describe("executeForeground", () => {
|
||||
expect(result).toBe("hello")
|
||||
})
|
||||
|
||||
it("passes the caller's terminal profile through to getOrCreateTerminal", async () => {
|
||||
const process = createFakeTerminalProcess({ lines: ["ok"] })
|
||||
const getOrCreateTerminal = vi.fn(async () => ({ terminal: { show: () => {} } }) as never)
|
||||
const terminalManager = {
|
||||
getOrCreateTerminal,
|
||||
runCommand: () => process,
|
||||
} as unknown as VscodeTerminalManager
|
||||
|
||||
await executeForeground("echo ok", "/workspace", terminalManager, 1000, undefined, undefined, "wsl-bash")
|
||||
|
||||
expect(getOrCreateTerminal).toHaveBeenCalledWith("/workspace", "wsl-bash")
|
||||
})
|
||||
|
||||
it("throws CommandExitError with the exit code on non-zero exit", async () => {
|
||||
const terminalManager = createFakeTerminalManager(
|
||||
createFakeTerminalProcess({ lines: ["boom"], completionDetails: { exitCode: 127 } }),
|
||||
@@ -172,4 +337,207 @@ describe("executeForeground", () => {
|
||||
expect((error as InstanceType<typeof CommandExitError>).output).toContain("Terminal closed")
|
||||
}
|
||||
})
|
||||
|
||||
it("unregisters its foreground handle when the command completes normally", async () => {
|
||||
const coordinator = new SdkForegroundCommandCoordinator()
|
||||
const terminalManager = createFakeTerminalManager(createFakeTerminalProcess({ lines: ["hello"] }))
|
||||
|
||||
const result = await executeForeground("echo hello", "/workspace", terminalManager, 1000, undefined, coordinator)
|
||||
|
||||
expect(result).toBe("hello")
|
||||
expect(coordinator.isRunning).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("executeForeground — Proceed While Running", () => {
|
||||
it("detach returns the partial output with the log file path, and later output lands in the log", async () => {
|
||||
const coordinator = new SdkForegroundCommandCoordinator()
|
||||
const { process, emitLine, complete } = createControllableTerminalProcess()
|
||||
const terminalManager = createFakeTerminalManager(process)
|
||||
|
||||
const resultPromise = executeForeground("devserver", "/workspace", terminalManager, 100_000, undefined, coordinator)
|
||||
|
||||
await waitFor(() => coordinator.isRunning)
|
||||
emitLine("listening on :3000")
|
||||
|
||||
expect(coordinator.proceedWhileRunning()).toBe(1)
|
||||
const result = await resultPromise
|
||||
|
||||
expect(result).toContain("still running")
|
||||
expect(result).toContain("listening on :3000")
|
||||
const logFilePath = /redirected to this file[^:]*: (.+)$/m.exec(result)?.[1]?.trim()
|
||||
expect(logFilePath).toBeTruthy()
|
||||
|
||||
// The handle is unregistered once the tool call returns.
|
||||
expect(coordinator.isRunning).toBe(false)
|
||||
|
||||
// Output emitted after detach is appended to the log file, and
|
||||
// completion closes it out with a completion marker.
|
||||
emitLine("compiled successfully")
|
||||
complete({ exitCode: 0 })
|
||||
await waitFor(() => {
|
||||
try {
|
||||
return fs.readFileSync(logFilePath!, "utf8").includes("[Command completed with exit code 0]")
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
})
|
||||
const log = fs.readFileSync(logFilePath!, "utf8")
|
||||
expect(log).toContain("listening on :3000") // buffered lines flushed at detach
|
||||
expect(log).toContain("compiled successfully") // streamed after detach
|
||||
fs.rmSync(logFilePath!, { force: true })
|
||||
})
|
||||
|
||||
it("detaches each parallel command into its own log file", async () => {
|
||||
const coordinator = new SdkForegroundCommandCoordinator()
|
||||
const first = createControllableTerminalProcess()
|
||||
const second = createControllableTerminalProcess()
|
||||
|
||||
const firstPromise = executeForeground(
|
||||
"first-cmd",
|
||||
"/workspace",
|
||||
createFakeTerminalManager(first.process),
|
||||
100_000,
|
||||
undefined,
|
||||
coordinator,
|
||||
)
|
||||
const secondPromise = executeForeground(
|
||||
"second-cmd",
|
||||
"/workspace",
|
||||
createFakeTerminalManager(second.process),
|
||||
100_000,
|
||||
undefined,
|
||||
coordinator,
|
||||
)
|
||||
|
||||
await waitFor(() => coordinator.isRunning)
|
||||
first.emitLine("first output")
|
||||
second.emitLine("second output")
|
||||
|
||||
expect(coordinator.proceedWhileRunning()).toBe(2)
|
||||
const [firstResult, secondResult] = await Promise.all([firstPromise, secondPromise])
|
||||
|
||||
const firstLog = /redirected to this file[^:]*: (.+)$/m.exec(firstResult)?.[1]?.trim()
|
||||
const secondLog = /redirected to this file[^:]*: (.+)$/m.exec(secondResult)?.[1]?.trim()
|
||||
expect(firstLog).toBeTruthy()
|
||||
expect(secondLog).toBeTruthy()
|
||||
expect(firstLog).not.toBe(secondLog)
|
||||
expect(coordinator.isRunning).toBe(false)
|
||||
|
||||
first.complete()
|
||||
second.complete()
|
||||
await waitFor(() => {
|
||||
try {
|
||||
return (
|
||||
fs.readFileSync(firstLog!, "utf8").includes("[Command completed]") &&
|
||||
fs.readFileSync(secondLog!, "utf8").includes("[Command completed]")
|
||||
)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
})
|
||||
expect(fs.readFileSync(firstLog!, "utf8")).toContain("first output")
|
||||
expect(fs.readFileSync(secondLog!, "utf8")).toContain("second output")
|
||||
fs.rmSync(firstLog!, { force: true })
|
||||
fs.rmSync(secondLog!, { force: true })
|
||||
})
|
||||
|
||||
it("stops logging before a line that would exceed the size cap", async () => {
|
||||
const coordinator = new SdkForegroundCommandCoordinator()
|
||||
const { process, emitLine, complete } = createControllableTerminalProcess()
|
||||
const terminalManager = createFakeTerminalManager(process)
|
||||
|
||||
const resultPromise = executeForeground("devserver", "/workspace", terminalManager, 100_000, undefined, coordinator)
|
||||
await waitFor(() => coordinator.isRunning)
|
||||
|
||||
expect(coordinator.proceedWhileRunning()).toBe(1)
|
||||
const result = await resultPromise
|
||||
const logFilePath = /redirected to this file[^:]*: (.+)$/m.exec(result)?.[1]?.trim()
|
||||
expect(logFilePath).toBeTruthy()
|
||||
|
||||
// A single line larger than the whole cap must not be written at all —
|
||||
// the cap is checked before writing, so one huge line (e.g. a dumped
|
||||
// blob) cannot blow the log far past PROCEED_LOG_MAX_BYTES.
|
||||
emitLine("small line before the blob")
|
||||
emitLine("x".repeat(PROCEED_LOG_MAX_BYTES))
|
||||
emitLine("after the cap")
|
||||
complete({ exitCode: 0 })
|
||||
|
||||
await waitFor(() => {
|
||||
try {
|
||||
return fs.readFileSync(logFilePath!, "utf8").includes("[Command completed with exit code 0]")
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
})
|
||||
const log = fs.readFileSync(logFilePath!, "utf8")
|
||||
expect(log).toContain("small line before the blob")
|
||||
expect(log).toContain(`[Log size cap of ${PROCEED_LOG_MAX_BYTES} bytes reached`)
|
||||
expect(log).not.toContain("xxxx")
|
||||
expect(log).not.toContain("after the cap")
|
||||
expect(log.length).toBeLessThan(PROCEED_LOG_MAX_BYTES)
|
||||
fs.rmSync(logFilePath!, { force: true })
|
||||
})
|
||||
|
||||
it("applies the size cap to lines buffered before detach", async () => {
|
||||
const coordinator = new SdkForegroundCommandCoordinator()
|
||||
const { process, emitLine, complete } = createControllableTerminalProcess()
|
||||
const terminalManager = createFakeTerminalManager(process)
|
||||
|
||||
const resultPromise = executeForeground("devserver", "/workspace", terminalManager, 100_000, undefined, coordinator)
|
||||
await waitFor(() => coordinator.isRunning)
|
||||
emitLine("x".repeat(PROCEED_LOG_MAX_BYTES))
|
||||
|
||||
expect(coordinator.proceedWhileRunning()).toBe(1)
|
||||
const result = await resultPromise
|
||||
const logFilePath = /redirected to this file[^:]*: (.+)$/m.exec(result)?.[1]?.trim()
|
||||
expect(logFilePath).toBeTruthy()
|
||||
complete({ exitCode: 0 })
|
||||
|
||||
await waitFor(() => {
|
||||
try {
|
||||
return fs.readFileSync(logFilePath!, "utf8").includes("[Command completed with exit code 0]")
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
})
|
||||
const log = fs.readFileSync(logFilePath!, "utf8")
|
||||
expect(log).toContain(`[Log size cap of ${PROCEED_LOG_MAX_BYTES} bytes reached`)
|
||||
expect(log).not.toContain("xxxx")
|
||||
expect(Buffer.byteLength(log)).toBeLessThanOrEqual(PROCEED_LOG_MAX_BYTES)
|
||||
fs.rmSync(logFilePath!, { force: true })
|
||||
})
|
||||
|
||||
it("freezes the partial output at detach while later output still reaches the log", async () => {
|
||||
const coordinator = new SdkForegroundCommandCoordinator()
|
||||
const { process, emitLine, complete } = createControllableTerminalProcess()
|
||||
const terminalManager = createFakeTerminalManager(process)
|
||||
|
||||
const resultPromise = executeForeground("devserver", "/workspace", terminalManager, 100_000, undefined, coordinator)
|
||||
await waitFor(() => coordinator.isRunning)
|
||||
emitLine("before detach")
|
||||
|
||||
expect(coordinator.proceedWhileRunning()).toBe(1)
|
||||
// Emitted after detach but before the tool call's result is built:
|
||||
// must appear only in the log, never in the partial output.
|
||||
emitLine("after detach")
|
||||
const result = await resultPromise
|
||||
|
||||
expect(result).toContain("before detach")
|
||||
expect(result).not.toContain("after detach")
|
||||
|
||||
const logFilePath = /redirected to this file[^:]*: (.+)$/m.exec(result)?.[1]?.trim()
|
||||
complete({ exitCode: 0 })
|
||||
await waitFor(() => {
|
||||
try {
|
||||
return fs.readFileSync(logFilePath!, "utf8").includes("[Command completed with exit code 0]")
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
})
|
||||
const log = fs.readFileSync(logFilePath!, "utf8")
|
||||
expect(log).toContain("before detach")
|
||||
expect(log).toContain("after detach")
|
||||
fs.rmSync(logFilePath!, { force: true })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -21,12 +21,16 @@ import {
|
||||
truncateCommandOutput,
|
||||
} from "@cline/core"
|
||||
import type { AgentTool } from "@cline/shared"
|
||||
import { telemetryService } from "@services/telemetry"
|
||||
import { TerminalUserInterventionAction, telemetryService } from "@services/telemetry"
|
||||
import { ClineTempManager } from "@services/temp"
|
||||
import * as fs from "fs"
|
||||
import { StateManager } from "@/core/storage/StateManager"
|
||||
import type { VscodeTerminalManager } from "@/hosts/vscode/terminal/VscodeTerminalManager"
|
||||
import { MAX_UNRETRIEVED_LINES } from "@/integrations/terminal/constants"
|
||||
import type { ITerminalProcess } from "@/integrations/terminal/types"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { getShellForProfile } from "@/utils/shell"
|
||||
import type { SdkForegroundCommandCoordinator } from "./sdk-foreground-command-coordinator"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
@@ -38,6 +42,14 @@ type VscodeTerminalExecutionMode = "vscodeTerminal" | "backgroundExec"
|
||||
/** Foreground VS Code terminals cannot be forcibly terminated; give long-running commands room to finish. */
|
||||
export const VSCODE_FOREGROUND_RUN_COMMANDS_TIMEOUT_MS = 60 * 60 * 1000
|
||||
|
||||
/**
|
||||
* Cap on the "Proceed While Running" log file. A detached devserver can log
|
||||
* for days; once the cap is hit we stop appending and note the truncation.
|
||||
* ClineTempManager's periodic cleanup (age + total-size caps) is the backstop
|
||||
* for the files themselves.
|
||||
*/
|
||||
export const PROCEED_LOG_MAX_BYTES = 10 * 1024 * 1024
|
||||
|
||||
/** Options for creating the VSCode run_commands tool. */
|
||||
export interface VscodeRunCommandsToolOptions {
|
||||
/** Workspace root directory. */
|
||||
@@ -48,6 +60,14 @@ export interface VscodeRunCommandsToolOptions {
|
||||
bashTimeoutMs?: number
|
||||
/** Terminal execution mode captured when this session's tool set is built. */
|
||||
vscodeTerminalExecutionMode?: VscodeTerminalExecutionMode
|
||||
/**
|
||||
* Registry of in-flight foreground executions, owned by SdkController.
|
||||
* When provided, each foreground command can be detached via the
|
||||
* "Proceed While Running" button. Foreground-only: background (SDK
|
||||
* child_process) executions cannot be detached — their abort signal
|
||||
* kills the process tree.
|
||||
*/
|
||||
foregroundCommands?: SdkForegroundCommandCoordinator
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -74,6 +94,69 @@ export function formatCommandForTerminal(command: ShellCommand): string {
|
||||
return [command.command, ...(command.args ?? [])].map(quoteShellArg).join(" ")
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream the rest of a detached command's output to a log file: write the
|
||||
* lines buffered so far, then append each further 'line' event until
|
||||
* 'completed'. The write volume is capped at PROCEED_LOG_MAX_BYTES; the
|
||||
* stream is always closed by the 'completed' event, which the terminal
|
||||
* process emits on every exit path (command end, Ctrl+C, terminal closed,
|
||||
* markerless fallback).
|
||||
*/
|
||||
function beginLogCapture(process: ITerminalProcess, terminalCommand: string, existingLines: string[]): string {
|
||||
const logFilePath = ClineTempManager.createTempFilePath("proceed-while-running")
|
||||
const stream = fs.createWriteStream(logFilePath, { flags: "a" })
|
||||
const sizeCapMessage = `[Log size cap of ${PROCEED_LOG_MAX_BYTES} bytes reached; further output is not logged.]`
|
||||
stream.on("error", (error) => {
|
||||
Logger.error(`[VscodeRunCommands] Failed writing proceed-while-running log ${logFilePath}:`, error)
|
||||
})
|
||||
|
||||
let bytesWritten = 0
|
||||
const tryWriteLine = (line: string): boolean => {
|
||||
const chunk = `${line}\n`
|
||||
const chunkBytes = Buffer.byteLength(chunk)
|
||||
if (bytesWritten + chunkBytes > PROCEED_LOG_MAX_BYTES) {
|
||||
return false
|
||||
}
|
||||
bytesWritten += chunkBytes
|
||||
stream.write(chunk)
|
||||
return true
|
||||
}
|
||||
|
||||
let sizeCapReached = !tryWriteLine(`[Running command: ${terminalCommand}]`)
|
||||
for (const line of existingLines) {
|
||||
if (!tryWriteLine(line)) {
|
||||
sizeCapReached = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const onLine = (line: string): void => {
|
||||
// Check the cap before writing: a single huge line (e.g. a dumped
|
||||
// binary blob or minified bundle) must not blow past the cap.
|
||||
if (!tryWriteLine(line)) {
|
||||
tryWriteLine(sizeCapMessage)
|
||||
process.removeListener("line", onLine)
|
||||
}
|
||||
}
|
||||
if (sizeCapReached) {
|
||||
tryWriteLine(sizeCapMessage)
|
||||
} else {
|
||||
process.on("line", onLine)
|
||||
}
|
||||
process.once("completed", (details) => {
|
||||
process.removeListener("line", onLine)
|
||||
const exitCode = details?.exitCode
|
||||
tryWriteLine(
|
||||
exitCode !== undefined && exitCode !== null
|
||||
? `[Command completed with exit code ${exitCode}]`
|
||||
: "[Command completed]",
|
||||
)
|
||||
stream.end()
|
||||
})
|
||||
|
||||
return logFilePath
|
||||
}
|
||||
|
||||
/** Exported for direct unit testing of the CommandExitError/terminalClosed mapping. */
|
||||
export async function executeForeground(
|
||||
command: ShellCommand,
|
||||
@@ -81,9 +164,11 @@ export async function executeForeground(
|
||||
terminalManager: VscodeTerminalManager,
|
||||
maxOutputChars: number,
|
||||
abortSignal?: AbortSignal,
|
||||
foregroundCommands?: SdkForegroundCommandCoordinator,
|
||||
terminalProfileId?: string,
|
||||
): Promise<string> {
|
||||
const terminalCommand = formatCommandForTerminal(command)
|
||||
const terminalInfo = await terminalManager.getOrCreateTerminal(cwd)
|
||||
const terminalInfo = await terminalManager.getOrCreateTerminal(cwd, terminalProfileId)
|
||||
terminalInfo.terminal.show()
|
||||
|
||||
const process = terminalManager.runCommand(terminalInfo, terminalCommand)
|
||||
@@ -100,7 +185,7 @@ export async function executeForeground(
|
||||
// truncateCommandOutput's own head/tail strategy below — since build/test
|
||||
// failures usually appear at the end of output.
|
||||
const maxBufferedLines = MAX_UNRETRIEVED_LINES
|
||||
process.on("line", (line: string) => {
|
||||
const bufferLine = (line: string): void => {
|
||||
if (outputLines.length < maxBufferedLines) {
|
||||
outputLines.push(line)
|
||||
} else {
|
||||
@@ -108,7 +193,8 @@ export async function executeForeground(
|
||||
outputLines.push(line)
|
||||
droppedLines++
|
||||
}
|
||||
})
|
||||
}
|
||||
process.on("line", bufferLine)
|
||||
|
||||
// Handle abort signal
|
||||
if (abortSignal) {
|
||||
@@ -121,8 +207,33 @@ export async function executeForeground(
|
||||
process.once("continue", cleanupAbortListener)
|
||||
}
|
||||
|
||||
// Wait for completion
|
||||
await process
|
||||
// "Proceed While Running": register a per-invocation handle so the user
|
||||
// can detach this command. Detaching redirects the remaining output to a
|
||||
// log file and resolves the awaited promise; the command keeps running in
|
||||
// the user's terminal (and the terminal stays busy until it completes).
|
||||
let detachedLogFilePath: string | undefined
|
||||
const unregister = foregroundCommands?.register({
|
||||
detach: () => {
|
||||
if (detachedLogFilePath !== undefined) {
|
||||
return
|
||||
}
|
||||
detachedLogFilePath = beginLogCapture(process, terminalCommand, outputLines)
|
||||
telemetryService.captureTerminalUserIntervention(TerminalUserInterventionAction.PROCESS_WHILE_RUNNING, "vscode")
|
||||
// detach() flushes any partial line (reaching both bufferLine and
|
||||
// the log) before resolving the awaited promise. After that the
|
||||
// partial output is final: stop buffering so the remaining
|
||||
// (log-only) output doesn't mutate outputLines while it's read.
|
||||
process.detach()
|
||||
process.removeListener("line", bufferLine)
|
||||
},
|
||||
})
|
||||
|
||||
try {
|
||||
// Wait for completion (or detach, which also resolves the promise)
|
||||
await process
|
||||
} finally {
|
||||
unregister?.()
|
||||
}
|
||||
if (abortSignal?.aborted) {
|
||||
throw new Error("Command execution aborted")
|
||||
}
|
||||
@@ -135,6 +246,14 @@ export async function executeForeground(
|
||||
maxChars: maxOutputChars,
|
||||
})
|
||||
|
||||
if (detachedLogFilePath !== undefined) {
|
||||
return [
|
||||
"The user chose to proceed while the command is still running in their terminal.",
|
||||
`This is partial output; further output is being redirected to this file, which you can read to check progress: ${detachedLogFilePath}`,
|
||||
output.length > 0 ? `Output so far:\n${output}` : "No output so far.",
|
||||
].join("\n")
|
||||
}
|
||||
|
||||
const completionDetails = process.getCompletionDetails?.()
|
||||
|
||||
// A terminal closed mid-command has no exit code and no reliable output —
|
||||
@@ -171,26 +290,56 @@ export async function executeForeground(
|
||||
// Tool factory
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* The shell selected by the user's terminal profile setting at one moment:
|
||||
* the profile ID for foreground terminal creation and the shell executable
|
||||
* it resolves to for background spawning and description building.
|
||||
*/
|
||||
interface ShellSnapshot {
|
||||
profileId: string
|
||||
shell: string
|
||||
}
|
||||
|
||||
/** Resolves the shell the user's terminal profile setting selects right now. */
|
||||
function takeShellSnapshot(): ShellSnapshot {
|
||||
// The setting is typed string, but guard empty values the same way the
|
||||
// settings handlers do (they skip persisting "" but older stores may hold one).
|
||||
const profileId = StateManager.get().getGlobalSettingsKey("defaultTerminalProfile") || "default"
|
||||
return { profileId, shell: getShellForProfile(profileId) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the custom `run_commands` tool for the VSCode extension.
|
||||
*
|
||||
* This tool suppresses and replaces the SDK's built-in `run_commands` tool.
|
||||
* The terminal execution mode is captured when the session's tool set is built.
|
||||
* Switching modes rebuilds the active SDK session so the tool timeout and
|
||||
* execution mode stay aligned.
|
||||
* The terminal execution mode is captured when the session's tool set is
|
||||
* built; switching modes rebuilds the active SDK session so the tool timeout
|
||||
* and execution path follow it.
|
||||
*
|
||||
* The shell is snapshotted each time the runtime reads the tool description,
|
||||
* which happens when a model request is built. Tool calls produced by that
|
||||
* request execute with the same snapshot, so changing the terminal profile
|
||||
* while the model is generating does not change the shell under commands the
|
||||
* model has already planned: the new shell is named in the next request (the
|
||||
* one carrying these tool results) and used by the commands it produces.
|
||||
*/
|
||||
export function createVscodeRunCommandsTool(options: VscodeRunCommandsToolOptions): AgentTool {
|
||||
return createShellTool(createVscodeShellExecutor(options), {
|
||||
const state = { snapshot: takeShellSnapshot() }
|
||||
return createShellTool(createVscodeShellExecutor(options, state), {
|
||||
cwd: options.cwd,
|
||||
bashTimeoutMs: options.bashTimeoutMs,
|
||||
shell: () => {
|
||||
state.snapshot = takeShellSnapshot()
|
||||
return state.snapshot.shell
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function createVscodeShellExecutor(options: VscodeRunCommandsToolOptions): ShellExecutor {
|
||||
function createVscodeShellExecutor(options: VscodeRunCommandsToolOptions, state: { snapshot: ShellSnapshot }): ShellExecutor {
|
||||
const { cwd, getTerminalManager } = options
|
||||
const executionMode = options.vscodeTerminalExecutionMode ?? "backgroundExec"
|
||||
|
||||
// Lazy-init background executor — recreated when the user's shell profile changes.
|
||||
// Lazy-init background executor — recreated when the snapshotted shell changes.
|
||||
let bgExecutor: ShellExecutor | undefined
|
||||
let bgExecutorShell: string | undefined
|
||||
|
||||
@@ -200,12 +349,12 @@ function createVscodeShellExecutor(options: VscodeRunCommandsToolOptions): Shell
|
||||
return async (command, commandCwd, context): Promise<string> => {
|
||||
Logger.log(`[VscodeRunCommands] Executing command in ${executionMode} mode`)
|
||||
|
||||
if (executionMode === "backgroundExec") {
|
||||
// Background path — use SDK's createShellExecutor
|
||||
// Resolve shell from the user's terminal profile setting
|
||||
const profileId = (StateManager.get().getGlobalSettingsKey("defaultTerminalProfile") as string) || "default"
|
||||
const shell = getShellForProfile(profileId)
|
||||
// Execute with the shell named in the model request that produced this
|
||||
// tool call, not the setting's current value (see createVscodeRunCommandsTool).
|
||||
const { profileId, shell } = state.snapshot
|
||||
|
||||
if (executionMode === "backgroundExec") {
|
||||
// Background path — use SDK's createShellExecutor.
|
||||
// Recreate the executor if the shell has changed
|
||||
if (!bgExecutor || bgExecutorShell !== shell) {
|
||||
bgExecutorShell = shell
|
||||
@@ -240,6 +389,14 @@ function createVscodeShellExecutor(options: VscodeRunCommandsToolOptions): Shell
|
||||
if (!terminalManager) {
|
||||
terminalManager = getTerminalManager()
|
||||
}
|
||||
return await executeForeground(command, commandCwd || cwd, terminalManager, MAX_COMMAND_OUTPUT_CHARS, context.signal)
|
||||
return await executeForeground(
|
||||
command,
|
||||
commandCwd || cwd,
|
||||
terminalManager,
|
||||
MAX_COMMAND_OUTPUT_CHARS,
|
||||
context.signal,
|
||||
options.foregroundCommands,
|
||||
profileId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { type AgentTool, type AgentToolContext, createTool } from "@cline/shared
|
||||
import type { VscodeTerminalManager } from "@/hosts/vscode/terminal/VscodeTerminalManager"
|
||||
import type { McpHub } from "@/services/mcp/McpHub"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import type { SdkForegroundCommandCoordinator } from "./sdk-foreground-command-coordinator"
|
||||
import { createVscodeRunCommandsTool, VSCODE_FOREGROUND_RUN_COMMANDS_TIMEOUT_MS } from "./vscode-run-commands-tool"
|
||||
|
||||
interface McpToolDescriptor {
|
||||
@@ -124,6 +125,8 @@ export interface VscodeExtraToolsOptions {
|
||||
getTerminalManager?: () => VscodeTerminalManager
|
||||
/** Current VS Code terminal execution mode, captured when the session tools are built. */
|
||||
vscodeTerminalExecutionMode?: "vscodeTerminal" | "backgroundExec"
|
||||
/** Registry of in-flight foreground executions for "Proceed While Running". */
|
||||
foregroundCommands?: SdkForegroundCommandCoordinator
|
||||
}
|
||||
|
||||
export async function createVscodeExtraTools(mcpHub: McpHub, options?: VscodeExtraToolsOptions): Promise<AgentTool[]> {
|
||||
@@ -159,6 +162,7 @@ export async function createVscodeExtraTools(mcpHub: McpHub, options?: VscodeExt
|
||||
getTerminalManager: options.getTerminalManager,
|
||||
bashTimeoutMs: executionMode === "vscodeTerminal" ? VSCODE_FOREGROUND_RUN_COMMANDS_TIMEOUT_MS : undefined,
|
||||
vscodeTerminalExecutionMode: executionMode,
|
||||
foregroundCommands: options.foregroundCommands,
|
||||
}),
|
||||
)
|
||||
Logger.log(
|
||||
|
||||
@@ -36,9 +36,10 @@ import type { VscodeTerminalManager } from "@/hosts/vscode/terminal/VscodeTermin
|
||||
import { getDistinctId } from "@/services/logging/distinctId"
|
||||
import type { McpHub } from "@/services/mcp/McpHub"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import type { SdkForegroundCommandCoordinator } from "./sdk-foreground-command-coordinator"
|
||||
import type { SdkSessionHost } from "./session-host"
|
||||
import { getEffectiveTerminalExecutionMode } from "./vscode-terminal-execution-mode"
|
||||
import { createVscodeExtraTools } from "./vscode-runtime-builder"
|
||||
import { getEffectiveTerminalExecutionMode } from "./vscode-terminal-execution-mode"
|
||||
|
||||
export interface VscodeSessionHostOptions {
|
||||
mcpHub: McpHub
|
||||
@@ -75,6 +76,8 @@ export interface VscodeSessionHostOptions {
|
||||
* with a custom tool that supports foreground/background terminal execution.
|
||||
*/
|
||||
getTerminalManager?: () => VscodeTerminalManager
|
||||
/** Registry of in-flight foreground executions for "Proceed While Running". */
|
||||
foregroundCommands?: SdkForegroundCommandCoordinator
|
||||
}
|
||||
|
||||
export class VscodeSessionHost implements SdkSessionHost {
|
||||
@@ -133,6 +136,7 @@ export class VscodeSessionHost implements SdkSessionHost {
|
||||
cwd: inputWithRemoteConfig.config.cwd,
|
||||
getTerminalManager: options.getTerminalManager,
|
||||
vscodeTerminalExecutionMode: getEffectiveTerminalExecutionMode(requestedTerminalExecutionMode),
|
||||
foregroundCommands: options.foregroundCommands,
|
||||
})
|
||||
return {
|
||||
...inputWithRemoteConfig,
|
||||
|
||||
@@ -124,6 +124,7 @@ export class WebviewGrpcBridge {
|
||||
stateManager,
|
||||
mcpHub: undefined,
|
||||
backgroundCommandRunning: false,
|
||||
foregroundCommandRunning: false,
|
||||
backgroundCommandTaskId: undefined,
|
||||
})
|
||||
await sendStateUpdate(state)
|
||||
|
||||
@@ -93,6 +93,11 @@ export interface ExtensionState {
|
||||
vscodeTerminalExecutionMode: string
|
||||
backgroundCommandRunning?: boolean
|
||||
backgroundCommandTaskId?: string
|
||||
/**
|
||||
* True while a foreground (VS Code terminal) command is awaited by a
|
||||
* run_commands tool call. Drives the "Proceed While Running" button.
|
||||
*/
|
||||
foregroundCommandRunning?: boolean
|
||||
lastCompletedCommandTs?: number
|
||||
userInfo?: UserInfo
|
||||
version: string
|
||||
|
||||
@@ -61,8 +61,10 @@
|
||||
* JetBrains exports trusted certificates from the OS and writes them to a
|
||||
* temporary file, then configures node TLS by setting NODE_EXTRA_CA_CERTS.
|
||||
*
|
||||
* CLI users should set the NODE_EXTRA_CA_CERTS environment variable if
|
||||
* necessary, because node does not automatically use the OS' trusted certs.
|
||||
* The CLI's npm wrapper (bin/cline) does the same automatically: it harvests
|
||||
* the OS trust store and points the child's NODE_EXTRA_CA_CERTS at a managed
|
||||
* bundle, because the Bun runtime does not read the OS store on its own. A
|
||||
* user-set NODE_EXTRA_CA_CERTS is merged in rather than replaced.
|
||||
*
|
||||
* ## Limitations in JetBrains & CLI
|
||||
*
|
||||
|
||||
@@ -101,6 +101,10 @@ export function createShellExecutor() {
|
||||
return async () => ""
|
||||
}
|
||||
|
||||
// The real createShellTool, so tests exercise the actual description
|
||||
// building and shell classification (getShellKind) rather than a stub that
|
||||
// would have to duplicate those invariants.
|
||||
export { createShellTool } from "../../../../sdk/packages/core/src/extensions/tools/definitions"
|
||||
// Real (dependency-light) edit-executor implementations, re-exported from the sdk source so
|
||||
// the diff-edit coordinator and its tests exercise the actual content/parse semantics. These
|
||||
// modules only pull in node:fs/node:path and the patch parser — not the heavy core runtime.
|
||||
@@ -114,13 +118,6 @@ export { createEditorExecutor } from "../../../../sdk/packages/core/src/extensio
|
||||
export type { EditFileInput } from "../../../../sdk/packages/core/src/extensions/tools/schemas"
|
||||
export type { ApplyPatchExecutor, EditorExecutor } from "../../../../sdk/packages/core/src/extensions/tools/types"
|
||||
|
||||
export function createShellTool(execute: unknown) {
|
||||
return {
|
||||
name: "run_commands",
|
||||
execute,
|
||||
}
|
||||
}
|
||||
|
||||
export interface SessionHistoryRecord {
|
||||
id: string
|
||||
metadata?: Record<string, unknown>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { afterEach, beforeEach, describe, it, mock } from "bun:test"
|
||||
import { expect } from "chai"
|
||||
import * as actualFs from "fs"
|
||||
import * as actualOs from "os"
|
||||
import * as vscode from "vscode"
|
||||
|
||||
@@ -15,6 +16,16 @@ const osMock = () => ({ ...osMockNamespace, default: osMockNamespace })
|
||||
mock.module("os", osMock)
|
||||
mock.module("node:os", osMock)
|
||||
|
||||
// getShell() probes the filesystem for PowerShell 7 when no Windows terminal
|
||||
// profile is configured. Route existsSync through a mutable delegate so tests
|
||||
// control which PowerShell installs "exist" regardless of the host machine.
|
||||
let existsSyncImpl: typeof actualFs.existsSync = actualFs.existsSync
|
||||
const existsSyncDelegate = ((path: unknown) => existsSyncImpl(path as string)) as typeof actualFs.existsSync
|
||||
const fsMockNamespace = { ...actualFs, existsSync: existsSyncDelegate }
|
||||
const fsMock = () => ({ ...fsMockNamespace, default: fsMockNamespace })
|
||||
mock.module("fs", fsMock)
|
||||
mock.module("node:fs", fsMock)
|
||||
|
||||
import { getShell } from "@utils/shell"
|
||||
|
||||
describe("Shell Detection Tests", () => {
|
||||
@@ -22,6 +33,7 @@ describe("Shell Detection Tests", () => {
|
||||
let originalEnv: NodeJS.ProcessEnv
|
||||
let originalGetConfig: typeof vscode.workspace.getConfiguration
|
||||
let originalUserInfo: typeof actualOs.userInfo
|
||||
let originalExistsSync: typeof actualFs.existsSync
|
||||
|
||||
// Helper to mock VS Code configuration
|
||||
function mockVsCodeConfig(platformKey: string, defaultProfileName: string | null, profiles: Record<string, any>) {
|
||||
@@ -45,6 +57,7 @@ describe("Shell Detection Tests", () => {
|
||||
originalEnv = { ...process.env }
|
||||
originalGetConfig = vscode.workspace.getConfiguration
|
||||
originalUserInfo = userInfoImpl
|
||||
originalExistsSync = existsSyncImpl
|
||||
|
||||
// Clear environment variables for a clean test
|
||||
delete process.env.SHELL
|
||||
@@ -52,6 +65,9 @@ describe("Shell Detection Tests", () => {
|
||||
|
||||
// Default userInfo() mock
|
||||
userInfoImpl = (() => ({ shell: null })) as any
|
||||
// Default: PowerShell 7 is not installed, so the Windows default
|
||||
// resolves to legacy Windows PowerShell.
|
||||
existsSyncImpl = (() => false) as any
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
@@ -60,6 +76,7 @@ describe("Shell Detection Tests", () => {
|
||||
process.env = originalEnv
|
||||
vscode.workspace.getConfiguration = originalGetConfig
|
||||
userInfoImpl = originalUserInfo
|
||||
existsSyncImpl = originalExistsSync
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
@@ -71,12 +88,63 @@ describe("Shell Detection Tests", () => {
|
||||
})
|
||||
|
||||
it("uses explicit PowerShell 7 path from VS Code config (profile path)", () => {
|
||||
existsSyncImpl = ((candidate: actualFs.PathLike) =>
|
||||
candidate === "C:\\Program Files\\PowerShell\\7\\pwsh.exe") as typeof actualFs.existsSync
|
||||
mockVsCodeConfig("windows", "PowerShell", {
|
||||
PowerShell: { path: "C:\\Program Files\\PowerShell\\7\\pwsh.exe" },
|
||||
})
|
||||
expect(getShell()).to.equal("C:\\Program Files\\PowerShell\\7\\pwsh.exe")
|
||||
})
|
||||
|
||||
it("expands and selects the first configured profile path when it exists", () => {
|
||||
process.env.windir = "C:\\Windows"
|
||||
existsSyncImpl = (() => true) as typeof actualFs.existsSync
|
||||
mockVsCodeConfig("windows", "Command Prompt", {
|
||||
"Command Prompt": {
|
||||
path: [`\${env:windir}\\Sysnative\\cmd.exe`, `\${env:windir}\\System32\\cmd.exe`],
|
||||
},
|
||||
})
|
||||
|
||||
expect(getShell()).to.equal("C:\\Windows\\Sysnative\\cmd.exe")
|
||||
})
|
||||
|
||||
it("falls through configured profile paths in order", () => {
|
||||
process.env.windir = "C:\\Windows"
|
||||
existsSyncImpl = ((candidate: actualFs.PathLike) =>
|
||||
candidate === "C:\\Windows\\System32\\cmd.exe") as typeof actualFs.existsSync
|
||||
mockVsCodeConfig("windows", "Command Prompt", {
|
||||
"Command Prompt": {
|
||||
path: [`\${env:windir}\\Sysnative\\cmd.exe`, `\${env:windir}\\System32\\cmd.exe`],
|
||||
},
|
||||
})
|
||||
|
||||
expect(getShell()).to.equal("C:\\Windows\\System32\\cmd.exe")
|
||||
})
|
||||
|
||||
it("skips profile paths with variable references it cannot expand", () => {
|
||||
existsSyncImpl = ((candidate: actualFs.PathLike) =>
|
||||
candidate === "C:\\Windows\\System32\\cmd.exe") as typeof actualFs.existsSync
|
||||
process.env.windir = "C:\\Windows"
|
||||
mockVsCodeConfig("windows", "Command Prompt", {
|
||||
"Command Prompt": {
|
||||
path: [`\${workspaceFolder}\\tools\\cmd.exe`, `\${env:windir}\\System32\\cmd.exe`],
|
||||
},
|
||||
})
|
||||
|
||||
expect(getShell()).to.equal("C:\\Windows\\System32\\cmd.exe")
|
||||
})
|
||||
|
||||
it("resolves a configured executable name from PATH", () => {
|
||||
process.env.PATH = "C:\\Tools;C:\\Windows\\System32"
|
||||
existsSyncImpl = ((candidate: actualFs.PathLike) =>
|
||||
candidate === "C:\\Windows\\System32\\cmd.exe") as typeof actualFs.existsSync
|
||||
mockVsCodeConfig("windows", "Command Prompt", {
|
||||
"Command Prompt": { path: "cmd.exe" },
|
||||
})
|
||||
|
||||
expect(getShell()).to.equal("C:\\Windows\\System32\\cmd.exe")
|
||||
})
|
||||
|
||||
it("uses PowerShell 7 path if source is 'PowerShell' but no explicit path", () => {
|
||||
mockVsCodeConfig("windows", "PowerShell", {
|
||||
PowerShell: { source: "PowerShell" },
|
||||
@@ -117,18 +185,36 @@ describe("Shell Detection Tests", () => {
|
||||
expect(getShell()).to.equal("C:\\Windows\\System32\\cmd.exe")
|
||||
})
|
||||
|
||||
it("respects userInfo() if no VS Code config is available", () => {
|
||||
it("defaults to PowerShell 7 when no profile is configured and pwsh is installed", () => {
|
||||
vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any
|
||||
userInfoImpl = () => ({ shell: "C:\\Custom\\PowerShell.exe" }) as any
|
||||
process.env.ProgramW6432 = "C:\\Program Files"
|
||||
existsSyncImpl = (() => true) as any
|
||||
|
||||
expect(getShell()).to.equal("C:\\Custom\\PowerShell.exe")
|
||||
expect(getShell()).to.equal("C:\\Program Files\\PowerShell\\7\\pwsh.exe")
|
||||
})
|
||||
|
||||
it("respects an odd COMSPEC if no userInfo shell is available", () => {
|
||||
it("defaults to Store-installed pwsh when that is the only pwsh present", () => {
|
||||
vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any
|
||||
process.env.LOCALAPPDATA = "C:\\Users\\Test\\AppData\\Local"
|
||||
const storePwsh = "C:\\Users\\Test\\AppData\\Local\\Microsoft\\WindowsApps\\pwsh.exe"
|
||||
existsSyncImpl = ((path: string) => path === storePwsh) as any
|
||||
|
||||
expect(getShell()).to.equal(storePwsh)
|
||||
})
|
||||
|
||||
it("defaults to legacy Windows PowerShell when no profile is configured and pwsh is absent", () => {
|
||||
vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any
|
||||
existsSyncImpl = (() => false) as any
|
||||
|
||||
expect(getShell()).to.equal("C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe")
|
||||
})
|
||||
|
||||
it("ignores userInfo() and COMSPEC — VS Code's default terminal ignores them too", () => {
|
||||
vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any
|
||||
userInfoImpl = () => ({ shell: "C:\\Custom\\OtherShell.exe" }) as any
|
||||
process.env.COMSPEC = "D:\\CustomCmd\\cmd.exe"
|
||||
|
||||
expect(getShell()).to.equal("D:\\CustomCmd\\cmd.exe")
|
||||
expect(getShell()).to.equal("C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -141,12 +227,31 @@ describe("Shell Detection Tests", () => {
|
||||
})
|
||||
|
||||
it("uses VS Code profile path if available", () => {
|
||||
existsSyncImpl = ((candidate: actualFs.PathLike) => candidate === "/usr/local/bin/fish") as typeof actualFs.existsSync
|
||||
mockVsCodeConfig("osx", "MyCustomShell", {
|
||||
MyCustomShell: { path: "/usr/local/bin/fish" },
|
||||
})
|
||||
expect(getShell()).to.equal("/usr/local/bin/fish")
|
||||
})
|
||||
|
||||
it("expands and selects the first existing path in an array-valued profile", () => {
|
||||
existsSyncImpl = ((candidate: actualFs.PathLike) => candidate === "/bin/zsh") as typeof actualFs.existsSync
|
||||
mockVsCodeConfig("osx", "MyCustomShell", {
|
||||
MyCustomShell: { path: ["/opt/homebrew/bin/zsh", "/bin/zsh"] },
|
||||
})
|
||||
expect(getShell()).to.equal("/bin/zsh")
|
||||
})
|
||||
|
||||
it("falls back past a configured path that does not exist", () => {
|
||||
existsSyncImpl = (() => false) as typeof actualFs.existsSync
|
||||
mockVsCodeConfig("osx", "MyCustomShell", {
|
||||
MyCustomShell: { path: "/missing/shell" },
|
||||
})
|
||||
userInfoImpl = () => ({ shell: "/opt/homebrew/bin/zsh" }) as any
|
||||
|
||||
expect(getShell()).to.equal("/opt/homebrew/bin/zsh")
|
||||
})
|
||||
|
||||
it("falls back to userInfo().shell if no VS Code config is available", () => {
|
||||
vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any
|
||||
userInfoImpl = () => ({ shell: "/opt/homebrew/bin/zsh" }) as any
|
||||
@@ -177,12 +282,30 @@ describe("Shell Detection Tests", () => {
|
||||
})
|
||||
|
||||
it("uses VS Code profile path if available", () => {
|
||||
existsSyncImpl = ((candidate: actualFs.PathLike) => candidate === "/usr/bin/fish") as typeof actualFs.existsSync
|
||||
mockVsCodeConfig("linux", "CustomProfile", {
|
||||
CustomProfile: { path: "/usr/bin/fish" },
|
||||
})
|
||||
expect(getShell()).to.equal("/usr/bin/fish")
|
||||
})
|
||||
|
||||
it("expands and selects the first existing path in an array-valued profile", () => {
|
||||
existsSyncImpl = ((candidate: actualFs.PathLike) => candidate === "/bin/bash") as typeof actualFs.existsSync
|
||||
mockVsCodeConfig("linux", "CustomProfile", {
|
||||
CustomProfile: { path: ["/usr/bin/fish", "/bin/bash"] },
|
||||
})
|
||||
expect(getShell()).to.equal("/bin/bash")
|
||||
})
|
||||
|
||||
it("resolves a bare executable name from PATH without PATHEXT probing", () => {
|
||||
process.env.PATH = "/opt/tools:/usr/bin"
|
||||
existsSyncImpl = ((candidate: actualFs.PathLike) => candidate === "/usr/bin/fish") as typeof actualFs.existsSync
|
||||
mockVsCodeConfig("linux", "CustomProfile", {
|
||||
CustomProfile: { path: "fish" },
|
||||
})
|
||||
expect(getShell()).to.equal("/usr/bin/fish")
|
||||
})
|
||||
|
||||
it("falls back to userInfo().shell if no VS Code config is available", () => {
|
||||
vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any
|
||||
userInfoImpl = () => ({ shell: "/usr/bin/zsh" }) as any
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as childProcess from "child_process"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { WINDOWS_POWERSHELL_7_PATH, WINDOWS_POWERSHELL_LEGACY_PATH } from "./shell"
|
||||
import { getWindowsPwshInstallPaths, WINDOWS_POWERSHELL_LEGACY_PATH } from "./shell"
|
||||
|
||||
const POWERSHELL_PROBE_TIMEOUT_MS = 1200
|
||||
|
||||
@@ -16,14 +16,7 @@ export function getFallbackWindowsPowerShellPath(): string {
|
||||
}
|
||||
|
||||
export function getWindowsPowerShellCandidates(): string[] {
|
||||
const programFiles = process.env.ProgramW6432 || process.env.ProgramFiles || "C:\\Program Files"
|
||||
|
||||
const envAbsoluteCandidates = [
|
||||
`${programFiles}\\PowerShell\\7\\pwsh.exe`,
|
||||
`${programFiles}\\PowerShell\\6\\pwsh.exe`,
|
||||
WINDOWS_POWERSHELL_7_PATH,
|
||||
WINDOWS_POWERSHELL_LEGACY_PATH,
|
||||
]
|
||||
const envAbsoluteCandidates = [...getWindowsPwshInstallPaths(), WINDOWS_POWERSHELL_LEGACY_PATH]
|
||||
|
||||
const commandNameFallbacks = ["pwsh.exe", "pwsh", "powershell.exe", "powershell"]
|
||||
|
||||
|
||||
+129
-24
@@ -1,5 +1,8 @@
|
||||
import { existsSync } from "fs"
|
||||
import { userInfo } from "os"
|
||||
import * as nodePath from "path"
|
||||
import * as vscode from "vscode"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
|
||||
export const WINDOWS_POWERSHELL_7_PATH = "C:\\Program Files\\PowerShell\\7\\pwsh.exe"
|
||||
export const WINDOWS_POWERSHELL_LEGACY_PATH = "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"
|
||||
@@ -29,21 +32,24 @@ const SHELL_PATHS = {
|
||||
FALLBACK: "/bin/sh",
|
||||
} as const
|
||||
|
||||
// VS Code permits `path: string | string[]` in terminal profiles on every
|
||||
// platform (the stock Windows "Command Prompt" profile is an array), so all
|
||||
// three profile shapes model both forms.
|
||||
interface MacTerminalProfile {
|
||||
path?: string
|
||||
path?: string | string[]
|
||||
}
|
||||
|
||||
type MacTerminalProfiles = Record<string, MacTerminalProfile>
|
||||
|
||||
interface WindowsTerminalProfile {
|
||||
path?: string
|
||||
path?: string | string[]
|
||||
source?: "PowerShell" | "WSL"
|
||||
}
|
||||
|
||||
type WindowsTerminalProfiles = Record<string, WindowsTerminalProfile>
|
||||
|
||||
interface LinuxTerminalProfile {
|
||||
path?: string
|
||||
path?: string | string[]
|
||||
}
|
||||
|
||||
type LinuxTerminalProfiles = Record<string, LinuxTerminalProfile>
|
||||
@@ -89,6 +95,83 @@ function getLinuxTerminalConfig() {
|
||||
// 2) Platform-Specific VS Code Shell Retrieval
|
||||
// -----------------------------------------------------
|
||||
|
||||
function isWindows(): boolean {
|
||||
return process.platform === "win32"
|
||||
}
|
||||
|
||||
/** The path module matching the host platform's separators and semantics. */
|
||||
function hostPath(): nodePath.PlatformPath {
|
||||
return isWindows() ? nodePath.win32 : nodePath.posix
|
||||
}
|
||||
|
||||
function getEnvironmentVariable(name: string): string | undefined {
|
||||
// Windows environment variable names are case-insensitive; POSIX names
|
||||
// are case-sensitive.
|
||||
if (!isWindows()) {
|
||||
return process.env[name]
|
||||
}
|
||||
const entry = Object.entries(process.env).find(([key]) => key.toLowerCase() === name.toLowerCase())
|
||||
return entry?.[1]
|
||||
}
|
||||
|
||||
/** Expands VS Code's `${env:NAME}` references in a profile path. */
|
||||
function expandShellPath(candidate: string): string {
|
||||
return candidate.replace(/\$\{env:([^}]+)\}/gi, (reference, name: string) => {
|
||||
return getEnvironmentVariable(name.trim()) ?? reference
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a profile path candidate to an existing executable: absolute or
|
||||
* relative paths are checked directly; bare names are searched on PATH
|
||||
* (with PATHEXT on Windows, mirroring how VS Code launches profiles).
|
||||
*/
|
||||
function findExecutable(candidate: string): string | null {
|
||||
const path = hostPath()
|
||||
if (path.basename(candidate) !== candidate) {
|
||||
const normalized = path.normalize(candidate)
|
||||
return existsSync(normalized) ? normalized : null
|
||||
}
|
||||
|
||||
const pathValue = getEnvironmentVariable("PATH")
|
||||
if (!pathValue) {
|
||||
return null
|
||||
}
|
||||
|
||||
const extensions =
|
||||
!isWindows() || path.extname(candidate) ? [""] : (getEnvironmentVariable("PATHEXT") ?? ".COM;.EXE;.BAT;.CMD").split(";")
|
||||
for (const directory of pathValue.split(path.delimiter)) {
|
||||
for (const extension of extensions) {
|
||||
const executable = path.join(directory, `${candidate}${extension}`)
|
||||
if (existsSync(executable)) {
|
||||
return executable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/** Resolves the first usable path in a VS Code terminal profile. */
|
||||
function resolveShellPath(configuredPath: string | string[] | undefined): string | null {
|
||||
const candidates = typeof configuredPath === "string" ? [configuredPath] : configuredPath
|
||||
for (const candidate of candidates ?? []) {
|
||||
const expandedPath = expandShellPath(candidate)
|
||||
if (expandedPath.includes("${")) {
|
||||
// Only ${env:NAME} references are expanded here. VS Code resolves
|
||||
// more variable kinds (e.g. ${workspaceFolder}); surface the gap
|
||||
// instead of silently skipping the user's configured shell.
|
||||
Logger.warn(`[shell] Skipping terminal profile path with unresolved variable reference: ${candidate}`)
|
||||
continue
|
||||
}
|
||||
const executable = findExecutable(expandedPath)
|
||||
if (executable) {
|
||||
return executable
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/** Attempts to retrieve a shell path from VS Code config on Windows. */
|
||||
function getWindowsShellFromVSCode(): string | null {
|
||||
const { defaultProfileName, profiles } = getWindowsTerminalConfig()
|
||||
@@ -97,14 +180,15 @@ function getWindowsShellFromVSCode(): string | null {
|
||||
}
|
||||
|
||||
const profile = profiles[defaultProfileName]
|
||||
const configuredShell = resolveShellPath(profile?.path)
|
||||
|
||||
// If the profile name indicates PowerShell, do version-based detection.
|
||||
// In testing it was found these typically do not have a path, and this
|
||||
// implementation manages to deductively get the correct version of PowerShell
|
||||
if (defaultProfileName.toLowerCase().includes("powershell")) {
|
||||
if (profile?.path) {
|
||||
if (configuredShell) {
|
||||
// If there's an explicit PowerShell path, return that
|
||||
return profile.path
|
||||
return configuredShell
|
||||
}
|
||||
if (profile?.source === "PowerShell") {
|
||||
// If the profile is sourced from PowerShell, assume the newest
|
||||
@@ -115,8 +199,8 @@ function getWindowsShellFromVSCode(): string | null {
|
||||
}
|
||||
|
||||
// If there's a specific path, return that immediately
|
||||
if (profile?.path) {
|
||||
return profile.path
|
||||
if (configuredShell) {
|
||||
return configuredShell
|
||||
}
|
||||
|
||||
// If the profile indicates WSL
|
||||
@@ -135,8 +219,7 @@ function getMacShellFromVSCode(): string | null {
|
||||
return null
|
||||
}
|
||||
|
||||
const profile = profiles[defaultProfileName]
|
||||
return profile?.path || null
|
||||
return resolveShellPath(profiles[defaultProfileName]?.path)
|
||||
}
|
||||
|
||||
/** Attempts to retrieve a shell path from VS Code config on Linux. */
|
||||
@@ -146,8 +229,7 @@ function getLinuxShellFromVSCode(): string | null {
|
||||
return null
|
||||
}
|
||||
|
||||
const profile = profiles[defaultProfileName]
|
||||
return profile?.path || null
|
||||
return resolveShellPath(profiles[defaultProfileName]?.path)
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
@@ -171,11 +253,6 @@ function getShellFromUserInfo(): string | null {
|
||||
function getShellFromEnv(): string | null {
|
||||
const { env } = process
|
||||
|
||||
if (process.platform === "win32") {
|
||||
// On Windows, COMSPEC typically holds cmd.exe
|
||||
return env.COMSPEC || "C:\\Windows\\System32\\cmd.exe"
|
||||
}
|
||||
|
||||
if (process.platform === "darwin") {
|
||||
// On macOS/Linux, SHELL is commonly the environment variable
|
||||
return env.SHELL || "/bin/zsh"
|
||||
@@ -304,6 +381,35 @@ export function getShellForProfile(profileId: string): string {
|
||||
// 5) Publicly Exposed Shell Getter
|
||||
// -----------------------------------------------------
|
||||
|
||||
/**
|
||||
* Absolute paths where a modern PowerShell (pwsh) may be installed, most
|
||||
* preferred first: MSI/ZIP installs under Program Files (either architecture),
|
||||
* then the Microsoft Store install under LOCALAPPDATA. This is the single
|
||||
* candidate list shared with the async prober in utils/powershell.ts.
|
||||
*/
|
||||
export function getWindowsPwshInstallPaths(): string[] {
|
||||
const programFiles = process.env.ProgramW6432 || process.env.ProgramFiles || "C:\\Program Files"
|
||||
const localAppData = process.env.LOCALAPPDATA
|
||||
return [
|
||||
`${programFiles}\\PowerShell\\7\\pwsh.exe`,
|
||||
`${programFiles}\\PowerShell\\6\\pwsh.exe`,
|
||||
SHELL_PATHS.POWERSHELL_7,
|
||||
...(localAppData ? [`${localAppData}\\Microsoft\\WindowsApps\\pwsh.exe`] : []),
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* The shell VS Code launches on Windows when the user has not configured a
|
||||
* default terminal profile: its built-in default is PowerShell (pwsh when
|
||||
* installed, Windows PowerShell otherwise) — never cmd.exe. Mirroring that
|
||||
* here keeps the "default" profile meaning the same shell whether commands
|
||||
* run in a visible VS Code terminal or a background child process.
|
||||
*/
|
||||
function getWindowsDefaultShell(): string {
|
||||
const pwsh = getWindowsPwshInstallPaths().find((candidate) => existsSync(candidate))
|
||||
return pwsh ?? SHELL_PATHS.POWERSHELL_LEGACY
|
||||
}
|
||||
|
||||
export function getShell(): string {
|
||||
// 1. Check VS Code config first.
|
||||
if (process.platform === "win32") {
|
||||
@@ -312,7 +418,12 @@ export function getShell(): string {
|
||||
if (windowsShell) {
|
||||
return windowsShell
|
||||
}
|
||||
} else if (process.platform === "darwin") {
|
||||
// No profile configured — match the shell VS Code's default terminal
|
||||
// would launch. userInfo()/COMSPEC are not consulted: VS Code's own
|
||||
// terminal ignores them too, and they would resolve to cmd.exe.
|
||||
return getWindowsDefaultShell()
|
||||
}
|
||||
if (process.platform === "darwin") {
|
||||
// macOS from VS Code
|
||||
const macShell = getMacShellFromVSCode()
|
||||
if (macShell) {
|
||||
@@ -338,12 +449,6 @@ export function getShell(): string {
|
||||
return envShell
|
||||
}
|
||||
|
||||
// 4. Finally, fall back to a default
|
||||
if (process.platform === "win32") {
|
||||
// On Windows, if we got here, we have no config, no COMSPEC, and one very messed up operating system.
|
||||
// Use CMD as a last resort
|
||||
return SHELL_PATHS.CMD
|
||||
}
|
||||
// On macOS/Linux, fallback to a POSIX shell - This is the behavior of our old shell detection method.
|
||||
// 4. Fall back to a POSIX shell - This is the behavior of our old shell detection method.
|
||||
return SHELL_PATHS.FALLBACK
|
||||
}
|
||||
|
||||
+3
-3
@@ -20,7 +20,7 @@ interface ActionButtonsProps {
|
||||
*/
|
||||
export const ActionButtons: React.FC<ActionButtonsProps> = ({ task, messages, chatState, mode, messageHandlers }) => {
|
||||
const { inputValue, selectedImages, selectedFiles, setSendingDisabled } = chatState
|
||||
const { turnState } = useExtensionState()
|
||||
const { turnState, foregroundCommandRunning } = useExtensionState()
|
||||
|
||||
// Tracks the ask the user last acted on. Clicking a footer button latches this so the
|
||||
// buttons disable immediately (and survive the trailing bookkeeping re-renders before the
|
||||
@@ -41,8 +41,8 @@ export const ActionButtons: React.FC<ActionButtonsProps> = ({ task, messages, ch
|
||||
// buttons immune to trailing bookkeeping messages and never disagree with the thinking
|
||||
// indicator (RC1).
|
||||
const buttonConfig = useMemo(() => {
|
||||
return getButtonConfigFromState(messages, turnState, mode)
|
||||
}, [messages, turnState, mode])
|
||||
return getButtonConfigFromState(messages, turnState, mode, foregroundCommandRunning)
|
||||
}, [messages, turnState, mode, foregroundCommandRunning])
|
||||
|
||||
// Identity of the ask that currently owns the footer buttons. The button config objects are
|
||||
// shared singletons (e.g. BUTTON_CONFIGS.tool_approve), so two consecutive identical asks
|
||||
|
||||
@@ -367,6 +367,15 @@ export function useMessageHandlers(messages: ClineMessage[], chatState: ChatStat
|
||||
clearInputState()
|
||||
break
|
||||
|
||||
case "proceed_while_running":
|
||||
// Detach the running foreground terminal command: the agent
|
||||
// receives the partial output plus a log file path for the
|
||||
// rest, and the command keeps running in the terminal.
|
||||
await TaskServiceClient.proceedWhileRunningCommand(EmptyRequest.create({})).catch((err) =>
|
||||
console.error("Failed to proceed while running:", err),
|
||||
)
|
||||
break
|
||||
|
||||
case "new_task":
|
||||
if (clineAsk === "new_task") {
|
||||
await TaskServiceClient.newTask(
|
||||
|
||||
@@ -252,4 +252,19 @@ describe("getButtonConfigFromState (dispatch + legacy fallback)", () => {
|
||||
const turnState: TurnState = { phase: "completed", seq: 3 }
|
||||
expect(getButtonConfigFromState(messages, turnState, "act")).toEqual(BUTTON_CONFIGS.completion_result)
|
||||
})
|
||||
|
||||
it("streaming phase shows Proceed While Running when a foreground command is running", () => {
|
||||
const messages: ClineMessage[] = [{ ts: 1, type: "say", say: "command", text: "npm run dev", partial: true }]
|
||||
const turnState: TurnState = { phase: "streaming", seq: 4 }
|
||||
expect(getButtonConfigFromState(messages, turnState, "act", true)).toEqual(BUTTON_CONFIGS.foreground_command_running)
|
||||
expect(getButtonConfigFromState(messages, turnState, "act", false)).toEqual(BUTTON_CONFIGS.partial)
|
||||
})
|
||||
|
||||
it("foreground command flag only affects the streaming phase", () => {
|
||||
const messages: ClineMessage[] = []
|
||||
expect(getButtonConfigFromState(messages, { phase: "completed", seq: 5 }, "act", true)).toEqual(
|
||||
BUTTON_CONFIGS.completion_result,
|
||||
)
|
||||
expect(getButtonConfigFromState(messages, { phase: "idle", seq: 6 }, "act", true)).toEqual(BUTTON_CONFIGS.default)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -8,6 +8,7 @@ export type ButtonActionType =
|
||||
| "approve" // Send yesButtonClicked
|
||||
| "reject" // Send noButtonClicked
|
||||
| "proceed" // Send messageResponse or yesButtonClicked
|
||||
| "proceed_while_running" // Detach the running foreground terminal command
|
||||
| "new_task" // Start a new task
|
||||
| "cancel" // Cancel streaming
|
||||
| "utility" // Execute utility function (condense, report_bug)
|
||||
@@ -188,6 +189,17 @@ export const BUTTON_CONFIGS: Record<string, ButtonConfig> = {
|
||||
secondaryAction: "cancel",
|
||||
},
|
||||
|
||||
// A foreground terminal command is running (SDK path): the user can detach
|
||||
// it and let the agent proceed with the partial output, or cancel the task.
|
||||
foreground_command_running: {
|
||||
sendingDisabled: true,
|
||||
enableButtons: true,
|
||||
primaryText: "Proceed While Running",
|
||||
secondaryText: "Cancel",
|
||||
primaryAction: "proceed_while_running",
|
||||
secondaryAction: "cancel",
|
||||
},
|
||||
|
||||
// Default states
|
||||
default: {
|
||||
sendingDisabled: false,
|
||||
@@ -360,12 +372,19 @@ export function getButtonConfigForMessages(messages: ClineMessage[], mode: Mode
|
||||
* The button SET is chosen by phase; the LABEL/variant for approvals (Save vs Approve, command
|
||||
* vs tool vs MCP vs subagents) comes from the anchored message (turnState.anchorTs).
|
||||
*/
|
||||
export function buttonsForPhase(turnState: TurnState, anchoredMessage: ClineMessage | undefined): ButtonConfig {
|
||||
export function buttonsForPhase(
|
||||
turnState: TurnState,
|
||||
anchoredMessage: ClineMessage | undefined,
|
||||
foregroundCommandRunning = false,
|
||||
): ButtonConfig {
|
||||
switch (turnState.phase) {
|
||||
case "idle":
|
||||
return BUTTON_CONFIGS.default
|
||||
case "streaming":
|
||||
return BUTTON_CONFIGS.partial
|
||||
// A running foreground terminal command offers "Proceed While Running":
|
||||
// detach the command (output continues to a log file) and let the
|
||||
// agent continue with the partial output.
|
||||
return foregroundCommandRunning ? BUTTON_CONFIGS.foreground_command_running : BUTTON_CONFIGS.partial
|
||||
case "completed":
|
||||
return BUTTON_CONFIGS.completion_result
|
||||
case "resumable":
|
||||
@@ -398,10 +417,11 @@ export function getButtonConfigFromState(
|
||||
messages: ClineMessage[],
|
||||
turnState: TurnState | undefined,
|
||||
mode: Mode = "act",
|
||||
foregroundCommandRunning = false,
|
||||
): ButtonConfig {
|
||||
if (turnState) {
|
||||
const anchored = turnState.anchorTs !== undefined ? messages.find((m) => m.ts === turnState.anchorTs) : undefined
|
||||
return buttonsForPhase(turnState, anchored)
|
||||
return buttonsForPhase(turnState, anchored, foregroundCommandRunning)
|
||||
}
|
||||
return getButtonConfigForMessages(messages, mode)
|
||||
}
|
||||
|
||||
@@ -510,7 +510,7 @@ const ApiOptions = ({
|
||||
<AIhubmixProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
|
||||
)}
|
||||
|
||||
{apiConfiguration && (selectedProvider.includes("openai") || isCustomProvider) && (
|
||||
{apiConfiguration && (selectedProvider === "openai" || isCustomProvider) && (
|
||||
<OpenAICompatibleProvider
|
||||
currentMode={currentMode}
|
||||
isPopup={isPopup}
|
||||
|
||||
@@ -97,6 +97,25 @@ describe("ApiOptions Component", () => {
|
||||
expect(modelIdInput).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it.each([
|
||||
["openai-native", "OpenAI API Key"],
|
||||
["openai-codex", "Sign in to OpenAI Codex"],
|
||||
])("renders only the dedicated form for %s", (provider, dedicatedFormText) => {
|
||||
mockExtensionState({
|
||||
planModeApiProvider: provider as any,
|
||||
actModeApiProvider: provider as any,
|
||||
})
|
||||
|
||||
render(
|
||||
<ExtensionStateContextProvider>
|
||||
<ApiOptions currentMode="plan" showModelOptions={false} />
|
||||
</ExtensionStateContextProvider>,
|
||||
)
|
||||
|
||||
expect(screen.getByText(dedicatedFormText)).toBeInTheDocument()
|
||||
expect(screen.queryByText("Custom Headers")).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders the OpenAI-compatible form for custom/unknown catalog providers", () => {
|
||||
vi.mocked(useProviderListings).mockReturnValue({
|
||||
providers: [
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import type { AuthState, UserOrganizationsResponse } from "@shared/proto/cline/account"
|
||||
import { act, render, screen } from "@testing-library/react"
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||
import { ClineAuthProvider, useClineAuth } from "./ClineAuthContext"
|
||||
|
||||
type AuthStatusCallbacks = {
|
||||
onResponse: (response: AuthState) => void
|
||||
}
|
||||
|
||||
const grpcMocks = vi.hoisted(() => ({
|
||||
getUserOrganizations: vi.fn(),
|
||||
subscribeToAuthStatusUpdate: vi.fn(),
|
||||
authStatusCallbacks: undefined as AuthStatusCallbacks | undefined,
|
||||
}))
|
||||
|
||||
vi.mock("@/services/grpc-client", () => ({
|
||||
AccountServiceClient: {
|
||||
getUserOrganizations: grpcMocks.getUserOrganizations,
|
||||
subscribeToAuthStatusUpdate: grpcMocks.subscribeToAuthStatusUpdate,
|
||||
},
|
||||
}))
|
||||
|
||||
function createDeferred<T>() {
|
||||
let resolve: (value: T) => void = () => {}
|
||||
const promise = new Promise<T>((promiseResolve) => {
|
||||
resolve = promiseResolve
|
||||
})
|
||||
return { promise, resolve }
|
||||
}
|
||||
|
||||
function AuthStateProbe() {
|
||||
const { clineUser, organizations } = useClineAuth()
|
||||
return (
|
||||
<>
|
||||
<div data-testid="user-state">{clineUser?.uid ?? "signed-out"}</div>
|
||||
<div data-testid="organizations-state">
|
||||
{organizations?.map((organization) => organization.organizationId).join(",") ?? "none"}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
describe("ClineAuthProvider", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
grpcMocks.authStatusCallbacks = undefined
|
||||
grpcMocks.subscribeToAuthStatusUpdate.mockImplementation((_request, callbacks: AuthStatusCallbacks) => {
|
||||
grpcMocks.authStatusCallbacks = callbacks
|
||||
return vi.fn()
|
||||
})
|
||||
})
|
||||
|
||||
it("does not restore organizations when an in-flight request resolves after sign-out", async () => {
|
||||
const organizationsRequest = createDeferred<UserOrganizationsResponse>()
|
||||
grpcMocks.getUserOrganizations.mockReturnValue(organizationsRequest.promise)
|
||||
|
||||
render(
|
||||
<ClineAuthProvider>
|
||||
<AuthStateProbe />
|
||||
</ClineAuthProvider>,
|
||||
)
|
||||
|
||||
act(() => {
|
||||
grpcMocks.authStatusCallbacks?.onResponse({ user: { uid: "user-1" } })
|
||||
})
|
||||
expect(grpcMocks.getUserOrganizations).toHaveBeenCalledTimes(1)
|
||||
|
||||
act(() => {
|
||||
grpcMocks.authStatusCallbacks?.onResponse({})
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
organizationsRequest.resolve({
|
||||
organizations: [
|
||||
{ organizationId: "stale-org", active: true, memberId: "member-1", name: "Stale Org", roles: [] },
|
||||
],
|
||||
})
|
||||
await organizationsRequest.promise
|
||||
})
|
||||
|
||||
expect(screen.getByTestId("user-state")).toHaveTextContent("signed-out")
|
||||
expect(screen.getByTestId("organizations-state")).toHaveTextContent("none")
|
||||
})
|
||||
})
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { UserOrganization } from "@shared/proto/cline/account"
|
||||
import type { AuthState, UserOrganization } from "@shared/proto/cline/account"
|
||||
import { EmptyRequest } from "@shared/proto/cline/common"
|
||||
import deepEqual from "fast-deep-equal"
|
||||
import type React from "react"
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react"
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { AccountServiceClient } from "@/services/grpc-client"
|
||||
|
||||
// Define User type (you may need to adjust this based on your actual User type)
|
||||
@@ -25,10 +25,15 @@ export const ClineAuthContext = createContext<ClineAuthContextType | undefined>(
|
||||
export const ClineAuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [user, setUser] = useState<ClineUser | null>(null)
|
||||
const [userOrganizations, setUserOrganizations] = useState<UserOrganization[] | null>(null)
|
||||
const organizationsRequestIdRef = useRef(0)
|
||||
|
||||
const getUserOrganizations = useCallback(async () => {
|
||||
const requestId = ++organizationsRequestIdRef.current
|
||||
try {
|
||||
const response = await AccountServiceClient.getUserOrganizations(EmptyRequest.create())
|
||||
if (requestId !== organizationsRequestIdRef.current) {
|
||||
return
|
||||
}
|
||||
setUserOrganizations((old) => {
|
||||
if (!deepEqual(response.organizations, old)) {
|
||||
return response.organizations
|
||||
@@ -52,22 +57,23 @@ export const ClineAuthProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
||||
// Handle auth status update events
|
||||
useEffect(() => {
|
||||
const cancelSubscription = AccountServiceClient.subscribeToAuthStatusUpdate(EmptyRequest.create(), {
|
||||
onResponse: async (response: any) => {
|
||||
setUser((oldUser) => {
|
||||
if (!response?.user?.uid) {
|
||||
return null
|
||||
}
|
||||
onResponse: (response: AuthState) => {
|
||||
const responseUser = response.user
|
||||
if (!responseUser?.uid) {
|
||||
organizationsRequestIdRef.current++
|
||||
setUser(null)
|
||||
setUserOrganizations(null)
|
||||
return
|
||||
}
|
||||
|
||||
if (response?.user && oldUser?.uid !== response.user.uid) {
|
||||
// Once we have a new user, fetch organizations that
|
||||
// allow us to display the active account in account view UI
|
||||
// and fetch the correct credit balance to display on mount
|
||||
getUserOrganizations()
|
||||
return response.user
|
||||
}
|
||||
// Refresh organizations on every auth status update, not just user
|
||||
// changes. Switching organizations doesn't change the uid, so gating
|
||||
// this on uid changes leaves stale `active` flags — which reset the
|
||||
// account view's org dropdown on remount. The deepEqual guard in
|
||||
// getUserOrganizations prevents no-op re-renders.
|
||||
getUserOrganizations()
|
||||
|
||||
return oldUser
|
||||
})
|
||||
setUser((oldUser) => (oldUser?.uid !== responseUser.uid ? responseUser : oldUser))
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
console.error("Error in auth callback subscription:", error)
|
||||
@@ -79,6 +85,7 @@ export const ClineAuthProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
||||
|
||||
// Cleanup function to cancel subscription when component unmounts
|
||||
return () => {
|
||||
organizationsRequestIdRef.current++
|
||||
cancelSubscription()
|
||||
}
|
||||
}, [getUserOrganizations])
|
||||
|
||||
@@ -311,6 +311,7 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
remoteConfigSettings: {},
|
||||
backgroundCommandRunning: false,
|
||||
backgroundCommandTaskId: undefined,
|
||||
foregroundCommandRunning: false,
|
||||
lastDismissedCliBannerVersion: 0,
|
||||
backgroundEditEnabled: false,
|
||||
showFeatureTips: true,
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
},
|
||||
"apps/cli": {
|
||||
"name": "@cline/cli",
|
||||
"version": "3.0.41",
|
||||
"version": "3.0.45",
|
||||
"bin": {
|
||||
"cline": "src/index.ts",
|
||||
},
|
||||
@@ -632,7 +632,7 @@
|
||||
},
|
||||
"sdk/packages/agents": {
|
||||
"name": "@cline/agents",
|
||||
"version": "0.0.62",
|
||||
"version": "0.0.65",
|
||||
"dependencies": {
|
||||
"@cline/llms": "workspace:*",
|
||||
"@cline/shared": "workspace:*",
|
||||
@@ -641,7 +641,7 @@
|
||||
},
|
||||
"sdk/packages/core": {
|
||||
"name": "@cline/core",
|
||||
"version": "0.0.62",
|
||||
"version": "0.0.65",
|
||||
"dependencies": {
|
||||
"@cline/agents": "workspace:*",
|
||||
"@cline/llms": "workspace:*",
|
||||
@@ -679,7 +679,7 @@
|
||||
},
|
||||
"sdk/packages/llms": {
|
||||
"name": "@cline/llms",
|
||||
"version": "0.0.62",
|
||||
"version": "0.0.65",
|
||||
"dependencies": {
|
||||
"@ai-sdk/amazon-bedrock": "^4.0.89",
|
||||
"@ai-sdk/anthropic": "^3.0.68",
|
||||
@@ -698,30 +698,36 @@
|
||||
"@streamparser/json": "^0.0.21",
|
||||
"ai": "^6.0.144",
|
||||
"ai-sdk-ollama": "^3.8.8",
|
||||
"ai-sdk-provider-claude-code": "^3.4.3",
|
||||
"ai-sdk-provider-codex-cli": "^1.1.0",
|
||||
"ai-sdk-provider-opencode-sdk": "^3.0.1",
|
||||
"dify-ai-provider": "^1.1.0",
|
||||
"nanoid": "^5.1.7",
|
||||
"zod": "^4.3.6",
|
||||
},
|
||||
"devDependencies": {
|
||||
"ai-sdk-provider-claude-code": "^3.4.3",
|
||||
"ai-sdk-provider-codex-cli": "^1.1.0",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@aws-sdk/client-bedrock-runtime": "^3.0.0",
|
||||
"ai-sdk-provider-claude-code": "^3.4.3",
|
||||
"ai-sdk-provider-codex-cli": "^1.1.0",
|
||||
},
|
||||
"optionalPeers": [
|
||||
"@aws-sdk/client-bedrock-runtime",
|
||||
"ai-sdk-provider-claude-code",
|
||||
"ai-sdk-provider-codex-cli",
|
||||
],
|
||||
},
|
||||
"sdk/packages/sdk": {
|
||||
"name": "@cline/sdk",
|
||||
"version": "0.0.62",
|
||||
"version": "0.0.65",
|
||||
"dependencies": {
|
||||
"@cline/core": "workspace:*",
|
||||
},
|
||||
},
|
||||
"sdk/packages/shared": {
|
||||
"name": "@cline/shared",
|
||||
"version": "0.0.62",
|
||||
"version": "0.0.65",
|
||||
"dependencies": {
|
||||
"aws4fetch": "^1.0.20",
|
||||
"jsonrepair": "^3.13.2",
|
||||
@@ -731,11 +737,31 @@
|
||||
},
|
||||
"sdk/packages/ui": {
|
||||
"name": "@cline/ui",
|
||||
"version": "0.0.0",
|
||||
"version": "0.1.0",
|
||||
"devDependencies": {
|
||||
"@fontsource-variable/schibsted-grotesk": "^5.2.8",
|
||||
"@fontsource/azeret-mono": "^5.2.9",
|
||||
"@storybook/addon-a11y": "^9.1.17",
|
||||
"@storybook/addon-docs": "^9.1.17",
|
||||
"@storybook/react-vite": "^9.1.6",
|
||||
"@tailwindcss/vite": "^4.2.0",
|
||||
"@types/react": "19.2.14",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"jsdom": "^26.0.0",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
"storybook": "^9.1.17",
|
||||
"tailwindcss": "^4.2.0",
|
||||
"typescript": "5.9.3",
|
||||
"vite": "^7.1.11",
|
||||
"vitest": "^4.0.18",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18.3.0 <20",
|
||||
"tailwindcss": ">=4.0.0 <5",
|
||||
},
|
||||
"optionalPeers": [
|
||||
"react",
|
||||
"tailwindcss",
|
||||
],
|
||||
},
|
||||
@@ -768,27 +794,27 @@
|
||||
|
||||
"@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.16.1", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-1ad+Sc/0sCtZGHthxxvgEUo5Wsbw16I+aF+YwdiLnPwkZG8KAGUEAPK6LM6Pf69lCyJPt1Aomk1d+8oE3C4ZEw=="],
|
||||
|
||||
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.131", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.95", "@ai-sdk/openai": "3.0.82", "@ai-sdk/provider": "3.0.13", "@ai-sdk/provider-utils": "4.0.37", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-UrbM28zGFJV6xTn7wpv/uCsp/wMKb79MCuZC3Ff1a59PGjLr+iMN+Nlul7cWV04pjE5u5kLP5SybtGZtDP2EkQ=="],
|
||||
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.133", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.96", "@ai-sdk/openai": "3.0.84", "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-B1WkPajjTkVvG3OZJPxZQ/ZWdIja71qybt9W5HHLF9ooTIlr0Ldo6QVOy31IiVO53iJEtUXu4rTEZlMJE3TzJA=="],
|
||||
|
||||
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.95", "", { "dependencies": { "@ai-sdk/provider": "3.0.13", "@ai-sdk/provider-utils": "4.0.37" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Q7NhioTX6m0hKni14Ip9EO6WedbIYcldQ/PsGB7gVAveRNog39FfX31f+9HYoEUrfm9L7QxIcB5aAzJV/hmNRg=="],
|
||||
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.96", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-6VQzaXQdm5FkX6NWOyKzV5GB11C8IqkgsKZE91lg/bdwyvnQJLDwal2qkE0+fC8CCGeW5d+VV8Mw/+H+OcDC1A=="],
|
||||
|
||||
"@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.145", "", { "dependencies": { "@ai-sdk/provider": "3.0.13", "@ai-sdk/provider-utils": "4.0.37", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cqSQ+I0Bjj2W9g1oFyE1O1mSowsWXb+U1wK9vtg5kRQqB95iWVIKbtdr14gGf46cueJSiBlKfPTT24gDDVuFmw=="],
|
||||
"@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.148", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-C/mTeSdmhu8dAnH3vZaRXffD8Oewo1r4QEGxvCdR8eC9b4PKPs2CLsbg3DVJH/X3Bea5fAu87Ob7P3fSS+oq/g=="],
|
||||
|
||||
"@ai-sdk/google": ["@ai-sdk/google@3.0.90", "", { "dependencies": { "@ai-sdk/provider": "3.0.13", "@ai-sdk/provider-utils": "4.0.37" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-nn2bSLFZDV5Xhl2oh+C3ckpBUM849zrHLoLe6B9DVu2DcgtIvxKYO0pKLO/vB9XQVKON5XORp7uV7P+60L5XUQ=="],
|
||||
"@ai-sdk/google": ["@ai-sdk/google@3.0.91", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-d/ho+sDjArFjreE2002t9jE4LXX3wde97dN2HCLCX1l41gaJV3wf/c/19axjUNfIf/4uUq+nJmhBO0lW/dg3yw=="],
|
||||
|
||||
"@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@4.0.158", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.95", "@ai-sdk/google": "3.0.90", "@ai-sdk/openai-compatible": "2.0.58", "@ai-sdk/provider": "3.0.13", "@ai-sdk/provider-utils": "4.0.37", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Z7KPZ2+M7DFnXPEDgElwazDQxDxYqd9HQdFLuCMSpc0No/1Dr0TKRT+Q5pJwstHJfIQeAMApFvalzGOfTe/ShQ=="],
|
||||
"@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@4.0.159", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.96", "@ai-sdk/google": "3.0.91", "@ai-sdk/openai-compatible": "2.0.59", "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-AvHvV3Nw+LaLjTBveP96hBbKFXHoQBMeQa1fd9SV1UnXCvmCfl7cQempb+pZnho12rbTFHwgdX419nFTMNmATw=="],
|
||||
|
||||
"@ai-sdk/mistral": ["@ai-sdk/mistral@3.0.47", "", { "dependencies": { "@ai-sdk/provider": "3.0.13", "@ai-sdk/provider-utils": "4.0.37" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-A/ov/CTrQ0rDztrvgYo9ql4u6tlyfTrwMN4u76zqLM0JqUWy82T82Y8HzP4fCQOs7gZggrruHaNbWnqnE9IwKA=="],
|
||||
"@ai-sdk/mistral": ["@ai-sdk/mistral@3.0.48", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-SOZMjV48dyAn1rsiZSN7emeO0KYKnr9/SqMFPpJYUddPcnLSjac9GGWVKn+LnSzD7Woh3lYbYbJWdbJOQ2U2sQ=="],
|
||||
|
||||
"@ai-sdk/openai": ["@ai-sdk/openai@3.0.82", "", { "dependencies": { "@ai-sdk/provider": "3.0.13", "@ai-sdk/provider-utils": "4.0.37" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Gn1YliuNMneXoBmuLX1kH/e5SR/VnU9FXLvJ8WyiV61Noo+wPdE4nuzxRGt3lfV6rla1wyCb1syV4jU0A310ew=="],
|
||||
"@ai-sdk/openai": ["@ai-sdk/openai@3.0.84", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cmgbeJL0bbY0yTJH4/AdmP5E7MjWRL9G8UdhIi0JlV/So03o82ORJofW8OzwCZPTORVQblFbpZXYGDcUd9NdUQ=="],
|
||||
|
||||
"@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.58", "", { "dependencies": { "@ai-sdk/provider": "3.0.13", "@ai-sdk/provider-utils": "4.0.37" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-0SXA0xVt18F4ki7ttVshqaM0oLXSB475ACOU0/2RK3OZS3UYqrmKF+DJwYBYUZmqCq2nZ2vkNZEXpWP2wfGsrQ=="],
|
||||
"@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.59", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CFsUizO+jL+jSlN13rW2nQE9EbWx+8PSFBdB3TtD0UGYOxtefCVa5hsrNo5NUOJJGX5f4xHiXE1i2m5nQ80QPg=="],
|
||||
|
||||
"@ai-sdk/provider": ["@ai-sdk/provider@3.0.13", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-ZPtVYt5QIJzOta1kdUiDuCx4HhFkvNPv/rvmZ2b1iXwybYjJsCnNYR4PAw4kW7rgVfDARvHXcU64efWuqNp6bw=="],
|
||||
"@ai-sdk/provider": ["@ai-sdk/provider@3.0.14", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-5X1k57JBJ4H7H1QjX7CnJYAB1I19r/trVZTMcSms7/kLNZ8RaU4Nt2agcwZzv82Hfx6Q7/TOLU7agAKeFfc8cA=="],
|
||||
|
||||
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.37", "", { "dependencies": { "@ai-sdk/provider": "3.0.13", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-VG4tpVXCuzm21U9xjg05BCMZnjZOazC72+MxBkLAa7hCKsnqNt542GYWUUqwmHSczJwgbSXN8UvaNgSerUaKdw=="],
|
||||
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.38", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-/HHGmtKllqjg1OLc023v9w9kK3laW7Z6TzfZukYQWCsGBbzB9p60zTvvpXFVcs44NZBVXL3viOa1HRKUbeee8g=="],
|
||||
|
||||
"@ai-sdk/react": ["@ai-sdk/react@3.0.223", "", { "dependencies": { "@ai-sdk/provider-utils": "4.0.37", "ai": "6.0.221", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ~19.0.1 || ~19.1.2 || ^19.2.1" } }, "sha512-b7Ri+wLOR9pZkKlEKii3ZuXi79Rh3rC5rYUEFDpXUOJLazztIE4MPM4dQRXCojYgOuPa61gVO2BFjuRVLGsOfw=="],
|
||||
"@ai-sdk/react": ["@ai-sdk/react@3.0.226", "", { "dependencies": { "@ai-sdk/provider-utils": "4.0.38", "ai": "6.0.224", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ~19.0.1 || ~19.1.2 || ^19.2.1" } }, "sha512-pPLSwLlxpXzYypkIpDRWENtIB1aDvi1TSxXrt5UZjLX1e0uHNaTUZJiGa52M8G0sN00a2Q2+ASARqMZKaTZbXQ=="],
|
||||
|
||||
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
|
||||
|
||||
@@ -796,23 +822,23 @@
|
||||
|
||||
"@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.3.205", "", { "optionalDependencies": { "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.205", "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.205", "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.205", "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.205", "@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.205", "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.205", "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.205", "@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.205" }, "peerDependencies": { "@anthropic-ai/sdk": ">=0.93.0", "@modelcontextprotocol/sdk": "^1.29.0", "zod": "^4.0.0" } }, "sha512-ft6iBw9kXudsusiXNpeybIPBJ07Z3tqp1ROSg5cEJqgA+9i+JJj2sRfQth+QD+lyenbbAU8yPieLxIimvfBhtw=="],
|
||||
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.3.207", "", { "optionalDependencies": { "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.207", "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.207", "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.207", "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.207", "@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.207", "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.207", "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.207", "@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.207" }, "peerDependencies": { "@anthropic-ai/sdk": ">=0.93.0", "@modelcontextprotocol/sdk": "^1.29.0", "zod": "^4.0.0" } }, "sha512-y0PkQRmQBi96MHiN5Xzfq+GaddxCZCqI/cXEQBLYBLXGa4i1nDSlulQqkMBj2RorrrSGQJ6Wdw+uhu6OfHNPzA=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-arm64": ["@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.205", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lrfJ4eVtzfPkCpbSkBOGSMQCBbvmW6nbPzgHE4IwMN3scZlpuFMUFqh2aaJa/X2SAcWD9H2S0t2WWvSRgM7BjA=="],
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-arm64": ["@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.207", "", { "os": "darwin", "cpu": "arm64" }, "sha512-08xSo1FDx8h0aLhL5tvcRxa2SMmcUV3aDWeZiEJVTclyiDAs61BgTjAxCg+SZcu1CndjJO8cfO0yM5dhamxz3g=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-x64": ["@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.205", "", { "os": "darwin", "cpu": "x64" }, "sha512-G6ETPmL5mNzJ2DFsWxG3jmsmrXgZX1N2ZCJvxaGUUpjTsKZJ4Tup1cWYvcd/m7o5fYZmx9REmgzTwsAIc1fdPQ=="],
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-x64": ["@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.207", "", { "os": "darwin", "cpu": "x64" }, "sha512-1o7K4EYqyCixZ/oeOZSh7AzSy6TM86xoOuf4VuORjPSS31hBnoqY0NGZd27+2VDs9LGtsdksmsTqcNGx9xd1hA=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64": ["@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.205", "", { "os": "linux", "cpu": "arm64" }, "sha512-CXzySK3PV3EizCRPXnxPqeaAtgrBFDnMFOVpMe36oC3U16yDb1b1tAJGqZi/7uFrVvAiaXvnSFxhUWnDDSaO+A=="],
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64": ["@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.207", "", { "os": "linux", "cpu": "arm64" }, "sha512-X4uezYOifDiNTTmmugfRCdg3nNamrr1LFRY9hg30vWYTShL+bbN+nfC3KaFfSYCl4GTtsEEUbYdOTC2F3bBpcA=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": ["@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.205", "", { "os": "linux", "cpu": "arm64" }, "sha512-91fgdG4aTnQ29sKOcUqgH4+tKCW2ut6PWGRSYmXNDbROasJm1rAlPdzC5brdu/e4c0CDSNV6TWyE5JCjaS/jlQ=="],
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": ["@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.207", "", { "os": "linux", "cpu": "arm64" }, "sha512-oPj+g2DslhH4Y9nCTs7al4t9wZv78FZwLFQwOCg99BXuz1o0ZOpKmxyvR7J9eBR+GPszeMMS8gYplQTiZC9o2w=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64": ["@anthropic-ai/claude-agent-sdk-linux-x64@0.3.205", "", { "os": "linux", "cpu": "x64" }, "sha512-siS+1iNqBSlGFZZvJY6+mhzZ/6/ec/TbX9GMuwmTF0E6fxGhIIp797jJxR1q8r6FAq7d39mEoRNhC0Ffo60uNQ=="],
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64": ["@anthropic-ai/claude-agent-sdk-linux-x64@0.3.207", "", { "os": "linux", "cpu": "x64" }, "sha512-Kg6BPH8Ee0ny/oEUWJmvT1jCRBne4jVpRSOMsJcYp1Fav1rMEgpU219oJJs+LWwx4ifuuLtNWedqJNnVw7mnKg=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": ["@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.205", "", { "os": "linux", "cpu": "x64" }, "sha512-vvsb7GlnA8CTSVvvTkrXjcSeRKqxSM7p/tU3Od9ICAZeWHglptekEyzLEApzLuLbI5ewfFF/F0q3NwOBbo18dg=="],
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": ["@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.207", "", { "os": "linux", "cpu": "x64" }, "sha512-uRv+D5oG/7EYr41FAJ9IPo2pZYBe2ZMaA6nSHCeizsgPxCSMtl5bNppmU21+jZJvo4hivObEgkGFERAhdGqygg=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk-win32-arm64": ["@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.205", "", { "os": "win32", "cpu": "arm64" }, "sha512-SpP5zF68weFez/6pKrGzq/UVAJDMDNphWqmkLfOpWTDBL5xy6XlIZw5Bl4EXoVnfi2VLFkwuffNeFe+9SdX7kw=="],
|
||||
"@anthropic-ai/claude-agent-sdk-win32-arm64": ["@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.207", "", { "os": "win32", "cpu": "arm64" }, "sha512-9fWpUzfkXlPAg2tf8JpQe7w9avFaomAUbfAwyAmykQgSIf66LwaJjvI5hNqhNqczRKyfsXPn3ei2S5HKlmFP+Q=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk-win32-x64": ["@anthropic-ai/claude-agent-sdk-win32-x64@0.3.205", "", { "os": "win32", "cpu": "x64" }, "sha512-kg2kkXyeSoFLruO3Ic2IruLxzBR0xCUtmlJHdWi3SYW7JhAKNJg4fcrdJsWcardmEw23Y2UDGDJbRyxqSVx6wg=="],
|
||||
"@anthropic-ai/claude-agent-sdk-win32-x64": ["@anthropic-ai/claude-agent-sdk-win32-x64@0.3.207", "", { "os": "win32", "cpu": "x64" }, "sha512-YPjVT0q6aXEM2MgN4CI6/9fqiTXwETji+4NoPOzCYuqAkhXZqp30Jsk7/NHqYGNNSfURKrsuAoliKB0rsbpbjg=="],
|
||||
|
||||
"@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.37.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-tHjX2YbkUBwEgg0JZU3EFSSAQPoK4qQR/NFYa8Vtzd5UAyXzZksCw2In69Rml4R/TyHPBfRYaLK35XiOe33pjw=="],
|
||||
|
||||
@@ -838,7 +864,7 @@
|
||||
|
||||
"@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.63", "", { "dependencies": { "@aws-sdk/core": "^3.975.1", "@aws-sdk/nested-clients": "^3.997.31", "@aws-sdk/types": "^3.974.0", "@smithy/core": "^3.29.2", "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-8qZLFhM69eKcS37m459ctPR05Qimycm/74OPVioe6wNZabMT54GYhwBju0+J656RkMasNSawWQu+c8CmBe3TUQ=="],
|
||||
|
||||
"@aws-sdk/credential-providers": ["@aws-sdk/credential-providers@3.1083.0", "", { "dependencies": { "@aws-sdk/core": "^3.975.1", "@aws-sdk/credential-provider-cognito-identity": "^3.972.56", "@aws-sdk/credential-provider-env": "^3.972.57", "@aws-sdk/credential-provider-http": "^3.972.59", "@aws-sdk/credential-provider-ini": "^3.973.1", "@aws-sdk/credential-provider-login": "^3.972.63", "@aws-sdk/credential-provider-node": "^3.972.66", "@aws-sdk/credential-provider-process": "^3.972.57", "@aws-sdk/credential-provider-sso": "^3.973.1", "@aws-sdk/credential-provider-web-identity": "^3.972.63", "@aws-sdk/nested-clients": "^3.997.31", "@aws-sdk/types": "^3.974.0", "@smithy/core": "^3.29.2", "@smithy/credential-provider-imds": "^4.4.7", "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-i2q3Jgt365lZp7BSDqDSf283WvISrXob1zsql093LK3G2svYRHRvcNv995SsKtAzRENwHem2TC2vWeghrgBkxg=="],
|
||||
"@aws-sdk/credential-providers": ["@aws-sdk/credential-providers@3.1085.0", "", { "dependencies": { "@aws-sdk/core": "^3.975.1", "@aws-sdk/credential-provider-cognito-identity": "^3.972.56", "@aws-sdk/credential-provider-env": "^3.972.57", "@aws-sdk/credential-provider-http": "^3.972.59", "@aws-sdk/credential-provider-ini": "^3.973.1", "@aws-sdk/credential-provider-login": "^3.972.63", "@aws-sdk/credential-provider-node": "^3.972.66", "@aws-sdk/credential-provider-process": "^3.972.57", "@aws-sdk/credential-provider-sso": "^3.973.1", "@aws-sdk/credential-provider-web-identity": "^3.972.63", "@aws-sdk/nested-clients": "^3.997.31", "@aws-sdk/types": "^3.974.0", "@smithy/core": "^3.29.2", "@smithy/credential-provider-imds": "^4.4.7", "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-t9bNvRalbNaU8lt/jA1LeFIwowf9ZDlBu/G2l8QnCVYs7yZ2muh/RYdMF0eT+PfgDto8yQ+ncrxFO0bi2uN6oA=="],
|
||||
|
||||
"@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.31", "", { "dependencies": { "@aws-sdk/core": "^3.975.1", "@aws-sdk/signature-v4-multi-region": "^3.996.39", "@aws-sdk/types": "^3.974.0", "@smithy/core": "^3.29.2", "@smithy/fetch-http-handler": "^5.6.4", "@smithy/node-http-handler": "^4.9.4", "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-BDHTpwcsZHEBNEJzOg/B1BkFYJxAXY50dau/NyVWs3d51F0WgIUGSWZot/Os+N3KpDhXeaXnz37mWffAvduREw=="],
|
||||
|
||||
@@ -962,7 +988,7 @@
|
||||
|
||||
"@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.2", "", {}, "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA=="],
|
||||
|
||||
"@bufbuild/buf": ["@bufbuild/buf@1.71.0", "", { "optionalDependencies": { "@bufbuild/buf-darwin-arm64": "1.71.0", "@bufbuild/buf-darwin-x64": "1.71.0", "@bufbuild/buf-linux-aarch64": "1.71.0", "@bufbuild/buf-linux-armv7": "1.71.0", "@bufbuild/buf-linux-x64": "1.71.0", "@bufbuild/buf-win32-arm64": "1.71.0", "@bufbuild/buf-win32-x64": "1.71.0" }, "bin": { "buf": "bin/buf", "protoc-gen-buf-breaking": "bin/protoc-gen-buf-breaking", "protoc-gen-buf-lint": "bin/protoc-gen-buf-lint" } }, "sha512-GDcjBCwLgHT/4nX4YSnYatZ7sDZDpHV6dxQvoT2/P6gKvV23O6hl8NryzLIRKmeau0FRXpQKHVy1dMfnBSpy+w=="],
|
||||
"@bufbuild/buf": ["@bufbuild/buf@1.71.0", "", { "optionalDependencies": { "@bufbuild/buf-darwin-arm64": "1.71.0", "@bufbuild/buf-darwin-x64": "1.71.0", "@bufbuild/buf-linux-aarch64": "1.71.0", "@bufbuild/buf-linux-armv7": "1.71.0", "@bufbuild/buf-linux-x64": "1.71.0", "@bufbuild/buf-win32-arm64": "1.71.0", "@bufbuild/buf-win32-x64": "1.71.0" }, "bin": { "buf": "bin/buf", "protoc-gen-buf-lint": "bin/protoc-gen-buf-lint", "protoc-gen-buf-breaking": "bin/protoc-gen-buf-breaking" } }, "sha512-GDcjBCwLgHT/4nX4YSnYatZ7sDZDpHV6dxQvoT2/P6gKvV23O6hl8NryzLIRKmeau0FRXpQKHVy1dMfnBSpy+w=="],
|
||||
|
||||
"@bufbuild/buf-darwin-arm64": ["@bufbuild/buf-darwin-arm64@1.71.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-qZ7xZQyen/jOKFPVs3dlN9pMA56PI4YEo3r4/9ixtiH9gyFgfowR31axsocUgXGThjiN8mvOA8WfpG2tvaSvsw=="],
|
||||
|
||||
@@ -1144,9 +1170,9 @@
|
||||
|
||||
"@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="],
|
||||
|
||||
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.5", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg=="],
|
||||
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.6", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.3.0", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA=="],
|
||||
|
||||
"@eslint/js": ["@eslint/js@9.39.4", "", {}, "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw=="],
|
||||
"@eslint/js": ["@eslint/js@9.39.5", "", {}, "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A=="],
|
||||
|
||||
"@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="],
|
||||
|
||||
@@ -1244,15 +1270,15 @@
|
||||
|
||||
"@firebase/webchannel-wrapper": ["@firebase/webchannel-wrapper@1.0.3", "", {}, "sha512-2xCRM9q9FlzGZCdgDMJwc0gyUkWFtkosy7Xxr6sFgQwn+wMNIWd7xIvYNauU1r64B5L5rsGKy/n9TKJ0aAFeqQ=="],
|
||||
|
||||
"@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="],
|
||||
"@floating-ui/core": ["@floating-ui/core@1.8.0", "", { "dependencies": { "@floating-ui/utils": "^0.2.12" } }, "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ=="],
|
||||
|
||||
"@floating-ui/dom": ["@floating-ui/dom@1.7.6", "", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="],
|
||||
"@floating-ui/dom": ["@floating-ui/dom@1.8.0", "", { "dependencies": { "@floating-ui/core": "^1.8.0", "@floating-ui/utils": "^0.2.12" } }, "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg=="],
|
||||
|
||||
"@floating-ui/react": ["@floating-ui/react@0.27.19", "", { "dependencies": { "@floating-ui/react-dom": "^2.1.8", "@floating-ui/utils": "^0.2.11", "tabbable": "^6.0.0" }, "peerDependencies": { "react": ">=17.0.0", "react-dom": ">=17.0.0" } }, "sha512-31B8h5mm8YxotlE7/AU/PhNAl8eWxAmjL/v2QOxroDNkTFLk3Uu82u63N3b6TXa4EGJeeZLVcd/9AlNlVqzeog=="],
|
||||
"@floating-ui/react": ["@floating-ui/react@0.27.20", "", { "dependencies": { "@floating-ui/react-dom": "^2.1.9", "@floating-ui/utils": "^0.2.12", "tabbable": "^6.0.0" }, "peerDependencies": { "react": ">=17.0.0", "react-dom": ">=17.0.0" } }, "sha512-CMqMy7OaXl9W0eq1Uy7L7i2Y/anPvHmFmESd2CEw0t5YvZhcVCeo4MBevAmswRllX7Y2dEidA4ozGPunLSTQpw=="],
|
||||
|
||||
"@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.8", "", { "dependencies": { "@floating-ui/dom": "^1.7.6" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A=="],
|
||||
"@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.9", "", { "dependencies": { "@floating-ui/dom": "^1.8.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg=="],
|
||||
|
||||
"@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="],
|
||||
"@floating-ui/utils": ["@floating-ui/utils@0.2.12", "", {}, "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww=="],
|
||||
|
||||
"@fontsource-variable/geist": ["@fontsource-variable/geist@5.2.9", "", {}, "sha512-TP+QSBG3wxKGPE33CbMy/L0Nu3qvJ6Fy81Yc4LnQ95xH+i+cfEp8fyU8/kfV14YwszxIFPhnoMTbjL71waVpyQ=="],
|
||||
|
||||
@@ -1624,6 +1650,8 @@
|
||||
|
||||
"@mapbox/node-pre-gyp": ["@mapbox/node-pre-gyp@2.0.3", "", { "dependencies": { "consola": "^3.2.3", "detect-libc": "^2.0.0", "https-proxy-agent": "^7.0.5", "node-fetch": "^2.6.7", "nopt": "^8.0.0", "semver": "^7.5.3", "tar": "^7.4.0" }, "bin": { "node-pre-gyp": "bin/node-pre-gyp" } }, "sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg=="],
|
||||
|
||||
"@mdx-js/react": ["@mdx-js/react@3.1.1", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw=="],
|
||||
|
||||
"@mermaid-js/parser": ["@mermaid-js/parser@1.2.0", "", { "dependencies": { "@chevrotain/types": "~11.1.2" } }, "sha512-oYPyv8A4As1yH5Bx+04iQEQxXuIQDe0GKCNSRgao6z8AM9jixXIfP0vsppRLvGf+nKIOb9/LdpWA4YuJiVvESA=="],
|
||||
|
||||
"@microsoft/fast-element": ["@microsoft/fast-element@1.14.0", "", {}, "sha512-zXvuSOzvsu8zDTy9eby8ix8VqLop2rwKRgp++ZN2kTCsoB3+QJVoaGD2T/Cyso2ViZQFXNpiNCVKfnmxBvmWkQ=="],
|
||||
@@ -1666,21 +1694,21 @@
|
||||
|
||||
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
|
||||
|
||||
"@openai/codex": ["@openai/codex@0.130.0", "", { "optionalDependencies": { "@openai/codex-darwin-arm64": "npm:@openai/codex@0.130.0-darwin-arm64", "@openai/codex-darwin-x64": "npm:@openai/codex@0.130.0-darwin-x64", "@openai/codex-linux-arm64": "npm:@openai/codex@0.130.0-linux-arm64", "@openai/codex-linux-x64": "npm:@openai/codex@0.130.0-linux-x64", "@openai/codex-win32-arm64": "npm:@openai/codex@0.130.0-win32-arm64", "@openai/codex-win32-x64": "npm:@openai/codex@0.130.0-win32-x64" }, "bin": { "codex": "bin/codex.js" } }, "sha512-WGDj+RZ3TXWC/7MlwprgLWOqzpwatPIINPhP3IRzHA0ni+o3QZ4i4xrS2uWwGmHUJ395J5JHwoZAAZYyfJyz6w=="],
|
||||
"@openai/codex": ["@openai/codex@0.144.1", "", { "optionalDependencies": { "@openai/codex-darwin-arm64": "npm:@openai/codex@0.144.1-darwin-arm64", "@openai/codex-darwin-x64": "npm:@openai/codex@0.144.1-darwin-x64", "@openai/codex-linux-arm64": "npm:@openai/codex@0.144.1-linux-arm64", "@openai/codex-linux-x64": "npm:@openai/codex@0.144.1-linux-x64", "@openai/codex-win32-arm64": "npm:@openai/codex@0.144.1-win32-arm64", "@openai/codex-win32-x64": "npm:@openai/codex@0.144.1-win32-x64" }, "bin": { "codex": "bin/codex.js" } }, "sha512-Xir1zqPfpenhdoAoshN53uonzbBXj18COyzRkFlVZpSNyEl5XtkuYu9oddELePFN7K/0sXUcSO34Ad5IeCXPbw=="],
|
||||
|
||||
"@openai/codex-darwin-arm64": ["@openai/codex@0.130.0-darwin-arm64", "", { "os": "darwin", "cpu": "arm64" }, "sha512-R9pkGC7kwC8yQ8el5hvBlmugQlcsG/pHMEFgZluu03X9fD2TezGxdq3KqRDRCZuMYl07ILamVEoqknuJ0cq7MA=="],
|
||||
"@openai/codex-darwin-arm64": ["@openai/codex@0.144.1-darwin-arm64", "", { "os": "darwin", "cpu": "arm64" }, "sha512-dABeDK+ATqMG54MGBd3VjpKfh5EOoqx9PKVQB2QYDaEXx3F6CdUCXue5QIMfr4OxziUj8pUcLAQyd+KFqiTUFw=="],
|
||||
|
||||
"@openai/codex-darwin-x64": ["@openai/codex@0.130.0-darwin-x64", "", { "os": "darwin", "cpu": "x64" }, "sha512-gJ+7J8djevgtdra+NgDAiQQPW+O3KTsgGfE3E5dpDfww3zS5OCeV0V2dhxqnJdlOjOSDw99o0P2LqBv19mhpRw=="],
|
||||
"@openai/codex-darwin-x64": ["@openai/codex@0.144.1-darwin-x64", "", { "os": "darwin", "cpu": "x64" }, "sha512-K2g3Q3tNxzFhV0SuzO6HcsYK7EQrp/o4HyeReyhkwVrwwUPoYwyIbB0IRjHIiDzRhbKriDccid2iyF5aPqdTcg=="],
|
||||
|
||||
"@openai/codex-linux-arm64": ["@openai/codex@0.130.0-linux-arm64", "", { "os": "linux", "cpu": "arm64" }, "sha512-tFtH0V9/hEI3d9y7zP92BXI9FM4Z3+STNQaOR52Czv18TRtCFUp7CbIUYaToopuq6UBfnE1VKr8RLhwT5FcbmA=="],
|
||||
"@openai/codex-linux-arm64": ["@openai/codex@0.144.1-linux-arm64", "", { "os": "linux", "cpu": "arm64" }, "sha512-451o15+XtaXCCb35t/KCyyPqXHnTPxPxtdqEYOnE3e4sH5AfnI/uVJwfdjOksMG6vRLy6R+fLvSDOMguRFLmQw=="],
|
||||
|
||||
"@openai/codex-linux-x64": ["@openai/codex@0.130.0-linux-x64", "", { "os": "linux", "cpu": "x64" }, "sha512-3VcNlez99xdnEf+kB1IOpWv9fICYV9PiGj4sLCO4TCcShLnyxe+YBGa3poknkvXLnMG0qiN9SMnYS2FGrMxQcA=="],
|
||||
"@openai/codex-linux-x64": ["@openai/codex@0.144.1-linux-x64", "", { "os": "linux", "cpu": "x64" }, "sha512-HNGVI+BulrOaC/0IzBvd6EL62j7LrlbFKibrhw6hZjjCjAeUYzRB2jB4qDzXN1NfqDi6Xrvniof3kwbwab24lg=="],
|
||||
|
||||
"@openai/codex-win32-arm64": ["@openai/codex@0.130.0-win32-arm64", "", { "os": "win32", "cpu": "arm64" }, "sha512-vdpmiNp57L/arZabltLXn8TyEtNa7W1meOEkr+3R6W/8ZyBt++wuqz1Orv134OT2grrcFJsIVCAIPiqUxCvBkA=="],
|
||||
"@openai/codex-win32-arm64": ["@openai/codex@0.144.1-win32-arm64", "", { "os": "win32", "cpu": "arm64" }, "sha512-L4aDVEh9o1u7WYoxpSyv3un9Bz26YZYocOFqE2oHdEQDL2s6/LdtutLQc3oUZruLlEbkNsjSU0HI1OKsP0+Ctg=="],
|
||||
|
||||
"@openai/codex-win32-x64": ["@openai/codex@0.130.0-win32-x64", "", { "os": "win32", "cpu": "x64" }, "sha512-FzMznm7fr5/nbjZgOujZ9Y9AbdGm7ji1FOoWiY3U+srqauvZaTgn6o6aCheSL7kuymu7nTLOO/cAyWV6NuesqQ=="],
|
||||
"@openai/codex-win32-x64": ["@openai/codex@0.144.1-win32-x64", "", { "os": "win32", "cpu": "x64" }, "sha512-qv2HOp6v/nVP31p5I5GxYyL0wa79PMzim1+W9CKSV0UldjFV9AMbualA8PeXcYhbvvh9Y1UASXxwjuQdlyfAvw=="],
|
||||
|
||||
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.17.15", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-gWRQOEggHTELJ9+BtelxnuAczk9qutCXVZenPgRPaT8oVxePf52jfWbqfhyFjnrN8Vlp8tCCTdkEpFW5pZAuEA=="],
|
||||
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.17.18", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-c/C9PhY8PrbcxDY+JIYtOZsrmMD0KzoVvxq+RGUrZ6LQp57SuVBbT4lfwA2G8Se5RNC1N5JtYjiuaXeECnF2SQ=="],
|
||||
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="],
|
||||
|
||||
@@ -1740,7 +1768,7 @@
|
||||
|
||||
"@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.9.0", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.9.0", "@opentelemetry/core": "2.9.0", "@opentelemetry/sdk-trace-base": "2.9.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-ec9a7ps37huy5itYk0MalaZdSLlM6AXWp/FhtEjgMpp5leEGojBDvAl/UWttQnkMZOvFHKzRESn8TD3yKTF5nQ=="],
|
||||
|
||||
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.42.0", "", {}, "sha512-icc5xCzndZfhuJMy5oqk5AvloWquR7jtae74qzpkKkhGp8BivK+oCcEXgGnjCdTfp8hA44l+w8gE8yYJbocJJw=="],
|
||||
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.43.0", "", {}, "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg=="],
|
||||
|
||||
"@opentui-ui/dialog": ["@opentui-ui/dialog@0.1.2", "", { "peerDependencies": { "@opentui/core": "^0.1.69", "@opentui/react": "^0.1.69", "@opentui/solid": "^0.1.69" }, "optionalPeers": ["@opentui/react", "@opentui/solid"] }, "sha512-EZ4FG5u5sxU75+6pcsJsLzsD5JqO05So/1ceZUKUu7nxZ9IF7gcZEi+MU4HnYC9cb2Q7w6hM9y3/iW+jE1C53w=="],
|
||||
|
||||
@@ -1772,7 +1800,7 @@
|
||||
|
||||
"@playwright/test": ["@playwright/test@1.61.1", "", { "dependencies": { "playwright": "1.61.1" }, "bin": { "playwright": "cli.js" } }, "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig=="],
|
||||
|
||||
"@posthog/core": ["@posthog/core@1.40.0", "", { "dependencies": { "@posthog/types": "^1.393.0" } }, "sha512-oGDbIwlTquNwdHbEL5ZLEkuW4UFkkEanfx3QAxDgyVbISv+OAA6YGQwrvo0JD3MUJEbJZvyh8XsX+WYDGw9XHw=="],
|
||||
"@posthog/core": ["@posthog/core@1.40.2", "", { "dependencies": { "@posthog/types": "^1.393.0" } }, "sha512-H12j7O9iHGvpK9t2ko8W4pvfbV1pBDxrsWC1LA6yp2RhzwvC4T3sWhu+AekDQJSRSrJEWlB0t/Ueq9QhPSq7FQ=="],
|
||||
|
||||
"@posthog/types": ["@posthog/types@1.393.0", "", {}, "sha512-vzWeEJZ7ERQhFRoQYaP5jzN1JvIu46UJyHXsuv+dTGW2r3sMgREOhNxXLZjmFHwZ8/FOHQoyqqQmXTCXZSfMSg=="],
|
||||
|
||||
@@ -2260,7 +2288,7 @@
|
||||
|
||||
"@simple-git/argv-parser": ["@simple-git/argv-parser@1.1.1", "", { "dependencies": { "@simple-git/args-pathspec": "^1.0.3" } }, "sha512-Q9lBcfQ+VQCpQqGJFHe5yooOS5hGdLFFbJ5R+R5aDsnkPCahtn1hSkMcORX65J2Z5lxSkD0lQorMsncuBQxYUw=="],
|
||||
|
||||
"@sinclair/typebox": ["@sinclair/typebox@0.34.49", "", {}, "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A=="],
|
||||
"@sinclair/typebox": ["@sinclair/typebox@0.34.52", "", {}, "sha512-XiMQh7qqVlxZzcVD+kkGMNGMzcTrDMLWI7S4x7z1MkCkbDPrekpZXEUK0eZqZFMuHQg2a2DZOcDIh9o5v3Gonw=="],
|
||||
|
||||
"@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="],
|
||||
|
||||
@@ -2278,21 +2306,21 @@
|
||||
|
||||
"@slack/web-api": ["@slack/web-api@7.19.0", "", { "dependencies": { "@slack/logger": "^4.0.1", "@slack/types": "^2.21.0", "@types/node": ">=18", "@types/retry": "0.12.0", "axios": "^1.16.0", "eventemitter3": "^5.0.1", "form-data": "^4.0.4", "is-electron": "2.2.2", "is-stream": "^2", "p-queue": "^6", "p-retry": "^4", "retry": "^0.13.1" } }, "sha512-ItjyjEZml+LDH8CjcCLRLJHh7VZtevPKExrRN3l5KWyBliyDnGAeoO4Y+K+fFBmRpKLYVPgqWMX4THldv2HVtA=="],
|
||||
|
||||
"@smithy/core": ["@smithy/core@3.29.2", "", { "dependencies": { "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-DXUk6yU0C1Q1tYvJh1VCtl8QOBcSoZpKwjTPkxT6A4MUQYHvgeKGByL8mrEdxnvhdf9nq5GyzmRb5n/vPgu3Lw=="],
|
||||
"@smithy/core": ["@smithy/core@3.29.3", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-L+Ys6ecjk5vwPMAKHBpPKlJ3DkqwNcnfEISXBZIsVvWG/XKXfsAP8mwIYlTeLcd2ElHdesPI8OuOmJSFAPhm6A=="],
|
||||
|
||||
"@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.4.7", "", { "dependencies": { "@smithy/core": "^3.29.2", "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-UEMLOoA0Fl4uYBxh6l0uN0H6EJe/A89OGeDNTteQeXpJ20BcpfIr4wlCY9pel1jEAUHAxaYwuqrYlrKdXE1GKQ=="],
|
||||
"@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.4.8", "", { "dependencies": { "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-q9J7JTiXrAhB8sDp4px97uEPT7CwKH61Co78grdNQvU8QZAdiuaSRhP0tUVf2ogy36RZTrlMU1rBmDEH+cnkiA=="],
|
||||
|
||||
"@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.4.7", "", { "dependencies": { "@smithy/core": "^3.29.2", "tslib": "^2.6.2" } }, "sha512-YNodWVjMFOMAyjQgpHBBCz62DbYu4xwhpt+z5HRf7OZPwrfHgNDCyUZxaC0fy9/2TWcO+niOMHSl8aMgfbWZTQ=="],
|
||||
"@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.4.8", "", { "dependencies": { "@smithy/core": "^3.29.3", "tslib": "^2.6.2" } }, "sha512-Q0qYaae6vcDW9JRyMbdXiONnbL+uNNExKzy7sippUzI1CRCfiCELGOR5tR0FQmMciJfsLgox5K82kl6BmwrnAg=="],
|
||||
|
||||
"@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.6.4", "", { "dependencies": { "@smithy/core": "^3.29.2", "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-psnst7NZWdAEvJvyW8YZEE7xNVMyLrQFfHtyrVFrxNyy+dKWkQ+rqC6oI5ZhxThpUy9RSfEshgm34zqbOxzsRw=="],
|
||||
"@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.6.5", "", { "dependencies": { "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-SuqeisTyPoiIPtIYru/sGxGyXzmZ+8nnFOhC+qRPglt06Ebd1yH//CDltZB2J/3WBNVhwfUaZ0EtHB3cm2X32g=="],
|
||||
|
||||
"@smithy/node-http-handler": ["@smithy/node-http-handler@4.9.4", "", { "dependencies": { "@smithy/core": "^3.29.2", "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-BNTop/fSOptmoVk8g+efwHCofFh37g70OWGAFES1TeAAJja1K5aAI8rTE26ETSc5k8IQuWY2kAIoPla01NgYrA=="],
|
||||
"@smithy/node-http-handler": ["@smithy/node-http-handler@4.9.5", "", { "dependencies": { "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-bNqdxTQTxmLbomSmlkZFz8L6B/feQ2HHzw4L2zY7Ecp2XffYAZq2uzdWDdxJHJFbEvqd+SRuluJso0P8+xPdbw=="],
|
||||
|
||||
"@smithy/signature-v4": ["@smithy/signature-v4@5.6.3", "", { "dependencies": { "@smithy/core": "^3.29.2", "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-8qVKKzqh7naF27ePmx0SkUfnGP/wBI9dyaeAmhHvopnbIlItUAmB/e6PkPCU3rRb2v9BY8D4EZXSoydSibatvw=="],
|
||||
"@smithy/signature-v4": ["@smithy/signature-v4@5.6.4", "", { "dependencies": { "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-B89bpf2t/y/wia6LZ+4JfHXYQT9PnVftsH05rgJKKIStS7r/4XSs9HOjtPoLtgcA6HCW9jVqX5DBbq7E0PAkiQ=="],
|
||||
|
||||
"@smithy/types": ["@smithy/types@4.16.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-aVUabzlBBmY0PfvVgLKQSOGFIL5/7R54JE3uD9a5Ay/jSED61SkuAcCYENNXJzYUvJ1NPrWO0P+rAXHCkbBUKw=="],
|
||||
"@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="],
|
||||
|
||||
"@smithy/util-utf8": ["@smithy/util-utf8@4.4.7", "", { "dependencies": { "@smithy/core": "^3.29.2", "tslib": "^2.6.2" } }, "sha512-NskAyOBZcHO+fa1HwkwWuPbMUQE7NZ4IWBnAc71E4f9IdoQ65WLkdJ2ed/n4Q8vePSgZUN/CbyGZLBiI7dMI+w=="],
|
||||
"@smithy/util-utf8": ["@smithy/util-utf8@4.4.8", "", { "dependencies": { "@smithy/core": "^3.29.3", "tslib": "^2.6.2" } }, "sha512-hwVELJTTRUqwrEvMI73PwsP27cO1HgkAKDoKelhMS3biTd8z5iXj76UF7oemuDba3X5eHZqjedeyBUhJLns+Kg=="],
|
||||
|
||||
"@so-ric/colorspace": ["@so-ric/colorspace@1.1.6", "", { "dependencies": { "color": "^5.0.2", "text-hex": "1.0.x" } }, "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw=="],
|
||||
|
||||
@@ -2302,12 +2330,18 @@
|
||||
|
||||
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@storybook/addon-a11y": ["@storybook/addon-a11y@9.1.20", "", { "dependencies": { "@storybook/global": "^5.0.0", "axe-core": "^4.2.0" }, "peerDependencies": { "storybook": "^9.1.20" } }, "sha512-VFZ34y4ApmFwIzPRs2OJrG6jtYhM5y91eCZLTlR/HMGQciKF4TdOJHjj+5vf91SOER5UDcLizXetpiUowiZSgw=="],
|
||||
|
||||
"@storybook/addon-docs": ["@storybook/addon-docs@9.1.20", "", { "dependencies": { "@mdx-js/react": "^3.0.0", "@storybook/csf-plugin": "9.1.20", "@storybook/icons": "^1.4.0", "@storybook/react-dom-shim": "9.1.20", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^9.1.20" } }, "sha512-eUIOd4u/p9994Nkv8Avn6r/xmS7D+RNmhmu6KGROefN3myLe3JfhSdimal2wDFe/h/OUNZ/LVVKMZrya9oEfKQ=="],
|
||||
|
||||
"@storybook/builder-vite": ["@storybook/builder-vite@9.1.20", "", { "dependencies": { "@storybook/csf-plugin": "9.1.20", "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^9.1.20", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-cdU3Q2/wEaT8h+mApFToRiF/0hYKH1eAkD0scQn67aODgp7xnkr0YHcdA+8w0Uxd2V7U8crV/cmT/HD0ELVOGw=="],
|
||||
|
||||
"@storybook/csf-plugin": ["@storybook/csf-plugin@9.1.20", "", { "dependencies": { "unplugin": "^1.3.1" }, "peerDependencies": { "storybook": "^9.1.20" } }, "sha512-HHgk50YQhML7mT01Mzf9N7lNMFHWN4HwwRP90kPT9Ct+Jhx7h3LBDbdmWjI96HwujcpY7eoYdTfpB1Sw8Z7nBQ=="],
|
||||
|
||||
"@storybook/global": ["@storybook/global@5.0.0", "", {}, "sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ=="],
|
||||
|
||||
"@storybook/icons": ["@storybook/icons@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta" } }, "sha512-hcFZIjW8yQz8O8//2WTIXylm5Xsgc+lW9ISLgUk1xGmptIJQRdlhVIXCpSyLrQaaRiyhQRaVg7l3BD9S216BHw=="],
|
||||
|
||||
"@storybook/react": ["@storybook/react@9.1.20", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/react-dom-shim": "9.1.20" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "storybook": "^9.1.20", "typescript": ">= 4.9.x" }, "optionalPeers": ["typescript"] }, "sha512-TJhqzggs7HCvLhTXKfx8HodnVq9YizsB2J31s9v6olU0UCxbCY+FYaCF+XdE8qUCyefGRZgHKzGBIczJ/q9e2g=="],
|
||||
|
||||
"@storybook/react-dom-shim": ["@storybook/react-dom-shim@9.1.20", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "storybook": "^9.1.20" } }, "sha512-UYdZavfPwHEqCKMqPssUOlyFVZiJExLxnSHwkICSZBmw3gxXJcp1aXWs7PvoZdWz2K4ztl3IcKErXXHeiY6w+A=="],
|
||||
@@ -2556,7 +2590,7 @@
|
||||
|
||||
"@types/get-folder-size": ["@types/get-folder-size@3.0.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-tSf/k7Undx6jKRwpChR9tl+0ZPf0BVwkjBRtJ5qSnz6iWm2ZRYMAS2MktC2u7YaTAFHmxpL/LBxI85M7ioJCSg=="],
|
||||
|
||||
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
|
||||
"@types/hast": ["@types/hast@3.0.5", "", { "dependencies": { "@types/unist": "*" } }, "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g=="],
|
||||
|
||||
"@types/istanbul-lib-coverage": ["@types/istanbul-lib-coverage@2.0.6", "", {}, "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w=="],
|
||||
|
||||
@@ -2578,6 +2612,8 @@
|
||||
|
||||
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
|
||||
|
||||
"@types/mdx": ["@types/mdx@2.0.14", "", {}, "sha512-T48PeuJtvLosNTPVhfnIp3i/n3a4g4Bad7YCq5k64D4u7NwDrAotikQ+5+sjtUvBmxCMlbo3dVL+C2dP0rWHzg=="],
|
||||
|
||||
"@types/mocha": ["@types/mocha@10.0.10", "", {}, "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q=="],
|
||||
|
||||
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
||||
@@ -2662,7 +2698,7 @@
|
||||
|
||||
"@typespec/ts-http-runtime": ["@typespec/ts-http-runtime@0.3.6", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" } }, "sha512-jIXhD0eWQ1JA6ln/5Dltyx22UxWNrw0hZmhy2rlv6m6KgF7kplHx3g0fzi09lNmTJQRR91OlemYp3xFnvDK9og=="],
|
||||
|
||||
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.2", "", {}, "sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA=="],
|
||||
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.3", "", {}, "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg=="],
|
||||
|
||||
"@upsetjs/venn.js": ["@upsetjs/venn.js@2.0.0", "", { "optionalDependencies": { "d3-selection": "^3.0.0", "d3-transition": "^3.0.1" } }, "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw=="],
|
||||
|
||||
@@ -2754,13 +2790,13 @@
|
||||
|
||||
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
|
||||
|
||||
"ai": ["ai@6.0.221", "", { "dependencies": { "@ai-sdk/gateway": "3.0.145", "@ai-sdk/provider": "3.0.13", "@ai-sdk/provider-utils": "4.0.37", "@opentelemetry/api": "^1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cB7qJbNTMuD5spdJEo+guejX0rkjhSQpc4PHITNB+iBFBnGYHLUZOM+uSeIkY4mS4sVVKBKm3kSQQoI5cUwU3g=="],
|
||||
"ai": ["ai@6.0.224", "", { "dependencies": { "@ai-sdk/gateway": "3.0.148", "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38", "@opentelemetry/api": "^1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-plo+hHwMANM+P6FjX8RN6W8c8so5NVZGwvDGCWGWwIbvI5tc0ZV1zhr9v/Kq78zyxKZWE50YCSYbqGiPFEhGww=="],
|
||||
|
||||
"ai-sdk-ollama": ["ai-sdk-ollama@3.8.8", "", { "dependencies": { "@ai-sdk/provider": "^3.0.10", "@ai-sdk/provider-utils": "^4.0.30", "jsonrepair": "^3.14.0", "ollama": "^0.6.3" }, "peerDependencies": { "ai": "^6.0.197" } }, "sha512-peWelPf6sVsRULQyYhfyu1dMZhwewRszsbQVfbuhNLflh+ncRXn6pe1BRE3NAcSsWd7JMRZAV5RzcuR3R9ZfaQ=="],
|
||||
|
||||
"ai-sdk-provider-claude-code": ["ai-sdk-provider-claude-code@3.5.1", "", { "dependencies": { "@ai-sdk/provider": "^3.0.0", "@ai-sdk/provider-utils": "^4.0.1", "@anthropic-ai/claude-agent-sdk": "^0.3.170" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-DxbOp3qIQTAhdvtynhW3Eq+NqAuU8UKVRVzBSdmVnCVMke8xC372Hy/j7FDalC5PkOOdiv7jI9yUzIk6vN1l6g=="],
|
||||
"ai-sdk-provider-claude-code": ["ai-sdk-provider-claude-code@3.5.3", "", { "dependencies": { "@ai-sdk/provider": "^3.0.0", "@ai-sdk/provider-utils": "^4.0.1", "@anthropic-ai/claude-agent-sdk": "^0.3.205" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-+uzxoU4X3ldzi14jbNvQ9SIitEmjXUM8wYwBC9xb7nfF0W03OB+EvzGrFMwkiE9mVSkFijXIDFzKBhMQ5zkqcg=="],
|
||||
|
||||
"ai-sdk-provider-codex-cli": ["ai-sdk-provider-codex-cli@1.2.2", "", { "dependencies": { "@ai-sdk/provider": "^3.0.0", "@ai-sdk/provider-utils": "^4.0.1", "jsonc-parser": "^3.3.1" }, "optionalDependencies": { "@openai/codex": "^0.130.0" }, "peerDependencies": { "zod": "^3.0.0 || ^4.0.0" } }, "sha512-hlIWo9KP7/hJaEjXZbxgjVO/FvMn3I+5RSht0PBp3GOBKVFkKeKlrIDvaA8Wi/nAYiAePESPem+fi1NVNfNQJA=="],
|
||||
"ai-sdk-provider-codex-cli": ["ai-sdk-provider-codex-cli@1.3.1", "", { "dependencies": { "@ai-sdk/provider": "^3.0.0", "@ai-sdk/provider-utils": "^4.0.1", "jsonc-parser": "^3.3.1" }, "optionalDependencies": { "@openai/codex": "^0.144.0" }, "peerDependencies": { "zod": "^3.0.0 || ^4.0.0" } }, "sha512-edTG3jkjF32ChGGctyivRi1Vyd/vLx6ktUuvlBTA5ennu4J3eKesLphKu+Lzv80nJQhrsqFXjM1aC1NfZc7elw=="],
|
||||
|
||||
"ai-sdk-provider-opencode-sdk": ["ai-sdk-provider-opencode-sdk@3.0.6", "", { "dependencies": { "@ai-sdk/provider": "^3.0.8", "@ai-sdk/provider-utils": "^4.0.15", "@opencode-ai/sdk": "^1.2.15" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CZ2I5z96HUdEKAT1ExhdxLE1bjD8efzT7I5GjBevWgwNGgYZjMPE7ZfQCJFVAzWqODwdTdxp7KSZjnmcd4h1Bw=="],
|
||||
|
||||
@@ -2826,6 +2862,8 @@
|
||||
|
||||
"aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="],
|
||||
|
||||
"axe-core": ["axe-core@4.12.1", "", {}, "sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA=="],
|
||||
|
||||
"axios": ["axios@1.16.1", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A=="],
|
||||
|
||||
"azure-devops-node-api": ["azure-devops-node-api@12.5.0", "", { "dependencies": { "tunnel": "0.0.6", "typed-rest-client": "^1.8.4" } }, "sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og=="],
|
||||
@@ -2848,7 +2886,7 @@
|
||||
|
||||
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.42", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q=="],
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.43", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ=="],
|
||||
|
||||
"basic-ftp": ["basic-ftp@5.3.1", "", {}, "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw=="],
|
||||
|
||||
@@ -2888,7 +2926,7 @@
|
||||
|
||||
"browser-stdout": ["browser-stdout@1.3.1", "", {}, "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw=="],
|
||||
|
||||
"browserslist": ["browserslist@4.28.5", "", { "dependencies": { "baseline-browser-mapping": "^2.10.42", "caniuse-lite": "^1.0.30001800", "electron-to-chromium": "^1.5.387", "node-releases": "^2.0.50", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-Cu2E6QejHWzuDMTkuwgpABFgDfZrXLQq5V13YOACZx4mFAG4IwGTbTfHPMr4WtxlHoXSM8FIuRwYYCz5XiabaQ=="],
|
||||
"browserslist": ["browserslist@4.28.6", "", { "dependencies": { "baseline-browser-mapping": "^2.10.42", "caniuse-lite": "^1.0.30001803", "electron-to-chromium": "^1.5.389", "node-releases": "^2.0.51", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw=="],
|
||||
|
||||
"buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
|
||||
|
||||
@@ -2932,7 +2970,7 @@
|
||||
|
||||
"camelize": ["camelize@1.0.1", "", {}, "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ=="],
|
||||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001803", "", {}, "sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg=="],
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001805", "", {}, "sha512-52noaS3DubycKSXaU30TwPGIp+POyQSUVa5jBEq3vkRkY0kjyb3LQgvhU6WGyCcyXqVLWO0Cw0Q6BSdD0kUfVA=="],
|
||||
|
||||
"case-anything": ["case-anything@2.1.13", "", {}, "sha512-zlOQ80VrQ2Ue+ymH5OuM/DlDq64mEm+B9UTdHULv5osUMD6HalNTblf2b1u/m6QecjsnOkBpqVZ+XPwIVsy7Ng=="],
|
||||
|
||||
@@ -3234,7 +3272,7 @@
|
||||
|
||||
"discord-interactions": ["discord-interactions@4.4.0", "", {}, "sha512-jjJx8iwAeJcj8oEauV43fue9lNqkf38fy60aSs2+G8D1nJmDxUIrk08o3h0F3wgwuBWWJUZO+X/VgfXsxpCiJA=="],
|
||||
|
||||
"discord.js": ["discord.js@14.26.4", "", { "dependencies": { "@discordjs/builders": "^1.14.1", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.2", "@discordjs/rest": "^2.6.1", "@discordjs/util": "^1.2.0", "@discordjs/ws": "^1.2.3", "@sapphire/snowflake": "3.5.3", "discord-api-types": "^0.38.40", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "magic-bytes.js": "^1.13.0", "tslib": "^2.6.3", "undici": "6.24.1" } }, "sha512-4oBp8tc6Kf8IDBwAHhbsMaAqx1b5fob9SNasZT7V6yyyUydoO5i5fGuX7TmvRtR+q/WgKRnRViRoAWnG7fNyvA=="],
|
||||
"discord.js": ["discord.js@14.26.5", "", { "dependencies": { "@discordjs/builders": "^1.14.1", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.2", "@discordjs/rest": "^2.6.1", "@discordjs/util": "^1.2.0", "@discordjs/ws": "^1.2.3", "@sapphire/snowflake": "3.5.3", "discord-api-types": "^0.38.48", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "magic-bytes.js": "^1.13.0", "tslib": "^2.6.3", "undici": "6.24.1" } }, "sha512-SGYgjiAs0o8ZzMC97XmFXKONyairJ9YzVda+LvoSKs9YYh2gPtQhY+liaV6H/w72jxYbu9ggiSqAXoF3FLOH4A=="],
|
||||
|
||||
"doctrine": ["doctrine@3.0.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w=="],
|
||||
|
||||
@@ -3248,7 +3286,7 @@
|
||||
|
||||
"domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
|
||||
|
||||
"dompurify": ["dompurify@3.4.11", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw=="],
|
||||
"dompurify": ["dompurify@3.4.12", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg=="],
|
||||
|
||||
"domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
|
||||
|
||||
@@ -3332,7 +3370,7 @@
|
||||
|
||||
"escodegen": ["escodegen@2.1.0", "", { "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2" }, "optionalDependencies": { "source-map": "~0.6.1" }, "bin": { "esgenerate": "bin/esgenerate.js", "escodegen": "bin/escodegen.js" } }, "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w=="],
|
||||
|
||||
"eslint": ["eslint@9.39.4", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ=="],
|
||||
"eslint": ["eslint@9.39.5", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.6", "@eslint/js": "9.39.5", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw=="],
|
||||
|
||||
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.1.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" } }, "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g=="],
|
||||
|
||||
@@ -3402,7 +3440,7 @@
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
"fast-equals": ["fast-equals@5.4.0", "", {}, "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw=="],
|
||||
"fast-equals": ["fast-equals@5.4.1", "", {}, "sha512-DjlFSM5Pk9cGcL0q5QXl66eGzx0N6szNgaswwc5ZphlBohjTVJSnGgI+rJVOgOi65qUoQnDZN4nDqi33udtydQ=="],
|
||||
|
||||
"fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="],
|
||||
|
||||
@@ -3622,7 +3660,7 @@
|
||||
|
||||
"highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="],
|
||||
|
||||
"hono": ["hono@4.12.28", "", {}, "sha512-YwUvVpSF7m1yOblFPrU3Hbo8XhPheBoiyfGuII6z19LnOr6JpDnyyp7LFNrfV56wS8tpvtBFGRISHN02pDdLOA=="],
|
||||
"hono": ["hono@4.12.29", "", {}, "sha512-1hNiRjawYrLq/4m3DQQjPGFg0VZkk4RjQJDff/excI6Dm9BiL75qxGrd7/c6YOxPdq6AscP3LiXhQ6fKFC1Waw=="],
|
||||
|
||||
"hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="],
|
||||
|
||||
@@ -3658,7 +3696,7 @@
|
||||
|
||||
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
|
||||
"ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
||||
"ignore": ["ignore@7.0.6", "", {}, "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw=="],
|
||||
|
||||
"image-q": ["image-q@4.0.0", "", { "dependencies": { "@types/node": "16.9.1" } }, "sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw=="],
|
||||
|
||||
@@ -4196,7 +4234,7 @@
|
||||
|
||||
"node-pty": ["node-pty@1.2.0-beta.11", "", { "dependencies": { "node-addon-api": "^7.1.0" } }, "sha512-THcUyu1WwdgoIyUvgXOZ70EOMXzheGa0q3tbEb5kUIfKgcpBJ+AJ9Q1kq0bKtYmQzr77usXiTORZTLmAUQlnoQ=="],
|
||||
|
||||
"node-releases": ["node-releases@2.0.50", "", {}, "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg=="],
|
||||
"node-releases": ["node-releases@2.0.51", "", {}, "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ=="],
|
||||
|
||||
"node-rsa": ["node-rsa@1.1.1", "", { "dependencies": { "asn1": "^0.2.4" } }, "sha512-Jd4cvbJMryN21r5HgxQOpMEqv+ooke/korixNNK3mGqfGJmy0M77WDDzo/05969+OkMy3XW1UuZsSmW9KQm7Fw=="],
|
||||
|
||||
@@ -4244,7 +4282,7 @@
|
||||
|
||||
"open-graph-scraper": ["open-graph-scraper@6.12.0", "", { "dependencies": { "chardet": "^2.2.0", "cheerio": "^1.2.0", "iconv-lite": "^0.7.2", "undici": "^7.28.0" } }, "sha512-x0fS3eHxdCox+rFBhQSVe+qBznSPn1pspp8A4BoaVEkiECZEwagEb8z06swLfaFFE2gefj1BvEBeJmdeGTDnYw=="],
|
||||
|
||||
"openai": ["openai@6.45.0", "", { "peerDependencies": { "@aws-sdk/credential-provider-node": ">=3.972.0 <4", "@smithy/hash-node": ">=4.3.0 <5", "@smithy/signature-v4": ">=5.4.0 <6", "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["@aws-sdk/credential-provider-node", "@smithy/hash-node", "@smithy/signature-v4", "ws", "zod"] }, "sha512-5DQVNErssk0afNpTTHUm/qZPU4iKR9OYdNid8Ib4puq4gHNNvGWZht2zY4h9a8JMF949Ik6m8gQutllVPbjdnw=="],
|
||||
"openai": ["openai@6.46.0", "", { "peerDependencies": { "@aws-sdk/credential-provider-node": ">=3.972.0 <4", "@smithy/hash-node": ">=4.3.0 <5", "@smithy/signature-v4": ">=5.4.0 <6", "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["@aws-sdk/credential-provider-node", "@smithy/hash-node", "@smithy/signature-v4", "ws", "zod"] }, "sha512-DFg6jEPT2RO+oAyXtddeUJU8zkGy1OQ1AjGzNIJUMQG03TTqvCpy9tBpQ+2VVVnvrl3E56F8GEin2JYtWpITtA=="],
|
||||
|
||||
"opentui-spinner": ["opentui-spinner@0.0.6", "", { "dependencies": { "cli-spinners": "^3.3.0" }, "peerDependencies": { "@opentui/core": "^0.1.49", "@opentui/react": "^0.1.49", "@opentui/solid": "^0.1.49", "typescript": "^5" }, "optionalPeers": ["@opentui/react", "@opentui/solid"] }, "sha512-xupLOeVQEAXEvVJCvHkfX6fChDWmJIPHe5jyUrVb8+n4XVTX8mBNhitFfB9v2ZbkC1H2UwPab/ElePHoW37NcA=="],
|
||||
|
||||
@@ -4370,15 +4408,15 @@
|
||||
|
||||
"points-on-path": ["points-on-path@0.2.1", "", { "dependencies": { "path-data-parser": "0.1.0", "points-on-curve": "0.2.0" } }, "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g=="],
|
||||
|
||||
"postcss": ["postcss@8.5.16", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg=="],
|
||||
"postcss": ["postcss@8.5.17", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-J7EF+8X+CzRPaJPOv9Ck2wNWJvGnnl3PcNPAdGg6GTLjyVpyQ0yATMSXRFRV01BviT/9Gwuc3rjEyJbDJG9a4w=="],
|
||||
|
||||
"postcss-selector-parser": ["postcss-selector-parser@7.1.4", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg=="],
|
||||
|
||||
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
|
||||
|
||||
"posthog-js": ["posthog-js@1.399.0", "", { "dependencies": { "@posthog/core": "^1.40.0", "@posthog/types": "^1.393.0", "core-js": "^3.49.0", "dompurify": "^3.3.2", "fflate": "^0.4.8", "preact": "^10.29.3", "query-selector-shadow-dom": "^1.0.1", "web-vitals": "^5.3.0" } }, "sha512-8l+uZJZM3+OAc0D0+iLBMDRVfWF9s26Rt0jv8EC3kMJcA/9oyOets0zDgqIZk2TTfUs3H96ycLE6Lwj1wWG16Q=="],
|
||||
"posthog-js": ["posthog-js@1.399.2", "", { "dependencies": { "@posthog/core": "^1.40.1", "@posthog/types": "^1.393.0", "core-js": "^3.49.0", "dompurify": "^3.3.2", "fflate": "^0.4.8", "preact": "^10.29.3", "query-selector-shadow-dom": "^1.0.1", "web-vitals": "^5.3.0" } }, "sha512-xcvrGEgUYtIVcWPRlVfc/NkMo0IP9nwnM/dzJIAzjDOSewkdDk/9T4Vz1+gooEhXdQCPfG44jSK95mVJsoySqA=="],
|
||||
|
||||
"posthog-node": ["posthog-node@5.40.0", "", { "dependencies": { "@posthog/core": "^1.39.6" }, "peerDependencies": { "rxjs": "^7.0.0" }, "optionalPeers": ["rxjs"] }, "sha512-DrLfHuauO0W6qruF80iqr5JdmLysef74XzOB4eh36oRLRhxCySLraTqsi2Pj161LZnp9/JNdRDxwT8ei8VK2YA=="],
|
||||
"posthog-node": ["posthog-node@5.41.0", "", { "dependencies": { "@posthog/core": "^1.40.2" }, "peerDependencies": { "rxjs": "^7.0.0" }, "optionalPeers": ["rxjs"] }, "sha512-jOkX6THOr5WD+FGUEaTxekas8c7NOC3TqJ2Byfe2KMimQdL/F/osz17uSbkNzR4V9WFZoe8YaGP3Xp0EUpPKGg=="],
|
||||
|
||||
"powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="],
|
||||
|
||||
@@ -4654,7 +4692,7 @@
|
||||
|
||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||
|
||||
"shell-quote": ["shell-quote@1.9.0", "", {}, "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA=="],
|
||||
"shell-quote": ["shell-quote@1.10.0", "", {}, "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA=="],
|
||||
|
||||
"shiki": ["shiki@4.3.1", "", { "dependencies": { "@shikijs/core": "4.3.1", "@shikijs/engine-javascript": "4.3.1", "@shikijs/engine-oniguruma": "4.3.1", "@shikijs/langs": "4.3.1", "@shikijs/themes": "4.3.1", "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw=="],
|
||||
|
||||
@@ -4808,7 +4846,7 @@
|
||||
|
||||
"symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="],
|
||||
|
||||
"systeminformation": ["systeminformation@5.31.15", "", { "os": "!aix", "bin": { "systeminformation": "lib/cli.js" } }, "sha512-7mqCtD28TK5dVdLAQONVa/Do/NBgMH2dxqf49nh6DIKoEWuDg6tkgGBP+dN22VEJVPZa/QqiHomhWNRn4WUNTQ=="],
|
||||
"systeminformation": ["systeminformation@5.31.16", "", { "os": "!aix", "bin": { "systeminformation": "lib/cli.js" } }, "sha512-37NsFaeaqwYxanLgdJAGWj8OXIRgvSBsDXGhlC0uUoHyl9nf4HrNsZjqtQ9Gc99bj9FQpffzYccPVuQ6gpjfqg=="],
|
||||
|
||||
"tabbable": ["tabbable@6.5.0", "", {}, "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA=="],
|
||||
|
||||
@@ -5046,7 +5084,7 @@
|
||||
|
||||
"victory-vendor": ["victory-vendor@36.9.2", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ=="],
|
||||
|
||||
"vite": ["vite@8.1.3", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.16", "rolldown": "~1.1.3", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA=="],
|
||||
"vite": ["vite@8.1.4", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.5", "postcss": "^8.5.16", "rolldown": "~1.1.4", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ=="],
|
||||
|
||||
"vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="],
|
||||
|
||||
@@ -5194,6 +5232,8 @@
|
||||
|
||||
"@cline/code/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
"@cline/ui/vite": ["vite@7.3.6", "", { "dependencies": { "esbuild": "^0.27.0 || ^0.28.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg=="],
|
||||
|
||||
"@cline/vscode-rollout/@types/node": ["@types/node@20.19.43", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA=="],
|
||||
|
||||
"@cline/vscode-rollout/@types/vscode": ["@types/vscode@1.84.0", "", {}, "sha512-lCGOSrhT3cL+foUEqc8G1PVZxoDbiMmxgnUZZTEnHF4mC47eKAUtBGAuMLY6o6Ua8PAuNCoKXbqPmJd1JYnQfg=="],
|
||||
|
||||
@@ -50,6 +50,7 @@ ClinePass includes the following models, tested and benchmarked for coding agent
|
||||
| Model | Model ID |
|
||||
|-------|------------|
|
||||
| GLM-5.2 | `cline-pass/glm-5.2` |
|
||||
| Kimi K3 | `cline-pass/kimi-k3` |
|
||||
| Kimi K2.7 Code | `cline-pass/kimi-k2.7-code` |
|
||||
| Kimi K2.6 | `cline-pass/kimi-k2.6` |
|
||||
| DeepSeek V4 Pro | `cline-pass/deepseek-v4-pro` |
|
||||
@@ -91,6 +92,7 @@ ClinePass is a flat monthly subscription, so you are not charged the individual
|
||||
| Model | Input | Output | Cached Read | Cached Write |
|
||||
|-------|-------|--------|-------------|--------------|
|
||||
| GLM-5.2 | $1.40 | $4.40 | $0.26 | - |
|
||||
| Kimi K3 | $3.00 | $15.00 | $0.30 | - |
|
||||
| Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - |
|
||||
| Kimi K2.6 | $0.95 | $4.00 | $0.16 | - |
|
||||
| DeepSeek V4 Pro | $1.74 | $3.48 | $0.0145 | - |
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
# Cline SDK Changelog
|
||||
|
||||
## 0.0.65
|
||||
|
||||
- Claude Code and Codex provider SDKs are now optional peer dependencies loaded on demand, dramatically cutting install size
|
||||
- Added Kimi K3 to the bundled ClinePass model fallback
|
||||
- Runs now retry once after refreshing expired OAuth credentials
|
||||
- Team runs: the spawn tool is no longer exposed to teammate agents
|
||||
- Team runs: errored teammate runs now report as failed instead of completed
|
||||
- Improved shell-command parsing to fix a Windows shell mismatch
|
||||
- New `@cline/ui` agent chat components with Storybook and npm packaging
|
||||
- Updated the bundled model catalog
|
||||
|
||||
## 0.0.64
|
||||
|
||||
- Improved max output token handling across providers (gateway routing, OpenAI vendor, and reasoning models)
|
||||
- Frontmatter and user-instruction files that start with a UTF-8 byte order mark (e.g. saved by Windows editors) now parse correctly
|
||||
|
||||
## 0.0.63
|
||||
|
||||
- The session runtime now emits `task.mistake_limit_reached` telemetry when the consecutive-mistake limit is hit, so every host (CLI, VS Code extension, hub daemon) captures it — including auto-stops when no host prompt is configured
|
||||
|
||||
## 0.0.62
|
||||
|
||||
- Fixed Ollama native API routing so context window and timeout settings work again
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
"!@cline/core/telemetry",
|
||||
"!@cline/core/rpc",
|
||||
"!@cline/shared/browser",
|
||||
"!@cline/shared/node",
|
||||
"!@cline/shared/types",
|
||||
"!@cline/shared/storage",
|
||||
"!@cline/shared/db"
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
type AgentToolContext,
|
||||
ClineCore,
|
||||
createTool,
|
||||
stripUtf8Bom,
|
||||
} from "@cline/core";
|
||||
import YAML from "yaml";
|
||||
import { z } from "zod";
|
||||
@@ -179,6 +180,9 @@ function parseFrontmatter(md: string): {
|
||||
data: Record<string, unknown>;
|
||||
body: string;
|
||||
} {
|
||||
// stripUtf8Bom keeps the frontmatter match below working for files saved with a leading
|
||||
// UTF-8 BOM (see cline/cline#12151).
|
||||
md = stripUtf8Bom(md);
|
||||
const m = md.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
||||
if (!m) return { data: {}, body: md.trim() };
|
||||
try {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@cline/agents",
|
||||
"version": "0.0.62",
|
||||
"version": "0.0.65",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/cline/cline",
|
||||
|
||||
@@ -69,6 +69,50 @@ describe("AgentRuntime", () => {
|
||||
expect(model.requests).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("fails a turn that hits the model output token limit before completion", async () => {
|
||||
const logger = {
|
||||
debug: vi.fn(),
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
};
|
||||
const model = new ScriptedModel([
|
||||
() => [
|
||||
{ type: "reasoning-delta", text: "thinking..." },
|
||||
{ type: "finish", reason: "max-tokens" },
|
||||
],
|
||||
]);
|
||||
const runtime = new AgentRuntime({ model, logger });
|
||||
|
||||
const result = await runtime.run("Hi");
|
||||
|
||||
expect(result.status).toBe("failed");
|
||||
expect(result.error?.message).toContain("maximum output token limit");
|
||||
expect(model.requests).toHaveLength(1);
|
||||
expect(result.messages).toHaveLength(2);
|
||||
expect(result.messages.at(-1)).toMatchObject({
|
||||
role: "assistant",
|
||||
content: [{ type: "reasoning", text: "thinking..." }],
|
||||
});
|
||||
expect(logger.log).toHaveBeenCalledWith(
|
||||
"Agent loop caught error",
|
||||
expect.objectContaining({
|
||||
severity: "error",
|
||||
status: "failed",
|
||||
errorMessage: expect.stringContaining("maximum output token limit"),
|
||||
iteration: 1,
|
||||
assistantContentPartCount: 1,
|
||||
}),
|
||||
);
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
"Agent run failed",
|
||||
expect.objectContaining({
|
||||
error: expect.objectContaining({
|
||||
message: expect.stringContaining("maximum output token limit"),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not persist an empty assistant message when the model stream fails", async () => {
|
||||
const model = new ScriptedModel([
|
||||
() => [{ type: "finish", reason: "error", error: "upstream failed" }],
|
||||
@@ -1725,6 +1769,14 @@ describe("AgentRuntime", () => {
|
||||
|
||||
expect(result.status).toBe("failed");
|
||||
expect(events).toContain("run-failed");
|
||||
expect(logger.log).toHaveBeenCalledWith(
|
||||
"Agent loop caught error",
|
||||
expect.objectContaining({
|
||||
severity: "error",
|
||||
status: "failed",
|
||||
errorMessage: "model failed",
|
||||
}),
|
||||
);
|
||||
expect(logger.error).toHaveBeenCalled();
|
||||
expect(telemetry.capture).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -34,6 +34,9 @@ import {
|
||||
} from "@cline/shared";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
const MAX_TOKENS_INCOMPLETE_TURN_MESSAGE =
|
||||
"Model reached the maximum output token limit before completing the turn";
|
||||
|
||||
// Local `createUID` helper. The clinee source imports this from
|
||||
// `@cline/shared` (see `packages/shared/dist/identifier.ts`), but
|
||||
// sdk-re's shared package does not expose it yet. Inlining here keeps
|
||||
@@ -645,6 +648,9 @@ export class AgentRuntime {
|
||||
finishReason,
|
||||
});
|
||||
|
||||
if (finishReason === "max-tokens" && toolCalls.length === 0) {
|
||||
throw new Error(MAX_TOKENS_INCOMPLETE_TURN_MESSAGE);
|
||||
}
|
||||
if (finishReason === "error" && toolCalls.length === 0) {
|
||||
throw new Error(this.state.lastError ?? "Model stream failed");
|
||||
}
|
||||
@@ -718,23 +724,33 @@ export class AgentRuntime {
|
||||
const normalized =
|
||||
error instanceof Error ? error : new Error(String(error));
|
||||
const isControlledStop = normalized instanceof ControlledStopError;
|
||||
const status =
|
||||
this.abortController.signal.aborted || isControlledStop
|
||||
? "aborted"
|
||||
: "failed";
|
||||
const isAborted = this.abortController.signal.aborted || isControlledStop;
|
||||
const status = isAborted ? "aborted" : "failed";
|
||||
this.state.status = status;
|
||||
this.state.lastError = normalized.message;
|
||||
const lastAssistantMessage = this.findLastAssistantMessage();
|
||||
const result: AgentRunResult = {
|
||||
agentId: this.state.agentId,
|
||||
agentRole: this.state.agentRole,
|
||||
runId: this.state.runId ?? createUID("run"),
|
||||
status,
|
||||
iterations: this.state.iteration,
|
||||
outputText: textFromMessage(this.findLastAssistantMessage()),
|
||||
outputText: textFromMessage(lastAssistantMessage),
|
||||
messages: cloneMessages(this.state.messages),
|
||||
usage: cloneUsage(this.state.usage),
|
||||
error: status === "failed" ? normalized : undefined,
|
||||
};
|
||||
this.config.logger?.log?.("Agent loop caught error", {
|
||||
severity: status === "failed" ? "error" : "warn",
|
||||
agentId: this.state.agentId,
|
||||
agentRole: this.state.agentRole,
|
||||
runId: result.runId,
|
||||
status,
|
||||
iteration: this.state.iteration,
|
||||
errorName: normalized.name,
|
||||
errorMessage: normalized.message,
|
||||
assistantContentPartCount: lastAssistantMessage?.content.length ?? 0,
|
||||
});
|
||||
await this.callAfterRunHooks(result);
|
||||
if (status === "failed") {
|
||||
await this.emit({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@cline/core",
|
||||
"description": "Cline Core SDK for Node Runtime",
|
||||
"version": "0.0.62",
|
||||
"version": "0.0.65",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/cline/cline",
|
||||
|
||||
@@ -86,6 +86,24 @@ ${content}`);
|
||||
expect(updateSkillMarkdownEnabledState(content, true)).toBe(content);
|
||||
});
|
||||
|
||||
// Regression test for https://github.com/cline/cline/issues/12151: a leading UTF-8 BOM
|
||||
// (e.g. saved by Windows Notepad's "UTF-8 with BOM" encoding) must not prevent frontmatter
|
||||
// from being recognized when toggling a skill's enabled state.
|
||||
it("disables a skill whose content starts with a UTF-8 BOM", () => {
|
||||
const content = `\uFEFF---
|
||||
name: code-review
|
||||
description: Review code carefully
|
||||
---
|
||||
First line.`;
|
||||
|
||||
const updated = updateSkillMarkdownEnabledState(content, false);
|
||||
const parsed = parseSkillConfigFromMarkdown(updated, "fallback");
|
||||
|
||||
expect(parsed.disabled).toBe(true);
|
||||
expect(parsed.frontmatter.name).toBe("code-review");
|
||||
expect(parsed.frontmatter.description).toBe("Review code carefully");
|
||||
});
|
||||
|
||||
it("writes toggled content and returns the resulting state", async () => {
|
||||
const tempRoot = await mkdtemp(join(tmpdir(), "core-skill-toggle-"));
|
||||
tempRoots.push(tempRoot);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { stripUtf8Bom } from "@cline/shared";
|
||||
import YAML from "yaml";
|
||||
|
||||
export interface ToggleSkillFrontmatterOptions {
|
||||
@@ -19,10 +20,15 @@ interface MarkdownFrontmatterParts {
|
||||
}
|
||||
|
||||
function parseMarkdownFrontmatter(content: string): MarkdownFrontmatterParts {
|
||||
// Strip a leading UTF-8 BOM (e.g. added by Windows Notepad's "UTF-8 with BOM" encoding),
|
||||
// which Node's `utf-8` decoding does not strip on its own. Without this the frontmatter
|
||||
// regex below never matches a file that starts with "\uFEFF---" (see cline/cline#12151).
|
||||
const normalizedContent = stripUtf8Bom(content);
|
||||
|
||||
const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
||||
const match = content.match(frontmatterRegex);
|
||||
const match = normalizedContent.match(frontmatterRegex);
|
||||
if (!match) {
|
||||
return { data: {}, body: content, hadFrontmatter: false };
|
||||
return { data: {}, body: normalizedContent, hadFrontmatter: false };
|
||||
}
|
||||
|
||||
const [, yamlContent, body] = match;
|
||||
|
||||
@@ -137,6 +137,23 @@ Document rollout and rollback steps.`,
|
||||
expect(workflow.disabled).toBe(true);
|
||||
});
|
||||
|
||||
// Regression test for https://github.com/cline/cline/issues/12151: a leading UTF-8 BOM
|
||||
// (e.g. saved by Windows Notepad's "UTF-8 with BOM" encoding) must not prevent frontmatter
|
||||
// from being recognized.
|
||||
it("parses markdown frontmatter when the content starts with a UTF-8 BOM", () => {
|
||||
const skill = parseSkillConfigFromMarkdown(
|
||||
`\uFEFF---
|
||||
name: my-skill
|
||||
description: A test skill
|
||||
---
|
||||
This is a test skill.`,
|
||||
"fallback",
|
||||
);
|
||||
expect(skill.name).toBe("my-skill");
|
||||
expect(skill.description).toBe("A test skill");
|
||||
expect(skill.instructions).toBe("This is a test skill.");
|
||||
});
|
||||
|
||||
it("emits typed events for skills, rules, and workflows in one watcher", async () => {
|
||||
const tempRoot = await mkdtemp(
|
||||
join(tmpdir(), "core-user-instructions-loader-"),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { readdir, readFile, stat } from "node:fs/promises";
|
||||
import { basename, dirname, extname, join, resolve } from "node:path";
|
||||
import { stripUtf8Bom } from "@cline/shared";
|
||||
import {
|
||||
AGENTS_RULES_FILE_NAME,
|
||||
RULES_CONFIG_DIRECTORY_NAME,
|
||||
@@ -193,10 +194,15 @@ async function discoverManagedPluginRoots(
|
||||
function parseMarkdownFrontmatter(
|
||||
content: string,
|
||||
): ParseMarkdownFrontmatterResult {
|
||||
// Strip a leading UTF-8 BOM (e.g. added by Windows Notepad's "UTF-8 with BOM" encoding),
|
||||
// which Node's `utf-8` decoding does not strip on its own. Without this the frontmatter
|
||||
// regex below never matches a file that starts with "\uFEFF---" (see cline/cline#12151).
|
||||
const normalizedContent = stripUtf8Bom(content);
|
||||
|
||||
const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
||||
const match = content.match(frontmatterRegex);
|
||||
const match = normalizedContent.match(frontmatterRegex);
|
||||
if (!match) {
|
||||
return { data: {}, body: content, hadFrontmatter: false };
|
||||
return { data: {}, body: normalizedContent, hadFrontmatter: false };
|
||||
}
|
||||
|
||||
const [, yamlContent, body] = match;
|
||||
@@ -211,7 +217,7 @@ function parseMarkdownFrontmatter(
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
data: {},
|
||||
body: content,
|
||||
body: normalizedContent,
|
||||
hadFrontmatter: true,
|
||||
parseError: message,
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
getToolContextTelemetry,
|
||||
} from "../../services/telemetry/tool-context";
|
||||
import {
|
||||
buildRunCommandsDescription,
|
||||
createDefaultTools,
|
||||
createReadFilesTool,
|
||||
createSearchTool,
|
||||
@@ -480,6 +481,71 @@ describe("default apply_patch tool", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("run_commands tool description", () => {
|
||||
it("names PowerShell with ';' sequencing for PowerShell shells", () => {
|
||||
const description = buildRunCommandsDescription("powershell", true);
|
||||
expect(description).toContain("Commands run through PowerShell");
|
||||
expect(description).toContain("use ';' to sequence commands");
|
||||
expect(description).toContain("in Windows environment");
|
||||
});
|
||||
|
||||
it("names cmd.exe with '&&' sequencing for cmd shells", () => {
|
||||
const description = buildRunCommandsDescription("cmd", true);
|
||||
expect(description).toContain("Commands run through cmd.exe");
|
||||
expect(description).toContain("use '&&' to sequence commands");
|
||||
expect(description).not.toContain("PowerShell");
|
||||
});
|
||||
|
||||
it("describes WSL bash with the /mnt working-directory mapping", () => {
|
||||
const description = buildRunCommandsDescription("wsl", true);
|
||||
expect(description).toContain("bash in WSL");
|
||||
expect(description).toContain("/mnt/<drive>");
|
||||
expect(description).not.toContain("PowerShell");
|
||||
});
|
||||
|
||||
it("notes the Windows host for POSIX shells on Windows only", () => {
|
||||
const onWindows = buildRunCommandsDescription("posix", true);
|
||||
expect(onWindows).toContain("POSIX (bash-compatible) shell on Windows");
|
||||
expect(onWindows).not.toContain("PowerShell");
|
||||
|
||||
const onUnix = buildRunCommandsDescription("posix", false);
|
||||
expect(onUnix).not.toContain("Windows");
|
||||
expect(onUnix).toContain("grep/head/tail");
|
||||
});
|
||||
|
||||
it("derives the createShellTool description from config.shell", () => {
|
||||
const posixTool = createShellTool(async () => "ok", {
|
||||
shell: "/bin/bash",
|
||||
});
|
||||
expect(posixTool.description).toContain(
|
||||
"Run non-interactive shell commands",
|
||||
);
|
||||
expect(posixTool.description).not.toContain("PowerShell");
|
||||
|
||||
const cmdTool = createShellTool(async () => "ok", {
|
||||
shell: "C:\\Windows\\System32\\cmd.exe",
|
||||
});
|
||||
expect(cmdTool.description).toContain("Commands run through cmd.exe");
|
||||
});
|
||||
|
||||
it("re-derives the description on each read when config.shell is a provider", () => {
|
||||
let shell = "/bin/bash";
|
||||
const tool = createShellTool(async () => "ok", {
|
||||
shell: () => shell,
|
||||
});
|
||||
expect(tool.description).not.toContain("PowerShell");
|
||||
|
||||
shell = "powershell.exe";
|
||||
expect(tool.description).toContain("Commands run through PowerShell");
|
||||
|
||||
// The property must survive the shallow copy the runtime performs when
|
||||
// building AgentToolDefinitions for a model request.
|
||||
shell = "cmd.exe";
|
||||
const definition = { ...tool };
|
||||
expect(definition.description).toContain("Commands run through cmd.exe");
|
||||
});
|
||||
});
|
||||
|
||||
describe("default run_commands tool", () => {
|
||||
function createTelemetryStub(): ITelemetryService {
|
||||
return {
|
||||
|
||||
@@ -8,6 +8,9 @@ import {
|
||||
type AgentTool,
|
||||
type AgentToolContext,
|
||||
createTool,
|
||||
getDefaultShell,
|
||||
getShellKind,
|
||||
type ShellKind,
|
||||
validateWithZod,
|
||||
zodToJsonSchema,
|
||||
} from "@cline/shared";
|
||||
@@ -395,15 +398,64 @@ const RUN_COMMANDS_SHARED_INSTRUCTIONS =
|
||||
"Use for listing files, checking git status, running builds, executing tests, etc. " +
|
||||
"Commands must be non-interactive. Commands that require follow-up input like pagers should be skipped or used with supported flags/env (e.g. git --no-pager, --non-interactive) to bypass the interaction steps. ";
|
||||
|
||||
/**
|
||||
* Build the run_commands tool description for the shell that will actually
|
||||
* execute the commands. The shell kind decides the syntax guidance (quoting,
|
||||
* sequencing, heredocs), and isWindows adds environment context for POSIX
|
||||
* shells running on a Windows host (e.g. Git Bash).
|
||||
*/
|
||||
export function buildRunCommandsDescription(
|
||||
shellKind: ShellKind,
|
||||
isWindows: boolean,
|
||||
): string {
|
||||
if (shellKind === "powershell" || shellKind === "cmd") {
|
||||
const shellName = shellKind === "powershell" ? "PowerShell" : "cmd.exe";
|
||||
const sequencingOperator = shellKind === "powershell" ? "';'" : "'&&'";
|
||||
return (
|
||||
"Run non-interactive shell commands from the root of the workspace in Windows environment. " +
|
||||
RUN_COMMANDS_SHARED_INSTRUCTIONS +
|
||||
`Output beyond ~${Math.round(MAX_COMMAND_OUTPUT_CHARS / 1000)}k characters is middle-truncated (start and end preserved); filter output when you need specific sections. ` +
|
||||
`Commands run through ${shellName}; quote paths and arguments for ${shellName} and use ${sequencingOperator} to sequence commands. ` +
|
||||
"Include multiple commands in the same call when they are independent and safe to run concurrently. When independent reads, searches, or edits are also needed, call those tools in the same response."
|
||||
);
|
||||
}
|
||||
|
||||
const environmentNote =
|
||||
shellKind === "wsl"
|
||||
? "Commands run through bash in WSL (wsl.exe); the Windows working directory is mounted under /mnt/<drive>. "
|
||||
: isWindows
|
||||
? "Commands run through a POSIX (bash-compatible) shell on Windows. "
|
||||
: "";
|
||||
return (
|
||||
"Run non-interactive shell commands from the root of the workspace. " +
|
||||
RUN_COMMANDS_SHARED_INSTRUCTIONS +
|
||||
environmentNote +
|
||||
"Commands should be properly shell-escaped and targeted to avoid error or timeout. Include multiple commands in the same call when they are independent complete shell commands and safe to run concurrently; multiline scripts and heredocs must be a single command string. When independent reads, searches, or edits are also needed, call those tools in the same response. " +
|
||||
`Output beyond ~${Math.round(MAX_COMMAND_OUTPUT_CHARS / 1000)}k characters is middle-truncated (start and end preserved); pipe through grep/head/tail when you need specific sections of large output. ` +
|
||||
"For long-running commands, run them in background and redirect output to a tmp file that you can read from later."
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the run_commands shell tool for the current platform.
|
||||
*
|
||||
* This preserves the SDK's platform-specific prompting/schema choices while
|
||||
* exposing a single generic shell-tool factory for host integrations.
|
||||
* exposing a single generic shell-tool factory for host integrations. Pass
|
||||
* config.shell (matching the executor's shell) so the syntax guidance in the
|
||||
* tool description matches the shell that actually runs the commands.
|
||||
*
|
||||
* config.shell may be a provider function instead of a string. The runtime
|
||||
* reads `description` when building each model request, so a provider is
|
||||
* consulted at that boundary: a shell change made while the model is
|
||||
* generating does not affect the request in flight, and the next request
|
||||
* names the new shell. The provider must return the shell the executor will
|
||||
* use for tool calls issued by that next request.
|
||||
*/
|
||||
export function createShellTool(
|
||||
executor: ShellExecutor,
|
||||
config: Pick<DefaultToolsConfig, "cwd" | "bashTimeoutMs"> = {},
|
||||
config: Pick<DefaultToolsConfig, "cwd" | "bashTimeoutMs"> & {
|
||||
shell?: string | (() => string);
|
||||
} = {},
|
||||
): AgentTool<unknown, ToolOperationResult[]> {
|
||||
const timeoutMs = config.bashTimeoutMs ?? 30000;
|
||||
const timeoutSource =
|
||||
@@ -412,19 +464,17 @@ export function createShellTool(
|
||||
: "configured_setting";
|
||||
const cwd = config.cwd ?? process.cwd();
|
||||
const isWindows = process.platform === "win32";
|
||||
const configShell = config.shell;
|
||||
const resolveShell =
|
||||
typeof configShell === "function"
|
||||
? configShell
|
||||
: () => configShell ?? getDefaultShell(process.platform);
|
||||
const describe = () =>
|
||||
buildRunCommandsDescription(getShellKind(resolveShell()), isWindows);
|
||||
|
||||
return createTool<unknown, ToolOperationResult[]>({
|
||||
const tool = createTool<unknown, ToolOperationResult[]>({
|
||||
name: "run_commands",
|
||||
description: isWindows
|
||||
? "Run non-interactive shell commands from the root of the workspace in Windows environment. " +
|
||||
RUN_COMMANDS_SHARED_INSTRUCTIONS +
|
||||
`Output beyond ~${Math.round(MAX_COMMAND_OUTPUT_CHARS / 1000)}k characters is middle-truncated (start and end preserved); filter output when you need specific sections. ` +
|
||||
"Commands run through PowerShell; quote paths and arguments for PowerShell and use ';' to sequence commands. Include multiple commands in the same call when they are independent and safe to run concurrently. When independent reads, searches, or edits are also needed, call those tools in the same response."
|
||||
: "Run non-interactive shell commands from the root of the workspace. " +
|
||||
RUN_COMMANDS_SHARED_INSTRUCTIONS +
|
||||
"Commands should be properly shell-escaped and targeted to avoid error or timeout. Include multiple commands in the same call when they are independent complete shell commands and safe to run concurrently; multiline scripts and heredocs must be a single command string. When independent reads, searches, or edits are also needed, call those tools in the same response. " +
|
||||
`Output beyond ~${Math.round(MAX_COMMAND_OUTPUT_CHARS / 1000)}k characters is middle-truncated (start and end preserved); pipe through grep/head/tail when you need specific sections of large output. ` +
|
||||
"For long-running commands, run them in background and redirect output to a tmp file that you can read from later.",
|
||||
description: describe(),
|
||||
inputSchema: zodToJsonSchema(RunCommandsInputSchema),
|
||||
timeoutMs: timeoutMs * 2,
|
||||
retryable: false,
|
||||
@@ -443,6 +493,17 @@ export function createShellTool(
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
if (typeof configShell === "function") {
|
||||
// The runtime rebuilds tool definitions from this property for every
|
||||
// model request, so a getter re-derives the description at exactly the
|
||||
// send-to-model boundary. AgentTool consumers only read `description`.
|
||||
Object.defineProperty(tool, "description", {
|
||||
get: describe,
|
||||
enumerable: true,
|
||||
});
|
||||
}
|
||||
return tool;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import type { ChildProcessWithoutNullStreams } from "node:child_process";
|
||||
import { EventEmitter } from "node:events";
|
||||
import type { AgentToolContext } from "@cline/shared";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const spawn = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("node:child_process", () => ({ spawn }));
|
||||
|
||||
import { createBuiltinTools } from "./index";
|
||||
|
||||
const context: AgentToolContext = {
|
||||
agentId: "agent-1",
|
||||
conversationId: "conversation-1",
|
||||
iteration: 1,
|
||||
};
|
||||
|
||||
function createSuccessfulChildProcess(): ChildProcessWithoutNullStreams {
|
||||
const child = Object.assign(new EventEmitter(), {
|
||||
stdout: new EventEmitter(),
|
||||
stderr: new EventEmitter(),
|
||||
stdin: new EventEmitter(),
|
||||
pid: 123,
|
||||
kill: vi.fn(() => true),
|
||||
});
|
||||
queueMicrotask(() => child.emit("close", 0));
|
||||
return child as unknown as ChildProcessWithoutNullStreams;
|
||||
}
|
||||
|
||||
async function executeRunCommands(
|
||||
options: Parameters<typeof createBuiltinTools>[0],
|
||||
) {
|
||||
const tool = createBuiltinTools(options).find(
|
||||
(candidate) => candidate.name === "run_commands",
|
||||
);
|
||||
if (!tool) {
|
||||
throw new Error("Expected run_commands tool");
|
||||
}
|
||||
|
||||
await tool.execute({ commands: ["echo ok"] }, context);
|
||||
return tool;
|
||||
}
|
||||
|
||||
describe("createBuiltinTools shell configuration", () => {
|
||||
beforeEach(() => {
|
||||
spawn.mockReset();
|
||||
spawn.mockImplementation(() => createSuccessfulChildProcess());
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "top-level shell",
|
||||
options: { shell: "cmd.exe" },
|
||||
expectedShell: "cmd.exe",
|
||||
expectedDescription: "Commands run through cmd.exe",
|
||||
},
|
||||
{
|
||||
name: "executor shell",
|
||||
options: { executorOptions: { bash: { shell: "powershell.exe" } } },
|
||||
expectedShell: "powershell.exe",
|
||||
expectedDescription: "Commands run through PowerShell",
|
||||
},
|
||||
{
|
||||
name: "top-level shell precedence",
|
||||
options: {
|
||||
shell: "cmd.exe",
|
||||
executorOptions: { bash: { shell: "powershell.exe" } },
|
||||
},
|
||||
expectedShell: "cmd.exe",
|
||||
expectedDescription: "Commands run through cmd.exe",
|
||||
},
|
||||
])("uses the $name for both description and execution", async ({
|
||||
options,
|
||||
expectedShell,
|
||||
expectedDescription,
|
||||
}) => {
|
||||
const tool = await executeRunCommands(options);
|
||||
|
||||
expect(tool.description).toContain(expectedDescription);
|
||||
expect(spawn).toHaveBeenCalledWith(
|
||||
expectedShell,
|
||||
expect.any(Array),
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -122,7 +122,7 @@ export type {
|
||||
// Convenience: Create Tools with Built-in Executors
|
||||
// =============================================================================
|
||||
|
||||
import type { AgentTool } from "@cline/shared";
|
||||
import { type AgentTool, getDefaultShell } from "@cline/shared";
|
||||
import { createDefaultTools } from "./definitions";
|
||||
import {
|
||||
createDefaultExecutors,
|
||||
@@ -136,11 +136,16 @@ import type { CreateDefaultToolsOptions, ToolExecutors } from "./types";
|
||||
export interface CreateBuiltinToolsOptions
|
||||
extends Omit<CreateDefaultToolsOptions, "executors"> {
|
||||
/**
|
||||
* Configuration for the built-in executors
|
||||
* Configuration for the built-in executors. `bash.shell` is used when the
|
||||
* top-level `shell` option is not set; the top-level option takes precedence.
|
||||
*/
|
||||
executorOptions?: DefaultExecutorsOptions;
|
||||
/**
|
||||
* Optional executor overrides/additions for tools without built-ins
|
||||
* Optional executor overrides/additions for tools without built-ins.
|
||||
* An overriding `bash` executor replaces the built-in one wholesale: it
|
||||
* decides its own shell, and the resolved `shell` option only shapes the
|
||||
* run_commands description. Overriders must honor that shell themselves
|
||||
* to keep the description truthful.
|
||||
*/
|
||||
executors?: Partial<ToolExecutors>;
|
||||
}
|
||||
@@ -180,14 +185,29 @@ export function createBuiltinTools(
|
||||
executors: executorOverrides,
|
||||
...toolsConfig
|
||||
} = options;
|
||||
// The top-level shell is the public tool configuration and takes precedence
|
||||
// over the legacy executor-specific location. Resolve it once so prompting
|
||||
// and execution cannot disagree.
|
||||
const shell =
|
||||
toolsConfig.shell ??
|
||||
executorOptions.bash?.shell ??
|
||||
getDefaultShell(process.platform);
|
||||
const resolvedExecutorOptions: DefaultExecutorsOptions = {
|
||||
...executorOptions,
|
||||
bash: {
|
||||
...executorOptions.bash,
|
||||
shell,
|
||||
},
|
||||
};
|
||||
|
||||
const executors = {
|
||||
...createDefaultExecutors(executorOptions),
|
||||
...createDefaultExecutors(resolvedExecutorOptions),
|
||||
...(executorOverrides ?? {}),
|
||||
};
|
||||
|
||||
return createDefaultTools({
|
||||
...toolsConfig,
|
||||
shell,
|
||||
executors,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -23,6 +23,23 @@ You are a code reviewer.`);
|
||||
});
|
||||
});
|
||||
|
||||
// Regression test for https://github.com/cline/cline/issues/12151: a leading UTF-8 BOM
|
||||
// (e.g. saved by Windows Notepad's "UTF-8 with BOM" encoding) must not prevent frontmatter
|
||||
// from being recognized.
|
||||
it("parses frontmatter when the content starts with a UTF-8 BOM", () => {
|
||||
const config = parseConfiguredAgentConfig(`\uFEFF---
|
||||
name: code-reviewer
|
||||
description: Reviews code
|
||||
---
|
||||
You are a code reviewer.`);
|
||||
|
||||
expect(config).toMatchObject({
|
||||
name: "code-reviewer",
|
||||
description: "Reviews code",
|
||||
systemPrompt: "You are a code reviewer.",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not treat delimiter lines in the body as frontmatter delimiters", () => {
|
||||
const config = parseConfiguredAgentConfig(`---
|
||||
name: code-reviewer
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { type Dirent, existsSync, readdirSync, readFileSync } from "node:fs";
|
||||
import { basename, extname, join } from "node:path";
|
||||
import { stripUtf8Bom } from "@cline/shared";
|
||||
import { resolveAgentConfigSearchPaths } from "@cline/shared/storage";
|
||||
import YAML from "yaml";
|
||||
import { z } from "zod";
|
||||
@@ -40,6 +41,11 @@ function splitFrontmatter(content: string): {
|
||||
frontmatter: string;
|
||||
body: string;
|
||||
} {
|
||||
// Strip a leading UTF-8 BOM (e.g. added by Windows Notepad's "UTF-8 with BOM" encoding),
|
||||
// which Node's `utf-8` decoding does not strip on its own. Without this the frontmatter
|
||||
// match below never matches a file that starts with "\uFEFF---" (see cline/cline#12151).
|
||||
content = stripUtf8Bom(content);
|
||||
|
||||
const firstLineMatch = content.match(/^(---)[^\S\r\n]*(?:\r?\n|$)/);
|
||||
if (!firstLineMatch) {
|
||||
throw new Error("Missing YAML frontmatter block in agent config file.");
|
||||
|
||||
@@ -24,6 +24,7 @@ export type DelegatedAgentConnectionConfig = Pick<
|
||||
| "apiKey"
|
||||
| "baseUrl"
|
||||
| "headers"
|
||||
| "onAuthError"
|
||||
| "providerConfig"
|
||||
| "knownModels"
|
||||
| "thinking"
|
||||
@@ -88,6 +89,7 @@ export function createDelegatedAgentConfigProvider(
|
||||
apiKey: runtimeConfig.apiKey,
|
||||
baseUrl: runtimeConfig.baseUrl,
|
||||
headers: runtimeConfig.headers,
|
||||
onAuthError: runtimeConfig.onAuthError,
|
||||
providerConfig: runtimeConfig.providerConfig,
|
||||
knownModels: runtimeConfig.knownModels,
|
||||
thinking: runtimeConfig.thinking,
|
||||
|
||||
@@ -647,3 +647,61 @@ describe("AgentTeamsRuntime teammate lifecycle events", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("AgentTeamsRuntime run failure reporting", () => {
|
||||
it("marks a run failed when the teammate result finishes with an error", async () => {
|
||||
const events: TeamEvent[] = [];
|
||||
// biome-ignore lint/complexity/useArrowFunction: `new SessionRuntime(...)` requires a non-arrow callable.
|
||||
createSessionRuntimeMock.mockImplementationOnce(function () {
|
||||
return {
|
||||
abort: vi.fn(),
|
||||
run: vi.fn(async () => ({
|
||||
text: "Unauthorized: Please re-authenticate your Cline account.",
|
||||
iterations: 8,
|
||||
finishReason: "error",
|
||||
durationMs: 100,
|
||||
usage: {
|
||||
inputTokens: 10,
|
||||
outputTokens: 20,
|
||||
cacheReadTokens: 0,
|
||||
cacheWriteTokens: 0,
|
||||
totalCost: 0,
|
||||
},
|
||||
messages: [],
|
||||
})),
|
||||
continue: vi.fn(),
|
||||
canStartRun: vi.fn(() => true),
|
||||
getAgentId: vi.fn(() => "teammate-1"),
|
||||
getConversationId: vi.fn(() => "conv-1"),
|
||||
getMessages: vi.fn(() => []),
|
||||
subscribeEvents: vi.fn(() => () => {}),
|
||||
};
|
||||
});
|
||||
const runtime = new AgentTeamsRuntime({
|
||||
teamName: "test-team",
|
||||
onTeamEvent: (event) => events.push(event),
|
||||
});
|
||||
|
||||
runtime.spawnTeammate({
|
||||
agentId: "alice",
|
||||
config: {
|
||||
providerId: "anthropic",
|
||||
modelId: "claude-sonnet-4-5-20250929",
|
||||
systemPrompt: "Helper teammate",
|
||||
tools: [],
|
||||
},
|
||||
});
|
||||
|
||||
const run = runtime.startTeammateRun("alice", "do the thing");
|
||||
const settledRun = await runtime.awaitRun(run.id, 1);
|
||||
|
||||
expect(settledRun.status).toBe("failed");
|
||||
expect(settledRun.error).toContain("Unauthorized");
|
||||
expect(events).toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: TeamMessageType.RunFailed,
|
||||
run: expect.objectContaining({ id: run.id, status: "failed" }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1218,6 +1218,13 @@ export class AgentTeamsRuntime {
|
||||
taskId: run.taskId,
|
||||
continueConversation: run.continueConversation,
|
||||
});
|
||||
// Model-stream failures surface as results with finishReason
|
||||
// "error" rather than throws; route them through the failure
|
||||
// path so the run is reported as failed (and retried when
|
||||
// maxRetries allows) instead of masquerading as completed.
|
||||
if (result.finishReason === "error") {
|
||||
throw new Error(result.text || "Teammate run failed");
|
||||
}
|
||||
run.status = "completed";
|
||||
run.result = result;
|
||||
run.endedAt = new Date();
|
||||
|
||||
@@ -230,6 +230,10 @@ function spawnTeamTeammate(
|
||||
teammateConfigProvider: options.teammateConfigProvider,
|
||||
createBaseTools: options.createBaseTools,
|
||||
allowSpawn: false,
|
||||
// Spawning is lead-only; exposing the tool to teammates just
|
||||
// makes them burn turns on "Only the lead agent can manage
|
||||
// teammates." rejections.
|
||||
includeSpawnTool: false,
|
||||
}),
|
||||
);
|
||||
options.runtime.spawnTeammate({
|
||||
|
||||
@@ -297,6 +297,14 @@ export interface DefaultToolsConfig {
|
||||
*/
|
||||
cwd?: string;
|
||||
|
||||
/**
|
||||
* Shell executable (name or full path) the run_commands executor will use.
|
||||
* The tool description tells the model which shell syntax to write, so this
|
||||
* must match the shell configured on the executor.
|
||||
* @default getDefaultShell(process.platform) — "/bin/bash" on Unix, "powershell" on Windows
|
||||
*/
|
||||
shell?: string;
|
||||
|
||||
/**
|
||||
* Timeout for file read operations in milliseconds
|
||||
* @default 10000
|
||||
|
||||
@@ -112,6 +112,7 @@ export {
|
||||
parseUserCommandEnvelope,
|
||||
registerDisposable,
|
||||
SDK_ERROR_TELEMETRY_EVENT,
|
||||
stripUtf8Bom,
|
||||
} from "@cline/shared";
|
||||
export * from "@cline/shared/storage";
|
||||
export {
|
||||
@@ -136,11 +137,6 @@ export {
|
||||
type UserRemoteConfigOrganization,
|
||||
type UserRemoteConfigResponse,
|
||||
} from "./account";
|
||||
export {
|
||||
hashSecret,
|
||||
setSdkLogger,
|
||||
sdkDebug,
|
||||
} from "./logging/early-logger";
|
||||
export {
|
||||
createOAuthClientCallbacks,
|
||||
type OAuthClientCallbacksOptions,
|
||||
@@ -406,6 +402,11 @@ export type {
|
||||
export * from "./hub";
|
||||
export { HubRuntimeHost } from "./hub/runtime-host/hub-runtime-host";
|
||||
export { RemoteRuntimeHost } from "./hub/runtime-host/remote-runtime-host";
|
||||
export {
|
||||
hashSecret,
|
||||
sdkDebug,
|
||||
setSdkLogger,
|
||||
} from "./logging/early-logger";
|
||||
export {
|
||||
buildRemoteConfigSessionBlobUploadMetadata,
|
||||
createRemoteConfigSessionMessagesArtifactUploader,
|
||||
@@ -651,6 +652,7 @@ export {
|
||||
captureMentionFailed,
|
||||
captureMentionSearchResults,
|
||||
captureMentionUsed,
|
||||
captureMistakeLimitReached,
|
||||
captureModeSwitch,
|
||||
captureProviderApiError,
|
||||
captureProviderConfigured,
|
||||
|
||||
@@ -495,6 +495,27 @@ export class LocalRuntimeHost implements RuntimeHost {
|
||||
configWithProvider.teamName = runtime.teamRuntime.getTeamName();
|
||||
}
|
||||
|
||||
// Auth-retry hook for every agent in the session (lead, teammates,
|
||||
// subagents): refresh OAuth credentials and propagate the new key to
|
||||
// all connections, then let the runtime retry the failed run. Without
|
||||
// this, a token that expires while the lead is blocked (e.g. in
|
||||
// team_await_runs) kills teammate runs with a raw provider 401.
|
||||
const onAuthError = async (): Promise<boolean> => {
|
||||
const liveSession = this.sessions.get(sessionId);
|
||||
if (!liveSession || !isOAuthProvider(liveSession.config.providerId)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
await this.syncOAuthCredentials(liveSession, { forceRefresh: true });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
runtime.delegatedAgentConfigProvider?.updateConnectionDefaults({
|
||||
onAuthError,
|
||||
});
|
||||
|
||||
const tools = [...runtime.tools, ...(configWithProvider.extraTools ?? [])];
|
||||
const extensions = runtime.extensions ?? bootstrap.extensions;
|
||||
const explicitInitialCompactionState = startInput.initialCompactionState;
|
||||
@@ -564,6 +585,7 @@ export class LocalRuntimeHost implements RuntimeHost {
|
||||
apiKey: providerConfig.apiKey,
|
||||
baseUrl: providerConfig.baseUrl,
|
||||
headers: providerConfig.headers,
|
||||
onAuthError,
|
||||
knownModels: providerConfig.knownModels,
|
||||
providerConfig,
|
||||
thinking: configWithProvider.thinking,
|
||||
|
||||
@@ -2189,6 +2189,61 @@ describe("SessionRuntime.run — tracker wiring (P1 #3)", () => {
|
||||
expect(abortCalls).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("captures task.mistake_limit_reached telemetry exactly once when the limit is hit", async () => {
|
||||
const capture = vi.fn();
|
||||
const telemetry = {
|
||||
capture,
|
||||
captureRequired: vi.fn(),
|
||||
setDistinctId: vi.fn(),
|
||||
setMetadata: vi.fn(),
|
||||
updateMetadata: vi.fn(),
|
||||
setCommonProperties: vi.fn(),
|
||||
updateCommonProperties: vi.fn(),
|
||||
isEnabled: vi.fn(() => true),
|
||||
recordCounter: vi.fn(),
|
||||
recordHistogram: vi.fn(),
|
||||
recordGauge: vi.fn(),
|
||||
flush: vi.fn(async () => {}),
|
||||
dispose: vi.fn(async () => {}),
|
||||
};
|
||||
const { deps } = makeScriptedRuntime({
|
||||
events: failedToolTurnEvents(),
|
||||
});
|
||||
const session = new SessionRuntime(
|
||||
makeAgentConfig({
|
||||
execution: { maxConsecutiveMistakes: 2 },
|
||||
sessionId: "sess_mistakes",
|
||||
telemetry,
|
||||
}),
|
||||
deps,
|
||||
);
|
||||
|
||||
await session.run("one");
|
||||
// First failed turn — counter 1 < 2, no telemetry yet.
|
||||
const limitEvents = () =>
|
||||
capture.mock.calls
|
||||
.map((call) => call[0])
|
||||
.filter((event) => event.event === "task.mistake_limit_reached");
|
||||
expect(limitEvents()).toHaveLength(0);
|
||||
|
||||
await session.continue("two");
|
||||
// Second failed turn hits the limit: exactly one event, even though
|
||||
// no `onConsecutiveMistakeLimitReached` callback is configured (the
|
||||
// tracker falls back to the default stop decision).
|
||||
const events = limitEvents();
|
||||
expect(events).toHaveLength(1);
|
||||
expect(events[0].properties).toMatchObject({
|
||||
ulid: "sess_mistakes",
|
||||
model: "claude-3-5-sonnet",
|
||||
provider: "anthropic",
|
||||
reason: "tool_execution_failed",
|
||||
consecutiveMistakes: 2,
|
||||
maxConsecutiveMistakes: 2,
|
||||
isSubagent: false,
|
||||
});
|
||||
expect(events[0].properties.agentId).toMatch(/^agent_/);
|
||||
});
|
||||
|
||||
it("aborts on hard-threshold loop detection of identical tool calls", async () => {
|
||||
const identical = (i: number): AgentRuntimeEvent => ({
|
||||
type: "tool-started",
|
||||
@@ -2342,3 +2397,90 @@ describe("SessionRuntime.run — tracker wiring (P1 #3)", () => {
|
||||
expect(abortCalls).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Auth retry
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("SessionRuntime auth retry", () => {
|
||||
const authFailure: Partial<AgentRunResult> = {
|
||||
status: "failed",
|
||||
error: new Error(
|
||||
"Unauthorized: Please make sure you're using the latest version of Cline and re-authenticate your Cline account.",
|
||||
),
|
||||
};
|
||||
|
||||
/** Runtime factory that scripts each successive AgentRuntime build. */
|
||||
function withSequencedRuntimes(scripts: FakeAgentRuntimeScript[]): {
|
||||
deps: SessionRuntimeOrchestratorDeps;
|
||||
createdCount: () => number;
|
||||
} {
|
||||
let created = 0;
|
||||
const deps: SessionRuntimeOrchestratorDeps = {
|
||||
createAgentRuntimeImpl: () => {
|
||||
const script = scripts[Math.min(created, scripts.length - 1)];
|
||||
created += 1;
|
||||
return makeFakeAgentRuntime(script).runtime;
|
||||
},
|
||||
};
|
||||
return { deps, createdCount: () => created };
|
||||
}
|
||||
|
||||
it("retries once with refreshed credentials when a run fails with an auth error", async () => {
|
||||
const onAuthError = vi.fn(async () => true);
|
||||
const capture = vi.fn();
|
||||
const telemetry = { capture } as unknown as AgentConfig["telemetry"];
|
||||
const { deps, createdCount } = withSequencedRuntimes([
|
||||
{ result: authFailure },
|
||||
{ result: { outputText: "recovered" } },
|
||||
]);
|
||||
const session = new SessionRuntime(
|
||||
makeAgentConfig({ onAuthError, telemetry }),
|
||||
deps,
|
||||
);
|
||||
|
||||
const result = await session.run("go");
|
||||
|
||||
expect(onAuthError).toHaveBeenCalledTimes(1);
|
||||
expect(createdCount()).toBe(2);
|
||||
expect(result.finishReason).toBe("completed");
|
||||
expect(result.text).toBe("recovered");
|
||||
expect(capture).toHaveBeenCalledWith({
|
||||
event: "user.auth_run_retry",
|
||||
properties: { provider: "anthropic", recovered: true },
|
||||
});
|
||||
});
|
||||
|
||||
it("returns the failed result when the host cannot refresh credentials", async () => {
|
||||
const onAuthError = vi.fn(async () => false);
|
||||
const { deps, createdCount } = withSequencedRuntimes([
|
||||
{ result: authFailure },
|
||||
]);
|
||||
const session = new SessionRuntime(makeAgentConfig({ onAuthError }), deps);
|
||||
|
||||
const result = await session.run("go");
|
||||
|
||||
expect(onAuthError).toHaveBeenCalledTimes(1);
|
||||
expect(createdCount()).toBe(1);
|
||||
expect(result.finishReason).toBe("error");
|
||||
});
|
||||
|
||||
it("does not invoke onAuthError for non-auth failures", async () => {
|
||||
const onAuthError = vi.fn(async () => true);
|
||||
const { deps, createdCount } = withSequencedRuntimes([
|
||||
{
|
||||
result: {
|
||||
status: "failed",
|
||||
error: new Error("Model stream failed"),
|
||||
},
|
||||
},
|
||||
]);
|
||||
const session = new SessionRuntime(makeAgentConfig({ onAuthError }), deps);
|
||||
|
||||
const result = await session.run("go");
|
||||
|
||||
expect(onAuthError).not.toHaveBeenCalled();
|
||||
expect(createdCount()).toBe(1);
|
||||
expect(result.finishReason).toBe("error");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
type ContributionRegistry,
|
||||
createContributionRegistry,
|
||||
type ITelemetryService,
|
||||
isLikelyAuthError,
|
||||
type LegacyAgentUsage,
|
||||
type LoopDetectionConfig,
|
||||
type Message,
|
||||
@@ -52,6 +53,10 @@ import {
|
||||
createAgentModelFromConfig,
|
||||
resolveKnownModelsFromConfig,
|
||||
} from "../../services/llms/handler-factory";
|
||||
import {
|
||||
captureAuthRunRetry,
|
||||
captureMistakeLimitReached,
|
||||
} from "../../services/telemetry/core-events";
|
||||
import { CLINE_INTERNAL_TELEMETRY_METADATA_KEY } from "../../services/telemetry/tool-context";
|
||||
import {
|
||||
getMessageBuilderOptionsFromEnv,
|
||||
@@ -276,10 +281,10 @@ export class SessionRuntime {
|
||||
private readonly agentId: string;
|
||||
private readonly parentAgentId?: string;
|
||||
private readonly logger?: BasicLogger;
|
||||
// Reserved for §3.4.4 telemetry parity (not yet consumed — §3.4.4
|
||||
// listed as explicitly deferred until telemetry wiring is added).
|
||||
// Typed as `readonly` to preserve the field slot for future use
|
||||
// without re-touching the constructor.
|
||||
// §3.4.4 telemetry parity. Currently consumed by the MistakeTracker's
|
||||
// `onLimitTelemetry` hook (task.mistake_limit_reached); most other
|
||||
// runtime telemetry is emitted host-side from the agent event stream
|
||||
// (services/agent-events.ts).
|
||||
readonly telemetry?: ITelemetryService;
|
||||
private readonly conversation: ConversationStore;
|
||||
private readonly mistakeTracker: MistakeTracker;
|
||||
@@ -401,6 +406,22 @@ export class SessionRuntime {
|
||||
this.mistakeTracker = new MistakeTracker({
|
||||
maxConsecutiveMistakes: maxMistakes,
|
||||
onLimitReached: config.onConsecutiveMistakeLimitReached,
|
||||
onLimitTelemetry: (context) => {
|
||||
// Read connection fields from `this.config` at fire time so a
|
||||
// mid-session `updateConnection` is reflected in the event.
|
||||
captureMistakeLimitReached(this.telemetry, {
|
||||
ulid: this.config.sessionId ?? this.conversation.getConversationId(),
|
||||
model: this.config.modelId,
|
||||
provider: this.config.providerId,
|
||||
reason: context.reason,
|
||||
consecutiveMistakes: context.consecutiveMistakes,
|
||||
maxConsecutiveMistakes: context.maxConsecutiveMistakes,
|
||||
agentId: this.agentId,
|
||||
conversationId: this.conversation.getConversationId(),
|
||||
parentAgentId: this.parentAgentId,
|
||||
isSubagent: Boolean(this.parentAgentId),
|
||||
});
|
||||
},
|
||||
emit: (event) => this.emitLegacyEvent(event),
|
||||
log: (level, message, metadata) =>
|
||||
leveledLog(this.logger, level, message, metadata),
|
||||
@@ -674,7 +695,7 @@ export class SessionRuntime {
|
||||
isContinue: boolean;
|
||||
}): Promise<AgentResult> {
|
||||
let activePromise!: Promise<AgentResult>;
|
||||
activePromise = this.executeRunInternal(input).finally(() => {
|
||||
activePromise = this.executeRunWithAuthRetry(input).finally(() => {
|
||||
if (this.activeRunPromise === activePromise) {
|
||||
this.activeRunPromise = null;
|
||||
}
|
||||
@@ -683,6 +704,37 @@ export class SessionRuntime {
|
||||
return activePromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry a run once when it failed with an auth-like error and the host
|
||||
* refreshed credentials via `config.onAuthError`. The failed attempt's
|
||||
* trail is already persisted to the conversation store, so the retry
|
||||
* continues from where the stream died instead of replaying the run.
|
||||
*/
|
||||
private async executeRunWithAuthRetry(input: {
|
||||
userMessage?: string;
|
||||
userImages?: string[];
|
||||
userFiles?: string[];
|
||||
isContinue: boolean;
|
||||
}): Promise<AgentResult> {
|
||||
const result = await this.executeRunInternal(input);
|
||||
if (
|
||||
result.finishReason !== "error" ||
|
||||
!this.config.onAuthError ||
|
||||
!isLikelyAuthError(result.text)
|
||||
) {
|
||||
return result;
|
||||
}
|
||||
const refreshed = await this.config.onAuthError().catch(() => false);
|
||||
if (!refreshed) {
|
||||
return result;
|
||||
}
|
||||
const retryResult = await this.executeRunInternal({ isContinue: true });
|
||||
captureAuthRunRetry(this.telemetry, this.config.providerId, {
|
||||
recovered: retryResult.finishReason !== "error",
|
||||
});
|
||||
return retryResult;
|
||||
}
|
||||
|
||||
private async executeRunInternal(input: {
|
||||
userMessage?: string;
|
||||
userImages?: string[];
|
||||
|
||||
@@ -60,6 +60,12 @@ export interface MistakeTrackerOptions {
|
||||
) =>
|
||||
| Promise<ConsecutiveMistakeLimitDecision>
|
||||
| ConsecutiveMistakeLimitDecision;
|
||||
/**
|
||||
* Observability hook fired exactly once per limit hit, right before the
|
||||
* limit decision is resolved — regardless of whether `onLimitReached` is
|
||||
* configured or what it decides. Used for telemetry.
|
||||
*/
|
||||
readonly onLimitTelemetry?: (ctx: ConsecutiveMistakeLimitContext) => void;
|
||||
readonly emit: (event: AgentEvent) => void;
|
||||
readonly log: LeveledLog;
|
||||
readonly agentId: string;
|
||||
@@ -107,14 +113,16 @@ export class MistakeTracker {
|
||||
return { action: "continue" };
|
||||
}
|
||||
|
||||
const limitContext: ConsecutiveMistakeLimitContext = {
|
||||
iteration: input.iteration,
|
||||
consecutiveMistakes: next,
|
||||
maxConsecutiveMistakes: max,
|
||||
reason: input.reason,
|
||||
details: input.details,
|
||||
};
|
||||
this.options.onLimitTelemetry?.(limitContext);
|
||||
const decision = await resolveConsecutiveMistakeDecision(
|
||||
{
|
||||
iteration: input.iteration,
|
||||
consecutiveMistakes: next,
|
||||
maxConsecutiveMistakes: max,
|
||||
reason: input.reason,
|
||||
details: input.details,
|
||||
},
|
||||
limitContext,
|
||||
this.options.onLimitReached,
|
||||
);
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
captureCompactionExecuted,
|
||||
captureCompactionSkipped,
|
||||
captureExtensionActivated,
|
||||
captureMistakeLimitReached,
|
||||
captureProviderConfigured,
|
||||
captureRunCommandsTimeout,
|
||||
captureTelemetryOptOut,
|
||||
@@ -270,6 +271,35 @@ describe("captureWorkspacePathResolved", () => {
|
||||
).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("captureMistakeLimitReached", () => {
|
||||
const baseProps = {
|
||||
ulid: "sess-1",
|
||||
model: "claude-3-5-sonnet",
|
||||
provider: "anthropic",
|
||||
reason: "tool_execution_failed",
|
||||
consecutiveMistakes: 3,
|
||||
maxConsecutiveMistakes: 3,
|
||||
};
|
||||
|
||||
test("emits task.mistake_limit_reached with limit context and a timestamp", () => {
|
||||
const stub = createTelemetryStub();
|
||||
captureMistakeLimitReached(stub.telemetry, baseProps);
|
||||
expect(stub.capture).toHaveBeenCalledTimes(1);
|
||||
expect(stub.captureRequired).not.toHaveBeenCalled();
|
||||
const { event, properties } = captureCallAt(stub, 0);
|
||||
expect(event).toBe("task.mistake_limit_reached");
|
||||
expect(properties).toMatchObject(baseProps);
|
||||
expect(typeof properties?.timestamp).toBe("string");
|
||||
});
|
||||
|
||||
test("no-ops when telemetry is undefined", () => {
|
||||
expect(() =>
|
||||
captureMistakeLimitReached(undefined, baseProps),
|
||||
).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("captureCompactionExecuted", () => {
|
||||
const baseProps = {
|
||||
ulid: "ulid-1",
|
||||
|
||||
@@ -49,6 +49,7 @@ export const CORE_TELEMETRY_EVENTS = {
|
||||
AUTH_FAILED: "user.auth_failed",
|
||||
AUTH_LOGGED_OUT: "user.auth_logged_out",
|
||||
AUTH_REFRESH_SOFT_FAILURE: "user.auth_refresh_soft_failure",
|
||||
AUTH_RUN_RETRY: "user.auth_run_retry",
|
||||
PROVIDER_CONFIGURED: "user.provider_configured",
|
||||
TELEMETRY_OPT_OUT: "user.opt_out",
|
||||
},
|
||||
@@ -63,6 +64,7 @@ export const CORE_TELEMETRY_EVENTS = {
|
||||
SKILL_USED: "task.skill_used",
|
||||
DIFF_EDIT_FAILED: "task.diff_edit_failed",
|
||||
PROVIDER_API_ERROR: "task.provider_api_error",
|
||||
MISTAKE_LIMIT_REACHED: "task.mistake_limit_reached",
|
||||
MENTION_USED: "task.mention_used",
|
||||
MENTION_FAILED: "task.mention_failed",
|
||||
MENTION_SEARCH_RESULTS: "task.mention_search_results",
|
||||
@@ -290,6 +292,23 @@ export function captureAuthRefreshSoftFailure(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires when a run failed with an auth-like error, credentials were
|
||||
* refreshed, and the run was retried once. `recovered: true` means the retry
|
||||
* completed — a run that would previously have surfaced a raw provider 401
|
||||
* (e.g. a teammate stranded on a spawn-time token snapshot past its TTL).
|
||||
*/
|
||||
export function captureAuthRunRetry(
|
||||
telemetry: ITelemetryService | undefined,
|
||||
provider?: string,
|
||||
details?: { recovered?: boolean },
|
||||
): void {
|
||||
emit(telemetry, CORE_TELEMETRY_EVENTS.USER.AUTH_RUN_RETRY, {
|
||||
provider,
|
||||
recovered: details?.recovered,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires when the user finishes configuring a "bring your own provider"
|
||||
* (API-key based) provider during onboarding or via settings.
|
||||
@@ -490,6 +509,28 @@ export function captureProviderApiError(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Records when the consecutive mistake limit is reached, right before the
|
||||
* limit decision (host prompt / auto-stop) is resolved.
|
||||
*/
|
||||
export function captureMistakeLimitReached(
|
||||
telemetry: ITelemetryService | undefined,
|
||||
properties: {
|
||||
ulid: string;
|
||||
model: string;
|
||||
provider?: string;
|
||||
/** What kind of mistake tripped the limit. */
|
||||
reason: string;
|
||||
consecutiveMistakes: number;
|
||||
maxConsecutiveMistakes: number;
|
||||
} & Partial<TelemetryAgentIdentityProperties>,
|
||||
): void {
|
||||
emit(telemetry, CORE_TELEMETRY_EVENTS.TASK.MISTAKE_LIMIT_REACHED, {
|
||||
...properties,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
export function captureRunCommandsTimeout(
|
||||
telemetry: ITelemetryService | undefined,
|
||||
properties: RunCommandsTimeoutTelemetryProperties,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@cline/llms",
|
||||
"version": "0.0.62",
|
||||
"version": "0.0.65",
|
||||
"description": "Config-driven SDK for selecting, extending, and instantiating LLM providers and models",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -41,6 +41,12 @@
|
||||
"peerDependenciesMeta": {
|
||||
"@aws-sdk/client-bedrock-runtime": {
|
||||
"optional": true
|
||||
},
|
||||
"ai-sdk-provider-claude-code": {
|
||||
"optional": true
|
||||
},
|
||||
"ai-sdk-provider-codex-cli": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -61,14 +67,18 @@
|
||||
"@streamparser/json": "^0.0.21",
|
||||
"ai": "^6.0.144",
|
||||
"ai-sdk-ollama": "^3.8.8",
|
||||
"ai-sdk-provider-claude-code": "^3.4.3",
|
||||
"ai-sdk-provider-codex-cli": "^1.1.0",
|
||||
"ai-sdk-provider-opencode-sdk": "^3.0.1",
|
||||
"dify-ai-provider": "^1.1.0",
|
||||
"nanoid": "^5.1.7",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@aws-sdk/client-bedrock-runtime": "^3.0.0"
|
||||
"@aws-sdk/client-bedrock-runtime": "^3.0.0",
|
||||
"ai-sdk-provider-claude-code": "^3.4.3",
|
||||
"ai-sdk-provider-codex-cli": "^1.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ai-sdk-provider-claude-code": "^3.4.3",
|
||||
"ai-sdk-provider-codex-cli": "^1.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,13 +77,16 @@ while generating the catalog. Conceptually:
|
||||
safeOutputTokens = min(
|
||||
modelReportedMaxOutput,
|
||||
contextWindow - estimatedPromptTokens - reserveTokens,
|
||||
userConfiguredOutputCap,
|
||||
userConfiguredOutputCap or productDefaultOutputCap,
|
||||
)
|
||||
```
|
||||
|
||||
The SDK gateway only sends an output token limit when the caller provides
|
||||
`request.options.maxTokens` or an equivalent host configuration. Catalog
|
||||
metadata does not become a request parameter by itself.
|
||||
The SDK gateway resolves an output token limit for the provider request. It uses
|
||||
`request.options.maxTokens` or an equivalent host configuration when present,
|
||||
otherwise it applies the product default output cap
|
||||
(`DEFAULT_GATEWAY_MAX_OUTPUT_TOKENS`, currently 32000) when the model catalog has
|
||||
either an output limit or a context window. Provider modules are responsible for
|
||||
forwarding that limit only to wire API surfaces that support it.
|
||||
|
||||
The exact request-limit policy belongs in the provider/gateway/core request
|
||||
path, not in generated catalog data.
|
||||
@@ -161,5 +164,5 @@ and observable.
|
||||
- `catalog-live.test.ts`: tests catalog normalization behavior.
|
||||
- `catalog.generated.ts`: checked-in generated provider/model catalog.
|
||||
- `../../scripts/generate-models.ts`: writes generated catalog output.
|
||||
- `../providers/ai-sdk.ts`: passes `maxOutputTokens` into AI SDK.
|
||||
- `../providers/ai-sdk.ts`: conditionally passes `maxOutputTokens` into AI SDK.
|
||||
- `../providers/gateway.ts`: resolves per-request/default `maxTokens`.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,7 @@ import {
|
||||
parseJsonStream,
|
||||
sanitizeSurrogates,
|
||||
} from "@cline/shared";
|
||||
import { jsonSchema, NoSuchToolError, streamText } from "ai";
|
||||
import { type CallSettings, jsonSchema, NoSuchToolError, streamText } from "ai";
|
||||
import { nanoid } from "nanoid";
|
||||
import { extractErrorMessage } from "./format";
|
||||
import {
|
||||
@@ -53,6 +53,18 @@ interface GatewayNormalizedUsage {
|
||||
}
|
||||
type ProviderModuleKind = AiSdkProviderOptionsTarget;
|
||||
|
||||
export function buildAiSdkStreamConfig(
|
||||
request: GatewayStreamRequest,
|
||||
_context: GatewayProviderContext,
|
||||
): Partial<CallSettings> {
|
||||
return {
|
||||
...(request.maxTokens !== undefined
|
||||
? { maxOutputTokens: request.maxTokens }
|
||||
: {}),
|
||||
temperature: request.temperature,
|
||||
};
|
||||
}
|
||||
|
||||
function buildCachedAiSdkMessages(
|
||||
request: GatewayStreamRequest,
|
||||
context: GatewayProviderContext,
|
||||
@@ -352,10 +364,15 @@ function toAiSdkMessages(
|
||||
}
|
||||
}
|
||||
|
||||
// A message left empty only because its reasoning was dropped is
|
||||
// omitted entirely instead of forwarded as an empty turn.
|
||||
const emptiedByDroppedReasoning = !includeReasoning && skippedReasoning;
|
||||
if (content.length > 0) {
|
||||
normalizedMessages.push({ role: message.role, content });
|
||||
} else if (!includeReasoning && skippedReasoning) {
|
||||
} else if (message.role === "user" || message.role === "assistant") {
|
||||
} else if (
|
||||
!emptiedByDroppedReasoning &&
|
||||
(message.role === "user" || message.role === "assistant")
|
||||
) {
|
||||
normalizedMessages.push({ role: message.role, content: "" });
|
||||
}
|
||||
}
|
||||
@@ -1178,6 +1195,9 @@ function createAiSdkProvider(kind: ProviderModuleKind): GatewayProviderFactory {
|
||||
context,
|
||||
kind,
|
||||
) as never;
|
||||
const requestConfig = provider.buildStreamConfig
|
||||
? provider.buildStreamConfig(request, context)
|
||||
: buildAiSdkStreamConfig(request, context);
|
||||
recordProviderRequestCapture({
|
||||
stage: "ai_sdk_prompt",
|
||||
request,
|
||||
@@ -1186,8 +1206,7 @@ function createAiSdkProvider(kind: ProviderModuleKind): GatewayProviderFactory {
|
||||
...(useSystemOption ? { system: systemPrompt } : {}),
|
||||
tools,
|
||||
providerOptions,
|
||||
maxOutputTokens: request.maxTokens,
|
||||
temperature: request.temperature,
|
||||
...requestConfig,
|
||||
},
|
||||
});
|
||||
stream = streamText({
|
||||
@@ -1195,16 +1214,13 @@ function createAiSdkProvider(kind: ProviderModuleKind): GatewayProviderFactory {
|
||||
messages: messages as never,
|
||||
...(useSystemOption ? { system: systemPrompt } : {}),
|
||||
tools: tools as never,
|
||||
temperature: request.temperature,
|
||||
...(request.maxTokens !== undefined
|
||||
? { maxOutputTokens: request.maxTokens }
|
||||
: {}),
|
||||
abortSignal: request.signal,
|
||||
experimental_repairToolCall: repairMalformedToolCall as never,
|
||||
experimental_telemetry: {
|
||||
isEnabled: langfuse,
|
||||
},
|
||||
providerOptions,
|
||||
...requestConfig,
|
||||
onError: ({ error: streamError }) => {
|
||||
const msg = extractErrorMessage(streamError);
|
||||
capturedError.current = msg;
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
type ProviderCapability,
|
||||
type ProviderConfigField,
|
||||
} from "@cline/shared";
|
||||
import { GENERATED_PROVIDER_MODELS } from "../catalog/catalog.generated";
|
||||
import { getGeneratedModelsForProvider } from "../catalog/catalog.generated-access";
|
||||
import {
|
||||
isCanonicalModelIdForAliasRules,
|
||||
@@ -307,7 +308,13 @@ function generatedModels(providerId: string): Record<string, ModelInfo> {
|
||||
}
|
||||
|
||||
function firstGeneratedModelId(providerId: string): string {
|
||||
const generatedModelList = Object.keys(generatedModels(providerId));
|
||||
// Use the catalog's authored order, not release-date order. The cline-pass
|
||||
// block mirrors the recommended-models endpoint, which lists the intended
|
||||
// default subscription model first — the newest model is not necessarily a
|
||||
// safe default.
|
||||
const generatedModelList = Object.keys(
|
||||
GENERATED_PROVIDER_MODELS.providers[providerId] ?? {},
|
||||
);
|
||||
if (!generatedModelList.length) {
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
toGatewayRequestMessages,
|
||||
} from "./compat";
|
||||
import { ClineNotSubscribedError } from "./errors";
|
||||
import { DEFAULT_GATEWAY_MAX_OUTPUT_TOKENS } from "./gateway";
|
||||
import type { Message } from "./types";
|
||||
|
||||
const streamTextSpy = vi.fn();
|
||||
@@ -361,7 +362,7 @@ describe("createGatewayApiHandler.createMessage", () => {
|
||||
openaiCompatibleSpy.mockClear();
|
||||
});
|
||||
|
||||
it("does not convert catalog maxTokens into request maxOutputTokens", async () => {
|
||||
it("uses the default maxOutputTokens without expanding to catalog maxTokens", async () => {
|
||||
streamTextSpy.mockReturnValue({
|
||||
fullStream: (async function* () {
|
||||
yield { type: "finish", finishReason: "stop" };
|
||||
@@ -395,7 +396,10 @@ describe("createGatewayApiHandler.createMessage", () => {
|
||||
const call = streamTextSpy.mock.calls.at(-1)?.[0] as
|
||||
| { maxOutputTokens?: unknown }
|
||||
| undefined;
|
||||
expect(call).not.toHaveProperty("maxOutputTokens");
|
||||
expect(call).toHaveProperty(
|
||||
"maxOutputTokens",
|
||||
DEFAULT_GATEWAY_MAX_OUTPUT_TOKENS,
|
||||
);
|
||||
});
|
||||
|
||||
it("sends configured OpenAI-compatible maxOutputTokens to the provider request", async () => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user