nexo-brain 2.6.6 → 2.6.7

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.
@@ -57,16 +57,20 @@ def normalize_objective(obj: dict | None) -> dict:
57
57
 
58
58
  if "evolution_mode" in source:
59
59
  mode = str(source.get("evolution_mode") or "auto").strip().lower()
60
+ if mode in {"public", "public_core", "contributor", "draft_prs"}:
61
+ mode = "public_core"
60
62
  else:
61
63
  legacy_mode = str(source.get("review_mode") or "").strip().lower()
62
64
  if legacy_mode in {"manual", "review"}:
63
65
  mode = "review"
64
66
  elif legacy_mode in {"managed", "hybrid", "owner", "core"}:
65
67
  mode = "managed"
68
+ elif legacy_mode in {"public", "public_core", "contributor", "draft_prs"}:
69
+ mode = "public_core"
66
70
  else:
67
71
  mode = "auto"
68
72
 
69
- if mode not in {"auto", "review", "managed"}:
73
+ if mode not in {"auto", "review", "managed", "public_core"}:
70
74
  mode = "auto"
71
75
 
72
76
  dimensions = source.get("dimensions")
@@ -276,6 +280,10 @@ def build_evolution_prompt(week_data: dict, objective: dict) -> str:
276
280
  mode_desc = f"owner-managed, max {max_auto} auto-applied changes with rollback and followups"
277
281
  safe_zones = "~/.nexo/scripts/, ~/.nexo/plugins/, ~/.nexo/brain/, NEXO_CODE/src, repo bin/docs/templates/tests"
278
282
  immutable_files = "db.py, server.py, plugin_loader.py, storage_router.py, nexo-watchdog.sh, evolution_cycle.py, CLAUDE.md, personality.md, user-profile.md"
283
+ elif mode == "public_core":
284
+ mode_desc = "public core contribution via isolated checkout and Draft PR"
285
+ safe_zones = "isolated public repo checkout only"
286
+ immutable_files = "personal runtime, ~/.nexo/**, local DBs/logs, CLAUDE.md, user-profile.md"
279
287
  else:
280
288
  mode_desc = f"public auto, max {max_auto} auto-applied changes in personal safe zones"
281
289
  safe_zones = "~/.nexo/scripts/, ~/.nexo/plugins/"
