keep-skill 0.4.1__tar.gz → 0.6.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 (34) hide show
  1. {keep_skill-0.4.1 → keep_skill-0.6.0}/PKG-INFO +12 -8
  2. {keep_skill-0.4.1 → keep_skill-0.6.0}/README.md +11 -7
  3. {keep_skill-0.4.1 → keep_skill-0.6.0}/SKILL.md +21 -15
  4. {keep_skill-0.4.1 → keep_skill-0.6.0}/keep/__init__.py +3 -2
  5. {keep_skill-0.4.1 → keep_skill-0.6.0}/keep/api.py +193 -41
  6. {keep_skill-0.4.1 → keep_skill-0.6.0}/keep/cli.py +134 -54
  7. {keep_skill-0.4.1 → keep_skill-0.6.0}/keep/config.py +12 -7
  8. keep_skill-0.6.0/keep/data/__init__.py +1 -0
  9. keep_skill-0.6.0/keep/data/system/__init__.py +1 -0
  10. {keep_skill-0.4.1 → keep_skill-0.6.0}/keep/store.py +3 -9
  11. {keep_skill-0.4.1 → keep_skill-0.6.0}/keep/types.py +4 -0
  12. {keep_skill-0.4.1 → keep_skill-0.6.0}/pyproject.toml +1 -6
  13. {keep_skill-0.4.1 → keep_skill-0.6.0}/.gitignore +0 -0
  14. {keep_skill-0.4.1 → keep_skill-0.6.0}/LICENSE +0 -0
  15. {keep_skill-0.4.1 → keep_skill-0.6.0}/keep/__main__.py +0 -0
  16. {keep_skill-0.4.1 → keep_skill-0.6.0}/keep/chunking.py +0 -0
  17. {keep_skill-0.4.1 → keep_skill-0.6.0}/keep/context.py +0 -0
  18. {keep_skill-0.4.1/docs → keep_skill-0.6.0/keep/data}/system/conversations.md +0 -0
  19. {keep_skill-0.4.1/docs → keep_skill-0.6.0/keep/data}/system/domains.md +0 -0
  20. {keep_skill-0.4.1/docs → keep_skill-0.6.0/keep/data}/system/now.md +0 -0
  21. {keep_skill-0.4.1 → keep_skill-0.6.0}/keep/document_store.py +0 -0
  22. {keep_skill-0.4.1 → keep_skill-0.6.0}/keep/errors.py +0 -0
  23. {keep_skill-0.4.1 → keep_skill-0.6.0}/keep/indexing.py +0 -0
  24. {keep_skill-0.4.1 → keep_skill-0.6.0}/keep/logging_config.py +0 -0
  25. {keep_skill-0.4.1 → keep_skill-0.6.0}/keep/paths.py +0 -0
  26. {keep_skill-0.4.1 → keep_skill-0.6.0}/keep/pending_summaries.py +0 -0
  27. {keep_skill-0.4.1 → keep_skill-0.6.0}/keep/providers/__init__.py +0 -0
  28. {keep_skill-0.4.1 → keep_skill-0.6.0}/keep/providers/base.py +0 -0
  29. {keep_skill-0.4.1 → keep_skill-0.6.0}/keep/providers/documents.py +0 -0
  30. {keep_skill-0.4.1 → keep_skill-0.6.0}/keep/providers/embedding_cache.py +0 -0
  31. {keep_skill-0.4.1 → keep_skill-0.6.0}/keep/providers/embeddings.py +0 -0
  32. {keep_skill-0.4.1 → keep_skill-0.6.0}/keep/providers/llm.py +0 -0
  33. {keep_skill-0.4.1 → keep_skill-0.6.0}/keep/providers/mlx.py +0 -0
  34. {keep_skill-0.4.1 → keep_skill-0.6.0}/keep/providers/summarization.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: keep-skill
3
- Version: 0.4.1
3
+ Version: 0.6.0
4
4
  Summary: Semantic memory - remember and search documents by meaning
5
5
  Project-URL: Homepage, https://github.com/hughpyle/keep
6
6
  Project-URL: Repository, https://github.com/hughpyle/keep
@@ -56,7 +56,7 @@ Description-Content-Type: text/markdown
56
56
  Index documents and notes. Search by meaning. Track changes over time.
57
57
 
58
58
  ```bash
59
- pip install 'keep-skill[local]'
59
+ uv tool install 'keep-skill[local]'
60
60
  keep init
61
61
 
62
62
  # Index content
@@ -90,16 +90,20 @@ Backed by ChromaDB for vectors, SQLite for metadata and versions.
90
90
  **Python 3.11–3.13 required.**
91
91
 
92
92
  ```bash
93
- # Recommended: local models (works offline)
94
- pip install 'keep-skill[local]'
95
-
96
- # Or with uv (faster):
93
+ # Recommended: uv (isolated environment, fast)
97
94
  uv tool install 'keep-skill[local]'
98
95
 
99
- # API-based alternative (requires OPENAI_API_KEY)
100
- pip install 'keep-skill[openai]'
96
+ # Alternative: pip in a virtual environment
97
+ python -m venv .venv && source .venv/bin/activate
98
+ pip install 'keep-skill[local]'
99
+
100
+ # API-based (requires OPENAI_API_KEY)
101
+ uv tool install 'keep-skill[openai]'
101
102
  ```
102
103
 
104
+ > **Note:** Always use an isolated environment (uv or venv). Installing with system pip
105
+ > may cause version conflicts with dependencies like typer.
106
+
103
107
  First run downloads embedding models (~3-5 minutes).
104
108
 
105
109
  ---
@@ -5,7 +5,7 @@
5
5
  Index documents and notes. Search by meaning. Track changes over time.
6
6
 
7
7
  ```bash
8
- pip install 'keep-skill[local]'
8
+ uv tool install 'keep-skill[local]'
9
9
  keep init
10
10
 
11
11
  # Index content
@@ -39,16 +39,20 @@ Backed by ChromaDB for vectors, SQLite for metadata and versions.
39
39
  **Python 3.11–3.13 required.**
40
40
 
41
41
  ```bash
42
- # Recommended: local models (works offline)
43
- pip install 'keep-skill[local]'
44
-
45
- # Or with uv (faster):
42
+ # Recommended: uv (isolated environment, fast)
46
43
  uv tool install 'keep-skill[local]'
47
44
 
48
- # API-based alternative (requires OPENAI_API_KEY)
49
- pip install 'keep-skill[openai]'
45
+ # Alternative: pip in a virtual environment
46
+ python -m venv .venv && source .venv/bin/activate
47
+ pip install 'keep-skill[local]'
48
+
49
+ # API-based (requires OPENAI_API_KEY)
50
+ uv tool install 'keep-skill[openai]'
50
51
  ```
