caura-bus-cli 0.3.1__tar.gz → 0.3.4__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 (21) hide show
  1. {caura_bus_cli-0.3.1/src/caura_bus_cli.egg-info → caura_bus_cli-0.3.4}/PKG-INFO +3 -3
  2. {caura_bus_cli-0.3.1 → caura_bus_cli-0.3.4}/pyproject.toml +3 -3
  3. {caura_bus_cli-0.3.1 → caura_bus_cli-0.3.4}/src/caura_bus_cli/hooks.py +2 -2
  4. {caura_bus_cli-0.3.1 → caura_bus_cli-0.3.4}/src/caura_bus_cli/main.py +4 -4
  5. {caura_bus_cli-0.3.1 → caura_bus_cli-0.3.4}/src/caura_bus_cli/runtime.py +4 -4
  6. {caura_bus_cli-0.3.1 → caura_bus_cli-0.3.4}/src/caura_bus_cli/setup.py +81 -124
  7. {caura_bus_cli-0.3.1 → caura_bus_cli-0.3.4/src/caura_bus_cli.egg-info}/PKG-INFO +3 -3
  8. caura_bus_cli-0.3.4/src/caura_bus_cli.egg-info/requires.txt +3 -0
  9. caura_bus_cli-0.3.1/src/caura_bus_cli.egg-info/requires.txt +0 -3
  10. {caura_bus_cli-0.3.1 → caura_bus_cli-0.3.4}/LICENSE +0 -0
  11. {caura_bus_cli-0.3.1 → caura_bus_cli-0.3.4}/NOTICE +0 -0
  12. {caura_bus_cli-0.3.1 → caura_bus_cli-0.3.4}/setup.cfg +0 -0
  13. {caura_bus_cli-0.3.1 → caura_bus_cli-0.3.4}/src/caura_bus_cli/__init__.py +0 -0
  14. {caura_bus_cli-0.3.1 → caura_bus_cli-0.3.4}/src/caura_bus_cli/broker.py +0 -0
  15. {caura_bus_cli-0.3.1 → caura_bus_cli-0.3.4}/src/caura_bus_cli/join.py +0 -0
  16. {caura_bus_cli-0.3.1 → caura_bus_cli-0.3.4}/src/caura_bus_cli/peer_instructions.md +0 -0
  17. {caura_bus_cli-0.3.1 → caura_bus_cli-0.3.4}/src/caura_bus_cli/py.typed +0 -0
  18. {caura_bus_cli-0.3.1 → caura_bus_cli-0.3.4}/src/caura_bus_cli.egg-info/SOURCES.txt +0 -0
  19. {caura_bus_cli-0.3.1 → caura_bus_cli-0.3.4}/src/caura_bus_cli.egg-info/dependency_links.txt +0 -0
  20. {caura_bus_cli-0.3.1 → caura_bus_cli-0.3.4}/src/caura_bus_cli.egg-info/entry_points.txt +0 -0
  21. {caura_bus_cli-0.3.1 → caura_bus_cli-0.3.4}/src/caura_bus_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: caura-bus-cli
3
- Version: 0.3.1
3
+ Version: 0.3.4
4
4
  Summary: Caura messaging CLI: setup / doctor / replay / agents / threads / send / status
5
5
  Author: Caura, Inc.
6
6
  License: Apache-2.0
@@ -10,7 +10,7 @@ Project-URL: Changelog, https://github.com/caura-ai/caura/blob/main/clients/coll
10
10
  Requires-Python: >=3.12
11
11
  License-File: LICENSE
12
12
  License-File: NOTICE
13
- Requires-Dist: caura-bus-core==0.3.1
14
- Requires-Dist: caura-bus-mcp==0.3.1
13
+ Requires-Dist: caura-bus-core==0.3.4
14
+ Requires-Dist: caura-bus-mcp==0.3.4
15
15
  Requires-Dist: typer>=0.12
16
16
  Dynamic: license-file
@@ -1,14 +1,14 @@
1
1
  [project]
2
2
  name = "caura-bus-cli"
