keep-skill 0.2.0__py3-none-any.whl → 0.3.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 +1 -1
- keep/api.py +265 -10
- keep/cli.py +254 -19
- keep/config.py +2 -2
- keep/document_store.py +351 -12
- keep/pending_summaries.py +6 -0
- keep/providers/embedding_cache.py +6 -0
- keep/store.py +111 -11
- keep_skill-0.3.0.dist-info/METADATA +218 -0
- {keep_skill-0.2.0.dist-info → keep_skill-0.3.0.dist-info}/RECORD +13 -13
- keep_skill-0.2.0.dist-info/METADATA +0 -304
- {keep_skill-0.2.0.dist-info → keep_skill-0.3.0.dist-info}/WHEEL +0 -0
- {keep_skill-0.2.0.dist-info → keep_skill-0.3.0.dist-info}/entry_points.txt +0 -0
- {keep_skill-0.2.0.dist-info → keep_skill-0.3.0.dist-info}/licenses/LICENSE +0 -0
keep/cli.py
CHANGED
|
@@ -16,7 +16,8 @@ from typing import Optional
|
|
|
16
16
|
import typer
|
|
17
17
|
from typing_extensions import Annotated
|
|
18
18
|
|
|
19
|
-
from .api import Keeper
|
|
19
|
+
from .api import Keeper, _text_content_id
|
|
20
|
+
from .document_store import VersionInfo
|
|
20
21
|
from .types import Item
|
|
21
22
|
from .logging_config import configure_quiet_mode, enable_debug_mode
|
|
22
23
|
|
|
@@ -65,15 +66,53 @@ app = typer.Typer(
|
|
|
65
66
|
)
|
|
66
67
|
|
|
67
68
|
|
|
68
|
-
def _format_yaml_frontmatter(
|
|
69
|
-
|
|
69
|
+
def _format_yaml_frontmatter(
|
|
70
|
+
item: Item,
|
|
71
|
+
version_nav: Optional[dict[str, list[VersionInfo]]] = None,
|
|
72
|
+
viewing_version: Optional[int] = None,
|
|
73
|
+
) -> str:
|
|
74
|
+
"""
|
|
75
|
+
Format item as YAML frontmatter with summary as content.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
item: The item to format
|
|
79
|
+
version_nav: Optional version navigation info (prev/next lists)
|
|
80
|
+
viewing_version: If viewing an old version, the version number
|
|
81
|
+
"""
|
|
70
82
|
lines = ["---", f"id: {item.id}"]
|
|
83
|
+
if viewing_version is not None:
|
|
84
|
+
lines.append(f"version: {viewing_version}")
|
|
71
85
|
if item.tags:
|
|
72
86
|
lines.append("tags:")
|
|
73
87
|
for k, v in sorted(item.tags.items()):
|
|
74
88
|
lines.append(f" {k}: {v}")
|
|
75
89
|
if item.score is not None:
|
|
76
90
|
lines.append(f"score: {item.score:.3f}")
|
|
91
|
+
|
|
92
|
+
# Add version navigation if available
|
|
93
|
+
if version_nav:
|
|
94
|
+
if version_nav.get("prev"):
|
|
95
|
+
lines.append("prev:")
|
|
96
|
+
for v in version_nav["prev"]:
|
|
97
|
+
# Show version number, date portion, and truncated summary
|
|
98
|
+
date_part = v.created_at[:10] if v.created_at else "unknown"
|
|
99
|
+
summary_preview = v.summary[:40].replace("\n", " ")
|
|
100
|
+
if len(v.summary) > 40:
|
|
101
|
+
summary_preview += "..."
|
|
102
|
+
lines.append(f" - {v.version}: {date_part} {summary_preview}")
|
|
103
|
+
if version_nav.get("next"):
|
|
104
|
+
lines.append("next:")
|
|
105
|
+
for v in version_nav["next"]:
|
|
106
|
+
date_part = v.created_at[:10] if v.created_at else "unknown"
|
|
107
|
+
summary_preview = v.summary[:40].replace("\n", " ")
|
|
108
|
+
if len(v.summary) > 40:
|
|
109
|
+
summary_preview += "..."
|
|
110
|
+
lines.append(f" - {v.version}: {date_part} {summary_preview}")
|
|
111
|
+
elif viewing_version is not None:
|
|
112
|
+
# Viewing old version and next is empty means current is next
|
|
113
|
+
lines.append("next:")
|
|
114
|
+
lines.append(" - current")
|
|
115
|
+
|
|
77
116
|
lines.append("---")
|
|
78
117
|
lines.append(item.summary) # Summary IS the content
|
|
79
118
|
return "\n".join(lines)
|
|
@@ -154,7 +193,12 @@ SinceOption = Annotated[
|
|
|
154
193
|
# Output Helpers
|
|
155
194
|
# -----------------------------------------------------------------------------
|
|
156
195
|
|
|
157
|
-
def _format_item(
|
|
196
|
+
def _format_item(
|
|
197
|
+
item: Item,
|
|
198
|
+
as_json: bool = False,
|
|
199
|
+
version_nav: Optional[dict[str, list[VersionInfo]]] = None,
|
|
200
|
+
viewing_version: Optional[int] = None,
|
|
201
|
+
) -> str:
|
|
158
202
|
"""
|
|
159
203
|
Format an item for display.
|
|
160
204
|
|
|
@@ -165,14 +209,23 @@ def _format_item(item: Item, as_json: bool = False) -> str:
|
|
|
165
209
|
return json.dumps(item.id) if as_json else item.id
|
|
166
210
|
|
|
167
211
|
if as_json:
|
|
168
|
-
|
|
212
|
+
result = {
|
|
169
213
|
"id": item.id,
|
|
170
214
|
"summary": item.summary,
|
|
171
215
|
"tags": item.tags,
|
|
172
216
|
"score": item.score,
|
|
173
|
-
}
|
|
217
|
+
}
|
|
218
|
+
if viewing_version is not None:
|
|
219
|
+
result["version"] = viewing_version
|
|
220
|
+
if version_nav:
|
|
221
|
+
result["version_nav"] = {
|
|
222
|
+
k: [{"version": v.version, "created_at": v.created_at, "summary": v.summary[:60]}
|
|
223
|
+
for v in versions]
|
|
224
|
+
for k, versions in version_nav.items()
|
|
225
|
+
}
|
|
226
|
+
return json.dumps(result)
|
|
174
227
|
|
|
175
|
-
return _format_yaml_frontmatter(item)
|
|
228
|
+
return _format_yaml_frontmatter(item, version_nav, viewing_version)
|
|
176
229
|
|
|
177
230
|
|
|
178
231
|
def _format_items(items: list[Item], as_json: bool = False) -> str:
|
|
@@ -299,6 +352,25 @@ def search(
|
|
|
299
352
|
typer.echo(_format_items(results, as_json=_get_json_output()))
|
|
300
353
|
|
|
301
354
|
|
|
355
|
+
@app.command("list")
|
|
356
|
+
def list_recent(
|
|
357
|
+
store: StoreOption = None,
|
|
358
|
+
collection: CollectionOption = "default",
|
|
359
|
+
limit: Annotated[int, typer.Option(
|
|
360
|
+
"--limit", "-n",
|
|
361
|
+
help="Number of items to show"
|
|
362
|
+
)] = 10,
|
|
363
|
+
):
|
|
364
|
+
"""
|
|
365
|
+
List recent items by update time.
|
|
366
|
+
|
|
367
|
+
Shows the most recently updated items, newest first.
|
|
368
|
+
"""
|
|
369
|
+
kp = _get_keeper(store, collection)
|
|
370
|
+
results = kp.list_recent(limit=limit)
|
|
371
|
+
typer.echo(_format_items(results, as_json=_get_json_output()))
|
|
372
|
+
|
|
373
|
+
|
|
302
374
|
@app.command()
|
|
303
375
|
def tag(
|
|
304
376
|
query: Annotated[Optional[str], typer.Argument(
|
|
@@ -446,9 +518,14 @@ def update(
|
|
|
446
518
|
|
|
447
519
|
Three input modes (auto-detected):
|
|
448
520
|
keep update file:///path # URI mode: has ://
|
|
449
|
-
keep update "my note" # Text mode:
|
|
521
|
+
keep update "my note" # Text mode: content-addressed ID
|
|
450
522
|
keep update - # Stdin mode: explicit -
|
|
451
523
|
echo "pipe" | keep update # Stdin mode: piped input
|
|
524
|
+
|
|
525
|
+
Text mode uses content-addressed IDs for versioning:
|
|
526
|
+
keep update "my note" # Creates _text:{hash}
|
|
527
|
+
keep update "my note" -t done # Same ID, new version (tag change)
|
|
528
|
+
keep update "different note" # Different ID (new doc)
|
|
452
529
|
"""
|
|
453
530
|
kp = _get_keeper(store, collection)
|
|
454
531
|
parsed_tags = _parse_tags(tags)
|
|
@@ -459,14 +536,16 @@ def update(
|
|
|
459
536
|
content = sys.stdin.read()
|
|
460
537
|
content, frontmatter_tags = _parse_frontmatter(content)
|
|
461
538
|
parsed_tags = {**frontmatter_tags, **parsed_tags} # CLI tags override
|
|
462
|
-
|
|
539
|
+
# Use content-addressed ID for stdin text (enables versioning)
|
|
540
|
+
doc_id = id or _text_content_id(content)
|
|
463
541
|
item = kp.remember(content, id=doc_id, summary=summary, tags=parsed_tags or None, lazy=lazy)
|
|
464
542
|
elif source and "://" in source:
|
|
465
543
|
# URI mode: fetch from URI (ID is the URI itself)
|
|
466
544
|
item = kp.update(source, tags=parsed_tags or None, summary=summary, lazy=lazy)
|
|
467
545
|
elif source:
|
|
468
546
|
# Text mode: inline content (no :// in source)
|
|
469
|
-
|
|
547
|
+
# Use content-addressed ID for text (enables versioning)
|
|
548
|
+
doc_id = id or _text_content_id(source)
|
|
470
549
|
item = kp.remember(source, id=doc_id, summary=summary, tags=parsed_tags or None, lazy=lazy)
|
|
471
550
|
else:
|
|
472
551
|
typer.echo("Error: Provide content, URI, or '-' for stdin", err=True)
|
|
@@ -488,6 +567,14 @@ def now(
|
|
|
488
567
|
"--reset",
|
|
489
568
|
help="Reset to default from system"
|
|
490
569
|
)] = False,
|
|
570
|
+
version: Annotated[Optional[int], typer.Option(
|
|
571
|
+
"--version", "-V",
|
|
572
|
+
help="Get specific version (0=current, 1=previous, etc.)"
|
|
573
|
+
)] = None,
|
|
574
|
+
history: Annotated[bool, typer.Option(
|
|
575
|
+
"--history", "-H",
|
|
576
|
+
help="List all versions"
|
|
577
|
+
)] = False,
|
|
491
578
|
store: StoreOption = None,
|
|
492
579
|
collection: CollectionOption = "default",
|
|
493
580
|
tags: Annotated[Optional[list[str]], typer.Option(
|
|
@@ -503,19 +590,87 @@ def now(
|
|
|
503
590
|
|
|
504
591
|
Examples:
|
|
505
592
|
keep now # Show current context
|
|
506
|
-
keep now "
|
|
507
|
-
keep now -f context.md #
|
|
508
|
-
keep now --reset # Reset to default
|
|
593
|
+
keep now "What's important now" # Update context
|
|
594
|
+
keep now -f context.md # Read content from file
|
|
595
|
+
keep now --reset # Reset to default from system
|
|
596
|
+
keep now -V 1 # Previous version
|
|
597
|
+
keep now --history # List all versions
|
|
509
598
|
"""
|
|
599
|
+
from .api import NOWDOC_ID
|
|
600
|
+
|
|
510
601
|
kp = _get_keeper(store, collection)
|
|
511
602
|
|
|
603
|
+
# Handle history listing
|
|
604
|
+
if history:
|
|
605
|
+
versions = kp.list_versions(NOWDOC_ID, limit=50, collection=collection)
|
|
606
|
+
current = kp.get(NOWDOC_ID, collection=collection)
|
|
607
|
+
|
|
608
|
+
if _get_json_output():
|
|
609
|
+
result = {
|
|
610
|
+
"id": NOWDOC_ID,
|
|
611
|
+
"current": {
|
|
612
|
+
"summary": current.summary if current else None,
|
|
613
|
+
} if current else None,
|
|
614
|
+
"versions": [
|
|
615
|
+
{
|
|
616
|
+
"version": v.version,
|
|
617
|
+
"summary": v.summary[:60],
|
|
618
|
+
"created_at": v.created_at,
|
|
619
|
+
}
|
|
620
|
+
for v in versions
|
|
621
|
+
],
|
|
622
|
+
}
|
|
623
|
+
typer.echo(json.dumps(result, indent=2))
|
|
624
|
+
else:
|
|
625
|
+
if current:
|
|
626
|
+
summary_preview = current.summary[:60].replace("\n", " ")
|
|
627
|
+
typer.echo(f"Current: {summary_preview}...")
|
|
628
|
+
if versions:
|
|
629
|
+
typer.echo(f"\nVersion history ({len(versions)} archived):")
|
|
630
|
+
for v in versions:
|
|
631
|
+
date_part = v.created_at[:10] if v.created_at else "unknown"
|
|
632
|
+
summary_preview = v.summary[:50].replace("\n", " ")
|
|
633
|
+
if len(v.summary) > 50:
|
|
634
|
+
summary_preview += "..."
|
|
635
|
+
typer.echo(f" v{v.version} ({date_part}): {summary_preview}")
|
|
636
|
+
else:
|
|
637
|
+
typer.echo("No version history.")
|
|
638
|
+
return
|
|
639
|
+
|
|
640
|
+
# Handle version retrieval
|
|
641
|
+
if version is not None:
|
|
642
|
+
offset = version
|
|
643
|
+
if offset == 0:
|
|
644
|
+
item = kp.get_now()
|
|
645
|
+
viewing_version = None
|
|
646
|
+
else:
|
|
647
|
+
item = kp.get_version(NOWDOC_ID, offset, collection=collection)
|
|
648
|
+
versions = kp.list_versions(NOWDOC_ID, limit=1, collection=collection)
|
|
649
|
+
if versions:
|
|
650
|
+
viewing_version = versions[0].version - (offset - 1)
|
|
651
|
+
else:
|
|
652
|
+
viewing_version = None
|
|
653
|
+
|
|
654
|
+
if item is None:
|
|
655
|
+
typer.echo(f"Version not found (offset {offset})", err=True)
|
|
656
|
+
raise typer.Exit(1)
|
|
657
|
+
|
|
658
|
+
version_nav = kp.get_version_nav(NOWDOC_ID, viewing_version, collection=collection)
|
|
659
|
+
typer.echo(_format_item(
|
|
660
|
+
item,
|
|
661
|
+
as_json=_get_json_output(),
|
|
662
|
+
version_nav=version_nav,
|
|
663
|
+
viewing_version=viewing_version,
|
|
664
|
+
))
|
|
665
|
+
return
|
|
666
|
+
|
|
512
667
|
# Determine if we're getting or setting
|
|
513
668
|
setting = content is not None or file is not None or reset
|
|
514
669
|
|
|
515
670
|
if setting:
|
|
516
671
|
if reset:
|
|
517
672
|
# Reset to default from system (delete first to clear old tags)
|
|
518
|
-
from .api import _load_frontmatter,
|
|
673
|
+
from .api import _load_frontmatter, SYSTEM_DOC_DIR
|
|
519
674
|
kp.delete(NOWDOC_ID)
|
|
520
675
|
try:
|
|
521
676
|
new_content, default_tags = _load_frontmatter(SYSTEM_DOC_DIR / "now.md")
|
|
@@ -545,28 +700,108 @@ def now(
|
|
|
545
700
|
item = kp.set_now(new_content, tags=parsed_tags or None)
|
|
546
701
|
typer.echo(_format_item(item, as_json=_get_json_output()))
|
|
547
702
|
else:
|
|
548
|
-
# Get current context
|
|
703
|
+
# Get current context with version navigation
|
|
549
704
|
item = kp.get_now()
|
|
550
|
-
|
|
705
|
+
version_nav = kp.get_version_nav(NOWDOC_ID, None, collection=collection)
|
|
706
|
+
typer.echo(_format_item(
|
|
707
|
+
item,
|
|
708
|
+
as_json=_get_json_output(),
|
|
709
|
+
version_nav=version_nav,
|
|
710
|
+
))
|
|
551
711
|
|
|
552
712
|
|
|
553
713
|
@app.command()
|
|
554
714
|
def get(
|
|
555
715
|
id: Annotated[str, typer.Argument(help="URI of item to retrieve")],
|
|
716
|
+
version: Annotated[Optional[int], typer.Option(
|
|
717
|
+
"--version", "-V",
|
|
718
|
+
help="Get specific version (0=current, 1=previous, etc.)"
|
|
719
|
+
)] = None,
|
|
720
|
+
history: Annotated[bool, typer.Option(
|
|
721
|
+
"--history", "-H",
|
|
722
|
+
help="List all versions"
|
|
723
|
+
)] = False,
|
|
556
724
|
store: StoreOption = None,
|
|
557
725
|
collection: CollectionOption = "default",
|
|
558
726
|
):
|
|
559
727
|
"""
|
|
560
728
|
Retrieve a specific item by ID.
|
|
729
|
+
|
|
730
|
+
Examples:
|
|
731
|
+
keep get doc:1 # Current version with prev nav
|
|
732
|
+
keep get doc:1 -V 1 # Previous version with prev/next nav
|
|
733
|
+
keep get doc:1 --history # List all versions
|
|
561
734
|
"""
|
|
562
735
|
kp = _get_keeper(store, collection)
|
|
563
|
-
|
|
736
|
+
|
|
737
|
+
if history:
|
|
738
|
+
# List all versions
|
|
739
|
+
versions = kp.list_versions(id, limit=50, collection=collection)
|
|
740
|
+
current = kp.get(id, collection=collection)
|
|
741
|
+
|
|
742
|
+
if _get_json_output():
|
|
743
|
+
result = {
|
|
744
|
+
"id": id,
|
|
745
|
+
"current": {
|
|
746
|
+
"summary": current.summary if current else None,
|
|
747
|
+
"tags": current.tags if current else {},
|
|
748
|
+
} if current else None,
|
|
749
|
+
"versions": [
|
|
750
|
+
{
|
|
751
|
+
"version": v.version,
|
|
752
|
+
"summary": v.summary,
|
|
753
|
+
"created_at": v.created_at,
|
|
754
|
+
}
|
|
755
|
+
for v in versions
|
|
756
|
+
],
|
|
757
|
+
}
|
|
758
|
+
typer.echo(json.dumps(result, indent=2))
|
|
759
|
+
else:
|
|
760
|
+
if current:
|
|
761
|
+
typer.echo(f"Current: {current.summary[:60]}...")
|
|
762
|
+
if versions:
|
|
763
|
+
typer.echo(f"\nVersion history ({len(versions)} archived):")
|
|
764
|
+
for v in versions:
|
|
765
|
+
date_part = v.created_at[:10] if v.created_at else "unknown"
|
|
766
|
+
summary_preview = v.summary[:50].replace("\n", " ")
|
|
767
|
+
if len(v.summary) > 50:
|
|
768
|
+
summary_preview += "..."
|
|
769
|
+
typer.echo(f" v{v.version} ({date_part}): {summary_preview}")
|
|
770
|
+
else:
|
|
771
|
+
typer.echo("No version history.")
|
|
772
|
+
return
|
|
773
|
+
|
|
774
|
+
# Get specific version or current
|
|
775
|
+
offset = version if version is not None else 0
|
|
776
|
+
|
|
777
|
+
if offset == 0:
|
|
778
|
+
item = kp.get(id, collection=collection)
|
|
779
|
+
viewing_version = None
|
|
780
|
+
else:
|
|
781
|
+
item = kp.get_version(id, offset, collection=collection)
|
|
782
|
+
# Calculate actual version number for display
|
|
783
|
+
versions = kp.list_versions(id, limit=1, collection=collection)
|
|
784
|
+
if versions:
|
|
785
|
+
viewing_version = versions[0].version - (offset - 1)
|
|
786
|
+
else:
|
|
787
|
+
viewing_version = None
|
|
564
788
|
|
|
565
789
|
if item is None:
|
|
566
|
-
|
|
790
|
+
if offset > 0:
|
|
791
|
+
typer.echo(f"Version not found: {id} (offset {offset})", err=True)
|
|
792
|
+
else:
|
|
793
|
+
typer.echo(f"Not found: {id}", err=True)
|
|
567
794
|
raise typer.Exit(1)
|
|
568
795
|
|
|
569
|
-
|
|
796
|
+
# Get version navigation
|
|
797
|
+
version_nav = kp.get_version_nav(id, viewing_version, collection=collection)
|
|
798
|
+
|
|
799
|
+
typer.echo(_format_item(
|
|
800
|
+
item,
|
|
801
|
+
as_json=_get_json_output(),
|
|
802
|
+
version_nav=version_nav,
|
|
803
|
+
viewing_version=viewing_version,
|
|
804
|
+
))
|
|
570
805
|
|
|
571
806
|
|
|
572
807
|
@app.command()
|
keep/config.py
CHANGED
|
@@ -21,7 +21,7 @@ except ImportError:
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
CONFIG_FILENAME = "keep.toml"
|
|
24
|
-
CONFIG_VERSION =
|
|
24
|
+
CONFIG_VERSION = 3 # Bumped for document versioning support
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
@dataclass
|
|
@@ -89,7 +89,7 @@ class StoreConfig:
|
|
|
89
89
|
default_tags: dict[str, str] = field(default_factory=dict)
|
|
90
90
|
|
|
91
91
|
# Maximum length for summaries (used for smart remember and validation)
|
|
92
|
-
max_summary_length: int =
|
|
92
|
+
max_summary_length: int = 500
|
|
93
93
|
|
|
94
94
|
@property
|
|
95
95
|
def config_path(self) -> Path:
|