|
10 | 10 | import click |
11 | 11 |
|
12 | 12 | from .models import AgentKind |
13 | | -from .store import MsgStore, DEFAULT_DB_PATH |
| 13 | +from .store import MsgStore, DEFAULT_DB_PATH, DEFAULT_DB_DIR |
14 | 14 |
|
15 | 15 |
|
16 | | -def _get_store(db_path: str | None = None) -> MsgStore: |
17 | | - return MsgStore(db_path or DEFAULT_DB_PATH) |
| 16 | +def _check_db_writable(db_dir: str) -> bool: |
| 17 | + """Check if we can write to the DB directory.""" |
| 18 | + from pathlib import Path |
| 19 | + try: |
| 20 | + Path(db_dir).mkdir(parents=True, exist_ok=True) |
| 21 | + test_file = os.path.join(db_dir, ".write_test") |
| 22 | + with open(test_file, "w") as f: |
| 23 | + f.write("test") |
| 24 | + os.remove(test_file) |
| 25 | + return True |
| 26 | + except OSError: |
| 27 | + return False |
| 28 | + |
| 29 | + |
| 30 | +def _get_local_db_path() -> str: |
| 31 | + """Get project-local DB path.""" |
| 32 | + try: |
| 33 | + result = subprocess.run( |
| 34 | + ["git", "rev-parse", "--show-toplevel"], |
| 35 | + capture_output=True, text=True, timeout=5, |
| 36 | + ) |
| 37 | + if result.returncode == 0: |
| 38 | + root = result.stdout.strip() |
| 39 | + return os.path.join(root, ".msg", "msg.db") |
| 40 | + except (FileNotFoundError, subprocess.TimeoutExpired): |
| 41 | + pass |
| 42 | + return os.path.join(os.getcwd(), ".msg", "msg.db") |
| 43 | + |
| 44 | + |
| 45 | +def _get_store( |
| 46 | + db_path: str | None = None, |
| 47 | + local: bool = False, |
| 48 | +) -> MsgStore: |
| 49 | + if db_path: |
| 50 | + return MsgStore(db_path) |
| 51 | + if local: |
| 52 | + return MsgStore(_get_local_db_path()) |
| 53 | + return MsgStore(DEFAULT_DB_PATH) |
18 | 54 |
|
19 | 55 |
|
20 | 56 | def _detect_tmux_pane() -> str | None: |
@@ -193,16 +229,50 @@ def _ensure_watcher_running(store: MsgStore) -> None: |
193 | 229 | @click.group() |
194 | 230 | @click.option( |
195 | 231 | "--db", default=None, |
196 | | - help="Path to msg database (default: ~/.msg/msg.db)", |
| 232 | + help="Path to msg database", |
| 233 | +) |
| 234 | +@click.option( |
| 235 | + "--local", is_flag=True, default=False, |
| 236 | + help="Use project-local DB instead of global ~/.msg/", |
197 | 237 | ) |
198 | 238 | @click.pass_context |
199 | | -def cli(ctx: click.Context, db: str | None) -> None: |
| 239 | +def cli( |
| 240 | + ctx: click.Context, |
| 241 | + db: str | None, |
| 242 | + local: bool, |
| 243 | +) -> None: |
200 | 244 | """msg: Inter-agent communication for coding agents.""" |
201 | 245 | ctx.ensure_object(dict) |
202 | | - store = _get_store(db) |
| 246 | + |
| 247 | + # If no explicit path, check if global DB is writable |
| 248 | + if not db and not local: |
| 249 | + if not _check_db_writable(DEFAULT_DB_DIR): |
| 250 | + home = os.path.expanduser("~") |
| 251 | + click.echo( |
| 252 | + "Cannot write to ~/.msg/ " |
| 253 | + "(sandbox restriction).\n\n" |
| 254 | + "Ask the user which option they prefer:\n\n" |
| 255 | + "Option A: Global messaging " |
| 256 | + "(agents across any project can talk)\n" |
| 257 | + " User needs to add to " |
| 258 | + "~/.codex/config.toml:\n" |
| 259 | + " [sandbox_workspace_write]\n" |
| 260 | + f' writable_roots = ["{home}/.msg"]\n' |
| 261 | + " Then restart Codex.\n\n" |
| 262 | + "Option B: Local messaging " |
| 263 | + "(this project only)\n" |
| 264 | + " Works immediately, no config " |
| 265 | + "changes needed.\n\n" |
| 266 | + "If user chooses B, re-run the same " |
| 267 | + "command with --local flag prepended " |
| 268 | + "after 'msg', e.g.: " |
| 269 | + "msg --local register <name>", |
| 270 | + err=True, |
| 271 | + ) |
| 272 | + sys.exit(1) |
| 273 | + |
| 274 | + store = _get_store(db=db, local=local) |
203 | 275 | ctx.obj["store"] = store |
204 | | - # Auto-start watcher if not running |
205 | | - # (skip if the command itself is 'watch' to avoid loop) |
206 | 276 | if ctx.invoked_subcommand != "watch": |
207 | 277 | _ensure_watcher_running(store) |
208 | 278 |
|
|
0 commit comments