3
- version = "0.3.1" # x-release-please-version
3
+ version = "0.3.4" # x-release-please-version
4
4
  description = "Caura messaging CLI: setup / doctor / replay / agents / threads / send / status"
5
5
  requires-python = ">=3.12"
6
6
  license = { text = "Apache-2.0" }
7
7
  authors = [{ name = "Caura, Inc." }]
8
8
  dependencies = [
9
- "caura-bus-core==0.3.1", # x-release-please-version
9
+ "caura-bus-core==0.3.4", # x-release-please-version
10
10
  # `caura-bus setup` wires this MCP server into Claude Code / Codex.
11
- "caura-bus-mcp==0.3.1", # x-release-please-version
11
+ "caura-bus-mcp==0.3.4", # x-release-please-version
12
12
  "typer>=0.12",
13
13
  ]
14
14
 
@@ -102,7 +102,7 @@ def install_hooks(
102
102
  written[event] = entry
103
103
  path.parent.mkdir(parents=True, exist_ok=True)
104
104
  temporary = path.with_suffix(".json.tmp")
105
- temporary.write_text(json.dumps(settings, indent=2) + "\n")
105
+ temporary.write_text(json.dumps(settings, indent=2) + "\n", encoding="utf-8")
106
106
  temporary.replace(path)
107
107
  return {"path": str(path), "hooks": written}
108
108
 
@@ -117,7 +117,7 @@ def load_settings(path):
117
117
  """
118
118
  if not path.exists():
119
119
  return {}
120
- text = path.read_text()
120
+ text = path.read_text(encoding="utf-8")
121
121
  if not text.strip():
122
122
  return {}
123
123
  try:
@@ -238,7 +238,7 @@ def setup(
238
238
  def tty_prompt(label: str, secret: bool = False) -> str:
239
239
  """Prompt on the controlling terminal, also when stdin is a pipe (``curl ... | sh``)."""
240
240
  try:
241
- tty = open("/dev/tty", "r+")
241
+ tty = open("/dev/tty", "r+", encoding="locale")
242
242
  except OSError:
243
243
  raise JoinError(f"no terminal to ask for the {label.lower()}") from None
244
244
  with tty:
@@ -275,12 +275,12 @@ def join(
275
275
  (`caura`) is installed, join prints the equivalent `caura agent connect`
276
276
  command and offers to run it instead.
277
277
  """
278
+ # Remove sign-in secrets before broker discovery or any other child process.
279
+ password = os.environ.pop("CAURA_PASSWORD", "")
280
+ totp_env = os.environ.pop("CAURA_TOTP_CODE", "").strip()
278
281
  if broker is not False:
279
282
  prefer_broker(url, name, runtime, directory, description, tenant, not no_hooks, broker, not no_memory)
280
283
  try:
281
- # Take the secrets out of the environment so no child process (claude mcp add) inherits them.
282
- password = os.environ.pop("CAURA_PASSWORD", "")
283
- totp_env = os.environ.pop("CAURA_TOTP_CODE", "").strip()
284
284
  if not email:
285
285
  email = tty_prompt("Caura email")
286
286
  password = password or tty_prompt("Caura password", secret=True)
@@ -95,12 +95,12 @@ class WakeState:
95
95
  self.recheck_seconds = recheck_seconds
96
96
 
97
97
  def load(self):
98
- return json.loads(self.path.read_text()) if self.path.exists() else {}
98
+ return json.loads(self.path.read_text(encoding="utf-8")) if self.path.exists() else {}
99
99
 
100
100
  def save(self, data):
101
101
  temporary = self.path.with_suffix(".tmp")
102
102
  fd = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
103
- with os.fdopen(fd, "w") as stream:
103
+ with os.fdopen(fd, "w", encoding="utf-8") as stream:
104
104
  json.dump(data, stream)
105
105
  stream.flush()
106
106
  os.fsync(stream.fileno())
@@ -212,7 +212,7 @@ def latest_codex_thread(directory, home=None):
212
212
  rollouts = sorted(sessions.rglob("rollout-*.jsonl"), key=lambda p: p.name, reverse=True)
213
213
  for rollout in rollouts:
214
214
  try:
215
- with rollout.open() as stream:
215
+ with rollout.open(encoding="utf-8") as stream:
216
216
  meta = json.loads(stream.readline())
217
217
  except (OSError, ValueError):
218
218
  continue
@@ -548,7 +548,7 @@ def load_key(key_file=None):
548
548
  path = Path(key_file)
549
549
  if path.stat().st_mode & 0o077:
550
550
  raise ValueError(f"{path} must be private to you (chmod 600 {path})")
551
- key = path.read_text().strip()
551
+ key = path.read_text(encoding="utf-8").strip()
552
552
  if not key:
553
553
  return False
554
554
  os.environ["CAURA_API_KEY"] = key
@@ -14,8 +14,8 @@ import os
14
14
  import re
15
15
  import shlex
16
16
  import shutil
17
- import subprocess
18
17
  import sys
18
+ import tempfile
19
19
  import tomllib
20
20
  from collections.abc import Callable
21
21
  from dataclasses import dataclass, field
@@ -130,7 +130,7 @@ def find_mcp_binary() -> Path | None:
130
130
 
131
131
 
132
132
  def peer_template() -> str:
133
- return files("caura_bus_cli").joinpath("peer_instructions.md").read_text()
133
+ return files("caura_bus_cli").joinpath("peer_instructions.md").read_text(encoding="utf-8")
134
134
 
135
135
 
136
136
  # --- 1. verify the key ----------------------------------------------------
@@ -166,7 +166,7 @@ def render_config(url: str, insecure: bool, identity: dict, existing: Path) -> s
166
166
  consultation: dict[str, Any] = {}
167
167
  if existing.is_file():
168
168
  try:
169
- old = tomllib.loads(existing.read_text())
169
+ old = tomllib.loads(existing.read_text(encoding="utf-8"))
170
170
  except (tomllib.TOMLDecodeError, OSError):
171
171
  old = {}
172
172
  if isinstance(old.get("peers"), list):
@@ -200,69 +200,75 @@ def render_config(url: str, insecure: bool, identity: dict, existing: Path) -> s
200
200
 
201
201
  def write_private(path: Path, text: str) -> None:
202
202
  path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
203
- temporary = path.with_name(path.name + ".tmp")
204
- fd = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
205
- with os.fdopen(fd, "w") as handle:
206
- handle.write(text)
207
- os.chmod(temporary, 0o600)
208
- temporary.replace(path)
203
+ fd, temporary = tempfile.mkstemp(dir=path.parent, prefix=".caura-bus-")
204
+ try:
205
+ with os.fdopen(fd, "w", encoding="utf-8") as handle:
206
+ os.fchmod(handle.fileno(), 0o600)
207
+ handle.write(text)
208
+ handle.flush()
209
+ os.fsync(handle.fileno())
210
+ os.replace(temporary, path)
211
+ finally:
212
+ if os.path.exists(temporary):
213
+ os.unlink(temporary)
209
214
 
210
215
 
211
216
  # --- 3. runtime wiring ----------------------------------------------------
212
217
 
213
218
 
214
- def claude_add_argv(claude: str, key: str, config: Path, mcp: Path) -> list[str]:
215
- return [
216
- claude,
217
- "mcp",
218
- "add",
219
- "--scope",
220
- "local",
221
- SERVER_NAME,
222
- "-e",
223
- f"CAURA_API_KEY={key}",
224
- "-e",
225
- f"CAURA_BUS_AGENT_CONFIG={config}",
226
- "--",
227
- str(mcp),
228
- ]
229
-
230
-
231
- def claude_manual_command(directory: Path, config: Path, mcp: Path) -> str:
232
- """The exact command to run by hand; the key comes from the user's environment."""
233
- argv = claude_add_argv("claude", "KEYPLACEHOLDER", config, mcp)
234
- command = shlex.join(argv).replace(
235
- "CAURA_API_KEY=KEYPLACEHOLDER", '"CAURA_API_KEY=${CAURA_API_KEY:?export your agent key first}"'
236
- )
237
- return f"cd {shlex.quote(str(directory))} && {command}"
238
-
239
-
240
- def wire_claude(directory: Path, key: str, config: Path, mcp: Path) -> str:
241
- claude = shutil.which("claude")
242
- if not claude:
219
+ def wire_claude_entries(directory: Path, entries: dict[str, dict]) -> str:
220
+ """Update local-scope entries together, with one backup before a change."""
221
+ path = claude_config_file()
222
+ # Claude local scope uses the nearest repository root, or the launch directory
223
+ # outside a repository. A worktree's .git marker is a file rather than a directory.
224
+ project = next((p for p in (directory, *directory.parents) if (p / ".git").exists()), directory)
225
+ try:
226
+ exists = path.exists()
227
+ text = path.read_text(encoding="utf-8") if exists else ""
228
+ data = json.loads(text) if text else {}
229
+ if not isinstance(data, dict):
230
+ raise ValueError("expected a JSON object")
231
+ projects = data.setdefault("projects", {})
232
+ if not isinstance(projects, dict):
233
+ raise ValueError("projects must be an object")
234
+ settings = projects.setdefault(str(project), {})
235
+ if not isinstance(settings, dict):
236
+ raise ValueError("project settings must be an object")
237
+ servers = settings.setdefault("mcpServers", {})
238
+ if not isinstance(servers, dict):
239
+ raise ValueError("mcpServers must be an object")
240
+ servers.update(entries)
241
+ updated = json.dumps(data, indent=2) + "\n"
242
+ if updated == text:
243
+ os.chmod(path, 0o600)
244
+ else:
245
+ if exists:
246
+ write_private(path.with_name(path.name + ".caura-bus-setup.bak"), text)
247
+ write_private(path, updated)
248
+ except (OSError, ValueError) as exc:
249
+ # JSON decode errors can carry snippets of the original file; do not echo them.
243
250
  raise SetupError(
244
- "`claude` is not on PATH. Run this yourself (with CAURA_API_KEY exported):\n "
245
- + claude_manual_command(directory, config, mcp)
246
- )
247
- # Replace a previous entry so re-running updates the key and paths in place.
248
- subprocess.run(
249
- [claude, "mcp", "remove", "--scope", "local", SERVER_NAME],
250
- cwd=directory,
251
- capture_output=True,
252
- text=True,
253
- check=False,
254
- )
255
- result = subprocess.run(
256
- claude_add_argv(claude, key, config, mcp),
257
- cwd=directory,
258
- capture_output=True,
259
- text=True,
260
- check=False,
261
- )
262
- if result.returncode != 0:
263
- detail = scrub((result.stderr or result.stdout).strip(), key)
264
- raise SetupError(f"`claude mcp add` failed ({result.returncode}): {detail}")
265
- return f"claude mcp add --scope local {SERVER_NAME} (project {show(directory)})"
251
+ f"could not update private Claude config {show(path)} ({type(exc).__name__})"
252
+ ) from None
253
+ return f"{', '.join(entries)} in {show(path)} (project {show(project)}; mode 600)"
254
+
255
+
256
+ def wire_claude(directory: Path, key: str, config: Path, mcp: Path, server_url: str | None = None) -> str:
257
+ entries = {
258
+ SERVER_NAME: {
259
+ "type": "stdio",
260
+ "command": str(mcp),
261
+ "args": [],
262
+ "env": {"CAURA_API_KEY": key, "CAURA_BUS_AGENT_CONFIG": str(config)},
263
+ },
264
+ }
265
+ if server_url is not None:
266
+ entries[MEMORY_SERVER_NAME] = {
267
+ "type": "http",
268
+ "url": memory_url(server_url),
269
+ "headers": {MEMORY_HEADER: key},
270
+ }
271
+ return wire_claude_entries(directory, entries)
266
272
 
267
273
 
268
274
  def codex_block(key: str, config: Path, mcp: Path) -> list[str]:
@@ -375,7 +381,7 @@ def update_codex_text(text: str, key: str, config: Path, mcp: Path) -> str:
375
381
  def wire_codex(tables: list[CodexTable], path: Path | None = None) -> list[str]:
376
382
  """Update every table in one write, with one backup of the previous file."""
377
383
  path = path or codex_config_path()
378
- text = path.read_text() if path.exists() else ""
384
+ text = path.read_text(encoding="utf-8") if path.exists() else ""
379
385
  updated = text
380
386
  for table in tables:
381
387
  updated = update_codex_table(updated, table)
@@ -451,7 +457,7 @@ def claude_config_file() -> Path:
451
457
 
452
458
  def read_json(path: Path) -> dict:
453
459
  try:
454
- data = json.loads(path.read_text())
460
+ data = json.loads(path.read_text(encoding="utf-8"))
455
461
  except (OSError, ValueError):
456
462
  return {}
457
463
  return data if isinstance(data, dict) else {}
@@ -492,57 +498,6 @@ def codex_memory_warnings(text: str, url: str) -> list[str]:
492
498
  ]
493
499
 
494
500
 
495
- def claude_memory_argv(claude: str, url: str, key: str) -> list[str]:
496
- # --header is variadic, so it goes after the name and URL.
497
- return [
498
- claude,
499
- "mcp",
500
- "add",
501
- "--scope",
502
- "local",
503
- "--transport",
504
- "http",
505
- MEMORY_SERVER_NAME,
506
- memory_url(url),
507
- "--header",
508
- f"{MEMORY_HEADER}: {key}",
509
- ]
510
-
511
-
512
- def claude_memory_manual_command(directory: Path, url: str) -> str:
513
- command = shlex.join(claude_memory_argv("claude", url, "KEYPLACEHOLDER")).replace(
514
- f"'{MEMORY_HEADER}: KEYPLACEHOLDER'",
515
- f'"{MEMORY_HEADER}: ${{CAURA_API_KEY:?export your agent key first}}"',
516
- )
517
- return f"cd {shlex.quote(str(directory))} && {command}"
518
-
519
-
520
- def wire_claude_memory(directory: Path, url: str, key: str) -> str:
521
- claude = shutil.which("claude")
522
- if not claude:
523
- raise SetupError(
524
- "`claude` is not on PATH. Run this yourself (with CAURA_API_KEY exported):\n "
525
- + claude_memory_manual_command(directory, url)
526
- )
527
- subprocess.run(
528
- [claude, "mcp", "remove", "--scope", "local", MEMORY_SERVER_NAME],
529
- cwd=directory,
530
- capture_output=True,
531
- text=True,
532
- check=False,
533
- )
534
- result = subprocess.run(
535
- claude_memory_argv(claude, url, key), cwd=directory, capture_output=True, text=True, check=False
536
- )
537
- if result.returncode != 0:
538
- detail = scrub((result.stderr or result.stdout).strip(), key)
539
- raise SetupError(f"`claude mcp add` for the memory server failed ({result.returncode}): {detail}")
540
- return (
541
- f"claude mcp add --scope local --transport http {MEMORY_SERVER_NAME} {memory_url(url)} "
542
- f"(project {show(directory)}; the key is stored in {show(claude_config_file())})"
543
- )
544
-
545
-
546
501
  # --- 4. peer instructions -------------------------------------------------
