personal memory agent
1# SPDX-License-Identifier: AGPL-3.0-only
2# Copyright (c) 2026 sol pbc
3
4"""CLI entry points for Convey web interface."""
5
6from __future__ import annotations
7
8import argparse
9import logging
10import os
11
12from flask import Flask
13
14from apps.events import discover_handlers, start_dispatcher, stop_dispatcher
15
16from .bridge import start_bridge, stop_bridge
17
18logger = logging.getLogger(__name__)
19
20
21def _resolve_config_password_hash() -> str:
22 """Return the configured Convey password hash from journal config."""
23 from think.utils import get_config
24
25 try:
26 config = get_config()
27 convey_config = config.get("convey", {})
28 return convey_config.get("password_hash", "")
29 except Exception:
30 return ""
31
32
33def run_service(
34 app: Flask,
35 *,
36 host: str = "0.0.0.0",
37 port: int,
38 debug: bool = False,
39 start_watcher: bool = True,
40) -> None:
41 """Run the Convey service, optionally starting the Cortex watcher."""
42
43 if start_watcher:
44 # In debug mode with reloader, only start in child process
45 # In non-debug mode, always start (no reloader)
46 # WERKZEUG_RUN_MAIN is set to 'true' only in the child/main process
47 should_start = not debug or os.environ.get("WERKZEUG_RUN_MAIN") == "true"
48 if should_start:
49 # Discover and start event handlers before bridge
50 discover_handlers()
51 start_dispatcher()
52 logger.info("Starting Callosum bridge")
53 start_bridge()
54 else:
55 logger.debug("Skipping bridge start in reloader parent process")
56
57 try:
58 app.run(host=host, port=port, debug=debug)
59 finally:
60 stop_bridge()
61 stop_dispatcher()
62
63
64def main() -> None:
65 """Main CLI entry point for convey command."""
66 from pathlib import Path
67
68 from think.utils import (
69 get_journal,
70 setup_cli,
71 write_service_port,
72 )
73
74 from . import create_app
75 from .maint import run_pending_tasks
76
77 parser = argparse.ArgumentParser(description="Convey web interface")
78 parser.add_argument(
79 "--port",
80 type=int,
81 required=True,
82 help="Port to serve on",
83 )
84 parser.add_argument(
85 "--skip-maint",
86 action="store_true",
87 help="Skip running pending maintenance tasks",
88 )
89 args = setup_cli(parser)
90 journal = get_journal()
91
92 # Run pending maintenance tasks before starting
93 if not args.skip_maint:
94 ran, succeeded = run_pending_tasks(Path(journal))
95 if ran > 0:
96 logger.info(f"Completed {succeeded}/{ran} maintenance task(s)")
97
98 app = create_app(journal)
99 password = _resolve_config_password_hash()
100 if password:
101 logger.info("Password authentication enabled")
102 else:
103 logger.warning(
104 "No password configured - run 'sol password set' to enable authentication"
105 )
106
107 # Write port to health directory for discovery by other tools
108 write_service_port("convey", args.port)
109 logger.info(f"Convey starting on port {args.port}")
110
111 run_service(app, host="0.0.0.0", port=args.port, debug=args.debug)