Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/opentab/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@
)
from opentab.state import apply_state, load_state, save_state, state_path
from opentab.stores.antigravity import AntigravityStore
from opentab.stores.bahulam import BahulamStore
from opentab.stores.cached import CachedStore
from opentab.stores.claude import (
CLAUDE_RETENTION_DEFAULT_DAYS,
Expand Down
20 changes: 15 additions & 5 deletions src/opentab/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
SOURCE_LABELS,
_default_antigravity_dir,
_default_gemini_dir,
_default_bahulam_dir,
_default_omp_dir,
_default_openclaw_dir,
_default_pi_dir,
Expand Down Expand Up @@ -98,15 +99,15 @@ def _add_global_args(parser: argparse.ArgumentParser) -> None:
"zaly",
"gemini",
"antigravity",
"bahulam",
"all",
"remote",
),
default="auto",
help="which harness's spend to browse: opencode · claude · codex · hermes · csv · "
"jsonl · copilot · vscode · pi · omp · openclaw · zaly · gemini · antigravity · "
"all (merged) · "
"remote "
"(other machines, via pull/export). Default auto merges every present local "
"bahulam · all (merged) · "
"remote (other machines, via pull/export). Default auto merges every present local "
"harness. Or just pass a file path -- e.g. `opentab requests.csv`. (--source is a "
"deprecated alias for --harness)",
)
Expand Down Expand Up @@ -178,6 +179,13 @@ def _add_global_args(parser: argparse.ArgumentParser) -> None:
help="Gemini home directory holding antigravity/conversations/*.db (for "
"--harness antigravity); honors $GEMINI_CLI_HOME, default ~/.gemini",
)
parser.add_argument(
"--bahulam-dir",
default=_default_bahulam_dir(),
help="Bahulam Code projects directory (for --harness bahulam); "
"honors $BAHULAM_PROJECTS_DIR, then $BAHULAM_HOME/projects, then $KEPLER_HOME/projects; "
"default ~/.bahulam/projects (falls back to ~/.kepler/projects if present)",
)
parser.add_argument(
"--csv",
default=None,
Expand Down Expand Up @@ -275,8 +283,8 @@ def _add_legacy_command_flags(parser: argparse.ArgumentParser) -> None:
metavar="DIR|SESSION",
help="print the cost of the most recently active agent session (subagent "
"subtree included) and exit, consulting every present harness backend "
"(OpenCode, Claude Code, Codex, Hermes, pi, omp, OpenClaw, Zaly, Gemini); with DIR "
"only "
"(OpenCode, Claude Code, Codex, Hermes, pi, omp, OpenClaw, Zaly, Gemini, "
"Antigravity, Bahulam Code); with DIR only "
"sessions of that project count, with a session id (ses_... or a UUID -- the "
"id is matched to its own backend) exactly that session is priced, and "
"--harness pins one backend. Made for a tmux status line: set -g "
Expand Down Expand Up @@ -549,6 +557,7 @@ def _build_parser() -> argparse.ArgumentParser:
"zaly_dir",
"gemini_dir",
"antigravity_dir",
"bahulam_dir",
"csv",
"jsonl",
"remotes",
Expand Down Expand Up @@ -1101,6 +1110,7 @@ def _project_key(directory: str) -> str:
"zaly",
"gemini",
"antigravity",
"bahulam",
)


Expand Down
4 changes: 4 additions & 0 deletions src/opentab/doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,7 @@ def _path_row(pkg_dir: str, full: bool) -> Row:
("zaly", "zaly_dir", _TREE, "sessions/*/*/session.jsonl", "**/session.jsonl", "--zaly-dir/$ZALY_DATA want the DATA directory holding sessions/, not sessions/ itself"),
("gemini", "gemini_dir", _TREE, "tmp/*/chats/**/*.json*", "**/*.json*", "--gemini-dir/$GEMINI_CLI_HOME want the .gemini HOME holding tmp/, not a chats directory"),
("antigravity", "antigravity_dir", _TREE, "antigravity*/conversations/*.db", "**/*.db", "--antigravity-dir wants the .gemini HOME holding antigravity/, not a conversations directory"),
("bahulam", "bahulam_dir", _TREE, "**/*.jsonl", "", "Bahulam Code writes transcripts under ~/.bahulam/projects; it honors $BAHULAM_PROJECTS_DIR, $BAHULAM_HOME, or $KEPLER_HOME, or pass --bahulam-dir"),
) # fmt: skip


