kyp-mem 0.5.1 → 0.6.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.
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  KYP-MEM gives AI coding agents two-layer memory:
7
7
 
8
- - **Session Memory(Episodic)** → remembers what happened across coding sessions
8
+ - **Session Memory (Episodic)** → remembers what happened across coding sessions
9
9
 
10
10
  - **Project Intelligence** → understands architecture, decisions, docs, and relationships
11
11
 
@@ -27,74 +27,74 @@ By intercepting the prompt, KYP-MEM automatically provided the agent with:
27
27
  - The vectorized semantic search results of past session logs.
28
28
  - The relevant markdown files from the project knowledge base.
29
29
 
30
-
31
-
32
-
33
- ## How it works
30
+ ## How It Works
34
31
 
35
32
  KYP-MEM operates as a Model Context Protocol (MCP) server that runs silently in the background, integrating directly with Claude Code.
36
33
 
37
- ## Two-Layer Memory System
34
+ ### 1. Episodic Memory (Sessions)
38
35
 
39
- ### 1. Episodic Memory
36
+ Every coding session is automatically captured with full context:
40
37
 
41
- Every coding session is automatically captured:
38
+ - User prompts (what was asked)
39
+ - File reads with content (what was found)
40
+ - File edits with diffs (what changed and why)
41
+ - Command outputs (what happened)
42
42
 
43
- - prompts
44
- - commands
45
- - files changed
46
- - bugs investigated
47
- - decisions made
43
+ At session end, Claude Sonnet synthesizes raw activity into a structured summary with **Summary**, **Investigated**, **Learned**, **Completed**, and **Next Steps** sections. Sessions are semantically searchable via ChromaDB vector embeddings.
48
44
 
49
- Sessions are semantically searchable.
45
+ ### 2. Project Intelligence (Vault)
50
46
 
51
- ### 2. Project Intelligence
47
+ KYP-MEM maintains structured project knowledge as Markdown files with `[[wikilinks]]`:
52
48
 
53
- KYP-MEM maintains structured project knowledge:
49
+ - Architecture docs, API references, setup guides
50
+ - Known issues, decision history, linked concepts
54
51
 
55
- - architecture
56
- - APIs
57
- - setup docs
58
- - known issues
59
- - linked concepts
60
- - decision history
52
+ The agent searches this on-demand via `kyp_search` when it needs project context.
61
53
 
62
- The agent continuously updates this knowledge as it learns.
54
+ ### How It All Connects
63
55
 
64
- 1. **Vault Storage:** Your knowledge base and session logs are stored locally as Markdown files in your `~/.kyp-mem/vault` directory.
65
- 2. **Vector Database:** Session logs are embedded into a local ChromaDB vector database, enabling semantic search ("Find me the session where we debugged the database connection").
66
- 3. **Auto-Learning Hooks:** KYP-MEM hooks into Claude Code's execution lifecycle. It silently listens to prompts, file reads, edits, and terminal commands. When a session ends, it automatically generates a comprehensive summary and timeline using an LLM and saves it to your Vault.
67
- 4. **Agent Tooling:** Claude is equipped with 14 custom MCP tools to read, write, search, and navigate your project's knowledge graph using `[[wikilinks]]`.
56
+ 1. **Session Start:** Recent session summaries are injected automatically the agent knows what happened last time.
57
+ 2. **During Work:** Hooks capture tool activity (reads, edits, commands) with actual content, not just file names.
58
+ 3. **Session End:** Sonnet synthesizes a rich, semantic summary and saves it to the vault + vector DB.
59
+ 4. **Future Sessions:** The agent can search past sessions semantically or look up project knowledge on demand.
68
60
 
69
61
  ## Installation
70
62
 
71
63
  ```bash
72
- npm i kyp-mem
73
-
74
- pip install kyp-mem (coming soon)
64
+ npm install -g kyp-mem
75
65
  ```
76
66
 
77
- ## Setup
67
+ That's it. The postinstall script automatically:
78
68
 
79
- Run the initialization commands to get started:
69
+ 1. Installs the Python package
70
+ 2. Creates the default vault at `~/.kyp-mem/vault`
71
+ 3. Registers the MCP server with Claude Code
72
+ 4. Installs session capture hooks
80
73
 
81
- ```bash
82
- kyp-mem init # Choose where to store your vault
83
- kyp-mem setup-claude # Register the MCP server with Claude Code
84
- kyp-mem install-hooks # Enable automatic session capture (Episodic Memory)
85
- ```
74
+ Restart Claude Code and you're ready to go.
75
+
76
+ ### Requirements
86
77
 