547
502
 
548
503
 
@@ -572,11 +527,11 @@ def merge_instructions(existing: str | None) -> tuple[str, str]:
572
527
 
573
528
 
574
529
  def write_instructions(path: Path) -> str:
575
- existing = path.read_text() if path.exists() else None
530
+ existing = path.read_text(encoding="utf-8") if path.exists() else None
576
531
  updated, action = merge_instructions(existing)
577
532
  if action != "unchanged":
578
533
  temporary = path.with_name(path.name + ".tmp")
579
- temporary.write_text(updated)
534
+ temporary.write_text(updated, encoding="utf-8")
580
535
  if path.exists():
581
536
  os.chmod(temporary, path.stat().st_mode & 0o777)
582
537
  temporary.replace(path)
@@ -699,7 +654,7 @@ def run_setup(options: Options, echo: Callable[[str], None], run_async) -> int:
699
654
  options.config_path = config_path
700
655
  text = render_config(url, insecure, identity, config_path)
701
656
  AgentConfig.model_validate(tomllib.loads(text))
702
- unchanged = config_path.is_file() and config_path.read_text() == text
657
+ unchanged = config_path.is_file() and config_path.read_text(encoding="utf-8") == text
703
658
  if options.dry_run:
704
659
  echo(f" {prefix}write agent config {show(config_path)} (mode 600)")
