diff --git a/docs/ai/design/feature-memory-db-path-config.md b/docs/ai/design/feature-memory-db-path-config.md new file mode 100644 index 00000000..499fcd8a --- /dev/null +++ b/docs/ai/design/feature-memory-db-path-config.md @@ -0,0 +1,81 @@ +--- +phase: design +title: System Design & Architecture +description: Resolve memory database path from project config with default fallback +--- + +# System Design & Architecture + +## Architecture Overview +```mermaid +graph TD + ProjectConfig[./.ai-devkit.json] --> ConfigManager + ConfigManager --> Resolver[CLI memory path resolver] + CLI[ai-devkit memory *] --> Resolver + Resolver --> MemoryAPI[@ai-devkit/memory API] + DefaultPath[~/.ai-devkit/memory.db] --> MemoryAPI + MemoryAPI --> DatabasePath[Effective DB path] + DatabasePath --> SQLite[(SQLite memory.db)] +``` + +- Keep `DEFAULT_DB_PATH` as the baseline fallback in the memory package. +- Add a CLI-owned resolver that can return a project override from `.ai-devkit.json`. +- Pass the resolved path from the CLI into `@ai-devkit/memory` before opening SQLite. +- Leave the standalone `@ai-devkit/memory` MCP server unchanged so it continues using `DEFAULT_DB_PATH`. + +## Data Models +- Extend `DevKitConfig` with an optional memory section: + ```ts + interface DevKitConfig { + memory?: { + path?: string; + }; + } + ``` +- Extend CLI-to-memory command options with optional `dbPath?: string`. +- Resolver output is either: + - an absolute filesystem path derived from `memory.path` + - `undefined`, which means the memory package falls back to `DEFAULT_DB_PATH` + +## API Design +- Add `ConfigManager.getMemoryDbPath(): Promise` to read project config safely. +- Resolve `memory.path` inside the CLI command layer, not inside `packages/memory`. +- Add optional `dbPath` support to the memory package command APIs used by the CLI: + - `memoryStoreCommand(options)` + - `memorySearchCommand(options)` + - `memoryUpdateCommand(options)` +- Keep `packages/memory/src/server.ts` and the `ai-devkit-memory` binary unchanged in this feature. + +## Component Breakdown +- `packages/cli/src/types.ts` + - Add optional `memory.path` typing to project config. +- `packages/cli/src/lib/Config.ts` + - Parse and validate `memory.path` from project config. + - Resolve relative paths against the config file directory. +- `packages/cli/src/commands/memory.ts` + - Load the project config once per command invocation. + - Pass resolved `dbPath` into the imported memory command API. +- `packages/memory/src/database/connection.ts` + - Continue exposing `DEFAULT_DB_PATH`. + - Accept explicit `dbPath` from callers without changing fallback semantics. +- `packages/memory/src/api.ts` + - Accept optional `dbPath` on CLI-facing command option types. + - Call `getDatabase({ dbPath })` so the explicit path only affects CLI-triggered operations that pass it. +- Tests + - Config parsing tests in CLI package. + - Memory command resolution tests in CLI and memory package. + - No standalone MCP server behavior change tests are needed beyond regression confidence. + +## Design Decisions +- Keep fallback logic in the memory package and config parsing in the CLI package to preserve clear responsibilities. +- Resolve relative paths from the project root so checked-in config behaves consistently across shells and CI. +- Treat missing, blank, or non-string `memory.path` as unset and fall back silently to `DEFAULT_DB_PATH` to preserve backward compatibility. +- Keep the package boundary intact by passing `dbPath` explicitly from the CLI rather than making `packages/memory` depend on `ConfigManager`. +- Apply the configured path to `store`, `search`, and `update` so CLI memory subcommands stay consistent. + +## Non-Functional Requirements +- No change to default behavior for projects without `memory.path`. +- No additional network or external service dependency. +- Path resolution must be deterministic across macOS and Linux path semantics already supported by Node's `path` module. +- Database initialization and schema migration behavior remain unchanged once the final path is selected. +- Standalone `@ai-devkit/memory` server startup and runtime behavior remain unchanged in this feature. diff --git a/docs/ai/implementation/feature-memory-db-path-config.md b/docs/ai/implementation/feature-memory-db-path-config.md new file mode 100644 index 00000000..6eac4d89 --- /dev/null +++ b/docs/ai/implementation/feature-memory-db-path-config.md @@ -0,0 +1,40 @@ +--- +phase: implementation +title: Implementation Guide +description: Implementation notes for project-configurable memory database paths +--- + +# Implementation Guide + +## Development Setup +- Use the feature worktree `feature-memory-db-path-config`. +- Install dependencies with `npm ci` from the worktree root. + +## Code Structure +- Config shape and parsing live in the CLI package. +- Effective database path selection is resolved in the CLI and passed explicitly into the memory package. + +## Implementation Notes +### Core Features +- Add typed support for `memory.path` in project config. +- Resolve relative configured paths from the project root. +- Pass the resolved path into `ai-devkit memory store`, `search`, and `update`. + +### Patterns & Best Practices +- Keep `DEFAULT_DB_PATH` as the fallback constant. +- Avoid duplicating path-resolution logic across CLI command handlers. + +## Integration Points +- `.ai-devkit.json` +- `ConfigManager` +- memory CLI command adapters + +## Error Handling +- Invalid or absent `memory.path` should not break memory commands; fall back to the default path. + +## Performance Considerations +- Path resolution should happen once per CLI command invocation before opening the database. + +## Security Notes +- Treat `memory.path` as a filesystem path only; no shell execution or interpolation. +- Standalone `@ai-devkit/memory` server behavior remains unchanged in this feature. diff --git a/docs/ai/planning/feature-memory-db-path-config.md b/docs/ai/planning/feature-memory-db-path-config.md new file mode 100644 index 00000000..49aeab68 --- /dev/null +++ b/docs/ai/planning/feature-memory-db-path-config.md @@ -0,0 +1,58 @@ +--- +phase: planning +title: Project Planning & Task Breakdown +description: Task breakdown for project-configurable memory database paths +--- + +# Project Planning & Task Breakdown + +## Milestones +- [x] Milestone 1: Project config schema and parsing support `memory.path` +- [x] Milestone 2: CLI memory command flows consume resolved database path +- [x] Milestone 3: Tests cover configured path and fallback behavior + +## Task Breakdown + +### Phase 1: Config Support +- [x] Task 1.1: Extend `packages/cli/src/types.ts` to type optional `memory.path` +- [x] Task 1.2: Add `ConfigManager.getMemoryDbPath()` in `packages/cli/src/lib/Config.ts` +- [x] Task 1.3: Add unit tests for project config parsing, including missing, invalid, absolute, and relative path cases + +### Phase 2: Memory Path Wiring +- [x] Task 2.1: Introduce a CLI-owned path-resolution flow that combines project config override with `DEFAULT_DB_PATH` +- [x] Task 2.2: Update `packages/memory/src/api.ts` and CLI entry points so store/search/update use the resolved path consistently +- [x] Task 2.3: Removed from scope during Phase 2 review. Standalone `@ai-devkit/memory` MCP server remains unchanged for this feature. + +### Phase 3: Verification +- [x] Task 3.1: Add or update CLI tests covering memory commands with configured `memory.path` +- [x] Task 3.2: Add or update memory package tests covering explicit `dbPath` wiring and configured-path persistence +- [x] Task 3.3: Run targeted verification for docs lint and relevant automated tests + +## Dependencies +- Task 1.1 precedes Task 1.2 because config typing should match the new parser surface. +- Task 1.2 precedes Task 2.1 and Task 2.2 because runtime resolution depends on the config accessor. +- Task 2.1 should land before Task 2.2 so CLI and memory API use one resolution rule. +- Verification tasks depend on both config support and runtime wiring being complete. + +## Timeline & Estimates +- Phase 1: Small, low-risk change +- Phase 2: Medium effort because path selection currently sits below config loading boundaries +- Phase 3: Small to medium effort depending on current test coverage for memory command setup + +## Risks & Mitigation +- Risk: CLI commands honor config but the standalone MCP server still uses the default database. + Mitigation: This is intentional and documented as out of scope for the feature. +- Risk: Relative paths resolve from process cwd instead of project root. + Mitigation: Resolve from the config file directory and add explicit unit tests. +- Risk: Invalid config values break existing users. + Mitigation: Treat invalid values as unset and retain `DEFAULT_DB_PATH`. + +## Resources Needed +- Existing config loading utilities in `packages/cli/src/lib/Config.ts` +- Existing database connection behavior in `packages/memory/src/database/connection.ts` +- Existing memory command tests in `packages/cli/src/__tests__/commands/memory.test.ts` + +## Progress Summary +- Completed implementation for project-configured `memory.path` in `ai-devkit` CLI flows. +- Preserved standalone `@ai-devkit/memory` behavior as approved during requirements review. +- Verified with targeted CLI tests, memory integration tests, feature doc lint, package builds, and a real built-CLI store/search run against a temporary project config. diff --git a/docs/ai/requirements/feature-memory-db-path-config.md b/docs/ai/requirements/feature-memory-db-path-config.md new file mode 100644 index 00000000..2f0be191 --- /dev/null +++ b/docs/ai/requirements/feature-memory-db-path-config.md @@ -0,0 +1,51 @@ +--- +phase: requirements +title: Requirements & Problem Understanding +description: Allow project config to override the default memory database path +--- + +# Requirements & Problem Understanding + +## Problem Statement +- Memory storage currently defaults to `~/.ai-devkit/memory.db` via `packages/memory/src/database/connection.ts`. +- Projects cannot pin memory storage to a repo-specific database, so contributors working in the same repository may read and write different global state. +- Users who want isolated project memory have no supported configuration path in `.ai-devkit.json`. + +## Goals & Objectives +- Allow project `.ai-devkit.json` to define `memory.path`. +- Keep the existing default database path as `~/.ai-devkit/memory.db` when no project override is configured. +- Make memory commands use the configured project path consistently for store, search, and update flows. +- Scope the change to `ai-devkit` project commands and avoid changing standalone `@ai-devkit/memory` package behavior. + +## Non-Goals +- Adding a global `memory.path` override in `~/.ai-devkit/.ai-devkit.json`. +- Redesigning unrelated `.ai-devkit.json` sections. +- Changing the default database filename or directory. +- Making standalone `@ai-devkit/memory` automatically read project `.ai-devkit.json`. + +## User Stories & Use Cases +- As a project maintainer, I can commit `"memory": { "path": ".ai-devkit/project-memory.db" }` to `.ai-devkit.json` so everyone on the repository uses the same project-local memory database. +- As a developer without project memory config, I continue using `~/.ai-devkit/memory.db` with no behavior change. +- As a user running `ai-devkit memory search`, `store`, or `update` inside a configured project, I want all commands to resolve the same configured database path automatically. +- As a user running the standalone `@ai-devkit/memory` MCP server directly, I continue using its package default path unless a later feature adds separate config support. + +## Success Criteria +- Project `.ai-devkit.json` accepts a `memory.path` string. +- When `memory.path` is present in project config, `ai-devkit memory` operations use that path instead of `~/.ai-devkit/memory.db`. +- When `memory.path` is absent, empty, or invalid, memory operations fall back to `~/.ai-devkit/memory.db`. +- Relative configured paths are resolved deterministically from the project root containing `.ai-devkit.json`. +- Tests cover configured path resolution and default fallback behavior. +- Standalone `@ai-devkit/memory` behavior remains unchanged in this feature. + +## Constraints & Assumptions +- `ConfigManager` already owns project `.ai-devkit.json` loading in `packages/cli/src/lib/Config.ts`. +- The current memory package hard-codes the default path in `packages/memory/src/database/connection.ts`; implementation must preserve that default for non-project-aware callers. +- The project config shape currently allows additive extension without a broader schema migration. +- The configured path should remain a plain filesystem path string; no environment-variable expansion is required unless existing config code already supports it. +- `packages/memory` should not gain a dependency on the CLI package just for this feature. + +## Questions & Open Items +- The config key will be `memory.path` in project `.ai-devkit.json`. +- Relative paths will be interpreted relative to the directory containing `.ai-devkit.json`, not the shell's current working directory. +- Scope decision: only `ai-devkit` project commands will honor `memory.path`; standalone `@ai-devkit/memory` is out of scope. +- No additional blocking questions remain for implementation. diff --git a/docs/ai/testing/feature-memory-db-path-config.md b/docs/ai/testing/feature-memory-db-path-config.md new file mode 100644 index 00000000..903b6ced --- /dev/null +++ b/docs/ai/testing/feature-memory-db-path-config.md @@ -0,0 +1,61 @@ +--- +phase: testing +title: Testing Strategy +description: Testing plan for project-configurable memory database paths +--- + +# Testing Strategy + +## Test Coverage Goals +- Cover 100% of new and changed code related to config parsing and path resolution. +- Verify both configured-path and default-path flows. +- Keep standalone `@ai-devkit/memory` server behavior unchanged. + +## Unit Tests +### Config parsing +- [x] Reads `memory.path` when it is a non-empty string +- [x] Ignores missing, blank, and non-string `memory.path` +- [x] Resolves relative `memory.path` from the project config directory + Implemented in `packages/cli/src/__tests__/lib/Config.test.ts` + +### Memory command resolution +- [x] `memory store` uses configured path when project config exists +- [x] `memory search` uses configured path when project config exists +- [x] `memory update` uses configured path when project config exists +- [x] Commands fall back to `~/.ai-devkit/memory.db` when no project override exists + Verified by default `dbPath: undefined` expectations and configured-path expectations in `packages/cli/src/__tests__/commands/memory.test.ts` + +## Integration Tests +- [x] Schema initialization succeeds when the configured path points to a new file +- [x] Memory API store/search/update calls use an explicit configured `dbPath` + Implemented in `packages/memory/tests/integration/api.test.ts` +- [x] Standalone memory MCP server remains out of scope and unchanged + Covered by design/requirements scope, not by new behavior tests + +## End-to-End Tests +- [x] Automated CLI e2e test uses a temp-project `.ai-devkit.json` with repo-local `memory.path` + Implemented in `e2e/cli.e2e.ts` +- [x] Manual smoke test with a checked-in `.ai-devkit.json` using a repo-local memory DB + Verified via built CLI store/search run in a temporary project directory with `.ai-devkit.json` pointing to `.ai-devkit/project-memory.db` + +## Test Data +- Temporary project directories with generated `.ai-devkit.json` +- Temporary database file paths for isolated runs + +## Test Reporting & Coverage +- Ran `npm test -- --runInBand Config.test.ts memory.test.ts` in `packages/cli` +- Ran `npm test -- --runInBand tests/integration/api.test.ts` in `packages/memory` +- Ran `npm run test:e2e -- cli.e2e.ts` +- Ran `npx ai-devkit@latest lint --feature memory-db-path-config` +- Did not run a full coverage report command in this phase; targeted suites were used for feature verification + +## Manual Testing +- Confirmed a repo-local configured DB file is created on first write +- Confirmed built CLI search reads back from the configured repo-local DB +- Default fallback is covered by unit tests rather than a separate manual run + +## Performance Testing +- No dedicated performance testing required beyond regression confidence + +## Bug Tracking +- Watch for regressions where one CLI memory subcommand omits `dbPath` and reverts to `~/.ai-devkit/memory.db` diff --git a/e2e/cli.e2e.ts b/e2e/cli.e2e.ts index 8b68fd7a..e86355d2 100644 --- a/e2e/cli.e2e.ts +++ b/e2e/cli.e2e.ts @@ -148,10 +148,22 @@ describe('lint command', () => { describe('memory commands', () => { let projectDir: string; let uid: string; + let projectMemoryDbPath: string; beforeEach(() => { projectDir = createTempProject(); uid = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; + projectMemoryDbPath = join(projectDir, '.ai-devkit', 'memory.db'); + writeConfigFile(projectDir, { + version: '1.0.0', + environments: [], + phases: [], + memory: { + path: '.ai-devkit/memory.db' + }, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString() + }); }); afterEach(() => { @@ -168,6 +180,7 @@ describe('memory commands', () => { const stored = JSON.parse(storeResult.stdout.trim()); expect(stored.success).toBe(true); expect(stored.id).toBeDefined(); + expect(existsSync(projectMemoryDbPath)).toBe(true); const searchResult = run(`memory search -q "${title}"`, { cwd: projectDir }); expect(searchResult.exitCode).toBe(0); diff --git a/packages/cli/src/__tests__/commands/memory.test.ts b/packages/cli/src/__tests__/commands/memory.test.ts index aac1187a..f028ab4e 100644 --- a/packages/cli/src/__tests__/commands/memory.test.ts +++ b/packages/cli/src/__tests__/commands/memory.test.ts @@ -4,10 +4,19 @@ import { registerMemoryCommand } from '../../commands/memory'; import { memorySearchCommand, memoryStoreCommand, memoryUpdateCommand } from '@ai-devkit/memory'; import { ui } from '../../util/terminal-ui'; +const mockGetMemoryDbPath = jest.fn<() => Promise>(); +const mockConfigManager = { + getMemoryDbPath: mockGetMemoryDbPath +}; + jest.mock('@ai-devkit/memory', () => ({ memoryStoreCommand: jest.fn(), memorySearchCommand: jest.fn(), memoryUpdateCommand: jest.fn() +}), { virtual: true }); + +jest.mock('../../lib/Config', () => ({ + ConfigManager: jest.fn(() => mockConfigManager) })); jest.mock('../../util/terminal-ui', () => ({ @@ -27,6 +36,7 @@ describe('memory command', () => { beforeEach(() => { jest.clearAllMocks(); + mockGetMemoryDbPath.mockResolvedValue(undefined); consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => undefined); }); @@ -51,10 +61,42 @@ describe('memory command', () => { 'This is a valid content body long enough to satisfy constraints.' ]); - expect(mockedMemoryStoreCommand).toHaveBeenCalled(); + expect(mockedMemoryStoreCommand).toHaveBeenCalledWith({ + title: 'A valid title 123', + content: 'This is a valid content body long enough to satisfy constraints.', + tags: undefined, + scope: 'global', + dbPath: undefined + }); expect(consoleLogSpy).toHaveBeenCalledWith(JSON.stringify(result, null, 2)); }); + it('passes resolved project dbPath to memory store', async () => { + mockGetMemoryDbPath.mockResolvedValue('/repo/.ai-devkit/project-memory.db'); + mockedMemoryStoreCommand.mockReturnValue({ + success: true, + id: 'mem-1', + message: 'stored' + }); + + const program = new Command(); + registerMemoryCommand(program); + await program.parseAsync([ + 'node', + 'test', + 'memory', + 'store', + '--title', + 'A valid title 123', + '--content', + 'This is a valid content body long enough to satisfy constraints.' + ]); + + expect(mockedMemoryStoreCommand).toHaveBeenCalledWith(expect.objectContaining({ + dbPath: '/repo/.ai-devkit/project-memory.db' + })); + }); + it('handles store errors by showing error and exiting', async () => { mockedMemoryStoreCommand.mockImplementation(() => { throw new Error('store failed'); @@ -104,7 +146,14 @@ describe('memory command', () => { 'Updated title for testing', ]); - expect(mockedMemoryUpdateCommand).toHaveBeenCalled(); + expect(mockedMemoryUpdateCommand).toHaveBeenCalledWith({ + id: 'mem-1', + title: 'Updated title for testing', + content: undefined, + tags: undefined, + scope: undefined, + dbPath: undefined + }); expect(consoleLogSpy).toHaveBeenCalledWith(JSON.stringify(result, null, 2)); }); @@ -161,7 +210,8 @@ describe('memory command', () => { query: 'dto', tags: undefined, scope: undefined, - limit: 5 + limit: 5, + dbPath: undefined }); expect(consoleLogSpy).toHaveBeenCalledWith(JSON.stringify(result, null, 2)); expect(mockedUi.table).not.toHaveBeenCalled(); @@ -191,7 +241,8 @@ describe('memory command', () => { query: 'memory', tags: undefined, scope: undefined, - limit: 3 + limit: 3, + dbPath: undefined }); expect(mockedUi.table).toHaveBeenCalledWith({ headers: ['id', 'title', 'scope'], @@ -200,6 +251,36 @@ describe('memory command', () => { expect(consoleLogSpy).not.toHaveBeenCalledWith(expect.stringContaining('"results"')); }); + it('passes resolved project dbPath to memory search and update', async () => { + mockGetMemoryDbPath.mockResolvedValue('/repo/.ai-devkit/project-memory.db'); + mockedMemorySearchCommand.mockReturnValue({ + results: [], + totalMatches: 0, + query: 'dto' + }); + mockedMemoryUpdateCommand.mockReturnValue({ + success: true, + id: 'mem-1', + message: 'Knowledge updated successfully' + }); + + let program = new Command(); + registerMemoryCommand(program); + await program.parseAsync(['node', 'test', 'memory', 'search', '--query', 'dto']); + + expect(mockedMemorySearchCommand).toHaveBeenCalledWith(expect.objectContaining({ + dbPath: '/repo/.ai-devkit/project-memory.db' + })); + + program = new Command(); + registerMemoryCommand(program); + await program.parseAsync(['node', 'test', 'memory', 'update', '--id', 'mem-1', '--title', 'Updated title for testing']); + + expect(mockedMemoryUpdateCommand).toHaveBeenCalledWith(expect.objectContaining({ + dbPath: '/repo/.ai-devkit/project-memory.db' + })); + }); + it('shows a warning when --table has no matching results', async () => { mockedMemorySearchCommand.mockReturnValue({ results: [], diff --git a/packages/cli/src/__tests__/lib/Config.test.ts b/packages/cli/src/__tests__/lib/Config.test.ts index 08ede59e..ff3d38cb 100644 --- a/packages/cli/src/__tests__/lib/Config.test.ts +++ b/packages/cli/src/__tests__/lib/Config.test.ts @@ -19,6 +19,9 @@ describe('ConfigManager', () => { mockFs = fs as jest.Mocked; mockPath = path as jest.Mocked; mockPath.join.mockImplementation((...args) => args.join('/')); + mockPath.dirname.mockImplementation((input: string) => input.split('/').slice(0, -1).join('/') || '/'); + mockPath.resolve.mockImplementation((...args) => args.join('/').replace(/\/+/g, '/')); + mockPath.isAbsolute.mockImplementation((input: string) => input.startsWith('/')); }); afterEach(() => { @@ -532,4 +535,89 @@ describe('ConfigManager', () => { expect(registries).toEqual({}); }); }); + + describe('getMemoryDbPath', () => { + it('returns undefined when config does not exist', async () => { + (mockFs.pathExists as any).mockResolvedValue(false); + + const result = await configManager.getMemoryDbPath(); + + expect(result).toBeUndefined(); + }); + + it('returns undefined when memory.path is missing', async () => { + (mockFs.pathExists as any).mockResolvedValue(true); + (mockFs.readJson as any).mockResolvedValue({ + version: '1.0.0', + environments: [], + phases: [], + createdAt: '2024-01-01T00:00:00.000Z', + updatedAt: '2024-01-01T00:00:00.000Z' + }); + + const result = await configManager.getMemoryDbPath(); + + expect(result).toBeUndefined(); + }); + + it('returns undefined when memory.path is blank or invalid', async () => { + (mockFs.pathExists as any).mockResolvedValue(true); + (mockFs.readJson as any).mockResolvedValue({ + version: '1.0.0', + environments: [], + phases: [], + memory: { path: ' ' }, + createdAt: '2024-01-01T00:00:00.000Z', + updatedAt: '2024-01-01T00:00:00.000Z' + }); + + await expect(configManager.getMemoryDbPath()).resolves.toBeUndefined(); + + (mockFs.readJson as any).mockResolvedValue({ + version: '1.0.0', + environments: [], + phases: [], + memory: { path: 42 }, + createdAt: '2024-01-01T00:00:00.000Z', + updatedAt: '2024-01-01T00:00:00.000Z' + }); + + await expect(configManager.getMemoryDbPath()).resolves.toBeUndefined(); + }); + + it('returns absolute memory.path as-is', async () => { + (mockFs.pathExists as any).mockResolvedValue(true); + (mockFs.readJson as any).mockResolvedValue({ + version: '1.0.0', + environments: [], + phases: [], + memory: { path: '/custom/memory.db' }, + createdAt: '2024-01-01T00:00:00.000Z', + updatedAt: '2024-01-01T00:00:00.000Z' + }); + + const result = await configManager.getMemoryDbPath(); + + expect(result).toBe('/custom/memory.db'); + expect(mockPath.isAbsolute).toHaveBeenCalledWith('/custom/memory.db'); + }); + + it('resolves relative memory.path from the config directory', async () => { + (mockFs.pathExists as any).mockResolvedValue(true); + (mockFs.readJson as any).mockResolvedValue({ + version: '1.0.0', + environments: [], + phases: [], + memory: { path: '.ai-devkit/project-memory.db' }, + createdAt: '2024-01-01T00:00:00.000Z', + updatedAt: '2024-01-01T00:00:00.000Z' + }); + + const result = await configManager.getMemoryDbPath(); + + expect(mockPath.dirname).toHaveBeenCalledWith('/test/dir/.ai-devkit.json'); + expect(mockPath.resolve).toHaveBeenCalledWith('/test/dir', '.ai-devkit/project-memory.db'); + expect(result).toBe('/test/dir/.ai-devkit/project-memory.db'); + }); + }); }); diff --git a/packages/cli/src/commands/memory.ts b/packages/cli/src/commands/memory.ts index ed4a07df..80415261 100644 --- a/packages/cli/src/commands/memory.ts +++ b/packages/cli/src/commands/memory.ts @@ -1,12 +1,18 @@ import type { Command } from 'commander'; import { memoryStoreCommand, memorySearchCommand, memoryUpdateCommand } from '@ai-devkit/memory'; import type { MemorySearchOptions, MemoryStoreOptions, MemoryUpdateOptions } from '@ai-devkit/memory'; +import { ConfigManager } from '../lib/Config'; import { ui } from '../util/terminal-ui'; import { truncate } from '../util/text'; const TITLE_MAX_LENGTH = 60; export function registerMemoryCommand(program: Command): void { + const resolveMemoryDbPath = async (): Promise => { + const configManager = new ConfigManager(); + return configManager.getMemoryDbPath(); + }; + const memoryCommand = program .command('memory') .description('Interact with the knowledge memory service'); @@ -18,9 +24,12 @@ export function registerMemoryCommand(program: Command): void { .requiredOption('-c, --content ', 'Content of the knowledge item (50-5000 chars)') .option('--tags ', 'Comma-separated tags (e.g., "api,backend")') .option('-s, --scope ', 'Scope: global, project:, or repo:', 'global') - .action((options: MemoryStoreOptions) => { + .action(async (options: MemoryStoreOptions) => { try { - const result = memoryStoreCommand(options); + const result = memoryStoreCommand({ + ...options, + dbPath: await resolveMemoryDbPath() + } as MemoryStoreOptions); console.log(JSON.stringify(result, null, 2)); } catch (error) { const message = error instanceof Error ? error.message : String(error); @@ -37,9 +46,12 @@ export function registerMemoryCommand(program: Command): void { .option('-c, --content ', 'New content (50-5000 chars)') .option('--tags ', 'Comma-separated new tags (replaces existing)') .option('-s, --scope ', 'New scope: global, project:, or repo:') - .action((options: MemoryUpdateOptions) => { + .action(async (options: MemoryUpdateOptions) => { try { - const result = memoryUpdateCommand(options); + const result = memoryUpdateCommand({ + ...options, + dbPath: await resolveMemoryDbPath() + } as MemoryUpdateOptions); console.log(JSON.stringify(result, null, 2)); } catch (error) { const message = error instanceof Error ? error.message : String(error); @@ -56,13 +68,14 @@ export function registerMemoryCommand(program: Command): void { .option('-s, --scope ', 'Scope filter') .option('-l, --limit ', 'Maximum results (1-20)', '5') .option('--table', 'Display results as a table with id, title, and scope') - .action((options: MemorySearchOptions & { limit?: string; table?: boolean }) => { + .action(async (options: MemorySearchOptions & { limit?: string; table?: boolean }) => { try { const { table, limit, ...searchOptions } = options; const result = memorySearchCommand({ ...searchOptions, - limit: limit ? parseInt(limit, 10) : 5 - }); + limit: limit ? parseInt(limit, 10) : 5, + dbPath: await resolveMemoryDbPath() + } as MemorySearchOptions); if (table) { if (result.results.length === 0) { diff --git a/packages/cli/src/lib/Config.ts b/packages/cli/src/lib/Config.ts index 8bf8b9b3..803d7fb3 100644 --- a/packages/cli/src/lib/Config.ts +++ b/packages/cli/src/lib/Config.ts @@ -85,6 +85,26 @@ export class ConfigManager { return config?.paths?.docs || DEFAULT_DOCS_DIR; } + async getMemoryDbPath(): Promise { + const config = await this.read() as any; + const configuredPath = config?.memory?.path; + + if (typeof configuredPath !== 'string') { + return undefined; + } + + const trimmedPath = configuredPath.trim(); + if (!trimmedPath) { + return undefined; + } + + if (path.isAbsolute(trimmedPath)) { + return trimmedPath; + } + + return path.resolve(path.dirname(this.configPath), trimmedPath); + } + async setDocsDir(docsDir: string): Promise { const config = await this.read(); if (!config) { diff --git a/packages/cli/src/types.ts b/packages/cli/src/types.ts index 0f9595d0..3725f3a4 100644 --- a/packages/cli/src/types.ts +++ b/packages/cli/src/types.ts @@ -29,6 +29,9 @@ export interface DevKitConfig { paths?: { docs?: string; }; + memory?: { + path?: string; + }; environments: EnvironmentCode[]; phases: Phase[]; skills?: ConfigSkill[]; diff --git a/packages/memory/src/api.ts b/packages/memory/src/api.ts index 496d54b0..73011790 100644 --- a/packages/memory/src/api.ts +++ b/packages/memory/src/api.ts @@ -1,7 +1,7 @@ import { storeKnowledge } from './handlers/store'; import { searchKnowledge } from './handlers/search'; import { updateKnowledge } from './handlers/update'; -import { closeDatabase } from './database'; +import { closeDatabase, getDatabase } from './database'; import type { StoreKnowledgeInput, SearchKnowledgeInput, StoreKnowledgeResult, SearchKnowledgeResult, UpdateKnowledgeInput, UpdateKnowledgeResult } from './types'; export { storeKnowledge, searchKnowledge, updateKnowledge }; @@ -13,6 +13,7 @@ export interface MemoryStoreOptions { content: string; tags?: string; scope?: string; + dbPath?: string; } export interface MemoryUpdateOptions { @@ -21,6 +22,7 @@ export interface MemoryUpdateOptions { content?: string; tags?: string; scope?: string; + dbPath?: string; } export interface MemorySearchOptions { @@ -28,10 +30,12 @@ export interface MemorySearchOptions { tags?: string; scope?: string; limit?: number; + dbPath?: string; } export function memoryStoreCommand(options: MemoryStoreOptions): StoreKnowledgeResult { try { + getDatabase({ dbPath: options.dbPath }); const input: StoreKnowledgeInput = { title: options.title, content: options.content, @@ -47,6 +51,7 @@ export function memoryStoreCommand(options: MemoryStoreOptions): StoreKnowledgeR export function memoryUpdateCommand(options: MemoryUpdateOptions): UpdateKnowledgeResult { try { + getDatabase({ dbPath: options.dbPath }); const input: UpdateKnowledgeInput = { id: options.id, title: options.title, @@ -63,6 +68,7 @@ export function memoryUpdateCommand(options: MemoryUpdateOptions): UpdateKnowled export function memorySearchCommand(options: MemorySearchOptions): SearchKnowledgeResult { try { + getDatabase({ dbPath: options.dbPath }); const input: SearchKnowledgeInput = { query: options.query, contextTags: options.tags ? options.tags.split(',').map(t => t.trim()) : undefined, diff --git a/packages/memory/tests/integration/api.test.ts b/packages/memory/tests/integration/api.test.ts new file mode 100644 index 00000000..3b30139c --- /dev/null +++ b/packages/memory/tests/integration/api.test.ts @@ -0,0 +1,52 @@ +import { existsSync, unlinkSync } from 'fs'; +import { homedir, tmpdir } from 'os'; +import { join } from 'path'; +import { memorySearchCommand, memoryStoreCommand, memoryUpdateCommand } from '../../src/api'; + +describe('memory api command helpers', () => { + const testDbPath = join(tmpdir(), `test-api-${Date.now()}-${Math.random().toString(36)}.db`); + const defaultDbPath = join(homedir(), '.ai-devkit', 'memory.db'); + + afterEach(() => { + for (const suffix of ['', '-wal', '-shm']) { + const currentTestPath = `${testDbPath}${suffix}`; + if (existsSync(currentTestPath)) { + unlinkSync(currentTestPath); + } + } + }); + + it('uses explicit dbPath for store, search, and update', () => { + const storeResult = memoryStoreCommand({ + title: 'A valid title 123', + content: 'This is a valid content body long enough to satisfy constraints for the integration test.', + dbPath: testDbPath + }); + + expect(storeResult.success).toBe(true); + expect(existsSync(testDbPath)).toBe(true); + + const searchResult = memorySearchCommand({ + query: 'valid title', + dbPath: testDbPath + }); + + expect(searchResult.results.some(result => result.id === storeResult.id)).toBe(true); + + const updateResult = memoryUpdateCommand({ + id: storeResult.id!, + title: 'A valid updated title 123', + dbPath: testDbPath + }); + + expect(updateResult.success).toBe(true); + + const updatedSearch = memorySearchCommand({ + query: 'updated title', + dbPath: testDbPath + }); + + expect(updatedSearch.results.some(result => result.id === storeResult.id)).toBe(true); + expect(defaultDbPath).not.toBe(testDbPath); + }); +});