51
52
 
53
+ > **Note:** Always use an isolated environment (uv or venv). Installing with system pip
54
+ > may cause version conflicts with dependencies like typer.
55
+
52
56
  First run downloads embedding models (~3-5 minutes).
53
57
 
54
58
  ---
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: keep
3
- version: 0.4.1
3
+ version: 0.6.0
4
4
  description: Associative memory for reflection and skillful action
5
5
  homepage: https://github.com/hughpyle/keep
6
6
  runtime: python:3.12-slim
@@ -79,10 +79,10 @@ This table is a summary. The full framework includes the Language-Action Perspec
79
79
 
80
80
  **Do this now — read the full framework:**
81
81
  ```bash
82
- keep get "file://$PWD/docs/system/conversations.md"
82
+ keep get "_system:conversations"
83
83
  ```
84
84
 
85
- The [conversations.md](docs/system/conversations.md) document is foundational. It teaches you to recognize where you are in work, what stance to take, and how to notice when things shift. Internalize it.
85
+ The conversations document is foundational. It teaches you to recognize where you are in work, what stance to take, and how to notice when things shift. Internalize it.
86
86
 
87
87
  ### The Triple Check
88
88
 
@@ -133,14 +133,14 @@ You've read the core patterns (Layer 2). There's more foundational material.
133
133
 
134
134
  **Do this now — read and index:**
135
135
  ```bash
136
- keep get "file://$PWD/docs/system/domains.md"
136
+ keep get "_system:domains"
137
137
  keep update "file://$PWD/docs/library/true_person_no_rank.md" -t type=teaching -t topic=commentary --lazy
138
138
  keep get "file://$PWD/docs/library/true_person_no_rank.md"
139
139
  ```
140
140
 
141
141
  These aren't test fixtures. They're seed wisdom:
142
142
 
143
- - **[domains.md](docs/system/domains.md)** — How to organize knowledge by domain (software, research, personal). Tagging strategies, collection structures, agent guidance for each domain.
143
+ - **`_system:domains`** — How to organize knowledge by domain (software, research, personal). Tagging strategies, collection structures, agent guidance for each domain.
144
144
 
145
145
  - **[true_person_no_rank.md](docs/library/true_person_no_rank.md)** — A Zen teaching with layers of commentary. Notice how each commentator re-enacts the pointing from a different angle — not explaining, not accumulating facts, but each responding to what students in that era needed. This is how knowledge in your memory should relate: organic, not hierarchical.
146
146
 
@@ -260,7 +260,7 @@ Don't dump everything into context. Navigate the tree:
260
260
  | `find` | Semantic similarity search | `keep find "authentication flow" --limit 5` |
261
261
  | `find --id` | Find similar to existing item | `keep find --id "docid" --limit 3` |
262
262
  | `search` | Full-text search in summaries | `keep search "OAuth"` |
263
- | `list` | List recent item IDs | `keep list` or `keep --full list` |
263
+ | `list` | List recent items | `keep list` or `keep --ids list` for IDs only |
264
264
  | `update` | Index content (URI, text, or stdin) | `keep update "note" -t key=value` |
265
265
  | `get` | Retrieve item (shows similar items) | `keep get "file:///path/to/doc.md"` |
266
266
  | `get --similar` | List similar items | `keep get ID --similar` or `-n 20` for more |
@@ -291,17 +291,23 @@ The `--lazy` flag:
291
291
 
292
292
  ### Output
293
293
 
294
- Default output uses YAML frontmatter format:
294
+ Three formats, consistent across all commands:
295
+
296
+ **Default: Summary lines** (one per item)
297
+ ```
298
+ file:///doc.md@V{0} 2026-01-15 Document about authentication...
299
+ _text:a1b2c3d4@V{0} 2026-01-14 URI detection patterns...
300
+ ```
301
+
302
+ **With `--full`: YAML frontmatter** (`keep get` and `keep now` default to this)
295
303
  ```yaml
296
304
  ---
297
305
  id: file:///path/to/doc.md
298
306
  tags: {project: myapp, domain: auth}
299
307
  similar:
300
- - doc:related-auth (0.89)
301
- - doc:token-notes (0.85)
302
- score: 0.823
308
+ - doc:related-auth@V{0} (0.89) 2026-01-15 Related authentication...
303
309
  prev:
304
- - v1: 2026-01-15 Previous summary...
310
+ - @V{1} 2026-01-14 Previous summary...
305
311
  ---
306
312
  Document summary here...
307
313
  ```
@@ -310,7 +316,7 @@ Global flags (before the command):
310
316
  ```bash
311
317
  keep --json find "auth" # JSON output
312
318
  keep --ids find "auth" # IDs only (for piping)
313
- keep --full list # Full items (overrides IDs-only default)
319
+ keep --full list # Full YAML frontmatter
314
320
  keep -v find "auth" # Debug logging
315
321
  ```
316
322
 
@@ -318,9 +324,9 @@ keep -v find "auth" # Debug logging
318
324
 
319
325
  Use `--ids` for Unix-style composition:
320
326
  ```bash
321
- keep --ids system | xargs keep get # Get all system docs
322
327
  keep --ids find "auth" | xargs keep get # Get full details of matches
323
328
  keep --ids tag project=foo | xargs keep tag-update --tag status=done
329
+ keep --ids list | xargs -I{} keep get "{}" # Get details for recent items
324
330
  ```
325
331
 
326
332
  ### Store Location
@@ -361,5 +367,5 @@ This is the practice. Not once, but every time.
361
367
  - [docs/AGENT-GUIDE.md](docs/AGENT-GUIDE.md) — Detailed patterns for working sessions
362
368
  - [docs/REFERENCE.md](docs/REFERENCE.md) — Complete CLI and API reference
363
369
  - [docs/QUICKSTART.md](docs/QUICKSTART.md) — Installation and setup
364
- - [docs/system/conversations.md](docs/system/conversations.md) — Full conversation framework
365
- - [docs/system/domains.md](docs/system/domains.md) — Domain-specific organization
370
+ - [keep/data/system/conversations.md](keep/data/system/conversations.md) — Full conversation framework (`_system:conversations`)
371
+ - [keep/data/system/domains.md](keep/data/system/domains.md) — Domain-specific organization (`_system:domains`)
@@ -38,13 +38,14 @@ if not os.environ.get("KEEP_VERBOSE"):
38
38
  os.environ.setdefault("HF_HUB_DISABLE_SYMLINKS_WARNING", "1")
39
39
 
40
40
  from .api import Keeper, NOWDOC_ID
