bub 0.3.2__tar.gz → 0.3.3__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 (60) hide show
  1. {bub-0.3.2 → bub-0.3.3}/PKG-INFO +26 -23
  2. {bub-0.3.2 → bub-0.3.3}/README.md +23 -22
  3. {bub-0.3.2 → bub-0.3.3}/pyproject.toml +6 -1
  4. {bub-0.3.2 → bub-0.3.3}/src/bub/__init__.py +1 -1
  5. {bub-0.3.2 → bub-0.3.3}/src/bub/__main__.py +14 -0
  6. {bub-0.3.2 → bub-0.3.3}/src/bub/builtin/agent.py +1 -3
  7. {bub-0.3.2 → bub-0.3.3}/src/bub/builtin/hook_impl.py +1 -1
  8. {bub-0.3.2 → bub-0.3.3}/src/bub/builtin/settings.py +2 -1
  9. {bub-0.3.2 → bub-0.3.3}/src/bub/builtin/shell_manager.py +2 -0
  10. {bub-0.3.2 → bub-0.3.3}/src/bub/builtin/store.py +3 -3
  11. {bub-0.3.2 → bub-0.3.3}/src/bub/builtin/tools.py +3 -1
  12. {bub-0.3.2 → bub-0.3.3}/tests/test_builtin_agent.py +9 -1
  13. {bub-0.3.2 → bub-0.3.3}/tests/test_framework.py +5 -5
  14. {bub-0.3.2 → bub-0.3.3}/tests/test_settings.py +23 -1
  15. bub-0.3.3/tests/test_tape_search_output.py +55 -0
  16. {bub-0.3.2 → bub-0.3.3}/LICENSE +0 -0
  17. {bub-0.3.2 → bub-0.3.3}/src/bub/builtin/__init__.py +0 -0
  18. {bub-0.3.2 → bub-0.3.3}/src/bub/builtin/auth.py +0 -0
  19. {bub-0.3.2 → bub-0.3.3}/src/bub/builtin/cli.py +0 -0
  20. {bub-0.3.2 → bub-0.3.3}/src/bub/builtin/context.py +0 -0
  21. {bub-0.3.2 → bub-0.3.3}/src/bub/builtin/tape.py +0 -0
  22. {bub-0.3.2 → bub-0.3.3}/src/bub/channels/__init__.py +0 -0
  23. {bub-0.3.2 → bub-0.3.3}/src/bub/channels/base.py +0 -0
  24. {bub-0.3.2 → bub-0.3.3}/src/bub/channels/cli/__init__.py +0 -0
  25. {bub-0.3.2 → bub-0.3.3}/src/bub/channels/cli/renderer.py +0 -0
  26. {bub-0.3.2 → bub-0.3.3}/src/bub/channels/handler.py +0 -0
  27. {bub-0.3.2 → bub-0.3.3}/src/bub/channels/manager.py +0 -0
  28. {bub-0.3.2 → bub-0.3.3}/src/bub/channels/message.py +0 -0
  29. {bub-0.3.2 → bub-0.3.3}/src/bub/channels/telegram.py +0 -0
  30. {bub-0.3.2 → bub-0.3.3}/src/bub/envelope.py +0 -0
  31. {bub-0.3.2 → bub-0.3.3}/src/bub/framework.py +0 -0
  32. {bub-0.3.2 → bub-0.3.3}/src/bub/hook_runtime.py +0 -0
  33. {bub-0.3.2 → bub-0.3.3}/src/bub/hookspecs.py +0 -0
  34. {bub-0.3.2 → bub-0.3.3}/src/bub/skills.py +0 -0
  35. {bub-0.3.2 → bub-0.3.3}/src/bub/tools.py +0 -0
  36. {bub-0.3.2 → bub-0.3.3}/src/bub/types.py +0 -0
  37. {bub-0.3.2 → bub-0.3.3}/src/bub/utils.py +0 -0
  38. {bub-0.3.2 → bub-0.3.3}/src/skills/README.md +0 -0
  39. {bub-0.3.2 → bub-0.3.3}/src/skills/gh/SKILL.md +0 -0
  40. {bub-0.3.2 → bub-0.3.3}/src/skills/skill-creator/SKILL.md +0 -0
  41. {bub-0.3.2 → bub-0.3.3}/src/skills/skill-creator/license.txt +0 -0
  42. {bub-0.3.2 → bub-0.3.3}/src/skills/skill-creator/scripts/init_skill.py +0 -0
  43. {bub-0.3.2 → bub-0.3.3}/src/skills/skill-creator/scripts/quick_validate.py +0 -0
  44. {bub-0.3.2 → bub-0.3.3}/src/skills/telegram/SKILL.md +0 -0
  45. {bub-0.3.2 → bub-0.3.3}/src/skills/telegram/scripts/telegram_edit.py +0 -0
  46. {bub-0.3.2 → bub-0.3.3}/src/skills/telegram/scripts/telegram_send.py +0 -0
  47. {bub-0.3.2 → bub-0.3.3}/tests/test_builtin_cli.py +0 -0
  48. {bub-0.3.2 → bub-0.3.3}/tests/test_builtin_hook_impl.py +0 -0
  49. {bub-0.3.2 → bub-0.3.3}/tests/test_builtin_tools.py +0 -0
  50. {bub-0.3.2 → bub-0.3.3}/tests/test_channels.py +0 -0
  51. {bub-0.3.2 → bub-0.3.3}/tests/test_cli_help.py +0 -0
  52. {bub-0.3.2 → bub-0.3.3}/tests/test_envelope.py +0 -0
  53. {bub-0.3.2 → bub-0.3.3}/tests/test_file_tape_store_entry_ids.py +0 -0
  54. {bub-0.3.2 → bub-0.3.3}/tests/test_fork_store_merge_back.py +0 -0
  55. {bub-0.3.2 → bub-0.3.3}/tests/test_hook_runtime.py +0 -0
  56. {bub-0.3.2 → bub-0.3.3}/tests/test_image_message.py +0 -0
  57. {bub-0.3.2 → bub-0.3.3}/tests/test_skills.py +0 -0
  58. {bub-0.3.2 → bub-0.3.3}/tests/test_subagent_tool.py +0 -0
  59. {bub-0.3.2 → bub-0.3.3}/tests/test_tools.py +0 -0
  60. {bub-0.3.2 → bub-0.3.3}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: bub
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: A common shape for agents that live alongside people.
5
5
  Author-Email: Chojan Shang <psiace@apache.org>, Frost Ming <me@frostming.com>, Yihong <zouzou0208@gmail.com>
