groundmemory 0.3.2__tar.gz → 0.3.4__tar.gz

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.
Files changed (64) hide show
  1. {groundmemory-0.3.2 → groundmemory-0.3.4}/.github/workflows/pypi-publish.yml +1 -1
  2. {groundmemory-0.3.2 → groundmemory-0.3.4}/PKG-INFO +3 -3
  3. {groundmemory-0.3.2 → groundmemory-0.3.4}/README.md +2 -2
  4. {groundmemory-0.3.2 → groundmemory-0.3.4}/groundmemory/bootstrap/injector.py +18 -6
  5. {groundmemory-0.3.2 → groundmemory-0.3.4}/groundmemory/core/storage.py +8 -4
  6. {groundmemory-0.3.2 → groundmemory-0.3.4}/groundmemory/core/workspace.py +38 -0
  7. {groundmemory-0.3.2 → groundmemory-0.3.4}/groundmemory/tools/base.py +17 -0
  8. {groundmemory-0.3.2 → groundmemory-0.3.4}/groundmemory/tools/memory_write.py +3 -1
  9. {groundmemory-0.3.2 → groundmemory-0.3.4}/pyproject.toml +1 -1
  10. {groundmemory-0.3.2 → groundmemory-0.3.4}/tests/test_memory_replace.py +6 -3
  11. {groundmemory-0.3.2 → groundmemory-0.3.4}/tests/test_memory_write.py +46 -1
  12. {groundmemory-0.3.2 → groundmemory-0.3.4}/tests/test_session.py +2 -0
  13. {groundmemory-0.3.2 → groundmemory-0.3.4}/.dockerignore +0 -0
  14. {groundmemory-0.3.2 → groundmemory-0.3.4}/.github/workflows/unit-tests.yml +0 -0
  15. {groundmemory-0.3.2 → groundmemory-0.3.4}/.gitignore +0 -0
  16. {groundmemory-0.3.2 → groundmemory-0.3.4}/.python-version +0 -0
  17. {groundmemory-0.3.2 → groundmemory-0.3.4}/DOCS.md +0 -0
  18. {groundmemory-0.3.2 → groundmemory-0.3.4}/Dockerfile +0 -0
  19. {groundmemory-0.3.2 → groundmemory-0.3.4}/LICENSE +0 -0
  20. {groundmemory-0.3.2 → groundmemory-0.3.4}/_assets/icon.png +0 -0
  21. {groundmemory-0.3.2 → groundmemory-0.3.4}/docker-compose.yml +0 -0
  22. {groundmemory-0.3.2 → groundmemory-0.3.4}/examples/anthropic_agent.py +0 -0
  23. {groundmemory-0.3.2 → groundmemory-0.3.4}/examples/openai_agent.py +0 -0
  24. {groundmemory-0.3.2 → groundmemory-0.3.4}/groundmemory/__init__.py +0 -0
  25. {groundmemory-0.3.2 → groundmemory-0.3.4}/groundmemory/adapters/__init__.py +0 -0
  26. {groundmemory-0.3.2 → groundmemory-0.3.4}/groundmemory/adapters/anthropic.py +0 -0
  27. {groundmemory-0.3.2 → groundmemory-0.3.4}/groundmemory/adapters/openai.py +0 -0
  28. {groundmemory-0.3.2 → groundmemory-0.3.4}/groundmemory/bootstrap/__init__.py +0 -0
  29. {groundmemory-0.3.2 → groundmemory-0.3.4}/groundmemory/bootstrap/compaction.py +0 -0
  30. {groundmemory-0.3.2 → groundmemory-0.3.4}/groundmemory/config/.env.example +0 -0
  31. {groundmemory-0.3.2 → groundmemory-0.3.4}/groundmemory/config/__init__.py +0 -0
  32. {groundmemory-0.3.2 → groundmemory-0.3.4}/groundmemory/config/groundmemory.yaml.example +0 -0
  33. {groundmemory-0.3.2 → groundmemory-0.3.4}/groundmemory/core/__init__.py +0 -0
  34. {groundmemory-0.3.2 → groundmemory-0.3.4}/groundmemory/core/chunker.py +0 -0
  35. {groundmemory-0.3.2 → groundmemory-0.3.4}/groundmemory/core/embeddings.py +0 -0
  36. {groundmemory-0.3.2 → groundmemory-0.3.4}/groundmemory/core/index.py +0 -0
  37. {groundmemory-0.3.2 → groundmemory-0.3.4}/groundmemory/core/relations.py +0 -0
  38. {groundmemory-0.3.2 → groundmemory-0.3.4}/groundmemory/core/search.py +0 -0
  39. {groundmemory-0.3.2 → groundmemory-0.3.4}/groundmemory/core/sync.py +0 -0
  40. {groundmemory-0.3.2 → groundmemory-0.3.4}/groundmemory/mcp_server.py +0 -0
  41. {groundmemory-0.3.2 → groundmemory-0.3.4}/groundmemory/session.py +0 -0
  42. {groundmemory-0.3.2 → groundmemory-0.3.4}/groundmemory/tools/__init__.py +0 -0
  43. {groundmemory-0.3.2 → groundmemory-0.3.4}/groundmemory/tools/memory_bootstrap.py +0 -0
  44. {groundmemory-0.3.2 → groundmemory-0.3.4}/groundmemory/tools/memory_delete.py +0 -0
  45. {groundmemory-0.3.2 → groundmemory-0.3.4}/groundmemory/tools/memory_dispatcher.py +0 -0
  46. {groundmemory-0.3.2 → groundmemory-0.3.4}/groundmemory/tools/memory_get.py +0 -0
  47. {groundmemory-0.3.2 → groundmemory-0.3.4}/groundmemory/tools/memory_list.py +0 -0
  48. {groundmemory-0.3.2 → groundmemory-0.3.4}/groundmemory/tools/memory_read.py +0 -0
  49. {groundmemory-0.3.2 → groundmemory-0.3.4}/groundmemory/tools/memory_relate.py +0 -0
  50. {groundmemory-0.3.2 → groundmemory-0.3.4}/groundmemory/tools/memory_replace.py +0 -0
  51. {groundmemory-0.3.2 → groundmemory-0.3.4}/groundmemory/tools/memory_search.py +0 -0
  52. {groundmemory-0.3.2 → groundmemory-0.3.4}/tests/__init__.py +0 -0
  53. {groundmemory-0.3.2 → groundmemory-0.3.4}/tests/conftest.py +0 -0
  54. {groundmemory-0.3.2 → groundmemory-0.3.4}/tests/test_embeddings_integration.py +0 -0
  55. {groundmemory-0.3.2 → groundmemory-0.3.4}/tests/test_mcp_server.py +0 -0
  56. {groundmemory-0.3.2 → groundmemory-0.3.4}/tests/test_memory_delete.py +0 -0
  57. {groundmemory-0.3.2 → groundmemory-0.3.4}/tests/test_memory_get.py +0 -0
  58. {groundmemory-0.3.2 → groundmemory-0.3.4}/tests/test_memory_list.py +0 -0
  59. {groundmemory-0.3.2 → groundmemory-0.3.4}/tests/test_memory_read.py +0 -0
  60. {groundmemory-0.3.2 → groundmemory-0.3.4}/tests/test_memory_relate.py +0 -0
  61. {groundmemory-0.3.2 → groundmemory-0.3.4}/tests/test_memory_search.py +0 -0
  62. {groundmemory-0.3.2 → groundmemory-0.3.4}/tests/test_relations_sync.py +0 -0
  63. {groundmemory-0.3.2 → groundmemory-0.3.4}/tests/test_workspace_security.py +0 -0
  64. {groundmemory-0.3.2 → groundmemory-0.3.4}/uv.lock +0 -0
