kyp-mem 0.7.4 → 0.9.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/kyp_mem/cli.py +39 -0
- package/kyp_mem/hooks.py +80 -19
- package/kyp_mem/server.py +26 -0
- package/kyp_mem/static/index.html +1147 -796
- package/package.json +1 -1
- package/pyproject.toml +1 -1
package/kyp_mem/cli.py
CHANGED
|
@@ -49,6 +49,10 @@ def main():
|
|
|
49
49
|
cfg_parser.add_argument("key", nargs="?", help="Config key (e.g. session_model)")
|
|
50
50
|
cfg_parser.add_argument("value", nargs="?", help="Value to set")
|
|
51
51
|
|
|
52
|
+
obj_parser = subparsers.add_parser("objective", help="Get or set a project's objective (injected at session start)")
|
|
53
|
+
obj_parser.add_argument("project", nargs="?", help="Project name (defaults to current directory name)")
|
|
54
|
+
obj_parser.add_argument("text", nargs="*", help="Objective text to set (omit to read the current objective)")
|
|
55
|
+
|
|
52
56
|
hook_parser = subparsers.add_parser("hook", help="Handle Claude Code hook events (internal)")
|
|
53
57
|
hook_sub = hook_parser.add_subparsers(dest="hook_command")
|
|
54
58
|
hook_sub.add_parser("session-start", help="Inject project context at session start")
|
|
@@ -79,6 +83,8 @@ def main():
|
|
|
79
83
|
_run_install_hooks(global_config=args.global_config, remove=args.remove)
|
|
80
84
|
elif args.command == "config":
|
|
81
85
|
_run_config(args.key, args.value)
|
|
86
|
+
elif args.command == "objective":
|
|
87
|
+
_run_objective(args.project, " ".join(args.text).strip())
|
|
82
88
|
elif args.command == "uninstall":
|
|
83
89
|
_run_uninstall(purge=args.purge)
|
|
84
90
|
elif args.command == "doctor":
|
|
@@ -460,6 +466,39 @@ def _run_config(key, value):
|
|
|
460
466
|
print(f" {G}✓{R} {key} = {value}")
|
|
461
467
|
|
|
462
468
|
|
|
469
|
+
def _run_objective(project, text):
|
|
470
|
+
from .config import get_vault_path
|
|
471
|
+
from .vault import Vault
|
|
472
|
+
|
|
473
|
+
project = project or Path.cwd().name
|
|
474
|
+
vault = Vault(get_vault_path())
|
|
475
|
+
path = f"{project}/Objective.md"
|
|
476
|
+
|
|
477
|
+
if not text:
|
|
478
|
+
note = vault.read(path)
|
|
479
|
+
print()
|
|
480
|
+
print(f" {C}KYP-MEM{R} — Objective for {G}{project}{R}")
|
|
481
|
+
print()
|
|
482
|
+
if not note:
|
|
483
|
+
print(f" {Y}(not set){R}")
|
|
484
|
+
print(f" {D} Set one: kyp-mem objective {project} \"<your goal>\"{R}")
|
|
485
|
+
else:
|
|
486
|
+
content = note.content.strip()
|
|
487
|
+
lines = content.split("\n")
|
|
488
|
+
if lines and lines[0].lstrip().startswith("# "):
|
|
489
|
+
content = "\n".join(lines[1:]).strip()
|
|
490
|
+
print(f" {content}")
|
|
491
|
+
print()
|
|
492
|
+
return
|
|
493
|
+
|
|
494
|
+
content = f"# Objective\n\n{text}\n"
|
|
495
|
+
vault.write_note(path, content, ["objective", project.lower().replace(" ", "-")], {})
|
|
496
|
+
print()
|
|
497
|
+
print(f" {G}✓{R} Objective saved for {G}{project}{R} ({path})")
|
|
498
|
+
print(f" {D} Injected at every session start.{R}")
|
|
499
|
+
print()
|
|
500
|
+
|
|
501
|
+
|
|
463
502
|
def _run_stats():
|
|
464
503
|
from .config import get_vault_path
|
|
465
504
|
from .vault import Vault
|
package/kyp_mem/hooks.py
CHANGED
|
@@ -145,8 +145,42 @@ def _build_stats_line(project_name, injected_chars, session_ids):
|
|
|
145
145
|
return None
|
|
146
146
|
|
|
147
147
|
|
|
148
|
+
def _objective_note_path(project_name):
|
|
149
|
+
"""Canonical vault path for a project's objective note."""
|
|
150
|
+
return f"{project_name}/Objective.md"
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _find_objective_path(vault, project_name):
|
|
154
|
+
"""Case-insensitive lookup of the objective note (vault casing may differ
|
|
155
|
+
from the cwd basename on case-insensitive filesystems)."""
|
|
156
|
+
target = f"{project_name}/objective.md".lower()
|
|
157
|
+
for p in vault.index.notes:
|
|
158
|
+
if p.lower() == target:
|
|
159
|
+
return p
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _read_objective(vault, project_name):
|
|
164
|
+
"""Return the objective text for a project, or None if not set.
|
|
165
|
+
|
|
166
|
+
Strips an optional leading ``# ...`` heading so only the objective body
|
|
167
|
+
is surfaced.
|
|
168
|
+
"""
|
|
169
|
+
path = _find_objective_path(vault, project_name)
|
|
170
|
+
if not path:
|
|
171
|
+
return None
|
|
172
|
+
note = vault.read(path)
|
|
173
|
+
if not note:
|
|
174
|
+
return None
|
|
175
|
+
lines = note.content.strip().split("\n")
|
|
176
|
+
if lines and lines[0].lstrip().startswith("# "):
|
|
177
|
+
lines = lines[1:]
|
|
178
|
+
text = "\n".join(lines).strip()
|
|
179
|
+
return text or None
|
|
180
|
+
|
|
181
|
+
|
|
148
182
|
def handle_session_start():
|
|
149
|
-
"""Inject recent session memory
|
|
183
|
+
"""Inject the project objective and recent session memory at session start."""
|
|
150
184
|
sys.stdin.read()
|
|
151
185
|
if _is_subprocess():
|
|
152
186
|
return
|
|
@@ -167,36 +201,63 @@ def handle_session_start():
|
|
|
167
201
|
# filesystems "KYP-MEM" and "kyp-mem" are the same directory).
|
|
168
202
|
prefix = f"{project_name}/".lower()
|
|
169
203
|
project_notes = [p for p in vault.index.notes if p.lower().startswith(prefix)]
|
|
170
|
-
|
|
171
|
-
|
|
204
|
+
|
|
205
|
+
objective = _read_objective(vault, project_name)
|
|
172
206
|
|
|
173
207
|
sessions = sorted(
|
|
174
208
|
(p for p in project_notes if "/sessions/" in p.lower()),
|
|
175
209
|
reverse=True,
|
|
176
210
|
)[:10]
|
|
177
|
-
if not sessions:
|
|
178
|
-
return
|
|
179
211
|
|
|
180
|
-
|
|
212
|
+
# Nothing to say: no objective to surface, no objective to request
|
|
213
|
+
# (project is already known but sessions just haven't been captured),
|
|
214
|
+
# and no sessions. Only stay silent when this is an established project
|
|
215
|
+
# with an objective but zero sessions — otherwise we always at least
|
|
216
|
+
# surface or request the objective.
|
|
217
|
+
if not objective and not project_notes and not sessions:
|
|
218
|
+
# Brand-new / unknown directory: still ask for an objective so the
|
|
219
|
+
# project starts with a clear goal.
|
|
220
|
+
pass
|
|
221
|
+
|
|
222
|
+
parts = [f"# [kyp-mem] {project_name} — Session Context"]
|
|
181
223
|
parts.append(f"Use `kyp_search` or `kyp_project_context` for architecture/project knowledge on demand.\n")
|
|
182
224
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
continue
|
|
188
|
-
parts.append(f"### {note.title}")
|
|
189
|
-
summary = _extract_session_summary(note.content)
|
|
190
|
-
parts.append(summary)
|
|
225
|
+
# --- Objective (always first) ---
|
|
226
|
+
if objective:
|
|
227
|
+
parts.append("## 🎯 Objective")
|
|
228
|
+
parts.append(objective)
|
|
191
229
|
parts.append("")
|
|
230
|
+
else:
|
|
231
|
+
parts.append("## 🎯 Objective — NOT SET")
|
|
232
|
+
parts.append(
|
|
233
|
+
f"No objective is recorded for **{project_name}**. Before anything else, "
|
|
234
|
+
"ask the user: **\"What is the main objective / goal for this project?\"** "
|
|
235
|
+
"When they answer, save it by calling "
|
|
236
|
+
f"`kyp_objective_set(project=\"{project_name}\", objective=\"...\")` "
|
|
237
|
+
f"(or `kyp_write(\"{project_name}/Objective.md\", ...)`). "
|
|
238
|
+
"Keep your work aligned to this objective every session."
|
|
239
|
+
)
|
|
240
|
+
parts.append("")
|
|
241
|
+
|
|
242
|
+
# --- Recent sessions ---
|
|
243
|
+
if sessions:
|
|
244
|
+
parts.append(f"## Last {len(sessions)} Sessions")
|
|
245
|
+
for sp in sessions:
|
|
246
|
+
note = vault.read(sp)
|
|
247
|
+
if not note:
|
|
248
|
+
continue
|
|
249
|
+
parts.append(f"### {note.title}")
|
|
250
|
+
summary = _extract_session_summary(note.content)
|
|
251
|
+
parts.append(summary)
|
|
252
|
+
parts.append("")
|
|
192
253
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
254
|
+
session_ids = {Path(sp).stem for sp in sessions}
|
|
255
|
+
stats_line = _build_stats_line(project_name, len("\n".join(parts)), session_ids)
|
|
256
|
+
if stats_line:
|
|
257
|
+
parts.append(stats_line)
|
|
197
258
|
|
|
198
259
|
parts.append("")
|
|
199
|
-
parts.append("**CRITICAL: Your FIRST response to the user MUST
|
|
260
|
+
parts.append("**CRITICAL: Your FIRST response to the user MUST surface this context — display the session summaries above (if any) and the objective. If the objective is NOT SET, ask the user for it as instructed. Do this immediately, formatted cleanly, before anything else.**")
|
|
200
261
|
|
|
201
262
|
output = "\n".join(parts)
|
|
202
263
|
|
package/kyp_mem/server.py
CHANGED
|
@@ -22,6 +22,7 @@ YOU MUST FOLLOW THESE INSTRUCTIONS when kyp-mem tools are available.
|
|
|
22
22
|
2. Call `kyp_project_context(project)` to load the project's knowledge base, notes, and recent session summaries.
|
|
23
23
|
3. If no project exists yet, call `kyp_project_context` anyway — if it returns empty, ask the user if you should create one.
|
|
24
24
|
4. Use the returned context to ground yourself: understand architecture, known bugs, past decisions, and what was done in recent sessions. Do NOT ask the user questions that are already answered in the project context.
|
|
25
|
+
5. Check the project objective with `kyp_objective_get(project)`. If none is set, ask the user for the project's main goal and save it with `kyp_objective_set(project, objective)`. Keep your work aligned to this objective.
|
|
25
26
|
|
|
26
27
|
### DURING WORK — WHEN TO SEARCH SESSIONS
|
|
27
28
|
Call `kyp_session_search(query)` when:
|
|
@@ -342,6 +343,31 @@ def kyp_sessions(project: str = "", limit: int = 10) -> str:
|
|
|
342
343
|
return "\n".join(lines)
|
|
343
344
|
|
|
344
345
|
|
|
346
|
+
@mcp.tool()
|
|
347
|
+
def kyp_objective_get(project: str) -> str:
|
|
348
|
+
"""Get the recorded objective / main goal for a project. The objective is injected at every session start. Returns a not-set message if none exists yet."""
|
|
349
|
+
note = vault.read(f"{project}/Objective.md")
|
|
350
|
+
if not note:
|
|
351
|
+
return f"No objective set for '{project}'. Ask the user for the project's main goal, then call kyp_objective_set."
|
|
352
|
+
content = note.content.strip()
|
|
353
|
+
lines = content.split("\n")
|
|
354
|
+
if lines and lines[0].lstrip().startswith("# "):
|
|
355
|
+
content = "\n".join(lines[1:]).strip()
|
|
356
|
+
return content or f"No objective set for '{project}'."
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
@mcp.tool()
|
|
360
|
+
def kyp_objective_set(project: str, objective: str) -> str:
|
|
361
|
+
"""Set (or replace) the objective / main goal for a project. This is injected into every future session start so work stays aligned. Call this once the user tells you what the project is for."""
|
|
362
|
+
objective = objective.strip()
|
|
363
|
+
if not objective:
|
|
364
|
+
return "Objective text is empty — nothing saved."
|
|
365
|
+
path = f"{project}/Objective.md"
|
|
366
|
+
content = f"# Objective\n\n{objective}\n"
|
|
367
|
+
vault.write_note(path, content, ["objective", project.lower().replace(" ", "-")], {})
|
|
368
|
+
return f"Objective saved for '{project}' ({path}). It will be injected at every session start."
|
|
369
|
+
|
|
370
|
+
|
|
345
371
|
@mcp.tool()
|
|
346
372
|
def kyp_project_context(project: str) -> str:
|
|
347
373
|
"""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."""
|