87
- Restart Claude Code. The agent will automatically have access to the memory tools.
78
+ - Node.js 18+
79
+ - Python 3.10+
80
+ - Claude Code CLI
81
+ - Anthropic API key (for session summarization with Sonnet)
88
82
 
89
- *(To enable globally for all projects run: `kyp-mem setup-claude --global` and `kyp-mem install-hooks --global`)*
83
+ ### Custom Vault Path
84
+
85
+ If you want to store your vault somewhere other than `~/.kyp-mem/vault`:
86
+
87
+ ```bash
88
+ kyp-mem init # Interactive prompt to choose vault location
89
+ ```
90
90
 
91
91
  ## The Agent's Workflow
92
92
 
93
- KYP-MEM embeds behavioral instructions directly into its tools. Without any prompting required from you, the agent will automatically:
93
+ KYP-MEM embeds behavioral instructions directly into its tools. Without any prompting from you, the agent will automatically:
94
94
 
95
- 1. **Load Context:** On session start, it loads the project's ground truth (`Knowledge.md`) and recent session summaries.
96
- 2. **Search Before Acting:** Before investigating bugs or making architectural decisions, it searches past episodic memory to avoid repeating work.
97
- 3. **Persist Knowledge:** After fixing a bug or making a decision, it uses its tools to update the project's knowledge base for future sessions.
95
+ 1. **Load Context:** On session start, it loads recent session summaries so it knows what happened last time.
96
+ 2. **Search Before Acting:** Before investigating bugs or making decisions, it searches past sessions to avoid repeating work.
97
+ 3. **Persist Knowledge:** After fixing a bug or making a decision, it updates the project's knowledge base for future sessions.
98
98
 
99
99
  ## Web UI
100
100
 
@@ -109,14 +109,29 @@ kyp-mem ui
109
109
 
110
110
  | Command | Description |
111
111
  |---------|-------------|
112
- | `kyp-mem init` | First-time setup choose vault location |
112
+ | `kyp-mem init` | Choose vault location (default: `~/.kyp-mem/vault`) |
113
113
  | `kyp-mem setup-claude` | Register MCP server with Claude Code |
114
114
  | `kyp-mem install-hooks` | Enable automatic session capture |
115
115
  | `kyp-mem serve` | Start MCP server (stdio, used by the agent) |
116
116
  | `kyp-mem ui` | Open the local web UI |
117
117
  | `kyp-mem stats` | Print vault statistics |
118
118
  | `kyp-mem tree` | Print vault file tree |
119
+ | `kyp-mem config` | View or set configuration (e.g. `kyp-mem config session_model`) |
119
120
  | `kyp-mem doctor` | Check installation and configuration health |
121
+ | `kyp-mem uninstall` | Remove hooks and MCP server from Claude Code |
122
+
123
+ ## Uninstall
124
+
125
+ ```bash
126
+ # Remove from Claude Code (keeps your vault data)
127
+ kyp-mem uninstall
128
+
129
+ # Remove from Claude Code AND delete all data
130
+ kyp-mem uninstall --purge
131
+
132
+ # Remove the npm package
133
+ npm uninstall -g kyp-mem
134
+ ```
120
135
 
121
136
  ## License
122
137
 
package/bin/cli.mjs CHANGED
@@ -84,22 +84,29 @@ if (args[0] === "hook") {
84
84
  if (tool.includes("kyp-mem") || tool.includes("kyp_mem")) process.exit(0);
85
85
 
86
86
  const input = data.tool_input || {};
87
+ const rawResp = data.tool_response || "";
88
+ const resp = (typeof rawResp === "string" ? rawResp : JSON.stringify(rawResp)).slice(0, 2000);
87
89
  const entry = { ts: new Date().toISOString(), tool, cwd: process.cwd() };
88
90
 
89
91
  if (tool === "Edit" || tool === "Write") {
90
92
  entry.file = input.file_path || "";
91
93
  entry.action = tool === "Edit" ? "edit" : "create";
94
+ if (input.old_string) entry.old_string = input.old_string.slice(0, 500);
95
+ if (input.new_string) entry.new_string = input.new_string.slice(0, 500);
92
96
  } else if (tool === "Read") {
93
97
  entry.file = input.file_path || "";
94
98
  entry.action = "read";
99
+ entry.content = resp;
95
100
  } else if (tool === "Bash") {
96
101
  entry.command = (input.command || "").slice(0, 300);
97
102
  entry.action = "command";
103
+ entry.output = resp;
98
104
  } else {
99
105
  entry.action = "other";
100
106
  entry.detail = tool;
101
107
  }
102
108
 
109
+ entry.response_chars = (typeof rawResp === "string" ? rawResp : JSON.stringify(rawResp)).length;
103
110
  mkdirSync(sessionDir, { recursive: true });
104
111
  appendFileSync(sessionFile, JSON.stringify(entry) + "\n");
105
112
  } catch (_) {
@@ -118,6 +125,16 @@ if (args[0] === "hook") {
118
125
  process.exit(0);
119
126
  }
120
127
 
128
+ if (hookType === "session-start") {
129
+ const py = findPython();
130
+ if (py) {
131
+ const [cmd, pre] = py;
132
+ const r = run(cmd, [...pre, "-m", "kyp_mem.cli", "hook", "session-start"], "inherit");
133
+ process.exit(r.status ?? 0);
134
+ }
135
+ process.exit(0);
136
+ }
137
+
121
138
  console.error("Unknown hook type:", hookType);
122
139
  process.exit(1);
123
140
  }
