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
mempalace/onboarding.py
ADDED
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
onboarding.py — MemPalace first-run setup.
|
|
4
|
+
|
|
5
|
+
Asks the user:
|
|
6
|
+
1. How they're using MemPalace (work / personal / combo)
|
|
7
|
+
2. Who the people in their life are (names, nicknames, relationships)
|
|
8
|
+
3. What their projects are
|
|
9
|
+
4. What they want their wings called
|
|
10
|
+
|
|
11
|
+
Seeds the entity_registry with confirmed data so MemPalace knows your world
|
|
12
|
+
from minute one — before a single session is indexed.
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
python3 -m mempalace.onboarding
|
|
16
|
+
or: mempalace init
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from mempalace.entity_registry import EntityRegistry
|
|
21
|
+
from mempalace.entity_detector import detect_entities, scan_for_detection
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
25
|
+
# Default wing taxonomies by mode
|
|
26
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
DEFAULT_WINGS = {
|
|
29
|
+
"work": [
|
|
30
|
+
"projects",
|
|
31
|
+
"clients",
|
|
32
|
+
"team",
|
|
33
|
+
"decisions",
|
|
34
|
+
"research",
|
|
35
|
+
],
|
|
36
|
+
"personal": [
|
|
37
|
+
"family",
|
|
38
|
+
"health",
|
|
39
|
+
"creative",
|
|
40
|
+
"reflections",
|
|
41
|
+
"relationships",
|
|
42
|
+
],
|
|
43
|
+
"combo": [
|
|
44
|
+
"family",
|
|
45
|
+
"work",
|
|
46
|
+
"health",
|
|
47
|
+
"creative",
|
|
48
|
+
"projects",
|
|
49
|
+
"reflections",
|
|
50
|
+
],
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
55
|
+
# Helpers
|
|
56
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _hr():
|
|
60
|
+
print(f"\n{'─' * 58}")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _header(text):
|
|
64
|
+
print(f"\n{'=' * 58}")
|
|
65
|
+
print(f" {text}")
|
|
66
|
+
print(f"{'=' * 58}")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _ask(prompt, default=None):
|
|
70
|
+
if default:
|
|
71
|
+
val = input(f" {prompt} [{default}]: ").strip()
|
|
72
|
+
return val if val else default
|
|
73
|
+
return input(f" {prompt}: ").strip()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _yn(prompt, default="y"):
|
|
77
|
+
val = input(f" {prompt} [{'Y/n' if default == 'y' else 'y/N'}]: ").strip().lower()
|
|
78
|
+
if not val:
|
|
79
|
+
return default == "y"
|
|
80
|
+
return val.startswith("y")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
84
|
+
# Step 1: Mode selection
|
|
85
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _ask_mode() -> str:
|
|
89
|
+
_header("Welcome to MemPalace")
|
|
90
|
+
print("""
|
|
91
|
+
MemPalace is a personal memory system. To work well, it needs to know
|
|
92
|
+
a little about your world — who the people are, what the projects
|
|
93
|
+
are, and how you want your memory organized.
|
|
94
|
+
|
|
95
|
+
This takes about 2 minutes. You can always update it later.
|
|
96
|
+
""")
|
|
97
|
+
print(" How are you using MemPalace?")
|
|
98
|
+
print()
|
|
99
|
+
print(" [1] Work — notes, projects, clients, colleagues, decisions")
|
|
100
|
+
print(" [2] Personal — diary, family, health, relationships, reflections")
|
|
101
|
+
print(" [3] Both — personal and professional mixed")
|
|
102
|
+
print()
|
|
103
|
+
|
|
104
|
+
while True:
|
|
105
|
+
choice = input(" Your choice [1/2/3]: ").strip()
|
|
106
|
+
if choice == "1":
|
|
107
|
+
return "work"
|
|
108
|
+
elif choice == "2":
|
|
109
|
+
return "personal"
|
|
110
|
+
elif choice == "3":
|
|
111
|
+
return "combo"
|
|
112
|
+
print(" Please enter 1, 2, or 3.")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
116
|
+
# Step 2: People
|
|
117
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _ask_people(mode: str) -> tuple[list, dict]:
|
|
121
|
+
"""Returns (people_list, aliases_dict)."""
|
|
122
|
+
people = []
|
|
123
|
+
aliases = {} # nickname → full name
|
|
124
|
+
|
|
125
|
+
if mode in ("personal", "combo"):
|
|
126
|
+
_hr()
|
|
127
|
+
print("""
|
|
128
|
+
Personal world — who are the important people in your life?
|
|
129
|
+
|
|
130
|
+
Format: name, relationship (e.g. "Riley, daughter" or just "Devon")
|
|
131
|
+
For nicknames, you'll be asked separately.
|
|
132
|
+
Type 'done' when finished.
|
|
133
|
+
""")
|
|
134
|
+
while True:
|
|
135
|
+
entry = input(" Person: ").strip()
|
|
136
|
+
if entry.lower() in ("done", ""):
|
|
137
|
+
break
|
|
138
|
+
parts = [p.strip() for p in entry.split(",", 1)]
|
|
139
|
+
name = parts[0]
|
|
140
|
+
relationship = parts[1] if len(parts) > 1 else ""
|
|
141
|
+
if name:
|
|
142
|
+
# Ask about nicknames
|
|
143
|
+
nick = input(f" Nickname for {name}? (or enter to skip): ").strip()
|
|
144
|
+
if nick:
|
|
145
|
+
aliases[nick] = name
|
|
146
|
+
people.append({"name": name, "relationship": relationship, "context": "personal"})
|
|
147
|
+
|
|
148
|
+
if mode in ("work", "combo"):
|
|
149
|
+
_hr()
|
|
150
|
+
print("""
|
|
151
|
+
Work world — who are the colleagues, clients, or collaborators
|
|
152
|
+
you'd want to find in your notes?
|
|
153
|
+
|
|
154
|
+
Format: name, role (e.g. "Ben, co-founder" or just "Sarah")
|
|
155
|
+
Type 'done' when finished.
|
|
156
|
+
""")
|
|
157
|
+
while True:
|
|
158
|
+
entry = input(" Person: ").strip()
|
|
159
|
+
if entry.lower() in ("done", ""):
|
|
160
|
+
break
|
|
161
|
+
parts = [p.strip() for p in entry.split(",", 1)]
|
|
162
|
+
name = parts[0]
|
|
163
|
+
role = parts[1] if len(parts) > 1 else ""
|
|
164
|
+
if name:
|
|
165
|
+
people.append({"name": name, "relationship": role, "context": "work"})
|
|
166
|
+
|
|
167
|
+
return people, aliases
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
171
|
+
# Step 3: Projects
|
|
172
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _ask_projects(mode: str) -> list:
|
|
176
|
+
if mode == "personal":
|
|
177
|
+
return []
|
|
178
|
+
|
|
179
|
+
_hr()
|
|
180
|
+
print("""
|
|
181
|
+
What are your main projects? (These help MemPalace distinguish project
|
|
182
|
+
names from person names — e.g. "Lantern" the project vs. "Lantern" the word.)
|
|
183
|
+
|
|
184
|
+
Type 'done' when finished.
|
|
185
|
+
""")
|
|
186
|
+
projects = []
|
|
187
|
+
while True:
|
|
188
|
+
proj = input(" Project: ").strip()
|
|
189
|
+
if proj.lower() in ("done", ""):
|
|
190
|
+
break
|
|
191
|
+
if proj:
|
|
192
|
+
projects.append(proj)
|
|
193
|
+
return projects
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
197
|
+
# Step 4: Wings
|
|
198
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _ask_wings(mode: str) -> list:
|
|
202
|
+
defaults = DEFAULT_WINGS[mode]
|
|
203
|
+
_hr()
|
|
204
|
+
print(f"""
|
|
205
|
+
Wings are the top-level categories in your memory palace.
|
|
206
|
+
|
|
207
|
+
Suggested wings for {mode} mode:
|
|
208
|
+
{", ".join(defaults)}
|
|
209
|
+
|
|
210
|
+
Press enter to keep these, or type your own comma-separated list.
|
|
211
|
+
""")
|
|
212
|
+
custom = input(" Wings: ").strip()
|
|
213
|
+
if custom:
|
|
214
|
+
return [w.strip() for w in custom.split(",") if w.strip()]
|
|
215
|
+
return defaults
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
219
|
+
# Step 5: Auto-detect from files
|
|
220
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _auto_detect(directory: str, known_people: list) -> list:
|
|
224
|
+
"""Scan directory for additional entity candidates."""
|
|
225
|
+
known_names = {p["name"].lower() for p in known_people}
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
files = scan_for_detection(directory)
|
|
229
|
+
if not files:
|
|
230
|
+
return []
|
|
231
|
+
detected = detect_entities(files)
|
|
232
|
+
new_people = [
|
|
233
|
+
e
|
|
234
|
+
for e in detected["people"]
|
|
235
|
+
if e["name"].lower() not in known_names and e["confidence"] >= 0.7
|
|
236
|
+
]
|
|
237
|
+
return new_people
|
|
238
|
+
except Exception:
|
|
239
|
+
return []
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
243
|
+
# Step 6: Ambiguity warnings
|
|
244
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _warn_ambiguous(people: list) -> list:
|
|
248
|
+
"""
|
|
249
|
+
Flag names that are also common English words.
|
|
250
|
+
Returns list of ambiguous names for user awareness.
|
|
251
|
+
"""
|
|
252
|
+
from mempalace.entity_registry import COMMON_ENGLISH_WORDS
|
|
253
|
+
|
|
254
|
+
ambiguous = []
|
|
255
|
+
for p in people:
|
|
256
|
+
if p["name"].lower() in COMMON_ENGLISH_WORDS:
|
|
257
|
+
ambiguous.append(p["name"])
|
|
258
|
+
return ambiguous
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
262
|
+
# Main onboarding flow
|
|
263
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _generate_aaak_bootstrap(
|
|
267
|
+
people: list, projects: list, wings: list, mode: str, config_dir: Path = None
|
|
268
|
+
):
|
|
269
|
+
"""
|
|
270
|
+
Generate AAAK entity registry + critical facts bootstrap from onboarding data.
|
|
271
|
+
These files teach the AI about the user's world from session one.
|
|
272
|
+
"""
|
|
273
|
+
mempalace_dir = Path(config_dir) if config_dir else Path.home() / ".mempalace"
|
|
274
|
+
mempalace_dir.mkdir(parents=True, exist_ok=True)
|
|
275
|
+
|
|
276
|
+
# Build AAAK entity codes (first 3 letters of name, uppercase)
|
|
277
|
+
entity_codes = {}
|
|
278
|
+
for p in people:
|
|
279
|
+
name = p["name"]
|
|
280
|
+
code = name[:3].upper()
|
|
281
|
+
# Handle collisions
|
|
282
|
+
while code in entity_codes.values():
|
|
283
|
+
code = name[:4].upper()
|
|
284
|
+
entity_codes[name] = code
|
|
285
|
+
|
|
286
|
+
# AAAK entity registry
|
|
287
|
+
registry_lines = [
|
|
288
|
+
"# AAAK Entity Registry",
|
|
289
|
+
"# Auto-generated by mempalace init. Update as needed.",
|
|
290
|
+
"",
|
|
291
|
+
"## People",
|
|
292
|
+
]
|
|
293
|
+
for p in people:
|
|
294
|
+
name = p["name"]
|
|
295
|
+
code = entity_codes[name]
|
|
296
|
+
rel = p.get("relationship", "")
|
|
297
|
+
registry_lines.append(f" {code}={name} ({rel})" if rel else f" {code}={name}")
|
|
298
|
+
|
|
299
|
+
if projects:
|
|
300
|
+
registry_lines.extend(["", "## Projects"])
|
|
301
|
+
for proj in projects:
|
|
302
|
+
code = proj[:4].upper()
|
|
303
|
+
registry_lines.append(f" {code}={proj}")
|
|
304
|
+
|
|
305
|
+
registry_lines.extend(
|
|
306
|
+
[
|
|
307
|
+
"",
|
|
308
|
+
"## AAAK Quick Reference",
|
|
309
|
+
" Symbols: ♡=love ★=importance ⚠=warning →=relationship |=separator",
|
|
310
|
+
" Structure: KEY:value | GROUP(details) | entity.attribute",
|
|
311
|
+
" Read naturally — expand codes, treat *markers* as emotional context.",
|
|
312
|
+
]
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
(mempalace_dir / "aaak_entities.md").write_text("\n".join(registry_lines))
|
|
316
|
+
|
|
317
|
+
# Critical facts bootstrap (pre-palace — before any mining)
|
|
318
|
+
facts_lines = [
|
|
319
|
+
"# Critical Facts (bootstrap — will be enriched after mining)",
|
|
320
|
+
"",
|
|
321
|
+
]
|
|
322
|
+
|
|
323
|
+
personal_people = [p for p in people if p.get("context") == "personal"]
|
|
324
|
+
work_people = [p for p in people if p.get("context") == "work"]
|
|
325
|
+
|
|
326
|
+
if personal_people:
|
|
327
|
+
facts_lines.append("## People (personal)")
|
|
328
|
+
for p in personal_people:
|
|
329
|
+
code = entity_codes[p["name"]]
|
|
330
|
+
rel = p.get("relationship", "")
|
|
331
|
+
facts_lines.append(
|
|
332
|
+
f"- **{p['name']}** ({code}) — {rel}" if rel else f"- **{p['name']}** ({code})"
|
|
333
|
+
)
|
|
334
|
+
facts_lines.append("")
|
|
335
|
+
|
|
336
|
+
if work_people:
|
|
337
|
+
facts_lines.append("## People (work)")
|
|
338
|
+
for p in work_people:
|
|
339
|
+
code = entity_codes[p["name"]]
|
|
340
|
+
rel = p.get("relationship", "")
|
|
341
|
+
facts_lines.append(
|
|
342
|
+
f"- **{p['name']}** ({code}) — {rel}" if rel else f"- **{p['name']}** ({code})"
|
|
343
|
+
)
|
|
344
|
+
facts_lines.append("")
|
|
345
|
+
|
|
346
|
+
if projects:
|
|
347
|
+
facts_lines.append("## Projects")
|
|
348
|
+
for proj in projects:
|
|
349
|
+
facts_lines.append(f"- **{proj}**")
|
|
350
|
+
facts_lines.append("")
|
|
351
|
+
|
|
352
|
+
facts_lines.extend(
|
|
353
|
+
[
|
|
354
|
+
"## Palace",
|
|
355
|
+
f"Wings: {', '.join(wings)}",
|
|
356
|
+
f"Mode: {mode}",
|
|
357
|
+
"",
|
|
358
|
+
"*This file will be enriched by palace_facts.py after mining.*",
|
|
359
|
+
]
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
(mempalace_dir / "critical_facts.md").write_text("\n".join(facts_lines))
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def run_onboarding(
|
|
366
|
+
directory: str = ".",
|
|
367
|
+
config_dir: Path = None,
|
|
368
|
+
auto_detect: bool = True,
|
|
369
|
+
) -> EntityRegistry:
|
|
370
|
+
"""
|
|
371
|
+
Run the full onboarding flow.
|
|
372
|
+
Returns the seeded EntityRegistry.
|
|
373
|
+
"""
|
|
374
|
+
# Step 1: Mode
|
|
375
|
+
mode = _ask_mode()
|
|
376
|
+
|
|
377
|
+
# Step 2: People
|
|
378
|
+
people, aliases = _ask_people(mode)
|
|
379
|
+
|
|
380
|
+
# Step 3: Projects
|
|
381
|
+
projects = _ask_projects(mode)
|
|
382
|
+
|
|
383
|
+
# Step 4: Wings (stored in config, not registry — just show user)
|
|
384
|
+
wings = _ask_wings(mode)
|
|
385
|
+
|
|
386
|
+
# Step 5: Auto-detect additional people from files
|
|
387
|
+
if auto_detect and _yn("\nScan your files for additional names we might have missed?"):
|
|
388
|
+
directory = _ask("Directory to scan", default=directory)
|
|
389
|
+
detected = _auto_detect(directory, people)
|
|
390
|
+
if detected:
|
|
391
|
+
_hr()
|
|
392
|
+
print(f"\n Found {len(detected)} additional name candidates:\n")
|
|
393
|
+
for e in detected:
|
|
394
|
+
print(
|
|
395
|
+
f" {e['name']:20} confidence={e['confidence']:.0%} "
|
|
396
|
+
f"({', '.join(e['signals'][:1])})"
|
|
397
|
+
)
|
|
398
|
+
print()
|
|
399
|
+
if _yn(" Add any of these to your registry?"):
|
|
400
|
+
for e in detected:
|
|
401
|
+
ans = input(f" {e['name']} — (p)erson, (s)kip? ").strip().lower()
|
|
402
|
+
if ans == "p":
|
|
403
|
+
rel = input(f" Relationship/role for {e['name']}? ").strip()
|
|
404
|
+
ctx = (
|
|
405
|
+
"personal"
|
|
406
|
+
if mode == "personal"
|
|
407
|
+
else (
|
|
408
|
+
"work"
|
|
409
|
+
if mode == "work"
|
|
410
|
+
else input(" Context — (p)ersonal or (w)ork? ")
|
|
411
|
+
.strip()
|
|
412
|
+
.lower()
|
|
413
|
+
.replace("w", "work")
|
|
414
|
+
.replace("p", "personal")
|
|
415
|
+
)
|
|
416
|
+
)
|
|
417
|
+
people.append({"name": e["name"], "relationship": rel, "context": ctx})
|
|
418
|
+
|
|
419
|
+
# Step 6: Warn about ambiguous names
|
|
420
|
+
ambiguous = _warn_ambiguous(people)
|
|
421
|
+
if ambiguous:
|
|
422
|
+
_hr()
|
|
423
|
+
print(f"""
|
|
424
|
+
Heads up — these names are also common English words:
|
|
425
|
+
{", ".join(ambiguous)}
|
|
426
|
+
|
|
427
|
+
MemPalace will check the context before treating them as person names.
|
|
428
|
+
For example: "I picked up Riley" → person.
|
|
429
|
+
"Have you ever tried" → adverb.
|
|
430
|
+
""")
|
|
431
|
+
|
|
432
|
+
# Build and save registry
|
|
433
|
+
registry = EntityRegistry.load(config_dir)
|
|
434
|
+
registry.seed(mode=mode, people=people, projects=projects, aliases=aliases)
|
|
435
|
+
|
|
436
|
+
# Generate AAAK entity registry + critical facts bootstrap
|
|
437
|
+
_generate_aaak_bootstrap(people, projects, wings, mode, config_dir)
|
|
438
|
+
|
|
439
|
+
# Summary
|
|
440
|
+
_header("Setup Complete")
|
|
441
|
+
print()
|
|
442
|
+
print(f" {registry.summary()}")
|
|
443
|
+
print(f"\n Wings: {', '.join(wings)}")
|
|
444
|
+
print(f"\n Registry saved to: {registry._path}")
|
|
445
|
+
print("\n AAAK entity registry: ~/.mempalace/aaak_entities.md")
|
|
446
|
+
print(" Critical facts bootstrap: ~/.mempalace/critical_facts.md")
|
|
447
|
+
print("\n Your AI will know your world from the first session.")
|
|
448
|
+
print()
|
|
449
|
+
|
|
450
|
+
return registry
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
454
|
+
# Quick setup (non-interactive, for testing)
|
|
455
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def quick_setup(
|
|
459
|
+
mode: str,
|
|
460
|
+
people: list,
|
|
461
|
+
projects: list = None,
|
|
462
|
+
aliases: dict = None,
|
|
463
|
+
config_dir: Path = None,
|
|
464
|
+
) -> EntityRegistry:
|
|
465
|
+
"""
|
|
466
|
+
Programmatic setup without interactive prompts.
|
|
467
|
+
Used in tests and benchmark scripts.
|
|
468
|
+
|
|
469
|
+
people: list of dicts {"name": str, "relationship": str, "context": str}
|
|
470
|
+
"""
|
|
471
|
+
registry = EntityRegistry.load(config_dir)
|
|
472
|
+
registry.seed(
|
|
473
|
+
mode=mode,
|
|
474
|
+
people=people,
|
|
475
|
+
projects=projects or [],
|
|
476
|
+
aliases=aliases or {},
|
|
477
|
+
)
|
|
478
|
+
return registry
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
482
|
+
# CLI
|
|
483
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
484
|
+
|
|
485
|
+
if __name__ == "__main__":
|
|
486
|
+
import sys
|
|
487
|
+
|
|
488
|
+
directory = sys.argv[1] if len(sys.argv) > 1 else "."
|
|
489
|
+
run_onboarding(directory=directory)
|