coding-friend-cli 1.37.0 → 1.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -2
- package/dist/{chunk-GZT3YPAP.js → chunk-32UAW3NX.js} +1 -1
- package/dist/chunk-5QCGEPQO.js +238 -0
- package/dist/{chunk-YIXAQ4R2.js → chunk-6NXZKAOV.js} +1 -1
- package/dist/{chunk-676YCW6T.js → chunk-776ZY62N.js} +99 -7
- package/dist/{chunk-DP6DFQ4G.js → chunk-BXSCYGWE.js} +2 -2
- package/dist/{chunk-W6D3J5DX.js → chunk-CGEMETKQ.js} +8 -6
- package/dist/{chunk-XEKQW4RN.js → chunk-ETTZRJ55.js} +1 -1
- package/dist/{chunk-IKLMHJH6.js → chunk-GIH6E7GJ.js} +62 -1
- package/dist/{chunk-6S7352JG.js → chunk-ONG4KRYP.js} +1 -1
- package/dist/{chunk-X2H7JJMR.js → chunk-PLSJW7QJ.js} +29 -1
- package/dist/{chunk-O5ZKLFSL.js → chunk-RCE4OWNX.js} +1 -1
- package/dist/{chunk-PI4P34KF.js → chunk-RFNKPAJW.js} +3 -1
- package/dist/{clean-WEYQZB34.js → clean-YWEMYDE5.js} +2 -2
- package/dist/{config-ZMR2AOBB.js → config-NG7SICM4.js} +12 -24
- package/dist/{dev-IGHMMEP4.js → dev-PTBNMKZT.js} +3 -3
- package/dist/{disable-HL5OEURT.js → disable-MO7AXYYF.js} +20 -3
- package/dist/{enable-CZIOCKKJ.js → enable-4GXLQEHD.js} +20 -3
- package/dist/{guide-CO2V4ZLZ.js → guide-HCR43LJN.js} +1 -1
- package/dist/{host-SSRI4JZV.js → host-EV6QIVTI.js} +2 -2
- package/dist/index.js +59 -47
- package/dist/{init-6MFQWPOP.js → init-KYE5CJMW.js} +115 -12
- package/dist/{install-37NN4CQH.js → install-XAFQY5YF.js} +79 -6
- package/dist/{learn-OPH3CI67.js → learn-MC4VDHZH.js} +2 -2
- package/dist/{mcp-DWKVMNC7.js → mcp-FV6YX3ZT.js} +7 -7
- package/dist/{mcp-serve-5GODJ4DJ.js → mcp-serve-PXREYQRZ.js} +2 -2
- package/dist/{mcp-serve-learn-OF3ZJUJC.js → mcp-serve-learn-UVXIDOEZ.js} +1 -1
- package/dist/{memory-TRDLADUM.js → memory-DF7OUOTV.js} +6 -6
- package/dist/{permission-I3OFJUYA.js → permission-VISPGMUK.js} +38 -4
- package/dist/{permissions-J46VP6US.js → permissions-2SBACQHV.js} +2 -2
- package/dist/{session-S4WKA4P5.js → session-QK6OZO3X.js} +2 -2
- package/dist/{status-YZEOUX46.js → status-FVFTJ6GS.js} +11 -10
- package/dist/{statusline-KKFJPTI7.js → statusline-AOZHEQSV.js} +2 -2
- package/dist/{uninstall-AVA3J554.js → uninstall-MF6FRUBO.js} +51 -4
- package/dist/{update-Q3ACCK4Z.js → update-QNUCSDSA.js} +6 -5
- package/dist/{update-check-25SCLHZO.js → update-check-7LKJLJPM.js} +1 -1
- package/lib/cf-memory/src/lib/claude-md.ts +82 -70
- package/lib/cf-memory/src/lib/status-frame.ts +2 -2
- package/lib/cf-memory/src/tools/delete.ts +1 -1
- package/lib/cf-memory/src/tools/store.ts +6 -6
- package/lib/cf-memory/src/tools/update.ts +5 -11
- package/package.json +32 -4
- package/lib/cf-memory/README.md +0 -317
- package/lib/cf-memory/src/__tests__/claude-md.test.ts +0 -343
- package/lib/cf-memory/src/__tests__/daemon-client.test.ts +0 -146
- package/lib/cf-memory/src/__tests__/daemon-e2e.test.ts +0 -223
- package/lib/cf-memory/src/__tests__/daemon.test.ts +0 -407
- package/lib/cf-memory/src/__tests__/dedup.test.ts +0 -103
- package/lib/cf-memory/src/__tests__/embeddings.test.ts +0 -292
- package/lib/cf-memory/src/__tests__/lazy-install.test.ts +0 -210
- package/lib/cf-memory/src/__tests__/markdown-backend.test.ts +0 -450
- package/lib/cf-memory/src/__tests__/migration.test.ts +0 -255
- package/lib/cf-memory/src/__tests__/migrations.test.ts +0 -288
- package/lib/cf-memory/src/__tests__/minisearch-backend.test.ts +0 -262
- package/lib/cf-memory/src/__tests__/ollama.test.ts +0 -48
- package/lib/cf-memory/src/__tests__/project-id.test.ts +0 -43
- package/lib/cf-memory/src/__tests__/schema.test.ts +0 -128
- package/lib/cf-memory/src/__tests__/search.test.ts +0 -115
- package/lib/cf-memory/src/__tests__/sqlite-lazy-db.test.ts +0 -213
- package/lib/cf-memory/src/__tests__/status-frame.test.ts +0 -136
- package/lib/cf-memory/src/__tests__/temporal-decay.test.ts +0 -54
- package/lib/cf-memory/src/__tests__/tier.test.ts +0 -388
- package/lib/cf-memory/src/__tests__/tools.test.ts +0 -83
- package/lib/cf-memory/vitest.config.ts +0 -7
- package/lib/learn-host/.prettierignore +0 -4
- package/lib/learn-host/.prettierrc +0 -8
- package/lib/learn-host/README.md +0 -113
- package/lib/learn-host/eslint.config.mjs +0 -6
- package/lib/learn-mcp/README.md +0 -187
- package/lib/learn-mcp/src/__tests__/docs.test.ts +0 -117
- package/lib/learn-mcp/vitest.config.ts +0 -8
package/lib/cf-memory/README.md
DELETED
|
@@ -1,317 +0,0 @@
|
|
|
1
|
-
# CF Memory
|
|
2
|
-
|
|
3
|
-
Persistent memory system for [Coding Friend](https://cf.dinhanhthi.com/). Provides MCP (Model Context Protocol) tools for storing, searching, and retrieving project knowledge across sessions.
|
|
4
|
-
|
|
5
|
-
## Architecture
|
|
6
|
-
|
|
7
|
-
CF Memory uses a **3-tier graceful degradation** design — it automatically detects and uses the best available backend:
|
|
8
|
-
|
|
9
|
-
| Tier | Backend | Features | When |
|
|
10
|
-
| --------------------- | -------------------------- | ------------------------------------------------------------------------------ | --------------------------- |
|
|
11
|
-
| **Tier 1** (Full) | SQLite + FTS5 + sqlite-vec | Hybrid search (BM25 keyword + semantic embeddings), RRF fusion, temporal decay | SQLite deps installed |
|
|
12
|
-
| **Tier 2** (Lite) | MiniSearch daemon | In-memory BM25 + fuzzy search, file watcher, Unix Domain Socket | Daemon running |
|
|
13
|
-
| **Tier 3** (Markdown) | Markdown file I/O | Substring matching, no external deps | Always available (fallback) |
|
|
14
|
-
|
|
15
|
-
Detection: Tier 1 → Tier 2 → Tier 3 (first available wins).
|
|
16
|
-
|
|
17
|
-
## MCP Interface
|
|
18
|
-
|
|
19
|
-
**6 tools:**
|
|
20
|
-
|
|
21
|
-
| Tool | Description |
|
|
22
|
-
| ----------------- | --------------------------------------------------------------- |
|
|
23
|
-
| `memory_store` | Store a new memory with title, description, type, tags, content |
|
|
24
|
-
| `memory_search` | Search memories by query with optional type/tags filter |
|
|
25
|
-
| `memory_retrieve` | Fetch a specific memory by ID |
|
|
26
|
-
| `memory_list` | List memories with optional type/category filter |
|
|
27
|
-
| `memory_update` | Update fields of an existing memory |
|
|
28
|
-
| `memory_delete` | Delete a memory by ID |
|
|
29
|
-
|
|
30
|
-
**2 resources:**
|
|
31
|
-
|
|
32
|
-
| Resource | Description |
|
|
33
|
-
| ---------------- | ------------------------------------------------ |
|
|
34
|
-
| `memory://index` | Browse all stored memories (JSON) |
|
|
35
|
-
| `memory://stats` | Storage statistics (total, by category, by type) |
|
|
36
|
-
|
|
37
|
-
## Memory Model
|
|
38
|
-
|
|
39
|
-
**Types** map to storage categories:
|
|
40
|
-
|
|
41
|
-
| Type | Category (folder) | Use case |
|
|
42
|
-
| ------------ | ----------------- | ------------------------------- |
|
|
43
|
-
| `fact` | `features/` | Feature descriptions, facts |
|
|
44
|
-
| `preference` | `conventions/` | Conventions, preferences |
|
|
45
|
-
| `context` | `decisions/` | Architecture decisions, context |
|
|
46
|
-
| `episode` | `bugs/` | Bug reports, debugging sessions |
|
|
47
|
-
| `procedure` | `infrastructure/` | Procedures, infra setup |
|
|
48
|
-
|
|
49
|
-
Memories are stored as Markdown files with YAML frontmatter in `docs/memory/<category>/`.
|
|
50
|
-
|
|
51
|
-
Convention memories (`preference` type → `conventions/` folder) are automatically synced to the project's `CLAUDE.md` under a `## CF Memory: Project Rules` section. Other memory types can opt-in to CLAUDE.md sync by setting `sync_to_claude_md: true` when storing or updating — useful for decisions, infrastructure rules, or any project-wide rules that future sessions must follow. Entries are tracked via HTML comments (`<!-- cf:<id> -->`) and are automatically added, updated, or removed when the corresponding memory is stored, updated, or deleted.
|
|
52
|
-
|
|
53
|
-
## Embedding Models
|
|
54
|
-
|
|
55
|
-
Tier 1 (SQLite) uses embedding models to generate vectors for semantic search. Two providers are supported:
|
|
56
|
-
|
|
57
|
-
### Providers
|
|
58
|
-
|
|
59
|
-
| Provider | How it works | Default model | Pros | Cons |
|
|
60
|
-
| ------------------- | ----------------------------------------------- | -------------------------------- | -------------------------------------------- | ------------------------------------------------ |
|
|
61
|
-
| **Transformers.js** | Runs in-process via `@huggingface/transformers` | `Xenova/all-MiniLM-L6-v2` (384d) | Zero config, no external service | Slower first load (~5s), limited model selection |
|
|
62
|
-
| **Ollama** | Calls local Ollama server API | `all-minilm:l6-v2` (384d) | Fast, wide model selection, GPU acceleration | Requires Ollama running separately |
|
|
63
|
-
|
|
64
|
-
### Recommended Models
|
|
65
|
-
|
|
66
|
-
| Model | Dims | Size | Notes |
|
|
67
|
-
| -------------------------- | ---- | ------- | --------------------------------------------------------------------- |
|
|
68
|
-
| `all-minilm:l6-v2` | 384 | ~23 MB | **Default** — fast, good for small-to-medium collections |
|
|
69
|
-
| `nomic-embed-text` | 768 | ~274 MB | **Recommended upgrade** — significantly better semantic understanding |
|
|
70
|
-
| `mxbai-embed-large` | 1024 | ~670 MB | Best quality, suitable for large collections (100+ memories) |
|
|
71
|
-
| `snowflake-arctic-embed:s` | 384 | ~67 MB | Alternative compact model |
|
|
72
|
-
| `snowflake-arctic-embed:m` | 768 | ~250 MB | Good balance of quality and speed |
|
|
73
|
-
| `bge-base-en-v1.5` | 768 | ~130 MB | Strong English-language model |
|
|
74
|
-
| `bge-large-en-v1.5` | 1024 | ~670 MB | Top-tier English model |
|
|
75
|
-
|
|
76
|
-
### Using Ollama for Embeddings
|
|
77
|
-
|
|
78
|
-
1. Install Ollama: https://ollama.com/download
|
|
79
|
-
2. Pull a model:
|
|
80
|
-
```bash
|
|
81
|
-
ollama pull nomic-embed-text
|
|
82
|
-
```
|
|
83
|
-
3. Configure in `.coding-friend/config.json`:
|
|
84
|
-
```json
|
|
85
|
-
{
|
|
86
|
-
"memory": {
|
|
87
|
-
"embedding": {
|
|
88
|
-
"provider": "ollama",
|
|
89
|
-
"model": "nomic-embed-text"
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
```
|
|
94
|
-
4. If you have existing memories, rebuild to re-embed:
|
|
95
|
-
```bash
|
|
96
|
-
cf memory rebuild
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
If Ollama is not running or the configured model is missing, the system falls back to Transformers.js automatically.
|
|
100
|
-
|
|
101
|
-
### Changing Models
|
|
102
|
-
|
|
103
|
-
When you switch to a model with different dimensions (e.g., 384 → 768):
|
|
104
|
-
|
|
105
|
-
1. The system detects the mismatch on startup
|
|
106
|
-
2. Vector search is **disabled** with a warning
|
|
107
|
-
3. Run `cf memory rebuild` to re-embed all memories
|
|
108
|
-
4. Vector search is re-enabled with the new dimensions
|
|
109
|
-
|
|
110
|
-
Markdown files are unaffected — they remain the source of truth.
|
|
111
|
-
|
|
112
|
-
### Dynamic Dimensions
|
|
113
|
-
|
|
114
|
-
CF Memory includes a lookup table of known models and their dimensions. For models not in the table, it defaults to 384 dimensions. If you use an exotic model, the system logs a warning suggesting you verify dimensions.
|
|
115
|
-
|
|
116
|
-
Known models include: all-MiniLM-L6-v2 (384d), nomic-embed-text (768d), mxbai-embed-large (1024d), snowflake-arctic-embed variants, and BGE variants.
|
|
117
|
-
|
|
118
|
-
## Project Structure
|
|
119
|
-
|
|
120
|
-
```
|
|
121
|
-
src/
|
|
122
|
-
├── index.ts # MCP server entry (stdio transport)
|
|
123
|
-
├── server.ts # Tool registration
|
|
124
|
-
├── bin/
|
|
125
|
-
│ └── cf-memory.ts # CLI entry point
|
|
126
|
-
├── tools/ # MCP tool handlers
|
|
127
|
-
│ ├── store.ts
|
|
128
|
-
│ ├── search.ts
|
|
129
|
-
│ ├── retrieve.ts
|
|
130
|
-
│ ├── list.ts
|
|
131
|
-
│ ├── update.ts
|
|
132
|
-
│ └── delete.ts
|
|
133
|
-
├── resources/
|
|
134
|
-
│ └── index.ts # memory://index + memory://stats
|
|
135
|
-
├── backends/
|
|
136
|
-
│ ├── markdown.ts # Tier 3: file-based backend
|
|
137
|
-
│ ├── minisearch.ts # Tier 2: in-memory search backend
|
|
138
|
-
│ └── sqlite/
|
|
139
|
-
│ ├── index.ts # Tier 1: SQLite backend
|
|
140
|
-
│ ├── schema.ts # DB schema + PRAGMAs
|
|
141
|
-
│ ├── migrations.ts # Version tracking, vec table
|
|
142
|
-
│ ├── search.ts # FTS5 + vector + RRF hybrid search
|
|
143
|
-
│ └── embeddings.ts # Ollama / Transformers.js pipeline
|
|
144
|
-
├── daemon/
|
|
145
|
-
│ ├── entry.ts # Daemon process entry
|
|
146
|
-
│ ├── process.ts # Start/stop, PID management
|
|
147
|
-
│ ├── server.ts # Hono HTTP app (UDS)
|
|
148
|
-
│ └── watcher.ts # File watcher + index rebuild
|
|
149
|
-
├── lib/
|
|
150
|
-
│ ├── types.ts # MemoryType, Memory, SearchResult, etc.
|
|
151
|
-
│ ├── backend.ts # MemoryBackend interface (8 methods)
|
|
152
|
-
│ ├── tier.ts # detectTier() + createBackendForTier()
|
|
153
|
-
│ ├── daemon-client.ts # HTTP client over Unix Domain Socket
|
|
154
|
-
│ ├── lazy-install.ts # On-demand dep installer
|
|
155
|
-
│ ├── dedup.ts # Jaccard similarity deduplication
|
|
156
|
-
│ ├── temporal-decay.ts # Recency + access scoring
|
|
157
|
-
│ └── ollama.ts # Ollama detection + embedding provider
|
|
158
|
-
└── __tests__/ # 14 test suites (vitest)
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
## Development
|
|
162
|
-
|
|
163
|
-
### Prerequisites
|
|
164
|
-
|
|
165
|
-
- Node.js 22+
|
|
166
|
-
- npm
|
|
167
|
-
|
|
168
|
-
### Setup
|
|
169
|
-
|
|
170
|
-
```bash
|
|
171
|
-
cd cli/lib/cf-memory
|
|
172
|
-
npm install
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
### Scripts
|
|
176
|
-
|
|
177
|
-
| Command | Description |
|
|
178
|
-
| -------------------- | ------------------------------------- |
|
|
179
|
-
| `npm run build` | Compile TypeScript → `dist/` |
|
|
180
|
-
| `npm run dev` | Run MCP server directly via tsx |
|
|
181
|
-
| `npm run dev:watch` | Watch mode — recompile on changes |
|
|
182
|
-
| `npm start` | Run compiled server (`dist/index.js`) |
|
|
183
|
-
| `npm test` | Run all tests (vitest) |
|
|
184
|
-
| `npm run test:watch` | Run tests in watch mode |
|
|
185
|
-
|
|
186
|
-
### Dev mode and `cf dev sync`
|
|
187
|
-
|
|
188
|
-
CF Memory lives in `cli/lib/cf-memory/` (part of the CLI package), **not** in `plugin/`. This means `cf dev sync` is **not needed** — the CLI resolves cf-memory via `getLibPath()` which points directly to `cli/lib/cf-memory/dist/`.
|
|
189
|
-
|
|
190
|
-
| Consumer | Needs `cf dev sync`? | How changes take effect |
|
|
191
|
-
| ----------------------------------- | --------------------------------- | ---------------------------------------------------------------------------- |
|
|
192
|
-
| CLI commands (`cf memory *`) | No | `dev:watch` recompiles `dist/` → picked up on next CLI invocation |
|
|
193
|
-
| MCP server (if configured) | No | Needs MCP server **restart** (long-running process) |
|
|
194
|
-
| Plugin skills (cf-fix, cf-ask, ...) | Yes (for skill file changes only) | Skills reference MCP tools by name — cf-memory code itself doesn't need sync |
|
|
195
|
-
|
|
196
|
-
In short: run `npm run dev:watch` (or `npm run dev` from the monorepo root) and your changes are immediately available to CLI commands. If you're running the MCP server, restart it after changes.
|
|
197
|
-
|
|
198
|
-
### Running the MCP server locally
|
|
199
|
-
|
|
200
|
-
```bash
|
|
201
|
-
# Via tsx (no build needed)
|
|
202
|
-
npm run dev
|
|
203
|
-
|
|
204
|
-
# Or build first, then run
|
|
205
|
-
npm run build
|
|
206
|
-
npm start
|
|
207
|
-
|
|
208
|
-
# With custom docs directory
|
|
209
|
-
MEMORY_DOCS_DIR=/path/to/docs/memory npm run dev
|
|
210
|
-
|
|
211
|
-
# Force a specific tier
|
|
212
|
-
MEMORY_TIER=markdown npm run dev # Options: auto | full | lite | markdown
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
### Running tests
|
|
216
|
-
|
|
217
|
-
```bash
|
|
218
|
-
npm test # Run once
|
|
219
|
-
npm run test:watch # Watch mode
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
### Working with the daemon (Tier 2)
|
|
223
|
-
|
|
224
|
-
```bash
|
|
225
|
-
# Start daemon manually (for testing)
|
|
226
|
-
node dist/daemon/entry.js ./docs/memory
|
|
227
|
-
|
|
228
|
-
# With idle timeout (ms) and tier
|
|
229
|
-
node dist/daemon/entry.js ./docs/memory 0 --tier=lite
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
The daemon runs a Hono HTTP server on a Unix Domain Socket at `~/.coding-friend/memory/daemon.sock`, with PID tracking at `~/.coding-friend/memory/daemon.pid`.
|
|
233
|
-
|
|
234
|
-
**Auto-reconnect with retry:** If the daemon dies (e.g., crash or explicit stop), the `DaemonClient` automatically respawns it on the next request with up to 3 retry attempts (1-second delay between retries). If all retries fail, it falls back to Tier 3 (markdown grep). This means mid-session daemon restarts are transparent — no manual intervention needed.
|
|
235
|
-
|
|
236
|
-
### Lazy dependencies (Tier 1)
|
|
237
|
-
|
|
238
|
-
Heavy dependencies for Tier 1 (SQLite + embeddings) are installed on-demand into `~/.coding-friend/memory/node_modules/`, not in this package's `node_modules/`. These include:
|
|
239
|
-
|
|
240
|
-
- `better-sqlite3` — native SQLite driver
|
|
241
|
-
- `sqlite-vec` — vector search extension
|
|
242
|
-
- `@huggingface/transformers` — local embeddings (fallback when Ollama is unavailable)
|
|
243
|
-
|
|
244
|
-
To install them manually: `cf memory init`
|
|
245
|
-
|
|
246
|
-
### Migration script
|
|
247
|
-
|
|
248
|
-
For migrating existing `docs/memory/` files to the current frontmatter format:
|
|
249
|
-
|
|
250
|
-
```bash
|
|
251
|
-
npx tsx scripts/migrate-frontmatter.ts [docsDir]
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
## Integration with the CLI
|
|
255
|
-
|
|
256
|
-
### MCP setup for end users
|
|
257
|
-
|
|
258
|
-
Users don't need to manually configure the MCP server. `cf init` includes a "CF Memory MCP" step that asks to add `coding-friend-memory` to the project's `.mcp.json` automatically. If skipped during init, users can run `cf memory mcp` to get the config for manual setup.
|
|
259
|
-
|
|
260
|
-
### CLI commands
|
|
261
|
-
|
|
262
|
-
The `cf` CLI exposes memory commands that use this package:
|
|
263
|
-
|
|
264
|
-
| Command | Description |
|
|
265
|
-
| -------------------------- | -------------------------------------------------------------------------------------------------------------------- |
|
|
266
|
-
| `cf memory status` | Show current tier, daemon status, memory count |
|
|
267
|
-
| `cf memory search <query>` | Search memories from the terminal |
|
|
268
|
-
| `cf memory list` | List all stored memories |
|
|
269
|
-
| `cf memory start-daemon` | Start the MiniSearch daemon (Tier 2) |
|
|
270
|
-
| `cf memory stop-daemon` | Stop the daemon |
|
|
271
|
-
| `cf memory rebuild` | Rebuild search index (Tier 1 direct or via daemon) |
|
|
272
|
-
| `cf memory init` | Install Tier 1 deps + import existing memories into SQLite (see [prerequisites](#prerequisites-for-tier-1-on-linux)) |
|
|
273
|
-
| `cf memory mcp` | Print MCP server config for use in Claude Desktop / other clients |
|
|
274
|
-
|
|
275
|
-
## Prerequisites for Tier 1 on Linux
|
|
276
|
-
|
|
277
|
-
Tier 1 uses `better-sqlite3` and `sqlite-vec`, which are native Node.js modules requiring C++ compilation. On a fresh Linux install, you need build tools before running `cf memory init`:
|
|
278
|
-
|
|
279
|
-
**Ubuntu/Debian:**
|
|
280
|
-
|
|
281
|
-
```bash
|
|
282
|
-
sudo apt update
|
|
283
|
-
sudo apt install -y build-essential python3
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
**Fedora/RHEL:**
|
|
287
|
-
|
|
288
|
-
```bash
|
|
289
|
-
sudo dnf groupinstall "Development Tools"
|
|
290
|
-
sudo dnf install python3
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
**Arch Linux:**
|
|
294
|
-
|
|
295
|
-
```bash
|
|
296
|
-
sudo pacman -S base-devel python
|
|
297
|
-
```
|
|
298
|
-
|
|
299
|
-
If these are missing, `cf memory init` will fail at the "Installing SQLite dependencies" step. You can still use Tier 2 (lite) or Tier 3 (markdown) without native dependencies — choose them during the init wizard.
|
|
300
|
-
|
|
301
|
-
**macOS** users need Xcode Command Line Tools: `xcode-select --install`.
|
|
302
|
-
|
|
303
|
-
## Environment Variables
|
|
304
|
-
|
|
305
|
-
| Variable | Default | Description |
|
|
306
|
-
| ----------------------------- | ------------------------ | ------------------------------------------------- |
|
|
307
|
-
| `MEMORY_DOCS_DIR` | `./docs/memory` | Path to memory storage directory |
|
|
308
|
-
| `MEMORY_TIER` | `auto` | Force a tier: `auto`, `full`, `lite`, `markdown` |
|
|
309
|
-
| `MEMORY_DAEMON_IDLE_TIMEOUT` | `0` (no timeout) | Daemon idle timeout in ms (`0` = never auto-stop) |
|
|
310
|
-
| `MEMORY_EMBEDDING_PROVIDER` | `transformers` | Embedding provider: `transformers` or `ollama` |
|
|
311
|
-
| `MEMORY_EMBEDDING_MODEL` | (provider default) | Embedding model name (e.g., `nomic-embed-text`) |
|
|
312
|
-
| `MEMORY_EMBEDDING_OLLAMA_URL` | `http://localhost:11434` | Ollama server URL |
|
|
313
|
-
| `OLLAMA_HOST` | `http://localhost:11434` | Ollama server URL (used by Ollama auto-detection) |
|
|
314
|
-
|
|
315
|
-
## Versioning
|
|
316
|
-
|
|
317
|
-
Part of the [coding-friend](https://github.com/dinhanhthi/coding-friend) monorepo. Versioned and released as part of the CLI package. Changelog: [`cli/CHANGELOG.md`](../../CHANGELOG.md).
|
|
@@ -1,343 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { mkdirSync, rmSync, writeFileSync, readFileSync, existsSync } from "fs";
|
|
3
|
-
import { join } from "path";
|
|
4
|
-
import { tmpdir } from "os";
|
|
5
|
-
import {
|
|
6
|
-
syncToClaudeMd,
|
|
7
|
-
removeFromClaudeMd,
|
|
8
|
-
updateInClaudeMd,
|
|
9
|
-
claudeMdPath,
|
|
10
|
-
SECTION_HEADER,
|
|
11
|
-
} from "../lib/claude-md.js";
|
|
12
|
-
|
|
13
|
-
let testDir: string;
|
|
14
|
-
let claudeMdFile: string;
|
|
15
|
-
let counter = 0;
|
|
16
|
-
|
|
17
|
-
beforeEach(() => {
|
|
18
|
-
testDir = join(tmpdir(), `cf-claude-md-test-${Date.now()}-${++counter}`);
|
|
19
|
-
mkdirSync(testDir, { recursive: true });
|
|
20
|
-
claudeMdFile = join(testDir, "CLAUDE.md");
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
afterEach(() => {
|
|
24
|
-
rmSync(testDir, { recursive: true, force: true });
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
describe("syncToClaudeMd", () => {
|
|
28
|
-
it("creates CLAUDE.md with section if it does not exist", () => {
|
|
29
|
-
syncToClaudeMd(
|
|
30
|
-
testDir,
|
|
31
|
-
"conventions/code-style",
|
|
32
|
-
"Use 2-space indentation",
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
expect(existsSync(claudeMdFile)).toBe(true);
|
|
36
|
-
const content = readFileSync(claudeMdFile, "utf-8");
|
|
37
|
-
expect(content).toContain(SECTION_HEADER);
|
|
38
|
-
expect(content).toContain("Use 2-space indentation");
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("appends section to existing CLAUDE.md that has no memory section", () => {
|
|
42
|
-
writeFileSync(claudeMdFile, "# My Project\n\nSome existing content.\n");
|
|
43
|
-
|
|
44
|
-
syncToClaudeMd(
|
|
45
|
-
testDir,
|
|
46
|
-
"conventions/code-style",
|
|
47
|
-
"Use 2-space indentation",
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
const content = readFileSync(claudeMdFile, "utf-8");
|
|
51
|
-
expect(content).toContain("# My Project");
|
|
52
|
-
expect(content).toContain("Some existing content.");
|
|
53
|
-
expect(content).toContain(SECTION_HEADER);
|
|
54
|
-
expect(content).toContain("Use 2-space indentation");
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("adds entry to existing memory section", () => {
|
|
58
|
-
writeFileSync(
|
|
59
|
-
claudeMdFile,
|
|
60
|
-
`# My Project\n\n${SECTION_HEADER}\n\n- Use 2-space indentation\n`,
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
syncToClaudeMd(
|
|
64
|
-
testDir,
|
|
65
|
-
"conventions/naming",
|
|
66
|
-
"Use camelCase for variables",
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
const content = readFileSync(claudeMdFile, "utf-8");
|
|
70
|
-
expect(content).toContain("Use 2-space indentation");
|
|
71
|
-
expect(content).toContain("Use camelCase for variables");
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("does not duplicate entries with same ID", () => {
|
|
75
|
-
syncToClaudeMd(
|
|
76
|
-
testDir,
|
|
77
|
-
"conventions/code-style",
|
|
78
|
-
"Use 2-space indentation",
|
|
79
|
-
);
|
|
80
|
-
syncToClaudeMd(
|
|
81
|
-
testDir,
|
|
82
|
-
"conventions/code-style",
|
|
83
|
-
"Use 4-space indentation",
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
const content = readFileSync(claudeMdFile, "utf-8");
|
|
87
|
-
// Should have the updated version, not both
|
|
88
|
-
expect(content).toContain("Use 4-space indentation");
|
|
89
|
-
expect(content).not.toContain("Use 2-space indentation");
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("stores entry with ID as HTML comment for tracking", () => {
|
|
93
|
-
syncToClaudeMd(
|
|
94
|
-
testDir,
|
|
95
|
-
"conventions/code-style",
|
|
96
|
-
"Use 2-space indentation",
|
|
97
|
-
);
|
|
98
|
-
|
|
99
|
-
const content = readFileSync(claudeMdFile, "utf-8");
|
|
100
|
-
expect(content).toContain("<!-- cf:conventions/code-style -->");
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it("handles multi-line description by taking first line only", () => {
|
|
104
|
-
syncToClaudeMd(
|
|
105
|
-
testDir,
|
|
106
|
-
"conventions/style",
|
|
107
|
-
"Line one\nLine two\nLine three",
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
const content = readFileSync(claudeMdFile, "utf-8");
|
|
111
|
-
expect(content).toContain("Line one");
|
|
112
|
-
// Multi-line content should be joined into a single bullet
|
|
113
|
-
expect(content.split(SECTION_HEADER)[1]).not.toContain("\nLine two");
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it("preserves content after the memory section", () => {
|
|
117
|
-
writeFileSync(
|
|
118
|
-
claudeMdFile,
|
|
119
|
-
`# My Project\n\n${SECTION_HEADER}\n\n- Old entry <!-- cf:conventions/old -->\n\n## Other Section\n\nMore content.\n`,
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
syncToClaudeMd(testDir, "conventions/new-rule", "New convention");
|
|
123
|
-
|
|
124
|
-
const content = readFileSync(claudeMdFile, "utf-8");
|
|
125
|
-
expect(content).toContain("## Other Section");
|
|
126
|
-
expect(content).toContain("More content.");
|
|
127
|
-
expect(content).toContain("New convention");
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
describe("removeFromClaudeMd", () => {
|
|
132
|
-
it("removes an entry by ID", () => {
|
|
133
|
-
syncToClaudeMd(
|
|
134
|
-
testDir,
|
|
135
|
-
"conventions/code-style",
|
|
136
|
-
"Use 2-space indentation",
|
|
137
|
-
);
|
|
138
|
-
syncToClaudeMd(testDir, "conventions/naming", "Use camelCase");
|
|
139
|
-
|
|
140
|
-
removeFromClaudeMd(testDir, "conventions/code-style");
|
|
141
|
-
|
|
142
|
-
const content = readFileSync(claudeMdFile, "utf-8");
|
|
143
|
-
expect(content).not.toContain("Use 2-space indentation");
|
|
144
|
-
expect(content).not.toContain("conventions/code-style");
|
|
145
|
-
expect(content).toContain("Use camelCase");
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it("does nothing if CLAUDE.md does not exist", () => {
|
|
149
|
-
// Should not throw
|
|
150
|
-
removeFromClaudeMd(testDir, "conventions/code-style");
|
|
151
|
-
expect(existsSync(claudeMdFile)).toBe(false);
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it("does nothing if ID is not found", () => {
|
|
155
|
-
writeFileSync(
|
|
156
|
-
claudeMdFile,
|
|
157
|
-
`# Project\n\n${SECTION_HEADER}\n\n- Some entry <!-- cf:conventions/other -->\n`,
|
|
158
|
-
);
|
|
159
|
-
|
|
160
|
-
removeFromClaudeMd(testDir, "conventions/nonexistent");
|
|
161
|
-
|
|
162
|
-
const content = readFileSync(claudeMdFile, "utf-8");
|
|
163
|
-
expect(content).toContain("Some entry");
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it("removes section header when last entry is removed", () => {
|
|
167
|
-
syncToClaudeMd(testDir, "conventions/only-one", "The only convention");
|
|
168
|
-
|
|
169
|
-
removeFromClaudeMd(testDir, "conventions/only-one");
|
|
170
|
-
|
|
171
|
-
const content = readFileSync(claudeMdFile, "utf-8");
|
|
172
|
-
expect(content).not.toContain(SECTION_HEADER);
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
describe("updateInClaudeMd", () => {
|
|
177
|
-
it("updates the description of an existing entry", () => {
|
|
178
|
-
syncToClaudeMd(testDir, "conventions/style", "Old style guide");
|
|
179
|
-
|
|
180
|
-
updateInClaudeMd(testDir, "conventions/style", "New style guide");
|
|
181
|
-
|
|
182
|
-
const content = readFileSync(claudeMdFile, "utf-8");
|
|
183
|
-
expect(content).toContain("New style guide");
|
|
184
|
-
expect(content).not.toContain("Old style guide");
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
it("does nothing if ID is not found (no upsert)", () => {
|
|
188
|
-
writeFileSync(claudeMdFile, "# Project\n");
|
|
189
|
-
|
|
190
|
-
updateInClaudeMd(testDir, "conventions/nonexistent", "Something");
|
|
191
|
-
|
|
192
|
-
const content = readFileSync(claudeMdFile, "utf-8");
|
|
193
|
-
expect(content).not.toContain("Something");
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it("does nothing if CLAUDE.md does not exist", () => {
|
|
197
|
-
updateInClaudeMd(testDir, "conventions/style", "Something");
|
|
198
|
-
expect(existsSync(claudeMdFile)).toBe(false);
|
|
199
|
-
});
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
describe("claudeMdPath", () => {
|
|
203
|
-
it("strips /docs/memory suffix to derive project root", () => {
|
|
204
|
-
const docsDir = join(testDir, "docs", "memory");
|
|
205
|
-
mkdirSync(docsDir, { recursive: true });
|
|
206
|
-
|
|
207
|
-
const result = claudeMdPath(docsDir);
|
|
208
|
-
expect(result).toBe(join(testDir, "CLAUDE.md"));
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it("strips /memory suffix as fallback", () => {
|
|
212
|
-
const docsDir = join(testDir, "memory");
|
|
213
|
-
mkdirSync(docsDir, { recursive: true });
|
|
214
|
-
|
|
215
|
-
const result = claudeMdPath(docsDir);
|
|
216
|
-
expect(result).toBe(join(testDir, "CLAUDE.md"));
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
it("uses docsDir as-is when no known suffix matches", () => {
|
|
220
|
-
const result = claudeMdPath(testDir);
|
|
221
|
-
expect(result).toBe(join(testDir, "CLAUDE.md"));
|
|
222
|
-
});
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
describe("syncToClaudeMd with realistic docsDir", () => {
|
|
226
|
-
it("writes CLAUDE.md to project root when docsDir is docs/memory", () => {
|
|
227
|
-
const docsDir = join(testDir, "docs", "memory");
|
|
228
|
-
mkdirSync(docsDir, { recursive: true });
|
|
229
|
-
|
|
230
|
-
syncToClaudeMd(docsDir, "conventions/style", "Use tabs");
|
|
231
|
-
|
|
232
|
-
const expectedPath = join(testDir, "CLAUDE.md");
|
|
233
|
-
expect(existsSync(expectedPath)).toBe(true);
|
|
234
|
-
const content = readFileSync(expectedPath, "utf-8");
|
|
235
|
-
expect(content).toContain("Use tabs");
|
|
236
|
-
expect(content).toContain(SECTION_HEADER);
|
|
237
|
-
});
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
describe("migration from old header", () => {
|
|
241
|
-
const OLD_HEADER = "## CF Memory: Conventions";
|
|
242
|
-
|
|
243
|
-
it("reads entries from old header and rewrites with new header on sync", () => {
|
|
244
|
-
writeFileSync(
|
|
245
|
-
claudeMdFile,
|
|
246
|
-
`# My Project\n\n${OLD_HEADER}\n\n- Old rule <!-- cf:conventions/old-rule -->\n`,
|
|
247
|
-
);
|
|
248
|
-
|
|
249
|
-
syncToClaudeMd(testDir, "conventions/new-rule", "New rule");
|
|
250
|
-
|
|
251
|
-
const content = readFileSync(claudeMdFile, "utf-8");
|
|
252
|
-
// Should have new header
|
|
253
|
-
expect(content).toContain(SECTION_HEADER);
|
|
254
|
-
// Should NOT have old header (unless they're the same)
|
|
255
|
-
if (SECTION_HEADER !== OLD_HEADER) {
|
|
256
|
-
expect(content).not.toContain(OLD_HEADER);
|
|
257
|
-
}
|
|
258
|
-
// Both entries should be preserved
|
|
259
|
-
expect(content).toContain("Old rule");
|
|
260
|
-
expect(content).toContain("New rule");
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
it("removes entries from old header format", () => {
|
|
264
|
-
writeFileSync(
|
|
265
|
-
claudeMdFile,
|
|
266
|
-
`# My Project\n\n${OLD_HEADER}\n\n- Old rule <!-- cf:conventions/old-rule -->\n- Keep this <!-- cf:conventions/keep -->\n`,
|
|
267
|
-
);
|
|
268
|
-
|
|
269
|
-
removeFromClaudeMd(testDir, "conventions/old-rule");
|
|
270
|
-
|
|
271
|
-
const content = readFileSync(claudeMdFile, "utf-8");
|
|
272
|
-
expect(content).not.toContain("Old rule");
|
|
273
|
-
expect(content).toContain("Keep this");
|
|
274
|
-
});
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
describe("syncToClaudeMd with non-convention categories", () => {
|
|
278
|
-
it("syncs decisions entries to CLAUDE.md", () => {
|
|
279
|
-
syncToClaudeMd(
|
|
280
|
-
testDir,
|
|
281
|
-
"decisions/api-versioning",
|
|
282
|
-
"Always use URL-based API versioning (v1, v2)",
|
|
283
|
-
);
|
|
284
|
-
|
|
285
|
-
const content = readFileSync(claudeMdFile, "utf-8");
|
|
286
|
-
expect(content).toContain(SECTION_HEADER);
|
|
287
|
-
expect(content).toContain("Always use URL-based API versioning");
|
|
288
|
-
expect(content).toContain("<!-- cf:decisions/api-versioning -->");
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
it("syncs infrastructure entries to CLAUDE.md", () => {
|
|
292
|
-
syncToClaudeMd(
|
|
293
|
-
testDir,
|
|
294
|
-
"infrastructure/deploy-checklist",
|
|
295
|
-
"Always run migrations before deploying",
|
|
296
|
-
);
|
|
297
|
-
|
|
298
|
-
const content = readFileSync(claudeMdFile, "utf-8");
|
|
299
|
-
expect(content).toContain("Always run migrations before deploying");
|
|
300
|
-
expect(content).toContain("<!-- cf:infrastructure/deploy-checklist -->");
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
it("mixes convention and non-convention entries in same section", () => {
|
|
304
|
-
syncToClaudeMd(testDir, "conventions/naming", "Use camelCase");
|
|
305
|
-
syncToClaudeMd(
|
|
306
|
-
testDir,
|
|
307
|
-
"decisions/db-choice",
|
|
308
|
-
"Use PostgreSQL for all new services",
|
|
309
|
-
);
|
|
310
|
-
|
|
311
|
-
const content = readFileSync(claudeMdFile, "utf-8");
|
|
312
|
-
expect(content).toContain("Use camelCase");
|
|
313
|
-
expect(content).toContain("Use PostgreSQL for all new services");
|
|
314
|
-
// Only one section header
|
|
315
|
-
const headerCount = content.split(SECTION_HEADER).length - 1;
|
|
316
|
-
expect(headerCount).toBe(1);
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
it("removes non-convention entries by ID", () => {
|
|
320
|
-
syncToClaudeMd(testDir, "decisions/api-versioning", "URL-based versioning");
|
|
321
|
-
syncToClaudeMd(testDir, "conventions/naming", "Use camelCase");
|
|
322
|
-
|
|
323
|
-
removeFromClaudeMd(testDir, "decisions/api-versioning");
|
|
324
|
-
|
|
325
|
-
const content = readFileSync(claudeMdFile, "utf-8");
|
|
326
|
-
expect(content).not.toContain("URL-based versioning");
|
|
327
|
-
expect(content).toContain("Use camelCase");
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
it("updates non-convention entries", () => {
|
|
331
|
-
syncToClaudeMd(testDir, "infrastructure/ci-rule", "Run lint before tests");
|
|
332
|
-
|
|
333
|
-
updateInClaudeMd(
|
|
334
|
-
testDir,
|
|
335
|
-
"infrastructure/ci-rule",
|
|
336
|
-
"Run lint and type-check before tests",
|
|
337
|
-
);
|
|
338
|
-
|
|
339
|
-
const content = readFileSync(claudeMdFile, "utf-8");
|
|
340
|
-
expect(content).toContain("Run lint and type-check before tests");
|
|
341
|
-
expect(content).not.toContain("Run lint before tests\n");
|
|
342
|
-
});
|
|
343
|
-
});
|