nexo-brain 7.25.3 → 7.25.4

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.
@@ -18,6 +18,7 @@ MAIN_CLEANUP_STATE_KEY = "local_context_main_tables_drained"
18
18
  LOCAL_CONTEXT_TABLES: tuple[str, ...] = (
19
19
  "local_index_roots",
20
20
  "local_index_exclusions",
21
+ "local_index_file_type_rules",
21
22
  "local_index_jobs",
22
23
  "local_index_checkpoints",
23
24
  "local_index_state",
@@ -103,10 +104,53 @@ def _ensure_schema(conn: sqlite3.Connection) -> None:
103
104
  _m63_local_context_layer(conn)
104
105
  _m64_local_context_live_dirs(conn)
105
106
  _ensure_entity_dossier_schema(conn)
106
- conn.execute("PRAGMA user_version=64")
107
+ _ensure_local_context_v2_schema(conn)
108
+ conn.execute("PRAGMA user_version=65")
107
109
  conn.commit()
108
110
 
109
111
 
112
+ def _table_columns(conn: sqlite3.Connection, table: str) -> set[str]:
113
+ rows = conn.execute(f"PRAGMA table_info({table})").fetchall()
114
+ return {str(row["name"] if isinstance(row, sqlite3.Row) else row[1]) for row in rows}
115
+
116
+
117
+ def _add_column_if_missing(conn: sqlite3.Connection, table: str, column: str, definition: str) -> None:
118
+ if column in _table_columns(conn, table):
119
+ return
120
+ conn.execute(f"ALTER TABLE {table} ADD COLUMN {column} {definition}")
121
+
122
+
123
+ def _ensure_local_context_v2_schema(conn: sqlite3.Connection) -> None:
124
+ """Idempotent local-index v2 schema for managed roots and file type rules."""
125
+ _add_column_if_missing(conn, "local_index_roots", "source", "TEXT NOT NULL DEFAULT 'legacy'")
126
+ _add_column_if_missing(conn, "local_index_roots", "remote", "INTEGER NOT NULL DEFAULT 0")
127
+ _add_column_if_missing(conn, "local_index_roots", "seed_version", "INTEGER NOT NULL DEFAULT 1")
128
+ _add_column_if_missing(conn, "local_index_exclusions", "source", "TEXT NOT NULL DEFAULT 'legacy'")
129
+ _add_column_if_missing(conn, "local_index_exclusions", "kind", "TEXT NOT NULL DEFAULT 'folder'")
130
+ conn.executescript(
131
+ """
132
+ CREATE TABLE IF NOT EXISTS local_index_file_type_rules (
133
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
134
+ extension TEXT NOT NULL,
135
+ action TEXT NOT NULL DEFAULT 'ignore',
136
+ source TEXT NOT NULL DEFAULT 'user',
137
+ priority INTEGER NOT NULL DEFAULT 0,
138
+ reason TEXT NOT NULL DEFAULT '',
139
+ created_at REAL NOT NULL,
140
+ updated_at REAL NOT NULL,
141
+ UNIQUE(extension, source)
142
+ );
143
+
144
+ CREATE INDEX IF NOT EXISTS idx_local_index_roots_source
145
+ ON local_index_roots(source, status);
146
+ CREATE INDEX IF NOT EXISTS idx_local_index_exclusions_source
147
+ ON local_index_exclusions(source);
148
+ CREATE INDEX IF NOT EXISTS idx_local_index_file_type_rules_ext
149
+ ON local_index_file_type_rules(extension, source);
150
+ """
151
+ )
152
+
153
+
110
154
  def _ensure_entity_dossier_schema(conn: sqlite3.Connection) -> None:
111
155
  conn.executescript(
112
156
  """
@@ -68,6 +68,20 @@ def _read_text(path: Path) -> str:
68
68
  return data.decode("utf-8", errors="replace")[:MAX_CHARS]
69
69
 
70
70
 
71
+ def _read_text_if_safe(path: Path) -> str:
72
+ data = path.read_bytes()[:MAX_TEXT_BYTES]
73
+ if not data or b"\x00" in data[:8192]:
74
+ return ""
75
+ text = _read_text(path)
76
+ if not text:
77
+ return ""
78
+ printable = sum(1 for char in text[:4096] if char.isprintable() or char.isspace())
79
+ sample_len = max(1, min(len(text), 4096))
80
+ if printable / sample_len < 0.85:
81
+ return ""
82
+ return text
83
+
84
+
71
85
  def _extract_csv(path: Path) -> str:
72
86
  text = _read_text(path)
73
87
  rows = []
@@ -291,7 +305,9 @@ def extract_text(path: Path) -> tuple[str, dict]:
291
305
  elif suffix == ".xlsx":
292
306
  text = _extract_xlsx(path)
293
307
  else:
294
- text = ""
308
+ text = _read_text_if_safe(path)
309
+ if text:
310
+ metadata["extractor"] = "generic_text"
295
311
  if contains_secret(text):
296
312
  metadata["content_secret_detected"] = True
297
313
  return clean_text(text), metadata
package/src/server.py CHANGED
@@ -1026,6 +1026,32 @@ def nexo_local_index_exclusions(action: str = "list", path: str = "", reason: st
1026
1026
  return json.dumps(result, ensure_ascii=False)
1027
1027
 
1028
1028
 
1029
+ @mcp.tool
1030
+ def nexo_local_index_filetypes(action: str = "list", extension: str = "", mode: str = "extract", reason: str = "user") -> str:
1031
+ """List, include, exclude or reset local memory file extension rules."""
1032
+ normalized = str(action or "list").strip().lower()
1033
+ if normalized == "list":
1034
+ result = local_context_api.list_file_type_rules(readonly=True)
1035
+ elif normalized in {"include", "add"}:
1036
+ result = local_context_api.set_file_type_rule(extension, action=mode or "extract", reason=reason)
1037
+ elif normalized in {"exclude", "ignore"}:
1038
+ result = local_context_api.set_file_type_rule(extension, action="ignore", reason=reason)
1039
+ elif normalized in {"remove", "delete"}:
1040
+ result = local_context_api.remove_file_type_rule(extension)
1041
+ elif normalized == "reset":
1042
+ result = local_context_api.reset_file_type_rules()
1043
+ else:
1044
+ result = {"ok": False, "error": "unknown_action", "allowed": ["list", "include", "exclude", "remove", "reset"]}
1045
+ return json.dumps(result, ensure_ascii=False)
1046
+
1047
+
1048
+ @mcp.tool
1049
+ def nexo_local_index_migrate_roots_v2(apply: bool = False) -> str:
1050
+ """Plan or apply Local Memory roots v2 cleanup."""
1051
+ result = local_context_api.migrate_roots_seed_v2(dry_run=not bool(apply))
1052
+ return json.dumps(result, ensure_ascii=False)
1053
+
1054
+
1029
1055
  @mcp.tool
1030
1056
  def nexo_local_context(
1031
1057
  query: str,
@@ -2502,6 +2502,37 @@
2502
2502
  },
2503
2503
  "triggers_after": []
2504
2504
  },
2505
+ "nexo_local_index_filetypes": {
2506
+ "description": "List, include, exclude, remove, or reset Local Context file extension rules",
2507
+ "category": "local_context",
2508
+ "source": "server",
2509
+ "requires": [],
2510
+ "provides": [
2511
+ "local_index_file_type_rules"
2512
+ ],
2513
+ "internal_calls": [],
2514
+ "enforcement": {
2515
+ "level": "none",
2516
+ "rules": []
2517
+ },
2518
+ "triggers_after": []
2519
+ },
2520
+ "nexo_local_index_migrate_roots_v2": {
2521
+ "description": "Plan or apply Local Context roots v2 cleanup",
2522
+ "category": "local_context",
2523
+ "source": "server",
2524
+ "requires": [],
2525
+ "provides": [
2526
+ "local_index_roots",
2527
+ "local_index_cleanup"
2528
+ ],
2529
+ "internal_calls": [],
2530
+ "enforcement": {
2531
+ "level": "none",
2532
+ "rules": []
2533
+ },
2534
+ "triggers_after": []
2535
+ },
2505
2536
  "nexo_local_index_models": {
2506
2537
  "description": "Inspect or warm Local Context local model availability",
2507
2538
  "category": "local_context",