mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-27 20:19:14 +00:00
Compare commits
6 Commits
feature/qw
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39e409b5e5 | ||
|
|
f5ee3df219 | ||
|
|
b03daebbc1 | ||
|
|
472df045d3 | ||
|
|
1baf5d795f | ||
|
|
98fd0f6a89 |
@@ -46,7 +46,7 @@ jobs:
|
||||
|
||||
- name: 'Log in to the Container registry'
|
||||
if: |-
|
||||
${{ github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) }}
|
||||
${{ github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || github.event.inputs.publish == 'true') }}
|
||||
uses: 'docker/login-action@v3' # ratchet:exclude
|
||||
with:
|
||||
registry: '${{ env.REGISTRY }}'
|
||||
|
||||
28
.github/workflows/ci.yml
vendored
28
.github/workflows/ci.yml
vendored
@@ -95,10 +95,19 @@ jobs:
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: 'package-lock.json'
|
||||
registry-url: 'https://registry.npmjs.org/'
|
||||
|
||||
- name: 'Configure npm for rate limiting'
|
||||
run: |-
|
||||
npm config set fetch-retry-mintimeout 20000
|
||||
npm config set fetch-retry-maxtimeout 120000
|
||||
npm config set fetch-retries 5
|
||||
npm config set fetch-timeout 300000
|
||||
|
||||
- name: 'Install dependencies'
|
||||
run: |-
|
||||
npm ci
|
||||
npm ci --prefer-offline --no-audit --progress=false
|
||||
|
||||
- name: 'Run formatter check'
|
||||
run: |-
|
||||
@@ -273,15 +282,24 @@ jobs:
|
||||
with:
|
||||
node-version: '${{ matrix.node-version }}'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: 'package-lock.json'
|
||||
registry-url: 'https://registry.npmjs.org/'
|
||||
|
||||
- name: 'Configure npm for rate limiting'
|
||||
run: |-
|
||||
npm config set fetch-retry-mintimeout 20000
|
||||
npm config set fetch-retry-maxtimeout 120000
|
||||
npm config set fetch-retries 5
|
||||
npm config set fetch-timeout 300000
|
||||
|
||||
- name: 'Install dependencies'
|
||||
run: |-
|
||||
npm ci --prefer-offline --no-audit --progress=false
|
||||
|
||||
- name: 'Build project'
|
||||
run: |-
|
||||
npm run build
|
||||
|
||||
- name: 'Install dependencies for testing'
|
||||
run: |-
|
||||
npm ci
|
||||
|
||||
- name: 'Run tests and generate reports'
|
||||
env:
|
||||
NO_COLOR: true
|
||||
|
||||
22
.github/workflows/e2e.yml
vendored
22
.github/workflows/e2e.yml
vendored
@@ -28,10 +28,19 @@ jobs:
|
||||
with:
|
||||
node-version: '${{ matrix.node-version }}'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: 'package-lock.json'
|
||||
registry-url: 'https://registry.npmjs.org/'
|
||||
|
||||
- name: 'Configure npm for rate limiting'
|
||||
run: |-
|
||||
npm config set fetch-retry-mintimeout 20000
|
||||
npm config set fetch-retry-maxtimeout 120000
|
||||
npm config set fetch-retries 5
|
||||
npm config set fetch-timeout 300000
|
||||
|
||||
- name: 'Install dependencies'
|
||||
run: |-
|
||||
npm ci
|
||||
npm ci --prefer-offline --no-audit --progress=false
|
||||
|
||||
- name: 'Build project'
|
||||
run: |-
|
||||
@@ -74,10 +83,19 @@ jobs:
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: 'package-lock.json'
|
||||
registry-url: 'https://registry.npmjs.org/'
|
||||
|
||||
- name: 'Configure npm for rate limiting'
|
||||
run: |-
|
||||
npm config set fetch-retry-mintimeout 20000
|
||||
npm config set fetch-retry-maxtimeout 120000
|
||||
npm config set fetch-retries 5
|
||||
npm config set fetch-timeout 300000
|
||||
|
||||
- name: 'Install dependencies'
|
||||
run: |-
|
||||
npm ci
|
||||
npm ci --prefer-offline --no-audit --progress=false
|
||||
|
||||
- name: 'Build project'
|
||||
run: |-
|
||||
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
OPENAI_API_KEY: '${{ secrets.OPENAI_API_KEY }}'
|
||||
OPENAI_BASE_URL: '${{ secrets.OPENAI_BASE_URL }}'
|
||||
OPENAI_MODEL: '${{ secrets.OPENAI_MODEL }}'
|
||||
settings_json: |
|
||||
settings_json: |-
|
||||
{
|
||||
"maxSessionTurns": 25,
|
||||
"coreTools": [
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
## Steps
|
||||
|
||||
1. Run: `gh label list --repo ${{ github.repository }} --limit 100` to get all available labels.
|
||||
2. Use right tool to review the issue title and body provided in the environment variables: "${ISSUE_TITLE}" and "${ISSUE_BODY}".
|
||||
2. Use shell command `echo` to check the issue title and body provided in the environment variables: "${ISSUE_TITLE}" and "${ISSUE_BODY}".
|
||||
3. Ignore any existing priorities or tags on the issue. Just report your findings.
|
||||
4. Select the most relevant labels from the existing labels, focusing on kind/*, area/*, sub-area/* and priority/*. For area/* and kind/* limit yourself to only the single most applicable label in each case.
|
||||
6. Apply the selected labels to this issue using: `gh issue edit ${{ github.event.issue.number }} --repo ${{ github.repository }} --add-label "label1,label2"`.
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
GITHUB_REPOSITORY: '${{ github.repository }}'
|
||||
run: |
|
||||
run: |-
|
||||
echo "🔍 Finding issues without labels..."
|
||||
NO_LABEL_ISSUES=$(gh issue list --repo ${{ github.repository }} --search "is:open is:issue no:label" --json number,title,body)
|
||||
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
OPENAI_API_KEY: '${{ secrets.OPENAI_API_KEY }}'
|
||||
OPENAI_BASE_URL: '${{ secrets.OPENAI_BASE_URL }}'
|
||||
OPENAI_MODEL: '${{ secrets.OPENAI_MODEL }}'
|
||||
settings_json: |
|
||||
settings_json: |-
|
||||
{
|
||||
"maxSessionTurns": 25,
|
||||
"coreTools": [
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
## Steps
|
||||
|
||||
1. Run: `gh label list --repo ${{ github.repository }} --limit 100` to get all available labels.
|
||||
2. Use right tool to check environment variable for issues to triage: $ISSUES_TO_TRIAGE (JSON array of issues)
|
||||
2. Use shell command `echo` to check environment variable for issues to triage: $ISSUES_TO_TRIAGE (JSON array of issues)
|
||||
3. Review the issue title, body and any comments provided in the environment variables.
|
||||
4. Ignore any existing priorities or tags on the issue.
|
||||
5. Select the most relevant labels from the existing labels, focusing on kind/*, area/*, sub-area/* and priority/*.
|
||||
|
||||
19
.github/workflows/qwen-code-pr-review.yml
vendored
19
.github/workflows/qwen-code-pr-review.yml
vendored
@@ -16,7 +16,7 @@ on:
|
||||
|
||||
jobs:
|
||||
review-pr:
|
||||
if: >
|
||||
if: |-
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(github.event_name == 'pull_request_target' &&
|
||||
github.event.action == 'opened' &&
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
${{ github.event_name == 'pull_request_target' || github.event_name == 'workflow_dispatch' }}
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
run: |
|
||||
run: |-
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
PR_NUMBER=${{ github.event.inputs.pr_number }}
|
||||
else
|
||||
@@ -82,7 +82,7 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
COMMENT_BODY: '${{ github.event.comment.body }}'
|
||||
run: |
|
||||
run: |-
|
||||
PR_NUMBER=${{ github.event.issue.number }}
|
||||
echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
|
||||
# Extract additional instructions from comment
|
||||
@@ -110,22 +110,15 @@ jobs:
|
||||
OPENAI_API_KEY: '${{ secrets.OPENAI_API_KEY }}'
|
||||
OPENAI_BASE_URL: '${{ secrets.OPENAI_BASE_URL }}'
|
||||
OPENAI_MODEL: '${{ secrets.OPENAI_MODEL }}'
|
||||
settings_json: |
|
||||
settings_json: |-
|
||||
{
|
||||
"coreTools": [
|
||||
"run_shell_command(echo)",
|
||||
"run_shell_command(gh pr view)",
|
||||
"run_shell_command(gh pr diff)",
|
||||
"run_shell_command(gh pr comment)",
|
||||
"run_shell_command(cat)",
|
||||
"run_shell_command(head)",
|
||||
"run_shell_command(tail)",
|
||||
"run_shell_command(grep)",
|
||||
"run_shell_command",
|
||||
"write_file"
|
||||
],
|
||||
"sandbox": false
|
||||
}
|
||||
prompt: |
|
||||
prompt: |-
|
||||
You are an expert code reviewer. You have access to shell commands to gather PR information and perform the review.
|
||||
|
||||
IMPORTANT: Use the available shell commands to gather information. Do not ask for information to be provided.
|
||||
|
||||
@@ -69,6 +69,11 @@ This guide provides solutions to common issues and debugging tips, including top
|
||||
- **Cause:** The `is-in-ci` package checks for the presence of `CI`, `CONTINUOUS_INTEGRATION`, or any environment variable with a `CI_` prefix. When any of these are found, it signals that the environment is non-interactive, which prevents the CLI from starting in its interactive mode.
|
||||
- **Solution:** If the `CI_` prefixed variable is not needed for the CLI to function, you can temporarily unset it for the command. e.g., `env -u CI_TOKEN qwen`
|
||||
|
||||
- **CLI becomes unresponsive during interactive selection dialogs**
|
||||
- **Issue:** When encountering selection dialogs with numbered options (such as shell command confirmation dialogs), the CLI appears "frozen" and does not respond to keyboard input.
|
||||
- **Cause:** This was caused by multiple keyboard input handlers (`useKeypress` hooks) being active simultaneously, creating conflicts in terminal raw mode control.
|
||||
- **Solution:** This issue has been fixed in recent versions. The keyboard input handling has been consolidated to prevent conflicts. If you encounter this issue, ensure you're using the latest version of Qwen Code.
|
||||
|
||||
- **DEBUG mode not working from project .env file**
|
||||
- **Issue:** Setting `DEBUG=true` in a project's `.env` file doesn't enable debug mode for the CLI.
|
||||
- **Cause:** The `DEBUG` and `DEBUG_MODE` variables are automatically excluded from project `.env` files to prevent interference with the CLI behavior.
|
||||
|
||||
@@ -35,7 +35,7 @@ describe('docsCommand', () => {
|
||||
throw new Error('docsCommand must have an action.');
|
||||
}
|
||||
|
||||
const docsUrl = 'https://github.com/QwenLM/qwen-code';
|
||||
const docsUrl = 'https://qwenlm.github.io/qwen-code-docs/en';
|
||||
|
||||
await docsCommand.action(mockContext, '');
|
||||
|
||||
@@ -57,7 +57,7 @@ describe('docsCommand', () => {
|
||||
|
||||
// Simulate a sandbox environment
|
||||
process.env.SANDBOX = 'gemini-sandbox';
|
||||
const docsUrl = 'https://github.com/QwenLM/qwen-code';
|
||||
const docsUrl = 'https://qwenlm.github.io/qwen-code-docs/en';
|
||||
|
||||
await docsCommand.action(mockContext, '');
|
||||
|
||||
@@ -80,7 +80,7 @@ describe('docsCommand', () => {
|
||||
|
||||
// Simulate the specific 'sandbox-exec' environment
|
||||
process.env.SANDBOX = 'sandbox-exec';
|
||||
const docsUrl = 'https://github.com/QwenLM/qwen-code';
|
||||
const docsUrl = 'https://qwenlm.github.io/qwen-code-docs/en';
|
||||
|
||||
await docsCommand.action(mockContext, '');
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ export const docsCommand: SlashCommand = {
|
||||
description: 'open full Qwen Code documentation in your browser',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: async (context: CommandContext): Promise<void> => {
|
||||
const docsUrl = 'https://github.com/QwenLM/qwen-code';
|
||||
const docsUrl = 'https://qwenlm.github.io/qwen-code-docs/en';
|
||||
|
||||
if (process.env.SANDBOX && process.env.SANDBOX !== 'sandbox-exec') {
|
||||
context.ui.addItem(
|
||||
|
||||
@@ -117,7 +117,7 @@ describe('memoryCommand', () => {
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Usage: /memory add [--global|--project] <text to remember>',
|
||||
content: 'Usage: /memory add <text to remember>',
|
||||
});
|
||||
|
||||
expect(mockContext.ui.addItem).not.toHaveBeenCalled();
|
||||
@@ -132,7 +132,7 @@ describe('memoryCommand', () => {
|
||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: `Attempting to save to memory : "${fact}"`,
|
||||
text: `Attempting to save to memory: "${fact}"`,
|
||||
},
|
||||
expect.any(Number),
|
||||
);
|
||||
@@ -143,61 +143,6 @@ describe('memoryCommand', () => {
|
||||
toolArgs: { fact },
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle --global flag and add scope to tool args', () => {
|
||||
if (!addCommand.action) throw new Error('Command has no action');
|
||||
|
||||
const fact = 'remember this globally';
|
||||
const result = addCommand.action(mockContext, `--global ${fact}`);
|
||||
|
||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: `Attempting to save to memory (global): "${fact}"`,
|
||||
},
|
||||
expect.any(Number),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'tool',
|
||||
toolName: 'save_memory',
|
||||
toolArgs: { fact, scope: 'global' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle --project flag and add scope to tool args', () => {
|
||||
if (!addCommand.action) throw new Error('Command has no action');
|
||||
|
||||
const fact = 'remember this for project';
|
||||
const result = addCommand.action(mockContext, `--project ${fact}`);
|
||||
|
||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: `Attempting to save to memory (project): "${fact}"`,
|
||||
},
|
||||
expect.any(Number),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'tool',
|
||||
toolName: 'save_memory',
|
||||
toolArgs: { fact, scope: 'project' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should return error if flag is provided but no fact follows', () => {
|
||||
if (!addCommand.action) throw new Error('Command has no action');
|
||||
|
||||
const result = addCommand.action(mockContext, '--global ');
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Usage: /memory add [--global|--project] <text to remember>',
|
||||
});
|
||||
|
||||
expect(mockContext.ui.addItem).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('/memory refresh', () => {
|
||||
@@ -228,7 +173,7 @@ describe('memoryCommand', () => {
|
||||
|
||||
mockContext = createMockCommandContext({
|
||||
services: {
|
||||
config: mockConfig,
|
||||
config: Promise.resolve(mockConfig),
|
||||
settings: {
|
||||
merged: {
|
||||
memoryDiscoveryMaxDirs: 1000,
|
||||
|
||||
@@ -7,11 +7,7 @@
|
||||
import {
|
||||
getErrorMessage,
|
||||
loadServerHierarchicalMemory,
|
||||
QWEN_DIR,
|
||||
} from '@qwen-code/qwen-code-core';
|
||||
import path from 'node:path';
|
||||
import os from 'os';
|
||||
import fs from 'fs/promises';
|
||||
import { MessageType } from '../types.js';
|
||||
import {
|
||||
CommandKind,
|
||||
@@ -45,136 +41,24 @@ export const memoryCommand: SlashCommand = {
|
||||
Date.now(),
|
||||
);
|
||||
},
|
||||
subCommands: [
|
||||
{
|
||||
name: '--project',
|
||||
description: 'Show project-level memory contents.',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: async (context) => {
|
||||
try {
|
||||
const projectMemoryPath = path.join(process.cwd(), 'QWEN.md');
|
||||
const memoryContent = await fs.readFile(
|
||||
projectMemoryPath,
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const messageContent =
|
||||
memoryContent.trim().length > 0
|
||||
? `Project memory content from ${projectMemoryPath}:\n\n---\n${memoryContent}\n---`
|
||||
: 'Project memory is currently empty.';
|
||||
|
||||
context.ui.addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: messageContent,
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
} catch (_error) {
|
||||
context.ui.addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: 'Project memory file not found or is currently empty.',
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '--global',
|
||||
description: 'Show global memory contents.',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: async (context) => {
|
||||
try {
|
||||
const globalMemoryPath = path.join(
|
||||
os.homedir(),
|
||||
QWEN_DIR,
|
||||
'QWEN.md',
|
||||
);
|
||||
const globalMemoryContent = await fs.readFile(
|
||||
globalMemoryPath,
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const messageContent =
|
||||
globalMemoryContent.trim().length > 0
|
||||
? `Global memory content:\n\n---\n${globalMemoryContent}\n---`
|
||||
: 'Global memory is currently empty.';
|
||||
|
||||
context.ui.addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: messageContent,
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
} catch (_error) {
|
||||
context.ui.addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: 'Global memory file not found or is currently empty.',
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'add',
|
||||
description:
|
||||
'Add content to the memory. Use --global for global memory or --project for project memory.',
|
||||
description: 'Add content to the memory.',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: (context, args): SlashCommandActionReturn | void => {
|
||||
if (!args || args.trim() === '') {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content:
|
||||
'Usage: /memory add [--global|--project] <text to remember>',
|
||||
content: 'Usage: /memory add <text to remember>',
|
||||
};
|
||||
}
|
||||
|
||||
const trimmedArgs = args.trim();
|
||||
let scope: 'global' | 'project' | undefined;
|
||||
let fact: string;
|
||||
|
||||
// Check for scope flags
|
||||
if (trimmedArgs.startsWith('--global ')) {
|
||||
scope = 'global';
|
||||
fact = trimmedArgs.substring('--global '.length).trim();
|
||||
} else if (trimmedArgs.startsWith('--project ')) {
|
||||
scope = 'project';
|
||||
fact = trimmedArgs.substring('--project '.length).trim();
|
||||
} else if (trimmedArgs === '--global' || trimmedArgs === '--project') {
|
||||
// Flag provided but no text after it
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content:
|
||||
'Usage: /memory add [--global|--project] <text to remember>',
|
||||
};
|
||||
} else {
|
||||
// No scope specified, will be handled by the tool
|
||||
fact = trimmedArgs;
|
||||
}
|
||||
|
||||
if (!fact || fact.trim() === '') {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content:
|
||||
'Usage: /memory add [--global|--project] <text to remember>',
|
||||
};
|
||||
}
|
||||
|
||||
const scopeText = scope ? `(${scope})` : '';
|
||||
context.ui.addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: `Attempting to save to memory ${scopeText}: "${fact}"`,
|
||||
text: `Attempting to save to memory: "${args.trim()}"`,
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
@@ -182,67 +66,9 @@ export const memoryCommand: SlashCommand = {
|
||||
return {
|
||||
type: 'tool',
|
||||
toolName: 'save_memory',
|
||||
toolArgs: scope ? { fact, scope } : { fact },
|
||||
toolArgs: { fact: args.trim() },
|
||||
};
|
||||
},
|
||||
subCommands: [
|
||||
{
|
||||
name: '--project',
|
||||
description: 'Add content to project-level memory.',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: (context, args): SlashCommandActionReturn | void => {
|
||||
if (!args || args.trim() === '') {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Usage: /memory add --project <text to remember>',
|
||||
};
|
||||
}
|
||||
|
||||
context.ui.addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: `Attempting to save to project memory: "${args.trim()}"`,
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
|
||||
return {
|
||||
type: 'tool',
|
||||
toolName: 'save_memory',
|
||||
toolArgs: { fact: args.trim(), scope: 'project' },
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '--global',
|
||||
description: 'Add content to global memory.',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: (context, args): SlashCommandActionReturn | void => {
|
||||
if (!args || args.trim() === '') {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Usage: /memory add --global <text to remember>',
|
||||
};
|
||||
}
|
||||
|
||||
context.ui.addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: `Attempting to save to global memory: "${args.trim()}"`,
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
|
||||
return {
|
||||
type: 'tool',
|
||||
toolName: 'save_memory',
|
||||
toolArgs: { fact: args.trim(), scope: 'global' },
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'refresh',
|
||||
@@ -258,7 +84,7 @@ export const memoryCommand: SlashCommand = {
|
||||
);
|
||||
|
||||
try {
|
||||
const config = context.services.config;
|
||||
const config = await context.services.config;
|
||||
if (config) {
|
||||
const { memoryContent, fileCount } =
|
||||
await loadServerHierarchicalMemory(
|
||||
|
||||
@@ -8,7 +8,6 @@ import { ToolConfirmationOutcome } from '@qwen-code/qwen-code-core';
|
||||
import { Box, Text } from 'ink';
|
||||
import React from 'react';
|
||||
import { Colors } from '../colors.js';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import {
|
||||
RadioButtonSelect,
|
||||
RadioSelectItem,
|
||||
@@ -31,15 +30,6 @@ export const ShellConfirmationDialog: React.FC<
|
||||
> = ({ request }) => {
|
||||
const { commands, onConfirm } = request;
|
||||
|
||||
useKeypress(
|
||||
(key) => {
|
||||
if (key.name === 'escape') {
|
||||
onConfirm(ToolConfirmationOutcome.Cancel);
|
||||
}
|
||||
},
|
||||
{ isActive: true },
|
||||
);
|
||||
|
||||
const handleSelect = (item: ToolConfirmationOutcome) => {
|
||||
if (item === ToolConfirmationOutcome.Cancel) {
|
||||
onConfirm(item);
|
||||
@@ -50,6 +40,10 @@ export const ShellConfirmationDialog: React.FC<
|
||||
}
|
||||
};
|
||||
|
||||
const handleEscape = () => {
|
||||
onConfirm(ToolConfirmationOutcome.Cancel);
|
||||
};
|
||||
|
||||
const options: Array<RadioSelectItem<ToolConfirmationOutcome>> = [
|
||||
{
|
||||
label: 'Yes, allow once',
|
||||
@@ -96,7 +90,12 @@ export const ShellConfirmationDialog: React.FC<
|
||||
<Text>Do you want to proceed?</Text>
|
||||
</Box>
|
||||
|
||||
<RadioButtonSelect items={options} onSelect={handleSelect} isFocused />
|
||||
<RadioButtonSelect
|
||||
items={options}
|
||||
onSelect={handleSelect}
|
||||
onEscape={handleEscape}
|
||||
isFocused
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
RadioSelectItem,
|
||||
} from '../shared/RadioButtonSelect.js';
|
||||
import { MaxSizedBox } from '../shared/MaxSizedBox.js';
|
||||
import { useKeypress } from '../../hooks/useKeypress.js';
|
||||
|
||||
export interface ToolConfirmationMessageProps {
|
||||
confirmationDetails: ToolCallConfirmationDetails;
|
||||
@@ -57,18 +56,11 @@ export const ToolConfirmationMessage: React.FC<
|
||||
onConfirm(outcome);
|
||||
};
|
||||
|
||||
useKeypress(
|
||||
(key) => {
|
||||
if (!isFocused) return;
|
||||
if (key.name === 'escape' || (key.ctrl && key.name === 'c')) {
|
||||
handleConfirm(ToolConfirmationOutcome.Cancel);
|
||||
}
|
||||
},
|
||||
{ isActive: isFocused },
|
||||
);
|
||||
|
||||
const handleSelect = (item: ToolConfirmationOutcome) => handleConfirm(item);
|
||||
|
||||
const handleEscape = () => handleConfirm(ToolConfirmationOutcome.Cancel);
|
||||
const handleCancel = () => handleConfirm(ToolConfirmationOutcome.Cancel);
|
||||
|
||||
let bodyContent: React.ReactNode | null = null; // Removed contextDisplay here
|
||||
let question: string;
|
||||
|
||||
@@ -283,6 +275,8 @@ export const ToolConfirmationMessage: React.FC<
|
||||
<RadioButtonSelect
|
||||
items={options}
|
||||
onSelect={handleSelect}
|
||||
onEscape={handleEscape}
|
||||
onCancel={handleCancel}
|
||||
isFocused={isFocused}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -34,6 +34,10 @@ export interface RadioButtonSelectProps<T> {
|
||||
onSelect: (value: T) => void;
|
||||
/** Function called when an item is highlighted. Receives the `value` of the selected item. */
|
||||
onHighlight?: (value: T) => void;
|
||||
/** Function called when escape key is pressed. */
|
||||
onEscape?: () => void;
|
||||
/** Function called when Ctrl+C is pressed. */
|
||||
onCancel?: () => void;
|
||||
/** Whether this select input is currently focused and should respond to input. */
|
||||
isFocused?: boolean;
|
||||
/** Whether to show the scroll arrows. */
|
||||
@@ -55,6 +59,8 @@ export function RadioButtonSelect<T>({
|
||||
initialIndex = 0,
|
||||
onSelect,
|
||||
onHighlight,
|
||||
onEscape,
|
||||
onCancel,
|
||||
isFocused,
|
||||
showScrollArrows = false,
|
||||
maxItemsToShow = 10,
|
||||
@@ -91,6 +97,18 @@ export function RadioButtonSelect<T>({
|
||||
const { sequence, name } = key;
|
||||
const isNumeric = showNumbers && /^[0-9]$/.test(sequence);
|
||||
|
||||
// Handle escape key
|
||||
if (name === 'escape') {
|
||||
onEscape?.();
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle Ctrl+C
|
||||
if (key.ctrl && name === 'c') {
|
||||
onCancel?.();
|
||||
return;
|
||||
}
|
||||
|
||||
// Any key press that is not a digit should clear the number input buffer.
|
||||
if (!isNumeric && numberInputTimer.current) {
|
||||
clearTimeout(numberInputTimer.current);
|
||||
|
||||
@@ -1351,7 +1351,9 @@ export class OpenAIContentGenerator implements ContentGenerator {
|
||||
|
||||
// Handle text content
|
||||
if (choice.delta?.content) {
|
||||
parts.push({ text: choice.delta.content });
|
||||
if (typeof choice.delta.content === 'string') {
|
||||
parts.push({ text: choice.delta.content });
|
||||
}
|
||||
}
|
||||
|
||||
// Handle tool calls - only accumulate during streaming, emit when complete
|
||||
@@ -1371,10 +1373,36 @@ export class OpenAIContentGenerator implements ContentGenerator {
|
||||
accumulatedCall.id = toolCall.id;
|
||||
}
|
||||
if (toolCall.function?.name) {
|
||||
// If this is a new function name, reset the arguments
|
||||
if (accumulatedCall.name !== toolCall.function.name) {
|
||||
accumulatedCall.arguments = '';
|
||||
}
|
||||
accumulatedCall.name = toolCall.function.name;
|
||||
}
|
||||
if (toolCall.function?.arguments) {
|
||||
accumulatedCall.arguments += toolCall.function.arguments;
|
||||
// Check if we already have a complete JSON object
|
||||
const currentArgs = accumulatedCall.arguments;
|
||||
const newArgs = toolCall.function.arguments;
|
||||
|
||||
// If current arguments already form a complete JSON and new arguments start a new object,
|
||||
// this indicates a new tool call with the same name
|
||||
let shouldReset = false;
|
||||
if (currentArgs && newArgs.trim().startsWith('{')) {
|
||||
try {
|
||||
JSON.parse(currentArgs);
|
||||
// If we can parse current arguments as complete JSON and new args start with {,
|
||||
// this is likely a new tool call
|
||||
shouldReset = true;
|
||||
} catch {
|
||||
// Current arguments are not complete JSON, continue accumulating
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldReset) {
|
||||
accumulatedCall.arguments = newArgs;
|
||||
} else {
|
||||
accumulatedCall.arguments += newArgs;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1562,7 +1590,7 @@ export class OpenAIContentGenerator implements ContentGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
messageContent = textParts.join('');
|
||||
messageContent = textParts.join('').trimEnd();
|
||||
}
|
||||
|
||||
const choice: OpenAIChoice = {
|
||||
|
||||
@@ -10,7 +10,7 @@ import * as path from 'path';
|
||||
import * as fs from 'fs/promises';
|
||||
import * as os from 'os';
|
||||
import type { ChildProcess } from 'node:child_process';
|
||||
import { getProjectHash, QWEN_DIR } from '../utils/paths.js';
|
||||
import { getProjectHash, GEMINI_DIR } from '../utils/paths.js';
|
||||
|
||||
const hoistedMockExec = vi.hoisted(() => vi.fn());
|
||||
vi.mock('node:child_process', () => ({
|
||||
@@ -157,7 +157,7 @@ describe('GitService', () => {
|
||||
let gitConfigPath: string;
|
||||
|
||||
beforeEach(() => {
|
||||
repoDir = path.join(homedir, QWEN_DIR, 'history', hash);
|
||||
repoDir = path.join(homedir, GEMINI_DIR, 'history', hash);
|
||||
gitConfigPath = path.join(repoDir, '.gitconfig');
|
||||
});
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import * as os from 'os';
|
||||
import { isNodeError } from '../utils/errors.js';
|
||||
import { exec } from 'node:child_process';
|
||||
import { simpleGit, SimpleGit, CheckRepoActions } from 'simple-git';
|
||||
import { getProjectHash, QWEN_DIR } from '../utils/paths.js';
|
||||
import { getProjectHash, GEMINI_DIR } from '../utils/paths.js';
|
||||
|
||||
export class GitService {
|
||||
private projectRoot: string;
|
||||
@@ -21,7 +21,7 @@ export class GitService {
|
||||
|
||||
private getHistoryDir(): string {
|
||||
const hash = getProjectHash(this.projectRoot);
|
||||
return path.join(os.homedir(), QWEN_DIR, 'history', hash);
|
||||
return path.join(os.homedir(), GEMINI_DIR, 'history', hash);
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
|
||||
@@ -522,16 +522,13 @@ describe('MemoryTool', () => {
|
||||
expect(result).not.toBe(false);
|
||||
|
||||
if (result && result.type === 'edit') {
|
||||
expect(result.title).toContain('Choose Memory Location');
|
||||
expect(result.title).toContain('GLOBAL');
|
||||
expect(result.title).toContain('PROJECT');
|
||||
expect(result.fileName).toBe('QWEN.md');
|
||||
expect(result.title).toBe('Choose Memory Storage Location');
|
||||
expect(result.fileName).toBe('Memory Storage Options');
|
||||
expect(result.fileDiff).toContain('Choose where to save this memory');
|
||||
expect(result.fileDiff).toContain('Test fact');
|
||||
expect(result.fileDiff).toContain('--- QWEN.md');
|
||||
expect(result.fileDiff).toContain('+++ QWEN.md');
|
||||
expect(result.fileDiff).toContain('+- Test fact');
|
||||
expect(result.originalContent).toContain('scope: global');
|
||||
expect(result.originalContent).toContain('INSTRUCTIONS:');
|
||||
expect(result.fileDiff).toContain('Global:');
|
||||
expect(result.fileDiff).toContain('Project:');
|
||||
expect(result.originalContent).toBe('');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -580,16 +577,13 @@ describe('MemoryTool', () => {
|
||||
expect(description).toBe(`${expectedPath} (project)`);
|
||||
});
|
||||
|
||||
it('should show choice prompt when scope is not specified', () => {
|
||||
it('should default to global scope when scope is not specified', () => {
|
||||
const params = { fact: 'Test fact' };
|
||||
const invocation = memoryTool.build(params);
|
||||
const description = invocation.getDescription();
|
||||
|
||||
const globalPath = path.join('~', '.qwen', 'QWEN.md');
|
||||
const projectPath = path.join(process.cwd(), 'QWEN.md');
|
||||
expect(description).toBe(
|
||||
`CHOOSE: ${globalPath} (global) OR ${projectPath} (project)`,
|
||||
);
|
||||
const expectedPath = path.join('~', '.qwen', 'QWEN.md');
|
||||
expect(description).toBe(`${expectedPath} (global)`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -199,12 +199,7 @@ class MemoryToolInvocation extends BaseToolInvocation<
|
||||
private static readonly allowlist: Set<string> = new Set();
|
||||
|
||||
getDescription(): string {
|
||||
if (!this.params.scope) {
|
||||
const globalPath = tildeifyPath(getMemoryFilePath('global'));
|
||||
const projectPath = tildeifyPath(getMemoryFilePath('project'));
|
||||
return `CHOOSE: ${globalPath} (global) OR ${projectPath} (project)`;
|
||||
}
|
||||
const scope = this.params.scope;
|
||||
const scope = this.params.scope || 'global';
|
||||
const memoryFilePath = getMemoryFilePath(scope);
|
||||
return `${tildeifyPath(memoryFilePath)} (${scope})`;
|
||||
}
|
||||
@@ -212,54 +207,26 @@ class MemoryToolInvocation extends BaseToolInvocation<
|
||||
override async shouldConfirmExecute(
|
||||
_abortSignal: AbortSignal,
|
||||
): Promise<ToolEditConfirmationDetails | false> {
|
||||
// When scope is not specified, show a choice dialog defaulting to global
|
||||
// If scope is not specified, prompt the user to choose
|
||||
if (!this.params.scope) {
|
||||
// Show preview of what would be added to global by default
|
||||
const defaultScope = 'global';
|
||||
const currentContent = await readMemoryFileContent(defaultScope);
|
||||
const newContent = computeNewContent(currentContent, this.params.fact);
|
||||
|
||||
const globalPath = tildeifyPath(getMemoryFilePath('global'));
|
||||
const projectPath = tildeifyPath(getMemoryFilePath('project'));
|
||||
|
||||
const fileName = path.basename(getMemoryFilePath(defaultScope));
|
||||
const choiceText = `Choose where to save this memory:
|
||||
|
||||
"${this.params.fact}"
|
||||
|
||||
Options:
|
||||
- Global: ${globalPath} (shared across all projects)
|
||||
- Project: ${projectPath} (current project only)
|
||||
|
||||
Preview of changes to be made to GLOBAL memory:
|
||||
`;
|
||||
const fileDiff =
|
||||
choiceText +
|
||||
Diff.createPatch(
|
||||
fileName,
|
||||
currentContent,
|
||||
newContent,
|
||||
'Current',
|
||||
'Proposed (Global)',
|
||||
DEFAULT_DIFF_OPTIONS,
|
||||
);
|
||||
|
||||
const confirmationDetails: ToolEditConfirmationDetails = {
|
||||
type: 'edit',
|
||||
title: `Choose Memory Location: GLOBAL (${globalPath}) or PROJECT (${projectPath})`,
|
||||
fileName,
|
||||
filePath: getMemoryFilePath(defaultScope),
|
||||
fileDiff,
|
||||
originalContent: `scope: global\n\n# INSTRUCTIONS:\n# - Click "Yes" to save to GLOBAL memory: ${globalPath}\n# - Click "Modify with external editor" and change "global" to "project" to save to PROJECT memory: ${projectPath}\n\n${currentContent}`,
|
||||
newContent: `scope: global\n\n# INSTRUCTIONS:\n# - Click "Yes" to save to GLOBAL memory: ${globalPath}\n# - Click "Modify with external editor" and change "global" to "project" to save to PROJECT memory: ${projectPath}\n\n${newContent}`,
|
||||
title: `Choose Memory Storage Location`,
|
||||
fileName: 'Memory Storage Options',
|
||||
filePath: '',
|
||||
fileDiff: `Choose where to save this memory:\n\n"${this.params.fact}"\n\nOptions:\n- Global: ${globalPath} (shared across all projects)\n- Project: ${projectPath} (current project only)\n\nPlease specify the scope parameter: "global" or "project"`,
|
||||
originalContent: '',
|
||||
newContent: `Memory to save: ${this.params.fact}\n\nScope options:\n- global: ${globalPath}\n- project: ${projectPath}`,
|
||||
onConfirm: async (_outcome: ToolConfirmationOutcome) => {
|
||||
// Will be handled in createUpdatedParams
|
||||
// This will be handled by the execution flow
|
||||
},
|
||||
};
|
||||
return confirmationDetails;
|
||||
}
|
||||
|
||||
// Only check allowlist when scope is specified
|
||||
const scope = this.params.scope;
|
||||
const memoryFilePath = getMemoryFilePath(scope);
|
||||
const allowlistKey = `${memoryFilePath}_${scope}`;
|
||||
@@ -312,25 +279,17 @@ Preview of changes to be made to GLOBAL memory:
|
||||
};
|
||||
}
|
||||
|
||||
// If scope is not specified and user didn't modify content, return error prompting for choice
|
||||
if (!this.params.scope && !modified_by_user) {
|
||||
const globalPath = tildeifyPath(getMemoryFilePath('global'));
|
||||
const projectPath = tildeifyPath(getMemoryFilePath('project'));
|
||||
const errorMessage = `Please specify where to save this memory:
|
||||
|
||||
Global: ${globalPath} (shared across all projects)
|
||||
Project: ${projectPath} (current project only)`;
|
||||
|
||||
// If scope is not specified, prompt the user to choose
|
||||
if (!this.params.scope) {
|
||||
const errorMessage =
|
||||
'Please specify where to save this memory. Use scope parameter: "global" for user-level (~/.qwen/QWEN.md) or "project" for current project (./QWEN.md).';
|
||||
return {
|
||||
llmContent: JSON.stringify({
|
||||
success: false,
|
||||
error: 'Please specify where to save this memory',
|
||||
}),
|
||||
returnDisplay: errorMessage,
|
||||
llmContent: JSON.stringify({ success: false, error: errorMessage }),
|
||||
returnDisplay: `${errorMessage}\n\nGlobal: ${tildeifyPath(getMemoryFilePath('global'))}\nProject: ${tildeifyPath(getMemoryFilePath('project'))}`,
|
||||
};
|
||||
}
|
||||
|
||||
const scope = this.params.scope || 'global';
|
||||
const scope = this.params.scope;
|
||||
const memoryFilePath = getMemoryFilePath(scope);
|
||||
|
||||
try {
|
||||
@@ -488,88 +447,24 @@ export class MemoryTool
|
||||
|
||||
getModifyContext(_abortSignal: AbortSignal): ModifyContext<SaveMemoryParams> {
|
||||
return {
|
||||
getFilePath: (params: SaveMemoryParams) => {
|
||||
// Determine scope from modified content or default
|
||||
let scope = params.scope || 'global';
|
||||
if (params.modified_content) {
|
||||
const scopeMatch = params.modified_content.match(
|
||||
/^scope:\s*(global|project)\s*\n/i,
|
||||
);
|
||||
if (scopeMatch) {
|
||||
scope = scopeMatch[1].toLowerCase() as 'global' | 'project';
|
||||
}
|
||||
}
|
||||
return getMemoryFilePath(scope);
|
||||
},
|
||||
getCurrentContent: async (params: SaveMemoryParams): Promise<string> => {
|
||||
// Check if content starts with scope directive
|
||||
if (params.modified_content) {
|
||||
const scopeMatch = params.modified_content.match(
|
||||
/^scope:\s*(global|project)\s*\n/i,
|
||||
);
|
||||
if (scopeMatch) {
|
||||
const scope = scopeMatch[1].toLowerCase() as 'global' | 'project';
|
||||
const content = await readMemoryFileContent(scope);
|
||||
const globalPath = tildeifyPath(getMemoryFilePath('global'));
|
||||
const projectPath = tildeifyPath(getMemoryFilePath('project'));
|
||||
return `scope: ${scope}\n\n# INSTRUCTIONS:\n# - Save as "global" for GLOBAL memory: ${globalPath}\n# - Save as "project" for PROJECT memory: ${projectPath}\n\n${content}`;
|
||||
}
|
||||
}
|
||||
const scope = params.scope || 'global';
|
||||
const content = await readMemoryFileContent(scope);
|
||||
const globalPath = tildeifyPath(getMemoryFilePath('global'));
|
||||
const projectPath = tildeifyPath(getMemoryFilePath('project'));
|
||||
return `scope: ${scope}\n\n# INSTRUCTIONS:\n# - Save as "global" for GLOBAL memory: ${globalPath}\n# - Save as "project" for PROJECT memory: ${projectPath}\n\n${content}`;
|
||||
},
|
||||
getFilePath: (params: SaveMemoryParams) =>
|
||||
getMemoryFilePath(params.scope || 'global'),
|
||||
getCurrentContent: async (params: SaveMemoryParams): Promise<string> =>
|
||||
readMemoryFileContent(params.scope || 'global'),
|
||||
getProposedContent: async (params: SaveMemoryParams): Promise<string> => {
|
||||
let scope = params.scope || 'global';
|
||||
|
||||
// Check if modified content has scope directive
|
||||
if (params.modified_content) {
|
||||
const scopeMatch = params.modified_content.match(
|
||||
/^scope:\s*(global|project)\s*\n/i,
|
||||
);
|
||||
if (scopeMatch) {
|
||||
scope = scopeMatch[1].toLowerCase() as 'global' | 'project';
|
||||
}
|
||||
}
|
||||
|
||||
const scope = params.scope || 'global';
|
||||
const currentContent = await readMemoryFileContent(scope);
|
||||
const newContent = computeNewContent(currentContent, params.fact);
|
||||
const globalPath = tildeifyPath(getMemoryFilePath('global'));
|
||||
const projectPath = tildeifyPath(getMemoryFilePath('project'));
|
||||
return `scope: ${scope}\n\n# INSTRUCTIONS:\n# - Save as "global" for GLOBAL memory: ${globalPath}\n# - Save as "project" for PROJECT memory: ${projectPath}\n\n${newContent}`;
|
||||
return computeNewContent(currentContent, params.fact);
|
||||
},
|
||||
createUpdatedParams: (
|
||||
_oldContent: string,
|
||||
modifiedProposedContent: string,
|
||||
originalParams: SaveMemoryParams,
|
||||
): SaveMemoryParams => {
|
||||
// Parse user's scope choice from modified content
|
||||
const scopeMatch = modifiedProposedContent.match(
|
||||
/^scope:\s*(global|project)/i,
|
||||
);
|
||||
const scope = scopeMatch
|
||||
? (scopeMatch[1].toLowerCase() as 'global' | 'project')
|
||||
: 'global';
|
||||
|
||||
// Strip out the scope directive and instruction lines, keep only the actual memory content
|
||||
const contentWithoutScope = modifiedProposedContent.replace(
|
||||
/^scope:\s*(global|project)\s*\n/,
|
||||
'',
|
||||
);
|
||||
const actualContent = contentWithoutScope
|
||||
.replace(/^#[^\n]*\n/gm, '')
|
||||
.replace(/^\s*\n/gm, '')
|
||||
.trim();
|
||||
|
||||
return {
|
||||
...originalParams,
|
||||
scope,
|
||||
modified_by_user: true,
|
||||
modified_content: actualContent,
|
||||
};
|
||||
},
|
||||
): SaveMemoryParams => ({
|
||||
...originalParams,
|
||||
modified_by_user: true,
|
||||
modified_content: modifiedProposedContent,
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import path from 'node:path';
|
||||
import os from 'os';
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
export const QWEN_DIR = '.qwen';
|
||||
export const GEMINI_DIR = '.qwen';
|
||||
export const GOOGLE_ACCOUNTS_FILENAME = 'google_accounts.json';
|
||||
const TMP_DIR_NAME = 'tmp';
|
||||
const COMMANDS_DIR_NAME = 'commands';
|
||||
@@ -181,7 +181,7 @@ export function getProjectHash(projectRoot: string): string {
|
||||
*/
|
||||
export function getProjectTempDir(projectRoot: string): string {
|
||||
const hash = getProjectHash(projectRoot);
|
||||
return path.join(os.homedir(), QWEN_DIR, TMP_DIR_NAME, hash);
|
||||
return path.join(os.homedir(), GEMINI_DIR, TMP_DIR_NAME, hash);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -189,7 +189,7 @@ export function getProjectTempDir(projectRoot: string): string {
|
||||
* @returns The path to the user's commands directory.
|
||||
*/
|
||||
export function getUserCommandsDir(): string {
|
||||
return path.join(os.homedir(), QWEN_DIR, COMMANDS_DIR_NAME);
|
||||
return path.join(os.homedir(), GEMINI_DIR, COMMANDS_DIR_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -198,5 +198,5 @@ export function getUserCommandsDir(): string {
|
||||
* @returns The path to the project's commands directory.
|
||||
*/
|
||||
export function getProjectCommandsDir(projectRoot: string): string {
|
||||
return path.join(projectRoot, QWEN_DIR, COMMANDS_DIR_NAME);
|
||||
return path.join(projectRoot, GEMINI_DIR, COMMANDS_DIR_NAME);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import path from 'node:path';
|
||||
import { promises as fsp, existsSync, readFileSync } from 'node:fs';
|
||||
import * as os from 'os';
|
||||
import { QWEN_DIR, GOOGLE_ACCOUNTS_FILENAME } from './paths.js';
|
||||
import { GEMINI_DIR, GOOGLE_ACCOUNTS_FILENAME } from './paths.js';
|
||||
|
||||
interface UserAccounts {
|
||||
active: string | null;
|
||||
@@ -15,7 +15,7 @@ interface UserAccounts {
|
||||
}
|
||||
|
||||
function getGoogleAccountsCachePath(): string {
|
||||
return path.join(os.homedir(), QWEN_DIR, GOOGLE_ACCOUNTS_FILENAME);
|
||||
return path.join(os.homedir(), GEMINI_DIR, GOOGLE_ACCOUNTS_FILENAME);
|
||||
}
|
||||
|
||||
async function readAccounts(filePath: string): Promise<UserAccounts> {
|
||||
|
||||
@@ -8,10 +8,10 @@ import * as os from 'os';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { QWEN_DIR } from './paths.js';
|
||||
import { GEMINI_DIR } from './paths.js';
|
||||
|
||||
const homeDir = os.homedir() ?? '';
|
||||
const geminiDir = path.join(homeDir, QWEN_DIR);
|
||||
const geminiDir = path.join(homeDir, GEMINI_DIR);
|
||||
const installationIdFile = path.join(geminiDir, 'installation_id');
|
||||
|
||||
function ensureGeminiDirExists() {
|
||||
|
||||
Reference in New Issue
Block a user