kyp-mem 0.4.1 → 0.4.3
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 +227 -78
- package/bin/cli.mjs +20 -0
- package/kyp_mem/__init__.py +1 -1
- package/kyp_mem/cli.py +31 -5
- package/kyp_mem/hooks.py +286 -19
- package/kyp_mem/server.py +17 -6
- package/kyp_mem/static/index.html +59 -23
- package/package.json +2 -2
- package/pyproject.toml +1 -1
package/README.md
CHANGED
|
@@ -1,140 +1,289 @@
|
|
|
1
1
|
# KYP-MEM — Know Your Project Memory
|
|
2
2
|
|
|
3
|
-
**Persistent
|
|
3
|
+
**Persistent memory for AI coding agents.** Your AI agent forgets everything between sessions. KYP-MEM fixes that.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
KYP-MEM is an MCP server that gives your AI agent a knowledge base and session memory. It remembers your architecture, past bugs, decisions, and what happened last session — so you never repeat yourself.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## How It Works
|
|
10
|
+
|
|
11
|
+
KYP-MEM gives your agent two types of memory:
|
|
12
|
+
|
|
13
|
+
### Knowledge Base — long-term project memory
|
|
14
|
+
|
|
15
|
+
Structured markdown notes organized by project. Architecture docs, API references, known bugs, key decisions, setup guides. The agent reads these before doing any work and updates them as it learns.
|
|
6
16
|
|
|
7
|
-
```bash
|
|
8
|
-
npm install -g kyp-mem
|
|
9
17
|
```
|
|
18
|
+
MyProject/
|
|
19
|
+
├── Knowledge.md # Architecture, bugs, decisions, notes
|
|
20
|
+
├── API.md # Endpoints and contracts
|
|
21
|
+
├── Setup.md # Environment setup guide
|
|
22
|
+
└── Sessions/
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Session Memory — what happened each session
|
|
10
26
|
|
|
11
|
-
|
|
27
|
+
Every coding session is automatically captured — files changed, commands run, prompts used. These notes are embedded into a vector database for semantic search, so the agent can find past work by meaning, not just keywords.
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
MyProject/Sessions/
|
|
31
|
+
├── 2026-05-12_143022.md # "Fixed auth bug, found rate limiter issue"
|
|
32
|
+
├── 2026-05-12_091544.md # "Investigated flaky tests — race condition"
|
|
33
|
+
└── 2026-05-11_162301.md # "Set up CI, decided on GitHub Actions"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### The Loop
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
Session Start
|
|
40
|
+
→ Agent loads project knowledge + recent sessions
|
|
41
|
+
→ Agent is grounded: knows architecture, past bugs, last session's next steps
|
|
42
|
+
|
|
43
|
+
During Work
|
|
44
|
+
→ Agent hits a bug → searches session memory → finds it was already investigated
|
|
45
|
+
→ Agent makes a decision → updates Knowledge.md so future sessions know
|
|
46
|
+
|
|
47
|
+
Session End
|
|
48
|
+
→ Hooks auto-capture everything into a structured session note
|
|
49
|
+
→ Note is embedded into the vector store for future semantic search
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Install
|
|
12
55
|
|
|
13
56
|
```bash
|
|
14
|
-
|
|
57
|
+
pip install kyp-mem
|
|
15
58
|
```
|
|
16
59
|
|
|
17
60
|
## Setup
|
|
18
61
|
|
|
19
62
|
```bash
|
|
20
|
-
kyp-mem init
|
|
21
|
-
kyp-mem setup-claude
|
|
22
|
-
kyp-mem install-hooks
|
|
63
|
+
kyp-mem init # Choose where to store your vault
|
|
64
|
+
kyp-mem setup-claude # Register MCP server with Claude Code
|
|
65
|
+
kyp-mem install-hooks # Enable automatic session capture
|
|
23
66
|
```
|
|
24
67
|
|
|
25
|
-
Restart Claude Code.
|
|
68
|
+
Restart Claude Code. KYP-MEM runs automatically with 14 tools available to the agent.
|
|
26
69
|
|
|
27
|
-
|
|
70
|
+
### Quick Setup (one command)
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
claude mcp add -s user -e KYP_VAULT="$HOME/.kyp-mem/vault" kyp-mem -- kyp-mem serve
|
|
74
|
+
```
|
|
28
75
|
|
|
29
|
-
|
|
76
|
+
### Enable for All Projects
|
|
30
77
|
|
|
31
78
|
```bash
|
|
79
|
+
kyp-mem setup-claude --global
|
|
32
80
|
kyp-mem install-hooks --global
|
|
33
81
|
```
|
|
34
82
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## What the Agent Does Automatically
|
|
86
|
+
|
|
87
|
+
KYP-MEM embeds behavioral instructions directly into tool descriptions. The agent follows these rules without any user action:
|
|
88
|
+
|
|
89
|
+
1. **Session start** — loads project knowledge + recent session history
|
|
90
|
+
2. **Before investigating bugs** — searches session memory first to avoid duplicate work
|
|
91
|
+
3. **Before making decisions** — checks if a prior session already decided this
|
|
92
|
+
4. **After fixing or learning something** — updates Knowledge.md for future sessions
|
|
93
|
+
5. **Never hallucinates** — if it's not in the knowledge base, it says so
|
|
94
|
+
|
|
95
|
+
No prompting needed. The agent reads the tool descriptions and follows the protocol.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Automatic Session Capture
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
kyp-mem install-hooks
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Installs three Claude Code hooks:
|
|
106
|
+
|
|
107
|
+
- **UserPromptSubmit** — captures every prompt you send to the agent
|
|
108
|
+
- **PostToolUse** — captures file edits, writes, and shell commands in real-time
|
|
109
|
+
- **Stop** — when the session ends, compiles all activity into a structured note
|
|
110
|
+
|
|
111
|
+
Each session note looks like this:
|
|
112
|
+
|
|
113
|
+
```markdown
|
|
114
|
+
# Session 2026-05-12_143022
|
|
115
|
+
|
|
116
|
+
**Project:** MyProject
|
|
117
|
+
**Actions:** 15 total, 12 substantive
|
|
118
|
+
|
|
119
|
+
## Summary
|
|
120
|
+
Fixed auth bug, refactored token refresh logic.
|
|
121
|
+
|
|
122
|
+
## PROMPTS
|
|
123
|
+
### 1. [14:25:01]
|
|
124
|
+
> fix the token refresh bug in auth.py
|
|
125
|
+
|
|
126
|
+
### 2. [14:30:22]
|
|
127
|
+
> also add retry logic for expired tokens
|
|
128
|
+
|
|
129
|
+
## INVESTIGATED
|
|
130
|
+
- grep for "token expired" across auth module
|
|
131
|
+
- read OAuth provider docs
|
|
132
|
+
|
|
133
|
+
## LEARNED
|
|
134
|
+
- Refresh tokens expire after 30 days, not 90
|
|
135
|
+
|
|
136
|
+
## COMPLETED
|
|
137
|
+
- Fixed token refresh in auth.py
|
|
138
|
+
- Added retry logic for expired tokens
|
|
139
|
+
|
|
140
|
+
## NEXT STEPS
|
|
141
|
+
- Add integration test for token refresh flow
|
|
142
|
+
```
|
|
38
143
|
|
|
39
144
|
Sessions with fewer than 3 substantive actions are automatically skipped.
|
|
40
145
|
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Semantic Search
|
|
149
|
+
|
|
150
|
+
Session notes are embedded into a vector database (ChromaDB). This enables search by meaning:
|
|
151
|
+
|
|
152
|
+
- Searching **"authentication failing"** finds sessions about "login bug" and "OAuth token expiry"
|
|
153
|
+
- Searching **"deploy process"** finds sessions about "CI pipeline setup" and "release workflow"
|
|
154
|
+
|
|
155
|
+
The agent doesn't need exact keywords — it finds semantically related past work.
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
41
159
|
## Web UI
|
|
42
160
|
|
|
43
161
|
```bash
|
|
44
162
|
kyp-mem ui
|
|
45
163
|
```
|
|
46
164
|
|
|
47
|
-
Opens at `localhost:3333` with:
|
|
48
|
-
- Quick switcher (`Cmd+O`) — fuzzy jump to any note
|
|
49
|
-
- Full-text search (`Cmd+K`)
|
|
50
|
-
- Tag filtering — clickable tag cloud, AND-filter
|
|
51
|
-
- Outline panel — heading TOC with click-to-scroll
|
|
52
|
-
- Backlink context — shows the surrounding line
|
|
53
|
-
- Unlinked mentions — finds references without `[[wikilinks]]`
|
|
54
|
-
- Inline editing — edit notes directly in the browser (`Cmd+S`)
|
|
55
|
-
- Local graph view — D3 force-directed graph of connections
|
|
56
|
-
- Resizable panels, collapsible tree, rendered markdown
|
|
165
|
+
Opens at `localhost:3333` with two panels:
|
|
57
166
|
|
|
58
|
-
|
|
167
|
+
**Session Memory** — semantic search across all sessions, grouped by project with timestamps
|
|
59
168
|
|
|
60
|
-
|
|
61
|
-
┌─────────────┐ ┌─────────────┐ ┌──────────────┐
|
|
62
|
-
│ Claude Code │──stdio──▶│ kyp-mem │──read/──▶│ ~/.kyp-mem/ │
|
|
63
|
-
│ (any AI) │◀─────────│ MCP server │ write │ vault/ │
|
|
64
|
-
└─────────────┘ └─────────────┘ │ *.md files │
|
|
65
|
-
└──────────────┘
|
|
66
|
-
```
|
|
169
|
+
**Knowledge Base** — file tree with folders, notes, tags. Full-text search, tag filtering, quick switcher (`Cmd+O`)
|
|
67
170
|
|
|
68
|
-
|
|
69
|
-
- **Markdown on disk** — plain `.md` files with YAML frontmatter, no database
|
|
70
|
-
- **In-memory index** — wikilinks, backlinks, tags, word-level search index
|
|
71
|
-
- **Lightweight reads** — brief mode by default (~100 tokens), full content opt-in
|
|
72
|
-
- **Graph navigation** — follow `[[links]]` instead of searching broadly
|
|
171
|
+
**Editor** — rendered markdown with `[[wikilink]]` navigation, inline editing
|
|
73
172
|
|
|
74
|
-
|
|
173
|
+
**Graph** — knowledge graph showing connections between notes, backlinks, and related content
|
|
75
174
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
|
83
|
-
|
|
84
|
-
| `
|
|
85
|
-
| `
|
|
86
|
-
| `kyp-mem tree` | Print vault tree |
|
|
87
|
-
| `kyp-mem doctor` | Check installation health |
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## MCP Tools (14 total)
|
|
178
|
+
|
|
179
|
+
### Agent Behavior
|
|
180
|
+
|
|
181
|
+
| Tool | Purpose |
|
|
182
|
+
|------|---------|
|
|
183
|
+
| `____kyp_instructions` | Embeds behavioral rules the agent follows automatically |
|
|
184
|
+
| `kyp_project_context` | Loads project knowledge + recent sessions at session start |
|
|
88
185
|
|
|
89
|
-
|
|
186
|
+
### Knowledge Base
|
|
90
187
|
|
|
91
|
-
| Tool |
|
|
92
|
-
|
|
93
|
-
| `kyp_list` | Browse folders and notes
|
|
94
|
-
| `kyp_read` |
|
|
95
|
-
| `kyp_write` | Create or update a note with tags and
|
|
188
|
+
| Tool | Purpose |
|
|
189
|
+
|------|---------|
|
|
190
|
+
| `kyp_list` | Browse folders and notes |
|
|
191
|
+
| `kyp_read` | Read a note (summary by default, `full=True` for complete) |
|
|
192
|
+
| `kyp_write` | Create or update a note with tags and `[[wikilinks]]` |
|
|
96
193
|
| `kyp_delete` | Delete a note |
|
|
97
194
|
| `kyp_search` | Full-text search with optional tag filter |
|
|
98
195
|
| `kyp_tags` | List all tags or filter notes by tag |
|
|
99
|
-
| `kyp_related` | Find related notes by links, tags,
|
|
196
|
+
| `kyp_related` | Find related notes by links, tags, proximity |
|
|
100
197
|
| `kyp_recent` | Recently modified notes |
|
|
101
198
|
| `kyp_stats` | Vault statistics |
|
|
102
199
|
|
|
103
|
-
|
|
200
|
+
### Session Memory
|
|
201
|
+
|
|
202
|
+
| Tool | Purpose |
|
|
203
|
+
|------|---------|
|
|
204
|
+
| `kyp_session_search` | Semantic search across all session logs |
|
|
205
|
+
| `kyp_session_create` | Manually create a session note |
|
|
206
|
+
| `kyp_sessions` | List sessions by project |
|
|
104
207
|
|
|
105
|
-
```markdown
|
|
106
|
-
---
|
|
107
|
-
tags: [project, trading, config]
|
|
108
|
-
created: 2026-05-12
|
|
109
208
|
---
|
|
110
209
|
|
|
111
|
-
|
|
210
|
+
## Adding to Your Project
|
|
112
211
|
|
|
113
|
-
|
|
114
|
-
```
|
|
212
|
+
Add a `CLAUDE.md` to any project root:
|
|
115
213
|
|
|
116
|
-
|
|
214
|
+
```markdown
|
|
215
|
+
# Project Memory
|
|
216
|
+
|
|
217
|
+
## Session Start (MANDATORY)
|
|
218
|
+
Call `kyp_project_context("PROJECT_NAME")` at the start of every session to load:
|
|
219
|
+
- Project knowledge base (architecture, bugs, decisions)
|
|
220
|
+
- Recent session history (what was done, what's next)
|
|
221
|
+
|
|
222
|
+
## During Work
|
|
223
|
+
- Before investigating bugs: `kyp_session_search("error or symptom")`
|
|
224
|
+
- Before making decisions: `kyp_session_search("topic")`
|
|
225
|
+
- After fixing bugs: update Knowledge.md via `kyp_write`
|
|
226
|
+
- After decisions: add to Key Decisions in Knowledge.md
|
|
227
|
+
|
|
228
|
+
## Rules
|
|
229
|
+
- Never hallucinate project details — check the knowledge base first.
|
|
230
|
+
- Use [[wikilinks]] to connect related notes.
|
|
231
|
+
- Sessions are captured automatically — no manual logging needed.
|
|
232
|
+
```
|
|
117
233
|
|
|
118
|
-
|
|
234
|
+
A template is available at `templates/CLAUDE.md.template`.
|
|
119
235
|
|
|
120
|
-
|
|
121
|
-
claude mcp add -s user -e KYP_VAULT="$HOME/.kyp-mem/vault" kyp-mem -- npx -y kyp-mem serve
|
|
122
|
-
```
|
|
236
|
+
---
|
|
123
237
|
|
|
124
|
-
##
|
|
238
|
+
## Vault Structure
|
|
125
239
|
|
|
126
240
|
```
|
|
127
241
|
~/.kyp-mem/
|
|
128
|
-
├── config.json
|
|
129
|
-
├──
|
|
242
|
+
├── config.json # Vault path configuration
|
|
243
|
+
├── chroma/ # Vector database for semantic search
|
|
130
244
|
└── vault/
|
|
131
|
-
├──
|
|
132
|
-
│ ├──
|
|
133
|
-
│
|
|
134
|
-
|
|
245
|
+
├── ProjectA/
|
|
246
|
+
│ ├── Knowledge.md # Ground truth: architecture, bugs, decisions
|
|
247
|
+
│ ├── API.md
|
|
248
|
+
│ └── Sessions/
|
|
249
|
+
│ ├── 2026-05-12_143022.md
|
|
250
|
+
│ └── 2026-05-11_091544.md
|
|
251
|
+
├── ProjectB/
|
|
252
|
+
│ ├── Knowledge.md
|
|
253
|
+
│ └── Sessions/
|
|
135
254
|
└── ...
|
|
136
255
|
```
|
|
137
256
|
|
|
257
|
+
Notes use YAML frontmatter for tags and `[[wikilinks]]` for cross-references:
|
|
258
|
+
|
|
259
|
+
```markdown
|
|
260
|
+
---
|
|
261
|
+
tags: [trading, config]
|
|
262
|
+
created: 2026-05-12
|
|
263
|
+
---
|
|
264
|
+
# Configuration
|
|
265
|
+
Settings are in `HedgeConfig`. See [[Risk Management]] for limits.
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## CLI Commands
|
|
271
|
+
|
|
272
|
+
| Command | What it does |
|
|
273
|
+
|---------|-------------|
|
|
274
|
+
| `kyp-mem init` | First-time setup — choose vault location |
|
|
275
|
+
| `kyp-mem setup-claude` | Register MCP server with Claude Code |
|
|
276
|
+
| `kyp-mem setup-claude --global` | Register globally (all projects) |
|
|
277
|
+
| `kyp-mem install-hooks` | Enable automatic session capture |
|
|
278
|
+
| `kyp-mem install-hooks --remove` | Remove session capture hooks |
|
|
279
|
+
| `kyp-mem serve` | Start MCP server (stdio, used by the agent) |
|
|
280
|
+
| `kyp-mem ui` | Open web UI at localhost:3333 |
|
|
281
|
+
| `kyp-mem stats` | Print vault statistics |
|
|
282
|
+
| `kyp-mem tree` | Print vault tree |
|
|
283
|
+
| `kyp-mem doctor` | Check installation health |
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
138
287
|
## License
|
|
139
288
|
|
|
140
289
|
MIT
|
package/bin/cli.mjs
CHANGED
|
@@ -60,6 +60,23 @@ if (args[0] === "hook") {
|
|
|
60
60
|
await new Promise((r) => process.stdin.on("end", r));
|
|
61
61
|
const raw = Buffer.concat(chunks).toString();
|
|
62
62
|
|
|
63
|
+
if (hookType === "user-prompt") {
|
|
64
|
+
try {
|
|
65
|
+
const data = JSON.parse(raw);
|
|
66
|
+
const prompt = (data.prompt || "").trim();
|
|
67
|
+
if (!prompt) process.exit(0);
|
|
68
|
+
const entry = {
|
|
69
|
+
ts: new Date().toISOString(),
|
|
70
|
+
cwd: process.env.CLAUDE_PROJECT_DIR || process.cwd(),
|
|
71
|
+
action: "prompt",
|
|
72
|
+
prompt,
|
|
73
|
+
};
|
|
74
|
+
mkdirSync(sessionDir, { recursive: true });
|
|
75
|
+
appendFileSync(sessionFile, JSON.stringify(entry) + "\n");
|
|
76
|
+
} catch (_) {}
|
|
77
|
+
process.exit(0);
|
|
78
|
+
}
|
|
79
|
+
|
|
63
80
|
if (hookType === "post-tool-use") {
|
|
64
81
|
try {
|
|
65
82
|
const data = JSON.parse(raw);
|
|
@@ -72,6 +89,9 @@ if (args[0] === "hook") {
|
|
|
72
89
|
if (tool === "Edit" || tool === "Write") {
|
|
73
90
|
entry.file = input.file_path || "";
|
|
74
91
|
entry.action = tool === "Edit" ? "edit" : "create";
|
|
92
|
+
} else if (tool === "Read") {
|
|
93
|
+
entry.file = input.file_path || "";
|
|
94
|
+
entry.action = "read";
|
|
75
95
|
} else if (tool === "Bash") {
|
|
76
96
|
entry.command = (input.command || "").slice(0, 300);
|
|
77
97
|
entry.action = "command";
|
package/kyp_mem/__init__.py
CHANGED
package/kyp_mem/cli.py
CHANGED
|
@@ -43,6 +43,12 @@ def main():
|
|
|
43
43
|
|
|
44
44
|
subparsers.add_parser("doctor", help="Check installation and config health")
|
|
45
45
|
|
|
46
|
+
hook_parser = subparsers.add_parser("hook", help="Handle Claude Code hook events (internal)")
|
|
47
|
+
hook_sub = hook_parser.add_subparsers(dest="hook_command")
|
|
48
|
+
hook_sub.add_parser("post-tool-use", help="Capture tool activity to session log")
|
|
49
|
+
hook_sub.add_parser("user-prompt", help="Capture user prompt to session log")
|
|
50
|
+
hook_sub.add_parser("stop", help="Compile session into vault note")
|
|
51
|
+
|
|
46
52
|
args = parser.parse_args()
|
|
47
53
|
|
|
48
54
|
if args.vault:
|
|
@@ -66,6 +72,14 @@ def main():
|
|
|
66
72
|
_run_install_hooks(global_config=args.global_config, remove=args.remove)
|
|
67
73
|
elif args.command == "doctor":
|
|
68
74
|
_run_doctor()
|
|
75
|
+
elif args.command == "hook":
|
|
76
|
+
from .hooks import handle_post_tool_use, handle_user_prompt, handle_stop
|
|
77
|
+
if args.hook_command == "post-tool-use":
|
|
78
|
+
handle_post_tool_use()
|
|
79
|
+
elif args.hook_command == "user-prompt":
|
|
80
|
+
handle_user_prompt()
|
|
81
|
+
elif args.hook_command == "stop":
|
|
82
|
+
handle_stop()
|
|
69
83
|
else:
|
|
70
84
|
_print_banner()
|
|
71
85
|
parser.print_help()
|
|
@@ -264,11 +278,17 @@ def _run_install_hooks(global_config: bool = False, remove: bool = False):
|
|
|
264
278
|
|
|
265
279
|
hooks = settings.setdefault("hooks", {})
|
|
266
280
|
|
|
281
|
+
def _has_kyp_hook(entry):
|
|
282
|
+
for hook in entry.get("hooks", []):
|
|
283
|
+
if "kyp-mem hook" in hook.get("command", ""):
|
|
284
|
+
return True
|
|
285
|
+
return "kyp-mem hook" in entry.get("command", "")
|
|
286
|
+
|
|
267
287
|
if remove:
|
|
268
288
|
changed = False
|
|
269
|
-
for event in ("PostToolUse", "Stop"):
|
|
289
|
+
for event in ("PostToolUse", "UserPromptSubmit", "Stop"):
|
|
270
290
|
if event in hooks:
|
|
271
|
-
hooks[event] = [h for h in hooks[event] if
|
|
291
|
+
hooks[event] = [h for h in hooks[event] if not _has_kyp_hook(h)]
|
|
272
292
|
if not hooks[event]:
|
|
273
293
|
del hooks[event]
|
|
274
294
|
changed = True
|
|
@@ -282,20 +302,26 @@ def _run_install_hooks(global_config: bool = False, remove: bool = False):
|
|
|
282
302
|
return
|
|
283
303
|
|
|
284
304
|
post_tool_hooks = hooks.setdefault("PostToolUse", [])
|
|
305
|
+
prompt_hooks = hooks.setdefault("UserPromptSubmit", [])
|
|
285
306
|
stop_hooks = hooks.setdefault("Stop", [])
|
|
286
307
|
|
|
287
|
-
post_tool_hooks = [h for h in post_tool_hooks if
|
|
288
|
-
|
|
308
|
+
post_tool_hooks = [h for h in post_tool_hooks if not _has_kyp_hook(h)]
|
|
309
|
+
prompt_hooks = [h for h in prompt_hooks if not _has_kyp_hook(h)]
|
|
310
|
+
stop_hooks = [h for h in stop_hooks if not _has_kyp_hook(h)]
|
|
289
311
|
|
|
290
312
|
post_tool_hooks.append({
|
|
291
|
-
"matcher": "Edit|Write|Bash",
|
|
313
|
+
"matcher": "Edit|Write|Read|Bash",
|
|
292
314
|
"hooks": [{"type": "command", "command": f"{mcp_command} hook post-tool-use"}],
|
|
293
315
|
})
|
|
316
|
+
prompt_hooks.append({
|
|
317
|
+
"hooks": [{"type": "command", "command": f"{mcp_command} hook user-prompt"}],
|
|
318
|
+
})
|
|
294
319
|
stop_hooks.append({
|
|
295
320
|
"hooks": [{"type": "command", "command": f"{mcp_command} hook stop"}],
|
|
296
321
|
})
|
|
297
322
|
|
|
298
323
|
hooks["PostToolUse"] = post_tool_hooks
|
|
324
|
+
hooks["UserPromptSubmit"] = prompt_hooks
|
|
299
325
|
hooks["Stop"] = stop_hooks
|
|
300
326
|
|
|
301
327
|
settings_path.write_text(json.dumps(settings, indent=2) + "\n")
|
package/kyp_mem/hooks.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""KYP-MEM session hooks — compile captured tool activity into vault notes."""
|
|
2
2
|
|
|
3
|
+
import os
|
|
3
4
|
import sys
|
|
4
5
|
import json
|
|
5
6
|
from datetime import datetime
|
|
@@ -11,6 +12,259 @@ CURRENT_SESSION = SESSION_DIR / "current.jsonl"
|
|
|
11
12
|
MIN_ACTIONS = 3
|
|
12
13
|
|
|
13
14
|
|
|
15
|
+
def handle_user_prompt():
|
|
16
|
+
raw = sys.stdin.read().strip()
|
|
17
|
+
if not raw:
|
|
18
|
+
return
|
|
19
|
+
try:
|
|
20
|
+
data = json.loads(raw)
|
|
21
|
+
except json.JSONDecodeError:
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
prompt = data.get("prompt", "").strip()
|
|
25
|
+
if not prompt:
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
entry = {
|
|
29
|
+
"ts": datetime.now().isoformat(),
|
|
30
|
+
"cwd": os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd()),
|
|
31
|
+
"action": "prompt",
|
|
32
|
+
"prompt": prompt,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
SESSION_DIR.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
with open(CURRENT_SESSION, "a") as f:
|
|
37
|
+
f.write(json.dumps(entry) + "\n")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def handle_post_tool_use():
|
|
41
|
+
raw = sys.stdin.read().strip()
|
|
42
|
+
if not raw:
|
|
43
|
+
return
|
|
44
|
+
try:
|
|
45
|
+
data = json.loads(raw)
|
|
46
|
+
except json.JSONDecodeError:
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
tool_name = data.get("tool_name", "")
|
|
50
|
+
tool_input = data.get("tool_input", {})
|
|
51
|
+
|
|
52
|
+
entry = {"ts": datetime.now().isoformat()}
|
|
53
|
+
|
|
54
|
+
cwd = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
|
|
55
|
+
entry["cwd"] = cwd
|
|
56
|
+
|
|
57
|
+
if tool_name == "Edit":
|
|
58
|
+
entry["action"] = "edit"
|
|
59
|
+
entry["file"] = tool_input.get("file_path", "")
|
|
60
|
+
elif tool_name == "Write":
|
|
61
|
+
entry["action"] = "create"
|
|
62
|
+
entry["file"] = tool_input.get("file_path", "")
|
|
63
|
+
elif tool_name == "Read":
|
|
64
|
+
entry["action"] = "read"
|
|
65
|
+
entry["file"] = tool_input.get("file_path", "")
|
|
66
|
+
elif tool_name == "Bash":
|
|
67
|
+
entry["action"] = "command"
|
|
68
|
+
entry["command"] = tool_input.get("command", "")
|
|
69
|
+
else:
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
SESSION_DIR.mkdir(parents=True, exist_ok=True)
|
|
73
|
+
with open(CURRENT_SESSION, "a") as f:
|
|
74
|
+
f.write(json.dumps(entry) + "\n")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _relative_path(filepath, project_dir):
|
|
78
|
+
try:
|
|
79
|
+
return str(Path(filepath).relative_to(project_dir))
|
|
80
|
+
except (ValueError, TypeError):
|
|
81
|
+
return Path(filepath).name if filepath else ""
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _group_files_by_dir(filepaths, project_dir):
|
|
85
|
+
groups = {}
|
|
86
|
+
for fp in sorted(filepaths):
|
|
87
|
+
rel = _relative_path(fp, project_dir)
|
|
88
|
+
parent = str(Path(rel).parent)
|
|
89
|
+
if parent == ".":
|
|
90
|
+
parent = "(root)"
|
|
91
|
+
groups.setdefault(parent, []).append(Path(rel).name)
|
|
92
|
+
return groups
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _classify_command(cmd):
|
|
96
|
+
if not cmd.strip():
|
|
97
|
+
return "other", ""
|
|
98
|
+
first = cmd.strip().split()[0]
|
|
99
|
+
search_cmds = {'grep', 'rg', 'ag', 'ack'}
|
|
100
|
+
explore_cmds = {'find', 'fd', 'ls', 'tree', 'du'}
|
|
101
|
+
read_cmds = {'cat', 'head', 'tail', 'less', 'more', 'wc', 'file'}
|
|
102
|
+
diff_cmds = {'diff', 'git diff', 'git log', 'git status', 'git show'}
|
|
103
|
+
test_cmds = {'pytest', 'python -m pytest', 'npm test', 'jest', 'mocha', 'cargo test', 'go test'}
|
|
104
|
+
build_cmds = {'npm run', 'npm install', 'pip install', 'make', 'cargo build', 'go build'}
|
|
105
|
+
server_cmds = {'python3 -m', 'uvicorn', 'node', 'npm start', 'flask run'}
|
|
106
|
+
git_cmds = {'git commit', 'git push', 'git add', 'git checkout', 'git branch', 'git merge', 'git stash'}
|
|
107
|
+
|
|
108
|
+
if first in search_cmds or cmd.strip().startswith('git grep'):
|
|
109
|
+
return "search", cmd
|
|
110
|
+
if first in explore_cmds:
|
|
111
|
+
return "explore", cmd
|
|
112
|
+
if first in read_cmds:
|
|
113
|
+
return "read_cmd", cmd
|
|
114
|
+
for prefix in diff_cmds:
|
|
115
|
+
if cmd.strip().startswith(prefix):
|
|
116
|
+
return "git_inspect", cmd
|
|
117
|
+
for prefix in test_cmds:
|
|
118
|
+
if cmd.strip().startswith(prefix):
|
|
119
|
+
return "test", cmd
|
|
120
|
+
for prefix in build_cmds:
|
|
121
|
+
if cmd.strip().startswith(prefix):
|
|
122
|
+
return "build", cmd
|
|
123
|
+
for prefix in git_cmds:
|
|
124
|
+
if cmd.strip().startswith(prefix):
|
|
125
|
+
return "git_write", cmd
|
|
126
|
+
for prefix in server_cmds:
|
|
127
|
+
if cmd.strip().startswith(prefix):
|
|
128
|
+
return "run", cmd
|
|
129
|
+
if first == 'curl':
|
|
130
|
+
return "api_test", cmd
|
|
131
|
+
return "other", cmd
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _build_investigated(files_read, commands_classified, project_dir):
|
|
135
|
+
items = []
|
|
136
|
+
seen_files = set()
|
|
137
|
+
|
|
138
|
+
search_cmds = [cmd for cls, cmd in commands_classified if cls == "search"]
|
|
139
|
+
for cmd in search_cmds[:8]:
|
|
140
|
+
parts = cmd.strip().split()
|
|
141
|
+
if len(parts) >= 2:
|
|
142
|
+
pattern = None
|
|
143
|
+
for i, p in enumerate(parts):
|
|
144
|
+
if not p.startswith('-') and i > 0:
|
|
145
|
+
pattern = p.strip("'\"")
|
|
146
|
+
break
|
|
147
|
+
if pattern:
|
|
148
|
+
items.append(f"- Searched for `{pattern[:60]}`")
|
|
149
|
+
else:
|
|
150
|
+
items.append(f"- `{cmd[:100]}`")
|
|
151
|
+
|
|
152
|
+
read_groups = _group_files_by_dir(files_read, project_dir)
|
|
153
|
+
for dir_name, filenames in read_groups.items():
|
|
154
|
+
unique = [f for f in filenames if f not in seen_files]
|
|
155
|
+
seen_files.update(unique)
|
|
156
|
+
if not unique:
|
|
157
|
+
continue
|
|
158
|
+
if len(unique) <= 3:
|
|
159
|
+
items.append(f"- Read {', '.join(f'`{f}`' for f in unique)}")
|
|
160
|
+
else:
|
|
161
|
+
items.append(f"- Read {len(unique)} files in `{dir_name}/`")
|
|
162
|
+
|
|
163
|
+
explore_cmds = [cmd for cls, cmd in commands_classified if cls == "explore"]
|
|
164
|
+
if explore_cmds:
|
|
165
|
+
items.append(f"- Explored project structure ({len(explore_cmds)} commands)")
|
|
166
|
+
|
|
167
|
+
git_cmds = [cmd for cls, cmd in commands_classified if cls == "git_inspect"]
|
|
168
|
+
if git_cmds:
|
|
169
|
+
items.append(f"- Inspected git history/diff ({len(git_cmds)} commands)")
|
|
170
|
+
|
|
171
|
+
api_cmds = [cmd for cls, cmd in commands_classified if cls == "api_test"]
|
|
172
|
+
if api_cmds:
|
|
173
|
+
items.append(f"- Tested API endpoints ({len(api_cmds)} requests)")
|
|
174
|
+
|
|
175
|
+
return items
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _build_learned(files_read, files_edited, files_created, commands_classified, project_dir):
|
|
179
|
+
items = []
|
|
180
|
+
read_set = {Path(f).name for f in files_read}
|
|
181
|
+
edit_set = {Path(f).name for f in files_edited}
|
|
182
|
+
create_set = {Path(f).name for f in files_created}
|
|
183
|
+
|
|
184
|
+
investigated_then_modified = read_set & edit_set
|
|
185
|
+
if investigated_then_modified:
|
|
186
|
+
names = sorted(investigated_then_modified)[:5]
|
|
187
|
+
items.append(f"- Investigated and modified: {', '.join(f'`{n}`' for n in names)}")
|
|
188
|
+
|
|
189
|
+
config_files = {f for f in (files_edited | files_created)
|
|
190
|
+
if any(f.endswith(ext) for ext in
|
|
191
|
+
('.json', '.toml', '.yaml', '.yml', '.ini', '.cfg', '.env', '.conf'))}
|
|
192
|
+
if config_files:
|
|
193
|
+
items.append(f"- Configuration changes: {', '.join(f'`{Path(f).name}`' for f in sorted(config_files)[:4])}")
|
|
194
|
+
|
|
195
|
+
test_cmds = [cmd for cls, cmd in commands_classified if cls == "test"]
|
|
196
|
+
if test_cmds:
|
|
197
|
+
items.append(f"- Ran tests ({len(test_cmds)} run{'s' if len(test_cmds) != 1 else ''})")
|
|
198
|
+
|
|
199
|
+
new_only = create_set - read_set
|
|
200
|
+
if new_only:
|
|
201
|
+
items.append(f"- Created new files: {', '.join(f'`{n}`' for n in sorted(new_only)[:5])}")
|
|
202
|
+
|
|
203
|
+
return items
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _build_completed(files_edited, files_created, commands_classified, project_dir):
|
|
207
|
+
items = []
|
|
208
|
+
|
|
209
|
+
edit_groups = _group_files_by_dir(files_edited, project_dir)
|
|
210
|
+
for dir_name, filenames in edit_groups.items():
|
|
211
|
+
if len(filenames) == 1:
|
|
212
|
+
items.append(f"- Modified `{filenames[0]}`")
|
|
213
|
+
else:
|
|
214
|
+
items.append(f"- Modified {len(filenames)} files in `{dir_name}/`: {', '.join(f'`{f}`' for f in filenames[:5])}")
|
|
215
|
+
|
|
216
|
+
create_groups = _group_files_by_dir(files_created, project_dir)
|
|
217
|
+
for dir_name, filenames in create_groups.items():
|
|
218
|
+
if len(filenames) == 1:
|
|
219
|
+
items.append(f"- Created `{filenames[0]}`")
|
|
220
|
+
else:
|
|
221
|
+
items.append(f"- Created {len(filenames)} files in `{dir_name}/`")
|
|
222
|
+
|
|
223
|
+
test_cmds = [cmd for cls, cmd in commands_classified if cls == "test"]
|
|
224
|
+
if test_cmds:
|
|
225
|
+
items.append(f"- Ran test suite")
|
|
226
|
+
|
|
227
|
+
git_writes = [cmd for cls, cmd in commands_classified if cls == "git_write"]
|
|
228
|
+
for cmd in git_writes:
|
|
229
|
+
if 'commit' in cmd:
|
|
230
|
+
items.append("- Committed changes to git")
|
|
231
|
+
break
|
|
232
|
+
for cmd in git_writes:
|
|
233
|
+
if 'push' in cmd:
|
|
234
|
+
items.append("- Pushed to remote")
|
|
235
|
+
break
|
|
236
|
+
|
|
237
|
+
build_cmds = [cmd for cls, cmd in commands_classified if cls == "build"]
|
|
238
|
+
if build_cmds:
|
|
239
|
+
items.append("- Ran build/install")
|
|
240
|
+
|
|
241
|
+
run_cmds = [cmd for cls, cmd in commands_classified if cls == "run"]
|
|
242
|
+
if run_cmds:
|
|
243
|
+
items.append("- Started/tested server")
|
|
244
|
+
|
|
245
|
+
return items
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _build_next_steps(files_edited, files_created, commands_classified):
|
|
249
|
+
items = []
|
|
250
|
+
|
|
251
|
+
git_writes = {cmd for cls, cmd in commands_classified if cls == "git_write"}
|
|
252
|
+
has_commit = any('commit' in cmd for cmd in git_writes)
|
|
253
|
+
has_push = any('push' in cmd for cmd in git_writes)
|
|
254
|
+
|
|
255
|
+
all_changed = files_edited | files_created
|
|
256
|
+
if all_changed and not has_commit:
|
|
257
|
+
items.append("- Commit pending changes")
|
|
258
|
+
if has_commit and not has_push:
|
|
259
|
+
items.append("- Push committed changes to remote")
|
|
260
|
+
|
|
261
|
+
test_cmds = [cmd for cls, cmd in commands_classified if cls == "test"]
|
|
262
|
+
if not test_cmds and all_changed:
|
|
263
|
+
items.append("- Run tests to verify changes")
|
|
264
|
+
|
|
265
|
+
return items
|
|
266
|
+
|
|
267
|
+
|
|
14
268
|
def handle_stop():
|
|
15
269
|
if not CURRENT_SESSION.exists():
|
|
16
270
|
return
|
|
@@ -40,9 +294,11 @@ def handle_stop():
|
|
|
40
294
|
project_name = Path(project_dir).name
|
|
41
295
|
session_id = datetime.now().strftime("%Y-%m-%d_%H%M%S")
|
|
42
296
|
|
|
297
|
+
files_read = set()
|
|
43
298
|
files_edited = set()
|
|
44
299
|
files_created = set()
|
|
45
300
|
commands = []
|
|
301
|
+
prompts = []
|
|
46
302
|
timeline = []
|
|
47
303
|
|
|
48
304
|
for e in entries:
|
|
@@ -50,7 +306,14 @@ def handle_stop():
|
|
|
50
306
|
ts = ts_raw[11:19] if len(ts_raw) >= 19 else ""
|
|
51
307
|
action = e.get("action", "")
|
|
52
308
|
|
|
53
|
-
if action == "
|
|
309
|
+
if action == "prompt":
|
|
310
|
+
prompts.append({"ts": ts, "text": e.get("prompt", "")})
|
|
311
|
+
timeline.append(f" {ts} — Prompt: {e.get('prompt', '')[:60]}...")
|
|
312
|
+
elif action == "read":
|
|
313
|
+
fp = e.get("file", "")
|
|
314
|
+
files_read.add(fp)
|
|
315
|
+
timeline.append(f" {ts} — Read `{Path(fp).name}`")
|
|
316
|
+
elif action == "edit":
|
|
54
317
|
fp = e.get("file", "")
|
|
55
318
|
files_edited.add(fp)
|
|
56
319
|
timeline.append(f" {ts} — Edit `{Path(fp).name}`")
|
|
@@ -64,6 +327,8 @@ def handle_stop():
|
|
|
64
327
|
short = cmd[:80] + "..." if len(cmd) > 80 else cmd
|
|
65
328
|
timeline.append(f" {ts} — `{short}`")
|
|
66
329
|
|
|
330
|
+
commands_classified = [_classify_command(cmd) for cmd in commands]
|
|
331
|
+
|
|
67
332
|
summary_items = []
|
|
68
333
|
if files_edited:
|
|
69
334
|
summary_items.append(f"Modified {len(files_edited)} file{'s' if len(files_edited) != 1 else ''}")
|
|
@@ -72,12 +337,10 @@ def handle_stop():
|
|
|
72
337
|
if commands:
|
|
73
338
|
summary_items.append(f"Ran {len(commands)} command{'s' if len(commands) != 1 else ''}")
|
|
74
339
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if first_word in investigate_keywords:
|
|
80
|
-
investigated_cmds.append(cmd)
|
|
340
|
+
investigated = _build_investigated(files_read, commands_classified, project_dir)
|
|
341
|
+
learned = _build_learned(files_read, files_edited, files_created, commands_classified, project_dir)
|
|
342
|
+
completed = _build_completed(files_edited, files_created, commands_classified, project_dir)
|
|
343
|
+
next_steps = _build_next_steps(files_edited, files_created, commands_classified)
|
|
81
344
|
|
|
82
345
|
parts = [f"# Session {session_id}", ""]
|
|
83
346
|
parts.append(f"**Project:** `{project_dir}`")
|
|
@@ -88,28 +351,32 @@ def handle_stop():
|
|
|
88
351
|
parts.append(", ".join(summary_items) + f" in `{project_name}`." if summary_items else "")
|
|
89
352
|
parts.append("")
|
|
90
353
|
|
|
354
|
+
parts.append("## PROMPTS")
|
|
355
|
+
if prompts:
|
|
356
|
+
for i, p in enumerate(prompts, 1):
|
|
357
|
+
parts.append(f"### {i}. [{p['ts']}]")
|
|
358
|
+
parts.append(f"> {p['text']}")
|
|
359
|
+
parts.append("")
|
|
360
|
+
parts.append("")
|
|
361
|
+
|
|
91
362
|
parts.append("## INVESTIGATED")
|
|
92
|
-
if
|
|
93
|
-
|
|
94
|
-
short = cmd[:120] + "..." if len(cmd) > 120 else cmd
|
|
95
|
-
parts.append(f"- `{short}`")
|
|
363
|
+
if investigated:
|
|
364
|
+
parts.extend(investigated)
|
|
96
365
|
parts.append("")
|
|
97
366
|
|
|
98
367
|
parts.append("## LEARNED")
|
|
99
|
-
|
|
368
|
+
if learned:
|
|
369
|
+
parts.extend(learned)
|
|
100
370
|
parts.append("")
|
|
101
371
|
|
|
102
372
|
parts.append("## COMPLETED")
|
|
103
|
-
if
|
|
104
|
-
|
|
105
|
-
parts.append(f"- Modified `{Path(f).name}`")
|
|
106
|
-
if files_created:
|
|
107
|
-
for f in sorted(files_created):
|
|
108
|
-
parts.append(f"- Created `{Path(f).name}`")
|
|
373
|
+
if completed:
|
|
374
|
+
parts.extend(completed)
|
|
109
375
|
parts.append("")
|
|
110
376
|
|
|
111
377
|
parts.append("## NEXT STEPS")
|
|
112
|
-
|
|
378
|
+
if next_steps:
|
|
379
|
+
parts.extend(next_steps)
|
|
113
380
|
parts.append("")
|
|
114
381
|
|
|
115
382
|
if timeline:
|
package/kyp_mem/server.py
CHANGED
|
@@ -324,11 +324,20 @@ def kyp_project_context(project: str) -> str:
|
|
|
324
324
|
"""CALL THIS AT SESSION START. Returns the project's full context: Knowledge.md (ground truth), project notes, and recent session summaries. Use this to understand architecture, known bugs, past decisions, and what was done recently. This prevents hallucination and avoids repeating past work."""
|
|
325
325
|
parts = []
|
|
326
326
|
|
|
327
|
+
MAX_KNOWLEDGE_CHARS = 3000
|
|
328
|
+
MAX_NOTE_PREVIEW_LINES = 6
|
|
329
|
+
MAX_SESSION_CHARS = 400
|
|
330
|
+
|
|
327
331
|
knowledge_path = f"{project}/Knowledge.md"
|
|
328
332
|
knowledge = vault.read(knowledge_path)
|
|
329
333
|
if knowledge:
|
|
330
334
|
parts.append("=== PROJECT KNOWLEDGE ===")
|
|
331
|
-
|
|
335
|
+
content = knowledge.content
|
|
336
|
+
if len(content) > MAX_KNOWLEDGE_CHARS:
|
|
337
|
+
parts.append(content[:MAX_KNOWLEDGE_CHARS])
|
|
338
|
+
parts.append(f"\n... (truncated — use kyp_read(\"{knowledge_path}\", full=True) for complete content)")
|
|
339
|
+
else:
|
|
340
|
+
parts.append(content)
|
|
332
341
|
parts.append("")
|
|
333
342
|
|
|
334
343
|
project_notes = []
|
|
@@ -341,9 +350,9 @@ def kyp_project_context(project: str) -> str:
|
|
|
341
350
|
for path, note in sorted(project_notes):
|
|
342
351
|
parts.append(f"\n--- {note.title} ({path}) ---")
|
|
343
352
|
preview = note.content.strip().split("\n")
|
|
344
|
-
parts.append("\n".join(preview[:
|
|
345
|
-
if len(preview) >
|
|
346
|
-
parts.append("...")
|
|
353
|
+
parts.append("\n".join(preview[:MAX_NOTE_PREVIEW_LINES]))
|
|
354
|
+
if len(preview) > MAX_NOTE_PREVIEW_LINES:
|
|
355
|
+
parts.append(f"... (use kyp_read(\"{path}\", full=True) for complete content)")
|
|
347
356
|
parts.append("")
|
|
348
357
|
|
|
349
358
|
sessions = []
|
|
@@ -360,9 +369,11 @@ def kyp_project_context(project: str) -> str:
|
|
|
360
369
|
content = note.content
|
|
361
370
|
timeline_idx = content.find("## Timeline")
|
|
362
371
|
if timeline_idx > 0:
|
|
363
|
-
|
|
372
|
+
content = content[:timeline_idx].strip()
|
|
373
|
+
if len(content) > MAX_SESSION_CHARS:
|
|
374
|
+
parts.append(content[:MAX_SESSION_CHARS] + "...")
|
|
364
375
|
else:
|
|
365
|
-
parts.append(content
|
|
376
|
+
parts.append(content)
|
|
366
377
|
parts.append("")
|
|
367
378
|
|
|
368
379
|
if not parts:
|
|
@@ -1573,29 +1573,52 @@ body.resizing .resize-handle { pointer-events: auto !important; }
|
|
|
1573
1573
|
color: var(--text-secondary);
|
|
1574
1574
|
}
|
|
1575
1575
|
|
|
1576
|
-
/* ============ BACKLINK
|
|
1577
|
-
.rp-item
|
|
1578
|
-
font-size: 10px;
|
|
1579
|
-
color: var(--text-muted);
|
|
1580
|
-
margin-top: 2px;
|
|
1581
|
-
line-height: 1.4;
|
|
1582
|
-
overflow: hidden;
|
|
1583
|
-
text-overflow: ellipsis;
|
|
1584
|
-
white-space: nowrap;
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
|
-
.rp-item-col {
|
|
1576
|
+
/* ============ BACKLINK & UNLINKED ITEMS ============ */
|
|
1577
|
+
.rp-link-item {
|
|
1588
1578
|
display: flex;
|
|
1589
|
-
|
|
1590
|
-
padding:
|
|
1579
|
+
align-items: center;
|
|
1580
|
+
padding: 4px 8px;
|
|
1591
1581
|
border-radius: var(--radius-sm);
|
|
1592
1582
|
cursor: pointer;
|
|
1593
1583
|
margin-bottom: 1px;
|
|
1594
1584
|
transition: background 0.1s;
|
|
1585
|
+
gap: 6px;
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
.rp-link-item:hover { background: var(--bg-hover); }
|
|
1589
|
+
|
|
1590
|
+
.rp-link-item .rp-link-icon {
|
|
1591
|
+
font-size: 9px;
|
|
1592
|
+
flex-shrink: 0;
|
|
1593
|
+
opacity: 0.5;
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
.rp-link-item .rp-link-icon.backlink { color: var(--neon-cyan); }
|
|
1597
|
+
.rp-link-item .rp-link-icon.unlinked { color: var(--text-muted); }
|
|
1598
|
+
|
|
1599
|
+
.rp-link-item .rp-link-title {
|
|
1600
|
+
font-size: 12px;
|
|
1601
|
+
color: var(--neon-cyan);
|
|
1602
|
+
font-weight: 500;
|
|
1595
1603
|
overflow: hidden;
|
|
1604
|
+
text-overflow: ellipsis;
|
|
1605
|
+
white-space: nowrap;
|
|
1606
|
+
transition: all 0.15s;
|
|
1596
1607
|
}
|
|
1597
1608
|
|
|
1598
|
-
.rp-item
|
|
1609
|
+
.rp-link-item:hover .rp-link-title {
|
|
1610
|
+
text-decoration: underline;
|
|
1611
|
+
text-underline-offset: 2px;
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
.rp-link-item.unlinked .rp-link-title {
|
|
1615
|
+
color: var(--text-secondary);
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
.rp-link-item.unlinked:hover .rp-link-title {
|
|
1619
|
+
color: var(--neon-cyan);
|
|
1620
|
+
text-decoration: underline;
|
|
1621
|
+
}
|
|
1599
1622
|
|
|
1600
1623
|
/* ============ TRANSITIONS ============ */
|
|
1601
1624
|
.fade-in { animation: fadeIn 0.15s ease; }
|
|
@@ -2188,10 +2211,8 @@ function renderRightPanel(note) {
|
|
|
2188
2211
|
document.getElementById('bl-count').textContent = backlinks.length;
|
|
2189
2212
|
backlinks.forEach(bl => {
|
|
2190
2213
|
const item = document.createElement('div');
|
|
2191
|
-
item.className = 'rp-item
|
|
2192
|
-
|
|
2193
|
-
if (bl.context) html += `<span class="rp-context">${bl.context}</span>`;
|
|
2194
|
-
item.innerHTML = html;
|
|
2214
|
+
item.className = 'rp-link-item';
|
|
2215
|
+
item.innerHTML = `<span class="rp-link-icon backlink">←</span><span class="rp-link-title">${bl.title || bl.path || bl}</span>`;
|
|
2195
2216
|
item.addEventListener('click', () => loadNote(bl.path || bl));
|
|
2196
2217
|
blList.appendChild(item);
|
|
2197
2218
|
});
|
|
@@ -2244,10 +2265,8 @@ function renderRightPanel(note) {
|
|
|
2244
2265
|
document.getElementById('unlinked-count').textContent = unlinked.length;
|
|
2245
2266
|
unlinked.forEach(u => {
|
|
2246
2267
|
const item = document.createElement('div');
|
|
2247
|
-
item.className = 'rp-item
|
|
2248
|
-
|
|
2249
|
-
if (u.context) html += `<span class="rp-context">${u.context}</span>`;
|
|
2250
|
-
item.innerHTML = html;
|
|
2268
|
+
item.className = 'rp-link-item unlinked';
|
|
2269
|
+
item.innerHTML = `<span class="rp-link-icon unlinked">≈</span><span class="rp-link-title">${u.title}</span>`;
|
|
2251
2270
|
item.addEventListener('click', () => loadNote(u.path));
|
|
2252
2271
|
ulList.appendChild(item);
|
|
2253
2272
|
});
|
|
@@ -2905,6 +2924,23 @@ async function init() {
|
|
|
2905
2924
|
`;
|
|
2906
2925
|
}
|
|
2907
2926
|
|
|
2927
|
+
// --- Auto-refresh: poll for vault changes ---
|
|
2928
|
+
let lastKnownStats = null;
|
|
2929
|
+
|
|
2930
|
+
async function pollForChanges() {
|
|
2931
|
+
try {
|
|
2932
|
+
const stats = await fetchJSON('/api/stats');
|
|
2933
|
+
const key = `${stats.notes}:${stats.links}:${stats.tags}`;
|
|
2934
|
+
if (lastKnownStats && lastKnownStats !== key) {
|
|
2935
|
+
await refreshTree();
|
|
2936
|
+
if (currentPath) loadNote(currentPath);
|
|
2937
|
+
}
|
|
2938
|
+
lastKnownStats = key;
|
|
2939
|
+
} catch {}
|
|
2940
|
+
}
|
|
2941
|
+
|
|
2942
|
+
setInterval(pollForChanges, 3000);
|
|
2943
|
+
|
|
2908
2944
|
initResize();
|
|
2909
2945
|
initGraphResize();
|
|
2910
2946
|
init();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kyp-mem",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"description": "Know Your Project — Persistent knowledge base for AI agents. MCP-powered with wikilinks, backlinks, auto-learning, and neon web UI.",
|
|
3
|
+
"version": "0.4.3",
|
|
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"
|
|
7
7
|
},
|
package/pyproject.toml
CHANGED
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "kyp-mem"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.2"
|
|
8
8
|
description = "Know Your Project — Persistent knowledge base for AI agents. MCP-powered with wikilinks, backlinks, auto-learning, and neon web UI."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|