@@ -1,4 +1,4 @@
1
- name: Publish Package and Create Release
1
+ name: Publish & Release
2
2
 
3
3
  on:
4
4
  push:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: groundmemory
3
- Version: 0.3.2
3
+ Version: 0.3.4
4
4
  Summary: Persistent, semantic memory for AI agents - mcp-native, local-first, framework-agnostic, production-ready
5
5
  Project-URL: Documentation, https://github.com/huss-mo/GroundMemory/blob/master/DOCS.md
6
6
  Project-URL: Issues, https://github.com/huss-mo/GroundMemory/issues
@@ -31,9 +31,9 @@ Description-Content-Type: text/markdown
31
31
  **Persistent, semantic memory for AI agents - mcp-native, local-first, framework-agnostic, production-ready.**
32
32
 
33
33
  [![Python 3.12+](https://img.shields.io/badge/python-3.12%2B-blue.svg)](https://www.python.org/downloads/)
34
- [![Release](https://github.com/huss-mo/GroundMemory/actions/workflows/publish-release.yml/badge.svg?event=push)](https://github.com/huss-mo/GroundMemory/actions/workflows/publish-release.yml)
34
+ [![Release](https://github.com/huss-mo/GroundMemory/actions/workflows/pypi-publish.yml/badge.svg?event=push)](https://github.com/huss-mo/GroundMemory/actions/workflows/pypi-publish.yml)
35
35
  [![Unit Tests](https://github.com/huss-mo/GroundMemory/actions/workflows/unit-tests.yml/badge.svg)](https://github.com/huss-mo/GroundMemory/actions/workflows/unit-tests.yml)
36
- [![Test Suite](https://img.shields.io/badge/test%20suite-380%20tests-blue.svg)](#running-the-test-suite)
36
+ [![Test Suite](https://img.shields.io/badge/test%20suite-386%20tests-blue.svg)](#running-the-test-suite)
37
37
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
38
38
  ![GitHub repo size](https://img.shields.io/github/repo-size/huss-mo/GroundMemory)
39
39
  ![GitHub language count](https://img.shields.io/github/languages/count/huss-mo/GroundMemory)
@@ -5,9 +5,9 @@
5
5
  **Persistent, semantic memory for AI agents - mcp-native, local-first, framework-agnostic, production-ready.**
6
6
 
7
7
  [![Python 3.12+](https://img.shields.io/badge/python-3.12%2B-blue.svg)](https://www.python.org/downloads/)
8
- [![Release](https://github.com/huss-mo/GroundMemory/actions/workflows/publish-release.yml/badge.svg?event=push)](https://github.com/huss-mo/GroundMemory/actions/workflows/publish-release.yml)
8
+ [![Release](https://github.com/huss-mo/GroundMemory/actions/workflows/pypi-publish.yml/badge.svg?event=push)](https://github.com/huss-mo/GroundMemory/actions/workflows/pypi-publish.yml)
9
9
  [![Unit Tests](https://github.com/huss-mo/GroundMemory/actions/workflows/unit-tests.yml/badge.svg)](https://github.com/huss-mo/GroundMemory/actions/workflows/unit-tests.yml)
10
- [![Test Suite](https://img.shields.io/badge/test%20suite-380%20tests-blue.svg)](#running-the-test-suite)
10
+ [![Test Suite](https://img.shields.io/badge/test%20suite-386%20tests-blue.svg)](#running-the-test-suite)
11
11
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
12
12
  ![GitHub repo size](https://img.shields.io/github/repo-size/huss-mo/GroundMemory)
13
13
  ![GitHub language count](https://img.shields.io/github/languages/count/huss-mo/GroundMemory)
@@ -44,8 +44,8 @@ def _read_capped(path: Path, max_chars: int) -> tuple[str, bool]:
44
44
  def _section(title: str, body: str, truncated: bool = False, source: str = "") -> str:
45
45
  """Wrap *body* in a labelled Markdown block."""
46
46
  marker = " [TRUNCATED - use memory_get to read the rest]" if truncated else ""
47
- source_line = f"*{source}*\n\n" if source else ""
48
- return f"### {title}{marker} ({source_line})\n\n{body}\n"
47
+ source_block = f" ({source})" if source else ""
48
+ return f"### {title}{marker}{source_block}\n\n{body}\n"
49
49
 
50
50
 
51
51
  # ---------------------------------------------------------------------------
@@ -107,12 +107,20 @@ def build_bootstrap_prompt(
107
107
  total_chars += len(body_text)
108
108
  return total_chars < cfg.max_total_chars
109
109
 
110
- # 1. Long-term memory (MEMORY.md)
111
- if cfg.inject_long_term_memory:
110
+ # Check whether first-run onboarding is active (file exists and is non-empty).
111
+ # When active, skip MEMORY.md and USER.md - they are empty/default and would
112
+ # only confuse the model during onboarding.
113
+ first_run_active = (
114
+ workspace.first_run_file.exists()
115
+ and workspace.first_run_file.read_text(encoding="utf-8").strip() != ""
116
+ )
117
+
118
+ # 1. Long-term memory (MEMORY.md) - skipped during first run
119
+ if cfg.inject_long_term_memory and not first_run_active:
112
120
  _add("Long-Term Memory", workspace.memory_file)
113
121
 
114
- # 2. User profile (USER.md)
115
- if cfg.inject_user_profile:
122
+ # 2. User profile (USER.md) - skipped during first run
123
+ if cfg.inject_user_profile and not first_run_active:
116
124
  _add("User Profile", workspace.user_file)
117
125
 
118
126
  # 3. Agent roster (AGENTS.md)
@@ -139,6 +147,10 @@ def build_bootstrap_prompt(
139
147
  if not _add(label, day_path, source=f"daily/{day_path.name}"):
140
148
  break # budget exhausted
141
149
 
150
+ # 6. First-run onboarding (FIRST_RUN.md) - injected last; skipped automatically once emptied
151
+ if first_run_active:
152
+ _add("First Run", workspace.first_run_file)
153
+
142
154
  if not sections:
143
155
  return ""
144
156
 
@@ -218,8 +218,10 @@ def hard_delete_lines(path: Path, start_line: int, end_line: int) -> dict:
218
218
 
219
219
  if start_line < 1 or start_line > total:
220
220
  return {"error": f"start_line {start_line} out of range (file has {total} lines)"}
221
- if end_line < start_line or end_line > total:
222
- return {"error": f"end_line {end_line} out of range (start={start_line}, total={total})"}
221
+ if end_line < start_line:
222
+ return {"error": f"end_line {end_line} must be >= start_line {start_line}"}
223
+ # Clamp end_line silently if it exceeds the file length
224
+ end_line = min(end_line, total)
223
225
 
224
226
  # Convert to 0-indexed
225
227
  s = start_line - 1
@@ -277,8 +279,10 @@ def replace_lines(path: Path, start_line: int, end_line: int, replacement: str)
277
279
 
278
280
  if start_line < 1 or start_line > total:
279
281
  return {"error": f"start_line {start_line} out of range (file has {total} lines)"}
280
- if end_line < start_line or end_line > total:
281
- return {"error": f"end_line {end_line} out of range (start={start_line}, total={total})"}
282
+ if end_line < start_line:
283
+ return {"error": f"end_line {end_line} must be >= start_line {start_line}"}
284
+ # Clamp end_line silently if it exceeds the file length
285
+ end_line = min(end_line, total)
282
286
 
283
287
  # Convert to 0-indexed
284
288
  s = start_line - 1
@@ -144,6 +144,39 @@ Every non-blank, non-comment line must follow this exact format:
144
144
  Lines that do not match will be rejected by the write tools.
145
145
  """
146
146
 
147
+ _DEFAULT_FIRST_RUN_MD = """\
148
+ # First Run
149
+
150
+ If you are seeing this section, then this is the user's very first session
151
+ with GroundMemory. Your memory is completely empty. You are meeting them for
152
+ the first time - make it feel like the start of something, not a setup wizard.
153
+
154
+ What to do:
155
+
156
+ 1. Open warmly and explain what GroundMemory means for them. Something like:
157
+
158
+ "Hey! Welcome - this is the start of something good. I have persistent memory,
159
+ so anything we talk about I'll carry with me into every future session. No need
160
+ to repeat yourself down the line."
161
+
162
+ Use your own words. Don't copy this exactly.
163
+
164
+ 2. In the same message, ask your questions - each on its own line, not crammed
165
+ together. Keep it light:
166
+
167
+ - What should you call them? (And invite them to give you a name too, if they want.)
168
+ - Is there anything they always want you to keep in mind?
169
+
170
+ End with something relaxed, like: "No rush - just getting to know you."
171
+
172
+ 3. Once they have answered, save what you learned:
173
+ - Name, preferences, working style -> USER.md
174
+ - How they want you to behave -> AGENTS.md
175
+ - You can also use the other memory file or tools (Ex. memory_relate) as you see fit
176
+
177
+ Do not mention this file to the user. Just have the conversation.
178
+ """
179
+
147
180
  _DEFAULT_RELATIONS_MD = """\
148
181
  # Relations
149
182
 
@@ -210,6 +243,10 @@ class Workspace:
210
243
  def relations_file(self) -> Path:
211
244
  return self.path / "RELATIONS.md"
212
245
 
246
+ @property
247
+ def first_run_file(self) -> Path:
248
+ return self.path / "FIRST_RUN.md"
249
+
213
250
  def daily_file(self, day: date | None = None) -> Path:
214
251
  """Return the path for the daily log of *day* (defaults to today)."""
215
252
  d = day or date.today()
@@ -229,6 +266,7 @@ class Workspace:
229
266
  self._seed(self.user_file, _DEFAULT_USER_MD)
230
267
  self._seed(self.agents_file, _DEFAULT_AGENTS_MD)
231
268
  self._seed(self.relations_file, _DEFAULT_RELATIONS_MD)
269
+ self._seed(self.first_run_file, _DEFAULT_FIRST_RUN_MD)
232
270
 
233
271
  @staticmethod
234
272
  def _seed(path: Path, content: str) -> None:
@@ -44,6 +44,22 @@ _IMMUTABLE_MSG = (
44
44
  )
45
45
 
46
46
 
47
+ def _auto_clear_first_run(session) -> None:
48
+ """
49
+ Silently empty FIRST_RUN.md after the first successful write operation.
50
+
51
+ This marks onboarding as complete without requiring the model to do it
52
+ explicitly. Safe to call on every write - does nothing once the file
53
+ is already empty.
54
+ """
55
+ try:
56
+ fr = session.workspace.first_run_file
57
+ if fr.exists() and fr.read_text(encoding="utf-8").strip():
58
+ fr.write_text("", encoding="utf-8")
59
+ except Exception: # noqa: BLE001
60
+ pass # Never let this interfere with the actual write result
61
+
62
+
47
63
  def sync_after_edit(
48
64
  session,
49
65
  resolved: Path,
@@ -80,4 +96,5 @@ def sync_after_edit(
80
96
  base_payload["format_reminder"] = RELATIONS_FORMAT_REMINDER
81
97
  if relation_sync_result:
82
98
  base_payload["relations_synced"] = relation_sync_result
99
+ _auto_clear_first_run(session)
83
100
  return ok(base_payload)
@@ -276,4 +276,6 @@ def run(
276
276
  sync.sync_file(daily_path, session.index, session.provider, session.config.chunking)
277
277
 
278
278
  result["mode"] = "append"
279
- return ok(result)
279
+ from groundmemory.tools.base import _auto_clear_first_run
280
+ _auto_clear_first_run(session)
281
+ return ok(result)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "groundmemory"
3
- version = "0.3.2"
3
+ version = "0.3.4"
4
4
  description = "Persistent, semantic memory for AI agents - mcp-native, local-first, framework-agnostic, production-ready"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -435,16 +435,19 @@ class TestReplaceLines:
435
435
  )
436
436
  assert r["status"] == "error"
437
437
 
438
- def test_replace_lines_error_end_line_beyond_file(self, session):
438
+ def test_replace_lines_end_line_beyond_file_clamped(self, session):
439
+ """end_line beyond file length is silently clamped to the last line."""
439
440
  _write_user(session, "a\nb\n")
440
441
  r = session.execute_tool(
441
442
  "memory_write",
442
443
  file="USER.md",
443
444
  start_line=1,
444
445
  end_line=5,
445
- content="x",
446
+ content="CLAMPED",
446
447
  )
447
- assert r["status"] == "error"
448
+ assert r["status"] == "ok"
449
+ content = _get(session, "USER.md")
450
+ assert "CLAMPED" in content
448
451
 
449
452
 
450
453
  class TestReplaceLinesImmutable:
@@ -313,4 +313,49 @@ class TestMemoryWriteDelete:
313
313
  content = _read(session, "AGENTS.md")
314
314
  assert "Rule B." not in content
315
315
  assert "Rule A." in content
316
- assert "Rule C." in content
316
+ assert "Rule C." in content
317
+
318
+
319
+ # ===========================================================================
320
+ # FIRST_RUN.md auto-clear
321
+ # ===========================================================================
322
+
323
+
324
+ class TestFirstRunAutoCleared:
325
+ """FIRST_RUN.md must be emptied automatically on the first successful write."""
326
+
327
+ def test_first_run_cleared_after_append(self, session):
328
+ assert session.workspace.first_run_file.read_text(encoding="utf-8").strip() != ""
329
+ session.execute_tool("memory_write", file="USER.md", content="Name is Alice.")
330
+ assert session.workspace.first_run_file.read_text(encoding="utf-8").strip() == ""
331
+
332
+ def test_first_run_cleared_after_replace_text(self, session):
333
+ _write_user(session, "old name\n")
334
+ assert session.workspace.first_run_file.read_text(encoding="utf-8").strip() != ""
335
+ session.execute_tool("memory_write", file="USER.md", search="old name", content="new name")
336
+ assert session.workspace.first_run_file.read_text(encoding="utf-8").strip() == ""
337
+
338
+ def test_first_run_cleared_after_replace_lines(self, session):
339
+ _write_user(session, "line one\nline two\n")
340
+ assert session.workspace.first_run_file.read_text(encoding="utf-8").strip() != ""
341
+ session.execute_tool("memory_write", file="USER.md", start_line=1, end_line=1, content="replaced")
342
+ assert session.workspace.first_run_file.read_text(encoding="utf-8").strip() == ""
343
+
344
+ def test_first_run_cleared_after_delete(self, session):
345
+ _write_user(session, "line one\nline two\n")
346
+ assert session.workspace.first_run_file.read_text(encoding="utf-8").strip() != ""
347
+ session.execute_tool("memory_write", file="USER.md", start_line=1, end_line=1, content="")
348
+ assert session.workspace.first_run_file.read_text(encoding="utf-8").strip() == ""
349
+
350
+ def test_first_run_already_empty_stays_empty(self, session):
351
+ session.workspace.first_run_file.write_text("", encoding="utf-8")
352
+ session.execute_tool("memory_write", file="USER.md", content="Another entry.")
353
+ assert session.workspace.first_run_file.read_text(encoding="utf-8").strip() == ""
354
+
355
+ def test_failed_write_does_not_clear_first_run(self, session):
356
+ """A failed write must not trigger the auto-clear."""
357
+ assert session.workspace.first_run_file.read_text(encoding="utf-8").strip() != ""
358
+ # This write will fail (empty content)
359
+ r = session.execute_tool("memory_write", file="USER.md", content="")
360
+ assert r["status"] == "error"
361
+ assert session.workspace.first_run_file.read_text(encoding="utf-8").strip() != ""
@@ -121,6 +121,8 @@ class TestSessionBootstrap:
121
121
  # An empty workspace should produce minimal/empty bootstrap
122
122
 
123
123
  def test_bootstrap_includes_long_term_memory(self, session):
124
+ # Simulate post-onboarding: FIRST_RUN.md must be empty so MEMORY.md is injected
125
+ session.workspace.first_run_file.write_text("", encoding="utf-8")
124
126
  content = "Bootstrap long term fact."
125
127
  session.execute_tool("memory_write", file="MEMORY.md", content=content)
126
128
  result = session.bootstrap()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes