kyp-mem 0.4.2 → 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/bin/cli.mjs +20 -0
- package/kyp_mem/cli.py +1 -1
- package/kyp_mem/hooks.py +213 -18
- package/kyp_mem/server.py +17 -6
- package/kyp_mem/static/index.html +59 -23
- package/package.json +2 -2
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/cli.py
CHANGED
|
@@ -310,7 +310,7 @@ def _run_install_hooks(global_config: bool = False, remove: bool = False):
|
|
|
310
310
|
stop_hooks = [h for h in stop_hooks if not _has_kyp_hook(h)]
|
|
311
311
|
|
|
312
312
|
post_tool_hooks.append({
|
|
313
|
-
"matcher": "Edit|Write|Bash",
|
|
313
|
+
"matcher": "Edit|Write|Read|Bash",
|
|
314
314
|
"hooks": [{"type": "command", "command": f"{mcp_command} hook post-tool-use"}],
|
|
315
315
|
})
|
|
316
316
|
prompt_hooks.append({
|
package/kyp_mem/hooks.py
CHANGED
|
@@ -60,6 +60,9 @@ def handle_post_tool_use():
|
|
|
60
60
|
elif tool_name == "Write":
|
|
61
61
|
entry["action"] = "create"
|
|
62
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", "")
|
|
63
66
|
elif tool_name == "Bash":
|
|
64
67
|
entry["action"] = "command"
|
|
65
68
|
entry["command"] = tool_input.get("command", "")
|
|
@@ -71,6 +74,197 @@ def handle_post_tool_use():
|
|
|
71
74
|
f.write(json.dumps(entry) + "\n")
|
|
72
75
|
|
|
73
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
|
+
|
|
74
268
|
def handle_stop():
|
|
75
269
|
if not CURRENT_SESSION.exists():
|
|
76
270
|
return
|
|
@@ -100,6 +294,7 @@ def handle_stop():
|
|
|
100
294
|
project_name = Path(project_dir).name
|
|
101
295
|
session_id = datetime.now().strftime("%Y-%m-%d_%H%M%S")
|
|
102
296
|
|
|
297
|
+
files_read = set()
|
|
103
298
|
files_edited = set()
|
|
104
299
|
files_created = set()
|
|
105
300
|
commands = []
|
|
@@ -114,6 +309,10 @@ def handle_stop():
|
|
|
114
309
|
if action == "prompt":
|
|
115
310
|
prompts.append({"ts": ts, "text": e.get("prompt", "")})
|
|
116
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}`")
|
|
117
316
|
elif action == "edit":
|
|
118
317
|
fp = e.get("file", "")
|
|
119
318
|
files_edited.add(fp)
|
|
@@ -128,6 +327,8 @@ def handle_stop():
|
|
|
128
327
|
short = cmd[:80] + "..." if len(cmd) > 80 else cmd
|
|
129
328
|
timeline.append(f" {ts} — `{short}`")
|
|
130
329
|
|
|
330
|
+
commands_classified = [_classify_command(cmd) for cmd in commands]
|
|
331
|
+
|
|
131
332
|
summary_items = []
|
|
132
333
|
if files_edited:
|
|
133
334
|
summary_items.append(f"Modified {len(files_edited)} file{'s' if len(files_edited) != 1 else ''}")
|
|
@@ -136,12 +337,10 @@ def handle_stop():
|
|
|
136
337
|
if commands:
|
|
137
338
|
summary_items.append(f"Ran {len(commands)} command{'s' if len(commands) != 1 else ''}")
|
|
138
339
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if first_word in investigate_keywords:
|
|
144
|
-
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)
|
|
145
344
|
|
|
146
345
|
parts = [f"# Session {session_id}", ""]
|
|
147
346
|
parts.append(f"**Project:** `{project_dir}`")
|
|
@@ -161,27 +360,23 @@ def handle_stop():
|
|
|
161
360
|
parts.append("")
|
|
162
361
|
|
|
163
362
|
parts.append("## INVESTIGATED")
|
|
164
|
-
if
|
|
165
|
-
|
|
166
|
-
short = cmd[:120] + "..." if len(cmd) > 120 else cmd
|
|
167
|
-
parts.append(f"- `{short}`")
|
|
363
|
+
if investigated:
|
|
364
|
+
parts.extend(investigated)
|
|
168
365
|
parts.append("")
|
|
169
366
|
|
|
170
367
|
parts.append("## LEARNED")
|
|
171
|
-
|
|
368
|
+
if learned:
|
|
369
|
+
parts.extend(learned)
|
|
172
370
|
parts.append("")
|
|
173
371
|
|
|
174
372
|
parts.append("## COMPLETED")
|
|
175
|
-
if
|
|
176
|
-
|
|
177
|
-
parts.append(f"- Modified `{Path(f).name}`")
|
|
178
|
-
if files_created:
|
|
179
|
-
for f in sorted(files_created):
|
|
180
|
-
parts.append(f"- Created `{Path(f).name}`")
|
|
373
|
+
if completed:
|
|
374
|
+
parts.extend(completed)
|
|
181
375
|
parts.append("")
|
|
182
376
|
|
|
183
377
|
parts.append("## NEXT STEPS")
|
|
184
|
-
|
|
378
|
+
if next_steps:
|
|
379
|
+
parts.extend(next_steps)
|
|
185
380
|
parts.append("")
|
|
186
381
|
|
|
187
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
|
},
|