sylo-fieldbrain 0.1.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.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +32 -0
  3. package/extensions/fieldbrain-tools.ts +495 -0
  4. package/extensions/index.ts +12 -0
  5. package/extensions/python-runner.ts +56 -0
  6. package/package.json +44 -0
  7. package/scripts/__pycache__/_db_lib.cpython-312.pyc +0 -0
  8. package/scripts/__pycache__/_json_out.cpython-312.pyc +0 -0
  9. package/scripts/__pycache__/models.cpython-312.pyc +0 -0
  10. package/scripts/_db_lib.py +228 -0
  11. package/scripts/_json_out.py +19 -0
  12. package/scripts/alembic/env.py +53 -0
  13. package/scripts/alembic/versions/001_baseline.py +38 -0
  14. package/scripts/alembic/versions/002_core_tables.py +429 -0
  15. package/scripts/alembic/versions/003_durability_content_in_db.py +106 -0
  16. package/scripts/alembic/versions/004_pgvector_embedding.py +42 -0
  17. package/scripts/alembic/versions/005_project_job_number.py +37 -0
  18. package/scripts/alembic/versions/006_project_parent.py +62 -0
  19. package/scripts/alembic/versions/__init__.py +1 -0
  20. package/scripts/alembic.ini +38 -0
  21. package/scripts/db_auto_migrate.py +103 -0
  22. package/scripts/db_bootstrap.py +207 -0
  23. package/scripts/db_check.py +105 -0
  24. package/scripts/db_migrate.py +85 -0
  25. package/scripts/fieldbrain_brain_delete.py +49 -0
  26. package/scripts/fieldbrain_brain_read.py +61 -0
  27. package/scripts/fieldbrain_brain_restore.py +49 -0
  28. package/scripts/fieldbrain_brain_revisions.py +54 -0
  29. package/scripts/fieldbrain_brain_write.py +67 -0
  30. package/scripts/fieldbrain_document_attach.py +81 -0
  31. package/scripts/fieldbrain_document_catalog.py +128 -0
  32. package/scripts/fieldbrain_document_ingest.py +62 -0
  33. package/scripts/fieldbrain_document_list.py +67 -0
  34. package/scripts/fieldbrain_document_promote.py +43 -0
  35. package/scripts/fieldbrain_log_create.py +55 -0
  36. package/scripts/fieldbrain_log_delete.py +39 -0
  37. package/scripts/fieldbrain_log_restore.py +39 -0
  38. package/scripts/fieldbrain_log_revisions.py +39 -0
  39. package/scripts/fieldbrain_log_search.py +58 -0
  40. package/scripts/fieldbrain_log_update.py +52 -0
  41. package/scripts/fieldbrain_project_create.py +72 -0
  42. package/scripts/fieldbrain_project_list.py +53 -0
  43. package/scripts/fieldbrain_search.py +74 -0
  44. package/scripts/fieldbrain_ui_brain_list.py +60 -0
  45. package/scripts/fieldbrain_ui_project_create.py +95 -0
  46. package/scripts/fieldbrain_ui_project_list.py +49 -0
  47. package/scripts/models.py +355 -0
  48. package/scripts/pgvector_enable.py +165 -0
  49. package/scripts/pgvector_guide.py +44 -0
  50. package/scripts/pgvector_install_files.py +66 -0
  51. package/scripts/pgvector_install_from_folder.py +148 -0
  52. package/scripts/postbuild-ui.mjs +13 -0
  53. package/scripts/requirements.txt +7 -0
  54. package/scripts/services/__init__.py +1 -0
  55. package/scripts/services/__pycache__/__init__.cpython-312.pyc +0 -0
  56. package/scripts/services/__pycache__/brain_paths.cpython-312.pyc +0 -0
  57. package/scripts/services/__pycache__/document_formats.cpython-312.pyc +0 -0
  58. package/scripts/services/__pycache__/document_service.cpython-312.pyc +0 -0
  59. package/scripts/services/__pycache__/pgvector_windows.cpython-312.pyc +0 -0
  60. package/scripts/services/__pycache__/project_naming.cpython-312.pyc +0 -0
  61. package/scripts/services/__pycache__/project_service.cpython-312.pyc +0 -0
  62. package/scripts/services/brain_paths.py +81 -0
  63. package/scripts/services/brain_service.py +280 -0
  64. package/scripts/services/document_catalog.py +186 -0
  65. package/scripts/services/document_formats.py +116 -0
  66. package/scripts/services/document_ingest.py +254 -0
  67. package/scripts/services/document_service.py +316 -0
  68. package/scripts/services/embedding_service.py +72 -0
  69. package/scripts/services/global_brain_support.py +36 -0
  70. package/scripts/services/maintenance_log_service.py +258 -0
  71. package/scripts/services/migrate_lock.py +24 -0
  72. package/scripts/services/pgvector_windows.py +190 -0
  73. package/scripts/services/project_naming.py +72 -0
  74. package/scripts/services/project_service.py +368 -0
  75. package/scripts/services/search_service.py +250 -0
  76. package/scripts/status.py +39 -0
  77. package/shared/README.md +7 -0
  78. package/skills/fieldbrain/SKILL.md +197 -0
  79. package/skills/fieldbrain/SKILL.md.bak +83 -0
  80. package/skills/fieldbrain/routes/fieldbrain/assets/index-B56utPpP.css +1 -0
  81. package/skills/fieldbrain/routes/fieldbrain/assets/index-DO0AD0df.js +55 -0
  82. package/skills/fieldbrain/routes/fieldbrain/fallback.md +7 -0
  83. package/skills/fieldbrain/routes/fieldbrain/index.html +13 -0
@@ -0,0 +1,228 @@
1
+ #!/usr/bin/env python3
2
+ """Postgres access for sylo-fieldbrain — single funnel for all tools."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import logging
8
+ import os
9
+ import urllib.parse
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from sqlalchemy import create_engine, text
14
+ from sqlalchemy.engine import Engine
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ # Bump when Alembic head revision changes (must match latest migration).
19
+ SCHEMA_VERSION = 6
20
+
21
+ DEFAULT_DATABASE_URL = "postgresql://fieldbrain:fieldbrain@localhost:5432/fieldbrain"
22
+ DEFAULT_OLLAMA_URL = "http://127.0.0.1:11434"
23
+
24
+ _engine_singleton: Engine | None = None
25
+
26
+
27
+ def package_root() -> Path:
28
+ return Path(__file__).resolve().parent.parent
29
+
30
+
31
+ def config_dir() -> Path:
32
+ override = os.environ.get("SYLO_FIELDBRAIN_CONFIG_DIR", "").strip()
33
+ if override:
34
+ return Path(override).expanduser()
35
+ legacy = os.environ.get("SYLO_LOGICSCOUT_CONFIG_DIR", "").strip()
36
+ if legacy:
37
+ return Path(legacy).expanduser()
38
+ return Path.home() / ".sylo" / "fieldbrain"
39
+
40
+
41
+ def config_path() -> Path:
42
+ return config_dir() / "database_config.json"
43
+
44
+
45
+ def load_database_config() -> dict[str, Any] | None:
46
+ path = config_path()
47
+ if not path.is_file():
48
+ return None
49
+ try:
50
+ data = json.loads(path.read_text(encoding="utf-8"))
51
+ if isinstance(data, dict):
52
+ return data
53
+ except (OSError, json.JSONDecodeError) as exc:
54
+ logger.warning("Failed to read %s: %s", path, exc)
55
+ return None
56
+
57
+
58
+ def save_database_config(
59
+ host: str,
60
+ port: int,
61
+ database: str,
62
+ username: str,
63
+ password: str = "",
64
+ ) -> Path:
65
+ """Persist connection fields. Empty password keeps existing when file already exists."""
66
+ path = config_path()
67
+ path.parent.mkdir(parents=True, exist_ok=True)
68
+ existing = load_database_config() or {}
69
+ pwd = password if password else str(existing.get("password") or "")
70
+ payload = {
71
+ "host": host.strip(),
72
+ "port": int(port),
73
+ "database": database.strip(),
74
+ "username": username.strip(),
75
+ "password": pwd,
76
+ }
77
+ path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
78
+ return path
79
+
80
+
81
+ def build_database_url() -> str:
82
+ env_url = os.environ.get("SYLO_FIELDBRAIN_DATABASE_URL", "").strip()
83
+ if not env_url:
84
+ env_url = os.environ.get("SYLO_LOGICSCOUT_DATABASE_URL", "").strip()
85
+ if env_url:
86
+ return env_url
87
+
88
+ config = load_database_config()
89
+ if config:
90
+ host = str(config.get("host") or "localhost")
91
+ port = int(config.get("port") or 5432)
92
+ database = str(config.get("database") or "fieldbrain")
93
+ username = str(config.get("username") or "fieldbrain")
94
+ password = str(config.get("password") or "")
95
+ safe_password = urllib.parse.quote_plus(password) if password else ""
96
+ auth = f"{username}:{safe_password}@" if safe_password else f"{username}@"
97
+ return f"postgresql://{auth}{host}:{port}/{database}"
98
+
99
+ legacy = os.environ.get("DATABASE_URL", "").strip()
100
+ if legacy.startswith("postgresql"):
101
+ return legacy
102
+
103
+ return DEFAULT_DATABASE_URL
104
+
105
+
106
+ def ollama_base_url() -> str:
107
+ url = os.environ.get("SYLO_FIELDBRAIN_OLLAMA_URL", "").strip()
108
+ if not url:
109
+ url = os.environ.get("SYLO_LOGICSCOUT_OLLAMA_URL", "").strip()
110
+ return url or DEFAULT_OLLAMA_URL
111
+
112
+
113
+ def reset_engine() -> None:
114
+ global _engine_singleton
115
+ if _engine_singleton is not None:
116
+ try:
117
+ _engine_singleton.dispose()
118
+ except Exception as exc:
119
+ logger.warning("Engine dispose failed: %s", exc)
120
+ _engine_singleton = None
121
+
122
+
123
+ def get_engine(*, echo: bool = False) -> Engine:
124
+ global _engine_singleton
125
+ if _engine_singleton is not None:
126
+ return _engine_singleton
127
+
128
+ url = build_database_url()
129
+ if not url.startswith("postgresql"):
130
+ raise RuntimeError(
131
+ f"FieldBrain requires PostgreSQL. Got {url!r}. "
132
+ "Set SYLO_FIELDBRAIN_DATABASE_URL or ~/.sylo/fieldbrain/database_config.json"
133
+ )
134
+ _engine_singleton = create_engine(url, pool_pre_ping=True, echo=echo)
135
+ return _engine_singleton
136
+
137
+
138
+ def read_applied_schema_version(engine: Engine | None = None) -> int | None:
139
+ eng = engine or get_engine()
140
+ with eng.connect() as conn:
141
+ row = conn.execute(
142
+ text("SELECT version FROM logicscout_schema_meta ORDER BY id DESC LIMIT 1")
143
+ ).fetchone()
144
+ if row is None:
145
+ return None
146
+ return int(row[0])
147
+
148
+
149
+ def verify_schema_version(engine: Engine | None = None) -> tuple[bool, str]:
150
+ """Return (ok, message). Fail loud on mismatch or missing meta table."""
151
+ eng = engine or get_engine()
152
+ try:
153
+ applied = read_applied_schema_version(eng)
154
+ except Exception as exc:
155
+ return False, f"Schema meta unreadable (run fieldbrain_db_migrate): {exc}"
156
+
157
+ if applied is None:
158
+ return False, "Database not initialized — run fieldbrain_db_migrate before using FieldBrain tools."
159
+
160
+ if applied != SCHEMA_VERSION:
161
+ return (
162
+ False,
163
+ f"Schema version mismatch: database has {applied}, package expects {SCHEMA_VERSION}. "
164
+ "Run fieldbrain_db_migrate on one Sylo install, then restart others.",
165
+ )
166
+ return True, f"Schema version {applied} OK"
167
+
168
+
169
+ def check_pgvector(engine: Engine | None = None) -> tuple[bool, str | None]:
170
+ eng = engine or get_engine()
171
+ with eng.connect() as conn:
172
+ try:
173
+ row = conn.execute(
174
+ text("SELECT extversion FROM pg_extension WHERE extname = 'vector'")
175
+ ).fetchone()
176
+ if row:
177
+ return True, str(row[0])
178
+ except Exception as exc:
179
+ return False, str(exc)
180
+ return False, None
181
+
182
+
183
+ def connection_summary() -> dict[str, Any]:
184
+ url = build_database_url()
185
+ parsed = urllib.parse.urlparse(url)
186
+ config = load_database_config()
187
+ return {
188
+ "database_url_host": parsed.hostname or "localhost",
189
+ "database_url_port": parsed.port or 5432,
190
+ "database_url_name": (parsed.path or "/fieldbrain").lstrip("/"),
191
+ "database_url_user": parsed.username or "fieldbrain",
192
+ "config_file": str(config_path()),
193
+ "config_file_exists": config_path().is_file(),
194
+ "config_source": "env" if os.environ.get("SYLO_FIELDBRAIN_DATABASE_URL") or os.environ.get("SYLO_LOGICSCOUT_DATABASE_URL") else (
195
+ "file" if config else "default"
196
+ ),
197
+ "ollama_url": ollama_base_url(),
198
+ "expected_schema_version": SCHEMA_VERSION,
199
+ }
200
+
201
+
202
+ def run_health_check() -> dict[str, Any]:
203
+ summary = connection_summary()
204
+ try:
205
+ engine = get_engine()
206
+ with engine.connect() as conn:
207
+ conn.execute(text("SELECT 1"))
208
+ summary["postgres_connected"] = True
209
+ except Exception as exc:
210
+ summary["postgres_connected"] = False
211
+ summary["postgres_error"] = str(exc)
212
+ return summary
213
+
214
+ from services.pgvector_windows import detect_postgres_major, windows_install_status
215
+
216
+ summary["postgres_major"] = detect_postgres_major(engine)
217
+ install = windows_install_status(summary["postgres_major"])
218
+ summary.update(install)
219
+
220
+ pg_ok, pg_detail = check_pgvector(engine)
221
+ summary["pgvector_available"] = pg_ok
222
+ summary["pgvector_detail"] = pg_detail
223
+
224
+ schema_ok, schema_msg = verify_schema_version(engine)
225
+ summary["schema_ok"] = schema_ok
226
+ summary["schema_message"] = schema_msg
227
+ summary["applied_schema_version"] = read_applied_schema_version(engine)
228
+ return summary
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env python3
2
+ """Shared JSON stdout helpers for sylo-logicscout scripts."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import sys
8
+ from typing import Any
9
+
10
+
11
+ def emit(payload: dict[str, Any]) -> None:
12
+ """Print JSON to stdout and exit with code 0 or 1."""
13
+ print(json.dumps(payload, indent=2))
14
+ if payload.get("ok") is False:
15
+ sys.exit(1)
16
+
17
+
18
+ def emit_error(message: str, **extra: Any) -> None:
19
+ emit({"ok": False, "error": message, **extra})
@@ -0,0 +1,53 @@
1
+ """Alembic environment for sylo-logicscout."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from logging.config import fileConfig
7
+ from pathlib import Path
8
+
9
+ from alembic import context
10
+ from sqlalchemy import engine_from_config, pool
11
+
12
+ scripts_dir = Path(__file__).resolve().parent.parent
13
+ if str(scripts_dir) not in sys.path:
14
+ sys.path.insert(0, str(scripts_dir))
15
+
16
+ from _db_lib import build_database_url # noqa: E402
17
+
18
+ config = context.config
19
+ if config.config_file_name is not None:
20
+ fileConfig(config.config_file_name)
21
+
22
+ config.set_main_option("sqlalchemy.url", build_database_url())
23
+ target_metadata = None
24
+
25
+
26
+ def run_migrations_offline() -> None:
27
+ url = config.get_main_option("sqlalchemy.url")
28
+ context.configure(
29
+ url=url,
30
+ target_metadata=target_metadata,
31
+ literal_binds=True,
32
+ dialect_opts={"paramstyle": "named"},
33
+ )
34
+ with context.begin_transaction():
35
+ context.run_migrations()
36
+
37
+
38
+ def run_migrations_online() -> None:
39
+ connectable = engine_from_config(
40
+ config.get_section(config.config_ini_section, {}),
41
+ prefix="sqlalchemy.",
42
+ poolclass=pool.NullPool,
43
+ )
44
+ with connectable.connect() as connection:
45
+ context.configure(connection=connection, target_metadata=target_metadata)
46
+ with context.begin_transaction():
47
+ context.run_migrations()
48
+
49
+
50
+ if context.is_offline_mode():
51
+ run_migrations_offline()
52
+ else:
53
+ run_migrations_online()
@@ -0,0 +1,38 @@
1
+ """Baseline schema meta + pgvector extension attempt.
2
+
3
+ Revision ID: 001_baseline
4
+ Revises:
5
+ Create Date: 2026-07-04
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+
13
+ revision = "001_baseline"
14
+ down_revision = None
15
+ branch_labels = None
16
+ depends_on = None
17
+
18
+
19
+ def upgrade() -> None:
20
+ op.execute(
21
+ """
22
+ CREATE TABLE IF NOT EXISTS logicscout_schema_meta (
23
+ id SERIAL PRIMARY KEY,
24
+ version INTEGER NOT NULL,
25
+ applied_at TIMESTAMP NOT NULL DEFAULT NOW()
26
+ )
27
+ """
28
+ )
29
+ op.execute(
30
+ """
31
+ INSERT INTO logicscout_schema_meta (version)
32
+ SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM logicscout_schema_meta)
33
+ """
34
+ )
35
+
36
+
37
+ def downgrade() -> None:
38
+ op.execute("DROP TABLE IF EXISTS logicscout_schema_meta")