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,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()