mempalace-code 1.0.0__py3-none-any.whl
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.
- mempalace/README.md +40 -0
- mempalace/__init__.py +6 -0
- mempalace/__main__.py +5 -0
- mempalace/cli.py +811 -0
- mempalace/config.py +149 -0
- mempalace/convo_miner.py +415 -0
- mempalace/dialect.py +1075 -0
- mempalace/entity_detector.py +853 -0
- mempalace/entity_registry.py +639 -0
- mempalace/export.py +378 -0
- mempalace/general_extractor.py +521 -0
- mempalace/knowledge_graph.py +410 -0
- mempalace/layers.py +515 -0
- mempalace/mcp_server.py +873 -0
- mempalace/migrate.py +153 -0
- mempalace/miner.py +1285 -0
- mempalace/normalize.py +328 -0
- mempalace/onboarding.py +489 -0
- mempalace/palace_graph.py +225 -0
- mempalace/py.typed +0 -0
- mempalace/room_detector_local.py +310 -0
- mempalace/searcher.py +305 -0
- mempalace/spellcheck.py +269 -0
- mempalace/split_mega_files.py +309 -0
- mempalace/storage.py +807 -0
- mempalace/version.py +3 -0
- mempalace_code-1.0.0.dist-info/METADATA +489 -0
- mempalace_code-1.0.0.dist-info/RECORD +32 -0
- mempalace_code-1.0.0.dist-info/WHEEL +4 -0
- mempalace_code-1.0.0.dist-info/entry_points.txt +2 -0
- mempalace_code-1.0.0.dist-info/licenses/LICENSE +192 -0
- mempalace_code-1.0.0.dist-info/licenses/NOTICE +17 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""
|
|
2
|
+
palace_graph.py — Graph traversal layer for MemPalace
|
|
3
|
+
======================================================
|
|
4
|
+
|
|
5
|
+
Builds a navigable graph from the palace structure:
|
|
6
|
+
- Nodes = rooms (named ideas)
|
|
7
|
+
- Edges = shared rooms across wings (tunnels)
|
|
8
|
+
- Edge types = halls (the corridors)
|
|
9
|
+
|
|
10
|
+
Enables queries like:
|
|
11
|
+
"Start at chromadb-setup in wing_code, walk to wing_myproject"
|
|
12
|
+
"Find all rooms connected to riley-college-apps"
|
|
13
|
+
"What topics bridge wing_hardware and wing_myproject?"
|
|
14
|
+
|
|
15
|
+
No external graph DB needed — built from ChromaDB metadata.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from collections import defaultdict, Counter
|
|
19
|
+
from .config import MempalaceConfig
|
|
20
|
+
from .storage import open_store
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _get_store(config=None):
|
|
24
|
+
config = config or MempalaceConfig()
|
|
25
|
+
try:
|
|
26
|
+
return open_store(config.palace_path, create=False)
|
|
27
|
+
except Exception:
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def build_graph(col=None, config=None):
|
|
32
|
+
"""
|
|
33
|
+
Build the palace graph from drawer metadata.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
nodes: dict of {room: {wings: set, halls: set, count: int}}
|
|
37
|
+
edges: list of {room, wing_a, wing_b, hall} — one per tunnel crossing
|
|
38
|
+
"""
|
|
39
|
+
if col is None:
|
|
40
|
+
col = _get_store(config)
|
|
41
|
+
if not col:
|
|
42
|
+
return {}, []
|
|
43
|
+
|
|
44
|
+
total = col.count()
|
|
45
|
+
room_data = defaultdict(lambda: {"wings": set(), "halls": set(), "count": 0, "dates": set()})
|
|
46
|
+
|
|
47
|
+
offset = 0
|
|
48
|
+
while offset < total:
|
|
49
|
+
batch = col.get(limit=1000, offset=offset, include=["metadatas"])
|
|
50
|
+
for meta in batch["metadatas"]:
|
|
51
|
+
room = meta.get("room", "")
|
|
52
|
+
wing = meta.get("wing", "")
|
|
53
|
+
hall = meta.get("hall", "")
|
|
54
|
+
date = meta.get("date", "")
|
|
55
|
+
if room and room != "general" and wing:
|
|
56
|
+
room_data[room]["wings"].add(wing)
|
|
57
|
+
if hall:
|
|
58
|
+
room_data[room]["halls"].add(hall)
|
|
59
|
+
if date:
|
|
60
|
+
room_data[room]["dates"].add(date)
|
|
61
|
+
room_data[room]["count"] += 1
|
|
62
|
+
if not batch["ids"]:
|
|
63
|
+
break
|
|
64
|
+
offset += len(batch["ids"])
|
|
65
|
+
|
|
66
|
+
# Build edges from rooms that span multiple wings
|
|
67
|
+
edges = []
|
|
68
|
+
for room, data in room_data.items():
|
|
69
|
+
wings = sorted(data["wings"])
|
|
70
|
+
if len(wings) >= 2:
|
|
71
|
+
for i, wa in enumerate(wings):
|
|
72
|
+
for wb in wings[i + 1 :]:
|
|
73
|
+
for hall in data["halls"]:
|
|
74
|
+
edges.append(
|
|
75
|
+
{
|
|
76
|
+
"room": room,
|
|
77
|
+
"wing_a": wa,
|
|
78
|
+
"wing_b": wb,
|
|
79
|
+
"hall": hall,
|
|
80
|
+
"count": data["count"],
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Convert sets to lists for JSON serialization
|
|
85
|
+
nodes = {}
|
|
86
|
+
for room, data in room_data.items():
|
|
87
|
+
nodes[room] = {
|
|
88
|
+
"wings": sorted(data["wings"]),
|
|
89
|
+
"halls": sorted(data["halls"]),
|
|
90
|
+
"count": data["count"],
|
|
91
|
+
"dates": sorted(data["dates"])[-5:] if data["dates"] else [],
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return nodes, edges
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def traverse(start_room: str, col=None, config=None, max_hops: int = 2):
|
|
98
|
+
"""
|
|
99
|
+
Walk the graph from a starting room. Find connected rooms
|
|
100
|
+
through shared wings.
|
|
101
|
+
|
|
102
|
+
Returns list of paths: [{room, wing, hall, hop_distance}]
|
|
103
|
+
"""
|
|
104
|
+
nodes, edges = build_graph(col, config)
|
|
105
|
+
|
|
106
|
+
if start_room not in nodes:
|
|
107
|
+
return {
|
|
108
|
+
"error": f"Room '{start_room}' not found",
|
|
109
|
+
"suggestions": _fuzzy_match(start_room, nodes),
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
start = nodes[start_room]
|
|
113
|
+
visited = {start_room}
|
|
114
|
+
results = [
|
|
115
|
+
{
|
|
116
|
+
"room": start_room,
|
|
117
|
+
"wings": start["wings"],
|
|
118
|
+
"halls": start["halls"],
|
|
119
|
+
"count": start["count"],
|
|
120
|
+
"hop": 0,
|
|
121
|
+
}
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
# BFS traversal
|
|
125
|
+
frontier = [(start_room, 0)]
|
|
126
|
+
while frontier:
|
|
127
|
+
current_room, depth = frontier.pop(0)
|
|
128
|
+
if depth >= max_hops:
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
current = nodes.get(current_room, {})
|
|
132
|
+
current_wings = set(current.get("wings", []))
|
|
133
|
+
|
|
134
|
+
# Find all rooms that share a wing with current room
|
|
135
|
+
for room, data in nodes.items():
|
|
136
|
+
if room in visited:
|
|
137
|
+
continue
|
|
138
|
+
shared_wings = current_wings & set(data["wings"])
|
|
139
|
+
if shared_wings:
|
|
140
|
+
visited.add(room)
|
|
141
|
+
results.append(
|
|
142
|
+
{
|
|
143
|
+
"room": room,
|
|
144
|
+
"wings": data["wings"],
|
|
145
|
+
"halls": data["halls"],
|
|
146
|
+
"count": data["count"],
|
|
147
|
+
"hop": depth + 1,
|
|
148
|
+
"connected_via": sorted(shared_wings),
|
|
149
|
+
}
|
|
150
|
+
)
|
|
151
|
+
if depth + 1 < max_hops:
|
|
152
|
+
frontier.append((room, depth + 1))
|
|
153
|
+
|
|
154
|
+
# Sort by relevance (hop distance, then count)
|
|
155
|
+
results.sort(key=lambda x: (x["hop"], -x["count"]))
|
|
156
|
+
return results[:50] # cap results
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def find_tunnels(wing_a: str = None, wing_b: str = None, col=None, config=None):
|
|
160
|
+
"""
|
|
161
|
+
Find rooms that connect two wings (or all tunnel rooms if no wings specified).
|
|
162
|
+
These are the "hallways" — same named idea appearing in multiple domains.
|
|
163
|
+
"""
|
|
164
|
+
nodes, edges = build_graph(col, config)
|
|
165
|
+
|
|
166
|
+
tunnels = []
|
|
167
|
+
for room, data in nodes.items():
|
|
168
|
+
wings = data["wings"]
|
|
169
|
+
if len(wings) < 2:
|
|
170
|
+
continue
|
|
171
|
+
|
|
172
|
+
if wing_a and wing_a not in wings:
|
|
173
|
+
continue
|
|
174
|
+
if wing_b and wing_b not in wings:
|
|
175
|
+
continue
|
|
176
|
+
|
|
177
|
+
tunnels.append(
|
|
178
|
+
{
|
|
179
|
+
"room": room,
|
|
180
|
+
"wings": wings,
|
|
181
|
+
"halls": data["halls"],
|
|
182
|
+
"count": data["count"],
|
|
183
|
+
"recent": data["dates"][-1] if data["dates"] else "",
|
|
184
|
+
}
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
tunnels.sort(key=lambda x: -x["count"])
|
|
188
|
+
return tunnels[:50]
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def graph_stats(col=None, config=None):
|
|
192
|
+
"""Summary statistics about the palace graph."""
|
|
193
|
+
nodes, edges = build_graph(col, config)
|
|
194
|
+
|
|
195
|
+
tunnel_rooms = sum(1 for n in nodes.values() if len(n["wings"]) >= 2)
|
|
196
|
+
wing_counts = Counter()
|
|
197
|
+
for data in nodes.values():
|
|
198
|
+
for w in data["wings"]:
|
|
199
|
+
wing_counts[w] += 1
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
"total_rooms": len(nodes),
|
|
203
|
+
"tunnel_rooms": tunnel_rooms,
|
|
204
|
+
"total_edges": len(edges),
|
|
205
|
+
"rooms_per_wing": dict(wing_counts.most_common()),
|
|
206
|
+
"top_tunnels": [
|
|
207
|
+
{"room": r, "wings": d["wings"], "count": d["count"]}
|
|
208
|
+
for r, d in sorted(nodes.items(), key=lambda x: -len(x[1]["wings"]))[:10]
|
|
209
|
+
if len(d["wings"]) >= 2
|
|
210
|
+
],
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _fuzzy_match(query: str, nodes: dict, n: int = 5):
|
|
215
|
+
"""Find rooms that approximately match a query string."""
|
|
216
|
+
query_lower = query.lower()
|
|
217
|
+
scored = []
|
|
218
|
+
for room in nodes:
|
|
219
|
+
# Simple substring matching
|
|
220
|
+
if query_lower in room:
|
|
221
|
+
scored.append((room, 1.0))
|
|
222
|
+
elif any(word in room for word in query_lower.split("-")):
|
|
223
|
+
scored.append((room, 0.5))
|
|
224
|
+
scored.sort(key=lambda x: -x[1])
|
|
225
|
+
return [r for r, _ in scored[:n]]
|
mempalace/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
room_detector_local.py — Local setup, no API required.
|
|
4
|
+
|
|
5
|
+
Two ways to define rooms without calling any AI:
|
|
6
|
+
1. Auto-detect from folder structure (zero config)
|
|
7
|
+
2. Define manually in mempalace.yaml
|
|
8
|
+
|
|
9
|
+
No internet. No API key. Your files stay on your machine.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
import yaml
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from collections import defaultdict
|
|
17
|
+
|
|
18
|
+
# Common room patterns — detected from folder names and filenames
|
|
19
|
+
# Format: {folder_keyword: room_name}
|
|
20
|
+
FOLDER_ROOM_MAP = {
|
|
21
|
+
"frontend": "frontend",
|
|
22
|
+
"front-end": "frontend",
|
|
23
|
+
"front_end": "frontend",
|
|
24
|
+
"client": "frontend",
|
|
25
|
+
"ui": "frontend",
|
|
26
|
+
"views": "frontend",
|
|
27
|
+
"components": "frontend",
|
|
28
|
+
"pages": "frontend",
|
|
29
|
+
"backend": "backend",
|
|
30
|
+
"back-end": "backend",
|
|
31
|
+
"back_end": "backend",
|
|
32
|
+
"server": "backend",
|
|
33
|
+
"api": "backend",
|
|
34
|
+
"routes": "backend",
|
|
35
|
+
"services": "backend",
|
|
36
|
+
"controllers": "backend",
|
|
37
|
+
"models": "backend",
|
|
38
|
+
"database": "backend",
|
|
39
|
+
"db": "backend",
|
|
40
|
+
"docs": "documentation",
|
|
41
|
+
"doc": "documentation",
|
|
42
|
+
"documentation": "documentation",
|
|
43
|
+
"wiki": "documentation",
|
|
44
|
+
"readme": "documentation",
|
|
45
|
+
"notes": "documentation",
|
|
46
|
+
"design": "design",
|
|
47
|
+
"designs": "design",
|
|
48
|
+
"mockups": "design",
|
|
49
|
+
"wireframes": "design",
|
|
50
|
+
"assets": "design",
|
|
51
|
+
"storyboard": "design",
|
|
52
|
+
"costs": "costs",
|
|
53
|
+
"cost": "costs",
|
|
54
|
+
"budget": "costs",
|
|
55
|
+
"finance": "costs",
|
|
56
|
+
"financial": "costs",
|
|
57
|
+
"pricing": "costs",
|
|
58
|
+
"invoices": "costs",
|
|
59
|
+
"accounting": "costs",
|
|
60
|
+
"meetings": "meetings",
|
|
61
|
+
"meeting": "meetings",
|
|
62
|
+
"calls": "meetings",
|
|
63
|
+
"meeting_notes": "meetings",
|
|
64
|
+
"standup": "meetings",
|
|
65
|
+
"minutes": "meetings",
|
|
66
|
+
"team": "team",
|
|
67
|
+
"staff": "team",
|
|
68
|
+
"hr": "team",
|
|
69
|
+
"hiring": "team",
|
|
70
|
+
"employees": "team",
|
|
71
|
+
"people": "team",
|
|
72
|
+
"research": "research",
|
|
73
|
+
"references": "research",
|
|
74
|
+
"reading": "research",
|
|
75
|
+
"papers": "research",
|
|
76
|
+
"planning": "planning",
|
|
77
|
+
"roadmap": "planning",
|
|
78
|
+
"strategy": "planning",
|
|
79
|
+
"specs": "planning",
|
|
80
|
+
"requirements": "planning",
|
|
81
|
+
"tests": "testing",
|
|
82
|
+
"test": "testing",
|
|
83
|
+
"testing": "testing",
|
|
84
|
+
"qa": "testing",
|
|
85
|
+
"scripts": "scripts",
|
|
86
|
+
"tools": "scripts",
|
|
87
|
+
"utils": "scripts",
|
|
88
|
+
"config": "configuration",
|
|
89
|
+
"configs": "configuration",
|
|
90
|
+
"settings": "configuration",
|
|
91
|
+
"infrastructure": "configuration",
|
|
92
|
+
"infra": "configuration",
|
|
93
|
+
"deploy": "configuration",
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def detect_rooms_from_folders(project_dir: str) -> list:
|
|
98
|
+
"""
|
|
99
|
+
Walk the project folder structure.
|
|
100
|
+
Find top-level subdirectories that match known room patterns.
|
|
101
|
+
Returns list of room dicts.
|
|
102
|
+
"""
|
|
103
|
+
project_path = Path(project_dir).expanduser().resolve()
|
|
104
|
+
found_rooms = {}
|
|
105
|
+
|
|
106
|
+
SKIP_DIRS = {
|
|
107
|
+
".git",
|
|
108
|
+
"node_modules",
|
|
109
|
+
"__pycache__",
|
|
110
|
+
".venv",
|
|
111
|
+
"venv",
|
|
112
|
+
"env",
|
|
113
|
+
"dist",
|
|
114
|
+
"build",
|
|
115
|
+
".next",
|
|
116
|
+
"coverage",
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
# Check top-level directories first (most reliable signal)
|
|
120
|
+
for item in project_path.iterdir():
|
|
121
|
+
if item.is_dir() and item.name not in SKIP_DIRS:
|
|
122
|
+
name_lower = item.name.lower().replace("-", "_")
|
|
123
|
+
if name_lower in FOLDER_ROOM_MAP:
|
|
124
|
+
room_name = FOLDER_ROOM_MAP[name_lower]
|
|
125
|
+
if room_name not in found_rooms:
|
|
126
|
+
found_rooms[room_name] = item.name
|
|
127
|
+
# Also check if folder name IS a good room name directly
|
|
128
|
+
elif len(item.name) > 2 and item.name[0].isalpha():
|
|
129
|
+
clean = item.name.lower().replace("-", "_").replace(" ", "_")
|
|
130
|
+
if clean not in found_rooms:
|
|
131
|
+
found_rooms[clean] = item.name
|
|
132
|
+
|
|
133
|
+
# Walk one level deeper for nested patterns
|
|
134
|
+
for item in project_path.iterdir():
|
|
135
|
+
if item.is_dir() and item.name not in SKIP_DIRS:
|
|
136
|
+
for subitem in item.iterdir():
|
|
137
|
+
if subitem.is_dir() and subitem.name not in SKIP_DIRS:
|
|
138
|
+
name_lower = subitem.name.lower().replace("-", "_")
|
|
139
|
+
if name_lower in FOLDER_ROOM_MAP:
|
|
140
|
+
room_name = FOLDER_ROOM_MAP[name_lower]
|
|
141
|
+
if room_name not in found_rooms:
|
|
142
|
+
found_rooms[room_name] = subitem.name
|
|
143
|
+
|
|
144
|
+
# Build room list
|
|
145
|
+
rooms = []
|
|
146
|
+
for room_name, original in found_rooms.items():
|
|
147
|
+
rooms.append(
|
|
148
|
+
{
|
|
149
|
+
"name": room_name,
|
|
150
|
+
"description": f"Files from {original}/",
|
|
151
|
+
"keywords": [room_name, original.lower()],
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Always add "general" as fallback
|
|
156
|
+
if not any(r["name"] == "general" for r in rooms):
|
|
157
|
+
rooms.append(
|
|
158
|
+
{
|
|
159
|
+
"name": "general",
|
|
160
|
+
"description": "Files that don't fit other rooms",
|
|
161
|
+
"keywords": [],
|
|
162
|
+
}
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
return rooms
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def detect_rooms_from_files(project_dir: str) -> list:
|
|
169
|
+
"""
|
|
170
|
+
Fallback: if folder structure gives no signal,
|
|
171
|
+
detect rooms from recurring filename patterns.
|
|
172
|
+
"""
|
|
173
|
+
project_path = Path(project_dir).expanduser().resolve()
|
|
174
|
+
keyword_counts = defaultdict(int)
|
|
175
|
+
|
|
176
|
+
SKIP_DIRS = {".git", "node_modules", "__pycache__", ".venv", "venv", "dist", "build"}
|
|
177
|
+
|
|
178
|
+
for root, dirs, filenames in os.walk(project_path):
|
|
179
|
+
dirs[:] = [d for d in dirs if d not in SKIP_DIRS]
|
|
180
|
+
for filename in filenames:
|
|
181
|
+
name_lower = filename.lower().replace("-", "_").replace(" ", "_")
|
|
182
|
+
for keyword, room in FOLDER_ROOM_MAP.items():
|
|
183
|
+
if keyword in name_lower:
|
|
184
|
+
keyword_counts[room] += 1
|
|
185
|
+
|
|
186
|
+
# Return rooms that appear more than twice
|
|
187
|
+
rooms = []
|
|
188
|
+
for room, count in sorted(keyword_counts.items(), key=lambda x: x[1], reverse=True):
|
|
189
|
+
if count >= 2:
|
|
190
|
+
rooms.append(
|
|
191
|
+
{
|
|
192
|
+
"name": room,
|
|
193
|
+
"description": f"Files related to {room}",
|
|
194
|
+
"keywords": [room],
|
|
195
|
+
}
|
|
196
|
+
)
|
|
197
|
+
if len(rooms) >= 6:
|
|
198
|
+
break
|
|
199
|
+
|
|
200
|
+
if not rooms:
|
|
201
|
+
rooms = [{"name": "general", "description": "All project files", "keywords": []}]
|
|
202
|
+
|
|
203
|
+
return rooms
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def print_proposed_structure(project_name: str, rooms: list, total_files: int, source: str):
|
|
207
|
+
print(f"\n{'=' * 55}")
|
|
208
|
+
print(" MemPalace Init — Local setup")
|
|
209
|
+
print(f"{'=' * 55}")
|
|
210
|
+
print(f"\n WING: {project_name}")
|
|
211
|
+
print(f" ({total_files} files found, rooms detected from {source})\n")
|
|
212
|
+
for room in rooms:
|
|
213
|
+
print(f" ROOM: {room['name']}")
|
|
214
|
+
print(f" {room['description']}")
|
|
215
|
+
print(f"\n{'─' * 55}")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def get_user_approval(rooms: list) -> list:
|
|
219
|
+
"""Same approval flow as AI version."""
|
|
220
|
+
print(" Review the proposed rooms above.")
|
|
221
|
+
print(" Options:")
|
|
222
|
+
print(" [enter] Accept all rooms")
|
|
223
|
+
print(" [edit] Remove or rename rooms")
|
|
224
|
+
print(" [add] Add a room manually")
|
|
225
|
+
print()
|
|
226
|
+
|
|
227
|
+
choice = input(" Your choice [enter/edit/add]: ").strip().lower()
|
|
228
|
+
|
|
229
|
+
if choice in ("", "y", "yes"):
|
|
230
|
+
return rooms
|
|
231
|
+
|
|
232
|
+
if choice == "edit":
|
|
233
|
+
print("\n Current rooms:")
|
|
234
|
+
for i, room in enumerate(rooms):
|
|
235
|
+
print(f" {i + 1}. {room['name']} — {room['description']}")
|
|
236
|
+
remove = input("\n Room numbers to REMOVE (comma-separated, or enter to skip): ").strip()
|
|
237
|
+
if remove:
|
|
238
|
+
to_remove = {int(x.strip()) - 1 for x in remove.split(",") if x.strip().isdigit()}
|
|
239
|
+
rooms = [r for i, r in enumerate(rooms) if i not in to_remove]
|
|
240
|
+
|
|
241
|
+
if choice == "add" or input("\n Add any missing rooms? [y/N]: ").strip().lower() == "y":
|
|
242
|
+
while True:
|
|
243
|
+
new_name = (
|
|
244
|
+
input(" New room name (or enter to stop): ").strip().lower().replace(" ", "_")
|
|
245
|
+
)
|
|
246
|
+
if not new_name:
|
|
247
|
+
break
|
|
248
|
+
new_desc = input(f" Description for '{new_name}': ").strip()
|
|
249
|
+
rooms.append({"name": new_name, "description": new_desc, "keywords": [new_name]})
|
|
250
|
+
print(f" Added: {new_name}")
|
|
251
|
+
|
|
252
|
+
return rooms
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def save_config(project_dir: str, project_name: str, rooms: list):
|
|
256
|
+
config = {
|
|
257
|
+
"wing": project_name,
|
|
258
|
+
"rooms": [
|
|
259
|
+
{
|
|
260
|
+
"name": r["name"],
|
|
261
|
+
"description": r["description"],
|
|
262
|
+
"keywords": r.get("keywords", [r["name"]]),
|
|
263
|
+
}
|
|
264
|
+
for r in rooms
|
|
265
|
+
],
|
|
266
|
+
}
|
|
267
|
+
config_path = Path(project_dir).expanduser().resolve() / "mempalace.yaml"
|
|
268
|
+
with open(config_path, "w") as f:
|
|
269
|
+
yaml.dump(config, f, default_flow_style=False, sort_keys=False)
|
|
270
|
+
|
|
271
|
+
print(f"\n Config saved: {config_path}")
|
|
272
|
+
print("\n Next step:")
|
|
273
|
+
print(f" mempalace mine {project_dir}")
|
|
274
|
+
print(f"\n{'=' * 55}\n")
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def detect_rooms_local(project_dir: str, yes: bool = False):
|
|
278
|
+
"""Main entry point for local setup."""
|
|
279
|
+
project_path = Path(project_dir).expanduser().resolve()
|
|
280
|
+
project_name = project_path.name.lower().replace(" ", "_").replace("-", "_")
|
|
281
|
+
|
|
282
|
+
if not project_path.exists():
|
|
283
|
+
print(f"ERROR: Directory not found: {project_dir}")
|
|
284
|
+
sys.exit(1)
|
|
285
|
+
|
|
286
|
+
# Count files
|
|
287
|
+
from .miner import scan_project
|
|
288
|
+
|
|
289
|
+
files = scan_project(project_dir)
|
|
290
|
+
|
|
291
|
+
# Try folder structure first
|
|
292
|
+
rooms = detect_rooms_from_folders(project_dir)
|
|
293
|
+
source = "folder structure"
|
|
294
|
+
|
|
295
|
+
# If only "general" found, try filename patterns
|
|
296
|
+
if len(rooms) <= 1:
|
|
297
|
+
rooms = detect_rooms_from_files(project_dir)
|
|
298
|
+
source = "filename patterns"
|
|
299
|
+
|
|
300
|
+
# If still nothing, just use general
|
|
301
|
+
if not rooms:
|
|
302
|
+
rooms = [{"name": "general", "description": "All project files", "keywords": []}]
|
|
303
|
+
source = "fallback (flat project)"
|
|
304
|
+
|
|
305
|
+
print_proposed_structure(project_name, rooms, len(files), source)
|
|
306
|
+
if yes:
|
|
307
|
+
approved_rooms = rooms
|
|
308
|
+
else:
|
|
309
|
+
approved_rooms = get_user_approval(rooms)
|
|
310
|
+
save_config(project_dir, project_name, approved_rooms)
|