705
660
  else:
@@ -717,15 +672,15 @@ def run_setup(options: Options, echo: Callable[[str], None], run_async) -> int:
717
672
  )
718
673
  if runtime == "claude":
719
674
  if options.dry_run:
720
- echo(f" {prefix}run in {directory}:")
721
- echo(" " + shlex.join(claude_add_argv("claude", mask(key), config_path, mcp)))
675
+ echo(f" {prefix}write local caura-bus entry in {show(claude_config_file())} (mode 600)")
722
676
  if options.memory:
723
- echo(" " + shlex.join(claude_memory_argv("claude", url, mask(key))))
677
+ echo(f" {prefix}write local caura {memory_url(url)} entry in the same private config")
724
678
  else:
725
679
  try:
726
- echo(" [ok] MCP server " + wire_claude(directory, key, config_path, mcp))
727
- if options.memory:
728
- echo(" [ok] memory MCP server " + wire_claude_memory(directory, url, key))
680
+ echo(
681
+ " [ok] MCP configuration "
682
+ + wire_claude(directory, key, config_path, mcp, url if options.memory else None)
683
+ )
729
684
  except SetupError as exc:
730
685
  echo(f" [!!] {exc}")
731
686
  return finish(options, runtime, directory, config_path, echo, run_async, key, failed=True)
