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
4 changes: 2 additions & 2 deletions docs/for-agents/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions docs/getting-started/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
18 changes: 10 additions & 8 deletions src/entrypoints/cli/commands/init-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -144,8 +144,10 @@ async function writeInitialMemoryFiles(
})
}

async function ensureKonteksGitignore(projectRoot: string): Promise<void> {
const gitignorePath = join(projectRoot, '.gitignore')
async function ensureKonteksGitignore(memoryDir: string): Promise<void> {
await mkdir(memoryDir)

const gitignorePath = join(memoryDir, '.gitignore')
const existing = await readFile(gitignorePath, 'utf8').catch(error => {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return ''
Expand All @@ -154,17 +156,17 @@ async function ensureKonteksGitignore(projectRoot: string): Promise<void> {
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 === '/*')
}
52 changes: 38 additions & 14 deletions tests/features/commands/init-command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,40 +146,61 @@ 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')

await init(projectRoot)

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 () => {
Expand Down Expand Up @@ -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')
Expand All @@ -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')
Expand Down Expand Up @@ -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',
)
})

Expand All @@ -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 () => {
Expand All @@ -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')
Expand Down