keep-skill 0.4.1__py3-none-any.whl → 0.6.0__py3-none-any.whl
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.
- keep/__init__.py +3 -2
- keep/api.py +193 -41
- keep/cli.py +134 -54
- keep/config.py +12 -7
- keep/data/__init__.py +1 -0
- keep/data/system/__init__.py +1 -0
- keep/data/system/conversations.md +299 -0
- keep/data/system/domains.md +179 -0
- keep/data/system/now.md +19 -0
- keep/store.py +3 -9
- keep/types.py +4 -0
- {keep_skill-0.4.1.dist-info → keep_skill-0.6.0.dist-info}/METADATA +12 -8
- {keep_skill-0.4.1.dist-info → keep_skill-0.6.0.dist-info}/RECORD +16 -11
- {keep_skill-0.4.1.dist-info → keep_skill-0.6.0.dist-info}/WHEEL +0 -0
- {keep_skill-0.4.1.dist-info → keep_skill-0.6.0.dist-info}/entry_points.txt +0 -0
- {keep_skill-0.4.1.dist-info → keep_skill-0.6.0.dist-info}/licenses/LICENSE +0 -0
keep/__init__.py
CHANGED
|
@@ -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.
|
|
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
|
]
|
keep/api.py
CHANGED
|
@@ -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 =
|
|
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
|
-
#
|
|
299
|
-
self.
|
|
353
|
+
# Migrate and ensure system documents (idempotent)
|
|
354
|
+
self._migrate_system_documents()
|
|
300
355
|
|
|
301
|
-
def
|
|
356
|
+
def _migrate_system_documents(self) -> dict:
|
|
302
357
|
"""
|
|
303
|
-
|
|
358
|
+
Migrate system documents to stable IDs and current version.
|
|
304
359
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
|
311
|
-
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
|
585
|
-
|
|
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
|
|
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 [
|
|
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
|
|
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
|
-
|
|
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:
|