@@ -740,7 +695,7 @@ def run_setup(options: Options, echo: Callable[[str], None], run_async) -> int:
740
695
  tables = [bus_table(key, config_path, mcp)]
741
696
  if options.memory:
742
697
  tables.append(memory_table(url, key))
743
- current = codex_path.read_text() if codex_path.exists() else ""
698
+ current = codex_path.read_text(encoding="utf-8") if codex_path.exists() else ""
744
699
  if options.dry_run:
745
700
  planned = current
746
701
  for table in tables:
@@ -768,7 +723,9 @@ def finish(options, runtime, directory, config_path, echo, run_async, key, *, fa
768
723
  prefix = "[dry-run] would " if options.dry_run else ""
769
724
  instructions = directory / INSTRUCTION_FILES[runtime]
770
725
  if options.dry_run:
771
- _, action = merge_instructions(instructions.read_text() if instructions.exists() else None)
726
+ _, action = merge_instructions(
727
+ instructions.read_text(encoding="utf-8") if instructions.exists() else None
728
+ )
772
729
  verb = {"created": "create", "appended": "append to", "updated": "update", "unchanged": "keep"}[
773
730
  action
774
731
  ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: caura-bus-cli
3
- Version: 0.3.1
3
+ Version: 0.3.4
4
4
  Summary: Caura messaging CLI: setup / doctor / replay / agents / threads / send / status
5
5
  Author: Caura, Inc.
6
6
  License: Apache-2.0
@@ -10,7 +10,7 @@ Project-URL: Changelog, https://github.com/caura-ai/caura/blob/main/clients/coll
10
10
  Requires-Python: >=3.12
11
11
  License-File: LICENSE
12
12
  License-File: NOTICE
13
- Requires-Dist: caura-bus-core==0.3.1
14
- Requires-Dist: caura-bus-mcp==0.3.1
13
+ Requires-Dist: caura-bus-core==0.3.4
14
+ Requires-Dist: caura-bus-mcp==0.3.4
15
15
  Requires-Dist: typer>=0.12
16
16
  Dynamic: license-file
@@ -0,0 +1,3 @@
1
+ caura-bus-core==0.3.4
2
+ caura-bus-mcp==0.3.4
3
+ typer>=0.12
@@ -1,3 +0,0 @@
1
- caura-bus-core==0.3.1
2
- caura-bus-mcp==0.3.1
3
- typer>=0.12
File without changes
File without changes
File without changes