create-claude-cabinet 0.9.0 → 0.11.0

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.
Files changed (44) hide show
  1. package/lib/cli.js +183 -5
  2. package/lib/copy.js +1 -0
  3. package/lib/omega-setup.js +44 -0
  4. package/lib/settings-merge.js +17 -34
  5. package/package.json +1 -1
  6. package/templates/rules/memory-capture.md +32 -39
  7. package/templates/scripts/cabinet-memory-adapter.py +23 -110
  8. package/templates/skills/cabinet/SKILL.md +53 -0
  9. package/templates/skills/cabinet-architecture/SKILL.md +8 -0
  10. package/templates/skills/cabinet-boundary-man/SKILL.md +5 -0
  11. package/templates/skills/cabinet-cc-health/SKILL.md +7 -8
  12. package/templates/skills/cabinet-data-integrity/SKILL.md +1 -0
  13. package/templates/skills/cabinet-debugger/SKILL.md +6 -0
  14. package/templates/skills/cabinet-framework-quality/SKILL.md +1 -0
  15. package/templates/skills/cabinet-historian/SKILL.md +99 -9
  16. package/templates/skills/cabinet-organized-mind/SKILL.md +6 -0
  17. package/templates/skills/cabinet-process-therapist/SKILL.md +6 -1
  18. package/templates/skills/cabinet-qa/SKILL.md +8 -0
  19. package/templates/skills/cabinet-record-keeper/SKILL.md +6 -1
  20. package/templates/skills/cabinet-roster-check/SKILL.md +5 -1
  21. package/templates/skills/cabinet-security/SKILL.md +8 -0
  22. package/templates/skills/cabinet-speed-freak/SKILL.md +1 -0
  23. package/templates/skills/cabinet-system-advocate/SKILL.md +8 -0
  24. package/templates/skills/cabinet-technical-debt/SKILL.md +1 -0
  25. package/templates/skills/cabinet-usability/SKILL.md +1 -0
  26. package/templates/skills/cabinet-user-advocate/SKILL.md +5 -0
  27. package/templates/skills/cabinet-workflow-cop/SKILL.md +1 -0
  28. package/templates/skills/cc-upgrade/SKILL.md +35 -0
  29. package/templates/skills/debrief/SKILL.md +74 -27
  30. package/templates/skills/debrief/phases/record-lessons.md +30 -1
  31. package/templates/skills/execute/SKILL.md +8 -4
  32. package/templates/skills/investigate/SKILL.md +15 -4
  33. package/templates/skills/memory/SKILL.md +79 -15
  34. package/templates/skills/menu/SKILL.md +19 -24
  35. package/templates/skills/onboard/SKILL.md +1 -1
  36. package/templates/skills/onboard/phases/detect-state.md +1 -1
  37. package/templates/skills/orient/SKILL.md +60 -14
  38. package/templates/skills/orient/phases/auto-maintenance.md +67 -23
  39. package/templates/skills/orient/phases/context.md +6 -13
  40. package/templates/skills/plan/SKILL.md +7 -4
  41. package/templates/skills/seed/SKILL.md +17 -3
  42. package/templates/skills/work-tracker/SKILL.md +56 -0
  43. package/templates/hooks/memory-post-compact.sh +0 -43
  44. package/templates/hooks/memory-session-start.sh +0 -59
package/lib/cli.js CHANGED
@@ -12,6 +12,177 @@ const { reset } = require('./reset');
12
12
 
13
13
  const VERSION = require('../package.json').version;
14
14
 
