Skip to content

Commit 5adeffc

Browse files
feat(sdk): load knowledge files from user home directory
Add support for loading knowledge files (~/.knowledge.md, ~/.AGENTS.md, or ~/.CLAUDE.md) from the user's home directory with priority fallback. Changes: - Add shared constants in common/src/constants/knowledge.ts: - KNOWLEDGE_FILE_NAMES array with priority order - PRIMARY_KNOWLEDGE_FILE_NAME for explicit primary file reference - isKnowledgeFile() with stricter matching (exact names or *.knowledge.md) - Add loadUserKnowledgeFiles() to load from home directory - Update initialSessionState to populate userKnowledgeFiles - Accept userKnowledgeFiles param that merges with home dir files - Update prompts to mention AGENTS.md support - Consolidate duplicate constants across CLI, SDK, and evals - Add comprehensive tests for all new functionality 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
1 parent 2088d93 commit 5adeffc

File tree

11 files changed

+742
-42
lines changed

11 files changed

+742
-42
lines changed

cli/src/commands/init.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, writeFileSync } from 'fs'
22
import path from 'path'
33

44
import { AnalyticsEvent } from '@codebuff/common/constants/analytics-events'
5+
import { PRIMARY_KNOWLEDGE_FILE_NAME } from '@codebuff/common/constants/knowledge'
56

67
// @ts-expect-error - Bun text import attribute not supported by TypeScript
78
import agentDefinitionSource from '../../../common/src/templates/initial-agents-dir/types/agent-definition' with { type: 'text' }
@@ -16,8 +17,6 @@ import { getSystemMessage } from '../utils/message-history'
1617

1718
import type { PostUserMessageFn } from '../types/contracts/send-message'
1819

19-
const KNOWLEDGE_FILE_NAME = 'knowledge.md'
20-
2120
const INITIAL_KNOWLEDGE_FILE = `# Project knowledge
2221
2322
This file gives Codebuff context about your project: goals, commands, conventions, and gotchas.
@@ -56,19 +55,19 @@ export function handleInitializationFlowLocally(): {
5655
postUserMessage: PostUserMessageFn
5756
} {
5857
const projectRoot = getProjectRoot()
59-
const knowledgePath = path.join(projectRoot, KNOWLEDGE_FILE_NAME)
58+
const knowledgePath = path.join(projectRoot, PRIMARY_KNOWLEDGE_FILE_NAME)
6059
const messages: string[] = []
6160

6261
if (existsSync(knowledgePath)) {
63-
messages.push(`📋 \`${KNOWLEDGE_FILE_NAME}\` already exists.`)
62+
messages.push(`📋 \`${PRIMARY_KNOWLEDGE_FILE_NAME}\` already exists.`)
6463
} else {
6564
writeFileSync(knowledgePath, INITIAL_KNOWLEDGE_FILE)
66-
messages.push(`✅ Created \`${KNOWLEDGE_FILE_NAME}\``)
65+
messages.push(`✅ Created \`${PRIMARY_KNOWLEDGE_FILE_NAME}\``)
6766

6867
// Track knowledge file creation
6968
trackEvent(AnalyticsEvent.KNOWLEDGE_FILE_UPDATED, {
7069
action: 'created',
71-
fileName: KNOWLEDGE_FILE_NAME,
70+
fileName: PRIMARY_KNOWLEDGE_FILE_NAME,
7271
fileSizeBytes: Buffer.byteLength(INITIAL_KNOWLEDGE_FILE, 'utf8'),
7372
})
7473
}