package/bin/install.mjs CHANGED
@@ -1,12 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { spawnSync } from "child_process";
4
+ import { mkdirSync } from "fs";
5
+ import { homedir } from "os";
4
6
  import { fileURLToPath } from "url";
5
- import { dirname, resolve } from "path";
7
+ import { dirname, join, resolve } from "path";
6
8
 
7
9
  const __dirname = dirname(fileURLToPath(import.meta.url));
8
10
  const root = resolve(__dirname, "..");
9
11
 
12
+ const G = "\x1b[32m";
13
+ const Y = "\x1b[33m";
14
+ const C = "\x1b[36m";
15
+ const D = "\x1b[90m";
16
+ const R = "\x1b[0m";
17
+
10
18
  function run(command, args, options = {}) {
11
19
  return spawnSync(command, args, {
12
20
  cwd: root,
@@ -53,25 +61,68 @@ if (process.env.KYP_MEM_SKIP_PYTHON_INSTALL === "1") {
53
61
  const python = findPython();
54
62
 
55
63
  if (!python) {
56
- console.log(" \x1b[33m!\x1b[0m Python 3 was not found.");
57
- console.log(" \x1b[33m!\x1b[0m Install Python 3.10+ and run: python3 -m pip install --user .");
64
+ console.log(` ${Y}!${R} Python 3 was not found.`);
65
+ console.log(` ${Y}!${R} Install Python 3.10+ and run: python3 -m pip install --user .`);
58
66
  process.exit(0);
59
67
  }
60
68
 
61
69
  const [pythonCommand, pythonPrefixArgs] = python;
62
70
 
63
- console.log(" Installing kyp-mem Python package...");
71
+ // Step 1: Install Python package
72
+ console.log(` Installing kyp-mem Python package...`);
64
73
 
65
- const result = run(
74
+ const pipResult = run(
66
75
  pythonCommand,
67
76
  [...pythonPrefixArgs, "-m", "pip", "install", "--user", "."],
68
77
  { stdio: "inherit" },
69
78
  );
70
79
 
71
- if (result.status === 0) {
72
- console.log(" \x1b[32m✓\x1b[0m kyp-mem installed successfully");
73
- } else {
74
- console.log(" \x1b[33m!\x1b[0m Could not auto-install the Python package.");
75
- console.log(` \x1b[33m!\x1b[0m Run manually from ${root}:`);
80
+ if (pipResult.status !== 0) {
81
+ console.log(` ${Y}!${R} Could not auto-install the Python package.`);
82
+ console.log(` ${Y}!${R} Run manually from ${root}:`);
76
83
  console.log(" python3 -m pip install --user .");
84
+ process.exit(0);
85
+ }
86
+
87
+ console.log(` ${G}✓${R} Python package installed`);
88
+
89
+ // Step 2: Create default vault directory
90
+ const vaultDir = join(homedir(), ".kyp-mem", "vault");
91
+ try {
92
+ mkdirSync(vaultDir, { recursive: true });
93
+ console.log(` ${G}✓${R} Vault ready at ${D}${vaultDir}${R}`);
94
+ } catch (_) {
95
+ console.log(` ${Y}!${R} Could not create vault at ${vaultDir}`);
77
96
  }
97
+
98
+ // Step 3: Register MCP server with Claude Code (global)
99
+ const setupResult = run(
100
+ pythonCommand,
101
+ [...pythonPrefixArgs, "-m", "kyp_mem.cli", "setup-claude", "--global"],
102
+ { stdio: "inherit" },
103
+ );
104
+
105
+ if (setupResult.status === 0) {
106
+ console.log(` ${G}✓${R} MCP server registered with Claude Code`);
107
+ } else {
108
+ console.log(` ${Y}!${R} Could not register MCP server — run manually: ${C}kyp-mem setup-claude --global${R}`);
109
+ }
110
+
111
+ // Step 4: Install hooks (global)
112
+ const hooksResult = run(
113
+ pythonCommand,
114
+ [...pythonPrefixArgs, "-m", "kyp_mem.cli", "install-hooks", "--global"],
115
+ { stdio: "inherit" },
116
+ );
117
+
118
+ if (hooksResult.status === 0) {
119
+ console.log(` ${G}✓${R} Session capture hooks installed`);
120
+ } else {
121
+ console.log(` ${Y}!${R} Could not install hooks — run manually: ${C}kyp-mem install-hooks --global${R}`);
122
+ }
123
+
124
+ console.log();
125
+ console.log(` ${C}KYP-MEM${R} is ready! Restart Claude Code to activate.`);
126
+ console.log(` ${D}Vault: ${vaultDir}${R}`);
127
+ console.log(` ${D}To customize vault path: kyp-mem init${R}`);
128
+ console.log();
package/kyp_mem/cli.py CHANGED
@@ -41,6 +41,8 @@ def main():
41
41
  help="Add hooks to global ~/.claude/settings.json (default: project)")
42
42
  ih.add_argument("--remove", action="store_true", help="Remove KYP-MEM hooks")
43
43
 
44
+ un = subparsers.add_parser("uninstall", help="Remove KYP-MEM from Claude Code (hooks + MCP server)")
45
+ un.add_argument("--purge", action="store_true", help="Also delete vault data and config at ~/.kyp-mem")
44
46
  subparsers.add_parser("doctor", help="Check installation and config health")
45
47
 
46
48
  cfg_parser = subparsers.add_parser("config", help="Get or set configuration values")
@@ -77,6 +79,8 @@ def main():
77
79
  _run_install_hooks(global_config=args.global_config, remove=args.remove)
78
80
  elif args.command == "config":
79
81
  _run_config(args.key, args.value)
82
+ elif args.command == "uninstall":
83
+ _run_uninstall(purge=args.purge)
80
84
  elif args.command == "doctor":
81
85
  _run_doctor()
82
86
  elif args.command == "hook":
@@ -267,7 +271,46 @@ def _write_legacy_claude_settings(
267
271
  return settings_path
268
272
 
269
273
 
270
- def _run_install_hooks(global_config: bool = False, remove: bool = False):
274
+ def _run_uninstall(purge: bool = False):
275
+ from .config import CONFIG_DIR
276
+
277
+ print()
278
+ print(f" {C}KYP-MEM{R} — Uninstall")
279
+ print()
280
+
281
+ # Remove hooks from global settings
282
+ _run_install_hooks(global_config=True, remove=True)
283
+
284
+ # Remove MCP server from Claude Code
285
+ claude_bin = shutil.which("claude")
286
+ if claude_bin:
287
+ subprocess.run(
288
+ [claude_bin, "mcp", "remove", "-s", "user", "kyp-mem"],
289
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, text=True,
290
+ )
291
+ print(f" {G}✓{R} MCP server removed from Claude Code")
292
+ else:
293
+ print(f" {Y}!{R} 'claude' CLI not found — remove kyp-mem MCP server manually")
294
+
295
+ if purge:
296
+ import shutil as sh
297
+ if CONFIG_DIR.exists():
298
+ sh.rmtree(CONFIG_DIR)
299
+ print(f" {G}✓{R} Deleted {CONFIG_DIR} (vault, config, sessions)")
300
+ else:
301
+ print(f" {D} {CONFIG_DIR} does not exist{R}")
302
+
303
+ print()
304
+ print(f" To finish, remove the npm package:")
305
+ print(f" {Y}npm uninstall -g kyp-mem{R}")
306
+ print()
307
+ if not purge:
308
+ print(f" {D}Your vault data at {CONFIG_DIR} was kept.{R}")
309
+ print(f" {D}To delete it too: kyp-mem uninstall --purge{R}")
310
+ print()
311
+
312
+
313
+
271
314
  mcp_command, _ = _get_mcp_command()
272
315
 
273
316
  if global_config:
package/kyp_mem/config.py CHANGED
@@ -34,4 +34,4 @@ def get_vault_path() -> str:
34
34
 
35
35
  def get_session_model() -> str:
36
36
  config = load_config()
37
- return config.get("session_model", "claude-haiku-4-5-20251001")
37
+ return config.get("session_model", "claude-sonnet-4-6")
package/kyp_mem/hooks.py CHANGED
@@ -74,7 +74,7 @@ def _record_injection(project, chars):
74
74
 
75
75
 
76
76
  def handle_session_start():
77
- """Inject project context into the conversation at session start."""
77
+ """Inject recent session memory into the conversation at session start."""
78
78
  sys.stdin.read()
79
79
 
80
80
  cwd = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
@@ -90,58 +90,31 @@ def handle_session_start():
90
90
  if not project_notes:
91
91
  return
92
92
 
93
- parts = [f"# [kyp-mem] {project_name} — Project Context"]
94
- parts.append(f"Vault: {get_vault_path()}")
95
- parts.append("")
96
-
97
- knowledge_path = f"{project_name}/Knowledge.md"
98
- knowledge = vault.read(knowledge_path)
99
- if knowledge:
100
- parts.append("## Knowledge")
101
- content = knowledge.content
102
- timeline_idx = content.find("## Timeline")
103
- if timeline_idx > 0:
104
- content = content[:timeline_idx].strip()
105
- if len(content) > 2000:
106
- parts.append(content[:2000] + "\n...")
107
- else:
108
- parts.append(content)
109
- parts.append("")
110
-
111
- other_notes = sorted(
112
- p for p in project_notes
113
- if "/Sessions/" not in p and p != knowledge_path
114
- )
115
- if other_notes:
116
- parts.append("## Project Notes")
117
- for p in other_notes:
118
- note = vault.index.notes.get(p)
119
- title = note.title if note else p
120
- tags = f" [{', '.join(note.tags)}]" if note and note.tags else ""
121
- parts.append(f"- {title} ({p}){tags}")
122
- parts.append("")
123
-
124
93
  sessions = sorted(
125
94
  (p for p in project_notes if "/Sessions/" in p),
126
95
  reverse=True,
127
96
  )[:3]
128
- if sessions:
129
- parts.append(f"## Recent Sessions (last {len(sessions)})")
130
- for sp in sessions:
131
- note = vault.read(sp)
132
- if not note:
133
- continue
134
- parts.append(f"### {note.title}")
135
- content = note.content
136
- timeline_idx = content.find("## Timeline")
137
- if timeline_idx > 0:
138
- content = content[:timeline_idx].strip()
139
- if len(content) > 300:
140
- content = content[:300] + "..."
141
- parts.append(content)
142
- parts.append("")
97
+ if not sessions:
98
+ return
99
+
100
+ parts = [f"# [kyp-mem] {project_name} — Recent Sessions"]
101
+ parts.append(f"Use `kyp_search` or `kyp_project_context` for architecture/project knowledge on demand.")
102
+ parts.append("")
143
103
 
144
- parts.append("Use `kyp_project_context` for full details. Use `kyp_session_search` to search past sessions.")
104
+ parts.append(f"## Last {len(sessions)} Sessions")
105
+ for sp in sessions:
106
+ note = vault.read(sp)
107
+ if not note:
108
+ continue
109
+ parts.append(f"### {note.title}")
110
+ content = note.content
111
+ timeline_idx = content.find("## TIMELINE")
112
+ if timeline_idx < 0:
113
+ timeline_idx = content.find("## Timeline")
114
+ if timeline_idx > 0:
115
+ content = content[:timeline_idx].strip()
116
+ parts.append(content)
117
+ parts.append("")
145
118
 
146
119
  output = "\n".join(parts)
147
120
  try:
@@ -195,13 +168,25 @@ def handle_post_tool_use():
195
168
  cwd = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
196
169
  entry["cwd"] = cwd
197
170
 
198
- # Measure response size for token economics
199
171
  tool_response = data.get("tool_response", "")
200
- response_chars = len(str(tool_response)) if tool_response else 0
172
+ if isinstance(tool_response, str):
173
+ resp_str = tool_response
174
+ elif tool_response:
175
+ resp_str = json.dumps(tool_response)
176
+ else:
177
+ resp_str = ""
178
+ response_chars = len(resp_str)
179
+ resp_truncated = resp_str[:2000]
201
180
 
202
181
  if tool_name == "Edit":
203
182
  entry["action"] = "edit"
204
183
  entry["file"] = tool_input.get("file_path", "")
184
+ old_s = tool_input.get("old_string", "")
185
+ new_s = tool_input.get("new_string", "")
186
+ if old_s:
187
+ entry["old_string"] = old_s[:500]
188
+ if new_s:
189
+ entry["new_string"] = new_s[:500]
205
190
  elif tool_name == "Write":
206
191
  entry["action"] = "create"
207
192
  entry["file"] = tool_input.get("file_path", "")
@@ -214,10 +199,12 @@ def handle_post_tool_use():
214
199
  except OSError:
215
200
  pass
216
201
  entry["response_chars"] = response_chars
202
+ entry["content"] = resp_truncated
217
203
  elif tool_name == "Bash":
218
204
  entry["action"] = "command"
219
205
  entry["command"] = tool_input.get("command", "")
220
206
  entry["response_chars"] = response_chars
207
+ entry["output"] = resp_truncated
221
208
  else:
222
209
  return
223
210
 
@@ -426,35 +413,41 @@ def _summarize_with_claude(raw_note, project_name):
426
413
  model = get_session_model()
427
414
  client = anthropic.Anthropic()
428
415
 
429
- prompt = f"""Summarize this coding session for "{project_name}" in plain English. A future AI agent will read this to understand what happenedwrite for that audience.
416
+ prompt = f"""Rewrite this raw coding session into a structured summary. A future AI agent reads this to pick up where you left off be precise and technical.
430
417
 
431
- You have: user prompts (what was asked), a timeline of file edits/reads/commands (what happened), and raw section data. Synthesize these into a coherent narrative.
418
+ You have: user prompts (the objectives), a timeline of file edits/reads/commands, and raw section data. Synthesize into a dense, specific narrative.
432
419
 
433
- Rules:
434
- - Summary: 2-3 sentences. State the objective (from prompts), what was done, and the outcome. Be specific: "Fixed navigation bug where clicking sessions broke the back button" not "Modified files and ran commands."
435
- - INVESTIGATED: What was explored and WHY. "Examined the session hook pipeline to understand why summaries were empty" not "Searched for `session-view`". Max 4 bullets.
436
- - LEARNED: Insights or discoveries. "The config CLI command was defined but never wired to the dispatcher" not "Investigated and modified: `cli.py`". Max 4 bullets.
437
- - COMPLETED: Concrete deliverables. "Added AI-powered session summarization using Claude Haiku" not "Modified `hooks.py`". Max 5 bullets.
438
- - NEXT STEPS: What should happen next session. Infer from context — unfinished work, unfixed bugs, natural follow-ups. Max 3 bullets.
420
+ ## Format rules
439
421
 
440
- NEVER include raw grep patterns, CSS class names, file paths, or command output. Write like you're telling a teammate what you did today.
422
+ - **Summary**: 1-2 sentences. State what was done and the outcome. Include error messages, feature names, or bug descriptions verbatim. Example: 'Debugged and fixed "Unknown hook type: session-start" error in kyp-mem; cleaned repository of session-specific files and prepared for release'
423
+ - **INVESTIGATED**: One dense paragraph (not bullets). List specific files, paths, and systems examined with semicolons. Include full relative paths and module names. Example: 'Global and project-level Claude Code settings.json; kyp-mem Python CLI source (cli.py, hooks.py); installed Node.js wrapper at /opt/homebrew/lib/node_modules/kyp-mem/bin/cli.mjs; hook dispatcher implementation; git commit history'
424
+ - **LEARNED**: One dense paragraph (not bullets). State technical insights with specifics — what was discovered, why it matters, root causes. Include version numbers, commit hashes, config values, error messages. Example: 'kyp-mem uses a Node.js wrapper with a "hook fast path" dispatcher that only handled 3 hook types (user-prompt, post-tool-use, stop); session-start was missing despite being implemented in Python backend'
425
+ - **COMPLETED**: One dense paragraph (not bullets). List concrete deliverables with specifics — file names modified, features added, tests passed, counts, commit hashes. Use semicolons to separate items. Example: 'Fixed .gitignore to exclude session-specific files (CLAUDE.md, PLAN-ui-rewrite.md, templates/); removed 3 tracked files from git history; committed cleanup to main (commit f0b114e: 4 files changed, 626 deletions)'
426
+ - **NEXT STEPS**: One dense paragraph (not bullets). Concrete actionable items for the next session. Example: 'Push commit f0b114e to GitHub; publish 0.5.1 release to npm with session-start hook support'
427
+
428
+ ## Critical rules
429
+ - ALWAYS include specific file names, paths, commit hashes, error messages, and counts
430
+ - Write dense paragraphs with semicolons, NOT bullet lists
431
+ - Never be vague: "Fixed 3 files" is bad, "Fixed .gitignore, cli.mjs, and hooks.py" is good
432
+ - If a commit hash appears in the timeline, include it
433
+ - Keep each section to one paragraph max
441
434
 
442
435
  Return ONLY this format (no preamble):
443
436
 
444
437
  ## Summary
445
- <text>
438
+ <1-2 sentences>
446
439
 
447
440
  ## INVESTIGATED
448
- - <item>
441
+ <one paragraph>
449
442
 
450
443
  ## LEARNED
451
- - <item>
444
+ <one paragraph>
452
445
 
453
446
  ## COMPLETED
454
- - <item>
447
+ <one paragraph>
455
448
 
456
449
  ## NEXT STEPS
457
- - <item>
450
+ <one paragraph>
458
451
 
459
452
  Raw session data:
460
453
  {raw_note}"""
@@ -503,7 +496,7 @@ def handle_stop():
503
496
  files_created = set()
504
497
  commands = []
505
498
  prompts = []
506
- timeline = []
499
+ events = []
507
500
 
508
501
  for e in entries:
509
502
  ts_raw = e.get("ts", "")
@@ -512,69 +505,96 @@ def handle_stop():
512
505
 
513
506
  if action == "prompt":
514
507
  prompts.append({"ts": ts, "text": e.get("prompt", "")})
515
- timeline.append(f" {ts} Prompt: {e.get('prompt', '')[:60]}...")
508
+ events.append({"ts": ts, "type": "prompt", "text": e.get("prompt", "")[:500]})
516
509
  elif action == "read":
517
510
  fp = e.get("file", "")
518
511
  files_read.add(fp)
519
- timeline.append(f" {ts} — Read `{Path(fp).name}`")
512
+ content = e.get("content", "")
513
+ events.append({"ts": ts, "type": "read", "file": fp, "content": content[:1000] if content else ""})
520
514
  elif action == "edit":
521
515
  fp = e.get("file", "")
522
516
  files_edited.add(fp)
523
- timeline.append(f" {ts} — Edit `{Path(fp).name}`")
517
+ events.append({
518
+ "ts": ts, "type": "edit", "file": fp,
519
+ "old": e.get("old_string", "")[:300],
520
+ "new": e.get("new_string", "")[:300],
521
+ })
524
522
  elif action == "create":
525
523
  fp = e.get("file", "")
526
524
  files_created.add(fp)
527
- timeline.append(f" {ts} Write `{Path(fp).name}`")
525
+ events.append({"ts": ts, "type": "create", "file": fp})
528
526
  elif action == "command":
529
527
  cmd = e.get("command", "")
528
+ output = e.get("output", "")
530
529
  commands.append(cmd)
531
- short = cmd[:80] + "..." if len(cmd) > 80 else cmd
532
- timeline.append(f" {ts} — `{short}`")
530
+ events.append({"ts": ts, "type": "command", "cmd": cmd[:300], "output": output[:1000] if output else ""})
533
531
 
534
532
  commands_classified = [_classify_command(cmd) for cmd in commands]
535
533
 
536
- summary_items = []
537
- if files_edited:
538
- summary_items.append(f"Modified {len(files_edited)} file{'s' if len(files_edited) != 1 else ''}")
539
- if files_created:
540
- summary_items.append(f"Created {len(files_created)} file{'s' if len(files_created) != 1 else ''}")
541
- if commands:
542
- summary_items.append(f"Ran {len(commands)} command{'s' if len(commands) != 1 else ''}")
534
+ # Build rich context for Sonnet — actual content, not just filenames
535
+ raw_parts = []
543
536
 
544
- investigated = _build_investigated(files_read, commands_classified, project_dir)
545
- learned = _build_learned(files_read, files_edited, files_created, commands_classified, project_dir)
546
- completed = _build_completed(files_edited, files_created, commands_classified, project_dir)
547
- next_steps = _build_next_steps(files_edited, files_created, commands_classified)
537
+ if prompts:
538
+ raw_parts.append("## USER PROMPTS (the objectives)")
539
+ for p in prompts:
540
+ raw_parts.append(f"[{p['ts']}] {p['text'][:500]}")
541
+ raw_parts.append("")
542
+
543
+ raw_parts.append("## SESSION EVENTS (chronological, with content)")
544
+ for ev in events:
545
+ if ev["type"] == "prompt":
546
+ raw_parts.append(f"\n### [{ev['ts']}] User asked:")
547
+ raw_parts.append(ev["text"])
548
+ elif ev["type"] == "read":
549
+ raw_parts.append(f"\n### [{ev['ts']}] Read `{ev['file']}`")
550
+ if ev.get("content"):
551
+ raw_parts.append(f"```\n{ev['content']}\n```")
552
+ elif ev["type"] == "edit":
553
+ raw_parts.append(f"\n### [{ev['ts']}] Edited `{ev['file']}`")
554
+ if ev.get("old"):
555
+ raw_parts.append(f"Replaced:\n```\n{ev['old']}\n```")
556
+ if ev.get("new"):
557
+ raw_parts.append(f"With:\n```\n{ev['new']}\n```")
558
+ elif ev["type"] == "create":
559
+ raw_parts.append(f"\n### [{ev['ts']}] Created `{ev['file']}`")
560
+ elif ev["type"] == "command":
561
+ raw_parts.append(f"\n### [{ev['ts']}] Ran: `{ev['cmd']}`")
562
+ if ev.get("output"):
563
+ raw_parts.append(f"Output:\n```\n{ev['output']}\n```")
548
564
 
549
- # Build raw note for Claude summarization
550
- raw_parts = []
551
- raw_parts.append("## Summary")
552
- raw_parts.append(", ".join(summary_items) + f" in `{project_name}`." if summary_items else "")
553
- raw_parts.append("")
554
- raw_parts.append("## INVESTIGATED")
555
- if investigated:
556
- raw_parts.extend(investigated)
557
565
  raw_parts.append("")
558
- raw_parts.append("## LEARNED")
559
- if learned:
560
- raw_parts.extend(learned)
566
+ raw_parts.append("## FILES MODIFIED")
567
+ for fp in sorted(files_edited):
568
+ raw_parts.append(f"- {_relative_path(fp, project_dir)}")
561
569
  raw_parts.append("")
562
- raw_parts.append("## COMPLETED")
563
- if completed:
564
- raw_parts.extend(completed)
570
+ raw_parts.append("## FILES CREATED")
571
+ for fp in sorted(files_created):
572
+ raw_parts.append(f"- {_relative_path(fp, project_dir)}")
565
573
  raw_parts.append("")
566
- raw_parts.append("## NEXT STEPS")
567
- if next_steps:
568
- raw_parts.extend(next_steps)
574
+ raw_parts.append("## FILES READ")
575
+ for fp in sorted(files_read):
576
+ raw_parts.append(f"- {_relative_path(fp, project_dir)}")
569
577
 
570
- # Prompts go FIRST they define the session's objective
571
- if prompts:
572
- raw_parts.insert(0, "## USER PROMPTS (what was asked)")
573
- for i, p in enumerate(prompts):
574
- raw_parts.insert(i + 1, f"- [{p['ts']}] {p['text'][:300]}")
575
- raw_parts.insert(len(prompts) + 1, "")
578
+ # Keep timeline for backward compat in case summarization fails
579
+ timeline = []
580
+ for ev in events:
581
+ if ev["type"] == "prompt":
582
+ timeline.append(f" {ev['ts']} — Prompt: {ev['text'][:60]}...")
583
+ elif ev["type"] == "read":
584
+ timeline.append(f" {ev['ts']} — Read `{Path(ev['file']).name}`")
585
+ elif ev["type"] == "edit":
586
+ timeline.append(f" {ev['ts']} — Edit `{Path(ev['file']).name}`")
587
+ elif ev["type"] == "create":
588
+ timeline.append(f" {ev['ts']} — Write `{Path(ev['file']).name}`")
589
+ elif ev["type"] == "command":
590
+ short = ev["cmd"][:80] + "..." if len(ev["cmd"]) > 80 else ev["cmd"]
591
+ timeline.append(f" {ev['ts']} — `{short}`")
592
+
593
+ investigated = _build_investigated(files_read, commands_classified, project_dir)
594
+ learned = _build_learned(files_read, files_edited, files_created, commands_classified, project_dir)
595
+ completed = _build_completed(files_edited, files_created, commands_classified, project_dir)
596
+ next_steps = _build_next_steps(files_edited, files_created, commands_classified)
576
597
 
577
- # Full timeline gives Claude the narrative arc
578
598
  if timeline:
579
599
  raw_parts.append("")
580
600
  raw_parts.append("## TIMELINE (what happened, chronological)")
@@ -639,6 +659,13 @@ def handle_stop():
639
659
  parts.append("")
640
660
  parts.append(summarized)
641
661
  else:
662
+ summary_items = []
663
+ if files_edited:
664
+ summary_items.append(f"Modified {len(files_edited)} file{'s' if len(files_edited) != 1 else ''}")
665
+ if files_created:
666
+ summary_items.append(f"Created {len(files_created)} file{'s' if len(files_created) != 1 else ''}")
667
+ if commands:
668
+ summary_items.append(f"Ran {len(commands)} command{'s' if len(commands) != 1 else ''}")
642
669
  parts.append("## Summary")
643
670
  parts.append(", ".join(summary_items) + f" in `{project_name}`." if summary_items else "")
644
671
  parts.append("")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kyp-mem",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "Know Your Project — Persistent & Session level knowledge base for AI agents. MCP-powered with wikilinks, backlinks, auto-learning, and neon web UI.",
5
5
  "bin": {
6
6
  "kyp-mem": "bin/cli.mjs"