kyp-mem 0.7.4 → 0.8.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 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 into the conversation at session start."""
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
- if not project_notes:
171
- return
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
- parts = [f"# [kyp-mem] {project_name} Recent Sessions"]
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
- parts.append(f"## Last {len(sessions)} Sessions")
184
- for sp in sessions:
185
- note = vault.read(sp)
186
- if not note:
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
- session_ids = {Path(sp).stem for sp in sessions}
194
- stats_line = _build_stats_line(project_name, len("\n".join(parts)), session_ids)
195
- if stats_line:
196
- parts.append(stats_line)
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 be displaying the session summaries below. Do NOT skip this. Do NOT wait for user input. Display them immediately, formatted cleanly, before doing anything else.**")
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."""
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kyp-mem",
3
- "version": "0.7.4",
3
+ "version": "0.8.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"
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.7.4"
7
+ version = "0.8.0"
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"}