smart-claude-memory-mcp 2.1.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.
Files changed (104) hide show
  1. package/.claude-plugin/plugin.json +38 -0
  2. package/CHANGELOG.md +52 -0
  3. package/LICENSE +21 -0
  4. package/README.md +790 -0
  5. package/dist/chunker.js +33 -0
  6. package/dist/chunker.js.map +1 -0
  7. package/dist/config.js +23 -0
  8. package/dist/config.js.map +1 -0
  9. package/dist/curriculum/daemon.js +190 -0
  10. package/dist/curriculum/daemon.js.map +1 -0
  11. package/dist/curriculum/scanner.js +237 -0
  12. package/dist/curriculum/scanner.js.map +1 -0
  13. package/dist/index.js +429 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/lib/migrations.js +128 -0
  16. package/dist/lib/migrations.js.map +1 -0
  17. package/dist/ollama.js +59 -0
  18. package/dist/ollama.js.map +1 -0
  19. package/dist/project-detect.js +102 -0
  20. package/dist/project-detect.js.map +1 -0
  21. package/dist/project.js +26 -0
  22. package/dist/project.js.map +1 -0
  23. package/dist/sleep/daemon.js +215 -0
  24. package/dist/sleep/daemon.js.map +1 -0
  25. package/dist/sleep/miner.js +285 -0
  26. package/dist/sleep/miner.js.map +1 -0
  27. package/dist/supabase.js +405 -0
  28. package/dist/supabase.js.map +1 -0
  29. package/dist/telemetry/emit.js +19 -0
  30. package/dist/telemetry/emit.js.map +1 -0
  31. package/dist/telemetry/pruner.js +141 -0
  32. package/dist/telemetry/pruner.js.map +1 -0
  33. package/dist/telemetry/types.js +2 -0
  34. package/dist/telemetry/types.js.map +1 -0
  35. package/dist/tools/backlog.js +599 -0
  36. package/dist/tools/backlog.js.map +1 -0
  37. package/dist/tools/batch-freeze-patterns.js +243 -0
  38. package/dist/tools/batch-freeze-patterns.js.map +1 -0
  39. package/dist/tools/bloat-audit.js +101 -0
  40. package/dist/tools/bloat-audit.js.map +1 -0
  41. package/dist/tools/checkpoint.js +259 -0
  42. package/dist/tools/checkpoint.js.map +1 -0
  43. package/dist/tools/compact.js +60 -0
  44. package/dist/tools/compact.js.map +1 -0
  45. package/dist/tools/conflict.js +102 -0
  46. package/dist/tools/conflict.js.map +1 -0
  47. package/dist/tools/curriculum.js +225 -0
  48. package/dist/tools/curriculum.js.map +1 -0
  49. package/dist/tools/frozen-cache.js +106 -0
  50. package/dist/tools/frozen-cache.js.map +1 -0
  51. package/dist/tools/health.js +368 -0
  52. package/dist/tools/health.js.map +1 -0
  53. package/dist/tools/hygiene.js +309 -0
  54. package/dist/tools/hygiene.js.map +1 -0
  55. package/dist/tools/image.js +107 -0
  56. package/dist/tools/image.js.map +1 -0
  57. package/dist/tools/list-global-patterns.js +101 -0
  58. package/dist/tools/list-global-patterns.js.map +1 -0
  59. package/dist/tools/orchestrator.js +113 -0
  60. package/dist/tools/orchestrator.js.map +1 -0
  61. package/dist/tools/policy.js +90 -0
  62. package/dist/tools/policy.js.map +1 -0
  63. package/dist/tools/refactor.js +220 -0
  64. package/dist/tools/refactor.js.map +1 -0
  65. package/dist/tools/save.js +42 -0
  66. package/dist/tools/save.js.map +1 -0
  67. package/dist/tools/search.js +189 -0
  68. package/dist/tools/search.js.map +1 -0
  69. package/dist/tools/setup.js +868 -0
  70. package/dist/tools/setup.js.map +1 -0
  71. package/dist/tools/shared-schemas.js +24 -0
  72. package/dist/tools/shared-schemas.js.map +1 -0
  73. package/dist/tools/skills.js +174 -0
  74. package/dist/tools/skills.js.map +1 -0
  75. package/dist/tools/sleep.js +239 -0
  76. package/dist/tools/sleep.js.map +1 -0
  77. package/dist/tools/sovereign-constitution.js +319 -0
  78. package/dist/tools/sovereign-constitution.js.map +1 -0
  79. package/dist/tools/summarize.js +55 -0
  80. package/dist/tools/summarize.js.map +1 -0
  81. package/dist/tools/sync.js +255 -0
  82. package/dist/tools/sync.js.map +1 -0
  83. package/dist/tools/system_dashboard.js +181 -0
  84. package/dist/tools/system_dashboard.js.map +1 -0
  85. package/dist/tools/update-rule.js +15 -0
  86. package/dist/tools/update-rule.js.map +1 -0
  87. package/dist/tools/verification.js +333 -0
  88. package/dist/tools/verification.js.map +1 -0
  89. package/dist/trajectory/daemon.js +270 -0
  90. package/dist/trajectory/daemon.js.map +1 -0
  91. package/dist/trajectory/stripper.js +124 -0
  92. package/dist/trajectory/stripper.js.map +1 -0
  93. package/dist/trajectory/summarizer.js +77 -0
  94. package/dist/trajectory/summarizer.js.map +1 -0
  95. package/dist/transactions/checkpoint.js +272 -0
  96. package/dist/transactions/checkpoint.js.map +1 -0
  97. package/dist/verification-gate.js +43 -0
  98. package/dist/verification-gate.js.map +1 -0
  99. package/dist/version.js +16 -0
  100. package/dist/version.js.map +1 -0
  101. package/hooks/README.md +54 -0
  102. package/hooks/md-policy.py +497 -0
  103. package/marketplace.json +13 -0
  104. package/package.json +66 -0
@@ -0,0 +1,497 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Zero-Local-MD policy + Guardian hook (PreToolUse for Write|Edit|Bash).
4
+
5
+ Enforces four rules:
6
+ 1. Zero-Local-MD: only CLAUDE.md / README.md / ARCHITECTURE.md allowed at project root.
7
+ 2. 750-line hard limit: block writes that PUSH a file past 750 lines. Files already
8
+ over the limit are "grandfathered" — edits allowed with a warning banner.
9
+ 3. Frozen features: for files matching a configured pattern, block `Write`; Edit only.
10
+ 4. Hard Stop / Manual Test Gate: if a pending-verification flag file exists, block
11
+ Write/Edit/Bash until confirm_verification clears it.
12
+
13
+ Environment variables:
14
+ CLAUDE_MD_POLICY_WORKSPACE absolute path of the project root (required for MD rule)
15
+ CLAUDE_MD_POLICY_ALLOW_ROOT_MD comma-separated allowlist (default: CLAUDE.md,README.md,ARCHITECTURE.md)
16
+ CLAUDE_MD_POLICY_TOKEN_LIMIT soft token limit for CLAUDE.md/MEMORY.md (default 3000)
17
+ SMART_CLAUDE_MEMORY_GATE_DIR where the verification flag lives (default: ~/.claude-memory)
18
+ SMART_CLAUDE_MEMORY_LINE_LIMIT override the 750-line limit
19
+ SMART_CLAUDE_MEMORY_FROZEN_PATTERNS comma-separated substrings that mark a file as frozen
20
+ SMART_CLAUDE_MEMORY_ORCHESTRATOR_MODE hard-block direct Write/Edit/Bash in the Orchestrator session
21
+
22
+ Legacy CLAUDE_MEMORY_* names are still honored as a one-time fallback; will be removed in v1.2.0.
23
+ """
24
+ from __future__ import annotations
25
+ import json
26
+ import os
27
+ import shutil
28
+ import sys
29
+ from datetime import datetime, timezone
30
+ from pathlib import Path
31
+
32
+ ALLOW_ROOT_DEFAULT = "CLAUDE.md,README.md,ARCHITECTURE.md"
33
+ BYTES_PER_TOKEN = 4
34
+
35
+ # Auto-generated files that bypass the 750-line hygiene check entirely.
36
+ EXCLUDE_EXACT_BASENAMES = {"types.ts"}
37
+ EXCLUDE_SUFFIXES = (".arb", ".l10n.dart", ".g.dart", ".freezed.dart")
38
+
39
+
40
+ def is_excluded(path: Path) -> bool:
41
+ name = path.name.lower()
42
+ if path.name in EXCLUDE_EXACT_BASENAMES:
43
+ return True
44
+ return any(name.endswith(suf) for suf in EXCLUDE_SUFFIXES)
45
+
46
+
47
+ def env_path(name: str) -> Path | None:
48
+ raw = os.environ.get(name, "")
49
+ if not raw:
50
+ return None
51
+ try:
52
+ return Path(raw).expanduser().resolve()
53
+ except OSError:
54
+ return None
55
+
56
+
57
+ def allow_root() -> set[str]:
58
+ raw = os.environ.get("CLAUDE_MD_POLICY_ALLOW_ROOT_MD", ALLOW_ROOT_DEFAULT)
59
+ return {n.strip() for n in raw.split(",") if n.strip()}
60
+
61
+
62
+ def token_soft_limit() -> int:
63
+ try:
64
+ return int(os.environ.get("CLAUDE_MD_POLICY_TOKEN_LIMIT", "3000"))
65
+ except ValueError:
66
+ return 3000
67
+
68
+
69
+ def line_hard_limit() -> int:
70
+ # TODO(v1.2.0): drop the legacy CLAUDE_MEMORY_LINE_LIMIT fallback.
71
+ raw = os.environ.get(
72
+ "SMART_CLAUDE_MEMORY_LINE_LIMIT",
73
+ os.environ.get("CLAUDE_MEMORY_LINE_LIMIT", "750"),
74
+ )
75
+ try:
76
+ return int(raw)
77
+ except ValueError:
78
+ return 750
79
+
80
+
81
+ import re as _re
82
+ import unicodedata as _unicodedata
83
+
84
+
85
+ def _slugify(s: str) -> str:
86
+ """Must mirror src/project.ts slugify() so cache project keys line up."""
87
+ s = _unicodedata.normalize("NFKD", s or "").lower()
88
+ s = "".join(c for c in s if not _unicodedata.combining(c))
89
+ s = _re.sub(r"[^a-z0-9\s_-]", "", s)
90
+ s = s.strip()
91
+ s = _re.sub(r"[\s_]+", "-", s)
92
+ s = _re.sub(r"-+", "-", s)
93
+ s = s.strip("-")
94
+ return s or "default"
95
+
96
+
97
+ def frozen_patterns_env() -> list[str]:
98
+ # TODO(v1.2.0): drop the legacy CLAUDE_MEMORY_FROZEN_PATTERNS fallback.
99
+ raw = os.environ.get(
100
+ "SMART_CLAUDE_MEMORY_FROZEN_PATTERNS",
101
+ os.environ.get("CLAUDE_MEMORY_FROZEN_PATTERNS", ""),
102
+ )
103
+ return [p.strip() for p in raw.split(",") if p.strip()]
104
+
105
+
106
+ def frozen_patterns_cache() -> list[str]:
107
+ """Read patterns the MCP server exported for the current workspace.
108
+
109
+ The MCP server writes ~/.claude-memory/frozen-patterns.json on startup
110
+ and after every addFrozenPattern/removeFrozenPattern call. Reading that
111
+ file is cheap; spawning a pg client per hook invocation would not be.
112
+ """
113
+ cache = gate_dir() / "frozen-patterns.json"
114
+ if not cache.exists():
115
+ return []
116
+ ws = env_path("CLAUDE_MD_POLICY_WORKSPACE")
117
+ if ws is None:
118
+ return []
119
+ project_id = _slugify(ws.name)
120
+ try:
121
+ data = json.loads(cache.read_text("utf8"))
122
+ except (OSError, ValueError):
123
+ return []
124
+ entries = (data.get("projects") or {}).get(project_id, [])
125
+ out: list[str] = []
126
+ for e in entries:
127
+ if isinstance(e, str):
128
+ out.append(e)
129
+ elif isinstance(e, dict) and isinstance(e.get("pattern"), str):
130
+ out.append(e["pattern"])
131
+ return out
132
+
133
+
134
+ def frozen_patterns() -> list[str]:
135
+ """Combined list: env-var configured + cloud-synced. Deduplicated."""
136
+ seen: set[str] = set()
137
+ merged: list[str] = []
138
+ for p in frozen_patterns_env() + frozen_patterns_cache():
139
+ if p and p not in seen:
140
+ seen.add(p)
141
+ merged.append(p)
142
+ return merged
143
+
144
+
145
+ def gate_dir() -> Path:
146
+ # TODO(v1.2.0): drop the legacy CLAUDE_MEMORY_GATE_DIR fallback.
147
+ # The on-disk dir `~/.claude-memory` is intentionally preserved to keep existing backups discoverable.
148
+ raw = os.environ.get(
149
+ "SMART_CLAUDE_MEMORY_GATE_DIR",
150
+ os.environ.get("CLAUDE_MEMORY_GATE_DIR", ""),
151
+ )
152
+ if raw:
153
+ return Path(raw).expanduser()
154
+ return Path.home() / ".claude-memory"
155
+
156
+
157
+ def flag_path() -> Path:
158
+ return gate_dir() / "verification-pending.json"
159
+
160
+
161
+ def backup_root() -> Path:
162
+ return gate_dir() / "backups"
163
+
164
+
165
+ def backup_index_path() -> Path:
166
+ return gate_dir() / "backup-index.json"
167
+
168
+
169
+ def _iso_timestamp_for_path() -> str:
170
+ return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H-%M-%S-%fZ")
171
+
172
+
173
+ def _update_backup_index(source: str, backup: str, tool_name: str, ts: str) -> None:
174
+ """Append-or-replace a {source → latest-backup} record the TS side reads
175
+ to tell Claude where to recover from if verification fails. Never throws."""
176
+ idx_path = backup_index_path()
177
+ try:
178
+ if idx_path.exists():
179
+ data = json.loads(idx_path.read_text("utf8"))
180
+ else:
181
+ data = {"entries": {}}
182
+ except (OSError, ValueError):
183
+ data = {"entries": {}}
184
+ entries = data.setdefault("entries", {})
185
+ entries[source] = {"backup": backup, "tool": tool_name, "timestamp": ts}
186
+ data["updated_at"] = ts
187
+ try:
188
+ idx_path.parent.mkdir(parents=True, exist_ok=True)
189
+ idx_path.write_text(json.dumps(data, indent=2), "utf8")
190
+ except OSError:
191
+ pass
192
+
193
+
194
+ def _make_backup(target: Path, tool_name: str) -> tuple[str | None, str | None]:
195
+ """Copy target into ~/.claude-memory/backups/<project>/<ts>/<relpath>.
196
+ Returns (backup_path, error_message). Both None if nothing to back up."""
197
+ if not target.exists() or not target.is_file():
198
+ return (None, None)
199
+
200
+ ws = env_path("CLAUDE_MD_POLICY_WORKSPACE")
201
+ project_slug = _slugify(ws.name) if ws is not None else "default"
202
+ ts = _iso_timestamp_for_path()
203
+
204
+ try:
205
+ rel = target.relative_to(ws) if ws is not None else Path(target.name)
206
+ except ValueError:
207
+ rel = Path(target.name)
208
+
209
+ dest = backup_root() / project_slug / ts / rel
210
+ try:
211
+ dest.parent.mkdir(parents=True, exist_ok=True)
212
+ shutil.copy2(str(target), str(dest))
213
+ except OSError as e:
214
+ return (None, f"backup copy failed: {e}")
215
+
216
+ _update_backup_index(str(target), str(dest), tool_name, ts)
217
+ return (str(dest), None)
218
+
219
+
220
+ def _target_matches_frozen(target: Path) -> bool:
221
+ target_str = str(target).replace("\\", "/")
222
+ for pat in frozen_patterns():
223
+ if pat and pat in target_str:
224
+ return True
225
+ return False
226
+
227
+
228
+ # ─── checks ────────────────────────────────────────────────────────────────
229
+
230
+ def check_orchestrator_advisory(tool_name: str) -> dict | None:
231
+ """Hard-block enforcement for the Orchestrator pattern (v1.1.0).
232
+
233
+ Enable with SMART_CLAUDE_MEMORY_ORCHESTRATOR_MODE=1. Hard-blocks direct
234
+ Write/Edit/Bash calls in the main session, forcing delegation to a
235
+ worker sub-agent via the delegate_task MCP tool. Sub-agents set
236
+ SMART_CLAUDE_MEMORY_ORCHESTRATOR_MODE=0 in their spawned env so this
237
+ hook does not trip them.
238
+
239
+ Legacy CLAUDE_MEMORY_ORCHESTRATOR_MODE is honored as a one-time fallback;
240
+ TODO(v1.2.0): drop the legacy name.
241
+ """
242
+ mode = os.environ.get(
243
+ "SMART_CLAUDE_MEMORY_ORCHESTRATOR_MODE",
244
+ os.environ.get("CLAUDE_MEMORY_ORCHESTRATOR_MODE", "0"),
245
+ )
246
+ if mode not in ("1", "true", "yes"):
247
+ return None
248
+ if tool_name not in {"Write", "Edit", "Bash"}:
249
+ return None
250
+ return {
251
+ "decision": "block",
252
+ "reason": (
253
+ "Orchestrator mode is ON — direct "
254
+ f"{tool_name} in the main session is blocked. Delegate this work "
255
+ "to a worker sub-agent via the delegate_task MCP tool instead."
256
+ ),
257
+ }
258
+
259
+
260
+ def check_verification_gate(tool_name: str) -> dict | None:
261
+ """If a pending-verification flag exists, block all destructive tools."""
262
+ if tool_name not in {"Write", "Edit", "Bash"}:
263
+ return None
264
+ fp = flag_path()
265
+ if not fp.exists():
266
+ return None
267
+ try:
268
+ payload = json.loads(fp.read_text("utf8"))
269
+ except (OSError, ValueError):
270
+ payload = {}
271
+ return {
272
+ "decision": "block",
273
+ "reason": (
274
+ "Hard Stop: a pending manual-verification gate is active. "
275
+ "After the most recent code change, you must manually confirm it works, "
276
+ "then call confirm_verification({success:true}) to clear this gate. "
277
+ f"Pending flag: {fp}. Details: {json.dumps(payload)[:200]}"
278
+ ),
279
+ }
280
+
281
+
282
+ def check_md_policy(target: Path) -> dict | None:
283
+ ws = env_path("CLAUDE_MD_POLICY_WORKSPACE")
284
+ if ws is None:
285
+ return None
286
+ if target.suffix.lower() != ".md":
287
+ return None
288
+ try:
289
+ target.relative_to(ws)
290
+ except ValueError:
291
+ return None
292
+ if target.parent != ws:
293
+ return {
294
+ "decision": "block",
295
+ "reason": (
296
+ f"Zero-Local-MD policy: `{target.name}` is outside the allowed root ({ws}). "
297
+ "Store it in cloud memory via save_memory or sync_local_memory."
298
+ ),
299
+ }
300
+ if target.name not in allow_root():
301
+ return {
302
+ "decision": "block",
303
+ "reason": (
304
+ f"Zero-Local-MD policy: only {sorted(allow_root())} are allowed at the root. "
305
+ f"`{target.name}` must live in cloud memory."
306
+ ),
307
+ }
308
+ return None
309
+
310
+
311
+ def check_line_limit(target: Path, tool_input: dict, tool_name: str) -> dict | None:
312
+ if is_excluded(target):
313
+ return None # auto-generated files bypass hygiene entirely
314
+
315
+ limit = line_hard_limit()
316
+ current_lines = 0
317
+ if target.exists():
318
+ try:
319
+ current_lines = target.read_text("utf8").count("\n") + 1
320
+ except OSError:
321
+ current_lines = 0
322
+
323
+ # Project the new line count.
324
+ if tool_name == "Write":
325
+ incoming = tool_input.get("content", "") or ""
326
+ projected = incoming.count("\n") + 1 if incoming else 0
327
+ else: # Edit
328
+ old_string = tool_input.get("old_string", "") or ""
329
+ new_string = tool_input.get("new_string", "") or ""
330
+ replace_all = bool(tool_input.get("replace_all", False))
331
+ old_nl = old_string.count("\n")
332
+ new_nl = new_string.count("\n")
333
+ if replace_all and old_string and target.exists():
334
+ try:
335
+ text = target.read_text("utf8")
336
+ occurrences = text.count(old_string)
337
+ except OSError:
338
+ occurrences = 1
339
+ projected = current_lines + (new_nl - old_nl) * occurrences
340
+ else:
341
+ projected = current_lines + (new_nl - old_nl)
342
+
343
+ # Grandfather rule — file was already oversized; edits allowed, with warning.
344
+ if current_lines > limit:
345
+ return {
346
+ "decision": "allow",
347
+ "warning": (
348
+ f"Grandfathered file: `{target.name}` is {current_lines} lines "
349
+ f"(over the {limit}-line limit). Edit permitted, but please prioritize "
350
+ f"splitting — run check_code_hygiene({{paths:['{target.as_posix()}']}}) "
351
+ "for an automatic refactor plan."
352
+ ),
353
+ }
354
+
355
+ # Ceiling rule — was under the limit; this operation must not cross it.
356
+ if projected > limit:
357
+ return {
358
+ "decision": "block",
359
+ "reason": (
360
+ f"block_and_refactor: `{target.name}` was {current_lines} lines (≤ {limit}); "
361
+ f"this {tool_name} would take it to ~{projected} lines, crossing the hard limit. "
362
+ f"Split the file first — run check_code_hygiene({{paths:['{target.as_posix()}']}}) "
363
+ "for a split plan, then apply the refactor before re-attempting this edit."
364
+ ),
365
+ }
366
+ return None
367
+
368
+
369
+ def check_frozen(target: Path, tool_name: str) -> dict | None:
370
+ patterns = frozen_patterns()
371
+ if not patterns:
372
+ return None
373
+ target_str = str(target).replace("\\", "/")
374
+ for pat in patterns:
375
+ if pat and pat in target_str:
376
+ if tool_name == "Write":
377
+ return {
378
+ "decision": "block",
379
+ "reason": (
380
+ "FROZEN: Use 'Edit' for surgical changes. If a full Refactor "
381
+ "is needed, justify it to the user and request an unfreeze. "
382
+ f"(Pattern: '{pat}')"
383
+ ),
384
+ }
385
+ # Edit on a frozen file is the intended path — allow silently.
386
+ break
387
+ return None
388
+
389
+
390
+ def check_memory_file_size(target: Path, incoming: str) -> dict | None:
391
+ if target.name not in {"CLAUDE.md", "MEMORY.md"}:
392
+ return None
393
+ est = len(incoming) // BYTES_PER_TOKEN
394
+ limit = token_soft_limit()
395
+ if est > limit:
396
+ return {
397
+ "decision": "allow",
398
+ "warning": (
399
+ f"{target.name} is ~{est} tokens, over the {limit} soft limit. "
400
+ "Consider calling summarize_memory_file to compress it back under the limit."
401
+ ),
402
+ }
403
+ return None
404
+
405
+
406
+ # ─── orchestration ────────────────────────────────────────────────────────
407
+
408
+ def decide(tool_name: str, tool_input: dict) -> dict:
409
+ # 0. Orchestrator-mode hard-block (v1.1.0). Opt-in via env var.
410
+ orch = check_orchestrator_advisory(tool_name)
411
+ if orch is not None and orch.get("decision") == "block":
412
+ return orch
413
+
414
+ # 1. Verification gate trumps everything (applies to Write/Edit/Bash).
415
+ gate = check_verification_gate(tool_name)
416
+ if gate is not None:
417
+ return gate
418
+
419
+ if tool_name not in {"Write", "Edit"}:
420
+ return {"decision": "allow"}
421
+
422
+ raw_path = tool_input.get("file_path") or tool_input.get("path") or ""
423
+ if not raw_path:
424
+ return {"decision": "allow"}
425
+ try:
426
+ target = Path(raw_path).resolve()
427
+ except OSError:
428
+ return {"decision": "allow"}
429
+
430
+ incoming = tool_input.get("content") or tool_input.get("new_string") or ""
431
+
432
+ # 2. Zero-Local-MD policy
433
+ r = check_md_policy(target)
434
+ if r is not None:
435
+ return r
436
+
437
+ # 3. Frozen features
438
+ r = check_frozen(target, tool_name)
439
+ if r is not None:
440
+ return r
441
+
442
+ # 4. 750-line rule (source files only; bypassed for binaries, images, and
443
+ # auto-generated files matching is_excluded()).
444
+ if target.suffix.lower() in {
445
+ ".ts", ".tsx", ".js", ".jsx",
446
+ ".py", ".sql", ".md",
447
+ ".json", ".yaml", ".yml", ".toml",
448
+ ".dart",
449
+ }:
450
+ r = check_line_limit(target, tool_input, tool_name)
451
+ if r is not None:
452
+ return r
453
+
454
+ # 5. CLAUDE.md / MEMORY.md size advisory
455
+ size_warning = check_memory_file_size(target, incoming)
456
+
457
+ # 6. Mandatory backup before the edit/write goes through.
458
+ # Write → always (full refactor risk).
459
+ # Edit → only on frozen files (the surgical path, but still worth a snapshot).
460
+ # Skipped if the file doesn't exist yet (nothing to back up).
461
+ backup_warning = None
462
+ if target.exists() and target.is_file():
463
+ should_backup = tool_name == "Write" or (
464
+ tool_name == "Edit" and _target_matches_frozen(target)
465
+ )
466
+ if should_backup:
467
+ bp, err = _make_backup(target, tool_name)
468
+ if bp:
469
+ backup_warning = (
470
+ f"Backup saved before {tool_name}: {bp}. "
471
+ "If verification fails, read this file to restore the prior state."
472
+ )
473
+ elif err:
474
+ backup_warning = f"Backup FAILED for {target.name}: {err} — proceed with caution."
475
+
476
+ warnings = [w for w in (
477
+ (size_warning or {}).get("warning"),
478
+ backup_warning,
479
+ ) if w]
480
+
481
+ if warnings:
482
+ return {"decision": "allow", "warning": " | ".join(warnings)}
483
+ return {"decision": "allow"}
484
+
485
+
486
+ def main() -> None:
487
+ try:
488
+ payload = json.load(sys.stdin)
489
+ except (json.JSONDecodeError, ValueError):
490
+ sys.stdout.write(json.dumps({"decision": "allow"}))
491
+ return
492
+ result = decide(payload.get("tool_name", ""), payload.get("tool_input", {}) or {})
493
+ sys.stdout.write(json.dumps(result))
494
+
495
+
496
+ if __name__ == "__main__":
497
+ main()
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "smart-claude-memory",
3
+ "version": "2.1.0",
4
+ "description": "Sovereign memory protocol for Claude Code — typed, dual-scope, observability-grade. Bring your own empty Supabase project; the plugin handles the rest.",
5
+ "author": {
6
+ "name": "NABILNET.AI",
7
+ "url": "https://nabilnet.ai"
8
+ },
9
+ "homepage": "https://nabilnet.ai",
10
+ "license": "MIT",
11
+ "keywords": ["claude-code", "memory", "mcp", "supabase", "ollama", "sovereign", "observability"],
12
+ "categories": ["memory", "observability"]
13
+ }
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "smart-claude-memory-mcp",
3
+ "version": "2.1.0",
4
+ "description": "Sovereign memory protocol for Claude Code — typed, dual-scope, observability-grade. Bring your own empty Supabase project; the plugin handles the rest.",
5
+ "author": "NABILNET.AI <https://nabilnet.ai>",
6
+ "license": "MIT",
7
+ "homepage": "https://nabilnet.ai",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/NABILNET-ORG/Smart-Claude-Memory.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/NABILNET-ORG/Smart-Claude-Memory/issues"
14
+ },
15
+ "keywords": [
16
+ "claude-code",
17
+ "memory",
18
+ "mcp",
19
+ "supabase",
20
+ "ollama",
21
+ "sovereign",
22
+ "observability"
23
+ ],
24
+ "type": "module",
25
+ "files": [
26
+ "dist/",
27
+ "hooks/",
28
+ ".claude-plugin/",
29
+ "README.md",
30
+ "LICENSE",
31
+ "CHANGELOG.md",
32
+ "marketplace.json"
33
+ ],
34
+ "bin": {
35
+ "smart-claude-memory-mcp": "dist/index.js"
36
+ },
37
+ "scripts": {
38
+ "lint:boundaries": "tsx scripts/lint-boundaries.ts",
39
+ "build": "npm run lint:boundaries && tsc",
40
+ "dev": "tsx src/index.ts",
41
+ "start": "node dist/index.js",
42
+ "schema": "tsx scripts/apply-schema.ts",
43
+ "backup": "tsx scripts/backup-and-remove.ts",
44
+ "test": "node --import tsx --experimental-test-module-mocks --no-warnings --test tests/trajectory-stripper.test.ts tests/trajectory-summarizer.test.ts tests/trajectory-daemon.test.ts tests/health.test.ts tests/migrations.test.ts tests/list-global-patterns.test.ts tests/capabilities.test.ts"
45
+ },
46
+ "engines": {
47
+ "node": ">=20"
48
+ },
49
+ "dependencies": {
50
+ "@modelcontextprotocol/sdk": "^1.29.0",
51
+ "@supabase/supabase-js": "^2.104.0",
52
+ "dotenv": "^17.4.2",
53
+ "glob": "^13.0.6",
54
+ "ollama": "^0.6.3",
55
+ "pg": "^8.20.0",
56
+ "zod": "^4.3.6"
57
+ },
58
+ "devDependencies": {
59
+ "@types/archiver": "^7.0.0",
60
+ "@types/node": "^25.6.0",
61
+ "@types/pg": "^8.20.0",
62
+ "archiver": "^7.0.1",
63
+ "tsx": "^4.21.0",
64
+ "typescript": "^6.0.3"
65
+ }
66
+ }