add settings command and update extension examples

This commit is contained in:
LaZzyMan 2026-01-20 16:43:04 +08:00
parent 2c22961f92
commit ba14e9e531
19 changed files with 708 additions and 24 deletions

View file

@ -13,6 +13,7 @@ import { disableCommand } from './extensions/disable.js';
import { enableCommand } from './extensions/enable.js';
import { linkCommand } from './extensions/link.js';
import { newCommand } from './extensions/new.js';
import { settingsCommand } from './extensions/settings.js';
export const extensionsCommand: CommandModule = {
command: 'extensions <command>',
@ -27,6 +28,7 @@ export const extensionsCommand: CommandModule = {
.command(enableCommand)
.command(linkCommand)
.command(newCommand)
.command(settingsCommand)
.demandCommand(1, 'You need at least one command before continuing.')
.version(false),
handler: () => {

View file

@ -0,0 +1,87 @@
---
name: diary-writer
description: generate a diary for user
color: yellow
tools:
- Glob
- Grep
- ListFiles
- ReadFile
- ReadManyFiles
- NotebookRead
- WebFetch
- TodoWrite
- WebSearch
modelConfig:
model: qwen3-coder-plus
---
You are a personal diary writing assistant who helps users capture their daily experiences, thoughts, and reflections in meaningful journal entries.
## Core Mission
Help users create thoughtful, well-structured diary entries that preserve their memories, emotions, and personal growth moments.
## Writing Style
**Tone & Voice**
- Warm, personal, and authentic
- Reflective and introspective
- Supportive without being overly sentimental
- Adapt to user's preferred style (casual, formal, poetic, etc.)
**Structure Options**
- Free-form narrative
- Bullet-point highlights
- Gratitude-focused entries
- Goal and achievement tracking
- Emotional processing format
## Capabilities
**1. Daily Entry Creation**
- Transform user's brief notes into full diary entries
- Expand on key moments with descriptive details
- Add context about weather, mood, or setting when relevant
- Include meaningful quotes or observations
**2. Reflection Prompts**
- Ask thoughtful questions to deepen entries
- Suggest areas worth exploring further
- Help identify patterns in thoughts and behaviors
- Encourage gratitude and positive reflection
**3. Memory Enhancement**
- Help recall specific details from the day
- Connect current events to past experiences
- Highlight personal growth and progress
- Preserve important conversations or interactions
**4. Organization**
- Suggest tags or themes for entries
- Create summaries for weekly/monthly reviews
- Track recurring topics or goals
- Maintain consistency in formatting
## Guidelines
- **Privacy First**: Treat all content as deeply personal and confidential
- **User's Voice**: Write in a way that sounds like the user, not generic
- **No Judgment**: Accept all emotions and experiences without criticism
- **Encourage Honesty**: Create a safe space for authentic expression
- **Balance**: Mix facts with feelings, events with reflections
## Output Format
When creating a diary entry, include:
1. **Date & Title** (optional creative title)
2. **Main Content** - The narrative or bullet points
3. **Reflection** - A brief closing thought or takeaway
4. **Tags** (optional) - For organization and future reference

View file

@ -0,0 +1,4 @@
{
"name": "agent-example",
"version": "1.0.0"
}

View file

@ -0,0 +1,4 @@
{
"name": "commands-example",
"version": "1.0.0"
}

View file

@ -1,4 +0,0 @@
{
"name": "custom-commands",
"version": "1.0.0"
}

View file

@ -1,5 +1,5 @@
{
"name": "excludeTools",
"name": "exclude-tools-example",
"version": "1.0.0",
"excludeTools": ["run_shell_command(rm -rf)"]
"excludeTools": ["Shell(rm -rf)"]
}

View file

@ -1,7 +1,7 @@
{
"name": "mcp-server-example",
"version": "1.0.0",
"description": "Example MCP Server for Gemini CLI Extension",
"description": "Example MCP Server for Qwen Code Extension",
"type": "module",
"main": "example.js",
"scripts": {

View file

@ -0,0 +1,4 @@
{
"name": "skills-example",
"version": "1.0.0"
}

View file

@ -0,0 +1,48 @@
---
name: synonyms
description: Generate synonyms for words or phrases. Use this skill when the user needs alternative words with similar meanings, wants to expand vocabulary, or seeks varied expressions for writing.
license: Complete terms in LICENSE.txt
---
This skill helps generate synonyms and alternative expressions for given words or phrases. It provides contextually appropriate alternatives to enhance vocabulary and improve writing variety.
The user provides a word, phrase, or sentence where they need synonym suggestions. They may specify the context, tone, or formality level desired.
## Synonym Generation Guidelines
When generating synonyms, consider:
- **Context**: The specific domain or situation where the word will be used
- **Tone**: Formal, informal, neutral, academic, conversational, etc.
- **Nuance**: Subtle differences in meaning between similar words
- **Register**: Appropriate level of formality for the intended audience
## Output Format
For each input word or phrase, provide:
1. **Direct Synonyms**: Words with nearly identical meanings
2. **Related Alternatives**: Words with similar but slightly different connotations
3. **Context Examples**: Brief usage examples when helpful
## Best Practices
- Prioritize commonly used synonyms over obscure alternatives
- Note any subtle differences in meaning or usage
- Consider regional variations when relevant
- Indicate formality levels (formal/informal/neutral)
- Provide multiple options to give users choices
## Example
**Input**: "happy"
**Synonyms**:
- **Direct**: joyful, cheerful, delighted, pleased, content
- **Informal**: thrilled, stoked, over the moon
- **Formal**: elated, gratified, blissful
- **Subtle variations**:
- _content_ - peaceful satisfaction
- _ecstatic_ - intense, overwhelming happiness
- _cheerful_ - outwardly expressing happiness

View file

@ -0,0 +1,345 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {
describe,
it,
expect,
vi,
beforeEach,
type MockInstance,
} from 'vitest';
import { settingsCommand } from './settings.js';
import yargs from 'yargs';
const mockGetLoadedExtensions = vi.hoisted(() => vi.fn());
const mockGetScopedEnvContents = vi.hoisted(() => vi.fn());
const mockUpdateSetting = vi.hoisted(() => vi.fn());
const mockPromptForSetting = vi.hoisted(() => vi.fn());
vi.mock('./utils.js', () => ({
getExtensionManager: vi.fn().mockResolvedValue({
getLoadedExtensions: mockGetLoadedExtensions,
}),
}));
vi.mock('@qwen-code/qwen-code-core', () => ({
ExtensionSettingScope: {
USER: 'user',
WORKSPACE: 'workspace',
},
getScopedEnvContents: mockGetScopedEnvContents,
promptForSetting: mockPromptForSetting,
updateSetting: mockUpdateSetting,
}));
describe('extensions settings command', () => {
it('should fail if no subcommand is provided', () => {
const validationParser = yargs([])
.command(settingsCommand)
.fail(false)
.locale('en');
expect(() => validationParser.parse('settings')).toThrow(
'Not enough non-option arguments: got 0, need at least 1',
);
});
it('should register set subcommand', () => {
const parser = yargs([]).command(settingsCommand).fail(false).locale('en');
expect(() => parser.parse('settings set')).toThrow(
'Not enough non-option arguments',
);
});
it('should register list subcommand', () => {
const parser = yargs([]).command(settingsCommand).fail(false).locale('en');
expect(() => parser.parse('settings list')).toThrow(
'Not enough non-option arguments',
);
});
it('should accept set command with name and setting', () => {
const parser = yargs([]).command(settingsCommand).fail(false).locale('en');
expect(() =>
parser.parse('settings set my-extension API_KEY'),
).not.toThrow();
});
it('should accept set command with scope option', () => {
const parser = yargs([]).command(settingsCommand).fail(false).locale('en');
expect(() =>
parser.parse('settings set my-extension API_KEY --scope=workspace'),
).not.toThrow();
});
it('should fail set command with invalid scope', () => {
const parser = yargs([]).command(settingsCommand).fail(false).locale('en');
expect(() =>
parser.parse('settings set my-extension API_KEY --scope=invalid'),
).toThrow();
});
it('should accept list command with name', () => {
const parser = yargs([]).command(settingsCommand).fail(false).locale('en');
expect(() => parser.parse('settings list my-extension')).not.toThrow();
});
});
describe('settings set handler', () => {
let consoleLogSpy: MockInstance;
beforeEach(() => {
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
vi.clearAllMocks();
});
it('should return early if extension manager is not available', async () => {
const { getExtensionManager } = await import('./utils.js');
vi.mocked(getExtensionManager).mockResolvedValueOnce(null as never);
const parser = yargs([]).command(settingsCommand);
await parser.parseAsync('settings set my-extension API_KEY');
expect(mockUpdateSetting).not.toHaveBeenCalled();
});
it('should return early if no extensions are loaded', async () => {
mockGetLoadedExtensions.mockReturnValueOnce([]);
const parser = yargs([]).command(settingsCommand);
await parser.parseAsync('settings set my-extension API_KEY');
expect(mockUpdateSetting).not.toHaveBeenCalled();
});
it('should log error if extension is not found', async () => {
mockGetLoadedExtensions.mockReturnValueOnce([
{ name: 'other-extension', id: 'other-id', config: {} },
]);
const parser = yargs([]).command(settingsCommand);
await parser.parseAsync('settings set my-extension API_KEY');
expect(consoleLogSpy).toHaveBeenCalledWith(
'Extension "my-extension" not found.',
);
expect(mockUpdateSetting).not.toHaveBeenCalled();
});
it('should call updateSetting with correct arguments for user scope', async () => {
const mockExtension = {
name: 'my-extension',
id: 'ext-id-123',
config: { name: 'my-extension', settings: [] },
};
mockGetLoadedExtensions.mockReturnValueOnce([mockExtension]);
const parser = yargs([]).command(settingsCommand);
await parser.parseAsync('settings set my-extension API_KEY');
expect(mockUpdateSetting).toHaveBeenCalledWith(
mockExtension.config,
mockExtension.id,
'API_KEY',
mockPromptForSetting,
'user',
);
});
it('should call updateSetting with workspace scope when specified', async () => {
const mockExtension = {
name: 'my-extension',
id: 'ext-id-123',
config: { name: 'my-extension', settings: [] },
};
mockGetLoadedExtensions.mockReturnValueOnce([mockExtension]);
const parser = yargs([]).command(settingsCommand);
await parser.parseAsync(
'settings set my-extension API_KEY --scope=workspace',
);
expect(mockUpdateSetting).toHaveBeenCalledWith(
mockExtension.config,
mockExtension.id,
'API_KEY',
mockPromptForSetting,
'workspace',
);
});
});
describe('settings list handler', () => {
let consoleLogSpy: MockInstance;
beforeEach(() => {
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
vi.clearAllMocks();
});
it('should return early if extension manager is not available', async () => {
const { getExtensionManager } = await import('./utils.js');
vi.mocked(getExtensionManager).mockResolvedValueOnce(null as never);
const parser = yargs([]).command(settingsCommand);
await parser.parseAsync('settings list my-extension');
expect(mockGetScopedEnvContents).not.toHaveBeenCalled();
});
it('should return early if no extensions are loaded', async () => {
mockGetLoadedExtensions.mockReturnValueOnce([]);
const parser = yargs([]).command(settingsCommand);
await parser.parseAsync('settings list my-extension');
expect(mockGetScopedEnvContents).not.toHaveBeenCalled();
});
it('should log error if extension is not found', async () => {
mockGetLoadedExtensions.mockReturnValueOnce([
{ name: 'other-extension', id: 'other-id', config: {} },
]);
const parser = yargs([]).command(settingsCommand);
await parser.parseAsync('settings list my-extension');
expect(consoleLogSpy).toHaveBeenCalledWith(
'Extension "my-extension" not found.',
);
});
it('should log message if extension has no settings', async () => {
const mockExtension = {
name: 'my-extension',
id: 'ext-id-123',
config: { name: 'my-extension' },
settings: [],
};
mockGetLoadedExtensions.mockReturnValueOnce([mockExtension]);
const parser = yargs([]).command(settingsCommand);
await parser.parseAsync('settings list my-extension');
expect(consoleLogSpy).toHaveBeenCalledWith(
'Extension "my-extension" has no settings to configure.',
);
});
it('should list settings with their values', async () => {
const mockExtension = {
name: 'my-extension',
id: 'ext-id-123',
config: { name: 'my-extension' },
settings: [
{
name: 'API Key',
envVar: 'API_KEY',
description: 'Your API key',
sensitive: false,
},
{
name: 'Secret Token',
envVar: 'SECRET_TOKEN',
description: 'A secret token',
sensitive: true,
},
],
};
mockGetLoadedExtensions.mockReturnValueOnce([mockExtension]);
mockGetScopedEnvContents
.mockResolvedValueOnce({ API_KEY: 'my-api-key' }) // user scope
.mockResolvedValueOnce({}); // workspace scope
const parser = yargs([]).command(settingsCommand);
await parser.parseAsync('settings list my-extension');
expect(consoleLogSpy).toHaveBeenCalledWith('Settings for "my-extension":');
expect(consoleLogSpy).toHaveBeenCalledWith('\n- API Key (API_KEY)');
expect(consoleLogSpy).toHaveBeenCalledWith(' Description: Your API key');
expect(consoleLogSpy).toHaveBeenCalledWith(' Value: my-api-key (user)');
});
it('should show workspace scope for workspace-scoped settings', async () => {
const mockExtension = {
name: 'my-extension',
id: 'ext-id-123',
config: { name: 'my-extension' },
settings: [
{
name: 'API Key',
envVar: 'API_KEY',
description: 'Your API key',
sensitive: false,
},
],
};
mockGetLoadedExtensions.mockReturnValueOnce([mockExtension]);
mockGetScopedEnvContents
.mockResolvedValueOnce({ API_KEY: 'user-value' }) // user scope
.mockResolvedValueOnce({ API_KEY: 'workspace-value' }); // workspace scope
const parser = yargs([]).command(settingsCommand);
await parser.parseAsync('settings list my-extension');
// Workspace should override user, and show (workspace) scope
expect(consoleLogSpy).toHaveBeenCalledWith(
' Value: workspace-value (workspace)',
);
});
it('should show [not set] for undefined settings', async () => {
const mockExtension = {
name: 'my-extension',
id: 'ext-id-123',
config: { name: 'my-extension' },
settings: [
{
name: 'API Key',
envVar: 'API_KEY',
description: 'Your API key',
sensitive: false,
},
],
};
mockGetLoadedExtensions.mockReturnValueOnce([mockExtension]);
mockGetScopedEnvContents
.mockResolvedValueOnce({}) // user scope
.mockResolvedValueOnce({}); // workspace scope
const parser = yargs([]).command(settingsCommand);
await parser.parseAsync('settings list my-extension');
expect(consoleLogSpy).toHaveBeenCalledWith(' Value: [not set]');
});
it('should show [value stored in keychain] for sensitive settings', async () => {
const mockExtension = {
name: 'my-extension',
id: 'ext-id-123',
config: { name: 'my-extension' },
settings: [
{
name: 'Secret Token',
envVar: 'SECRET_TOKEN',
description: 'A secret token',
sensitive: true,
},
],
};
mockGetLoadedExtensions.mockReturnValueOnce([mockExtension]);
mockGetScopedEnvContents
.mockResolvedValueOnce({ SECRET_TOKEN: 'secret-value' }) // user scope
.mockResolvedValueOnce({}); // workspace scope
const parser = yargs([]).command(settingsCommand);
await parser.parseAsync('settings list my-extension');
expect(consoleLogSpy).toHaveBeenCalledWith(
' Value: [value stored in keychain] (user)',
);
});
});

View file

@ -0,0 +1,148 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { CommandModule } from 'yargs';
import { getExtensionManager } from './utils.js';
import {
ExtensionSettingScope,
getScopedEnvContents,
promptForSetting,
updateSetting,
} from '@qwen-code/qwen-code-core';
// --- SET COMMAND ---
interface SetArgs {
name: string;
setting: string;
scope: string;
}
const setCommand: CommandModule<object, SetArgs> = {
command: 'set [--scope] <name> <setting>',
describe: 'Set a specific setting for an extension.',
builder: (yargs) =>
yargs
.positional('name', {
describe: 'Name of the extension to configure.',
type: 'string',
demandOption: true,
})
.positional('setting', {
describe: 'The setting to configure (name or env var).',
type: 'string',
demandOption: true,
})
.option('scope', {
describe: 'The scope to set the setting in.',
type: 'string',
choices: ['user', 'workspace'],
default: 'user',
}),
handler: async (args) => {
const { name, setting, scope } = args;
const extensionManager = await getExtensionManager();
if (!extensionManager) return;
const extensions = extensionManager.getLoadedExtensions();
if (!extensions || extensions.length === 0) return;
const extension = extensions.find((e) => e.name === name);
if (!extension) {
console.log(`Extension "${name}" not found.`);
return;
}
await updateSetting(
extension.config,
extension.id,
setting,
promptForSetting,
scope as ExtensionSettingScope,
);
},
};
// --- LIST COMMAND ---
interface ListArgs {
name: string;
}
const listCommand: CommandModule<object, ListArgs> = {
command: 'list <name>',
describe: 'List all settings for an extension.',
builder: (yargs) =>
yargs.positional('name', {
describe: 'Name of the extension.',
type: 'string',
demandOption: true,
}),
handler: async (args) => {
const { name } = args;
const extensionManager = await getExtensionManager();
if (!extensionManager) return;
const extensions = extensionManager.getLoadedExtensions();
if (!extensions || extensions.length === 0) return;
const extension = extensions.find((e) => e.name === name);
if (!extension) {
console.log(`Extension "${name}" not found.`);
return;
}
if (!extension || !extension.settings || extension.settings.length === 0) {
console.log(`Extension "${name}" has no settings to configure.`);
return;
}
const userSettings = await getScopedEnvContents(
extension.config,
extension.id,
ExtensionSettingScope.USER,
);
const workspaceSettings = await getScopedEnvContents(
extension.config,
extension.id,
ExtensionSettingScope.WORKSPACE,
);
const mergedSettings = { ...userSettings, ...workspaceSettings };
console.log(`Settings for "${name}":`);
for (const setting of extension.settings) {
const value = mergedSettings[setting.envVar];
let displayValue: string;
let scopeInfo = '';
if (workspaceSettings[setting.envVar] !== undefined) {
scopeInfo = ' (workspace)';
} else if (userSettings[setting.envVar] !== undefined) {
scopeInfo = ' (user)';
}
if (value === undefined) {
displayValue = '[not set]';
} else if (setting.sensitive) {
displayValue = '[value stored in keychain]';
} else {
displayValue = value;
}
console.log(`
- ${setting.name} (${setting.envVar})`);
console.log(` Description: ${setting.description}`);
console.log(` Value: ${displayValue}${scopeInfo}`);
}
},
};
// --- SETTINGS COMMAND ---
export const settingsCommand: CommandModule = {
command: 'settings <command>',
describe: 'Manage extension settings.',
builder: (yargs) =>
yargs
.command(setCommand)
.command(listCommand)
.demandCommand(1, 'You need to specify a command (set or list).')
.version(false),
handler: () => {
// This handler is not called when a subcommand is provided.
// Yargs will show the help menu.
},
};