@@ -340,6 +348,48 @@ Max 3 proposals. Quality over quantity. If nothing needs improving, say so."""
340
348
  return prompt
341
349
 
342
350
 
351
+ def build_public_contribution_prompt(*, repo_root: str, cycle_number: int) -> str:
352
+ """Prompt for the public-core contributor mode.
353
+
354
+ This prompt must never rely on private runtime state. It should inspect only
355
+ the isolated public repo checkout, make one coherent improvement, and end
356
+ by returning machine-readable summary JSON.
357
+ """
358
+
359
+ return f"""You are NEXO Public Evolution.
360
+
361
+ You are running inside an isolated checkout of the public NEXO repository.
362
+ Your job is to make one technically coherent improvement to the public core and
363
+ prepare it for a Draft PR.
364
+
365
+ STRICT RULES:
366
+ - Work only inside this repository checkout: {repo_root}
367
+ - You may modify only public core surfaces: src/, bin/, tests/, templates/, hooks/, migrations/, .claude-plugin/
368
+ - Do not read or use ~/.nexo, local DBs, personal scripts, emails, logs, prompts, secrets, or any user-identifying paths
369
+ - Do not push, open PRs, or change git remotes yourself
370
+ - Do not touch README, website, gh-pages, changelog, or release metadata in this mode
371
+ - Focus on one concrete improvement only
372
+ - Run validation for the files you touched
373
+
374
+ What to do:
375
+ 1. Inspect the repo and find a real, self-contained improvement in reliability, install/update behavior, cron recovery, diagnostics, hooks, tests, or other core infrastructure.
376
+ 2. Implement the change directly in this checkout.
377
+ 3. Run the smallest relevant validation commands.
378
+ 4. Return ONLY valid JSON with this shape:
379
+
380
+ {{
381
+ "title": "type: short title",
382
+ "problem": "what was wrong",
383
+ "summary": "what you changed",
384
+ "tests": ["command 1", "command 2"],
385
+ "risks": ["risk 1", "risk 2"]
386
+ }}
387
+
388
+ Cycle: #{cycle_number}
389
+ Quality over quantity. One strong improvement is better than three weak ones.
390
+ """
391
+
392
+
343
393
  def max_auto_changes(total_evolutions: int) -> int:
344
394
  """Progressive trust: 1 for first 4 cycles, 2 for next 4, then 3."""
345
395
  if total_evolutions < 4:
@@ -236,7 +236,7 @@ def handle_session_diary_read(session_id: str = '', last_n: int = 3, last_day: b
236
236
  Args:
237
237
  session_id: Specific session ID to read (optional)
238
238
  last_n: Number of recent entries to return (default 3)
239
- last_day: If true, returns ALL entries from the most recent day (multi-terminal aware). Use this at startup.
239
+ last_day: If true, returns the recent continuity window (~36h), including the previous evening. Use this at startup.
240
240
  domain: Filter by project context: ecommerce, project-a, nexo, project-b, server, other
241
241
  brief: If true, returns ONLY the last diary entry with summary + mental_state + context_next.
242
242
  Use this at startup for fast context loading (~1K chars instead of full dump).
@@ -0,0 +1,135 @@
1
+ """NEXO Personal Plugins — scaffold persistent MCP tools in NEXO_HOME/plugins."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+
9
+ from db import init_db
10
+ from script_registry import create_script
11
+
12
+
13
+ NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
14
+ NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(Path(__file__).resolve().parents[1])))
15
+
16
+
17
+ def _plugins_dir() -> Path:
18
+ path = NEXO_HOME / "plugins"
19
+ path.mkdir(parents=True, exist_ok=True)
20
+ return path
21
+
22
+
23
+ def _safe_slug(value: str) -> str:
24
+ chars: list[str] = []
25
+ for ch in str(value or "").lower():
26
+ if ch.isalnum():
27
+ chars.append(ch)
28
+ elif ch in {"-", "_", " "}:
29
+ chars.append("-")
30
+ slug = "".join(chars).strip("-")
31
+ return slug or "plugin"
32
+
33
+
34
+ def _template_path(name: str) -> Path | None:
35
+ candidates = [
36
+ NEXO_CODE.parent / "templates" / name,
37
+ NEXO_HOME / "templates" / name,
38
+ ]
39
+ for candidate in candidates:
40
+ if candidate.is_file():
41
+ return candidate
42
+ return None
43
+
44
+
45
+ def _load_template() -> str:
46
+ template = _template_path("plugin-template.py")
47
+ if template:
48
+ return template.read_text()
49
+ return (
50
+ "from __future__ import annotations\n"
51
+ "import json\n\n"
52
+ "def handle_example_tool(payload_json: str = \"{}\") -> str:\n"
53
+ " try:\n"
54
+ " payload = json.loads(payload_json or \"{}\")\n"
55
+ " except Exception as exc:\n"
56
+ " return json.dumps({\"ok\": False, \"error\": f\"invalid json: {exc}\"}, ensure_ascii=False)\n"
57
+ " return json.dumps({\"ok\": True, \"payload\": payload}, ensure_ascii=False)\n\n"
58
+ "TOOLS = [\n"
59
+ " (handle_example_tool, \"nexo_example_tool\", \"Example personal MCP tool scaffold.\"),\n"
60
+ "]\n"
61
+ )
62
+
63
+
64
+ def _render_plugin_template(*, plugin_stem: str, tool_name: str, description: str) -> str:
65
+ content = _load_template()
66
+ handler_name = f"handle_{plugin_stem.replace('-', '_')}"
67
+ content = content.replace("handle_example_tool", handler_name)
68
+ content = content.replace("nexo_example_tool", tool_name)
69
+ content = content.replace(
70
+ "Personal plugin scaffold created. Edit this handler in NEXO_HOME/plugins.",
71
+ description or f"Personal plugin scaffold for {plugin_stem}.",
72
+ )
73
+ content = content.replace(
74
+ "Example personal MCP tool scaffold. Edit it in NEXO_HOME/plugins.",
75
+ description or f"Personal MCP tool scaffold for {plugin_stem}.",
76
+ )
77
+ return content
78
+
79
+
80
+ def handle_personal_plugin_create(
81
+ name: str,
82
+ description: str = "",
83
+ tool_name: str = "",
84
+ create_companion_script: bool = False,
85
+ script_runtime: str = "python",
86
+ force: bool = False,
87
+ ) -> str:
88
+ """Create a personal MCP plugin scaffold in NEXO_HOME/plugins.
89
+
90
+ Optionally also creates a companion script in NEXO_HOME/scripts.
91
+ """
92
+ init_db()
93
+ plugin_stem = _safe_slug(name)
94
+ filename = f"{plugin_stem}.py"
95
+ tool_name = (tool_name or f"nexo_{plugin_stem.replace('-', '_')}").strip()
96
+ plugin_path = _plugins_dir() / filename
97
+ if plugin_path.exists() and not force:
98
+ return json.dumps({
99
+ "ok": False,
100
+ "error": f"Plugin already exists: {plugin_path}",
101
+ }, ensure_ascii=False)
102
+
103
+ content = _render_plugin_template(
104
+ plugin_stem=plugin_stem,
105
+ tool_name=tool_name,
106
+ description=description or f"Personal MCP tool for {name}.",
107
+ )
108
+ plugin_path.write_text(content)
109
+
110
+ script_result = None
111
+ if create_companion_script:
112
+ script_result = create_script(
113
+ plugin_stem,
114
+ description=f"Companion script for plugin {plugin_stem}",
115
+ runtime=script_runtime,
116
+ force=force,
117
+ )
118
+
119
+ return json.dumps({
120
+ "ok": True,
121
+ "name": plugin_stem,
122
+ "tool_name": tool_name,
123
+ "plugin_path": str(plugin_path),
124
+ "companion_script": script_result,
125
+ "next_step": f"Load the plugin with nexo_plugin_load(filename='{filename}') after editing it.",
126
+ }, ensure_ascii=False)
127
+
128
+
129
+ TOOLS = [
130
+ (
131
+ handle_personal_plugin_create,
132
+ "nexo_personal_plugin_create",
133
+ "Create a persistent personal MCP plugin scaffold in NEXO_HOME/plugins, optionally with a companion script in NEXO_HOME/scripts.",
134
+ ),
135
+ ]
@@ -341,11 +341,20 @@ def _rollback_npm_package(target_version: str) -> str | None:
341
341
  return None
