nexo-brain 6.0.4 → 6.0.6
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 +1 -1
- package/bin/nexo-brain.js +79 -3
- package/package.json +1 -1
- package/src/auto_update.py +43 -1
- package/src/client_sync.py +23 -1
- package/src/hook_guardrails.py +36 -1
- package/src/plugin_loader.py +11 -1
- package/src/plugins/update.py +12 -1
- package/src/script_registry.py +4 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "6.0.
|
|
3
|
+
"version": "6.0.6",
|
|
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,7 @@
|
|
|
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 `6.0.
|
|
21
|
+
Version `6.0.6` is the current packaged-runtime line: the installer no longer leaks `export PATH="$NEXO_HOME/bin:$PATH"` into the developer's real shell profile (`~/.bash_profile`, `~/.bashrc`, `~/.zshrc`) when `NEXO_HOME` points somewhere other than the canonical `$HOME/.nexo` — the classic case being any pytest run with `NEXO_HOME=/tmp/pytest-xxx`. Both the Python path (`src/auto_update.py::_ensure_runtime_cli_in_shell`) and the two JavaScript twins in `bin/nexo-brain.js` (install Step 8 and the migration path that restores the operator alias) now consult `_should_skip_shell_profile_backfill()` / `shouldSkipShellProfileBackfill()` and skip the write whenever `NEXO_HOME` is non-canonical, with `NEXO_SKIP_SHELL_PROFILE=1` as an explicit escape hatch. Fresh installs at `$HOME/.nexo` are unaffected. Release also carries v6.0.5's strict-hook `unknown target` fix and the pytest CI gate that caught this regression in the first place.
|
|
22
22
|
|
|
23
23
|
Previously in `6.0.2`: adds the reserved caller prefix `personal/*` so scripts living in `~/.nexo/scripts/` can invoke the automation backend with their own caller id without editing `src/resonance_map.py`. New kwarg `tier` (`"maximo"` / `"alto"` / `"medio"` / `"bajo"`) on `run_automation_prompt`, `run_automation_interactive`, `nexo_helper.run_automation_text`, `nexo_helper.run_automation_json`, and `nexo-agent-run.py --tier`. Precedence for `personal/*` callers: explicit `tier=` → explicit `reasoning_effort=` → `calibration.preferences.default_resonance` → `DEFAULT_RESONANCE` (`alto`). Registered callers keep their behaviour unchanged. New guide: [`docs/personal-scripts-guide.md`](docs/personal-scripts-guide.md).
|
|
24
24
|
|
package/bin/nexo-brain.js
CHANGED
|
@@ -21,6 +21,27 @@ const path = require("path");
|
|
|
21
21
|
const readline = require("readline");
|
|
22
22
|
|
|
23
23
|
let NEXO_HOME = process.env.NEXO_HOME || path.join(require("os").homedir(), ".nexo");
|
|
24
|
+
|
|
25
|
+
function shouldSkipShellProfileBackfill() {
|
|
26
|
+
// Mirror of _should_skip_shell_profile_backfill() in src/auto_update.py.
|
|
27
|
+
// Prevent the installer from leaking ``export PATH`` / operator alias lines
|
|
28
|
+
// into the developer's real shell profile whenever NEXO_HOME is not the
|
|
29
|
+
// canonical $HOME/.nexo path (pytest tmp dirs, CI sandboxes, containers).
|
|
30
|
+
const flag = String(process.env.NEXO_SKIP_SHELL_PROFILE || "").trim().toLowerCase();
|
|
31
|
+
if (["1", "true", "yes", "on"].includes(flag)) {
|
|
32
|
+
return { skip: true, reason: `NEXO_SKIP_SHELL_PROFILE=${flag}` };
|
|
33
|
+
}
|
|
34
|
+
const canonical = path.join(require("os").homedir(), ".nexo");
|
|
35
|
+
let actual = NEXO_HOME;
|
|
36
|
+
try {
|
|
37
|
+
actual = path.resolve(NEXO_HOME);
|
|
38
|
+
} catch {}
|
|
39
|
+
if (actual !== canonical) {
|
|
40
|
+
return { skip: true, reason: `NEXO_HOME=${actual} is not the canonical ${canonical}` };
|
|
41
|
+
}
|
|
42
|
+
return { skip: false, reason: "" };
|
|
43
|
+
}
|
|
44
|
+
|
|
24
45
|
const CLAUDE_SETTINGS = path.join(
|
|
25
46
|
require("os").homedir(),
|
|
26
47
|
".claude",
|
|
@@ -93,10 +114,50 @@ const RESONANCE_TIER_NAMES = ["maximo", "alto", "medio", "bajo"];
|
|
|
93
114
|
const DEFAULT_RESONANCE_TIER = _RESONANCE_TIERS.default_tier || "alto";
|
|
94
115
|
|
|
95
116
|
function isEphemeralInstall(nexoHome) {
|
|
96
|
-
const
|
|
117
|
+
const os = require("os");
|
|
118
|
+
const homeDir = os.homedir();
|
|
97
119
|
const allowEphemeral = process.env.NEXO_ALLOW_EPHEMERAL_INSTALL === "1";
|
|
98
120
|
if (allowEphemeral) return false;
|
|
99
|
-
|
|
121
|
+
|
|
122
|
+
const normalize = (candidate) => {
|
|
123
|
+
if (!candidate) return "";
|
|
124
|
+
let resolved = String(candidate);
|
|
125
|
+
try {
|
|
126
|
+
resolved = fs.realpathSync.native(resolved);
|
|
127
|
+
} catch {
|
|
128
|
+
try {
|
|
129
|
+
resolved = fs.realpathSync(resolved);
|
|
130
|
+
} catch {
|
|
131
|
+
resolved = path.resolve(resolved);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return resolved.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const tempRoots = new Set();
|
|
138
|
+
for (const root of [os.tmpdir(), "/tmp", "/var/folders", "/private/var/folders"]) {
|
|
139
|
+
const normalized = normalize(root);
|
|
140
|
+
if (!normalized) continue;
|
|
141
|
+
tempRoots.add(normalized);
|
|
142
|
+
if (normalized === "/tmp") {
|
|
143
|
+
tempRoots.add("/private/tmp");
|
|
144
|
+
} else if (normalized === "/private/tmp") {
|
|
145
|
+
tempRoots.add("/tmp");
|
|
146
|
+
} else if (normalized.startsWith("/var/")) {
|
|
147
|
+
tempRoots.add(`/private${normalized}`);
|
|
148
|
+
} else if (normalized.startsWith("/private/var/")) {
|
|
149
|
+
tempRoots.add(normalized.replace(/^\/private/, ""));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const isWithin = (candidate, root) => (
|
|
154
|
+
candidate === root || candidate.startsWith(`${root}/`)
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
return [nexoHome, homeDir]
|
|
158
|
+
.map(normalize)
|
|
159
|
+
.filter(Boolean)
|
|
160
|
+
.some((candidate) => Array.from(tempRoots).some((root) => isWithin(candidate, root)));
|
|
100
161
|
}
|
|
101
162
|
|
|
102
163
|
const rl = readline.createInterface({
|
|
@@ -1816,6 +1877,10 @@ async function main() {
|
|
|
1816
1877
|
const migOperatorName = installed.operator_name || "NEXO";
|
|
1817
1878
|
const migAliasName = migOperatorName.toLowerCase();
|
|
1818
1879
|
if (migAliasName !== "nexo") {
|
|
1880
|
+
const migSkip = shouldSkipShellProfileBackfill();
|
|
1881
|
+
if (migSkip.skip) {
|
|
1882
|
+
log(` Skipping shell profile alias restore — ${migSkip.reason}`);
|
|
1883
|
+
} else {
|
|
1819
1884
|
const migAliasLine = `alias ${migAliasName}='nexo chat .'`;
|
|
1820
1885
|
const migAliasComment = `# ${migOperatorName} — open the configured NEXO terminal client`;
|
|
1821
1886
|
const migNexoPathLine = `export PATH="${path.join(NEXO_HOME, "bin")}:$PATH"`;
|
|
@@ -1844,6 +1909,7 @@ async function main() {
|
|
|
1844
1909
|
log(` Restored '${migAliasName}' alias in ${path.basename(rcFile)}`);
|
|
1845
1910
|
}
|
|
1846
1911
|
}
|
|
1912
|
+
}
|
|
1847
1913
|
}
|
|
1848
1914
|
|
|
1849
1915
|
console.log("");
|
|
@@ -3325,7 +3391,10 @@ ${doScan ? `- Stack: ${Object.keys(profileData.code.languages || {}).slice(0, 5)
|
|
|
3325
3391
|
schedule = await maybeConfigurePublicContribution(schedule, useDefaults);
|
|
3326
3392
|
schedule = await maybeConfigureFullDiskAccess(schedule, useDefaults, python);
|
|
3327
3393
|
const enabledOptionals = { dashboard: doDashboard, automation: schedule.automation_enabled !== false };
|
|
3328
|
-
|
|
3394
|
+
const smokeTestMode = process.env.NEXO_TESTING_SMOKE === "1";
|
|
3395
|
+
if (smokeTestMode) {
|
|
3396
|
+
log("Smoke test mode detected — skipping LaunchAgents installation.");
|
|
3397
|
+
} else if (isEphemeralInstall(NEXO_HOME)) {
|
|
3329
3398
|
log("Ephemeral HOME/NEXO_HOME detected — skipping LaunchAgents installation.");
|
|
3330
3399
|
} else {
|
|
3331
3400
|
installAllProcesses(platform, python, NEXO_HOME, schedule, LAUNCH_AGENTS, enabledOptionals);
|
|
@@ -3347,6 +3416,12 @@ ${doScan ? `- Stack: ${Object.keys(profileData.code.languages || {}).slice(0, 5)
|
|
|
3347
3416
|
|
|
3348
3417
|
// Step 8: Create shell alias and add runtime CLI to PATH
|
|
3349
3418
|
const aliasName = operatorName.toLowerCase();
|
|
3419
|
+
const installSkipShell = shouldSkipShellProfileBackfill();
|
|
3420
|
+
if (installSkipShell.skip) {
|
|
3421
|
+
log(`Skipping shell profile setup — ${installSkipShell.reason}`);
|
|
3422
|
+
log(`(Runtime CLI wrapper still installed at ${path.join(NEXO_HOME, "bin", "nexo")}; add it to PATH manually if needed.)`);
|
|
3423
|
+
console.log("");
|
|
3424
|
+
} else {
|
|
3350
3425
|
const nexoPathLine = `export PATH="${path.join(NEXO_HOME, "bin")}:$PATH"`;
|
|
3351
3426
|
const nexoPathComment = "# NEXO runtime CLI";
|
|
3352
3427
|
|
|
@@ -3399,6 +3474,7 @@ ${doScan ? `- Stack: ${Object.keys(profileData.code.languages || {}).slice(0, 5)
|
|
|
3399
3474
|
log(`After setup, open a new terminal and type: ${aliasName} or nexo`);
|
|
3400
3475
|
}
|
|
3401
3476
|
console.log("");
|
|
3477
|
+
}
|
|
3402
3478
|
|
|
3403
3479
|
// Step 9: Generate CLAUDE.md template
|
|
3404
3480
|
log("Generating operator instructions...");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "6.0.
|
|
3
|
+
"version": "6.0.6",
|
|
4
4
|
"mcpName": "io.github.wazionapps/nexo",
|
|
5
5
|
"description": "NEXO Brain \u2014 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",
|
package/src/auto_update.py
CHANGED
|
@@ -18,7 +18,18 @@ import time
|
|
|
18
18
|
from pathlib import Path
|
|
19
19
|
|
|
20
20
|
from runtime_home import export_resolved_nexo_home, managed_nexo_home
|
|
21
|
-
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
from tree_hygiene import is_duplicate_artifact_name
|
|
24
|
+
except ModuleNotFoundError as exc:
|
|
25
|
+
if getattr(exc, "name", "") != "tree_hygiene":
|
|
26
|
+
raise
|
|
27
|
+
|
|
28
|
+
# Older installed runtimes may update into code that references
|
|
29
|
+
# tree_hygiene.py before that module has been copied over. Fall back
|
|
30
|
+
# to "no duplicate" so the update can complete and deliver the module.
|
|
31
|
+
def is_duplicate_artifact_name(_path) -> bool:
|
|
32
|
+
return False
|
|
22
33
|
|
|
23
34
|
NEXO_HOME = export_resolved_nexo_home()
|
|
24
35
|
DATA_DIR = NEXO_HOME / "data"
|
|
@@ -372,7 +383,38 @@ def _shell_rc_files() -> list[Path]:
|
|
|
372
383
|
return [home_dir / ".bash_profile", home_dir / ".bashrc"]
|
|
373
384
|
|
|
374
385
|
|
|
386
|
+
def _should_skip_shell_profile_backfill() -> tuple[bool, str]:
|
|
387
|
+
"""Decide whether to skip writing ``export PATH`` into the user's shell rc.
|
|
388
|
+
|
|
389
|
+
The backfill must only touch the real user shell profile when the runtime
|
|
390
|
+
is installed at the canonical ``$HOME/.nexo`` location. When ``NEXO_HOME``
|
|
391
|
+
points elsewhere (pytest ``tmp_path``, CI sandbox, containerised test
|
|
392
|
+
harness) writing to ``~/.bash_profile`` / ``~/.bashrc`` / ``~/.zshrc``
|
|
393
|
+
leaks a non-canonical ``export PATH`` line into the developer's real
|
|
394
|
+
shell, which is exactly the bug reported for v6.0.x. Users can force-skip
|
|
395
|
+
via ``NEXO_SKIP_SHELL_PROFILE=1``.
|
|
396
|
+
"""
|
|
397
|
+
flag = os.environ.get("NEXO_SKIP_SHELL_PROFILE", "").strip().lower()
|
|
398
|
+
if flag in {"1", "true", "yes", "on"}:
|
|
399
|
+
return True, f"NEXO_SKIP_SHELL_PROFILE={flag}"
|
|
400
|
+
try:
|
|
401
|
+
canonical = managed_nexo_home()
|
|
402
|
+
except Exception:
|
|
403
|
+
canonical = Path.home() / ".nexo"
|
|
404
|
+
try:
|
|
405
|
+
same = NEXO_HOME.resolve(strict=False) == canonical.resolve(strict=False)
|
|
406
|
+
except Exception:
|
|
407
|
+
same = NEXO_HOME == canonical
|
|
408
|
+
if not same:
|
|
409
|
+
return True, f"NEXO_HOME={NEXO_HOME} is not the canonical {canonical}"
|
|
410
|
+
return False, ""
|
|
411
|
+
|
|
412
|
+
|
|
375
413
|
def _ensure_runtime_cli_in_shell():
|
|
414
|
+
skip, reason = _should_skip_shell_profile_backfill()
|
|
415
|
+
if skip:
|
|
416
|
+
_log(f"Skipping shell profile backfill — {reason}")
|
|
417
|
+
return
|
|
376
418
|
path_line = f'export PATH="{NEXO_HOME / "bin"}:$PATH"'
|
|
377
419
|
comment = "# NEXO runtime CLI"
|
|
378
420
|
for rc_file in _shell_rc_files():
|
package/src/client_sync.py
CHANGED
|
@@ -542,9 +542,31 @@ CORE_HOOK_SPECS = [
|
|
|
542
542
|
},
|
|
543
543
|
]
|
|
544
544
|
|
|
545
|
+
# Claude Code can retain legacy managed hooks from older/plugin-style installs
|
|
546
|
+
# or from transient test runtimes. Treat their handler basenames as managed so
|
|
547
|
+
# the next sync prunes them instead of leaving broken duplicates behind.
|
|
545
548
|
LEGACY_CORE_HOOK_IDENTITIES_BY_EVENT = {
|
|
549
|
+
"SessionStart": {
|
|
550
|
+
"session_start.py",
|
|
551
|
+
},
|
|
552
|
+
"Stop": {
|
|
553
|
+
"stop.py",
|
|
554
|
+
},
|
|
555
|
+
"UserPromptSubmit": {
|
|
556
|
+
"auto_capture.py",
|
|
557
|
+
},
|
|
546
558
|
"PostToolUse": {
|
|
547
559
|
"heartbeat-guard.sh",
|
|
560
|
+
"post_tool_use.py",
|
|
561
|
+
},
|
|
562
|
+
"PreCompact": {
|
|
563
|
+
"pre_compact.py",
|
|
564
|
+
},
|
|
565
|
+
"Notification": {
|
|
566
|
+
"notification.py",
|
|
567
|
+
},
|
|
568
|
+
"SubagentStop": {
|
|
569
|
+
"subagent_stop.py",
|
|
548
570
|
},
|
|
549
571
|
}
|
|
550
572
|
|
|
@@ -599,7 +621,7 @@ def _hook_identity(command: str) -> str:
|
|
|
599
621
|
text = str(command or "")
|
|
600
622
|
if ".session-start-ts" in text:
|
|
601
623
|
return "session-start-ts"
|
|
602
|
-
match = re.search(r"([A-Za-z0-9._-]+\.sh)\b", text)
|
|
624
|
+
match = re.search(r"([A-Za-z0-9._-]+\.(?:sh|py))\b", text)
|
|
603
625
|
if match:
|
|
604
626
|
return match.group(1)
|
|
605
627
|
return text.strip()
|
package/src/hook_guardrails.py
CHANGED
|
@@ -429,6 +429,38 @@ def _collect_automation_live_repo_blocks(
|
|
|
429
429
|
return blocks
|
|
430
430
|
|
|
431
431
|
|
|
432
|
+
def _read_claude_session_id_from_coordination() -> str:
|
|
433
|
+
"""Fallback claude_session_id when Claude Code's PreToolUse payload omits it.
|
|
434
|
+
|
|
435
|
+
SessionStart hook writes the active Claude Code session UUID to
|
|
436
|
+
``<NEXO_HOME>/coordination/.claude-session-id``. When the PreToolUse
|
|
437
|
+
payload omits ``session_id`` (observed across several Claude Code
|
|
438
|
+
versions), the pre-tool guardrail would lose correlation with the open
|
|
439
|
+
NEXO session and block every write with "unknown target" (learning
|
|
440
|
+
#411). Reading the coordination file restores the correlation without
|
|
441
|
+
relaxing fail-closed semantics: if the file is missing or empty the
|
|
442
|
+
caller still blocks.
|
|
443
|
+
"""
|
|
444
|
+
candidates = []
|
|
445
|
+
nexo_home = os.environ.get("NEXO_HOME", "").strip()
|
|
446
|
+
if nexo_home:
|
|
447
|
+
candidates.append(Path(nexo_home).expanduser() / "coordination" / ".claude-session-id")
|
|
448
|
+
candidates.append(Path.home() / ".nexo" / "coordination" / ".claude-session-id")
|
|
449
|
+
seen: set[str] = set()
|
|
450
|
+
for path in candidates:
|
|
451
|
+
key = str(path)
|
|
452
|
+
if key in seen:
|
|
453
|
+
continue
|
|
454
|
+
seen.add(key)
|
|
455
|
+
try:
|
|
456
|
+
value = path.read_text().strip()
|
|
457
|
+
except (FileNotFoundError, OSError):
|
|
458
|
+
continue
|
|
459
|
+
if value:
|
|
460
|
+
return value
|
|
461
|
+
return ""
|
|
462
|
+
|
|
463
|
+
|
|
432
464
|
def process_pre_tool_event(payload: dict) -> dict:
|
|
433
465
|
tool_name = str(payload.get("tool_name", "")).strip()
|
|
434
466
|
op = _operation_kind(tool_name)
|
|
@@ -439,7 +471,10 @@ def process_pre_tool_event(payload: dict) -> dict:
|
|
|
439
471
|
files = _extract_touched_files(tool_input)
|
|
440
472
|
strictness = get_protocol_strictness()
|
|
441
473
|
conn = get_db()
|
|
442
|
-
|
|
474
|
+
claude_sid = str(payload.get("session_id", "") or "").strip()
|
|
475
|
+
if not claude_sid:
|
|
476
|
+
claude_sid = _read_claude_session_id_from_coordination()
|
|
477
|
+
sid = _resolve_nexo_sid(conn, claude_sid)
|
|
443
478
|
automation_blocks = _collect_automation_live_repo_blocks(
|
|
444
479
|
conn,
|
|
445
480
|
sid=sid,
|
package/src/plugin_loader.py
CHANGED
|
@@ -10,7 +10,17 @@ import time
|
|
|
10
10
|
|
|
11
11
|
from db import get_db
|
|
12
12
|
from fastmcp.tools import Tool
|
|
13
|
-
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
from tree_hygiene import is_duplicate_artifact_name
|
|
16
|
+
except ModuleNotFoundError as exc:
|
|
17
|
+
if getattr(exc, "name", "") != "tree_hygiene":
|
|
18
|
+
raise
|
|
19
|
+
|
|
20
|
+
# Keep older runtimes bootable long enough to receive tree_hygiene.py
|
|
21
|
+
# during update; duplicate filtering will resume once the module lands.
|
|
22
|
+
def is_duplicate_artifact_name(_path) -> bool:
|
|
23
|
+
return False
|
|
14
24
|
|
|
15
25
|
SERVER_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
16
26
|
PLUGINS_DIR = os.path.join(SERVER_DIR, "plugins")
|
package/src/plugins/update.py
CHANGED
|
@@ -11,7 +11,18 @@ import time
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
|
|
13
13
|
from runtime_home import export_resolved_nexo_home
|
|
14
|
-
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
from tree_hygiene import is_duplicate_artifact_name
|
|
17
|
+
except ModuleNotFoundError as exc:
|
|
18
|
+
if getattr(exc, "name", "") != "tree_hygiene":
|
|
19
|
+
raise
|
|
20
|
+
|
|
21
|
+
# Older packaged runtimes may import update.py before tree_hygiene.py
|
|
22
|
+
# has been copied in. Allow the update to finish so the missing module
|
|
23
|
+
# can be delivered by the same upgrade.
|
|
24
|
+
def is_duplicate_artifact_name(_path) -> bool:
|
|
25
|
+
return False
|
|
15
26
|
|
|
16
27
|
# db_guard landed in v5.5.5. When plugins/update.py is imported from a runtime
|
|
17
28
|
# that still ships the v5.5.4 tree (e.g. mid-upgrade), the import will fail —
|
package/src/script_registry.py
CHANGED
|
@@ -304,6 +304,10 @@ def _is_ignored(path: Path) -> bool:
|
|
|
304
304
|
"""Check if file should be ignored entirely."""
|
|
305
305
|
if path.name in _IGNORED_FILES:
|
|
306
306
|
return True
|
|
307
|
+
if re.search(r"\.bak(?:-[\w.-]+)?$", path.name, re.IGNORECASE):
|
|
308
|
+
return True
|
|
309
|
+
if path.name.endswith("~"):
|
|
310
|
+
return True
|
|
307
311
|
if path.name.startswith("."):
|
|
308
312
|
return True
|
|
309
313
|
try:
|