15
+ /**
16
+ * Parse YAML frontmatter from a SKILL.md file (between first two --- lines).
17
+ * Returns an object with extracted fields, or null if no frontmatter found.
18
+ * Supports one level of nesting (e.g., directives: { orient: "...", debrief: "..." }).
19
+ */
20
+ function parseFrontmatter(content) {
21
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
22
+ if (!match) return null;
23
+ const fm = {};
24
+ let currentKey = null;
25
+ let currentValue = '';
26
+ let nestedKey = null; // Parent key when parsing a nested map
27
+ let nestedMap = null; // The nested map being built
28
+ let nestedSubKey = null; // Current sub-key within the nested map
29
+ let nestedSubValue = '';
30
+
31
+ function flushNested() {
32
+ if (nestedSubKey && nestedMap) {
33
+ nestedMap[nestedSubKey] = nestedSubValue.trim();
34
+ nestedSubKey = null;
35
+ nestedSubValue = '';
36
+ }
37
+ if (nestedKey && nestedMap) {
38
+ fm[nestedKey] = nestedMap;
39
+ nestedKey = null;
40
+ nestedMap = null;
41
+ }
42
+ }
43
+
44
+ function flushCurrent() {
45
+ if (currentKey) {
46
+ fm[currentKey] = currentValue.trim();
47
+ currentKey = null;
48
+ currentValue = '';
49
+ }
50
+ }
51
+
52
+ for (const line of match[1].split('\n')) {
53
+ // Inside a nested map?
54
+ if (nestedKey) {
55
+ // Nested sub-key (indented key: value)
56
+ const subKv = line.match(/^ ([a-z][a-z0-9_-]*)\s*:\s*(.*)/);
57
+ if (subKv) {
58
+ // Save previous sub-key
59
+ if (nestedSubKey) {
60
+ nestedMap[nestedSubKey] = nestedSubValue.trim();
61
+ }
62
+ nestedSubKey = subKv[1];
63
+ nestedSubValue = subKv[2].replace(/^[>|]\s*$/, '');
64
+ continue;
65
+ }
66
+ // Continuation of nested sub-value (deeply indented)
67
+ if (nestedSubKey && /^ /.test(line)) {
68
+ nestedSubValue += ' ' + line.trim();
69
+ continue;
70
+ }
71
+ // Not indented — end of nested map
72
+ if (nestedSubKey) {
73
+ nestedMap[nestedSubKey] = nestedSubValue.trim();
74
+ nestedSubKey = null;
75
+ nestedSubValue = '';
76
+ }
77
+ fm[nestedKey] = nestedMap;
78
+ nestedKey = null;
79
+ nestedMap = null;
80
+ // Fall through to parse this line as a top-level key
81
+ }
82
+
83
+ // Continuation line (indented, for scalar values)
84
+ if (currentKey && /^[\s]/.test(line)) {
85
+ currentValue += ' ' + line.trim();
86
+ continue;
87
+ }
88
+
89
+ flushCurrent();
90
+
91
+ // New key-value pair
92
+ const kvMatch = line.match(/^([a-z][a-z0-9_-]*)\s*:\s*(.*)/);
93
+ if (kvMatch) {
94
+ const val = kvMatch[2].trim();
95
+ // Empty value after colon = start of nested map or block scalar
96
+ if (val === '' || val === '>' || val === '|') {
97
+ // Peek: could be a nested map or a block scalar.
98
+ // We'll treat it as a nested map if the next indented line has a colon.
99
+ // For now, start as nested and fall back if no sub-keys found.
100
+ nestedKey = kvMatch[1];
101
+ nestedMap = {};
102
+ currentKey = null;
103
+ } else {
104
+ currentKey = kvMatch[1];
105
+ currentValue = val;
106
+ }
107
+ }
108
+ }
109
+ // Flush remaining
110
+ if (nestedSubKey && nestedMap) {
111
+ nestedMap[nestedSubKey] = nestedSubValue.trim();
112
+ }
113
+ if (nestedKey && nestedMap) {
114
+ fm[nestedKey] = Object.keys(nestedMap).length > 0 ? nestedMap : '';
115
+ }
116
+ flushCurrent();
117
+ return fm;
118
+ }
119
+
120
+ /**
121
+ * Generate .claude/skills/_index.json from all installed SKILL.md files.
122
+ * Consumers (menu, cabinet, audit, plan, execute) read this instead of
123
+ * scanning and parsing dozens of individual files.
124
+ */
125
+ function generateSkillIndex(projectDir) {
126
+ const skillsDir = path.join(projectDir, '.claude', 'skills');
127
+ if (!fs.existsSync(skillsDir)) return 0;
128
+
129
+ const entries = [];
130
+ const dirs = fs.readdirSync(skillsDir, { withFileTypes: true });
131
+ for (const dir of dirs) {
132
+ if (!dir.isDirectory()) continue;
133
+ const skillFile = path.join(skillsDir, dir.name, 'SKILL.md');
134
+ if (!fs.existsSync(skillFile)) continue;
135
+
136
+ const content = fs.readFileSync(skillFile, 'utf8');
137
+ const fm = parseFrontmatter(content);
138
+ if (!fm || !fm.name) continue;
139
+
140
+ // Extract first sentence of description (before "Use when:")
141
+ let shortDesc = (fm.description || '').replace(/\s*Use when:.*$/is, '').trim();
142
+ // Collapse to first sentence
143
+ const sentenceEnd = shortDesc.match(/\.\s/);
144
+ if (sentenceEnd) shortDesc = shortDesc.slice(0, sentenceEnd.index + 1);
145
+
146
+ const isCabinet = dir.name.startsWith('cabinet-');
147
+ const entry = {
148
+ name: fm.name,
149
+ path: `.claude/skills/${dir.name}/SKILL.md`,
150
+ description: shortDesc,
151
+ type: isCabinet ? 'cabinet' : 'workflow',
152
+ };
153
+
154
+ // Invocability flags
155
+ if (fm['disable-model-invocation'] === 'true') entry.manual = true;
156
+ if (fm['user-invocable'] === 'false') entry.userInvocable = false;
157
+
158
+ // Standing mandate (for audit/plan/execute/orient/debrief member selection)
159
+ if (fm['standing-mandate']) {
160
+ entry.standingMandate = fm['standing-mandate'].split(/,\s*/).map(s => s.trim());
161
+ }
162
+
163
+ // Directives (scoped tasks for orient/debrief/etc.)
164
+ if (fm.directives && typeof fm.directives === 'object') {
165
+ entry.directives = fm.directives;
166
+ }
167
+
168
+ entries.push(entry);
169
+ }
170
+
171
+ entries.sort((a, b) => a.name.localeCompare(b.name));
172
+
173
+ const index = {
174
+ skills: entries,
175
+ generatedAt: new Date().toISOString(),
176
+ version: VERSION,
177
+ };
178
+
179
+ fs.writeFileSync(
180
+ path.join(skillsDir, '_index.json'),
181
+ JSON.stringify(index, null, 2) + '\n'
182
+ );
183
+ return entries.length;
184
+ }
185
+
15
186
  const MODULES = {
16
187
  'session-loop': {
17
188
  name: 'Session Loop (orient + debrief)',
@@ -33,7 +204,7 @@ const MODULES = {
33
204
  mandatory: false,
34
205
  default: true,
35
206
  lean: false,
36
- templates: ['scripts/pib-db.js', 'scripts/pib-db-schema.sql', 'scripts/work-tracker-server.mjs', 'scripts/work-tracker-ui.html'],
207
+ templates: ['scripts/pib-db.js', 'scripts/pib-db-schema.sql', 'scripts/work-tracker-server.mjs', 'scripts/work-tracker-ui.html', 'skills/work-tracker'],
37
208
  needsDb: true,
38
209
  },
39
210
  'planning': {
@@ -59,7 +230,7 @@ const MODULES = {
59
230
  default: true,
60
231
  lean: true,
61
232
  templates: [
62
- 'skills/audit', 'skills/pulse', 'skills/triage-audit',
233
+ 'skills/audit', 'skills/pulse', 'skills/triage-audit', 'skills/cabinet',
63
234
  'cabinet', 'briefing',
64
235
  'skills/cabinet-accessibility', 'skills/cabinet-anti-confirmation',
65
236
  'skills/cabinet-architecture', 'skills/cabinet-boundary-man',
@@ -103,7 +274,7 @@ const MODULES = {
103
274
  default: true,
104
275
  lean: false,
105
276
  needsOmega: true,
106
- templates: ['skills/memory', 'hooks/memory-session-start.sh', 'hooks/memory-post-compact.sh', 'scripts/cabinet-memory-adapter.py', 'rules/memory-capture.md'],
277
+ templates: ['skills/memory', 'scripts/cabinet-memory-adapter.py', 'rules/memory-capture.md'],
107
278
  },
108
279
  };
109
280
 
@@ -558,8 +729,7 @@ async function run() {
558
729
 
559
730
  // --- Merge hooks into settings.json ---
560
731
  if (selectedModules.includes('hooks') && !flags.dryRun) {
561
- const includeMemory = selectedModules.includes('memory');
562
- const settingsPath = mergeSettings(projectDir, { includeDb, includeMemory });
732
+ const settingsPath = mergeSettings(projectDir, { includeDb });
563
733
  console.log(` ⚙️ Merged hooks into ${path.relative(projectDir, settingsPath)}`);
564
734
  }
565
735
 
@@ -737,6 +907,14 @@ async function run() {
737
907
  }
738
908
  }
739
909
 
910
+ // --- Generate skill index ---
911
+ if (!flags.dryRun) {
912
+ const indexCount = generateSkillIndex(projectDir);
913
+ if (indexCount > 0) {
914
+ console.log(` 📇 Indexed ${indexCount} skills in .claude/skills/_index.json`);
915
+ }
916
+ }
917
+
740
918
  // --- Write metadata ---
741
919
  if (!flags.dryRun) {
742
920
  createMetadata(projectDir, {
package/lib/copy.js CHANGED
@@ -75,6 +75,7 @@ async function walkAndCopy(srcRoot, destRoot, currentSrc, results, dryRun, skipC
75
75
  if (!dryRun) fs.copyFileSync(srcPath, destPath);
76
76
  results.overwritten.push(relPath);
77
77
  results.manifest[relPath] = incomingHash;
78
+ console.log(` Updated: ${displayPath}`);
78
79
  } else {
79
80
  results.skipped.push(relPath);
80
81
  // Record the hash of what's actually on disk, not the template —
@@ -106,6 +106,21 @@ function setupOmega() {
106
106
  try {
107
107
  execSync(`"${VENV_PYTHON}" -c "import omega"`, { stdio: 'pipe' });
108
108
  results.push('Existing omega venv is valid');
109
+ // Ensure cross-encoder model is downloaded (added in v0.9.1)
110
+ try {
111
+ const hasReranker = execSync(
112
+ `"${VENV_PYTHON}" -c "from omega.reranker import _get_model_dir; print('yes' if _get_model_dir() else 'no')"`,
113
+ { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
114
+ ).trim();
115
+ if (hasReranker === 'no') {
116
+ console.log(' Downloading cross-encoder model...');
117
+ execSync(`"${VENV_PYTHON}" -c "from omega.reranker import download_model; download_model()"`, {
118
+ stdio: 'pipe',
119
+ timeout: 120000,
120
+ });
121
+ results.push('Downloaded cross-encoder reranker model');
122
+ }
123
+ } catch { /* non-fatal */ }
109
124
  return results;
110
125
  } catch {
111
126
  // Venv is broken — nuke and rebuild (D5)
@@ -135,6 +150,35 @@ function setupOmega() {
135
150
  });
136
151
  results.push('Downloaded ONNX embedding model (bge-small-en-v1.5)');
137
152
 
153
+ // 6. Download cross-encoder reranker model (improves query result ranking)
154
+ console.log(' Downloading cross-encoder model...');
155
+ try {
156
+ execSync(`"${VENV_PYTHON}" -c "from omega.reranker import download_model; download_model()"`, {
157
+ stdio: 'pipe',
158
+ timeout: 120000,
159
+ });
160
+ results.push('Downloaded cross-encoder reranker model');
161
+ } catch {
162
+ // Non-fatal — queries work without it, just less accurate ranking
163
+ results.push('Cross-encoder model download skipped (optional)');
164
+ }
165
+
166
+ // 7. Configure omega native hooks in global settings
167
+ // Omega hooks live in ~/.claude/settings.json (global) — they run for all
168
+ // projects and handle memory capture/recall natively. This is idempotent:
169
+ // omega checks if hooks already exist before adding them.
170
+ console.log(' Configuring omega hooks...');
171
+ try {
172
+ execSync(`"${VENV_PYTHON}" -m omega.cli hooks setup`, {
173
+ stdio: 'pipe',
174
+ timeout: 30000,
175
+ });
176
+ results.push('Configured omega native hooks (global settings)');
177
+ } catch {
178
+ // Non-fatal — hooks can be set up manually with `omega hooks setup`
179
+ results.push('Omega hooks setup skipped (run `omega hooks setup` manually)');
180
+ }
181
+
138
182
  return results;
139
183
  }
140
184
 
@@ -1,31 +1,6 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
 
4
- const MEMORY_HOOKS = {
5
- SessionStart: [
6
- {
7
- matcher: 'startup|resume|compact',
8
- hooks: [
9
- {
10
- type: 'command',
11
- command: '.claude/hooks/memory-session-start.sh',
12
- },
13
- ],
14
- },
15
- ],
16
- PostCompact: [
17
- {
18
- matcher: '',
19
- hooks: [
20
- {
21
- type: 'command',
22
- command: '.claude/hooks/memory-post-compact.sh',
23
- },
24
- ],
25
- },
26
- ],
27
- };
28
-
29
4
  const DEFAULT_HOOKS = {
30
5
  PreToolUse: [
31
6
  {
@@ -75,7 +50,7 @@ const DEFAULT_HOOKS = {
75
50
  * Merge PIB hooks into the project's .claude/settings.json.
76
51
  * Creates the file if it doesn't exist. Preserves existing hooks.
77
52
  */
78
- function mergeSettings(projectDir, { includeDb = true, includeMemory = false } = {}) {
53
+ function mergeSettings(projectDir, { includeDb = true } = {}) {
79
54
  const settingsDir = path.join(projectDir, '.claude');
80
55
  const settingsPath = path.join(settingsDir, 'settings.json');
81
56
 
@@ -90,16 +65,24 @@ function mergeSettings(projectDir, { includeDb = true, includeMemory = false } =
90
65
 
91
66
  if (!settings.hooks) settings.hooks = {};
92
67
 
93
- // Build the complete hook set for this install
94
- const allHooks = { ...DEFAULT_HOOKS };
95
- if (includeMemory) {
96
- for (const [event, hooks] of Object.entries(MEMORY_HOOKS)) {
97
- allHooks[event] = [...(allHooks[event] || []), ...hooks];
98
- }
68
+ // Remove legacy CC memory hooks (v0.9.x and earlier).
69
+ // These are now handled by omega's native hooks in global settings.
70
+ const LEGACY_MEMORY_COMMANDS = [
71
+ 'memory-session-start.sh',
72
+ 'memory-post-compact.sh',
73
+ ];
74
+ for (const [event, entries] of Object.entries(settings.hooks)) {
75
+ if (!Array.isArray(entries)) continue;
76
+ settings.hooks[event] = entries.filter(entry => {
77
+ if (!entry.hooks || !Array.isArray(entry.hooks)) return true;
78
+ return !entry.hooks.some(h =>
79
+ LEGACY_MEMORY_COMMANDS.some(cmd => (h.command || '').includes(cmd))
80
+ );
81
+ });
99
82
  }
100
83
 
101
84
  // Merge each hook event type
102
- for (const [event, newHooks] of Object.entries(allHooks)) {
85
+ for (const [event, newHooks] of Object.entries(DEFAULT_HOOKS)) {
103
86
  if (!settings.hooks[event]) {
104
87
  settings.hooks[event] = newHooks;
105
88
  } else {
@@ -123,4 +106,4 @@ function mergeSettings(projectDir, { includeDb = true, includeMemory = false } =
123
106
  return settingsPath;
124
107
  }
125
108
 
126
- module.exports = { mergeSettings, DEFAULT_HOOKS, MEMORY_HOOKS };
109
+ module.exports = { mergeSettings, DEFAULT_HOOKS };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-claude-cabinet",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
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"
@@ -1,43 +1,42 @@
1
1
  # Memory Capture Rules
2
2
 
3
- When omega memory is active (check: `~/.claude-cabinet/omega-venv/bin/python3`
4
- exists and `scripts/cabinet-memory-adapter.py` exists), these rules govern
5
- what gets captured and when.
3
+ When omega memory is active (check: `omega hooks doctor` reports OK),
4
+ these rules govern what gets captured and when.
6
5
 
7
- ## During Sessions — What to Capture
6
+ ## How Capture Works
8
7
 
9
- Capture to omega when you observe any of these during a session:
8
+ Omega handles memory capture natively through its hooks in
9
+ `~/.claude/settings.json` (global). No project-level hook scripts needed.
10
10
 
11
- **Decisions with reasoning.** When the user makes a non-obvious choice
12
- (architecture, naming, tradeoffs, tool selection), capture the decision
13
- AND the reasoning. "Chose SQLite over Postgres because single-user,
14
- no server dependency"not just "uses SQLite."
11
+ **Automatic capture (omega native hooks):**
12
+ - `auto_capture` (UserPromptSubmit) detects decisions/lessons from user messages in real time
13
+ - `assistant_capture` (Stop) extracts insights from assistant responses at session end
14
+ - `session_stop` (Stop)session summary, activity report, auto-reflection
15
+ - `surface_memories` (PostToolUse) — surfaces relevant memories before file edits
15
16
 
16
- **Discovered constraints.** When something that seemed possible turns
17
- out to have a limitation, gotcha, or prerequisite. "Python venv on
18
- Debian requires separate python3-venv package" the kind of thing
19
- that wastes 30 minutes if you don't know it.
17
+ **Manual capture (adapter or omega MCP tools):**
18
+ - Use `omega_store()` MCP tool directly, or
19
+ - Use the adapter for project-scoped storage:
20
+ ```bash
21
+ echo '{"text": "the memory", "type": "decision"}' | \
22
+ ~/.claude-cabinet/omega-venv/bin/python3 scripts/cabinet-memory-adapter.py store
23
+ ```
20
24
 
21
- **User preferences revealed through correction.** When the user says
22
- "no, not like that" or redirects your approach, capture what they
23
- actually want. "User prefers single bundled PRs for refactors, not
24
- many small ones."
25
+ **Memory types:** `decision`, `lesson_learned`, `user_preference`, `constraint`, `error_pattern`
25
26
 
26
- **Pattern establishment.** When a convention is established for the
27
- first time — naming pattern, file organization, workflow step. Not
28
- the convention itself (that's in the code), but that it was a
29
- deliberate choice.
27
+ ## What to Capture Manually
30
28
 
31
- ## How to Capture
29
+ Omega's auto_capture hook catches many decisions and lessons from
30
+ conversation flow. Manual capture is for things the hooks miss:
32
31
 
33
- Use the adapter never call omega directly from shell:
32
+ **Decisions with reasoning.** Non-obvious architectural choices where
33
+ the "why" matters as much as the "what."
34
34
 
35
- ```bash
36
- echo '{"text": "the memory", "type": "decision"}' | \
37
- ~/.claude-cabinet/omega-venv/bin/python3 scripts/cabinet-memory-adapter.py store
38
- ```
35
+ **Discovered constraints.** Limitations or gotchas that waste time
36
+ if you don't know them in advance.
39
37
 
40
- Memory types: `decision`, `lesson`, `preference`, `constraint`, `pattern`
38
+ **User preferences revealed through correction.** When the user
39
+ redirects your approach — capture what they actually want.
41
40
 
42
41
  ## What NOT to Capture
43
42
 
@@ -49,17 +48,11 @@ Memory types: `decision`, `lesson`, `preference`, `constraint`, `pattern`
49
48
 
50
49
  ## Capture Cadence
51
50
 
52
- Do NOT capture after every interaction. Capture when something worth
53
- remembering actually happens. Most messages in a session produce nothing
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.
51
+ Omega's native hooks handle most capture automatically. Manual capture
52
+ should be rare only when something important happened that the hooks
53
+ wouldn't detect (e.g., a nuanced architectural decision discussed
54
+ verbally, or a constraint discovered through external research).
61
55
 
62
56
  Over-capturing degrades retrieval quality. The test: *"Would a future
63
57
  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.
58
+ noise or ephemera, skip it.
@@ -1,21 +1,23 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- Cabinet Memory Adapter — single Python file wrapping all omega interaction.
3
+ Cabinet Memory Adapter — single Python file wrapping omega interaction.
4
4
 
5
- Called by hook scripts via the venv Python. All omega calls go through here.
5
+ Called by skills and scripts via the venv Python. Provides project-scoped
6
+ tiered retrieval (omega's main gap) and a stable JSON-in/JSON-out interface.
6
7
  Designed for D2 (never block Claude Code) and D3 (graceful degradation).
7
8
 
9
+ Note: Session-level hooks (welcome, capture, session start/stop) are handled
10
+ by omega's native hooks configured in ~/.claude/settings.json (global).
11
+ This adapter handles skill-invoked operations only.
12
+
8
13
  Usage:
9
14
  cabinet-memory-adapter.py <command> [options]
10
15
 
11
16
  Commands:
12
- welcome Surface relevant memories for session start
13
- capture Store a memory from hook input (PostCompact)
14
- store Store a memory directly (called by debrief)
15
- query Query memories by text
17
+ store Store a memory directly (called by debrief, /memory)
18
+ query Query memories by text (with project-scoped tiering)
16
19
  delete Delete a memory by full node_id
17
20
  list List all memories with full node_ids
18
- status Check omega health
19
21
 
20
22
  All commands read JSON from stdin when applicable.
21
23
  All commands output JSON to stdout.
@@ -59,87 +61,15 @@ def _import_omega():
59
61
  return None
60
62
 
61
63
 
62
- def cmd_welcome():
63
- """Surface relevant memories for session start.
64
-
65
- Reads session context from stdin (session_id, cwd, source).
66
- Calls omega.welcome() to get relevant memories.
67
- Outputs memories as context text for SessionStart hook stdout.
68
- """
69
- data = _read_stdin()
70
- omega = _import_omega()
71
- if not omega:
72
- _error("omega not available")
73
- return
74
-
75
- try:
76
- cwd = data.get("cwd", os.getcwd())
77
- project_name = os.path.basename(cwd)
78
-
79
- result = omega.welcome(project=project_name)
80
- if not result:
81
- _output({"ok": True, "context": ""})
82
- return
83
-
84
- # welcome() returns a dict with memory count, recent memories, etc.
85
- if isinstance(result, dict):
86
- context = (
87
- result.get("observation_prefix", "")
88
- or result.get("summary", "")
89
- or result.get("context", "")
90
- )
91
- if not context and result.get("memory_count", 0) == 0:
92
- _output({"ok": True, "context": ""})
93
- return
94
- if not context:
95
- context = json.dumps(result, indent=2)
96
- _output({"ok": True, "context": context})
97
- elif isinstance(result, str):
98
- _output({"ok": True, "context": result})
99
- else:
100
- _output({"ok": True, "context": str(result)})
101
- except Exception as e:
102
- _error(f"welcome failed: {e}")
103
-
104
-
105
- def cmd_capture():
106
- """Capture context from PostCompact summary.
107
-
108
- Reads compact_summary from stdin.
109
- Extracts key decisions, lessons, and reasoning chains.
110
- Stores them in omega.
111
- """
112
- data = _read_stdin()
113
- omega = _import_omega()
114
- if not omega:
115
- _error("omega not available")
116
- return
117
-
118
- summary = data.get("compact_summary", "")
119
- if not summary:
120
- _output({"ok": True, "stored": 0, "reason": "no summary"})
121
- return
122
-
123
- session_id = data.get("session_id", "unknown")
124
- cwd = data.get("cwd", os.getcwd())
125
- project_name = os.path.basename(cwd)
126
-
127
- try:
128
- result = omega.auto_capture(
129
- summary,
130
- event_type="compaction",
131
- session_id=session_id,
132
- project=project_name,
133
- )
134
- count = 0
135
- if isinstance(result, dict):
136
- count = result.get("stored", 0)
137
- elif isinstance(result, (list, tuple)):
138
- count = len(result)
139
-
140
- _output({"ok": True, "stored": count})
141
- except Exception as e:
142
- _error(f"capture failed: {e}")
64
+ # Map friendly type names to omega's native event_type values.
65
+ # This ensures permanent TTL for types that should never expire.
66
+ _TYPE_MAP = {
67
+ "lesson": "lesson_learned",
68
+ "preference": "user_preference",
69
+ "error": "error_pattern",
70
+ # These already match omega's native types:
71
+ # "decision", "constraint", "error_pattern", "lesson_learned", "user_preference"
72
+ }
143
73
 
144
74
 
145
75
  def cmd_store():
@@ -147,7 +77,9 @@ def cmd_store():
147
77
 
148
78
  Reads JSON from stdin with fields:
149
79
  text: the memory content (required)
150
- type: event_type for omega (default: "lesson")
80
+ type: event_type for omega (default: "lesson_learned")
81
+ Accepts friendly names: lesson, preference, error
82
+ which are mapped to omega native types.
151
83
  tags: list of tags (stored in metadata, optional)
152
84
  project: project name (default: basename of cwd)
153
85
  """
@@ -163,7 +95,8 @@ def cmd_store():
163
95
  return
164
96
 
165
97
  try:
166
- event_type = data.get("type", "lesson")
98
+ raw_type = data.get("type", "lesson")
99
+ event_type = _TYPE_MAP.get(raw_type, raw_type)
167
100
  project = data.get("project", os.path.basename(os.getcwd()))
168
101
  metadata = {}
169
102
  if data.get("tags"):
@@ -335,23 +268,6 @@ def cmd_query():
335
268
  _error(f"query failed: {e}")
336
269
 
337
270
 
338
- def cmd_status():
339
- """Check omega health status."""
340
- omega = _import_omega()
341
- if not omega:
342
- _output({"ok": False, "status": "omega not available"})
343
- return
344
-
345
- try:
346
- result = omega.status()
347
- if isinstance(result, dict):
348
- _output({"ok": True, **result})
349
- else:
350
- _output({"ok": True, "status": str(result)})
351
- except Exception as e:
352
- _error(f"status failed: {e}")
353
-
354
-
355
271
  def cmd_delete():
356
272
  """Delete a memory by its full node_id.
357
273
 
@@ -433,13 +349,10 @@ def cmd_list():
433
349
 
434
350
 
435
351
  COMMANDS = {
436
- "welcome": cmd_welcome,
437
- "capture": cmd_capture,
438
352
  "store": cmd_store,
439
353
  "query": cmd_query,
440
354
  "delete": cmd_delete,
441
355
  "list": cmd_list,
442
- "status": cmd_status,
443
356
  }
444
357
 
445
358
  if __name__ == "__main__":