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 +59 -44
- package/bin/cli.mjs +17 -0
- package/bin/install.mjs +61 -10
- package/kyp_mem/cli.py +44 -1
- package/kyp_mem/config.py +1 -1
- package/kyp_mem/hooks.py +133 -106
- package/package.json +1 -1
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
|
-
|
|
34
|
+
### 1. Episodic Memory (Sessions)
|
|
38
35
|
|
|
39
|
-
|
|
36
|
+
Every coding session is automatically captured with full context:
|
|
40
37
|
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
|
|
45
|
+
### 2. Project Intelligence (Vault)
|
|
50
46
|
|
|
51
|
-
|
|
47
|
+
KYP-MEM maintains structured project knowledge as Markdown files with `[[wikilinks]]`:
|
|
52
48
|
|
|
53
|
-
|
|
49
|
+
- Architecture docs, API references, setup guides
|
|
50
|
+
- Known issues, decision history, linked concepts
|
|
54
51
|
|
|
55
|
-
-
|
|
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
|
-
|
|
54
|
+
### How It All Connects
|
|
63
55
|
|
|
64
|
-
1. **
|
|
65
|
-
2. **
|
|
66
|
-
3. **
|
|
67
|
-
4. **
|
|
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
|
|
73
|
-
|
|
74
|
-
pip install kyp-mem (coming soon)
|
|
64
|
+
npm install -g kyp-mem
|
|
75
65
|
```
|
|
76
66
|
|
|
77
|
-
|
|
67
|
+
That's it. The postinstall script automatically:
|
|
78
68
|
|
|
79
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
78
|
+
- Node.js 18+
|
|
79
|
+
- Python 3.10+
|
|
80
|
+
- Claude Code CLI
|
|
81
|
+
- Anthropic API key (for session summarization with Sonnet)
|
|
88
82
|
|
|
89
|
-
|
|
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
|
|
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
|
|
96
|
-
2. **Search Before Acting:** Before investigating bugs or making
|
|
97
|
-
3. **Persist Knowledge:** After fixing a bug or making a decision, it
|
|
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` |
|
|
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(
|
|
57
|
-
console.log(
|
|
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
|
-
|
|
71
|
+
// Step 1: Install Python package
|
|
72
|
+
console.log(` Installing kyp-mem Python package...`);
|
|
64
73
|
|
|
65
|
-
const
|
|
74
|
+
const pipResult = run(
|
|
66
75
|
pythonCommand,
|
|
67
76
|
[...pythonPrefixArgs, "-m", "pip", "install", "--user", "."],
|
|
68
77
|
{ stdio: "inherit" },
|
|
69
78
|
);
|
|
70
79
|
|
|
71
|
-
if (
|
|
72
|
-
console.log(
|
|
73
|
-
}
|
|
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
|
|
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
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
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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("
|
|
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
|
-
|
|
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"""
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
438
|
+
<1-2 sentences>
|
|
446
439
|
|
|
447
440
|
## INVESTIGATED
|
|
448
|
-
|
|
441
|
+
<one paragraph>
|
|
449
442
|
|
|
450
443
|
## LEARNED
|
|
451
|
-
|
|
444
|
+
<one paragraph>
|
|
452
445
|
|
|
453
446
|
## COMPLETED
|
|
454
|
-
|
|
447
|
+
<one paragraph>
|
|
455
448
|
|
|
456
449
|
## NEXT STEPS
|
|
457
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
537
|
-
|
|
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
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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("##
|
|
559
|
-
|
|
560
|
-
raw_parts.
|
|
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("##
|
|
563
|
-
|
|
564
|
-
raw_parts.
|
|
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("##
|
|
567
|
-
|
|
568
|
-
raw_parts.
|
|
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
|
-
#
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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.
|
|
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"
|