nexo-brain 0.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 (38) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +241 -0
  3. package/bin/create-nexo.js +593 -0
  4. package/package.json +32 -0
  5. package/scripts/pre-commit-check.sh +55 -0
  6. package/src/cognitive.py +1224 -0
  7. package/src/db.py +2283 -0
  8. package/src/hooks/caffeinate-guard.sh +8 -0
  9. package/src/hooks/capture-session.sh +19 -0
  10. package/src/hooks/session-start.sh +27 -0
  11. package/src/hooks/session-stop.sh +11 -0
  12. package/src/plugin_loader.py +136 -0
  13. package/src/plugins/__init__.py +0 -0
  14. package/src/plugins/agents.py +52 -0
  15. package/src/plugins/backup.py +103 -0
  16. package/src/plugins/cognitive_memory.py +305 -0
  17. package/src/plugins/entities.py +61 -0
  18. package/src/plugins/episodic_memory.py +391 -0
  19. package/src/plugins/evolution.py +113 -0
  20. package/src/plugins/guard.py +346 -0
  21. package/src/plugins/preferences.py +47 -0
  22. package/src/scripts/nexo-auto-update.py +213 -0
  23. package/src/scripts/nexo-catchup.py +179 -0
  24. package/src/scripts/nexo-cognitive-decay.py +82 -0
  25. package/src/scripts/nexo-daily-self-audit.py +532 -0
  26. package/src/scripts/nexo-postmortem-consolidator.py +594 -0
  27. package/src/scripts/nexo-sleep.py +762 -0
  28. package/src/scripts/nexo-synthesis.py +537 -0
  29. package/src/server.py +560 -0
  30. package/src/tools_coordination.py +102 -0
  31. package/src/tools_credentials.py +64 -0
  32. package/src/tools_learnings.py +180 -0
  33. package/src/tools_menu.py +208 -0
  34. package/src/tools_reminders.py +80 -0
  35. package/src/tools_reminders_crud.py +157 -0
  36. package/src/tools_sessions.py +169 -0
  37. package/src/tools_task_history.py +57 -0
  38. package/templates/CLAUDE.md.template +89 -0
