Compare commits

..

58 Commits

Author SHA1 Message Date
tanzhenxin
fc1dac9dc7 update 2025-12-22 14:32:51 +08:00
tanzhenxin
338eb9038d fix e2e workflow 2025-12-22 14:28:36 +08:00
tanzhenxin
e0b9044833 Merge pull request #1310 from QwenLM/fix/process-info-robust-20251222
Improve robustness of getProcessInfo with try-catch and empty output fallback
2025-12-22 14:02:51 +08:00
xuewenjie
f33f43e2f7 feat: improve getProcessInfo robustness with try-catch and empty output fallback 2025-12-22 11:38:38 +08:00
tanzhenxin
4e7929850c Merge pull request #1309 from QwenLM/chore/v0.6.0
pump version to 0.6.0
2025-12-22 09:58:20 +08:00
tanzhenxin
9cc5c3ed8f pump version to 0.6.0 2025-12-22 09:35:30 +08:00
tanzhenxin
a92be72e88 Merge pull request #1257 from QwenLM/feat/ide-companion-discovery
IDE companion discovery: switch to ~/.qwen/ide lock files
2025-12-19 18:29:11 +08:00
tanzhenxin
52cd1da4a7 update documentation 2025-12-19 18:16:59 +08:00
tanzhenxin
c5c556a326 remove pid from lockfile name of ide connection file 2025-12-19 18:12:04 +08:00
tanzhenxin
a8a863581b Merge branch 'main' into feat/ide-companion-discovery 2025-12-19 16:49:24 +08:00
tanzhenxin
e4468cfcbc Merge pull request #1231 from QwenLM/fix/windows-startup-and-exit-hangs
fix: optimize windows process tree retrieval to prevent hang
2025-12-19 16:48:40 +08:00
tanzhenxin
3bf30ead67 Merge pull request #1262 from QwenLM/chore/vscode-ide-companion-update-vscode-engine
chore(vscode-ide-companion): update vscode engine version from ^1.99.0 to ^1.85.0
2025-12-19 16:47:33 +08:00
Mingholy
a786f61e49 Merge pull request #1265 from QwenLM/mingholy/chore/bundle-cli-into-sdk
Bundle CLI into SDK package and separate CLI & SDK E2E tests
2025-12-19 16:45:35 +08:00
mingholy.lmh
fa7d857945 fix: remove unused cli finding code 2025-12-19 16:18:22 +08:00
mingholy.lmh
90489933fd fix: lint issues 2025-12-19 15:52:11 +08:00
mingholy.lmh
3354b56a05 docs(sdk): update sdk docs 2025-12-19 15:47:11 +08:00
mingholy.lmh
d40447cee4 fix: failed test cases 2025-12-19 15:44:04 +08:00
mingholy.lmh
ba87cf63f6 chore: build and bundle CLI for SDK release 2025-12-19 15:44:04 +08:00
mingholy.lmh
00a8c6a924 chore: separate CLI and SDK integration test 2025-12-19 15:44:04 +08:00
mingholy.lmh
156134d3d4 chore(sdk): bundle CLI into SDK package and inherit the dependencies 2025-12-19 15:44:04 +08:00
tanzhenxin
b720209888 Merge pull request #1261 from QwenLM/fix/vscode-ide-companion-opt-task-stop
fix(vscode-ide-companion): Optimize stream termination handling and fix style layering issues
2025-12-19 15:15:04 +08:00
tanzhenxin
dfe667c364 Merge pull request #1269 from QwenLM/mingholy/fix/sampling-params
fix: default values of sampling params
2025-12-19 15:14:26 +08:00
xuewenjie
1386fba278 Revert other files to main, keep only process-utils changes 2025-12-19 15:01:26 +08:00
xuewenjie
d942250905 test: sync test files with code changes for IDE detection
- Update detect-ide.test.ts to remove ideProcessInfo parameter (now optional)
- Update process-utils.test.ts to match simplified Windows process detection logic
- Remove tests for removed IDE detection strategies (Strategy 1-4)
- All tests now passing (13 tests in detect-ide.test.ts, 6 tests in process-utils.test.ts)
2025-12-19 13:24:19 +08:00
xuewenjie
ec32a24508 fix: update ide-client tests to match new config file naming scheme
- Update config file naming from qwen-code-ide-server-{pid}-{timestamp}.json to qwen-code-ide-server-{port}.json
- Add readdir mock to return config file list
- Add validateWorkspacePath mock for workspace validation
- Add workspacePath field to all config objects in tests
- Remove getIdeProcessInfo dependency from tests
- All 23 tests now passing
2025-12-19 11:36:05 +08:00
xuewenjie
c2b59038ae fix: escape backslashes in PowerShell command strings (CodeQL security fix)
Fixes CodeQL security alert: Incomplete string escaping or encoding

