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.
Files changed (44) hide show
  1. {memctl-0.2.0 → memctl-0.3.0}/PKG-INFO +92 -12
  2. {memctl-0.2.0 → memctl-0.3.0}/README.md +91 -11
  3. {memctl-0.2.0 → memctl-0.3.0}/memctl/__init__.py +1 -1
  4. {memctl-0.2.0 → memctl-0.3.0}/memctl/cli.py +269 -0
  5. {memctl-0.2.0 → memctl-0.3.0}/memctl/ingest.py +6 -2
  6. memctl-0.3.0/memctl/inspect.py +586 -0
  7. memctl-0.3.0/memctl/mount.py +111 -0
  8. {memctl-0.2.0 → memctl-0.3.0}/memctl/store.py +183 -5
  9. memctl-0.3.0/memctl/sync.py +368 -0
  10. {memctl-0.2.0 → memctl-0.3.0}/memctl.egg-info/PKG-INFO +92 -12
  11. {memctl-0.2.0 → memctl-0.3.0}/memctl.egg-info/SOURCES.txt +6 -0
  12. {memctl-0.2.0 → memctl-0.3.0}/pyproject.toml +1 -1
  13. {memctl-0.2.0 → memctl-0.3.0}/tests/test_cli.py +211 -1
  14. {memctl-0.2.0 → memctl-0.3.0}/tests/test_forward_compat.py +50 -0
  15. memctl-0.3.0/tests/test_inspect.py +571 -0
  16. memctl-0.3.0/tests/test_mount.py +118 -0
  17. {memctl-0.2.0 → memctl-0.3.0}/tests/test_store.py +177 -1
  18. memctl-0.3.0/tests/test_sync.py +297 -0
  19. {memctl-0.2.0 → memctl-0.3.0}/LICENSE +0 -0
  20. {memctl-0.2.0 → memctl-0.3.0}/memctl/config.py +0 -0
  21. {memctl-0.2.0 → memctl-0.3.0}/memctl/consolidate.py +0 -0
  22. {memctl-0.2.0 → memctl-0.3.0}/memctl/extract.py +0 -0
  23. {memctl-0.2.0 → memctl-0.3.0}/memctl/loop.py +0 -0
  24. {memctl-0.2.0 → memctl-0.3.0}/memctl/mcp/__init__.py +0 -0
  25. {memctl-0.2.0 → memctl-0.3.0}/memctl/mcp/formatting.py +0 -0
  26. {memctl-0.2.0 → memctl-0.3.0}/memctl/mcp/server.py +0 -0
  27. {memctl-0.2.0 → memctl-0.3.0}/memctl/mcp/tools.py +0 -0
  28. {memctl-0.2.0 → memctl-0.3.0}/memctl/policy.py +0 -0
  29. {memctl-0.2.0 → memctl-0.3.0}/memctl/proposer.py +0 -0
  30. {memctl-0.2.0 → memctl-0.3.0}/memctl/similarity.py +0 -0
  31. {memctl-0.2.0 → memctl-0.3.0}/memctl/types.py +0 -0
  32. {memctl-0.2.0 → memctl-0.3.0}/memctl.egg-info/dependency_links.txt +0 -0
  33. {memctl-0.2.0 → memctl-0.3.0}/memctl.egg-info/entry_points.txt +0 -0
  34. {memctl-0.2.0 → memctl-0.3.0}/memctl.egg-info/requires.txt +0 -0
  35. {memctl-0.2.0 → memctl-0.3.0}/memctl.egg-info/top_level.txt +0 -0
  36. {memctl-0.2.0 → memctl-0.3.0}/setup.cfg +0 -0
  37. {memctl-0.2.0 → memctl-0.3.0}/tests/test_contracts.py +0 -0
  38. {memctl-0.2.0 → memctl-0.3.0}/tests/test_extract.py +0 -0
  39. {memctl-0.2.0 → memctl-0.3.0}/tests/test_ingest.py +0 -0
  40. {memctl-0.2.0 → memctl-0.3.0}/tests/test_loop.py +0 -0
  41. {memctl-0.2.0 → memctl-0.3.0}/tests/test_pipe_compose.py +0 -0
  42. {memctl-0.2.0 → memctl-0.3.0}/tests/test_policy.py +0 -0
  43. {memctl-0.2.0 → memctl-0.3.0}/tests/test_similarity.py +0 -0
  44. {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.2.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 and manage
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 (9 tables + schema_meta)
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
- ├── cli.py 9 CLI commands
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
- 16 source files. ~5,300 lines. Zero compiled dependencies for core.
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. 9 tables + 1 FTS5 virtual table:
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 registry |
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=1`.
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
- pip install ragix[all]
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 .memory/memory.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 | Identical | Identical |
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
- 332 tests covering types, store, policy, ingest, text extraction, similarity, loop controller, forward compatibility, contracts, CLI (subprocess), and pipe composition.
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 and manage
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 (9 tables + schema_meta)
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
- ├── cli.py 9 CLI commands
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
- 16 source files. ~5,300 lines. Zero compiled dependencies for core.
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. 9 tables + 1 FTS5 virtual table:
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 registry |
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=1`.
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
- pip install ragix[all]
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 .memory/memory.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 | Identical | Identical |
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
- 332 tests covering types, store, policy, ingest, text extraction, similarity, loop controller, forward compatibility, contracts, CLI (subprocess), and pipe composition.
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
 
@@ -7,7 +7,7 @@ Policy-governed, content-addressed, and forward-compatible with RAGIX.
7
7
  Author: Olivier Vitrac, PhD, HDR | olivier.vitrac@adservio.fr | Adservio
8
8
  """
9
9
 
10
- __version__ = "0.2.0"
10
+ __version__ = "0.3.0"
11
11
 
12
12
  from memctl.types import (
13
13
  MemoryItem,
@@ -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(