openbase-coder 0.1.3__tar.gz → 0.1.5__tar.gz

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 (117) hide show
  1. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/PKG-INFO +1 -1
  2. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/_version.py +2 -2
  3. openbase_coder-0.1.5/openbase_coder_cli/brain_score.py +27 -0
  4. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/cartesia_voice_catalog.py +6 -0
  5. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/cli/node.py +13 -2
  6. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/cli/setup.py +227 -2
  7. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/livekit_agent/livekit.py +29 -77
  8. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/livekit_voice_route.py +7 -55
  9. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/openbase_coder_cli_app/brain_readiness.py +21 -10
  10. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/.gitignore +0 -0
  11. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/LICENSE +0 -0
  12. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/README.md +0 -0
  13. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/__init__.py +0 -0
  14. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/__main__.py +0 -0
  15. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/cli/__init__.py +0 -0
  16. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/cli/auth.py +0 -0
  17. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/cli/boilersync.py +0 -0
  18. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/cli/bootstrap.py +0 -0
  19. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/cli/claude_chrome.py +0 -0
  20. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/cli/codex_sync.py +0 -0
  21. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/cli/computer_use.py +0 -0
  22. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/cli/doctor.py +0 -0
  23. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/cli/local_server.py +0 -0
  24. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/cli/plugins.py +0 -0
  25. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/cli/restart.py +0 -0
  26. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/cli/routines.py +0 -0
  27. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/cli/server.py +0 -0
  28. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/cli/services.py +0 -0
  29. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/cli/super_agent_name.py +0 -0
  30. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/cli/user.py +0 -0
  31. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/cli/utils.py +0 -0
  32. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/cli_helpers.py +0 -0
  33. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/config/__init__.py +0 -0
  34. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/config/asgi.py +0 -0
  35. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/config/authentication.py +0 -0
  36. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/config/jwt_validation.py +0 -0
  37. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/config/proxy.py +0 -0
  38. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/config/serializers.py +0 -0
  39. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/config/settings.py +0 -0
  40. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/config/token_manager.py +0 -0
  41. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/config/urls.py +0 -0
  42. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/config/viewsets.py +0 -0
  43. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/config/wsgi.py +0 -0
  44. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/dispatcher_config.py +0 -0
  45. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/entrypoint/manage.py +0 -0
  46. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/errors.py +0 -0
  47. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/livekit_agent/__init__.py +0 -0
  48. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/livekit_agent/codex_app_client.py +0 -0
  49. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/livekit_agent/codex_thread_state.py +0 -0
  50. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/livekit_agent/codex_transport.py +0 -0
  51. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/livekit_agent/codex_turns.py +0 -0
  52. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/livekit_agent/speech_formatter.py +0 -0
  53. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/livekit_agent/speech_replacements.py +0 -0
  54. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/livekit_announcer.py +0 -0
  55. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/livekit_start_announcer.py +0 -0
  56. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/livekit_voice_history.py +0 -0
  57. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/logging.py +0 -0
  58. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/mcp/__init__.py +0 -0
  59. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/mcp/apps.py +0 -0
  60. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/mcp/mcp.py +0 -0
  61. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/mcp/models.py +0 -0
  62. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/mcp/projects.py +0 -0
  63. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/mcp/session_manager.py +0 -0
  64. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/mcp/thread_import.py +0 -0
  65. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/multi_config.py +0 -0
  66. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/openbase_coder_cli_app/__init__.py +0 -0
  67. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/openbase_coder_cli_app/agents_md.py +0 -0
  68. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/openbase_coder_cli_app/approvals.py +0 -0
  69. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/openbase_coder_cli_app/apps.py +0 -0
  70. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/openbase_coder_cli_app/auth.py +0 -0
  71. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/openbase_coder_cli_app/common.py +0 -0
  72. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/openbase_coder_cli_app/consumers.py +0 -0
  73. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/openbase_coder_cli_app/diagnostics.py +0 -0
  74. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/openbase_coder_cli_app/livekit.py +0 -0
  75. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/openbase_coder_cli_app/middleware.py +0 -0
  76. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/openbase_coder_cli_app/migrations/0001_initial.py +0 -0
  77. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/openbase_coder_cli_app/migrations/0002_remove_cron_models.py +0 -0
  78. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/openbase_coder_cli_app/migrations/__init__.py +0 -0
  79. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/openbase_coder_cli_app/models.py +0 -0
  80. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/openbase_coder_cli_app/plugins_tools.py +0 -0
  81. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/openbase_coder_cli_app/projects.py +0 -0
  82. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/openbase_coder_cli_app/reports.py +0 -0
  83. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/openbase_coder_cli_app/routines.py +0 -0
  84. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/openbase_coder_cli_app/routing.py +0 -0
  85. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/openbase_coder_cli_app/serializers.py +0 -0
  86. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/openbase_coder_cli_app/services_views.py +0 -0
  87. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/openbase_coder_cli_app/skills.py +0 -0
  88. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/openbase_coder_cli_app/thread_cache.py +0 -0
  89. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/openbase_coder_cli_app/thread_metadata.py +0 -0
  90. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/openbase_coder_cli_app/threads.py +0 -0
  91. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/openbase_coder_cli_app/urls.py +0 -0
  92. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/openbase_coder_cli_app/views.py +0 -0
  93. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/paths.py +0 -0
  94. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/plugins/__init__.py +0 -0
  95. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/plugins/api.py +0 -0
  96. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/plugins/console.py +0 -0
  97. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/plugins/install.py +0 -0
  98. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/plugins/manager.py +0 -0
  99. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/plugins/models.py +0 -0
  100. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/plugins/skills.py +0 -0
  101. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/plugins/sources.py +0 -0
  102. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/plugins/spec.py +0 -0
  103. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/plugins/store.py +0 -0
  104. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/services/__init__.py +0 -0
  105. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/services/boilersync.py +0 -0
  106. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/services/console_settings.py +0 -0
  107. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/services/definitions.py +0 -0
  108. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/services/installation.py +0 -0
  109. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/services/launchctl_tools.py +0 -0
  110. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/services/launchd.py +0 -0
  111. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/services/openbase_services.py +0 -0
  112. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/services/registry.py +0 -0
  113. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/services/restart.py +0 -0
  114. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/services/tailnet_devices.py +0 -0
  115. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/services/uv_tools.py +0 -0
  116. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/openbase_coder_cli/services/voice_warning.py +0 -0
  117. {openbase_coder-0.1.3 → openbase_coder-0.1.5}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openbase-coder