342
342
 
343
343
 
344
- def _handle_packaged_update() -> str:
344
+ def _emit_progress(progress_fn, message: str) -> None:
345
+ if callable(progress_fn):
346
+ try:
347
+ progress_fn(message)
348
+ except Exception:
349
+ pass
350
+
351
+
352
+ def _handle_packaged_update(progress_fn=None) -> str:
345
353
  """Update a packaged (npm) install — no git repo available."""
346
354
  old_version = _read_version()
347
355
 
348
356
  # 1. Backup databases BEFORE any changes
357
+ _emit_progress(progress_fn, "Backing up runtime databases...")
349
358
  backup_dir, backup_err = _backup_databases()
350
359
  if backup_err:
351
360
  return f"ABORTED at backup: {backup_err}"
@@ -353,12 +362,14 @@ def _handle_packaged_update() -> str:
353
362
  # 2. Backup NEXO_HOME code tree BEFORE npm update
354
363
  # postinstall copies hooks/core/plugins/scripts into NEXO_HOME,
355
364
  # so we need a full snapshot to restore on failure.
365
+ _emit_progress(progress_fn, "Backing up runtime files...")
356
366
  code_backup_dir, code_err = _backup_code_tree()
357
367
  if code_err:
358
368
  return f"ABORTED at code tree backup: {code_err}"
359
369
 
360
370
  # 3. Run npm update (postinstall.js will migrate NEXO_HOME in-place)
361
371
  try:
