keep-skill 0.1.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/providers/mlx.py CHANGED
@@ -15,21 +15,18 @@ from .base import EmbeddingProvider, SummarizationProvider, get_registry
15
15
 
16
16
  class MLXEmbedding:
17
17
  """
18
- Embedding provider using MLX on Apple Silicon.
19
-
20
- Uses sentence-transformer compatible models converted to MLX format.
21
-
18
+ Embedding provider using MPS (Metal) acceleration on Apple Silicon.
19
+
20
+ Uses sentence-transformer models with GPU acceleration via Metal Performance Shaders.
21
+
22
22
  Requires: pip install mlx sentence-transformers
23
23
  """
24
-
25
- def __init__(self, model: str = "mlx-community/bge-small-en-v1.5"):
24
+
25
+ def __init__(self, model: str = "all-MiniLM-L6-v2"):
26
26
  """
27
27
  Args:
28
- model: Model name from mlx-community hub or local path.
29
- Good options:
30
- - mlx-community/bge-small-en-v1.5 (small, fast)
31
- - mlx-community/bge-base-en-v1.5 (balanced)
32
- - mlx-community/bge-large-en-v1.5 (best quality)
28
+ model: Model name from sentence-transformers hub.
29
+ Default: all-MiniLM-L6-v2 (384 dims, fast, no auth required)
33
30
  """
34
31
  try:
35
32
  import mlx.core as mx
@@ -39,17 +36,22 @@ class MLXEmbedding:
39
36
  "MLXEmbedding requires 'mlx' and 'sentence-transformers'. "
40
37
  "Install with: pip install mlx sentence-transformers"
41
38
  )
42
-
39
+
43
40
  self.model_name = model
44
-
45
- # sentence-transformers can use MLX backend on Apple Silicon
46
- # For MLX-specific models, we use the direct approach
47
- if model.startswith("mlx-community/"):
48
- # Use sentence-transformers which auto-detects MLX
49
- self._model = SentenceTransformer(model, device="mps")
50
- else:
51
- self._model = SentenceTransformer(model)
52
-
41
+
42
+ # Check if model is already cached locally to avoid network calls
43
+ local_only = False
44
+ try:
45
+ from huggingface_hub import try_to_load_from_cache
46
+ repo_id = model if "/" in model else f"sentence-transformers/{model}"
47
+ cached = try_to_load_from_cache(repo_id, "config.json")
48
+ local_only = cached is not None
49
+ except ImportError:
50
+ pass
51
+
52
+ # Use MPS (Metal) for GPU acceleration on Apple Silicon
53
+ self._model = SentenceTransformer(model, device="mps", local_files_only=local_only)
54
+
53
55
  self._dimension: int | None = None
54
56
 
55
57
  @property
keep/store.py CHANGED
@@ -49,13 +49,14 @@ class ChromaStore:
49
49
  pluggable backends (SQLite+faiss, Postgres+pgvector, etc.)
50
50
  """
51
51
 
52
- def __init__(self, store_path: Path, embedding_dimension: int):
52
+ def __init__(self, store_path: Path, embedding_dimension: Optional[int] = None):
53
53
  """
54
54
  Initialize or open a ChromaDb store.
55
-
55
+
56
56
  Args:
57
57
  store_path: Directory for persistent storage
58
- embedding_dimension: Expected dimension of embeddings (for validation)
58
+ embedding_dimension: Expected dimension of embeddings (for validation).
59
+ Can be None for read-only access; will be set on first write.
59
60
  """
60
61
  try:
61
62
  import chromadb
@@ -123,7 +124,7 @@ class ChromaStore:
123
124
  ) -> None:
124
125
  """
125
126
  Insert or update an item in the store.
126
-
127
+
127
128
  Args:
128
129
  collection: Collection name
129
130
  id: Item identifier (URI or custom)
@@ -131,14 +132,17 @@ class ChromaStore:
131
132
  summary: Human-readable summary (stored as document)
132
133
  tags: All tags (source + system + generated)
