nexo-brain 2.6.6 → 2.6.9
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/.claude-plugin/plugin.json +2 -2
- package/bin/nexo-brain.js +117 -0
- package/package.json +1 -1
- package/src/auto_close_sessions.py +24 -4
- package/src/auto_update.py +31 -5
- package/src/cli.py +112 -2
- package/src/db/_episodic.py +5 -16
- package/src/evolution_cycle.py +51 -1
- package/src/plugins/episodic_memory.py +1 -1
- package/src/plugins/personal_plugins.py +135 -0
- package/src/plugins/update.py +25 -3
- package/src/public_contribution.py +396 -0
- package/src/scripts/nexo-evolution-run.py +394 -2
- package/templates/plugin-template.py +36 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "2.6.
|
|
4
|
-
"description": "Local cognitive runtime for Claude Code
|
|
3
|
+
"version": "2.6.9",
|
|
4
|
+
"description": "Local cognitive runtime for Claude Code \u2014 persistent memory, overnight learning, doctor diagnostics, personal scripts, recovery-aware jobs, startup preflight, and optional dashboard/power helper.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "NEXO Brain",
|
|
7
7
|
"email": "info@nexo-brain.com",
|
package/bin/nexo-brain.js
CHANGED
|
@@ -32,6 +32,7 @@ const LAUNCH_AGENTS = path.join(
|
|
|
32
32
|
"LaunchAgents"
|
|
33
33
|
);
|
|
34
34
|
const MACOS_FDA_SETTINGS_URL = "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles";
|
|
35
|
+
const PUBLIC_CONTRIBUTION_UPSTREAM = "wazionapps/nexo";
|
|
35
36
|
|
|
36
37
|
function isEphemeralInstall(nexoHome) {
|
|
37
38
|
const homeDir = require("os").homedir();
|
|
@@ -433,6 +434,22 @@ function getDefaultSchedule(timezone) {
|
|
|
433
434
|
full_disk_access_status: "unset",
|
|
434
435
|
full_disk_access_status_version: 1,
|
|
435
436
|
full_disk_access_reasons: [],
|
|
437
|
+
public_contribution: {
|
|
438
|
+
enabled: false,
|
|
439
|
+
mode: "unset",
|
|
440
|
+
consent_version: 1,
|
|
441
|
+
github_user: "",
|
|
442
|
+
upstream_repo: PUBLIC_CONTRIBUTION_UPSTREAM,
|
|
443
|
+
fork_repo: "",
|
|
444
|
+
machine_id: crypto.createHash("sha1").update(require("os").hostname()).digest("hex").slice(0, 12),
|
|
445
|
+
active_pr_url: "",
|
|
446
|
+
active_pr_number: null,
|
|
447
|
+
active_branch: "",
|
|
448
|
+
status: "unset",
|
|
449
|
+
cooldown_until: "",
|
|
450
|
+
last_run_at: "",
|
|
451
|
+
last_result: "",
|
|
452
|
+
},
|
|
436
453
|
processes: {
|
|
437
454
|
"cognitive-decay": { hour: 3, minute: 0 },
|
|
438
455
|
"postmortem": { hour: 23, minute: 30 },
|
|
@@ -445,6 +462,23 @@ function getDefaultSchedule(timezone) {
|
|
|
445
462
|
};
|
|
446
463
|
}
|
|
447
464
|
|
|
465
|
+
function normalizePublicContributionConfig(config = {}) {
|
|
466
|
+
const base = getDefaultSchedule().public_contribution;
|
|
467
|
+
const merged = { ...base, ...(config || {}) };
|
|
468
|
+
merged.enabled = Boolean(merged.enabled);
|
|
469
|
+
merged.mode = String(merged.mode || "unset").toLowerCase();
|
|
470
|
+
merged.status = String(merged.status || "unset").toLowerCase();
|
|
471
|
+
merged.github_user = String(merged.github_user || "").trim();
|
|
472
|
+
merged.fork_repo = String(merged.fork_repo || "").trim();
|
|
473
|
+
merged.upstream_repo = String(merged.upstream_repo || PUBLIC_CONTRIBUTION_UPSTREAM).trim() || PUBLIC_CONTRIBUTION_UPSTREAM;
|
|
474
|
+
merged.active_pr_url = String(merged.active_pr_url || "").trim();
|
|
475
|
+
merged.active_branch = String(merged.active_branch || "").trim();
|
|
476
|
+
merged.cooldown_until = String(merged.cooldown_until || "").trim();
|
|
477
|
+
merged.last_run_at = String(merged.last_run_at || "").trim();
|
|
478
|
+
merged.last_result = String(merged.last_result || "").trim();
|
|
479
|
+
return merged;
|
|
480
|
+
}
|
|
481
|
+
|
|
448
482
|
async function maybeConfigurePowerPolicy(schedule, useDefaults) {
|
|
449
483
|
const current = String((schedule && schedule.power_policy) || "unset").toLowerCase();
|
|
450
484
|
if (current && current !== "unset") {
|
|
@@ -477,6 +511,84 @@ async function maybeConfigurePowerPolicy(schedule, useDefaults) {
|
|
|
477
511
|
return schedule;
|
|
478
512
|
}
|
|
479
513
|
|
|
514
|
+
function ghLogin() {
|
|
515
|
+
const login = run("gh api user --jq .login 2>/dev/null");
|
|
516
|
+
return (login || "").trim();
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function ensureFork(login) {
|
|
520
|
+
if (!login) return { ok: false, message: "Missing GitHub login.", forkRepo: "" };
|
|
521
|
+
const forkRepo = `${login}/nexo`;
|
|
522
|
+
const existing = run(`gh repo view "${forkRepo}" --json nameWithOwner 2>/dev/null`);
|
|
523
|
+
if (existing) return { ok: true, message: "", forkRepo };
|
|
524
|
+
const created = run(`gh repo fork "${PUBLIC_CONTRIBUTION_UPSTREAM}" --clone=false --remote=false 2>/dev/null`);
|
|
525
|
+
if (created !== null) return { ok: true, message: "", forkRepo };
|
|
526
|
+
return { ok: false, message: `Could not ensure fork ${forkRepo}.`, forkRepo: "" };
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
async function maybeConfigurePublicContribution(schedule, useDefaults) {
|
|
530
|
+
const current = normalizePublicContributionConfig((schedule && schedule.public_contribution) || {});
|
|
531
|
+
if (current.mode && current.mode !== "unset") {
|
|
532
|
+
schedule.public_contribution = current;
|
|
533
|
+
fs.writeFileSync(path.join(NEXO_HOME, "config", "schedule.json"), JSON.stringify(schedule, null, 2));
|
|
534
|
+
return schedule;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (useDefaults || !process.stdin.isTTY || !process.stdout.isTTY) {
|
|
538
|
+
schedule.public_contribution = current;
|
|
539
|
+
fs.writeFileSync(path.join(NEXO_HOME, "config", "schedule.json"), JSON.stringify(schedule, null, 2));
|
|
540
|
+
return schedule;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
console.log("");
|
|
544
|
+
log("Optional public contribution mode:");
|
|
545
|
+
log("If enabled, this machine may prepare core NEXO improvements from an isolated checkout and open a Draft PR to the public repository.");
|
|
546
|
+
log("NEXO never auto-merges, and it pauses public evolution on this machine while that Draft PR stays open.");
|
|
547
|
+
log("Public contribution must never publish personal scripts, runtime data, local prompts, logs, or secrets.");
|
|
548
|
+
const answer = (await ask(" Enable public contribution via Draft PRs on this machine? [y/N/later]: ")).trim().toLowerCase();
|
|
549
|
+
if (answer === "y" || answer === "yes") {
|
|
550
|
+
const login = ghLogin();
|
|
551
|
+
if (!login) {
|
|
552
|
+
current.enabled = false;
|
|
553
|
+
current.mode = "pending_auth";
|
|
554
|
+
current.status = "pending_auth";
|
|
555
|
+
current.github_user = "";
|
|
556
|
+
current.fork_repo = "";
|
|
557
|
+
log("GitHub CLI authentication is missing. Contributor mode is pending until 'gh auth login' succeeds.");
|
|
558
|
+
} else {
|
|
559
|
+
const fork = ensureFork(login);
|
|
560
|
+
if (!fork.ok) {
|
|
561
|
+
current.enabled = false;
|
|
562
|
+
current.mode = "pending_auth";
|
|
563
|
+
current.status = "pending_auth";
|
|
564
|
+
current.github_user = login;
|
|
565
|
+
current.fork_repo = "";
|
|
566
|
+
log(fork.message || "Could not ensure a GitHub fork.");
|
|
567
|
+
} else {
|
|
568
|
+
current.enabled = true;
|
|
569
|
+
current.mode = "draft_prs";
|
|
570
|
+
current.status = "active";
|
|
571
|
+
current.github_user = login;
|
|
572
|
+
current.fork_repo = fork.forkRepo;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
} else if (answer === "later" || answer === "l" || answer === "") {
|
|
576
|
+
current.enabled = false;
|
|
577
|
+
current.mode = "unset";
|
|
578
|
+
current.status = "unset";
|
|
579
|
+
} else {
|
|
580
|
+
current.enabled = false;
|
|
581
|
+
current.mode = "off";
|
|
582
|
+
current.status = "off";
|
|
583
|
+
current.github_user = "";
|
|
584
|
+
current.fork_repo = "";
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
schedule.public_contribution = current;
|
|
588
|
+
fs.writeFileSync(path.join(NEXO_HOME, "config", "schedule.json"), JSON.stringify(schedule, null, 2));
|
|
589
|
+
return schedule;
|
|
590
|
+
}
|
|
591
|
+
|
|
480
592
|
/**
|
|
481
593
|
* Resolve the venv python path for an existing NEXO_HOME installation.
|
|
482
594
|
*/
|
|
@@ -980,6 +1092,7 @@ async function main() {
|
|
|
980
1092
|
// Regenerate all core LaunchAgents / systemd timers
|
|
981
1093
|
let migSchedule = loadOrCreateSchedule(NEXO_HOME);
|
|
982
1094
|
migSchedule = await maybeConfigurePowerPolicy(migSchedule, useDefaults);
|
|
1095
|
+
migSchedule = await maybeConfigurePublicContribution(migSchedule, useDefaults);
|
|
983
1096
|
const migPython = findVenvPython(NEXO_HOME) || "python3";
|
|
984
1097
|
migSchedule = await maybeConfigureFullDiskAccess(migSchedule, useDefaults, migPython);
|
|
985
1098
|
let migOptionals = {};
|
|
@@ -1616,6 +1729,7 @@ async function main() {
|
|
|
1616
1729
|
);
|
|
1617
1730
|
|
|
1618
1731
|
// Copy source files
|
|
1732
|
+
log("Copying core runtime files...");
|
|
1619
1733
|
const srcDir = path.join(__dirname, "..", "src");
|
|
1620
1734
|
const pluginsSrcDir = path.join(srcDir, "plugins");
|
|
1621
1735
|
const scriptsSrcDir = path.join(srcDir, "scripts");
|
|
@@ -1694,6 +1808,7 @@ async function main() {
|
|
|
1694
1808
|
fs.writeFileSync(runtimeCliPath, runtimeCli);
|
|
1695
1809
|
fs.chmodSync(runtimeCliPath, 0o755);
|
|
1696
1810
|
|
|
1811
|
+
log("Copying core packages...");
|
|
1697
1812
|
// Core packages (directories with __init__.py)
|
|
1698
1813
|
["db", "cognitive", "doctor"].forEach(pkg => {
|
|
1699
1814
|
const pkgSrc = path.join(srcDir, pkg);
|
|
@@ -1702,6 +1817,7 @@ async function main() {
|
|
|
1702
1817
|
}
|
|
1703
1818
|
});
|
|
1704
1819
|
|
|
1820
|
+
log("Copying plugins, scripts, and templates...");
|
|
1705
1821
|
// Plugins (all .py files in plugins/)
|
|
1706
1822
|
fs.mkdirSync(path.join(NEXO_HOME, "plugins"), { recursive: true });
|
|
1707
1823
|
if (fs.existsSync(pluginsSrcDir)) {
|
|
@@ -2254,6 +2370,7 @@ ${doScan ? `- Stack: ${Object.keys(profileData.code.languages || {}).slice(0, 5)
|
|
|
2254
2370
|
log("Setting up automated processes...");
|
|
2255
2371
|
let schedule = loadOrCreateSchedule(NEXO_HOME);
|
|
2256
2372
|
schedule = await maybeConfigurePowerPolicy(schedule, useDefaults);
|
|
2373
|
+
schedule = await maybeConfigurePublicContribution(schedule, useDefaults);
|
|
2257
2374
|
schedule = await maybeConfigureFullDiskAccess(schedule, useDefaults, python);
|
|
2258
2375
|
const enabledOptionals = { dashboard: doDashboard };
|
|
2259
2376
|
if (isEphemeralInstall(NEXO_HOME)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.9",
|
|
4
4
|
"mcpName": "io.github.wazionapps/nexo",
|
|
5
5
|
"description": "NEXO — local cognitive runtime for Claude Code. Persistent memory, overnight learning, recovery-aware crons, personal scripts, doctor diagnostics, startup preflight, and optional power helper.",
|
|
6
6
|
"bin": {
|
|
@@ -17,7 +17,7 @@ os.environ["NEXO_SKIP_FS_INDEX"] = "1" # Skip FTS rebuild on import
|
|
|
17
17
|
|
|
18
18
|
from db import (
|
|
19
19
|
init_db, get_db, get_diary_draft, delete_diary_draft,
|
|
20
|
-
get_orphan_sessions, write_session_diary, now_epoch,
|
|
20
|
+
get_orphan_sessions, read_checkpoint, write_session_diary, now_epoch,
|
|
21
21
|
SESSION_STALE_SECONDS,
|
|
22
22
|
)
|
|
23
23
|
|
|
@@ -67,9 +67,18 @@ def promote_draft_to_diary(sid: str, draft: dict, task: str = ""):
|
|
|
67
67
|
context_hint = draft.get("last_context_hint", "")
|
|
68
68
|
hb_count = draft.get("heartbeat_count", 0)
|
|
69
69
|
|
|
70
|
+
checkpoint = read_checkpoint(sid) or {}
|
|
70
71
|
summary_parts = []
|
|
71
72
|
if draft.get("summary_draft"):
|
|
72
73
|
summary_parts.append(draft["summary_draft"])
|
|
74
|
+
if task and task not in " ".join(summary_parts):
|
|
75
|
+
summary_parts.append(f"Final task: {task}")
|
|
76
|
+
if context_hint:
|
|
77
|
+
summary_parts.append(f"Latest context: {context_hint[:300]}")
|
|
78
|
+
if checkpoint.get("current_goal"):
|
|
79
|
+
summary_parts.append(f"Current goal: {str(checkpoint['current_goal'])[:300]}")
|
|
80
|
+
if checkpoint.get("next_step"):
|
|
81
|
+
summary_parts.append(f"Next step was: {str(checkpoint['next_step'])[:240]}")
|
|
73
82
|
|
|
74
83
|
tool_summary = get_tool_log_summary(sid)
|
|
75
84
|
if tool_summary:
|
|
@@ -98,6 +107,10 @@ def promote_draft_to_diary(sid: str, draft: dict, task: str = ""):
|
|
|
98
107
|
context_next = f"Last topic: {context_hint}"
|
|
99
108
|
if tasks:
|
|
100
109
|
context_next += f" | Tasks: {', '.join(tasks[-5:])}"
|
|
110
|
+
if checkpoint.get("reasoning_thread"):
|
|
111
|
+
context_next += f" | Reasoning: {str(checkpoint['reasoning_thread'])[:240]}"
|
|
112
|
+
if checkpoint.get("active_files"):
|
|
113
|
+
context_next += f" | Active files: {str(checkpoint['active_files'])[:180]}"
|
|
101
114
|
|
|
102
115
|
write_session_diary(
|
|
103
116
|
session_id=sid,
|
|
@@ -131,12 +144,19 @@ def main():
|
|
|
131
144
|
if draft:
|
|
132
145
|
promote_draft_to_diary(sid, draft, task=session.get("task", ""))
|
|
133
146
|
else:
|
|
147
|
+
checkpoint = read_checkpoint(sid) or {}
|
|
148
|
+
tool_summary = get_tool_log_summary(sid)
|
|
149
|
+
summary_parts = [f"Auto-closed session. Task: {session.get('task', 'unknown')}"]
|
|
150
|
+
if checkpoint.get("current_goal"):
|
|
151
|
+
summary_parts.append(f"Current goal: {str(checkpoint['current_goal'])[:300]}")
|
|
152
|
+
if tool_summary:
|
|
153
|
+
summary_parts.append(tool_summary)
|
|
134
154
|
write_session_diary(
|
|
135
155
|
session_id=sid,
|
|
136
156
|
decisions="No decisions logged",
|
|
137
|
-
summary=
|
|
138
|
-
context_next="",
|
|
139
|
-
mental_state="[auto-close] No draft available.
|
|
157
|
+
summary=" | ".join(summary_parts),
|
|
158
|
+
context_next=str(checkpoint.get("next_step") or ""),
|
|
159
|
+
mental_state="[auto-close] No draft available. Diary reconstructed from task/checkpoint/tool logs.",
|
|
140
160
|
self_critique="[auto-close] Session terminated without diary or draft.",
|
|
141
161
|
source="auto-close",
|
|
142
162
|
)
|
package/src/auto_update.py
CHANGED
|
@@ -1236,7 +1236,7 @@ def _restore_runtime_tree(backup_dir: str, dest: Path = NEXO_HOME) -> None:
|
|
|
1236
1236
|
shutil.copy2(str(item), str(target))
|
|
1237
1237
|
|
|
1238
1238
|
|
|
1239
|
-
def _copy_runtime_from_source(src_dir: Path, repo_dir: Path, dest: Path = NEXO_HOME) -> dict:
|
|
1239
|
+
def _copy_runtime_from_source(src_dir: Path, repo_dir: Path, dest: Path = NEXO_HOME, progress_fn=None) -> dict:
|
|
1240
1240
|
import shutil
|
|
1241
1241
|
|
|
1242
1242
|
packages = ["db", "cognitive", "doctor", "dashboard", "rules", "crons", "hooks"]
|
|
@@ -1253,6 +1253,7 @@ def _copy_runtime_from_source(src_dir: Path, repo_dir: Path, dest: Path = NEXO_H
|
|
|
1253
1253
|
copied_packages = 0
|
|
1254
1254
|
copied_files = 0
|
|
1255
1255
|
|
|
1256
|
+
_emit_progress(progress_fn, "Copying core packages...")
|
|
1256
1257
|
for pkg in packages:
|
|
1257
1258
|
pkg_src = src_dir / pkg
|
|
1258
1259
|
pkg_dest = dest / pkg
|
|
@@ -1266,12 +1267,14 @@ def _copy_runtime_from_source(src_dir: Path, repo_dir: Path, dest: Path = NEXO_H
|
|
|
1266
1267
|
)
|
|
1267
1268
|
copied_packages += 1
|
|
1268
1269
|
|
|
1270
|
+
_emit_progress(progress_fn, "Copying core modules...")
|
|
1269
1271
|
for name in flat_files:
|
|
1270
1272
|
src_file = src_dir / name
|
|
1271
1273
|
if src_file.is_file():
|
|
1272
1274
|
shutil.copy2(str(src_file), str(dest / name))
|
|
1273
1275
|
copied_files += 1
|
|
1274
1276
|
|
|
1277
|
+
_emit_progress(progress_fn, "Copying plugin modules...")
|
|
1275
1278
|
plugins_src = src_dir / "plugins"
|
|
1276
1279
|
plugins_dest = dest / "plugins"
|
|
1277
1280
|
if plugins_src.is_dir():
|
|
@@ -1280,6 +1283,7 @@ def _copy_runtime_from_source(src_dir: Path, repo_dir: Path, dest: Path = NEXO_H
|
|
|
1280
1283
|
if item.is_file() and item.suffix == ".py":
|
|
1281
1284
|
shutil.copy2(str(item), str(plugins_dest / item.name))
|
|
1282
1285
|
|
|
1286
|
+
_emit_progress(progress_fn, "Copying scripts...")
|
|
1283
1287
|
scripts_src = src_dir / "scripts"
|
|
1284
1288
|
scripts_dest = dest / "scripts"
|
|
1285
1289
|
if scripts_src.is_dir():
|
|
@@ -1297,6 +1301,7 @@ def _copy_runtime_from_source(src_dir: Path, repo_dir: Path, dest: Path = NEXO_H
|
|
|
1297
1301
|
if item.suffix == ".sh":
|
|
1298
1302
|
dst.chmod(0o755)
|
|
1299
1303
|
|
|
1304
|
+
_emit_progress(progress_fn, "Copying templates and version metadata...")
|
|
1300
1305
|
templates_src = repo_dir / "templates"
|
|
1301
1306
|
templates_dest = dest / "templates"
|
|
1302
1307
|
if templates_src.is_dir():
|
|
@@ -1317,6 +1322,7 @@ def _copy_runtime_from_source(src_dir: Path, repo_dir: Path, dest: Path = NEXO_H
|
|
|
1317
1322
|
except Exception:
|
|
1318
1323
|
pass
|
|
1319
1324
|
|
|
1325
|
+
_emit_progress(progress_fn, "Copying core skills and runtime wrapper...")
|
|
1320
1326
|
skills_src = src_dir / "skills"
|
|
1321
1327
|
skills_dest = dest / "skills-core"
|
|
1322
1328
|
if skills_src.is_dir():
|
|
@@ -1365,10 +1371,11 @@ def _reinstall_runtime_pip_deps(runtime_root: Path = NEXO_HOME) -> bool:
|
|
|
1365
1371
|
return False
|
|
1366
1372
|
|
|
1367
1373
|
|
|
1368
|
-
def _run_runtime_post_sync(dest: Path = NEXO_HOME) -> tuple[bool, list[str]]:
|
|
1374
|
+
def _run_runtime_post_sync(dest: Path = NEXO_HOME, progress_fn=None) -> tuple[bool, list[str]]:
|
|
1369
1375
|
actions: list[str] = []
|
|
1370
1376
|
env = {**os.environ, "NEXO_HOME": str(dest), "NEXO_CODE": str(dest)}
|
|
1371
1377
|
try:
|
|
1378
|
+
_emit_progress(progress_fn, "Initializing database and reconciling personal schedules...")
|
|
1372
1379
|
init_result = subprocess.run(
|
|
1373
1380
|
[
|
|
1374
1381
|
sys.executable,
|
|
@@ -1394,6 +1401,7 @@ def _run_runtime_post_sync(dest: Path = NEXO_HOME) -> tuple[bool, list[str]]:
|
|
|
1394
1401
|
except Exception as e:
|
|
1395
1402
|
return False, [f"runtime init error: {e}"]
|
|
1396
1403
|
|
|
1404
|
+
_emit_progress(progress_fn, "Reconciling Python dependencies...")
|
|
1397
1405
|
if _reinstall_runtime_pip_deps(dest):
|
|
1398
1406
|
actions.append("pip-deps")
|
|
1399
1407
|
else:
|
|
@@ -1402,6 +1410,7 @@ def _run_runtime_post_sync(dest: Path = NEXO_HOME) -> tuple[bool, list[str]]:
|
|
|
1402
1410
|
sync_path = dest / "crons" / "sync.py"
|
|
1403
1411
|
if sync_path.is_file():
|
|
1404
1412
|
try:
|
|
1413
|
+
_emit_progress(progress_fn, "Syncing core cron definitions...")
|
|
1405
1414
|
sync_result = subprocess.run(
|
|
1406
1415
|
[sys.executable, str(sync_path)],
|
|
1407
1416
|
cwd=str(dest),
|
|
@@ -1418,10 +1427,12 @@ def _run_runtime_post_sync(dest: Path = NEXO_HOME) -> tuple[bool, list[str]]:
|
|
|
1418
1427
|
|
|
1419
1428
|
from runtime_power import apply_power_policy
|
|
1420
1429
|
|
|
1430
|
+
_emit_progress(progress_fn, "Refreshing runtime power helper...")
|
|
1421
1431
|
power_result = apply_power_policy()
|
|
1422
1432
|
if power_result.get("ok"):
|
|
1423
1433
|
actions.append(f"power:{power_result.get('action')}")
|
|
1424
1434
|
|
|
1435
|
+
_emit_progress(progress_fn, "Verifying runtime imports...")
|
|
1425
1436
|
verify = subprocess.run(
|
|
1426
1437
|
[sys.executable, "-c", "import server"],
|
|
1427
1438
|
cwd=str(dest),
|
|
@@ -1460,11 +1471,20 @@ def _write_update_summary(summary: dict):
|
|
|
1460
1471
|
_log(f"Failed to write update summary: {e}")
|
|
1461
1472
|
|
|
1462
1473
|
|
|
1463
|
-
def
|
|
1474
|
+
def _emit_progress(progress_fn, message: str) -> None:
|
|
1475
|
+
if callable(progress_fn):
|
|
1476
|
+
try:
|
|
1477
|
+
progress_fn(message)
|
|
1478
|
+
except Exception:
|
|
1479
|
+
pass
|
|
1480
|
+
|
|
1481
|
+
|
|
1482
|
+
def manual_sync_update(*, interactive: bool = False, allow_source_pull: bool = True, progress_fn=None) -> dict:
|
|
1464
1483
|
src_dir, repo_dir = _resolve_sync_source()
|
|
1465
1484
|
if src_dir is None or repo_dir is None:
|
|
1466
1485
|
return {"ok": False, "mode": "sync", "error": "No source repo recorded for this runtime."}
|
|
1467
1486
|
|
|
1487
|
+
_emit_progress(progress_fn, "Checking recorded source repository...")
|
|
1468
1488
|
source_status = _source_repo_status(repo_dir)
|
|
1469
1489
|
pulled = False
|
|
1470
1490
|
old_head = source_status.get("local_head")
|
|
@@ -1474,17 +1494,21 @@ def manual_sync_update(*, interactive: bool = False, allow_source_pull: bool = T
|
|
|
1474
1494
|
elif source_status.get("diverged"):
|
|
1475
1495
|
_log("Source repo diverged; syncing local tree without remote pull.")
|
|
1476
1496
|
elif source_status.get("behind"):
|
|
1497
|
+
_emit_progress(progress_fn, "Pulling latest source changes...")
|
|
1477
1498
|
rc, _, pull_err = _git_in_repo(repo_dir, "pull", "--ff-only", timeout=60)
|
|
1478
1499
|
if rc != 0:
|
|
1479
1500
|
return {"ok": False, "mode": "sync", "error": pull_err or "git pull failed"}
|
|
1480
1501
|
pulled = True
|
|
1481
1502
|
|
|
1503
|
+
_emit_progress(progress_fn, "Creating runtime backups...")
|
|
1482
1504
|
db_backup_dir = _backup_dbs()
|
|
1483
1505
|
tree_backup_dir = _backup_runtime_tree(NEXO_HOME)
|
|
1484
1506
|
sync_result = {"ok": False, "mode": "sync", "pulled_source": pulled, "backup_dir": db_backup_dir, "tree_backup": tree_backup_dir}
|
|
1485
1507
|
try:
|
|
1486
|
-
|
|
1487
|
-
|
|
1508
|
+
_emit_progress(progress_fn, "Syncing runtime files...")
|
|
1509
|
+
copy_stats = _copy_runtime_from_source(src_dir, repo_dir, NEXO_HOME, progress_fn=progress_fn)
|
|
1510
|
+
_emit_progress(progress_fn, "Reconciling runtime state...")
|
|
1511
|
+
ok, actions = _run_runtime_post_sync(NEXO_HOME, progress_fn=progress_fn)
|
|
1488
1512
|
if not ok:
|
|
1489
1513
|
raise RuntimeError("; ".join(actions))
|
|
1490
1514
|
sync_result.update({
|
|
@@ -1496,7 +1520,9 @@ def manual_sync_update(*, interactive: bool = False, allow_source_pull: bool = T
|
|
|
1496
1520
|
"source": copy_stats["source"],
|
|
1497
1521
|
"repo": copy_stats["repo"],
|
|
1498
1522
|
})
|
|
1523
|
+
_emit_progress(progress_fn, "Runtime update completed.")
|
|
1499
1524
|
except Exception as e:
|
|
1525
|
+
_emit_progress(progress_fn, "Update failed; restoring previous runtime state...")
|
|
1500
1526
|
_restore_runtime_tree(tree_backup_dir, NEXO_HOME)
|
|
1501
1527
|
if db_backup_dir:
|
|
1502
1528
|
_restore_dbs(db_backup_dir)
|
package/src/cli.py
CHANGED
|
@@ -22,6 +22,7 @@ Entry points:
|
|
|
22
22
|
nexo skills approve ID [--execution-level ...] [--approved-by ...] [--json]
|
|
23
23
|
nexo skills featured [--limit N] [--json]
|
|
24
24
|
nexo skills evolution [--json]
|
|
25
|
+
nexo contributor status|on|off [--json]
|
|
25
26
|
nexo doctor [--tier boot|runtime|deep|all] [--json] [--fix]
|
|
26
27
|
"""
|
|
27
28
|
from __future__ import annotations
|
|
@@ -458,8 +459,18 @@ def _update(args):
|
|
|
458
459
|
ensure_full_disk_access_choice,
|
|
459
460
|
format_full_disk_access_label,
|
|
460
461
|
)
|
|
462
|
+
from public_contribution import (
|
|
463
|
+
ensure_public_contribution_choice,
|
|
464
|
+
format_public_contribution_label,
|
|
465
|
+
)
|
|
461
466
|
|
|
462
467
|
interactive = sys.stdin.isatty() and sys.stdout.isatty()
|
|
468
|
+
progress_messages: list[str] = []
|
|
469
|
+
|
|
470
|
+
def progress(message: str) -> None:
|
|
471
|
+
progress_messages.append(message)
|
|
472
|
+
if not args.json:
|
|
473
|
+
print(f"[NEXO] {message}", flush=True)
|
|
463
474
|
|
|
464
475
|
dest = NEXO_HOME
|
|
465
476
|
src_dir, repo_dir = _resolve_sync_source()
|
|
@@ -475,20 +486,25 @@ def _update(args):
|
|
|
475
486
|
)
|
|
476
487
|
return 1
|
|
477
488
|
|
|
478
|
-
result = handle_update()
|
|
489
|
+
result = handle_update(progress_fn=progress)
|
|
479
490
|
choice = ensure_power_policy_choice(interactive=interactive, reason="update")
|
|
480
491
|
power_result = apply_power_policy(choice.get("policy"))
|
|
481
492
|
fda_choice = ensure_full_disk_access_choice(interactive=interactive, reason="update")
|
|
493
|
+
contrib_choice = ensure_public_contribution_choice(interactive=interactive, reason="update")
|
|
482
494
|
if args.json:
|
|
483
495
|
print(json.dumps({
|
|
484
496
|
"mode": "packaged",
|
|
485
497
|
"message": result,
|
|
498
|
+
"progress": progress_messages,
|
|
486
499
|
"power_policy": choice.get("policy"),
|
|
487
500
|
"power_action": power_result.get("action"),
|
|
488
501
|
"power_details": power_result.get("details"),
|
|
489
502
|
"full_disk_access_status": fda_choice.get("status"),
|
|
490
503
|
"full_disk_access_reasons": fda_choice.get("reasons"),
|
|
491
504
|
"full_disk_access_message": fda_choice.get("message"),
|
|
505
|
+
"public_contribution_mode": contrib_choice.get("mode"),
|
|
506
|
+
"public_contribution_status": contrib_choice.get("status"),
|
|
507
|
+
"public_contribution_message": contrib_choice.get("message"),
|
|
492
508
|
}, indent=2, ensure_ascii=False))
|
|
493
509
|
else:
|
|
494
510
|
print(result)
|
|
@@ -500,21 +516,31 @@ def _update(args):
|
|
|
500
516
|
print(f"Full Disk Access: {format_full_disk_access_label(fda_choice.get('status'))}")
|
|
501
517
|
if fda_choice.get("message"):
|
|
502
518
|
print(f"Full Disk Access: {fda_choice.get('message')}")
|
|
519
|
+
if contrib_choice.get("prompted"):
|
|
520
|
+
print(f"Contributor mode: {format_public_contribution_label(contrib_choice)}")
|
|
521
|
+
if contrib_choice.get("message"):
|
|
522
|
+
print(f"Contributor mode: {contrib_choice.get('message')}")
|
|
503
523
|
return 0 if "UPDATE SUCCESSFUL" in result or "Already up to date" in result else 1
|
|
504
524
|
|
|
505
525
|
choice = ensure_power_policy_choice(interactive=interactive, reason="update")
|
|
506
526
|
power_result = apply_power_policy(choice.get("policy"))
|
|
507
527
|
fda_choice = ensure_full_disk_access_choice(interactive=interactive, reason="update")
|
|
508
|
-
|
|
528
|
+
contrib_choice = ensure_public_contribution_choice(interactive=interactive, reason="update")
|
|
529
|
+
result = manual_sync_update(interactive=interactive, allow_source_pull=True, progress_fn=progress)
|
|
509
530
|
result["power_policy"] = choice.get("policy")
|
|
510
531
|
result["power_action"] = power_result.get("action")
|
|
511
532
|
result["power_details"] = power_result.get("details")
|
|
512
533
|
result["full_disk_access_status"] = fda_choice.get("status")
|
|
513
534
|
result["full_disk_access_reasons"] = fda_choice.get("reasons")
|
|
535
|
+
result["public_contribution_mode"] = contrib_choice.get("mode")
|
|
536
|
+
result["public_contribution_status"] = contrib_choice.get("status")
|
|
537
|
+
result["progress"] = progress_messages
|
|
514
538
|
if power_result.get("message"):
|
|
515
539
|
result["power_message"] = power_result.get("message")
|
|
516
540
|
if fda_choice.get("message"):
|
|
517
541
|
result["full_disk_access_message"] = fda_choice.get("message")
|
|
542
|
+
if contrib_choice.get("message"):
|
|
543
|
+
result["public_contribution_message"] = contrib_choice.get("message")
|
|
518
544
|
if args.json:
|
|
519
545
|
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
520
546
|
else:
|
|
@@ -534,11 +560,80 @@ def _update(args):
|
|
|
534
560
|
print(f" Full Disk Access: {format_full_disk_access_label(fda_choice.get('status'))}")
|
|
535
561
|
if fda_choice.get("message"):
|
|
536
562
|
print(f" Full Disk Access: {fda_choice.get('message')}")
|
|
563
|
+
if contrib_choice.get("prompted"):
|
|
564
|
+
print(f" Contributor mode: {format_public_contribution_label(contrib_choice)}")
|
|
565
|
+
if contrib_choice.get("message"):
|
|
566
|
+
print(f" Contributor mode: {contrib_choice.get('message')}")
|
|
537
567
|
else:
|
|
538
568
|
print(f"UPDATE FAILED: {result.get('error', 'sync failed')}", file=sys.stderr)
|
|
539
569
|
return 0 if result.get("ok") else 1
|
|
540
570
|
|
|
541
571
|
|
|
572
|
+
def _contributor_status(args):
|
|
573
|
+
from public_contribution import (
|
|
574
|
+
format_public_contribution_label,
|
|
575
|
+
load_public_contribution_config,
|
|
576
|
+
refresh_public_contribution_state,
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
config = refresh_public_contribution_state(load_public_contribution_config())
|
|
580
|
+
payload = {
|
|
581
|
+
"enabled": bool(config.get("enabled")),
|
|
582
|
+
"mode": config.get("mode"),
|
|
583
|
+
"status": config.get("status"),
|
|
584
|
+
"label": format_public_contribution_label(config),
|
|
585
|
+
"github_user": config.get("github_user"),
|
|
586
|
+
"fork_repo": config.get("fork_repo"),
|
|
587
|
+
"active_pr_url": config.get("active_pr_url"),
|
|
588
|
+
"active_branch": config.get("active_branch"),
|
|
589
|
+
"cooldown_until": config.get("cooldown_until"),
|
|
590
|
+
"last_result": config.get("last_result"),
|
|
591
|
+
}
|
|
592
|
+
if args.json:
|
|
593
|
+
print(json.dumps(payload, indent=2, ensure_ascii=False))
|
|
594
|
+
else:
|
|
595
|
+
print(f"Contributor mode: {payload['label']}")
|
|
596
|
+
if payload["github_user"]:
|
|
597
|
+
print(f" GitHub user: {payload['github_user']}")
|
|
598
|
+
if payload["fork_repo"]:
|
|
599
|
+
print(f" Fork: {payload['fork_repo']}")
|
|
600
|
+
if payload["active_pr_url"]:
|
|
601
|
+
print(f" Active Draft PR: {payload['active_pr_url']}")
|
|
602
|
+
if payload["cooldown_until"]:
|
|
603
|
+
print(f" Cooldown until: {payload['cooldown_until']}")
|
|
604
|
+
if payload["last_result"]:
|
|
605
|
+
print(f" Last result: {payload['last_result']}")
|
|
606
|
+
return 0
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def _contributor_on(args):
|
|
610
|
+
from public_contribution import ensure_public_contribution_choice, format_public_contribution_label
|
|
611
|
+
|
|
612
|
+
interactive = sys.stdin.isatty() and sys.stdout.isatty()
|
|
613
|
+
if not interactive:
|
|
614
|
+
print("Contributor mode requires an interactive terminal to confirm GitHub Draft PR consent.", file=sys.stderr)
|
|
615
|
+
return 1
|
|
616
|
+
config = ensure_public_contribution_choice(interactive=True, reason="contributor", force_prompt=True)
|
|
617
|
+
if args.json:
|
|
618
|
+
print(json.dumps(config, indent=2, ensure_ascii=False))
|
|
619
|
+
else:
|
|
620
|
+
print(f"Contributor mode: {format_public_contribution_label(config)}")
|
|
621
|
+
if config.get("message"):
|
|
622
|
+
print(config.get("message"))
|
|
623
|
+
return 0 if config.get("mode") == "draft_prs" else 1
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def _contributor_off(args):
|
|
627
|
+
from public_contribution import disable_public_contribution, format_public_contribution_label
|
|
628
|
+
|
|
629
|
+
config = disable_public_contribution()
|
|
630
|
+
if args.json:
|
|
631
|
+
print(json.dumps(config, indent=2, ensure_ascii=False))
|
|
632
|
+
else:
|
|
633
|
+
print(f"Contributor mode: {format_public_contribution_label(config)}")
|
|
634
|
+
return 0
|
|
635
|
+
|
|
636
|
+
|
|
542
637
|
def _service_control(service_name: str, action: str) -> int:
|
|
543
638
|
"""Control a LaunchAgent/systemd service: on, off, status."""
|
|
544
639
|
import platform as plat
|
|
@@ -767,6 +862,7 @@ Commands:
|
|
|
767
862
|
Personal scripts
|
|
768
863
|
nexo skills list|apply|sync|approve Executable skills
|
|
769
864
|
nexo update Update installed runtime
|
|
865
|
+
nexo contributor status|on|off Public Draft PR contribution mode
|
|
770
866
|
nexo dashboard on|off|status Web dashboard control
|
|
771
867
|
|
|
772
868
|
Run 'nexo <command> --help' for details.
|
|
@@ -861,6 +957,11 @@ def main():
|
|
|
861
957
|
doctor_parser.add_argument("--json", action="store_true", help="JSON output")
|
|
862
958
|
doctor_parser.add_argument("--fix", action="store_true", help="Apply deterministic fixes")
|
|
863
959
|
|
|
960
|
+
# -- contributor --
|
|
961
|
+
contributor_parser = sub.add_parser("contributor", help="Public Draft PR contribution mode")
|
|
962
|
+
contributor_parser.add_argument("action", choices=["status", "on", "off"], help="Manage contributor mode")
|
|
963
|
+
contributor_parser.add_argument("--json", action="store_true", help="JSON output")
|
|
964
|
+
|
|
864
965
|
# -- skills --
|
|
865
966
|
skills_parser = sub.add_parser("skills", help="Skills v2 runtime")
|
|
866
967
|
skills_sub = skills_parser.add_subparsers(dest="skills_command")
|
|
@@ -946,6 +1047,15 @@ def main():
|
|
|
946
1047
|
return _update(args)
|
|
947
1048
|
elif args.command == "doctor":
|
|
948
1049
|
return _doctor(args)
|
|
1050
|
+
elif args.command == "contributor":
|
|
1051
|
+
if args.action == "status":
|
|
1052
|
+
return _contributor_status(args)
|
|
1053
|
+
elif args.action == "on":
|
|
1054
|
+
return _contributor_on(args)
|
|
1055
|
+
elif args.action == "off":
|
|
1056
|
+
return _contributor_off(args)
|
|
1057
|
+
contributor_parser.print_help()
|
|
1058
|
+
return 0
|
|
949
1059
|
elif args.command == "skills":
|
|
950
1060
|
if args.skills_command == "list":
|
|
951
1061
|
return _skills_list(args)
|
package/src/db/_episodic.py
CHANGED
|
@@ -573,7 +573,7 @@ def read_session_diary(session_id: str = '', last_n: int = 3, last_day: bool = F
|
|
|
573
573
|
"""Read session diary entries.
|
|
574
574
|
|
|
575
575
|
- session_id: returns entries for that specific session
|
|
576
|
-
- last_day: returns
|
|
576
|
+
- last_day: returns the recent continuity window (~36h), including the previous evening
|
|
577
577
|
- last_n: returns last N entries (default)
|
|
578
578
|
- domain: filter by project context (nexo, other)
|
|
579
579
|
- include_automated: if False (default), excludes automated sessions (auto-close,
|
|
@@ -605,21 +605,11 @@ def read_session_diary(session_id: str = '', last_n: int = 3, last_day: bool = F
|
|
|
605
605
|
(session_id,) + domain_params
|
|
606
606
|
).fetchall()
|
|
607
607
|
elif last_day:
|
|
608
|
-
# Get all entries from the most recent calendar day (human sessions only)
|
|
609
|
-
if domain:
|
|
610
|
-
latest = conn.execute(
|
|
611
|
-
f"SELECT date(created_at) as day FROM session_diary WHERE domain = ?{source_clause} ORDER BY created_at DESC LIMIT 1",
|
|
612
|
-
(domain,)
|
|
613
|
-
).fetchone()
|
|
614
|
-
else:
|
|
615
|
-
latest = conn.execute(
|
|
616
|
-
f"SELECT date(created_at) as day FROM session_diary WHERE 1=1{source_clause} ORDER BY created_at DESC LIMIT 1"
|
|
617
|
-
).fetchone()
|
|
618
|
-
if not latest:
|
|
619
|
-
return []
|
|
620
608
|
rows = conn.execute(
|
|
621
|
-
f"SELECT * FROM session_diary
|
|
622
|
-
(
|
|
609
|
+
f"SELECT * FROM session_diary "
|
|
610
|
+
f"WHERE created_at >= datetime('now', '-36 hours'){domain_clause}{source_clause} "
|
|
611
|
+
f"ORDER BY created_at DESC",
|
|
612
|
+
domain_params
|
|
623
613
|
).fetchall()
|
|
624
614
|
else:
|
|
625
615
|
rows = conn.execute(
|
|
@@ -770,4 +760,3 @@ def recall(query: str, days: int = 30) -> list[dict]:
|
|
|
770
760
|
results.sort(key=lambda r: r.get('created_at', ''), reverse=True)
|
|
771
761
|
return results[:20]
|
|
772
762
|
|
|
773
|
-
|