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.
- package/commands/memory.md +51 -0
- package/commands/tools.md +78 -0
- package/commands/update.md +37 -0
- package/dispatch-workflow.mjs +155 -0
- package/hook-handoff.mjs +222 -0
- package/package.json +6 -1
|
@@ -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
|
+
}
|
package/hook-handoff.mjs
ADDED
|
@@ -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.
|
|
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",
|