41
- from .types import Item, filter_non_system_tags, SYSTEM_TAG_PREFIX
41
+ from .types import Item, filter_non_system_tags, SYSTEM_TAG_PREFIX, INTERNAL_TAGS
42
42
 
43
- __version__ = "0.4.1"
43
+ __version__ = "0.6.0"
44
44
  __all__ = [
45
45
  "Keeper",
46
46
  "Item",
47
47
  "NOWDOC_ID",
48
48
  "filter_non_system_tags",
49
49
  "SYSTEM_TAG_PREFIX",
50
+ "INTERNAL_TAGS",
50
51
  ]
@@ -9,6 +9,7 @@ This is the minimal working implementation focused on:
9
9
  """
10
10
 
11
11
  import hashlib
12
+ import importlib.resources
12
13
  import logging
13
14
  import re
14
15
  from datetime import datetime, timezone, timedelta
@@ -100,6 +101,24 @@ def _filter_by_date(items: list, since: str) -> list:
100
101
  if item.tags.get("_updated_date", "0000-00-00") >= cutoff
101
102
  ]
102
103
 
104
+
105
+ def _record_to_item(rec, score: float = None) -> "Item":
106
+ """
107
+ Convert a DocumentRecord to an Item with timestamp tags.
108
+
109
+ Adds _updated, _created, _updated_date from the record's columns
110
+ to ensure consistent timestamp exposure across all retrieval methods.
111
+ """
112
+ from .types import Item
113
+ tags = {
114
+ **rec.tags,
115
+ "_updated": rec.updated_at,
116
+ "_created": rec.created_at,
117
+ "_updated_date": rec.updated_at[:10] if rec.updated_at else "",
118
+ }
119
+ return Item(id=rec.id, summary=rec.summary, tags=tags, score=score)
120
+
121
+
103
122
  import os
104
123
  import subprocess
105
124
  import sys
@@ -135,8 +154,44 @@ ENV_TAG_PREFIX = "KEEP_TAG_"
135
154
  # Fixed ID for the current working context (singleton)
136
155
  NOWDOC_ID = "_now:default"
137
156
 
157
+
158
+ def _get_system_doc_dir() -> Path:
159
+ """
160
+ Get path to system docs, works in both dev and installed environments.
161
+
162
+ Tries in order:
163
+ 1. Package data via importlib.resources (installed packages)
164
+ 2. Relative path inside package (development)
165
+ 3. Legacy path outside package (backwards compatibility)
166
+ """
167
+ # Try package data first (works for installed packages)
168
+ try:
169
+ with importlib.resources.as_file(
170
+ importlib.resources.files("keep.data.system")
171
+ ) as path:
172
+ if path.exists():
173
+ return path
174
+ except (ModuleNotFoundError, TypeError):
175
+ pass
176
+
177
+ # Fallback to relative path inside package (development)
178
+ dev_path = Path(__file__).parent / "data" / "system"
179
+ if dev_path.exists():
180
+ return dev_path
181
+
182
+ # Legacy fallback (old structure)
183
+ return Path(__file__).parent.parent / "docs" / "system"
184
+
185
+
138
186
  # Path to system documents
139
- SYSTEM_DOC_DIR = Path(__file__).parent.parent / "docs" / "system"
187
+ SYSTEM_DOC_DIR = _get_system_doc_dir()
188
+
189
+ # Stable IDs for system documents (path-independent)
190
+ SYSTEM_DOC_IDS = {
191
+ "now.md": "_system:now",
192
+ "conversations.md": "_system:conversations",
193
+ "domains.md": "_system:domains",
194
+ }
140
195
 
141
196
 
142
197
  def _load_frontmatter(path: Path) -> tuple[str, dict[str, str]]:
@@ -295,32 +350,88 @@ class Keeper:
295
350
  embedding_dimension=embedding_dim,
296
351
  )
297
352
 
298
- # Preload system documents (only if not already present)
299
- self._ensure_system_documents()
353
+ # Migrate and ensure system documents (idempotent)
354
+ self._migrate_system_documents()
300
355
 
301
- def _ensure_system_documents(self) -> None:
356
+ def _migrate_system_documents(self) -> dict:
302
357
  """
303
- Ensure system documents are loaded into the store.
358
+ Migrate system documents to stable IDs and current version.
304
359
 
305
- Scans all .md files in docs/system/. Each file is indexed with its
306
- file:// URI as the ID and `_category: system` tag for identification.
307
- Content becomes the summary directly (no auto-summarization).
360
+ Handles:
361
+ - Migration from old file:// URIs to _system:{name} IDs
362
+ - Fresh creation for new stores
363
+ - Version upgrades when bundled content changes
364
+ - Cleanup of old file:// URIs (from before path was changed)
308
365
 
309
366
  Called during init. Only loads docs that don't already exist,
310
- so user modifications are preserved and no network access occurs
311
- if docs are already present.
367
+ so user modifications are preserved. Updates config version
368
+ after successful migration.
369
+
370
+ Returns:
371
+ Dict with migration stats: created, migrated, skipped, cleaned
312
372
  """
373
+ from .config import SYSTEM_DOCS_VERSION, save_config
374
+
375
+ stats = {"created": 0, "migrated": 0, "skipped": 0, "cleaned": 0}
376
+
377
+ # Skip if already at current version
378
+ if self._config.system_docs_version >= SYSTEM_DOCS_VERSION:
379
+ return stats
380
+
381
+ # Build reverse lookup: filename -> new stable ID
382
+ filename_to_id = {name: doc_id for name, doc_id in SYSTEM_DOC_IDS.items()}
383
+
384
+ # First pass: clean up old file:// URIs with category=system tag
385
+ # These may have different paths than current SYSTEM_DOC_DIR
386
+ try:
387
+ old_system_docs = self.query_tag("category", "system")
388
+ for doc in old_system_docs:
389
+ if doc.id.startswith("file://") and doc.id.endswith(".md"):
390
+ # Extract filename from path
391
+ filename = Path(doc.id.replace("file://", "")).name
392
+ new_id = filename_to_id.get(filename)
393
+ if new_id and not self.exists(new_id):
394
+ # Migrate content to new ID
395
+ self.remember(doc.summary, id=new_id, tags=doc.tags)
396
+ self.delete(doc.id)
397
+ stats["migrated"] += 1
398
+ logger.info("Migrated system doc: %s -> %s", doc.id, new_id)
399
+ elif new_id:
400
+ # New ID already exists, just clean up old one
401
+ self.delete(doc.id)
402
+ stats["cleaned"] += 1
403
+ logger.info("Cleaned up old system doc: %s", doc.id)
404
+ except Exception as e:
405
+ logger.debug("Error scanning old system docs: %s", e)
406
+
407
+ # Second pass: create any missing system docs from bundled content
313
408
  for path in SYSTEM_DOC_DIR.glob("*.md"):
409
+ new_id = SYSTEM_DOC_IDS.get(path.name)
410
+ if new_id is None:
411
+ logger.debug("Skipping unknown system doc: %s", path.name)
412
+ continue
413
+
414
+ # Skip if already exists
415
+ if self.exists(new_id):
416
+ stats["skipped"] += 1
417
+ continue
418
+
314
419
  try:
315
- uri = f"file://{path.resolve()}"
316
- if not self.exists(uri):
317
- content, tags = _load_frontmatter(path)
318
- tags["category"] = "system"
319
- self.remember(content, id=uri, tags=tags)
420
+ content, tags = _load_frontmatter(path)
421
+ tags["category"] = "system"
422
+ self.remember(content, id=new_id, tags=tags)
423
+ stats["created"] += 1
424
+ logger.info("Created system doc: %s", new_id)
320
425
  except FileNotFoundError:
321
426
  # System file missing - skip silently
322
427
  pass
323
428
 
429
+ # Update config version
430
+ self._config.system_docs_version = SYSTEM_DOCS_VERSION
431
+ save_config(self._config)
432
+
433
+ return stats
434
+
324
435
  def _get_embedding_provider(self) -> EmbeddingProvider:
325
436
  """