common/src/constants/knowledge.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import path from 'path'
2+
3+
/**
4+
* The primary/default knowledge file name.
5+
* Used when creating new knowledge files.
6+
*/
7+
export const PRIMARY_KNOWLEDGE_FILE_NAME = 'knowledge.md'
8+
9+
/**
10+
* Knowledge file names in priority order (highest priority first).
11+
* Used for both project knowledge files and home directory user knowledge files.
12+
*/
13+
export const KNOWLEDGE_FILE_NAMES = [
14+
PRIMARY_KNOWLEDGE_FILE_NAME,
15+
'AGENTS.md',
16+
'CLAUDE.md',
17+
] as const
18+
19+
/**
20+
* Pre-computed lowercase knowledge file names for efficient matching.
21+
*/
22+
export const KNOWLEDGE_FILE_NAMES_LOWERCASE = KNOWLEDGE_FILE_NAMES.map((name) =>
23+
name.toLowerCase(),
24+
)
25+
26+
/**
27+
* Checks if a file path is a knowledge file.
28+
* Matches:
29+
* - Exact file names: knowledge.md, AGENTS.md, CLAUDE.md (case-insensitive)
30+
* - Pattern: *.knowledge.md (e.g., authentication.knowledge.md)
31+
*/
32+
export function isKnowledgeFile(filePath: string): boolean {
33+
const fileName = path.basename(filePath).toLowerCase()
34+
35+
// Check for exact matches with standard knowledge file names
36+
if (KNOWLEDGE_FILE_NAMES_LOWERCASE.includes(fileName)) {
37+
return true
38+
}
39+
40+
// Check for *.knowledge.md pattern (e.g., authentication.knowledge.md)
41+
if (fileName.endsWith('.knowledge.md')) {
42+
return true
43+
}
44+
45+
return false
46+
}

evals/scaffolding.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import fs from 'fs'
33
import path from 'path'
44

55
import { getFileTokenScores } from '@codebuff/code-map/parse'
6+
import { isKnowledgeFile } from '@codebuff/common/constants/knowledge'
67
import { getSystemInfo } from '@codebuff/common/util/system-info'
78