3
- Version: 0.1.3
3
+ Version: 0.1.5
4
4
  Summary: OpenBase Coder CLI with embedded server
5
5
  Project-URL: Repository, https://github.com/openbase-community/openbase-coder
6
6
  Project-URL: Issues, https://github.com/openbase-community/openbase-coder/issues
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.1.3'
22
- __version_tuple__ = version_tuple = (0, 1, 3)
21
+ __version__ = version = '0.1.5'
22
+ __version_tuple__ = version_tuple = (0, 1, 5)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from pathlib import Path
5
+
6
+
7
+ def brain_score_token_file() -> Path:
8
+ return Path(
9
+ os.getenv(
10
+ "OPENBASE_BRAIN_SCORE_TOKEN_FILE",
11
+ str(Path.home() / ".openbase" / "brain_score_token"),
12
+ )
13
+ ).expanduser()
14
+
15
+
16
+ def load_brain_score_token() -> str:
17
+ configured = os.getenv("OPENBASE_BRAIN_SCORE_TOKEN", "").strip()
18
+ if configured:
19
+ return configured
20
+ try:
21
+ return brain_score_token_file().read_text(encoding="utf-8").strip()
22
+ except (FileNotFoundError, OSError):
23
+ return ""
24
+
25
+
26
+ def brain_score_token_configured() -> bool:
27
+ return bool(load_brain_score_token())
@@ -96,6 +96,12 @@ CARTESIA_VOICE_CATALOG: tuple[CartesiaVoiceCatalogEntry, ...] = (
96
96
  CartesiaVoiceCatalogEntry("91b4cf29-5166-44eb-8054-30d40ecc8081", "Tina", "en", "US", "feminine"),
97
97
  )
98
98
 
99
+ DEFAULT_SUPER_AGENT_VOICE_IDS = tuple(
100
+ voice.id
101
+ for voice in CARTESIA_VOICE_CATALOG
102
+ if voice.id != "9626c31c-bec5-4cca-baa8-f8ba9e84c8bc"
103
+ )
104
+
99
105
  _CATALOG_BY_ID = {voice.id: voice for voice in CARTESIA_VOICE_CATALOG}
