mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
Merge pull request #450 from QwenLM/feature/qwenmd
use sub-command to switch between project and global memory ops
This commit is contained in:
@@ -117,7 +117,7 @@ describe('memoryCommand', () => {
|
|||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
type: 'message',
|
type: 'message',
|
||||||
messageType: 'error',
|
messageType: 'error',
|
||||||
content: 'Usage: /memory add <text to remember>',
|
content: 'Usage: /memory add [--global|--project] <text to remember>',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockContext.ui.addItem).not.toHaveBeenCalled();
|
expect(mockContext.ui.addItem).not.toHaveBeenCalled();
|
||||||
@@ -132,7 +132,7 @@ describe('memoryCommand', () => {
|
|||||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||||||
{
|
{
|
||||||
type: MessageType.INFO,
|
type: MessageType.INFO,
|
||||||
text: `Attempting to save to memory: "${fact}"`,
|
text: `Attempting to save to memory : "${fact}"`,
|
||||||
},
|
},
|
||||||
expect.any(Number),
|
expect.any(Number),
|
||||||
);
|
);
|
||||||
@@ -143,6 +143,61 @@ describe('memoryCommand', () => {
|
|||||||
toolArgs: { fact },
|
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', () => {
|
describe('/memory refresh', () => {
|
||||||
@@ -173,7 +228,7 @@ describe('memoryCommand', () => {
|
|||||||
|
|
||||||
mockContext = createMockCommandContext({
|
mockContext = createMockCommandContext({
|
||||||
services: {
|
services: {
|
||||||
config: Promise.resolve(mockConfig),
|
config: mockConfig,
|
||||||
settings: {
|
settings: {
|
||||||
merged: {
|
merged: {
|
||||||
memoryDiscoveryMaxDirs: 1000,
|
memoryDiscoveryMaxDirs: 1000,
|
||||||
|
|||||||
@@ -7,7 +7,11 @@
|
|||||||
import {
|
import {
|
||||||
getErrorMessage,
|
getErrorMessage,
|
||||||
loadServerHierarchicalMemory,
|
loadServerHierarchicalMemory,
|
||||||
|
QWEN_DIR,
|
||||||
} from '@qwen-code/qwen-code-core';
|
} 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 { MessageType } from '../types.js';
|
||||||
import {
|
import {
|
||||||
CommandKind,
|
CommandKind,
|
||||||
@@ -41,24 +45,136 @@ export const memoryCommand: SlashCommand = {
|
|||||||
Date.now(),
|
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',
|
name: 'add',
|
||||||
description: 'Add content to the memory.',
|
description:
|
||||||
|
'Add content to the memory. Use --global for global memory or --project for project memory.',
|
||||||
kind: CommandKind.BUILT_IN,
|
kind: CommandKind.BUILT_IN,
|
||||||
action: (context, args): SlashCommandActionReturn | void => {
|
action: (context, args): SlashCommandActionReturn | void => {
|
||||||
if (!args || args.trim() === '') {
|
if (!args || args.trim() === '') {
|
||||||
return {
|
return {
|
||||||
type: 'message',
|
type: 'message',
|
||||||
messageType: 'error',
|
messageType: 'error',
|
||||||
content: 'Usage: /memory add <text to remember>',
|
content:
|
||||||
|
'Usage: /memory add [--global|--project] <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(
|
context.ui.addItem(
|
||||||
{
|
{
|
||||||
type: MessageType.INFO,
|
type: MessageType.INFO,
|
||||||
text: `Attempting to save to memory: "${args.trim()}"`,
|
text: `Attempting to save to memory ${scopeText}: "${fact}"`,
|
||||||
},
|
},
|
||||||
Date.now(),
|
Date.now(),
|
||||||
);
|
);
|
||||||
@@ -66,9 +182,67 @@ export const memoryCommand: SlashCommand = {
|
|||||||
return {
|
return {
|
||||||
type: 'tool',
|
type: 'tool',
|
||||||
toolName: 'save_memory',
|
toolName: 'save_memory',
|
||||||
toolArgs: { fact: args.trim() },
|
toolArgs: scope ? { fact, scope } : { fact },
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
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',
|
name: 'refresh',
|
||||||
@@ -84,7 +258,7 @@ export const memoryCommand: SlashCommand = {
|
|||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const config = await context.services.config;
|
const config = context.services.config;
|
||||||
if (config) {
|
if (config) {
|
||||||
const { memoryContent, fileCount } =
|
const { memoryContent, fileCount } =
|
||||||
await loadServerHierarchicalMemory(
|
await loadServerHierarchicalMemory(
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import * as path from 'path';
|
|||||||
import * as fs from 'fs/promises';
|
import * as fs from 'fs/promises';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import type { ChildProcess } from 'node:child_process';
|
import type { ChildProcess } from 'node:child_process';
|
||||||
import { getProjectHash, GEMINI_DIR } from '../utils/paths.js';
|
import { getProjectHash, QWEN_DIR } from '../utils/paths.js';
|
||||||
|
|
||||||
const hoistedMockExec = vi.hoisted(() => vi.fn());
|
const hoistedMockExec = vi.hoisted(() => vi.fn());
|
||||||
vi.mock('node:child_process', () => ({
|
vi.mock('node:child_process', () => ({
|
||||||
@@ -157,7 +157,7 @@ describe('GitService', () => {
|
|||||||
let gitConfigPath: string;
|
let gitConfigPath: string;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
repoDir = path.join(homedir, GEMINI_DIR, 'history', hash);
|
repoDir = path.join(homedir, QWEN_DIR, 'history', hash);
|
||||||
gitConfigPath = path.join(repoDir, '.gitconfig');
|
gitConfigPath = path.join(repoDir, '.gitconfig');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import * as os from 'os';
|
|||||||
import { isNodeError } from '../utils/errors.js';
|
import { isNodeError } from '../utils/errors.js';
|
||||||
import { exec } from 'node:child_process';
|
import { exec } from 'node:child_process';
|
||||||
import { simpleGit, SimpleGit, CheckRepoActions } from 'simple-git';
|
import { simpleGit, SimpleGit, CheckRepoActions } from 'simple-git';
|
||||||
import { getProjectHash, GEMINI_DIR } from '../utils/paths.js';
|
import { getProjectHash, QWEN_DIR } from '../utils/paths.js';
|
||||||
|
|
||||||
export class GitService {
|
export class GitService {
|
||||||
private projectRoot: string;
|
private projectRoot: string;
|
||||||
@@ -21,7 +21,7 @@ export class GitService {
|
|||||||
|
|
||||||
private getHistoryDir(): string {
|
private getHistoryDir(): string {
|
||||||
const hash = getProjectHash(this.projectRoot);
|
const hash = getProjectHash(this.projectRoot);
|
||||||
return path.join(os.homedir(), GEMINI_DIR, 'history', hash);
|
return path.join(os.homedir(), QWEN_DIR, 'history', hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
async initialize(): Promise<void> {
|
async initialize(): Promise<void> {
|
||||||
|
|||||||
@@ -522,13 +522,16 @@ describe('MemoryTool', () => {
|
|||||||
expect(result).not.toBe(false);
|
expect(result).not.toBe(false);
|
||||||
|
|
||||||
if (result && result.type === 'edit') {
|
if (result && result.type === 'edit') {
|
||||||
expect(result.title).toBe('Choose Memory Storage Location');
|
expect(result.title).toContain('Choose Memory Location');
|
||||||
expect(result.fileName).toBe('Memory Storage Options');
|
expect(result.title).toContain('GLOBAL');
|
||||||
expect(result.fileDiff).toContain('Choose where to save this memory');
|
expect(result.title).toContain('PROJECT');
|
||||||
|
expect(result.fileName).toBe('QWEN.md');
|
||||||
expect(result.fileDiff).toContain('Test fact');
|
expect(result.fileDiff).toContain('Test fact');
|
||||||
expect(result.fileDiff).toContain('Global:');
|
expect(result.fileDiff).toContain('--- QWEN.md');
|
||||||
expect(result.fileDiff).toContain('Project:');
|
expect(result.fileDiff).toContain('+++ QWEN.md');
|
||||||
expect(result.originalContent).toBe('');
|
expect(result.fileDiff).toContain('+- Test fact');
|
||||||
|
expect(result.originalContent).toContain('scope: global');
|
||||||
|
expect(result.originalContent).toContain('INSTRUCTIONS:');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -577,13 +580,16 @@ describe('MemoryTool', () => {
|
|||||||
expect(description).toBe(`${expectedPath} (project)`);
|
expect(description).toBe(`${expectedPath} (project)`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should default to global scope when scope is not specified', () => {
|
it('should show choice prompt when scope is not specified', () => {
|
||||||
const params = { fact: 'Test fact' };
|
const params = { fact: 'Test fact' };
|
||||||
const invocation = memoryTool.build(params);
|
const invocation = memoryTool.build(params);
|
||||||
const description = invocation.getDescription();
|
const description = invocation.getDescription();
|
||||||
|
|
||||||
const expectedPath = path.join('~', '.qwen', 'QWEN.md');
|
const globalPath = path.join('~', '.qwen', 'QWEN.md');
|
||||||
expect(description).toBe(`${expectedPath} (global)`);
|
const projectPath = path.join(process.cwd(), 'QWEN.md');
|
||||||
|
expect(description).toBe(
|
||||||
|
`CHOOSE: ${globalPath} (global) OR ${projectPath} (project)`,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -199,7 +199,12 @@ class MemoryToolInvocation extends BaseToolInvocation<
|
|||||||
private static readonly allowlist: Set<string> = new Set();
|
private static readonly allowlist: Set<string> = new Set();
|
||||||
|
|
||||||
getDescription(): string {
|
getDescription(): string {
|
||||||
const scope = this.params.scope || 'global';
|
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 memoryFilePath = getMemoryFilePath(scope);
|
const memoryFilePath = getMemoryFilePath(scope);
|
||||||
return `${tildeifyPath(memoryFilePath)} (${scope})`;
|
return `${tildeifyPath(memoryFilePath)} (${scope})`;
|
||||||
}
|
}
|
||||||
@@ -207,26 +212,54 @@ class MemoryToolInvocation extends BaseToolInvocation<
|
|||||||
override async shouldConfirmExecute(
|
override async shouldConfirmExecute(
|
||||||
_abortSignal: AbortSignal,
|
_abortSignal: AbortSignal,
|
||||||
): Promise<ToolEditConfirmationDetails | false> {
|
): Promise<ToolEditConfirmationDetails | false> {
|
||||||
// If scope is not specified, prompt the user to choose
|
// When scope is not specified, show a choice dialog defaulting to global
|
||||||
if (!this.params.scope) {
|
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 globalPath = tildeifyPath(getMemoryFilePath('global'));
|
||||||
const projectPath = tildeifyPath(getMemoryFilePath('project'));
|
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 = {
|
const confirmationDetails: ToolEditConfirmationDetails = {
|
||||||
type: 'edit',
|
type: 'edit',
|
||||||
title: `Choose Memory Storage Location`,
|
title: `Choose Memory Location: GLOBAL (${globalPath}) or PROJECT (${projectPath})`,
|
||||||
fileName: 'Memory Storage Options',
|
fileName,
|
||||||
filePath: '',
|
filePath: getMemoryFilePath(defaultScope),
|
||||||
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"`,
|
fileDiff,
|
||||||
originalContent: '',
|
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: `Memory to save: ${this.params.fact}\n\nScope options:\n- global: ${globalPath}\n- project: ${projectPath}`,
|
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}`,
|
||||||
onConfirm: async (_outcome: ToolConfirmationOutcome) => {
|
onConfirm: async (_outcome: ToolConfirmationOutcome) => {
|
||||||
// This will be handled by the execution flow
|
// Will be handled in createUpdatedParams
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return confirmationDetails;
|
return confirmationDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only check allowlist when scope is specified
|
||||||
const scope = this.params.scope;
|
const scope = this.params.scope;
|
||||||
const memoryFilePath = getMemoryFilePath(scope);
|
const memoryFilePath = getMemoryFilePath(scope);
|
||||||
const allowlistKey = `${memoryFilePath}_${scope}`;
|
const allowlistKey = `${memoryFilePath}_${scope}`;
|
||||||
@@ -279,17 +312,25 @@ class MemoryToolInvocation extends BaseToolInvocation<
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// If scope is not specified, prompt the user to choose
|
// If scope is not specified and user didn't modify content, return error prompting for choice
|
||||||
if (!this.params.scope) {
|
if (!this.params.scope && !modified_by_user) {
|
||||||
const errorMessage =
|
const globalPath = tildeifyPath(getMemoryFilePath('global'));
|
||||||
'Please specify where to save this memory. Use scope parameter: "global" for user-level (~/.qwen/QWEN.md) or "project" for current project (./QWEN.md).';
|
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)`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
llmContent: JSON.stringify({ success: false, error: errorMessage }),
|
llmContent: JSON.stringify({
|
||||||
returnDisplay: `${errorMessage}\n\nGlobal: ${tildeifyPath(getMemoryFilePath('global'))}\nProject: ${tildeifyPath(getMemoryFilePath('project'))}`,
|
success: false,
|
||||||
|
error: 'Please specify where to save this memory',
|
||||||
|
}),
|
||||||
|
returnDisplay: errorMessage,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const scope = this.params.scope;
|
const scope = this.params.scope || 'global';
|
||||||
const memoryFilePath = getMemoryFilePath(scope);
|
const memoryFilePath = getMemoryFilePath(scope);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -447,24 +488,88 @@ export class MemoryTool
|
|||||||
|
|
||||||
getModifyContext(_abortSignal: AbortSignal): ModifyContext<SaveMemoryParams> {
|
getModifyContext(_abortSignal: AbortSignal): ModifyContext<SaveMemoryParams> {
|
||||||
return {
|
return {
|
||||||
getFilePath: (params: SaveMemoryParams) =>
|
getFilePath: (params: SaveMemoryParams) => {
|
||||||
getMemoryFilePath(params.scope || 'global'),
|
// Determine scope from modified content or default
|
||||||
getCurrentContent: async (params: SaveMemoryParams): Promise<string> =>
|
let scope = params.scope || 'global';
|
||||||
readMemoryFileContent(params.scope || 'global'),
|
if (params.modified_content) {
|
||||||
getProposedContent: async (params: SaveMemoryParams): Promise<string> => {
|
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 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}`;
|
||||||
|
},
|
||||||
|
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 currentContent = await readMemoryFileContent(scope);
|
const currentContent = await readMemoryFileContent(scope);
|
||||||
return computeNewContent(currentContent, params.fact);
|
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}`;
|
||||||
},
|
},
|
||||||
createUpdatedParams: (
|
createUpdatedParams: (
|
||||||
_oldContent: string,
|
_oldContent: string,
|
||||||
modifiedProposedContent: string,
|
modifiedProposedContent: string,
|
||||||
originalParams: SaveMemoryParams,
|
originalParams: SaveMemoryParams,
|
||||||
): 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,
|
...originalParams,
|
||||||
|
scope,
|
||||||
modified_by_user: true,
|
modified_by_user: true,
|
||||||
modified_content: modifiedProposedContent,
|
modified_content: actualContent,
|
||||||
}),
|
};
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import path from 'node:path';
|
|||||||
import os from 'os';
|
import os from 'os';
|
||||||
import * as crypto from 'crypto';
|
import * as crypto from 'crypto';
|
||||||
|
|
||||||
export const GEMINI_DIR = '.qwen';
|
export const QWEN_DIR = '.qwen';
|
||||||
export const GOOGLE_ACCOUNTS_FILENAME = 'google_accounts.json';
|
export const GOOGLE_ACCOUNTS_FILENAME = 'google_accounts.json';
|
||||||
const TMP_DIR_NAME = 'tmp';
|
const TMP_DIR_NAME = 'tmp';
|
||||||
const COMMANDS_DIR_NAME = 'commands';
|
const COMMANDS_DIR_NAME = 'commands';
|
||||||
@@ -181,7 +181,7 @@ export function getProjectHash(projectRoot: string): string {
|
|||||||
*/
|
*/
|
||||||
export function getProjectTempDir(projectRoot: string): string {
|
export function getProjectTempDir(projectRoot: string): string {
|
||||||
const hash = getProjectHash(projectRoot);
|
const hash = getProjectHash(projectRoot);
|
||||||
return path.join(os.homedir(), GEMINI_DIR, TMP_DIR_NAME, hash);
|
return path.join(os.homedir(), QWEN_DIR, TMP_DIR_NAME, hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -189,7 +189,7 @@ export function getProjectTempDir(projectRoot: string): string {
|
|||||||
* @returns The path to the user's commands directory.
|
* @returns The path to the user's commands directory.
|
||||||
*/
|
*/
|
||||||
export function getUserCommandsDir(): string {
|
export function getUserCommandsDir(): string {
|
||||||
return path.join(os.homedir(), GEMINI_DIR, COMMANDS_DIR_NAME);
|
return path.join(os.homedir(), QWEN_DIR, COMMANDS_DIR_NAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -198,5 +198,5 @@ export function getUserCommandsDir(): string {
|
|||||||
* @returns The path to the project's commands directory.
|
* @returns The path to the project's commands directory.
|
||||||
*/
|
*/
|
||||||
export function getProjectCommandsDir(projectRoot: string): string {
|
export function getProjectCommandsDir(projectRoot: string): string {
|
||||||
return path.join(projectRoot, GEMINI_DIR, COMMANDS_DIR_NAME);
|
return path.join(projectRoot, QWEN_DIR, COMMANDS_DIR_NAME);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { promises as fsp, existsSync, readFileSync } from 'node:fs';
|
import { promises as fsp, existsSync, readFileSync } from 'node:fs';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import { GEMINI_DIR, GOOGLE_ACCOUNTS_FILENAME } from './paths.js';
|
import { QWEN_DIR, GOOGLE_ACCOUNTS_FILENAME } from './paths.js';
|
||||||
|
|
||||||
interface UserAccounts {
|
interface UserAccounts {
|
||||||
active: string | null;
|
active: string | null;
|
||||||
@@ -15,7 +15,7 @@ interface UserAccounts {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getGoogleAccountsCachePath(): string {
|
function getGoogleAccountsCachePath(): string {
|
||||||
return path.join(os.homedir(), GEMINI_DIR, GOOGLE_ACCOUNTS_FILENAME);
|
return path.join(os.homedir(), QWEN_DIR, GOOGLE_ACCOUNTS_FILENAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readAccounts(filePath: string): Promise<UserAccounts> {
|
async function readAccounts(filePath: string): Promise<UserAccounts> {
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ import * as os from 'os';
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { randomUUID } from 'crypto';
|
import { randomUUID } from 'crypto';
|
||||||
import { GEMINI_DIR } from './paths.js';
|
import { QWEN_DIR } from './paths.js';
|
||||||
|
|
||||||
const homeDir = os.homedir() ?? '';
|
const homeDir = os.homedir() ?? '';
|
||||||
const geminiDir = path.join(homeDir, GEMINI_DIR);
|
const geminiDir = path.join(homeDir, QWEN_DIR);
|
||||||
const installationIdFile = path.join(geminiDir, 'installation_id');
|
const installationIdFile = path.join(geminiDir, 'installation_id');
|
||||||
|
|
||||||
function ensureGeminiDirExists() {
|
function ensureGeminiDirExists() {
|
||||||
|
|||||||
Reference in New Issue
Block a user