89
import {
@@ -36,9 +37,7 @@ export async function getProjectFileContext(
3637
fs: fs.promises,
3738
})
3839
const allFilePaths = getAllFilePaths(fileTree)
39-
const knowledgeFilePaths = allFilePaths.filter((filePath) =>
40-
filePath.endsWith('knowledge.md'),
41-
)
40+
const knowledgeFilePaths = allFilePaths.filter(isKnowledgeFile)
4241
const knowledgeFiles: Record<string, string> = {}
4342
for (const filePath of knowledgeFilePaths) {
4443
const content = readMockFile(projectPath, filePath)

packages/agent-runtime/src/system-prompt/prompts.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ import type { ProjectFileContext } from '@codebuff/common/util/file'
1414
export const knowledgeFilesPrompt = `
1515
# Knowledge files
1616
17-
Knowledge files are your guide to the project. Knowledge files (files ending in "knowledge.md" or "CLAUDE.md") within a directory capture knowledge about that portion of the codebase. They are another way to take notes in this "Memento"-style environment.
17+
Knowledge files are your guide to the project. Knowledge files (files ending in "knowledge.md", "AGENTS.md", or "CLAUDE.md") within a directory capture knowledge about that portion of the codebase. They are another way to take notes in this "Memento"-style environment.
1818
1919
Knowledge files were created by previous engineers working on the codebase, and they were given these same instructions. They contain key concepts or helpful tips that are not obvious from the code. e.g., let's say I want to use a package manager aside from the default. That is hard to find in the codebase and would therefore be an appropriate piece of information to add to a knowledge file.
2020
2121
Each knowledge file should develop over time into a concise but rich repository of knowledge about the files within the directory, subdirectories, or the specific file it's associated with.
2222
23-
There is a special class of user knowledge files that are stored in the user's home directory, e.g. \`~/.knowledge.md\`. These files are available to be read, but you cannot edit them because they are outside of the project directory. Do not try to edit them.
23+
There is a special class of user knowledge files that are stored in the user's home directory, e.g. \`~/.knowledge.md\`, \`~/.AGENTS.md\`, or \`~/.CLAUDE.md\`. These files are available to be read, but you cannot edit them because they are outside of the project directory. Do not try to edit them.
2424
2525
When should you update a knowledge file?
2626
- If the user gives broad advice to "always do x", that is a good candidate for updating a knowledge file with a concise rule to follow or bit of advice so you won't make the mistake again.

packages/agent-runtime/src/templates/strings.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { KNOWLEDGE_FILE_NAMES_LOWERCASE } from '@codebuff/common/constants/knowledge'
12
import { escapeString } from '@codebuff/common/util/string'
23
import { z } from 'zod/v4'
34

@@ -117,9 +118,11 @@ export async function formatPrompt(
117118
Object.entries({
118119
...Object.fromEntries(
119120
Object.entries(fileContext.knowledgeFiles)
120-
.filter(([path]) =>
121-
['knowledge.md', 'CLAUDE.md'].includes(path),
122-
)
121+
.filter(([filePath]) => {
122+
const lowerPath = filePath.toLowerCase()
123+
// Root-level knowledge files only (knowledge.md, AGENTS.md, CLAUDE.md)
124+
return KNOWLEDGE_FILE_NAMES_LOWERCASE.includes(lowerPath)
125+
})
123126
.map(([path, content]) => [path, content.trim()]),
124127
),
125128
...fileContext.userKnowledgeFiles,

sdk/README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,24 @@ main()
136136

137137
## API Reference
138138

139+
### Knowledge Files
140+
141+
Knowledge files provide project context to the agent. The SDK auto-discovers:
142+
143+
- **Project files**: `knowledge.md`, `AGENTS.md`, or `CLAUDE.md` in each directory (priority order)
144+
- **User files**: `~/.knowledge.md`, `~/.AGENTS.md`, or `~/.CLAUDE.md` (case-insensitive)
145+
146+
Override with `knowledgeFiles` (replaces project files) or `userKnowledgeFiles` (merges with home directory files):
147+
148+
```typescript
149+
await client.run({
150+
agent: 'codebuff/base@0.0.16',
151+
prompt: 'Help me refactor',
152+
knowledgeFiles: { 'knowledge.md': '# Guidelines\n- Use TypeScript' },
153+
userKnowledgeFiles: { '~/.knowledge.md': '# Preferences\n- Be concise' },
154+
})
155+
```
156+
139157
### File Filtering
140158

141159
The `fileFilter` option controls which files the agent can read:

sdk/src/__tests__/knowledge-file-selection.test.ts

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,132 @@
11
import { describe, expect, test } from 'bun:test'
22

3-
import { selectKnowledgeFilePaths } from '../run-state'
3+
import {
4+
KNOWLEDGE_FILE_NAMES,
5+
isKnowledgeFile,
6+
selectHighestPriorityKnowledgeFile,
7+
selectKnowledgeFilePaths,
8+
} from '../run-state'
9+
10+
describe('KNOWLEDGE_FILE_NAMES', () => {
11+
test('contains expected file names in priority order', () => {
12+
expect(KNOWLEDGE_FILE_NAMES).toEqual([
13+
'knowledge.md',
14+
'AGENTS.md',
15+
'CLAUDE.md',
16+
])
17+
})
18+
})
19+
20+
describe('isKnowledgeFile', () => {
21+
test('returns true for knowledge.md', () => {
22+
expect(isKnowledgeFile('knowledge.md')).toBe(true)
23+
expect(isKnowledgeFile('src/knowledge.md')).toBe(true)
24+
expect(isKnowledgeFile('KNOWLEDGE.MD')).toBe(true)
25+
})
26+
27+
test('returns true for AGENTS.md', () => {
28+
expect(isKnowledgeFile('AGENTS.md')).toBe(true)
29+
expect(isKnowledgeFile('src/agents.md')).toBe(true)
30+
expect(isKnowledgeFile('Agents.MD')).toBe(true)
31+
})
32+
33+
test('returns true for CLAUDE.md', () => {
34+
expect(isKnowledgeFile('CLAUDE.md')).toBe(true)
35+
expect(isKnowledgeFile('src/claude.md')).toBe(true)
36+
expect(isKnowledgeFile('Claude.MD')).toBe(true)
37+
})
38+
39+
test('returns true for *.knowledge.md pattern', () => {
40+
expect(isKnowledgeFile('authentication.knowledge.md')).toBe(true)
41+
expect(isKnowledgeFile('src/api.knowledge.md')).toBe(true)
42+
expect(isKnowledgeFile('docs/AUTH.KNOWLEDGE.MD')).toBe(true)
43+
expect(isKnowledgeFile('foo.bar.knowledge.md')).toBe(true)
44+
})
45+
46+
test('returns false for non-knowledge files', () => {
47+
expect(isKnowledgeFile('README.md')).toBe(false)
48+
expect(isKnowledgeFile('src/utils.ts')).toBe(false)
49+
expect(isKnowledgeFile('knowledge.txt')).toBe(false)
50+
expect(isKnowledgeFile('agents.txt')).toBe(false)
51+
})
52+
53+
test('returns false for files with knowledge in name but no dot separator', () => {
54+
// These should NOT match - stricter matching requires exact filename or .knowledge.md suffix
55+
expect(isKnowledgeFile('myknowledge.md')).toBe(false)
56+
expect(isKnowledgeFile('src/authknowledge.md')).toBe(false)
57+
expect(isKnowledgeFile('preknowledge.md')).toBe(false)
58+
})
59+
60+
test('returns false for similar but non-matching patterns', () => {
61+
// .agents.md and .claude.md patterns should NOT match
62+
expect(isKnowledgeFile('auth.agents.md')).toBe(false)
63+
expect(isKnowledgeFile('auth.claude.md')).toBe(false)
64+
expect(isKnowledgeFile('foo.AGENTS.md')).toBe(false)
65+
expect(isKnowledgeFile('foo.CLAUDE.md')).toBe(false)
66+
})
67+
})
68+
69+
describe('selectHighestPriorityKnowledgeFile', () => {
70+
test('returns undefined for empty array', () => {
71+
expect(selectHighestPriorityKnowledgeFile([])).toBeUndefined()
72+
})
73+
74+
test('returns undefined when no knowledge files present', () => {
75+
expect(
76+
selectHighestPriorityKnowledgeFile(['README.md', 'src/utils.ts']),
77+
).toBeUndefined()
78+
})
79+
80+
test('returns the only knowledge file', () => {
81+
expect(selectHighestPriorityKnowledgeFile(['AGENTS.md'])).toBe('AGENTS.md')
82+
})
83+
84+
test('prefers knowledge.md over AGENTS.md', () => {
85+
expect(
86+
selectHighestPriorityKnowledgeFile(['AGENTS.md', 'knowledge.md']),
87+
).toBe('knowledge.md')
88+
})
89+
90+
test('prefers knowledge.md over CLAUDE.md', () => {
91+
expect(
92+
selectHighestPriorityKnowledgeFile(['CLAUDE.md', 'knowledge.md']),
93+
).toBe('knowledge.md')
94+
})
95+
96+
test('prefers AGENTS.md over CLAUDE.md', () => {
97+
expect(selectHighestPriorityKnowledgeFile(['CLAUDE.md', 'AGENTS.md'])).toBe(
98+
'AGENTS.md',
99+
)
100+
})
101+
102+
test('prefers knowledge.md when all three exist', () => {
103+
expect(
104+
selectHighestPriorityKnowledgeFile([
105+
'CLAUDE.md',
106+
'AGENTS.md',
107+
'knowledge.md',
108+
]),
109+
).toBe('knowledge.md')
110+
})
111+
112+
test('handles case-insensitive matching', () => {
113+
expect(selectHighestPriorityKnowledgeFile(['KNOWLEDGE.MD'])).toBe(
114+
'KNOWLEDGE.MD',
115+
)
116+
expect(selectHighestPriorityKnowledgeFile(['agents.md'])).toBe('agents.md')
117+
expect(selectHighestPriorityKnowledgeFile(['Claude.md'])).toBe('Claude.md')
118+
})
119+
120+
test('filters out non-knowledge files before selecting', () => {
121+
expect(
122+
selectHighestPriorityKnowledgeFile([
123+
'README.md',
124+
'AGENTS.md',
125+
'utils.ts',
126+
]),
127+
).toBe('AGENTS.md')
128+
})
129+
})
4130

5131
describe('selectKnowledgeFilePaths', () => {
6132
test('selects knowledge.md when it exists alone', () => {

0 commit comments

Comments
 (0)