133
134
  """
134
- if len(embedding) != self._embedding_dimension:
135
+ # Validate or set embedding dimension
136
+ if self._embedding_dimension is None:
137
+ self._embedding_dimension = len(embedding)
138
+ elif len(embedding) != self._embedding_dimension:
135
139
  raise ValueError(
136
140
  f"Embedding dimension mismatch: expected {self._embedding_dimension}, "
137
141
  f"got {len(embedding)}"
138
142
  )
139
-
143
+
140
144
  coll = self._get_collection(collection)
141
-
145
+
142
146
  # Add timestamp if not present
143
147
  now = datetime.now(timezone.utc).isoformat()
144
148
  if "_updated" not in tags:
@@ -154,36 +158,122 @@ class ChromaStore:
154
158
  tags = {**tags, "_created": now}
155
159
  else:
156
160
  tags = {**tags, "_created": now}
157
-
161
+
158
162
  # Add date portion for easier date queries
159
163
  tags = {**tags, "_updated_date": now[:10]}
160
-
164
+
161
165
  coll.upsert(
162
166
  ids=[id],
163
167
  embeddings=[embedding],
164
168
  documents=[summary],
165
169
  metadatas=[self._tags_to_metadata(tags)],
166
170
  )
171
+
172
+ def upsert_version(
173
+ self,
174
+ collection: str,
175
+ id: str,
176
+ version: int,
177
+ embedding: list[float],
178
+ summary: str,
179
+ tags: dict[str, str],
180
+ ) -> None:
181
+ """
182
+ Store an archived version with a versioned ID.
183
+
184
+ The versioned ID format is: {id}@v{version}
185
+ Metadata includes _version and _base_id for filtering/navigation.
186
+
187
+ Args:
188
+ collection: Collection name
189
+ id: Base item identifier (not versioned)
190
+ version: Version number (1=oldest archived)
191
+ embedding: Vector embedding
192
+ summary: Human-readable summary
193
+ tags: All tags from the archived version
194
+ """
195
+ if self._embedding_dimension is None:
196
+ self._embedding_dimension = len(embedding)
197
+ elif len(embedding) != self._embedding_dimension:
198
+ raise ValueError(
199
+ f"Embedding dimension mismatch: expected {self._embedding_dimension}, "
200
+ f"got {len(embedding)}"
201
+ )
202
+
203
+ coll = self._get_collection(collection)
204
+
205
+ # Versioned ID format
206
+ versioned_id = f"{id}@v{version}"
207
+
208
+ # Add version metadata
209
+ versioned_tags = dict(tags)
210
+ versioned_tags["_version"] = str(version)
211
+ versioned_tags["_base_id"] = id
212
+
213
+ coll.upsert(
214
+ ids=[versioned_id],
215
+ embeddings=[embedding],
216
+ documents=[summary],
217
+ metadatas=[self._tags_to_metadata(versioned_tags)],
218
+ )
219
+
220
+ def get_content_hash(self, collection: str, id: str) -> Optional[str]:
221
+ """
222
+ Get the content hash of an existing item.
223
+
224
+ Used to check if content changed (to skip re-embedding).
225
+
226
+ Args:
227
+ collection: Collection name
228
+ id: Item identifier
229
+
230
+ Returns:
231
+ Content hash if item exists and has one, None otherwise
232
+ """
233
+ coll = self._get_collection(collection)
234
+ result = coll.get(ids=[id], include=["metadatas"])
235
+
236
+ if not result["ids"]:
237
+ return None
238
+
239
+ metadata = result["metadatas"][0] or {}
240
+ return metadata.get("_content_hash")
167
241
 
168
- def delete(self, collection: str, id: str) -> bool:
242
+ def delete(self, collection: str, id: str, delete_versions: bool = True) -> bool:
169
243
  """
170
244
  Delete an item from the store.
171
-
245
+
172
246
  Args:
173
247
  collection: Collection name
174
248
  id: Item identifier
175
-
249
+ delete_versions: If True, also delete versioned copies ({id}@v{N})
250
+
176
251
  Returns:
177
252
  True if item existed and was deleted, False if not found
178
253
  """
179
254
  coll = self._get_collection(collection)
180
-
255
+
181
256
  # Check existence first
182
257
  existing = coll.get(ids=[id])
183
258
  if not existing["ids"]:
184
259
  return False
185
-
260
+
186
261
  coll.delete(ids=[id])
262
+
263
+ if delete_versions:
264
+ # Delete all versioned copies
265
+ # Query by _base_id metadata to find all versions
266
+ try:
267
+ versions = coll.get(
268
+ where={"_base_id": id},
269
+ include=[],
270
+ )
271
+ if versions["ids"]:
272
+ coll.delete(ids=versions["ids"])
273
+ except Exception:
274
+ # Metadata filter might fail if no versions exist
275
+ pass
276
+
187
277
  return True
188
278
 
189
279
  def update_summary(self, collection: str, id: str, summary: str) -> bool:
@@ -222,6 +312,40 @@ class ChromaStore:
222
312
  )
223
313
  return True
224
314
 
315
+ def update_tags(self, collection: str, id: str, tags: dict[str, str]) -> bool:
316
+ """
317
+ Update tags of an existing item without changing embedding or summary.
318
+
319
+ Args:
320
+ collection: Collection name
321
+ id: Item identifier
322
+ tags: New tags dict (replaces existing)
323
+
324
+ Returns:
325
+ True if item was updated, False if not found
326
+ """
327
+ coll = self._get_collection(collection)
328
+
329
+ # Get existing item
330
+ existing = coll.get(ids=[id], include=["metadatas"])
331
+ if not existing["ids"]:
332
+ return False
333
+
334
+ # Update timestamp
335
+ now = datetime.now(timezone.utc).isoformat()
336
+ tags = dict(tags) # Copy to avoid mutating input
337
+ tags["_updated"] = now
338
+ tags["_updated_date"] = now[:10]
339
+
340
+ # Convert to metadata format
341
+ metadata = self._tags_to_metadata(tags)
342
+
343
+ coll.update(
344
+ ids=[id],
345
+ metadatas=[metadata],
346
+ )
347
+ return True
348
+
225
349
  # -------------------------------------------------------------------------
226
350
  # Read Operations
227
351
  # -------------------------------------------------------------------------
@@ -257,7 +381,21 @@ class ChromaStore:
257
381
  coll = self._get_collection(collection)
258
382
  result = coll.get(ids=[id], include=[])
259
383
  return bool(result["ids"])
260
-
384
+
385
+ def list_ids(self, collection: str) -> list[str]:
386
+ """
387
+ List all document IDs in a collection.
388
+
389
+ Args:
390
+ collection: Collection name
391
+
392
+ Returns:
393
+ List of document IDs
394
+ """
395
+ coll = self._get_collection(collection)
396
+ result = coll.get(include=[])
397
+ return result["ids"]
398
+
261
399
  def query_embedding(
262
400
  self,
263
401
  collection: str,
@@ -340,27 +478,33 @@ class ChromaStore:
340
478
  collection: str,
341
479
  query: str,
342
480
  limit: int = 10,
481
+ where: dict[str, Any] | None = None,
343
482
  ) -> list[StoreResult]:
344
483
  """
345
484
  Query by full-text search on document content (summaries).
346
-
485
+
347
486
  Args:
348
487
  collection: Collection name
349
488
  query: Text to search for
350
489
  limit: Maximum results to return
351
-
490
+ where: Optional metadata filter (Chroma where clause)
491
+
352
492
  Returns:
353
493
  List of matching results
354
494
  """
355
495
  coll = self._get_collection(collection)
356
-
496
+
357
497
  # Chroma's where_document does substring matching
358
- result = coll.get(
359
- where_document={"$contains": query},
360
- limit=limit,
361
- include=["documents", "metadatas"],
362
- )
363
-
498
+ get_params = {
499
+ "where_document": {"$contains": query},
500
+ "limit": limit,
501
+ "include": ["documents", "metadatas"],
502
+ }
503
+ if where:
504
+ get_params["where"] = where
505
+
506
+ result = coll.get(**get_params)
507
+
364
508
  results = []
365
509
  for i, id in enumerate(result["ids"]):
366
510
  results.append(StoreResult(
@@ -368,7 +512,7 @@ class ChromaStore:
368
512
  summary=result["documents"][i] or "",
369
513
  tags=self._metadata_to_tags(result["metadatas"][i]),
370
514
  ))
371
-
515
+
372
516
  return results
373
517
 
374
518
  # -------------------------------------------------------------------------
@@ -0,0 +1,218 @@
1
+ Metadata-Version: 2.4
2
+ Name: keep-skill
3
+ Version: 0.3.0
4
+ Summary: Semantic memory - remember and search documents by meaning
5
+ Project-URL: Homepage, https://github.com/hughpyle/keep
6
+ Project-URL: Repository, https://github.com/hughpyle/keep
7
+ Author: Hugh Pyle
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: agents,chromadb,embeddings,semantic-memory,vector-search
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Software Development :: Libraries
19
+ Classifier: Topic :: Text Processing
20
+ Requires-Python: <3.14,>=3.11
21
+ Requires-Dist: chromadb>=0.4
22
+ Requires-Dist: tomli-w>=1.0
23
+ Requires-Dist: typer>=0.9
24
+ Provides-Extra: anthropic
25
+ Requires-Dist: anthropic>=0.40.0; extra == 'anthropic'
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest-cov>=4.0; extra == 'dev'
28
+ Requires-Dist: pytest>=7.0; extra == 'dev'
29
+ Provides-Extra: documents
30
+ Requires-Dist: beautifulsoup4>=4.9; extra == 'documents'
31
+ Requires-Dist: pypdf>=5.0; extra == 'documents'
32
+ Provides-Extra: gemini
33
+ Requires-Dist: google-genai>=1.0.0; extra == 'gemini'
34
+ Provides-Extra: local
35
+ Requires-Dist: beautifulsoup4>=4.9; extra == 'local'
36
+ Requires-Dist: mlx-lm>=0.10; (platform_system == 'Darwin' and platform_machine == 'arm64') and extra == 'local'
37
+ Requires-Dist: mlx>=0.10; (platform_system == 'Darwin' and platform_machine == 'arm64') and extra == 'local'
38
+ Requires-Dist: pypdf>=5.0; extra == 'local'
39
+ Requires-Dist: sentence-transformers>=2.2; extra == 'local'
40
+ Provides-Extra: mlx
41
+ Requires-Dist: mlx-lm>=0.10; extra == 'mlx'
42
+ Requires-Dist: mlx>=0.10; extra == 'mlx'
43
+ Provides-Extra: openai
44
+ Requires-Dist: openai>=1.0; extra == 'openai'
45
+ Provides-Extra: openclaw
46
+ Requires-Dist: anthropic>=0.18.0; extra == 'openclaw'
47
+ Requires-Dist: google-genai>=1.0.0; extra == 'openclaw'
48
+ Provides-Extra: sentence-transformers
49
+ Requires-Dist: sentence-transformers>=2.2; extra == 'sentence-transformers'
50
+ Description-Content-Type: text/markdown
51
+
52
+ # keep
53
+
54
+ **Semantic memory with version history.**
55
+
56
+ Index documents and notes. Search by meaning. Track changes over time.
57
+
58
+ ```bash
59
+ pip install 'keep-skill[local]'
60
+ keep init
61
+
62
+ # Index content
63
+ keep update path/to/document.md -t project=myapp
64
+ keep update "Rate limit is 100 req/min" -t topic=api
65
+
66
+ # Search by meaning
67
+ keep find "what's the rate limit?"
68
+
69
+ # Track what you're working on
70
+ keep now "Debugging auth flow"
71
+ keep now -V 1 # Previous context
72
+ ```
73
+
74
+ ---
75
+
76
+ ## What It Does
77
+
78
+ - **Semantic search** — Find by meaning, not just keywords
79
+ - **Version history** — All documents retain history on update
80
+ - **Tag organization** — Filter and navigate with key=value tags
81
+ - **Recency decay** — Recent items rank higher in search
82
+ - **Works offline** — Local embedding models by default
83
+
84
+ Backed by ChromaDB for vectors, SQLite for metadata and versions.
85
+
86
+ ---
87
+
88
+ ## Installation
89
+
90
+ **Python 3.11–3.13 required.**
91
+
92
+ ```bash
93
+ # Recommended: local models (works offline)
94
+ pip install 'keep-skill[local]'
95
+
96
+ # Or with uv (faster):
97
+ uv tool install 'keep-skill[local]'
98
+
99
+ # API-based alternative (requires OPENAI_API_KEY)
100
+ pip install 'keep-skill[openai]'
101
+ ```
102
+
103
+ First run downloads embedding models (~3-5 minutes).
104
+
105
+ ---
106
+
107
+ ## Quick Start
108
+
109
+ ```bash
110
+ keep init # Creates .keep/ at repo root
111
+
112
+ # Index files and notes
113
+ keep update file:///path/to/doc.md -t project=myapp
114
+ keep update "Important insight" -t type=note
115
+
116
+ # Search
117
+ keep find "authentication flow" --limit 5
118
+ keep find "auth" --since P7D # Last 7 days
119
+
120
+ # Retrieve
121
+ keep get file:///path/to/doc.md
122
+ keep get ID -V 1 # Previous version
123
+ keep get ID --history # All versions
124
+
125
+ # Tags
126
+ keep tag project=myapp # Find by tag
127
+ keep tag --list # List all tags
128
+
129
+ # Current context
130
+ keep now # Show what you're working on
131
+ keep now "Fixing login bug" # Update context
132
+ ```
133
+
134
+ ### Python API
135
+
136
+ ```python
137
+ from keep import Keeper
138
+
139
+ kp = Keeper()
140
+
141
+ # Index
142
+ kp.update("file:///path/to/doc.md", tags={"project": "myapp"})
143
+ kp.remember("Rate limit is 100 req/min", tags={"topic": "api"})
144
+
145
+ # Search
146
+ results = kp.find("rate limit", limit=5)
147
+ for r in results:
148
+ print(f"[{r.score:.2f}] {r.summary}")
149
+
150
+ # Version history
151
+ prev = kp.get_version("doc:1", offset=1)
152
+ versions = kp.list_versions("doc:1")
153
+ ```
154
+
155
+ See [docs/QUICKSTART.md](docs/QUICKSTART.md) for configuration and more examples.
156
+
157
+ ---
158
+
159
+ ## Documentation
160
+
161
+ - **[docs/QUICKSTART.md](docs/QUICKSTART.md)** — Setup, configuration, lazy summarization
162
+ - **[docs/REFERENCE.md](docs/REFERENCE.md)** — Complete CLI and API reference
163
+ - **[docs/AGENT-GUIDE.md](docs/AGENT-GUIDE.md)** — Working session patterns
164
+ - **[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)** — How it works under the hood
165
+ - **[SKILL.md](SKILL.md)** — The reflective practice (for AI agents)
166
+
167
+ ---
168
+
169
+ ## For AI Agents
170
+
171
+ This library was designed as an agent skill — persistent memory that helps agents reflect before acting and learn from experience.
172
+
173
+ **The practice:**
174
+ - Pause before acting — `keep find` what you already know
175
+ - Notice breakdowns — when assumptions surface, index them
176
+ - Reflect after — `keep update` learnings for future sessions
177
+
178
+ See **[SKILL.md](SKILL.md)** for the full practice guide.
179
+
180
+ ---
181
+
182
+ ## Status
183
+
184
+ **Current:** v0.3.0
185
+
186
+ **Working:**
187
+ - ✅ Semantic search with embeddings
188
+ - ✅ Document versioning (all updates retain history)
189
+ - ✅ Content-addressed IDs for text (same content = same ID)
190
+ - ✅ Tag queries and full-text search
191
+ - ✅ Current context tracking (`keep now`)
192
+ - ✅ Recency decay (recent items rank higher)
193
+ - ✅ Lazy summarization (background processing)
194
+ - ✅ Provider abstraction (local or API-based)
195
+
196
+ **Planned** (see [later/](later/)):
197
+ - ⏳ Private/shared routing
198
+ - ⏳ Relationship graphs between items
199
+ - ⏳ LLM-based auto-tagging
200
+
201
+ ---
202
+
203
+ ## License
204
+
205
+ MIT
206
+
207
+ ---
208
+
209
+ ## Contributing
210
+
211
+ Published on [PyPI as `keep-skill`](https://pypi.org/project/keep-skill/).
212
+
213
+ Issues and PRs welcome:
214
+ - Provider implementations
215
+ - Performance improvements
216
+ - Documentation clarity
217
+
218
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
@@ -0,0 +1,28 @@
1
+ keep/__init__.py,sha256=ZHguWMkzsurTDqV7_t5Nlr5VHlwVkmG6pelE0Ivfs6I,1581
2
+ keep/__main__.py,sha256=3Uu70IhIDIjh8OW6jp9jQQ3dF2lKdJWi_3FtRIQMiMY,104
3
+ keep/api.py,sha256=rArSGmbQkVR_x5qcMXVZcQr97daWxyettnHplgnykEM,56951
4
+ keep/chunking.py,sha256=neAXOLSvVwbUxapbqq7nZrbSNSzMXuhxj-ODoOSodsU,11830
5
+ keep/cli.py,sha256=yxO9FS0N1c_ffE02NV8tC-4NNj8ZNK_dkFufjqVG84A,34085
6
+ keep/config.py,sha256=RRnHHvhc9KkJBYt0rpAFIvAVXw40b56xtT74TFIBiDU,15832
7
+ keep/context.py,sha256=CNpjmrv6eW2kV1E0MO6qAQfhYKRlfzAL--6v4Mj1nFY,71
8
+ keep/document_store.py,sha256=UswqKIGSc5E-r7Tg9k0g5-byYnuar3e9FieQ7WNod9k,29109
9
+ keep/errors.py,sha256=G9e5FbdfeugyfHOuL_SPZlM5jgWWnwsX4hM7IzanBZc,857
10
+ keep/indexing.py,sha256=dpPYo3WXnIhFDWinz5ZBZVk7_qumeNpP4EpOIY0zMbs,6063
11
+ keep/logging_config.py,sha256=IGwkgIyg-TfYaT4MnoCXfmjeHAe_wsB_XQ1QhVT_ro8,3503
12
+ keep/paths.py,sha256=Dv7pM6oo2QgjL6sj5wPjhuMOK2wqUkfd4Kz08TwJ1ps,3331
13
+ keep/pending_summaries.py,sha256=_irGe7P1Lmog2c5cEgx-BElpq4YJW-tEmF5A3IUZQbQ,5727
14
+ keep/store.py,sha256=RmKOAWTWvUlMcLoEbyAfR99Cxcshh_2SHhNkKYGAkqw,17509
15
+ keep/types.py,sha256=f6uOSYsYt6mj1ulKn2iRkooi__dWCiOQFPD6he2eID4,2149
16
+ keep/providers/__init__.py,sha256=GFX_12g9OdjmpFUkTekOQBOWvcRW2Ae6yidfVVW2SiI,1095
17
+ keep/providers/base.py,sha256=7Ug4Kj9fK2Dq4zDcZjn-GKsoZBOAlB9b-FMk969ER-g,14590
18
+ keep/providers/documents.py,sha256=EXeSy5i3RUL0kciIC6w3ldAEfbTIyC5fgfzC_WAI0iY,8211
19
+ keep/providers/embedding_cache.py,sha256=gna6PZEJanbn2GUN0vj1b1MC0xVWePM9cot2KgZUdu8,8856
20
+ keep/providers/embeddings.py,sha256=zi8GyitKexdbCJyU1nLrUhGt_zzPn3udYrrPZ5Ak8Wo,9081
21
+ keep/providers/llm.py,sha256=BxROKOklKbkGsHcSADPNNgWQExgSN6Bg4KPQIxVuB3U,12441
22
+ keep/providers/mlx.py,sha256=aNl00r9tGi5tCGj2ArYH7CmDHtL1jLjVzb1rofU1DAo,9050
23
+ keep/providers/summarization.py,sha256=MlVTcYipaqp2lT-QYnznp0AMuPVG36QfcTQnvY7Gb-Q,3409
24
+ keep_skill-0.3.0.dist-info/METADATA,sha256=ET8OzV76N3JI_bWcJt6j6xE0uYmROmgB27U1gq9RAk0,6312
25
+ keep_skill-0.3.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
26
+ keep_skill-0.3.0.dist-info/entry_points.txt,sha256=W8yiI4kNeW0IC8ji4EHRWrvdhFxzaqTIePUhJAJAMOo,39
27
+ keep_skill-0.3.0.dist-info/licenses/LICENSE,sha256=zsm0tpvtyUkevcjn5BIvs9jAho8iwxq3Ax9647AaOSg,1086
28
+ keep_skill-0.3.0.dist-info/RECORD,,