Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 58 additions & 3 deletions packages/cli/src/ui/commands/memoryCommand.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ describe('memoryCommand', () => {
expect(result).toEqual({
type: 'message',
messageType: 'error',
content: 'Usage: /memory add <text to remember>',
content: 'Usage: /memory add [--global|--project] <text to remember>',
});

expect(mockContext.ui.addItem).not.toHaveBeenCalled();
Expand All @@ -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),
);
Expand All @@ -143,6 +143,61 @@ 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', () => {
Expand Down Expand Up @@ -173,7 +228,7 @@ describe('memoryCommand', () => {

mockContext = createMockCommandContext({
services: {
config: Promise.resolve(mockConfig),
config: mockConfig,
settings: {
merged: {
memoryDiscoveryMaxDirs: 1000,
Expand Down
184 changes: 179 additions & 5 deletions packages/cli/src/ui/commands/memoryCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@
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,
Expand Down Expand Up @@ -41,34 +45,204 @@ 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.',
description:
'Add content to the memory. Use --global for global memory or --project for project memory.',
kind: CommandKind.BUILT_IN,
action: (context, args): SlashCommandActionReturn | void => {
if (!args || args.trim() === '') {
return {
type: 'message',
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(
{
type: MessageType.INFO,
text: `Attempting to save to memory: "${args.trim()}"`,
text: `Attempting to save to memory ${scopeText}: "${fact}"`,
},
Date.now(),
);

return {
type: 'tool',
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',
Expand All @@ -84,7 +258,7 @@ export const memoryCommand: SlashCommand = {
);

try {
const config = await context.services.config;
const config = context.services.config;
if (config) {
const { memoryContent, fileCount } =
await loadServerHierarchicalMemory(
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/services/gitService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, GEMINI_DIR } from '../utils/paths.js';
import { getProjectHash, QWEN_DIR } from '../utils/paths.js';

const hoistedMockExec = vi.hoisted(() => vi.fn());
vi.mock('node:child_process', () => ({
Expand Down Expand Up @@ -157,7 +157,7 @@ describe('GitService', () => {
let gitConfigPath: string;

beforeEach(() => {
repoDir = path.join(homedir, GEMINI_DIR, 'history', hash);
repoDir = path.join(homedir, QWEN_DIR, 'history', hash);
gitConfigPath = path.join(repoDir, '.gitconfig');
});

Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/services/gitService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, GEMINI_DIR } from '../utils/paths.js';
import { getProjectHash, QWEN_DIR } from '../utils/paths.js';

export class GitService {
private projectRoot: string;
Expand All @@ -21,7 +21,7 @@ export class GitService {

private getHistoryDir(): string {
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> {
Expand Down
24 changes: 15 additions & 9 deletions packages/core/src/tools/memoryTool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -522,13 +522,16 @@ describe('MemoryTool', () => {
expect(result).not.toBe(false);

if (result && result.type === 'edit') {
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.title).toContain('Choose Memory Location');
expect(result.title).toContain('GLOBAL');
expect(result.title).toContain('PROJECT');
expect(result.fileName).toBe('QWEN.md');
expect(result.fileDiff).toContain('Test fact');
expect(result.fileDiff).toContain('Global:');
expect(result.fileDiff).toContain('Project:');
expect(result.originalContent).toBe('');
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:');
}
});

Expand Down Expand Up @@ -577,13 +580,16 @@ describe('MemoryTool', () => {
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 invocation = memoryTool.build(params);
const description = invocation.getDescription();

const expectedPath = path.join('~', '.qwen', 'QWEN.md');
expect(description).toBe(`${expectedPath} (global)`);
const globalPath = path.join('~', '.qwen', 'QWEN.md');
const projectPath = path.join(process.cwd(), 'QWEN.md');
expect(description).toBe(
`CHOOSE: ${globalPath} (global) OR ${projectPath} (project)`,
);
});
});
});
Loading
Loading