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.
- package/LICENSE +21 -0
- package/README.md +32 -0
- package/extensions/fieldbrain-tools.ts +495 -0
- package/extensions/index.ts +12 -0
- package/extensions/python-runner.ts +56 -0
- package/package.json +44 -0
- package/scripts/__pycache__/_db_lib.cpython-312.pyc +0 -0
- package/scripts/__pycache__/_json_out.cpython-312.pyc +0 -0
- package/scripts/__pycache__/models.cpython-312.pyc +0 -0
- package/scripts/_db_lib.py +228 -0
- package/scripts/_json_out.py +19 -0
- package/scripts/alembic/env.py +53 -0
- package/scripts/alembic/versions/001_baseline.py +38 -0
- package/scripts/alembic/versions/002_core_tables.py +429 -0
- package/scripts/alembic/versions/003_durability_content_in_db.py +106 -0
- package/scripts/alembic/versions/004_pgvector_embedding.py +42 -0
- package/scripts/alembic/versions/005_project_job_number.py +37 -0
- package/scripts/alembic/versions/006_project_parent.py +62 -0
- package/scripts/alembic/versions/__init__.py +1 -0
- package/scripts/alembic.ini +38 -0
- package/scripts/db_auto_migrate.py +103 -0
- package/scripts/db_bootstrap.py +207 -0
- package/scripts/db_check.py +105 -0
- package/scripts/db_migrate.py +85 -0
- package/scripts/fieldbrain_brain_delete.py +49 -0
- package/scripts/fieldbrain_brain_read.py +61 -0
- package/scripts/fieldbrain_brain_restore.py +49 -0
- package/scripts/fieldbrain_brain_revisions.py +54 -0
- package/scripts/fieldbrain_brain_write.py +67 -0
- package/scripts/fieldbrain_document_attach.py +81 -0
- package/scripts/fieldbrain_document_catalog.py +128 -0
- package/scripts/fieldbrain_document_ingest.py +62 -0
- package/scripts/fieldbrain_document_list.py +67 -0
- package/scripts/fieldbrain_document_promote.py +43 -0
- package/scripts/fieldbrain_log_create.py +55 -0
- package/scripts/fieldbrain_log_delete.py +39 -0
- package/scripts/fieldbrain_log_restore.py +39 -0
- package/scripts/fieldbrain_log_revisions.py +39 -0
- package/scripts/fieldbrain_log_search.py +58 -0
- package/scripts/fieldbrain_log_update.py +52 -0
- package/scripts/fieldbrain_project_create.py +72 -0
- package/scripts/fieldbrain_project_list.py +53 -0
- package/scripts/fieldbrain_search.py +74 -0
- package/scripts/fieldbrain_ui_brain_list.py +60 -0
- package/scripts/fieldbrain_ui_project_create.py +95 -0
- package/scripts/fieldbrain_ui_project_list.py +49 -0
- package/scripts/models.py +355 -0
- package/scripts/pgvector_enable.py +165 -0
- package/scripts/pgvector_guide.py +44 -0
- package/scripts/pgvector_install_files.py +66 -0
- package/scripts/pgvector_install_from_folder.py +148 -0
- package/scripts/postbuild-ui.mjs +13 -0
- package/scripts/requirements.txt +7 -0
- package/scripts/services/__init__.py +1 -0
- package/scripts/services/__pycache__/__init__.cpython-312.pyc +0 -0
- package/scripts/services/__pycache__/brain_paths.cpython-312.pyc +0 -0
- package/scripts/services/__pycache__/document_formats.cpython-312.pyc +0 -0
- package/scripts/services/__pycache__/document_service.cpython-312.pyc +0 -0
- package/scripts/services/__pycache__/pgvector_windows.cpython-312.pyc +0 -0
- package/scripts/services/__pycache__/project_naming.cpython-312.pyc +0 -0
- package/scripts/services/__pycache__/project_service.cpython-312.pyc +0 -0
- package/scripts/services/brain_paths.py +81 -0
- package/scripts/services/brain_service.py +280 -0
- package/scripts/services/document_catalog.py +186 -0
- package/scripts/services/document_formats.py +116 -0
- package/scripts/services/document_ingest.py +254 -0
- package/scripts/services/document_service.py +316 -0
- package/scripts/services/embedding_service.py +72 -0
- package/scripts/services/global_brain_support.py +36 -0
- package/scripts/services/maintenance_log_service.py +258 -0
- package/scripts/services/migrate_lock.py +24 -0
- package/scripts/services/pgvector_windows.py +190 -0
- package/scripts/services/project_naming.py +72 -0
- package/scripts/services/project_service.py +368 -0
- package/scripts/services/search_service.py +250 -0
- package/scripts/status.py +39 -0
- package/shared/README.md +7 -0
- package/skills/fieldbrain/SKILL.md +197 -0
- package/skills/fieldbrain/SKILL.md.bak +83 -0
- package/skills/fieldbrain/routes/fieldbrain/assets/index-B56utPpP.css +1 -0
- package/skills/fieldbrain/routes/fieldbrain/assets/index-DO0AD0df.js +55 -0
- package/skills/fieldbrain/routes/fieldbrain/fallback.md +7 -0
- package/skills/fieldbrain/routes/fieldbrain/index.html +13 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[alembic]
|
|
2
|
+
script_location = alembic
|
|
3
|
+
prepend_sys_path = .
|
|
4
|
+
|
|
5
|
+
sqlalchemy.url = postgresql://logicscout:logicscout@localhost:5432/logicscout
|
|
6
|
+
|
|
7
|
+
[loggers]
|
|
8
|
+
keys = root,sqlalchemy,alembic
|
|
9
|
+
|
|
10
|
+
[handlers]
|
|
11
|
+
keys = console
|
|
12
|
+
|
|
13
|
+
[formatters]
|
|
14
|
+
keys = generic
|
|
15
|
+
|
|
16
|
+
[logger_root]
|
|
17
|
+
level = WARN
|
|
18
|
+
handlers = console
|
|
19
|
+
|
|
20
|
+
[logger_sqlalchemy]
|
|
21
|
+
level = WARN
|
|
22
|
+
handlers =
|
|
23
|
+
qualname = sqlalchemy.engine
|
|
24
|
+
|
|
25
|
+
[logger_alembic]
|
|
26
|
+
level = INFO
|
|
27
|
+
handlers =
|
|
28
|
+
qualname = alembic
|
|
29
|
+
|
|
30
|
+
[handler_console]
|
|
31
|
+
class = StreamHandler
|
|
32
|
+
args = (sys.stderr,)
|
|
33
|
+
level = NOTSET
|
|
34
|
+
formatter = generic
|
|
35
|
+
|
|
36
|
+
[formatter_generic]
|
|
37
|
+
format = %(levelname)-5.5s [%(name)s] %(message)s
|
|
38
|
+
datefmt = %H:%M:%S
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Apply Alembic migrations when the shared DB is behind this package (startup + Settings button)."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
|
|
8
|
+
from sqlalchemy import text
|
|
9
|
+
|
|
10
|
+
from _db_lib import SCHEMA_VERSION, get_engine, read_applied_schema_version, reset_engine
|
|
11
|
+
from _json_out import emit, emit_error
|
|
12
|
+
from db_migrate import apply_migrations
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main() -> None:
|
|
16
|
+
parser = argparse.ArgumentParser()
|
|
17
|
+
parser.add_argument(
|
|
18
|
+
"--dry-run",
|
|
19
|
+
action="store_true",
|
|
20
|
+
help="Report whether migration would run without changing the database",
|
|
21
|
+
)
|
|
22
|
+
args = parser.parse_args()
|
|
23
|
+
|
|
24
|
+
reset_engine()
|
|
25
|
+
try:
|
|
26
|
+
engine = get_engine()
|
|
27
|
+
with engine.connect() as conn:
|
|
28
|
+
conn.execute(text("SELECT 1"))
|
|
29
|
+
except Exception as exc:
|
|
30
|
+
emit(
|
|
31
|
+
{
|
|
32
|
+
"ok": True,
|
|
33
|
+
"migrated": False,
|
|
34
|
+
"skipped": True,
|
|
35
|
+
"reason": "not_connected",
|
|
36
|
+
"error": str(exc),
|
|
37
|
+
"operator_chat": f"Auto-migrate skipped: could not connect to Postgres ({exc}).",
|
|
38
|
+
}
|
|
39
|
+
)
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
applied = read_applied_schema_version()
|
|
44
|
+
except Exception as exc:
|
|
45
|
+
if args.dry_run:
|
|
46
|
+
emit_error(f"Could not read schema version: {exc}")
|
|
47
|
+
applied = None
|
|
48
|
+
|
|
49
|
+
if applied == SCHEMA_VERSION:
|
|
50
|
+
emit(
|
|
51
|
+
{
|
|
52
|
+
"ok": True,
|
|
53
|
+
"migrated": False,
|
|
54
|
+
"already_current": True,
|
|
55
|
+
"applied_schema_version": applied,
|
|
56
|
+
"expected_schema_version": SCHEMA_VERSION,
|
|
57
|
+
"operator_chat": f"Database already at schema version {applied}.",
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
if applied is not None and applied > SCHEMA_VERSION:
|
|
63
|
+
emit_error(
|
|
64
|
+
f"Database schema {applied} is newer than this Sylo build ({SCHEMA_VERSION}). "
|
|
65
|
+
"Update Sylo on this machine (git pull / reinstall).",
|
|
66
|
+
applied_schema_version=applied,
|
|
67
|
+
expected_schema_version=SCHEMA_VERSION,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
if args.dry_run:
|
|
71
|
+
emit(
|
|
72
|
+
{
|
|
73
|
+
"ok": True,
|
|
74
|
+
"migrated": False,
|
|
75
|
+
"would_migrate": True,
|
|
76
|
+
"applied_schema_version": applied,
|
|
77
|
+
"expected_schema_version": SCHEMA_VERSION,
|
|
78
|
+
"operator_chat": (
|
|
79
|
+
f"Database at version {applied}; package expects {SCHEMA_VERSION}. "
|
|
80
|
+
"Migration runs on Sylo startup or via Apply database updates in Settings."
|
|
81
|
+
),
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
new_version = apply_migrations()
|
|
88
|
+
except Exception as exc:
|
|
89
|
+
emit_error(f"Migration failed: {exc}")
|
|
90
|
+
|
|
91
|
+
emit(
|
|
92
|
+
{
|
|
93
|
+
"ok": True,
|
|
94
|
+
"migrated": True,
|
|
95
|
+
"applied_schema_version": new_version,
|
|
96
|
+
"expected_schema_version": SCHEMA_VERSION,
|
|
97
|
+
"operator_chat": f"FieldBrain database updated to schema version {new_version}.",
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
if __name__ == "__main__":
|
|
103
|
+
main()
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""One-time Postgres bootstrap: create FieldBrain role + database, pgvector, migrate."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import os
|
|
8
|
+
import urllib.parse
|
|
9
|
+
|
|
10
|
+
from sqlalchemy import create_engine, text
|
|
11
|
+
|
|
12
|
+
from _db_lib import SCHEMA_VERSION, read_applied_schema_version, reset_engine
|
|
13
|
+
from _json_out import emit, emit_error
|
|
14
|
+
from db_migrate import alembic_config
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _quote_ident(name: str) -> str:
|
|
18
|
+
return '"' + name.replace('"', '""') + '"'
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _quote_literal(value: str) -> str:
|
|
22
|
+
return "'" + value.replace("'", "''") + "'"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _build_url(host: str, port: int, database: str, username: str, password: str) -> str:
|
|
26
|
+
safe_password = urllib.parse.quote_plus(password) if password else ""
|
|
27
|
+
auth = f"{urllib.parse.quote_plus(username)}:{safe_password}@" if safe_password else f"{urllib.parse.quote_plus(username)}@"
|
|
28
|
+
return f"postgresql://{auth}{host}:{port}/{database}"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _role_exists(conn, role: str) -> bool:
|
|
32
|
+
row = conn.execute(
|
|
33
|
+
text("SELECT 1 FROM pg_roles WHERE rolname = :name"),
|
|
34
|
+
{"name": role},
|
|
35
|
+
).fetchone()
|
|
36
|
+
return row is not None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _database_exists(conn, database: str) -> bool:
|
|
40
|
+
row = conn.execute(
|
|
41
|
+
text("SELECT 1 FROM pg_database WHERE datname = :name"),
|
|
42
|
+
{"name": database},
|
|
43
|
+
).fetchone()
|
|
44
|
+
return row is not None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _try_pgvector(app_url: str) -> tuple[bool, str | None]:
|
|
48
|
+
engine = create_engine(app_url, isolation_level="AUTOCOMMIT", pool_pre_ping=True)
|
|
49
|
+
try:
|
|
50
|
+
with engine.connect() as conn:
|
|
51
|
+
conn.execute(text("CREATE EXTENSION IF NOT EXISTS vector"))
|
|
52
|
+
with engine.connect() as conn:
|
|
53
|
+
row = conn.execute(
|
|
54
|
+
text("SELECT extversion FROM pg_extension WHERE extname = 'vector'")
|
|
55
|
+
).fetchone()
|
|
56
|
+
if row:
|
|
57
|
+
return True, str(row[0])
|
|
58
|
+
return False, "CREATE EXTENSION succeeded but vector not visible"
|
|
59
|
+
except Exception as exc:
|
|
60
|
+
return False, str(exc)
|
|
61
|
+
finally:
|
|
62
|
+
engine.dispose()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _run_migrate(app_url: str) -> tuple[int | None, str | None]:
|
|
66
|
+
from alembic import command
|
|
67
|
+
|
|
68
|
+
os.environ["SYLO_FIELDBRAIN_DATABASE_URL"] = app_url
|
|
69
|
+
reset_engine()
|
|
70
|
+
try:
|
|
71
|
+
command.upgrade(alembic_config(), "head")
|
|
72
|
+
except Exception as exc:
|
|
73
|
+
return None, str(exc)
|
|
74
|
+
reset_engine()
|
|
75
|
+
try:
|
|
76
|
+
applied = read_applied_schema_version()
|
|
77
|
+
except Exception as exc:
|
|
78
|
+
return None, f"Migration ran but schema version unreadable: {exc}"
|
|
79
|
+
if applied != SCHEMA_VERSION:
|
|
80
|
+
return applied, f"Expected schema {SCHEMA_VERSION}, got {applied}"
|
|
81
|
+
return applied, None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def main() -> None:
|
|
85
|
+
parser = argparse.ArgumentParser()
|
|
86
|
+
parser.add_argument("--admin-user", default="postgres")
|
|
87
|
+
parser.add_argument("--admin-password", default="")
|
|
88
|
+
parser.add_argument("--host", default="localhost")
|
|
89
|
+
parser.add_argument("--port", type=int, default=5432)
|
|
90
|
+
parser.add_argument("--app-database", default="fieldbrain")
|
|
91
|
+
parser.add_argument("--app-username", default="fieldbrain")
|
|
92
|
+
parser.add_argument("--app-password", default="fieldbrain")
|
|
93
|
+
parser.add_argument("--skip-migrate", action="store_true")
|
|
94
|
+
args = parser.parse_args()
|
|
95
|
+
|
|
96
|
+
admin_password = (args.admin_password or os.environ.get("SYLO_FIELDBRAIN_BOOTSTRAP_ADMIN_PASSWORD") or "").strip()
|
|
97
|
+
if not admin_password:
|
|
98
|
+
emit_error("Postgres superuser password is required for bootstrap (not stored after this run).")
|
|
99
|
+
|
|
100
|
+
host = args.host.strip() or "localhost"
|
|
101
|
+
port = int(args.port)
|
|
102
|
+
app_db = args.app_database.strip() or "fieldbrain"
|
|
103
|
+
app_user = args.app_username.strip() or "fieldbrain"
|
|
104
|
+
app_password = args.app_password if args.app_password is not None else "fieldbrain"
|
|
105
|
+
|
|
106
|
+
admin_user = args.admin_user.strip() or "postgres"
|
|
107
|
+
admin_url = _build_url(host, port, "postgres", admin_user, admin_password)
|
|
108
|
+
steps: list[str] = []
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
admin_engine = create_engine(admin_url, isolation_level="AUTOCOMMIT", pool_pre_ping=True)
|
|
112
|
+
with admin_engine.connect() as conn:
|
|
113
|
+
conn.execute(text("SELECT 1"))
|
|
114
|
+
steps.append(f"Connected as superuser {admin_user}@{host}:{port}")
|
|
115
|
+
except Exception as exc:
|
|
116
|
+
emit_error(
|
|
117
|
+
f"Could not connect as Postgres superuser: {exc}",
|
|
118
|
+
hint="Use the password you set at PostgreSQL install (or reset via pg_hba.conf trust).",
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
with admin_engine.connect() as conn:
|
|
123
|
+
if _role_exists(conn, app_user):
|
|
124
|
+
conn.execute(
|
|
125
|
+
text(
|
|
126
|
+
f"ALTER USER {_quote_ident(app_user)} "
|
|
127
|
+
f"WITH PASSWORD {_quote_literal(app_password)}"
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
steps.append(f"Role {app_user} already existed — password updated")
|
|
131
|
+
else:
|
|
132
|
+
conn.execute(
|
|
133
|
+
text(
|
|
134
|
+
f"CREATE USER {_quote_ident(app_user)} "
|
|
135
|
+
f"WITH PASSWORD {_quote_literal(app_password)}"
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
steps.append(f"Created role {app_user}")
|
|
139
|
+
|
|
140
|
+
if _database_exists(conn, app_db):
|
|
141
|
+
steps.append(f"Database {app_db} already existed — skipped CREATE DATABASE")
|
|
142
|
+
else:
|
|
143
|
+
conn.execute(
|
|
144
|
+
text(
|
|
145
|
+
f"CREATE DATABASE {_quote_ident(app_db)} "
|
|
146
|
+
f"OWNER {_quote_ident(app_user)}"
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
steps.append(f"Created database {app_db}")
|
|
150
|
+
except Exception as exc:
|
|
151
|
+
admin_engine.dispose()
|
|
152
|
+
emit_error(f"Bootstrap DDL failed: {exc}", steps=steps)
|
|
153
|
+
admin_engine.dispose()
|
|
154
|
+
|
|
155
|
+
app_url = _build_url(host, port, app_db, app_user, app_password)
|
|
156
|
+
pgvector_ok, pgvector_detail = _try_pgvector(app_url)
|
|
157
|
+
if pgvector_ok:
|
|
158
|
+
steps.append(f"pgvector enabled ({pgvector_detail})")
|
|
159
|
+
else:
|
|
160
|
+
steps.append(
|
|
161
|
+
"pgvector not installed — semantic search unavailable until you install pgvector "
|
|
162
|
+
f"and run CREATE EXTENSION vector ({pgvector_detail})"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
applied_version: int | None = None
|
|
166
|
+
if not args.skip_migrate:
|
|
167
|
+
applied_version, migrate_err = _run_migrate(app_url)
|
|
168
|
+
if migrate_err:
|
|
169
|
+
emit_error(
|
|
170
|
+
migrate_err,
|
|
171
|
+
steps=steps,
|
|
172
|
+
pgvector_ok=pgvector_ok,
|
|
173
|
+
app_database=app_db,
|
|
174
|
+
app_username=app_user,
|
|
175
|
+
)
|
|
176
|
+
steps.append(f"Migrated to schema version {applied_version}")
|
|
177
|
+
|
|
178
|
+
lines = [
|
|
179
|
+
f"FieldBrain database ready: {app_user}@{host}:{port}/{app_db}",
|
|
180
|
+
*steps,
|
|
181
|
+
"Superuser password was not saved. Runtime uses the FieldBrain user only.",
|
|
182
|
+
]
|
|
183
|
+
if not pgvector_ok:
|
|
184
|
+
lines.append(
|
|
185
|
+
"Optional semantic search: download pgvector, browse to the folder in FieldBrain Settings, "
|
|
186
|
+
"then Install pgvector & enable semantic search."
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
emit(
|
|
190
|
+
{
|
|
191
|
+
"ok": True,
|
|
192
|
+
"operator_chat": "\n".join(lines),
|
|
193
|
+
"steps": steps,
|
|
194
|
+
"host": host,
|
|
195
|
+
"port": port,
|
|
196
|
+
"app_database": app_db,
|
|
197
|
+
"app_username": app_user,
|
|
198
|
+
"pgvector_ok": pgvector_ok,
|
|
199
|
+
"pgvector_detail": pgvector_detail,
|
|
200
|
+
"applied_schema_version": applied_version,
|
|
201
|
+
"expected_schema_version": SCHEMA_VERSION,
|
|
202
|
+
}
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
if __name__ == "__main__":
|
|
207
|
+
main()
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Postgres + pgvector + schema version check for fieldbrain_db_check tool."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
|
|
8
|
+
from _db_lib import SCHEMA_VERSION, connection_summary, run_health_check
|
|
9
|
+
from _json_out import emit, emit_error
|
|
10
|
+
from services.pgvector_windows import layman_pgvector_steps
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main() -> None:
|
|
14
|
+
parser = argparse.ArgumentParser()
|
|
15
|
+
parser.add_argument(
|
|
16
|
+
"--guided",
|
|
17
|
+
action="store_true",
|
|
18
|
+
help="Include guided setup steps when checks fail",
|
|
19
|
+
)
|
|
20
|
+
args = parser.parse_args()
|
|
21
|
+
|
|
22
|
+
summary = run_health_check()
|
|
23
|
+
connected = summary.get("postgres_connected") is True
|
|
24
|
+
schema_ok = summary.get("schema_ok") is True
|
|
25
|
+
pgvector_ok = summary.get("pgvector_available") is True
|
|
26
|
+
|
|
27
|
+
if not connected:
|
|
28
|
+
payload = {
|
|
29
|
+
"ok": False,
|
|
30
|
+
"error": summary.get("postgres_error", "Could not connect to PostgreSQL"),
|
|
31
|
+
**summary,
|
|
32
|
+
}
|
|
33
|
+
if args.guided:
|
|
34
|
+
payload["guided_setup"] = _guided_postgres_steps()
|
|
35
|
+
emit(payload)
|
|
36
|
+
|
|
37
|
+
lines = [
|
|
38
|
+
f"PostgreSQL: connected ({summary.get('database_url_host')}:{summary.get('database_url_port')}/{summary.get('database_url_name')})",
|
|
39
|
+
f"Config: {summary.get('config_source')} ({summary.get('config_file')})",
|
|
40
|
+
f"Schema: {summary.get('schema_message')}",
|
|
41
|
+
f"pgvector: {'enabled' if pgvector_ok else 'missing — semantic search unavailable until installed'}",
|
|
42
|
+
f"Ollama endpoint: {summary.get('ollama_url')}",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
if not schema_ok:
|
|
46
|
+
applied = summary.get("applied_schema_version")
|
|
47
|
+
expected = summary.get("expected_schema_version", SCHEMA_VERSION)
|
|
48
|
+
needs_migration = (
|
|
49
|
+
summary.get("postgres_connected") is True
|
|
50
|
+
and isinstance(applied, int)
|
|
51
|
+
and applied < expected
|
|
52
|
+
) or (
|
|
53
|
+
summary.get("postgres_connected") is True and applied is None
|
|
54
|
+
)
|
|
55
|
+
payload = {
|
|
56
|
+
"ok": False,
|
|
57
|
+
"error": summary.get("schema_message"),
|
|
58
|
+
"operator_chat": "\n".join(lines),
|
|
59
|
+
"needs_migration": needs_migration,
|
|
60
|
+
**summary,
|
|
61
|
+
}
|
|
62
|
+
if args.guided:
|
|
63
|
+
payload["guided_setup"] = _guided_migrate_steps()
|
|
64
|
+
emit(payload)
|
|
65
|
+
|
|
66
|
+
payload: dict = {
|
|
67
|
+
"ok": True,
|
|
68
|
+
"operator_chat": "\n".join(lines),
|
|
69
|
+
**summary,
|
|
70
|
+
"expected_schema_version": SCHEMA_VERSION,
|
|
71
|
+
}
|
|
72
|
+
if not pgvector_ok:
|
|
73
|
+
major = summary.get("postgres_major")
|
|
74
|
+
payload["pgvector_setup"] = layman_pgvector_steps(
|
|
75
|
+
major if isinstance(major, int) else None,
|
|
76
|
+
files_installed=bool(summary.get("vector_files_installed")),
|
|
77
|
+
db_mode="remote"
|
|
78
|
+
if str(summary.get("database_url_host") or "") not in ("localhost", "127.0.0.1")
|
|
79
|
+
else "local",
|
|
80
|
+
)
|
|
81
|
+
emit(payload)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _guided_postgres_steps() -> list[str]:
|
|
85
|
+
return [
|
|
86
|
+
"Install PostgreSQL 13+ as a Windows service — download from https://www.postgresql.org/download/windows/",
|
|
87
|
+
"Optional — semantic search: download pgvector for your Postgres major version from "
|
|
88
|
+
"https://github.com/andreiramani/pgvector_pgsql_windows/releases and extract the zip.",
|
|
89
|
+
"FieldBrain Settings → Create database & migrate (button; superuser password not saved).",
|
|
90
|
+
"Optional: Browse to the pgvector folder or zip → Install pgvector & enable semantic search.",
|
|
91
|
+
"Test connection. Shop laptops: set dbMode remote + server IP.",
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _guided_migrate_steps() -> list[str]:
|
|
96
|
+
return [
|
|
97
|
+
"Sylo auto-applies database updates on startup when FieldBrain is enabled (one machine is enough for a shared DB).",
|
|
98
|
+
"Or FieldBrain Settings → Apply database updates (next to Test connection).",
|
|
99
|
+
"Or chat: fieldbrain_db_migrate.",
|
|
100
|
+
"After migrate succeeds, restart other Sylo installs (they pick up the new schema automatically).",
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
if __name__ == "__main__":
|
|
105
|
+
main()
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Apply Alembic migrations (fieldbrain_db_migrate tool)."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from alembic import command
|
|
10
|
+
from alembic.config import Config
|
|
11
|
+
|
|
12
|
+
from _db_lib import SCHEMA_VERSION, build_database_url, read_applied_schema_version, reset_engine
|
|
13
|
+
from _json_out import emit, emit_error
|
|
14
|
+
from services.migrate_lock import migration_advisory_lock
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def alembic_config() -> Config:
|
|
18
|
+
scripts_dir = Path(__file__).resolve().parent
|
|
19
|
+
cfg = Config(str(scripts_dir / "alembic.ini"))
|
|
20
|
+
cfg.set_main_option("script_location", str(scripts_dir / "alembic"))
|
|
21
|
+
cfg.set_main_option("sqlalchemy.url", build_database_url())
|
|
22
|
+
return cfg
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def apply_migrations() -> int:
|
|
26
|
+
"""Upgrade to head under advisory lock; return applied schema version."""
|
|
27
|
+
reset_engine()
|
|
28
|
+
from _db_lib import get_engine
|
|
29
|
+
|
|
30
|
+
engine = get_engine()
|
|
31
|
+
with migration_advisory_lock(engine):
|
|
32
|
+
command.upgrade(alembic_config(), "head")
|
|
33
|
+
reset_engine()
|
|
34
|
+
applied = read_applied_schema_version()
|
|
35
|
+
if applied != SCHEMA_VERSION:
|
|
36
|
+
raise RuntimeError(
|
|
37
|
+
f"Migration finished but schema version is {applied}, expected {SCHEMA_VERSION}."
|
|
38
|
+
)
|
|
39
|
+
return int(applied)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def main() -> None:
|
|
43
|
+
parser = argparse.ArgumentParser()
|
|
44
|
+
parser.add_argument(
|
|
45
|
+
"--check-only",
|
|
46
|
+
action="store_true",
|
|
47
|
+
help="Report current schema version without migrating",
|
|
48
|
+
)
|
|
49
|
+
args = parser.parse_args()
|
|
50
|
+
|
|
51
|
+
reset_engine()
|
|
52
|
+
url = build_database_url()
|
|
53
|
+
|
|
54
|
+
if args.check_only:
|
|
55
|
+
try:
|
|
56
|
+
applied = read_applied_schema_version()
|
|
57
|
+
except Exception as exc:
|
|
58
|
+
emit_error(f"Could not read schema version: {exc}", database_url_host=url)
|
|
59
|
+
emit(
|
|
60
|
+
{
|
|
61
|
+
"ok": True,
|
|
62
|
+
"applied_schema_version": applied,
|
|
63
|
+
"expected_schema_version": SCHEMA_VERSION,
|
|
64
|
+
"operator_chat": f"Applied schema version: {applied}; package expects {SCHEMA_VERSION}.",
|
|
65
|
+
}
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
applied = apply_migrations()
|
|
70
|
+
except Exception as exc:
|
|
71
|
+
emit_error(f"Migration failed: {exc}")
|
|
72
|
+
|
|
73
|
+
emit(
|
|
74
|
+
{
|
|
75
|
+
"ok": True,
|
|
76
|
+
"migrated": True,
|
|
77
|
+
"applied_schema_version": applied,
|
|
78
|
+
"expected_schema_version": SCHEMA_VERSION,
|
|
79
|
+
"operator_chat": f"FieldBrain database migrated to schema version {applied}.",
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
if __name__ == "__main__":
|
|
85
|
+
main()
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Soft-delete a brain document (logicscout_brain_delete tool)."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
|
|
8
|
+
from _db_lib import get_engine, verify_schema_version
|
|
9
|
+
from _json_out import emit, emit_error
|
|
10
|
+
from services.brain_service import soft_delete_brain
|
|
11
|
+
from sqlalchemy.orm import Session
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main() -> None:
|
|
15
|
+
parser = argparse.ArgumentParser()
|
|
16
|
+
parser.add_argument("--path", required=True)
|
|
17
|
+
parser.add_argument("--scope", choices=("global", "project"), default="project")
|
|
18
|
+
parser.add_argument("--project-id", type=int, default=None)
|
|
19
|
+
args = parser.parse_args()
|
|
20
|
+
|
|
21
|
+
ok, msg = verify_schema_version()
|
|
22
|
+
if not ok:
|
|
23
|
+
emit_error(msg)
|
|
24
|
+
|
|
25
|
+
if args.scope == "project" and args.project_id is None:
|
|
26
|
+
emit_error("--project-id is required when scope is project")
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
with Session(get_engine()) as session:
|
|
30
|
+
result = soft_delete_brain(
|
|
31
|
+
session,
|
|
32
|
+
scope=args.scope,
|
|
33
|
+
relative_path=args.path,
|
|
34
|
+
project_id=args.project_id,
|
|
35
|
+
)
|
|
36
|
+
except ValueError as exc:
|
|
37
|
+
emit_error(str(exc))
|
|
38
|
+
|
|
39
|
+
emit(
|
|
40
|
+
{
|
|
41
|
+
"ok": True,
|
|
42
|
+
**result,
|
|
43
|
+
"operator_chat": f"Soft-deleted brain {args.scope}:{args.path} (revision saved).",
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
if __name__ == "__main__":
|
|
49
|
+
main()
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Read brain markdown (logicscout_brain_read tool)."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
|
|
8
|
+
from _db_lib import get_engine, verify_schema_version
|
|
9
|
+
from _json_out import emit, emit_error
|
|
10
|
+
from services.brain_service import read_brain_markdown
|
|
11
|
+
from sqlalchemy.orm import Session
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main() -> None:
|
|
15
|
+
parser = argparse.ArgumentParser()
|
|
16
|
+
parser.add_argument("--path", required=True, help="Relative brain path, e.g. gotchas/conveyor_jam.md")
|
|
17
|
+
parser.add_argument(
|
|
18
|
+
"--scope",
|
|
19
|
+
choices=("global", "project"),
|
|
20
|
+
default="project",
|
|
21
|
+
help="global org brain or project brain",
|
|
22
|
+
)
|
|
23
|
+
parser.add_argument("--project-id", type=int, default=None, help="Required when scope=project")
|
|
24
|
+
args = parser.parse_args()
|
|
25
|
+
|
|
26
|
+
ok, msg = verify_schema_version()
|
|
27
|
+
if not ok:
|
|
28
|
+
emit_error(msg)
|
|
29
|
+
|
|
30
|
+
if args.scope == "project" and args.project_id is None:
|
|
31
|
+
emit_error("--project-id is required when scope is project")
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
with Session(get_engine()) as session:
|
|
35
|
+
result = read_brain_markdown(
|
|
36
|
+
session,
|
|
37
|
+
scope=args.scope,
|
|
38
|
+
relative_path=args.path,
|
|
39
|
+
project_id=args.project_id,
|
|
40
|
+
)
|
|
41
|
+
except ValueError as exc:
|
|
42
|
+
emit_error(str(exc))
|
|
43
|
+
|
|
44
|
+
preview = result["content"]
|
|
45
|
+
if len(preview) > 1200:
|
|
46
|
+
preview = preview[:1200] + "\n… (truncated in chat; full content in JSON)"
|
|
47
|
+
|
|
48
|
+
emit(
|
|
49
|
+
{
|
|
50
|
+
"ok": True,
|
|
51
|
+
**result,
|
|
52
|
+
"operator_chat": (
|
|
53
|
+
f"Brain {result['scope']}:{result['relative_path']} "
|
|
54
|
+
f"({result['size_bytes']} bytes)\n\n{preview}"
|
|
55
|
+
),
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
if __name__ == "__main__":
|
|
61
|
+
main()
|