claude-memory-agent 3.0.2 → 3.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.
- package/dashboard.html +88 -38
- package/hooks/auto-detect-response.py +3 -6
- package/hooks/detect-correction.py +8 -7
- package/hooks/grounding-hook.py +19 -3
- package/hooks/log-tool-use.py +3 -6
- package/hooks/log-user-request.py +14 -6
- package/hooks/pre-tool-decision.py +3 -6
- package/hooks/session_end_hook.py +37 -0
- package/hooks/session_start.py +10 -0
- package/hooks/stop_hook.py +123 -0
- package/install.py +139 -44
- package/main.py +133 -13
- package/mcp_proxy.py +6 -0
- package/package.json +2 -2
- package/services/agent_registry.py +260 -12
- package/services/database.py +188 -1
- package/services/soul.py +467 -0
package/install.py
CHANGED
|
@@ -542,11 +542,18 @@ def setup_hooks(config: Dict[str, str]) -> bool:
|
|
|
542
542
|
print_warning("Hooks directory not found in agent, skipping hook setup")
|
|
543
543
|
return True
|
|
544
544
|
|
|
545
|
-
# Hooks to install
|
|
545
|
+
# Hooks to install (all 8 active hooks)
|
|
546
546
|
hooks_to_install = [
|
|
547
547
|
"session_start.py",
|
|
548
|
-
"
|
|
548
|
+
"session_end_hook.py",
|
|
549
|
+
"stop_hook.py",
|
|
549
550
|
"grounding-hook.py",
|
|
551
|
+
"log-user-request.py",
|
|
552
|
+
"detect-correction.py",
|
|
553
|
+
"pre-tool-decision.py",
|
|
554
|
+
"log-tool-use.py",
|
|
555
|
+
"auto-detect-response.py",
|
|
556
|
+
"extract_memories.py", # Required by session_end_hook
|
|
550
557
|
]
|
|
551
558
|
|
|
552
559
|
installed = 0
|
|
@@ -574,54 +581,126 @@ def setup_hooks(config: Dict[str, str]) -> bool:
|
|
|
574
581
|
|
|
575
582
|
|
|
576
583
|
def configure_hooks_json(auto: bool = False) -> bool:
|
|
577
|
-
"""Configure hooks.json
|
|
578
|
-
hooks_file = get_claude_settings_dir() / "hooks.json"
|
|
584
|
+
"""Configure hooks in settings.json (Claude Code hook format).
|
|
579
585
|
|
|
580
|
-
|
|
586
|
+
Writes hooks config directly into ~/.claude/settings.json
|
|
587
|
+
using the correct Claude Code settings format.
|
|
588
|
+
"""
|
|
589
|
+
settings_file = get_claude_settings_file()
|
|
590
|
+
python_exe = sys.executable
|
|
591
|
+
hooks_dir = get_hooks_dir()
|
|
592
|
+
|
|
593
|
+
# Build the hooks config in Claude Code settings.json format
|
|
581
594
|
hooks_config = {
|
|
582
|
-
"
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
595
|
+
"UserPromptSubmit": [
|
|
596
|
+
{
|
|
597
|
+
"matcher": "",
|
|
598
|
+
"hooks": [
|
|
599
|
+
{
|
|
600
|
+
"type": "command",
|
|
601
|
+
"command": f"{python_exe} {hooks_dir / 'detect-correction.py'}",
|
|
602
|
+
"async": True,
|
|
603
|
+
},
|
|
604
|
+
{
|
|
605
|
+
"type": "command",
|
|
606
|
+
"command": f"{python_exe} {hooks_dir / 'log-user-request.py'}",
|
|
607
|
+
"async": True,
|
|
608
|
+
},
|
|
609
|
+
{
|
|
610
|
+
"type": "command",
|
|
611
|
+
"command": f"{python_exe} {hooks_dir / 'grounding-hook.py'}",
|
|
612
|
+
},
|
|
613
|
+
],
|
|
614
|
+
}
|
|
615
|
+
],
|
|
616
|
+
"PreToolUse": [
|
|
617
|
+
{
|
|
618
|
+
"matcher": "Edit|Write|Bash|Task",
|
|
619
|
+
"hooks": [
|
|
620
|
+
{
|
|
621
|
+
"type": "command",
|
|
622
|
+
"command": f"{python_exe} {hooks_dir / 'pre-tool-decision.py'}",
|
|
623
|
+
"async": True,
|
|
624
|
+
}
|
|
625
|
+
],
|
|
626
|
+
}
|
|
627
|
+
],
|
|
628
|
+
"PostToolUse": [
|
|
629
|
+
{
|
|
630
|
+
"matcher": "Edit|Write|Bash|Read",
|
|
631
|
+
"hooks": [
|
|
632
|
+
{
|
|
633
|
+
"type": "command",
|
|
634
|
+
"command": f"{python_exe} {hooks_dir / 'log-tool-use.py'}",
|
|
635
|
+
"async": True,
|
|
636
|
+
}
|
|
637
|
+
],
|
|
638
|
+
}
|
|
639
|
+
],
|
|
640
|
+
"Stop": [
|
|
641
|
+
{
|
|
642
|
+
"matcher": "",
|
|
643
|
+
"hooks": [
|
|
644
|
+
{
|
|
645
|
+
"type": "command",
|
|
646
|
+
"command": f"{python_exe} {hooks_dir / 'stop_hook.py'}",
|
|
647
|
+
"async": True,
|
|
648
|
+
},
|
|
649
|
+
{
|
|
650
|
+
"type": "command",
|
|
651
|
+
"command": f"{python_exe} {hooks_dir / 'auto-detect-response.py'}",
|
|
652
|
+
"async": True,
|
|
653
|
+
},
|
|
654
|
+
],
|
|
655
|
+
}
|
|
656
|
+
],
|
|
657
|
+
"SessionStart": [
|
|
658
|
+
{
|
|
659
|
+
"matcher": "",
|
|
660
|
+
"hooks": [
|
|
661
|
+
{
|
|
662
|
+
"type": "command",
|
|
663
|
+
"command": f"{python_exe} {hooks_dir / 'session_start.py'}",
|
|
664
|
+
}
|
|
665
|
+
],
|
|
666
|
+
}
|
|
667
|
+
],
|
|
668
|
+
"SessionEnd": [
|
|
669
|
+
{
|
|
670
|
+
"matcher": "",
|
|
671
|
+
"hooks": [
|
|
672
|
+
{
|
|
673
|
+
"type": "command",
|
|
674
|
+
"command": f"{python_exe} {hooks_dir / 'session_end_hook.py'}",
|
|
675
|
+
"async": True,
|
|
676
|
+
}
|
|
677
|
+
],
|
|
678
|
+
}
|
|
679
|
+
],
|
|
603
680
|
}
|
|
604
681
|
|
|
605
|
-
#
|
|
606
|
-
|
|
682
|
+
# Load existing settings
|
|
683
|
+
settings = {}
|
|
684
|
+
if settings_file.exists():
|
|
607
685
|
try:
|
|
608
|
-
|
|
609
|
-
# In auto mode, always merge; otherwise ask
|
|
610
|
-
should_update = auto or prompt_yes_no("hooks.json exists. Update with memory agent hooks?", default=True)
|
|
611
|
-
if should_update:
|
|
612
|
-
if "hooks" not in existing:
|
|
613
|
-
existing["hooks"] = {}
|
|
614
|
-
existing["hooks"].update(hooks_config["hooks"])
|
|
615
|
-
hooks_config = existing
|
|
616
|
-
else:
|
|
617
|
-
print_success("Keeping existing hooks.json")
|
|
618
|
-
return True
|
|
686
|
+
settings = json.loads(settings_file.read_text())
|
|
619
687
|
except json.JSONDecodeError:
|
|
620
|
-
|
|
688
|
+
print_warning(f"Existing {settings_file.name} is invalid, creating backup")
|
|
689
|
+
shutil.copy(settings_file, settings_file.with_suffix(".json.bak"))
|
|
690
|
+
settings = {}
|
|
691
|
+
|
|
692
|
+
# Check if hooks already configured
|
|
693
|
+
if "hooks" in settings:
|
|
694
|
+
should_update = auto or prompt_yes_no("Hooks already configured in settings.json. Update with memory agent hooks?", default=True)
|
|
695
|
+
if not should_update:
|
|
696
|
+
print_success("Keeping existing hooks config")
|
|
697
|
+
return True
|
|
698
|
+
|
|
699
|
+
settings["hooks"] = hooks_config
|
|
621
700
|
|
|
622
701
|
try:
|
|
623
|
-
|
|
624
|
-
print_success(f"Configured hooks: {
|
|
702
|
+
settings_file.write_text(json.dumps(settings, indent=2))
|
|
703
|
+
print_success(f"Configured hooks in: {settings_file}")
|
|
625
704
|
return True
|
|
626
705
|
except Exception as e:
|
|
627
706
|
print_error(f"Failed to configure hooks: {e}")
|
|
@@ -762,9 +841,25 @@ def uninstall() -> bool:
|
|
|
762
841
|
except Exception as e:
|
|
763
842
|
print_warning(f"Could not update settings: {e}")
|
|
764
843
|
|
|
765
|
-
# Remove hooks
|
|
844
|
+
# Remove hooks from settings.json
|
|
845
|
+
if settings_file.exists():
|
|
846
|
+
try:
|
|
847
|
+
settings = json.loads(settings_file.read_text())
|
|
848
|
+
if "hooks" in settings:
|
|
849
|
+
del settings["hooks"]
|
|
850
|
+
settings_file.write_text(json.dumps(settings, indent=2))
|
|
851
|
+
print_success("Removed hooks config from settings.json")
|
|
852
|
+
except Exception as e:
|
|
853
|
+
print_warning(f"Could not update hooks in settings: {e}")
|
|
854
|
+
|
|
855
|
+
# Remove hook files
|
|
766
856
|
hooks_dir = get_hooks_dir()
|
|
767
|
-
hooks_to_remove = [
|
|
857
|
+
hooks_to_remove = [
|
|
858
|
+
"session_start.py", "session_end_hook.py", "stop_hook.py",
|
|
859
|
+
"grounding-hook.py", "log-user-request.py", "detect-correction.py",
|
|
860
|
+
"pre-tool-decision.py", "log-tool-use.py", "auto-detect-response.py",
|
|
861
|
+
"extract_memories.py",
|
|
862
|
+
]
|
|
768
863
|
for hook in hooks_to_remove:
|
|
769
864
|
hook_file = hooks_dir / hook
|
|
770
865
|
if hook_file.exists():
|
package/main.py
CHANGED
|
@@ -111,11 +111,15 @@ from services.claude_md_sync import get_claude_md_sync
|
|
|
111
111
|
# Cross-session awareness
|
|
112
112
|
from services.session_awareness import get_session_awareness
|
|
113
113
|
|
|
114
|
+
# Soul layer — persistent personality and learning
|
|
115
|
+
from services.soul import SoulService
|
|
116
|
+
|
|
114
117
|
# Agent registry for dashboard
|
|
115
118
|
from services.agent_registry import (
|
|
116
119
|
AVAILABLE_AGENTS, AVAILABLE_MCPS, AVAILABLE_HOOKS,
|
|
117
120
|
AGENT_CATEGORIES, get_agents_by_category, get_agent_by_id,
|
|
118
|
-
load_configured_hooks, load_configured_mcps
|
|
121
|
+
load_configured_hooks, load_configured_mcps,
|
|
122
|
+
discover_agents, discover_categories, find_agent_by_id, toggle_agent
|
|
119
123
|
)
|
|
120
124
|
|
|
121
125
|
load_dotenv()
|
|
@@ -186,6 +190,7 @@ metrics = OperationMetrics()
|
|
|
186
190
|
|
|
187
191
|
# Initialize services
|
|
188
192
|
db = DatabaseService()
|
|
193
|
+
soul_service = SoulService(db)
|
|
189
194
|
from config import config as _cfg
|
|
190
195
|
embeddings = EmbeddingService(
|
|
191
196
|
provider_type=_cfg.EMBEDDING_PROVIDER,
|
|
@@ -1772,6 +1777,37 @@ async def _handle_session_append_file(query, params, session_id):
|
|
|
1772
1777
|
)
|
|
1773
1778
|
|
|
1774
1779
|
|
|
1780
|
+
# ============================================================
|
|
1781
|
+
# SOUL LAYER SKILL HANDLERS
|
|
1782
|
+
# ============================================================
|
|
1783
|
+
|
|
1784
|
+
async def _handle_soul_brief(query, params, session_id):
|
|
1785
|
+
project_path = params.get("project_path", "")
|
|
1786
|
+
brief = await soul_service.generate_soul_brief(project_path)
|
|
1787
|
+
return {"success": True, "brief": brief}
|
|
1788
|
+
|
|
1789
|
+
async def _handle_soul_capture(query, params, session_id):
|
|
1790
|
+
sid = params.get("session_id") or session_id or ""
|
|
1791
|
+
project_path = params.get("project_path", "")
|
|
1792
|
+
fragment_type = params.get("fragment_type", "")
|
|
1793
|
+
content = params.get("content", "")
|
|
1794
|
+
if not fragment_type or not content:
|
|
1795
|
+
return {"success": False, "error": "fragment_type and content required"}
|
|
1796
|
+
frag_id = await soul_service.capture_soul_fragment(
|
|
1797
|
+
session_id=sid,
|
|
1798
|
+
fragment_type=fragment_type,
|
|
1799
|
+
content=content,
|
|
1800
|
+
project_path=project_path,
|
|
1801
|
+
)
|
|
1802
|
+
return {"success": frag_id is not None, "fragment_id": frag_id}
|
|
1803
|
+
|
|
1804
|
+
async def _handle_soul_integrate(query, params, session_id):
|
|
1805
|
+
sid = params.get("session_id") or session_id or ""
|
|
1806
|
+
project_path = params.get("project_path", "")
|
|
1807
|
+
result = await soul_service.run_soul_integration(sid, project_path)
|
|
1808
|
+
return {"success": True, **result}
|
|
1809
|
+
|
|
1810
|
+
|
|
1775
1811
|
# ============================================================
|
|
1776
1812
|
# SKILL DISPATCH TABLE
|
|
1777
1813
|
# ============================================================
|
|
@@ -1928,6 +1964,11 @@ SKILL_DISPATCH = {
|
|
|
1928
1964
|
"session_conflicts": _handle_session_conflicts,
|
|
1929
1965
|
"session_post_activity": _handle_session_post_activity,
|
|
1930
1966
|
"session_append_file": _handle_session_append_file,
|
|
1967
|
+
|
|
1968
|
+
# Soul Layer
|
|
1969
|
+
"soul_brief": _handle_soul_brief,
|
|
1970
|
+
"soul_capture": _handle_soul_capture,
|
|
1971
|
+
"soul_integrate": _handle_soul_integrate,
|
|
1931
1972
|
}
|
|
1932
1973
|
|
|
1933
1974
|
|
|
@@ -4362,17 +4403,41 @@ async def api_mark_insight_applied(insight_id: int):
|
|
|
4362
4403
|
# ============= Agent Configuration API =============
|
|
4363
4404
|
|
|
4364
4405
|
@app.get("/api/agents")
|
|
4365
|
-
async def get_all_agents():
|
|
4366
|
-
"""Get all
|
|
4406
|
+
async def get_all_agents(project_path: Optional[str] = None):
|
|
4407
|
+
"""Get all discovered agents from disk with categories."""
|
|
4408
|
+
agents = discover_agents(project_path)
|
|
4409
|
+
categories = discover_categories(agents)
|
|
4410
|
+
enabled_count = sum(1 for a in agents if a["enabled"])
|
|
4367
4411
|
return {
|
|
4368
4412
|
"success": True,
|
|
4369
|
-
"agents":
|
|
4370
|
-
"categories":
|
|
4371
|
-
"by_category": get_agents_by_category(),
|
|
4372
|
-
"total": len(
|
|
4413
|
+
"agents": agents,
|
|
4414
|
+
"categories": categories,
|
|
4415
|
+
"by_category": get_agents_by_category(agents),
|
|
4416
|
+
"total": len(agents),
|
|
4417
|
+
"enabled": enabled_count,
|
|
4373
4418
|
}
|
|
4374
4419
|
|
|
4375
4420
|
|
|
4421
|
+
@app.post("/api/agents/{agent_id}/toggle")
|
|
4422
|
+
async def toggle_agent_endpoint(agent_id: str, request: Request):
|
|
4423
|
+
"""Toggle an agent between enabled/disabled by moving files on disk."""
|
|
4424
|
+
try:
|
|
4425
|
+
body = await request.json()
|
|
4426
|
+
enabled = body.get("enabled", False)
|
|
4427
|
+
project_path = body.get("project_path")
|
|
4428
|
+
|
|
4429
|
+
agent = toggle_agent(agent_id, enabled, project_path)
|
|
4430
|
+
return {
|
|
4431
|
+
"success": True,
|
|
4432
|
+
"agent": agent,
|
|
4433
|
+
}
|
|
4434
|
+
except FileNotFoundError as e:
|
|
4435
|
+
return {"success": False, "error": str(e)}
|
|
4436
|
+
except Exception as e:
|
|
4437
|
+
logger.error(f"Error toggling agent {agent_id}: {e}")
|
|
4438
|
+
return {"success": False, "error": str(e)}
|
|
4439
|
+
|
|
4440
|
+
|
|
4376
4441
|
@app.get("/api/mcps")
|
|
4377
4442
|
async def get_all_mcps():
|
|
4378
4443
|
"""Get all available MCP servers with live configured status."""
|
|
@@ -4435,7 +4500,8 @@ async def get_project_config(project_path: str):
|
|
|
4435
4500
|
(project_path,)
|
|
4436
4501
|
)
|
|
4437
4502
|
|
|
4438
|
-
# Build agent status map
|
|
4503
|
+
# Build agent status map from disk discovery
|
|
4504
|
+
live_agents = discover_agents(project_path)
|
|
4439
4505
|
agent_status = {}
|
|
4440
4506
|
for config in (agent_configs or []):
|
|
4441
4507
|
agent_status[config['agent_id']] = {
|
|
@@ -4444,12 +4510,12 @@ async def get_project_config(project_path: str):
|
|
|
4444
4510
|
'settings': json.loads(config['settings']) if config['settings'] else {}
|
|
4445
4511
|
}
|
|
4446
4512
|
|
|
4447
|
-
# Fill in defaults for
|
|
4448
|
-
for agent in
|
|
4513
|
+
# Fill in defaults for agents not in DB config
|
|
4514
|
+
for agent in live_agents:
|
|
4449
4515
|
if agent['id'] not in agent_status:
|
|
4450
4516
|
agent_status[agent['id']] = {
|
|
4451
|
-
'enabled': agent['
|
|
4452
|
-
'priority':
|
|
4517
|
+
'enabled': agent['enabled'],
|
|
4518
|
+
'priority': 5,
|
|
4453
4519
|
'settings': {}
|
|
4454
4520
|
}
|
|
4455
4521
|
|
|
@@ -4502,7 +4568,7 @@ async def get_project_config(project_path: str):
|
|
|
4502
4568
|
"hooks": hook_status,
|
|
4503
4569
|
"stats": {
|
|
4504
4570
|
"enabled_agents": sum(1 for a in agent_status.values() if a['enabled']),
|
|
4505
|
-
"total_agents": len(
|
|
4571
|
+
"total_agents": len(live_agents),
|
|
4506
4572
|
"enabled_mcps": sum(1 for m in mcp_status.values() if m.get('enabled')),
|
|
4507
4573
|
"total_mcps": len(live_mcps),
|
|
4508
4574
|
"configured_mcps": sum(1 for m in mcp_status.values() if m.get('configured')),
|
|
@@ -5645,6 +5711,60 @@ async def api_post_session_activity(request: Request):
|
|
|
5645
5711
|
return {"success": False, "error": str(e)}
|
|
5646
5712
|
|
|
5647
5713
|
|
|
5714
|
+
# ============= Soul Layer REST Endpoints =============
|
|
5715
|
+
|
|
5716
|
+
|
|
5717
|
+
@app.get("/api/soul/brief")
|
|
5718
|
+
async def api_soul_brief(project_path: str = ""):
|
|
5719
|
+
"""Get the soul brief for a project."""
|
|
5720
|
+
try:
|
|
5721
|
+
brief = await soul_service.generate_soul_brief(project_path)
|
|
5722
|
+
return {"success": True, "brief": brief}
|
|
5723
|
+
except Exception as e:
|
|
5724
|
+
logger.error(f"Soul brief failed: {e}")
|
|
5725
|
+
return {"success": False, "error": str(e), "brief": ""}
|
|
5726
|
+
|
|
5727
|
+
|
|
5728
|
+
@app.post("/api/soul/capture")
|
|
5729
|
+
async def api_soul_capture(request: Request):
|
|
5730
|
+
"""Capture a soul fragment from a session response."""
|
|
5731
|
+
try:
|
|
5732
|
+
body = await request.json()
|
|
5733
|
+
session_id = body.get("session_id", "")
|
|
5734
|
+
project_path = body.get("project_path", "")
|
|
5735
|
+
fragment_type = body.get("fragment_type", "")
|
|
5736
|
+
content = body.get("content", "")
|
|
5737
|
+
|
|
5738
|
+
if not fragment_type or not content:
|
|
5739
|
+
return {"success": False, "error": "fragment_type and content required"}
|
|
5740
|
+
|
|
5741
|
+
frag_id = await soul_service.capture_soul_fragment(
|
|
5742
|
+
session_id=session_id,
|
|
5743
|
+
fragment_type=fragment_type,
|
|
5744
|
+
content=content,
|
|
5745
|
+
project_path=project_path,
|
|
5746
|
+
)
|
|
5747
|
+
return {"success": frag_id is not None, "fragment_id": frag_id}
|
|
5748
|
+
except Exception as e:
|
|
5749
|
+
logger.error(f"Soul capture failed: {e}")
|
|
5750
|
+
return {"success": False, "error": str(e)}
|
|
5751
|
+
|
|
5752
|
+
|
|
5753
|
+
@app.post("/api/soul/integrate")
|
|
5754
|
+
async def api_soul_integrate(request: Request):
|
|
5755
|
+
"""Run soul integration for a session — merges fragments into soul_state."""
|
|
5756
|
+
try:
|
|
5757
|
+
body = await request.json()
|
|
5758
|
+
session_id = body.get("session_id", "")
|
|
5759
|
+
project_path = body.get("project_path", "")
|
|
5760
|
+
|
|
5761
|
+
result = await soul_service.run_soul_integration(session_id, project_path)
|
|
5762
|
+
return {"success": True, **result}
|
|
5763
|
+
except Exception as e:
|
|
5764
|
+
logger.error(f"Soul integration failed: {e}")
|
|
5765
|
+
return {"success": False, "error": str(e)}
|
|
5766
|
+
|
|
5767
|
+
|
|
5648
5768
|
# ============= Aggregated Grounding Context (v2) =============
|
|
5649
5769
|
|
|
5650
5770
|
|
package/mcp_proxy.py
CHANGED
|
@@ -180,6 +180,12 @@ async def memory_ask(
|
|
|
180
180
|
elif label == "sessions":
|
|
181
181
|
results["active_sessions"] = result.get("sessions", [])
|
|
182
182
|
|
|
183
|
+
# -- Soul context enrichment (lightweight DB read) --
|
|
184
|
+
if project_path:
|
|
185
|
+
soul_data = await _rest_get("/api/soul/brief", {"project_path": project_path})
|
|
186
|
+
if soul_data and soul_data.get("brief"):
|
|
187
|
+
results["soul_context"] = {"brief": soul_data["brief"]}
|
|
188
|
+
|
|
183
189
|
results["success"] = bool(results.get("memories") or results.get("patterns"))
|
|
184
190
|
return json.dumps(results, default=str)
|
|
185
191
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-memory-agent",
|
|
3
|
-
"version": "3.0
|
|
4
|
-
"description": "Persistent semantic memory
|
|
3
|
+
"version": "3.1.0",
|
|
4
|
+
"description": "Persistent semantic memory for Claude Code with soul layer, personality learning, and anti-hallucination grounding",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
|
7
7
|
"claude-code",
|