claude-eidetic 0.1.1 → 0.1.3
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 +333 -0
- package/dist/config.d.ts +25 -0
- package/dist/config.js +29 -10
- package/dist/core/cleanup.d.ts +8 -0
- package/dist/core/cleanup.js +41 -0
- package/dist/core/doc-indexer.d.ts +13 -0
- package/dist/core/doc-indexer.js +76 -0
- package/dist/core/doc-searcher.d.ts +13 -0
- package/dist/core/doc-searcher.js +65 -0
- package/dist/core/file-category.d.ts +7 -0
- package/dist/core/file-category.js +75 -0
- package/dist/core/indexer.js +12 -4
- package/dist/core/preview.d.ts +1 -2
- package/dist/core/preview.js +2 -5
- package/dist/core/repo-map.d.ts +33 -0
- package/dist/core/repo-map.js +144 -0
- package/dist/core/searcher.d.ts +1 -13
- package/dist/core/searcher.js +20 -24
- package/dist/core/snapshot-io.js +2 -2
- package/dist/core/sync.d.ts +5 -25
- package/dist/core/sync.js +90 -65
- package/dist/core/targeted-indexer.d.ts +19 -0
- package/dist/core/targeted-indexer.js +127 -0
- package/dist/embedding/factory.d.ts +0 -13
- package/dist/embedding/factory.js +0 -17
- package/dist/embedding/openai.d.ts +2 -14
- package/dist/embedding/openai.js +7 -20
- package/dist/errors.d.ts +2 -0
- package/dist/errors.js +2 -0
- package/dist/format.d.ts +12 -0
- package/dist/format.js +160 -31
- package/dist/hooks/post-tool-use.d.ts +13 -0
- package/dist/hooks/post-tool-use.js +113 -0
- package/dist/hooks/stop-hook.d.ts +11 -0
- package/dist/hooks/stop-hook.js +121 -0
- package/dist/hooks/targeted-runner.d.ts +11 -0
- package/dist/hooks/targeted-runner.js +66 -0
- package/dist/index.js +90 -31
- package/dist/infra/qdrant-bootstrap.js +14 -12
- package/dist/memory/history.d.ts +19 -0
- package/dist/memory/history.js +40 -0
- package/dist/memory/llm.d.ts +2 -0
- package/dist/memory/llm.js +56 -0
- package/dist/memory/prompts.d.ts +5 -0
- package/dist/memory/prompts.js +36 -0
- package/dist/memory/reconciler.d.ts +12 -0
- package/dist/memory/reconciler.js +36 -0
- package/dist/memory/store.d.ts +20 -0
- package/dist/memory/store.js +206 -0
- package/dist/memory/types.d.ts +28 -0
- package/dist/memory/types.js +2 -0
- package/dist/paths.d.ts +3 -4
- package/dist/paths.js +14 -4
- package/dist/precompact/hook.d.ts +9 -0
- package/dist/precompact/hook.js +170 -0
- package/dist/precompact/index-runner.d.ts +9 -0
- package/dist/precompact/index-runner.js +52 -0
- package/dist/precompact/note-writer.d.ts +15 -0
- package/dist/precompact/note-writer.js +109 -0
- package/dist/precompact/session-indexer.d.ts +13 -0
- package/dist/precompact/session-indexer.js +31 -0
- package/dist/precompact/tier0-inject.d.ts +16 -0
- package/dist/precompact/tier0-inject.js +88 -0
- package/dist/precompact/tier0-writer.d.ts +16 -0
- package/dist/precompact/tier0-writer.js +74 -0
- package/dist/precompact/transcript-parser.d.ts +10 -0
- package/dist/precompact/transcript-parser.js +148 -0
- package/dist/precompact/types.d.ts +93 -0
- package/dist/precompact/types.js +5 -0
- package/dist/precompact/utils.d.ts +29 -0
- package/dist/precompact/utils.js +95 -0
- package/dist/setup-message.d.ts +2 -2
- package/dist/setup-message.js +39 -20
- package/dist/splitter/ast.js +84 -22
- package/dist/splitter/line.d.ts +0 -4
- package/dist/splitter/line.js +1 -7
- package/dist/splitter/symbol-extract.d.ts +16 -0
- package/dist/splitter/symbol-extract.js +61 -0
- package/dist/splitter/types.d.ts +5 -0
- package/dist/splitter/types.js +1 -1
- package/dist/state/doc-metadata.d.ts +18 -0
- package/dist/state/doc-metadata.js +59 -0
- package/dist/state/registry.d.ts +1 -3
- package/dist/state/snapshot.d.ts +0 -1
- package/dist/state/snapshot.js +3 -19
- package/dist/tool-schemas.d.ts +251 -1
- package/dist/tool-schemas.js +331 -24
- package/dist/tools.d.ts +69 -0
- package/dist/tools.js +286 -17
- package/dist/vectordb/milvus.d.ts +7 -5
- package/dist/vectordb/milvus.js +116 -19
- package/dist/vectordb/qdrant.d.ts +8 -10
- package/dist/vectordb/qdrant.js +105 -33
- package/dist/vectordb/types.d.ts +20 -0
- package/messages.yaml +50 -0
- package/package.json +87 -62
package/README.md
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
# claude-eidetic
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
[](https://github.com/eidetics/claude-eidetic/actions/workflows/ci.yml)
|
|
5
|
+
[](https://www.npmjs.com/package/claude-eidetic)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
Semantic code search, persistent memory, and session continuity for Claude Code. One plugin, not three.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 🚀 Quick Start
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
claude plugin install eidetics/claude-eidetic
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
export OPENAI_API_KEY=sk-... # for embeddings (default)
|
|
20
|
+
export ANTHROPIC_API_KEY=sk-ant-... # for memory extraction (default)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Index your codebase once, then search by meaning:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
index_codebase(path="/your/project")
|
|
27
|
+
search_code("how does authentication work")
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## ✨ Features
|
|
33
|
+
|
|
34
|
+
### 🔍 Semantic Code Search
|
|
35
|
+
|
|
36
|
+
**Find code by meaning, not keywords.** Search across your entire codebase with natural language, returning the most relevant functions and classes, not a list of files to read.
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
search_code("how does the retry logic work")
|
|
40
|
+
search_code("authentication middleware", extensionFilter=[".ts"])
|
|
41
|
+
search_code(project="backend", query="auth flow")
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 🏗️ Architecture at a Glance
|
|
45
|
+
|
|
46
|
+
**Get every class, function, and method in one call.** `browse_structure` returns a condensed map of your codebase with signatures, grouped by file, replacing a Glob + Read cascade with a single tool call.
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
browse_structure(path="/my/project", kind="class")
|
|
50
|
+
list_symbols(path="/my/project", nameFilter="handle")
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 📚 Documentation Cache
|
|
54
|
+
|
|
55
|
+
**Fetch docs once, search them forever.** Cache external documentation as searchable embeddings. Retrieve relevant passages instantly, far cheaper than re-fetching the same page each session.
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
index_document(content=..., library="react", topic="hooks", source="https://...")
|
|
59
|
+
search_documents("React useCallback dependencies", library="react")
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 🧠 Persistent Memory
|
|
63
|
+
|
|
64
|
+
**Claude remembers your preferences between sessions.** `add_memory` uses an LLM to extract structured facts from conversation text (coding style, architecture decisions, debugging insights) and deduplicates them semantically. Not a static config file you forget to update.
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
add_memory("Always use absolute imports, never relative")
|
|
68
|
+
search_memory("how does this team handle errors")
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 🔄 Session Continuity
|
|
72
|
+
|
|
73
|
+
**Every session picks up where the last one left off.** When a session ends (or context compacts mid-session), Eidetic automatically writes a structured note capturing files changed, tasks, commands, and decisions. `/catchup` at the start of a new session reconstructs exactly where you were. No user action required.
|
|
74
|
+
|
|
75
|
+
### 👻 Invisible Optimizations
|
|
76
|
+
|
|
77
|
+
Eight hook events fire automatically, nudging toward cheaper tools, redirecting file reads for 15-20% token savings, tracking changed files, and saving session state on exit.
|
|
78
|
+
|
|
79
|
+
<details>
|
|
80
|
+
<summary><strong>Hook event details</strong></summary>
|
|
81
|
+
|
|
82
|
+
| Hook | Trigger | What it does |
|
|
83
|
+
|---|---|---|
|
|
84
|
+
| `SessionStart` | Session opens | Validates config, injects last-session context |
|
|
85
|
+
| `UserPromptSubmit` | Every message | Nudges toward `search_code` over Grep/Explore for conceptual queries |
|
|
86
|
+
| `PreToolUse` (Read) | Before every Read | Blocks Read for text files, redirects to `read_file` for 15-20% token savings |
|
|
87
|
+
| `PreToolUse` (WebFetch / query-docs) | Before doc fetches | Suggests `search_documents` if library is cached (allows fetch either way) |
|
|
88
|
+
| `PostToolUse` (Write / Edit) | After every file write | Tracks changed files in a shadow git index |
|
|
89
|
+
| `Stop` | After Claude responds | Commits shadow index; triggers targeted re-index of changed files only |
|
|
90
|
+
| `PreCompact` | Before context compaction | Captures session state to notes before memory is lost |
|
|
91
|
+
| `SessionEnd` | Session closes | Writes session note (files, tasks, commands); extracts developer memories via LLM |
|
|
92
|
+
|
|
93
|
+
</details>
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 🗺️ When to Use What
|
|
98
|
+
|
|
99
|
+
| Need | Use | Notes |
|
|
100
|
+
|---|---|---|
|
|
101
|
+
| Find implementations by concept | `search_code` | Semantic, natural language queries |
|
|
102
|
+
| Exact string or regex match | Grep | Grep wins for exact matches |
|
|
103
|
+
| Find file by exact name | Glob | Glob wins for name patterns |
|
|
104
|
+
| Understand module structure | `browse_structure` | One call vs Glob + Read cascade |
|
|
105
|
+
| Read a specific known file | `read_file` | Cheaper than built-in Read for code files |
|
|
106
|
+
| Search cached documentation | `search_documents` | Far cheaper than re-fetching |
|
|
107
|
+
| Recall project conventions | `search_memory` | Global across all projects and sessions |
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## 📖 Skills Reference
|
|
112
|
+
|
|
113
|
+
| Skill | What it does |
|
|
114
|
+
|---|---|
|
|
115
|
+
| `/search` | Guided semantic search with best-practice prompts |
|
|
116
|
+
| `/index` | Index or re-index a codebase with dry-run option |
|
|
117
|
+
| `/cache-docs` | Fetch and cache external documentation |
|
|
118
|
+
| `/catchup` | Search session notes and reconstruct where you left off |
|
|
119
|
+
| `/wrapup` | Extract decisions, rationale, open questions, and next actions from the conversation |
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
<details>
|
|
124
|
+
<summary><strong>🔥 Why does this exist? (The Problem)</strong></summary>
|
|
125
|
+
|
|
126
|
+
Every new Claude Code session starts cold. You re-explain the architecture. You re-fetch the same docs. Claude reads the same files repeatedly, burning tokens just to get back to where you were.
|
|
127
|
+
|
|
128
|
+
| Task | Without Eidetic | With Eidetic |
|
|
129
|
+
|---|---|---|
|
|
130
|
+
| Find where auth errors are handled | Grep cascade, read 8 files, ~10,700 tokens | `search_code("auth error handling")` ~220 tokens |
|
|
131
|
+
| Resume after context compaction | Re-explain 20 min of context, ~2,000 tokens | `/catchup` ~200 tokens |
|
|
132
|
+
| Look up React hooks docs | Fetch docs page, ~5,000 tokens | `search_documents("React useEffect")` ~20 tokens |
|
|
133
|
+
| Read a 400-line file | Built-in Read with line numbers, ~900 tokens | `read_file(path)` ~740 tokens |
|
|
134
|
+
|
|
135
|
+
</details>
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## 📦 Installation
|
|
140
|
+
|
|
141
|
+
### Plugin (recommended)
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
claude plugin install eidetics/claude-eidetic
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The plugin auto-starts the MCP server, installs skills, and configures hooks.
|
|
148
|
+
|
|
149
|
+
### npx (manual MCP config)
|
|
150
|
+
|
|
151
|
+
Add to your `.mcp.json`:
|
|
152
|
+
|
|
153
|
+
```json
|
|
154
|
+
{
|
|
155
|
+
"mcpServers": {
|
|
156
|
+
"claude-eidetic": {
|
|
157
|
+
"command": "npx",
|
|
158
|
+
"args": ["-y", "claude-eidetic"],
|
|
159
|
+
"env": {
|
|
160
|
+
"OPENAI_API_KEY": "sk-...",
|
|
161
|
+
"ANTHROPIC_API_KEY": "sk-ant-..."
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`ANTHROPIC_API_KEY` is needed for the memory LLM (default provider). Omit it if using `MEMORY_LLM_PROVIDER=openai` or `ollama`.
|
|
169
|
+
|
|
170
|
+
### Global install
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
npm install -g claude-eidetic
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### From source
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
git clone https://github.com/eidetics/claude-eidetic
|
|
180
|
+
cd claude-eidetic
|
|
181
|
+
npm install && npx tsc && npm start
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Requirements
|
|
185
|
+
|
|
186
|
+
- Node.js >= 20.0.0
|
|
187
|
+
- An API key (OpenAI for embeddings, Anthropic for memory extraction, or Ollama for both free)
|
|
188
|
+
- Docker (optional): Qdrant auto-provisions via Docker if not already running
|
|
189
|
+
- C/C++ build tools: required by tree-sitter native bindings (`node-gyp`)
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## ⚙️ Configuration
|
|
194
|
+
|
|
195
|
+
All configuration is via environment variables. No config files.
|
|
196
|
+
|
|
197
|
+
### Using Ollama (free, local)
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
export EMBEDDING_PROVIDER=ollama
|
|
201
|
+
export MEMORY_LLM_PROVIDER=ollama
|
|
202
|
+
# No API keys needed
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
<details>
|
|
206
|
+
<summary><strong>Full configuration reference</strong></summary>
|
|
207
|
+
|
|
208
|
+
| Variable | Default | Description |
|
|
209
|
+
|---|---|---|
|
|
210
|
+
| `OPENAI_API_KEY` | _(required for openai)_ | OpenAI API key for embeddings and/or memory |
|
|
211
|
+
| `ANTHROPIC_API_KEY` | _(required for anthropic memory)_ | Anthropic API key for memory LLM |
|
|
212
|
+
| `EMBEDDING_PROVIDER` | `openai` | `openai`, `ollama`, or `local` |
|
|
213
|
+
| `EMBEDDING_MODEL` | `text-embedding-3-small` (openai) / `nomic-embed-text` (ollama) | Embedding model name |
|
|
214
|
+
| `EMBEDDING_BATCH_SIZE` | `100` | Batch size for embedding requests (1-2048) |
|
|
215
|
+
| `INDEXING_CONCURRENCY` | `8` | Parallel file indexing workers (1-32) |
|
|
216
|
+
| `OPENAI_BASE_URL` | _(none)_ | Custom OpenAI-compatible endpoint |
|
|
217
|
+
| `OLLAMA_BASE_URL` | `http://localhost:11434/v1` | Ollama server URL |
|
|
218
|
+
| `VECTORDB_PROVIDER` | `qdrant` | `qdrant` or `milvus` |
|
|
219
|
+
| `QDRANT_URL` | `http://localhost:6333` | Qdrant server URL |
|
|
220
|
+
| `QDRANT_API_KEY` | _(none)_ | Qdrant API key (for remote/cloud instances) |
|
|
221
|
+
| `MILVUS_ADDRESS` | `localhost:19530` | Milvus server address |
|
|
222
|
+
| `MILVUS_TOKEN` | _(none)_ | Milvus authentication token |
|
|
223
|
+
| `EIDETIC_DATA_DIR` | `~/.eidetic/` | Data root for snapshots, memory DB, registry |
|
|
224
|
+
| `CUSTOM_EXTENSIONS` | `[]` | JSON array of extra file extensions to index (e.g., `[".dart",".arb"]`) |
|
|
225
|
+
| `CUSTOM_IGNORE_PATTERNS` | `[]` | JSON array of glob patterns to exclude |
|
|
226
|
+
| `MEMORY_LLM_PROVIDER` | `anthropic` | `anthropic`, `openai`, or `ollama` |
|
|
227
|
+
| `MEMORY_LLM_MODEL` | `claude-haiku-4-5-20251001` (anthropic) / `gpt-4o-mini` (openai) / `llama3.2` (ollama) | Model for memory extraction |
|
|
228
|
+
| `MEMORY_LLM_BASE_URL` | _(none)_ | Custom base URL for memory LLM |
|
|
229
|
+
| `MEMORY_LLM_API_KEY` | _(none)_ | API key override for memory LLM |
|
|
230
|
+
|
|
231
|
+
</details>
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
<details>
|
|
236
|
+
<summary><strong>🔧 Tool Reference</strong></summary>
|
|
237
|
+
|
|
238
|
+
### 🔍 Code Search
|
|
239
|
+
|
|
240
|
+
| Tool | Description |
|
|
241
|
+
|---|---|
|
|
242
|
+
| `search_code` | Hybrid semantic search over indexed codebase. Returns compact results by default. |
|
|
243
|
+
| `index_codebase` | Index a directory. Supports `dryRun`, `force`, `customExtensions`, `customIgnorePatterns`. |
|
|
244
|
+
| `list_indexed` | List all indexed codebases with file/chunk counts and status. |
|
|
245
|
+
| `get_indexing_status` | Check indexing progress for a path or project. |
|
|
246
|
+
| `clear_index` | Remove the search index for a codebase. |
|
|
247
|
+
| `cleanup_vectors` | Remove orphaned vectors for deleted files. No re-embedding cost. |
|
|
248
|
+
| `browse_structure` | Condensed structural map: classes, functions, methods with signatures, grouped by file. |
|
|
249
|
+
| `list_symbols` | Compact symbol table with name/kind/file/line. Supports name, kind, and path filters. |
|
|
250
|
+
|
|
251
|
+
### 📄 File Reading
|
|
252
|
+
|
|
253
|
+
| Tool | Description |
|
|
254
|
+
|---|---|
|
|
255
|
+
| `read_file` | Read file without line-number overhead. Cheaper than built-in Read for code files. |
|
|
256
|
+
|
|
257
|
+
### 📚 Documentation Cache
|
|
258
|
+
|
|
259
|
+
| Tool | Description |
|
|
260
|
+
|---|---|
|
|
261
|
+
| `index_document` | Cache external documentation for semantic search. Supports TTL for staleness tracking. |
|
|
262
|
+
| `search_documents` | Search cached docs. Far cheaper than re-fetching the same page. |
|
|
263
|
+
|
|
264
|
+
### 🧠 Memory
|
|
265
|
+
|
|
266
|
+
| Tool | Description |
|
|
267
|
+
|---|---|
|
|
268
|
+
| `add_memory` | LLM-extracted facts from text. Auto-deduplicates. Seven categories. |
|
|
269
|
+
| `search_memory` | Semantic search over stored memories. Filterable by category. |
|
|
270
|
+
| `list_memories` | List all memories, optionally filtered by category. |
|
|
271
|
+
| `delete_memory` | Delete a specific memory by UUID. |
|
|
272
|
+
| `memory_history` | View change history for a memory (additions, updates, deletions). |
|
|
273
|
+
|
|
274
|
+
</details>
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## 🌐 Supported Languages
|
|
279
|
+
|
|
280
|
+
**AST-aware** (functions and classes chunked intact):
|
|
281
|
+
|
|
282
|
+
<p>
|
|
283
|
+
<img src="https://img.shields.io/badge/JavaScript-F7DF1E?style=flat-square&logo=javascript&logoColor=black" alt="JavaScript"/>
|
|
284
|
+
<img src="https://img.shields.io/badge/TypeScript-3178C6?style=flat-square&logo=typescript&logoColor=white" alt="TypeScript"/>
|
|
285
|
+
<img src="https://img.shields.io/badge/React_(JSX/TSX)-61DAFB?style=flat-square&logo=react&logoColor=black" alt="JSX/TSX"/>
|
|
286
|
+
<img src="https://img.shields.io/badge/Python-3776AB?style=flat-square&logo=python&logoColor=white" alt="Python"/>
|
|
287
|
+
<img src="https://img.shields.io/badge/Go-00ADD8?style=flat-square&logo=go&logoColor=white" alt="Go"/>
|
|
288
|
+
<img src="https://img.shields.io/badge/Java-ED8B00?style=flat-square&logo=openjdk&logoColor=white" alt="Java"/>
|
|
289
|
+
<img src="https://img.shields.io/badge/Rust-000000?style=flat-square&logo=rust&logoColor=white" alt="Rust"/>
|
|
290
|
+
<img src="https://img.shields.io/badge/C-A8B9CC?style=flat-square&logo=c&logoColor=black" alt="C"/>
|
|
291
|
+
<img src="https://img.shields.io/badge/C++-00599C?style=flat-square&logo=cplusplus&logoColor=white" alt="C++"/>
|
|
292
|
+
<img src="https://img.shields.io/badge/C%23-512BD4?style=flat-square&logo=csharp&logoColor=white" alt="C#"/>
|
|
293
|
+
</p>
|
|
294
|
+
|
|
295
|
+
**Line-based fallback** (sliding window chunking for everything else):
|
|
296
|
+
|
|
297
|
+
<p>
|
|
298
|
+
<img src="https://img.shields.io/badge/Markdown-000000?style=flat-square&logo=markdown&logoColor=white" alt="Markdown"/>
|
|
299
|
+
<img src="https://img.shields.io/badge/YAML-CB171E?style=flat-square&logo=yaml&logoColor=white" alt="YAML"/>
|
|
300
|
+
<img src="https://img.shields.io/badge/JSON-000000?style=flat-square&logo=json&logoColor=white" alt="JSON"/>
|
|
301
|
+
<img src="https://img.shields.io/badge/Ruby-CC342D?style=flat-square&logo=ruby&logoColor=white" alt="Ruby"/>
|
|
302
|
+
<img src="https://img.shields.io/badge/PHP-777BB4?style=flat-square&logo=php&logoColor=white" alt="PHP"/>
|
|
303
|
+
<img src="https://img.shields.io/badge/Swift-F05138?style=flat-square&logo=swift&logoColor=white" alt="Swift"/>
|
|
304
|
+
<img src="https://img.shields.io/badge/Kotlin-7F52FF?style=flat-square&logo=kotlin&logoColor=white" alt="Kotlin"/>
|
|
305
|
+
<img src="https://img.shields.io/badge/and_more...-30363d?style=flat-square" alt="and more"/>
|
|
306
|
+
</p>
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## 🛠️ Development
|
|
311
|
+
|
|
312
|
+
```bash
|
|
313
|
+
npm install && npx tsc # install and build
|
|
314
|
+
npm run dev # watch mode (tsx)
|
|
315
|
+
npm test # unit tests (no external services needed)
|
|
316
|
+
npm run test:integration # requires Qdrant at localhost:6333 + OPENAI_API_KEY
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
**Commit format:** `type(scope): description`
|
|
320
|
+
Types: `feat`, `fix`, `docs`, `refactor`, `perf`, `chore`, `test`
|
|
321
|
+
Scopes: `embedding`, `vectordb`, `splitter`, `indexer`, `mcp`, `infra`, `config`
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## 🙏 Acknowledgements
|
|
326
|
+
|
|
327
|
+
Heavily inspired by [mem0](https://github.com/mem0ai/mem0), [claude-mem](https://github.com/thedotmack/claude-mem), and [claude-context](https://github.com/zilliztech/claude-context). Documentation retrieval powered by [context7](https://github.com/upstash/context7).
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## 📄 License
|
|
332
|
+
|
|
333
|
+
MIT
|
package/dist/config.d.ts
CHANGED
|
@@ -15,6 +15,11 @@ declare const configSchema: z.ZodEffects<z.ZodObject<{
|
|
|
15
15
|
eideticDataDir: z.ZodDefault<z.ZodString>;
|
|
16
16
|
customExtensions: z.ZodEffects<z.ZodDefault<z.ZodArray<z.ZodString, "many">>, string[], unknown>;
|
|
17
17
|
customIgnorePatterns: z.ZodEffects<z.ZodDefault<z.ZodArray<z.ZodString, "many">>, string[], unknown>;
|
|
18
|
+
memoryLlmProvider: z.ZodDefault<z.ZodEnum<["openai", "ollama", "anthropic"]>>;
|
|
19
|
+
memoryLlmModel: z.ZodOptional<z.ZodString>;
|
|
20
|
+
memoryLlmBaseUrl: z.ZodOptional<z.ZodString>;
|
|
21
|
+
memoryLlmApiKey: z.ZodOptional<z.ZodString>;
|
|
22
|
+
anthropicApiKey: z.ZodDefault<z.ZodString>;
|
|
18
23
|
}, "strip", z.ZodTypeAny, {
|
|
19
24
|
embeddingProvider: "openai" | "ollama" | "local";
|
|
20
25
|
openaiApiKey: string;
|
|
@@ -27,10 +32,15 @@ declare const configSchema: z.ZodEffects<z.ZodObject<{
|
|
|
27
32
|
eideticDataDir: string;
|
|
28
33
|
customExtensions: string[];
|
|
29
34
|
customIgnorePatterns: string[];
|
|
35
|
+
memoryLlmProvider: "openai" | "ollama" | "anthropic";
|
|
36
|
+
anthropicApiKey: string;
|
|
30
37
|
openaiBaseUrl?: string | undefined;
|
|
31
38
|
embeddingModel?: string | undefined;
|
|
32
39
|
qdrantApiKey?: string | undefined;
|
|
33
40
|
milvusToken?: string | undefined;
|
|
41
|
+
memoryLlmModel?: string | undefined;
|
|
42
|
+
memoryLlmBaseUrl?: string | undefined;
|
|
43
|
+
memoryLlmApiKey?: string | undefined;
|
|
34
44
|
}, {
|
|
35
45
|
embeddingProvider?: "openai" | "ollama" | "local" | undefined;
|
|
36
46
|
openaiApiKey?: string | undefined;
|
|
@@ -47,8 +57,14 @@ declare const configSchema: z.ZodEffects<z.ZodObject<{
|
|
|
47
57
|
eideticDataDir?: string | undefined;
|
|
48
58
|
customExtensions?: unknown;
|
|
49
59
|
customIgnorePatterns?: unknown;
|
|
60
|
+
memoryLlmProvider?: "openai" | "ollama" | "anthropic" | undefined;
|
|
61
|
+
memoryLlmModel?: string | undefined;
|
|
62
|
+
memoryLlmBaseUrl?: string | undefined;
|
|
63
|
+
memoryLlmApiKey?: string | undefined;
|
|
64
|
+
anthropicApiKey?: string | undefined;
|
|
50
65
|
}>, {
|
|
51
66
|
embeddingModel: string;
|
|
67
|
+
memoryLlmModel: string;
|
|
52
68
|
embeddingProvider: "openai" | "ollama" | "local";
|
|
53
69
|
openaiApiKey: string;
|
|
54
70
|
ollamaBaseUrl: string;
|
|
@@ -60,9 +76,13 @@ declare const configSchema: z.ZodEffects<z.ZodObject<{
|
|
|
60
76
|
eideticDataDir: string;
|
|
61
77
|
customExtensions: string[];
|
|
62
78
|
customIgnorePatterns: string[];
|
|
79
|
+
memoryLlmProvider: "openai" | "ollama" | "anthropic";
|
|
80
|
+
anthropicApiKey: string;
|
|
63
81
|
openaiBaseUrl?: string | undefined;
|
|
64
82
|
qdrantApiKey?: string | undefined;
|
|
65
83
|
milvusToken?: string | undefined;
|
|
84
|
+
memoryLlmBaseUrl?: string | undefined;
|
|
85
|
+
memoryLlmApiKey?: string | undefined;
|
|
66
86
|
}, {
|
|
67
87
|
embeddingProvider?: "openai" | "ollama" | "local" | undefined;
|
|
68
88
|
openaiApiKey?: string | undefined;
|
|
@@ -79,6 +99,11 @@ declare const configSchema: z.ZodEffects<z.ZodObject<{
|
|
|
79
99
|
eideticDataDir?: string | undefined;
|
|
80
100
|
customExtensions?: unknown;
|
|
81
101
|
customIgnorePatterns?: unknown;
|
|
102
|
+
memoryLlmProvider?: "openai" | "ollama" | "anthropic" | undefined;
|
|
103
|
+
memoryLlmModel?: string | undefined;
|
|
104
|
+
memoryLlmBaseUrl?: string | undefined;
|
|
105
|
+
memoryLlmApiKey?: string | undefined;
|
|
106
|
+
anthropicApiKey?: string | undefined;
|
|
82
107
|
}>;
|
|
83
108
|
export type Config = z.infer<typeof configSchema>;
|
|
84
109
|
export declare function loadConfig(): Config;
|
package/dist/config.js
CHANGED
|
@@ -2,7 +2,8 @@ import os from 'node:os';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { ConfigError } from './errors.js';
|
|
5
|
-
const configSchema = z
|
|
5
|
+
const configSchema = z
|
|
6
|
+
.object({
|
|
6
7
|
embeddingProvider: z.enum(['openai', 'ollama', 'local']).default('openai'),
|
|
7
8
|
openaiApiKey: z.string().default(''),
|
|
8
9
|
openaiBaseUrl: z.string().optional(),
|
|
@@ -16,36 +17,54 @@ const configSchema = z.object({
|
|
|
16
17
|
milvusAddress: z.string().default('localhost:19530'),
|
|
17
18
|
milvusToken: z.string().optional(),
|
|
18
19
|
eideticDataDir: z.string().default(path.join(os.homedir(), '.eidetic')),
|
|
19
|
-
customExtensions: z.preprocess((val) => typeof val === 'string' ? JSON.parse(val) : val, z.array(z.string()).default([])),
|
|
20
|
-
customIgnorePatterns: z.preprocess((val) => typeof val === 'string' ? JSON.parse(val) : val, z.array(z.string()).default([])),
|
|
21
|
-
|
|
20
|
+
customExtensions: z.preprocess((val) => (typeof val === 'string' ? JSON.parse(val) : val), z.array(z.string()).default([])),
|
|
21
|
+
customIgnorePatterns: z.preprocess((val) => (typeof val === 'string' ? JSON.parse(val) : val), z.array(z.string()).default([])),
|
|
22
|
+
memoryLlmProvider: z.enum(['openai', 'ollama', 'anthropic']).default('anthropic'),
|
|
23
|
+
memoryLlmModel: z.string().optional(),
|
|
24
|
+
memoryLlmBaseUrl: z.string().optional(),
|
|
25
|
+
memoryLlmApiKey: z.string().optional(),
|
|
26
|
+
anthropicApiKey: z.string().default(''),
|
|
27
|
+
})
|
|
28
|
+
.transform((cfg) => ({
|
|
22
29
|
...cfg,
|
|
23
30
|
// Default embedding model depends on provider
|
|
24
|
-
embeddingModel: cfg.embeddingModel
|
|
25
|
-
|
|
31
|
+
embeddingModel: cfg.embeddingModel ??
|
|
32
|
+
(cfg.embeddingProvider === 'ollama' ? 'nomic-embed-text' : 'text-embedding-3-small'),
|
|
33
|
+
// Default memory LLM model depends on provider
|
|
34
|
+
memoryLlmModel: cfg.memoryLlmModel ??
|
|
35
|
+
(cfg.memoryLlmProvider === 'ollama'
|
|
36
|
+
? 'llama3.2'
|
|
37
|
+
: cfg.memoryLlmProvider === 'anthropic'
|
|
38
|
+
? 'claude-haiku-4-5-20251001'
|
|
39
|
+
: 'gpt-4o-mini'),
|
|
26
40
|
}));
|
|
27
41
|
let cachedConfig = null;
|
|
28
42
|
export function loadConfig() {
|
|
29
43
|
const raw = {
|
|
30
44
|
embeddingProvider: process.env.EMBEDDING_PROVIDER,
|
|
31
|
-
openaiApiKey: process.env.OPENAI_API_KEY ?? '',
|
|
45
|
+
openaiApiKey: (process.env.OPENAI_API_KEY ?? '').trim().replace(/^["']|["']$/g, ''),
|
|
32
46
|
openaiBaseUrl: process.env.OPENAI_BASE_URL || undefined,
|
|
33
47
|
ollamaBaseUrl: process.env.OLLAMA_BASE_URL,
|
|
34
48
|
embeddingModel: process.env.EMBEDDING_MODEL || undefined,
|
|
35
49
|
embeddingBatchSize: process.env.EMBEDDING_BATCH_SIZE,
|
|
36
50
|
indexingConcurrency: process.env.INDEXING_CONCURRENCY,
|
|
37
51
|
qdrantUrl: process.env.QDRANT_URL,
|
|
38
|
-
qdrantApiKey: process.env.QDRANT_API_KEY || undefined,
|
|
52
|
+
qdrantApiKey: process.env.QDRANT_API_KEY?.trim().replace(/^["']|["']$/g, '') || undefined,
|
|
39
53
|
vectordbProvider: process.env.VECTORDB_PROVIDER,
|
|
40
54
|
milvusAddress: process.env.MILVUS_ADDRESS,
|
|
41
|
-
milvusToken: process.env.MILVUS_TOKEN || undefined,
|
|
55
|
+
milvusToken: process.env.MILVUS_TOKEN?.trim().replace(/^["']|["']$/g, '') || undefined,
|
|
42
56
|
eideticDataDir: process.env.EIDETIC_DATA_DIR,
|
|
43
57
|
customExtensions: process.env.CUSTOM_EXTENSIONS,
|
|
44
58
|
customIgnorePatterns: process.env.CUSTOM_IGNORE_PATTERNS,
|
|
59
|
+
memoryLlmProvider: process.env.MEMORY_LLM_PROVIDER,
|
|
60
|
+
memoryLlmModel: process.env.MEMORY_LLM_MODEL || undefined,
|
|
61
|
+
memoryLlmBaseUrl: process.env.MEMORY_LLM_BASE_URL || undefined,
|
|
62
|
+
memoryLlmApiKey: process.env.MEMORY_LLM_API_KEY?.trim().replace(/^["']|["']$/g, '') || undefined,
|
|
63
|
+
anthropicApiKey: (process.env.ANTHROPIC_API_KEY ?? '').trim().replace(/^["']|["']$/g, ''),
|
|
45
64
|
};
|
|
46
65
|
const result = configSchema.safeParse(raw);
|
|
47
66
|
if (!result.success) {
|
|
48
|
-
const issues = result.error.issues.map(i => ` ${i.path.join('.')}: ${i.message}`).join('\n');
|
|
67
|
+
const issues = result.error.issues.map((i) => ` ${i.path.join('.')}: ${i.message}`).join('\n');
|
|
49
68
|
throw new ConfigError(`Invalid configuration:\n${issues}`);
|
|
50
69
|
}
|
|
51
70
|
const config = result.data;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { VectorDB } from '../vectordb/types.js';
|
|
2
|
+
export interface CleanupResult {
|
|
3
|
+
removedFiles: string[];
|
|
4
|
+
totalRemoved: number;
|
|
5
|
+
durationMs: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function cleanupVectors(rootPath: string, vectordb: VectorDB, onProgress?: (pct: number, msg: string) => void, customExtensions?: string[], customIgnorePatterns?: string[]): Promise<CleanupResult>;
|
|
8
|
+
//# sourceMappingURL=cleanup.d.ts.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { scanFiles, buildSnapshot, diffSnapshots } from './sync.js';
|
|
2
|
+
import { loadSnapshot, saveSnapshot } from './snapshot-io.js';
|
|
3
|
+
import { pathToCollectionName } from '../paths.js';
|
|
4
|
+
import { IndexingError } from '../errors.js';
|
|
5
|
+
export async function cleanupVectors(rootPath, vectordb, onProgress, customExtensions, customIgnorePatterns) {
|
|
6
|
+
const startTime = Date.now();
|
|
7
|
+
const previousSnapshot = loadSnapshot(rootPath);
|
|
8
|
+
if (!previousSnapshot) {
|
|
9
|
+
throw new IndexingError(`No snapshot found for ${rootPath}. Index the codebase first before running cleanup.`);
|
|
10
|
+
}
|
|
11
|
+
onProgress?.(10, 'Scanning files on disk...');
|
|
12
|
+
const filePaths = await scanFiles(rootPath, customExtensions ?? [], customIgnorePatterns ?? []);
|
|
13
|
+
onProgress?.(40, 'Building current snapshot...');
|
|
14
|
+
const currentSnapshot = buildSnapshot(rootPath, filePaths);
|
|
15
|
+
onProgress?.(60, 'Diffing snapshots...');
|
|
16
|
+
const { removed } = diffSnapshots(previousSnapshot, currentSnapshot);
|
|
17
|
+
if (removed.length === 0) {
|
|
18
|
+
onProgress?.(100, 'No removed files found.');
|
|
19
|
+
return { removedFiles: [], totalRemoved: 0, durationMs: Date.now() - startTime };
|
|
20
|
+
}
|
|
21
|
+
const collectionName = pathToCollectionName(rootPath);
|
|
22
|
+
let deletedCount = 0;
|
|
23
|
+
for (const rel of removed) {
|
|
24
|
+
onProgress?.(60 + Math.round((deletedCount / removed.length) * 35), `Deleting vectors for ${rel}...`);
|
|
25
|
+
await vectordb.deleteByPath(collectionName, rel);
|
|
26
|
+
deletedCount++;
|
|
27
|
+
}
|
|
28
|
+
// Save updated snapshot (removes the deleted file entries)
|
|
29
|
+
const updatedSnapshot = { ...previousSnapshot };
|
|
30
|
+
for (const rel of removed) {
|
|
31
|
+
Reflect.deleteProperty(updatedSnapshot, rel);
|
|
32
|
+
}
|
|
33
|
+
saveSnapshot(rootPath, updatedSnapshot);
|
|
34
|
+
onProgress?.(100, 'Cleanup complete.');
|
|
35
|
+
return {
|
|
36
|
+
removedFiles: removed,
|
|
37
|
+
totalRemoved: removed.length,
|
|
38
|
+
durationMs: Date.now() - startTime,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=cleanup.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Embedding } from '../embedding/types.js';
|
|
2
|
+
import type { VectorDB } from '../vectordb/types.js';
|
|
3
|
+
export interface DocIndexResult {
|
|
4
|
+
library: string;
|
|
5
|
+
topic: string;
|
|
6
|
+
source: string;
|
|
7
|
+
collectionName: string;
|
|
8
|
+
totalChunks: number;
|
|
9
|
+
estimatedTokens: number;
|
|
10
|
+
durationMs: number;
|
|
11
|
+
}
|
|
12
|
+
export declare function indexDocument(content: string, source: string, library: string, topic: string, embedding: Embedding, vectordb: VectorDB, ttlDays?: number): Promise<DocIndexResult>;
|
|
13
|
+
//# sourceMappingURL=doc-indexer.d.ts.map
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { LineSplitter } from '../splitter/line.js';
|
|
3
|
+
import { docCollectionName } from '../paths.js';
|
|
4
|
+
import { upsertDocEntry } from '../state/doc-metadata.js';
|
|
5
|
+
import { IndexingError } from '../errors.js';
|
|
6
|
+
import { getConfig } from '../config.js';
|
|
7
|
+
export async function indexDocument(content, source, library, topic, embedding, vectordb, ttlDays = 7) {
|
|
8
|
+
const start = Date.now();
|
|
9
|
+
if (!content || content.trim().length === 0) {
|
|
10
|
+
throw new IndexingError('Document content is empty.');
|
|
11
|
+
}
|
|
12
|
+
if (!source)
|
|
13
|
+
throw new IndexingError('Document source is required.');
|
|
14
|
+
if (!library)
|
|
15
|
+
throw new IndexingError('Library name is required.');
|
|
16
|
+
if (!topic)
|
|
17
|
+
throw new IndexingError('Topic is required.');
|
|
18
|
+
const collection = docCollectionName(library);
|
|
19
|
+
const config = getConfig();
|
|
20
|
+
const splitter = new LineSplitter();
|
|
21
|
+
const chunks = splitter.split(content, 'markdown', source);
|
|
22
|
+
if (chunks.length === 0) {
|
|
23
|
+
throw new IndexingError('Document produced no chunks after splitting.');
|
|
24
|
+
}
|
|
25
|
+
const exists = await vectordb.hasCollection(collection);
|
|
26
|
+
if (!exists) {
|
|
27
|
+
await vectordb.createCollection(collection, embedding.dimension);
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
await vectordb.deleteByPath(collection, source);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// collection may be new with no matching docs
|
|
34
|
+
}
|
|
35
|
+
const batchSize = config.embeddingBatchSize;
|
|
36
|
+
let totalChunks = 0;
|
|
37
|
+
let totalTokens = 0;
|
|
38
|
+
for (let i = 0; i < chunks.length; i += batchSize) {
|
|
39
|
+
const batch = chunks.slice(i, i + batchSize);
|
|
40
|
+
const texts = batch.map((c) => c.content);
|
|
41
|
+
const estimation = embedding.estimateTokens(texts);
|
|
42
|
+
totalTokens += estimation.estimatedTokens;
|
|
43
|
+
const vectors = await embedding.embedBatch(texts);
|
|
44
|
+
const documents = batch.map((chunk, j) => ({
|
|
45
|
+
id: randomUUID(),
|
|
46
|
+
content: chunk.content,
|
|
47
|
+
vector: vectors[j],
|
|
48
|
+
relativePath: source,
|
|
49
|
+
startLine: chunk.startLine,
|
|
50
|
+
endLine: chunk.endLine,
|
|
51
|
+
fileExtension: '.md',
|
|
52
|
+
language: 'markdown',
|
|
53
|
+
}));
|
|
54
|
+
await vectordb.insert(collection, documents);
|
|
55
|
+
totalChunks += batch.length;
|
|
56
|
+
}
|
|
57
|
+
upsertDocEntry({
|
|
58
|
+
library,
|
|
59
|
+
topic,
|
|
60
|
+
source,
|
|
61
|
+
collectionName: collection,
|
|
62
|
+
indexedAt: new Date().toISOString(),
|
|
63
|
+
ttlDays,
|
|
64
|
+
totalChunks,
|
|
65
|
+
});
|
|
66
|
+
return {
|
|
67
|
+
library,
|
|
68
|
+
topic,
|
|
69
|
+
source,
|
|
70
|
+
collectionName: collection,
|
|
71
|
+
totalChunks,
|
|
72
|
+
estimatedTokens: totalTokens,
|
|
73
|
+
durationMs: Date.now() - start,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=doc-indexer.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Embedding } from '../embedding/types.js';
|
|
2
|
+
import type { VectorDB, SearchResult } from '../vectordb/types.js';
|
|
3
|
+
export interface DocSearchResult extends SearchResult {
|
|
4
|
+
library: string;
|
|
5
|
+
topic: string;
|
|
6
|
+
source: string;
|
|
7
|
+
stale: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function searchDocuments(query: string, embedding: Embedding, vectordb: VectorDB, options?: {
|
|
10
|
+
library?: string;
|
|
11
|
+
limit?: number;
|
|
12
|
+
}): Promise<DocSearchResult[]>;
|
|
13
|
+
//# sourceMappingURL=doc-searcher.d.ts.map
|