claude-mem-lite 2.3.0 → 2.3.1

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.
@@ -0,0 +1,51 @@
1
+ ---
2
+ name: memory
3
+ description: Save content to memory — with explicit content, instructions, or auto-summarize current session
4
+ ---
5
+
6
+ # Memory Save
7
+
8
+ Save important content to your long-term memory database.
9
+
10
+ ## Commands
11
+
12
+ - `/mem:memory <content>` — Save the given content directly to memory
13
+ - `/mem:memory` (no args) — Auto-summarize recent session highlights and save key findings
14
+
15
+ ## Instructions
16
+
17
+ When the user invokes `/mem:memory`, determine the intent:
18
+
19
+ ### With explicit content
20
+
21
+ If the user provides content after the command:
22
+
23
+ 1. Analyze the content to determine appropriate type (decision, bugfix, feature, refactor, discovery, change)
24
+ 2. Generate a concise title from the content
25
+ 3. Call `mem_save` with:
26
+ - `content`: the provided text
27
+ - `title`: auto-generated title
28
+ - `type`: inferred type (default: "discovery")
29
+ - `importance`: 2 (notable — user explicitly requested save)
30
+
31
+ ### With instructions/prompt
32
+
33
+ If the user provides instructions like "save the database schema we discussed" or "remember the fix for the auth bug":
34
+
35
+ 1. Review recent conversation context
36
+ 2. Extract the relevant information per the user's instruction
37
+ 3. Call `mem_save` with extracted content, appropriate title and type, importance=2
38
+
39
+ ### No arguments (auto-save)
40
+
41
+ If no content is provided:
42
+
43
+ 1. Review the current session's recent key findings
44
+ 2. Identify: decisions made, bugs fixed, patterns discovered, important code changes
45
+ 3. For each significant finding (max 5), call `mem_save` with:
46
+ - Clear title and structured content
47
+ - Appropriate type and importance level (1=routine, 2=notable)
48
+ 4. Skip trivial or already-saved items
49
+ 5. Report what was saved in a concise summary
50
+
51
+ Always set importance=2 for explicit saves (user chose to save), importance=1 for auto-saves of routine items, importance=2 for auto-saves of notable discoveries.
@@ -0,0 +1,78 @@
1
+ ---
2
+ name: tools
3
+ description: Import skills and agents from GitHub repositories into the tool resource registry
4
+ ---
5
+
6
+ # Tool Import
7
+
8
+ Import skills and agents from GitHub repositories into the resource registry for intelligent dispatch.
9
+
10
+ ## Commands
11
+
12
+ - `/mem:tools <github-url>` — Import all skills/agents from a GitHub repo
13
+ - `/mem:tools <github-url> <instructions>` — Import with specific instructions (add/remove specific items)
14
+ - `/mem:tools <instructions>` — Directly add/remove/modify tools by prompt (no URL needed)
15
+ - `/mem:tools` (no args) — Show current registry stats and import help
16
+
17
+ ## Instructions
18
+
19
+ When the user invokes `/mem:tools`:
20
+
21
+ ### With GitHub URL
22
+
23
+ 1. Use WebFetch to fetch the repository README and skill/agent files:
24
+ - Try `https://raw.githubusercontent.com/{owner}/{repo}/main/README.md`
25
+ - Look for skill definitions (`.md` files with frontmatter), agent definitions, plugin.json
26
+ - If the repo has a `commands/` directory, fetch skill files from there
27
+ 2. Identify all skills and agents in the repository
28
+ 3. For each tool found, extract metadata using your understanding of the content:
29
+ - `name`: tool name (lowercase, hyphenated)
30
+ - `resource_type`: "skill" or "agent"
31
+ - `repo_url`: the GitHub URL
32
+ - `intent_tags`: comma-separated intent keywords (what the tool helps with)
33
+ - `domain_tags`: comma-separated technology/domain tags
34
+ - `capability_summary`: one-line description of what the tool does
35
+ - `trigger_patterns`: when to recommend this tool (natural language)
36
+ - `keywords`: additional search terms
37
+ - `tech_stack`: technology stack tags
38
+ - `use_cases`: usage scenarios
39
+ 4. Call `mem_registry(action="import", ...)` for each tool with extracted metadata
40
+ 5. Call `mem_registry(action="reindex")` to update FTS5 index
41
+ 6. Report imported tools in a table format
42
+
43
+ ### With GitHub URL + instructions
44
+
45
+ If the user provides instructions after the URL:
46
+ - "only add the TDD skill" → import only matching tools from that repo
47
+ - "remove the old testing tool" → call `mem_registry(action="remove", ...)`
48
+ - Follow user instructions for selective add/remove/modify operations
49
+
50
+ ### With instructions only (no URL)
51
+
52
+ If the user provides a prompt without a GitHub URL, parse the intent:
53
+
54
+ **Adding a tool:**
55
+ - "添加一个叫 my-linter 的 skill" or "add a skill called my-linter"
56
+ - → Ask for metadata (or infer from context): capability_summary, intent_tags, domain_tags, trigger_patterns
57
+ - → Call `mem_registry(action="import", name="my-linter", resource_type="skill", ...)`
58
+
59
+ **Removing a tool:**
60
+ - "删除 old-testing skill" or "remove the old-testing agent"
61
+ - → Call `mem_registry(action="remove", name="old-testing", resource_type="skill")`
62
+
63
+ **Listing/searching:**
64
+ - "有哪些 testing 相关的工具" or "list all agents"
65
+ - → Call `mem_registry(action="list", type="agent")` or search by keywords
66
+
67
+ **Modifying a tool:**
68
+ - "更新 my-linter 的描述" or "update tags for my-tool"
69
+ - → Call `mem_registry(action="import", ...)` with updated metadata (upsert)
70
+
71
+ Always call `mem_registry(action="reindex")` after any add/remove/modify operations.
72
+
73
+ ### Without URL or instructions
74
+
75
+ If no arguments provided:
76
+ 1. Call `mem_registry(action="stats")` to show current registry state
77
+ 2. Call `mem_registry(action="list")` to show all registered tools
78
+ 3. Explain usage examples
@@ -0,0 +1,37 @@
1
+ ---
2
+ name: update
3
+ description: Auto-maintain memory and resource registry — deduplicate, merge, decay, cleanup, reindex
4
+ ---
5
+
6
+ # Memory & Registry Maintenance
7
+
8
+ Run intelligent maintenance on both the memory database and tool resource registry.
9
+
10
+ ## Usage
11
+
12
+ - `/mem:update` — Run full maintenance cycle
13
+
14
+ ## Instructions
15
+
16
+ When the user invokes `/mem:update`, perform the following maintenance cycle:
17
+
18
+ ### Phase 1: Memory Maintenance
19
+
20
+ 1. Call `mem_maintain(action="scan")` to analyze maintenance candidates
21
+ 2. Report scan results to the user (duplicates, stale items, broken items, boostable items)
22
+ 3. Call `mem_maintain(action="execute", operations=["cleanup","decay","boost"])` to apply safe automatic changes
23
+ 4. If duplicates were found in scan, review them and call `mem_maintain(action="execute", operations=["dedup"], merge_ids=[[keepId, removeId1, ...], ...])` — keep the more important/recent observation in each pair
24
+ 5. Run `mem_compress(preview=false)` for old low-value observations
25
+
26
+ ### Phase 2: Registry Maintenance
27
+
28
+ 1. Call `mem_registry(action="stats")` to get registry overview
29
+ 2. Call `mem_registry(action="reindex")` to rebuild FTS5 search index
30
+ 3. Report updated stats
31
+
32
+ ### Phase 3: Summary
33
+
34
+ Summarize all maintenance actions taken in zh-CN:
35
+ - Memory: observations cleaned, decayed, boosted, deduplicated, compressed
36
+ - Registry: total resources, adoption rates, reindex status
37
+ - Overall health assessment
@@ -0,0 +1,155 @@
1
+ // claude-mem-lite: Workflow-aware dispatch intelligence
2
+ // Suite auto-flow protection, explicit request detection, stage model
3
+
4
+ // ─── Stage Model ─────────────────────────────────────────────────────────────
5
+
6
+ export const STAGES = ['ANALYZE', 'PLAN', 'REVIEW_PLAN', 'EXECUTE', 'TEST', 'REVIEW_CODE', 'COMMIT'];
7
+
8
+ // Skill invocation name → workflow stage
9
+ const SKILL_STAGE_MAP = {
10
+ 'superpowers:brainstorming': 'ANALYZE',
11
+ 'superpowers:writing-plans': 'PLAN',
12
+ 'gsd:start': 'PLAN',
13
+ 'gsd:prd': 'PLAN',
14
+ 'superpowers:executing-plans': 'EXECUTE',
15
+ 'superpowers:subagent-driven-development': 'EXECUTE',
16
+ 'gsd:resume': 'EXECUTE',
17
+ 'superpowers:test-driven-development': 'TEST',
18
+ 'superpowers:systematic-debugging': 'EXECUTE',
19
+ 'superpowers:verification-before-completion': 'TEST',
20
+ 'superpowers:requesting-code-review': 'REVIEW_CODE',
21
+ 'superpowers:receiving-code-review': 'REVIEW_CODE',
22
+ 'superpowers:finishing-a-development-branch': 'COMMIT',
23
+ 'commit-commands:commit': 'COMMIT',
24
+ 'commit-commands:commit-push-pr': 'COMMIT',
25
+ 'commit-commands:clean_gone': 'COMMIT',
26
+ };
27
+
28
+ // User intent → stage mapping (for stage inference from prompt)
29
+ export const INTENT_STAGE_MAP = {
30
+ 'plan': 'PLAN',
31
+ 'review': 'REVIEW_CODE',
32
+ 'test': 'TEST',
33
+ 'fix': 'EXECUTE',
34
+ 'clean': 'EXECUTE',
35
+ 'commit': 'COMMIT',
36
+ 'deploy': 'COMMIT',
37
+ 'design': 'ANALYZE',
38
+ 'doc': 'COMMIT',
39
+ 'build': 'EXECUTE',
40
+ 'fast': 'EXECUTE',
41
+ 'lint': 'EXECUTE',
42
+ 'db': 'EXECUTE',
43
+ 'api': 'EXECUTE',
44
+ 'secure': 'EXECUTE',
45
+ 'infra': 'EXECUTE',
46
+ };
47
+
48
+ // ─── Suite Auto-Flow Protection ──────────────────────────────────────────────
49
+
50
+ export const SUITE_AUTO_FLOWS = {
51
+ superpowers: {
52
+ stages: ['ANALYZE', 'PLAN', 'EXECUTE', 'TEST', 'REVIEW_CODE', 'COMMIT'],
53
+ gaps: ['REVIEW_PLAN'],
54
+ },
55
+ gsd: {
56
+ stages: ['PLAN', 'EXECUTE', 'TEST', 'REVIEW_CODE'],
57
+ gaps: ['ANALYZE', 'REVIEW_PLAN', 'COMMIT'],
58
+ },
59
+ 'feature-dev': {
60
+ stages: ['ANALYZE', 'EXECUTE', 'REVIEW_CODE'],
61
+ gaps: ['PLAN', 'REVIEW_PLAN', 'TEST', 'COMMIT'],
62
+ },
63
+ 'commit-commands': {
64
+ stages: ['COMMIT'],
65
+ gaps: ['ANALYZE', 'PLAN', 'REVIEW_PLAN', 'EXECUTE', 'TEST', 'REVIEW_CODE'],
66
+ },
67
+ };
68
+
69
+ /**
70
+ * Detect if a suite auto-flow is active based on recent Skill tool events.
71
+ * Scans backwards to find the most recent Skill invocation from a known suite.
72
+ * @param {object[]} sessionEvents Array of tool events
73
+ * @returns {{suite: string, flow: object, lastSkill: string}|null}
74
+ */
75
+ export function detectActiveSuite(sessionEvents) {
76
+ if (!sessionEvents || sessionEvents.length === 0) return null;
77
+
78
+ for (let i = sessionEvents.length - 1; i >= 0; i--) {
79
+ const e = sessionEvents[i];
80
+ if (e.tool_name === 'Skill' && e.tool_input?.skill) {
81
+ const skill = e.tool_input.skill;
82
+ const suite = skill.split(':')[0];
83
+ if (SUITE_AUTO_FLOWS[suite]) {
84
+ return { suite, flow: SUITE_AUTO_FLOWS[suite], lastSkill: skill };
85
+ }
86
+ }
87
+ }
88
+ return null;
89
+ }
90
+
91
+ /**
92
+ * Determine if a recommendation should be made for the current stage.
93
+ * @param {{suite: string, flow: object}|null} activeSuite Active suite info
94
+ * @param {string} currentStage Current workflow stage
95
+ * @returns {{shouldRecommend: boolean, reason: string}}
96
+ */
97
+ export function shouldRecommendForStage(activeSuite, currentStage) {
98
+ if (!activeSuite) return { shouldRecommend: true, reason: 'no_suite' };
99
+
100
+ const { flow } = activeSuite;
101
+ if (flow.stages.includes(currentStage)) {
102
+ return { shouldRecommend: false, reason: 'suite_covers_stage' };
103
+ }
104
+ if (flow.gaps.includes(currentStage)) {
105
+ return { shouldRecommend: true, reason: 'suite_gap' };
106
+ }
107
+ return { shouldRecommend: true, reason: 'unknown_stage' };
108
+ }
109
+
110
+ /**
111
+ * Infer the current workflow stage from intent or from the last skill used.
112
+ * @param {string} primaryIntent Primary intent from signal extraction
113
+ * @param {{lastSkill: string}|null} activeSuite Active suite info
114
+ * @returns {string|null} Stage name or null
115
+ */
116
+ export function inferCurrentStage(primaryIntent, activeSuite) {
117
+ if (activeSuite?.lastSkill && SKILL_STAGE_MAP[activeSuite.lastSkill]) {
118
+ return SKILL_STAGE_MAP[activeSuite.lastSkill];
119
+ }
120
+ return INTENT_STAGE_MAP[primaryIntent] || null;
121
+ }
122
+
123
+ // ─── Explicit Request Detection ──────────────────────────────────────────────
124
+
125
+ const EXPLICIT_REQUEST_PATTERNS = [
126
+ // EN: "use the playwright skill", "try the ppt skill"
127
+ /(?:use|try|invoke|run|activate|load)\s+(?:the\s+)?(\S+?)\s+(?:skill|agent|tool|plugin)\b/i,
128
+ // CN: "用ppt的技能", "帮我用playwright的skill"
129
+ /(?:用|使用|帮我用|试试|启用)\s*(\S+?)\s*(?:的|的技能|的skill|的agent|技能|skill|agent|工具|插件)/,
130
+ // "有没有xxx的skill", "is there a xxx agent"
131
+ /(?:有没有|有无|是否有|do you have|is there)\s*(?:一个|a|an)?\s*(\S+?)\s*(?:的|skill|agent|技能|工具)/i,
132
+ // "推荐一个xxx", "recommend a xxx agent"
133
+ /(?:推荐|suggest|recommend)\s*(?:一个|a|an)?\s*(\S+?)\s*(?:的|skill|agent|技能|工具)/i,
134
+ ];
135
+
136
+ /**
137
+ * Detect if the user is explicitly requesting a specific tool/skill.
138
+ * Highest priority — bypasses all dispatch restrictions.
139
+ * @param {string} userPrompt User's prompt text
140
+ * @returns {{isExplicit: boolean, searchTerm?: string}}
141
+ */
142
+ export function detectExplicitRequest(userPrompt) {
143
+ if (!userPrompt) return { isExplicit: false };
144
+
145
+ for (const pattern of EXPLICIT_REQUEST_PATTERNS) {
146
+ const match = userPrompt.match(pattern);
147
+ if (match && match[1]) {
148
+ const term = match[1].replace(/['"]/g, '').trim();
149
+ if (term.length >= 2 && term.length <= 30) {
150
+ return { isExplicit: true, searchTerm: term };
151
+ }
152
+ }
153
+ }
154
+ return { isExplicit: false };
155
+ }
@@ -0,0 +1,222 @@
1
+ // claude-mem-lite: Cross-session handoff extraction, detection, and injection
2
+ // Extracted for testability — hook.mjs has module-level side effects
3
+
4
+ import { basename } from 'path';
5
+ import { truncate, extractMatchKeywords, tokenizeHandoff, isSpecificTerm } from './utils.mjs';
6
+ import {
7
+ HANDOFF_EXPIRY_CLEAR, HANDOFF_EXPIRY_EXIT, HANDOFF_MATCH_THRESHOLD, CONTINUE_KEYWORDS,
8
+ } from './hook-shared.mjs';
9
+
10
+ /**
11
+ * Build and save a handoff snapshot to session_handoffs table.
12
+ * Called synchronously during handleStop (/exit) or handleSessionStart (/clear).
13
+ * @param {Database} db Opened main database
14
+ * @param {string} sessionId Session being handed off
15
+ * @param {string} project Project identifier
16
+ * @param {'clear'|'exit'} type Handoff type
17
+ * @param {object|null} episodeSnapshot Episode buffer captured before flushing
18
+ */
19
+ export function buildAndSaveHandoff(db, sessionId, project, type, episodeSnapshot) {
20
+ // 1. Working objective — from user prompts
21
+ const prompts = db.prepare(`
22
+ SELECT prompt_text FROM user_prompts
23
+ WHERE content_session_id = ?
24
+ ORDER BY prompt_number ASC LIMIT 5
25
+ `).all(sessionId);
26
+ if (prompts.length === 0) return; // Empty session — nothing to hand off
27
+
28
+ const workingOn = prompts.map(p => truncate(p.prompt_text, 200)).join(' → ');
29
+
30
+ // 2. Completed — from observations (include narrative for richer handoff)
31
+ const completed = db.prepare(`
32
+ SELECT title, type, narrative FROM observations
33
+ WHERE memory_session_id = ? AND COALESCE(compressed_into, 0) = 0
34
+ ORDER BY created_at_epoch DESC LIMIT 15
35
+ `).all(sessionId);
36
+
37
+ // 3. Unfinished — episode snapshot + full session edit history from narratives
38
+ let unfinished = '';
39
+ if (episodeSnapshot?.entries) {
40
+ const pendingDescs = episodeSnapshot.entries
41
+ .filter(e => e.isSignificant || e.isError)
42
+ .map(e => e.desc);
43
+ if (pendingDescs.length > 0) unfinished = pendingDescs.join('; ');
44
+ }
45
+ // Only the most recent bugfix is an "unfinished" signal (earlier ones are likely resolved)
46
+ if (!unfinished) {
47
+ const lastBugfix = completed.find(o => o.type === 'bugfix');
48
+ if (lastBugfix) unfinished = lastBugfix.title;
49
+ }
50
+ // Enrich unfinished with full session edit history from observation narratives.
51
+ // Since handoff is UPSERT (max 2 rows per project), storing more data is free.
52
+ const narratives = completed
53
+ .filter(c => c.narrative)
54
+ .map(c => c.narrative);
55
+ if (narratives.length > 0) {
56
+ const editHistory = narratives.join('\n');
57
+ unfinished = [unfinished, editHistory].filter(Boolean).join('\n---\n');
58
+ }
59
+
60
+ // 4. Key files — from episode snapshot + observations
61
+ const fileSet = new Set();
62
+ if (episodeSnapshot?.files) episodeSnapshot.files.forEach(f => fileSet.add(f));
63
+ const obsFiles = db.prepare(`
64
+ SELECT files_modified FROM observations
65
+ WHERE memory_session_id = ? AND files_modified IS NOT NULL
66
+ ORDER BY created_at_epoch DESC LIMIT 10
67
+ `).all(sessionId);
68
+ for (const row of obsFiles) {
69
+ try { JSON.parse(row.files_modified).forEach(f => fileSet.add(f)); } catch {}
70
+ }
71
+
72
+ // 5. Key decisions — high importance observations
73
+ const decisions = db.prepare(`
74
+ SELECT title FROM observations
75
+ WHERE memory_session_id = ? AND COALESCE(importance, 1) >= 2
76
+ AND COALESCE(compressed_into, 0) = 0
77
+ ORDER BY created_at_epoch DESC LIMIT 5
78
+ `).all(sessionId);
79
+
80
+ // 6. Match keywords
81
+ const allText = [workingOn, ...completed.map(c => c.title).filter(Boolean), unfinished].join(' ');
82
+ const keywords = extractMatchKeywords(allText, [...fileSet]);
83
+
84
+ // UPSERT
85
+ db.prepare(`
86
+ INSERT INTO session_handoffs (project, type, session_id, working_on, completed, unfinished, key_files, key_decisions, match_keywords, created_at_epoch)
87
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
88
+ ON CONFLICT(project, type) DO UPDATE SET
89
+ session_id = excluded.session_id,
90
+ working_on = excluded.working_on,
91
+ completed = excluded.completed,
92
+ unfinished = excluded.unfinished,
93
+ key_files = excluded.key_files,
94
+ key_decisions = excluded.key_decisions,
95
+ match_keywords = excluded.match_keywords,
96
+ created_at_epoch = excluded.created_at_epoch
97
+ `).run(
98
+ project, type, sessionId,
99
+ truncate(workingOn, 1000),
100
+ completed.map(c => `[${c.type}] ${c.title}`).join('\n'),
101
+ truncate(unfinished, 3000),
102
+ JSON.stringify([...fileSet].slice(0, 20)),
103
+ decisions.map(d => d.title).join('\n'),
104
+ keywords,
105
+ Date.now()
106
+ );
107
+ }
108
+
109
+ /**
110
+ * Detect if user's prompt indicates continuation of previous work.
111
+ * Stage 1: Explicit keyword match (zero false positives).
112
+ * Stage 2: FTS5-style term overlap with handoff keywords.
113
+ * @param {Database} db Opened main database
114
+ * @param {string} promptText User's prompt text
115
+ * @param {string} project Project identifier
116
+ * @returns {boolean}
117
+ */
118
+ export function detectContinuationIntent(db, promptText, project) {
119
+ // Stage 1: Explicit keyword match — always works, even without handoff
120
+ if (CONTINUE_KEYWORDS.test(promptText)) return true;
121
+
122
+ // Stage 2: FTS5-style term overlap with handoff keywords
123
+ const handoffs = db.prepare(`
124
+ SELECT type, match_keywords, created_at_epoch FROM session_handoffs
125
+ WHERE project = ? ORDER BY created_at_epoch DESC
126
+ `).all(project);
127
+ if (handoffs.length === 0) return false;
128
+
129
+ // Filter expired handoffs
130
+ const now = Date.now();
131
+ const validHandoffs = handoffs.filter(h => {
132
+ const age = now - h.created_at_epoch;
133
+ const maxAge = h.type === 'clear' ? HANDOFF_EXPIRY_CLEAR : HANDOFF_EXPIRY_EXIT;
134
+ return age <= maxAge;
135
+ });
136
+ if (validHandoffs.length === 0) return false;
137
+
138
+ // Use the most recent valid handoff for keyword matching
139
+ const handoff = validHandoffs[0];
140
+ const promptTokens = tokenizeHandoff(promptText);
141
+ const handoffTokens = new Set(tokenizeHandoff(handoff.match_keywords));
142
+
143
+ let score = 0;
144
+ for (const token of promptTokens) {
145
+ if (handoffTokens.has(token)) {
146
+ score += isSpecificTerm(token) ? 2 : 1;
147
+ }
148
+ }
149
+
150
+ return score >= HANDOFF_MATCH_THRESHOLD;
151
+ }
152
+
153
+ /**
154
+ * Render handoff injection text for stdout.
155
+ * Reads the most recent handoff + optional session summary.
156
+ * @param {Database} db Opened main database
157
+ * @param {string} project Project identifier
158
+ * @returns {string|null} Injection text or null if no handoff
159
+ */
160
+ export function renderHandoffInjection(db, project) {
161
+ const now = Date.now();
162
+ // Fetch recent handoffs and find the most recent non-expired one.
163
+ // A newer but expired 'clear' handoff (1h) must not shadow a still-valid 'exit' handoff (7d).
164
+ const handoffs = db.prepare(`
165
+ SELECT * FROM session_handoffs
166
+ WHERE project = ? ORDER BY created_at_epoch DESC LIMIT 5
167
+ `).all(project);
168
+ const handoff = handoffs.find(h => {
169
+ const age = now - h.created_at_epoch;
170
+ const maxAge = h.type === 'clear' ? HANDOFF_EXPIRY_CLEAR : HANDOFF_EXPIRY_EXIT;
171
+ return age <= maxAge;
172
+ });
173
+ if (!handoff) return null;
174
+
175
+ const ageSec = Math.round((Date.now() - handoff.created_at_epoch) / 1000);
176
+ const ageStr = ageSec < 60 ? `${ageSec}s` :
177
+ ageSec < 3600 ? `${Math.round(ageSec / 60)}m` :
178
+ ageSec < 86400 ? `${Math.round(ageSec / 3600)}h` :
179
+ `${Math.round(ageSec / 86400)}d`;
180
+
181
+ const lines = [`<session-handoff source="${handoff.type}" age="${ageStr}">`];
182
+
183
+ if (handoff.working_on) {
184
+ lines.push('## Working On', handoff.working_on, '');
185
+ }
186
+ if (handoff.completed) {
187
+ lines.push('## Completed', ...handoff.completed.split('\n').map(l => `- ${l}`), '');
188
+ }
189
+ if (handoff.unfinished) {
190
+ lines.push('## Unfinished', ...handoff.unfinished.split('; ').map(l => `- ${l}`), '');
191
+ }
192
+ if (handoff.key_files) {
193
+ try {
194
+ const files = JSON.parse(handoff.key_files);
195
+ if (files.length > 0) lines.push('## Key Files', files.map(f => basename(f)).join(', '), '');
196
+ } catch {}
197
+ }
198
+ if (handoff.key_decisions) {
199
+ lines.push('## Key Decisions', ...handoff.key_decisions.split('\n').map(l => `- ${l}`), '');
200
+ }
201
+
202
+ lines.push('</session-handoff>');
203
+
204
+ // Append session summary if available (long-gap enrichment)
205
+ try {
206
+ const summary = db.prepare(`
207
+ SELECT completed, next_steps, remaining_items FROM session_summaries
208
+ WHERE memory_session_id = ? AND project = ?
209
+ ORDER BY created_at_epoch DESC LIMIT 1
210
+ `).get(handoff.session_id, project);
211
+ if (summary && (summary.completed || summary.next_steps || summary.remaining_items)) {
212
+ lines.push('');
213
+ lines.push('<session-summary source="haiku">');
214
+ if (summary.completed) lines.push(summary.completed);
215
+ if (summary.remaining_items) lines.push(`Remaining: ${summary.remaining_items}`);
216
+ if (summary.next_steps) lines.push(`Next steps: ${summary.next_steps}`);
217
+ lines.push('</session-summary>');
218
+ }
219
+ } catch {}
220
+
221
+ return lines.join('\n');
222
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "2.3.0",
3
+ "version": "2.3.1",
4
4
  "description": "Lightweight persistent memory system for Claude Code",
5
5
  "type": "module",
6
6
  "engines": {
@@ -32,10 +32,12 @@
32
32
  "hook-semaphore.mjs",
33
33
  "hook-episode.mjs",
34
34
  "hook-context.mjs",
35
+ "hook-handoff.mjs",
35
36
  "haiku-client.mjs",
36
37
  "dispatch.mjs",
37
38
  "dispatch-inject.mjs",
38
39
  "dispatch-feedback.mjs",
40
+ "dispatch-workflow.mjs",
39
41
  "registry.mjs",
40
42
  "registry-retriever.mjs",
41
43
  "registry-indexer.mjs",
@@ -47,6 +49,9 @@
47
49
  "schema.mjs",
48
50
  "skill.md",
49
51
  "commands/mem.md",
52
+ "commands/memory.md",
53
+ "commands/update.md",
54
+ "commands/tools.md",
50
55
  "hooks/hooks.json",
51
56
  "scripts/launch.mjs",
52
57
  "scripts/setup.sh",