372
+ _emit_progress(progress_fn, "Downloading and applying the latest npm package...")
362
373
  result = subprocess.run(
363
374
  ["npm", "update", "-g", "nexo-brain"],
364
375
  capture_output=True, text=True, timeout=120,
@@ -402,16 +413,19 @@ def _handle_packaged_update() -> str:
402
413
  errors = []
403
414
 
404
415
  # Reinstall pip deps for new version
416
+ _emit_progress(progress_fn, "Reconciling Python dependencies...")
405
417
  pip_err = _reinstall_pip_deps()
406
418
  if pip_err:
407
419
  errors.append(f"pip deps: {pip_err}")
408
420
 
409
421
  # Run migrations
422
+ _emit_progress(progress_fn, "Running runtime migrations...")
410
423
  mig_err = _run_migrations()
411
424
  if mig_err:
412
425
  errors.append(f"migrations: {mig_err}")
413
426
 
414
427
  # Verify server can still import
428
+ _emit_progress(progress_fn, "Verifying runtime import health...")
415
429
  verify_err = _verify_import()
416
430
  if verify_err:
417
431
  errors.append(f"verification: {verify_err}")
@@ -457,7 +471,7 @@ def _handle_packaged_update() -> str:
457
471
  return "\n".join(lines)
458
472
 
459
473
 
460
- def handle_update(remote: str = "origin", branch: str = "main") -> str:
474
+ def handle_update(remote: str = "origin", branch: str = "main", progress_fn=None) -> str:
461
475
  """Pull latest NEXO code, backup databases, run migrations, and verify.
462
476
 
463
477
  Supports both git checkouts and packaged (npm) installs.
@@ -477,7 +491,7 @@ def handle_update(remote: str = "origin", branch: str = "main") -> str:
477
491
  """
478
492
  # Packaged install — no git repo
479
493
  if not _is_git_repo():
480
- return _handle_packaged_update()
494
+ return _handle_packaged_update(progress_fn=progress_fn)
481
495
 
482
496
  steps_done = []
483
497
  old_commit = None
@@ -485,6 +499,7 @@ def handle_update(remote: str = "origin", branch: str = "main") -> str:
485
499
 
486
500
  try:
487
501
  # Step 1: Check dirty (full worktree)
502
+ _emit_progress(progress_fn, "Checking repository state...")
488
503
  dirty_err = _check_dirty()
489
504
  if dirty_err:
490
505
  return f"ABORTED: {dirty_err}"
@@ -498,12 +513,14 @@ def handle_update(remote: str = "origin", branch: str = "main") -> str:
498
513
  return "ABORTED: Not a git repository or git not available."
499
514
 
500
515
  # Step 2: Backup databases
516
+ _emit_progress(progress_fn, "Backing up runtime databases...")
501
517
  backup_dir, backup_err = _backup_databases()
502
518
  if backup_err:
503
519
  return f"ABORTED at backup: {backup_err}"
504
520
  steps_done.append("backup")
505
521
 
506
522
  # Step 3: git pull
523
+ _emit_progress(progress_fn, "Pulling latest source changes...")
507
524
  rc, pull_out, pull_err = _git("pull", remote, branch)
508
525
  if rc != 0:
509
526
  return f"ABORTED at git pull: {pull_err or pull_out}"
@@ -517,6 +534,7 @@ def handle_update(remote: str = "origin", branch: str = "main") -> str:
517
534
 
518
535
  # Step 5: Reinstall pip dependencies if requirements.txt changed
519
536
  if deps_changed or version_changed:
537
+ _emit_progress(progress_fn, "Reconciling Python dependencies...")
520
538
  pip_err = _reinstall_pip_deps()
521
539
  if pip_err:
522
540
  raise RuntimeError(f"Pip install failed: {pip_err}")
@@ -524,12 +542,14 @@ def handle_update(remote: str = "origin", branch: str = "main") -> str:
524
542
 
525
543
  # Step 6: Run migrations if version changed
526
544
  if version_changed:
545
+ _emit_progress(progress_fn, "Running runtime migrations...")
527
546
  mig_err = _run_migrations()
528
547
  if mig_err:
529
548
  raise RuntimeError(f"Migration failed: {mig_err}")
530
549
  steps_done.append("migrations")
531
550
 
532
551
  # Step 7: Verify import
552
+ _emit_progress(progress_fn, "Verifying runtime import health...")
533
553
  verify_err = _verify_import()
534
554
  if verify_err:
535
555
  raise RuntimeError(f"Verification failed: {verify_err}")
@@ -540,6 +560,7 @@ def handle_update(remote: str = "origin", branch: str = "main") -> str:
540
560
  try:
541
561
  cron_sync_path = SRC_DIR / "crons" / "sync.py"
542
562
  if cron_sync_path.exists():
563
+ _emit_progress(progress_fn, "Syncing core cron definitions...")
543
564
  r = subprocess.run(
544
565
  [sys.executable, str(cron_sync_path)],
545
566
  capture_output=True, text=True, timeout=30,
@@ -557,6 +578,7 @@ def handle_update(remote: str = "origin", branch: str = "main") -> str:
557
578
 
558
579
  # Step 9: Sync hooks to NEXO_HOME
559
580
  try:
581
+ _emit_progress(progress_fn, "Syncing core Claude hooks...")
560
582
  _sync_hooks_to_home()
561
583
  steps_done.append("hook-sync")
562
584
  except Exception as e: