diff --git a/docs/for-agents/installation.md b/docs/for-agents/installation.md index 6d844e7..943b6b9 100644 --- a/docs/for-agents/installation.md +++ b/docs/for-agents/installation.md @@ -83,7 +83,7 @@ Initialization should: * Create `.konteks/` for project-local memory. * Initialize the local SQLite memory store. -* Add `.konteks/` to `.gitignore`. +* Create `.konteks/.gitignore` so memory artifacts stay untracked. * Extract and index the current project state. If initialization reports that the project is already initialized, treat that as success and continue. @@ -189,7 +189,7 @@ The save prompt should persist compact durable memories first, then one session ## Guidance * Ask before making broad edits to global agent configuration. -* Do not commit `.konteks/`; initialization should add it to `.gitignore`. +* Do not commit `.konteks/`; initialization should keep it untracked with `.konteks/.gitignore`. * Do not add Konteks as an application dependency unless the user explicitly asks for that. * Do not invent custom memory directories; Konteks uses `.konteks/` in the project root. * Do not configure MCP to launch Konteks through `npx`, plain `bunx`, `pnpm dlx`, or `yarn dlx`. Use `bunx --bun konteks-cli mcp` for Bun users and direct `konteks-cli mcp` for Node users. diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md index 9bc6777..0373e4f 100644 --- a/docs/getting-started/quickstart.md +++ b/docs/getting-started/quickstart.md @@ -28,10 +28,10 @@ yarn dlx konteks-cli init * Creates a `.konteks/` directory for local memory storage. * Initializes the `memory.sqlite` substrate. -* Adds `.konteks/` to your `.gitignore`. +* Creates `.konteks/.gitignore` so memory artifacts stay untracked. * Extracts and indexes the current project state. -Do not commit `.konteks/`; initialization adds it to `.gitignore` so project memory stays local. +Do not commit `.konteks/`; initialization keeps project memory untracked with a local `.konteks/.gitignore`. ### 2. Set Up MCP diff --git a/src/entrypoints/cli/commands/init-command.ts b/src/entrypoints/cli/commands/init-command.ts index 05799c7..2c2d54e 100644 --- a/src/entrypoints/cli/commands/init-command.ts +++ b/src/entrypoints/cli/commands/init-command.ts @@ -38,7 +38,7 @@ export default class InitCommand extends BaseCommand { .print(color => color.primary('Initializing project memory')) .print('') - await ensureKonteksGitignore(context.projectRoot) + await ensureKonteksGitignore(context.memoryDir) const files = await scanProjectFiles(context.projectRoot) const selection = await reviewDetectedGrammars(files) @@ -96,7 +96,7 @@ async function initializeProjectMemory(options: { await writeInitialMemoryFiles(context, options.grammars ?? []) await ensureProjectMemory() - await ensureKonteksGitignore(context.projectRoot) + await ensureKonteksGitignore(context.memoryDir) const extractor = createProjectExtractor({ onProgress: options.onProgress, @@ -144,8 +144,10 @@ async function writeInitialMemoryFiles( }) } -async function ensureKonteksGitignore(projectRoot: string): Promise { - const gitignorePath = join(projectRoot, '.gitignore') +async function ensureKonteksGitignore(memoryDir: string): Promise { + await mkdir(memoryDir) + + const gitignorePath = join(memoryDir, '.gitignore') const existing = await readFile(gitignorePath, 'utf8').catch(error => { if ((error as NodeJS.ErrnoException).code === 'ENOENT') { return '' @@ -154,17 +156,17 @@ async function ensureKonteksGitignore(projectRoot: string): Promise { throw error }) - if (hasKonteksIgnoreEntry(existing)) { + if (hasKonteksCatchAllIgnore(existing)) { return } const prefix = existing.length > 0 && !existing.endsWith('\n') ? '\n' : '' - await writeFile(gitignorePath, `${existing}${prefix}.konteks/\n`) + await writeFile(gitignorePath, `${existing}${prefix}*\n`) } -function hasKonteksIgnoreEntry(content: string): boolean { +function hasKonteksCatchAllIgnore(content: string): boolean { return content .split(/\r?\n/) .map(line => line.trim()) - .some(line => line === '.konteks' || line === '.konteks/') + .some(line => line === '*' || line === '/*') } diff --git a/tests/features/commands/init-command.test.ts b/tests/features/commands/init-command.test.ts index 5ae6a26..dddc92a 100644 --- a/tests/features/commands/init-command.test.ts +++ b/tests/features/commands/init-command.test.ts @@ -146,17 +146,20 @@ describe('InitCommand', () => { return output.join('\n') }) - it('adds .konteks to .gitignore during init', async () => { + it('creates a local .konteks/.gitignore during init', async () => { const projectRoot = await makeTempProject() await init(projectRoot) + await expect( + readFile(join(projectRoot, '.konteks', '.gitignore'), 'utf8'), + ).resolves.toBe('*\n') await expect( readFile(join(projectRoot, '.gitignore'), 'utf8'), - ).resolves.toBe('.konteks/\n') + ).rejects.toHaveProperty('code', 'ENOENT') }) - it('preserves existing .gitignore entries', async () => { + it('preserves existing root .gitignore entries', async () => { const projectRoot = await makeTempProject() await writeFile(join(projectRoot, '.gitignore'), 'node_modules\n') @@ -164,22 +167,40 @@ describe('InitCommand', () => { await expect( readFile(join(projectRoot, '.gitignore'), 'utf8'), - ).resolves.toBe('node_modules\n.konteks/\n') + ).resolves.toBe('node_modules\n') + await expect( + readFile(join(projectRoot, '.konteks', '.gitignore'), 'utf8'), + ).resolves.toBe('*\n') }) - it('does not duplicate existing .konteks ignore entries', async () => { + it('adds a catch-all to an existing local .konteks/.gitignore', async () => { const projectRoot = await makeTempProject() await mkdir(join(projectRoot, '.konteks')) await writeFile( - join(projectRoot, '.gitignore'), - 'node_modules\n.konteks/\n', + join(projectRoot, '.konteks', '.gitignore'), + 'memory.sqlite\n', ) await init(projectRoot) await expect( - readFile(join(projectRoot, '.gitignore'), 'utf8'), - ).resolves.toBe('node_modules\n.konteks/\n') + readFile(join(projectRoot, '.konteks', '.gitignore'), 'utf8'), + ).resolves.toBe('memory.sqlite\n*\n') + }) + + it('does not duplicate an existing local catch-all .konteks/.gitignore', async () => { + const projectRoot = await makeTempProject() + await mkdir(join(projectRoot, '.konteks')) + await writeFile( + join(projectRoot, '.konteks', '.gitignore'), + '# Konteks memory\n*\n', + ) + + await init(projectRoot) + + await expect( + readFile(join(projectRoot, '.konteks', '.gitignore'), 'utf8'), + ).resolves.toBe('# Konteks memory\n*\n') }) it('skips when the project is already initialized', async () => { @@ -211,7 +232,7 @@ describe('InitCommand', () => { expect(plainOutput).toContain('Initializing project memory') expect(plainOutput).toContain( - '✓ Extracted 2 modules and 2 sections from 2 files', + '✓ Extracted 1 modules and 1 sections from 1 files', ) expect(plainOutput).not.toContain('Loaded 0 language parsers') expect(plainOutput).toContain('✓ Preparing dependencies') @@ -225,8 +246,8 @@ describe('InitCommand', () => { expect(plainOutput).toContain('vectors indexed') expect(plainOutput).toContain('✓ Generated project summary') expect(plainOutput).toContain('Project memory ready') - expect(plainOutput).toContain('Files indexed 2') - expect(plainOutput).toContain('Vectors indexed 4') + expect(plainOutput).toContain('Files indexed 1') + expect(plainOutput).toContain('Vectors indexed 2') expect(plainOutput).not.toContain('Documents extracted') expect(plainOutput).not.toContain('Initialized Konteks at') expect(plainOutput).not.toContain('Extracted 2 files into 2 sections') @@ -340,7 +361,7 @@ describe('InitCommand', () => { expect(output).not.toContain('Loading tokenizer.json') expect(output).not.toContain('Loaded 1 language parsers') expect(output).toContain( - '✓ Extracted 4 modules and 3 sections from 3 files', + '✓ Extracted 3 modules and 2 sections from 2 files', ) }) @@ -357,6 +378,9 @@ describe('InitCommand', () => { 'utf8', ), ).resolves.toContain('"version": 1') + await expect( + readFile(join(projectRoot, '.konteks', '.gitignore'), 'utf8'), + ).resolves.toBe('*\n') }) it('continues from existing sections when the manifest is missing', async () => { @@ -375,7 +399,7 @@ describe('InitCommand', () => { const resumedManifest = JSON.parse(await readFile(manifestPath, 'utf8')) expect(output).toContain( - '✓ Extracted 2 modules and 2 sections from 2 files', + '✓ Extracted 1 modules and 1 sections from 1 files', ) expect(output).not.toContain('Documents extracted') expect(output).not.toContain('Extracted 5 documents from 0 files')