Expand Down Expand Up @@ -962,6 +963,9 @@ def file_rows(args: argparse.Namespace, full: bool = False) -> list[Row]:
"ZALY_DATA",
"ZALY_ROOT",
"ZALY_STATE",
"BAHULAM_PROJECTS_DIR",
"BAHULAM_HOME",
"KEPLER_HOME",
"XDG_CONFIG_HOME",
"XDG_STATE_HOME",
"XDG_DATA_HOME",
Expand Down
47 changes: 47 additions & 0 deletions src/opentab/sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
CONVERSATION_DIRS as ANTIGRAVITY_DIRS,
)
from opentab.stores.antigravity import AntigravityStore, default_antigravity_dir
from opentab.stores.bahulam import BahulamStore
from opentab.stores.cached import CachedStore
from opentab.stores.claude import ClaudeStore
from opentab.stores.codex import CodexStore, codex_archive_dirs
Expand Down Expand Up @@ -102,6 +103,32 @@ def _default_zaly_dir() -> str:
return default_zaly_data_dir()


def _default_bahulam_dir() -> str:
"""Return the default Bahulam Code projects directory.

Resolution order (first match wins, empty values skipped):
1. ``$BAHULAM_PROJECTS_DIR`` — explicit override
2. ``$BAHULAM_HOME/projects`` — Bahulam home relocated
3. ``$KEPLER_HOME/projects`` — legacy alias from the Kepler-branded builds
4. ``~/.bahulam/projects`` — Bahulam default
5. ``~/.kepler/projects`` — legacy default, only if it exists on disk
(skipping this last-resort check would silently point at a phantom
path when neither install layout is present)
"""
override = (os.environ.get("BAHULAM_PROJECTS_DIR") or "").strip()
if override:
return override
for env_name in ("BAHULAM_HOME", "KEPLER_HOME"):
home = (os.environ.get(env_name) or "").strip()
if home:
return os.path.join(os.path.expanduser(home), "projects")
bahulam_default = os.path.expanduser("~/.bahulam/projects")
kepler_legacy = os.path.expanduser("~/.kepler/projects")
if not os.path.isdir(bahulam_default) and os.path.isdir(kepler_legacy):
return kepler_legacy
return bahulam_default


_PATH_SLOT = {
"csv": "csv",
"jsonl": "jsonl",
Expand All @@ -117,6 +144,7 @@ def _default_zaly_dir() -> str:
"zaly": "zaly_dir",
"gemini": "gemini_dir",
"antigravity": "antigravity_dir",
"bahulam": "bahulam_dir",
}


Expand Down Expand Up @@ -270,6 +298,11 @@ def _antigravity_available(root_dir: str) -> bool:
return False


def _bahulam_available(root_dir: str) -> bool:
"""Return ``True`` if *root_dir* contains at least one ``.jsonl`` file."""
return _jsonl_dir_available(root_dir)


def _copilot_otel_available(args: argparse.Namespace) -> bool:
if _jsonl_dir_available(getattr(args, "copilot_dir", "")):
return True
Expand Down Expand Up @@ -314,6 +347,7 @@ def _vscode_available(args: argparse.Namespace) -> bool:
"zaly": "Zaly",
"gemini": "Gemini",
"antigravity": "Antigravity",
"bahulam": "Bahulam Code",
"all": "all",
}

Expand All @@ -328,6 +362,7 @@ def _vscode_available(args: argparse.Namespace) -> bool:
"Zaly": "zaly --session",
"Gemini": "gemini --resume",
"Antigravity": "antigravity",
"Bahulam Code": "bahulam resume",
}


Expand All @@ -350,6 +385,7 @@ def _detect_fingerprint(args: argparse.Namespace) -> tuple:
"zaly_dir",
"gemini_dir",
"antigravity_dir",
"bahulam_dir",
)
) + (os.environ.get("COPILOT_OTEL_FILE_EXPORTER_PATH", ""),)

Expand Down Expand Up @@ -390,6 +426,8 @@ def available_sources(args: argparse.Namespace) -> list[str]:
keys.append("gemini")
if _antigravity_available(getattr(args, "antigravity_dir", "")):
keys.append("antigravity")
if _bahulam_available(getattr(args, "bahulam_dir", "")):
keys.append("bahulam")
args._available_sources = (fp, keys)
return list(keys)

Expand Down Expand Up @@ -546,6 +584,15 @@ def _build_store(args: argparse.Namespace, key: str) -> tuple[object, str]:
AntigravityStore(args.antigravity_dir, args),
"OpenTab: loading Antigravity conversations…\r",
)
if key == "bahulam":
if not _bahulam_available(getattr(args, "bahulam_dir", "")):
raise SystemExit(
"No Bahulam Code sessions found. Point --bahulam-dir (or "
"$BAHULAM_PROJECTS_DIR / $BAHULAM_HOME / $KEPLER_HOME) at "
"~/.bahulam/projects (or ~/.kepler/projects if you migrated from "
f"Kepler) (looked in {getattr(args, 'bahulam_dir', '')})."
)
return BahulamStore(args.bahulam_dir, args), "OpenTab: loading Bahulam Code sessions…\r"
kind, problem = opencode_db_verdict(args.db)
if kind:
raise SystemExit(problem)
Expand Down
Loading