100
106
  _CATALOG_BY_NORMALIZED_NAME = {
101
107
  " ".join(voice.name.casefold().split()): voice for voice in CARTESIA_VOICE_CATALOG
@@ -24,6 +24,7 @@ def run_workspace_package_command(workspace_dir: Path, package_dir: Path, *args:
24
24
  subprocess.run(
25
25
  [executable, *command_prefix, *command_args],
26
26
  cwd=str(package_dir),
27
+ env=_package_manager_env(executable, command_args),
27
28
  check=True,
28
29
  )
29
30
  return True
@@ -93,12 +94,22 @@ def _normalize_package_manager_args(
93
94
  workspace_dir: Path,
94
95
  args: tuple[str, ...],
95
96
  ) -> tuple[str, ...]:
96
- """Keep managed workspace installs reproducible when using pnpm."""
97
+ """Avoid mutating or validating partial install-set lockfiles with pnpm."""
97
98
  if (
98
99
  Path(executable).name == "pnpm"
99
100
  and args == ("install",)
100
101
  and (workspace_dir / "pnpm-lock.yaml").is_file()
101
102
  ):
102
- return ("install", "--frozen-lockfile")
103
+ return ("install", "--no-lockfile", "--shamefully-hoist")
103
104
 
104
105
  return args
106
+
107
+
108
+ def _package_manager_env(
109
+ executable: str,
110
+ args: tuple[str, ...],
111
+ ) -> dict[str, str] | None:
112
+ if Path(executable).name == "pnpm" and args and args[0] == "install":
113
+ return {**os.environ, "CI": "true"}
114
+
115
+ return None
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import json
3
4
  import platform
4
5
  import secrets
5
6
  import shlex
@@ -14,6 +15,7 @@ from openbase_coder_cli.cli.node import run_workspace_package_command
14
15
  from openbase_coder_cli.paths import (
15
16
  CODEX_AGENTS_MD_PATH,
16
17
  CODEX_DIRECT_LIVEKIT_INSTRUCTIONS_PATH,
18
+ CODEX_DISPATCHER_CONFIG_PATH,
17
19
  CODEX_DISPATCHER_INSTRUCTIONS_PATH,
18
20
  CODEX_HOME_DIR,
19
21
  CODEX_SUPER_AGENT_INSTRUCTIONS_PATH,
@@ -34,6 +36,21 @@ CODEX_HOME_DEFAULT_FILES = (
34
36
  ("DISPATCHER_INSTRUCTIONS.md", CODEX_DISPATCHER_INSTRUCTIONS_PATH),
35
37
  ("SUPER_AGENT_INSTRUCTIONS.md", CODEX_SUPER_AGENT_INSTRUCTIONS_PATH),
36
38
  )
39
+ SUPER_AGENTS_MCP_TABLE = "mcp_servers.super-agents"
40
+ SUPER_AGENTS_MCP_COMMAND = "super-agents-mcp"
41
+ CODEX_HOME_PERMISSION_VALUES = (
42
+ ("sandbox_mode", json.dumps("danger-full-access")),
43
+ (
44
+ "approval_policy",
45
+ "{ granular = { sandbox_approval = false, rules = false, "
46
+ "mcp_elicitations = false, request_permissions = false, "
47
+ "skill_approval = false } }",
48
+ ),
49
+ )
50
+ CODEX_HOME_DEFAULT_DISPATCHER_CONFIG = {
51
+ "dispatcher_reasoning_effort": "low",
52
+ "super_agents_reasoning_effort": "high",
53
+ }
37
54
 
38
55
 
39
56
  @click.command()
@@ -109,11 +126,15 @@ def setup(
109
126
  # --- Symlink Codex auth into the service CODEX_HOME ---
110
127
  _symlink_codex_auth()
111
128
  _ensure_codex_home_default_files(workspace_dir)
129
+ _ensure_codex_home_dispatcher_config()
112
130
  _symlink_codex_home_skills(workspace_dir)
113
131
 
114
132
  # --- Initialize CLI workspace ---
115
133
  _init_cli_workspace(workspace_dir)
116
134
 
135
+ # --- Configure the service CODEX_HOME ---
136
+ _ensure_codex_home_config(workspace_dir)
137
+
117
138
  # --- Install/update user-facing CLI shim ---
118
139
  _install_cli_shim(workspace_dir)
119
140
 
@@ -142,7 +163,9 @@ def _clone_workspace(workspace_dir: str) -> None:
142
163
  if ws.exists() and (ws / ".git").is_dir():
143
164
  click.echo(f"Workspace already exists at {ws}, pulling latest...")
144
165
  _update_existing_workspace(ws)
166
+ _remove_managed_repo_symlinks(ws)
145
167
  _multi_sync(ws)
168
+ _update_install_set_repos(ws)
146
169
  return
147
170
 
148
171
  click.echo(f"Cloning workspace to {ws}...")
@@ -151,6 +174,7 @@ def _clone_workspace(workspace_dir: str) -> None:
151
174
  check=True,
152
175
  )
153
176
  _multi_sync(ws)
177
+ _update_install_set_repos(ws)
154
178
 
155
179
 
156
180
  def _update_existing_workspace(ws: Path) -> None:
@@ -176,6 +200,57 @@ def _update_existing_workspace(ws: Path) -> None:
176
200
  subprocess.run(["git", "-C", str(ws), "pull", "--ff-only"], check=True)
177
201
 
178
202
 
203
+ def _remove_managed_repo_symlinks(ws: Path) -> None:
204
+ if ws.resolve() != DEFAULT_WORKSPACE_DIR.resolve():
205
+ return
206
+
207
+ for repo_name in _install_set_repo_names(ws):
208
+ repo_path = ws / repo_name
209
+ if repo_path.is_symlink():
210
+ click.echo(f"Removing symlinked install repo at {repo_path}")
211
+ repo_path.unlink()
212
+
213
+
214
+ def _update_install_set_repos(ws: Path) -> None:
215
+ for repo_name in _install_set_repo_names(ws):
216
+ repo_path = ws / repo_name
217
+ if not (repo_path / ".git").exists():
218
+ continue
219
+
220
+ if ws.resolve() == DEFAULT_WORKSPACE_DIR.resolve():
221
+ click.echo(f"Updating managed install repo {repo_name}...")
222
+ subprocess.run(
223
+ ["git", "-C", str(repo_path), "fetch", "origin", "main"],
224
+ check=True,
225
+ )
226
+ subprocess.run(
227
+ ["git", "-C", str(repo_path), "reset", "--hard", "origin/main"],
228
+ check=True,
229
+ )
230
+ else:
231
+ subprocess.run(["git", "-C", str(repo_path), "pull", "--ff-only"], check=True)
232
+
233
+
234
+ def _install_set_repo_names(ws: Path) -> list[str]:
235
+ multi_json_path = ws / "multi.json"
236
+ if not multi_json_path.is_file():
237
+ return []
238
+
239
+ try:
240
+ multi_json = json.loads(multi_json_path.read_text(encoding="utf-8"))
241
+ except (OSError, json.JSONDecodeError):
242
+ return []
243
+
244
+ names = []
245
+ for repo in multi_json.get("repos", []):
246
+ install_sets = repo.get("installSets")
247
+ if install_sets is not None and WORKSPACE_INSTALL_SET not in install_sets:
248
+ continue
249
+ if name := repo.get("name"):
250
+ names.append(name)
251
+ return names
252
+
253
+
179
254
  def _multi_sync(ws_path: Path) -> None:
180
255
  click.echo(f"Running multi sync --install-set {WORKSPACE_INSTALL_SET}...")
181
256
  sync_workspace(ws_path, install_set=WORKSPACE_INSTALL_SET)
@@ -255,6 +330,23 @@ def _ensure_codex_home_default_files(workspace_dir: str) -> None:
255
330
  click.echo(f"Created Codex home default at {target_path}")
256
331
 
257
332
 
333
+ def _ensure_codex_home_dispatcher_config() -> None:
334
+ """Create the missing Openbase dispatcher config."""
335
+ if CODEX_DISPATCHER_CONFIG_PATH.exists():
336
+ click.echo(
337
+ f"Codex home dispatcher config already exists at "
338
+ f"{CODEX_DISPATCHER_CONFIG_PATH}"
339
+ )
340
+ return
341
+
342
+ CODEX_DISPATCHER_CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
343
+ CODEX_DISPATCHER_CONFIG_PATH.write_text(
344
+ json.dumps(CODEX_HOME_DEFAULT_DISPATCHER_CONFIG, indent=2) + "\n",
345
+ encoding="utf-8",
346
+ )
347
+ click.echo(f"Created Codex home dispatcher config at {CODEX_DISPATCHER_CONFIG_PATH}")
348
+
349
+
258
350
  def _symlink_codex_home_skills(workspace_dir: str) -> None:
259
351
  """Symlink workspace-owned skills into the Openbase Codex home."""
260
352
  source_root = Path(workspace_dir) / CODEX_HOME_SKILLS_SOURCE_DIR
@@ -284,6 +376,141 @@ def _symlink_codex_home_skills(workspace_dir: str) -> None:
284
376
  click.echo(f"Linked Codex home skill {target_path} → {source_path}")
285
377
 
286
378
 
379
+ def _ensure_codex_home_config(workspace_dir: str) -> None:
380
+ """Configure Openbase's service Codex home."""
381
+ CODEX_HOME_DIR.mkdir(parents=True, exist_ok=True)
382
+ config_path = CODEX_HOME_DIR / "config.toml"
383
+ command_path, args = _super_agents_mcp_command(Path(workspace_dir))
384
+ block = (
385
+ f"[{SUPER_AGENTS_MCP_TABLE}]\n"
386
+ f"command = {json.dumps(str(command_path))}\n"
387
+ f"{_toml_args_line(args)}"
388
+ )
389
+
390
+ if not command_path.is_file():
391
+ click.echo(
392
+ f"Super Agents MCP command not found at {command_path}; "
393
+ "writing the expected config path anyway."
394
+ )
395
+
396
+ existing = ""
397
+ if config_path.is_file():
398
+ existing = config_path.read_text(encoding="utf-8")
399
+
400
+ updated = _ensure_toml_root_values(existing, CODEX_HOME_PERMISSION_VALUES)
401
+ updated = _replace_toml_table(updated, SUPER_AGENTS_MCP_TABLE, block)
402
+ if updated == existing:
403
+ click.echo(f"Codex home config already configured at {config_path}")
404
+ return
405
+
406
+ config_path.write_text(updated, encoding="utf-8")
407
+ click.echo(f"Configured Codex home config at {config_path}")
408
+
409
+
410
+ def _super_agents_mcp_command(workspace_dir: Path) -> tuple[Path, list[str]]:
411
+ candidates = (
412
+ workspace_dir / ".venv" / "bin" / SUPER_AGENTS_MCP_COMMAND,
413
+ workspace_dir / "cli" / ".venv" / "bin" / SUPER_AGENTS_MCP_COMMAND,
414
+ )
415
+ for candidate in candidates:
416
+ if candidate.is_file():
417
+ return candidate, []
418
+
419
+ if command := which(SUPER_AGENTS_MCP_COMMAND):
420
+ return Path(command), []
421
+
422
+ if uv_bin := which("uv"):
423
+ run_dir = workspace_dir / "cli"
424
+ if not run_dir.is_dir():
425
+ run_dir = workspace_dir
426
+ return Path(uv_bin), [
427
+ "--directory",
428
+ str(run_dir),
429
+ "run",
430
+ SUPER_AGENTS_MCP_COMMAND,
431
+ ]
432
+
433
+ return candidates[0], []
434
+
435
+
436
+ def _toml_args_line(args: list[str]) -> str:
437
+ if not args:
438
+ return ""
439
+ return f"args = {json.dumps(args)}\n"
440
+
441
+
442
+ def _ensure_toml_root_values(
443
+ text: str,
444
+ values: tuple[tuple[str, str], ...],
445
+ ) -> str:
446
+ lines = text.splitlines()
447
+ first_table_index = next(
448
+ (
449
+ index
450
+ for index, line in enumerate(lines)
451
+ if line.strip().startswith("[") and line.strip().endswith("]")
452
+ ),
453
+ len(lines),
454
+ )
455
+ root_lines = lines[:first_table_index]
456
+ table_lines = lines[first_table_index:]
457
+ keys = {key for key, _value in values}
458
+ updated_root = [
459
+ line
460
+ for line in root_lines
461
+ if _toml_root_key(line) not in keys
462
+ ]
463
+
464
+ while updated_root and not updated_root[-1].strip():
465
+ updated_root.pop()
466
+
467
+ for key, value in values:
468
+ updated_root.append(f"{key} = {value}")
469
+
470
+ while table_lines and not table_lines[0].strip():
471
+ table_lines.pop(0)
472
+
473
+ if table_lines:
474
+ return "\n".join(updated_root) + "\n\n" + "\n".join(table_lines) + "\n"
475
+ return "\n".join(updated_root) + "\n"
476
+
477
+
478
+ def _toml_root_key(line: str) -> str | None:
479
+ stripped = line.strip()
480
+ if not stripped or stripped.startswith("#") or "=" not in stripped:
481
+ return None
482
+ return stripped.split("=", 1)[0].strip()
483
+
484
+
485
+ def _replace_toml_table(text: str, table_name: str, block: str) -> str:
486
+ target_header = f"[{table_name}]"
487
+ lines = text.splitlines()
488
+ output: list[str] = []
489
+ index = 0
490
+
491
+ while index < len(lines):
492
+ if lines[index].strip() == target_header:
493
+ index += 1
494
+ while index < len(lines):
495
+ stripped = lines[index].strip()
496
+ if stripped.startswith("[") and stripped.endswith("]"):
497
+ break
498
+ index += 1
499
+ while output and not output[-1].strip():
500
+ output.pop()
501
+ continue
502
+
503
+ output.append(lines[index])
504
+ index += 1
505
+
506
+ while output and not output[-1].strip():
507
+ output.pop()
508
+
509
+ if output:
510
+ return "\n".join(output) + "\n\n" + block
511
+ return block
512
+
513
+
287
514
  def _workspace_skill_sources(source_root: Path) -> list[Path]:
288
515
  candidate_roots = [source_root / "skills", source_root]
289
516
  seen: set[Path] = set()
@@ -419,8 +646,6 @@ def _ensure_env_file(
419
646
  "LIVEKIT_CODEX_THREAD_CWD=~",
420
647
  "# Cartesia voice used by the LiveKit agent TTS.",
421
648
  "CARTESIA_VOICE_ID=9626c31c-bec5-4cca-baa8-f8ba9e84c8bc",
422
- "# Optional comma-separated voices for direct thread routing: voice-id:Display Name.",
423
- "# CARTESIA_SUPER_AGENT_VOICES=f786b574-daa5-4673-aa0c-cbe3e8534c02:Alice",
424
649
  "OPENBASE_CODER_CLI_OAUTH_CLIENT_ID=openbase-coder-cli",
425
650
  ]
426
651
 
@@ -42,6 +42,15 @@ from livekit.agents.types import DEFAULT_API_CONNECT_OPTIONS, NOT_GIVEN
42
42
  from livekit.plugins import assemblyai, cartesia, deepgram, silero
43
43
  from livekit.plugins.turn_detector.multilingual import MultilingualModel
44
44
 
45
+ from openbase_coder_cli.brain_score import (
46
+ brain_score_token_configured,
47
+ brain_score_token_file,
48
+ load_brain_score_token,
49
+ )
50
+ from openbase_coder_cli.cartesia_voice_catalog import (
51
+ DEFAULT_SUPER_AGENT_VOICE_IDS,
52
+ cartesia_voice_for_id,
53
+ )
45
54
  from openbase_coder_cli.dispatcher_config import dispatcher_voice
46
55
  from openbase_coder_cli.livekit_agent.codex_app_client import CodexAppServerClient
47
56
  from openbase_coder_cli.livekit_agent.speech_formatter import format_for_speech
@@ -165,12 +174,7 @@ BRAIN_SCORE_OUTPUT_PATH = Path(
165
174
  str(Path.home() / ".openbase" / "brain_score.json"),
166
175
  )
167
176
  ).expanduser()
168
- BRAIN_SCORE_TOKEN_FILE = Path(
169
- os.getenv(
170
- "OPENBASE_BRAIN_SCORE_TOKEN_FILE",
171
- str(Path.home() / ".openbase" / "brain_score_token"),
172
- )
173
- ).expanduser()
177
+ BRAIN_SCORE_TOKEN_FILE = brain_score_token_file()
174
178
  BRAIN_SCORE_LATITUDE = os.getenv("OPENBASE_BRAIN_SCORE_LATITUDE", "").strip()
175
179
  BRAIN_SCORE_LONGITUDE = os.getenv("OPENBASE_BRAIN_SCORE_LONGITUDE", "").strip()
176
180
 
@@ -181,27 +185,6 @@ class CartesiaVoice:
181
185
  name: str
182
186
 
183
187
 
184
- def _super_agent_voices(env: Mapping[str, str]) -> tuple[CartesiaVoice, ...]:
185
- named_configured = env.get("CARTESIA_SUPER_AGENT_VOICES")
186
- if named_configured is not None:
187
- return _parse_voices(named_configured)
188
-
189
- configured = env.get("CARTESIA_SUPER_AGENT_VOICE_IDS")
190
- if configured is not None:
191
- return _voices_from_ids(_parse_voice_ids(configured))
192
-
193
- dispatcher_voice_id = env.get("CARTESIA_VOICE_ID", DEFAULT_CARTESIA_VOICE_ID)
194
- announcer_voice_id = env.get(
195
- "CARTESIA_ANNOUNCER_VOICE_ID",
196
- DEFAULT_CARTESIA_ANNOUNCER_VOICE_ID,
197
- )
198
- return _voices_from_ids(
199
- voice_id
200
- for voice_id in (announcer_voice_id,)
201
- if voice_id and voice_id != dispatcher_voice_id
202
- )
203
-
204
-
205
188
  def dispatcher_voice_config(
206
189
  *,
207
190
  config_path: str | Path | None = None,
@@ -212,49 +195,27 @@ def dispatcher_voice_config(
212
195
  return CartesiaVoice(voice_id=configured["id"], name=configured["name"])
213
196
 
214
197
 
215
- def _parse_voice_ids(value: str) -> tuple[str, ...]:
216
- return tuple(
217
- voice_id for voice_id in (part.strip() for part in value.split(",")) if voice_id
218
- )
219
-
220
-
221
- def _parse_voices(value: str) -> tuple[CartesiaVoice, ...]:
222
- voices: list[CartesiaVoice] = []
223
- for part in (part.strip() for part in value.split(",")):
224
- if not part:
225
- continue
226
- voice_id, separator, name = part.partition(":")
227
- trimmed_voice_id = voice_id.strip()
228
- if not trimmed_voice_id:
229
- continue
230
- trimmed_name = name.strip() if separator else ""
231
- voices.append(
232
- CartesiaVoice(
233
- voice_id=trimmed_voice_id,
234
- name=trimmed_name or f"Voice {len(voices) + 1}",
235
- )
236
- )
237
- return tuple(voices)
238
-
239
-
240
198
  def _voices_from_ids(voice_ids) -> tuple[CartesiaVoice, ...]:
241
199
  return tuple(
242
- CartesiaVoice(voice_id=voice_id, name=f"Voice {index + 1}")
200
+ CartesiaVoice(
201
+ voice_id=voice_id,
202
+ name=cartesia_voice_for_id(voice_id).name
203
+ if cartesia_voice_for_id(voice_id)
204
+ else f"Voice {index + 1}",
205
+ )
243
206
  for index, voice_id in enumerate(voice_ids)
244
207
  )
245
208
 
246
209
 
247
- CARTESIA_SUPER_AGENT_VOICES = _super_agent_voices(os.environ)
248
- CARTESIA_SUPER_AGENT_VOICE_IDS = tuple(
249
- voice.voice_id for voice in CARTESIA_SUPER_AGENT_VOICES
250
- )
210
+ SUPER_AGENT_VOICE_IDS = DEFAULT_SUPER_AGENT_VOICE_IDS
211
+ SUPER_AGENT_VOICES = _voices_from_ids(SUPER_AGENT_VOICE_IDS)
251
212
 
252
213
 
253
214
  def _current_super_agent_voices() -> tuple[CartesiaVoice, ...]:
254
- voice_ids = tuple(voice.voice_id for voice in CARTESIA_SUPER_AGENT_VOICES)
255
- if voice_ids == tuple(CARTESIA_SUPER_AGENT_VOICE_IDS):
256
- return CARTESIA_SUPER_AGENT_VOICES
257
- return _voices_from_ids(CARTESIA_SUPER_AGENT_VOICE_IDS)
215
+ voice_ids = tuple(voice.voice_id for voice in SUPER_AGENT_VOICES)
216
+ if voice_ids == tuple(SUPER_AGENT_VOICE_IDS):
217
+ return SUPER_AGENT_VOICES
218
+ return _voices_from_ids(SUPER_AGENT_VOICE_IDS)
258
219
 
259
220
 
260
221
  def _normalize_spoken_command(text: str) -> str:
@@ -507,20 +468,11 @@ def _event_text_hash(text: str) -> str:
507
468
 
508
469
 
509
470
  def _load_brain_score_token() -> str:
510
- configured = os.getenv("OPENBASE_BRAIN_SCORE_TOKEN", "").strip()
511
- if configured:
512
- return configured
513
- try:
514
- return BRAIN_SCORE_TOKEN_FILE.read_text(encoding="utf-8").strip()
515
- except FileNotFoundError:
516
- return ""
517
- except OSError:
518
- logger.warning(
519
- "Unable to read brain score token file %s",
520
- BRAIN_SCORE_TOKEN_FILE,
521
- exc_info=True,
522
- )
523
- return ""
471
+ return load_brain_score_token()
472
+
473
+
474
+ def _brain_score_enabled() -> bool:
475
+ return BRAIN_SCORE_ENABLED and brain_score_token_configured()
524
476
 
525
477
 
526
478
  class BrainScoreSTT(livekit_stt.STT):
@@ -2559,12 +2511,12 @@ def _build_stt():
2559
2511
  if LIVEKIT_STT_PROVIDER == "deepgram":
2560
2512
  logger.info("Using Deepgram STT")
2561
2513
  stt = deepgram.STT(api_key=DEEPGRAM_API_KEY)
2562
- stt = BrainScoreSTT(stt) if BRAIN_SCORE_ENABLED else stt
2514
+ stt = BrainScoreSTT(stt) if _brain_score_enabled() else stt
2563
2515
  return LoggingSTT(stt) if LIVEKIT_VERBOSE_LOGGING else stt
2564
2516
  if LIVEKIT_STT_PROVIDER == "assemblyai":
2565
2517
  logger.info("Using AssemblyAI STT")
2566
2518
  stt = assemblyai.STT(api_key=ASSEMBLY_AI_API_KEY)
2567
- stt = BrainScoreSTT(stt) if BRAIN_SCORE_ENABLED else stt
2519
+ stt = BrainScoreSTT(stt) if _brain_score_enabled() else stt
2568
2520
  return LoggingSTT(stt) if LIVEKIT_VERBOSE_LOGGING else stt
2569
2521
 
2570
2522
  raise ValueError(f"Unsupported LIVEKIT_STT_PROVIDER={LIVEKIT_STT_PROVIDER!r}")
@@ -6,13 +6,13 @@ import json
6
6
  import os
7
7
  import time
8
8
  import uuid
9
- from collections.abc import Mapping
10
9
  from dataclasses import asdict, dataclass
11
10
  from pathlib import Path
12
11
 
13
12
  import livekit.api as livekit_api
14
13
 
15
14
  from openbase_coder_cli.cartesia_voice_catalog import (
15
+ DEFAULT_SUPER_AGENT_VOICE_IDS,
16
16
  cartesia_voice_for_id,
17
17
  cartesia_voice_for_name,
18
18
  )
@@ -120,52 +120,6 @@ def instruction_override_supported() -> bool:
120
120
  return True
121
121
 
122
122
 
123
- def _super_agent_voices(env: Mapping[str, str]) -> tuple[CartesiaVoice, ...]:
124
- named_configured = env.get("CARTESIA_SUPER_AGENT_VOICES")
125
- if named_configured is not None:
126
- return _parse_voices(named_configured)
127
-
128
- configured = env.get("CARTESIA_SUPER_AGENT_VOICE_IDS")
129
- if configured is not None:
130
- return _voices_from_ids(_parse_voice_ids(configured))
131
-
132
- dispatcher_voice_id = env.get("CARTESIA_VOICE_ID", DEFAULT_CARTESIA_VOICE_ID)
133
- announcer_voice_id = env.get(
134
- "CARTESIA_ANNOUNCER_VOICE_ID",
135
- DEFAULT_CARTESIA_ANNOUNCER_VOICE_ID,
136
- )
137
- return _voices_from_ids(
138
- voice_id
139
- for voice_id in (announcer_voice_id,)
140
- if voice_id and voice_id != dispatcher_voice_id
141
- )
142
-
143
-
144
- def _parse_voice_ids(value: str) -> tuple[str, ...]:
145
- return tuple(
146
- voice_id for voice_id in (part.strip() for part in value.split(",")) if voice_id
147
- )
148
-
149
-
150
- def _parse_voices(value: str) -> tuple[CartesiaVoice, ...]:
151
- voices: list[CartesiaVoice] = []
152
- for part in (part.strip() for part in value.split(",")):
153
- if not part:
154
- continue
155
- voice_id, separator, name = part.partition(":")
156
- trimmed_voice_id = voice_id.strip()
157
- if not trimmed_voice_id:
158
- continue
159
- trimmed_name = name.strip() if separator else ""
160
- voices.append(
161
- CartesiaVoice(
162
- voice_id=trimmed_voice_id,
163
- name=trimmed_name or f"Voice {len(voices) + 1}",
164
- )
165
- )
166
- return tuple(voices)
167
-
168
-
169
123
  def _voices_from_ids(voice_ids) -> tuple[CartesiaVoice, ...]:
170
124
  return tuple(
171
125
  CartesiaVoice(
@@ -178,17 +132,15 @@ def _voices_from_ids(voice_ids) -> tuple[CartesiaVoice, ...]:
178
132
  )
179
133
 
180
134
 
181
- CARTESIA_SUPER_AGENT_VOICES = _super_agent_voices(os.environ)
182
- CARTESIA_SUPER_AGENT_VOICE_IDS = tuple(
183
- voice.voice_id for voice in CARTESIA_SUPER_AGENT_VOICES
184
- )
135
+ SUPER_AGENT_VOICE_IDS = DEFAULT_SUPER_AGENT_VOICE_IDS
136
+ SUPER_AGENT_VOICES = _voices_from_ids(SUPER_AGENT_VOICE_IDS)
185
137
 
186
138
 
187
139
  def _current_super_agent_voices() -> tuple[CartesiaVoice, ...]:
188
- voice_ids = tuple(voice.voice_id for voice in CARTESIA_SUPER_AGENT_VOICES)
189
- if voice_ids == tuple(CARTESIA_SUPER_AGENT_VOICE_IDS):
190
- return CARTESIA_SUPER_AGENT_VOICES
191
- return _voices_from_ids(CARTESIA_SUPER_AGENT_VOICE_IDS)
140
+ voice_ids = tuple(voice.voice_id for voice in SUPER_AGENT_VOICES)
141
+ if voice_ids == tuple(SUPER_AGENT_VOICE_IDS):
142
+ return SUPER_AGENT_VOICES
143
+ return _voices_from_ids(SUPER_AGENT_VOICE_IDS)
192
144
 
193
145
 
194
146
  def stable_super_agent_voice(
@@ -9,6 +9,8 @@ from typing import Any
9
9
  from rest_framework.decorators import api_view
10
10
  from rest_framework.response import Response
11
11
 
12
+ from openbase_coder_cli.brain_score import brain_score_token_configured
13
+
12
14
 
13
15
  def default_brain_score_output_path() -> Path:
14
16
  return Path(
@@ -54,19 +56,27 @@ def _read_brain_score_file(path: Path) -> dict[str, Any] | None:
54
56
  return payload
55
57
 
56
58
 
59
+ def _unavailable_response(*, disabled_reason: str | None = None) -> dict[str, Any]:
60
+ return {
61
+ "available": False,
62
+ "brain_readiness_score": None,
63
+ "brs": None,
64
+ "parallel_voice_threshold": None,
65
+ "updated_at": None,
66
+ "computed_at": None,
67
+ "chunk_index": None,
68
+ "age_seconds": None,
69
+ "disabled_reason": disabled_reason,
70
+ }
71
+
72
+
57
73
  def build_brain_readiness_response(path: Path | None = None) -> dict[str, Any]:
74
+ if not brain_score_token_configured():
75
+ return _unavailable_response(disabled_reason="missing_token")
76
+
58
77
  payload = _read_brain_score_file(path or default_brain_score_output_path())
59
78
  if payload is None:
60
- return {
61
- "available": False,
62
- "brain_readiness_score": None,
63
- "brs": None,
64
- "parallel_voice_threshold": None,
65
- "updated_at": None,
66
- "computed_at": None,
67
- "chunk_index": None,
68
- "age_seconds": None,
69
- }
79
+ return _unavailable_response()
70
80
 
71
81
  score = _coerce_score(payload.get("brs"))
72
82
  updated_at = payload.get("updated_at")
@@ -83,6 +93,7 @@ def build_brain_readiness_response(path: Path | None = None) -> dict[str, Any]:
83
93
  "computed_at": payload.get("computed_at"),
84
94
  "chunk_index": payload.get("chunk_index"),
85
95
  "age_seconds": age_seconds,
96
+ "disabled_reason": None,
86
97
  }
87
98
 
88
99
 
File without changes
File without changes