nexo-brain 7.25.6 → 7.26.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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +3 -1
- package/bin/nexo-brain.js +233 -29
- package/codex/openai-codex-0.133.0.tgz +0 -0
- package/package.json +7 -1
- package/src/agent_runner.py +96 -31
- package/src/cli.py +117 -4
- package/src/client_preferences.py +293 -1
- package/src/client_sync.py +327 -1
- package/src/db/_schema.py +23 -0
- package/src/db/_sessions.py +75 -24
- package/src/provider_runtime.py +39 -0
- package/src/scripts/deep-sleep/extract.py +2 -0
- package/src/scripts/deep-sleep/synthesize.py +1 -0
- package/src/scripts/nexo-cron-wrapper.sh +108 -25
- package/src/scripts/nexo-morning-agent.py +1 -1
- package/src/server.py +3 -1
- package/src/tools_automation_sessions.py +2 -1
- package/src/tools_sessions.py +13 -8
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.26.0",
|
|
4
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",
|
package/README.md
CHANGED
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
|
|
19
19
|
[Watch the overview video](https://nexo-brain.com/watch/) · [Watch on YouTube](https://www.youtube.com/watch?v=i2lkGhKyVqI) · [Open the infographic](https://nexo-brain.com/assets/nexo-brain-infographic-v5.png)
|
|
20
20
|
|
|
21
|
-
Version `7.
|
|
21
|
+
Version `7.26.0` is the current packaged-runtime line. Minor release over v7.25.6 - provider runtime parity lets Desktop-managed Brain choose Anthropic Claude or OpenAI Codex, keep provider metadata on sessions/automation/crons, and provision the managed Codex runtime from bundled Desktop resources.
|
|
22
|
+
|
|
23
|
+
Previously in `7.25.6`: patch release over v7.25.5 - existing Local Memory sidecar databases repair legacy root/exclusion columns before source-dependent indexes are created, and core background crons prefer the NEXO-managed Python runtime.
|
|
22
24
|
|
|
23
25
|
Previously in `7.25.4`: patch release over v7.25.3 - Local Memory starts from safe user-content and email roots, adds configurable included/excluded file types, and cleans legacy whole-disk index state with backup or archive-rebuild safety.
|
|
24
26
|
|
package/bin/nexo-brain.js
CHANGED
|
@@ -18,6 +18,7 @@ const { execSync, spawnSync } = require("child_process");
|
|
|
18
18
|
const crypto = require("crypto");
|
|
19
19
|
const fs = require("fs");
|
|
20
20
|
const { createRequire } = require("module");
|
|
21
|
+
const os = require("os");
|
|
21
22
|
const path = require("path");
|
|
22
23
|
const readline = require("readline");
|
|
23
24
|
// Force relative launcher helpers to resolve from bin/ even under test harnesses.
|
|
@@ -1672,6 +1673,7 @@ function getDefaultSchedule(timezone) {
|
|
|
1672
1673
|
default_terminal_client: "claude_code",
|
|
1673
1674
|
automation_enabled: true,
|
|
1674
1675
|
automation_backend: "claude_code",
|
|
1676
|
+
provider_runtime: defaultProviderRuntime("anthropic", "anthropic"),
|
|
1675
1677
|
// v6.0.0 — model/reasoning_effort have moved to src/resonance_tiers.json
|
|
1676
1678
|
// keyed by the operator's preferences.default_resonance. The shape
|
|
1677
1679
|
// below stays so that downstream readers that iterate the profile
|
|
@@ -1813,9 +1815,31 @@ function detectInstalledClients() {
|
|
|
1813
1815
|
: [];
|
|
1814
1816
|
const desktopAppPath = desktopApps.find((candidate) => fs.existsSync(candidate)) || "";
|
|
1815
1817
|
const managedClaudeBin = resolveManagedClaudeBinary();
|
|
1818
|
+
const managedCodexBin = resolveManagedCodexBinary();
|
|
1819
|
+
const desktopManaged = isDesktopManagedInstall();
|
|
1820
|
+
if (desktopManaged) {
|
|
1821
|
+
const managedCodexReady = managedCodexBin && codexVendorPresent(managedClaudePrefix());
|
|
1822
|
+
return {
|
|
1823
|
+
claude_code: {
|
|
1824
|
+
installed: Boolean(managedClaudeBin),
|
|
1825
|
+
path: managedClaudeBin,
|
|
1826
|
+
detectedBy: managedClaudeBin ? "managed_binary" : "missing",
|
|
1827
|
+
},
|
|
1828
|
+
codex: {
|
|
1829
|
+
installed: Boolean(managedCodexReady),
|
|
1830
|
+
path: managedCodexReady ? managedCodexBin : "",
|
|
1831
|
+
detectedBy: managedCodexReady ? "managed_binary" : "missing",
|
|
1832
|
+
},
|
|
1833
|
+
claude_desktop: {
|
|
1834
|
+
installed: Boolean(desktopAppPath || fs.existsSync(desktopConfig)),
|
|
1835
|
+
path: desktopAppPath || desktopConfig,
|
|
1836
|
+
detectedBy: desktopAppPath ? "app" : (fs.existsSync(desktopConfig) ? "config" : "missing"),
|
|
1837
|
+
},
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1816
1840
|
const persistedClaudeBin = readPersistedClaudeCliPath();
|
|
1817
1841
|
const claudeBin = managedClaudeBin || persistedClaudeBin || run("which claude", { env: buildManagedCliEnv() }) || run("which claude") || "";
|
|
1818
|
-
const codexBin = run("which codex") || "";
|
|
1842
|
+
const codexBin = managedCodexBin || run("which codex", { env: buildManagedCliEnv() }) || run("which codex") || "";
|
|
1819
1843
|
return {
|
|
1820
1844
|
claude_code: {
|
|
1821
1845
|
installed: Boolean(claudeBin),
|
|
@@ -1905,6 +1929,14 @@ function resolveManagedClaudeBinary() {
|
|
|
1905
1929
|
return candidates.find((candidate) => candidate && fs.existsSync(candidate)) || "";
|
|
1906
1930
|
}
|
|
1907
1931
|
|
|
1932
|
+
function resolveManagedCodexBinary() {
|
|
1933
|
+
const prefix = managedClaudePrefix();
|
|
1934
|
+
const candidates = process.platform === "win32"
|
|
1935
|
+
? [path.join(prefix, "codex.cmd"), path.join(prefix, "bin", "codex.cmd")]
|
|
1936
|
+
: [path.join(prefix, "bin", "codex"), path.join(prefix, "codex")];
|
|
1937
|
+
return candidates.find((candidate) => candidate && fs.existsSync(candidate)) || "";
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1908
1940
|
function readPersistedClaudeCliPath() {
|
|
1909
1941
|
const candidates = [
|
|
1910
1942
|
path.join(NEXO_HOME, "config", "claude-cli-path"),
|
|
@@ -2000,6 +2032,60 @@ function defaultClientRuntimeProfiles() {
|
|
|
2000
2032
|
};
|
|
2001
2033
|
}
|
|
2002
2034
|
|
|
2035
|
+
function defaultProviderRuntime(selectedProvider = "anthropic", automationProvider = selectedProvider) {
|
|
2036
|
+
const automationBackend = automationProvider === "openai" ? "codex" : (automationProvider === "anthropic" ? "claude_code" : "none");
|
|
2037
|
+
return {
|
|
2038
|
+
schema_version: 1,
|
|
2039
|
+
selected_chat_provider: selectedProvider,
|
|
2040
|
+
automation_provider: automationProvider,
|
|
2041
|
+
automation_backend: automationBackend,
|
|
2042
|
+
providers: {
|
|
2043
|
+
anthropic: {
|
|
2044
|
+
client: "claude_code",
|
|
2045
|
+
runtime_account_status: {
|
|
2046
|
+
surface: "desktop_login",
|
|
2047
|
+
status: "unknown",
|
|
2048
|
+
plan: null,
|
|
2049
|
+
last_checked_at: null,
|
|
2050
|
+
detail: null,
|
|
2051
|
+
},
|
|
2052
|
+
install_status: {
|
|
2053
|
+
installed: false,
|
|
2054
|
+
managed: true,
|
|
2055
|
+
binary_path: null,
|
|
2056
|
+
version: null,
|
|
2057
|
+
},
|
|
2058
|
+
},
|
|
2059
|
+
openai: {
|
|
2060
|
+
client: "codex",
|
|
2061
|
+
runtime_account_status: {
|
|
2062
|
+
surface: "desktop_login",
|
|
2063
|
+
status: "unknown",
|
|
2064
|
+
plan: null,
|
|
2065
|
+
last_checked_at: null,
|
|
2066
|
+
detail: null,
|
|
2067
|
+
},
|
|
2068
|
+
install_status: {
|
|
2069
|
+
installed: false,
|
|
2070
|
+
managed: true,
|
|
2071
|
+
binary_path: null,
|
|
2072
|
+
version: null,
|
|
2073
|
+
},
|
|
2074
|
+
},
|
|
2075
|
+
},
|
|
2076
|
+
fallback_policy: {
|
|
2077
|
+
chat: "ask",
|
|
2078
|
+
automation: "fail_closed",
|
|
2079
|
+
},
|
|
2080
|
+
last_provider_change: {
|
|
2081
|
+
changed_at: null,
|
|
2082
|
+
from_provider: null,
|
|
2083
|
+
to_provider: null,
|
|
2084
|
+
source: null,
|
|
2085
|
+
},
|
|
2086
|
+
};
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2003
2089
|
function runtimeClientLabel(client) {
|
|
2004
2090
|
if (client === "claude_code") return "Claude Code";
|
|
2005
2091
|
if (client === "codex") return "Codex";
|
|
@@ -2039,6 +2125,7 @@ function defaultClientSetup(detected) {
|
|
|
2039
2125
|
default_terminal_client: "claude_code",
|
|
2040
2126
|
automation_enabled: true,
|
|
2041
2127
|
automation_backend: "claude_code",
|
|
2128
|
+
provider_runtime: defaultProviderRuntime("anthropic", "anthropic"),
|
|
2042
2129
|
client_runtime_profiles: defaultClientRuntimeProfiles(),
|
|
2043
2130
|
client_install_preferences: {
|
|
2044
2131
|
claude_code: "ask",
|
|
@@ -2057,6 +2144,17 @@ function applyClientSetupToSchedule(schedule, setup) {
|
|
|
2057
2144
|
schedule.default_terminal_client = setup.default_terminal_client;
|
|
2058
2145
|
schedule.automation_enabled = Boolean(setup.automation_enabled);
|
|
2059
2146
|
schedule.automation_backend = schedule.automation_enabled ? setup.automation_backend : "none";
|
|
2147
|
+
const selectedProvider = setup.default_terminal_client === "codex" ? "openai" : "anthropic";
|
|
2148
|
+
const automationProvider = schedule.automation_backend === "codex"
|
|
2149
|
+
? "openai"
|
|
2150
|
+
: (schedule.automation_backend === "claude_code" ? "anthropic" : "none");
|
|
2151
|
+
schedule.provider_runtime = {
|
|
2152
|
+
...defaultProviderRuntime(selectedProvider, automationProvider),
|
|
2153
|
+
...(setup.provider_runtime || {}),
|
|
2154
|
+
selected_chat_provider: selectedProvider,
|
|
2155
|
+
automation_provider: automationProvider,
|
|
2156
|
+
automation_backend: schedule.automation_backend,
|
|
2157
|
+
};
|
|
2060
2158
|
schedule.client_runtime_profiles = {
|
|
2061
2159
|
...defaultClientRuntimeProfiles(),
|
|
2062
2160
|
...(setup.client_runtime_profiles || {}),
|
|
@@ -2090,6 +2188,9 @@ function installClaudeCodeCli(platform) {
|
|
|
2090
2188
|
const npmViaDesktop = desktopNode && bundledNpmCli;
|
|
2091
2189
|
let installEnv = buildManagedCliEnv();
|
|
2092
2190
|
if (desktopNode) installEnv = withDesktopNodeShim(installEnv, desktopNode);
|
|
2191
|
+
if (desktopManaged && !npmViaDesktop) {
|
|
2192
|
+
return { installed: false, path: "" };
|
|
2193
|
+
}
|
|
2093
2194
|
|
|
2094
2195
|
// OFFLINE-FIRST v0.32.4: install claude-code wrapper + ALL its native packs
|
|
2095
2196
|
// from bundled tarballs. Path: resources/brain-bundle/claude-code/*.tgz.
|
|
@@ -2186,22 +2287,7 @@ function installClaudeCodeCli(platform) {
|
|
|
2186
2287
|
}
|
|
2187
2288
|
}
|
|
2188
2289
|
|
|
2189
|
-
if (desktopManaged) {
|
|
2190
|
-
spawnSync(
|
|
2191
|
-
"npm",
|
|
2192
|
-
["install", "-g", "--prefix", managedPrefix, "@anthropic-ai/claude-code"],
|
|
2193
|
-
{
|
|
2194
|
-
stdio: "inherit",
|
|
2195
|
-
env: installEnv,
|
|
2196
|
-
},
|
|
2197
|
-
);
|
|
2198
|
-
claudeInstalled = detectInstalledClients().claude_code.path || "";
|
|
2199
|
-
if (claudeInstalled) {
|
|
2200
|
-
persistClaudeCliPath(claudeInstalled);
|
|
2201
|
-
return { installed: true, path: claudeInstalled };
|
|
2202
|
-
}
|
|
2203
|
-
return { installed: false, path: "" };
|
|
2204
|
-
}
|
|
2290
|
+
if (desktopManaged) return { installed: false, path: "" };
|
|
2205
2291
|
|
|
2206
2292
|
spawnSync("npx", ["-y", "@anthropic-ai/claude-code", "--version"], {
|
|
2207
2293
|
stdio: "pipe",
|
|
@@ -2221,11 +2307,128 @@ function installClaudeCodeCli(platform) {
|
|
|
2221
2307
|
return { installed: Boolean(claudeInstalled), path: claudeInstalled || "" };
|
|
2222
2308
|
}
|
|
2223
2309
|
|
|
2310
|
+
function installBundledCodexVendor(bundleDir, managedPrefix) {
|
|
2311
|
+
const packageRoots = [
|
|
2312
|
+
path.join(managedPrefix, "lib", "node_modules", "@openai", "codex"),
|
|
2313
|
+
path.join(managedPrefix, "node_modules", "@openai", "codex"),
|
|
2314
|
+
];
|
|
2315
|
+
const packageRoot = packageRoots.find((candidate) => fs.existsSync(candidate)) || packageRoots[0];
|
|
2316
|
+
if (!fs.existsSync(packageRoot)) return false;
|
|
2317
|
+
const allTgz = fs.readdirSync(bundleDir).filter((f) => f.endsWith(".tgz"));
|
|
2318
|
+
const platformSlug = `${process.platform}-${process.arch}`;
|
|
2319
|
+
const nativePacks = allTgz.filter((f) => f.includes(`-${platformSlug}.tgz`) || f.includes(`-${platformSlug}-`));
|
|
2320
|
+
if (!nativePacks.length) return false;
|
|
2321
|
+
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "nexo-codex-vendor-"));
|
|
2322
|
+
try {
|
|
2323
|
+
for (const pack of nativePacks) {
|
|
2324
|
+
const tgzPath = path.join(bundleDir, pack);
|
|
2325
|
+
const extract = spawnSync("tar", ["-xzf", tgzPath, "-C", tmpRoot], { stdio: "pipe" });
|
|
2326
|
+
if (extract.status !== 0) continue;
|
|
2327
|
+
const vendorRoot = path.join(tmpRoot, "package", "vendor");
|
|
2328
|
+
if (!fs.existsSync(vendorRoot)) continue;
|
|
2329
|
+
fs.mkdirSync(path.join(packageRoot, "vendor"), { recursive: true });
|
|
2330
|
+
for (const entry of fs.readdirSync(vendorRoot)) {
|
|
2331
|
+
fs.cpSync(path.join(vendorRoot, entry), path.join(packageRoot, "vendor", entry), { recursive: true, force: true });
|
|
2332
|
+
}
|
|
2333
|
+
return true;
|
|
2334
|
+
}
|
|
2335
|
+
} finally {
|
|
2336
|
+
try { fs.rmSync(tmpRoot, { recursive: true, force: true }); } catch {}
|
|
2337
|
+
}
|
|
2338
|
+
return false;
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
function codexVendorPresent(managedPrefix) {
|
|
2342
|
+
const packageRoots = [
|
|
2343
|
+
path.join(managedPrefix, "lib", "node_modules", "@openai", "codex"),
|
|
2344
|
+
path.join(managedPrefix, "node_modules", "@openai", "codex"),
|
|
2345
|
+
];
|
|
2346
|
+
for (const packageRoot of packageRoots) {
|
|
2347
|
+
const vendorRoot = path.join(packageRoot, "vendor");
|
|
2348
|
+
if (!fs.existsSync(vendorRoot)) continue;
|
|
2349
|
+
try {
|
|
2350
|
+
const stack = [vendorRoot];
|
|
2351
|
+
while (stack.length) {
|
|
2352
|
+
const current = stack.pop();
|
|
2353
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
2354
|
+
const target = path.join(current, entry.name);
|
|
2355
|
+
if (entry.isDirectory()) {
|
|
2356
|
+
stack.push(target);
|
|
2357
|
+
} else if (entry.isFile() && path.basename(path.dirname(target)) === "bin" && entry.name.startsWith("codex")) {
|
|
2358
|
+
return true;
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
} catch {}
|
|
2363
|
+
}
|
|
2364
|
+
return false;
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2224
2367
|
function installCodexCli() {
|
|
2225
|
-
const
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
const
|
|
2368
|
+
const desktopNode = String(process.env.NEXO_DESKTOP_NODE || "").trim();
|
|
2369
|
+
const bundledNpmCli = String(process.env.NEXO_DESKTOP_NPM_CLI || "").trim();
|
|
2370
|
+
const managedPrefix = managedClaudePrefix();
|
|
2371
|
+
const desktopManaged = isDesktopManagedInstall();
|
|
2372
|
+
let before = detectInstalledClients().codex.path || "";
|
|
2373
|
+
if (before && (!desktopManaged || codexVendorPresent(managedPrefix))) return { installed: true, path: before };
|
|
2374
|
+
if (before && desktopManaged) {
|
|
2375
|
+
log(" Managed Codex wrapper exists, but native vendor is missing; repairing bundled vendor.");
|
|
2376
|
+
}
|
|
2377
|
+
const npmViaDesktop = desktopNode && bundledNpmCli;
|
|
2378
|
+
let installEnv = buildManagedCliEnv();
|
|
2379
|
+
if (desktopNode) installEnv = withDesktopNodeShim(installEnv, desktopNode);
|
|
2380
|
+
if (desktopManaged && !npmViaDesktop) {
|
|
2381
|
+
return { installed: false, path: "" };
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
const bundledCodexDir = path.join(__dirname, "..", "codex");
|
|
2385
|
+
if (fs.existsSync(bundledCodexDir)) {
|
|
2386
|
+
const allTgz = fs.readdirSync(bundledCodexDir).filter((f) => f.endsWith(".tgz")).sort();
|
|
2387
|
+
const wrapper = allTgz.find((f) => /^openai-codex-\d+\.\d+\.\d+\.tgz$/.test(f));
|
|
2388
|
+
if (wrapper) {
|
|
2389
|
+
const tgzPath = path.join(bundledCodexDir, wrapper);
|
|
2390
|
+
log(" Installing Codex from bundled tarball (offline wrapper + native vendor)...");
|
|
2391
|
+
spawnSync(
|
|
2392
|
+
npmViaDesktop ? desktopNode : "npm",
|
|
2393
|
+
[
|
|
2394
|
+
...(npmViaDesktop ? [bundledNpmCli] : []),
|
|
2395
|
+
"install",
|
|
2396
|
+
"-g",
|
|
2397
|
+
"--prefix",
|
|
2398
|
+
managedPrefix,
|
|
2399
|
+
"--offline",
|
|
2400
|
+
"--no-audit",
|
|
2401
|
+
"--no-fund",
|
|
2402
|
+
tgzPath,
|
|
2403
|
+
],
|
|
2404
|
+
{ stdio: "inherit", env: installEnv },
|
|
2405
|
+
);
|
|
2406
|
+
const bundledVendorInstalled = installBundledCodexVendor(bundledCodexDir, managedPrefix);
|
|
2407
|
+
before = detectInstalledClients().codex.path || "";
|
|
2408
|
+
if (before && bundledVendorInstalled) return { installed: true, path: before };
|
|
2409
|
+
if (before && !bundledVendorInstalled) {
|
|
2410
|
+
log(" Bundled Codex wrapper installed, but native vendor extraction failed; falling back to online install.");
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
if (desktopNode && bundledNpmCli) {
|
|
2416
|
+
spawnSync(
|
|
2417
|
+
desktopNode,
|
|
2418
|
+
[bundledNpmCli, "install", "-g", "--prefix", managedPrefix, "@openai/codex"],
|
|
2419
|
+
{
|
|
2420
|
+
stdio: "inherit",
|
|
2421
|
+
env: { ...installEnv, ELECTRON_RUN_AS_NODE: "1" },
|
|
2422
|
+
},
|
|
2423
|
+
);
|
|
2424
|
+
before = detectInstalledClients().codex.path || "";
|
|
2425
|
+
if (before && (!desktopManaged || codexVendorPresent(managedPrefix))) return { installed: true, path: before };
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
if (desktopManaged) return { installed: false, path: "" };
|
|
2429
|
+
|
|
2430
|
+
spawnSync("npm", ["install", "-g", "--prefix", managedPrefix, "@openai/codex"], { stdio: "inherit", env: installEnv });
|
|
2431
|
+
const codexInstalled = detectInstalledClients().codex.path || "";
|
|
2229
2432
|
return { installed: Boolean(codexInstalled), path: codexInstalled };
|
|
2230
2433
|
}
|
|
2231
2434
|
|
|
@@ -2284,19 +2487,19 @@ async function configureClientSetup({ lang, useDefaults, autoInstall, detected }
|
|
|
2284
2487
|
const required = requiredCliClients(setup);
|
|
2285
2488
|
for (const client of required) {
|
|
2286
2489
|
if (detected[client] && detected[client].installed) continue;
|
|
2287
|
-
if (desktopManaged && client === "claude_code") {
|
|
2288
|
-
const
|
|
2490
|
+
if (desktopManaged && (client === "claude_code" || client === "codex")) {
|
|
2491
|
+
const bundledClientDir = path.join(__dirname, "..", client === "claude_code" ? "claude-code" : "codex");
|
|
2289
2492
|
let hasBundle = false;
|
|
2290
2493
|
try {
|
|
2291
|
-
if (fs.existsSync(
|
|
2292
|
-
hasBundle = fs.readdirSync(
|
|
2494
|
+
if (fs.existsSync(bundledClientDir)) {
|
|
2495
|
+
hasBundle = fs.readdirSync(bundledClientDir).some((f) => f.endsWith(".tgz"));
|
|
2293
2496
|
}
|
|
2294
2497
|
} catch (_) {}
|
|
2295
2498
|
if (!hasBundle) {
|
|
2296
|
-
log(
|
|
2499
|
+
log(`${runtimeClientLabel(client)} install deferred to Desktop final sync.`);
|
|
2297
2500
|
continue;
|
|
2298
2501
|
}
|
|
2299
|
-
log(
|
|
2502
|
+
log(`Bundled ${runtimeClientLabel(client)} tarball detected — installing offline now.`);
|
|
2300
2503
|
}
|
|
2301
2504
|
let shouldInstall = useDefaults || autoInstall === "auto";
|
|
2302
2505
|
if (!shouldInstall && process.stdin.isTTY && process.stdout.isTTY) {
|
|
@@ -2326,8 +2529,9 @@ async function configureClientSetup({ lang, useDefaults, autoInstall, detected }
|
|
|
2326
2529
|
}
|
|
2327
2530
|
|
|
2328
2531
|
if (setup.automation_enabled && setup.automation_backend !== "none" && !detected[setup.automation_backend]?.installed) {
|
|
2329
|
-
if (desktopManaged && setup.automation_backend === "claude_code") {
|
|
2330
|
-
|
|
2532
|
+
if (desktopManaged && (setup.automation_backend === "claude_code" || setup.automation_backend === "codex")) {
|
|
2533
|
+
const label = setup.automation_backend === "claude_code" ? "Claude Code" : "Codex";
|
|
2534
|
+
log(`${label} will be provisioned by Desktop after the core runtime is ready.`);
|
|
2331
2535
|
return { setup, detected };
|
|
2332
2536
|
}
|
|
2333
2537
|
const label = setup.automation_backend === "claude_code" ? "Claude Code" : "Codex";
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.26.0",
|
|
4
4
|
"mcpName": "io.github.wazionapps/nexo",
|
|
5
5
|
"description": "NEXO Brain — Shared brain for AI agents. Persistent memory, semantic RAG, natural forgetting, metacognitive guard, trust scoring, 150+ MCP tools. Works with Claude Code, Codex, Claude Desktop & any MCP client. 100% local, free.",
|
|
6
6
|
"homepage": "https://nexo-brain.com",
|
|
@@ -65,6 +65,11 @@
|
|
|
65
65
|
"name": "@anthropic-ai/claude-code",
|
|
66
66
|
"type": "npm-global",
|
|
67
67
|
"optional": false
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"name": "@openai/codex",
|
|
71
|
+
"type": "npm-global",
|
|
72
|
+
"optional": true
|
|
68
73
|
}
|
|
69
74
|
],
|
|
70
75
|
"engines": {
|
|
@@ -87,6 +92,7 @@
|
|
|
87
92
|
"!templates/**/__pycache__",
|
|
88
93
|
"!templates/**/*.pyc",
|
|
89
94
|
"!templates/**/*.pyo",
|
|
95
|
+
"codex/openai-codex-0.133.0.tgz",
|
|
90
96
|
".claude-plugin/",
|
|
91
97
|
".mcp.json",
|
|
92
98
|
"hooks/hooks.json",
|
package/src/agent_runner.py
CHANGED
|
@@ -24,8 +24,12 @@ from client_preferences import (
|
|
|
24
24
|
BACKEND_NONE,
|
|
25
25
|
CLIENT_CLAUDE_CODE,
|
|
26
26
|
CLIENT_CODEX,
|
|
27
|
+
client_to_provider,
|
|
27
28
|
TERMINAL_CLIENT_KEYS,
|
|
28
29
|
load_client_preferences,
|
|
30
|
+
_desktop_product_requested,
|
|
31
|
+
_managed_codex_binary,
|
|
32
|
+
_managed_codex_vendor_present,
|
|
29
33
|
normalize_client_key,
|
|
30
34
|
resolve_automation_backend,
|
|
31
35
|
resolve_client_runtime_profile,
|
|
@@ -54,6 +58,10 @@ class TerminalClientUnavailableError(AgentRunnerError):
|
|
|
54
58
|
class AutomationBackendUnavailableError(AgentRunnerError):
|
|
55
59
|
"""Raised when the configured automation backend is unavailable."""
|
|
56
60
|
|
|
61
|
+
|
|
62
|
+
def _automation_provider_for_backend(backend: str) -> str:
|
|
63
|
+
return client_to_provider(backend) or ""
|
|
64
|
+
|
|
57
65
|
def _canonical_pricing_model(model: str) -> str:
|
|
58
66
|
lowered = str(model or "").strip().lower()
|
|
59
67
|
lowered = lowered.split("[", 1)[0]
|
|
@@ -199,6 +207,7 @@ def _record_automation_start(
|
|
|
199
207
|
*,
|
|
200
208
|
caller: str,
|
|
201
209
|
backend: str,
|
|
210
|
+
provider: str = "",
|
|
202
211
|
session_type: str,
|
|
203
212
|
task_profile: str,
|
|
204
213
|
model: str,
|
|
@@ -225,14 +234,15 @@ def _record_automation_start(
|
|
|
225
234
|
cur = conn.execute(
|
|
226
235
|
"""
|
|
227
236
|
INSERT INTO automation_runs (
|
|
228
|
-
caller, backend, session_type, task_profile, model,
|
|
237
|
+
caller, backend, provider, session_type, task_profile, model,
|
|
229
238
|
reasoning_effort, resonance_tier, cwd, output_format,
|
|
230
239
|
prompt_chars, status, started_at, pid
|
|
231
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'running', datetime('now'), ?)
|
|
240
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'running', datetime('now'), ?)
|
|
232
241
|
""",
|
|
233
242
|
(
|
|
234
243
|
caller or "",
|
|
235
244
|
backend,
|
|
245
|
+
provider or _automation_provider_for_backend(backend),
|
|
236
246
|
session_type or "headless",
|
|
237
247
|
task_profile or "default",
|
|
238
248
|
model or "",
|
|
@@ -339,6 +349,7 @@ def _record_automation_end(
|
|
|
339
349
|
def _record_automation_run(
|
|
340
350
|
*,
|
|
341
351
|
backend: str,
|
|
352
|
+
provider: str = "",
|
|
342
353
|
task_profile: str,
|
|
343
354
|
model: str,
|
|
344
355
|
reasoning_effort: str,
|
|
@@ -359,6 +370,7 @@ def _record_automation_run(
|
|
|
359
370
|
row_id, err = _record_automation_start(
|
|
360
371
|
caller=caller,
|
|
361
372
|
backend=backend,
|
|
373
|
+
provider=provider or _automation_provider_for_backend(backend),
|
|
362
374
|
session_type=session_type,
|
|
363
375
|
task_profile=task_profile,
|
|
364
376
|
model=model,
|
|
@@ -378,14 +390,57 @@ def _record_automation_run(
|
|
|
378
390
|
)
|
|
379
391
|
|
|
380
392
|
|
|
393
|
+
def _record_backend_unavailable(
|
|
394
|
+
*,
|
|
395
|
+
backend: str,
|
|
396
|
+
caller: str,
|
|
397
|
+
cwd: Path,
|
|
398
|
+
prompt: str,
|
|
399
|
+
) -> None:
|
|
400
|
+
profile = resolve_client_runtime_profile(backend)
|
|
401
|
+
_record_automation_run(
|
|
402
|
+
backend=backend,
|
|
403
|
+
provider=_automation_provider_for_backend(backend),
|
|
404
|
+
task_profile="default",
|
|
405
|
+
model=profile.get("model", ""),
|
|
406
|
+
reasoning_effort=profile.get("reasoning_effort", ""),
|
|
407
|
+
cwd=cwd,
|
|
408
|
+
output_format="text",
|
|
409
|
+
prompt=prompt,
|
|
410
|
+
returncode=2,
|
|
411
|
+
duration_ms=0,
|
|
412
|
+
telemetry={
|
|
413
|
+
"telemetry_source": "backend_unavailable",
|
|
414
|
+
"cost_source": "missing",
|
|
415
|
+
"usage": {},
|
|
416
|
+
"warnings": ["backend_unavailable", "fallback_blocked"],
|
|
417
|
+
"raw": {
|
|
418
|
+
"event": "backend_unavailable",
|
|
419
|
+
"backend": backend,
|
|
420
|
+
"provider": _automation_provider_for_backend(backend),
|
|
421
|
+
"fallback_policy": "fail_closed",
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
caller=caller,
|
|
425
|
+
session_type="headless",
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
|
|
381
429
|
def _resolve_claude_cli() -> str:
|
|
382
430
|
return _shared_resolve_claude_cli()
|
|
383
431
|
|
|
384
432
|
|
|
385
433
|
def _resolve_codex_cli() -> str:
|
|
434
|
+
home = Path.home()
|
|
435
|
+
if _desktop_product_requested(home):
|
|
436
|
+
managed_path = _managed_codex_binary(home)
|
|
437
|
+
return managed_path if managed_path and _managed_codex_vendor_present(home) else ""
|
|
386
438
|
env_path = os.environ.get("CODEX_BIN", "").strip()
|
|
387
439
|
if env_path and Path(env_path).exists():
|
|
388
440
|
return env_path
|
|
441
|
+
managed_path = _managed_codex_binary(home)
|
|
442
|
+
if managed_path and _managed_codex_vendor_present(home):
|
|
443
|
+
return managed_path
|
|
389
444
|
return shutil.which("codex") or ""
|
|
390
445
|
|
|
391
446
|
|
|
@@ -790,6 +845,7 @@ def run_automation_interactive(
|
|
|
790
845
|
row_id, _record_err = _record_automation_start(
|
|
791
846
|
caller=caller,
|
|
792
847
|
backend=resolved_client,
|
|
848
|
+
provider=_automation_provider_for_backend(resolved_client),
|
|
793
849
|
session_type=session_type,
|
|
794
850
|
task_profile="",
|
|
795
851
|
model="",
|
|
@@ -905,15 +961,6 @@ def _backend_is_available(backend: str) -> bool:
|
|
|
905
961
|
|
|
906
962
|
|
|
907
963
|
def _resolve_available_backend(selected_backend: str, *, preferences: dict | None = None) -> str:
|
|
908
|
-
if _backend_is_available(selected_backend):
|
|
909
|
-
return selected_backend
|
|
910
|
-
prefs = preferences or load_client_preferences()
|
|
911
|
-
preferred = resolve_automation_backend(preferences=prefs)
|
|
912
|
-
for candidate in (preferred, CLIENT_CLAUDE_CODE, CLIENT_CODEX):
|
|
913
|
-
if candidate == selected_backend or candidate == BACKEND_NONE:
|
|
914
|
-
continue
|
|
915
|
-
if _backend_is_available(candidate):
|
|
916
|
-
return candidate
|
|
917
964
|
return selected_backend
|
|
918
965
|
|
|
919
966
|
|
|
@@ -1011,7 +1058,7 @@ _ANTHROPIC_API_KEY_SEARCH_PATHS = (
|
|
|
1011
1058
|
|
|
1012
1059
|
|
|
1013
1060
|
def _resolve_anthropic_api_key() -> str:
|
|
1014
|
-
"""Locate
|
|
1061
|
+
"""Locate a legacy Anthropic API key for opt-in bare-mode invocations.
|
|
1015
1062
|
|
|
1016
1063
|
``claude --bare`` skips macOS Keychain auth entirely, so the child
|
|
1017
1064
|
must find the API key in ``ANTHROPIC_API_KEY`` or via ``apiKeyHelper``.
|
|
@@ -1039,15 +1086,16 @@ def _resolve_anthropic_api_key() -> str:
|
|
|
1039
1086
|
return ""
|
|
1040
1087
|
|
|
1041
1088
|
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1089
|
+
def _allow_anthropic_api_key_bare_mode() -> bool:
|
|
1090
|
+
value = os.environ.get("NEXO_ALLOW_ANTHROPIC_API_BARE", "").strip().lower()
|
|
1091
|
+
return value in {"1", "true", "yes", "on"}
|
|
1092
|
+
|
|
1093
|
+
|
|
1094
|
+
# v7.26 provider-runtime contract: Desktop-managed automation must use the
|
|
1095
|
+
# selected account runtime (Claude Code or Codex), not an Anthropic API key
|
|
1096
|
+
# side path. Bare mode remains a hidden legacy opt-in for controlled local
|
|
1097
|
+
# experiments only via NEXO_ALLOW_ANTHROPIC_API_BARE=1.
|
|
1098
|
+
BARE_MODE_SAFE_CALLERS: frozenset[str] = frozenset()
|
|
1051
1099
|
|
|
1052
1100
|
# Execution contracts keep background agents disciplined without polluting
|
|
1053
1101
|
# machine-only child calls that must return strict JSON.
|
|
@@ -1153,6 +1201,17 @@ def run_automation_prompt(
|
|
|
1153
1201
|
if selected_backend == BACKEND_NONE:
|
|
1154
1202
|
raise AutomationBackendUnavailableError("Automation backend is disabled in config.")
|
|
1155
1203
|
selected_backend = _resolve_available_backend(selected_backend, preferences=prefs)
|
|
1204
|
+
if not _backend_is_available(selected_backend):
|
|
1205
|
+
cwd_path = Path(cwd).expanduser().resolve() if cwd else Path.cwd()
|
|
1206
|
+
_record_backend_unavailable(
|
|
1207
|
+
backend=selected_backend,
|
|
1208
|
+
caller=caller,
|
|
1209
|
+
cwd=cwd_path,
|
|
1210
|
+
prompt=prompt,
|
|
1211
|
+
)
|
|
1212
|
+
raise AutomationBackendUnavailableError(
|
|
1213
|
+
f"{selected_backend} automation backend selected but launcher is not installed; fallback blocked."
|
|
1214
|
+
)
|
|
1156
1215
|
|
|
1157
1216
|
# Resonance map decides (model, effort) for every call. ``caller`` is
|
|
1158
1217
|
# MANDATORY — every script that invokes the automation backend must be
|
|
@@ -1256,13 +1315,13 @@ def run_automation_prompt(
|
|
|
1256
1315
|
# completes in seconds.
|
|
1257
1316
|
#
|
|
1258
1317
|
# Selection rules:
|
|
1259
|
-
# - bare_mode=True explicit →
|
|
1318
|
+
# - bare_mode=True explicit → allow only when the legacy API-key
|
|
1319
|
+
# escape hatch is enabled.
|
|
1260
1320
|
# - bare_mode=None (default) + caller in BARE_MODE_SAFE_CALLERS
|
|
1261
|
-
# → auto-enable.
|
|
1321
|
+
# → auto-enable. v7.26 keeps this set empty for provider parity.
|
|
1262
1322
|
# - bare_mode=False → never.
|
|
1263
|
-
# - --bare disables keychain auth, so
|
|
1264
|
-
# ANTHROPIC_API_KEY. If
|
|
1265
|
-
# normal mode with a warning on stderr rather than failing.
|
|
1323
|
+
# - --bare disables keychain auth, so the legacy path requires
|
|
1324
|
+
# ANTHROPIC_API_KEY. If disabled or missing, use normal account auth.
|
|
1266
1325
|
resolved_bare = False
|
|
1267
1326
|
if bare_mode is True:
|
|
1268
1327
|
resolved_bare = True
|
|
@@ -1271,12 +1330,14 @@ def run_automation_prompt(
|
|
|
1271
1330
|
|
|
1272
1331
|
bare_api_key = ""
|
|
1273
1332
|
if resolved_bare:
|
|
1274
|
-
|
|
1275
|
-
if not bare_api_key:
|
|
1276
|
-
# Silent fallback: we would rather take the slower path
|
|
1277
|
-
# than force the caller to fail-closed on an env quirk.
|
|
1333
|
+
if not _allow_anthropic_api_key_bare_mode():
|
|
1278
1334
|
resolved_bare = False
|
|
1279
|
-
|
|
1335
|
+
else:
|
|
1336
|
+
bare_api_key = _resolve_anthropic_api_key()
|
|
1337
|
+
if not bare_api_key:
|
|
1338
|
+
# Silent fallback: we would rather take the slower path
|
|
1339
|
+
# than force the caller to fail-closed on an env quirk.
|
|
1340
|
+
resolved_bare = False
|
|
1280
1341
|
# Headless claude -p does NOT reliably honour permissions.allow from
|
|
1281
1342
|
# settings.json for MCP tool calls — it can stall waiting for an
|
|
1282
1343
|
# approval that will never come in non-interactive mode. All NEXO
|
|
@@ -1293,6 +1354,8 @@ def run_automation_prompt(
|
|
|
1293
1354
|
run_env = dict(run_env)
|
|
1294
1355
|
run_env["ANTHROPIC_API_KEY"] = bare_api_key
|
|
1295
1356
|
else:
|
|
1357
|
+
run_env = dict(run_env)
|
|
1358
|
+
run_env.pop("ANTHROPIC_API_KEY", None)
|
|
1296
1359
|
cmd.append("--dangerously-skip-permissions")
|
|
1297
1360
|
if resolved_model:
|
|
1298
1361
|
cmd.extend(["--model", resolved_model])
|
|
@@ -1342,6 +1405,7 @@ def run_automation_prompt(
|
|
|
1342
1405
|
telemetry["automation_contract"] = automation_contract
|
|
1343
1406
|
recorded, record_error = _record_automation_run(
|
|
1344
1407
|
backend=selected_backend,
|
|
1408
|
+
provider=_automation_provider_for_backend(selected_backend),
|
|
1345
1409
|
task_profile=task_profile,
|
|
1346
1410
|
model=resolved_model,
|
|
1347
1411
|
reasoning_effort=resolved_effort,
|
|
@@ -1420,6 +1484,7 @@ def run_automation_prompt(
|
|
|
1420
1484
|
telemetry["automation_contract"] = automation_contract
|
|
1421
1485
|
recorded, record_error = _record_automation_run(
|
|
1422
1486
|
backend=selected_backend,
|
|
1487
|
+
provider=_automation_provider_for_backend(selected_backend),
|
|
1423
1488
|
task_profile=task_profile,
|
|
1424
1489
|
model=resolved_model,
|
|
1425
1490
|
reasoning_effort=resolved_effort,
|