memctl 0.2.0__tar.gz → 0.3.0__tar.gz
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.
- {memctl-0.2.0 → memctl-0.3.0}/PKG-INFO +92 -12
- {memctl-0.2.0 → memctl-0.3.0}/README.md +91 -11
- {memctl-0.2.0 → memctl-0.3.0}/memctl/__init__.py +1 -1
- {memctl-0.2.0 → memctl-0.3.0}/memctl/cli.py +269 -0
- {memctl-0.2.0 → memctl-0.3.0}/memctl/ingest.py +6 -2
- memctl-0.3.0/memctl/inspect.py +586 -0
- memctl-0.3.0/memctl/mount.py +111 -0
- {memctl-0.2.0 → memctl-0.3.0}/memctl/store.py +183 -5
- memctl-0.3.0/memctl/sync.py +368 -0
- {memctl-0.2.0 → memctl-0.3.0}/memctl.egg-info/PKG-INFO +92 -12
- {memctl-0.2.0 → memctl-0.3.0}/memctl.egg-info/SOURCES.txt +6 -0
- {memctl-0.2.0 → memctl-0.3.0}/pyproject.toml +1 -1
- {memctl-0.2.0 → memctl-0.3.0}/tests/test_cli.py +211 -1
- {memctl-0.2.0 → memctl-0.3.0}/tests/test_forward_compat.py +50 -0
- memctl-0.3.0/tests/test_inspect.py +571 -0
- memctl-0.3.0/tests/test_mount.py +118 -0
- {memctl-0.2.0 → memctl-0.3.0}/tests/test_store.py +177 -1
- memctl-0.3.0/tests/test_sync.py +297 -0
- {memctl-0.2.0 → memctl-0.3.0}/LICENSE +0 -0
- {memctl-0.2.0 → memctl-0.3.0}/memctl/config.py +0 -0
- {memctl-0.2.0 → memctl-0.3.0}/memctl/consolidate.py +0 -0
- {memctl-0.2.0 → memctl-0.3.0}/memctl/extract.py +0 -0
- {memctl-0.2.0 → memctl-0.3.0}/memctl/loop.py +0 -0
- {memctl-0.2.0 → memctl-0.3.0}/memctl/mcp/__init__.py +0 -0
- {memctl-0.2.0 → memctl-0.3.0}/memctl/mcp/formatting.py +0 -0
- {memctl-0.2.0 → memctl-0.3.0}/memctl/mcp/server.py +0 -0
- {memctl-0.2.0 → memctl-0.3.0}/memctl/mcp/tools.py +0 -0
- {memctl-0.2.0 → memctl-0.3.0}/memctl/policy.py +0 -0
- {memctl-0.2.0 → memctl-0.3.0}/memctl/proposer.py +0 -0
- {memctl-0.2.0 → memctl-0.3.0}/memctl/similarity.py +0 -0
- {memctl-0.2.0 → memctl-0.3.0}/memctl/types.py +0 -0
- {memctl-0.2.0 → memctl-0.3.0}/memctl.egg-info/dependency_links.txt +0 -0
- {memctl-0.2.0 → memctl-0.3.0}/memctl.egg-info/entry_points.txt +0 -0
- {memctl-0.2.0 → memctl-0.3.0}/memctl.egg-info/requires.txt +0 -0
- {memctl-0.2.0 → memctl-0.3.0}/memctl.egg-info/top_level.txt +0 -0
- {memctl-0.2.0 → memctl-0.3.0}/setup.cfg +0 -0
- {memctl-0.2.0 → memctl-0.3.0}/tests/test_contracts.py +0 -0
- {memctl-0.2.0 → memctl-0.3.0}/tests/test_extract.py +0 -0
- {memctl-0.2.0 → memctl-0.3.0}/tests/test_ingest.py +0 -0
- {memctl-0.2.0 → memctl-0.3.0}/tests/test_loop.py +0 -0
- {memctl-0.2.0 → memctl-0.3.0}/tests/test_pipe_compose.py +0 -0
- {memctl-0.2.0 → memctl-0.3.0}/tests/test_policy.py +0 -0
- {memctl-0.2.0 → memctl-0.3.0}/tests/test_similarity.py +0 -0
- {memctl-0.2.0 → memctl-0.3.0}/tests/test_types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memctl
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A Unix-native memory control plane for LLM orchestration
|
|
5
5
|
Author-email: Olivier Vitrac <olivier.vitrac@adservio.fr>
|
|
6
6
|
License: MIT
|
|
@@ -150,7 +150,22 @@ memctl search "authentication"
|
|
|
150
150
|
memctl search "database" --json -k 5
|
|
151
151
|
```
|
|
152
152
|
|
|
153
|
-
### 5. Inspect
|
|
153
|
+
### 5. Inspect a folder (one-liner)
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
# Auto-mounts, auto-syncs, and inspects — all in one command
|
|
157
|
+
memctl inspect docs/
|
|
158
|
+
|
|
159
|
+
# Same in JSON (for scripts)
|
|
160
|
+
memctl inspect docs/ --json
|
|
161
|
+
|
|
162
|
+
# Skip sync (use cached state)
|
|
163
|
+
memctl inspect docs/ --no-sync
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`inspect` auto-mounts the folder if needed, checks staleness, syncs only if stale, and produces a structural summary. All implicit actions are announced on stderr.
|
|
167
|
+
|
|
168
|
+
### 6. Manage
|
|
154
169
|
|
|
155
170
|
```bash
|
|
156
171
|
memctl show MEM-abc123def456 # Show item details
|
|
@@ -180,6 +195,9 @@ memctl <command> [options]
|
|
|
180
195
|
| `stats` | Store statistics |
|
|
181
196
|
| `consolidate [--dry-run]` | Deterministic merge of similar STM items |
|
|
182
197
|
| `loop QUERY --llm CMD` | Bounded recall-answer loop with LLM |
|
|
198
|
+
| `mount PATH` | Register a folder as a structured source |
|
|
199
|
+
| `sync [PATH]` | Delta-sync mounted folders into the store |
|
|
200
|
+
| `inspect [PATH]` | Structural inspection with auto-mount and auto-sync |
|
|
183
201
|
| `serve` | Start MCP server (requires `memctl[mcp]`) |
|
|
184
202
|
|
|
185
203
|
### Global Flags
|
|
@@ -291,6 +309,52 @@ memctl push "database schema" --source src/ \
|
|
|
291
309
|
memctl loop --replay trace.jsonl "original question"
|
|
292
310
|
```
|
|
293
311
|
|
|
312
|
+
#### `memctl mount`
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
memctl mount PATH [--name NAME] [--ignore PATTERN ...] [--lang HINT]
|
|
316
|
+
memctl mount --list
|
|
317
|
+
memctl mount --remove ID_OR_NAME
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Registers a folder as a structured source. Stores metadata only — no scanning, no ingestion. The folder contents are synced separately via `sync` or automatically via `inspect`.
|
|
321
|
+
|
|
322
|
+
#### `memctl sync`
|
|
323
|
+
|
|
324
|
+
```bash
|
|
325
|
+
memctl sync [PATH] [--full] [--json] [--quiet]
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Delta-syncs mounted folders into the memory store. Uses a 3-tier delta rule:
|
|
329
|
+
1. **New file** (not in DB) → ingest
|
|
330
|
+
2. **Size + mtime match** → fast skip (no hashing)
|
|
331
|
+
3. **Hash compare** → ingest only if content changed
|
|
332
|
+
|
|
333
|
+
If `PATH` is given but not yet mounted, it is auto-registered first. `--full` forces re-processing of all files.
|
|
334
|
+
|
|
335
|
+
#### `memctl inspect`
|
|
336
|
+
|
|
337
|
+
```bash
|
|
338
|
+
# Orchestration mode — auto-mounts, auto-syncs, and inspects
|
|
339
|
+
memctl inspect PATH [--sync auto|always|never] [--no-sync] [--mount-mode persist|ephemeral]
|
|
340
|
+
[--budget N] [--ignore PATTERN ...] [--json] [--quiet]
|
|
341
|
+
|
|
342
|
+
# Classic mode — inspect an existing mount by ID/name
|
|
343
|
+
memctl inspect --mount ID_OR_NAME [--budget N] [--json] [--quiet]
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
When given a positional `PATH`, inspect operates in **orchestration mode**:
|
|
347
|
+
1. **Auto-mount** — registers the folder if not already mounted
|
|
348
|
+
2. **Staleness check** — compares disk inventory (path/size/mtime triples) against the store
|
|
349
|
+
3. **Auto-sync** — runs delta sync only if stale (or always/never per `--sync`)
|
|
350
|
+
4. **Inspect** — generates a deterministic structural summary
|
|
351
|
+
|
|
352
|
+
Output includes file/chunk/size totals, per-folder breakdown, per-extension distribution, top-5 largest files, and rule-based observations. All paths in output are mount-relative (never absolute).
|
|
353
|
+
|
|
354
|
+
`--mount-mode ephemeral` removes the mount record after inspection (corpus data is preserved). `--no-sync` is shorthand for `--sync never`.
|
|
355
|
+
|
|
356
|
+
All implicit actions (mount, sync) are announced on stderr. `--quiet` suppresses them.
|
|
357
|
+
|
|
294
358
|
---
|
|
295
359
|
|
|
296
360
|
## Environment Variables
|
|
@@ -348,6 +412,15 @@ memctl search "" --json | jq -c '.[]'
|
|
|
348
412
|
|
|
349
413
|
# Iterative recall-answer loop with trace
|
|
350
414
|
memctl push "auth flow" --source docs/ | memctl loop "auth flow" --llm "claude -p" --trace
|
|
415
|
+
|
|
416
|
+
# One-liner: inspect a folder (auto-mount + auto-sync)
|
|
417
|
+
memctl inspect docs/
|
|
418
|
+
|
|
419
|
+
# Inspect in JSON, pipe to jq for extension breakdown
|
|
420
|
+
memctl inspect src/ --json | jq '.extensions'
|
|
421
|
+
|
|
422
|
+
# Inspect without syncing (use cached state)
|
|
423
|
+
memctl inspect docs/ --no-sync --json
|
|
351
424
|
```
|
|
352
425
|
|
|
353
426
|
---
|
|
@@ -402,14 +475,17 @@ Tool names use the `memory_*` prefix for drop-in compatibility with RAGIX.
|
|
|
402
475
|
```
|
|
403
476
|
memctl/
|
|
404
477
|
├── types.py Data model (MemoryItem, MemoryProposal, MemoryEvent, MemoryLink)
|
|
405
|
-
├── store.py SQLite + FTS5 + WAL backend (
|
|
478
|
+
├── store.py SQLite + FTS5 + WAL backend (10 tables + schema_meta)
|
|
406
479
|
├── extract.py Text extraction (text files + binary format dispatch)
|
|
407
480
|
├── ingest.py Paragraph chunking, SHA-256 dedup, source resolution
|
|
408
481
|
├── policy.py Write governance (30 patterns: secrets, injection, instructional)
|
|
409
482
|
├── config.py Dataclass configuration
|
|
410
483
|
├── similarity.py Stdlib text similarity (Jaccard + SequenceMatcher)
|
|
411
484
|
├── loop.py Bounded recall-answer loop controller
|
|
412
|
-
├──
|
|
485
|
+
├── mount.py Folder mount registration and management
|
|
486
|
+
├── sync.py Delta sync with 3-tier change detection
|
|
487
|
+
├── inspect.py Structural inspection and orchestration
|
|
488
|
+
├── cli.py 12 CLI commands
|
|
413
489
|
├── consolidate.py Deterministic merge (Jaccard clustering, no LLM)
|
|
414
490
|
├── proposer.py LLM output parsing (delimiter + regex)
|
|
415
491
|
└── mcp/
|
|
@@ -418,7 +494,7 @@ memctl/
|
|
|
418
494
|
└── server.py FastMCP server entry point
|
|
419
495
|
```
|
|
420
496
|
|
|
421
|
-
|
|
497
|
+
19 source files. ~7,300 lines. Zero compiled dependencies for core.
|
|
422
498
|
|
|
423
499
|
### Memory Tiers
|
|
424
500
|
|
|
@@ -485,7 +561,7 @@ Deterministic, no-LLM merge pipeline:
|
|
|
485
561
|
|
|
486
562
|
## Database Schema
|
|
487
563
|
|
|
488
|
-
Single SQLite file with WAL mode.
|
|
564
|
+
Single SQLite file with WAL mode. 10 tables + 1 FTS5 virtual table:
|
|
489
565
|
|
|
490
566
|
| Table | Purpose |
|
|
491
567
|
|-------|---------|
|
|
@@ -494,13 +570,14 @@ Single SQLite file with WAL mode. 9 tables + 1 FTS5 virtual table:
|
|
|
494
570
|
| `memory_events` | Audit log (every read/write/consolidate) |
|
|
495
571
|
| `memory_links` | Directional relationships (supersedes, supports, etc.) |
|
|
496
572
|
| `memory_embeddings` | Reserved for RAGIX (empty in memctl) |
|
|
497
|
-
| `corpus_hashes` | SHA-256 file dedup
|
|
573
|
+
| `corpus_hashes` | SHA-256 file dedup + mount metadata (mount_id, rel_path, ext, size_bytes, mtime_epoch, lang_hint) |
|
|
498
574
|
| `corpus_metadata` | Corpus-level metadata |
|
|
499
575
|
| `schema_meta` | Schema version, creation info |
|
|
500
576
|
| `memory_palace_locations` | Reserved for RAGIX |
|
|
577
|
+
| `memory_mounts` | Registered folder mounts (path, name, ignore patterns, lang hint) |
|
|
501
578
|
| `memory_items_fts` | FTS5 virtual table for full-text search |
|
|
502
579
|
|
|
503
|
-
Schema version is tracked in `schema_meta`. Current: `SCHEMA_VERSION=
|
|
580
|
+
Schema version is tracked in `schema_meta`. Current: `SCHEMA_VERSION=2`. Migration from v1 is additive (ALTER TABLE ADD COLUMN) and idempotent.
|
|
504
581
|
|
|
505
582
|
---
|
|
506
583
|
|
|
@@ -509,17 +586,20 @@ Schema version is tracked in `schema_meta`. Current: `SCHEMA_VERSION=1`.
|
|
|
509
586
|
memctl is extracted from [RAGIX](https://github.com/ovitrac/RAGIX) and maintains schema-identical databases. To upgrade:
|
|
510
587
|
|
|
511
588
|
```bash
|
|
512
|
-
|
|
589
|
+
git clone git@github.com:ovitrac/RAGIX.git
|
|
590
|
+
cd RAGIX
|
|
591
|
+
pip install -e .[all]
|
|
513
592
|
# Point at the same database — all items carry over
|
|
514
|
-
ragix memory stats --db
|
|
593
|
+
ragix memory stats --db /path/to/your/.memory/memory.db
|
|
515
594
|
```
|
|
516
595
|
|
|
517
596
|
| Feature | memctl | RAGIX |
|
|
518
597
|
|---------|--------|-------|
|
|
519
|
-
| SQLite schema |
|
|
598
|
+
| SQLite schema | Forward-compatible (RAGIX can open memctl DBs) | Superset |
|
|
520
599
|
| Injection format | `format_version=1` | `format_version=1` |
|
|
521
600
|
| MCP tool names | `memory_*` | `memory_*` |
|
|
522
601
|
| FTS5 recall | Yes | Yes (+ hybrid embeddings) |
|
|
602
|
+
| Folder mount + sync | Yes (v0.3+) | No |
|
|
523
603
|
| Embeddings | No | Yes (FAISS + Ollama) |
|
|
524
604
|
| LLM-assisted merge | No | Yes |
|
|
525
605
|
| Graph-RAG | No | Yes |
|
|
@@ -573,7 +653,7 @@ pip install memctl[dev]
|
|
|
573
653
|
pytest tests/ -v
|
|
574
654
|
```
|
|
575
655
|
|
|
576
|
-
|
|
656
|
+
479 tests across 14 test files covering types, store, policy, ingest, text extraction, similarity, loop controller, mount, sync, inspect, forward compatibility, contracts, CLI (subprocess), and pipe composition.
|
|
577
657
|
|
|
578
658
|
---
|
|
579
659
|
|
|
@@ -107,7 +107,22 @@ memctl search "authentication"
|
|
|
107
107
|
memctl search "database" --json -k 5
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
-
### 5. Inspect
|
|
110
|
+
### 5. Inspect a folder (one-liner)
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
# Auto-mounts, auto-syncs, and inspects — all in one command
|
|
114
|
+
memctl inspect docs/
|
|
115
|
+
|
|
116
|
+
# Same in JSON (for scripts)
|
|
117
|
+
memctl inspect docs/ --json
|
|
118
|
+
|
|
119
|
+
# Skip sync (use cached state)
|
|
120
|
+
memctl inspect docs/ --no-sync
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`inspect` auto-mounts the folder if needed, checks staleness, syncs only if stale, and produces a structural summary. All implicit actions are announced on stderr.
|
|
124
|
+
|
|
125
|
+
### 6. Manage
|
|
111
126
|
|
|
112
127
|
```bash
|
|
113
128
|
memctl show MEM-abc123def456 # Show item details
|
|
@@ -137,6 +152,9 @@ memctl <command> [options]
|
|
|
137
152
|
| `stats` | Store statistics |
|
|
138
153
|
| `consolidate [--dry-run]` | Deterministic merge of similar STM items |
|
|
139
154
|
| `loop QUERY --llm CMD` | Bounded recall-answer loop with LLM |
|
|
155
|
+
| `mount PATH` | Register a folder as a structured source |
|
|
156
|
+
| `sync [PATH]` | Delta-sync mounted folders into the store |
|
|
157
|
+
| `inspect [PATH]` | Structural inspection with auto-mount and auto-sync |
|
|
140
158
|
| `serve` | Start MCP server (requires `memctl[mcp]`) |
|
|
141
159
|
|
|
142
160
|
### Global Flags
|
|
@@ -248,6 +266,52 @@ memctl push "database schema" --source src/ \
|
|
|
248
266
|
memctl loop --replay trace.jsonl "original question"
|
|
249
267
|
```
|
|
250
268
|
|
|
269
|
+
#### `memctl mount`
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
memctl mount PATH [--name NAME] [--ignore PATTERN ...] [--lang HINT]
|
|
273
|
+
memctl mount --list
|
|
274
|
+
memctl mount --remove ID_OR_NAME
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Registers a folder as a structured source. Stores metadata only — no scanning, no ingestion. The folder contents are synced separately via `sync` or automatically via `inspect`.
|
|
278
|
+
|
|
279
|
+
#### `memctl sync`
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
memctl sync [PATH] [--full] [--json] [--quiet]
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Delta-syncs mounted folders into the memory store. Uses a 3-tier delta rule:
|
|
286
|
+
1. **New file** (not in DB) → ingest
|
|
287
|
+
2. **Size + mtime match** → fast skip (no hashing)
|
|
288
|
+
3. **Hash compare** → ingest only if content changed
|
|
289
|
+
|
|
290
|
+
If `PATH` is given but not yet mounted, it is auto-registered first. `--full` forces re-processing of all files.
|
|
291
|
+
|
|
292
|
+
#### `memctl inspect`
|
|
293
|
+
|
|
294
|
+
```bash
|
|
295
|
+
# Orchestration mode — auto-mounts, auto-syncs, and inspects
|
|
296
|
+
memctl inspect PATH [--sync auto|always|never] [--no-sync] [--mount-mode persist|ephemeral]
|
|
297
|
+
[--budget N] [--ignore PATTERN ...] [--json] [--quiet]
|
|
298
|
+
|
|
299
|
+
# Classic mode — inspect an existing mount by ID/name
|
|
300
|
+
memctl inspect --mount ID_OR_NAME [--budget N] [--json] [--quiet]
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
When given a positional `PATH`, inspect operates in **orchestration mode**:
|
|
304
|
+
1. **Auto-mount** — registers the folder if not already mounted
|
|
305
|
+
2. **Staleness check** — compares disk inventory (path/size/mtime triples) against the store
|
|
306
|
+
3. **Auto-sync** — runs delta sync only if stale (or always/never per `--sync`)
|
|
307
|
+
4. **Inspect** — generates a deterministic structural summary
|
|
308
|
+
|
|
309
|
+
Output includes file/chunk/size totals, per-folder breakdown, per-extension distribution, top-5 largest files, and rule-based observations. All paths in output are mount-relative (never absolute).
|
|
310
|
+
|
|
311
|
+
`--mount-mode ephemeral` removes the mount record after inspection (corpus data is preserved). `--no-sync` is shorthand for `--sync never`.
|
|
312
|
+
|
|
313
|
+
All implicit actions (mount, sync) are announced on stderr. `--quiet` suppresses them.
|
|
314
|
+
|
|
251
315
|
---
|
|
252
316
|
|
|
253
317
|
## Environment Variables
|
|
@@ -305,6 +369,15 @@ memctl search "" --json | jq -c '.[]'
|
|
|
305
369
|
|
|
306
370
|
# Iterative recall-answer loop with trace
|
|
307
371
|
memctl push "auth flow" --source docs/ | memctl loop "auth flow" --llm "claude -p" --trace
|
|
372
|
+
|
|
373
|
+
# One-liner: inspect a folder (auto-mount + auto-sync)
|
|
374
|
+
memctl inspect docs/
|
|
375
|
+
|
|
376
|
+
# Inspect in JSON, pipe to jq for extension breakdown
|
|
377
|
+
memctl inspect src/ --json | jq '.extensions'
|
|
378
|
+
|
|
379
|
+
# Inspect without syncing (use cached state)
|
|
380
|
+
memctl inspect docs/ --no-sync --json
|
|
308
381
|
```
|
|
309
382
|
|
|
310
383
|
---
|
|
@@ -359,14 +432,17 @@ Tool names use the `memory_*` prefix for drop-in compatibility with RAGIX.
|
|
|
359
432
|
```
|
|
360
433
|
memctl/
|
|
361
434
|
├── types.py Data model (MemoryItem, MemoryProposal, MemoryEvent, MemoryLink)
|
|
362
|
-
├── store.py SQLite + FTS5 + WAL backend (
|
|
435
|
+
├── store.py SQLite + FTS5 + WAL backend (10 tables + schema_meta)
|
|
363
436
|
├── extract.py Text extraction (text files + binary format dispatch)
|
|
364
437
|
├── ingest.py Paragraph chunking, SHA-256 dedup, source resolution
|
|
365
438
|
├── policy.py Write governance (30 patterns: secrets, injection, instructional)
|
|
366
439
|
├── config.py Dataclass configuration
|
|
367
440
|
├── similarity.py Stdlib text similarity (Jaccard + SequenceMatcher)
|
|
368
441
|
├── loop.py Bounded recall-answer loop controller
|
|
369
|
-
├──
|
|
442
|
+
├── mount.py Folder mount registration and management
|
|
443
|
+
├── sync.py Delta sync with 3-tier change detection
|
|
444
|
+
├── inspect.py Structural inspection and orchestration
|
|
445
|
+
├── cli.py 12 CLI commands
|
|
370
446
|
├── consolidate.py Deterministic merge (Jaccard clustering, no LLM)
|
|
371
447
|
├── proposer.py LLM output parsing (delimiter + regex)
|
|
372
448
|
└── mcp/
|
|
@@ -375,7 +451,7 @@ memctl/
|
|
|
375
451
|
└── server.py FastMCP server entry point
|
|
376
452
|
```
|
|
377
453
|
|
|
378
|
-
|
|
454
|
+
19 source files. ~7,300 lines. Zero compiled dependencies for core.
|
|
379
455
|
|
|
380
456
|
### Memory Tiers
|
|
381
457
|
|
|
@@ -442,7 +518,7 @@ Deterministic, no-LLM merge pipeline:
|
|
|
442
518
|
|
|
443
519
|
## Database Schema
|
|
444
520
|
|
|
445
|
-
Single SQLite file with WAL mode.
|
|
521
|
+
Single SQLite file with WAL mode. 10 tables + 1 FTS5 virtual table:
|
|
446
522
|
|
|
447
523
|
| Table | Purpose |
|
|
448
524
|
|-------|---------|
|
|
@@ -451,13 +527,14 @@ Single SQLite file with WAL mode. 9 tables + 1 FTS5 virtual table:
|
|
|
451
527
|
| `memory_events` | Audit log (every read/write/consolidate) |
|
|
452
528
|
| `memory_links` | Directional relationships (supersedes, supports, etc.) |
|
|
453
529
|
| `memory_embeddings` | Reserved for RAGIX (empty in memctl) |
|
|
454
|
-
| `corpus_hashes` | SHA-256 file dedup
|
|
530
|
+
| `corpus_hashes` | SHA-256 file dedup + mount metadata (mount_id, rel_path, ext, size_bytes, mtime_epoch, lang_hint) |
|
|
455
531
|
| `corpus_metadata` | Corpus-level metadata |
|
|
456
532
|
| `schema_meta` | Schema version, creation info |
|
|
457
533
|
| `memory_palace_locations` | Reserved for RAGIX |
|
|
534
|
+
| `memory_mounts` | Registered folder mounts (path, name, ignore patterns, lang hint) |
|
|
458
535
|
| `memory_items_fts` | FTS5 virtual table for full-text search |
|
|
459
536
|
|
|
460
|
-
Schema version is tracked in `schema_meta`. Current: `SCHEMA_VERSION=
|
|
537
|
+
Schema version is tracked in `schema_meta`. Current: `SCHEMA_VERSION=2`. Migration from v1 is additive (ALTER TABLE ADD COLUMN) and idempotent.
|
|
461
538
|
|
|
462
539
|
---
|
|
463
540
|
|
|
@@ -466,17 +543,20 @@ Schema version is tracked in `schema_meta`. Current: `SCHEMA_VERSION=1`.
|
|
|
466
543
|
memctl is extracted from [RAGIX](https://github.com/ovitrac/RAGIX) and maintains schema-identical databases. To upgrade:
|
|
467
544
|
|
|
468
545
|
```bash
|
|
469
|
-
|
|
546
|
+
git clone git@github.com:ovitrac/RAGIX.git
|
|
547
|
+
cd RAGIX
|
|
548
|
+
pip install -e .[all]
|
|
470
549
|
# Point at the same database — all items carry over
|
|
471
|
-
ragix memory stats --db
|
|
550
|
+
ragix memory stats --db /path/to/your/.memory/memory.db
|
|
472
551
|
```
|
|
473
552
|
|
|
474
553
|
| Feature | memctl | RAGIX |
|
|
475
554
|
|---------|--------|-------|
|
|
476
|
-
| SQLite schema |
|
|
555
|
+
| SQLite schema | Forward-compatible (RAGIX can open memctl DBs) | Superset |
|
|
477
556
|
| Injection format | `format_version=1` | `format_version=1` |
|
|
478
557
|
| MCP tool names | `memory_*` | `memory_*` |
|
|
479
558
|
| FTS5 recall | Yes | Yes (+ hybrid embeddings) |
|
|
559
|
+
| Folder mount + sync | Yes (v0.3+) | No |
|
|
480
560
|
| Embeddings | No | Yes (FAISS + Ollama) |
|
|
481
561
|
| LLM-assisted merge | No | Yes |
|
|
482
562
|
| Graph-RAG | No | Yes |
|
|
@@ -530,7 +610,7 @@ pip install memctl[dev]
|
|
|
530
610
|
pytest tests/ -v
|
|
531
611
|
```
|
|
532
612
|
|
|
533
|
-
|
|
613
|
+
479 tests across 14 test files covering types, store, policy, ingest, text extraction, similarity, loop controller, mount, sync, inspect, forward compatibility, contracts, CLI (subprocess), and pipe composition.
|
|
534
614
|
|
|
535
615
|
---
|
|
536
616
|
|
|
@@ -10,6 +10,9 @@ Commands:
|
|
|
10
10
|
memctl stats — store metrics
|
|
11
11
|
memctl consolidate [--dry-run] — merge + promote STM items
|
|
12
12
|
memctl loop "query" --llm CMD — bounded recall-answer loop
|
|
13
|
+
memctl mount <path> [--name N] — register folder for sync
|
|
14
|
+
memctl sync [<path>] [--full] — scan + ingest mounted folders
|
|
15
|
+
memctl inspect [--mount M] [--budget N] — structural injection block → stdout
|
|
13
16
|
memctl serve [--fts-tokenizer FR] — start MCP server (foreground)
|
|
14
17
|
|
|
15
18
|
Environment variables:
|
|
@@ -670,6 +673,205 @@ def cmd_loop(args: argparse.Namespace) -> None:
|
|
|
670
673
|
sys.exit(1)
|
|
671
674
|
|
|
672
675
|
|
|
676
|
+
# ===========================================================================
|
|
677
|
+
# Command: mount (register folder for sync)
|
|
678
|
+
# ===========================================================================
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
def cmd_mount(args: argparse.Namespace) -> None:
|
|
682
|
+
"""Register, list, or remove folder mounts."""
|
|
683
|
+
from memctl.mount import register_mount, list_mounts, remove_mount
|
|
684
|
+
|
|
685
|
+
db_path = _resolve_db(args)
|
|
686
|
+
|
|
687
|
+
if getattr(args, "list", False):
|
|
688
|
+
mounts = list_mounts(db_path)
|
|
689
|
+
if getattr(args, "json", False):
|
|
690
|
+
print(json.dumps(mounts, indent=2, ensure_ascii=False))
|
|
691
|
+
return
|
|
692
|
+
if not mounts:
|
|
693
|
+
_info("No mounts registered.")
|
|
694
|
+
return
|
|
695
|
+
else:
|
|
696
|
+
for m in mounts:
|
|
697
|
+
synced = m["last_sync_at"] or "never"
|
|
698
|
+
label = f" ({m['name']})" if m["name"] else ""
|
|
699
|
+
print(f" {m['mount_id']}{label} {m['path']} synced={synced}")
|
|
700
|
+
return
|
|
701
|
+
|
|
702
|
+
if getattr(args, "remove", None):
|
|
703
|
+
ok = remove_mount(db_path, args.remove)
|
|
704
|
+
if ok:
|
|
705
|
+
_info(f"Removed mount: {args.remove}")
|
|
706
|
+
else:
|
|
707
|
+
_warn(f"Mount not found: {args.remove}")
|
|
708
|
+
sys.exit(1)
|
|
709
|
+
return
|
|
710
|
+
|
|
711
|
+
# Register mode (default)
|
|
712
|
+
if not args.path:
|
|
713
|
+
_warn("Usage: memctl mount <path> [--name NAME] [--ignore PATTERN...]")
|
|
714
|
+
sys.exit(1)
|
|
715
|
+
|
|
716
|
+
ignore = getattr(args, "ignore", None) or []
|
|
717
|
+
lang = getattr(args, "lang", None)
|
|
718
|
+
name = getattr(args, "name", None)
|
|
719
|
+
|
|
720
|
+
try:
|
|
721
|
+
mount_id = register_mount(
|
|
722
|
+
db_path, args.path,
|
|
723
|
+
name=name,
|
|
724
|
+
ignore_patterns=ignore,
|
|
725
|
+
lang_hint=lang,
|
|
726
|
+
)
|
|
727
|
+
except (FileNotFoundError, NotADirectoryError) as e:
|
|
728
|
+
_warn(f"Error: {e}")
|
|
729
|
+
sys.exit(1)
|
|
730
|
+
|
|
731
|
+
_info(f"Mounted: {mount_id} → {os.path.realpath(args.path)}")
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
# ===========================================================================
|
|
735
|
+
# Command: sync (scan + ingest mounted folders)
|
|
736
|
+
# ===========================================================================
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
def cmd_sync(args: argparse.Namespace) -> None:
|
|
740
|
+
"""Sync mounted folders into the memory store."""
|
|
741
|
+
from memctl.sync import sync_mount, sync_all
|
|
742
|
+
|
|
743
|
+
db_path = _resolve_db(args)
|
|
744
|
+
full = getattr(args, "full", False)
|
|
745
|
+
quiet = getattr(args, "quiet", False)
|
|
746
|
+
|
|
747
|
+
if getattr(args, "path", None):
|
|
748
|
+
# Sync specific path
|
|
749
|
+
try:
|
|
750
|
+
result = sync_mount(
|
|
751
|
+
db_path, args.path,
|
|
752
|
+
delta=not full,
|
|
753
|
+
quiet=quiet,
|
|
754
|
+
)
|
|
755
|
+
except (FileNotFoundError, NotADirectoryError) as e:
|
|
756
|
+
_warn(f"Error: {e}")
|
|
757
|
+
sys.exit(1)
|
|
758
|
+
|
|
759
|
+
if getattr(args, "json", False):
|
|
760
|
+
print(json.dumps(result.to_dict(), indent=2, ensure_ascii=False))
|
|
761
|
+
else:
|
|
762
|
+
_info(
|
|
763
|
+
f"[sync] {result.files_new} new, {result.files_changed} changed, "
|
|
764
|
+
f"{result.files_unchanged} unchanged, {result.chunks_created} chunks"
|
|
765
|
+
)
|
|
766
|
+
else:
|
|
767
|
+
# Sync all mounts
|
|
768
|
+
results = sync_all(db_path, delta=not full, quiet=quiet)
|
|
769
|
+
if not results:
|
|
770
|
+
_info("[sync] No mounts registered. Use 'memctl mount <path>' or 'memctl sync <path>'.")
|
|
771
|
+
return
|
|
772
|
+
if getattr(args, "json", False):
|
|
773
|
+
print(json.dumps(
|
|
774
|
+
{k: v.to_dict() for k, v in results.items()},
|
|
775
|
+
indent=2, ensure_ascii=False,
|
|
776
|
+
))
|
|
777
|
+
else:
|
|
778
|
+
for path, r in results.items():
|
|
779
|
+
_info(
|
|
780
|
+
f"[sync] {path}: {r.files_new} new, {r.files_changed} changed, "
|
|
781
|
+
f"{r.files_unchanged} unchanged, {r.chunks_created} chunks"
|
|
782
|
+
)
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
# ===========================================================================
|
|
786
|
+
# Command: inspect (structural injection block → stdout)
|
|
787
|
+
# ===========================================================================
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
def cmd_inspect(args: argparse.Namespace) -> None:
|
|
791
|
+
"""Generate a structural injection block from corpus metadata.
|
|
792
|
+
|
|
793
|
+
With positional path: auto-mount + auto-sync + inspect (orchestration mode).
|
|
794
|
+
Without positional path: inspect existing store (classic mode).
|
|
795
|
+
"""
|
|
796
|
+
from memctl.inspect import inspect_mount, inspect_stats
|
|
797
|
+
|
|
798
|
+
db_path = _resolve_db(args)
|
|
799
|
+
budget = _resolve_budget(args)
|
|
800
|
+
|
|
801
|
+
# --- Orchestration mode: positional path provided ---
|
|
802
|
+
path = getattr(args, "path", None)
|
|
803
|
+
if path is not None:
|
|
804
|
+
from memctl.inspect import inspect_path
|
|
805
|
+
|
|
806
|
+
# Warn if --mount filter is also given (path takes precedence)
|
|
807
|
+
if getattr(args, "mount", None):
|
|
808
|
+
_warn("--mount filter ignored when positional path is provided")
|
|
809
|
+
|
|
810
|
+
sync_mode = (
|
|
811
|
+
"never" if getattr(args, "no_sync", False)
|
|
812
|
+
else getattr(args, "sync", "auto")
|
|
813
|
+
)
|
|
814
|
+
mount_mode = getattr(args, "mount_mode", "persist")
|
|
815
|
+
ignore = getattr(args, "ignore", None) or None
|
|
816
|
+
|
|
817
|
+
try:
|
|
818
|
+
result = inspect_path(
|
|
819
|
+
db_path, path,
|
|
820
|
+
sync_mode=sync_mode,
|
|
821
|
+
mount_mode=mount_mode,
|
|
822
|
+
budget=budget,
|
|
823
|
+
ignore_patterns=ignore,
|
|
824
|
+
log=_info,
|
|
825
|
+
)
|
|
826
|
+
except (FileNotFoundError, NotADirectoryError) as e:
|
|
827
|
+
_warn(str(e))
|
|
828
|
+
sys.exit(1)
|
|
829
|
+
except ValueError as e:
|
|
830
|
+
_warn(str(e))
|
|
831
|
+
sys.exit(1)
|
|
832
|
+
|
|
833
|
+
if getattr(args, "json", False):
|
|
834
|
+
print(json.dumps(result.to_dict(), indent=2, ensure_ascii=False))
|
|
835
|
+
else:
|
|
836
|
+
text = inspect_mount(
|
|
837
|
+
db_path,
|
|
838
|
+
mount_id=result.mount_id,
|
|
839
|
+
mount_label=result.mount_label,
|
|
840
|
+
budget=budget,
|
|
841
|
+
)
|
|
842
|
+
print(text)
|
|
843
|
+
return
|
|
844
|
+
|
|
845
|
+
# --- Classic mode: no positional path ---
|
|
846
|
+
mount_filter = getattr(args, "mount", None)
|
|
847
|
+
mount_id = None
|
|
848
|
+
mount_label = None
|
|
849
|
+
if mount_filter:
|
|
850
|
+
from memctl.store import MemoryStore
|
|
851
|
+
store = MemoryStore(db_path=db_path)
|
|
852
|
+
try:
|
|
853
|
+
m = store.read_mount(mount_filter)
|
|
854
|
+
finally:
|
|
855
|
+
store.close()
|
|
856
|
+
if m is None:
|
|
857
|
+
_warn(f"Mount not found: {mount_filter}")
|
|
858
|
+
sys.exit(1)
|
|
859
|
+
mount_id = m["mount_id"]
|
|
860
|
+
mount_label = m.get("name") or m["path"]
|
|
861
|
+
|
|
862
|
+
if getattr(args, "json", False):
|
|
863
|
+
stats = inspect_stats(db_path, mount_id=mount_id)
|
|
864
|
+
print(json.dumps(stats, indent=2, ensure_ascii=False))
|
|
865
|
+
else:
|
|
866
|
+
text = inspect_mount(
|
|
867
|
+
db_path,
|
|
868
|
+
mount_id=mount_id,
|
|
869
|
+
mount_label=mount_label,
|
|
870
|
+
budget=budget,
|
|
871
|
+
)
|
|
872
|
+
print(text)
|
|
873
|
+
|
|
874
|
+
|
|
673
875
|
# ===========================================================================
|
|
674
876
|
# Command: serve (start MCP server)
|
|
675
877
|
# ===========================================================================
|
|
@@ -890,6 +1092,73 @@ def main() -> None:
|
|
|
890
1092
|
)
|
|
891
1093
|
p_loop.set_defaults(func=cmd_loop)
|
|
892
1094
|
|
|
1095
|
+
# -- mount -------------------------------------------------------------
|
|
1096
|
+
p_mount = sub.add_parser(
|
|
1097
|
+
"mount", parents=[_common],
|
|
1098
|
+
help="Register folder for sync",
|
|
1099
|
+
)
|
|
1100
|
+
p_mount.add_argument("path", nargs="?", default=None, help="Folder path to mount")
|
|
1101
|
+
p_mount.add_argument("--name", default=None, help="Human-readable label")
|
|
1102
|
+
p_mount.add_argument(
|
|
1103
|
+
"--ignore", nargs="+", default=None,
|
|
1104
|
+
help="Glob patterns to exclude during sync",
|
|
1105
|
+
)
|
|
1106
|
+
p_mount.add_argument(
|
|
1107
|
+
"--lang", default=None, choices=["fr", "en", "mix"],
|
|
1108
|
+
help="Language hint for FTS tokenizer selection",
|
|
1109
|
+
)
|
|
1110
|
+
p_mount.add_argument("--list", action="store_true", default=argparse.SUPPRESS, help="List mounts")
|
|
1111
|
+
p_mount.add_argument("--remove", default=None, metavar="ID", help="Remove mount by ID or name")
|
|
1112
|
+
p_mount.set_defaults(func=cmd_mount)
|
|
1113
|
+
|
|
1114
|
+
# -- sync --------------------------------------------------------------
|
|
1115
|
+
p_sync = sub.add_parser(
|
|
1116
|
+
"sync", parents=[_common],
|
|
1117
|
+
help="Scan + ingest mounted folders",
|
|
1118
|
+
)
|
|
1119
|
+
p_sync.add_argument("path", nargs="?", default=None, help="Folder path (auto-registers)")
|
|
1120
|
+
p_sync.add_argument(
|
|
1121
|
+
"--full", action="store_true",
|
|
1122
|
+
help="Re-process all files (ignore delta cache)",
|
|
1123
|
+
)
|
|
1124
|
+
p_sync.set_defaults(func=cmd_sync)
|
|
1125
|
+
|
|
1126
|
+
# -- inspect -----------------------------------------------------------
|
|
1127
|
+
p_inspect = sub.add_parser(
|
|
1128
|
+
"inspect", parents=[_common],
|
|
1129
|
+
help="Structural injection block → stdout (auto-mounts/syncs if path given)",
|
|
1130
|
+
)
|
|
1131
|
+
p_inspect.add_argument(
|
|
1132
|
+
"path", nargs="?", default=None,
|
|
1133
|
+
help="Folder to inspect (auto-mounts and auto-syncs as needed)",
|
|
1134
|
+
)
|
|
1135
|
+
p_inspect.add_argument(
|
|
1136
|
+
"--mount", default=None, metavar="ID_OR_NAME",
|
|
1137
|
+
help="Filter by registered mount (classic mode, no positional path)",
|
|
1138
|
+
)
|
|
1139
|
+
p_inspect.add_argument(
|
|
1140
|
+
"--budget", type=int, default=None,
|
|
1141
|
+
help="Token budget (default: MEMCTL_BUDGET or 2200)",
|
|
1142
|
+
)
|
|
1143
|
+
p_inspect.add_argument(
|
|
1144
|
+
"--sync", default="auto", choices=["auto", "always", "never"],
|
|
1145
|
+
help="Sync mode: auto (default), always, or never",
|
|
1146
|
+
)
|
|
1147
|
+
p_inspect.add_argument(
|
|
1148
|
+
"--no-sync", action="store_true", default=False,
|
|
1149
|
+
help="Skip sync (equivalent to --sync=never)",
|
|
1150
|
+
)
|
|
1151
|
+
p_inspect.add_argument(
|
|
1152
|
+
"--mount-mode", dest="mount_mode", default="persist",
|
|
1153
|
+
choices=["persist", "ephemeral"],
|
|
1154
|
+
help="Keep mount (persist, default) or remove after inspect (ephemeral)",
|
|
1155
|
+
)
|
|
1156
|
+
p_inspect.add_argument(
|
|
1157
|
+
"--ignore", nargs="+", default=None,
|
|
1158
|
+
help="Glob patterns to exclude when auto-mounting",
|
|
1159
|
+
)
|
|
1160
|
+
p_inspect.set_defaults(func=cmd_inspect)
|
|
1161
|
+
|
|
893
1162
|
# -- serve -------------------------------------------------------------
|
|
894
1163
|
p_serve = sub.add_parser("serve", parents=[_common], help="Start MCP server")
|
|
895
1164
|
p_serve.add_argument(
|