@@ -0,0 +1,537 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ NEXO Synthesis Engine — Daily intelligence brief.
4
+
5
+ Runs every 2 hours via LaunchAgent. Executes ONCE per day (internal gate).
6
+
7
+ Zero external dependencies beyond stdlib + sqlite3.
8
+ """
9
+
10
+ import fcntl
11
+ import json
12
+ import os
13
+ import sqlite3
14
+ import sys
15
+ from collections import Counter, defaultdict
16
+ from datetime import datetime, date, timedelta
17
+ from pathlib import Path
18
+
19
+ # ─── Paths ────────────────────────────────────────────────────────────────────
20
+ HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
21
+ CLAUDE_DIR = HOME / "claude"
22
+ COORD_DIR = CLAUDE_DIR / "coordination"
23
+
24
+ NEXO_DB = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo"))) / "nexo.db"
25
+
26
+ OUTPUT_FILE = COORD_DIR / "daily-synthesis.md"
27
+ SYNTHESIS_LOG = COORD_DIR / "synthesis-log.json"
28
+ LAST_RUN_FILE = COORD_DIR / "synthesis-last-run"
29
+ LOCK_FILE = COORD_DIR / "synthesis.lock"
30
+
31
+ TODAY = date.today()
32
+ TODAY_STR = TODAY.isoformat()
33
+ SEVEN_DAYS_AGO = (TODAY - timedelta(days=7)).isoformat()
34
+ TOMORROW = (TODAY + timedelta(days=1)).isoformat()
35
+
36
+
37
+ # ─── Utilities ────────────────────────────────────────────────────────────────
38
+
39
+ def log(msg: str):
40
+ ts = datetime.now().strftime("%H:%M:%S")
41
+ print(f"[{ts}] {msg}", flush=True)
42
+
43
+
44
+ def should_run() -> bool:
45
+ """Gate: run at most once per day."""
46
+ if LAST_RUN_FILE.exists():
47
+ last = LAST_RUN_FILE.read_text().strip()
48
+ if last == TODAY_STR:
49
+ log(f"Already ran today ({TODAY_STR}). Skipping.")
50
+ return False
51
+ return True
52
+
53
+
54
+ def mark_done():
55
+ LAST_RUN_FILE.write_text(TODAY_STR)
56
+
57
+
58
+ def acquire_lock():
59
+ lock_fd = open(LOCK_FILE, "w")
60
+ try:
61
+ fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
62
+ return lock_fd
63
+ except BlockingIOError:
64
+ log("Another instance is running. Exiting.")
65
+ sys.exit(0)
66
+
67
+
68
+ def release_lock(lock_fd):
69
+ fcntl.flock(lock_fd, fcntl.LOCK_UN)
70
+ lock_fd.close()
71
+ try:
72
+ LOCK_FILE.unlink()
73
+ except FileNotFoundError:
74
+ pass
75
+
76
+
77
+ def safe_query(db_path: Path, sql: str, params=()) -> list:
78
+ """Run a query against a SQLite DB, return rows or [] on any error."""
79
+ if not db_path.exists():
80
+ return []
81
+ try:
82
+ conn = sqlite3.connect(db_path)
83
+ conn.row_factory = sqlite3.Row
84
+ cur = conn.execute(sql, params)
85
+ rows = [dict(r) for r in cur.fetchall()]
86
+ conn.close()
87
+ return rows
88
+ except Exception as e:
89
+ log(f"Query error on {db_path.name}: {e}")
90
+ return []
91
+
92
+
93
+ def truncate(text: str, max_len: int = 200) -> str:
94
+ if not text:
95
+ return ""
96
+ text = text.strip().replace("\n", " ")
97
+ return text[:max_len] + ("…" if len(text) > max_len else "")
98
+
99
+
100
+ # ─── Section builders ─────────────────────────────────────────────────────────
101
+
102
+ def section_learnings() -> str:
103
+ rows = safe_query(
104
+ NEXO_DB,
105
+ "SELECT category, title, content, reasoning FROM learnings "
106
+ "WHERE date(created_at, 'unixepoch') = ? ORDER BY created_at DESC",
107
+ (TODAY_STR,),
108
+ )
109
+ if not rows:
110
+ return "Sin errores nuevos registrados."
111
+
112
+ lines = []
113
+ for r in rows:
114
+ cat = r.get("category") or "general"
115
+ title = r.get("title") or ""
116
+ content = truncate(r.get("content") or "", 180)
117
+ lines.append(f"- **[{cat}]** {title}: {content}")
118
+ return "\n".join(lines)
119
+
120
+
121
+ def section_decisions() -> str:
122
+ # decisions table uses columns: domain, decision, alternatives, based_on, outcome
123
+ rows = safe_query(
124
+ NEXO_DB,
125
+ "SELECT domain, decision, alternatives, based_on, outcome FROM decisions "
126
+ "WHERE date(created_at) = ? ORDER BY created_at DESC",
127
+ (TODAY_STR,),
128
+ )
129
+ if not rows:
130
+ return "Sin decisiones registradas."
131
+
132
+ lines = []
133
+ for r in rows:
134
+ domain = r.get("domain") or ""
135
+ chosen = truncate(r.get("decision") or "", 160)
136
+ discarded = truncate(r.get("alternatives") or "", 120)
137
+ why = truncate(r.get("based_on") or "", 120)
138
+ outcome = r.get("outcome") or ""
139
+
140
+ line = f"- **[{domain}]** Elegido: {chosen}"
141
+ if discarded:
142
+ line += f"\n Descartado: {discarded}"
143
+ if why:
144
+ line += f"\n Por: {why}"
145
+ if outcome:
146
+ line += f"\n Resultado: {truncate(outcome, 100)}"
147
+ lines.append(line)
148
+ return "\n".join(lines)
149
+
150
+
151
+ def section_changes() -> str:
152
+ rows = safe_query(
153
+ NEXO_DB,
154
+ "SELECT files, what_changed, why, risks, affects FROM change_log "
155
+ "WHERE date(created_at) = ? ORDER BY created_at DESC",
156
+ (TODAY_STR,),
157
+ )
158
+ if not rows:
159
+ return "Sin cambios de código registrados."
160
+
161
+ # Group by "system" (first part of first file path)
162
+ by_system = defaultdict(list)
163
+ for r in rows:
164
+ files_raw = r.get("files") or ""
165
+ # Take first file, extract top-level system name
166
+ first_file = files_raw.split(",")[0].strip()
167
+ parts = [p for p in first_file.replace("\\", "/").split("/") if p and p != "_public"]
168
+ system = parts[0] if parts else "misc"
169
+ by_system[system].append(r)
170
+
171
+ lines = []
172
+ for system, entries in by_system.items():
173
+ lines.append(f"**{system}** ({len(entries)} cambio{'s' if len(entries) > 1 else ''}):")
174
+ for r in entries[:3]: # cap per system
175
+ what = truncate(r.get("what_changed") or "", 160)
176
+ risks = truncate(r.get("risks") or "", 100)
177
+ lines.append(f" - {what}")
178
+ if risks:
179
+ lines.append(f" ⚠ Riesgos: {risks}")
180
+ return "\n".join(lines)
181
+
182
+
183
+ def section_patterns() -> str:
184
+ # Learnings by category — last 7 days
185
+ learn_rows = safe_query(
186
+ NEXO_DB,
187
+ "SELECT category, title FROM learnings "
188
+ "WHERE date(created_at, 'unixepoch') >= ? ORDER BY created_at DESC",
189
+ (SEVEN_DAYS_AGO,),
190
+ )
191
+ # change_log — last 7 days
192
+ change_rows = safe_query(
193
+ NEXO_DB,
194
+ "SELECT files FROM change_log WHERE date(created_at) >= ?",
195
+ (SEVEN_DAYS_AGO,),
196
+ )
197
+
198
+ total_learn = len(learn_rows)
199
+ total_changes = len(change_rows)
200
+
201
+ if total_learn < 3 and total_changes < 3:
202
+ return "Datos insuficientes para análisis de patrones (< 7 días)."
203
+
204
+ lines = []
205
+
206
+ # Categories with most learnings
207
+ if learn_rows:
208
+ cat_counter = Counter(r.get("category") or "general" for r in learn_rows)
209
+ top_cats = cat_counter.most_common(3)
210
+ lines.append(f"**Áreas con más errores** (últimos 7d, {total_learn} learnings):")
211
+ for cat, count in top_cats:
212
+ lines.append(f" - {cat}: {count} {'error' if count == 1 else 'errores'}")
213
+
214
+ # Systems most touched in change_log
215
+ if change_rows:
216
+ sys_counter: Counter = Counter()
217
+ for r in change_rows:
218
+ files_raw = r.get("files") or ""
219
+ for f in files_raw.split(",")[:3]:
220
+ f = f.strip()
221
+ parts = [p for p in f.replace("\\", "/").split("/") if p and p != "_public"]
222
+ if parts:
223
+ sys_counter[parts[0]] += 1
224
+ top_sys = sys_counter.most_common(3)
225
+ lines.append(f"**Sistemas más tocados** (últimos 7d, {total_changes} cambios):")
226
+ for sys_name, count in top_sys:
227
+ lines.append(f" - {sys_name}: {count} {'modificación' if count == 1 else 'modificaciones'}")
228
+
229
+ # Recurring error patterns — categories with learnings on 3+ different days
230
+ if learn_rows:
231
+ # Get daily breakdown per category
232
+ daily_cats = safe_query(
233
+ NEXO_DB,
234
+ "SELECT category, date(created_at, 'unixepoch') as day "
235
+ "FROM learnings WHERE date(created_at, 'unixepoch') >= ? "
236
+ "GROUP BY category, day",
237
+ (SEVEN_DAYS_AGO,),
238
+ )
239
+ if daily_cats:
240
+ cat_days = Counter(r.get("category") or "general" for r in daily_cats)
241
+ recurring = [(c, d) for c, d in cat_days.items() if d >= 3]
242
+ if recurring:
243
+ lines.append("**Categorías con errores recurrentes** (3+ días distintos):")
244
+ for cat, days in sorted(recurring, key=lambda x: -x[1]):
245
+ lines.append(f" - {cat}: errores en {days} días — punto débil")
246
+
247
+ return "\n".join(lines) if lines else "Sin patrones significativos detectados."
248
+
249
+
250
+ def section_manana() -> str:
251
+ lines = []
252
+
253
+ # Reminders due <= tomorrow, PENDIENTE
254
+ rem_rows = safe_query(
255
+ NEXO_DB,
256
+ "SELECT id, date, description, category FROM reminders "
257
+ "WHERE status LIKE 'PENDIENTE%' AND date IS NOT NULL AND date <= ? "
258
+ "ORDER BY date ASC",
259
+ (TOMORROW,),
260
+ )
261
+ if rem_rows:
262
+ lines.append("### Recordatorios vencidos/mañana")
263
+ for r in rem_rows:
264
+ d = r.get("date") or ""
265
+ cat = r.get("category") or ""
266
+ desc = truncate(r.get("description") or "", 150)
267
+ overdue = " ⚠ VENCIDO" if d and d < TODAY_STR else ""
268
+ lines.append(f"- [{d}]{overdue} {desc}" + (f" ({cat})" if cat else ""))
269
+ else:
270
+ lines.append("### Recordatorios\nNinguno vencido ni para mañana.")
271
+
272
+ # Followups due <= tomorrow, PENDIENTE
273
+ fol_rows = safe_query(
274
+ NEXO_DB,
275
+ "SELECT id, date, description FROM followups "
276
+ "WHERE status = 'PENDIENTE' AND date IS NOT NULL AND date <= ? "
277
+ "ORDER BY date ASC",
278
+ (TOMORROW,),
279
+ )
280
+ if fol_rows:
281
+ lines.append("### Followups vencidos/mañana")
282
+ for r in fol_rows:
283
+ d = r.get("date") or ""
284
+ desc = truncate(r.get("description") or "", 150)
285
+ overdue = " ⚠ VENCIDO" if d and d < TODAY_STR else ""
286
+ lines.append(f"- [{d}]{overdue} {desc}")
287
+ else:
288
+ lines.append("### Followups\nNinguno vencido ni para mañana.")
289
+
290
+ # Last 3 session diary entries — pending + next_session_context
291
+ diary_rows = safe_query(
292
+ NEXO_DB,
293
+ "SELECT domain, pending, context_next, created_at FROM session_diary "
294
+ "ORDER BY created_at DESC LIMIT 3",
295
+ )
296
+ if diary_rows:
297
+ lines.append("### Contexto activo (últimas sesiones)")
298
+ for r in diary_rows:
299
+ domain = r.get("domain") or "general"
300
+ pending = truncate(r.get("pending") or "", 200)
301
+ nxt = truncate(r.get("context_next") or "", 200)
302
+ ts = r.get("created_at") or ""
303
+ if pending or nxt:
304
+ lines.append(f"**[{domain}]** ({ts[:16]}):")
305
+ if pending:
306
+ lines.append(f" Pendiente: {pending}")
307
+ if nxt:
308
+ lines.append(f" Para la próxima: {nxt}")
309
+
310
+ return "\n".join(lines) if lines else "Sin elementos para mañana."
311
+
312
+
313
+ def section_autoevaluacion() -> str:
314
+ diary_rows = safe_query(
315
+ NEXO_DB,
316
+ "SELECT mental_state, francisco_signals, self_critique, summary, created_at FROM session_diary "
317
+ "WHERE date(created_at) = ? ORDER BY created_at DESC",
318
+ (TODAY_STR,),
319
+ )
320
+ if not diary_rows:
321
+ return "Sin diarios de sesión registrados hoy."
322
+
323
+ lines = []
324
+
325
+ # Self-critique section (NEW — most important)
326
+ all_critiques = []
327
+ for r in diary_rows:
328
+ sc = r.get("self_critique") or ""
329
+ if sc.strip() and not sc.strip().lower().startswith("sin autocrítica"):
330
+ all_critiques.append(truncate(sc, 300))
331
+
332
+ if all_critiques:
333
+ lines.append(f"**AUTOCRÍTICAS ({len(all_critiques)} sesiones con fallos detectados):**")
334
+ for c in all_critiques[:5]:
335
+ lines.append(f" - {c}")
336
+ lines.append("**ACCIÓN:** Estas autocríticas deben informar el comportamiento de mañana. Si un patrón se repite 3+ días, el consolidador nocturno lo promoverá a memoria permanente.")
337
+ lines.append("")
338
+
339
+ # francisco_signals patterns
340
+ all_signals = []
341
+ mental_states = []
342
+ for r in diary_rows:
343
+ sig = r.get("francisco_signals") or ""
344
+ if sig.strip():
345
+ all_signals.append(truncate(sig, 200))
346
+ ms = r.get("mental_state") or ""
347
+ if ms.strip():
348
+ mental_states.append(truncate(ms, 200))
349
+
350
+ if francisco_signals_text := "\n".join(f" - {s}" for s in all_signals[:3] if s):
351
+ lines.append(f"**Señales de the user:**\n{francisco_signals_text}")
352
+
353
+ if mental_states:
354
+ lines.append(f"**Estado mental de sesiones:**")
355
+ for ms in mental_states[:2]:
356
+ lines.append(f" - {ms}")
357
+
358
+ # Derive what to do differently based on signal analysis
359
+ if all_signals:
360
+ # Detect repeated corrections
361
+ correction_words = ["corrig", "frustrad", "no lo entiend", "exig", "repet",
362
+ "no debería", "por qué no", "otra vez", "cansando",
363
+ "siempre espera", "reactivo", "no te adelant"]
364
+ correction_count = sum(
365
+ 1 for s in all_signals
366
+ if any(w in s.lower() for w in correction_words)
367
+ )
368
+ if correction_count >= 2:
369
+ lines.append(f"**ALERTA:** the user corrigió {correction_count} veces hoy — revisar qué se está repitiendo.")
370
+ lines.append("**Para mañana:** Revisar señales anteriores antes de actuar.")
371
+ elif not diary_rows:
372
+ lines.append("**Para mañana:** Recordar escribir diario al cerrar sesión.")
373
+
374
+ # Check for postmortem daily summary
375
+ postmortem_file = COORD_DIR / "postmortem-daily.md"
376
+ if postmortem_file.exists():
377
+ pm_content = postmortem_file.read_text().strip()
378
+ if "Promovido a memoria permanente" in pm_content:
379
+ lines.append("")
380
+ lines.append("**REGLAS NUEVAS PERMANENTES (generadas anoche por el consolidador):**")
381
+ for line in pm_content.split("\n"):
382
+ if line.startswith("- ") and "Promovido" not in line:
383
+ lines.append(f" {line}")
384
+
385
+ return "\n".join(lines) if lines else "Sin datos de auto-evaluación."
386
+
387
+
388
+ def section_francisco_observer() -> str:
389
+ """Track the user's patterns: forgotten ideas, abandoned topics, recurring requests."""
390
+ lines = []
391
+
392
+ # 1. Reminders without dates (ideas that accumulate without agenda)
393
+ no_date = safe_query(
394
+ NEXO_DB,
395
+ "SELECT id, description FROM reminders "
396
+ "WHERE date IS NULL AND status LIKE 'PENDIENTE%' ORDER BY rowid",
397
+ )
398
+ if no_date:
399
+ lines.append(f"**Ideas sin agenda:** {len(no_date)} reminders sin fecha")
400
+ # Show oldest 3 as examples
401
+ for r in no_date[:3]:
402
+ desc = truncate(r.get("description") or "", 80)
403
+ lines.append(f" - {r.get('id')}: {desc}")
404
+ if len(no_date) > 3:
405
+ lines.append(f" - ... y {len(no_date) - 3} más")
406
+
407
+ # 2. Followups waiting on the user or external responses
408
+ waiting = safe_query(
409
+ NEXO_DB,
410
+ "SELECT id, description, date FROM followups "
411
+ "WHERE status = 'PENDIENTE' "
412
+ "AND (description LIKE '%María%' OR description LIKE '%respuesta%' "
413
+ " OR description LIKE '%preguntar%' OR description LIKE '%confirme%' "
414
+ " OR description LIKE '%decidió%') "
415
+ "ORDER BY date",
416
+ )
417
+ if waiting:
418
+ lines.append(f"**Esperando respuesta/decisión de the user o terceros:** {len(waiting)}")
419
+ for r in waiting[:5]:
420
+ d = r.get("date") or "sin fecha"
421
+ desc = truncate(r.get("description") or "", 100)
422
+ lines.append(f" - {r.get('id')} ({d}): {desc}")
423
+
424
+ # 3. Overdue reminders that keep getting postponed (same reminder, multiple updates)
425
+ # Detect by looking at reminders with dates far past
426
+ stale = safe_query(
427
+ NEXO_DB,
428
+ "SELECT id, description, date FROM reminders "
429
+ "WHERE status LIKE 'PENDIENTE%' AND date IS NOT NULL AND date < ? "
430
+ "ORDER BY date ASC LIMIT 5",
431
+ (TODAY_STR,),
432
+ )
433
+ if stale:
434
+ lines.append(f"**Recordatorios vencidos no atendidos:**")
435
+ for r in stale:
436
+ desc = truncate(r.get("description") or "", 80)
437
+ lines.append(f" - {r.get('id')} (venció {r.get('date')}): {desc}")
438
+
439
+ if not lines:
440
+ return "Sin observaciones sobre patrones de the user."
441
+
442
+ return "\n".join(lines)
443
+
444
+
445
+ # ─── Log history ──────────────────────────────────────────────────────────────
446
+
447
+ def append_synthesis_log(entry: dict):
448
+ log_data = []
449
+ if SYNTHESIS_LOG.exists():
450
+ try:
451
+ log_data = json.loads(SYNTHESIS_LOG.read_text())
452
+ except Exception:
453
+ log_data = []
454
+ log_data.append(entry)
455
+ # Keep last 30 entries
456
+ log_data = log_data[-30:]
457
+ SYNTHESIS_LOG.write_text(json.dumps(log_data, ensure_ascii=False, indent=2))
458
+
459
+
460
+ # ─── Main ─────────────────────────────────────────────────────────────────────
461
+
462
+ def main():
463
+ log("NEXO Synthesis Engine starting.")
464
+
465
+ if not should_run():
466
+ sys.exit(0)
467
+
468
+ lock_fd = acquire_lock()
469
+
470
+ try:
471
+ COORD_DIR.mkdir(parents=True, exist_ok=True)
472
+
473
+ now = datetime.now()
474
+ ts = now.strftime("%Y-%m-%d %H:%M")
475
+ log("Querying databases...")
476
+
477
+ s_learnings = section_learnings()
478
+ s_decisions = section_decisions()
479
+ s_changes = section_changes()
480
+ s_patterns = section_patterns()
481
+ s_manana = section_manana()
482
+ s_autoeval = section_autoevaluacion()
483
+ s_francisco = section_francisco_observer()
484
+
485
+ md = f"""# NEXO Daily Synthesis — {TODAY_STR}
486
+ Generated at {ts}
487
+
488
+ ## Errores y Lecciones (hoy)
489
+ {s_learnings}
490
+
491
+ ## Decisiones Tomadas
492
+ {s_decisions}
493
+
494
+ ## Sistemas Tocados
495
+ {s_changes}
496
+
497
+ ## Patrones Detectados
498
+ {s_patterns}
499
+
500
+ ## the user — Observaciones
501
+ {s_francisco}
502
+
503
+ ## Mañana
504
+ {s_manana}
505
+
506
+ ## Auto-Evaluación
507
+ {s_autoeval}
508
+ """
509
+
510
+ OUTPUT_FILE.write_text(md, encoding="utf-8")
511
+ log(f"Written: {OUTPUT_FILE}")
512
+
513
+ line_count = len(md.splitlines())
514
+ log(f"Output: {line_count} lines.")
515
+
516
+ # Log history
517
+ append_synthesis_log({
518
+ "date": TODAY_STR,
519
+ "generated_at": ts,
520
+ "lines": line_count,
521
+ "learnings_today": s_learnings.count("\n- ") + (1 if s_learnings.startswith("- ") else 0),
522
+ })
523
+
524
+ mark_done()
525
+ log("Done.")
526
+
527
+ except Exception as e:
528
+ log(f"Fatal error: {e}")
529
+ import traceback
530
+ traceback.print_exc()
531
+ sys.exit(1)
532
+ finally:
533
+ release_lock(lock_fd)
534
+
535
+
536
+ if __name__ == "__main__":
537
+ main()