6
6
  Classifier: Intended Audience :: Developers
@@ -27,6 +27,8 @@ Requires-Dist: python-telegram-bot>=21.0
27
27
  Requires-Dist: loguru>=0.7.2
28
28
  Requires-Dist: rapidfuzz>=3.14.3
29
29
  Requires-Dist: aiohttp>=3.13.3
30
+ Provides-Extra: logfire
31
+ Requires-Dist: logfire>=4.31.0; extra == "logfire"
30
32
  Description-Content-Type: text/markdown
31
33
 
32
34
  # Bub
@@ -84,10 +86,10 @@ If `AGENTS.md` exists in the workspace, it is appended to the system prompt auto
84
86
 
85
87
  Key source files:
86
88
 
87
- - Turn orchestrator: [`src/bub/framework.py`](src/bub/framework.py)
88
- - Hook contract: [`src/bub/hookspecs.py`](src/bub/hookspecs.py)
89
- - Builtin hooks: [`src/bub/builtin/hook_impl.py`](src/bub/builtin/hook_impl.py)
90
- - Skill discovery: [`src/bub/skills.py`](src/bub/skills.py)
89
+ - Turn orchestrator: [`src/bub/framework.py`](https://github.com/bubbuild/bub/blob/main/src/bub/framework.py)
90
+ - Hook contract: [`src/bub/hookspecs.py`](https://github.com/bubbuild/bub/blob/main/src/bub/hookspecs.py)
91
+ - Builtin hooks: [`src/bub/builtin/hook_impl.py`](https://github.com/bubbuild/bub/blob/main/src/bub/builtin/hook_impl.py)
92
+ - Skill discovery: [`src/bub/skills.py`](https://github.com/bubbuild/bub/blob/main/src/bub/skills.py)
91
93
 
92
94
  ## What Sets It Apart
93
95
 
@@ -134,15 +136,16 @@ Lines starting with `,` enter internal command mode (`,help`, `,skill name=my-sk
134
136
 
135
137
  ## Configuration
136
138
 
137
- | Variable | Default | Description |
138
- | --------------------------- | ---------------------------------- | ----------------------------------------------- |
139
- | `BUB_MODEL` | `openrouter:qwen/qwen3-coder-next` | Model identifier |
140
- | `BUB_API_KEY` | — | Provider key (optional with `bub login openai`) |
141
- | `BUB_API_BASE` | — | Custom provider endpoint |
142
- | `BUB_API_FORMAT` | `completion` | `completion`, `responses`, or `messages` |
143
- | `BUB_MAX_STEPS` | `50` | Max tool-use loop iterations |
144
- | `BUB_MAX_TOKENS` | `1024` | Max tokens per model call |
145
- | `BUB_MODEL_TIMEOUT_SECONDS` | | Model call timeout (seconds) |
139
+ | Variable | Default | Description |
140
+ |----------|---------|-------------|
141
+ | `BUB_MODEL` | `openrouter:qwen/qwen3-coder-next` | Model identifier |
142
+ | `BUB_API_KEY` | — | Provider key (optional with `bub login openai`) |
143
+ | `BUB_API_BASE` | — | Custom provider endpoint |
144
+ | `BUB_API_FORMAT` | `completion` | `completion`, `responses`, or `messages` |
145
+ | `BUB_CLIENT_ARGS` | | JSON object forwarded to the underlying model client |
146
+ | `BUB_MAX_STEPS` | `50` | Max tool-use loop iterations |
147
+ | `BUB_MAX_TOKENS` | `1024` | Max tokens per model call |
148
+ | `BUB_MODEL_TIMEOUT_SECONDS` | — | Model call timeout (seconds) |
146
149
 
147
150
  ## Background
148
151
 
@@ -152,13 +155,13 @@ Read more: [Context from Tape](https://tape.systems) · [Socialized Evaluation a
152
155
 
153
156
  ## Docs
154
157
 
155
- - [Architecture](docs/architecture.md) — lifecycle, hook precedence, error handling
156
- - [Features](docs/features.md) — what ships today and current boundaries
157
- - [Channels](docs/channels/index.md) — CLI, Telegram, and custom adapters
158
- - [Skills](docs/skills.md) — discovery and authoring
159
- - [Extension Guide](docs/extension-guide.md) — hooks, tools, plugin packaging
160
- - [Deployment](docs/deployment.md) — Docker, environment, upgrades
161
- - [Posts](docs/posts/index.md) — design notes
158
+ - [Architecture](https://bub.build/architecture/) — lifecycle, hook precedence, error handling
159
+ - [Features](https://bub.build/features/) — what ships today and current boundaries
160
+ - [Channels](https://bub.build/channels/) — CLI, Telegram, and custom adapters
161
+ - [Skills](https://bub.build/skills/) — discovery and authoring
162
+ - [Extension Guide](https://bub.build/extension-guide/) — hooks, tools, plugin packaging
163
+ - [Deployment](https://bub.build/deployment/) — Docker, environment, upgrades
164
+ - [Posts](https://bub.build/posts/) — design notes
162
165
 
163
166
  ## Development
164
167
 
@@ -168,8 +171,8 @@ uv run mypy src
168
171
  uv run pytest -q
169
172
  ```
170
173
 
171
- See [CONTRIBUTING.md](./CONTRIBUTING.md).
174
+ See [CONTRIBUTING.md](https://github.com/bubbuild/bub/blob/main/CONTRIBUTING.md).
172
175
 
173
176
  ## License
174
177
 
175
- [Apache-2.0](./LICENSE)
178
+ [Apache-2.0](https://github.com/bubbuild/bub/blob/main/LICENSE)
@@ -53,10 +53,10 @@ If `AGENTS.md` exists in the workspace, it is appended to the system prompt auto
53
53
 
54
54
  Key source files:
55
55
 
56
- - Turn orchestrator: [`src/bub/framework.py`](src/bub/framework.py)
57
- - Hook contract: [`src/bub/hookspecs.py`](src/bub/hookspecs.py)
58
- - Builtin hooks: [`src/bub/builtin/hook_impl.py`](src/bub/builtin/hook_impl.py)
59
- - Skill discovery: [`src/bub/skills.py`](src/bub/skills.py)
56
+ - Turn orchestrator: [`src/bub/framework.py`](https://github.com/bubbuild/bub/blob/main/src/bub/framework.py)
57
+ - Hook contract: [`src/bub/hookspecs.py`](https://github.com/bubbuild/bub/blob/main/src/bub/hookspecs.py)
58
+ - Builtin hooks: [`src/bub/builtin/hook_impl.py`](https://github.com/bubbuild/bub/blob/main/src/bub/builtin/hook_impl.py)
59
+ - Skill discovery: [`src/bub/skills.py`](https://github.com/bubbuild/bub/blob/main/src/bub/skills.py)
60
60
 
61
61
  ## What Sets It Apart
62
62
 
@@ -103,15 +103,16 @@ Lines starting with `,` enter internal command mode (`,help`, `,skill name=my-sk
103
103
 
104
104
  ## Configuration
105
105
 
106
- | Variable | Default | Description |
107
- | --------------------------- | ---------------------------------- | ----------------------------------------------- |
108
- | `BUB_MODEL` | `openrouter:qwen/qwen3-coder-next` | Model identifier |
109
- | `BUB_API_KEY` | — | Provider key (optional with `bub login openai`) |
110
- | `BUB_API_BASE` | — | Custom provider endpoint |
111
- | `BUB_API_FORMAT` | `completion` | `completion`, `responses`, or `messages` |
112
- | `BUB_MAX_STEPS` | `50` | Max tool-use loop iterations |
113
- | `BUB_MAX_TOKENS` | `1024` | Max tokens per model call |
114
- | `BUB_MODEL_TIMEOUT_SECONDS` | | Model call timeout (seconds) |
106
+ | Variable | Default | Description |
107
+ |----------|---------|-------------|
108
+ | `BUB_MODEL` | `openrouter:qwen/qwen3-coder-next` | Model identifier |
109
+ | `BUB_API_KEY` | — | Provider key (optional with `bub login openai`) |
110
+ | `BUB_API_BASE` | — | Custom provider endpoint |
111
+ | `BUB_API_FORMAT` | `completion` | `completion`, `responses`, or `messages` |
112
+ | `BUB_CLIENT_ARGS` | | JSON object forwarded to the underlying model client |
113
+ | `BUB_MAX_STEPS` | `50` | Max tool-use loop iterations |
114
+ | `BUB_MAX_TOKENS` | `1024` | Max tokens per model call |
115
+ | `BUB_MODEL_TIMEOUT_SECONDS` | — | Model call timeout (seconds) |
115
116
 
116
117
  ## Background
117
118
 
@@ -121,13 +122,13 @@ Read more: [Context from Tape](https://tape.systems) · [Socialized Evaluation a
121
122
 
122
123
  ## Docs
123
124
 
124
- - [Architecture](docs/architecture.md) — lifecycle, hook precedence, error handling
125
- - [Features](docs/features.md) — what ships today and current boundaries
126
- - [Channels](docs/channels/index.md) — CLI, Telegram, and custom adapters
127
- - [Skills](docs/skills.md) — discovery and authoring
128
- - [Extension Guide](docs/extension-guide.md) — hooks, tools, plugin packaging
129
- - [Deployment](docs/deployment.md) — Docker, environment, upgrades
130
- - [Posts](docs/posts/index.md) — design notes
125
+ - [Architecture](https://bub.build/architecture/) — lifecycle, hook precedence, error handling
126
+ - [Features](https://bub.build/features/) — what ships today and current boundaries
127
+ - [Channels](https://bub.build/channels/) — CLI, Telegram, and custom adapters
128
+ - [Skills](https://bub.build/skills/) — discovery and authoring
129
+ - [Extension Guide](https://bub.build/extension-guide/) — hooks, tools, plugin packaging
130
+ - [Deployment](https://bub.build/deployment/) — Docker, environment, upgrades
131
+ - [Posts](https://bub.build/posts/) — design notes
131
132
 
132
133
  ## Development
133
134
 
@@ -137,8 +138,8 @@ uv run mypy src
137
138
  uv run pytest -q
138
139
  ```
139
140
 
140
- See [CONTRIBUTING.md](./CONTRIBUTING.md).
141
+ See [CONTRIBUTING.md](https://github.com/bubbuild/bub/blob/main/CONTRIBUTING.md).
141
142
 
142
143
  ## License
143
144
 
144
- [Apache-2.0](./LICENSE)
145
+ [Apache-2.0](https://github.com/bubbuild/bub/blob/main/LICENSE)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "bub"
3
- version = "0.3.2"
3
+ version = "0.3.3"
4
4
  description = "A common shape for agents that live alongside people."
5
5
  authors = [
6
6
  { name = "Chojan Shang", email = "psiace@apache.org" },
@@ -42,6 +42,11 @@ Documentation = "https://bub.build"
42
42
  [project.scripts]
43
43
  bub = "bub.__main__:app"
44
44
 
45
+ [project.optional-dependencies]
46
+ logfire = [
47
+ "logfire>=4.31.0",
48
+ ]
49
+
45
50
  [dependency-groups]
46
51
  dev = [
47
52
  "pytest>=7.2.0",
@@ -5,4 +5,4 @@ from bub.hookspecs import hookimpl
5
5
  from bub.tools import tool
6
6
 
7
7
  __all__ = ["BubFramework", "hookimpl", "tool"]
8
- __version__ = "0.3.2"
8
+ __version__ = "0.3.3"
@@ -7,7 +7,21 @@ import typer
7
7
  from bub.framework import BubFramework
8
8
 
9
9
 
10
+ def _instrument_bub() -> None:
11
+ try:
12
+ import logfire
13
+
14
+ logfire.configure()
15
+ except ImportError:
16
+ pass
17
+ else:
18
+ from loguru import logger
19
+
20
+ logger.configure(handlers=[logfire.loguru_handler()])
21
+
22
+
10
23
  def create_cli_app() -> typer.Typer:
24
+ _instrument_bub()
11
25
  framework = BubFramework()
12
26
  framework.load_hooks()
13
27
  app = framework.create_cli_app()
@@ -28,7 +28,6 @@ from bub.types import State
28
28
  from bub.utils import workspace_from_state
29
29
 
30
30
  CONTINUE_PROMPT = "Continue the task."
31
- DEFAULT_BUB_HEADERS = {"HTTP-Referer": "https://bub.build/", "X-Title": "Bub"}
32
31
  HINT_RE = re.compile(r"\$([A-Za-z0-9_.-]+)")
33
32
 
34
33
 
@@ -222,7 +221,6 @@ class Agent:
222
221
  allowed_tools: Collection[str] | None = None,
223
222
  allowed_skills: Collection[str] | None = None,
224
223
  ) -> ToolAutoResult:
225
- extra_options = {"extra_headers": DEFAULT_BUB_HEADERS} if self.settings.model.startswith("openrouter:") else {}
226
224
  prompt_text = prompt if isinstance(prompt, str) else _extract_text_from_parts(prompt)
227
225
  if allowed_tools is not None:
228
226
  allowed_tools = {name.casefold() for name in allowed_tools}
@@ -240,7 +238,6 @@ class Agent:
240
238
  max_tokens=self.settings.max_tokens,
241
239
  tools=model_tools(tools),
242
240
  model=model,
243
- **extra_options,
244
241
  )
245
242
 
246
243
  def _system_prompt(self, prompt: str, state: State, allowed_skills: set[str] | None = None) -> str:
@@ -284,6 +281,7 @@ def _build_llm(settings: AgentSettings, tape_store: AsyncTapeStore, tape_context
284
281
  fallback_models=settings.fallback_models,
285
282
  api_key_resolver=openai_codex_oauth_resolver(),
286
283
  tape_store=tape_store,
284
+ client_args=settings.client_args,
287
285
  api_format=settings.api_format,
288
286
  context=tape_context,
289
287
  verbose=settings.verbose,
@@ -117,7 +117,7 @@ class BuiltinImpl:
117
117
  app.command("chat")(cli.chat)
118
118
  app.add_typer(cli.login_app)
119
119
  app.command("hooks", hidden=True)(cli.list_hooks)
120
- app.command("message", hidden=True)(app.command("gateway")(cli.gateway))
120
+ app.command("gateway")(cli.gateway)
121
121
 
122
122
  def _read_agents_file(self, state: State) -> str:
123
123
  workspace = state.get("_runtime_workspace", str(Path.cwd()))
@@ -5,7 +5,7 @@ import pathlib
5
5
  import re
6
6
  from collections.abc import Callable
7
7
  from functools import lru_cache
8
- from typing import Literal
8
+ from typing import Any, Literal
9
9
 
10
10
  from pydantic import Field
11
11
  from pydantic_settings import BaseSettings, PydanticBaseSettingsSource, SettingsConfigDict, YamlConfigSettingsSource
@@ -45,6 +45,7 @@ class AgentSettings(BaseSettings):
45
45
  max_steps: int = 50
46
46
  max_tokens: int = DEFAULT_MAX_TOKENS
47
47
  model_timeout_seconds: int | None = None
48
+ client_args: dict[str, Any] | None = None
48
49
  verbose: int = Field(default=0, description="Verbosity level for logging. Higher means more verbose.", ge=0, le=2)
49
50
 
50
51
  @classmethod
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import contextlib
5
+ import os
5
6
  import uuid
6
7
  from dataclasses import dataclass, field
7
8
 
@@ -38,6 +39,7 @@ class ShellManager:
38
39
  cwd=cwd,
39
40
  stdout=asyncio.subprocess.PIPE,
40
41
  stderr=asyncio.subprocess.PIPE,
42
+ executable="/bin/bash" if os.name != "nt" else None,
41
43
  )
42
44
  shell = ManagedShell(shell_id=f"bash-{uuid.uuid4().hex[:8]}", cmd=cmd, cwd=cwd, process=process)
43
45
  shell.read_tasks.extend([
@@ -37,7 +37,7 @@ class ForkTapeStore:
37
37
 
38
38
  @property
39
39
  def _current(self) -> TapeStore:
40
- return current_store.get(_emtpy_store)
40
+ return current_store.get(_empty_store)
41
41
 
42
42
  @property
43
43
  def _fork_tape(self) -> str | None:
@@ -52,7 +52,7 @@ class ForkTapeStore:
52
52
 
53
53
  async def reset(self, tape: str) -> None:
54
54
  self._current.reset(tape)
55
- if self._current is _emtpy_store or self._fork_tape != tape:
55
+ if self._current is _empty_store or self._fork_tape != tape:
56
56
  await self._parent.reset(tape)
57
57
  return
58
58
  current_tape_was_reset.set(True)
@@ -138,7 +138,7 @@ class EmptyTapeStore:
138
138
  pass
139
139
 
140
140
 
141
- _emtpy_store = EmptyTapeStore()
141
+ _empty_store = EmptyTapeStore()
142
142
 
143
143
 
144
144
  class FileTapeStore(InMemoryQueryMixin):
@@ -206,7 +206,9 @@ async def tape_search(param: SearchInput, *, context: ToolContext) -> str:
206
206
  if "[tape.search]" in entry_str:
207
207
  continue
208
208
  lines.append(entry_str)
209
- return f"[tape.search]: {len(entries)} matches" + "".join(f"\n{line}" for line in lines)
209
+ return f"[tape.search]: {len(lines)} matches ({len(entries) - len(lines)} filtered)" + "".join(
210
+ f"\n{line}" for line in lines
211
+ )
210
212
 
211
213
 
212
214
  @tool(context=True, name="tape.reset")
@@ -26,7 +26,12 @@ def test_build_llm_passes_codex_resolver_to_republic(monkeypatch) -> None:
26
26
  monkeypatch.setattr(agent_module, "LLM", FakeLLM)
27
27
  monkeypatch.setattr(openai_codex, "openai_codex_oauth_resolver", lambda: resolver)
28
28
 
29
- settings = AgentSettings(model="openai:gpt-5-codex", api_key=None, api_base=None)
29
+ settings = AgentSettings(
30
+ model="openai:gpt-5-codex",
31
+ api_key=None,
32
+ api_base=None,
33
+ client_args={"extra_headers": {"HTTP-Referer": "https://openclaw.ai", "X-Title": "OpenClaw"}},
34
+ )
30
35
  tape_store = object()
31
36
 
32
37
  agent_module._build_llm(settings, tape_store, "ctx")
@@ -34,6 +39,9 @@ def test_build_llm_passes_codex_resolver_to_republic(monkeypatch) -> None:
34
39
  assert captured["args"] == ("openai:gpt-5-codex",)
35
40
  assert captured["kwargs"]["api_key"] is None
36
41
  assert captured["kwargs"]["api_base"] is None
42
+ assert captured["kwargs"]["client_args"] == {
43
+ "extra_headers": {"HTTP-Referer": "https://openclaw.ai", "X-Title": "OpenClaw"},
44
+ }
37
45
  assert captured["kwargs"]["api_key_resolver"] is resolver
38
46
  assert captured["kwargs"]["tape_store"] is tape_store
39
47
  assert captured["kwargs"]["context"] == "ctx"
@@ -94,19 +94,19 @@ def test_get_system_prompt_uses_priority_order_and_skips_empty_results() -> None
94
94
  assert prompt == "low\n\nhigh"
95
95
 
96
96
 
97
- def test_builtin_cli_exposes_login_and_keeps_message_hidden_alias() -> None:
97
+ def test_builtin_cli_exposes_login_and_gateway_command() -> None:
98
98
  framework = BubFramework()
99
99
  framework.load_hooks()
100
100
  app = framework.create_cli_app()
101
101
  runner = CliRunner()
102
102
 
103
103
  help_result = runner.invoke(app, ["--help"])
104
- alias_result = runner.invoke(app, ["message", "--help"])
104
+ gateway_result = runner.invoke(app, ["gateway", "--help"])
105
105
 
106
106
  assert help_result.exit_code == 0
107
107
  assert "login" in help_result.stdout
108
108
  assert "gateway" in help_result.stdout
109
109
  assert "│ message" not in help_result.stdout
110
- assert alias_result.exit_code == 0
111
- assert "bub message" in alias_result.stdout
112
- assert "Start message listeners" in alias_result.stdout
110
+ assert gateway_result.exit_code == 0
111
+ assert "bub gateway" in gateway_result.stdout
112
+ assert "Start message listeners" in gateway_result.stdout
@@ -37,11 +37,12 @@ def test_settings_per_provider_keys() -> None:
37
37
  assert settings.api_base["openai"] == "https://api.openai.com"
38
38
 
39
39
 
40
- def test_settings_no_keys_returns_none() -> None:
40
+ def test_settings_no_keys_return_none() -> None:
41
41
  settings = _settings_with_env({})
42
42
 
43
43
  assert settings.api_key is None
44
44
  assert settings.api_base is None
45
+ assert settings.client_args is None
45
46
 
46
47
 
47
48
  def test_settings_provider_names_are_lowercased() -> None:
@@ -74,6 +75,10 @@ api_key:
74
75
  openai: sk-yaml
75
76
  api_base:
76
77
  openai: https://api.openai.com
78
+ client_args:
79
+ extra_headers:
80
+ HTTP-Referer: https://openclaw.ai
81
+ X-Title: OpenClaw
77
82
  """.strip(),
78
83
  )
79
84
 
@@ -85,6 +90,9 @@ api_base:
85
90
  assert settings.max_steps == 77
86
91
  assert settings.api_key == {"openai": "sk-yaml"}
87
92
  assert settings.api_base == {"openai": "https://api.openai.com"}
93
+ assert settings.client_args == {
94
+ "extra_headers": {"HTTP-Referer": "https://openclaw.ai", "X-Title": "OpenClaw"},
95
+ }
88
96
 
89
97
 
90
98
  def test_env_settings_override_yaml(tmp_path: Path) -> None:
@@ -94,6 +102,10 @@ def test_env_settings_override_yaml(tmp_path: Path) -> None:
94
102
  model: openai:gpt-5
95
103
  api_key: sk-yaml
96
104
  max_steps: 77
105
+ client_args:
106
+ extra_headers:
107
+ HTTP-Referer: https://yaml.example
108
+ X-Title: YAML App
97
109
  """.strip(),
98
110
  )
99
111
 
@@ -103,6 +115,7 @@ max_steps: 77
103
115
  "BUB_HOME": str(tmp_path),
104
116
  "BUB_MODEL": "anthropic:claude-3-7-sonnet",
105
117
  "BUB_API_KEY": "sk-env",
118
+ "BUB_CLIENT_ARGS": '{"extra_headers":{"HTTP-Referer":"https://env.example","X-Title":"Env App"}}',
106
119
  "BUB_MAX_STEPS": "12",
107
120
  },
108
121
  clear=True,
@@ -112,6 +125,15 @@ max_steps: 77
112
125
  assert settings.model == "anthropic:claude-3-7-sonnet"
113
126
  assert settings.api_key == "sk-env"
114
127
  assert settings.max_steps == 12
128
+ assert settings.client_args == {
129
+ "extra_headers": {"HTTP-Referer": "https://env.example", "X-Title": "Env App"},
130
+ }
131
+
132
+
133
+ def test_settings_client_args_can_be_disabled() -> None:
134
+ settings = _settings_with_env({"BUB_CLIENT_ARGS": "null"})
135
+
136
+ assert settings.client_args is None
115
137
 
116
138
 
117
139
  def test_load_settings_reads_yaml_from_bub_home(tmp_path: Path) -> None:
@@ -0,0 +1,55 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ import pytest
6
+ from republic import ToolContext
7
+
8
+ import bub.builtin.tools as builtin_tools
9
+ from bub.builtin.tools import tape_search
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class _FakeEntry:
14
+ date: str
15
+ payload: object
16
+
17
+
18
+ class _FakeTapes:
19
+ def __init__(self, entries: list[_FakeEntry]) -> None:
20
+ self._entries = entries
21
+ self._store = object()
22
+
23
+ async def search(self, _query: object) -> list[_FakeEntry]:
24
+ return list(self._entries)
25
+
26
+
27
+ class _FakeAgent:
28
+ def __init__(self, entries: list[_FakeEntry]) -> None:
29
+ self.tapes = _FakeTapes(entries)
30
+
31
+
32
+ @pytest.mark.asyncio
33
+ async def test_tape_search_reports_shown_matches_and_filtered_count(monkeypatch) -> None:
34
+ entries = [
35
+ _FakeEntry(date="2026-01-01T00:00:00Z", payload={"content": "ok"}),
36
+ _FakeEntry(date="2026-01-01T00:00:01Z", payload={"content": "[tape.search]: 1 matches"}),
37
+ ]
38
+ monkeypatch.setattr(builtin_tools, "_get_agent", lambda _context: _FakeAgent(entries))
39
+
40
+ output = await tape_search.run(query="x", context=ToolContext(tape="tape", run_id="run", state={}))
41
+
42
+ assert output.splitlines()[0] == "[tape.search]: 1 matches (1 filtered)"
43
+
44
+
45
+ @pytest.mark.asyncio
46
+ async def test_tape_search_reports_zero_filtered_explicitly(monkeypatch) -> None:
47
+ entries = [
48
+ _FakeEntry(date="2026-01-01T00:00:00Z", payload={"content": "a"}),
49
+ _FakeEntry(date="2026-01-01T00:00:01Z", payload={"content": "b"}),
50
+ ]
51
+ monkeypatch.setattr(builtin_tools, "_get_agent", lambda _context: _FakeAgent(entries))
52
+
53
+ output = await tape_search.run(query="x", context=ToolContext(tape="tape", run_id="run", state={}))
54
+
55
+ assert output.splitlines()[0] == "[tape.search]: 2 matches (0 filtered)"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes