create-claude-cabinet 0.8.3 → 0.8.5
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.
- package/package.json +1 -1
- package/templates/rules/memory-capture.md +12 -4
- package/templates/scripts/__pycache__/cabinet-memory-adapter.cpython-314.pyc +0 -0
- package/templates/scripts/cabinet-memory-adapter.py +152 -33
- package/templates/skills/cabinet-historian/SKILL.md +4 -0
- package/templates/skills/memory/SKILL.md +14 -5
package/package.json
CHANGED
|
@@ -51,7 +51,15 @@ Memory types: `decision`, `lesson`, `preference`, `constraint`, `pattern`
|
|
|
51
51
|
|
|
52
52
|
Do NOT capture after every interaction. Capture when something worth
|
|
53
53
|
remembering actually happens. Most messages in a session produce nothing
|
|
54
|
-
worth storing.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
worth storing.
|
|
55
|
+
|
|
56
|
+
Cadence scales with session length and discovery density. A short
|
|
57
|
+
focused session might produce 0-1 memories. A long session with
|
|
58
|
+
multiple discoveries, corrections, and decisions could produce 5-10+.
|
|
59
|
+
The right number is however many genuinely worth-remembering things
|
|
60
|
+
happened — no artificial cap.
|
|
61
|
+
|
|
62
|
+
Over-capturing degrades retrieval quality. The test: *"Would a future
|
|
63
|
+
session benefit from knowing this?"* If yes, capture it. If it's just
|
|
64
|
+
noise or ephemera, skip it. The debrief sweep catches anything
|
|
65
|
+
important that was missed during the session.
|
|
@@ -149,6 +149,7 @@ def cmd_store():
|
|
|
149
149
|
text: the memory content (required)
|
|
150
150
|
type: event_type for omega (default: "lesson")
|
|
151
151
|
tags: list of tags (stored in metadata, optional)
|
|
152
|
+
project: project name (default: basename of cwd)
|
|
152
153
|
"""
|
|
153
154
|
data = _read_stdin()
|
|
154
155
|
omega = _import_omega()
|
|
@@ -163,12 +164,14 @@ def cmd_store():
|
|
|
163
164
|
|
|
164
165
|
try:
|
|
165
166
|
event_type = data.get("type", "lesson")
|
|
167
|
+
project = data.get("project", os.path.basename(os.getcwd()))
|
|
166
168
|
metadata = {}
|
|
167
169
|
if data.get("tags"):
|
|
168
170
|
metadata["tags"] = data["tags"]
|
|
169
171
|
result = omega.store(
|
|
170
172
|
text,
|
|
171
173
|
event_type=event_type,
|
|
174
|
+
project=project,
|
|
172
175
|
metadata=metadata if metadata else None,
|
|
173
176
|
)
|
|
174
177
|
_output({"ok": True, "id": result if isinstance(result, str) else None})
|
|
@@ -177,11 +180,17 @@ def cmd_store():
|
|
|
177
180
|
|
|
178
181
|
|
|
179
182
|
def cmd_query():
|
|
180
|
-
"""Query memories by text.
|
|
183
|
+
"""Query memories by text with tiered project scoping.
|
|
181
184
|
|
|
182
185
|
Reads JSON from stdin with fields:
|
|
183
186
|
text: the query text (required)
|
|
184
187
|
limit: max results (default: 5)
|
|
188
|
+
type: filter by event_type (optional)
|
|
189
|
+
project: project name for scoping (default: basename of cwd)
|
|
190
|
+
scope: "project" (default), "all", or "tiered"
|
|
191
|
+
- "project": only memories from this project
|
|
192
|
+
- "all": memories from all projects
|
|
193
|
+
- "tiered": project-scoped first, then cross-project to fill limit
|
|
185
194
|
"""
|
|
186
195
|
data = _read_stdin()
|
|
187
196
|
omega = _import_omega()
|
|
@@ -197,29 +206,131 @@ def cmd_query():
|
|
|
197
206
|
try:
|
|
198
207
|
limit = data.get("limit", 5)
|
|
199
208
|
event_type = data.get("type")
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
209
|
+
project = data.get("project", os.path.basename(os.getcwd()))
|
|
210
|
+
scope = data.get("scope", "tiered")
|
|
211
|
+
|
|
212
|
+
def _parse_results(results):
|
|
213
|
+
"""Normalize query results into a list of dicts.
|
|
214
|
+
|
|
215
|
+
omega.query() returns a formatted markdown string. We parse it
|
|
216
|
+
back into structured data. Also queries the DB to get the project
|
|
217
|
+
field for each memory (not included in the formatted output).
|
|
218
|
+
"""
|
|
219
|
+
if isinstance(results, (list, tuple)):
|
|
220
|
+
out = []
|
|
221
|
+
for r in results:
|
|
222
|
+
if isinstance(r, dict):
|
|
223
|
+
out.append({
|
|
224
|
+
"text": r.get("content", r.get("text", str(r))),
|
|
225
|
+
"type": r.get("event_type", "unknown"),
|
|
226
|
+
"score": r.get("score", 0),
|
|
227
|
+
"project": r.get("project", ""),
|
|
228
|
+
"id": r.get("node_id", ""),
|
|
229
|
+
})
|
|
230
|
+
else:
|
|
231
|
+
out.append({"text": str(r)})
|
|
232
|
+
return out
|
|
233
|
+
|
|
234
|
+
if not isinstance(results, str) or not results.strip():
|
|
235
|
+
return []
|
|
236
|
+
|
|
237
|
+
# Parse formatted markdown from omega.query()
|
|
238
|
+
# Format: ## N. [type] `mem-xxx` (str: 0.xx)\ncontent\n*timestamp*
|
|
239
|
+
import re
|
|
240
|
+
entries = []
|
|
241
|
+
blocks = re.split(r'\n## \d+\.', results)
|
|
242
|
+
for block in blocks:
|
|
243
|
+
block = block.strip()
|
|
244
|
+
if not block:
|
|
245
|
+
continue
|
|
246
|
+
id_match = re.search(r'`(mem-[a-f0-9]+)`', block)
|
|
247
|
+
type_match = re.search(r'\[(\w+)\]', block)
|
|
248
|
+
score_match = re.search(r'str: ([\d.]+)', block)
|
|
249
|
+
# Content is everything after the first line, minus the timestamp
|
|
250
|
+
lines = block.split('\n')
|
|
251
|
+
content_lines = [l for l in lines[1:] if not l.startswith('*')]
|
|
252
|
+
content = '\n'.join(content_lines).strip()
|
|
253
|
+
|
|
254
|
+
if not id_match:
|
|
255
|
+
continue # skip header/noise lines
|
|
256
|
+
entry = {
|
|
257
|
+
"text": content,
|
|
258
|
+
"type": type_match.group(1) if type_match else "unknown",
|
|
259
|
+
"score": float(score_match.group(1)) if score_match else 0,
|
|
260
|
+
"id": id_match.group(1) if id_match else "",
|
|
261
|
+
"project": "",
|
|
262
|
+
}
|
|
263
|
+
entries.append(entry)
|
|
264
|
+
|
|
265
|
+
# Enrich with project from DB
|
|
266
|
+
if entries:
|
|
267
|
+
try:
|
|
268
|
+
import sqlite3
|
|
269
|
+
db_path = os.path.expanduser("~/.omega/omega.db")
|
|
270
|
+
conn = sqlite3.connect(db_path)
|
|
271
|
+
ids = [e["id"] for e in entries if e["id"]]
|
|
272
|
+
if ids:
|
|
273
|
+
placeholders = ",".join("?" * len(ids))
|
|
274
|
+
rows = conn.execute(
|
|
275
|
+
f"SELECT node_id, project FROM memories WHERE node_id IN ({placeholders})",
|
|
276
|
+
ids,
|
|
277
|
+
).fetchall()
|
|
278
|
+
proj_map = {r[0]: r[1] or "" for r in rows}
|
|
279
|
+
for e in entries:
|
|
280
|
+
e["project"] = proj_map.get(e["id"], "")
|
|
281
|
+
conn.close()
|
|
282
|
+
except Exception:
|
|
283
|
+
pass # D3: project enrichment is best-effort
|
|
284
|
+
|
|
285
|
+
return entries
|
|
286
|
+
|
|
287
|
+
# All scopes fetch from omega broadly, then filter in the adapter.
|
|
288
|
+
# omega.query() doesn't reliably pass project_path to the store,
|
|
289
|
+
# so we do project filtering ourselves.
|
|
290
|
+
over_fetch = limit * 3 # fetch extra so filtering still yields enough
|
|
291
|
+
|
|
292
|
+
if scope == "all":
|
|
293
|
+
results = omega.query(text, limit=over_fetch, event_type=event_type)
|
|
294
|
+
parsed = _parse_results(results)[:limit]
|
|
295
|
+
_output({"ok": True, "results": parsed, "scope": "all"})
|
|
296
|
+
|
|
297
|
+
elif scope == "project":
|
|
298
|
+
results = omega.query(text, limit=over_fetch, event_type=event_type)
|
|
299
|
+
parsed = _parse_results(results)
|
|
300
|
+
filtered = [m for m in parsed if m.get("project") == project][:limit]
|
|
301
|
+
_output({"ok": True, "results": filtered, "scope": "project", "project": project})
|
|
302
|
+
|
|
303
|
+
else: # tiered (default)
|
|
304
|
+
results = omega.query(text, limit=over_fetch, event_type=event_type)
|
|
305
|
+
parsed = _parse_results(results)
|
|
306
|
+
|
|
307
|
+
# Tier 1: project-scoped
|
|
308
|
+
project_memories = [m for m in parsed if m.get("project") == project][:limit]
|
|
309
|
+
for m in project_memories:
|
|
310
|
+
m["tier"] = "project"
|
|
311
|
+
|
|
312
|
+
# Tier 2: cross-project to fill remaining slots
|
|
313
|
+
remaining = limit - len(project_memories)
|
|
314
|
+
cross_memories = []
|
|
315
|
+
if remaining > 0:
|
|
316
|
+
seen_ids = {m["id"] for m in project_memories if m["id"]}
|
|
317
|
+
for m in parsed:
|
|
318
|
+
if m.get("project") != project and m.get("id") not in seen_ids:
|
|
319
|
+
m["tier"] = "cross-project"
|
|
320
|
+
cross_memories.append(m)
|
|
321
|
+
if len(cross_memories) >= remaining:
|
|
322
|
+
break
|
|
323
|
+
|
|
324
|
+
combined = project_memories + cross_memories
|
|
325
|
+
_output({
|
|
326
|
+
"ok": True,
|
|
327
|
+
"results": combined,
|
|
328
|
+
"scope": "tiered",
|
|
329
|
+
"project": project,
|
|
330
|
+
"project_count": len(project_memories),
|
|
331
|
+
"cross_project_count": len(cross_memories),
|
|
332
|
+
})
|
|
205
333
|
|
|
206
|
-
# query() returns a formatted string of results
|
|
207
|
-
if isinstance(results, str):
|
|
208
|
-
_output({"ok": True, "results": results})
|
|
209
|
-
elif isinstance(results, (list, tuple)):
|
|
210
|
-
memories = []
|
|
211
|
-
for r in results:
|
|
212
|
-
if isinstance(r, dict):
|
|
213
|
-
memories.append({
|
|
214
|
-
"text": r.get("content", r.get("text", str(r))),
|
|
215
|
-
"type": r.get("event_type", "unknown"),
|
|
216
|
-
"score": r.get("score", 0),
|
|
217
|
-
})
|
|
218
|
-
else:
|
|
219
|
-
memories.append({"text": str(r)})
|
|
220
|
-
_output({"ok": True, "memories": memories})
|
|
221
|
-
else:
|
|
222
|
-
_output({"ok": True, "results": str(results)})
|
|
223
334
|
except Exception as e:
|
|
224
335
|
_error(f"query failed: {e}")
|
|
225
336
|
|
|
@@ -278,6 +389,7 @@ def cmd_list():
|
|
|
278
389
|
Returns all memories so delete can target the correct ID.
|
|
279
390
|
Reads JSON from stdin with optional fields:
|
|
280
391
|
type: filter by event_type (optional)
|
|
392
|
+
project: filter by project (optional)
|
|
281
393
|
limit: max results (default: 50)
|
|
282
394
|
"""
|
|
283
395
|
data = _read_stdin()
|
|
@@ -291,21 +403,28 @@ def cmd_list():
|
|
|
291
403
|
|
|
292
404
|
conn = sqlite3.connect(db_path)
|
|
293
405
|
event_type = data.get("type")
|
|
406
|
+
project = data.get("project")
|
|
294
407
|
limit = data.get("limit", 50)
|
|
295
408
|
|
|
409
|
+
where_clauses = []
|
|
410
|
+
params = []
|
|
296
411
|
if event_type:
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
412
|
+
where_clauses.append("event_type = ?")
|
|
413
|
+
params.append(event_type)
|
|
414
|
+
if project:
|
|
415
|
+
where_clauses.append("project = ?")
|
|
416
|
+
params.append(project)
|
|
417
|
+
|
|
418
|
+
where = f"WHERE {' AND '.join(where_clauses)}" if where_clauses else ""
|
|
419
|
+
params.append(limit)
|
|
420
|
+
|
|
421
|
+
rows = conn.execute(
|
|
422
|
+
f"SELECT node_id, content, event_type, project, created_at FROM memories {where} ORDER BY created_at DESC LIMIT ?",
|
|
423
|
+
params,
|
|
424
|
+
).fetchall()
|
|
306
425
|
|
|
307
426
|
memories = [
|
|
308
|
-
{"id": r[0], "text": r[1], "type": r[2], "
|
|
427
|
+
{"id": r[0], "text": r[1], "type": r[2], "project": r[3] or "", "created": r[4]}
|
|
309
428
|
for r in rows
|
|
310
429
|
]
|
|
311
430
|
_output({"ok": True, "memories": memories, "count": len(memories)})
|
|
@@ -71,12 +71,16 @@ to help you do your job.
|
|
|
71
71
|
echo '{"text": "your query here", "limit": 10}' | \
|
|
72
72
|
~/.claude-cabinet/omega-venv/bin/python3 scripts/cabinet-memory-adapter.py query
|
|
73
73
|
```
|
|
74
|
+
Queries use **tiered scoping** by default: project memories first,
|
|
75
|
+
then cross-project to fill remaining slots. Use `"scope": "project"`
|
|
76
|
+
for strict project-only, or `"scope": "all"` for global search.
|
|
74
77
|
|
|
75
78
|
**Storing** (when you discover something worth remembering):
|
|
76
79
|
```bash
|
|
77
80
|
echo '{"text": "the lesson or decision", "type": "lesson"}' | \
|
|
78
81
|
~/.claude-cabinet/omega-venv/bin/python3 scripts/cabinet-memory-adapter.py store
|
|
79
82
|
```
|
|
83
|
+
Stores are automatically tagged with the current project name.
|
|
80
84
|
|
|
81
85
|
Memory types: `decision` (architectural choices), `lesson` (gotchas,
|
|
82
86
|
discoveries), `preference` (user corrections), `constraint` (limitations
|
|
@@ -38,10 +38,10 @@ echo '<json>' | ~/.claude-cabinet/omega-venv/bin/python3 scripts/cabinet-memory-
|
|
|
38
38
|
| Command | Input | Output |
|
|
39
39
|
|---------|-------|--------|
|
|
40
40
|
| `welcome` | `{}` or hook JSON | `{ok, context}` — relevant memories |
|
|
41
|
-
| `store` | `{text, type, tags?}` | `{ok, id}` — stored memory ID |
|
|
42
|
-
| `query` | `{text, limit?, type?}` | `{ok, results}` — semantic search |
|
|
41
|
+
| `store` | `{text, type, tags?, project?}` | `{ok, id}` — stored memory ID |
|
|
42
|
+
| `query` | `{text, limit?, type?, project?, scope?}` | `{ok, results}` — semantic search |
|
|
43
43
|
| `delete` | `{id}` | `{ok, deleted}` — requires full node_id |
|
|
44
|
-
| `list` | `{type?, limit?}` | `{ok, memories, count}` — all memories with full IDs |
|
|
44
|
+
| `list` | `{type?, project?, limit?}` | `{ok, memories, count}` — all memories with full IDs |
|
|
45
45
|
| `capture` | hook JSON with `compact_summary` | `{ok, stored}` — auto-capture count |
|
|
46
46
|
| `status` | `{}` | `{ok, ...health info}` |
|
|
47
47
|
|
|
@@ -71,8 +71,17 @@ echo '{"text": "the user query", "limit": 10}' | \
|
|
|
71
71
|
~/.claude-cabinet/omega-venv/bin/python3 scripts/cabinet-memory-adapter.py query
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
**Scope options** (pass `scope` in the JSON):
|
|
75
|
+
- `"tiered"` (default) — project memories first, then cross-project to fill
|
|
76
|
+
- `"project"` — only memories from this project
|
|
77
|
+
- `"all"` — memories from all projects equally
|
|
78
|
+
|
|
79
|
+
The `project` field defaults to the current directory name. Results
|
|
80
|
+
include a `tier` field ("project" or "cross-project") in tiered mode.
|
|
81
|
+
|
|
82
|
+
Present results conversationally — highlight the most relevant matches,
|
|
83
|
+
their types, and which project they came from when cross-project results
|
|
84
|
+
appear.
|
|
76
85
|
|
|
77
86
|
### Remember — user wants to store something
|
|
78
87
|
|