- Add escapeForPowerShellDoubleQuotes() helper function
- Properly escape both backslashes and double quotes in correct order
- Prevents command injection vulnerabilities in Windows process detection
- All existing tests pass
2025-12-18 17:32:11 +08:00
xuewenjie
27bf42b4f5 test: sync process-utils.test.ts with implementation logic
- Update Windows test cases to match multi-strategy IDE detection
- Add test for Strategy 1: known IDE process detection (code.exe)
- Add test for Strategy 3: shell parent detection (cmd.exe)
- Add test for Strategy 2: Git Bash with missing parent handling
- Fix test expectations to align with actual implementation behavior
- All 7 test cases now pass successfully
2025-12-18 17:28:56 +08:00
tanzhenxin
d07ae35c90 Merge pull request #1286 from afarber/1220-resume-alias
fix(cli): add -r and -C aliases for --resume and --continue options
2025-12-18 16:32:36 +08:00
xuewenjie
cb59b5a9dc refactor(core): optimize Windows process detection and remove debug logging
- Replace execFileAsync with execAsync for complex PowerShell commands in getProcessInfo
- Remove unnecessary getProcessInfo retry logic when parent not in processMap
- Remove all debug logging code (writeDebugLog function and fs import)
- Improve performance by ~1.6-2.6 seconds per detection
- Keep execFileAsync for simple commands in getProcessTableWindows
2025-12-18 16:24:40 +08:00
xuewenjie
01e62a2120 refactor: remove unused fs import from process-utils.ts 2025-12-18 15:06:01 +08:00
Alexander Farber
d464f61b72 Change -C to -c 2025-12-18 07:36:04 +01:00
Alexander Farber
f866f7f071 Add -r and -C aliases for --resume and --continue options 2025-12-18 07:36:04 +01:00
xuewenjie
7eabf543b4 Merge branch 'main' of github.com:QwenLM/qwen-code into fix/windows-startup-and-exit-hangs 2025-12-18 13:24:21 +08:00
Alexander Farber
8106a6b0f4 Handle PAT tokens and credentials in git remote URL parsing (#1225) 2025-12-18 00:44:46 +08:00
pomelo
c0839dceac Merge pull request #1266 from QwenLM/docs-fix
docs:Fix the errors in the document
2025-12-17 22:04:27 +08:00
yiliang114
12f84fb730 fix(vscode-ide-companion): optimize stream termination handling and remove timeout for session_prompt 2025-12-17 21:00:26 +08:00
joeytoday
f9a1ee2442 docs: updated vscode showcase video 2025-12-17 16:47:37 +08:00
joeytoday
f824004f99 docs: updated links in index.md 2025-12-17 15:03:23 +08:00
Mingholy
e274b4469a Merge pull request #1214 from kfxmvp/fix/issue-1186-schema-converter
fix: add configurable OpenAPI 3.0 schema compliance for Gemini compatibility (#1186)
2025-12-17 11:12:57 +08:00
joeytoday
a4e3d764d3 docs: updated all links, click and open in vscode, new showcase video in overview 2025-12-17 11:10:31 +08:00
tanzhenxin
0a39c91264 Merge pull request #1275 from QwenLM/fix/integration-test
remove one flaky integration test
2025-12-17 10:06:28 +08:00
yiliang114
49b3e0dc92 chore(vscode-ide-companion): update vscode engine version from ^1.99.0 to ^1.85.0 2025-12-16 19:54:19 +08:00
mingholy.lmh
25d9c4f1a7 fix: default values of sampling params 2025-12-16 17:09:42 +08:00
joeytoday
d1a6b3207e docs: updated inline links 2025-12-16 17:01:47 +08:00
pomelo-nwu
1c62499977 feat: fix link 2025-12-16 15:40:01 +08:00
pomelo-nwu
4b8b4e2fe8 feat: update docs 2025-12-16 15:32:21 +08:00
tanzhenxin
f9da1b819e Merge branch 'main' into feat/ide-companion-discovery 2025-12-16 14:11:25 +08:00
pomelo-nwu
36fb6b8291 feat: update docs 2025-12-16 13:48:10 +08:00
xuewenjie
f47c762620 Merge branch 'main' of github.com:QwenLM/qwen-code into fix/windows-startup-and-exit-hangs 2025-12-16 11:35:13 +08:00
kefuxin
573c33f68a Merge remote-tracking branch 'upstream/main' into fix/issue-1186-schema-converter 2025-12-16 11:08:51 +08:00
yiliang114
32c085cf7d chore(vscode-ide-companion): update vscode engine version from ^1.99.0 to ^1.85.0 2025-12-15 23:54:26 +08:00
yiliang114
725843f9b3 fix(vscode-ide-companion): optimize stream termination handling and fix style layering issues 2025-12-15 23:41:36 +08:00
yiliang114
54fd63f04b fix(vscode-ide-companion): optimize stream termination handling and fix style layering issues
Unify stream termination using the `sendStreamEnd` method to avoid duplicate code.
Add stream termination reason identification and handling for timeout and session expiration scenarios.
Centralize cleanup logic for various stream termination states in the WebView message hooks.
Adjust Tailwind CSS styles to resolve component layering display issues.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-15 22:41:38 +08:00
tanzhenxin
59c3d3d0f9 IDE companion discovery: switch to ~/.qwen/ide lock files 2025-12-15 20:15:26 +08:00
xuewenjie
4f2b2d0a3e fix: optimize windows process tree retrieval to prevent hang 2025-12-12 15:10:16 +08:00
kefuxin
44794121a8 docs: update MCP server schema compliance documentation
Update documentation to reflect the new `schemaCompliance` setting and detailed OpenAPI 3.0 transformation rules.

Suggested-by: afarber
2025-12-12 10:38:00 +08:00
kefuxin
84cccfe99a feat: add i18n for schemaCompliance setting 2025-12-11 14:30:38 +08:00
kefuxin
b6a3ab11e0 fix: improve Gemini compatibility by adding configurable schema converter
This commit addresses issue #1186 by introducing a configurable schema compliance
mechanism for tool definitions sent to LLMs.

Key changes:
1.  **New Configuration**: Added `model.generationConfig.schemaCompliance` setting (defaults to 'auto', optional 'openapi_30').
2.  **Schema Converter**: Implemented `toOpenAPI30` converter in `packages/core` to strictly downgrade modern JSON Schema to OpenAPI 3.0.3 (required for Gemini API), handling:
    -   Nullable types (`["string", "null"]` -> `nullable: true`)
    -   Numeric exclusive limits
    -   Const to Enum conversion
    -   Removal of tuples and invalid keywords (``, `dependencies`, etc.)
3.  **Tests**: Added comprehensive unit tests for the schema converter and updated pipeline tests.

Fixes #1186
2025-12-11 14:23:27 +08:00
66 changed files with 1252 additions and 1036 deletions

View File

@@ -18,8 +18,6 @@ jobs:
- 'sandbox:docker' - 'sandbox:docker'
node-version: node-version:
- '20.x' - '20.x'
- '22.x'
- '24.x'
steps: steps:
- name: 'Checkout' - name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5 uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
@@ -67,10 +65,13 @@ jobs:
OPENAI_BASE_URL: '${{ secrets.OPENAI_BASE_URL }}' OPENAI_BASE_URL: '${{ secrets.OPENAI_BASE_URL }}'
OPENAI_MODEL: '${{ secrets.OPENAI_MODEL }}' OPENAI_MODEL: '${{ secrets.OPENAI_MODEL }}'
KEEP_OUTPUT: 'true' KEEP_OUTPUT: 'true'
SANDBOX: '${{ matrix.sandbox }}'
VERBOSE: 'true' VERBOSE: 'true'
run: |- run: |-
npm run "test:integration:${SANDBOX}" if [[ "${{ matrix.sandbox }}" == "sandbox:docker" ]]; then
npm run test:integration:sandbox:docker
else
npm run test:integration:sandbox:none
fi
e2e-test-macos: e2e-test-macos:
name: 'E2E Test - macOS' name: 'E2E Test - macOS'

View File

@@ -121,6 +121,11 @@ jobs:
IS_PREVIEW: '${{ steps.vars.outputs.is_preview }}' IS_PREVIEW: '${{ steps.vars.outputs.is_preview }}'
MANUAL_VERSION: '${{ inputs.version }}' MANUAL_VERSION: '${{ inputs.version }}'
- name: 'Build CLI Bundle'
run: |
npm run build
npm run bundle
- name: 'Run Tests' - name: 'Run Tests'
if: |- if: |-
${{ github.event.inputs.force_skip_tests != 'true' }} ${{ github.event.inputs.force_skip_tests != 'true' }}
@@ -132,13 +137,6 @@ jobs:
OPENAI_BASE_URL: '${{ secrets.OPENAI_BASE_URL }}' OPENAI_BASE_URL: '${{ secrets.OPENAI_BASE_URL }}'
OPENAI_MODEL: '${{ secrets.OPENAI_MODEL }}' OPENAI_MODEL: '${{ secrets.OPENAI_MODEL }}'
- name: 'Build CLI for Integration Tests'
if: |-
${{ github.event.inputs.force_skip_tests != 'true' }}
run: |
npm run build
npm run bundle
- name: 'Run SDK Integration Tests' - name: 'Run SDK Integration Tests'
if: |- if: |-
${{ github.event.inputs.force_skip_tests != 'true' }} ${{ github.event.inputs.force_skip_tests != 'true' }}

View File

@@ -133,8 +133,8 @@ jobs:
${{ github.event.inputs.force_skip_tests != 'true' }} ${{ github.event.inputs.force_skip_tests != 'true' }}
run: | run: |
npm run preflight npm run preflight
npm run test:integration:sandbox:none npm run test:integration:cli:sandbox:none
npm run test:integration:sandbox:docker npm run test:integration:cli:sandbox:docker
env: env:
OPENAI_API_KEY: '${{ secrets.OPENAI_API_KEY }}' OPENAI_API_KEY: '${{ secrets.OPENAI_API_KEY }}'
OPENAI_BASE_URL: '${{ secrets.OPENAI_BASE_URL }}' OPENAI_BASE_URL: '${{ secrets.OPENAI_BASE_URL }}'

View File

@@ -627,7 +627,12 @@ The MCP integration tracks several states:
### Schema Compatibility ### Schema Compatibility
- **Property stripping:** The system automatically removes certain schema properties (`$schema`, `additionalProperties`) for Qwen API compatibility - **Schema compliance mode:** By default (`schemaCompliance: "auto"`), tool schemas are passed through as-is. Set `"model": { "generationConfig": { "schemaCompliance": "openapi_30" } }` in your `settings.json` to convert models to Strict OpenAPI 3.0 format.
- **OpenAPI 3.0 transformations:** When `openapi_30` mode is enabled, the system handles:
- Nullable types: `["string", "null"]` -> `type: "string", nullable: true`
- Const values: `const: "foo"` -> `enum: ["foo"]`
- Exclusive limits: numeric `exclusiveMinimum` -> boolean form with `minimum`
- Keyword removal: `$schema`, `$id`, `dependencies`, `patternProperties`
- **Name sanitization:** Tool names are automatically sanitized to meet API requirements - **Name sanitization:** Tool names are automatically sanitized to meet API requirements
- **Conflict resolution:** Tool name conflicts between servers are resolved through automatic prefixing - **Conflict resolution:** Tool name conflicts between servers are resolved through automatic prefixing

View File

@@ -14,7 +14,7 @@ Learn how to use Qwen Code as an end user. This section covers:
- Configuration options - Configuration options
- Troubleshooting - Troubleshooting
### [Developer Guide](./developers/contributing) ### [Developer Guide](./developers/architecture)
Learn how to contribute to and develop Qwen Code. This section covers: Learn how to contribute to and develop Qwen Code. This section covers:

View File

@@ -189,8 +189,8 @@ Then select "create" and follow the prompts to define:
> - Create project-specific subagents in `.qwen/agents/` for team sharing > - Create project-specific subagents in `.qwen/agents/` for team sharing
> - Use descriptive `description` fields to enable automatic delegation > - Use descriptive `description` fields to enable automatic delegation
> - Limit tool access to what each subagent actually needs > - Limit tool access to what each subagent actually needs
> - Know more about [Sub Agents](/users/features/sub-agents) > - Know more about [Sub Agents](./features/sub-agents)
> - Know more about [Approval Mode](/users/features/approval-mode) > - Know more about [Approval Mode](./features/approval-mode)
## Work with tests ## Work with tests
@@ -318,7 +318,7 @@ This provides a directory listing with file information.
Show me the data from @github: repos/owner/repo/issues Show me the data from @github: repos/owner/repo/issues
``` ```
This fetches data from connected MCP servers using the format @server: resource. See [MCP](/users/features/mcp) for details. This fetches data from connected MCP servers using the format @server: resource. See [MCP](./features/mcp) for details.
> [!tip] > [!tip]
> >

View File

@@ -6,7 +6,7 @@ Qwen Code includes the ability to automatically ignore files, similar to `.gitig
## How it works ## How it works
When you add a path to your `.qwenignore` file, tools that respect this file will exclude matching files and directories from their operations. For example, when you use the [`read_many_files`](/developers/tools/multi-file) command, any paths in your `.qwenignore` file will be automatically excluded. When you add a path to your `.qwenignore` file, tools that respect this file will exclude matching files and directories from their operations. For example, when you use the [`read_many_files`](../../developers/tools/multi-file) command, any paths in your `.qwenignore` file will be automatically excluded.
For the most part, `.qwenignore` follows the conventions of `.gitignore` files: For the most part, `.qwenignore` follows the conventions of `.gitignore` files:

View File

@@ -2,7 +2,7 @@
> [!tip] > [!tip]
> >
> **Authentication / API keys:** Authentication (Qwen OAuth vs OpenAI-compatible API) and auth-related environment variables (like `OPENAI_API_KEY`) are documented in **[Authentication](/users/configuration/auth)**. > **Authentication / API keys:** Authentication (Qwen OAuth vs OpenAI-compatible API) and auth-related environment variables (like `OPENAI_API_KEY`) are documented in **[Authentication](../configuration/auth)**.
> [!note] > [!note]
> >
@@ -42,7 +42,7 @@ Qwen Code uses JSON settings files for persistent configuration. There are four
In addition to a project settings file, a project's `.qwen` directory can contain other project-specific files related to Qwen Code's operation, such as: In addition to a project settings file, a project's `.qwen` directory can contain other project-specific files related to Qwen Code's operation, such as:
- [Custom sandbox profiles](/users/features/sandbox) (e.g. `.qwen/sandbox-macos-custom.sb`, `.qwen/sandbox.Dockerfile`). - [Custom sandbox profiles](../features/sandbox) (e.g. `.qwen/sandbox-macos-custom.sb`, `.qwen/sandbox.Dockerfile`).
### Available settings in `settings.json` ### Available settings in `settings.json`
@@ -69,7 +69,7 @@ Settings are organized into categories. All settings should be placed within the
| Setting | Type | Description | Default | | Setting | Type | Description | Default |
| ---------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | | ---------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
| `ui.theme` | string | The color theme for the UI. See [Themes](/users/configuration/themes) for available options. | `undefined` | | `ui.theme` | string | The color theme for the UI. See [Themes](../configuration/themes) for available options. | `undefined` |
| `ui.customThemes` | object | Custom theme definitions. | `{}` | | `ui.customThemes` | object | Custom theme definitions. | `{}` |
| `ui.hideWindowTitle` | boolean | Hide the window title bar. | `false` | | `ui.hideWindowTitle` | boolean | Hide the window title bar. | `false` |
| `ui.hideTips` | boolean | Hide helpful tips in the UI. | `false` | | `ui.hideTips` | boolean | Hide helpful tips in the UI. | `false` |
@@ -326,7 +326,7 @@ The CLI keeps a history of shell commands you run. To avoid conflicts between di
Environment variables are a common way to configure applications, especially for sensitive information (like tokens) or for settings that might change between environments. Environment variables are a common way to configure applications, especially for sensitive information (like tokens) or for settings that might change between environments.
Qwen Code can automatically load environment variables from `.env` files. Qwen Code can automatically load environment variables from `.env` files.
For authentication-related variables (like `OPENAI_*`) and the recommended `.qwen/.env` approach, see **[Authentication](/users/configuration/auth)**. For authentication-related variables (like `OPENAI_*`) and the recommended `.qwen/.env` approach, see **[Authentication](../configuration/auth)**.
> [!tip] > [!tip]
> >
@@ -357,38 +357,38 @@ Arguments passed directly when running the CLI can override other configurations
### Command-Line Arguments Table ### Command-Line Arguments Table
| Argument | Alias | Description | Possible Values | Notes | | Argument | Alias | Description | Possible Values | Notes |
| ---------------------------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ---------------------------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--model` | `-m` | Specifies the Qwen model to use for this session. | Model name | Example: `npm start -- --model qwen3-coder-plus` | | `--model` | `-m` | Specifies the Qwen model to use for this session. | Model name | Example: `npm start -- --model qwen3-coder-plus` |
| `--prompt` | `-p` | Used to pass a prompt directly to the command. This invokes Qwen Code in a non-interactive mode. | Your prompt text | For scripting examples, use the `--output-format json` flag to get structured output. | | `--prompt` | `-p` | Used to pass a prompt directly to the command. This invokes Qwen Code in a non-interactive mode. | Your prompt text | For scripting examples, use the `--output-format json` flag to get structured output. |
| `--prompt-interactive` | `-i` | Starts an interactive session with the provided prompt as the initial input. | Your prompt text | The prompt is processed within the interactive session, not before it. Cannot be used when piping input from stdin. Example: `qwen -i "explain this code"` | | `--prompt-interactive` | `-i` | Starts an interactive session with the provided prompt as the initial input. | Your prompt text | The prompt is processed within the interactive session, not before it. Cannot be used when piping input from stdin. Example: `qwen -i "explain this code"` |
| `--output-format` | `-o` | Specifies the format of the CLI output for non-interactive mode. | `text`, `json`, `stream-json` | `text`: (Default) The standard human-readable output. `json`: A machine-readable JSON output emitted at the end of execution. `stream-json`: Streaming JSON messages emitted as they occur during execution. For structured output and scripting, use the `--output-format json` or `--output-format stream-json` flag. See [Headless Mode](/users/features/headless) for detailed information. | | `--output-format` | `-o` | Specifies the format of the CLI output for non-interactive mode. | `text`, `json`, `stream-json` | `text`: (Default) The standard human-readable output. `json`: A machine-readable JSON output emitted at the end of execution. `stream-json`: Streaming JSON messages emitted as they occur during execution. For structured output and scripting, use the `--output-format json` or `--output-format stream-json` flag. See [Headless Mode](../features/headless) for detailed information. |
| `--input-format` | | Specifies the format consumed from standard input. | `text`, `stream-json` | `text`: (Default) Standard text input from stdin or command-line arguments. `stream-json`: JSON message protocol via stdin for bidirectional communication. Requirement: `--input-format stream-json` requires `--output-format stream-json` to be set. When using `stream-json`, stdin is reserved for protocol messages. See [Headless Mode](/users/features/headless) for detailed information. | | `--input-format` | | Specifies the format consumed from standard input. | `text`, `stream-json` | `text`: (Default) Standard text input from stdin or command-line arguments. `stream-json`: JSON message protocol via stdin for bidirectional communication. Requirement: `--input-format stream-json` requires `--output-format stream-json` to be set. When using `stream-json`, stdin is reserved for protocol messages. See [Headless Mode](../features/headless) for detailed information. |
| `--include-partial-messages` | | Include partial assistant messages when using `stream-json` output format. When enabled, emits stream events (message_start, content_block_delta, etc.) as they occur during streaming. | | Default: `false`. Requirement: Requires `--output-format stream-json` to be set. See [Headless Mode](/users/features/headless) for detailed information about stream events. | | `--include-partial-messages` | | Include partial assistant messages when using `stream-json` output format. When enabled, emits stream events (message_start, content_block_delta, etc.) as they occur during streaming. | | Default: `false`. Requirement: Requires `--output-format stream-json` to be set. See [Headless Mode](../features/headless) for detailed information about stream events. |
| `--sandbox` | `-s` | Enables sandbox mode for this session. | | | | `--sandbox` | `-s` | Enables sandbox mode for this session. | | |
| `--sandbox-image` | | Sets the sandbox image URI. | | | | `--sandbox-image` | | Sets the sandbox image URI. | | |
| `--debug` | `-d` | Enables debug mode for this session, providing more verbose output. | | | | `--debug` | `-d` | Enables debug mode for this session, providing more verbose output. | | |
| `--all-files` | `-a` | If set, recursively includes all files within the current directory as context for the prompt. | | | | `--all-files` | `-a` | If set, recursively includes all files within the current directory as context for the prompt. | | |
| `--help` | `-h` | Displays help information about command-line arguments. | | | | `--help` | `-h` | Displays help information about command-line arguments. | | |
| `--show-memory-usage` | | Displays the current memory usage. | | | | `--show-memory-usage` | | Displays the current memory usage. | | |
| `--yolo` | | Enables YOLO mode, which automatically approves all tool calls. | | | | `--yolo` | | Enables YOLO mode, which automatically approves all tool calls. | | |
| `--approval-mode` | | Sets the approval mode for tool calls. | `plan`, `default`, `auto-edit`, `yolo` | Supported modes: `plan`: Analyze only—do not modify files or execute commands. `default`: Require approval for file edits or shell commands (default behavior). `auto-edit`: Automatically approve edit tools (edit, write_file) while prompting for others. `yolo`: Automatically approve all tool calls (equivalent to `--yolo`). Cannot be used together with `--yolo`. Use `--approval-mode=yolo` instead of `--yolo` for the new unified approach. Example: `qwen --approval-mode auto-edit`<br>See more about [Approval Mode](/users/features/approval-mode). | | `--approval-mode` | | Sets the approval mode for tool calls. | `plan`, `default`, `auto-edit`, `yolo` | Supported modes: `plan`: Analyze only—do not modify files or execute commands. `default`: Require approval for file edits or shell commands (default behavior). `auto-edit`: Automatically approve edit tools (edit, write_file) while prompting for others. `yolo`: Automatically approve all tool calls (equivalent to `--yolo`). Cannot be used together with `--yolo`. Use `--approval-mode=yolo` instead of `--yolo` for the new unified approach. Example: `qwen --approval-mode auto-edit`<br>See more about [Approval Mode](../features/approval-mode). |
| `--allowed-tools` | | A comma-separated list of tool names that will bypass the confirmation dialog. | Tool names | Example: `qwen --allowed-tools "Shell(git status)"` | | `--allowed-tools` | | A comma-separated list of tool names that will bypass the confirmation dialog. | Tool names | Example: `qwen --allowed-tools "Shell(git status)"` |
| `--telemetry` | | Enables [telemetry](/developers/development/telemetry). | | | | `--telemetry` | | Enables [telemetry](/developers/development/telemetry). | | |
| `--telemetry-target` | | Sets the telemetry target. | | See [telemetry](/developers/development/telemetry) for more information. | | `--telemetry-target` | | Sets the telemetry target. | | See [telemetry](/developers/development/telemetry) for more information. |
| `--telemetry-otlp-endpoint` | | Sets the OTLP endpoint for telemetry. | | See [telemetry](/developers/development/telemetry) for more information. | | `--telemetry-otlp-endpoint` | | Sets the OTLP endpoint for telemetry. | | See [telemetry](../../developers/development/telemetry) for more information. |
| `--telemetry-otlp-protocol` | | Sets the OTLP protocol for telemetry (`grpc` or `http`). | | Defaults to `grpc`. See [telemetry](/developers/development/telemetry) for more information. | | `--telemetry-otlp-protocol` | | Sets the OTLP protocol for telemetry (`grpc` or `http`). | | Defaults to `grpc`. See [telemetry](../../developers/development/telemetry) for more information. |
| `--telemetry-log-prompts` | | Enables logging of prompts for telemetry. | | See [telemetry](/developers/development/telemetry) for more information. | | `--telemetry-log-prompts` | | Enables logging of prompts for telemetry. | | See [telemetry](../../developers/development/telemetry) for more information. |
| `--checkpointing` | | Enables [checkpointing](/users/features/checkpointing). | | | | `--checkpointing` | | Enables [checkpointing](../features/checkpointing). | | |
| `--extensions` | `-e` | Specifies a list of extensions to use for the session. | Extension names | If not provided, all available extensions are used. Use the special term `qwen -e none` to disable all extensions. Example: `qwen -e my-extension -e my-other-extension` | | `--extensions` | `-e` | Specifies a list of extensions to use for the session. | Extension names | If not provided, all available extensions are used. Use the special term `qwen -e none` to disable all extensions. Example: `qwen -e my-extension -e my-other-extension` |
| `--list-extensions` | `-l` | Lists all available extensions and exits. | | | | `--list-extensions` | `-l` | Lists all available extensions and exits. | | |
| `--proxy` | | Sets the proxy for the CLI. | Proxy URL | Example: `--proxy http://localhost:7890`. | | `--proxy` | | Sets the proxy for the CLI. | Proxy URL | Example: `--proxy http://localhost:7890`. |
| `--include-directories` | | Includes additional directories in the workspace for multi-directory support. | Directory paths | Can be specified multiple times or as comma-separated values. 5 directories can be added at maximum. Example: `--include-directories /path/to/project1,/path/to/project2` or `--include-directories /path/to/project1 --include-directories /path/to/project2` | | `--include-directories` | | Includes additional directories in the workspace for multi-directory support. | Directory paths | Can be specified multiple times or as comma-separated values. 5 directories can be added at maximum. Example: `--include-directories /path/to/project1,/path/to/project2` or `--include-directories /path/to/project1 --include-directories /path/to/project2` |
| `--screen-reader` | | Enables screen reader mode, which adjusts the TUI for better compatibility with screen readers. | | | | `--screen-reader` | | Enables screen reader mode, which adjusts the TUI for better compatibility with screen readers. | | |
| `--version` | | Displays the version of the CLI. | | | | `--version` | | Displays the version of the CLI. | | |
| `--openai-logging` | | Enables logging of OpenAI API calls for debugging and analysis. | | This flag overrides the `enableOpenAILogging` setting in `settings.json`. | | `--openai-logging` | | Enables logging of OpenAI API calls for debugging and analysis. | | This flag overrides the `enableOpenAILogging` setting in `settings.json`. |
| `--openai-logging-dir` | | Sets a custom directory path for OpenAI API logs. | Directory path | This flag overrides the `openAILoggingDir` setting in `settings.json`. Supports absolute paths, relative paths, and `~` expansion. Example: `qwen --openai-logging-dir "~/qwen-logs" --openai-logging` | | `--openai-logging-dir` | | Sets a custom directory path for OpenAI API logs. | Directory path | This flag overrides the `openAILoggingDir` setting in `settings.json`. Supports absolute paths, relative paths, and `~` expansion. Example: `qwen --openai-logging-dir "~/qwen-logs" --openai-logging` |
| `--tavily-api-key` | | Sets the Tavily API key for web search functionality for this session. | API key | Example: `qwen --tavily-api-key tvly-your-api-key-here` | | `--tavily-api-key` | | Sets the Tavily API key for web search functionality for this session. | API key | Example: `qwen --tavily-api-key tvly-your-api-key-here` |
## Context Files (Hierarchical Instructional Context) ## Context Files (Hierarchical Instructional Context)
@@ -438,11 +438,11 @@ This example demonstrates how you can provide general project context, specific
- Location: The CLI also scans for the configured context file in subdirectories _below_ the current working directory (respecting common ignore patterns like `node_modules`, `.git`, etc.). The breadth of this search is limited to 200 directories by default, but can be configured with the `context.discoveryMaxDirs` setting in your `settings.json` file. - Location: The CLI also scans for the configured context file in subdirectories _below_ the current working directory (respecting common ignore patterns like `node_modules`, `.git`, etc.). The breadth of this search is limited to 200 directories by default, but can be configured with the `context.discoveryMaxDirs` setting in your `settings.json` file.
- Scope: Allows for highly specific instructions relevant to a particular component, module, or subsection of your project. - Scope: Allows for highly specific instructions relevant to a particular component, module, or subsection of your project.
- **Concatenation & UI Indication:** The contents of all found context files are concatenated (with separators indicating their origin and path) and provided as part of the system prompt. The CLI footer displays the count of loaded context files, giving you a quick visual cue about the active instructional context. - **Concatenation & UI Indication:** The contents of all found context files are concatenated (with separators indicating their origin and path) and provided as part of the system prompt. The CLI footer displays the count of loaded context files, giving you a quick visual cue about the active instructional context.
- **Importing Content:** You can modularize your context files by importing other Markdown files using the `@path/to/file.md` syntax. For more details, see the [Memory Import Processor documentation](/users/configuration/memory). - **Importing Content:** You can modularize your context files by importing other Markdown files using the `@path/to/file.md` syntax. For more details, see the [Memory Import Processor documentation](../configuration/memory).
- **Commands for Memory Management:** - **Commands for Memory Management:**
- Use `/memory refresh` to force a re-scan and reload of all context files from all configured locations. This updates the AI's instructional context. - Use `/memory refresh` to force a re-scan and reload of all context files from all configured locations. This updates the AI's instructional context.
- Use `/memory show` to display the combined instructional context currently loaded, allowing you to verify the hierarchy and content being used by the AI. - Use `/memory show` to display the combined instructional context currently loaded, allowing you to verify the hierarchy and content being used by the AI.
- See the [Commands documentation](/users/reference/cli-reference) for full details on the `/memory` command and its sub-commands (`show` and `refresh`). - See the [Commands documentation](../features/commands) for full details on the `/memory` command and its sub-commands (`show` and `refresh`).
By understanding and utilizing these configuration layers and the hierarchical nature of context files, you can effectively manage the AI's memory and tailor Qwen Code's responses to your specific needs and projects. By understanding and utilizing these configuration layers and the hierarchical nature of context files, you can effectively manage the AI's memory and tailor Qwen Code's responses to your specific needs and projects.
@@ -450,7 +450,7 @@ By understanding and utilizing these configuration layers and the hierarchical n
Qwen Code can execute potentially unsafe operations (like shell commands and file modifications) within a sandboxed environment to protect your system. Qwen Code can execute potentially unsafe operations (like shell commands and file modifications) within a sandboxed environment to protect your system.
[Sandbox](/users/features/sandbox) is disabled by default, but you can enable it in a few ways: [Sandbox](../features/sandbox) is disabled by default, but you can enable it in a few ways:
- Using `--sandbox` or `-s` flag. - Using `--sandbox` or `-s` flag.
- Setting `GEMINI_SANDBOX` environment variable. - Setting `GEMINI_SANDBOX` environment variable.

View File

@@ -32,7 +32,7 @@ Qwen Code comes with a selection of pre-defined themes, which you can list using
### Theme Persistence ### Theme Persistence
Selected themes are saved in Qwen Code's [configuration](./configuration.md) so your preference is remembered across sessions. Selected themes are saved in Qwen Code's [configuration](../configuration/settings) so your preference is remembered across sessions.
--- ---
@@ -146,7 +146,7 @@ The theme file must be a valid JSON file that follows the same structure as a cu
- Select your custom theme using the `/theme` command in Qwen Code. Your custom theme will appear in the theme selection dialog. - Select your custom theme using the `/theme` command in Qwen Code. Your custom theme will appear in the theme selection dialog.
- Or, set it as the default by adding `"theme": "MyCustomTheme"` to the `ui` object in your `settings.json`. - Or, set it as the default by adding `"theme": "MyCustomTheme"` to the `ui` object in your `settings.json`.
- Custom themes can be set at the user, project, or system level, and follow the same [configuration precedence](./configuration.md) as other settings. - Custom themes can be set at the user, project, or system level, and follow the same [configuration precedence](../configuration/settings) as other settings.
## Themes Preview ## Themes Preview

View File

@@ -56,6 +56,6 @@ If you need to change a decision or see all your settings, you have a couple of
For advanced users, it's helpful to know the exact order of operations for how trust is determined: For advanced users, it's helpful to know the exact order of operations for how trust is determined:
1. **IDE Trust Signal**: If you are using the [IDE Integration](/users/ide-integration/ide-integration), the CLI first asks the IDE if the workspace is trusted. The IDE's response takes highest priority. 1. **IDE Trust Signal**: If you are using the [IDE Integration](../ide-integration/ide-integration), the CLI first asks the IDE if the workspace is trusted. The IDE's response takes highest priority.
2. **Local Trust File**: If the IDE is not connected, the CLI checks the central `~/.qwen/trustedFolders.json` file. 2. **Local Trust File**: If the IDE is not connected, the CLI checks the central `~/.qwen/trustedFolders.json` file.

View File

@@ -1,3 +1,5 @@
# Approval Mode
Qwen Code offers three distinct permission modes that allow you to flexibly control how AI interacts with your code and system based on task complexity and risk level. Qwen Code offers three distinct permission modes that allow you to flexibly control how AI interacts with your code and system based on task complexity and risk level.
## Permission Modes Comparison ## Permission Modes Comparison

View File

@@ -203,7 +203,7 @@ Key command-line options for headless usage:
| `--continue` | Resume the most recent session for this project | `qwen --continue -p "Pick up where we left off"` | | `--continue` | Resume the most recent session for this project | `qwen --continue -p "Pick up where we left off"` |
| `--resume [sessionId]` | Resume a specific session (or choose interactively) | `qwen --resume 123e... -p "Finish the refactor"` | | `--resume [sessionId]` | Resume a specific session (or choose interactively) | `qwen --resume 123e... -p "Finish the refactor"` |
For complete details on all available configuration options, settings files, and environment variables, see the [Configuration Guide](/users/configuration/settings). For complete details on all available configuration options, settings files, and environment variables, see the [Configuration Guide](../configuration/settings).
## Examples ## Examples
@@ -276,7 +276,7 @@ tail -5 usage.log
## Resources ## Resources
- [CLI Configuration](/users/configuration/settings#command-line-arguments) - Complete configuration guide - [CLI Configuration](../configuration/settings#command-line-arguments) - Complete configuration guide
- [Authentication](/users/configuration/settings#environment-variables-for-api-access) - Setup authentication - [Authentication](../configuration/settings#environment-variables-for-api-access) - Setup authentication
- [Commands](/users/reference/cli-reference) - Interactive commands reference - [Commands](../features/commands) - Interactive commands reference
- [Tutorials](/users/quickstart) - Step-by-step automation guides - [Tutorials](../quickstart) - Step-by-step automation guides

View File

@@ -12,6 +12,7 @@ With MCP servers connected, you can ask Qwen Code to:
- Automate workflows (repeatable tasks exposed as tools/prompts) - Automate workflows (repeatable tasks exposed as tools/prompts)
> [!tip] > [!tip]
>
> If youre looking for the “one command to get started”, jump to [Quick start](#quick-start). > If youre looking for the “one command to get started”, jump to [Quick start](#quick-start).
## Quick start ## Quick start
@@ -51,7 +52,8 @@ qwen mcp add --scope user --transport http my-server http://localhost:3000/mcp
``` ```
> [!tip] > [!tip]
> For advanced configuration layers (system defaults/system settings and precedence rules), see [Settings](/users/configuration/settings). >
> For advanced configuration layers (system defaults/system settings and precedence rules), see [Settings](../configuration/settings).
## Configure servers ## Configure servers
@@ -64,6 +66,7 @@ qwen mcp add --scope user --transport http my-server http://localhost:3000/mcp
| `stdio` | Local process (scripts, CLIs, Docker) on your machine | `command`, `args` (+ optional `cwd`, `env`) | | `stdio` | Local process (scripts, CLIs, Docker) on your machine | `command`, `args` (+ optional `cwd`, `env`) |
> [!note] > [!note]
>
> If a server supports both, prefer **HTTP** over **SSE**. > If a server supports both, prefer **HTTP** over **SSE**.
### Configure via `settings.json` vs `qwen mcp add` ### Configure via `settings.json` vs `qwen mcp add`

View File

@@ -220,6 +220,6 @@ qwen -s -p "run shell command: mount | grep workspace"
## Related documentation ## Related documentation
- [Configuration](/users/configuration/settings): Full configuration options. - [Configuration](../configuration/settings): Full configuration options.
- [Commands](/users/reference/cli-reference): Available commands. - [Commands](../features/commands): Available commands.
- [Troubleshooting](/users/support/troubleshooting): General troubleshooting. - [Troubleshooting](../support/troubleshooting): General troubleshooting.

View File

@@ -16,16 +16,15 @@ The plugin **MUST** run a local HTTP server that implements the **Model Context
- **Endpoint:** The server should expose a single endpoint (e.g., `/mcp`) for all MCP communication. - **Endpoint:** The server should expose a single endpoint (e.g., `/mcp`) for all MCP communication.
- **Port:** The server **MUST** listen on a dynamically assigned port (i.e., listen on port `0`). - **Port:** The server **MUST** listen on a dynamically assigned port (i.e., listen on port `0`).
### 2. Discovery Mechanism: The Port File ### 2. Discovery Mechanism: The Lock File
For Qwen Code to connect, it needs to discover which IDE instance it's running in and what port your server is using. The plugin **MUST** facilitate this by creating a "discovery file." For Qwen Code to connect, it needs to discover what port your server is using. The plugin **MUST** facilitate this by creating a "lock file" and setting the port environment variable.
- **How the CLI Finds the File:** The CLI determines the Process ID (PID) of the IDE it's running in by traversing the process tree. It then looks for a discovery file that contains this PID in its name. - **How the CLI Finds the File:** The CLI reads the port from `QWEN_CODE_IDE_SERVER_PORT`, then reads `~/.qwen/ide/<PORT>.lock`. (Legacy fallbacks exist for older extensions; see note below.)
- **File Location:** The file must be created in a specific directory: `os.tmpdir()/qwen/ide/`. Your plugin must create this directory if it doesn't exist. - **File Location:** The file must be created in a specific directory: `~/.qwen/ide/`. Your plugin must create this directory if it doesn't exist.
- **File Naming Convention:** The filename is critical and **MUST** follow the pattern: - **File Naming Convention:** The filename is critical and **MUST** follow the pattern:
`qwen-code-ide-server-${PID}-${PORT}.json` `<PORT>.lock`
- `${PID}`: The process ID of the parent IDE process. Your plugin must determine this PID and include it in the filename. - `<PORT>`: The port your MCP server is listening on.
- `${PORT}`: The port your MCP server is listening on.
- **File Content & Workspace Validation:** The file **MUST** contain a JSON object with the following structure: - **File Content & Workspace Validation:** The file **MUST** contain a JSON object with the following structure:
```json ```json
@@ -33,21 +32,20 @@ For Qwen Code to connect, it needs to discover which IDE instance it's running i
"port": 12345, "port": 12345,
"workspacePath": "/path/to/project1:/path/to/project2", "workspacePath": "/path/to/project1:/path/to/project2",
"authToken": "a-very-secret-token", "authToken": "a-very-secret-token",
"ideInfo": { "ppid": 1234,
"name": "vscode", "ideName": "VS Code"
"displayName": "VS Code"
}
} }
``` ```
- `port` (number, required): The port of the MCP server. - `port` (number, required): The port of the MCP server.
- `workspacePath` (string, required): A list of all open workspace root paths, delimited by the OS-specific path separator (`:` for Linux/macOS, `;` for Windows). The CLI uses this path to ensure it's running in the same project folder that's open in the IDE. If the CLI's current working directory is not a sub-directory of `workspacePath`, the connection will be rejected. Your plugin **MUST** provide the correct, absolute path(s) to the root of the open workspace(s). - `workspacePath` (string, required): A list of all open workspace root paths, delimited by the OS-specific path separator (`:` for Linux/macOS, `;` for Windows). The CLI uses this path to ensure it's running in the same project folder that's open in the IDE. If the CLI's current working directory is not a sub-directory of `workspacePath`, the connection will be rejected. Your plugin **MUST** provide the correct, absolute path(s) to the root of the open workspace(s).
- `authToken` (string, required): A secret token for securing the connection. The CLI will include this token in an `Authorization: Bearer <token>` header on all requests. - `authToken` (string, required): A secret token for securing the connection. The CLI will include this token in an `Authorization: Bearer <token>` header on all requests.
- `ideInfo` (object, required): Information about the IDE. - `ppid` (number, required): The parent process ID of the IDE process.
- `name` (string, required): A short, lowercase identifier for the IDE (e.g., `vscode`, `jetbrains`). - `ideName` (string, required): A user-friendly name for the IDE (e.g., `VS Code`, `JetBrains IDE`).
- `displayName` (string, required): A user-friendly name for the IDE (e.g., `VS Code`, `JetBrains IDE`).
- **Authentication:** To secure the connection, the plugin **MUST** generate a unique, secret token and include it in the discovery file. The CLI will then include this token in the `Authorization` header for all requests to the MCP server (e.g., `Authorization: Bearer a-very-secret-token`). Your server **MUST** validate this token on every request and reject any that are unauthorized. - **Authentication:** To secure the connection, the plugin **MUST** generate a unique, secret token and include it in the discovery file. The CLI will then include this token in the `Authorization` header for all requests to the MCP server (e.g., `Authorization: Bearer a-very-secret-token`). Your server **MUST** validate this token on every request and reject any that are unauthorized.
- **Tie-Breaking with Environment Variables (Recommended):** For the most reliable experience, your plugin **SHOULD** both create the discovery file and set the `QWEN_CODE_IDE_SERVER_PORT` environment variable in the integrated terminal. The file serves as the primary discovery mechanism, but the environment variable is crucial for tie-breaking. If a user has multiple IDE windows open for the same workspace, the CLI uses the `QWEN_CODE_IDE_SERVER_PORT` variable to identify and connect to the correct window's server. - **Environment Variables (Required):** Your plugin **MUST** set `QWEN_CODE_IDE_SERVER_PORT` in the integrated terminal so the CLI can locate the correct `<PORT>.lock` file.
**Legacy note:** For extensions older than v0.5.1, Qwen Code may fall back to reading JSON files in the system temp directory named `qwen-code-ide-server-<PID>.json` or `qwen-code-ide-server-<PORT>.json`. New integrations should not rely on these legacy files.
## II. The Context Interface ## II. The Context Interface

View File

@@ -2,7 +2,7 @@
Qwen Code can integrate with your IDE to provide a more seamless and context-aware experience. This integration allows the CLI to understand your workspace better and enables powerful features like native in-editor diffing. Qwen Code can integrate with your IDE to provide a more seamless and context-aware experience. This integration allows the CLI to understand your workspace better and enables powerful features like native in-editor diffing.
Currently, the only supported IDE is [Visual Studio Code](https://code.visualstudio.com/) and other editors that support VS Code extensions. To build support for other editors, see the [IDE Companion Extension Spec](/users/ide-integration/ide-companion-spec). Currently, the only supported IDE is [Visual Studio Code](https://code.visualstudio.com/) and other editors that support VS Code extensions. To build support for other editors, see the [IDE Companion Extension Spec](../ide-integration/ide-companion-spec).
## Features ## Features

View File

@@ -6,41 +6,14 @@
Use it to perform GitHub pull request reviews, triage issues, perform code analysis and modification, and more using [Qwen Code] conversationally (e.g., `@qwencoder fix this issue`) directly inside your GitHub repositories. Use it to perform GitHub pull request reviews, triage issues, perform code analysis and modification, and more using [Qwen Code] conversationally (e.g., `@qwencoder fix this issue`) directly inside your GitHub repositories.
- [qwen-code-action](#qwen-code-action)
- [Overview](#overview)
- [Features](#features)
- [Quick Start](#quick-start)
- [1. Get a Qwen API Key](#1-get-a-qwen-api-key)
- [2. Add it as a GitHub Secret](#2-add-it-as-a-github-secret)
- [3. Update your .gitignore](#3-update-your-gitignore)
- [4. Choose a Workflow](#4-choose-a-workflow)
- [5. Try it out](#5-try-it-out)
- [Workflows](#workflows)
- [Qwen Code Dispatch](#qwen-code-dispatch)
- [Issue Triage](#issue-triage)
- [Pull Request Review](#pull-request-review)
- [Qwen Code CLI Assistant](#qwen-code-cli-assistant)
- [Configuration](#configuration)
- [Inputs](#inputs)
- [Outputs](#outputs)
- [Repository Variables](#repository-variables)
- [Secrets](#secrets)
- [Authentication](#authentication)
- [GitHub Authentication](#github-authentication)
- [Extensions](#extensions)
- [Best Practices](#best-practices)
- [Customization](#customization)
- [Contributing](#contributing)
## Features ## Features
- **Automation**: Trigger workflows based on events (e.g. issue opening) or schedules (e.g. nightly). - **Automation**: Trigger workflows based on events (e.g. issue opening) or schedules (e.g. nightly).
- **On-demand Collaboration**: Trigger workflows in issue and pull request - **On-demand Collaboration**: Trigger workflows in issue and pull request
comments by mentioning the [Qwen Code CLI] (e.g., `@qwencoder /review`). comments by mentioning the [Qwen Code CLI](./features/commands) (e.g., `@qwencoder /review`).
- **Extensible with Tools**: Leverage [Qwen Code] models' tool-calling capabilities to - **Extensible with Tools**: Leverage [Qwen Code](../developers/tools/introduction.md) models' tool-calling capabilities to interact with other CLIs like the [GitHub CLI] (`gh`).
interact with other CLIs like the [GitHub CLI] (`gh`).
- **Customizable**: Use a `QWEN.md` file in your repository to provide - **Customizable**: Use a `QWEN.md` file in your repository to provide
project-specific instructions and context to [Qwen Code CLI]. project-specific instructions and context to [Qwen Code CLI](./features/commands).
## Quick Start ## Quick Start
@@ -48,7 +21,7 @@ Get started with Qwen Code CLI in your repository in just a few minutes:
### 1. Get a Qwen API Key ### 1. Get a Qwen API Key
Obtain your API key from [DashScope] (Alibaba Cloud's AI platform) Obtain your API key from [DashScope](https://help.aliyun.com/zh/model-studio/qwen-code) (Alibaba Cloud's AI platform)
### 2. Add it as a GitHub Secret ### 2. Add it as a GitHub Secret
@@ -90,7 +63,7 @@ You have two options to set up a workflow:
**Option B: Manually copy workflows** **Option B: Manually copy workflows**
1. Copy the pre-built workflows from the [`examples/workflows`](./examples/workflows) directory to your repository's `.github/workflows` directory. Note: the `qwen-dispatch.yml` workflow must also be copied, which triggers the workflows to run. 1. Copy the pre-built workflows from the [`examples/workflows`](./common-workflow) directory to your repository's `.github/workflows` directory. Note: the `qwen-dispatch.yml` workflow must also be copied, which triggers the workflows to run.
### 5. Try it out ### 5. Try it out
@@ -119,30 +92,19 @@ This action provides several pre-built workflows for different use cases. Each w
### Qwen Code Dispatch ### Qwen Code Dispatch
This workflow acts as a central dispatcher for Qwen Code CLI, routing requests to This workflow acts as a central dispatcher for Qwen Code CLI, routing requests to the appropriate workflow based on the triggering event and the command provided in the comment. For a detailed guide on how to set up the dispatch workflow, go to the [Qwen Code Dispatch workflow documentation](./common-workflow).
the appropriate workflow based on the triggering event and the command provided
in the comment. For a detailed guide on how to set up the dispatch workflow, go
to the
[Qwen Code Dispatch workflow documentation](./examples/workflows/qwen-dispatch).
### Issue Triage ### Issue Triage
This action can be used to triage GitHub Issues automatically or on a schedule. This action can be used to triage GitHub Issues automatically or on a schedule. For a detailed guide on how to set up the issue triage system, go to the [GitHub Issue Triage workflow documentation](./examples/workflows/issue-triage).
For a detailed guide on how to set up the issue triage system, go to the
[GitHub Issue Triage workflow documentation](./examples/workflows/issue-triage).
### Pull Request Review ### Pull Request Review
This action can be used to automatically review pull requests when they are This action can be used to automatically review pull requests when they are opened. For a detailed guide on how to set up the pull request review system, go to the [GitHub PR Review workflow documentation](./common-workflow).
opened. For a detailed guide on how to set up the pull request review system,
go to the [GitHub PR Review workflow documentation](./examples/workflows/pr-review).
### Qwen Code CLI Assistant ### Qwen Code CLI Assistant
This type of action can be used to invoke a general-purpose, conversational Qwen Code This type of action can be used to invoke a general-purpose, conversational Qwen Code AI assistant within the pull requests and issues to perform a wide range of tasks. For a detailed guide on how to set up the general-purpose Qwen Code CLI workflow, go to the [Qwen Code Assistant workflow documentation](./common-workflow).
AI assistant within the pull requests and issues to perform a wide range of
tasks. For a detailed guide on how to set up the general-purpose Qwen Code CLI workflow,
go to the [Qwen Code Assistant workflow documentation](./examples/workflows/qwen-assistant).
## Configuration ## Configuration
@@ -222,8 +184,7 @@ To add a secret:
2. Enter the secret name and value. 2. Enter the secret name and value.
3. Save. 3. Save.
For more information, refer to the For more information, refer to the [official GitHub documentation on creating and using encrypted secrets][secrets].
[official GitHub documentation on creating and using encrypted secrets][secrets].
## Authentication ## Authentication
@@ -239,7 +200,7 @@ You can authenticate with GitHub in two ways:
authentication, we recommend creating a custom GitHub App. authentication, we recommend creating a custom GitHub App.
For detailed setup instructions for both Qwen and GitHub authentication, go to the For detailed setup instructions for both Qwen and GitHub authentication, go to the
[**Authentication documentation**](./docs/authentication.md). [**Authentication documentation**](./configuration/auth).
## Extensions ## Extensions
@@ -247,7 +208,7 @@ The Qwen Code CLI can be extended with additional functionality through extensio
These extensions are installed from source from their GitHub repositories. These extensions are installed from source from their GitHub repositories.
For detailed instructions on how to set up and configure extensions, go to the For detailed instructions on how to set up and configure extensions, go to the
[Extensions documentation](./docs/extensions.md). [Extensions documentation](../developers/extensions/extension).
## Best Practices ## Best Practices
@@ -258,20 +219,18 @@ Key recommendations include:
- **Securing Your Repository:** Implementing branch and tag protection, and restricting pull request approvers. - **Securing Your Repository:** Implementing branch and tag protection, and restricting pull request approvers.
- **Monitoring and Auditing:** Regularly reviewing action logs and enabling OpenTelemetry for deeper insights into performance and behavior. - **Monitoring and Auditing:** Regularly reviewing action logs and enabling OpenTelemetry for deeper insights into performance and behavior.
For a comprehensive guide on securing your repository and workflows, please refer to our [**Best Practices documentation**](./docs/best-practices.md). For a comprehensive guide on securing your repository and workflows, please refer to our [**Best Practices documentation**](./common-workflow).
## Customization ## Customization
Create a [QWEN.md] file in the root of your repository to provide Create a QWEN.md file in the root of your repository to provide
project-specific context and instructions to [Qwen Code CLI]. This is useful for defining project-specific context and instructions to [Qwen Code CLI](./common-workflow). This is useful for defining
coding conventions, architectural patterns, or other guidelines the model should coding conventions, architectural patterns, or other guidelines the model should
follow for a given repository. follow for a given repository.
## Contributing ## Contributing
Contributions are welcome! Check out the Qwen Code CLI Contributions are welcome! Check out the Qwen Code CLI **Contributing Guide** for more details on how to get started.
[**Contributing Guide**](./CONTRIBUTING.md) for more details on how to get
started.
[secrets]: https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions [secrets]: https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions
[Qwen Code]: https://github.com/QwenLM/qwen-code [Qwen Code]: https://github.com/QwenLM/qwen-code

View File

@@ -4,7 +4,7 @@
<br/> <br/>
<video src="https://cloud.video.taobao.com/vod/JnvYMhUia2EKFAaiuErqNpzWE9mz3odG76vArAHNg94.mp4" controls width="800"> <video src="https://cloud.video.taobao.com/vod/IKKwfM-kqNI3OJjM_U8uMCSMAoeEcJhs6VNCQmZxUfk.mp4" controls width="800">
Your browser does not support the video tag. Your browser does not support the video tag.
</video> </video>

View File

@@ -7,7 +7,7 @@
### Features ### Features
- **Native agent experience**: Integrated AI assistant panel within Zed's interface - **Native agent experience**: Integrated AI assistant panel within Zed's interface
- **Agent Control Protocol**: Full support for ACP enabling advanced IDE interactions - **Agent Client Protocol**: Full support for ACP enabling advanced IDE interactions
- **File management**: @-mention files to add them to the conversation context - **File management**: @-mention files to add them to the conversation context
- **Conversation history**: Access to past conversations within Zed - **Conversation history**: Access to past conversations within Zed

View File

@@ -36,13 +36,13 @@ Select **Qwen OAuth (Free)** authentication and follow the prompts to log in. Th
what does this project do? what does this project do?
``` ```
![](https://gw.alicdn.com/imgextra/i2/O1CN01XoPbZm1CrsZzvMQ6m_!!6000000000135-1-tps-772-646.gif) ![](https://cloud.video.taobao.com/vod/j7-QtQScn8UEAaEdiv619fSkk5p-t17orpDbSqKVL5A.mp4)
You'll be prompted to log in on first use. That's it! [Continue with Quickstart (5 mins) →](/users/quickstart) You'll be prompted to log in on first use. That's it! [Continue with Quickstart (5 mins) →](./quickstart)
> [!tip] > [!tip]
> >
> See [troubleshooting](/users/support/troubleshooting) if you hit issues. > See [troubleshooting](./support/troubleshooting) if you hit issues.
> [!note] > [!note]
> >
@@ -52,11 +52,11 @@ You'll be prompted to log in on first use. That's it! [Continue with Quickstart
- **Build features from descriptions**: Tell Qwen Code what you want to build in plain language. It will make a plan, write the code, and ensure it works. - **Build features from descriptions**: Tell Qwen Code what you want to build in plain language. It will make a plan, write the code, and ensure it works.
- **Debug and fix issues**: Describe a bug or paste an error message. Qwen Code will analyze your codebase, identify the problem, and implement a fix. - **Debug and fix issues**: Describe a bug or paste an error message. Qwen Code will analyze your codebase, identify the problem, and implement a fix.
- **Navigate any codebase**: Ask anything about your team's codebase, and get a thoughtful answer back. Qwen Code maintains awareness of your entire project structure, can find up-to-date information from the web, and with [MCP](/users/features/mcp) can pull from external datasources like Google Drive, Figma, and Slack. - **Navigate any codebase**: Ask anything about your team's codebase, and get a thoughtful answer back. Qwen Code maintains awareness of your entire project structure, can find up-to-date information from the web, and with [MCP](./features/mcp) can pull from external datasources like Google Drive, Figma, and Slack.
- **Automate tedious tasks**: Fix fiddly lint issues, resolve merge conflicts, and write release notes. Do all this in a single command from your developer machines, or automatically in CI. - **Automate tedious tasks**: Fix fiddly lint issues, resolve merge conflicts, and write release notes. Do all this in a single command from your developer machines, or automatically in CI.
## Why developers love Qwen Code ## Why developers love Qwen Code
- **Works in your terminal**: Not another chat window. Not another IDE. Qwen Code meets you where you already work, with the tools you already love. - **Works in your terminal**: Not another chat window. Not another IDE. Qwen Code meets you where you already work, with the tools you already love.
- **Takes action**: Qwen Code can directly edit files, run commands, and create commits. Need more? [MCP](/users/features/mcp) lets Qwen Code read your design docs in Google Drive, update your tickets in Jira, or use _your_ custom developer tooling. - **Takes action**: Qwen Code can directly edit files, run commands, and create commits. Need more? [MCP](./features/mcp) lets Qwen Code read your design docs in Google Drive, update your tickets in Jira, or use _your_ custom developer tooling.
- **Unix philosophy**: Qwen Code is composable and scriptable. `tail -f app.log | qwen -p "Slack me if you see any anomalies appear in this log stream"` _works_. Your CI can run `qwen -p "If there are new text strings, translate them into French and raise a PR for @lang-fr-team to review"`. - **Unix philosophy**: Qwen Code is composable and scriptable. `tail -f app.log | qwen -p "Slack me if you see any anomalies appear in this log stream"` _works_. Your CI can run `qwen -p "If there are new text strings, translate them into French and raise a PR for @lang-fr-team to review"`.

View File

@@ -206,7 +206,7 @@ Here are the most important commands for daily use:
| → `output [language]` | Set LLM output language | `/language output Chinese` | | → `output [language]` | Set LLM output language | `/language output Chinese` |
| `/quit` | Exit Qwen Code immediately | `/quit` or `/exit` | | `/quit` | Exit Qwen Code immediately | `/quit` or `/exit` |
See the [CLI reference](/users/reference/cli-reference) for a complete list of commands. See the [CLI reference](./features/commands) for a complete list of commands.
## Pro tips for beginners ## Pro tips for beginners
@@ -225,9 +225,9 @@ See the [CLI reference](/users/reference/cli-reference) for a complete list of c
3. build a webpage that allows users to see and edit their information 3. build a webpage that allows users to see and edit their information
``` ```
**Let Claude explore first** **Let Qwen Code explore first**
- Before making changes, let Claude understand your code: - Before making changes, let Qwen Code understand your code:
``` ```
analyze the database schema analyze the database schema

View File

@@ -23,7 +23,7 @@ When you authenticate using your qwen.ai account, these Terms of Service and Pri
- **Terms of Service:** Your use is governed by the [Qwen Terms of Service](https://qwen.ai/termsservice). - **Terms of Service:** Your use is governed by the [Qwen Terms of Service](https://qwen.ai/termsservice).
- **Privacy Notice:** The collection and use of your data is described in the [Qwen Privacy Policy](https://qwen.ai/privacypolicy). - **Privacy Notice:** The collection and use of your data is described in the [Qwen Privacy Policy](https://qwen.ai/privacypolicy).
For details about authentication setup, quotas, and supported features, see [Authentication Setup](/users/configuration/settings). For details about authentication setup, quotas, and supported features, see [Authentication Setup](../configuration/settings).
## 2. If you are using OpenAI-Compatible API Authentication ## 2. If you are using OpenAI-Compatible API Authentication
@@ -37,7 +37,7 @@ Qwen Code supports various OpenAI-compatible providers. Please refer to your spe
## Usage Statistics and Telemetry ## Usage Statistics and Telemetry
Qwen Code may collect anonymous usage statistics and [telemetry](/developers/development/telemetry) data to improve the user experience and product quality. This data collection is optional and can be controlled through configuration settings. Qwen Code may collect anonymous usage statistics and [telemetry](../../developers/development/telemetry) data to improve the user experience and product quality. This data collection is optional and can be controlled through configuration settings.
### What Data is Collected ### What Data is Collected
@@ -91,4 +91,4 @@ You can switch between Qwen OAuth and OpenAI-compatible API authentication at an
2. **Within the CLI**: Use the `/auth` command to reconfigure your authentication method 2. **Within the CLI**: Use the `/auth` command to reconfigure your authentication method
3. **Environment variables**: Set up `.env` files for automatic OpenAI-compatible API authentication 3. **Environment variables**: Set up `.env` files for automatic OpenAI-compatible API authentication
For detailed instructions, see the [Authentication Setup](/users/configuration/settings#environment-variables-for-api-access) documentation. For detailed instructions, see the [Authentication Setup](../configuration/settings#environment-variables-for-api-access) documentation.

View File

@@ -31,7 +31,7 @@ This guide provides solutions to common issues and debugging tips, including top
1. In your home directory: `~/.qwen/settings.json`. 1. In your home directory: `~/.qwen/settings.json`.
2. In your project's root directory: `./.qwen/settings.json`. 2. In your project's root directory: `./.qwen/settings.json`.
Refer to [Qwen Code Configuration](/users/configuration/settings) for more details. Refer to [Qwen Code Configuration](../configuration/settings) for more details.
- **Q: Why don't I see cached token counts in my stats output?** - **Q: Why don't I see cached token counts in my stats output?**
- A: Cached token information is only displayed when cached tokens are being used. This feature is available for API key users (Qwen API key or Google Cloud Vertex AI) but not for OAuth users (such as Google Personal/Enterprise accounts like Google Gmail or Google Workspace, respectively). This is because the Qwen Code Assist API does not support cached content creation. You can still view your total token usage using the `/stats` command. - A: Cached token information is only displayed when cached tokens are being used. This feature is available for API key users (Qwen API key or Google Cloud Vertex AI) but not for OAuth users (such as Google Personal/Enterprise accounts like Google Gmail or Google Workspace, respectively). This is because the Qwen Code Assist API does not support cached content creation. You can still view your total token usage using the `/stats` command.
@@ -59,7 +59,7 @@ This guide provides solutions to common issues and debugging tips, including top
- **Error: "Operation not permitted", "Permission denied", or similar.** - **Error: "Operation not permitted", "Permission denied", or similar.**
- **Cause:** When sandboxing is enabled, Qwen Code may attempt operations that are restricted by your sandbox configuration, such as writing outside the project directory or system temp directory. - **Cause:** When sandboxing is enabled, Qwen Code may attempt operations that are restricted by your sandbox configuration, such as writing outside the project directory or system temp directory.
- **Solution:** Refer to the [Configuration: Sandboxing](/users/features/sandbox) documentation for more information, including how to customize your sandbox configuration. - **Solution:** Refer to the [Configuration: Sandboxing](../features/sandbox) documentation for more information, including how to customize your sandbox configuration.
- **Qwen Code is not running in interactive mode in "CI" environments** - **Qwen Code is not running in interactive mode in "CI" environments**
- **Issue:** Qwen Code does not enter interactive mode (no prompt appears) if an environment variable starting with `CI_` (e.g. `CI_TOKEN`) is set. This is because the `is-in-ci` package, used by the underlying UI framework, detects these variables and assumes a non-interactive CI environment. - **Issue:** Qwen Code does not enter interactive mode (no prompt appears) if an environment variable starting with `CI_` (e.g. `CI_TOKEN`) is set. This is because the `is-in-ci` package, used by the underlying UI framework, detects these variables and assumes a non-interactive CI environment.

87
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "@qwen-code/qwen-code", "name": "@qwen-code/qwen-code",
"version": "0.5.1", "version": "0.6.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@qwen-code/qwen-code", "name": "@qwen-code/qwen-code",
"version": "0.5.1", "version": "0.6.0",
"workspaces": [ "workspaces": [
"packages/*" "packages/*"
], ],
@@ -568,7 +568,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
}, },
@@ -592,7 +591,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@@ -2157,7 +2155,6 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"engines": { "engines": {
"node": ">=8.0.0" "node": ">=8.0.0"
} }
@@ -3671,7 +3668,6 @@
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.10.4", "@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5", "@babel/runtime": "^7.12.5",
@@ -4142,7 +4138,6 @@
"integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"csstype": "^3.0.2" "csstype": "^3.0.2"
} }
@@ -4153,7 +4148,6 @@
"integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==", "integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"peerDependencies": { "peerDependencies": {
"@types/react": "^19.0.0" "@types/react": "^19.0.0"
} }
@@ -4359,7 +4353,6 @@
"integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==", "integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.35.0", "@typescript-eslint/scope-manager": "8.35.0",
"@typescript-eslint/types": "8.35.0", "@typescript-eslint/types": "8.35.0",
@@ -5135,7 +5128,6 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@@ -5530,7 +5522,8 @@
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/array-includes": { "node_modules/array-includes": {
"version": "3.1.9", "version": "3.1.9",
@@ -6865,6 +6858,7 @@
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"safe-buffer": "5.2.1" "safe-buffer": "5.2.1"
}, },
@@ -7982,7 +7976,6 @@
"integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==", "integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
@@ -8518,6 +8511,7 @@
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"accepts": "~1.3.8", "accepts": "~1.3.8",
"array-flatten": "1.1.1", "array-flatten": "1.1.1",
@@ -8579,6 +8573,7 @@
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
} }
@@ -8588,6 +8583,7 @@
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"ms": "2.0.0" "ms": "2.0.0"
} }
@@ -8597,6 +8593,7 @@
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">= 0.8" "node": ">= 0.8"
} }
@@ -8763,6 +8760,7 @@
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"debug": "2.6.9", "debug": "2.6.9",
"encodeurl": "~2.0.0", "encodeurl": "~2.0.0",
@@ -8781,6 +8779,7 @@
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"ms": "2.0.0" "ms": "2.0.0"
} }
@@ -8789,13 +8788,15 @@
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/finalhandler/node_modules/statuses": { "node_modules/finalhandler/node_modules/statuses": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">= 0.8" "node": ">= 0.8"
} }
@@ -9909,7 +9910,6 @@
"resolved": "https://registry.npmjs.org/ink/-/ink-6.2.3.tgz", "resolved": "https://registry.npmjs.org/ink/-/ink-6.2.3.tgz",
"integrity": "sha512-fQkfEJjKbLXIcVWEE3MvpYSnwtbbmRsmeNDNz1pIuOFlwE+UF2gsy228J36OXKZGWJWZJKUigphBSqCNMcARtg==", "integrity": "sha512-fQkfEJjKbLXIcVWEE3MvpYSnwtbbmRsmeNDNz1pIuOFlwE+UF2gsy228J36OXKZGWJWZJKUigphBSqCNMcARtg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@alcalzone/ansi-tokenize": "^0.2.0", "@alcalzone/ansi-tokenize": "^0.2.0",
"ansi-escapes": "^7.0.0", "ansi-escapes": "^7.0.0",
@@ -11864,6 +11864,7 @@
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
} }
@@ -13162,7 +13163,8 @@
"version": "0.1.12", "version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/path-type": { "node_modules/path-type": {
"version": "3.0.0", "version": "3.0.0",
@@ -13821,7 +13823,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -13832,7 +13833,6 @@
"integrity": "sha512-cq/o30z9W2Wb4rzBefjv5fBalHU0rJGZCHAkf/RHSBWSSYwh8PlQTqqOJmgIIbBtpj27T6FIPXeomIjZtCNVqA==", "integrity": "sha512-cq/o30z9W2Wb4rzBefjv5fBalHU0rJGZCHAkf/RHSBWSSYwh8PlQTqqOJmgIIbBtpj27T6FIPXeomIjZtCNVqA==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"shell-quote": "^1.6.1", "shell-quote": "^1.6.1",
"ws": "^7" "ws": "^7"
@@ -13866,7 +13866,6 @@
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"scheduler": "^0.26.0" "scheduler": "^0.26.0"
}, },
@@ -15932,7 +15931,6 @@
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -16112,8 +16110,7 @@
"version": "2.8.1", "version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD", "license": "0BSD"
"peer": true
}, },
"node_modules/tsx": { "node_modules/tsx": {
"version": "4.20.3", "version": "4.20.3",
@@ -16121,7 +16118,6 @@
"integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==", "integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "~0.25.0", "esbuild": "~0.25.0",
"get-tsconfig": "^4.7.5" "get-tsconfig": "^4.7.5"
@@ -16316,7 +16312,6 @@
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -16391,6 +16386,7 @@
"version": "7.15.0", "version": "7.15.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.15.0.tgz", "resolved": "https://registry.npmjs.org/undici/-/undici-7.15.0.tgz",
"integrity": "sha512-7oZJCPvvMvTd0OlqWsIxTuItTpJBpU1tcbVl24FMn3xt3+VSunwUasmfPJRE57oNO1KsZ4PgA1xTdAX4hq8NyQ==", "integrity": "sha512-7oZJCPvvMvTd0OlqWsIxTuItTpJBpU1tcbVl24FMn3xt3+VSunwUasmfPJRE57oNO1KsZ4PgA1xTdAX4hq8NyQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=20.18.1" "node": ">=20.18.1"
@@ -16623,6 +16619,7 @@
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">= 0.4.0" "node": ">= 0.4.0"
} }
@@ -16678,7 +16675,6 @@
"integrity": "sha512-ixXJB1YRgDIw2OszKQS9WxGHKwLdCsbQNkpJN171udl6szi/rIySHL6/Os3s2+oE4P/FLD4dxg4mD7Wust+u5g==", "integrity": "sha512-ixXJB1YRgDIw2OszKQS9WxGHKwLdCsbQNkpJN171udl6szi/rIySHL6/Os3s2+oE4P/FLD4dxg4mD7Wust+u5g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"fdir": "^6.4.6", "fdir": "^6.4.6",
@@ -16792,7 +16788,6 @@
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -16806,7 +16801,6 @@
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/chai": "^5.2.2", "@types/chai": "^5.2.2",
"@vitest/expect": "3.2.4", "@vitest/expect": "3.2.4",
@@ -17485,7 +17479,6 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }
@@ -17501,7 +17494,7 @@
}, },
"packages/cli": { "packages/cli": {
"name": "@qwen-code/qwen-code", "name": "@qwen-code/qwen-code",
"version": "0.5.1", "version": "0.6.0",
"dependencies": { "dependencies": {
"@google/genai": "1.16.0", "@google/genai": "1.16.0",
"@iarna/toml": "^2.2.5", "@iarna/toml": "^2.2.5",
@@ -17532,7 +17525,7 @@
"strip-ansi": "^7.1.0", "strip-ansi": "^7.1.0",
"strip-json-comments": "^3.1.1", "strip-json-comments": "^3.1.1",
"tar": "^7.5.2", "tar": "^7.5.2",
"undici": "^7.10.0", "undici": "^6.22.0",
"update-notifier": "^7.3.1", "update-notifier": "^7.3.1",
"wrap-ansi": "9.0.2", "wrap-ansi": "9.0.2",
"yargs": "^17.7.2", "yargs": "^17.7.2",
@@ -17614,9 +17607,18 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"packages/cli/node_modules/undici": {
"version": "6.22.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.22.0.tgz",
"integrity": "sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==",
"license": "MIT",
"engines": {
"node": ">=18.17"
}
},
"packages/core": { "packages/core": {
"name": "@qwen-code/qwen-code-core", "name": "@qwen-code/qwen-code-core",
"version": "0.5.1", "version": "0.6.0",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@google/genai": "1.16.0", "@google/genai": "1.16.0",
@@ -17659,7 +17661,7 @@
"simple-git": "^3.28.0", "simple-git": "^3.28.0",
"strip-ansi": "^7.1.0", "strip-ansi": "^7.1.0",
"tiktoken": "^1.0.21", "tiktoken": "^1.0.21",
"undici": "^7.10.0", "undici": "^6.22.0",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"ws": "^8.18.0" "ws": "^8.18.0"
}, },
@@ -17747,7 +17749,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -17755,12 +17756,22 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"packages/core/node_modules/undici": {
"version": "6.22.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.22.0.tgz",
"integrity": "sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==",
"license": "MIT",
"engines": {
"node": ">=18.17"
}
},
"packages/sdk-typescript": { "packages/sdk-typescript": {
"name": "@qwen-code/sdk", "name": "@qwen-code/sdk",
"version": "0.5.1", "version": "0.6.0",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.0.4" "@modelcontextprotocol/sdk": "^1.0.4",
"tiktoken": "^1.0.21"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.14.0", "@types/node": "^20.14.0",
@@ -20186,7 +20197,7 @@
}, },
"packages/test-utils": { "packages/test-utils": {
"name": "@qwen-code/qwen-code-test-utils", "name": "@qwen-code/qwen-code-test-utils",
"version": "0.5.1", "version": "0.6.0",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"devDependencies": { "devDependencies": {
@@ -20198,7 +20209,7 @@
}, },
"packages/vscode-ide-companion": { "packages/vscode-ide-companion": {
"name": "qwen-code-vscode-ide-companion", "name": "qwen-code-vscode-ide-companion",
"version": "0.5.1", "version": "0.6.0",
"license": "LICENSE", "license": "LICENSE",
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.15.1", "@modelcontextprotocol/sdk": "^1.15.1",
@@ -20218,7 +20229,7 @@
"@types/react": "^19.1.8", "@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6", "@types/react-dom": "^19.1.6",
"@types/semver": "^7.7.1", "@types/semver": "^7.7.1",
"@types/vscode": "^1.99.0", "@types/vscode": "^1.85.0",
"@typescript-eslint/eslint-plugin": "^8.31.1", "@typescript-eslint/eslint-plugin": "^8.31.1",
"@typescript-eslint/parser": "^8.31.1", "@typescript-eslint/parser": "^8.31.1",
"@vscode/vsce": "^3.6.0", "@vscode/vsce": "^3.6.0",
@@ -20233,7 +20244,7 @@
"vitest": "^3.2.4" "vitest": "^3.2.4"
}, },
"engines": { "engines": {
"vscode": "^1.99.0" "vscode": "^1.85.0"
} }
}, },
"packages/vscode-ide-companion/node_modules/@types/react": { "packages/vscode-ide-companion/node_modules/@types/react": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "@qwen-code/qwen-code", "name": "@qwen-code/qwen-code",
"version": "0.5.1", "version": "0.6.0",
"engines": { "engines": {
"node": ">=20.0.0" "node": ">=20.0.0"
}, },
@@ -13,7 +13,7 @@
"url": "git+https://github.com/QwenLM/qwen-code.git" "url": "git+https://github.com/QwenLM/qwen-code.git"
}, },
"config": { "config": {
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.5.1" "sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.6.0"
}, },
"scripts": { "scripts": {
"start": "cross-env node scripts/start.js", "start": "cross-env node scripts/start.js",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@qwen-code/qwen-code", "name": "@qwen-code/qwen-code",
"version": "0.5.1", "version": "0.6.0",
"description": "Qwen Code", "description": "Qwen Code",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -33,7 +33,7 @@
"dist" "dist"
], ],
"config": { "config": {
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.5.1" "sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.6.0"
}, },
"dependencies": { "dependencies": {
"@google/genai": "1.16.0", "@google/genai": "1.16.0",
@@ -64,7 +64,7 @@
"strip-ansi": "^7.1.0", "strip-ansi": "^7.1.0",
"strip-json-comments": "^3.1.1", "strip-json-comments": "^3.1.1",
"tar": "^7.5.2", "tar": "^7.5.2",
"undici": "^7.10.0", "undici": "^6.22.0",
"extract-zip": "^2.0.1", "extract-zip": "^2.0.1",
"update-notifier": "^7.3.1", "update-notifier": "^7.3.1",
"wrap-ansi": "9.0.2", "wrap-ansi": "9.0.2",

View File

@@ -206,6 +206,18 @@ describe('parseArguments', () => {
expect(argv.prompt).toBeUndefined(); expect(argv.prompt).toBeUndefined();
}); });
it('should allow -r flag as alias for --resume', async () => {
process.argv = ['node', 'script.js', '-r', 'session-123'];
const argv = await parseArguments({} as Settings);
expect(argv.resume).toBe('session-123');
});
it('should allow -c flag as alias for --continue', async () => {
process.argv = ['node', 'script.js', '-c'];
const argv = await parseArguments({} as Settings);
expect(argv.continue).toBe(true);
});
it('should convert positional query argument to prompt by default', async () => { it('should convert positional query argument to prompt by default', async () => {
process.argv = ['node', 'script.js', 'Hi Gemini']; process.argv = ['node', 'script.js', 'Hi Gemini'];
const argv = await parseArguments({} as Settings); const argv = await parseArguments({} as Settings);

View File

@@ -299,7 +299,6 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
'Set the approval mode: plan (plan only), default (prompt for approval), auto-edit (auto-approve edit tools), yolo (auto-approve all tools)', 'Set the approval mode: plan (plan only), default (prompt for approval), auto-edit (auto-approve edit tools), yolo (auto-approve all tools)',
}) })
.option('checkpointing', { .option('checkpointing', {
alias: 'c',
type: 'boolean', type: 'boolean',
description: 'Enables checkpointing of file edits', description: 'Enables checkpointing of file edits',
default: false, default: false,
@@ -422,12 +421,14 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
default: false, default: false,
}) })
.option('continue', { .option('continue', {
alias: 'c',
type: 'boolean', type: 'boolean',
description: description:
'Resume the most recent session for the current project.', 'Resume the most recent session for the current project.',
default: false, default: false,
}) })
.option('resume', { .option('resume', {
alias: 'r',
type: 'string', type: 'string',
description: description:
'Resume a specific session by its ID. Use without an ID to show session picker.', 'Resume a specific session by its ID. Use without an ID to show session picker.',

View File

@@ -659,6 +659,22 @@ const SETTINGS_SCHEMA = {
childKey: 'disableCacheControl', childKey: 'disableCacheControl',
showInDialog: true, showInDialog: true,
}, },
schemaCompliance: {
type: 'enum',
label: 'Tool Schema Compliance',
category: 'Generation Configuration',
requiresRestart: false,
default: 'auto',
description:
'The compliance mode for tool schemas sent to the model. Use "openapi_30" for strict OpenAPI 3.0 compatibility (e.g., for Gemini).',
parentKey: 'generationConfig',
childKey: 'schemaCompliance',
showInDialog: true,
options: [
{ value: 'auto', label: 'Auto (Default)' },
{ value: 'openapi_30', label: 'OpenAPI 3.0 Strict' },
],
},
}, },
}, },
}, },

View File

@@ -310,6 +310,7 @@ export default {
'Tool Output Truncation Lines': 'Tool Output Truncation Lines', 'Tool Output Truncation Lines': 'Tool Output Truncation Lines',
'Folder Trust': 'Folder Trust', 'Folder Trust': 'Folder Trust',
'Vision Model Preview': 'Vision Model Preview', 'Vision Model Preview': 'Vision Model Preview',
'Tool Schema Compliance': 'Tool Schema Compliance',
// Settings enum options // Settings enum options
'Auto (detect from system)': 'Auto (detect from system)', 'Auto (detect from system)': 'Auto (detect from system)',
Text: 'Text', Text: 'Text',

View File

@@ -300,6 +300,7 @@ export default {
'Tool Output Truncation Lines': '工具输出截断行数', 'Tool Output Truncation Lines': '工具输出截断行数',
'Folder Trust': '文件夹信任', 'Folder Trust': '文件夹信任',
'Vision Model Preview': '视觉模型预览', 'Vision Model Preview': '视觉模型预览',
'Tool Schema Compliance': '工具 Schema 兼容性',
// Settings enum options // Settings enum options
'Auto (detect from system)': '自动(从系统检测)', 'Auto (detect from system)': '自动(从系统检测)',
Text: '文本', Text: '文本',

View File

@@ -76,6 +76,105 @@ describe('getGitHubRepoInfo', async () => {
); );
expect(getGitHubRepoInfo()).toEqual({ owner: 'owner', repo: 'repo' }); expect(getGitHubRepoInfo()).toEqual({ owner: 'owner', repo: 'repo' });
}); });
// Tests for credential formats
it('returns the owner and repo for URL with classic PAT token (ghp_)', async () => {
vi.mocked(child_process.execSync).mockReturnValueOnce(
'https://ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx@github.com/owner/repo.git',
);
expect(getGitHubRepoInfo()).toEqual({ owner: 'owner', repo: 'repo' });
});
it('returns the owner and repo for URL with fine-grained PAT token (github_pat_)', async () => {
vi.mocked(child_process.execSync).mockReturnValueOnce(
'https://github_pat_xxxxxxxxxxxxxxxxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx@github.com/owner/repo.git',
);
expect(getGitHubRepoInfo()).toEqual({ owner: 'owner', repo: 'repo' });
});
it('returns the owner and repo for URL with username:password format', async () => {
vi.mocked(child_process.execSync).mockReturnValueOnce(
'https://username:password@github.com/owner/repo.git',
);
expect(getGitHubRepoInfo()).toEqual({ owner: 'owner', repo: 'repo' });
});
it('returns the owner and repo for URL with OAuth token (oauth2:token)', async () => {
vi.mocked(child_process.execSync).mockReturnValueOnce(
'https://oauth2:gho_xxxxxxxxxxxx@github.com/owner/repo.git',
);
expect(getGitHubRepoInfo()).toEqual({ owner: 'owner', repo: 'repo' });
});
it('returns the owner and repo for URL with GitHub Actions token (x-access-token)', async () => {
vi.mocked(child_process.execSync).mockReturnValueOnce(
'https://x-access-token:ghs_xxxxxxxxxxxx@github.com/owner/repo.git',
);
expect(getGitHubRepoInfo()).toEqual({ owner: 'owner', repo: 'repo' });
});
// Tests for case insensitivity
it('returns the owner and repo for URL with uppercase GITHUB.COM', async () => {
vi.mocked(child_process.execSync).mockReturnValueOnce(
'https://GITHUB.COM/owner/repo.git',
);
expect(getGitHubRepoInfo()).toEqual({ owner: 'owner', repo: 'repo' });
});
it('returns the owner and repo for URL with mixed case GitHub.Com', async () => {
vi.mocked(child_process.execSync).mockReturnValueOnce(
'https://GitHub.Com/owner/repo.git',
);
expect(getGitHubRepoInfo()).toEqual({ owner: 'owner', repo: 'repo' });
});
// Tests for SSH format
it('returns the owner and repo for SSH URL', async () => {
vi.mocked(child_process.execSync).mockReturnValueOnce(
'git@github.com:owner/repo.git',
);
expect(getGitHubRepoInfo()).toEqual({ owner: 'owner', repo: 'repo' });
});
it('throws for non-GitHub SSH URL', async () => {
vi.mocked(child_process.execSync).mockReturnValueOnce(
'git@gitlab.com:owner/repo.git',
);
expect(() => {
getGitHubRepoInfo();
}).toThrowError(/Owner & repo could not be extracted from remote URL/);
});
// Tests for edge cases
it('returns the owner and repo for URL without .git suffix', async () => {
vi.mocked(child_process.execSync).mockReturnValueOnce(
'https://github.com/owner/repo',
);
expect(getGitHubRepoInfo()).toEqual({ owner: 'owner', repo: 'repo' });
});
it('throws for non-GitHub HTTPS URL', async () => {
vi.mocked(child_process.execSync).mockReturnValueOnce(
'https://gitlab.com/owner/repo.git',
);
expect(() => {
getGitHubRepoInfo();
}).toThrowError(/Owner & repo could not be extracted from remote URL/);
});
it('handles repo names containing .git substring', async () => {
vi.mocked(child_process.execSync).mockReturnValueOnce(
'https://github.com/owner/my.git.repo.git',
);
expect(getGitHubRepoInfo()).toEqual({
owner: 'owner',
repo: 'my.git.repo',
});
});
}); });
describe('getGitRepoRoot', async () => { describe('getGitRepoRoot', async () => {

View File

@@ -103,17 +103,38 @@ export function getGitHubRepoInfo(): { owner: string; repo: string } {
encoding: 'utf-8', encoding: 'utf-8',
}).trim(); }).trim();
// Matches either https://github.com/owner/repo.git or git@github.com:owner/repo.git // Handle SCP-style SSH URLs (git@github.com:owner/repo.git)
const match = remoteUrl.match( let urlToParse = remoteUrl;
/(?:https?:\/\/|git@)github\.com(?::|\/)([^/]+)\/([^/]+?)(?:\.git)?$/, if (remoteUrl.startsWith('git@github.com:')) {
); urlToParse = remoteUrl.replace('git@github.com:', '');
} else if (remoteUrl.startsWith('git@')) {
// If the regex fails match, throw an error. // SSH URL for a different provider (GitLab, Bitbucket, etc.)
if (!match || !match[1] || !match[2]) {
throw new Error( throw new Error(
`Owner & repo could not be extracted from remote URL: ${remoteUrl}`, `Owner & repo could not be extracted from remote URL: ${remoteUrl}`,
); );
} }
return { owner: match[1], repo: match[2] }; let parsedUrl: URL;
try {
parsedUrl = new URL(urlToParse, 'https://github.com');
} catch {
throw new Error(
`Owner & repo could not be extracted from remote URL: ${remoteUrl}`,
);
}
if (parsedUrl.host !== 'github.com') {
throw new Error(
`Owner & repo could not be extracted from remote URL: ${remoteUrl}`,
);
}
const parts = parsedUrl.pathname.split('/').filter((part) => part !== '');
if (parts.length !== 2 || !parts[0] || !parts[1]) {
throw new Error(
`Owner & repo could not be extracted from remote URL: ${remoteUrl}`,
);
}
return { owner: parts[0], repo: parts[1].replace(/\.git$/, '') };
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@qwen-code/qwen-code-core", "name": "@qwen-code/qwen-code-core",
"version": "0.5.1", "version": "0.6.0",
"description": "Qwen Code Core", "description": "Qwen Code Core",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -63,7 +63,7 @@
"simple-git": "^3.28.0", "simple-git": "^3.28.0",
"strip-ansi": "^7.1.0", "strip-ansi": "^7.1.0",
"tiktoken": "^1.0.21", "tiktoken": "^1.0.21",
"undici": "^7.10.0", "undici": "^6.22.0",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"ws": "^8.18.0" "ws": "^8.18.0"
}, },

View File

@@ -15,6 +15,7 @@ export const OAUTH_FILE = 'oauth_creds.json';
const TMP_DIR_NAME = 'tmp'; const TMP_DIR_NAME = 'tmp';
const BIN_DIR_NAME = 'bin'; const BIN_DIR_NAME = 'bin';
const PROJECT_DIR_NAME = 'projects'; const PROJECT_DIR_NAME = 'projects';
const IDE_DIR_NAME = 'ide';
export class Storage { export class Storage {
private readonly targetDir: string; private readonly targetDir: string;
@@ -59,6 +60,10 @@ export class Storage {
return path.join(Storage.getGlobalQwenDir(), TMP_DIR_NAME); return path.join(Storage.getGlobalQwenDir(), TMP_DIR_NAME);
} }
static getGlobalIdeDir(): string {
return path.join(Storage.getGlobalQwenDir(), IDE_DIR_NAME);
}
static getGlobalBinDir(): string { static getGlobalBinDir(): string {
return path.join(Storage.getGlobalQwenDir(), BIN_DIR_NAME); return path.join(Storage.getGlobalQwenDir(), BIN_DIR_NAME);
} }

View File

@@ -151,8 +151,7 @@ describe('BaseLlmClient', () => {
contents: defaultOptions.contents, contents: defaultOptions.contents,
config: { config: {
abortSignal: defaultOptions.abortSignal, abortSignal: defaultOptions.abortSignal,
temperature: 0, topP: 0.8,
topP: 1,
tools: [ tools: [
{ {
functionDeclarations: [ functionDeclarations: [
@@ -189,7 +188,7 @@ describe('BaseLlmClient', () => {
expect.objectContaining({ expect.objectContaining({
config: expect.objectContaining({ config: expect.objectContaining({
temperature: 0.8, temperature: 0.8,
topP: 1, // Default should remain if not overridden topP: 0.8, // Default should remain if not overridden
topK: 10, topK: 10,
tools: expect.any(Array), tools: expect.any(Array),
}), }),

View File

@@ -66,8 +66,7 @@ export interface GenerateJsonOptions {
export class BaseLlmClient { export class BaseLlmClient {
// Default configuration for utility tasks // Default configuration for utility tasks
private readonly defaultUtilityConfig: GenerateContentConfig = { private readonly defaultUtilityConfig: GenerateContentConfig = {
temperature: 0, topP: 0.8,
topP: 1,
}; };
constructor( constructor(

View File

@@ -2310,7 +2310,7 @@ ${JSON.stringify(
abortSignal, abortSignal,
systemInstruction: getCoreSystemPrompt(''), systemInstruction: getCoreSystemPrompt(''),
temperature: 0.5, temperature: 0.5,
topP: 1, topP: 0.8,
}, },
contents, contents,
}, },

View File

@@ -94,8 +94,7 @@ const MAX_TURNS = 100;
export class GeminiClient { export class GeminiClient {
private chat?: GeminiChat; private chat?: GeminiChat;
private readonly generateContentConfig: GenerateContentConfig = { private readonly generateContentConfig: GenerateContentConfig = {
temperature: 0, topP: 0.8,
topP: 1,
}; };
private sessionTurnCount = 0; private sessionTurnCount = 0;

View File

@@ -76,6 +76,8 @@ export type ContentGeneratorConfig = {
}; };
proxy?: string | undefined; proxy?: string | undefined;
userAgent?: string; userAgent?: string;
// Schema compliance mode for tool definitions
schemaCompliance?: 'auto' | 'openapi_30';
}; };
export function createContentGeneratorConfig( export function createContentGeneratorConfig(

View File

@@ -22,6 +22,10 @@ import { GenerateContentResponse, FinishReason } from '@google/genai';
import type OpenAI from 'openai'; import type OpenAI from 'openai';
import { safeJsonParse } from '../../utils/safeJsonParse.js'; import { safeJsonParse } from '../../utils/safeJsonParse.js';
import { StreamingToolCallParser } from './streamingToolCallParser.js'; import { StreamingToolCallParser } from './streamingToolCallParser.js';
import {
convertSchema,
type SchemaComplianceMode,
} from '../../utils/schemaConverter.js';
/** /**
* Extended usage type that supports both OpenAI standard format and alternative formats * Extended usage type that supports both OpenAI standard format and alternative formats
@@ -80,11 +84,13 @@ interface ParsedParts {
*/ */
export class OpenAIContentConverter { export class OpenAIContentConverter {
private model: string; private model: string;
private schemaCompliance: SchemaComplianceMode;
private streamingToolCallParser: StreamingToolCallParser = private streamingToolCallParser: StreamingToolCallParser =
new StreamingToolCallParser(); new StreamingToolCallParser();
constructor(model: string) { constructor(model: string, schemaCompliance: SchemaComplianceMode = 'auto') {
this.model = model; this.model = model;
this.schemaCompliance = schemaCompliance;
} }
/** /**
@@ -205,6 +211,10 @@ export class OpenAIContentConverter {
); );
} }
if (parameters) {
parameters = convertSchema(parameters, this.schemaCompliance);
}
openAITools.push({ openAITools.push({
type: 'function', type: 'function',
function: { function: {

View File

@@ -108,7 +108,10 @@ describe('ContentGenerationPipeline', () => {
describe('constructor', () => { describe('constructor', () => {
it('should initialize with correct configuration', () => { it('should initialize with correct configuration', () => {
expect(mockProvider.buildClient).toHaveBeenCalled(); expect(mockProvider.buildClient).toHaveBeenCalled();
expect(OpenAIContentConverter).toHaveBeenCalledWith('test-model'); expect(OpenAIContentConverter).toHaveBeenCalledWith(
'test-model',
undefined,
);
}); });
}); });

View File

@@ -34,6 +34,7 @@ export class ContentGenerationPipeline {
this.client = this.config.provider.buildClient(); this.client = this.config.provider.buildClient();
this.converter = new OpenAIContentConverter( this.converter = new OpenAIContentConverter(
this.contentGeneratorConfig.model, this.contentGeneratorConfig.model,
this.contentGeneratorConfig.schemaCompliance,
); );
} }

View File

@@ -32,6 +32,7 @@ vi.mock('node:fs', async (importOriginal) => {
...actual.promises, ...actual.promises,
readFile: vi.fn(), readFile: vi.fn(),
readdir: vi.fn(), readdir: vi.fn(),
stat: vi.fn(),
}, },
realpathSync: (p: string) => p, realpathSync: (p: string) => p,
existsSync: () => false, existsSync: () => false,
@@ -68,6 +69,7 @@ describe('IdeClient', () => {
command: 'test-ide', command: 'test-ide',
}); });
vi.mocked(os.tmpdir).mockReturnValue('/tmp'); vi.mocked(os.tmpdir).mockReturnValue('/tmp');
vi.mocked(os.homedir).mockReturnValue('/home/test');
// Mock MCP client and transports // Mock MCP client and transports
mockClient = { mockClient = {
@@ -97,19 +99,15 @@ describe('IdeClient', () => {
describe('connect', () => { describe('connect', () => {
it('should connect using HTTP when port is provided in config file', async () => { it('should connect using HTTP when port is provided in config file', async () => {
process.env['QWEN_CODE_IDE_SERVER_PORT'] = '8080';
const config = { port: '8080' }; const config = { port: '8080' };
vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config)); vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config));
(
vi.mocked(fs.promises.readdir) as Mock<
(path: fs.PathLike) => Promise<string[]>
>
).mockResolvedValue([]);
const ideClient = await IdeClient.getInstance(); const ideClient = await IdeClient.getInstance();
await ideClient.connect(); await ideClient.connect();
expect(fs.promises.readFile).toHaveBeenCalledWith( expect(fs.promises.readFile).toHaveBeenCalledWith(
path.join('/tmp', 'qwen-code-ide-server-12345.json'), path.join('/home/test', '.qwen', 'ide', '8080.lock'),
'utf8', 'utf8',
); );
expect(StreamableHTTPClientTransport).toHaveBeenCalledWith( expect(StreamableHTTPClientTransport).toHaveBeenCalledWith(
@@ -120,16 +118,13 @@ describe('IdeClient', () => {
expect(ideClient.getConnectionStatus().status).toBe( expect(ideClient.getConnectionStatus().status).toBe(
IDEConnectionStatus.Connected, IDEConnectionStatus.Connected,
); );
delete process.env['QWEN_CODE_IDE_SERVER_PORT'];
}); });
it('should connect using stdio when stdio config is provided in file', async () => { it('should connect using stdio when stdio config is provided in file', async () => {
process.env['QWEN_CODE_IDE_SERVER_PORT'] = '8080';
const config = { stdio: { command: 'test-cmd', args: ['--foo'] } }; const config = { stdio: { command: 'test-cmd', args: ['--foo'] } };
vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config)); vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config));
(
vi.mocked(fs.promises.readdir) as Mock<
(path: fs.PathLike) => Promise<string[]>
>
).mockResolvedValue([]);
const ideClient = await IdeClient.getInstance(); const ideClient = await IdeClient.getInstance();
await ideClient.connect(); await ideClient.connect();
@@ -142,19 +137,16 @@ describe('IdeClient', () => {
expect(ideClient.getConnectionStatus().status).toBe( expect(ideClient.getConnectionStatus().status).toBe(
IDEConnectionStatus.Connected, IDEConnectionStatus.Connected,
); );
delete process.env['QWEN_CODE_IDE_SERVER_PORT'];
}); });
it('should prioritize port over stdio when both are in config file', async () => { it('should prioritize port over stdio when both are in config file', async () => {
process.env['QWEN_CODE_IDE_SERVER_PORT'] = '8080';
const config = { const config = {
port: '8080', port: '8080',
stdio: { command: 'test-cmd', args: ['--foo'] }, stdio: { command: 'test-cmd', args: ['--foo'] },
}; };
vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config)); vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config));
(
vi.mocked(fs.promises.readdir) as Mock<
(path: fs.PathLike) => Promise<string[]>
>
).mockResolvedValue([]);
const ideClient = await IdeClient.getInstance(); const ideClient = await IdeClient.getInstance();
await ideClient.connect(); await ideClient.connect();
@@ -164,6 +156,7 @@ describe('IdeClient', () => {
expect(ideClient.getConnectionStatus().status).toBe( expect(ideClient.getConnectionStatus().status).toBe(
IDEConnectionStatus.Connected, IDEConnectionStatus.Connected,
); );
delete process.env['QWEN_CODE_IDE_SERVER_PORT'];
}); });
it('should connect using HTTP when port is provided in environment variables', async () => { it('should connect using HTTP when port is provided in environment variables', async () => {
@@ -263,7 +256,8 @@ describe('IdeClient', () => {
}); });
describe('getConnectionConfigFromFile', () => { describe('getConnectionConfigFromFile', () => {
it('should return config from the specific pid file if it exists', async () => { it('should return config from the env port lock file if it exists', async () => {
process.env['QWEN_CODE_IDE_SERVER_PORT'] = '1234';
const config = { port: '1234', workspacePath: '/test/workspace' }; const config = { port: '1234', workspacePath: '/test/workspace' };
vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config)); vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config));
@@ -277,18 +271,14 @@ describe('IdeClient', () => {
expect(result).toEqual(config); expect(result).toEqual(config);
expect(fs.promises.readFile).toHaveBeenCalledWith( expect(fs.promises.readFile).toHaveBeenCalledWith(
path.join('/tmp', 'qwen-code-ide-server-12345.json'), path.join('/home/test', '.qwen', 'ide', '1234.lock'),
'utf8', 'utf8',
); );
delete process.env['QWEN_CODE_IDE_SERVER_PORT'];
}); });
it('should return undefined if no config files are found', async () => { it('should return undefined if no config files are found', async () => {
vi.mocked(fs.promises.readFile).mockRejectedValue(new Error('not found')); vi.mocked(fs.promises.readFile).mockRejectedValue(new Error('not found'));
(
vi.mocked(fs.promises.readdir) as Mock<
(path: fs.PathLike) => Promise<string[]>
>
).mockResolvedValue([]);
const ideClient = await IdeClient.getInstance(); const ideClient = await IdeClient.getInstance();
const result = await ( const result = await (
@@ -300,20 +290,15 @@ describe('IdeClient', () => {
expect(result).toBeUndefined(); expect(result).toBeUndefined();
}); });
it('should find and parse a single config file with the new naming scheme', async () => { it('should read legacy pid config when available', async () => {
const config = { port: '5678', workspacePath: '/test/workspace' }; const config = {
vi.mocked(fs.promises.readFile).mockRejectedValueOnce( port: '5678',
new Error('not found'), workspacePath: '/test/workspace',
); // For old path ppid: 12345,
( };
vi.mocked(fs.promises.readdir) as Mock< vi.mocked(fs.promises.readFile).mockResolvedValueOnce(
(path: fs.PathLike) => Promise<string[]> JSON.stringify(config),
> );
).mockResolvedValue(['qwen-code-ide-server-12345-123.json']);
vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config));
vi.spyOn(IdeClient, 'validateWorkspacePath').mockReturnValue({
isValid: true,
});
const ideClient = await IdeClient.getInstance(); const ideClient = await IdeClient.getInstance();
const result = await ( const result = await (
@@ -324,110 +309,18 @@ describe('IdeClient', () => {
expect(result).toEqual(config); expect(result).toEqual(config);
expect(fs.promises.readFile).toHaveBeenCalledWith( expect(fs.promises.readFile).toHaveBeenCalledWith(
path.join('/tmp/gemini/ide', 'qwen-code-ide-server-12345-123.json'), path.join('/tmp', 'qwen-code-ide-server-12345.json'),
'utf8', 'utf8',
); );
}); });
it('should filter out configs with invalid workspace paths', async () => { it('should fall back to legacy port file when pid file is missing', async () => {
const validConfig = {
port: '5678',
workspacePath: '/test/workspace',
};
const invalidConfig = {
port: '1111',
workspacePath: '/invalid/workspace',
};
vi.mocked(fs.promises.readFile).mockRejectedValueOnce(
new Error('not found'),
);
(
vi.mocked(fs.promises.readdir) as Mock<
(path: fs.PathLike) => Promise<string[]>
>
).mockResolvedValue([
'qwen-code-ide-server-12345-111.json',
'qwen-code-ide-server-12345-222.json',
]);
vi.mocked(fs.promises.readFile)
.mockResolvedValueOnce(JSON.stringify(invalidConfig))
.mockResolvedValueOnce(JSON.stringify(validConfig));
const validateSpy = vi
.spyOn(IdeClient, 'validateWorkspacePath')
.mockReturnValueOnce({ isValid: false })
.mockReturnValueOnce({ isValid: true });
const ideClient = await IdeClient.getInstance();
const result = await (
ideClient as unknown as {
getConnectionConfigFromFile: () => Promise<unknown>;
}
).getConnectionConfigFromFile();
expect(result).toEqual(validConfig);
expect(validateSpy).toHaveBeenCalledWith(
'/invalid/workspace',
'/test/workspace/sub-dir',
);
expect(validateSpy).toHaveBeenCalledWith(
'/test/workspace',
'/test/workspace/sub-dir',
);
});
it('should return the first valid config when multiple workspaces are valid', async () => {
const config1 = { port: '1111', workspacePath: '/test/workspace' };
const config2 = { port: '2222', workspacePath: '/test/workspace2' };
vi.mocked(fs.promises.readFile).mockRejectedValueOnce(
new Error('not found'),
);
(
vi.mocked(fs.promises.readdir) as Mock<
(path: fs.PathLike) => Promise<string[]>
>
).mockResolvedValue([
'qwen-code-ide-server-12345-111.json',
'qwen-code-ide-server-12345-222.json',
]);
vi.mocked(fs.promises.readFile)
.mockResolvedValueOnce(JSON.stringify(config1))
.mockResolvedValueOnce(JSON.stringify(config2));
vi.spyOn(IdeClient, 'validateWorkspacePath').mockReturnValue({
isValid: true,
});
const ideClient = await IdeClient.getInstance();
const result = await (
ideClient as unknown as {
getConnectionConfigFromFile: () => Promise<unknown>;
}
).getConnectionConfigFromFile();
expect(result).toEqual(config1);
});
it('should prioritize the config matching the port from the environment variable', async () => {
process.env['QWEN_CODE_IDE_SERVER_PORT'] = '2222'; process.env['QWEN_CODE_IDE_SERVER_PORT'] = '2222';
const config1 = { port: '1111', workspacePath: '/test/workspace' };
const config2 = { port: '2222', workspacePath: '/test/workspace2' }; const config2 = { port: '2222', workspacePath: '/test/workspace2' };
vi.mocked(fs.promises.readFile).mockRejectedValueOnce(
new Error('not found'),
);
(
vi.mocked(fs.promises.readdir) as Mock<
(path: fs.PathLike) => Promise<string[]>
>
).mockResolvedValue([
'qwen-code-ide-server-12345-111.json',
'qwen-code-ide-server-12345-222.json',
]);
vi.mocked(fs.promises.readFile) vi.mocked(fs.promises.readFile)
.mockResolvedValueOnce(JSON.stringify(config1)) .mockRejectedValueOnce(new Error('not found')) // ~/.qwen/ide/<port>.lock
.mockRejectedValueOnce(new Error('not found')) // legacy pid file
.mockResolvedValueOnce(JSON.stringify(config2)); .mockResolvedValueOnce(JSON.stringify(config2));
vi.spyOn(IdeClient, 'validateWorkspacePath').mockReturnValue({
isValid: true,
});
const ideClient = await IdeClient.getInstance(); const ideClient = await IdeClient.getInstance();
const result = await ( const result = await (
@@ -437,28 +330,23 @@ describe('IdeClient', () => {
).getConnectionConfigFromFile(); ).getConnectionConfigFromFile();
expect(result).toEqual(config2); expect(result).toEqual(config2);
expect(fs.promises.readFile).toHaveBeenCalledWith(
path.join('/tmp', 'qwen-code-ide-server-12345.json'),
'utf8',
);
expect(fs.promises.readFile).toHaveBeenCalledWith(
path.join('/tmp', 'qwen-code-ide-server-2222.json'),
'utf8',
);
delete process.env['QWEN_CODE_IDE_SERVER_PORT']; delete process.env['QWEN_CODE_IDE_SERVER_PORT'];
}); });
it('should handle invalid JSON in one of the config files', async () => { it('should fall back to legacy config when env lock file has invalid JSON', async () => {
const validConfig = { port: '2222', workspacePath: '/test/workspace' }; process.env['QWEN_CODE_IDE_SERVER_PORT'] = '3333';
vi.mocked(fs.promises.readFile).mockRejectedValueOnce( const config = { port: '1111', workspacePath: '/test/workspace' };
new Error('not found'),
);
(
vi.mocked(fs.promises.readdir) as Mock<
(path: fs.PathLike) => Promise<string[]>
>
).mockResolvedValue([
'qwen-code-ide-server-12345-111.json',
'qwen-code-ide-server-12345-222.json',
]);
vi.mocked(fs.promises.readFile) vi.mocked(fs.promises.readFile)
.mockResolvedValueOnce('invalid json') .mockResolvedValueOnce('invalid json')
.mockResolvedValueOnce(JSON.stringify(validConfig)); .mockResolvedValueOnce(JSON.stringify(config));
vi.spyOn(IdeClient, 'validateWorkspacePath').mockReturnValue({
isValid: true,
});
const ideClient = await IdeClient.getInstance(); const ideClient = await IdeClient.getInstance();
const result = await ( const result = await (
@@ -467,96 +355,7 @@ describe('IdeClient', () => {
} }
).getConnectionConfigFromFile(); ).getConnectionConfigFromFile();
expect(result).toEqual(validConfig); expect(result).toEqual(config);
});
it('should return undefined if readdir throws an error', async () => {
vi.mocked(fs.promises.readFile).mockRejectedValueOnce(
new Error('not found'),
);
vi.mocked(fs.promises.readdir).mockRejectedValue(
new Error('readdir failed'),
);
const ideClient = await IdeClient.getInstance();
const result = await (
ideClient as unknown as {
getConnectionConfigFromFile: () => Promise<unknown>;
}
).getConnectionConfigFromFile();
expect(result).toBeUndefined();
});
it('should ignore files with invalid names', async () => {
const validConfig = { port: '3333', workspacePath: '/test/workspace' };
vi.mocked(fs.promises.readFile).mockRejectedValueOnce(
new Error('not found'),
);
(
vi.mocked(fs.promises.readdir) as Mock<
(path: fs.PathLike) => Promise<string[]>
>
).mockResolvedValue([
'qwen-code-ide-server-12345-111.json', // valid
'not-a-config-file.txt', // invalid
'qwen-code-ide-server-asdf.json', // invalid
]);
vi.mocked(fs.promises.readFile).mockResolvedValueOnce(
JSON.stringify(validConfig),
);
vi.spyOn(IdeClient, 'validateWorkspacePath').mockReturnValue({
isValid: true,
});
const ideClient = await IdeClient.getInstance();
const result = await (
ideClient as unknown as {
getConnectionConfigFromFile: () => Promise<unknown>;
}
).getConnectionConfigFromFile();
expect(result).toEqual(validConfig);
expect(fs.promises.readFile).toHaveBeenCalledWith(
path.join('/tmp/gemini/ide', 'qwen-code-ide-server-12345-111.json'),
'utf8',
);
expect(fs.promises.readFile).not.toHaveBeenCalledWith(
path.join('/tmp/gemini/ide', 'not-a-config-file.txt'),
'utf8',
);
});
it('should match env port string to a number port in the config', async () => {
process.env['QWEN_CODE_IDE_SERVER_PORT'] = '3333';
const config1 = { port: 1111, workspacePath: '/test/workspace' };
const config2 = { port: 3333, workspacePath: '/test/workspace2' };
vi.mocked(fs.promises.readFile).mockRejectedValueOnce(
new Error('not found'),
);
(
vi.mocked(fs.promises.readdir) as Mock<
(path: fs.PathLike) => Promise<string[]>
>
).mockResolvedValue([
'qwen-code-ide-server-12345-111.json',
'qwen-code-ide-server-12345-222.json',
]);
vi.mocked(fs.promises.readFile)
.mockResolvedValueOnce(JSON.stringify(config1))
.mockResolvedValueOnce(JSON.stringify(config2));
vi.spyOn(IdeClient, 'validateWorkspacePath').mockReturnValue({
isValid: true,
});
const ideClient = await IdeClient.getInstance();
const result = await (
ideClient as unknown as {
getConnectionConfigFromFile: () => Promise<unknown>;
}
).getConnectionConfigFromFile();
expect(result).toEqual(config2);
delete process.env['QWEN_CODE_IDE_SERVER_PORT']; delete process.env['QWEN_CODE_IDE_SERVER_PORT'];
}); });
}); });

View File

@@ -8,6 +8,7 @@ import * as fs from 'node:fs';
import { isSubpath } from '../utils/paths.js'; import { isSubpath } from '../utils/paths.js';
import { detectIde, type IdeInfo } from '../ide/detect-ide.js'; import { detectIde, type IdeInfo } from '../ide/detect-ide.js';
import { ideContextStore } from './ideContext.js'; import { ideContextStore } from './ideContext.js';
import { Storage } from '../config/storage.js';
import { import {
IdeContextNotificationSchema, IdeContextNotificationSchema,
IdeDiffAcceptedNotificationSchema, IdeDiffAcceptedNotificationSchema,
@@ -572,98 +573,103 @@ export class IdeClient {
| (ConnectionConfig & { workspacePath?: string; ideInfo?: IdeInfo }) | (ConnectionConfig & { workspacePath?: string; ideInfo?: IdeInfo })
| undefined | undefined
> { > {
if (!this.ideProcessInfo) {
return undefined;
}
// For backwards compatability
try {
const portFile = path.join(
os.tmpdir(),
`qwen-code-ide-server-${this.ideProcessInfo.pid}.json`,
);
const portFileContents = await fs.promises.readFile(portFile, 'utf8');
return JSON.parse(portFileContents);
} catch (_) {
// For newer extension versions, the file name matches the pattern
// /^qwen-code-ide-server-${pid}-\d+\.json$/. If multiple IDE
// windows are open, multiple files matching the pattern are expected to
// exist.
}
const portFileDir = path.join(os.tmpdir(), 'gemini', 'ide');
let portFiles;
try {
portFiles = await fs.promises.readdir(portFileDir);
} catch (e) {
logger.debug('Failed to read IDE connection directory:', e);
return undefined;
}
if (!portFiles) {
return undefined;
}
const fileRegex = new RegExp(
`^qwen-code-ide-server-${this.ideProcessInfo.pid}-\\d+\\.json$`,
);
const matchingFiles = portFiles
.filter((file) => fileRegex.test(file))
.sort();
if (matchingFiles.length === 0) {
return undefined;
}
let fileContents: string[];
try {
fileContents = await Promise.all(
matchingFiles.map((file) =>
fs.promises.readFile(path.join(portFileDir, file), 'utf8'),
),
);
} catch (e) {
logger.debug('Failed to read IDE connection config file(s):', e);
return undefined;
}
const parsedContents = fileContents.map((content) => {
try {
return JSON.parse(content);
} catch (e) {
logger.debug('Failed to parse JSON from config file: ', e);
return undefined;
}
});
const validWorkspaces = parsedContents.filter((content) => {
if (!content) {
return false;
}
const { isValid } = IdeClient.validateWorkspacePath(
content.workspacePath,
process.cwd(),
);
return isValid;
});
if (validWorkspaces.length === 0) {
return undefined;
}
if (validWorkspaces.length === 1) {
return validWorkspaces[0];
}
const portFromEnv = this.getPortFromEnv(); const portFromEnv = this.getPortFromEnv();
if (portFromEnv) { if (portFromEnv) {
const matchingPort = validWorkspaces.find( try {
(content) => String(content.port) === portFromEnv, const ideDir = Storage.getGlobalIdeDir();
); const lockFile = path.join(ideDir, `${portFromEnv}.lock`);
if (matchingPort) { const lockFileContents = await fs.promises.readFile(lockFile, 'utf8');
return matchingPort; return JSON.parse(lockFileContents);
} catch (_) {
// Fall through to legacy discovery.
} }
} }
return validWorkspaces[0]; // Legacy discovery for VSCode extension < v0.5.1.
return this.getLegacyConnectionConfig(portFromEnv);
}
// Legacy connection files were written in the global temp directory.
private async getLegacyConnectionConfig(
portFromEnv?: string,
): Promise<
| (ConnectionConfig & { workspacePath?: string; ideInfo?: IdeInfo })
| undefined
> {
if (this.ideProcessInfo) {
try {
const portFile = path.join(
os.tmpdir(),
`qwen-code-ide-server-${this.ideProcessInfo.pid}.json`,
);
const portFileContents = await fs.promises.readFile(portFile, 'utf8');
return JSON.parse(portFileContents);
} catch (_) {
// For older/newer extension versions, the file name matches the pattern
// /^qwen-code-ide-server-${pid}-\d+\.json$/. If multiple IDE
// windows are open, multiple files matching the pattern are expected to
// exist.
}
}
if (portFromEnv) {
try {
const portFile = path.join(
os.tmpdir(),
`qwen-code-ide-server-${portFromEnv}.json`,
);
const portFileContents = await fs.promises.readFile(portFile, 'utf8');
return JSON.parse(portFileContents);
} catch (_) {
// Ignore and fall through.
}
}
return undefined;
}
protected async getAllConnectionConfigs(
ideDir: string,
): Promise<
ConnectionConfig & Array<{ workspacePath?: string; ideInfo?: IdeInfo }>
> {
const fileRegex = new RegExp('^\\d+\\.lock$');
let lockFiles: string[];
try {
lockFiles = (await fs.promises.readdir(ideDir)).filter((file) =>
fileRegex.test(file),
);
} catch (e) {
logger.debug('Failed to read IDE connection directory:', e);
return [];
}
const fileContents = await Promise.all(
lockFiles.map(async (file) => {
const fullPath = path.join(ideDir, file);
try {
const stat = await fs.promises.stat(fullPath);
const content = await fs.promises.readFile(fullPath, 'utf8');
try {
const parsed = JSON.parse(content);
return { file, mtimeMs: stat.mtimeMs, parsed };
} catch (e) {
logger.debug('Failed to parse JSON from lock file: ', e);
return { file, mtimeMs: stat.mtimeMs, parsed: undefined };
}
} catch (e) {
// If we can't stat/read the file, treat it as very old so it doesn't
// win ties, and skip parsing by returning undefined content.
logger.debug('Failed to read/stat IDE lock file:', e);
return { file, mtimeMs: -Infinity, parsed: undefined };
}
}),
);
return fileContents
.filter(({ parsed }) => parsed !== undefined)
.sort((a, b) => b.mtimeMs - a.mtimeMs)
.map(({ parsed }) => parsed);
} }
private createProxyAwareFetch() { private createProxyAwareFetch() {

View File

@@ -50,7 +50,7 @@ describe('getIdeProcessInfo', () => {
expect(result).toEqual({ pid: 700, command: '/usr/lib/vscode/code' }); expect(result).toEqual({ pid: 700, command: '/usr/lib/vscode/code' });
}); });
it('should return parent process info if grandparent lookup fails', async () => { it('should return shell process info if grandparent lookup fails', async () => {
(os.platform as Mock).mockReturnValue('linux'); (os.platform as Mock).mockReturnValue('linux');
mockedExec mockedExec
.mockResolvedValueOnce({ stdout: '800 /bin/bash' }) // pid 1000 -> ppid 800 (shell) .mockResolvedValueOnce({ stdout: '800 /bin/bash' }) // pid 1000 -> ppid 800 (shell)
@@ -63,134 +63,96 @@ describe('getIdeProcessInfo', () => {
}); });
describe('on Windows', () => { describe('on Windows', () => {
it('should traverse up and find the great-grandchild of the root process', async () => { it('should return great-grandparent process using heuristic', async () => {
(os.platform as Mock).mockReturnValue('win32'); (os.platform as Mock).mockReturnValue('win32');
const processInfoMap = new Map([
[ const processes = [
1000, {
{ ProcessId: 1000,
stdout: ParentProcessId: 900,
'{"Name":"node.exe","ParentProcessId":900,"CommandLine":"node.exe"}', Name: 'node.exe',
}, CommandLine: 'node.exe',
], },
[ {
900, ProcessId: 900,
{ ParentProcessId: 800,
stdout: Name: 'powershell.exe',
'{"Name":"powershell.exe","ParentProcessId":800,"CommandLine":"powershell.exe"}', CommandLine: 'powershell.exe',
}, },
], {
[ ProcessId: 800,
800, ParentProcessId: 700,
{ Name: 'code.exe',
stdout: CommandLine: 'code.exe',
'{"Name":"code.exe","ParentProcessId":700,"CommandLine":"code.exe"}', },
}, {
], ProcessId: 700,
[ ParentProcessId: 0,
700, Name: 'wininit.exe',
{ CommandLine: 'wininit.exe',
stdout: },
'{"Name":"wininit.exe","ParentProcessId":0,"CommandLine":"wininit.exe"}', ];
},
], mockedExec.mockImplementation((file: string, _args: string[]) => {
]); if (file === 'powershell') {
mockedExec.mockImplementation((command: string) => { return Promise.resolve({ stdout: JSON.stringify(processes) });
const pidMatch = command.match(/ProcessId=(\d+)/);
if (pidMatch) {
const pid = parseInt(pidMatch[1], 10);
return Promise.resolve(processInfoMap.get(pid));
} }
return Promise.reject(new Error('Invalid command for mock')); return Promise.resolve({ stdout: '' });
}); });
const result = await getIdeProcessInfo(); const result = await getIdeProcessInfo();
// Process chain: 1000 (node.exe) -> 900 (powershell.exe) -> 800 (code.exe) -> 700 (wininit.exe)
// ancestors = [1000, 900, 800, 700], length = 4
// Heuristic: return ancestors[length-3] = ancestors[1] = 900 (powershell.exe)
expect(result).toEqual({ pid: 900, command: 'powershell.exe' }); expect(result).toEqual({ pid: 900, command: 'powershell.exe' });
}); });
it('should handle non-existent process gracefully', async () => { it('should handle empty process list gracefully', async () => {
(os.platform as Mock).mockReturnValue('win32'); (os.platform as Mock).mockReturnValue('win32');
mockedExec mockedExec.mockResolvedValue({ stdout: '[]' });
.mockResolvedValueOnce({ stdout: '' }) // Non-existent PID returns empty due to -ErrorAction SilentlyContinue
.mockResolvedValueOnce({
stdout:
'{"Name":"fallback.exe","ParentProcessId":0,"CommandLine":"fallback.exe"}',
}); // Fallback call
const result = await getIdeProcessInfo(); const result = await getIdeProcessInfo();
expect(result).toEqual({ pid: 1000, command: 'fallback.exe' }); // Should return current pid and empty command because process not found in map
expect(result).toEqual({ pid: 1000, command: '' });
}); });
it('should handle malformed JSON output gracefully', async () => { it('should handle malformed JSON output gracefully', async () => {
(os.platform as Mock).mockReturnValue('win32'); (os.platform as Mock).mockReturnValue('win32');
mockedExec mockedExec.mockResolvedValue({ stdout: '{"invalid":json}' });
.mockResolvedValueOnce({ stdout: '{"invalid":json}' }) // Malformed JSON
.mockResolvedValueOnce({
stdout:
'{"Name":"fallback.exe","ParentProcessId":0,"CommandLine":"fallback.exe"}',
}); // Fallback call
const result = await getIdeProcessInfo(); const result = await getIdeProcessInfo();
expect(result).toEqual({ pid: 1000, command: 'fallback.exe' }); expect(result).toEqual({ pid: 1000, command: '' });
}); });
it('should handle PowerShell errors without crashing the process chain', async () => { it('should return last ancestor if chain is too short', async () => {
(os.platform as Mock).mockReturnValue('win32'); (os.platform as Mock).mockReturnValue('win32');
const processInfoMap = new Map([
[1000, { stdout: '' }], // First process doesn't exist (empty due to -ErrorAction)
[
1001,
{
stdout:
'{"Name":"parent.exe","ParentProcessId":800,"CommandLine":"parent.exe"}',
},
],
[
800,
{
stdout:
'{"Name":"ide.exe","ParentProcessId":0,"CommandLine":"ide.exe"}',
},
],
]);
// Mock the process.pid to test traversal with missing processes const processes = [
Object.defineProperty(process, 'pid', { {
value: 1001, ProcessId: 1000,
configurable: true, ParentProcessId: 900,
}); Name: 'node.exe',
CommandLine: 'node.exe',
},
{
ProcessId: 900,
ParentProcessId: 0,
Name: 'explorer.exe',
CommandLine: 'explorer.exe',
},
];
mockedExec.mockImplementation((command: string) => { mockedExec.mockImplementation((file: string, _args: string[]) => {
const pidMatch = command.match(/ProcessId=(\d+)/); if (file === 'powershell') {
if (pidMatch) { return Promise.resolve({ stdout: JSON.stringify(processes) });
const pid = parseInt(pidMatch[1], 10);
return Promise.resolve(processInfoMap.get(pid) || { stdout: '' });
} }
return Promise.reject(new Error('Invalid command for mock')); return Promise.resolve({ stdout: '' });
}); });
const result = await getIdeProcessInfo(); const result = await getIdeProcessInfo();
// Should return the current process command since traversal continues despite missing processes // ancestors = [1000, 900], length = 2 (< 3)
expect(result).toEqual({ pid: 1001, command: 'parent.exe' }); // Heuristic: return ancestors[length-1] = ancestors[1] = 900 (explorer.exe)
expect(result).toEqual({ pid: 900, command: 'explorer.exe' });
// Reset process.pid
Object.defineProperty(process, 'pid', {
value: 1000,
configurable: true,
});
});
it('should handle partial JSON data with defaults', async () => {
(os.platform as Mock).mockReturnValue('win32');
mockedExec
.mockResolvedValueOnce({ stdout: '{"Name":"partial.exe"}' }) // Missing ParentProcessId, defaults to 0
.mockResolvedValueOnce({
stdout:
'{"Name":"root.exe","ParentProcessId":0,"CommandLine":"root.exe"}',
}); // Get grandparent info
const result = await getIdeProcessInfo();
expect(result).toEqual({ pid: 1000, command: 'root.exe' });
}); });
}); });
}); });

View File

@@ -4,74 +4,43 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import { exec } from 'node:child_process'; import { exec, execFile } from 'node:child_process';
import { promisify } from 'node:util'; import { promisify } from 'node:util';
import os from 'node:os'; import os from 'node:os';
import path from 'node:path'; import path from 'node:path';
const execAsync = promisify(exec); const execAsync = promisify(exec);
const execFileAsync = promisify(execFile);
const MAX_TRAVERSAL_DEPTH = 32; const MAX_TRAVERSAL_DEPTH = 32;
/**
* Fetches the parent process ID, name, and command for a given process ID.
*
* @param pid The process ID to inspect.
* @returns A promise that resolves to the parent's PID, name, and command.
*/
async function getProcessInfo(pid: number): Promise<{ async function getProcessInfo(pid: number): Promise<{
parentPid: number; parentPid: number;
name: string; name: string;
command: string; command: string;
}> { }> {
// Only used for Unix systems (macOS and Linux)
try { try {
const platform = os.platform(); const command = `ps -o ppid=,command= -p ${pid}`;
if (platform === 'win32') { const { stdout } = await execAsync(command);
const powershellCommand = [ const trimmedStdout = stdout.trim();
'$p = Get-CimInstance Win32_Process', if (!trimmedStdout) {
`-Filter 'ProcessId=${pid}'`, return { parentPid: 0, name: '', command: '' };
'-ErrorAction SilentlyContinue;',
'if ($p) {',
'@{Name=$p.Name;ParentProcessId=$p.ParentProcessId;CommandLine=$p.CommandLine}',
'| ConvertTo-Json',
'}',
].join(' ');
const { stdout } = await execAsync(`powershell "${powershellCommand}"`);
const output = stdout.trim();
if (!output) return { parentPid: 0, name: '', command: '' };
const {
Name = '',
ParentProcessId = 0,
CommandLine = '',
} = JSON.parse(output);
return {
parentPid: ParentProcessId,
name: Name,
command: CommandLine ?? '',
};
} else {
const command = `ps -o ppid=,command= -p ${pid}`;
const { stdout } = await execAsync(command);
const trimmedStdout = stdout.trim();
if (!trimmedStdout) {
return { parentPid: 0, name: '', command: '' };
}
const ppidString = trimmedStdout.split(/\s+/)[0];
const parentPid = parseInt(ppidString, 10);
const fullCommand = trimmedStdout.substring(ppidString.length).trim();
const processName = path.basename(fullCommand.split(' ')[0]);
return {
parentPid: isNaN(parentPid) ? 1 : parentPid,
name: processName,
command: fullCommand,
};
} }
const parts = trimmedStdout.split(/\s+/);
const ppidString = parts[0];
const parentPid = parseInt(ppidString, 10);
const fullCommand = trimmedStdout.substring(ppidString.length).trim();
const processName = path.basename(fullCommand.split(' ')[0]);
return {
parentPid: isNaN(parentPid) ? 1 : parentPid,
name: processName,
command: fullCommand,
};
} catch (_e) { } catch (_e) {
console.debug(`Failed to get process info for pid ${pid}:`, _e);
return { parentPid: 0, name: '', command: '' }; return { parentPid: 0, name: '', command: '' };
} }
} }
/** /**
* Finds the IDE process info on Unix-like systems. * Finds the IDE process info on Unix-like systems.
* *
@@ -106,15 +75,15 @@ async function getIdeProcessInfoForUnix(): Promise<{
} catch { } catch {
// Ignore if getting grandparent fails, we'll just use the parent pid. // Ignore if getting grandparent fails, we'll just use the parent pid.
} }
const { command } = await getProcessInfo(idePid); const { command: ideCommand } = await getProcessInfo(idePid);
return { pid: idePid, command }; return { pid: idePid, command: ideCommand };
} }
if (parentPid <= 1) { if (parentPid <= 1) {
break; // Reached the root break; // Reached the root
} }
currentPid = parentPid; currentPid = parentPid;
} catch { } catch (_e) {
// Process in chain died // Process in chain died
break; break;
} }
@@ -124,50 +93,104 @@ async function getIdeProcessInfoForUnix(): Promise<{
return { pid: currentPid, command }; return { pid: currentPid, command };
} }
interface ProcessInfo {
pid: number;
parentPid: number;
name: string;
command: string;
}
interface RawProcessInfo {
ProcessId?: number;
ParentProcessId?: number;
Name?: string;
CommandLine?: string;
}
/** /**
* Finds the IDE process info on Windows. * Fetches the entire process table on Windows.
*
* The strategy is to find the great-grandchild of the root process.
*
* @returns A promise that resolves to the PID and command of the IDE process.
*/ */
async function getProcessTableWindows(): Promise<Map<number, ProcessInfo>> {
const processMap = new Map<number, ProcessInfo>();
try {
const powershellCommand =
'Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,Name,CommandLine | ConvertTo-Json -Compress';
const { stdout } = await execFileAsync(
'powershell',
['-NoProfile', '-NonInteractive', '-Command', powershellCommand],
{ maxBuffer: 10 * 1024 * 1024 },
);
if (!stdout.trim()) {
return processMap;
}
let processes: RawProcessInfo | RawProcessInfo[];
try {
processes = JSON.parse(stdout);
} catch (_e) {
return processMap;
}
if (!Array.isArray(processes)) {
processes = [processes];
}
for (const p of processes) {
if (p && typeof p.ProcessId === 'number') {
processMap.set(p.ProcessId, {
pid: p.ProcessId,
parentPid: p.ParentProcessId || 0,
name: p.Name || '',
command: p.CommandLine || '',
});
}
}
} catch (_e) {
// Fallback or error handling if PowerShell fails
}
return processMap;
}
async function getIdeProcessInfoForWindows(): Promise<{ async function getIdeProcessInfoForWindows(): Promise<{
pid: number; pid: number;
command: string; command: string;
}> { }> {
let currentPid = process.pid; // Fetch the entire process table in one go.
let previousPid = process.pid; const processMap = await getProcessTableWindows();
for (let i = 0; i < MAX_TRAVERSAL_DEPTH; i++) { const myPid = process.pid;
try { const myProc = processMap.get(myPid);
const { parentPid } = await getProcessInfo(currentPid);
if (parentPid > 0) { if (!myProc) {
try { // Fallback: return current process info if snapshot fails
const { parentPid: grandParentPid } = await getProcessInfo(parentPid); return { pid: myPid, command: '' };
if (grandParentPid === 0) { }
// We've found the grandchild of the root (`currentPid`). The IDE
// process is its child, which we've stored in `previousPid`.
const { command } = await getProcessInfo(previousPid);
return { pid: previousPid, command };
}
} catch {
// getting grandparent failed, proceed
}
}
if (parentPid <= 0) { // Perform tree traversal in memory
break; // Reached the root const ancestors: ProcessInfo[] = [];
} let curr: ProcessInfo | undefined = myProc;
previousPid = currentPid;
currentPid = parentPid; for (let i = 0; i < MAX_TRAVERSAL_DEPTH && curr; i++) {
} catch { ancestors.push(curr);
// Process in chain died
if (curr.parentPid === 0 || !processMap.has(curr.parentPid)) {
// Parent process not in map, stop traversal
break; break;
} }
curr = processMap.get(curr.parentPid);
} }
const { command } = await getProcessInfo(currentPid);
return { pid: currentPid, command }; // Use heuristic: return the great-grandparent (ancestors[length-3])
if (ancestors.length >= 3) {
const target = ancestors[ancestors.length - 3];
return { pid: target.pid, command: target.command };
} else if (ancestors.length > 0) {
const target = ancestors[ancestors.length - 1];
return { pid: target.pid, command: target.command };
}
return { pid: myPid, command: myProc.command };
} }
/** /**

View File

@@ -0,0 +1,118 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect } from 'vitest';
import { convertSchema } from './schemaConverter.js';
describe('convertSchema', () => {
describe('mode: auto (default)', () => {
it('should preserve type arrays', () => {
const input = { type: ['string', 'null'] };
expect(convertSchema(input, 'auto')).toEqual(input);
});
it('should preserve items array (tuples)', () => {
const input = {
type: 'array',
items: [{ type: 'string' }, { type: 'number' }],
};
expect(convertSchema(input, 'auto')).toEqual(input);
});
it('should preserve mixed enums', () => {
const input = { enum: [1, 2, '3'] };
expect(convertSchema(input, 'auto')).toEqual(input);
});
it('should preserve unsupported keywords', () => {
const input = {
$schema: 'http://json-schema.org/draft-07/schema#',
exclusiveMinimum: 10,
type: 'number',
};
expect(convertSchema(input, 'auto')).toEqual(input);
});
});
describe('mode: openapi_30 (strict)', () => {
it('should convert type arrays to nullable', () => {
const input = { type: ['string', 'null'] };
const expected = { type: 'string', nullable: true };
expect(convertSchema(input, 'openapi_30')).toEqual(expected);
});
it('should fallback to first type for non-nullable arrays', () => {
const input = { type: ['string', 'number'] };
const expected = { type: 'string' };
expect(convertSchema(input, 'openapi_30')).toEqual(expected);
});
it('should convert const to enum', () => {
const input = { const: 'foo' };
const expected = { enum: ['foo'] };
expect(convertSchema(input, 'openapi_30')).toEqual(expected);
});
it('should convert exclusiveMinimum number to boolean', () => {
const input = { type: 'number', exclusiveMinimum: 10 };
const expected = {
type: 'number',
minimum: 10,
exclusiveMinimum: true,
};
expect(convertSchema(input, 'openapi_30')).toEqual(expected);
});
it('should convert nested objects recursively', () => {
const input = {
type: 'object',
properties: {
prop1: { type: ['integer', 'null'], exclusiveMaximum: 5 },
},
};
const expected = {
type: 'object',
properties: {
prop1: {
type: 'integer',
nullable: true,
maximum: 5,
exclusiveMaximum: true,
},
},
};
expect(convertSchema(input, 'openapi_30')).toEqual(expected);
});
it('should stringify enums', () => {
const input = { enum: [1, 2, '3'] };
const expected = { enum: ['1', '2', '3'] };
expect(convertSchema(input, 'openapi_30')).toEqual(expected);
});
it('should remove tuple items (array of schemas)', () => {
const input = {
type: 'array',
items: [{ type: 'string' }, { type: 'number' }],
};
const expected = { type: 'array' };
expect(convertSchema(input, 'openapi_30')).toEqual(expected);
});
it('should remove unsupported keywords', () => {
const input = {
$schema: 'http://json-schema.org/draft-07/schema#',
$id: '#foo',
type: 'string',
default: 'bar',
dependencies: { foo: ['bar'] },
patternProperties: { '^foo': { type: 'string' } },
};
const expected = { type: 'string' };
expect(convertSchema(input, 'openapi_30')).toEqual(expected);
});
});
});

View File

@@ -0,0 +1,135 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Utility for converting JSON Schemas to be compatible with different LLM providers.
* Specifically focuses on downgrading modern JSON Schema (Draft 7/2020-12) to
* OpenAPI 3.0 compatible Schema Objects, which is required for Google Gemini API.
*/
export type SchemaComplianceMode = 'auto' | 'openapi_30';
/**
* Converts a JSON Schema to be compatible with the specified compliance mode.
*/
export function convertSchema(
schema: Record<string, unknown>,
mode: SchemaComplianceMode = 'auto',
): Record<string, unknown> {
if (mode === 'openapi_30') {
return toOpenAPI30(schema);
}
// Default ('auto') mode now does nothing.
return schema;
}
/**
* Converts Modern JSON Schema to OpenAPI 3.0 Schema Object.
* Attempts to preserve semantics where possible through transformations.
*/
function toOpenAPI30(schema: Record<string, unknown>): Record<string, unknown> {
const convert = (obj: unknown): unknown => {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
if (Array.isArray(obj)) {
return obj.map(convert);
}
const source = obj as Record<string, unknown>;
const target: Record<string, unknown> = {};
// 1. Type Handling
if (Array.isArray(source['type'])) {
const types = source['type'] as string[];
// Handle ["string", "null"] pattern common in modern schemas
if (types.length === 2 && types.includes('null')) {
target['type'] = types.find((t) => t !== 'null');
target['nullable'] = true;
} else {
// Fallback for other unions: take the first non-null type
// OpenAPI 3.0 doesn't support type arrays.
// Ideal fix would be anyOf, but simple fallback is safer for now.
target['type'] = types[0];
}
} else if (source['type'] !== undefined) {
target['type'] = source['type'];
}
// 2. Const Handling (Draft 6+) -> Enum (OpenAPI 3.0)
if (source['const'] !== undefined) {
target['enum'] = [source['const']];
delete target['const'];
}
// 3. Exclusive Limits (Draft 6+ number) -> (Draft 4 boolean)
// exclusiveMinimum: 10 -> minimum: 10, exclusiveMinimum: true
if (typeof source['exclusiveMinimum'] === 'number') {
target['minimum'] = source['exclusiveMinimum'];
target['exclusiveMinimum'] = true;
}
if (typeof source['exclusiveMaximum'] === 'number') {
target['maximum'] = source['exclusiveMaximum'];
target['exclusiveMaximum'] = true;
}
// 4. Array Items (Tuple -> Single Schema)
// OpenAPI 3.0 items must be a schema object, not an array of schemas
if (Array.isArray(source['items'])) {
// Tuple support is tricky.
// Best effort: Use the first item's schema as a generic array type
// or convert to an empty object (any type) if mixed.
// For now, we'll strip it to allow validation to pass (accepts any items)
// This matches the legacy behavior but is explicit.
// Ideally, we could use `oneOf` on the items if we wanted to be stricter.
delete target['items'];
} else if (
typeof source['items'] === 'object' &&
source['items'] !== null
) {
target['items'] = convert(source['items']);
}
// 5. Enum Stringification
// Gemini strictly requires enums to be strings
if (Array.isArray(source['enum'])) {
target['enum'] = source['enum'].map(String);
}
// 6. Recursively process other properties
for (const [key, value] of Object.entries(source)) {
// Skip fields we've already handled or want to remove
if (
key === 'type' ||
key === 'const' ||
key === 'exclusiveMinimum' ||
key === 'exclusiveMaximum' ||
key === 'items' ||
key === 'enum' ||
key === '$schema' ||
key === '$id' ||
key === 'default' || // Optional: Gemini sometimes complains about defaults conflicting with types
key === 'dependencies' ||
key === 'patternProperties'
) {
continue;
}
target[key] = convert(value);
}
// Preserve default if it doesn't conflict (simple pass-through)
// if (source['default'] !== undefined) {
// target['default'] = source['default'];
// }
return target;
};
return convert(schema) as Record<string, unknown>;
}

View File

@@ -13,9 +13,8 @@ npm install @qwen-code/sdk
## Requirements ## Requirements
- Node.js >= 20.0.0 - Node.js >= 20.0.0
- [Qwen Code](https://github.com/QwenLM/qwen-code) >= 0.4.0 (stable) installed and accessible in PATH
> **Note for nvm users**: If you use nvm to manage Node.js versions, the SDK may not be able to auto-detect the Qwen Code executable. You should explicitly set the `pathToQwenExecutable` option to the full path of the `qwen` binary. > From v0.1.1, the CLI is bundled with the SDK. So no standalone CLI installation is needed.
## Quick Start ## Quick Start
@@ -372,6 +371,23 @@ try {
} }
``` ```
## FAQ / Troubleshooting
### Version 0.1.0 Requirements
If you're using SDK version **0.1.0**, please note the following requirements:
#### Qwen Code Installation Required
Version 0.1.0 requires [Qwen Code](https://github.com/QwenLM/qwen-code) **>= 0.4.0** to be installed separately and accessible in your PATH.
```bash
# Install Qwen Code globally
npm install -g qwen-code@^0.4.0
```
**Note**: From version **0.1.1** onwards, the CLI is bundled with the SDK, so no separate Qwen Code installation is needed.
## License ## License
Apache-2.0 - see [LICENSE](./LICENSE) for details. Apache-2.0 - see [LICENSE](./LICENSE) for details.

View File

@@ -1,6 +1,6 @@
{ {
"name": "@qwen-code/sdk", "name": "@qwen-code/sdk",
"version": "0.5.1", "version": "0.6.0",
"description": "TypeScript SDK for programmatic access to qwen-code CLI", "description": "TypeScript SDK for programmatic access to qwen-code CLI",
"main": "./dist/index.cjs", "main": "./dist/index.cjs",
"module": "./dist/index.mjs", "module": "./dist/index.mjs",
@@ -45,7 +45,8 @@
"node": ">=18.0.0" "node": ">=18.0.0"
}, },
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.0.4" "@modelcontextprotocol/sdk": "^1.0.4",
"tiktoken": "^1.0.21"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.14.0", "@types/node": "^20.14.0",

View File

@@ -91,3 +91,35 @@ if (existsSync(licenseSource)) {
console.warn('Could not copy LICENSE:', error.message); console.warn('Could not copy LICENSE:', error.message);
} }
} }
console.log('Bundling CLI into SDK package...');
const repoRoot = join(rootDir, '..', '..');
const rootDistDir = join(repoRoot, 'dist');
if (!existsSync(rootDistDir) || !existsSync(join(rootDistDir, 'cli.js'))) {
console.log('Building CLI bundle...');
try {
execSync('npm run bundle', { stdio: 'inherit', cwd: repoRoot });
} catch (error) {
console.error('Failed to build CLI bundle:', error.message);
throw error;
}
}
const cliDistDir = join(rootDir, 'dist', 'cli');
mkdirSync(cliDistDir, { recursive: true });
console.log('Copying CLI bundle...');
cpSync(join(rootDistDir, 'cli.js'), join(cliDistDir, 'cli.js'));
const vendorSource = join(rootDistDir, 'vendor');
if (existsSync(vendorSource)) {
cpSync(vendorSource, join(cliDistDir, 'vendor'), { recursive: true });
}
const localesSource = join(rootDistDir, 'locales');
if (existsSync(localesSource)) {
cpSync(localesSource, join(cliDistDir, 'locales'), { recursive: true });
}
console.log('CLI bundle copied successfully to SDK package');

View File

@@ -2,24 +2,16 @@
* CLI path auto-detection and subprocess spawning utilities * CLI path auto-detection and subprocess spawning utilities
* *
* Supports multiple execution modes: * Supports multiple execution modes:
* 1. Native binary: 'qwen' (production) * 1. Bundled CLI: Node.js bundle included in the SDK package (default)
* 2. Node.js bundle: 'node /path/to/cli.js' (production validation) * 2. Node.js bundle: 'node /path/to/cli.js' (custom path)
* 3. Bun bundle: 'bun /path/to/cli.js' (alternative runtime) * 3. Bun bundle: 'bun /path/to/cli.js' (alternative runtime)
* 4. TypeScript source: 'tsx /path/to/index.ts' (development) * 4. TypeScript source: 'tsx /path/to/index.ts' (development)
*
* Auto-detection locations for native binary:
* 1. QWEN_CODE_CLI_PATH environment variable
* 2. ~/.volta/bin/qwen
* 3. ~/.npm-global/bin/qwen
* 4. /usr/local/bin/qwen
* 5. ~/.local/bin/qwen
* 6. ~/node_modules/.bin/qwen
* 7. ~/.yarn/bin/qwen
*/ */
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import * as path from 'node:path'; import * as path from 'node:path';
import { execSync } from 'node:child_process'; import { execSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
/** /**
* Executable types supported by the SDK * Executable types supported by the SDK
@@ -40,49 +32,38 @@ export type SpawnInfo = {
originalInput: string; originalInput: string;
}; };
export function findNativeCliPath(): string { function getBundledCliPath(): string | null {
const homeDir = process.env['HOME'] || process.env['USERPROFILE'] || ''; try {
const currentFile =
typeof __filename !== 'undefined'
? __filename
: fileURLToPath(import.meta.url);
const candidates: Array<string | undefined> = [ const currentDir = path.dirname(currentFile);
// 1. Environment variable (highest priority)
process.env['QWEN_CODE_CLI_PATH'],
// 2. Volta bin const bundledCliPath = path.join(currentDir, 'cli', 'cli.js');
path.join(homeDir, '.volta', 'bin', 'qwen'),
// 3. Global npm installations if (fs.existsSync(bundledCliPath)) {
path.join(homeDir, '.npm-global', 'bin', 'qwen'), return bundledCliPath;
// 4. Common Unix binary locations
'/usr/local/bin/qwen',
// 5. User local bin
path.join(homeDir, '.local', 'bin', 'qwen'),
// 6. Node modules bin in home directory
path.join(homeDir, 'node_modules', '.bin', 'qwen'),
// 7. Yarn global bin
path.join(homeDir, '.yarn', 'bin', 'qwen'),
];
// Find first existing candidate
for (const candidate of candidates) {
if (candidate && fs.existsSync(candidate)) {
return path.resolve(candidate);
} }
return null;
} catch {
return null;
}
}
export function findNativeCliPath(): string {
const bundledCli = getBundledCliPath();
if (bundledCli) {
return bundledCli;
} }
// Not found - throw helpful error
throw new Error( throw new Error(
'qwen CLI not found. Please:\n' + 'Bundled qwen CLI not found. The CLI should be included in the SDK package.\n' +
' 1. Install qwen globally: npm install -g qwen\n' + 'If you need to use a custom CLI, provide explicit executable:\n' +
' 2. Or provide explicit executable: query({ pathToQwenExecutable: "/path/to/qwen" })\n' + ' query({ pathToQwenExecutable: "/path/to/cli.js" })\n' +
' 3. Or set environment variable: QWEN_CODE_CLI_PATH="/path/to/qwen"\n' +
'\n' +
'For development/testing, you can also use:\n' +
' • TypeScript source: query({ pathToQwenExecutable: "/path/to/index.ts" })\n' + ' • TypeScript source: query({ pathToQwenExecutable: "/path/to/index.ts" })\n' +
' • Node.js bundle: query({ pathToQwenExecutable: "/path/to/cli.js" })\n' +
' • Force specific runtime: query({ pathToQwenExecutable: "bun:/path/to/cli.js" })', ' • Force specific runtime: query({ pathToQwenExecutable: "bun:/path/to/cli.js" })',
); );
} }

View File

@@ -38,6 +38,8 @@ describe('CLI Path Utilities', () => {
mockFs.statSync.mockReturnValue({ mockFs.statSync.mockReturnValue({
isFile: () => true, isFile: () => true,
} as ReturnType<typeof import('fs').statSync>); } as ReturnType<typeof import('fs').statSync>);
// Default: return true for existsSync (can be overridden in specific tests)
mockFs.existsSync.mockReturnValue(true);
}); });
afterEach(() => { afterEach(() => {
@@ -50,28 +52,26 @@ describe('CLI Path Utilities', () => {
describe('parseExecutableSpec', () => { describe('parseExecutableSpec', () => {
describe('auto-detection (no spec provided)', () => { describe('auto-detection (no spec provided)', () => {
it('should auto-detect native CLI when no spec provided', () => { it('should auto-detect bundled CLI when no spec provided', () => {
// Mock environment variable // Mock existsSync to return true for bundled CLI
const originalEnv = process.env['QWEN_CODE_CLI_PATH']; mockFs.existsSync.mockImplementation((p) => {
process.env['QWEN_CODE_CLI_PATH'] = '/usr/local/bin/qwen'; const pathStr = p.toString();
mockFs.existsSync.mockReturnValue(true); return (
pathStr.includes('cli/cli.js') || pathStr.includes('cli\\cli.js')
);
});
const result = parseExecutableSpec(); const result = parseExecutableSpec();
expect(result).toEqual({ expect(result.executablePath).toContain('cli.js');
executablePath: path.resolve('/usr/local/bin/qwen'), expect(result.isExplicitRuntime).toBe(false);
isExplicitRuntime: false,
});
// Restore env
process.env['QWEN_CODE_CLI_PATH'] = originalEnv;
}); });
it('should throw when auto-detection fails', () => { it('should throw when bundled CLI not found', () => {
mockFs.existsSync.mockReturnValue(false); mockFs.existsSync.mockReturnValue(false);
expect(() => parseExecutableSpec()).toThrow( expect(() => parseExecutableSpec()).toThrow(
'qwen CLI not found. Please:', 'Bundled qwen CLI not found',
); );
}); });
}); });
@@ -361,65 +361,44 @@ describe('CLI Path Utilities', () => {
}); });
describe('auto-detection fallback', () => { describe('auto-detection fallback', () => {
it('should auto-detect when no spec provided', () => { it('should auto-detect bundled CLI when no spec provided', () => {
// Mock environment variable // Mock existsSync to return true for bundled CLI
const originalEnv = process.env['QWEN_CODE_CLI_PATH']; mockFs.existsSync.mockImplementation((p) => {
process.env['QWEN_CODE_CLI_PATH'] = '/usr/local/bin/qwen'; const pathStr = p.toString();
return (
pathStr.includes('cli/cli.js') || pathStr.includes('cli\\cli.js')
);
});
const result = prepareSpawnInfo(); const result = prepareSpawnInfo();
expect(result).toEqual({ expect(result.command).toBe(process.execPath);
command: path.resolve('/usr/local/bin/qwen'), expect(result.args[0]).toContain('cli.js');
args: [], expect(result.type).toBe('node');
type: 'native', expect(result.originalInput).toBe('');
originalInput: '',
});
// Restore env
process.env['QWEN_CODE_CLI_PATH'] = originalEnv;
}); });
}); });
}); });
describe('findNativeCliPath', () => { describe('findNativeCliPath', () => {
it('should find CLI from environment variable', () => { it('should find bundled CLI', () => {
const originalEnv = process.env['QWEN_CODE_CLI_PATH']; // Mock existsSync to return true for bundled CLI
process.env['QWEN_CODE_CLI_PATH'] = '/custom/path/to/qwen';
mockFs.existsSync.mockReturnValue(true);
const result = findNativeCliPath();
expect(result).toBe(path.resolve('/custom/path/to/qwen'));
process.env['QWEN_CODE_CLI_PATH'] = originalEnv;
});
it('should search common installation locations', () => {
const originalEnv = process.env['QWEN_CODE_CLI_PATH'];
delete process.env['QWEN_CODE_CLI_PATH'];
// Mock fs.existsSync to return true for volta bin
// Use path.join to match platform-specific path separators
const voltaBinPath = path.join('.volta', 'bin', 'qwen');
mockFs.existsSync.mockImplementation((p) => { mockFs.existsSync.mockImplementation((p) => {
return p.toString().includes(voltaBinPath); const pathStr = p.toString();
return (
pathStr.includes('cli/cli.js') || pathStr.includes('cli\\cli.js')
);
}); });
const result = findNativeCliPath(); const result = findNativeCliPath();
expect(result).toContain(voltaBinPath); expect(result).toContain('cli.js');
process.env['QWEN_CODE_CLI_PATH'] = originalEnv;
}); });
it('should throw descriptive error when CLI not found', () => { it('should throw descriptive error when bundled CLI not found', () => {
const originalEnv = process.env['QWEN_CODE_CLI_PATH'];
delete process.env['QWEN_CODE_CLI_PATH'];
mockFs.existsSync.mockReturnValue(false); mockFs.existsSync.mockReturnValue(false);
expect(() => findNativeCliPath()).toThrow('qwen CLI not found. Please:'); expect(() => findNativeCliPath()).toThrow('Bundled qwen CLI not found');
process.env['QWEN_CODE_CLI_PATH'] = originalEnv;
}); });
}); });
@@ -634,13 +613,10 @@ describe('CLI Path Utilities', () => {
mockFs.existsSync.mockReturnValue(false); mockFs.existsSync.mockReturnValue(false);
expect(() => parseExecutableSpec('/missing/file')).toThrow( expect(() => parseExecutableSpec('/missing/file')).toThrow(
'Set QWEN_CODE_CLI_PATH environment variable', 'Executable file not found at',
); );
expect(() => parseExecutableSpec('/missing/file')).toThrow( expect(() => parseExecutableSpec('/missing/file')).toThrow(
'Install qwen globally: npm install -g qwen', 'Please check the file path and ensure the file exists',
);
expect(() => parseExecutableSpec('/missing/file')).toThrow(
'Force specific runtime: bun:/path/to/cli.js or tsx:/path/to/index.ts',
); );
}); });
}); });

View File

@@ -1,6 +1,6 @@
{ {
"name": "@qwen-code/qwen-code-test-utils", "name": "@qwen-code/qwen-code-test-utils",
"version": "0.5.1", "version": "0.6.0",
"private": true, "private": true,
"main": "src/index.ts", "main": "src/index.ts",
"license": "Apache-2.0", "license": "Apache-2.0",

View File

@@ -2,7 +2,7 @@
"name": "qwen-code-vscode-ide-companion", "name": "qwen-code-vscode-ide-companion",
"displayName": "Qwen Code Companion", "displayName": "Qwen Code Companion",
"description": "Enable Qwen Code with direct access to your VS Code workspace.", "description": "Enable Qwen Code with direct access to your VS Code workspace.",
"version": "0.5.1", "version": "0.6.0",
"publisher": "qwenlm", "publisher": "qwenlm",
"icon": "assets/icon.png", "icon": "assets/icon.png",
"repository": { "repository": {
@@ -11,7 +11,7 @@
"directory": "packages/vscode-ide-companion" "directory": "packages/vscode-ide-companion"
}, },
"engines": { "engines": {
"vscode": "^1.99.0" "vscode": "^1.85.0"
}, },
"license": "LICENSE", "license": "LICENSE",
"preview": true, "preview": true,
@@ -137,7 +137,7 @@
"@types/react": "^19.1.8", "@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6", "@types/react-dom": "^19.1.6",
"@types/semver": "^7.7.1", "@types/semver": "^7.7.1",
"@types/vscode": "^1.99.0", "@types/vscode": "^1.85.0",
"@typescript-eslint/eslint-plugin": "^8.31.1", "@typescript-eslint/eslint-plugin": "^8.31.1",
"@typescript-eslint/parser": "^8.31.1", "@typescript-eslint/parser": "^8.31.1",
"@vscode/vsce": "^3.6.0", "@vscode/vsce": "^3.6.0",

View File

@@ -35,7 +35,7 @@ function npmBin() {
function run(cmd, args, opts = {}) { function run(cmd, args, opts = {}) {
const res = spawnSync(cmd, args, { const res = spawnSync(cmd, args, {
stdio: 'inherit', stdio: 'inherit',
shell: process.platform === 'win32' ? true : false, shell: process.platform === 'win32',
...opts, ...opts,
}); });
if (res.error) { if (res.error) {

View File

@@ -27,13 +27,14 @@ vi.mock('node:fs/promises', () => ({
writeFile: vi.fn(() => Promise.resolve(undefined)), writeFile: vi.fn(() => Promise.resolve(undefined)),
unlink: vi.fn(() => Promise.resolve(undefined)), unlink: vi.fn(() => Promise.resolve(undefined)),
chmod: vi.fn(() => Promise.resolve(undefined)), chmod: vi.fn(() => Promise.resolve(undefined)),
mkdir: vi.fn(() => Promise.resolve(undefined)),
})); }));
vi.mock('node:os', async (importOriginal) => { vi.mock('node:os', async (importOriginal) => {
const actual = await importOriginal<typeof os>(); const actual = await importOriginal<typeof os>();
return { return {
...actual, ...actual,
tmpdir: vi.fn(() => '/tmp'), homedir: vi.fn(() => '/home/test'),
}; };
}); });
@@ -128,30 +129,24 @@ describe('IDEServer', () => {
); );
const port = getPortFromMock(replaceMock); const port = getPortFromMock(replaceMock);
const expectedPortFile = path.join( const expectedLockFile = path.join(
'/tmp', '/home/test',
`qwen-code-ide-server-${port}.json`, '.qwen',
); 'ide',
const expectedPpidPortFile = path.join( `${port}.lock`,
'/tmp',
`qwen-code-ide-server-${process.ppid}.json`,
); );
const expectedContent = JSON.stringify({ const expectedContent = JSON.stringify({
port: parseInt(port, 10), port: parseInt(port, 10),
workspacePath: expectedWorkspacePaths, workspacePath: expectedWorkspacePaths,
ppid: process.ppid, ppid: process.ppid,
authToken: 'test-auth-token', authToken: 'test-auth-token',
ideName: 'VS Code',
}); });
expect(fs.writeFile).toHaveBeenCalledWith( expect(fs.writeFile).toHaveBeenCalledWith(
expectedPortFile, expectedLockFile,
expectedContent, expectedContent,
); );
expect(fs.writeFile).toHaveBeenCalledWith( expect(fs.chmod).toHaveBeenCalledWith(expectedLockFile, 0o600);
expectedPpidPortFile,
expectedContent,
);
expect(fs.chmod).toHaveBeenCalledWith(expectedPortFile, 0o600);
expect(fs.chmod).toHaveBeenCalledWith(expectedPpidPortFile, 0o600);
}); });
it('should set a single folder path', async () => { it('should set a single folder path', async () => {
@@ -166,30 +161,24 @@ describe('IDEServer', () => {
); );
const port = getPortFromMock(replaceMock); const port = getPortFromMock(replaceMock);
const expectedPortFile = path.join( const expectedLockFile = path.join(
'/tmp', '/home/test',
`qwen-code-ide-server-${port}.json`, '.qwen',
); 'ide',
const expectedPpidPortFile = path.join( `${port}.lock`,
'/tmp',
`qwen-code-ide-server-${process.ppid}.json`,
); );
const expectedContent = JSON.stringify({ const expectedContent = JSON.stringify({
port: parseInt(port, 10), port: parseInt(port, 10),
workspacePath: '/foo/bar', workspacePath: '/foo/bar',
ppid: process.ppid, ppid: process.ppid,
authToken: 'test-auth-token', authToken: 'test-auth-token',
ideName: 'VS Code',
}); });
expect(fs.writeFile).toHaveBeenCalledWith( expect(fs.writeFile).toHaveBeenCalledWith(
expectedPortFile, expectedLockFile,
expectedContent, expectedContent,
); );
expect(fs.writeFile).toHaveBeenCalledWith( expect(fs.chmod).toHaveBeenCalledWith(expectedLockFile, 0o600);
expectedPpidPortFile,
expectedContent,
);
expect(fs.chmod).toHaveBeenCalledWith(expectedPortFile, 0o600);
expect(fs.chmod).toHaveBeenCalledWith(expectedPpidPortFile, 0o600);
}); });
it('should set an empty string if no folders are open', async () => { it('should set an empty string if no folders are open', async () => {
@@ -204,30 +193,24 @@ describe('IDEServer', () => {
); );
const port = getPortFromMock(replaceMock); const port = getPortFromMock(replaceMock);
const expectedPortFile = path.join( const expectedLockFile = path.join(
'/tmp', '/home/test',
`qwen-code-ide-server-${port}.json`, '.qwen',
); 'ide',
const expectedPpidPortFile = path.join( `${port}.lock`,
'/tmp',
`qwen-code-ide-server-${process.ppid}.json`,
); );
const expectedContent = JSON.stringify({ const expectedContent = JSON.stringify({
port: parseInt(port, 10), port: parseInt(port, 10),
workspacePath: '', workspacePath: '',
ppid: process.ppid, ppid: process.ppid,
authToken: 'test-auth-token', authToken: 'test-auth-token',
ideName: 'VS Code',
}); });
expect(fs.writeFile).toHaveBeenCalledWith( expect(fs.writeFile).toHaveBeenCalledWith(
expectedPortFile, expectedLockFile,
expectedContent, expectedContent,
); );
expect(fs.writeFile).toHaveBeenCalledWith( expect(fs.chmod).toHaveBeenCalledWith(expectedLockFile, 0o600);
expectedPpidPortFile,
expectedContent,
);
expect(fs.chmod).toHaveBeenCalledWith(expectedPortFile, 0o600);
expect(fs.chmod).toHaveBeenCalledWith(expectedPpidPortFile, 0o600);
}); });
it('should update the path when workspace folders change', async () => { it('should update the path when workspace folders change', async () => {
@@ -256,30 +239,24 @@ describe('IDEServer', () => {
); );
const port = getPortFromMock(replaceMock); const port = getPortFromMock(replaceMock);
const expectedPortFile = path.join( const expectedLockFile = path.join(
'/tmp', '/home/test',
`qwen-code-ide-server-${port}.json`, '.qwen',
); 'ide',
const expectedPpidPortFile = path.join( `${port}.lock`,
'/tmp',
`qwen-code-ide-server-${process.ppid}.json`,
); );
const expectedContent = JSON.stringify({ const expectedContent = JSON.stringify({
port: parseInt(port, 10), port: parseInt(port, 10),
workspacePath: expectedWorkspacePaths, workspacePath: expectedWorkspacePaths,
ppid: process.ppid, ppid: process.ppid,
authToken: 'test-auth-token', authToken: 'test-auth-token',
ideName: 'VS Code',
}); });
expect(fs.writeFile).toHaveBeenCalledWith( expect(fs.writeFile).toHaveBeenCalledWith(
expectedPortFile, expectedLockFile,
expectedContent, expectedContent,
); );
expect(fs.writeFile).toHaveBeenCalledWith( expect(fs.chmod).toHaveBeenCalledWith(expectedLockFile, 0o600);
expectedPpidPortFile,
expectedContent,
);
expect(fs.chmod).toHaveBeenCalledWith(expectedPortFile, 0o600);
expect(fs.chmod).toHaveBeenCalledWith(expectedPpidPortFile, 0o600);
// Simulate removing a folder // Simulate removing a folder
vscodeMock.workspace.workspaceFolders = [{ uri: { fsPath: '/baz/qux' } }]; vscodeMock.workspace.workspaceFolders = [{ uri: { fsPath: '/baz/qux' } }];
@@ -294,36 +271,26 @@ describe('IDEServer', () => {
workspacePath: '/baz/qux', workspacePath: '/baz/qux',
ppid: process.ppid, ppid: process.ppid,
authToken: 'test-auth-token', authToken: 'test-auth-token',
ideName: 'VS Code',
}); });
expect(fs.writeFile).toHaveBeenCalledWith( expect(fs.writeFile).toHaveBeenCalledWith(
expectedPortFile, expectedLockFile,
expectedContent2, expectedContent2,
); );
expect(fs.writeFile).toHaveBeenCalledWith( expect(fs.chmod).toHaveBeenCalledWith(expectedLockFile, 0o600);
expectedPpidPortFile,
expectedContent2,
);
expect(fs.chmod).toHaveBeenCalledWith(expectedPortFile, 0o600);
expect(fs.chmod).toHaveBeenCalledWith(expectedPpidPortFile, 0o600);
}); });
it('should clear env vars and delete port file on stop', async () => { it('should clear env vars and delete lock file on stop', async () => {
await ideServer.start(mockContext); await ideServer.start(mockContext);
const replaceMock = mockContext.environmentVariableCollection.replace; const replaceMock = mockContext.environmentVariableCollection.replace;
const port = getPortFromMock(replaceMock); const port = getPortFromMock(replaceMock);
const portFile = path.join('/tmp', `qwen-code-ide-server-${port}.json`); const lockFile = path.join('/home/test', '.qwen', 'ide', `${port}.lock`);
const ppidPortFile = path.join( expect(fs.writeFile).toHaveBeenCalledWith(lockFile, expect.any(String));
'/tmp',
`qwen-code-ide-server-${process.ppid}.json`,
);
expect(fs.writeFile).toHaveBeenCalledWith(portFile, expect.any(String));
expect(fs.writeFile).toHaveBeenCalledWith(ppidPortFile, expect.any(String));
await ideServer.stop(); await ideServer.stop();
expect(mockContext.environmentVariableCollection.clear).toHaveBeenCalled(); expect(mockContext.environmentVariableCollection.clear).toHaveBeenCalled();
expect(fs.unlink).toHaveBeenCalledWith(portFile); expect(fs.unlink).toHaveBeenCalledWith(lockFile);
expect(fs.unlink).toHaveBeenCalledWith(ppidPortFile);
}); });
it.skipIf(process.platform !== 'win32')( it.skipIf(process.platform !== 'win32')(
@@ -344,30 +311,24 @@ describe('IDEServer', () => {
); );
const port = getPortFromMock(replaceMock); const port = getPortFromMock(replaceMock);
const expectedPortFile = path.join( const expectedLockFile = path.join(
'/tmp', '/home/test',
`qwen-code-ide-server-${port}.json`, '.qwen',
); 'ide',
const expectedPpidPortFile = path.join( `${port}.lock`,
'/tmp',
`qwen-code-ide-server-${process.ppid}.json`,
); );
const expectedContent = JSON.stringify({ const expectedContent = JSON.stringify({
port: parseInt(port, 10), port: parseInt(port, 10),
workspacePath: expectedWorkspacePaths, workspacePath: expectedWorkspacePaths,
ppid: process.ppid, ppid: process.ppid,
authToken: 'test-auth-token', authToken: 'test-auth-token',
ideName: 'VS Code',
}); });
expect(fs.writeFile).toHaveBeenCalledWith( expect(fs.writeFile).toHaveBeenCalledWith(
expectedPortFile, expectedLockFile,
expectedContent, expectedContent,
); );
expect(fs.writeFile).toHaveBeenCalledWith( expect(fs.chmod).toHaveBeenCalledWith(expectedLockFile, 0o600);
expectedPpidPortFile,
expectedContent,
);
expect(fs.chmod).toHaveBeenCalledWith(expectedPortFile, 0o600);
expect(fs.chmod).toHaveBeenCalledWith(expectedPpidPortFile, 0o600);
}, },
); );
@@ -379,7 +340,7 @@ describe('IDEServer', () => {
port = (ideServer as unknown as { port: number }).port; port = (ideServer as unknown as { port: number }).port;
}); });
it('should allow request without auth token for backwards compatibility', async () => { it('should reject request without auth token', async () => {
const response = await fetch(`http://localhost:${port}/mcp`, { const response = await fetch(`http://localhost:${port}/mcp`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
@@ -390,7 +351,9 @@ describe('IDEServer', () => {
id: 1, id: 1,
}), }),
}); });
expect(response.status).not.toBe(401); expect(response.status).toBe(401);
const body = await response.text();
expect(body).toBe('Unauthorized');
}); });
it('should allow request with valid auth token', async () => { it('should allow request with valid auth token', async () => {
@@ -550,6 +513,7 @@ describe('IDEServer HTTP endpoints', () => {
headers: { headers: {
Host: `localhost:${port}`, Host: `localhost:${port}`,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Authorization: 'Bearer test-auth-token',
}, },
}, },
JSON.stringify({ jsonrpc: '2.0', method: 'initialize' }), JSON.stringify({ jsonrpc: '2.0', method: 'initialize' }),

View File

@@ -10,6 +10,7 @@ import {
IdeContextNotificationSchema, IdeContextNotificationSchema,
OpenDiffRequestSchema, OpenDiffRequestSchema,
} from '@qwen-code/qwen-code-core/src/ide/types.js'; } from '@qwen-code/qwen-code-core/src/ide/types.js';
import { detectIdeFromEnv } from '@qwen-code/qwen-code-core/src/ide/detect-ide.js';
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'; import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
@@ -38,12 +39,24 @@ class CORSError extends Error {
const MCP_SESSION_ID_HEADER = 'mcp-session-id'; const MCP_SESSION_ID_HEADER = 'mcp-session-id';
const IDE_SERVER_PORT_ENV_VAR = 'QWEN_CODE_IDE_SERVER_PORT'; const IDE_SERVER_PORT_ENV_VAR = 'QWEN_CODE_IDE_SERVER_PORT';
const IDE_WORKSPACE_PATH_ENV_VAR = 'QWEN_CODE_IDE_WORKSPACE_PATH'; const IDE_WORKSPACE_PATH_ENV_VAR = 'QWEN_CODE_IDE_WORKSPACE_PATH';
const QWEN_DIR = '.qwen';
const IDE_DIR = 'ide';
async function getGlobalIdeDir(): Promise<string> {
const homeDir = os.homedir();
// Prefer home dir, but fall back to tmpdir if unavailable (matches core Storage behavior).
const baseDir = homeDir
? path.join(homeDir, QWEN_DIR)
: path.join(os.tmpdir(), QWEN_DIR);
const ideDir = path.join(baseDir, IDE_DIR);
await fs.mkdir(ideDir, { recursive: true });
return ideDir;
}
interface WritePortAndWorkspaceArgs { interface WritePortAndWorkspaceArgs {
context: vscode.ExtensionContext; context: vscode.ExtensionContext;
port: number; port: number;
portFile: string; lockFile: string;
ppidPortFile: string;
authToken: string; authToken: string;
log: (message: string) => void; log: (message: string) => void;
} }
@@ -51,8 +64,7 @@ interface WritePortAndWorkspaceArgs {
async function writePortAndWorkspace({ async function writePortAndWorkspace({
context, context,
port, port,
portFile, lockFile,
ppidPortFile,
authToken, authToken,
log, log,
}: WritePortAndWorkspaceArgs): Promise<void> { }: WritePortAndWorkspaceArgs): Promise<void> {
@@ -71,26 +83,24 @@ async function writePortAndWorkspace({
workspacePath, workspacePath,
); );
const ideInfo = detectIdeFromEnv();
const content = JSON.stringify({ const content = JSON.stringify({
port, port,
workspacePath, workspacePath,
ppid: process.ppid, ppid: process.ppid,
authToken, authToken,
ideName: ideInfo.displayName,
}); });
log(`Writing port file to: ${portFile}`); log(`Writing IDE lock file to: ${lockFile}`);
log(`Writing ppid port file to: ${ppidPortFile}`);
try { try {
await Promise.all([ await fs.mkdir(path.dirname(lockFile), { recursive: true });
fs.writeFile(portFile, content).then(() => fs.chmod(portFile, 0o600)), await fs.writeFile(lockFile, content);
fs await fs.chmod(lockFile, 0o600);
.writeFile(ppidPortFile, content)
.then(() => fs.chmod(ppidPortFile, 0o600)),
]);
} catch (err) { } catch (err) {
const message = err instanceof Error ? err.message : String(err); const message = err instanceof Error ? err.message : String(err);
log(`Failed to write port to file: ${message}`); log(`Failed to write IDE lock file: ${message}`);
} }
} }
@@ -121,8 +131,7 @@ export class IDEServer {
private server: HTTPServer | undefined; private server: HTTPServer | undefined;
private context: vscode.ExtensionContext | undefined; private context: vscode.ExtensionContext | undefined;
private log: (message: string) => void; private log: (message: string) => void;
private portFile: string | undefined; private lockFile: string | undefined;
private ppidPortFile: string | undefined;
private port: number | undefined; private port: number | undefined;
private authToken: string | undefined; private authToken: string | undefined;
private transports: { [sessionId: string]: StreamableHTTPServerTransport } = private transports: { [sessionId: string]: StreamableHTTPServerTransport } =
@@ -174,19 +183,24 @@ export class IDEServer {
app.use((req, res, next) => { app.use((req, res, next) => {
const authHeader = req.headers.authorization; const authHeader = req.headers.authorization;
if (authHeader) { if (!authHeader) {
const parts = authHeader.split(' '); this.log('Missing Authorization header. Rejecting request.');
if (parts.length !== 2 || parts[0] !== 'Bearer') { res.status(401).send('Unauthorized');
this.log('Malformed Authorization header. Rejecting request.'); return;
res.status(401).send('Unauthorized'); }
return;
} const parts = authHeader.split(' ');
const token = parts[1]; if (parts.length !== 2 || parts[0] !== 'Bearer') {
if (token !== this.authToken) { this.log('Malformed Authorization header. Rejecting request.');
this.log('Invalid auth token provided. Rejecting request.'); res.status(401).send('Unauthorized');
res.status(401).send('Unauthorized'); return;
return; }
}
const token = parts[1];
if (token !== this.authToken) {
this.log('Invalid auth token provided. Rejecting request.');
res.status(401).send('Unauthorized');
return;
} }
next(); next();
}); });
@@ -327,22 +341,21 @@ export class IDEServer {
const address = (this.server as HTTPServer).address(); const address = (this.server as HTTPServer).address();
if (address && typeof address !== 'string') { if (address && typeof address !== 'string') {
this.port = address.port; this.port = address.port;
this.portFile = path.join( try {
os.tmpdir(), const ideDir = await getGlobalIdeDir();
`qwen-code-ide-server-${this.port}.json`, // Name the lock file by port to support multiple server instances.
); this.lockFile = path.join(ideDir, `${this.port}.lock`);
this.ppidPortFile = path.join( } catch (err) {
os.tmpdir(), const message = err instanceof Error ? err.message : String(err);
`qwen-code-ide-server-${process.ppid}.json`, this.log(`Failed to determine IDE lock directory: ${message}`);
); }
this.log(`IDE server listening on http://127.0.0.1:${this.port}`); this.log(`IDE server listening on http://127.0.0.1:${this.port}`);
if (this.authToken) { if (this.authToken && this.lockFile) {
await writePortAndWorkspace({ await writePortAndWorkspace({
context, context,
port: this.port, port: this.port,
portFile: this.portFile, lockFile: this.lockFile,
ppidPortFile: this.ppidPortFile,
authToken: this.authToken, authToken: this.authToken,
log: this.log, log: this.log,
}); });
@@ -371,15 +384,13 @@ export class IDEServer {
this.context && this.context &&
this.server && this.server &&
this.port && this.port &&
this.portFile && this.lockFile &&
this.ppidPortFile &&
this.authToken this.authToken
) { ) {
await writePortAndWorkspace({ await writePortAndWorkspace({
context: this.context, context: this.context,
port: this.port, port: this.port,
portFile: this.portFile, lockFile: this.lockFile,
ppidPortFile: this.ppidPortFile,
authToken: this.authToken, authToken: this.authToken,
log: this.log, log: this.log,
}); });
@@ -405,16 +416,9 @@ export class IDEServer {
if (this.context) { if (this.context) {
this.context.environmentVariableCollection.clear(); this.context.environmentVariableCollection.clear();
} }
if (this.portFile) { if (this.lockFile) {
try { try {
await fs.unlink(this.portFile); await fs.unlink(this.lockFile);
} catch (_err) {
// Ignore errors if the file doesn't exist.
}
}
if (this.ppidPortFile) {
try {
await fs.unlink(this.ppidPortFile);
} catch (_err) { } catch (_err) {
// Ignore errors if the file doesn't exist. // Ignore errors if the file doesn't exist.
} }

View File

@@ -54,27 +54,31 @@ export class AcpSessionManager {
}; };
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// different timeout durations based on methods // No timeout for session_prompt as LLM tasks can take 5-10 minutes or longer
let timeoutDuration = 60000; // default 60 seconds // The request should always terminate with a stop_reason
if ( let timeoutId: NodeJS.Timeout | undefined;
method === AGENT_METHODS.session_prompt || let timeoutDuration: number | undefined;
method === AGENT_METHODS.initialize
) {
timeoutDuration = 120000; // 2min for session_prompt and initialize
}
const timeoutId = setTimeout(() => { if (method !== AGENT_METHODS.session_prompt) {
pendingRequests.delete(id); // Set timeout for other methods
reject(new Error(`Request ${method} timed out`)); timeoutDuration = method === AGENT_METHODS.initialize ? 120000 : 60000;
}, timeoutDuration); timeoutId = setTimeout(() => {
pendingRequests.delete(id);
reject(new Error(`Request ${method} timed out`));
}, timeoutDuration);
}
const pendingRequest: PendingRequest<T> = { const pendingRequest: PendingRequest<T> = {
resolve: (value: T) => { resolve: (value: T) => {
clearTimeout(timeoutId); if (timeoutId) {
clearTimeout(timeoutId);
}
resolve(value); resolve(value);
}, },
reject: (error: Error) => { reject: (error: Error) => {
clearTimeout(timeoutId); if (timeoutId) {
clearTimeout(timeoutId);
}
reject(error); reject(error);
}, },
timeoutId, timeoutId,

View File

@@ -144,10 +144,7 @@ export const InputForm: React.FC<InputFormProps> = ({
: ''; : '';
return ( return (
<div <div className="p-1 px-4 pb-4 absolute bottom-0 left-0 right-0 bg-gradient-to-b from-transparent to-[var(--app-primary-background)]">
className="p-1 px-4 pb-4 absolute bottom-0 left-0 right-0"
style={{ backgroundColor: 'var(--app-primary-background)' }}
>
<div className="block"> <div className="block">
<form className="composer-form" onSubmit={onSubmit}> <form className="composer-form" onSubmit={onSubmit}>
{/* Inner background layer */} {/* Inner background layer */}

View File

@@ -48,5 +48,5 @@
} }
.assistant-message-container.assistant-message-loading::after { .assistant-message-container.assistant-message-loading::after {
display: none display: none;
} }

View File

@@ -172,7 +172,8 @@
/* Loading animation for toolcall header */ /* Loading animation for toolcall header */
@keyframes toolcallHeaderPulse { @keyframes toolcallHeaderPulse {
0%, 100% { 0%,
100% {
opacity: 1; opacity: 1;
} }
50% { 50% {

View File

@@ -152,6 +152,24 @@ export class SessionMessageHandler extends BaseMessageHandler {
this.currentStreamContent = ''; this.currentStreamContent = '';
} }
/**
* Notify the webview that streaming has finished.
*/
private sendStreamEnd(reason?: string): void {
const data: { timestamp: number; reason?: string } = {
timestamp: Date.now(),
};
if (reason) {
data.reason = reason;
}
this.sendToWebView({
type: 'streamEnd',
data,
});
}
/** /**
* Prompt user to login and invoke the registered login handler/command. * Prompt user to login and invoke the registered login handler/command.
* Returns true if a login was initiated. * Returns true if a login was initiated.
@@ -373,10 +391,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
); );
} }
this.sendToWebView({ this.sendStreamEnd();
type: 'streamEnd',
data: { timestamp: Date.now() },
});
} catch (error) { } catch (error) {
console.error('[SessionMessageHandler] Error sending message:', error); console.error('[SessionMessageHandler] Error sending message:', error);
@@ -398,10 +413,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
if (isAbortLike) { if (isAbortLike) {
// Do not show VS Code error popup for intentional cancellations. // Do not show VS Code error popup for intentional cancellations.
// Ensure the webview knows the stream ended due to user action. // Ensure the webview knows the stream ended due to user action.
this.sendToWebView({ this.sendStreamEnd('user_cancelled');
type: 'streamEnd',
data: { timestamp: Date.now(), reason: 'user_cancelled' },
});
return; return;
} }
// Check for session not found error and handle it appropriately // Check for session not found error and handle it appropriately
@@ -423,12 +435,39 @@ export class SessionMessageHandler extends BaseMessageHandler {
type: 'sessionExpired', type: 'sessionExpired',
data: { message: 'Session expired. Please login again.' }, data: { message: 'Session expired. Please login again.' },
}); });
this.sendStreamEnd('session_expired');
} else { } else {
vscode.window.showErrorMessage(`Error sending message: ${error}`); const isTimeoutError =
this.sendToWebView({ lower.includes('timeout') || lower.includes('timed out');
type: 'error', if (isTimeoutError) {
data: { message: errorMsg }, // Note: session_prompt no longer has a timeout, so this should rarely occur
}); // This path may still be hit for other methods (initialize, etc.) or network-level timeouts
console.warn(
'[SessionMessageHandler] Request timed out; suppressing popup',
);
const timeoutMessage: ChatMessage = {
role: 'assistant',
content:
'Request timed out. This may be due to a network issue. Please try again.',
timestamp: Date.now(),
};
// Send a timeout message to the WebView
this.sendToWebView({
type: 'message',
data: timeoutMessage,
});
this.sendStreamEnd('timeout');
} else {
// Handling of Non-Timeout Errors
vscode.window.showErrorMessage(`Error sending message: ${error}`);
this.sendToWebView({
type: 'error',
data: { message: errorMsg },
});
this.sendStreamEnd('error');
}
} }
} }
} }

View File

@@ -15,6 +15,14 @@ import type { ToolCallUpdate } from '../../types/chatTypes.js';
import type { ApprovalModeValue } from '../../types/approvalModeValueTypes.js'; import type { ApprovalModeValue } from '../../types/approvalModeValueTypes.js';
import type { PlanEntry } from '../../types/chatTypes.js'; import type { PlanEntry } from '../../types/chatTypes.js';
const FORCE_CLEAR_STREAM_END_REASONS = new Set([
'user_cancelled',
'cancelled',
'timeout',
'error',
'session_expired',
]);
interface UseWebViewMessagesProps { interface UseWebViewMessagesProps {
// Session management // Session management
sessionManagement: { sessionManagement: {
@@ -364,12 +372,12 @@ export const useWebViewMessages = ({
).toLowerCase(); ).toLowerCase();
/** /**
* Handle different types of stream end reasons: * Handle different types of stream end reasons that require a full reset:
* - 'user_cancelled': User explicitly cancelled operation * - 'user_cancelled' / 'cancelled': user explicitly cancelled
* - 'cancelled': General cancellation * - 'timeout' / 'error' / 'session_expired': request failed unexpectedly
* For these cases, immediately clear all active states * For these cases, immediately clear all active states.
*/ */
if (reason === 'user_cancelled' || reason === 'cancelled') { if (FORCE_CLEAR_STREAM_END_REASONS.has(reason)) {
// Clear active execution tool call tracking, reset state // Clear active execution tool call tracking, reset state
activeExecToolCallsRef.current.clear(); activeExecToolCallsRef.current.clear();
// Clear waiting response state to ensure UI returns to normal // Clear waiting response state to ensure UI returns to normal
@@ -393,6 +401,9 @@ export const useWebViewMessages = ({
} }
case 'error': case 'error':
handlers.messageHandling.endStreaming();
handlers.messageHandling.clearThinking();
activeExecToolCallsRef.current.clear();
handlers.messageHandling.clearWaitingForResponse(); handlers.messageHandling.clearWaitingForResponse();
break; break;

View File

@@ -43,7 +43,7 @@
/* Composer: form wrapper */ /* Composer: form wrapper */
.composer-form { .composer-form {
@apply relative flex flex-col max-w-[680px] mx-auto rounded-large border shadow-sm transition-colors duration-200; @apply relative flex flex-col max-w-[680px] mx-auto rounded-large border shadow-sm transition-colors duration-200 z-[1];
background: var(--app-input-secondary-background); background: var(--app-input-secondary-background);
border-color: var(--app-input-border); border-color: var(--app-input-border);
color: var(--app-input-foreground); color: var(--app-input-foreground);
@@ -51,7 +51,8 @@
.composer-form:focus-within { .composer-form:focus-within {
/* match existing highlight behavior */ /* match existing highlight behavior */
border-color: var(--app-input-highlight); border-color: var(--app-input-highlight);
box-shadow: 0 1px 2px color-mix(in srgb, var(--app-input-highlight), transparent 80%); box-shadow: 0 1px 2px
color-mix(in srgb, var(--app-input-highlight), transparent 80%);
} }
/* Composer: input editable area */ /* Composer: input editable area */
@@ -66,7 +67,7 @@
The data attribute is needed because some browsers insert a <br> in The data attribute is needed because some browsers insert a <br> in
contentEditable, which breaks :empty matching. */ contentEditable, which breaks :empty matching. */
.composer-input:empty:before, .composer-input:empty:before,
.composer-input[data-empty="true"]::before { .composer-input[data-empty='true']::before {
content: attr(data-placeholder); content: attr(data-placeholder);
color: var(--app-input-placeholder-foreground); color: var(--app-input-placeholder-foreground);
pointer-events: none; pointer-events: none;
@@ -80,7 +81,7 @@
outline: none; outline: none;
} }
.composer-input:disabled, .composer-input:disabled,
.composer-input[contenteditable="false"] { .composer-input[contenteditable='false'] {
color: #999; color: #999;
cursor: not-allowed; cursor: not-allowed;
} }