nexo-brain 7.25.6 → 7.26.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.
@@ -10,6 +10,7 @@ import shlex
10
10
  import shutil
11
11
  import subprocess
12
12
  import sys
13
+ import tarfile
13
14
  from pathlib import Path
14
15
 
15
16
  from calibration_runtime import load_runtime_calibration
@@ -80,6 +81,7 @@ except Exception:
80
81
 
81
82
 
82
83
  CLAUDE_CODE_NPM_PACKAGE = "@anthropic-ai/claude-code"
84
+ CODEX_NPM_PACKAGE = "@openai/codex"
83
85
  DEFAULT_ASSISTANT_NAME = "Nova"
84
86
  HOOK_TIMEOUTS_BY_EVENT = {
85
87
  "SessionStart": 40,
@@ -235,6 +237,159 @@ def _managed_claude_prefix(user_home: Path | None = None) -> Path:
235
237
  return home / ".nexo" / "runtime" / "bootstrap" / "npm-global"
236
238
 
237
239
 
240
+ def _brain_bundle_root() -> Path:
241
+ return Path(__file__).resolve().parents[1]
242
+
243
+
244
+ def _platform_slug() -> str:
245
+ if sys.platform.startswith("darwin"):
246
+ os_part = "darwin"
247
+ elif sys.platform.startswith("linux"):
248
+ os_part = "linux"
249
+ elif sys.platform.startswith("win"):
250
+ os_part = "win32"
251
+ else:
252
+ os_part = sys.platform
253
+ machine = __import__("platform").machine().lower()
254
+ if machine in {"x86_64", "amd64"}:
255
+ arch = "x64"
256
+ elif machine in {"aarch64", "arm64"}:
257
+ arch = "arm64"
258
+ else:
259
+ arch = machine
260
+ return f"{os_part}-{arch}"
261
+
262
+
263
+ def _install_npm_package_from_bundle(
264
+ *,
265
+ bundle_dir: Path,
266
+ wrapper_pattern: str,
267
+ package_name: str,
268
+ managed_prefix: Path,
269
+ env: dict[str, str],
270
+ attempts: list[str],
271
+ ) -> bool:
272
+ if not bundle_dir.is_dir():
273
+ return False
274
+ all_tgz = sorted(item for item in bundle_dir.iterdir() if item.name.endswith(".tgz"))
275
+ wrapper_re = re.compile(wrapper_pattern)
276
+ wrapper = next((item for item in all_tgz if wrapper_re.match(item.name)), None)
277
+ if not wrapper:
278
+ return False
279
+ slug = _platform_slug()
280
+ native_packs = [
281
+ item
282
+ for item in all_tgz
283
+ if item != wrapper and (f"-{slug}.tgz" in item.name or f"-{slug}-" in item.name)
284
+ ]
285
+ tgz_paths = [wrapper, *native_packs] if native_packs else [wrapper]
286
+ desktop_node, bundled_npm_cli = _bundled_npm_runtime()
287
+ if desktop_node and bundled_npm_cli:
288
+ cmd = [
289
+ desktop_node,
290
+ bundled_npm_cli,
291
+ "install",
292
+ "-g",
293
+ "--prefix",
294
+ str(managed_prefix),
295
+ "--offline",
296
+ "--no-audit",
297
+ "--no-fund",
298
+ *(str(item) for item in tgz_paths),
299
+ ]
300
+ run_env = {**env, "ELECTRON_RUN_AS_NODE": "1"}
301
+ else:
302
+ cmd = [
303
+ "npm",
304
+ "install",
305
+ "-g",
306
+ "--prefix",
307
+ str(managed_prefix),
308
+ "--offline",
309
+ "--no-audit",
310
+ "--no-fund",
311
+ *(str(item) for item in tgz_paths),
312
+ ]
313
+ run_env = env
314
+ try:
315
+ install = subprocess.run(
316
+ cmd,
317
+ capture_output=True,
318
+ text=True,
319
+ timeout=300,
320
+ env=run_env,
321
+ )
322
+ if install.returncode == 0:
323
+ return True
324
+ attempts.append((install.stderr or install.stdout or f"{package_name} bundled install failed").strip())
325
+ except Exception as exc:
326
+ attempts.append(f"{package_name} bundled install failed: {exc}")
327
+ return False
328
+
329
+
330
+ def _install_codex_vendor_from_bundle(*, bundle_dir: Path, managed_prefix: Path, attempts: list[str]) -> bool:
331
+ package_roots = [
332
+ managed_prefix / "lib" / "node_modules" / "@openai" / "codex",
333
+ managed_prefix / "node_modules" / "@openai" / "codex",
334
+ ]
335
+ package_root = next((item for item in package_roots if item.exists()), package_roots[0])
336
+ if not package_root.exists() or not bundle_dir.is_dir():
337
+ return False
338
+ slug = _platform_slug()
339
+ native_packs = sorted(
340
+ item for item in bundle_dir.iterdir()
341
+ if item.name.endswith(".tgz") and (f"-{slug}.tgz" in item.name or f"-{slug}-" in item.name)
342
+ )
343
+ if not native_packs:
344
+ attempts.append(f"no bundled Codex native vendor found for {slug}")
345
+ return False
346
+ vendor_dest = package_root / "vendor"
347
+ vendor_dest.mkdir(parents=True, exist_ok=True)
348
+ for pack in native_packs:
349
+ try:
350
+ with tarfile.open(pack, "r:gz") as archive:
351
+ members = [member for member in archive.getmembers() if member.name.startswith("package/vendor/")]
352
+ for member in members:
353
+ relative = Path(member.name).relative_to("package/vendor")
354
+ target = vendor_dest / relative
355
+ if member.isdir():
356
+ target.mkdir(parents=True, exist_ok=True)
357
+ continue
358
+ if not member.isfile():
359
+ continue
360
+ target.parent.mkdir(parents=True, exist_ok=True)
361
+ source = archive.extractfile(member)
362
+ if source is None:
363
+ continue
364
+ with source, target.open("wb") as out:
365
+ shutil.copyfileobj(source, out)
366
+ try:
367
+ target.chmod(member.mode)
368
+ except Exception:
369
+ pass
370
+ return True
371
+ except Exception as exc:
372
+ attempts.append(f"Codex native vendor extract failed for {pack.name}: {exc}")
373
+ return False
374
+
375
+
376
+ def _codex_vendor_present(managed_prefix: Path) -> bool:
377
+ package_roots = [
378
+ managed_prefix / "lib" / "node_modules" / "@openai" / "codex",
379
+ managed_prefix / "node_modules" / "@openai" / "codex",
380
+ ]
381
+ for package_root in package_roots:
382
+ vendor_root = package_root / "vendor"
383
+ if not vendor_root.exists():
384
+ continue
385
+ try:
386
+ if any(candidate.is_file() for candidate in vendor_root.rglob("bin/codex*")):
387
+ return True
388
+ except Exception:
389
+ continue
390
+ return False
391
+
392
+
238
393
  def _desktop_product_requested(user_home: Path | None = None) -> bool:
239
394
  if str(os.environ.get("NEXO_DESKTOP_MANAGED", "")).strip() == "1":
240
395
  return True
@@ -289,6 +444,13 @@ def _installed_client_path(client_key: str, *, user_home: Path | None = None) ->
289
444
  return ""
290
445
 
291
446
 
447
+ def _sync_codex_binary(home_path: Path) -> str:
448
+ if _desktop_product_requested(home_path):
449
+ info = detect_installed_clients(user_home=home_path).get("codex", {})
450
+ return str(info.get("path") or "") if info.get("installed") else ""
451
+ return shutil.which("codex") or ""
452
+
453
+
292
454
  def ensure_claude_code_installed(*, user_home: str | os.PathLike[str] | None = None) -> dict:
293
455
  home_path = Path(user_home).expanduser() if user_home else _user_home()
294
456
  desktop_managed = _desktop_product_requested(home_path)
@@ -471,6 +633,167 @@ def ensure_claude_code_installed(*, user_home: str | os.PathLike[str] | None = N
471
633
  }
472
634
 
473
635
 
636
+ def ensure_codex_installed(*, user_home: str | os.PathLike[str] | None = None) -> dict:
637
+ home_path = Path(user_home).expanduser() if user_home else _user_home()
638
+ desktop_managed = _desktop_product_requested(home_path)
639
+ managed_prefix = _managed_claude_prefix(home_path)
640
+ existing = _installed_client_path("codex", user_home=home_path)
641
+ if existing and (not desktop_managed or _codex_vendor_present(managed_prefix)):
642
+ return {
643
+ "ok": True,
644
+ "client": "codex",
645
+ "installed": True,
646
+ "changed": False,
647
+ "action": "already_installed_managed" if desktop_managed else "already_installed",
648
+ "path": existing,
649
+ "attempts": [],
650
+ }
651
+
652
+ attempts: list[str] = []
653
+ if existing and desktop_managed:
654
+ attempts.append("managed Codex wrapper exists but native vendor is missing; repairing bundled vendor")
655
+ env = _cli_install_env(home_path)
656
+ env.setdefault("npm_config_prefix", str(managed_prefix))
657
+ env["PATH"] = os.pathsep.join(
658
+ [str(managed_prefix / "bin"), *(item for item in str(env.get("PATH", "")).split(os.pathsep) if item)]
659
+ )
660
+
661
+ desktop_node, bundled_npm_cli = _bundled_npm_runtime()
662
+ bundle_dir = _brain_bundle_root() / "codex"
663
+ if desktop_managed and not (desktop_node and bundled_npm_cli):
664
+ vendor_installed = _install_codex_vendor_from_bundle(
665
+ bundle_dir=bundle_dir,
666
+ managed_prefix=managed_prefix,
667
+ attempts=attempts,
668
+ )
669
+ installed_after_vendor = _installed_client_path("codex", user_home=home_path)
670
+ if installed_after_vendor and vendor_installed:
671
+ return {
672
+ "ok": True,
673
+ "client": "codex",
674
+ "installed": True,
675
+ "changed": True,
676
+ "action": "installed_bundled_vendor",
677
+ "path": installed_after_vendor,
678
+ "attempts": attempts,
679
+ }
680
+ return {
681
+ "ok": False,
682
+ "client": "codex",
683
+ "installed": False,
684
+ "changed": False,
685
+ "action": "failed",
686
+ "path": "",
687
+ "attempts": attempts,
688
+ "error": (
689
+ "Desktop-managed Codex install requires the NEXO Desktop bundled Codex runtime; "
690
+ "global `npm -g` fallbacks are disabled."
691
+ ),
692
+ }
693
+
694
+ wrapper_installed = _install_npm_package_from_bundle(
695
+ bundle_dir=bundle_dir,
696
+ wrapper_pattern=r"^openai-codex-\d+\.\d+\.\d+\.tgz$",
697
+ package_name=CODEX_NPM_PACKAGE,
698
+ managed_prefix=managed_prefix,
699
+ env=env,
700
+ attempts=attempts,
701
+ )
702
+ vendor_installed = _install_codex_vendor_from_bundle(bundle_dir=bundle_dir, managed_prefix=managed_prefix, attempts=attempts)
703
+ installed_after_bundle = _installed_client_path("codex", user_home=home_path)
704
+ if installed_after_bundle and vendor_installed:
705
+ return {
706
+ "ok": True,
707
+ "client": "codex",
708
+ "installed": True,
709
+ "changed": True,
710
+ "action": "installed_via_bundled_tarballs",
711
+ "path": installed_after_bundle,
712
+ "attempts": attempts,
713
+ }
714
+ if wrapper_installed and not vendor_installed:
715
+ attempts.append("bundled Codex wrapper installed but native vendor extraction failed; falling back to online install")
716
+
717
+ if desktop_node and bundled_npm_cli:
718
+ try:
719
+ install = subprocess.run(
720
+ [
721
+ desktop_node,
722
+ bundled_npm_cli,
723
+ "install",
724
+ "-g",
725
+ "--prefix",
726
+ str(managed_prefix),
727
+ CODEX_NPM_PACKAGE,
728
+ ],
729
+ capture_output=True,
730
+ text=True,
731
+ timeout=300,
732
+ env={**env, "ELECTRON_RUN_AS_NODE": "1"},
733
+ )
734
+ if install.returncode != 0:
735
+ attempts.append((install.stderr or install.stdout or "bundled npm install failed").strip())
736
+ except Exception as exc:
737
+ attempts.append(f"bundled npm install failed: {exc}")
738
+ installed_after_npm = _installed_client_path("codex", user_home=home_path)
739
+ if installed_after_npm and (not desktop_managed or _codex_vendor_present(managed_prefix)):
740
+ return {
741
+ "ok": True,
742
+ "client": "codex",
743
+ "installed": True,
744
+ "changed": True,
745
+ "action": "installed_via_bundled_npm",
746
+ "path": installed_after_npm,
747
+ "attempts": attempts,
748
+ }
749
+ if installed_after_npm and desktop_managed:
750
+ attempts.append("managed Codex wrapper installed but native vendor is missing after bundled npm install")
751
+
752
+ if desktop_managed:
753
+ error = (
754
+ "Desktop-managed Codex install did not produce the managed "
755
+ "`~/.nexo/runtime/bootstrap/npm-global/bin/codex` binary."
756
+ )
757
+ return {
758
+ "ok": False,
759
+ "client": "codex",
760
+ "installed": False,
761
+ "changed": False,
762
+ "action": "failed",
763
+ "path": "",
764
+ "attempts": attempts,
765
+ "error": error,
766
+ }
767
+
768
+ install_error = ""
769
+ try:
770
+ install = subprocess.run(
771
+ ["npm", "install", "-g", "--prefix", str(managed_prefix), CODEX_NPM_PACKAGE],
772
+ capture_output=True,
773
+ text=True,
774
+ timeout=180,
775
+ env=env,
776
+ )
777
+ if install.returncode != 0:
778
+ install_error = (install.stderr or install.stdout or "npm install failed").strip()
779
+ attempts.append(install_error)
780
+ except Exception as exc:
781
+ install_error = f"npm install failed: {exc}"
782
+ attempts.append(install_error)
783
+
784
+ installed_path = _installed_client_path("codex", user_home=home_path)
785
+ return {
786
+ "ok": bool(installed_path),
787
+ "client": "codex",
788
+ "installed": bool(installed_path),
789
+ "changed": bool(installed_path),
790
+ "action": "installed" if installed_path else "failed",
791
+ "path": installed_path or "",
792
+ "attempts": attempts,
793
+ **({} if installed_path else {"error": install_error or "Codex install did not produce a `codex` binary in PATH"}),
794
+ }
795
+
796
+
474
797
  def build_server_config(
475
798
  *,
476
799
  nexo_home: str | os.PathLike[str] | None = None,
@@ -1334,7 +1657,7 @@ def sync_codex(
1334
1657
  operator_name=operator_name,
1335
1658
  client="codex",
1336
1659
  )
1337
- codex_bin = shutil.which("codex")
1660
+ codex_bin = _sync_codex_binary(home_path)
1338
1661
  config_path = _codex_config_path(home_path)
1339
1662
  hooks_path = _codex_hooks_path(home_path)
1340
1663
  if not codex_bin:
@@ -1423,6 +1746,7 @@ def sync_all_clients(
1423
1746
  enabled_clients: list[str] | tuple[str, ...] | set[str] | None = None,
1424
1747
  preferences: dict | None = None,
1425
1748
  auto_install_missing_claude: bool = False,
1749
+ auto_install_missing_codex: bool = False,
1426
1750
  ) -> dict:
1427
1751
  nexo_home_path = Path(nexo_home).expanduser() if nexo_home else _default_nexo_home()
1428
1752
  guardian_runtime_surfaces_result: dict = {
@@ -1469,6 +1793,8 @@ def sync_all_clients(
1469
1793
  install_results: dict[str, dict] = {}
1470
1794
  if auto_install_missing_claude and "claude_code" in enabled_set:
1471
1795
  install_results["claude_code"] = ensure_claude_code_installed(user_home=user_home)
1796
+ if auto_install_missing_codex and "codex" in enabled_set:
1797
+ install_results["codex"] = ensure_codex_installed(user_home=user_home)
1472
1798
 
1473
1799
  def _safe(label: str, fn) -> dict:
1474
1800
  if label not in enabled_set:
package/src/db/_schema.py CHANGED
@@ -1117,6 +1117,28 @@ def _m41_automation_sessions_columns(conn):
1117
1117
  )
1118
1118
 
1119
1119
 
1120
+ def _m69_provider_runtime_metadata(conn):
1121
+ """Add provider/runtime metadata required for Anthropic/OpenAI parity."""
1122
+ if not _table_exists(conn, "automation_runs"):
1123
+ _m28_automation_runs(conn)
1124
+ if not _table_exists(conn, "cron_runs"):
1125
+ _m17_cron_runs(conn)
1126
+
1127
+ if _table_exists(conn, "automation_runs"):
1128
+ _migrate_add_column(conn, "automation_runs", "provider", "TEXT DEFAULT ''")
1129
+ _migrate_add_column(conn, "automation_runs", "runtime_version", "TEXT DEFAULT ''")
1130
+ _migrate_add_column(conn, "automation_runs", "runtime_session_id", "TEXT DEFAULT ''")
1131
+ _migrate_add_index(conn, "idx_automation_runs_provider", "automation_runs", "provider")
1132
+ if _table_exists(conn, "cron_runs"):
1133
+ _migrate_add_column(conn, "cron_runs", "provider", "TEXT DEFAULT ''")
1134
+ _migrate_add_column(conn, "cron_runs", "backend", "TEXT DEFAULT ''")
1135
+ _migrate_add_column(conn, "cron_runs", "runtime_snapshot", "TEXT DEFAULT '{}'")
1136
+ _migrate_add_index(conn, "idx_cron_runs_provider", "cron_runs", "provider")
1137
+ if _table_exists(conn, "sessions"):
1138
+ _migrate_add_column(conn, "sessions", "session_provider", "TEXT DEFAULT ''")
1139
+ _migrate_add_index(conn, "idx_sessions_provider", "sessions", "session_provider")
1140
+
1141
+
1120
1142
  def _m42_v6_0_1_hotfix(conn):
1121
1143
  """v6.0.1 hotfix — last_heartbeat_ts on sessions + hook_inbox_reminders.
1122
1144
 
@@ -2247,6 +2269,7 @@ MIGRATIONS = [
2247
2269
  (66, "transcript_index", _m66_transcript_index),
2248
2270
  (67, "diary_quality_backfill_repair", _m67_diary_quality_backfill_repair),
2249
2271
  (68, "memory_fabric_index", _m68_memory_fabric_index),
2272
+ (69, "provider_runtime_metadata", _m69_provider_runtime_metadata),
2250
2273
  ]
2251
2274
 
2252
2275
 
@@ -19,6 +19,16 @@ import re
19
19
  _SID_EXACT = re.compile(r'^nexo-\d+-\d+$')
20
20
  _SID_SEARCH = re.compile(r'nexo-\d+-\d+')
21
21
 
22
+
23
+ def _provider_from_session_client(value: str | None) -> str:
24
+ candidate = str(value or "").strip().lower().replace("-", "_").replace(" ", "_")
25
+ if candidate in {"anthropic", "claude", "claude_code", "claudecode"}:
26
+ return "anthropic"
27
+ if candidate in {"openai", "codex", "openai_codex"}:
28
+ return "openai"
29
+ return ""
30
+
31
+
22
32
  def _validate_sid(sid: str) -> str:
23
33
  """Validate and sanitize SID. Extracts clean SID if embedded in text."""
24
34
  if not sid:
@@ -41,6 +51,7 @@ def register_session(
41
51
  *,
42
52
  external_session_id: str = "",
43
53
  session_client: str = "",
54
+ session_provider: str = "",
44
55
  conversation_id: str = "",
45
56
  ) -> dict:
46
57
  """Register or re-register a session."""
@@ -48,27 +59,49 @@ def register_session(
48
59
  conn = get_db()
49
60
  now = now_epoch()
50
61
  linked_session_id = (external_session_id or claude_session_id or "").strip()
51
- conn.execute(
52
- "INSERT OR REPLACE INTO sessions (sid, task, started_epoch, last_update_epoch, local_time, claude_session_id, external_session_id, session_client, conversation_id) "
53
- "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
54
- (
55
- sid,
56
- task,
57
- now,
58
- now,
59
- local_time_str(),
60
- linked_session_id,
61
- linked_session_id,
62
- (session_client or "").strip(),
63
- (conversation_id or "").strip(),
62
+ provider = _provider_from_session_client(session_provider) or _provider_from_session_client(session_client)
63
+ try:
64
+ conn.execute(
65
+ "INSERT OR REPLACE INTO sessions (sid, task, started_epoch, last_update_epoch, local_time, claude_session_id, external_session_id, session_client, session_provider, conversation_id) "
66
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
67
+ (
68
+ sid,
69
+ task,
70
+ now,
71
+ now,
72
+ local_time_str(),
73
+ linked_session_id,
74
+ linked_session_id,
75
+ (session_client or "").strip(),
76
+ provider,
77
+ (conversation_id or "").strip(),
78
+ )
79
+ )
80
+ except sqlite3.OperationalError as exc:
81
+ if "session_provider" not in str(exc):
82
+ raise
83
+ conn.execute(
84
+ "INSERT OR REPLACE INTO sessions (sid, task, started_epoch, last_update_epoch, local_time, claude_session_id, external_session_id, session_client, conversation_id) "
85
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
86
+ (
87
+ sid,
88
+ task,
89
+ now,
90
+ now,
91
+ local_time_str(),
92
+ linked_session_id,
93
+ linked_session_id,
94
+ (session_client or "").strip(),
95
+ (conversation_id or "").strip(),
96
+ )
64
97
  )
65
- )
66
98
  conn.commit()
67
99
  return {
68
100
  "sid": sid,
69
101
  "task": task,
70
102
  "external_session_id": linked_session_id,
71
103
  "session_client": (session_client or "").strip(),
104
+ "session_provider": provider,
72
105
  "conversation_id": (conversation_id or "").strip(),
73
106
  }
74
107
 
@@ -115,11 +148,20 @@ def get_active_sessions() -> list[dict]:
115
148
  """Get all sessions updated within STALE threshold."""
116
149
  conn = get_db()
117
150
  cutoff = now_epoch() - SESSION_STALE_SECONDS
118
- rows = conn.execute(
119
- "SELECT sid, task, started_epoch, last_update_epoch, local_time, conversation_id "
120
- "FROM sessions WHERE last_update_epoch > ?",
121
- (cutoff,)
122
- ).fetchall()
151
+ try:
152
+ rows = conn.execute(
153
+ "SELECT sid, task, started_epoch, last_update_epoch, local_time, conversation_id, session_provider "
154
+ "FROM sessions WHERE last_update_epoch > ?",
155
+ (cutoff,)
156
+ ).fetchall()
157
+ except sqlite3.OperationalError as exc:
158
+ if "session_provider" not in str(exc):
159
+ raise
160
+ rows = conn.execute(
161
+ "SELECT sid, task, started_epoch, last_update_epoch, local_time, conversation_id "
162
+ "FROM sessions WHERE last_update_epoch > ?",
163
+ (cutoff,)
164
+ ).fetchall()
123
165
  return [dict(r) for r in rows]
124
166
 
125
167
 
@@ -248,11 +290,20 @@ def search_sessions(keyword: str) -> list[dict]:
248
290
  """Find sessions whose task contains keyword (case-insensitive)."""
249
291
  conn = get_db()
250
292
  cutoff = now_epoch() - SESSION_STALE_SECONDS
251
- rows = conn.execute(
252
- "SELECT sid, task, last_update_epoch, local_time, conversation_id FROM sessions "
253
- "WHERE last_update_epoch > ? AND LOWER(task) LIKE ?",
254
- (cutoff, f"%{keyword.lower()}%")
255
- ).fetchall()
293
+ try:
294
+ rows = conn.execute(
295
+ "SELECT sid, task, last_update_epoch, local_time, conversation_id, session_provider FROM sessions "
296
+ "WHERE last_update_epoch > ? AND LOWER(task) LIKE ?",
297
+ (cutoff, f"%{keyword.lower()}%")
298
+ ).fetchall()
299
+ except sqlite3.OperationalError as exc:
300
+ if "session_provider" not in str(exc):
301
+ raise
302
+ rows = conn.execute(
303
+ "SELECT sid, task, last_update_epoch, local_time, conversation_id FROM sessions "
304
+ "WHERE last_update_epoch > ? AND LOWER(task) LIKE ?",
305
+ (cutoff, f"%{keyword.lower()}%")
306
+ ).fetchall()
256
307
  return [dict(r) for r in rows]
257
308
 
258
309
 
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+ """Provider-runtime contract for Anthropic/Claude Code and OpenAI/Codex."""
4
+
5
+ from client_preferences import (
6
+ BACKEND_NONE,
7
+ CLIENT_CLAUDE_CODE,
8
+ CLIENT_CODEX,
9
+ CLIENT_TO_PROVIDER,
10
+ PROVIDER_ANTHROPIC,
11
+ PROVIDER_NONE,
12
+ PROVIDER_OPENAI,
13
+ PROVIDER_TO_CLIENT,
14
+ client_to_provider,
15
+ default_provider_runtime,
16
+ normalize_provider_key,
17
+ normalize_provider_runtime,
18
+ provider_to_client,
19
+ resolve_automation_provider,
20
+ resolve_selected_chat_provider,
21
+ )
22
+
23
+ __all__ = [
24
+ "BACKEND_NONE",
25
+ "CLIENT_CLAUDE_CODE",
26
+ "CLIENT_CODEX",
27
+ "CLIENT_TO_PROVIDER",
28
+ "PROVIDER_ANTHROPIC",
29
+ "PROVIDER_NONE",
30
+ "PROVIDER_OPENAI",
31
+ "PROVIDER_TO_CLIENT",
32
+ "client_to_provider",
33
+ "default_provider_runtime",
34
+ "normalize_provider_key",
35
+ "normalize_provider_runtime",
36
+ "provider_to_client",
37
+ "resolve_automation_provider",
38
+ "resolve_selected_chat_provider",
39
+ ]
@@ -318,6 +318,7 @@ def analyze_session(
318
318
  output_format="text",
319
319
  append_system_prompt=json_system_prompt,
320
320
  allowed_tools="Read,Grep,Bash",
321
+ bare_mode=False,
321
322
  )
322
323
 
323
324
  if result.returncode != 0:
@@ -348,6 +349,7 @@ def analyze_session(
348
349
  timeout=120,
349
350
  output_format="text",
350
351
  append_system_prompt=json_system_prompt,
352
+ bare_mode=False,
351
353
  )
352
354
  if convert_result.returncode == 0:
353
355
  debug_output = convert_result.stdout
@@ -247,6 +247,7 @@ def main():
247
247
  timeout=CLAUDE_TIMEOUT,
248
248
  output_format="text",
249
249
  allowed_tools="Read,Grep,Bash",
250
+ bare_mode=False,
250
251
  )
251
252
 
252
253
  if result.returncode != 0: