mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-28 11:41:04 +00:00
fix(mcp): update OAuth client names and improve MCP commands
- Rename MCP OAuth client names from 'Gemini CLI' to 'Qwen Code' - Update MCP add/remove/list commands with improved error handling - Add comprehensive tests for OAuth provider - Fix token storage test assertions - Clean up unused i18n translation keys - Update gemini-converter and window title references Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
parent
36931e1eab
commit
21e711469d
27 changed files with 244 additions and 124 deletions
|
|
@ -14,7 +14,7 @@
|
||||||
* schema object which has stricter typing and recursion restrictions.
|
* schema object which has stricter typing and recursion restrictions.
|
||||||
* If this test fails, it's likely because either the GenAI SDK or Gemini API
|
* If this test fails, it's likely because either the GenAI SDK or Gemini API
|
||||||
* has become more restrictive about the type of tool parameter schemas that
|
* has become more restrictive about the type of tool parameter schemas that
|
||||||
* are accepted. If this occurs: Gemini CLI previously attempted to detect
|
* are accepted. If this occurs: Qwen Code previously attempted to detect
|
||||||
* such tools and proactively remove them from the set of tools provided in
|
* such tools and proactively remove them from the set of tools provided in
|
||||||
* the Gemini API call (as FunctionDeclaration objects). It may be appropriate
|
* the Gemini API call (as FunctionDeclaration objects). It may be appropriate
|
||||||
* to resurrect that behavior but note that it's difficult to keep the
|
* to resurrect that behavior but note that it's difficult to keep the
|
||||||
|
|
|
||||||
|
|
@ -65,22 +65,18 @@ describe('mcp add command', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add a stdio server to project settings', async () => {
|
it('should add a stdio server to user settings by default', async () => {
|
||||||
await parser.parseAsync(
|
await parser.parseAsync(
|
||||||
'add my-server /path/to/server arg1 arg2 -e FOO=bar',
|
'add my-server /path/to/server arg1 arg2 -e FOO=bar',
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(mockSetValue).toHaveBeenCalledWith(
|
expect(mockSetValue).toHaveBeenCalledWith(SettingScope.User, 'mcpServers', {
|
||||||
SettingScope.Workspace,
|
'my-server': {
|
||||||
'mcpServers',
|
command: '/path/to/server',
|
||||||
{
|
args: ['arg1', 'arg2'],
|
||||||
'my-server': {
|
env: { FOO: 'bar' },
|
||||||
command: '/path/to/server',
|
|
||||||
args: ['arg1', 'arg2'],
|
|
||||||
env: { FOO: 'bar' },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add an sse server to user settings', async () => {
|
it('should add an sse server to user settings', async () => {
|
||||||
|
|
@ -96,21 +92,17 @@ describe('mcp add command', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add an http server to project settings', async () => {
|
it('should add an http server to user settings by default', async () => {
|
||||||
await parser.parseAsync(
|
await parser.parseAsync(
|
||||||
'add --transport http http-server https://example.com/mcp -H "Authorization: Bearer your-token"',
|
'add --transport http http-server https://example.com/mcp -H "Authorization: Bearer your-token"',
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(mockSetValue).toHaveBeenCalledWith(
|
expect(mockSetValue).toHaveBeenCalledWith(SettingScope.User, 'mcpServers', {
|
||||||
SettingScope.Workspace,
|
'http-server': {
|
||||||
'mcpServers',
|
httpUrl: 'https://example.com/mcp',
|
||||||
{
|
headers: { Authorization: 'Bearer your-token' },
|
||||||
'http-server': {
|
|
||||||
httpUrl: 'https://example.com/mcp',
|
|
||||||
headers: { Authorization: 'Bearer your-token' },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle MCP server args with -- separator', async () => {
|
it('should handle MCP server args with -- separator', async () => {
|
||||||
|
|
@ -118,16 +110,12 @@ describe('mcp add command', () => {
|
||||||
'add my-server npx -- -y http://example.com/some-package',
|
'add my-server npx -- -y http://example.com/some-package',
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(mockSetValue).toHaveBeenCalledWith(
|
expect(mockSetValue).toHaveBeenCalledWith(SettingScope.User, 'mcpServers', {
|
||||||
SettingScope.Workspace,
|
'my-server': {
|
||||||
'mcpServers',
|
command: 'npx',
|
||||||
{
|
args: ['-y', 'http://example.com/some-package'],
|
||||||
'my-server': {
|
|
||||||
command: 'npx',
|
|
||||||
args: ['-y', 'http://example.com/some-package'],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle unknown options as MCP server args', async () => {
|
it('should handle unknown options as MCP server args', async () => {
|
||||||
|
|
@ -135,16 +123,12 @@ describe('mcp add command', () => {
|
||||||
'add test-server npx -y http://example.com/some-package',
|
'add test-server npx -y http://example.com/some-package',
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(mockSetValue).toHaveBeenCalledWith(
|
expect(mockSetValue).toHaveBeenCalledWith(SettingScope.User, 'mcpServers', {
|
||||||
SettingScope.Workspace,
|
'test-server': {
|
||||||
'mcpServers',
|
command: 'npx',
|
||||||
{
|
args: ['-y', 'http://example.com/some-package'],
|
||||||
'test-server': {
|
|
||||||
command: 'npx',
|
|
||||||
args: ['-y', 'http://example.com/some-package'],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when handling scope and directory', () => {
|
describe('when handling scope and directory', () => {
|
||||||
|
|
@ -166,10 +150,10 @@ describe('mcp add command', () => {
|
||||||
setupMocks('/path/to/project', '/path/to/project');
|
setupMocks('/path/to/project', '/path/to/project');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use project scope by default', async () => {
|
it('should use user scope by default', async () => {
|
||||||
await parser.parseAsync(`add ${serverName} ${command}`);
|
await parser.parseAsync(`add ${serverName} ${command}`);
|
||||||
expect(mockSetValue).toHaveBeenCalledWith(
|
expect(mockSetValue).toHaveBeenCalledWith(
|
||||||
SettingScope.Workspace,
|
SettingScope.User,
|
||||||
'mcpServers',
|
'mcpServers',
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
|
|
@ -199,10 +183,10 @@ describe('mcp add command', () => {
|
||||||
setupMocks('/path/to/project/subdir', '/path/to/project');
|
setupMocks('/path/to/project/subdir', '/path/to/project');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use project scope by default', async () => {
|
it('should use user scope by default', async () => {
|
||||||
await parser.parseAsync(`add ${serverName} ${command}`);
|
await parser.parseAsync(`add ${serverName} ${command}`);
|
||||||
expect(mockSetValue).toHaveBeenCalledWith(
|
expect(mockSetValue).toHaveBeenCalledWith(
|
||||||
SettingScope.Workspace,
|
SettingScope.User,
|
||||||
'mcpServers',
|
'mcpServers',
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
|
|
@ -214,22 +198,14 @@ describe('mcp add command', () => {
|
||||||
setupMocks('/home/user', '/home/user');
|
setupMocks('/home/user', '/home/user');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show an error by default', async () => {
|
it('should use user scope by default without error', async () => {
|
||||||
const mockProcessExit = vi
|
await parser.parseAsync(`add ${serverName} ${command}`);
|
||||||
.spyOn(process, 'exit')
|
expect(mockSetValue).toHaveBeenCalledWith(
|
||||||
.mockImplementation((() => {
|
SettingScope.User,
|
||||||
throw new Error('process.exit called');
|
'mcpServers',
|
||||||
}) as (code?: number) => never);
|
expect.any(Object),
|
||||||
|
|
||||||
await expect(
|
|
||||||
parser.parseAsync(`add ${serverName} ${command}`),
|
|
||||||
).rejects.toThrow('process.exit called');
|
|
||||||
|
|
||||||
expect(mockWriteStderrLine).toHaveBeenCalledWith(
|
|
||||||
'Error: Please use --scope user to edit settings in the home directory.',
|
|
||||||
);
|
);
|
||||||
expect(mockProcessExit).toHaveBeenCalledWith(1);
|
expect(mockWriteStderrLine).not.toHaveBeenCalled();
|
||||||
expect(mockSetValue).not.toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show an error when --scope=project is used explicitly', async () => {
|
it('should show an error when --scope=project is used explicitly', async () => {
|
||||||
|
|
@ -266,16 +242,16 @@ describe('mcp add command', () => {
|
||||||
setupMocks('/home/user/some/dir', '/home/user/some/dir');
|
setupMocks('/home/user/some/dir', '/home/user/some/dir');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use project scope by default', async () => {
|
it('should use user scope by default', async () => {
|
||||||
await parser.parseAsync(`add ${serverName} ${command}`);
|
await parser.parseAsync(`add ${serverName} ${command}`);
|
||||||
expect(mockSetValue).toHaveBeenCalledWith(
|
expect(mockSetValue).toHaveBeenCalledWith(
|
||||||
SettingScope.Workspace,
|
SettingScope.User,
|
||||||
'mcpServers',
|
'mcpServers',
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should write to the WORKSPACE scope, not the USER scope', async () => {
|
it('should write to the USER scope by default', async () => {
|
||||||
await parser.parseAsync(`add my-new-server echo`);
|
await parser.parseAsync(`add my-new-server echo`);
|
||||||
|
|
||||||
// We expect setValue to be called once.
|
// We expect setValue to be called once.
|
||||||
|
|
@ -284,8 +260,8 @@ describe('mcp add command', () => {
|
||||||
// We get the scope that setValue was called with.
|
// We get the scope that setValue was called with.
|
||||||
const calledScope = mockSetValue.mock.calls[0][0];
|
const calledScope = mockSetValue.mock.calls[0][0];
|
||||||
|
|
||||||
// We assert that the scope was Workspace, not User.
|
// We assert that the scope was User by default.
|
||||||
expect(calledScope).toBe(SettingScope.Workspace);
|
expect(calledScope).toBe(SettingScope.User);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -294,10 +270,10 @@ describe('mcp add command', () => {
|
||||||
setupMocks('/tmp/foo', '/tmp/foo');
|
setupMocks('/tmp/foo', '/tmp/foo');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use project scope by default', async () => {
|
it('should use user scope by default', async () => {
|
||||||
await parser.parseAsync(`add ${serverName} ${command}`);
|
await parser.parseAsync(`add ${serverName} ${command}`);
|
||||||
expect(mockSetValue).toHaveBeenCalledWith(
|
expect(mockSetValue).toHaveBeenCalledWith(
|
||||||
SettingScope.Workspace,
|
SettingScope.User,
|
||||||
'mcpServers',
|
'mcpServers',
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
|
|
@ -328,12 +304,12 @@ describe('mcp add command', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update the existing server in the project scope', async () => {
|
it('should update the existing server in the user scope by default', async () => {
|
||||||
await parser.parseAsync(
|
await parser.parseAsync(
|
||||||
`add ${serverName} ${updatedCommand} ${updatedArgs.join(' ')}`,
|
`add ${serverName} ${updatedCommand} ${updatedArgs.join(' ')}`,
|
||||||
);
|
);
|
||||||
expect(mockSetValue).toHaveBeenCalledWith(
|
expect(mockSetValue).toHaveBeenCalledWith(
|
||||||
SettingScope.Workspace,
|
SettingScope.User,
|
||||||
'mcpServers',
|
'mcpServers',
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
[serverName]: expect.objectContaining({
|
[serverName]: expect.objectContaining({
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// File for 'gemini mcp add' command
|
// File for 'qwen mcp add' command
|
||||||
import type { CommandModule } from 'yargs';
|
import type { CommandModule } from 'yargs';
|
||||||
import { loadSettings, SettingScope } from '../../config/settings.js';
|
import { loadSettings, SettingScope } from '../../config/settings.js';
|
||||||
import { writeStdoutLine, writeStderrLine } from '../../utils/stdioHelpers.js';
|
import { writeStdoutLine, writeStderrLine } from '../../utils/stdioHelpers.js';
|
||||||
|
|
@ -159,7 +159,7 @@ export const addCommand: CommandModule = {
|
||||||
alias: 's',
|
alias: 's',
|
||||||
describe: 'Configuration scope (user or project)',
|
describe: 'Configuration scope (user or project)',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
default: 'project',
|
default: 'user',
|
||||||
choices: ['user', 'project'],
|
choices: ['user', 'project'],
|
||||||
})
|
})
|
||||||
.option('transport', {
|
.option('transport', {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// File for 'gemini mcp list' command
|
// File for 'qwen mcp list' command
|
||||||
import type { CommandModule } from 'yargs';
|
import type { CommandModule } from 'yargs';
|
||||||
import { loadSettings } from '../../config/settings.js';
|
import { loadSettings } from '../../config/settings.js';
|
||||||
import { writeStdoutLine } from '../../utils/stdioHelpers.js';
|
import { writeStdoutLine } from '../../utils/stdioHelpers.js';
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import { removeCommand } from './remove.js';
|
||||||
|
|
||||||
const mockWriteStdoutLine = vi.hoisted(() => vi.fn());
|
const mockWriteStdoutLine = vi.hoisted(() => vi.fn());
|
||||||
const mockWriteStderrLine = vi.hoisted(() => vi.fn());
|
const mockWriteStderrLine = vi.hoisted(() => vi.fn());
|
||||||
|
const mockDeleteCredentials = vi.hoisted(() => vi.fn());
|
||||||
|
|
||||||
vi.mock('../../utils/stdioHelpers.js', () => ({
|
vi.mock('../../utils/stdioHelpers.js', () => ({
|
||||||
writeStdoutLine: mockWriteStdoutLine,
|
writeStdoutLine: mockWriteStdoutLine,
|
||||||
|
|
@ -35,6 +36,17 @@ vi.mock('../../config/settings.js', async () => {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => {
|
||||||
|
const actual =
|
||||||
|
await importOriginal<typeof import('@qwen-code/qwen-code-core')>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
MCPOAuthTokenStorage: vi.fn(() => ({
|
||||||
|
deleteCredentials: mockDeleteCredentials,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const mockedLoadSettings = loadSettings as vi.Mock;
|
const mockedLoadSettings = loadSettings as vi.Mock;
|
||||||
|
|
||||||
describe('mcp remove command', () => {
|
describe('mcp remove command', () => {
|
||||||
|
|
@ -59,24 +71,45 @@ describe('mcp remove command', () => {
|
||||||
setValue: mockSetValue,
|
setValue: mockSetValue,
|
||||||
});
|
});
|
||||||
mockWriteStdoutLine.mockClear();
|
mockWriteStdoutLine.mockClear();
|
||||||
|
mockDeleteCredentials.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should remove a server from project settings', async () => {
|
it('should remove a server from user settings by default', async () => {
|
||||||
await parser.parseAsync('remove test-server');
|
await parser.parseAsync('remove test-server');
|
||||||
|
|
||||||
expect(mockSetValue).toHaveBeenCalledWith(
|
expect(mockSetValue).toHaveBeenCalledWith(
|
||||||
SettingScope.Workspace,
|
SettingScope.User,
|
||||||
'mcpServers',
|
'mcpServers',
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show a message if server not found', async () => {
|
it('should clean up OAuth tokens when removing a server', async () => {
|
||||||
|
await parser.parseAsync('remove test-server');
|
||||||
|
|
||||||
|
expect(mockDeleteCredentials).toHaveBeenCalledWith('test-server');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not fail if OAuth token cleanup fails', async () => {
|
||||||
|
mockDeleteCredentials.mockRejectedValue(new Error('cleanup failed'));
|
||||||
|
|
||||||
|
await parser.parseAsync('remove test-server');
|
||||||
|
|
||||||
|
// Server should still be removed from settings despite token cleanup failure
|
||||||
|
expect(mockSetValue).toHaveBeenCalledWith(
|
||||||
|
SettingScope.User,
|
||||||
|
'mcpServers',
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not clean up OAuth tokens if server not found', async () => {
|
||||||
await parser.parseAsync('remove non-existent-server');
|
await parser.parseAsync('remove non-existent-server');
|
||||||
|
|
||||||
expect(mockSetValue).not.toHaveBeenCalled();
|
expect(mockSetValue).not.toHaveBeenCalled();
|
||||||
|
expect(mockDeleteCredentials).not.toHaveBeenCalled();
|
||||||
expect(mockWriteStdoutLine).toHaveBeenCalledWith(
|
expect(mockWriteStdoutLine).toHaveBeenCalledWith(
|
||||||
'Server "non-existent-server" not found in project settings.',
|
'Server "non-existent-server" not found in user settings.',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,11 @@
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// File for 'gemini mcp remove' command
|
// File for 'qwen mcp remove' command
|
||||||
import type { CommandModule } from 'yargs';
|
import type { CommandModule } from 'yargs';
|
||||||
import { loadSettings, SettingScope } from '../../config/settings.js';
|
import { loadSettings, SettingScope } from '../../config/settings.js';
|
||||||
import { writeStdoutLine } from '../../utils/stdioHelpers.js';
|
import { writeStdoutLine } from '../../utils/stdioHelpers.js';
|
||||||
|
import { MCPOAuthTokenStorage } from '@qwen-code/qwen-code-core';
|
||||||
|
|
||||||
async function removeMcpServer(
|
async function removeMcpServer(
|
||||||
name: string,
|
name: string,
|
||||||
|
|
@ -32,6 +33,14 @@ async function removeMcpServer(
|
||||||
|
|
||||||
settings.setValue(settingsScope, 'mcpServers', mcpServers);
|
settings.setValue(settingsScope, 'mcpServers', mcpServers);
|
||||||
|
|
||||||
|
// Clean up any stored OAuth tokens for this server
|
||||||
|
try {
|
||||||
|
const tokenStorage = new MCPOAuthTokenStorage();
|
||||||
|
await tokenStorage.deleteCredentials(name);
|
||||||
|
} catch {
|
||||||
|
// Token cleanup is best-effort; don't fail the remove operation
|
||||||
|
}
|
||||||
|
|
||||||
writeStdoutLine(`Server "${name}" removed from ${scope} settings.`);
|
writeStdoutLine(`Server "${name}" removed from ${scope} settings.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -40,7 +49,7 @@ export const removeCommand: CommandModule = {
|
||||||
describe: 'Remove a server',
|
describe: 'Remove a server',
|
||||||
builder: (yargs) =>
|
builder: (yargs) =>
|
||||||
yargs
|
yargs
|
||||||
.usage('Usage: gemini mcp remove [options] <name>')
|
.usage('Usage: qwen mcp remove [options] <name>')
|
||||||
.positional('name', {
|
.positional('name', {
|
||||||
describe: 'Name of the server',
|
describe: 'Name of the server',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
|
@ -50,7 +59,7 @@ export const removeCommand: CommandModule = {
|
||||||
alias: 's',
|
alias: 's',
|
||||||
describe: 'Configuration scope (user or project)',
|
describe: 'Configuration scope (user or project)',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
default: 'project',
|
default: 'user',
|
||||||
choices: ['user', 'project'],
|
choices: ['user', 'project'],
|
||||||
}),
|
}),
|
||||||
handler: async (argv) => {
|
handler: async (argv) => {
|
||||||
|
|
|
||||||
|
|
@ -336,7 +336,7 @@ export async function main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// We are now past the logic handling potentially launching a child process
|
// We are now past the logic handling potentially launching a child process
|
||||||
// to run Gemini CLI. It is now safe to perform expensive initialization that
|
// to run Qwen Code. It is now safe to perform expensive initialization that
|
||||||
// may have side effects.
|
// may have side effects.
|
||||||
|
|
||||||
// Initialize output language file before config loads to ensure it's included in context
|
// Initialize output language file before config loads to ensure it's included in context
|
||||||
|
|
|
||||||
|
|
@ -1055,9 +1055,6 @@ export default {
|
||||||
// MCP Status
|
// MCP Status
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
'No MCP servers configured.': 'Keine MCP-Server konfiguriert.',
|
'No MCP servers configured.': 'Keine MCP-Server konfiguriert.',
|
||||||
'Please view MCP documentation in your browser:':
|
|
||||||
'Bitte sehen Sie die MCP-Dokumentation in Ihrem Browser:',
|
|
||||||
'or use the cli /docs command': 'oder verwenden Sie den CLI-Befehl /docs',
|
|
||||||
'⏳ MCP servers are starting up ({{count}} initializing)...':
|
'⏳ MCP servers are starting up ({{count}} initializing)...':
|
||||||
'⏳ MCP-Server werden gestartet ({{count}} werden initialisiert)...',
|
'⏳ MCP-Server werden gestartet ({{count}} werden initialisiert)...',
|
||||||
'Note: First startup may take longer. Tool availability will update automatically.':
|
'Note: First startup may take longer. Tool availability will update automatically.':
|
||||||
|
|
|
||||||
|
|
@ -1042,9 +1042,6 @@ export default {
|
||||||
// MCP Status
|
// MCP Status
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
'No MCP servers configured.': 'No MCP servers configured.',
|
'No MCP servers configured.': 'No MCP servers configured.',
|
||||||
'Please view MCP documentation in your browser:':
|
|
||||||
'Please view MCP documentation in your browser:',
|
|
||||||
'or use the cli /docs command': 'or use the cli /docs command',
|
|
||||||
'⏳ MCP servers are starting up ({{count}} initializing)...':
|
'⏳ MCP servers are starting up ({{count}} initializing)...':
|
||||||
'⏳ MCP servers are starting up ({{count}} initializing)...',
|
'⏳ MCP servers are starting up ({{count}} initializing)...',
|
||||||
'Note: First startup may take longer. Tool availability will update automatically.':
|
'Note: First startup may take longer. Tool availability will update automatically.':
|
||||||
|
|
|
||||||
|
|
@ -746,9 +746,6 @@ export default {
|
||||||
'Press Ctrl+D again to exit.': 'Ctrl+D をもう一度押すと終了します',
|
'Press Ctrl+D again to exit.': 'Ctrl+D をもう一度押すと終了します',
|
||||||
'Press Esc again to clear.': 'Esc をもう一度押すとクリアします',
|
'Press Esc again to clear.': 'Esc をもう一度押すとクリアします',
|
||||||
// MCP Status
|
// MCP Status
|
||||||
'Please view MCP documentation in your browser:':
|
|
||||||
'ブラウザでMCPドキュメントを確認してください:',
|
|
||||||
'or use the cli /docs command': 'または CLI の /docs コマンドを使用',
|
|
||||||
'⏳ MCP servers are starting up ({{count}} initializing)...':
|
'⏳ MCP servers are starting up ({{count}} initializing)...':
|
||||||
'⏳ MCPサーバーを起動中({{count}} 初期化中)...',
|
'⏳ MCPサーバーを起動中({{count}} 初期化中)...',
|
||||||
'Note: First startup may take longer. Tool availability will update automatically.':
|
'Note: First startup may take longer. Tool availability will update automatically.':
|
||||||
|
|
|
||||||
|
|
@ -1065,9 +1065,6 @@ export default {
|
||||||
// MCP Status
|
// MCP Status
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
'No MCP servers configured.': 'Nenhum servidor MCP configurado.',
|
'No MCP servers configured.': 'Nenhum servidor MCP configurado.',
|
||||||
'Please view MCP documentation in your browser:':
|
|
||||||
'Veja a documentação do MCP no seu navegador:',
|
|
||||||
'or use the cli /docs command': 'ou use o comando cli /docs',
|
|
||||||
'⏳ MCP servers are starting up ({{count}} initializing)...':
|
'⏳ MCP servers are starting up ({{count}} initializing)...':
|
||||||
'⏳ Servidores MCP estão iniciando ({{count}} inicializando)...',
|
'⏳ Servidores MCP estão iniciando ({{count}} inicializando)...',
|
||||||
'Note: First startup may take longer. Tool availability will update automatically.':
|
'Note: First startup may take longer. Tool availability will update automatically.':
|
||||||
|
|
|
||||||
|
|
@ -1057,9 +1057,6 @@ export default {
|
||||||
// Статус MCP
|
// Статус MCP
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
'No MCP servers configured.': 'Не настроено MCP-серверов.',
|
'No MCP servers configured.': 'Не настроено MCP-серверов.',
|
||||||
'Please view MCP documentation in your browser:':
|
|
||||||
'Пожалуйста, просмотрите документацию MCP в браузере:',
|
|
||||||
'or use the cli /docs command': 'или используйте команду cli /docs',
|
|
||||||
'⏳ MCP servers are starting up ({{count}} initializing)...':
|
'⏳ MCP servers are starting up ({{count}} initializing)...':
|
||||||
'⏳ MCP-серверы запускаются ({{count}} инициализируется)...',
|
'⏳ MCP-серверы запускаются ({{count}} инициализируется)...',
|
||||||
'Note: First startup may take longer. Tool availability will update automatically.':
|
'Note: First startup may take longer. Tool availability will update automatically.':
|
||||||
|
|
|
||||||
|
|
@ -985,9 +985,6 @@ export default {
|
||||||
// MCP Status
|
// MCP Status
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
'No MCP servers configured.': '未配置 MCP 服务器',
|
'No MCP servers configured.': '未配置 MCP 服务器',
|
||||||
'Please view MCP documentation in your browser:':
|
|
||||||
'请在浏览器中查看 MCP 文档:',
|
|
||||||
'or use the cli /docs command': '或使用 cli /docs 命令',
|
|
||||||
'⏳ MCP servers are starting up ({{count}} initializing)...':
|
'⏳ MCP servers are starting up ({{count}} initializing)...':
|
||||||
'⏳ MCP 服务器正在启动({{count}} 个正在初始化)...',
|
'⏳ MCP 服务器正在启动({{count}} 个正在初始化)...',
|
||||||
'Note: First startup may take longer. Tool availability will update automatically.':
|
'Note: First startup may take longer. Tool availability will update automatically.':
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ import { setupGithubCommand } from '../ui/commands/setupGithubCommand.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads the core, hard-coded slash commands that are an integral part
|
* Loads the core, hard-coded slash commands that are an integral part
|
||||||
* of the Gemini CLI application.
|
* of the Qwen Code application.
|
||||||
*/
|
*/
|
||||||
export class BuiltinCommandLoader implements ICommandLoader {
|
export class BuiltinCommandLoader implements ICommandLoader {
|
||||||
constructor(private config: Config | null) {}
|
constructor(private config: Config | null) {}
|
||||||
|
|
|
||||||
|
|
@ -1229,7 +1229,7 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||||
|
|
||||||
useKeypress(handleGlobalKeypress, { isActive: true });
|
useKeypress(handleGlobalKeypress, { isActive: true });
|
||||||
|
|
||||||
// Update terminal title with Gemini CLI status and thoughts
|
// Update terminal title with Qwen Code status and thoughts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Respect both showStatusInTitle and hideWindowTitle settings
|
// Respect both showStatusInTitle and hideWindowTitle settings
|
||||||
if (
|
if (
|
||||||
|
|
@ -1256,7 +1256,7 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||||
lastTitleRef.current = paddedTitle;
|
lastTitleRef.current = paddedTitle;
|
||||||
stdout.write(`\x1b]2;${paddedTitle}\x07`);
|
stdout.write(`\x1b]2;${paddedTitle}\x07`);
|
||||||
}
|
}
|
||||||
// Note: We don't need to reset the window title on exit because Gemini CLI is already doing that elsewhere
|
// Note: We don't need to reset the window title on exit because Qwen Code is already doing that elsewhere
|
||||||
}, [
|
}, [
|
||||||
streamingState,
|
streamingState,
|
||||||
thought,
|
thought,
|
||||||
|
|
|
||||||
|
|
@ -49,13 +49,6 @@ export const McpStatus: React.FC<McpStatusProps> = ({
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column">
|
<Box flexDirection="column">
|
||||||
<Text>{t('No MCP servers configured.')}</Text>
|
<Text>{t('No MCP servers configured.')}</Text>
|
||||||
<Text>
|
|
||||||
{t('Please view MCP documentation in your browser:')}{' '}
|
|
||||||
<Text color={theme.text.link}>
|
|
||||||
https://goo.gle/gemini-cli-docs-mcp
|
|
||||||
</Text>{' '}
|
|
||||||
{t('or use the cli /docs command')}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Computes the window title for the Gemini CLI application.
|
* Computes the window title for the Qwen Code application.
|
||||||
*
|
*
|
||||||
* @param folderName - The name of the current folder/workspace to display in the title
|
* @param folderName - The name of the current folder/workspace to display in the title
|
||||||
* @returns The computed window title, either from CLI_TITLE environment variable or the default Gemini title
|
* @returns The computed window title, either from CLI_TITLE environment variable or the default Gemini title
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converter for Gemini CLI extensions to Qwen Code format.
|
* Converter for Gemini extensions to Qwen Code format.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as fs from 'node:fs';
|
import * as fs from 'node:fs';
|
||||||
|
|
@ -28,7 +28,7 @@ export interface GeminiExtensionConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a Gemini CLI extension config to Qwen Code format.
|
* Converts a Gemini extension config to Qwen Code format.
|
||||||
* @param extensionDir Path to the Gemini extension directory
|
* @param extensionDir Path to the Gemini extension directory
|
||||||
* @returns Qwen ExtensionConfig
|
* @returns Qwen ExtensionConfig
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,13 @@
|
||||||
* OAuth client name used for MCP dynamic client registration.
|
* OAuth client name used for MCP dynamic client registration.
|
||||||
* This name must match the allowlist on MCP servers like Figma.
|
* This name must match the allowlist on MCP servers like Figma.
|
||||||
*/
|
*/
|
||||||
export const MCP_OAUTH_CLIENT_NAME = 'Gemini CLI MCP Client';
|
export const MCP_OAUTH_CLIENT_NAME = 'Qwen Code MCP Client';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OAuth client name for service account impersonation provider.
|
* OAuth client name for service account impersonation provider.
|
||||||
*/
|
*/
|
||||||
export const MCP_SA_IMPERSONATION_CLIENT_NAME =
|
export const MCP_SA_IMPERSONATION_CLIENT_NAME =
|
||||||
'Gemini CLI (Service Account Impersonation)';
|
'Qwen Code (Service Account Impersonation)';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Port for OAuth redirect callback server.
|
* Port for OAuth redirect callback server.
|
||||||
|
|
|
||||||
|
|
@ -1082,10 +1082,88 @@ describe('MCPOAuthProvider', () => {
|
||||||
expect(capturedUrl!).toContain('code_challenge=code_challenge_mock');
|
expect(capturedUrl!).toContain('code_challenge=code_challenge_mock');
|
||||||
expect(capturedUrl!).toContain('code_challenge_method=S256');
|
expect(capturedUrl!).toContain('code_challenge_method=S256');
|
||||||
expect(capturedUrl!).toContain('scope=read+write');
|
expect(capturedUrl!).toContain('scope=read+write');
|
||||||
|
// resource should be the full canonical URI per MCP spec / RFC 8707
|
||||||
expect(capturedUrl!).toContain('resource=https%3A%2F%2Fauth.example.com');
|
expect(capturedUrl!).toContain('resource=https%3A%2F%2Fauth.example.com');
|
||||||
expect(capturedUrl!).toContain('audience=https%3A%2F%2Fapi.example.com');
|
expect(capturedUrl!).toContain('audience=https%3A%2F%2Fapi.example.com');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Regression test for https://github.com/QwenLM/qwen-code/issues/1749
|
||||||
|
// Scenario: user runs `qwen mcp add --transport http yuque https://mcp.alibaba-inc.com/yuque/mcp`
|
||||||
|
// then `/mcp auth yuque`. Per MCP spec / RFC 8707, the resource param should be the
|
||||||
|
// full canonical URI "https://mcp.alibaba-inc.com/yuque/mcp", not just the host.
|
||||||
|
it('should use full canonical URI as resource parameter (issue #1749)', async () => {
|
||||||
|
let capturedAuthUrl: string | undefined;
|
||||||
|
mockOpenBrowserSecurely.mockImplementation((url: string) => {
|
||||||
|
capturedAuthUrl = url;
|
||||||
|
return Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
let callbackHandler: unknown;
|
||||||
|
vi.mocked(http.createServer).mockImplementation((handler) => {
|
||||||
|
callbackHandler = handler;
|
||||||
|
return mockHttpServer as unknown as http.Server;
|
||||||
|
});
|
||||||
|
|
||||||
|
mockHttpServer.listen.mockImplementation((port, callback) => {
|
||||||
|
callback?.();
|
||||||
|
setTimeout(() => {
|
||||||
|
const mockReq = {
|
||||||
|
url: '/oauth/callback?code=auth_code_123&state=bW9ja19zdGF0ZV8xNl9ieXRlcw',
|
||||||
|
};
|
||||||
|
const mockRes = {
|
||||||
|
writeHead: vi.fn(),
|
||||||
|
end: vi.fn(),
|
||||||
|
};
|
||||||
|
(callbackHandler as (req: unknown, res: unknown) => void)(
|
||||||
|
mockReq,
|
||||||
|
mockRes,
|
||||||
|
);
|
||||||
|
}, 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Capture the token exchange request to verify resource param there too
|
||||||
|
let capturedTokenBody: string | undefined;
|
||||||
|
mockFetch.mockImplementation(
|
||||||
|
(url: string, options?: { body?: string }) => {
|
||||||
|
if (options?.body) {
|
||||||
|
capturedTokenBody = options.body;
|
||||||
|
}
|
||||||
|
return Promise.resolve(
|
||||||
|
createMockResponse({
|
||||||
|
ok: true,
|
||||||
|
contentType: 'application/json',
|
||||||
|
text: JSON.stringify(mockTokenResponse),
|
||||||
|
json: mockTokenResponse,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const authProvider = new MCPOAuthProvider();
|
||||||
|
|
||||||
|
// Simulating what mcpCommand.ts does:
|
||||||
|
// serverName = "yuque" (the name the user gave)
|
||||||
|
// mcpServerUrl = "https://mcp.alibaba-inc.com/yuque/mcp" (server.httpUrl || server.url)
|
||||||
|
const serverName = 'yuque';
|
||||||
|
const mcpServerUrl = 'https://mcp.alibaba-inc.com/yuque/mcp';
|
||||||
|
|
||||||
|
await authProvider.authenticate(serverName, mockConfig, mcpServerUrl);
|
||||||
|
|
||||||
|
// Verify the authorization URL contains the full canonical URI as resource
|
||||||
|
expect(capturedAuthUrl).toBeDefined();
|
||||||
|
const authUrl = new URL(capturedAuthUrl!);
|
||||||
|
const resourceInAuthUrl = authUrl.searchParams.get('resource');
|
||||||
|
expect(resourceInAuthUrl).toBe('https://mcp.alibaba-inc.com/yuque/mcp');
|
||||||
|
|
||||||
|
// Verify the token exchange request also uses the full canonical URI
|
||||||
|
expect(capturedTokenBody).toBeDefined();
|
||||||
|
const tokenParams = new URLSearchParams(capturedTokenBody!);
|
||||||
|
const resourceInTokenExchange = tokenParams.get('resource');
|
||||||
|
expect(resourceInTokenExchange).toBe(
|
||||||
|
'https://mcp.alibaba-inc.com/yuque/mcp',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should correctly append parameters to an authorization URL that already has query params', async () => {
|
it('should correctly append parameters to an authorization URL that already has query params', async () => {
|
||||||
// Mock to capture the URL that would be opened
|
// Mock to capture the URL that would be opened
|
||||||
let capturedUrl: string;
|
let capturedUrl: string;
|
||||||
|
|
|
||||||
|
|
@ -251,7 +251,7 @@ export class MCPOAuthProvider {
|
||||||
<html>
|
<html>
|
||||||
<body>
|
<body>
|
||||||
<h1>Authentication Successful!</h1>
|
<h1>Authentication Successful!</h1>
|
||||||
<p>You can close this window and return to Gemini CLI.</p>
|
<p>You can close this window and return to Qwen Code.</p>
|
||||||
<script>window.close();</script>
|
<script>window.close();</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -293,18 +293,53 @@ describe('OAuthUtils', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('buildResourceParameter', () => {
|
describe('buildResourceParameter', () => {
|
||||||
it('should build resource parameter from endpoint URL', () => {
|
it('should return canonical URI with full path', () => {
|
||||||
const result = OAuthUtils.buildResourceParameter(
|
const result = OAuthUtils.buildResourceParameter(
|
||||||
'https://example.com/oauth/token',
|
'https://example.com/oauth/token',
|
||||||
);
|
);
|
||||||
expect(result).toBe('https://example.com');
|
expect(result).toBe('https://example.com/oauth/token');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle URLs with ports', () => {
|
it('should handle URLs with ports', () => {
|
||||||
const result = OAuthUtils.buildResourceParameter(
|
const result = OAuthUtils.buildResourceParameter(
|
||||||
'https://example.com:8080/oauth/token',
|
'https://example.com:8080/oauth/token',
|
||||||
);
|
);
|
||||||
expect(result).toBe('https://example.com:8080');
|
expect(result).toBe('https://example.com:8080/oauth/token');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should strip query and fragment per RFC 8707', () => {
|
||||||
|
const result = OAuthUtils.buildResourceParameter(
|
||||||
|
'https://example.com/mcp?foo=bar#frag',
|
||||||
|
);
|
||||||
|
expect(result).toBe('https://example.com/mcp');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove trailing slash from paths', () => {
|
||||||
|
expect(
|
||||||
|
OAuthUtils.buildResourceParameter('https://example.com/mcp/'),
|
||||||
|
).toBe('https://example.com/mcp');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle root URL consistently', () => {
|
||||||
|
// Both "https://example.com" and "https://example.com/" should
|
||||||
|
// produce the same canonical form without trailing slash
|
||||||
|
expect(OAuthUtils.buildResourceParameter('https://example.com')).toBe(
|
||||||
|
'https://example.com',
|
||||||
|
);
|
||||||
|
expect(OAuthUtils.buildResourceParameter('https://example.com/')).toBe(
|
||||||
|
'https://example.com',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Regression test for https://github.com/QwenLM/qwen-code/issues/1749
|
||||||
|
// Per MCP spec, resource should be the canonical URI including the path,
|
||||||
|
// so multi-tenant servers can distinguish between different MCP servers.
|
||||||
|
it('should preserve full path for multi-tenant MCP servers (issue #1749)', () => {
|
||||||
|
const result = OAuthUtils.buildResourceParameter(
|
||||||
|
'https://mcp.alibaba-inc.com/yuque/mcp',
|
||||||
|
);
|
||||||
|
// Must include the full path, not just the host
|
||||||
|
expect(result).toBe('https://mcp.alibaba-inc.com/yuque/mcp');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -355,11 +355,25 @@ export class OAuthUtils {
|
||||||
/**
|
/**
|
||||||
* Build a resource parameter for OAuth requests.
|
* Build a resource parameter for OAuth requests.
|
||||||
*
|
*
|
||||||
* @param endpointUrl The endpoint URL
|
* Per MCP spec and RFC 8707, the resource parameter MUST be the
|
||||||
* @returns The resource parameter value
|
* canonical URI of the MCP server. Clients SHOULD provide the most
|
||||||
|
* specific URI they can. The URI MUST NOT include a fragment and
|
||||||
|
* SHOULD NOT include a query component.
|
||||||
|
*
|
||||||
|
* @param endpointUrl The MCP server endpoint URL
|
||||||
|
* @returns The canonical resource URI
|
||||||
*/
|
*/
|
||||||
static buildResourceParameter(endpointUrl: string): string {
|
static buildResourceParameter(endpointUrl: string): string {
|
||||||
const url = new URL(endpointUrl);
|
const url = new URL(endpointUrl);
|
||||||
return `${url.protocol}//${url.host}`;
|
// Build canonical URI: scheme + host + path (no query, no fragment)
|
||||||
|
// per RFC 8707 Section 2 and MCP spec Resource Parameter Implementation
|
||||||
|
const path = url.pathname === '/' ? '' : url.pathname;
|
||||||
|
let canonical = `${url.protocol}//${url.host}${path}`;
|
||||||
|
// Remove trailing slash from non-root paths for consistency
|
||||||
|
// (MCP spec recommends form without trailing slash)
|
||||||
|
if (canonical.endsWith('/') && path !== '') {
|
||||||
|
canonical = canonical.slice(0, -1);
|
||||||
|
}
|
||||||
|
return canonical;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ describe('BaseTokenStorage', () => {
|
||||||
let storage: TestTokenStorage;
|
let storage: TestTokenStorage;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
storage = new TestTokenStorage('gemini-cli-mcp-oauth');
|
storage = new TestTokenStorage('qwen-code-mcp-oauth');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('validateCredentials', () => {
|
describe('validateCredentials', () => {
|
||||||
|
|
|
||||||
|
|
@ -101,8 +101,8 @@ describe('HybridTokenStorage', () => {
|
||||||
expect(await storage.getStorageType()).toBe(TokenStorageType.KEYCHAIN);
|
expect(await storage.getStorageType()).toBe(TokenStorageType.KEYCHAIN);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use file storage when GEMINI_FORCE_FILE_STORAGE is set', async () => {
|
it('should use file storage when QWEN_CODE_FORCE_FILE_STORAGE is set', async () => {
|
||||||
process.env['GEMINI_FORCE_FILE_STORAGE'] = 'true';
|
process.env['QWEN_CODE_FORCE_FILE_STORAGE'] = 'true';
|
||||||
mockFileStorage.getCredentials.mockResolvedValue(null);
|
mockFileStorage.getCredentials.mockResolvedValue(null);
|
||||||
|
|
||||||
await storage.getCredentials('test-server');
|
await storage.getCredentials('test-server');
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { FileTokenStorage } from './file-token-storage.js';
|
||||||
import type { TokenStorage, OAuthCredentials } from './types.js';
|
import type { TokenStorage, OAuthCredentials } from './types.js';
|
||||||
import { TokenStorageType } from './types.js';
|
import { TokenStorageType } from './types.js';
|
||||||
|
|
||||||
const FORCE_FILE_STORAGE_ENV_VAR = 'GEMINI_FORCE_FILE_STORAGE';
|
const FORCE_FILE_STORAGE_ENV_VAR = 'QWEN_CODE_FORCE_FILE_STORAGE';
|
||||||
|
|
||||||
export class HybridTokenStorage extends BaseTokenStorage {
|
export class HybridTokenStorage extends BaseTokenStorage {
|
||||||
private storage: TokenStorage | null = null;
|
private storage: TokenStorage | null = null;
|
||||||
|
|
|
||||||
|
|
@ -166,7 +166,7 @@ async function main() {
|
||||||
|
|
||||||
console.log(`\n✨ Local OTEL collector for GCP is running.`);
|
console.log(`\n✨ Local OTEL collector for GCP is running.`);
|
||||||
console.log(
|
console.log(
|
||||||
'\n🚀 To send telemetry, run the Gemini CLI in a separate terminal window.',
|
'\n🚀 To send telemetry, run Qwen Code in a separate terminal window.',
|
||||||
);
|
);
|
||||||
console.log(`\n📄 Collector logs are being written to: ${OTEL_LOG_FILE}`);
|
console.log(`\n📄 Collector logs are being written to: ${OTEL_LOG_FILE}`);
|
||||||
console.log(
|
console.log(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue