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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-claude-cabinet",
3
- "version": "0.8.3",
3
+ "version": "0.8.5",
4
4
  "description": "Claude Cabinet — opinionated process scaffolding for Claude Code projects",
5
5
  "bin": {
6
6
  "create-claude-cabinet": "bin/create-claude-cabinet.js"
@@ -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. A typical session might generate 0-3 memories.
55
-
56
- Over-capturing degrades retrieval quality. When in doubt, don't capture.
57
- The debrief sweep catches anything important that was missed.
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
- results = omega.query(
201
- text,
202
- limit=limit,
203
- event_type=event_type,
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
- rows = conn.execute(
298
- "SELECT node_id, content, event_type, created_at FROM memories WHERE event_type = ? ORDER BY created_at DESC LIMIT ?",
299
- (event_type, limit),
300
- ).fetchall()
301
- else:
302
- rows = conn.execute(
303
- "SELECT node_id, content, event_type, created_at FROM memories ORDER BY created_at DESC LIMIT ?",
304
- (limit,),
305
- ).fetchall()
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], "created": r[3]}
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
- Present results conversationally highlight the most relevant matches
75
- and their types.
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