keep-skill 0.4.1__py3-none-any.whl → 0.4.2__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/api.py CHANGED
@@ -962,6 +962,28 @@ class Keeper:
962
962
 
963
963
  return filtered
964
964
 
965
+ def get_version_offset(self, item: Item, collection: Optional[str] = None) -> int:
966
+ """
967
+ Get version offset (0=current, 1=previous, ...) for an item.
968
+
969
+ Converts the internal version number (1=oldest, 2=next...) to the
970
+ user-visible offset format (0=current, 1=previous, 2=two-ago...).
971
+
972
+ Args:
973
+ item: Item to get version offset for
974
+ collection: Target collection
975
+
976
+ Returns:
977
+ Version offset (0 for current version)
978
+ """
979
+ version_tag = item.tags.get("_version")
980
+ if not version_tag:
981
+ return 0 # Current version
982
+ base_id = item.tags.get("_base_id", item.id)
983
+ coll = self._resolve_collection(collection)
984
+ version_count = self._document_store.version_count(coll, base_id)
985
+ return version_count - int(version_tag) + 1
986
+
965
987
  def query_fulltext(
966
988
  self,
967
989
  query: str,
keep/cli.py CHANGED
@@ -17,7 +17,6 @@ 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
 
@@ -87,6 +86,7 @@ def _format_yaml_frontmatter(
87
86
  version_nav: Optional[dict[str, list[VersionInfo]]] = None,
88
87
  viewing_offset: Optional[int] = None,
89
88
  similar_items: Optional[list[Item]] = None,
89
+ similar_offsets: Optional[dict[str, int]] = None,
90
90
  ) -> str:
91
91
  """
92
92
  Format item as YAML frontmatter with summary as content.
@@ -96,6 +96,7 @@ def _format_yaml_frontmatter(
96
96
  version_nav: Optional version navigation info (prev/next lists)
97
97
  viewing_offset: If viewing an old version, the offset (1=previous, 2=two ago)
98
98
  similar_items: Optional list of similar items to display
99
+ similar_offsets: Version offsets for similar items (item.id -> offset)
99
100
 
100
101
  Note: Offset computation (v1, v2, etc.) assumes version_nav lists
101
102
  are ordered newest-first, matching list_versions() ordering.
@@ -110,14 +111,20 @@ def _format_yaml_frontmatter(
110
111
  if item.score is not None:
111
112
  lines.append(f"score: {item.score:.3f}")
112
113
 
113
- # Add similar items if available
114
+ # Add similar items if available (version-scoped IDs with date and summary)
114
115
  if similar_items:
115
116
  lines.append("similar:")
116
117
  for sim_item in similar_items:
118
+ base_id = sim_item.tags.get("_base_id", sim_item.id)
119
+ offset = (similar_offsets or {}).get(sim_item.id, 0)
117
120
  score_str = f"({sim_item.score:.2f})" if sim_item.score else ""
118
- lines.append(f" - {sim_item.id} {score_str}")
121
+ date_part = sim_item.tags.get("_updated", sim_item.tags.get("_created", ""))[:10]
122
+ summary_preview = sim_item.summary[:40].replace("\n", " ")
123
+ if len(sim_item.summary) > 40:
124
+ summary_preview += "..."
125
+ lines.append(f" - {base_id}@V{{{offset}}} {score_str} {date_part} {summary_preview}")
119
126
 
120
- # Add version navigation if available
127
+ # Add version navigation (just @V{N} since ID is shown at top, with date + summary)
121
128
  if version_nav:
122
129
  # Current offset (0 if viewing current)
123
130
  current_offset = viewing_offset if viewing_offset is not None else 0
@@ -125,27 +132,25 @@ def _format_yaml_frontmatter(
125
132
  if version_nav.get("prev"):
126
133
  lines.append("prev:")
127
134
  for i, v in enumerate(version_nav["prev"]):
128
- # Offset for this prev item: current_offset + i + 1
129
135
  prev_offset = current_offset + i + 1
130
- date_part = v.created_at[:10] if v.created_at else "unknown"
136
+ date_part = v.created_at[:10] if v.created_at else ""
131
137
  summary_preview = v.summary[:40].replace("\n", " ")
132
138
  if len(v.summary) > 40:
133
139
  summary_preview += "..."
134
- lines.append(f" - v{prev_offset}: {date_part} {summary_preview}")
140
+ lines.append(f" - @V{{{prev_offset}}} {date_part} {summary_preview}")
135
141
  if version_nav.get("next"):
136
142
  lines.append("next:")
137
143
  for i, v in enumerate(version_nav["next"]):
138
- # Offset for this next item: current_offset - i - 1
139
144
  next_offset = current_offset - i - 1
140
- date_part = v.created_at[:10] if v.created_at else "unknown"
145
+ date_part = v.created_at[:10] if v.created_at else ""
141
146
  summary_preview = v.summary[:40].replace("\n", " ")
142
147
  if len(v.summary) > 40:
143
148
  summary_preview += "..."
144
- lines.append(f" - v{next_offset}: {date_part} {summary_preview}")
149
+ lines.append(f" - @V{{{next_offset}}} {date_part} {summary_preview}")
145
150
  elif viewing_offset is not None:
146
151
  # Viewing old version and next is empty means current is next
147
152
  lines.append("next:")
148
- lines.append(" - v0 (current)")
153
+ lines.append(f" - @V{{0}}")
149
154
 
150
155
  lines.append("---")
151
156
  lines.append(item.summary) # Summary IS the content
@@ -188,11 +193,13 @@ def main_callback(
188
193
  item = kp.get_now()
189
194
  version_nav = kp.get_version_nav(NOWDOC_ID, None, collection="default")
190
195
  similar_items = kp.get_similar_for_display(NOWDOC_ID, limit=3, collection="default")
196
+ similar_offsets = {s.id: kp.get_version_offset(s) for s in similar_items}
191
197
  typer.echo(_format_item(
192
198
  item,
193
199
  as_json=_get_json_output(),
194
200
  version_nav=version_nav,
195
201
  similar_items=similar_items,
202
+ similar_offsets=similar_offsets,
196
203
  ))
197
204
 
198
205
 
@@ -245,6 +252,7 @@ def _format_item(
245
252
  version_nav: Optional[dict[str, list[VersionInfo]]] = None,
246
253
  viewing_offset: Optional[int] = None,
247
254
  similar_items: Optional[list[Item]] = None,
255
+ similar_offsets: Optional[dict[str, int]] = None,
248
256
  ) -> str:
249
257
  """
250
258
  Format an item for display.
@@ -258,6 +266,7 @@ def _format_item(
258
266
  version_nav: Optional version navigation info (prev/next lists)
259
267
  viewing_offset: If viewing an old version, the offset (1=previous, 2=two ago)
260
268
  similar_items: Optional list of similar items to display
269
+ similar_offsets: Version offsets for similar items (item.id -> offset)
261
270
  """
262
271
  if _get_ids_output():
263
272
  return json.dumps(item.id) if as_json else item.id
@@ -274,7 +283,12 @@ def _format_item(
274
283
  result["vid"] = f"{item.id}@V{{{viewing_offset}}}"
275
284
  if similar_items:
276
285
  result["similar"] = [
277
- {"id": s.id, "score": s.score, "summary": s.summary[:60]}
286
+ {
287
+ "id": f"{s.tags.get('_base_id', s.id)}@V{{{(similar_offsets or {}).get(s.id, 0)}}}",
288
+ "score": s.score,
289
+ "date": s.tags.get("_updated", s.tags.get("_created", ""))[:10],
290
+ "summary": s.summary[:60],
291
+ }
278
292
  for s in similar_items
279
293
  ]
280
294
  if version_nav:
@@ -304,7 +318,7 @@ def _format_item(
304
318
  result["version_nav"]["next"] = [{"offset": 0, "vid": f"{item.id}@V{{0}}", "label": "current"}]
305
319
  return json.dumps(result)
306
320
 
307
- return _format_yaml_frontmatter(item, version_nav, viewing_offset, similar_items)
321
+ return _format_yaml_frontmatter(item, version_nav, viewing_offset, similar_items, similar_offsets)
308
322
 
309
323
 
310
324
  def _format_items(items: list[Item], as_json: bool = False) -> str:
@@ -417,7 +431,7 @@ def find(
417
431
 
418
432
  @app.command()
419
433
  def search(
420
- query: Annotated[str, typer.Argument(help="Full-text search query")],
434
+ query: Annotated[str, typer.Argument(default=..., help="Full-text search query")],
421
435
  store: StoreOption = None,
422
436
  collection: CollectionOption = "default",
423
437
  limit: LimitOption = 10,
@@ -519,7 +533,7 @@ def tag(
519
533
 
520
534
  @app.command("tag-update")
521
535
  def tag_update(
522
- ids: Annotated[list[str], typer.Argument(help="Document IDs to tag")],
536
+ ids: Annotated[list[str], typer.Argument(default=..., help="Document IDs to tag")],
523
537
  tags: Annotated[Optional[list[str]], typer.Option(
524
538
  "--tag", "-t",
525
539
  help="Tag as key=value (empty value removes: key=)"
@@ -807,17 +821,19 @@ def now(
807
821
  item = kp.get_now()
808
822
  version_nav = kp.get_version_nav(NOWDOC_ID, None, collection=collection)
809
823
  similar_items = kp.get_similar_for_display(NOWDOC_ID, limit=3, collection=collection)
824
+ similar_offsets = {s.id: kp.get_version_offset(s) for s in similar_items}
810
825
  typer.echo(_format_item(
811
826
  item,
812
827
  as_json=_get_json_output(),
813
828
  version_nav=version_nav,
814
829
  similar_items=similar_items,
830
+ similar_offsets=similar_offsets,
815
831
  ))
816
832
 
817
833
 
818
834
  @app.command()
819
835
  def get(
820
- id: Annotated[str, typer.Argument(help="URI of item (append @V{N} for version)")],
836
+ id: Annotated[str, typer.Argument(default=..., help="URI of item (append @V{N} for version)")],
821
837
  version: Annotated[Optional[int], typer.Option(
822
838
  "--version", "-V",
823
839
  help="Get specific version (0=current, 1=previous, etc.)"
@@ -927,18 +943,22 @@ def get(
927
943
  if similar:
928
944
  # List similar items
929
945
  similar_items = kp.get_similar_for_display(actual_id, limit=limit, collection=collection)
946
+ similar_offsets = {s.id: kp.get_version_offset(s) for s in similar_items}
930
947
 
931
948
  if _get_ids_output():
932
- # Output IDs one per line
949
+ # Output version-scoped IDs one per line
933
950
  for item in similar_items:
934
- typer.echo(item.id)
951
+ base_id = item.tags.get("_base_id", item.id)
952
+ offset = similar_offsets.get(item.id, 0)
953
+ typer.echo(f"{base_id}@V{{{offset}}}")
935
954
  elif _get_json_output():
936
955
  result = {
937
956
  "id": actual_id,
938
957
  "similar": [
939
958
  {
940
- "id": item.id,
959
+ "id": f"{item.tags.get('_base_id', item.id)}@V{{{similar_offsets.get(item.id, 0)}}}",
941
960
  "score": item.score,
961
+ "date": item.tags.get("_updated", item.tags.get("_created", ""))[:10],
942
962
  "summary": item.summary[:60],
943
963
  }
944
964
  for item in similar_items
@@ -949,11 +969,14 @@ def get(
949
969
  typer.echo(f"Similar to {actual_id}:")
950
970
  if similar_items:
951
971
  for item in similar_items:
972
+ base_id = item.tags.get("_base_id", item.id)
973
+ offset = similar_offsets.get(item.id, 0)
952
974
  score_str = f"({item.score:.2f})" if item.score else ""
975
+ date_part = item.tags.get("_updated", item.tags.get("_created", ""))[:10]
953
976
  summary_preview = item.summary[:50].replace("\n", " ")
954
977
  if len(item.summary) > 50:
955
978
  summary_preview += "..."
956
- typer.echo(f" {item.id} {score_str} {summary_preview}")
979
+ typer.echo(f" {base_id}@V{{{offset}}} {score_str} {date_part} {summary_preview}")
957
980
  else:
958
981
  typer.echo(" No similar items found.")
959
982
  return
@@ -985,8 +1008,10 @@ def get(
985
1008
 
986
1009
  # Get similar items (unless suppressed or viewing old version)
987
1010
  similar_items = None
1011
+ similar_offsets = None
988
1012
  if not no_similar and offset == 0:
989
1013
  similar_items = kp.get_similar_for_display(actual_id, limit=3, collection=collection)
1014
+ similar_offsets = {s.id: kp.get_version_offset(s) for s in similar_items}
990
1015
 
991
1016
  typer.echo(_format_item(
992
1017
  item,
@@ -994,6 +1019,7 @@ def get(
994
1019
  version_nav=version_nav,
995
1020
  viewing_offset=offset if offset > 0 else None,
996
1021
  similar_items=similar_items,
1022
+ similar_offsets=similar_offsets,
997
1023
  ))
998
1024
 
999
1025
 
keep/config.py CHANGED
@@ -14,10 +14,7 @@ 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"
@@ -387,9 +384,6 @@ def save_config(config: StoreConfig) -> None:
387
384
 
388
385
  Creates the directory if it doesn't exist.
389
386
  """
390
- if tomli_w is None:
391
- raise RuntimeError("tomli_w is required to save config. Install with: pip install tomli-w")
392
-
393
387
  # Ensure config directory exists
394
388
  config_location = config.config_dir if config.config_dir else config.path
395
389
  config_location.mkdir(parents=True, exist_ok=True)
keep/store.py CHANGED
@@ -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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: keep-skill
3
- Version: 0.4.1
3
+ Version: 0.4.2
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
  ---
@@ -1,9 +1,9 @@
1
1
  keep/__init__.py,sha256=1WVkySoomQuf9-o3pIKs1CC2OwIyfkiaCxn-mO6nhd8,1581
2
2
  keep/__main__.py,sha256=3Uu70IhIDIjh8OW6jp9jQQ3dF2lKdJWi_3FtRIQMiMY,104
3
- keep/api.py,sha256=EaPkI9Gv1r5LM8ZibqquGP_59xf4kQlCdv7nfct8QZw,58894
3
+ keep/api.py,sha256=7oa6jdmeE55dATOEVw18sXGVBVndjiD_7tMDcP_xl_0,59746
4
4
  keep/chunking.py,sha256=neAXOLSvVwbUxapbqq7nZrbSNSzMXuhxj-ODoOSodsU,11830
5
- keep/cli.py,sha256=3flcfZHjvnHY8TxT-HJBBAuKnDusPAUD3ZwuMYYU7sw,40470
6
- keep/config.py,sha256=RRnHHvhc9KkJBYt0rpAFIvAVXw40b56xtT74TFIBiDU,15832
5
+ keep/cli.py,sha256=DzZ5Dy4U25q-Mnbv5WbfRsZaN-ped24GDk34fVddmFA,42440
6
+ keep/config.py,sha256=xhsTS_55HSzYxFNGt2z0q_0Ne-s9L9_3om8Uf_8gHB4,15643
7
7
  keep/context.py,sha256=CNpjmrv6eW2kV1E0MO6qAQfhYKRlfzAL--6v4Mj1nFY,71
8
8
  keep/document_store.py,sha256=UswqKIGSc5E-r7Tg9k0g5-byYnuar3e9FieQ7WNod9k,29109
9
9
  keep/errors.py,sha256=G9e5FbdfeugyfHOuL_SPZlM5jgWWnwsX4hM7IzanBZc,857
@@ -11,7 +11,7 @@ keep/indexing.py,sha256=dpPYo3WXnIhFDWinz5ZBZVk7_qumeNpP4EpOIY0zMbs,6063
11
11
  keep/logging_config.py,sha256=IGwkgIyg-TfYaT4MnoCXfmjeHAe_wsB_XQ1QhVT_ro8,3503
12
12
  keep/paths.py,sha256=Dv7pM6oo2QgjL6sj5wPjhuMOK2wqUkfd4Kz08TwJ1ps,3331
13
13
  keep/pending_summaries.py,sha256=_irGe7P1Lmog2c5cEgx-BElpq4YJW-tEmF5A3IUZQbQ,5727
14
- keep/store.py,sha256=d_exdEBQM7agpJcXL-nGsl46zXPQ2t35C8QF-NQX_Bw,18097
14
+ keep/store.py,sha256=SBc2QdTyApdDDVjm2uZQI6tGbV5Hurfetgj7dyTO65o,17881
15
15
  keep/types.py,sha256=f6uOSYsYt6mj1ulKn2iRkooi__dWCiOQFPD6he2eID4,2149
16
16
  keep/providers/__init__.py,sha256=GFX_12g9OdjmpFUkTekOQBOWvcRW2Ae6yidfVVW2SiI,1095
17
17
  keep/providers/base.py,sha256=7Ug4Kj9fK2Dq4zDcZjn-GKsoZBOAlB9b-FMk969ER-g,14590
@@ -21,8 +21,8 @@ keep/providers/embeddings.py,sha256=zi8GyitKexdbCJyU1nLrUhGt_zzPn3udYrrPZ5Ak8Wo,
21
21
  keep/providers/llm.py,sha256=BxROKOklKbkGsHcSADPNNgWQExgSN6Bg4KPQIxVuB3U,12441
22
22
  keep/providers/mlx.py,sha256=aNl00r9tGi5tCGj2ArYH7CmDHtL1jLjVzb1rofU1DAo,9050
23
23
  keep/providers/summarization.py,sha256=MlVTcYipaqp2lT-QYnznp0AMuPVG36QfcTQnvY7Gb-Q,3409
24
- keep_skill-0.4.1.dist-info/METADATA,sha256=sasoFYgfL9FK4nuIK99pruVoiUH65I-SN30giqc2wHs,6387
25
- keep_skill-0.4.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
26
- keep_skill-0.4.1.dist-info/entry_points.txt,sha256=W8yiI4kNeW0IC8ji4EHRWrvdhFxzaqTIePUhJAJAMOo,39
27
- keep_skill-0.4.1.dist-info/licenses/LICENSE,sha256=zsm0tpvtyUkevcjn5BIvs9jAho8iwxq3Ax9647AaOSg,1086
28
- keep_skill-0.4.1.dist-info/RECORD,,
24
+ keep_skill-0.4.2.dist-info/METADATA,sha256=GKkPrekD30dauxLoogAmSaoD2zhIxltJ4RDkg7yPvzk,6606
25
+ keep_skill-0.4.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
26
+ keep_skill-0.4.2.dist-info/entry_points.txt,sha256=W8yiI4kNeW0IC8ji4EHRWrvdhFxzaqTIePUhJAJAMOo,39
27
+ keep_skill-0.4.2.dist-info/licenses/LICENSE,sha256=zsm0tpvtyUkevcjn5BIvs9jAho8iwxq3Ax9647AaOSg,1086
28
+ keep_skill-0.4.2.dist-info/RECORD,,