326
437
  Get embedding provider, creating it lazily on first use.
@@ -581,12 +692,8 @@ class Keeper:
581
692
 
582
693
  # Return the stored item
583
694
  doc_record = self._document_store.get(coll, id)
584
- return Item(
585
- id=doc_record.id,
586
- summary=doc_record.summary,
587
- tags=doc_record.tags,
588
- )
589
-
695
+ return _record_to_item(doc_record)
696
+
590
697
  def remember(
591
698
  self,
592
699
  content: str,
@@ -759,11 +866,7 @@ class Keeper:
759
866
 
760
867
  # Return the stored item
761
868
  doc_record = self._document_store.get(coll, id)
762
- return Item(
763
- id=doc_record.id,
764
- summary=doc_record.summary,
765
- tags=doc_record.tags,
766
- )
869
+ return _record_to_item(doc_record)
767
870
 
768
871
  # -------------------------------------------------------------------------
769
872
  # Query Operations
@@ -962,6 +1065,28 @@ class Keeper:
962
1065
 
963
1066
  return filtered
964
1067
 
1068
+ def get_version_offset(self, item: Item, collection: Optional[str] = None) -> int:
1069
+ """
1070
+ Get version offset (0=current, 1=previous, ...) for an item.
1071
+
1072
+ Converts the internal version number (1=oldest, 2=next...) to the
1073
+ user-visible offset format (0=current, 1=previous, 2=two-ago...).
1074
+
1075
+ Args:
1076
+ item: Item to get version offset for
1077
+ collection: Target collection
1078
+
1079
+ Returns:
1080
+ Version offset (0 for current version)
1081
+ """
1082
+ version_tag = item.tags.get("_version")
1083
+ if not version_tag:
1084
+ return 0 # Current version
1085
+ base_id = item.tags.get("_base_id", item.id)
1086
+ coll = self._resolve_collection(collection)
1087
+ version_count = self._document_store.version_count(coll, base_id)
1088
+ return version_count - int(version_tag) + 1
1089
+
965
1090
  def query_fulltext(
966
1091
  self,
967
1092
  query: str,
@@ -1033,7 +1158,7 @@ class Keeper:
1033
1158
  docs = self._document_store.query_by_tag_key(
1034
1159
  coll, key, limit=limit, since_date=since_date
1035
1160
  )
1036
- return [Item(id=d.id, summary=d.summary, tags=d.tags) for d in docs]
1161
+ return [_record_to_item(d) for d in docs]
1037
1162
 
1038
1163
  # Build tag filter from positional or keyword args
1039
1164
  tag_filter = {}
@@ -1107,11 +1232,7 @@ class Keeper:
1107
1232
  # Try document store first (canonical)
1108
1233
  doc_record = self._document_store.get(coll, id)
1109
1234
  if doc_record:
1110
- return Item(
1111
- id=doc_record.id,
1112
- summary=doc_record.summary,
1113
- tags=doc_record.tags,
1114
- )
1235
+ return _record_to_item(doc_record)
1115
1236
 
1116
1237
  # Fall back to ChromaDB for legacy data
1117
1238
  result = self._store.get(coll, id)
@@ -1249,7 +1370,7 @@ class Keeper:
1249
1370
 
1250
1371
  A singleton document representing what you're currently working on.
1251
1372
  If it doesn't exist, creates one with default content and tags from
1252
- docs/system/now.md.
1373
+ the bundled system now.md file.
1253
1374
 
1254
1375
  Returns:
1255
1376
  The current context Item (never None - auto-creates if missing)
@@ -1306,6 +1427,44 @@ class Keeper:
1306
1427
  """
1307
1428
  return self.query_tag("category", "system", collection=collection)
1308
1429
 
1430
+ def reset_system_documents(self) -> dict:
1431
+ """
1432
+ Force reload all system documents from bundled content.
1433
+
1434
+ This overwrites any user modifications to system documents.
1435
+ Use with caution - primarily for recovery or testing.
1436
+
1437
+ Returns:
1438
+ Dict with stats: reset count
1439
+ """
1440
+ from .config import SYSTEM_DOCS_VERSION, save_config
1441
+
1442
+ stats = {"reset": 0}
1443
+
1444
+ for path in SYSTEM_DOC_DIR.glob("*.md"):
1445
+ new_id = SYSTEM_DOC_IDS.get(path.name)
1446
+ if new_id is None:
1447
+ continue
1448
+
1449
+ try:
1450
+ content, tags = _load_frontmatter(path)
1451
+ tags["category"] = "system"
1452
+
1453
+ # Delete existing (if any) and create fresh
1454
+ self.delete(new_id)
1455
+ self.remember(content, id=new_id, tags=tags)
1456
+ stats["reset"] += 1
1457
+ logger.info("Reset system doc: %s", new_id)
1458
+
1459
+ except FileNotFoundError:
1460
+ logger.warning("System doc file not found: %s", path)
1461
+
1462
+ # Update config version
1463
+ self._config.system_docs_version = SYSTEM_DOCS_VERSION
1464
+ save_config(self._config)
1465
+
1466
+ return stats
1467
+
1309
1468
  def tag(
1310
1469
  self,
1311
1470
  id: str,
@@ -1410,14 +1569,7 @@ class Keeper:
1410
1569
  coll = self._resolve_collection(collection)
1411
1570
  records = self._document_store.list_recent(coll, limit)
1412
1571
 
1413
- return [
1414
- Item(
1415
- id=rec.id,
1416
- summary=rec.summary,
1417
- tags=rec.tags,
1418
- score=None,
1419
- )
1420
- for rec in records
1572
+ return [_record_to_item(rec) for rec in records
1421
1573
  ]
1422
1574
 
1423
1575
  def embedding_cache_stats(self) -> dict:
@@ -17,10 +17,13 @@ from typing import Optional
17
17
  import typer
18
18
  from typing_extensions import Annotated
19
19
 
20
-
21
20
  # Pattern for version identifier suffix: @V{N} where N is digits only
22
21
  VERSION_SUFFIX_PATTERN = re.compile(r'@V\{(\d+)\}$')
23
22
 
23
+ # URI scheme pattern per RFC 3986: scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
24
+ # Used to distinguish URIs from plain text in the update command
25
+ _URI_SCHEME_PATTERN = re.compile(r'^[a-zA-Z][a-zA-Z0-9+.-]*://')
26
+
24
27
  from .api import Keeper, _text_content_id
25
28
  from .document_store import VersionInfo
26
29
  from .types import Item
@@ -82,11 +85,29 @@ app = typer.Typer(
82
85
  )
83
86
 
84
87
 
88
+ # -----------------------------------------------------------------------------
89
+ # Output Formatting
90
+ #
91
+ # Three output formats, controlled by global flags:
92
+ # --ids: versioned ID only (id@V{N})
93
+ # --full: YAML frontmatter with tags, similar items, version nav
94
+ # default: summary line (id@V{N} date summary)
95
+ #
96
+ # JSON output (--json) works with any of the above.
97
+ # -----------------------------------------------------------------------------
98
+
99
+ def _filter_display_tags(tags: dict) -> dict:
100
+ """Filter out internal-only tags for display."""
101
+ from .types import INTERNAL_TAGS
102
+ return {k: v for k, v in tags.items() if k not in INTERNAL_TAGS}
103
+
104
+
85
105
  def _format_yaml_frontmatter(
86
106
  item: Item,
87
107
  version_nav: Optional[dict[str, list[VersionInfo]]] = None,
88
108
  viewing_offset: Optional[int] = None,
89
109
  similar_items: Optional[list[Item]] = None,
110
+ similar_offsets: Optional[dict[str, int]] = None,
90
111
  ) -> str:
91
112
  """
92
113
  Format item as YAML frontmatter with summary as content.
@@ -96,6 +117,7 @@ def _format_yaml_frontmatter(
96
117
  version_nav: Optional version navigation info (prev/next lists)
97
118
  viewing_offset: If viewing an old version, the offset (1=previous, 2=two ago)
98
119
  similar_items: Optional list of similar items to display
120
+ similar_offsets: Version offsets for similar items (item.id -> offset)
99
121
 
100
122
  Note: Offset computation (v1, v2, etc.) assumes version_nav lists
101
123
  are ordered newest-first, matching list_versions() ordering.
@@ -104,20 +126,27 @@ def _format_yaml_frontmatter(
104
126
  lines = ["---", f"id: {item.id}"]
105
127
  if viewing_offset is not None:
106
128
  lines.append(f"version: {viewing_offset}")
107
- if item.tags:
108
- tag_items = ", ".join(f"{k}: {v}" for k, v in sorted(item.tags.items()))
129
+ display_tags = _filter_display_tags(item.tags)
130
+ if display_tags:
131
+ tag_items = ", ".join(f"{k}: {v}" for k, v in sorted(display_tags.items()))
109
132
  lines.append(f"tags: {{{tag_items}}}")
110
133
  if item.score is not None:
111
134
  lines.append(f"score: {item.score:.3f}")
112
135
 
113
- # Add similar items if available
136
+ # Add similar items if available (version-scoped IDs with date and summary)
114
137
  if similar_items:
115
138
  lines.append("similar:")
116
139
  for sim_item in similar_items:
140
+ base_id = sim_item.tags.get("_base_id", sim_item.id)
141
+ offset = (similar_offsets or {}).get(sim_item.id, 0)
117
142
  score_str = f"({sim_item.score:.2f})" if sim_item.score else ""
118
- lines.append(f" - {sim_item.id} {score_str}")
143
+ date_part = sim_item.tags.get("_updated", sim_item.tags.get("_created", ""))[:10]
144
+ summary_preview = sim_item.summary[:40].replace("\n", " ")
145
+ if len(sim_item.summary) > 40:
146
+ summary_preview += "..."
147
+ lines.append(f" - {base_id}@V{{{offset}}} {score_str} {date_part} {summary_preview}")
119
148
 
120
- # Add version navigation if available
149
+ # Add version navigation (just @V{N} since ID is shown at top, with date + summary)
121
150
  if version_nav:
122
151
  # Current offset (0 if viewing current)
123
152
  current_offset = viewing_offset if viewing_offset is not None else 0
@@ -125,33 +154,56 @@ def _format_yaml_frontmatter(
125
154
  if version_nav.get("prev"):
126
155
  lines.append("prev:")
127
156
  for i, v in enumerate(version_nav["prev"]):
128
- # Offset for this prev item: current_offset + i + 1
129
157
  prev_offset = current_offset + i + 1
130
- date_part = v.created_at[:10] if v.created_at else "unknown"
158
+ date_part = v.created_at[:10] if v.created_at else ""
131
159
  summary_preview = v.summary[:40].replace("\n", " ")
132
160
  if len(v.summary) > 40:
133
161
  summary_preview += "..."
134
- lines.append(f" - v{prev_offset}: {date_part} {summary_preview}")
162
+ lines.append(f" - @V{{{prev_offset}}} {date_part} {summary_preview}")
135
163
  if version_nav.get("next"):
136
164
  lines.append("next:")
137
165
  for i, v in enumerate(version_nav["next"]):
138
- # Offset for this next item: current_offset - i - 1
139
166
  next_offset = current_offset - i - 1
140
- date_part = v.created_at[:10] if v.created_at else "unknown"
167
+ date_part = v.created_at[:10] if v.created_at else ""
141
168
  summary_preview = v.summary[:40].replace("\n", " ")
142
169
  if len(v.summary) > 40:
143
170
  summary_preview += "..."
144
- lines.append(f" - v{next_offset}: {date_part} {summary_preview}")
171
+ lines.append(f" - @V{{{next_offset}}} {date_part} {summary_preview}")
145
172
  elif viewing_offset is not None:
146
173
  # Viewing old version and next is empty means current is next
147
174
  lines.append("next:")
148
- lines.append(" - v0 (current)")
175
+ lines.append(f" - @V{{0}}")
149
176
 
150
177
  lines.append("---")
151
178
  lines.append(item.summary) # Summary IS the content
152
179
  return "\n".join(lines)
153
180
 
154
181
 
182
+ def _format_summary_line(item: Item) -> str:
183
+ """Format item as single summary line: id@version date summary"""
184
+ # Get version-scoped ID
185
+ base_id = item.tags.get("_base_id", item.id)
186
+ version = item.tags.get("_version", "0")
187
+ versioned_id = f"{base_id}@V{{{version}}}"
188
+
189
+ # Get date (from _updated_date or _updated or _created)
190
+ date = item.tags.get("_updated_date") or item.tags.get("_updated", "")[:10] or item.tags.get("_created", "")[:10] or ""
191
+
192
+ # Truncate summary to ~60 chars, collapse newlines
193
+ summary = item.summary.replace("\n", " ")
194
+ if len(summary) > 60:
195
+ summary = summary[:57].rsplit(" ", 1)[0] + "..."
196
+
197
+ return f"{versioned_id} {date} {summary}"
198
+
199
+
200
+ def _format_versioned_id(item: Item) -> str:
201
+ """Format item ID with version suffix: id@V{N}"""
202
+ base_id = item.tags.get("_base_id", item.id)
203
+ version = item.tags.get("_version", "0")
204
+ return f"{base_id}@V{{{version}}}"
205
+
206
+
155
207
  @app.callback(invoke_without_command=True)
156
208
  def main_callback(
157
209
  ctx: typer.Context,
@@ -188,11 +240,13 @@ def main_callback(
188
240
  item = kp.get_now()
189
241
  version_nav = kp.get_version_nav(NOWDOC_ID, None, collection="default")
190
242
  similar_items = kp.get_similar_for_display(NOWDOC_ID, limit=3, collection="default")
243
+ similar_offsets = {s.id: kp.get_version_offset(s) for s in similar_items}
191
244
  typer.echo(_format_item(
192
245
  item,
193
246
  as_json=_get_json_output(),
194
247
  version_nav=version_nav,
195
248
  similar_items=similar_items,
249
+ similar_offsets=similar_offsets,
196
250
  ))
197
251
 
198
252
 
@@ -235,38 +289,39 @@ SinceOption = Annotated[
235
289
  ]
236
290
 
237
291
 
238
- # -----------------------------------------------------------------------------
239
- # Output Helpers
240
- # -----------------------------------------------------------------------------
241
-
242
292
  def _format_item(
243
293
  item: Item,
244
294
  as_json: bool = False,
245
295
  version_nav: Optional[dict[str, list[VersionInfo]]] = None,
246
296
  viewing_offset: Optional[int] = None,
247
297
  similar_items: Optional[list[Item]] = None,
298
+ similar_offsets: Optional[dict[str, int]] = None,
248
299
  ) -> str:
249
300
  """
250
- Format an item for display.
301
+ Format a single item for display.
251
302
 
252
- Text format: YAML frontmatter (matches docs/system format)
253
- With --ids: just the ID (for piping)
303
+ Output selection:
304
+ --ids: versioned ID only
305
+ --full or version_nav/similar_items present: YAML frontmatter
306
+ default: summary line (id@V{N} date summary)
254
307
 
255
308
  Args:
256
309
  item: The item to format
257
310
  as_json: Output as JSON
258
- version_nav: Optional version navigation info (prev/next lists)
259
- viewing_offset: If viewing an old version, the offset (1=previous, 2=two ago)
260
- similar_items: Optional list of similar items to display
311
+ version_nav: Version navigation info (triggers full format)
312
+ viewing_offset: Version offset if viewing old version (triggers full format)
313
+ similar_items: Similar items to display (triggers full format)
314
+ similar_offsets: Version offsets for similar items
261
315
  """
262
316
  if _get_ids_output():
263
- return json.dumps(item.id) if as_json else item.id
317
+ versioned_id = _format_versioned_id(item)
318
+ return json.dumps(versioned_id) if as_json else versioned_id
264
319
 
265
320
  if as_json:
266
321
  result = {
267
322
  "id": item.id,
268
323
  "summary": item.summary,
269
- "tags": item.tags,
324
+ "tags": _filter_display_tags(item.tags),
270
325
  "score": item.score,
271
326
  }
272
327
  if viewing_offset is not None:
@@ -274,7 +329,12 @@ def _format_item(
274
329
  result["vid"] = f"{item.id}@V{{{viewing_offset}}}"
275
330
  if similar_items:
276
331
  result["similar"] = [
277
- {"id": s.id, "score": s.score, "summary": s.summary[:60]}
332
+ {
333
+ "id": f"{s.tags.get('_base_id', s.id)}@V{{{(similar_offsets or {}).get(s.id, 0)}}}",
334
+ "score": s.score,
335
+ "date": s.tags.get("_updated", s.tags.get("_created", ""))[:10],
336
+ "summary": s.summary[:60],
337
+ }
278
338
  for s in similar_items
279
339
  ]
280
340
  if version_nav:
@@ -304,13 +364,18 @@ def _format_item(
304
364
  result["version_nav"]["next"] = [{"offset": 0, "vid": f"{item.id}@V{{0}}", "label": "current"}]
305
365
  return json.dumps(result)
306
366
 
307
- return _format_yaml_frontmatter(item, version_nav, viewing_offset, similar_items)
367
+ # Full format when:
368
+ # - --full flag is set
369
+ # - version navigation or similar items are provided (can't display in summary)
370
+ if _get_full_output() or version_nav or similar_items or viewing_offset is not None:
371
+ return _format_yaml_frontmatter(item, version_nav, viewing_offset, similar_items, similar_offsets)
372
+ return _format_summary_line(item)
308
373
 
309
374
 
310
375
  def _format_items(items: list[Item], as_json: bool = False) -> str:
311
376
  """Format multiple items for display."""
312
377
  if _get_ids_output():
313
- ids = [item.id for item in items]
378
+ ids = [_format_versioned_id(item) for item in items]
314
379
  return json.dumps(ids) if as_json else "\n".join(ids)
315
380
 
316
381
  if as_json:
@@ -318,15 +383,20 @@ def _format_items(items: list[Item], as_json: bool = False) -> str:
318
383
  {
319
384
  "id": item.id,
320
385
  "summary": item.summary,
321
- "tags": item.tags,
386
+ "tags": _filter_display_tags(item.tags),
322
387
  "score": item.score,
323
388
  }
324
389
  for item in items
325
390
  ], indent=2)
326
- else:
327
- if not items:
328
- return "No results."
329
- return "\n\n".join(_format_item(item, as_json=False) for item in items)
391
+
392
+ if not items:
393
+ return "No results."
394
+
395
+ # Full format: YAML frontmatter with double-newline separator
396
+ # Default: summary lines with single-newline separator
397
+ if _get_full_output():
398
+ return "\n\n".join(_format_yaml_frontmatter(item) for item in items)
399
+ return "\n".join(_format_summary_line(item) for item in items)
330
400
 
331
401
 
332
402
  def _get_keeper(store: Optional[Path], collection: str) -> Keeper:
@@ -417,7 +487,7 @@ def find(
417
487
 
418
488
  @app.command()
419
489
  def search(
420
- query: Annotated[str, typer.Argument(help="Full-text search query")],
490
+ query: Annotated[str, typer.Argument(default=..., help="Full-text search query")],
421
491
  store: StoreOption = None,
422
492
  collection: CollectionOption = "default",
423
493
  limit: LimitOption = 10,
@@ -443,22 +513,11 @@ def list_recent(
443
513
  """
444
514
  List recent items by update time.
445
515
 
446
- Shows IDs by default (composable). Use --full for detailed output.
516
+ Default: summary lines. Use --ids for IDs only, --full for YAML.
447
517
  """
448
518
  kp = _get_keeper(store, collection)
449
519
  results = kp.list_recent(limit=limit)
450
-
451
- # Determine output mode: --full > --ids > command default (IDs for list)
452
- if _get_json_output():
453
- # JSON always outputs full items
454
- typer.echo(_format_items(results, as_json=True))
455
- elif _get_full_output():
456
- # --full flag: full YAML output
457
- typer.echo(_format_items(results, as_json=False))
458
- else:
459
- # Default for list: IDs only (composable)
460
- for item in results:
461
- typer.echo(item.id)
520
+ typer.echo(_format_items(results, as_json=_get_json_output()))
462
521
 
463
522
 
464
523
  @app.command()
@@ -519,7 +578,7 @@ def tag(
519
578
 
520
579
  @app.command("tag-update")
521
580
  def tag_update(
522
- ids: Annotated[list[str], typer.Argument(help="Document IDs to tag")],
581
+ ids: Annotated[list[str], typer.Argument(default=..., help="Document IDs to tag")],
523
582
  tags: Annotated[Optional[list[str]], typer.Option(
524
583
  "--tag", "-t",
525
584
  help="Tag as key=value (empty value removes: key=)"
@@ -629,7 +688,7 @@ def update(
629
688
  # Use content-addressed ID for stdin text (enables versioning)
630
689
  doc_id = id or _text_content_id(content)
631
690
  item = kp.remember(content, id=doc_id, summary=summary, tags=parsed_tags or None, lazy=lazy)
632
- elif source and "://" in source:
691
+ elif source and _URI_SCHEME_PATTERN.match(source):
633
692
  # URI mode: fetch from URI (ID is the URI itself)
634
693
  item = kp.update(source, tags=parsed_tags or None, summary=summary, lazy=lazy)
635
694
  elif source:
@@ -807,17 +866,19 @@ def now(
807
866
  item = kp.get_now()
808
867
  version_nav = kp.get_version_nav(NOWDOC_ID, None, collection=collection)
809
868
  similar_items = kp.get_similar_for_display(NOWDOC_ID, limit=3, collection=collection)
869
+ similar_offsets = {s.id: kp.get_version_offset(s) for s in similar_items}
810
870
  typer.echo(_format_item(
811
871
  item,
812
872
  as_json=_get_json_output(),
813
873
  version_nav=version_nav,
814
874
  similar_items=similar_items,
875
+ similar_offsets=similar_offsets,
815
876
  ))
816
877
 
817
878
 
818
879
  @app.command()
819
880
  def get(
820
- id: Annotated[str, typer.Argument(help="URI of item (append @V{N} for version)")],
881
+ id: Annotated[str, typer.Argument(default=..., help="URI of item (append @V{N} for version)")],
821
882
  version: Annotated[Optional[int], typer.Option(
822
883
  "--version", "-V",
823
884
  help="Get specific version (0=current, 1=previous, etc.)"
@@ -927,18 +988,22 @@ def get(
927
988
  if similar:
928
989
  # List similar items
929
990
  similar_items = kp.get_similar_for_display(actual_id, limit=limit, collection=collection)
991
+ similar_offsets = {s.id: kp.get_version_offset(s) for s in similar_items}
930
992
 
931
993
  if _get_ids_output():
932
- # Output IDs one per line
994
+ # Output version-scoped IDs one per line
933
995
  for item in similar_items:
934
- typer.echo(item.id)
996
+ base_id = item.tags.get("_base_id", item.id)
997
+ offset = similar_offsets.get(item.id, 0)
998
+ typer.echo(f"{base_id}@V{{{offset}}}")
935
999
  elif _get_json_output():
936
1000
  result = {
937
1001
  "id": actual_id,
938
1002
  "similar": [
939
1003
  {
940
- "id": item.id,
1004
+ "id": f"{item.tags.get('_base_id', item.id)}@V{{{similar_offsets.get(item.id, 0)}}}",
941
1005
  "score": item.score,
1006
+ "date": item.tags.get("_updated", item.tags.get("_created", ""))[:10],
942
1007
  "summary": item.summary[:60],
943
1008
  }
944
1009
  for item in similar_items
@@ -949,11 +1014,14 @@ def get(
949
1014
  typer.echo(f"Similar to {actual_id}:")
950
1015
  if similar_items:
951
1016
  for item in similar_items:
1017
+ base_id = item.tags.get("_base_id", item.id)
1018
+ offset = similar_offsets.get(item.id, 0)
952
1019
  score_str = f"({item.score:.2f})" if item.score else ""
1020
+ date_part = item.tags.get("_updated", item.tags.get("_created", ""))[:10]
953
1021
  summary_preview = item.summary[:50].replace("\n", " ")
954
1022
  if len(item.summary) > 50:
955
1023
  summary_preview += "..."
956
- typer.echo(f" {item.id} {score_str} {summary_preview}")
1024
+ typer.echo(f" {base_id}@V{{{offset}}} {score_str} {date_part} {summary_preview}")
957
1025
  else:
958
1026
  typer.echo(" No similar items found.")
959
1027
  return
@@ -985,8 +1053,10 @@ def get(
985
1053
 
986
1054
  # Get similar items (unless suppressed or viewing old version)
987
1055
  similar_items = None
1056
+ similar_offsets = None
988
1057
  if not no_similar and offset == 0:
989
1058
  similar_items = kp.get_similar_for_display(actual_id, limit=3, collection=collection)
1059
+ similar_offsets = {s.id: kp.get_version_offset(s) for s in similar_items}
990
1060
 
991
1061
  typer.echo(_format_item(
992
1062
  item,
@@ -994,6 +1064,7 @@ def get(
994
1064
  version_nav=version_nav,
995
1065
  viewing_offset=offset if offset > 0 else None,
996
1066
  similar_items=similar_items,
1067
+ similar_offsets=similar_offsets,
997
1068
  ))
998
1069
 
999
1070
 
@@ -1019,6 +1090,10 @@ def list_collections(
1019
1090
 
1020
1091
  @app.command()
1021
1092
  def init(
1093
+ reset_system_docs: Annotated[bool, typer.Option(
1094
+ "--reset-system-docs",
1095
+ help="Force reload system documents from bundled content (overwrites modifications)"
1096
+ )] = False,
1022
1097
  store: StoreOption = None,
1023
1098
  collection: CollectionOption = "default",
1024
1099
  ):
@@ -1027,6 +1102,11 @@ def init(
1027
1102
  """
1028
1103
  kp = _get_keeper(store, collection)
1029
1104
 
1105
+ # Handle reset if requested
1106
+ if reset_system_docs:
1107
+ stats = kp.reset_system_documents()
1108
+ typer.echo(f"Reset {stats['reset']} system documents")
1109
+
1030
1110
  # Show config and store paths
1031
1111
  config = kp._config
1032
1112
  config_path = config.config_path if config else None
@@ -14,14 +14,12 @@ from pathlib import Path
14
14
  from typing import Any, Optional
15
15
 
16
16
  # tomli_w for writing TOML (tomllib is read-only)
17
- try:
18
- import tomli_w
19
- except ImportError:
20
- tomli_w = None # type: ignore
17
+ import tomli_w
21
18
 
22
19
 
23
20
  CONFIG_FILENAME = "keep.toml"
24
21
  CONFIG_VERSION = 3 # Bumped for document versioning support
22
+ SYSTEM_DOCS_VERSION = 1 # Increment when bundled system docs content changes
25
23
 
26
24
 
27
25
  @dataclass
@@ -91,6 +89,9 @@ class StoreConfig:
91
89
  # Maximum length for summaries (used for smart remember and validation)
92
90
  max_summary_length: int = 500
93
91
 
92
+ # System docs version (tracks which bundled docs have been applied to this store)
93
+ system_docs_version: int = 0
94
+
94
95
  @property
95
96
  def config_path(self) -> Path:
96
97
  """Path to the TOML config file."""
@@ -354,6 +355,9 @@ def load_config(config_dir: Path) -> StoreConfig:
354
355
  # Parse max_summary_length (default 500)
355
356
  max_summary_length = data.get("store", {}).get("max_summary_length", 500)
356
357
 
358
+ # Parse system_docs_version (default 0 for stores that predate this feature)
359
+ system_docs_version = data.get("store", {}).get("system_docs_version", 0)
360
+
357
361
  return StoreConfig(
358
362
  path=actual_store,
359
363
  config_dir=config_dir,
@@ -366,6 +370,7 @@ def load_config(config_dir: Path) -> StoreConfig:
366
370
  embedding_identity=parse_embedding_identity(data.get("embedding_identity")),
367
371
  default_tags=default_tags,
368
372
  max_summary_length=max_summary_length,
373
+ system_docs_version=system_docs_version,
369
374
  )
370
375
 
371
376
 
@@ -387,9 +392,6 @@ def save_config(config: StoreConfig) -> None:
387
392
 
388
393
  Creates the directory if it doesn't exist.
389
394
  """
390
- if tomli_w is None:
391
- raise RuntimeError("tomli_w is required to save config. Install with: pip install tomli-w")
392
-
393
395
  # Ensure config directory exists
394
396
  config_location = config.config_dir if config.config_dir else config.path
395
397
  config_location.mkdir(parents=True, exist_ok=True)
@@ -410,6 +412,9 @@ def save_config(config: StoreConfig) -> None:
410
412
  # Only write max_summary_length if not default
411
413
  if config.max_summary_length != 500:
412
414
  store_section["max_summary_length"] = config.max_summary_length
415
+ # Write system_docs_version if set (tracks migration state)
416
+ if config.system_docs_version > 0:
417
+ store_section["system_docs_version"] = config.system_docs_version
413
418
 
414
419
  data = {
415
420
  "store": store_section,
@@ -0,0 +1 @@
1
+ # Package data for keep
@@ -0,0 +1 @@
1
+ # System documents for keep
@@ -58,15 +58,9 @@ class ChromaStore:
58
58
  embedding_dimension: Expected dimension of embeddings (for validation).
59
59
  Can be None for read-only access; will be set on first write.
60
60
  """
61
- try:
62
- import chromadb
63
- from chromadb.config import Settings
64
- except ImportError:
65
- raise RuntimeError(
66
- "ChromaStore requires 'chromadb' library. "
67
- "Install with: pip install chromadb"
68
- )
69
-
61
+ import chromadb
62
+ from chromadb.config import Settings
63
+
70
64
  self._store_path = store_path
71
65
  self._embedding_dimension = embedding_dimension
72
66
 
@@ -9,6 +9,10 @@ from typing import Optional
9
9
  # System tag prefix - tags starting with this are managed by the system
10
10
  SYSTEM_TAG_PREFIX = "_"
11
11
 
12
+ # Tags used internally but hidden from display output
13
+ # These exist for efficient queries/sorting but aren't user-facing
14
+ INTERNAL_TAGS = frozenset({"_updated_date"})
15
+
12
16
 
13
17
  def filter_non_system_tags(tags: dict[str, str]) -> dict[str, str]:
14
18
  """
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "keep-skill"
7
- version = "0.4.1"
7
+ version = "0.6.0"
8
8
  description = "Semantic memory - remember and search documents by meaning"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11,<3.14"
@@ -72,16 +72,11 @@ keep = "keep.cli:main"
72
72
 
73
73
  [tool.hatch.build.targets.wheel]
74
74
  packages = ["keep"]
75
- artifacts = [
76
- "SKILL.md",
77
- "docs/system/**/*.md",
78
- ]
79
75
 
80
76
  [tool.hatch.build.targets.sdist]
81
77
  include = [
82
78
  "/keep",
83
79
  "/SKILL.md",
84
- "/docs/system",
85
80
  "/README.md",
86
81
  "/LICENSE",
87
82
  ]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes