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,39 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""List revision history for a maintenance log entry (logicscout_log_revisions 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.maintenance_log_service import list_log_revisions
|
|
11
|
+
from sqlalchemy.orm import Session
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main() -> None:
|
|
15
|
+
parser = argparse.ArgumentParser()
|
|
16
|
+
parser.add_argument("--entry-id", type=int, required=True)
|
|
17
|
+
parser.add_argument("--limit", type=int, default=20)
|
|
18
|
+
args = parser.parse_args()
|
|
19
|
+
|
|
20
|
+
ok, msg = verify_schema_version()
|
|
21
|
+
if not ok:
|
|
22
|
+
emit_error(msg)
|
|
23
|
+
|
|
24
|
+
with Session(get_engine()) as session:
|
|
25
|
+
revisions = list_log_revisions(session, args.entry_id, limit=args.limit)
|
|
26
|
+
|
|
27
|
+
emit(
|
|
28
|
+
{
|
|
29
|
+
"ok": True,
|
|
30
|
+
"entry_id": args.entry_id,
|
|
31
|
+
"revisions": revisions,
|
|
32
|
+
"count": len(revisions),
|
|
33
|
+
"operator_chat": f"{len(revisions)} revision(s) for log entry {args.entry_id}.",
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
if __name__ == "__main__":
|
|
39
|
+
main()
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Search maintenance log entries (logicscout_log_search 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.maintenance_log_service import search_log_entries
|
|
11
|
+
from sqlalchemy.orm import Session
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main() -> None:
|
|
15
|
+
parser = argparse.ArgumentParser()
|
|
16
|
+
parser.add_argument("--query", default="", help="Full-text search (title, body, fault, tag)")
|
|
17
|
+
parser.add_argument("--project-id", type=int, default=None, help="Filter by project id")
|
|
18
|
+
parser.add_argument("--fault-code", default="", help="Filter by fault code (substring)")
|
|
19
|
+
parser.add_argument("--equipment-tag", default="", help="Filter by equipment tag (substring)")
|
|
20
|
+
parser.add_argument("--limit", type=int, default=25, help="Max results (default 25)")
|
|
21
|
+
args = parser.parse_args()
|
|
22
|
+
|
|
23
|
+
ok, msg = verify_schema_version()
|
|
24
|
+
if not ok:
|
|
25
|
+
emit_error(msg)
|
|
26
|
+
|
|
27
|
+
with Session(get_engine()) as session:
|
|
28
|
+
entries = search_log_entries(
|
|
29
|
+
session,
|
|
30
|
+
query=args.query,
|
|
31
|
+
project_id=args.project_id,
|
|
32
|
+
fault_code=args.fault_code or None,
|
|
33
|
+
equipment_tag=args.equipment_tag or None,
|
|
34
|
+
limit=args.limit,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
lines = []
|
|
38
|
+
for e in entries:
|
|
39
|
+
proj = f" [{e.get('project_name')}]" if e.get("project_name") else ""
|
|
40
|
+
fault = f" fault={e['fault_code']}" if e.get("fault_code") else ""
|
|
41
|
+
tag = f" tag={e['equipment_tag']}" if e.get("equipment_tag") else ""
|
|
42
|
+
lines.append(f"#{e['id']} {e['title']}{proj}{fault}{tag}")
|
|
43
|
+
|
|
44
|
+
emit(
|
|
45
|
+
{
|
|
46
|
+
"ok": True,
|
|
47
|
+
"count": len(entries),
|
|
48
|
+
"entries": entries,
|
|
49
|
+
"operator_chat": (
|
|
50
|
+
f"Found {len(entries)} log entr{'y' if len(entries) == 1 else 'ies'}.\n"
|
|
51
|
+
+ ("\n".join(lines) if lines else "(none)")
|
|
52
|
+
),
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if __name__ == "__main__":
|
|
58
|
+
main()
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Update a maintenance log entry with revision history (logicscout_log_update 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.maintenance_log_service import update_log_entry
|
|
11
|
+
from sqlalchemy.orm import Session
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main() -> None:
|
|
15
|
+
parser = argparse.ArgumentParser()
|
|
16
|
+
parser.add_argument("--entry-id", type=int, required=True)
|
|
17
|
+
parser.add_argument("--title", default=None)
|
|
18
|
+
parser.add_argument("--body", default=None)
|
|
19
|
+
parser.add_argument("--fault-code", default=None)
|
|
20
|
+
parser.add_argument("--equipment-tag", default=None)
|
|
21
|
+
parser.add_argument("--logged-by", default=None)
|
|
22
|
+
args = parser.parse_args()
|
|
23
|
+
|
|
24
|
+
ok, msg = verify_schema_version()
|
|
25
|
+
if not ok:
|
|
26
|
+
emit_error(msg)
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
with Session(get_engine()) as session:
|
|
30
|
+
result = update_log_entry(
|
|
31
|
+
session,
|
|
32
|
+
args.entry_id,
|
|
33
|
+
title=args.title,
|
|
34
|
+
body=args.body,
|
|
35
|
+
fault_code=args.fault_code,
|
|
36
|
+
equipment_tag=args.equipment_tag,
|
|
37
|
+
logged_by=args.logged_by,
|
|
38
|
+
)
|
|
39
|
+
except ValueError as exc:
|
|
40
|
+
emit_error(str(exc))
|
|
41
|
+
|
|
42
|
+
emit(
|
|
43
|
+
{
|
|
44
|
+
"ok": True,
|
|
45
|
+
"entry": result,
|
|
46
|
+
"operator_chat": f"Updated maintenance log entry {args.entry_id} (previous version saved to revisions).",
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
if __name__ == "__main__":
|
|
52
|
+
main()
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Create a LogicScout project (logicscout_project_create tool)."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
from _db_lib import get_engine, verify_schema_version
|
|
10
|
+
from _json_out import emit, emit_error
|
|
11
|
+
from services.brain_paths import ensure_project_brain_layout
|
|
12
|
+
from services.project_service import create_project
|
|
13
|
+
from sqlalchemy.orm import Session
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main() -> None:
|
|
17
|
+
parser = argparse.ArgumentParser()
|
|
18
|
+
parser.add_argument("--name", required=True, help="12345 (job) or 12345-001 (sub-project)")
|
|
19
|
+
parser.add_argument("--org-id", default="default", help="Org id (default: default)")
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
"--tags-json",
|
|
22
|
+
default="",
|
|
23
|
+
help='JSON array of tag strings, e.g. ["line3","packaging"]',
|
|
24
|
+
)
|
|
25
|
+
args = parser.parse_args()
|
|
26
|
+
|
|
27
|
+
ok, msg = verify_schema_version()
|
|
28
|
+
if not ok:
|
|
29
|
+
emit_error(msg)
|
|
30
|
+
|
|
31
|
+
tags: list[str] | None = None
|
|
32
|
+
if args.tags_json.strip():
|
|
33
|
+
try:
|
|
34
|
+
parsed = json.loads(args.tags_json)
|
|
35
|
+
if isinstance(parsed, list):
|
|
36
|
+
tags = [str(x) for x in parsed]
|
|
37
|
+
else:
|
|
38
|
+
emit_error("--tags-json must be a JSON array")
|
|
39
|
+
except json.JSONDecodeError as exc:
|
|
40
|
+
emit_error(f"Invalid --tags-json: {exc}")
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
with Session(get_engine()) as session:
|
|
44
|
+
project = create_project(
|
|
45
|
+
session,
|
|
46
|
+
name=args.name,
|
|
47
|
+
project_tags=tags,
|
|
48
|
+
org_id=args.org_id,
|
|
49
|
+
)
|
|
50
|
+
ensure_project_brain_layout(project["id"])
|
|
51
|
+
if project.get("parent_project_id"):
|
|
52
|
+
ensure_project_brain_layout(int(project["parent_project_id"]))
|
|
53
|
+
except ValueError as exc:
|
|
54
|
+
emit_error(str(exc))
|
|
55
|
+
|
|
56
|
+
level = project.get("level", "other")
|
|
57
|
+
if level == "sub_project":
|
|
58
|
+
chat = f"Created sub-project {project['id']}: {project['name']} (under project {project.get('job_number')})."
|
|
59
|
+
else:
|
|
60
|
+
chat = f"Created project {project['id']}: {project['name']}."
|
|
61
|
+
|
|
62
|
+
emit(
|
|
63
|
+
{
|
|
64
|
+
"ok": True,
|
|
65
|
+
"project": project,
|
|
66
|
+
"operator_chat": chat,
|
|
67
|
+
}
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
if __name__ == "__main__":
|
|
72
|
+
main()
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""List LogicScout projects (logicscout_project_list 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.project_service import list_projects, list_projects_with_stats
|
|
11
|
+
from sqlalchemy.orm import Session
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main() -> None:
|
|
15
|
+
parser = argparse.ArgumentParser()
|
|
16
|
+
parser.add_argument("--include-hidden", action="store_true", help="Include system-hidden projects")
|
|
17
|
+
parser.add_argument("--with-stats", action="store_true", help="Include brain/doc/log counts per project")
|
|
18
|
+
args = parser.parse_args()
|
|
19
|
+
|
|
20
|
+
ok, msg = verify_schema_version()
|
|
21
|
+
if not ok:
|
|
22
|
+
emit_error(msg)
|
|
23
|
+
|
|
24
|
+
with Session(get_engine()) as session:
|
|
25
|
+
if args.with_stats:
|
|
26
|
+
projects = list_projects_with_stats(session, include_hidden=args.include_hidden)
|
|
27
|
+
else:
|
|
28
|
+
projects = list_projects(session, include_hidden=args.include_hidden)
|
|
29
|
+
|
|
30
|
+
lines = []
|
|
31
|
+
for p in projects:
|
|
32
|
+
if args.with_stats and p.get("stats"):
|
|
33
|
+
s = p["stats"]
|
|
34
|
+
lines.append(
|
|
35
|
+
f"{p['id']}: {p['name']} — {s.get('brain_entries', 0)} brain, "
|
|
36
|
+
f"{s.get('library_docs', 0)} docs, {s.get('maintenance_logs', 0)} logs"
|
|
37
|
+
)
|
|
38
|
+
else:
|
|
39
|
+
lines.append(f"{p['id']}: {p['name']}")
|
|
40
|
+
emit(
|
|
41
|
+
{
|
|
42
|
+
"ok": True,
|
|
43
|
+
"count": len(projects),
|
|
44
|
+
"projects": projects,
|
|
45
|
+
"operator_chat": (
|
|
46
|
+
f"Found {len(projects)} project(s).\n" + ("\n".join(lines) if lines else "(none)")
|
|
47
|
+
),
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
if __name__ == "__main__":
|
|
53
|
+
main()
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Hybrid search across project knowledge (logicscout_search 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.search_service import hybrid_search
|
|
11
|
+
from sqlalchemy.orm import Session
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main() -> None:
|
|
15
|
+
parser = argparse.ArgumentParser()
|
|
16
|
+
parser.add_argument(
|
|
17
|
+
"--project-id",
|
|
18
|
+
type=int,
|
|
19
|
+
default=None,
|
|
20
|
+
help="Project to search (project + global). Omit for global-only search.",
|
|
21
|
+
)
|
|
22
|
+
parser.add_argument("--query", required=True, help="Search query text")
|
|
23
|
+
parser.add_argument("--limit", type=int, default=25, help="Max results (default 25)")
|
|
24
|
+
parser.add_argument(
|
|
25
|
+
"--source-type",
|
|
26
|
+
default="",
|
|
27
|
+
help="Optional filter: document, brain, tag, rung, etc.",
|
|
28
|
+
)
|
|
29
|
+
args = parser.parse_args()
|
|
30
|
+
|
|
31
|
+
ok, msg = verify_schema_version()
|
|
32
|
+
if not ok:
|
|
33
|
+
emit_error(msg)
|
|
34
|
+
|
|
35
|
+
with Session(get_engine()) as session:
|
|
36
|
+
results = hybrid_search(
|
|
37
|
+
session,
|
|
38
|
+
args.project_id,
|
|
39
|
+
args.query,
|
|
40
|
+
limit=args.limit,
|
|
41
|
+
source_type=args.source_type or None,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
lines = []
|
|
45
|
+
for r in results:
|
|
46
|
+
scope_tag = "global" if r.get("scope") == "global" else f"project {r.get('project_id')}"
|
|
47
|
+
lines.append(
|
|
48
|
+
f"[{r['score']}] ({scope_tag}) #{r['id']} {r['type']} {r['label']}: {r['snippet'][:160]}"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
where = (
|
|
52
|
+
f"project {args.project_id} + global library/brain"
|
|
53
|
+
if args.project_id is not None
|
|
54
|
+
else "global library/brain only"
|
|
55
|
+
)
|
|
56
|
+
emit(
|
|
57
|
+
{
|
|
58
|
+
"ok": True,
|
|
59
|
+
"project_id": args.project_id,
|
|
60
|
+
"search_scope": "project_plus_global" if args.project_id is not None else "global_only",
|
|
61
|
+
"query": args.query,
|
|
62
|
+
"count": len(results),
|
|
63
|
+
"results": results,
|
|
64
|
+
"operator_chat": (
|
|
65
|
+
f"Search ({where}) for {args.query!r}: "
|
|
66
|
+
f"{len(results)} hit(s).\n"
|
|
67
|
+
+ ("\n".join(lines) if lines else "(none)")
|
|
68
|
+
),
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == "__main__":
|
|
74
|
+
main()
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""List brain markdown files for FieldBrain dashboard."""
|
|
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 list_brain_documents
|
|
11
|
+
from sqlalchemy.orm import Session
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main() -> None:
|
|
15
|
+
parser = argparse.ArgumentParser()
|
|
16
|
+
parser.add_argument(
|
|
17
|
+
"--scope",
|
|
18
|
+
choices=("global", "project"),
|
|
19
|
+
default="project",
|
|
20
|
+
help="global org brain or project brain",
|
|
21
|
+
)
|
|
22
|
+
parser.add_argument("--project-id", type=int, default=None, help="Required when scope=project")
|
|
23
|
+
parser.add_argument("--search", default="", help="Filter by path substring")
|
|
24
|
+
parser.add_argument("--limit", type=int, default=200)
|
|
25
|
+
args = parser.parse_args()
|
|
26
|
+
|
|
27
|
+
ok, msg = verify_schema_version()
|
|
28
|
+
if not ok:
|
|
29
|
+
emit_error(msg)
|
|
30
|
+
|
|
31
|
+
if args.scope == "project" and args.project_id is None:
|
|
32
|
+
emit_error("--project-id is required when scope is project")
|
|
33
|
+
|
|
34
|
+
with Session(get_engine()) as session:
|
|
35
|
+
brains = list_brain_documents(
|
|
36
|
+
session,
|
|
37
|
+
scope=args.scope,
|
|
38
|
+
project_id=args.project_id,
|
|
39
|
+
search=args.search or None,
|
|
40
|
+
limit=args.limit,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
lines = [f"#{b['id']} {b['relative_path']}" for b in brains]
|
|
44
|
+
emit(
|
|
45
|
+
{
|
|
46
|
+
"ok": True,
|
|
47
|
+
"scope": args.scope,
|
|
48
|
+
"project_id": args.project_id,
|
|
49
|
+
"count": len(brains),
|
|
50
|
+
"brains": brains,
|
|
51
|
+
"operator_chat": (
|
|
52
|
+
f"Listed {len(brains)} brain file(s) ({args.scope}).\n"
|
|
53
|
+
+ ("\n".join(lines) if lines else "(none)")
|
|
54
|
+
),
|
|
55
|
+
}
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
if __name__ == "__main__":
|
|
60
|
+
main()
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Create a project from FieldBrain dashboard (fieldbrain:projectCreate IPC)."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
from _db_lib import get_engine, verify_schema_version
|
|
10
|
+
from _json_out import emit, emit_error
|
|
11
|
+
from services.brain_paths import ensure_project_brain_layout
|
|
12
|
+
from services.project_service import create_project_structured
|
|
13
|
+
from sqlalchemy.orm import Session
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main() -> None:
|
|
17
|
+
parser = argparse.ArgumentParser()
|
|
18
|
+
parser.add_argument("--job-number", default="", help="Five-digit project number (e.g. 12345)")
|
|
19
|
+
parser.add_argument(
|
|
20
|
+
"--sub-project-number",
|
|
21
|
+
default="",
|
|
22
|
+
help="Optional three-digit sub-project (e.g. 001) → creates 12345-001",
|
|
23
|
+
)
|
|
24
|
+
parser.add_argument("--name", default="", help="Legacy: full name 12345 or 12345-001")
|
|
25
|
+
parser.add_argument(
|
|
26
|
+
"--tags-json",
|
|
27
|
+
default="",
|
|
28
|
+
help='JSON array of tag strings, e.g. ["line3"]',
|
|
29
|
+
)
|
|
30
|
+
args = parser.parse_args()
|
|
31
|
+
|
|
32
|
+
ok, msg = verify_schema_version()
|
|
33
|
+
if not ok:
|
|
34
|
+
emit_error(msg)
|
|
35
|
+
|
|
36
|
+
tags: list[str] | None = None
|
|
37
|
+
if args.tags_json.strip():
|
|
38
|
+
try:
|
|
39
|
+
parsed = json.loads(args.tags_json)
|
|
40
|
+
if isinstance(parsed, list):
|
|
41
|
+
tags = [str(x) for x in parsed]
|
|
42
|
+
else:
|
|
43
|
+
emit_error("--tags-json must be a JSON array")
|
|
44
|
+
except json.JSONDecodeError as exc:
|
|
45
|
+
emit_error(f"Invalid --tags-json: {exc}")
|
|
46
|
+
|
|
47
|
+
job_number = args.job_number.strip()
|
|
48
|
+
sub_number = args.sub_project_number.strip()
|
|
49
|
+
legacy_name = args.name.strip()
|
|
50
|
+
|
|
51
|
+
if legacy_name and not job_number:
|
|
52
|
+
from services.project_naming import parse_project_name
|
|
53
|
+
|
|
54
|
+
job, suffix = parse_project_name(legacy_name)
|
|
55
|
+
if not job:
|
|
56
|
+
emit_error("Provide --job-number (12345) or legacy --name in 12345 / 12345-001 format.")
|
|
57
|
+
job_number = job
|
|
58
|
+
sub_number = suffix or ""
|
|
59
|
+
|
|
60
|
+
if not job_number:
|
|
61
|
+
emit_error("Project number is required (five digits, e.g. 12345).")
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
with Session(get_engine()) as session:
|
|
65
|
+
project = create_project_structured(
|
|
66
|
+
session,
|
|
67
|
+
job_number=job_number,
|
|
68
|
+
sub_project_number=sub_number or None,
|
|
69
|
+
project_tags=tags,
|
|
70
|
+
)
|
|
71
|
+
ensure_project_brain_layout(project["id"])
|
|
72
|
+
if project.get("parent_job_id"):
|
|
73
|
+
ensure_project_brain_layout(int(project["parent_job_id"]))
|
|
74
|
+
except ValueError as exc:
|
|
75
|
+
emit_error(str(exc))
|
|
76
|
+
|
|
77
|
+
if project["level"] == "sub_project":
|
|
78
|
+
chat = (
|
|
79
|
+
f"Created sub-project {project['id']}: {project['name']} "
|
|
80
|
+
f"(under project {project.get('job_number')})."
|
|
81
|
+
)
|
|
82
|
+
else:
|
|
83
|
+
chat = f"Created project {project['id']}: {project['name']}."
|
|
84
|
+
|
|
85
|
+
emit(
|
|
86
|
+
{
|
|
87
|
+
"ok": True,
|
|
88
|
+
"project": project,
|
|
89
|
+
"operator_chat": chat,
|
|
90
|
+
}
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
if __name__ == "__main__":
|
|
95
|
+
main()
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""List projects with stats for FieldBrain dashboard (fieldbrain:projectList IPC)."""
|
|
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.project_service import list_projects_with_stats
|
|
11
|
+
from sqlalchemy.orm import Session
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main() -> None:
|
|
15
|
+
parser = argparse.ArgumentParser()
|
|
16
|
+
parser.add_argument("--include-hidden", action="store_true", help="Include system-hidden projects")
|
|
17
|
+
args = parser.parse_args()
|
|
18
|
+
|
|
19
|
+
ok, msg = verify_schema_version()
|
|
20
|
+
if not ok:
|
|
21
|
+
emit_error(msg)
|
|
22
|
+
|
|
23
|
+
with Session(get_engine()) as session:
|
|
24
|
+
projects = list_projects_with_stats(session, include_hidden=args.include_hidden)
|
|
25
|
+
|
|
26
|
+
lines = []
|
|
27
|
+
for p in projects:
|
|
28
|
+
stats = p.get("stats") or {}
|
|
29
|
+
lines.append(
|
|
30
|
+
f"{p['id']}: {p['name']} — "
|
|
31
|
+
f"{stats.get('brain_entries', 0)} brain, "
|
|
32
|
+
f"{stats.get('library_docs', 0)} docs, "
|
|
33
|
+
f"{stats.get('maintenance_logs', 0)} logs"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
emit(
|
|
37
|
+
{
|
|
38
|
+
"ok": True,
|
|
39
|
+
"count": len(projects),
|
|
40
|
+
"projects": projects,
|
|
41
|
+
"operator_chat": (
|
|
42
|
+
f"Found {len(projects)} project(s).\n" + ("\n".join(lines) if lines else "(none)")
|
|
43
|
+
),
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
if __name__ == "__main__":
|
|
49
|
+
main()
|