diff --git a/.codebuffignore b/.codebuffignore
deleted file mode 100644
index b296ea3ace..0000000000
--- a/.codebuffignore
+++ /dev/null
@@ -1,3 +0,0 @@
-!codebuff.json
-__mock-projects__
-test-repos
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 991a4c08fa..cc1034d904 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,7 +19,7 @@ tsconfig.tsbuildinfo
__mock-projects__
npm-app/src/__tests__/data/
.aider*
-.codebuff*
+.codebirds*
**.log
debug/
diff --git a/AGENTS.md b/AGENTS.md
index 36688314f7..7ba0f954de 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,6 +1,6 @@
-# Freebuff
+# Codebirds
-Freebuff is the public, free coding agent built from the Codebuff agent framework.
+Codebirds is an open-source AI coding agent built from a composable agent framework.
## Key Technologies
@@ -19,7 +19,7 @@ Freebuff is the public, free coding agent built from the Codebuff agent framewor
- `packages/agent-runtime/` - agent runtime and tool handling
- `packages/code-map/` - source parsing helpers
- `packages/llm-providers/` - public LLM provider shims
-- `freebuff/` - Freebuff CLI, release files, and e2e tests
+- `codebirds/` - Codebirds CLI, release files, and e2e tests
- `scripts/tmux/` - tmux helpers for CLI testing
## Conventions
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 3728a4973e..6758ccdfc0 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,6 +1,6 @@
# Contributing
-This repository is a public mirror of the Freebuff/Codebuff source tree. The private repository is the source of truth, so accepted public contributions are ported into the private repo and then exported back here.
+This repository is a public mirror of the Codebirds source tree. The private repository is the source of truth, so accepted public contributions are ported into the private repo and then exported back here.
## Public Contributions
@@ -13,7 +13,7 @@ Good public PRs are usually scoped to:
- `packages/agent-runtime/`
- `packages/code-map/`
- `packages/llm-providers/`
-- `freebuff/`, excluding the private web app
+- `codebirds/`, excluding the private web app
- `scripts/tmux/`
- public docs
@@ -33,10 +33,10 @@ Build the SDK:
bun run build:sdk
```
-Build the Freebuff binary:
+Build the Codebirds binary:
```bash
-bun run build:freebuff
+bun run build:codebirds
```
## Pull Request Flow
diff --git a/NOTICE b/NOTICE
index 6d7bedce4a..69c51b35ff 100644
--- a/NOTICE
+++ b/NOTICE
@@ -1,4 +1,4 @@
-Codebuff
-Copyright 2025 Codebuff
+Codebirds
+Copyright 2025 Codebirds
-This product includes software developed for the Codebuff project.
\ No newline at end of file
+This product includes software developed for the Codebirds project.
\ No newline at end of file
diff --git a/README.md b/README.md
index 716cf6e703..6bd722f160 100644
--- a/README.md
+++ b/README.md
@@ -1,43 +1,16 @@
-# Codebuff & Freebuff
+# Codebirds
English | [简体中文](./README.zh-CN.md)
-**[Codebuff](https://codebuff.com)** is an open-source AI coding assistant that edits your codebase through natural language instructions. **[Freebuff](https://www.npmjs.com/package/freebuff)** is the free, ad-supported version — no subscription, no credits, no configuration.
-
-Instead of using one model for everything, Codebuff coordinates specialized agents that work together to understand your project and make precise changes.
+**Codebirds** is an open-source AI coding assistant that edits your codebase through natural language instructions.
-
+
-Codebuff beats Claude Code at 61% vs 53% on [our evals](evals/README.md) across 175+ coding tasks over multiple open-source repos that simulate real-world tasks.
-
-## Freebuff: the free coding agent
-
-**[Freebuff](https://www.npmjs.com/package/freebuff)** is the free, ad-supported version of Codebuff. No subscription. No credits. No configuration. Just install and start coding in your terminal.
-
-### Install
-
-```bash
-npm install -g freebuff
-```
-
-### Usage
-
-```bash
-cd ~/my-project
-freebuff
-```
+Codebirds beats Claude Code at 61% vs 53% on [our evals](evals/README.md) across 175+ coding tasks over multiple open-source repos that simulate real-world tasks.
-Then tell Freebuff what you want — it finds the right files, makes the changes, and runs your tests.
-
-### Why Freebuff?
-
-- **Best open-source models** — Powered by the strongest open-source models available, like DeepSeek, Kimi, and MiniMax — no proprietary lock-in.
-- **Fast** — 5–10× speed up. Faster models plus context gathering in seconds rather than minutes.
-- **Loaded** — Built-in web research, browser use, and more.
-
-### Features
+## Key Features
- **File mentions** — Use `@filename` to reference specific files
- **Agent mentions** — Use `@AgentName` to invoke specialized agents
@@ -60,27 +33,13 @@ Then tell Freebuff what you want — it finds the right files, makes the changes
| `/logout` | Sign out |
| `/exit` | Quit |
-### FAQ
-
-**How can it be free?** Freebuff is supported by text ads.
-
-**What models do you use?** The best open-source models available. In full mode you can choose from DeepSeek V4 Pro, MiMo 2.5 Pro, Kimi K2.6, DeepSeek V4 Flash, MiMo 2.5, and MiniMax M3. Limited mode uses DeepSeek V4 Flash and MiMo 2.5. Gemini 3.1 Flash Lite handles file finding and research.
-
-**Which countries is Freebuff available in?** All countries. Freebuff runs in "full" mode in the US, Canada, UK, EU, and other select countries, and in "limited" mode everywhere else (or while using a VPN). See [freebuff.com](https://freebuff.com) for the full list.
-
-**What is limited mode?** Limited mode lets you use Freebuff outside the full-access countries, or while using a VPN. It includes DeepSeek V4 Flash and MiMo 2.5, with 5 one-hour sessions per day.
-
-**Are you training on my data?** No. We don't share your data with third parties that would train on it or use it for another purpose, unless you choose a model clearly labeled as "Collects data for training."
-
-**What data do you store?** We don't store your codebase. We only collect minimal logs for debugging purposes.
-
---
-The rest of this README covers **Codebuff**, the full platform Freebuff is built on — its multi-agent architecture, custom agents, and SDK.
+This README covers **Codebirds** — its multi-agent architecture, custom agents, and SDK.
## How it works
-When you ask Codebuff to "add authentication to my API," it might invoke:
+When you ask Codebirds to "add authentication to my API," it might invoke:
1. A **File Picker Agent** to scan your codebase to understand the architecture and find relevant files
2. A **Planner Agent** to plan which files need changes and in what order
@@ -88,7 +47,7 @@ When you ask Codebuff to "add authentication to my API," it might invoke:
4. A **Reviewer Agent** to validate changes
-
+
This multi-agent approach gives you better context understanding, more accurate edits, and fewer errors compared to single-model tools.
@@ -98,30 +57,30 @@ This multi-agent approach gives you better context understanding, more accurate
Install:
```bash
-npm install -g codebuff
+npm install -g codebirds
```
Run:
```bash
cd your-project
-codebuff
+codebirds
```
-Then just tell Codebuff what you want and it handles the rest:
+Then just tell Codebirds what you want and it handles the rest:
- "Fix the SQL injection vulnerability in user registration"
- "Add rate limiting to all API endpoints"
- "Refactor the database connection code for better performance"
-Codebuff will find the right files, makes changes across your codebase, and runs tests to make sure nothing breaks.
+Codebirds will find the right files, makes changes across your codebase, and runs tests to make sure nothing breaks.
## Create custom agents
-To get started building your own agents, start Codebuff and run the `/init` command:
+To get started building your own agents, start Codebirds and run the `/init` command:
```bash
-codebuff
+codebirds
```
Then inside the CLI:
@@ -132,7 +91,7 @@ Then inside the CLI:
This creates:
```
-knowledge.md # Project context for Codebuff
+knowledge.md # Project context for Codebirds
.agents/
└── types/ # TypeScript type definitions
├── agent-definition.ts
@@ -169,27 +128,27 @@ export default {
## SDK: Run agents in production
-Install the [SDK package](https://www.npmjs.com/package/@codebuff/sdk) -- note this is different than the CLI codebuff package.
+Install the [SDK package](https://www.npmjs.com/package/@codebirds/sdk) -- note this is different than the CLI codebirds package.
```bash
-npm install @codebuff/sdk
+npm install @codebirds/sdk
```
Import the client and run agents!
```typescript
-import { CodebuffClient } from '@codebuff/sdk'
+import { CodebirdsClient } from '@codebirds/sdk'
// 1. Initialize the client
-const client = new CodebuffClient({
+const client = new CodebirdsClient({
apiKey: 'your-api-key',
cwd: '/path/to/your/project',
- onError: (error) => console.error('Codebuff error:', error.message),
+ onError: (error) => console.error('Codebirds error:', error.message),
})
// 2. Do a coding task...
const result = await client.run({
- agent: 'base', // Codebuff's base coding agent
+ agent: 'base', // Codebirds' base coding agent
prompt: 'Add error handling to all API endpoints',
handleEvent: (event) => {
console.log('Progress', event)
@@ -214,17 +173,17 @@ await client.run({
})
```
-Learn more about the SDK [here](https://www.npmjs.com/package/@codebuff/sdk).
+Learn more about the SDK [here](https://www.npmjs.com/package/@codebirds/sdk).
-## Why choose Codebuff
+## Why choose Codebirds
**Custom workflows**: TypeScript generators let you mix AI generation with programmatic control. Agents can spawn subagents, branch on conditions, and run multi-step processes.
-**Any model on OpenRouter**: Unlike Claude Code which locks you into Anthropic's models, Codebuff supports any model available on [OpenRouter](https://openrouter.ai/models) - from Claude and GPT to specialized models like Qwen, DeepSeek, and others. Switch models for different tasks or use the latest releases without waiting for platform updates.
+**Any model on OpenRouter**: Unlike Claude Code which locks you into Anthropic's models, Codebirds supports any model available on [OpenRouter](https://openrouter.ai/models) - from Claude and GPT to specialized models like Qwen, DeepSeek, and others. Switch models for different tasks or use the latest releases without waiting for platform updates.
-**Reuse any published agent**: Compose existing [published agents](https://www.codebuff.com/store) to get a leg up. Codebuff agents are the new MCP!
+**Reuse any published agent**: Compose existing [published agents](https://www.codebirds.com/store) to get a leg up. Codebirds agents are the new MCP!
-**SDK**: Build Codebuff into your applications. Create custom tools, integrate with CI/CD, or embed coding assistance into your products.
+**SDK**: Build Codebirds into your applications. Create custom tools, integrate with CI/CD, or embed coding assistance into your products.
## Advanced Usage
@@ -233,13 +192,13 @@ Learn more about the SDK [here](https://www.npmjs.com/package/@codebuff/sdk).
Create your own agents with specialized workflows using the `/init` command:
```bash
-codebuff
+codebirds
/init
```
This creates a custom agent structure in `.agents/` that you can customize.
-## Contributing to Codebuff
+## Contributing to Codebirds
We ❤️ contributions from the community - whether you're fixing bugs, tweaking our agents, or improving documentation.
@@ -275,30 +234,28 @@ Some ways you can help:
- 🐛 **Fix bugs** or add features
- 🤖 **Create specialized agents** and publish them to the Agent Store
- 📚 **Improve documentation** or write tutorials
-- 💡 **Share ideas** in our [GitHub Issues](https://github.com/CodebuffAI/codebuff/issues)
+- 💡 **Share ideas** in our [GitHub Issues](https://github.com/CodebirdsAI/codebirds/issues)
## Get started
### Install
-**CLI**: `npm install -g codebuff`
-
-**SDK**: `npm install @codebuff/sdk`
+**CLI**: `npm install -g codebirds`
-**Freebuff (free)**: `npm install -g freebuff`
+**SDK**: `npm install @codebirds/sdk`
### Resources
-**Documentation**: [codebuff.com/docs](https://codebuff.com/docs)
+**Documentation**: [codebirds.com/docs](https://codebirds.com/docs)
-**Community**: [Discord](https://codebuff.com/discord)
+**Community**: [Discord](https://codebirds.com/discord)
-**Issues & Ideas**: [GitHub Issues](https://github.com/CodebuffAI/codebuff/issues)
+**Issues & Ideas**: [GitHub Issues](https://github.com/CodebirdsAI/codebirds/issues)
**Contributing**: [CONTRIBUTING.md](./CONTRIBUTING.md) - Start here to contribute!
-**Support**: [support@codebuff.com](mailto:support@codebuff.com)
+**Support**: [support@codebirds.com](mailto:support@codebirds.com)
## Star History
-[](https://www.star-history.com/#CodebuffAI/codebuff&Date)
+[](https://www.star-history.com/#CodebirdsAI/codebirds&Date)
diff --git a/README.zh-CN.md b/README.zh-CN.md
index 984e2f94cb..54bc9a6a02 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -1,21 +1,21 @@
-# Codebuff & Freebuff
+# Codebirds
[English](./README.md) | 简体中文
-**[Codebuff](https://codebuff.com)** 是一款开源的 AI 编程助手,能根据自然语言指令直接修改你的代码库。**[Freebuff](https://www.npmjs.com/package/freebuff)** 是它的免费、广告支持版本——无需订阅、无需积分、零配置。
+**Codebirds** 是一款开源的 AI 编程助手,能根据自然语言指令直接修改你的代码库。
-与那种"一个模型干所有事"的工具不同,Codebuff 会协调多个专业化的智能体(agent)协同工作,理解你的项目并做出精准的改动。
+与那种"一个模型干所有事"的工具不同,Codebirds 会协调多个专业化的智能体(agent)协同工作,理解你的项目并做出精准的改动。
-
+
-在我们的[评测](evals/README.md)中,Codebuff 在 175+ 个真实开源仓库的编码任务上以 61% 对 53% 的成绩领先 Claude Code。
+在我们的[评测](evals/README.md)中,Codebirds 在 175+ 个真实开源仓库的编码任务上以 61% 对 53% 的成绩领先 Claude Code。
## 工作原理
-当你让 Codebuff "给我的 API 加上身份验证"时,它可能会调用:
+当你让 Codebirds "给我的 API 加上身份验证"时,它可能会调用:
1. **File Picker Agent** —— 扫描代码库、理解架构、找出相关文件
2. **Planner Agent** —— 规划哪些文件需要改、按什么顺序改
@@ -23,7 +23,7 @@
4. **Reviewer Agent** —— 校验改动是否正确
-
+
相比单模型工具,这种多智能体方案能带来更准的上下文理解、更精确的修改,以及更少的错误。
@@ -33,30 +33,30 @@
安装:
```bash
-npm install -g codebuff
+npm install -g codebirds
```
运行:
```bash
cd your-project
-codebuff
+codebirds
```
-然后直接告诉 Codebuff 你想做什么,剩下的它自己搞定:
+然后直接告诉 Codebirds 你想做什么,剩下的它自己搞定:
- "修掉用户注册里的 SQL 注入漏洞"
- "给所有 API 端点加上限流"
- "重构数据库连接代码,提升性能"
-Codebuff 会找到对应的文件,跨多个文件做改动,并跑测试确认没有破坏现有功能。
+Codebirds 会找到对应的文件,跨多个文件做改动,并跑测试确认没有破坏现有功能。
## 创建自定义智能体
-要开始构建自己的智能体,先启动 Codebuff 然后执行 `/init`:
+要开始构建自己的智能体,先启动 Codebirds 然后执行 `/init`:
```bash
-codebuff
+codebirds
```
进入 CLI 后:
@@ -67,7 +67,7 @@ codebuff
这会生成:
```
-knowledge.md # Codebuff 用的项目上下文
+knowledge.md # Codebirds 用的项目上下文
.agents/
└── types/ # TypeScript 类型定义
├── agent-definition.ts
@@ -104,27 +104,27 @@ export default {
## SDK:在生产环境里跑智能体
-安装 [SDK 包](https://www.npmjs.com/package/@codebuff/sdk)——注意这跟 CLI 用的 codebuff 包是两个不同的包。
+安装 [SDK 包](https://www.npmjs.com/package/@codebirds/sdk)——注意这跟 CLI 用的 codebirds 包是两个不同的包。
```bash
-npm install @codebuff/sdk
+npm install @codebirds/sdk
```
引入 client,开始跑智能体:
```typescript
-import { CodebuffClient } from '@codebuff/sdk'
+import { CodebirdsClient } from '@codebirds/sdk'
// 1. 初始化 client
-const client = new CodebuffClient({
+const client = new CodebirdsClient({
apiKey: 'your-api-key',
cwd: '/path/to/your/project',
- onError: (error) => console.error('Codebuff error:', error.message),
+ onError: (error) => console.error('Codebirds error:', error.message),
})
// 2. 跑一个编码任务……
const result = await client.run({
- agent: 'base', // Codebuff 默认的基础编码智能体
+ agent: 'base', // Codebirds 默认的基础编码智能体
prompt: 'Add error handling to all API endpoints',
handleEvent: (event) => {
console.log('Progress', event)
@@ -149,29 +149,17 @@ await client.run({
})
```
-更多 SDK 用法请看[这里](https://www.npmjs.com/package/@codebuff/sdk)。
+更多 SDK 用法请看[这里](https://www.npmjs.com/package/@codebirds/sdk)。
-## Freebuff:免费的编程智能体
-
-不想订阅?**[Freebuff](https://www.npmjs.com/package/freebuff)** 是 Codebuff 的免费版本——无需订阅、无需积分、零配置,装上就能用。
-
-```bash
-npm install -g freebuff
-cd your-project
-freebuff
-```
-
-Freebuff 由广告支持,使用经过优化、兼顾速度与质量的模型。内置网页检索、浏览器使用等能力。详情见 [Freebuff README](./freebuff/README.md)。
-
-## 为什么选 Codebuff
+## 为什么选 Codebirds
**自定义工作流**:用 TypeScript 生成器把 AI 生成和程序化控制混着用。智能体可以派生子智能体、按条件分支、跑多步流程。
-**OpenRouter 上的任何模型**:Claude Code 把你锁死在 Anthropic 的模型上,Codebuff 不一样——它支持 [OpenRouter](https://openrouter.ai/models) 上的所有模型,从 Claude、GPT 到 Qwen、DeepSeek 这类专用模型都行。可以按任务切换模型,也能随时用上最新发布的模型,不必等平台跟进。
+**OpenRouter 上的任何模型**:Claude Code 把你锁死在 Anthropic 的模型上,Codebirds 不一样——它支持 [OpenRouter](https://openrouter.ai/models) 上的所有模型,从 Claude、GPT 到 Qwen、DeepSeek 这类专用模型都行。可以按任务切换模型,也能随时用上最新发布的模型,不必等平台跟进。
-**复用已发布的智能体**:把社区[已发布的智能体](https://www.codebuff.com/store)拼起来用,少走弯路。Codebuff 智能体就是新一代的 MCP!
+**复用已发布的智能体**:把社区[已发布的智能体](https://www.codebirds.com/store)拼起来用,少走弯路。Codebirds 智能体就是新一代的 MCP!
-**SDK**:把 Codebuff 嵌进你自己的应用里。可以创建自定义工具、对接 CI/CD,或把编码能力内嵌进你的产品。
+**SDK**:把 Codebirds 嵌进你自己的应用里。可以创建自定义工具、对接 CI/CD,或把编码能力内嵌进你的产品。
## 进阶用法
@@ -180,7 +168,7 @@ Freebuff 由广告支持,使用经过优化、兼顾速度与质量的模型
用 `/init` 命令创建带专门工作流的智能体:
```bash
-codebuff
+codebirds
/init
```
@@ -222,30 +210,28 @@ sudo apt-get install tmux
- 🐛 **修 bug** 或新增功能
- 🤖 **打造专用智能体**并发布到 Agent Store
- 📚 **完善文档**或撰写教程
-- 💡 **分享想法**:在 [GitHub Issues](https://github.com/CodebuffAI/codebuff/issues) 留言
+- 💡 **分享想法**:在 [GitHub Issues](https://github.com/CodebirdsAI/codebirds/issues) 留言
## 开始使用
### 安装
-**CLI**:`npm install -g codebuff`
-
-**SDK**:`npm install @codebuff/sdk`
+**CLI**:`npm install -g codebirds`
-**Freebuff(免费版)**:`npm install -g freebuff`
+**SDK**:`npm install @codebirds/sdk`
### 资源
-**文档**:[codebuff.com/docs](https://codebuff.com/docs)
+**文档**:[codebirds.com/docs](https://codebirds.com/docs)
-**社区**:[Discord](https://codebuff.com/discord)
+**社区**:[Discord](https://codebirds.com/discord)
-**Issue 与想法**:[GitHub Issues](https://github.com/CodebuffAI/codebuff/issues)
+**Issue 与想法**:[GitHub Issues](https://github.com/CodebirdsAI/codebirds/issues)
**贡献指南**:[CONTRIBUTING.md](./CONTRIBUTING.md) ——想贡献从这里开始!
-**支持**:[support@codebuff.com](mailto:support@codebuff.com)
+**支持**:[support@codebirds.com](mailto:support@codebirds.com)
## Star 历史
-[](https://www.star-history.com/#CodebuffAI/codebuff&Date)
+[](https://www.star-history.com/#CodebirdsAI/codebirds&Date)
diff --git a/SECURITY.md b/SECURITY.md
index 00aa7c5e04..6daa9fe44f 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -1,7 +1,7 @@
# Reporting Security Issues
-If you believe you have found a security vulnerability in CodeBuff, we encourage you to let us know right away. We will investigate all legitimate reports and do our best to quickly fix the problem.
+If you believe you have found a security vulnerability in Codebirds, we encourage you to let us know right away. We will investigate all legitimate reports and do our best to quickly fix the problem.
-Email us at: `support@codebuff.com`
+Email us at: `support@codebirds.com`
Please do not report security vulnerabilities through public GitHub issues.
\ No newline at end of file
diff --git a/WINDOWS.md b/WINDOWS.md
index 885783a7e3..4cee876e29 100644
--- a/WINDOWS.md
+++ b/WINDOWS.md
@@ -1,4 +1,4 @@
-## Codebuff for Windows dev setup
+## Codebirds for Windows dev setup
Welcome!
@@ -21,14 +21,14 @@ Supposedly you can also use Window's Remote Desktop to access the machine instea
## Set-up guide:
-You shouldn't need this - but just in case you stop using Shadow.tech, or make a new account, here's a guide on how to get from a blank Windows install to a Codebuff install.
+You shouldn't need this - but just in case you stop using Shadow.tech, or make a new account, here's a guide on how to get from a blank Windows install to a Codebirds install.
Surprisingly: most guides in fact recommend running everything in an Admin PowerShell, contra to advice to not use sudo on eg: Linux/macOS.
- Install Choco: Open PowerShell as Admin, and run the command from https://chocolatey.org/install
- Install NVM: Restart PowerShell (still as Admin) and run `choco install nvm -y`
- Install Node: Restart PowerShell (still as Admin) and run `nvm install node`
-- Install Codebuff: Run `npm i -g codebuff`
+- Install Codebirds: Run `npm i -g codebirds`
---
@@ -40,13 +40,13 @@ Running into problems? Here are solutions to the most common Windows-specific is
**Symptom**:
```powershell
-PS C:\> codebuff
+PS C:\> codebirds
❌ Failed to determine latest version
Please check your internet connection and try again
```
**Cause**:
-Codebuff checks GitHub for the latest release on first run. This fails when:
+Codebirds checks GitHub for the latest release on first run. This fails when:
- Corporate firewall blocks `github.com`
- Proxy settings not configured
- Network connectivity issues
@@ -56,25 +56,25 @@ Codebuff checks GitHub for the latest release on first run. This fails when:
1. **Set the `HTTPS_PROXY` environment variable** (if behind corporate proxy):
- Codebuff natively supports proxy environment variables. This is the recommended fix:
+ Codebirds natively supports proxy environment variables. This is the recommended fix:
**PowerShell:**
```powershell
$env:HTTPS_PROXY = "http://your-proxy-server:port"
- codebuff
+ codebirds
```
**CMD:**
```cmd
set HTTPS_PROXY=http://your-proxy-server:port
- codebuff
+ codebirds
```
To make it permanent, add `HTTPS_PROXY` to your Windows System Environment Variables (Settings → System → Advanced → Environment Variables).
2. **Verify network access**:
```powershell
- curl https://registry.npmjs.org/codebuff/latest
+ curl https://registry.npmjs.org/codebirds/latest
```
If this fails, you have a network/firewall issue.
@@ -83,18 +83,18 @@ Codebuff checks GitHub for the latest release on first run. This fails when:
npm config set proxy http://your-proxy-server:port
npm config set https-proxy http://your-proxy-server:port
```
- Note: This only helps with `npm install`. Codebuff's own downloads use `HTTPS_PROXY` instead.
+ Note: This only helps with `npm install`. Codebirds's own downloads use `HTTPS_PROXY` instead.
-4. **Disable VPN temporarily** or whitelist `registry.npmjs.org` and `codebuff.com` in your firewall
+4. **Disable VPN temporarily** or whitelist `registry.npmjs.org` and `codebirds.com` in your firewall
5. **Clear npm cache and reinstall**:
```powershell
npm cache clean --force
- npm uninstall -g codebuff
- npm install -g codebuff
+ npm uninstall -g codebirds
+ npm install -g codebirds
```
-**Reference**: Issue [#294](https://github.com/CodebuffAI/codebuff/issues/294)
+**Reference**: Issue [#294](https://github.com/CodebirdsAI/codebirds/issues/294)
---
@@ -106,7 +106,7 @@ Bash is required but was not found on this Windows system.
```
**Cause**:
-Codebuff requires bash for command execution. This error appears when:
+Codebirds requires bash for command execution. This error appears when:
- Git for Windows is not installed
- You're not running inside WSL
- bash.exe is not in your PATH
@@ -115,21 +115,21 @@ Codebuff requires bash for command execution. This error appears when:
1. **Install Git for Windows** (recommended):
- Download from https://git-scm.com/download/win
- - This installs `bash.exe` which Codebuff will automatically detect
+ - This installs `bash.exe` which Codebirds will automatically detect
- Works in PowerShell, CMD, or Git Bash terminals
2. **Use WSL (Windows Subsystem for Linux)**:
- Provides full Linux environment with native bash
- Install: `wsl --install` in PowerShell (Admin)
- - Run codebuff inside WSL for best compatibility
+ - Run codebirds inside WSL for best compatibility
3. **Set custom bash path** (advanced):
- If bash.exe is installed in a non-standard location:
```powershell
- set CODEBUFF_GIT_BASH_PATH=C:\path\to\bash.exe
+ set CODEBIRDS_GIT_BASH_PATH=C:\path\to\bash.exe
```
-**Reference**: Issue [#274](https://github.com/CodebuffAI/codebuff/issues/274)
+**Reference**: Issue [#274](https://github.com/CodebirdsAI/codebirds/issues/274)
---
@@ -145,14 +145,14 @@ Complex git commands may have issues with Windows path handling or shell escapin
1. **Ensure Git for Windows is installed**:
- Download from https://git-scm.com/download/win
- - Codebuff uses bash.exe from Git for Windows for command execution
+ - Codebirds uses bash.exe from Git for Windows for command execution
2. **Use WSL for complex operations**:
- Provides full Linux environment with native bash
- Install: `wsl --install` in PowerShell (Admin)
- - Run codebuff inside WSL for best compatibility
+ - Run codebirds inside WSL for best compatibility
-**Reference**: Issue [#274](https://github.com/CodebuffAI/codebuff/issues/274)
+**Reference**: Issue [#274](https://github.com/CodebirdsAI/codebirds/issues/274)
---
@@ -168,13 +168,13 @@ TLCWeb > Unable to login. Please try again by typing "login" in the terminal.
```
**Cause**:
-When running Codebuff in Git Bash (MINGW64), the `start` command is not available in PATH. The browser auto-open feature fails.
+When running Codebirds in Git Bash (MINGW64), the `start` command is not available in PATH. The browser auto-open feature fails.
**Solutions**:
1. **Manually open the login URL** (easiest):
- - Codebuff displays the login URL after the error
- - Copy the full URL starting with `https://codebuff.com/login?auth_code=...`
+ - Codebirds displays the login URL after the error
+ - Copy the full URL starting with `https://codebirds.com/login?auth_code=...`
- Paste into your browser
- Complete login in browser
- Return to terminal - login will succeed
@@ -187,11 +187,11 @@ When running Codebuff in Git Bash (MINGW64), the `start` command is not availabl
3. **Clear cache if login still fails** (per issue #299):
```powershell
npm cache clean --force
- npm uninstall -g codebuff
- npm install -g codebuff
+ npm uninstall -g codebirds
+ npm install -g codebirds
```
-**Reference**: Issue [#299](https://github.com/CodebuffAI/codebuff/issues/299)
+**Reference**: Issue [#299](https://github.com/CodebirdsAI/codebirds/issues/299)
---
@@ -201,14 +201,14 @@ When running Codebuff in Git Bash (MINGW64), the `start` command is not availabl
This is **not an error** - it's an informational message indicating:
- Your local binary needs to be downloaded/updated
- "error" is a placeholder version (not a real error state)
-- Codebuff will automatically download the correct version
+- Codebirds will automatically download the correct version
**What to do**:
-- Wait for the download to complete: "Download complete! Starting Codebuff..."
+- Wait for the download to complete: "Download complete! Starting Codebirds..."
- If download fails, check your internet connection
- If it persists, try the solutions in "Failed to determine latest version" above
-**Reference**: Issue [#299](https://github.com/CodebuffAI/codebuff/issues/299)
+**Reference**: Issue [#299](https://github.com/CodebirdsAI/codebirds/issues/299)
---
@@ -216,9 +216,9 @@ This is **not an error** - it's an informational message indicating:
If these solutions don't resolve your problem:
-1. **Search existing issues**: https://github.com/CodebuffAI/codebuff/issues
-2. **Open a new issue**: https://github.com/CodebuffAI/codebuff/issues/new
-3. **Join Discord community**: https://codebuff.com/discord
+1. **Search existing issues**: https://github.com/CodebirdsAI/codebirds/issues
+2. **Open a new issue**: https://github.com/CodebirdsAI/codebirds/issues/new
+3. **Join Discord community**: https://codebirds.com/discord
When reporting issues, please include:
- Windows version: `winver` command
diff --git a/agents/__tests__/base2.test.ts b/agents/__tests__/base2.test.ts
index 26cc73d0f2..02d42c23ac 100644
--- a/agents/__tests__/base2.test.ts
+++ b/agents/__tests__/base2.test.ts
@@ -1,23 +1,23 @@
import { describe, expect, test } from 'bun:test'
import {
- FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID,
- FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID,
- FREEBUFF_KIMI_MODEL_ID,
- FREEBUFF_MINIMAX_MODEL_ID,
- FREEBUFF_MINIMAX_M3_MODEL_ID,
- FREEBUFF_MIMO_V25_MODEL_ID,
- FREEBUFF_MIMO_V25_PRO_MODEL_ID,
-} from '@codebuff/common/constants/freebuff-models'
+ CODEBIRDS_DEEPSEEK_V4_FLASH_MODEL_ID,
+ CODEBIRDS_DEEPSEEK_V4_PRO_MODEL_ID,
+ CODEBIRDS_KIMI_MODEL_ID,
+ CODEBIRDS_MINIMAX_MODEL_ID,
+ CODEBIRDS_MINIMAX_M3_MODEL_ID,
+ CODEBIRDS_MIMO_V25_MODEL_ID,
+ CODEBIRDS_MIMO_V25_PRO_MODEL_ID,
+} from '@codebirds/common/constants/codebirds-models'
import { createBase2 } from '../base2/base2'
import codeReviewerLite from '../reviewer/code-reviewer-lite'
describe('base2 reviewer selection', () => {
- test('Codebuff lite uses MiniMax M3 and its matching reviewer', () => {
+ test('Codebirds lite uses MiniMax M3 and its matching reviewer', () => {
const base2 = createBase2('lite')
- expect(base2.model).toBe(FREEBUFF_MINIMAX_M3_MODEL_ID)
+ expect(base2.model).toBe(CODEBIRDS_MINIMAX_M3_MODEL_ID)
expect(base2.spawnableAgents).toContain('code-reviewer-minimax-m3')
expect(base2.instructionsPrompt).toContain(
'Spawn a code-reviewer-minimax-m3',
@@ -26,17 +26,17 @@ describe('base2 reviewer selection', () => {
})
test('legacy lite reviewer definition uses DeepSeek V4 Flash', () => {
- expect(codeReviewerLite.model).toBe(FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID)
+ expect(codeReviewerLite.model).toBe(CODEBIRDS_DEEPSEEK_V4_FLASH_MODEL_ID)
})
test.each([
- [FREEBUFF_MINIMAX_MODEL_ID, 'code-reviewer-minimax'],
- [FREEBUFF_MINIMAX_M3_MODEL_ID, 'code-reviewer-minimax-m3'],
- [FREEBUFF_KIMI_MODEL_ID, 'code-reviewer-kimi'],
- [FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID, 'code-reviewer-deepseek'],
- [FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID, 'code-reviewer-deepseek-flash'],
- [FREEBUFF_MIMO_V25_PRO_MODEL_ID, 'code-reviewer-mimo-pro'],
- [FREEBUFF_MIMO_V25_MODEL_ID, 'code-reviewer-mimo'],
+ [CODEBIRDS_MINIMAX_MODEL_ID, 'code-reviewer-minimax'],
+ [CODEBIRDS_MINIMAX_M3_MODEL_ID, 'code-reviewer-minimax-m3'],
+ [CODEBIRDS_KIMI_MODEL_ID, 'code-reviewer-kimi'],
+ [CODEBIRDS_DEEPSEEK_V4_PRO_MODEL_ID, 'code-reviewer-deepseek'],
+ [CODEBIRDS_DEEPSEEK_V4_FLASH_MODEL_ID, 'code-reviewer-deepseek-flash'],
+ [CODEBIRDS_MIMO_V25_PRO_MODEL_ID, 'code-reviewer-mimo-pro'],
+ [CODEBIRDS_MIMO_V25_MODEL_ID, 'code-reviewer-mimo'],
])('uses matching reviewer for model %p', (model, expectedReviewer) => {
const base2 = createBase2('free', { model })
@@ -92,7 +92,7 @@ describe('base2 context pruning', () => {
test('free Kimi mode defaults context pruning to 250k tokens', () => {
expect(
- getContextPrunerParams('free', { model: FREEBUFF_KIMI_MODEL_ID }),
+ getContextPrunerParams('free', { model: CODEBIRDS_KIMI_MODEL_ID }),
).toEqual({
maxContextLength: 250_000,
cacheExpiryMs: 30 * 60 * 1000,
@@ -102,7 +102,7 @@ describe('base2 context pruning', () => {
test('free non-MiniMax/Kimi models default context pruning to 400k tokens', () => {
expect(
getContextPrunerParams('free', {
- model: FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID,
+ model: CODEBIRDS_DEEPSEEK_V4_FLASH_MODEL_ID,
}),
).toEqual({
maxContextLength: 400_000,
@@ -137,9 +137,9 @@ describe('base2 context pruning', () => {
)
test.each([
- [FREEBUFF_MINIMAX_MODEL_ID, 200_000],
- [FREEBUFF_KIMI_MODEL_ID, 250_000],
- [FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID, 400_000],
+ [CODEBIRDS_MINIMAX_MODEL_ID, 200_000],
+ [CODEBIRDS_KIMI_MODEL_ID, 250_000],
+ [CODEBIRDS_DEEPSEEK_V4_PRO_MODEL_ID, 400_000],
] as const)(
'non-free model %p defaults context pruning to %p tokens',
(model, maxContextLength) => {
@@ -150,12 +150,12 @@ describe('base2 context pruning', () => {
)
test.each([
- ['free', { model: FREEBUFF_MINIMAX_MODEL_ID }, 200_000],
- ['free', { model: FREEBUFF_KIMI_MODEL_ID }, 250_000],
- ['free', { model: FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID }, 400_000],
- ['default', { model: FREEBUFF_MINIMAX_MODEL_ID }, 200_000],
- ['default', { model: FREEBUFF_KIMI_MODEL_ID }, 250_000],
- ['default', { model: FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID }, 400_000],
+ ['free', { model: CODEBIRDS_MINIMAX_MODEL_ID }, 200_000],
+ ['free', { model: CODEBIRDS_KIMI_MODEL_ID }, 250_000],
+ ['free', { model: CODEBIRDS_DEEPSEEK_V4_PRO_MODEL_ID }, 400_000],
+ ['default', { model: CODEBIRDS_MINIMAX_MODEL_ID }, 200_000],
+ ['default', { model: CODEBIRDS_KIMI_MODEL_ID }, 250_000],
+ ['default', { model: CODEBIRDS_DEEPSEEK_V4_PRO_MODEL_ID }, 400_000],
] as const)(
'serialized %s handleSteps for model %p defaults to %p tokens',
(mode, options, maxContextLength) => {
@@ -170,7 +170,7 @@ describe('base2 context pruning', () => {
getContextPrunerParams(
'default',
{
- model: FREEBUFF_KIMI_MODEL_ID,
+ model: CODEBIRDS_KIMI_MODEL_ID,
},
{
maxContextLength: 123_000,
diff --git a/agents/__tests__/editor.test.ts b/agents/__tests__/editor.test.ts
index f4ed457d1b..7b9cb8e9c6 100644
--- a/agents/__tests__/editor.test.ts
+++ b/agents/__tests__/editor.test.ts
@@ -146,9 +146,9 @@ describe('editor agent', () => {
expect(editor.instructionsPrompt).toContain('content')
})
- test('contains codebuff_tool_call format', () => {
- expect(editor.instructionsPrompt).toContain('')
- expect(editor.instructionsPrompt).toContain(' ')
+ test('contains codebirds_tool_call format', () => {
+ expect(editor.instructionsPrompt).toContain('')
+ expect(editor.instructionsPrompt).toContain(' ')
})
test('instructs not to call set_output', () => {
diff --git a/agents/__tests__/file-picker.test.ts b/agents/__tests__/file-picker.test.ts
index f82d829510..98ed6cb411 100644
--- a/agents/__tests__/file-picker.test.ts
+++ b/agents/__tests__/file-picker.test.ts
@@ -576,7 +576,7 @@ describe('file-picker agent', () => {
describe('system prompt', () => {
test('contains file tree placeholder', () => {
- expect(filePicker.systemPrompt).toContain('{CODEBUFF_FILE_TREE_PROMPT}')
+ expect(filePicker.systemPrompt).toContain('{CODEBIRDS_FILE_TREE_PROMPT}')
})
test('describes file finding purpose', () => {
diff --git a/agents/base-chat.ts b/agents/base-chat.ts
index f0fec00413..f54619eee6 100644
--- a/agents/base-chat.ts
+++ b/agents/base-chat.ts
@@ -1,11 +1,11 @@
-import { FREEBUFF_DEEPSEEK_V4_FLASH_FIREWORKS_MODEL_ID } from '@codebuff/common/constants/freebuff-models'
+import { CODEBIRDS_DEEPSEEK_V4_FLASH_FIREWORKS_MODEL_ID } from '@codebirds/common/constants/codebirds-models'
import { publisher } from './constants'
import type { SecretAgentDefinition } from './types/secret-agent-definition'
/**
- * Conversational agent behind freebuff.com/chat. Runs with no filesystem, but
+ * Conversational agent behind codebirds.com/chat. Runs with no filesystem, but
* can spawn researcher-web to look things up on the live internet and call
* gravity_index to recommend third-party developer services. The chat server
* may override `model` per request (DeepSeek Flash vs Pro for full-access
@@ -14,9 +14,9 @@ import type { SecretAgentDefinition } from './types/secret-agent-definition'
const definition: SecretAgentDefinition = {
id: 'base-chat',
publisher,
- model: FREEBUFF_DEEPSEEK_V4_FLASH_FIREWORKS_MODEL_ID,
+ model: CODEBIRDS_DEEPSEEK_V4_FLASH_FIREWORKS_MODEL_ID,
displayName: 'Freebuff Chat',
- spawnerPrompt: 'General-purpose chat assistant for freebuff.com/chat.',
+ spawnerPrompt: 'General-purpose chat assistant for codebirds.com/chat.',
inputSchema: {
prompt: {
type: 'string',
@@ -27,7 +27,7 @@ const definition: SecretAgentDefinition = {
toolNames: ['spawn_agents', 'gravity_index', 'suggest_followups'],
spawnableAgents: ['researcher-web', 'thinker-gemini'],
- systemPrompt: `You are Freebuff Chat, a friendly, sharp assistant made by Freebuff (freebuff.com), the home of free AI coding tools. You are chatting with a user in a web interface that renders markdown.`,
+ systemPrompt: `You are Freebuff Chat, a friendly, sharp assistant made by Freebuff (codebirds.com), the home of free AI coding tools. You are chatting with a user in a web interface that renders markdown.`,
instructionsPrompt: `Be direct and helpful. Use markdown when it improves clarity (code blocks, lists, tables), and keep answers as short as they can be while fully answering the question.
When the user is choosing a third-party developer service (database, auth, payments, hosting, email, monitoring, analytics, AI APIs, storage, CMS, search, etc.) or asks what provider to use for something, use the gravity_index tool instead of answering from memory: \`search\` with a query that includes their stack and constraints when they want a recommendation, or \`browse\`/\`list_categories\`/\`get_service\` to explore options. Ground your answer in the result. When a search result includes a tracked setup link (\`credential_request.setup_url\` or \`click_url\`), present that exact URL prominently as a markdown link like "Get your {service} API key" — never swap in the vendor homepage for it. Since you can't edit the user's files, share the relevant setup steps and env vars in chat instead of trying to install anything.
diff --git a/agents/base2/base-deep.ts b/agents/base2/base-deep.ts
index 58e780eb55..6e8dd25e23 100644
--- a/agents/base2/base-deep.ts
+++ b/agents/base2/base-deep.ts
@@ -1,4 +1,4 @@
-import { buildArray } from '@codebuff/common/util/array'
+import { buildArray } from '@codebirds/common/util/array'
import { publisher } from '../constants'
import {
@@ -7,7 +7,7 @@ import {
} from '../types/secret-agent-definition'
function buildDeepSystemPrompt(noAskUser: boolean, noLearning: boolean): string {
- return `You are Buffy, a strategic assistant that orchestrates complex coding tasks through specialized sub-agents. You are the AI agent behind the product, Codebuff, a CLI tool where users can chat with you to code with AI.
+ return `You are Buffy, a strategic assistant that orchestrates complex coding tasks through specialized sub-agents. You are the AI agent behind the product, Codebirds, a CLI tool where users can chat with you to code with AI.
# Core Mandates
@@ -36,7 +36,7 @@ Use the spawn_agents tool to spawn specialized agents to help you complete the u
- **No need to include context:** When prompting an agent, realize that many agents can already see the entire conversation history, so you can be brief in prompting them without needing to include context.
- **Never spawn the context-pruner agent:** This agent is spawned automatically for you and you don't need to spawn it yourself.
-# Codebuff Meta-information
+# Codebirds Meta-information
Users send prompts to you in one of a few user-selected modes, like DEFAULT, MAX, or PLAN.
@@ -44,7 +44,7 @@ Every prompt sent consumes the user's credits, which is calculated based on the
The user can use the "/usage" command to see how many credits they have used and have left, so you can tell them to check their usage this way.
-For other questions, you can direct them to codebuff.com, or especially codebuff.com/docs for detailed information about the product.
+For other questions, you can direct them to codebirds.com, or especially codebirds.com/docs for detailed information about the product.
# Other response guidelines
diff --git a/agents/base2/base2-free-deepseek-flash.ts b/agents/base2/base2-free-deepseek-flash.ts
index 77dd48543e..51ae3a6703 100644
--- a/agents/base2/base2-free-deepseek-flash.ts
+++ b/agents/base2/base2-free-deepseek-flash.ts
@@ -1,10 +1,10 @@
-import { FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID } from '@codebuff/common/constants/freebuff-models'
+import { CODEBIRDS_DEEPSEEK_V4_FLASH_MODEL_ID } from '@codebirds/common/constants/codebirds-models'
import { createBase2 } from './base2'
const definition = {
...createBase2('free', {
- model: FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID,
+ model: CODEBIRDS_DEEPSEEK_V4_FLASH_MODEL_ID,
}),
id: 'base2-free-deepseek-flash',
displayName: 'Buffy the DeepSeek Flash Free Orchestrator',
diff --git a/agents/base2/base2-free-deepseek.ts b/agents/base2/base2-free-deepseek.ts
index b73bb4730e..59e5f8ae1d 100644
--- a/agents/base2/base2-free-deepseek.ts
+++ b/agents/base2/base2-free-deepseek.ts
@@ -1,10 +1,10 @@
-import { FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID } from '@codebuff/common/constants/freebuff-models'
+import { CODEBIRDS_DEEPSEEK_V4_PRO_MODEL_ID } from '@codebirds/common/constants/codebirds-models'
import { createBase2 } from './base2'
const definition = {
...createBase2('free', {
- model: FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID,
+ model: CODEBIRDS_DEEPSEEK_V4_PRO_MODEL_ID,
}),
id: 'base2-free-deepseek',
displayName: 'Buffy the DeepSeek Free Orchestrator',
diff --git a/agents/base2/base2-free-glm.ts b/agents/base2/base2-free-glm.ts
index f590b3cdd9..7a4ce764fa 100644
--- a/agents/base2/base2-free-glm.ts
+++ b/agents/base2/base2-free-glm.ts
@@ -1,10 +1,10 @@
-import { FREEBUFF_GLM_V52_MODEL_ID } from '@codebuff/common/constants/freebuff-models'
+import { CODEBIRDS_GLM_V52_MODEL_ID } from '@codebirds/common/constants/codebirds-models'
import { createBase2 } from './base2'
const definition = {
...createBase2('free', {
- model: FREEBUFF_GLM_V52_MODEL_ID,
+ model: CODEBIRDS_GLM_V52_MODEL_ID,
}),
id: 'base2-free-glm',
displayName: 'Buffy the GLM 5.2 Free Orchestrator',
diff --git a/agents/base2/base2-free-kimi.ts b/agents/base2/base2-free-kimi.ts
index fc31625eef..2b01172ef2 100644
--- a/agents/base2/base2-free-kimi.ts
+++ b/agents/base2/base2-free-kimi.ts
@@ -1,10 +1,10 @@
-import { FREEBUFF_KIMI_MODEL_ID } from '@codebuff/common/constants/freebuff-models'
+import { CODEBIRDS_KIMI_MODEL_ID } from '@codebirds/common/constants/codebirds-models'
import { createBase2 } from './base2'
const definition = {
...createBase2('free', {
- model: FREEBUFF_KIMI_MODEL_ID,
+ model: CODEBIRDS_KIMI_MODEL_ID,
}),
id: 'base2-free-kimi',
displayName: 'Buffy the Kimi Free Orchestrator',
diff --git a/agents/base2/base2-free-mimo-pro.ts b/agents/base2/base2-free-mimo-pro.ts
index 5fca1979c8..4a9ef2134c 100644
--- a/agents/base2/base2-free-mimo-pro.ts
+++ b/agents/base2/base2-free-mimo-pro.ts
@@ -1,10 +1,10 @@
-import { FREEBUFF_MIMO_V25_PRO_MODEL_ID } from '@codebuff/common/constants/freebuff-models'
+import { CODEBIRDS_MIMO_V25_PRO_MODEL_ID } from '@codebirds/common/constants/codebirds-models'
import { createBase2 } from './base2'
const definition = {
...createBase2('free', {
- model: FREEBUFF_MIMO_V25_PRO_MODEL_ID,
+ model: CODEBIRDS_MIMO_V25_PRO_MODEL_ID,
}),
id: 'base2-free-mimo-pro',
displayName: 'Buffy the MiMo Pro Free Orchestrator',
diff --git a/agents/base2/base2-free-mimo.ts b/agents/base2/base2-free-mimo.ts
index 51bf2de41a..623ec4bd95 100644
--- a/agents/base2/base2-free-mimo.ts
+++ b/agents/base2/base2-free-mimo.ts
@@ -1,10 +1,10 @@
-import { FREEBUFF_MIMO_V25_MODEL_ID } from '@codebuff/common/constants/freebuff-models'
+import { CODEBIRDS_MIMO_V25_MODEL_ID } from '@codebirds/common/constants/codebirds-models'
import { createBase2 } from './base2'
const definition = {
...createBase2('free', {
- model: FREEBUFF_MIMO_V25_MODEL_ID,
+ model: CODEBIRDS_MIMO_V25_MODEL_ID,
}),
id: 'base2-free-mimo',
displayName: 'Buffy the MiMo Free Orchestrator',
diff --git a/agents/base2/base2-free-minimax-m3.ts b/agents/base2/base2-free-minimax-m3.ts
index a1d41af84d..6ea4050188 100644
--- a/agents/base2/base2-free-minimax-m3.ts
+++ b/agents/base2/base2-free-minimax-m3.ts
@@ -1,10 +1,10 @@
-import { FREEBUFF_MINIMAX_M3_MODEL_ID } from '@codebuff/common/constants/freebuff-models'
+import { CODEBIRDS_MINIMAX_M3_MODEL_ID } from '@codebirds/common/constants/codebirds-models'
import { createBase2 } from './base2'
const definition = {
...createBase2('free', {
- model: FREEBUFF_MINIMAX_M3_MODEL_ID,
+ model: CODEBIRDS_MINIMAX_M3_MODEL_ID,
}),
id: 'base2-free-minimax-m3',
displayName: 'Buffy the MiniMax M3 Free Orchestrator',
diff --git a/agents/base2/base2-kimi-2-7-code.ts b/agents/base2/base2-kimi-2-7-code.ts
index d5b9af70d1..0692d79f13 100644
--- a/agents/base2/base2-kimi-2-7-code.ts
+++ b/agents/base2/base2-kimi-2-7-code.ts
@@ -1,4 +1,4 @@
-import { moonshotModels } from '@codebuff/common/constants/model-config'
+import { moonshotModels } from '@codebirds/common/constants/model-config'
import { createBase2 } from './base2'
diff --git a/agents/base2/base2-mimo.ts b/agents/base2/base2-mimo.ts
index c49787928f..a2167b9cf7 100644
--- a/agents/base2/base2-mimo.ts
+++ b/agents/base2/base2-mimo.ts
@@ -1,4 +1,4 @@
-import { mimoModels } from '@codebuff/common/constants/model-config'
+import { mimoModels } from '@codebirds/common/constants/model-config'
import { createBase2 } from './base2'
diff --git a/agents/base2/base2.ts b/agents/base2/base2.ts
index d1d501717c..73e425bba8 100644
--- a/agents/base2/base2.ts
+++ b/agents/base2/base2.ts
@@ -1,18 +1,18 @@
-import { buildArray } from '@codebuff/common/util/array'
-import { COMPOSIO_META_TOOL_NAMES } from '@codebuff/common/constants/composio'
+import { buildArray } from '@codebirds/common/util/array'
+import { COMPOSIO_META_TOOL_NAMES } from '@codebirds/common/constants/composio'
import {
- FREEBUFF_GEMINI_THINKER_AGENT_ID,
- FREEBUFF_GEMINI_THINKER_INSTRUCTIONS_PROMPT,
- FREEBUFF_GEMINI_THINKER_STEP_PROMPT,
- FREEBUFF_GEMINI_THINKER_SYSTEM_INSTRUCTION,
-} from '@codebuff/common/constants/freebuff-gemini-thinker'
-import { FREEBUFF_REVIEWER_AGENT_ID_BY_MODEL } from '@codebuff/common/constants/free-agents'
+ CODEBIRDS_GEMINI_THINKER_AGENT_ID,
+ CODEBIRDS_GEMINI_THINKER_INSTRUCTIONS_PROMPT,
+ CODEBIRDS_GEMINI_THINKER_STEP_PROMPT,
+ CODEBIRDS_GEMINI_THINKER_SYSTEM_INSTRUCTION,
+} from '@codebirds/common/constants/codebirds-gemini-thinker'
+import { CODEBIRDS_REVIEWER_AGENT_ID_BY_MODEL } from '@codebirds/common/constants/free-agents'
import {
canFreebuffModelSpawnGeminiThinker,
- FREEBUFF_KIMI_MODEL_ID,
- FREEBUFF_MINIMAX_MODEL_ID,
- FREEBUFF_MINIMAX_M3_MODEL_ID,
-} from '@codebuff/common/constants/freebuff-models'
+ CODEBIRDS_KIMI_MODEL_ID,
+ CODEBIRDS_MINIMAX_MODEL_ID,
+ CODEBIRDS_MINIMAX_M3_MODEL_ID,
+} from '@codebirds/common/constants/codebirds-models'
import { publisher } from '../constants'
import {
@@ -53,16 +53,16 @@ export function createBase2(
const model =
modelOverride ??
(mode === 'lite'
- ? FREEBUFF_MINIMAX_M3_MODEL_ID
+ ? CODEBIRDS_MINIMAX_M3_MODEL_ID
: mode === 'free'
- ? FREEBUFF_MINIMAX_MODEL_ID
+ ? CODEBIRDS_MINIMAX_MODEL_ID
: 'anthropic/claude-opus-4.8')
- // Smart freebuff model variants (Kimi, DeepSeek) can offload deeper
+ // Smart codebirds model variants (Kimi, DeepSeek) can offload deeper
// reasoning. Fast MiniMax omits the extra round trip by construction.
const hasFreeGeminiThinker =
isFree && canFreebuffModelSpawnGeminiThinker(model)
const freeCodeReviewerAgentId =
- FREEBUFF_REVIEWER_AGENT_ID_BY_MODEL[model] ?? 'code-reviewer-lite'
+ CODEBIRDS_REVIEWER_AGENT_ID_BY_MODEL[model] ?? 'code-reviewer-lite'
const contextPrunerMaxContextLength =
getBase2ContextPrunerMaxContextLength(model)
const defaultProviderOptions = isFree
@@ -134,12 +134,12 @@ export function createBase2(
isFree && !noReview && freeCodeReviewerAgentId,
isDefault && 'code-reviewer',
isMax && 'code-reviewer-multi-prompt',
- hasFreeGeminiThinker && FREEBUFF_GEMINI_THINKER_AGENT_ID,
+ hasFreeGeminiThinker && CODEBIRDS_GEMINI_THINKER_AGENT_ID,
'thinker-gpt',
'context-pruner',
),
- systemPrompt: `You are Buffy, a strategic assistant that orchestrates complex coding tasks through specialized sub-agents. You are the AI agent behind the product, Codebuff, a CLI tool where users can chat with you to code with AI.
+ systemPrompt: `You are Buffy, a strategic assistant that orchestrates complex coding tasks through specialized sub-agents. You are the AI agent behind the product, Codebirds, a CLI tool where users can chat with you to code with AI.
Current date: ${PLACEHOLDER.CURRENT_DATE}.
@@ -200,8 +200,8 @@ Use the spawn_agents tool to spawn specialized agents to help you complete the u
${buildArray(
'- Spawn context-gathering agents (file pickers, code searchers, and web/docs researchers) before making edits. Use the list_directory and glob tools directly for searching and exploring the codebase.',
isFree &&
- 'Do not spawn the thinker-gpt agent, unless the user asks. Not everyone has connected their ChatGPT subscription to Codebuff to allow for it.',
- hasFreeGeminiThinker && FREEBUFF_GEMINI_THINKER_SYSTEM_INSTRUCTION,
+ 'Do not spawn the thinker-gpt agent, unless the user asks. Not everyone has connected their ChatGPT subscription to Codebirds to allow for it.',
+ hasFreeGeminiThinker && CODEBIRDS_GEMINI_THINKER_SYSTEM_INSTRUCTION,
isDefault &&
'- Spawn the editor agent to implement the changes after you have gathered all the context you need.',
(isDefault || isMax) &&
@@ -220,7 +220,7 @@ Use the spawn_agents tool to spawn specialized agents to help you complete the u
- **No need to include context:** When prompting an agent, realize that many agents can already see the entire conversation history, so you can be brief in prompting them without needing to include context.
- **Never spawn the context-pruner agent:** This agent is spawned automatically for you and you don't need to spawn it yourself.
-# Codebuff Meta-information
+# Codebirds Meta-information
You are running on the ${model} model.
@@ -230,7 +230,7 @@ Every prompt sent consumes the user's credits, which is calculated based on the
The user can use the "/usage" command to see how many credits they have used and have left, so you can tell them to check their usage this way.
-For other questions, you can direct them to codebuff.com, or especially codebuff.com/docs for detailed information about the product.
+For other questions, you can direct them to codebirds.com, or especially codebirds.com/docs for detailed information about the product.
# Other response guidelines
@@ -365,8 +365,8 @@ type Base2HandleSteps = NonNullable
function getBase2ContextPrunerMaxContextLength(
model: SecretAgentDefinition['model'],
): 200_000 | 250_000 | 400_000 {
- if (model === FREEBUFF_MINIMAX_MODEL_ID) return 200_000
- if (model === FREEBUFF_KIMI_MODEL_ID) return 250_000
+ if (model === CODEBIRDS_MINIMAX_MODEL_ID) return 200_000
+ if (model === CODEBIRDS_KIMI_MODEL_ID) return 250_000
return 400_000
}
@@ -543,7 +543,7 @@ ${buildArray(
'After getting context on the user request from the codebase or from research, use the ask_user tool to ask the user for important clarifications on their request or alternate implementation strategies. You should skip this step if the choice is obvious -- only ask the user if you need their help making the best choice.',
(isDefault || isMax || isFree) &&
`- For any task requiring 3+ steps, use the write_todos tool to write out your step-by-step implementation plan. Include ALL of the applicable tasks in the list.${isFast || noReview ? '' : ' You should include a step to review the changes after you have implemented the changes.'}:${hasNoValidation ? '' : ' You should include at least one step to validate/test your changes: be specific about whether to typecheck, run tests, run lints, etc.'} You may be able to do reviewing and validation in parallel in the same step. Skip write_todos for simple tasks like quick edits or answering questions.`,
- hasFreeGeminiThinker && FREEBUFF_GEMINI_THINKER_INSTRUCTIONS_PROMPT,
+ hasFreeGeminiThinker && CODEBIRDS_GEMINI_THINKER_INSTRUCTIONS_PROMPT,
(isDefault || isMax) &&
`- For quick problems, briefly explain your reasoning to the user. If you need to think longer, write your thoughts within the tags. Finally, for complex problems, spawn the thinker agent to help find the best solution. (gpt-5-agent is a last resort for complex problems)`,
isDefault &&
@@ -594,7 +594,7 @@ function buildImplementationStepPrompt({
return buildArray(
isMax &&
`Keep working until the user's request is completely satisfied${!hasNoValidation ? ' and validated' : ''}, or until you require more information from the user.`,
- hasFreeGeminiThinker && FREEBUFF_GEMINI_THINKER_STEP_PROMPT,
+ hasFreeGeminiThinker && CODEBIRDS_GEMINI_THINKER_STEP_PROMPT,
isMax &&
`You must spawn the 'editor-multi-prompt' agent to implement code changes rather than using the str_replace or write_file tools, since it will generate the best code changes.`,
(isDefault || isMax) &&
diff --git a/agents/browser-use/browser-use.test.ts b/agents/browser-use/browser-use.test.ts
index 1fad69e7d8..11bb554e3a 100644
--- a/agents/browser-use/browser-use.test.ts
+++ b/agents/browser-use/browser-use.test.ts
@@ -13,9 +13,9 @@
import * as fs from 'fs'
import * as path from 'path'
-import { CodebuffClient, loadLocalAgents } from '@codebuff/sdk'
+import { CodebirdsClient, loadLocalAgents } from '@codebirds/sdk'
-import type { AgentDefinition } from '@codebuff/sdk'
+import type { AgentDefinition } from '@codebirds/sdk'
const TRACE_DIR = path.join(process.cwd(), 'debug', 'browser-agent-traces')
@@ -53,7 +53,7 @@ interface TraceEvent {
}
async function runTask(
- client: CodebuffClient,
+ client: CodebirdsClient,
task: TaskDefinition,
agentDefinitions: AgentDefinition[],
taskIndex: number,
@@ -168,8 +168,8 @@ async function main() {
}
console.log(`Loaded browser-use agent (model: ${browserAgent.model})`)
- const client = new CodebuffClient({
- apiKey: process.env.CODEBUFF_API_KEY,
+ const client = new CodebirdsClient({
+ apiKey: process.env.CODEBIRDS_API_KEY,
cwd: process.cwd(),
})
diff --git a/agents/constants.ts b/agents/constants.ts
index 86591a2f32..68b43291e5 100644
--- a/agents/constants.ts
+++ b/agents/constants.ts
@@ -1 +1 @@
-export const publisher = 'codebuff'
+export const publisher = 'codebirds'
diff --git a/agents/e2e/base-deep.e2e.test.ts b/agents/e2e/base-deep.e2e.test.ts
index 2ca99935c9..d220b3c1ab 100644
--- a/agents/e2e/base-deep.e2e.test.ts
+++ b/agents/e2e/base-deep.e2e.test.ts
@@ -3,15 +3,15 @@ import os from 'os'
import path from 'path'
import { fileURLToPath } from 'url'
-import { API_KEY_ENV_VAR } from '@codebuff/common/old-constants'
-import { CodebuffClient, getUserCredentials } from '@codebuff/sdk'
+import { API_KEY_ENV_VAR } from '@codebirds/common/old-constants'
+import { CodebirdsClient, getUserCredentials } from '@codebirds/sdk'
import { beforeAll, describe, expect, it } from 'bun:test'
import { $ } from 'bun'
import baseDeep from '../base2/base-deep'
import thinkerCodex from '../thinker/thinker-gpt'
-import type { PrintModeEvent } from '@codebuff/common/types/print-mode'
+import type { PrintModeEvent } from '@codebirds/common/types/print-mode'
describe('Base Deep Agent Integration', () => {
const repoRoot = path.resolve(
@@ -211,7 +211,7 @@ describe('Base Deep Agent Integration', () => {
if (!apiKey) return
const events: PrintModeEvent[] = []
- const client = new CodebuffClient({
+ const client = new CodebirdsClient({
apiKey,
cwd: '/tmp/base-deep-thinker-test',
projectFiles: {
@@ -264,7 +264,7 @@ describe('Base Deep Agent Integration', () => {
const notePath = path.join(tmpDir, 'note.txt')
await fs.promises.writeFile(notePath, 'status: draft\n', 'utf-8')
- const client = new CodebuffClient({
+ const client = new CodebirdsClient({
apiKey,
cwd: tmpDir,
agentDefinitions: [baseDeep, thinkerCodex],
@@ -395,7 +395,7 @@ describe('Base Deep Agent Integration', () => {
}
const events: PrintModeEvent[] = []
- const client = new CodebuffClient({
+ const client = new CodebirdsClient({
apiKey,
cwd: tmpDir,
agentDefinitions: [baseDeep, thinkerCodex],
@@ -487,14 +487,14 @@ describe('Base Deep Agent Integration', () => {
)
slowIt(
- 'works on a shallow-cloned codebuff repo for a commit-inspired focused task',
+ 'works on a shallow-cloned codebirds repo for a commit-inspired focused task',
async () => {
const apiKey = getApiKeyOrSkip()
if (!apiKey) return
const cloneDir = await createShallowClone()
const events: PrintModeEvent[] = []
- const client = new CodebuffClient({
+ const client = new CodebirdsClient({
apiKey,
cwd: cloneDir,
agentDefinitions: [baseDeep, thinkerCodex],
@@ -528,7 +528,7 @@ describe('Base Deep Agent Integration', () => {
await writeTrace({
testName:
- 'works on a shallow-cloned codebuff repo for a commit-inspired focused task',
+ 'works on a shallow-cloned codebirds repo for a commit-inspired focused task',
events,
runOutput: run.output,
cwd: cloneDir,
@@ -549,7 +549,7 @@ describe('Base Deep Agent Integration', () => {
const cloneDir = await createShallowClone()
const events: PrintModeEvent[] = []
- const client = new CodebuffClient({
+ const client = new CodebirdsClient({
apiKey,
cwd: cloneDir,
agentDefinitions: [baseDeep, thinkerCodex],
diff --git a/agents/e2e/base2-free-summary-format.e2e.test.ts b/agents/e2e/base2-free-summary-format.e2e.test.ts
index 00f0a7b0db..6c35e796f3 100644
--- a/agents/e2e/base2-free-summary-format.e2e.test.ts
+++ b/agents/e2e/base2-free-summary-format.e2e.test.ts
@@ -2,20 +2,20 @@ import fs from 'fs'
import os from 'os'
import path from 'path'
-import { API_KEY_ENV_VAR } from '@codebuff/common/constants/paths'
+import { API_KEY_ENV_VAR } from '@codebirds/common/constants/paths'
import {
- CodebuffClient,
+ CodebirdsClient,
initialSessionState,
withMessageHistory,
type AgentDefinition,
type Message,
-} from '@codebuff/sdk'
+} from '@codebirds/sdk'
import { beforeAll, describe, expect, it } from 'bun:test'
import base2Free from '../base2/base2-free'
import contextPruner from '../context-pruner'
-import type { PrintModeEvent } from '@codebuff/common/types/print-mode'
+import type { PrintModeEvent } from '@codebirds/common/types/print-mode'
/**
* Patterns that indicate the model is imitating the summarized tool call format
@@ -311,7 +311,7 @@ describe('Base2-Free Summary Format Compliance', () => {
await fs.promises.writeFile(fullPath, content, 'utf-8')
}
- const client = new CodebuffClient({
+ const client = new CodebirdsClient({
apiKey,
cwd: tmpDir,
projectFiles: PROJECT_FILES,
@@ -449,7 +449,7 @@ describe('Base2-Free Summary Format Compliance', () => {
await fs.promises.writeFile(fullPath, content, 'utf-8')
}
- const client = new CodebuffClient({
+ const client = new CodebirdsClient({
apiKey,
cwd: tmpDir,
projectFiles: PROJECT_FILES,
diff --git a/agents/e2e/context-pruner.e2e.test.ts b/agents/e2e/context-pruner.e2e.test.ts
index 0ac2c5ec2f..376261e47a 100644
--- a/agents/e2e/context-pruner.e2e.test.ts
+++ b/agents/e2e/context-pruner.e2e.test.ts
@@ -1,17 +1,17 @@
-import { API_KEY_ENV_VAR } from '@codebuff/common/old-constants'
+import { API_KEY_ENV_VAR } from '@codebirds/common/old-constants'
import {
- CodebuffClient,
+ CodebirdsClient,
initialSessionState,
withMessageHistory,
type AgentDefinition,
type Message,
type ToolMessage,
type JSONValue,
-} from '@codebuff/sdk'
+} from '@codebirds/sdk'
import { describe, expect, it } from 'bun:test'
-import type { ToolCallPart } from '@codebuff/common/types/messages/content-part'
+import type { ToolCallPart } from '@codebirds/common/types/messages/content-part'
/**
* Type guard to check if a content part is a tool-call part with toolCallId.
@@ -143,7 +143,7 @@ Do not do anything else. Just spawn context-pruner and then report the result.`,
createMessage('user', 'Now spawn the context-pruner'),
]
- const client = new CodebuffClient({
+ const client = new CodebirdsClient({
apiKey,
agentDefinitions: [testAgent],
})
@@ -274,7 +274,7 @@ Do not do anything else. Just spawn context-pruner and then report the result.`,
createMessage('user', 'Now prune the context'),
]
- const client = new CodebuffClient({
+ const client = new CodebirdsClient({
apiKey,
agentDefinitions: [testAgent],
})
diff --git a/agents/e2e/context-pruning-threshold.e2e.test.ts b/agents/e2e/context-pruning-threshold.e2e.test.ts
index fbb2988ce3..30c6d7326b 100644
--- a/agents/e2e/context-pruning-threshold.e2e.test.ts
+++ b/agents/e2e/context-pruning-threshold.e2e.test.ts
@@ -22,21 +22,21 @@
* Both count as successful pruning for our purposes.
*/
-import { API_KEY_ENV_VAR } from '@codebuff/common/old-constants'
+import { API_KEY_ENV_VAR } from '@codebirds/common/old-constants'
import {
- CodebuffClient,
+ CodebirdsClient,
initialSessionState,
withMessageHistory,
type AgentDefinition,
type Message,
type ToolMessage,
type JSONValue,
-} from '@codebuff/sdk'
+} from '@codebirds/sdk'
import { describe, expect, it } from 'bun:test'
import contextPruner from '../context-pruner'
-import type { ToolCallPart } from '@codebuff/common/types/messages/content-part'
+import type { ToolCallPart } from '@codebirds/common/types/messages/content-part'
/**
* Type guard to check if a content part is a tool-call part with toolCallId.
@@ -317,7 +317,7 @@ describe('Context Pruning Threshold E2E', () => {
// With maxContextLength=100k, this should be well below the pruning threshold
const messages = buildMessageHistory(30_000)
- const client = new CodebuffClient({
+ const client = new CodebirdsClient({
apiKey,
agentDefinitions: [testAgent, contextPruner],
})
@@ -395,7 +395,7 @@ describe('Context Pruning Threshold E2E', () => {
// With maxContextLength=50k, this should exceed the pruning threshold
const messages = buildMessageHistory(80_000)
- const client = new CodebuffClient({
+ const client = new CodebirdsClient({
apiKey,
agentDefinitions: [testAgent, contextPruner],
})
@@ -491,7 +491,7 @@ describe('Context Pruning Threshold E2E', () => {
const TARGET_ESTIMATED_TOKENS = 95_000
const messages = buildMessageHistory(TARGET_ESTIMATED_TOKENS)
- const client = new CodebuffClient({
+ const client = new CodebirdsClient({
apiKey,
agentDefinitions: [testAgent, contextPruner],
})
diff --git a/agents/e2e/editor-best-of-n.e2e.test.ts b/agents/e2e/editor-best-of-n.e2e.test.ts
index 9eb1a85b13..35c455c7c8 100644
--- a/agents/e2e/editor-best-of-n.e2e.test.ts
+++ b/agents/e2e/editor-best-of-n.e2e.test.ts
@@ -1,9 +1,9 @@
-import { API_KEY_ENV_VAR } from '@codebuff/common/old-constants'
-import { CodebuffClient } from '@codebuff/sdk'
+import { API_KEY_ENV_VAR } from '@codebirds/common/old-constants'
+import { CodebirdsClient } from '@codebirds/sdk'
import { describe, expect, it } from 'bun:test'
-import type { PrintModeEvent } from '@codebuff/common/types/print-mode'
+import type { PrintModeEvent } from '@codebirds/common/types/print-mode'
/**
* Integration tests for the editor-best-of-n-max agent.
@@ -46,7 +46,7 @@ console.log(subtract(5, 3))
}),
}
- const client = new CodebuffClient({
+ const client = new CodebirdsClient({
apiKey,
cwd: '/tmp/test-best-of-n-project',
projectFiles,
diff --git a/agents/e2e/file-explorer.e2e.test.ts b/agents/e2e/file-explorer.e2e.test.ts
index 960ca48e34..e1ba5167d5 100644
--- a/agents/e2e/file-explorer.e2e.test.ts
+++ b/agents/e2e/file-explorer.e2e.test.ts
@@ -1,11 +1,11 @@
-import { API_KEY_ENV_VAR } from '@codebuff/common/old-constants'
-import { CodebuffClient } from '@codebuff/sdk'
+import { API_KEY_ENV_VAR } from '@codebirds/common/old-constants'
+import { CodebirdsClient } from '@codebirds/sdk'
import { describe, expect, it } from 'bun:test'
import fileListerDefinition from '../file-explorer/file-lister'
import filePickerDefinition from '../file-explorer/file-picker'
-import type { PrintModeEvent } from '@codebuff/common/types/print-mode'
+import type { PrintModeEvent } from '@codebirds/common/types/print-mode'
/**
* Integration tests for agents that use the read_subtree tool.
@@ -91,7 +91,7 @@ export interface User {
'# Test Project\n\nA simple test project for integration testing.',
}
- const client = new CodebuffClient({
+ const client = new CodebirdsClient({
apiKey,
cwd: '/tmp/test-project',
projectFiles,
@@ -154,7 +154,7 @@ export interface User {
'package.json': JSON.stringify({ name: 'mono-repo', version: '2.0.0' }),
}
- const client = new CodebuffClient({
+ const client = new CodebirdsClient({
apiKey,
cwd: '/tmp/test-project',
projectFiles,
@@ -206,7 +206,7 @@ export interface User {
'package.json': JSON.stringify({ name: 'full-stack-app' }),
}
- const client = new CodebuffClient({
+ const client = new CodebirdsClient({
apiKey,
cwd: '/tmp/test-project',
projectFiles,
@@ -287,7 +287,7 @@ export class AuthService {
const localFilePickerDef = filePickerDefinition
const localFileListerDef = fileListerDefinition
- const client = new CodebuffClient({
+ const client = new CodebirdsClient({
apiKey,
cwd: '/tmp/test-project-picker',
projectFiles,
diff --git a/agents/e2e/gravity-index.e2e.test.ts b/agents/e2e/gravity-index.e2e.test.ts
index 64bdc9fd2d..bec7e992df 100644
--- a/agents/e2e/gravity-index.e2e.test.ts
+++ b/agents/e2e/gravity-index.e2e.test.ts
@@ -2,13 +2,13 @@ import fs from 'fs'
import os from 'os'
import path from 'path'
-import { API_KEY_ENV_VAR } from '@codebuff/common/constants/paths'
-import { CodebuffClient, type AgentDefinition } from '@codebuff/sdk'
+import { API_KEY_ENV_VAR } from '@codebirds/common/constants/paths'
+import { CodebirdsClient, type AgentDefinition } from '@codebirds/sdk'
import { describe, expect, it } from 'bun:test'
import base2Free from '../base2/base2-free'
-import type { PrintModeEvent } from '@codebuff/common/types/print-mode'
+import type { PrintModeEvent } from '@codebirds/common/types/print-mode'
describe('Gravity Index SDK E2E', () => {
it(
@@ -40,7 +40,7 @@ For this E2E test, use the gravity_index tool when asked to recommend third-part
} satisfies AgentDefinition
try {
- const client = new CodebuffClient({
+ const client = new CodebirdsClient({
apiKey,
cwd: tmpDir,
projectFiles: {
diff --git a/agents/editor/best-of-n/editor-implementor.ts b/agents/editor/best-of-n/editor-implementor.ts
index c82ec3a466..9271ea396d 100644
--- a/agents/editor/best-of-n/editor-implementor.ts
+++ b/agents/editor/best-of-n/editor-implementor.ts
@@ -45,7 +45,7 @@ Your task is to write out ALL the code changes needed to complete the user's req
IMPORTANT: Use propose_str_replace and propose_write_file tools to make your edits. These tools draft changes without actually applying them - they will be reviewed first. DO NOT use any other tools. Do not spawn any agents, read files, or set output.
You can make multiple tool calls across multiple steps to complete the implementation. Only the file changes will be passed on, so you can say whatever you want to help you think. Do not write any final summary as that would be a waste of tokens because no one is reading it.
-
+
{
"cb_tool_name": "propose_str_replace",
"path": "path/to/file",
@@ -60,18 +60,18 @@ You can make multiple tool calls across multiple steps to complete the implement
},
]
}
-
+
OR for new files or major rewrites:
-
+
{
"cb_tool_name": "propose_write_file",
"path": "path/to/file",
"instructions": "What the change does",
"content": "Complete file content"
}
-
+
${
isGpt5 || isGemini
? ``
@@ -86,21 +86,21 @@ You can also use tags interspersed between tool calls to think about the
[ Long think about the best way to implement the changes ]
-
+
[ First tool call to implement the feature ]
-
+
-
+
[ Second tool call to implement the feature ]
-
+
[ Thoughts about a tricky part of the implementation ]
-
+
[ Third tool call to implement the feature ]
-
+
`
}
diff --git a/agents/editor/editor.ts b/agents/editor/editor.ts
index 60b649a210..bdff87bf3f 100644
--- a/agents/editor/editor.ts
+++ b/agents/editor/editor.ts
@@ -55,7 +55,7 @@ Important: You can not make any other tool calls besides editing files. You cann
Write out what changes you would make using the tool call format below. Use this exact format for each file change:
-
+
{
"cb_tool_name": "str_replace",
"path": "path/to/file",
@@ -70,18 +70,18 @@ Write out what changes you would make using the tool call format below. Use this
},
]
}
-
+
OR for new files or major rewrites:
-
+
{
"cb_tool_name": "write_file",
"path": "path/to/file",
"instructions": "What the change does",
"content": "Complete file content"
}
-
+
${
EDITOR_VARIANTS_WITH_THINK_TAGS.has(model)
@@ -95,21 +95,21 @@ You can also use tags interspersed between tool calls to think about the
[ Long think about the best way to implement the changes ]
-
+
[ First tool call to implement the feature ]
-
+
-
+
[ Second tool call to implement the feature ]
-
+
[ Thoughts about a tricky part of the implementation ]
-
+
[ Third tool call to implement the feature ]
-
+
`
: ''
diff --git a/agents/file-explorer/glob-matcher.ts b/agents/file-explorer/glob-matcher.ts
index 5f2f0f8894..28b90205f5 100644
--- a/agents/file-explorer/glob-matcher.ts
+++ b/agents/file-explorer/glob-matcher.ts
@@ -2,7 +2,7 @@
import { publisher } from '../constants'
import type { SecretAgentDefinition } from '../types/secret-agent-definition'
-import type { JSONValue } from '@codebuff/common/types/json'
+import type { JSONValue } from '@codebirds/common/types/json'
interface GlobQuery {
pattern: string
diff --git a/agents/general-agent/general-agent.ts b/agents/general-agent/general-agent.ts
index f025f5a67e..a0ad4a6280 100644
--- a/agents/general-agent/general-agent.ts
+++ b/agents/general-agent/general-agent.ts
@@ -1,4 +1,4 @@
-import { buildArray } from '@codebuff/common/util/array'
+import { buildArray } from '@codebirds/common/util/array'
import { publisher } from '../constants'
diff --git a/agents/librarian/librarian.test.ts b/agents/librarian/librarian.test.ts
index bd2d29d955..71aa917b65 100644
--- a/agents/librarian/librarian.test.ts
+++ b/agents/librarian/librarian.test.ts
@@ -13,9 +13,9 @@
import * as fs from 'fs'
import * as path from 'path'
-import { CodebuffClient, loadLocalAgents } from '@codebuff/sdk'
+import { CodebirdsClient, loadLocalAgents } from '@codebirds/sdk'
-import type { AgentDefinition } from '@codebuff/sdk'
+import type { AgentDefinition } from '@codebirds/sdk'
const TRACE_DIR = path.join(process.cwd(), 'debug', 'librarian-traces')
@@ -53,7 +53,7 @@ interface LibrarianOutput {
}
async function runTask(
- client: CodebuffClient,
+ client: CodebirdsClient,
task: TaskDefinition,
agentDefinitions: AgentDefinition[],
taskIndex: number,
@@ -245,8 +245,8 @@ async function main() {
}
console.log(`Loaded librarian agent (model: ${librarianAgent.model})`)
- const client = new CodebuffClient({
- apiKey: process.env.CODEBUFF_API_KEY,
+ const client = new CodebirdsClient({
+ apiKey: process.env.CODEBIRDS_API_KEY,
cwd: process.cwd(),
})
diff --git a/agents/package.json b/agents/package.json
index 073b1a089c..958c5b7168 100644
--- a/agents/package.json
+++ b/agents/package.json
@@ -1,5 +1,5 @@
{
- "name": "@codebuff/agents",
+ "name": "@codebirds/agents",
"version": "0.0.0",
"private": true,
"type": "module",
diff --git a/agents/reviewer/code-reviewer-deepseek-flash.ts b/agents/reviewer/code-reviewer-deepseek-flash.ts
index 23550079f2..bfa2510cf2 100644
--- a/agents/reviewer/code-reviewer-deepseek-flash.ts
+++ b/agents/reviewer/code-reviewer-deepseek-flash.ts
@@ -1,4 +1,4 @@
-import { FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID } from '@codebuff/common/constants/freebuff-models'
+import { CODEBIRDS_DEEPSEEK_V4_FLASH_MODEL_ID } from '@codebirds/common/constants/codebirds-models'
import { publisher } from '../constants'
import type { SecretAgentDefinition } from '../types/secret-agent-definition'
@@ -7,7 +7,7 @@ import { createReviewer } from './code-reviewer'
const definition: SecretAgentDefinition = {
id: 'code-reviewer-deepseek-flash',
publisher,
- ...createReviewer(FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID),
+ ...createReviewer(CODEBIRDS_DEEPSEEK_V4_FLASH_MODEL_ID),
}
export default definition
diff --git a/agents/reviewer/code-reviewer-glm.ts b/agents/reviewer/code-reviewer-glm.ts
index 086031ae8e..bc26ce6495 100644
--- a/agents/reviewer/code-reviewer-glm.ts
+++ b/agents/reviewer/code-reviewer-glm.ts
@@ -1,4 +1,4 @@
-import { FREEBUFF_GLM_V52_MODEL_ID } from '@codebuff/common/constants/freebuff-models'
+import { CODEBIRDS_GLM_V52_MODEL_ID } from '@codebirds/common/constants/codebirds-models'
import { publisher } from '../constants'
import type { SecretAgentDefinition } from '../types/secret-agent-definition'
@@ -7,7 +7,7 @@ import { createReviewer } from './code-reviewer'
const definition: SecretAgentDefinition = {
id: 'code-reviewer-glm',
publisher,
- ...createReviewer(FREEBUFF_GLM_V52_MODEL_ID),
+ ...createReviewer(CODEBIRDS_GLM_V52_MODEL_ID),
}
export default definition
diff --git a/agents/reviewer/code-reviewer-lite.ts b/agents/reviewer/code-reviewer-lite.ts
index ae246c5f63..49dfe483ae 100644
--- a/agents/reviewer/code-reviewer-lite.ts
+++ b/agents/reviewer/code-reviewer-lite.ts
@@ -1,4 +1,4 @@
-import { deepseekModels } from '@codebuff/common/constants/model-config'
+import { deepseekModels } from '@codebirds/common/constants/model-config'
import { publisher } from '../constants'
import type { SecretAgentDefinition } from '../types/secret-agent-definition'
diff --git a/agents/reviewer/code-reviewer-mimo-pro.ts b/agents/reviewer/code-reviewer-mimo-pro.ts
index 3f960dd389..8752960a2d 100644
--- a/agents/reviewer/code-reviewer-mimo-pro.ts
+++ b/agents/reviewer/code-reviewer-mimo-pro.ts
@@ -1,4 +1,4 @@
-import { FREEBUFF_MIMO_V25_PRO_MODEL_ID } from '@codebuff/common/constants/freebuff-models'
+import { CODEBIRDS_MIMO_V25_PRO_MODEL_ID } from '@codebirds/common/constants/codebirds-models'
import { publisher } from '../constants'
import type { SecretAgentDefinition } from '../types/secret-agent-definition'
@@ -7,7 +7,7 @@ import { createReviewer } from './code-reviewer'
const definition: SecretAgentDefinition = {
id: 'code-reviewer-mimo-pro',
publisher,
- ...createReviewer(FREEBUFF_MIMO_V25_PRO_MODEL_ID),
+ ...createReviewer(CODEBIRDS_MIMO_V25_PRO_MODEL_ID),
}
export default definition
diff --git a/agents/reviewer/code-reviewer-mimo.ts b/agents/reviewer/code-reviewer-mimo.ts
index 6110c19133..5ed068ff59 100644
--- a/agents/reviewer/code-reviewer-mimo.ts
+++ b/agents/reviewer/code-reviewer-mimo.ts
@@ -1,4 +1,4 @@
-import { FREEBUFF_MIMO_V25_MODEL_ID } from '@codebuff/common/constants/freebuff-models'
+import { CODEBIRDS_MIMO_V25_MODEL_ID } from '@codebirds/common/constants/codebirds-models'
import { publisher } from '../constants'
import type { SecretAgentDefinition } from '../types/secret-agent-definition'
@@ -7,7 +7,7 @@ import { createReviewer } from './code-reviewer'
const definition: SecretAgentDefinition = {
id: 'code-reviewer-mimo',
publisher,
- ...createReviewer(FREEBUFF_MIMO_V25_MODEL_ID),
+ ...createReviewer(CODEBIRDS_MIMO_V25_MODEL_ID),
}
export default definition
diff --git a/agents/reviewer/code-reviewer-minimax-m3.ts b/agents/reviewer/code-reviewer-minimax-m3.ts
index 945a9ef8a2..0baf3a9bbb 100644
--- a/agents/reviewer/code-reviewer-minimax-m3.ts
+++ b/agents/reviewer/code-reviewer-minimax-m3.ts
@@ -1,4 +1,4 @@
-import { FREEBUFF_MINIMAX_M3_MODEL_ID } from '@codebuff/common/constants/freebuff-models'
+import { CODEBIRDS_MINIMAX_M3_MODEL_ID } from '@codebirds/common/constants/codebirds-models'
import { publisher } from '../constants'
import type { SecretAgentDefinition } from '../types/secret-agent-definition'
@@ -7,7 +7,7 @@ import { createReviewer } from './code-reviewer'
const definition: SecretAgentDefinition = {
id: 'code-reviewer-minimax-m3',
publisher,
- ...createReviewer(FREEBUFF_MINIMAX_M3_MODEL_ID),
+ ...createReviewer(CODEBIRDS_MINIMAX_M3_MODEL_ID),
}
export default definition
diff --git a/agents/reviewer/code-reviewer.ts b/agents/reviewer/code-reviewer.ts
index 8b6363ff8e..731876ba5b 100644
--- a/agents/reviewer/code-reviewer.ts
+++ b/agents/reviewer/code-reviewer.ts
@@ -4,7 +4,7 @@ import {
type SecretAgentDefinition,
} from '../types/secret-agent-definition'
-import type { Model } from '@codebuff/common/old-constants'
+import type { Model } from '@codebirds/common/old-constants'
export const createReviewer = (
model: Model,
diff --git a/agents/tsconfig.json b/agents/tsconfig.json
index dbb372c162..8f38117316 100644
--- a/agents/tsconfig.json
+++ b/agents/tsconfig.json
@@ -5,8 +5,8 @@
"skipLibCheck": true,
"types": ["bun", "node"],
"paths": {
- "@codebuff/sdk": ["../sdk/src/index.ts"],
- "@codebuff/common/*": ["../common/src/*"]
+ "@codebirds/sdk": ["../sdk/src/index.ts"],
+ "@codebirds/common/*": ["../common/src/*"]
}
},
"include": ["**/*.ts"]
diff --git a/agents/types/agent-definition.ts b/agents/types/agent-definition.ts
index 6315dbeb17..1f61e7d410 100644
--- a/agents/types/agent-definition.ts
+++ b/agents/types/agent-definition.ts
@@ -1,7 +1,7 @@
/**
- * Codebuff Agent Type Definitions
+ * Codebirds Agent Type Definitions
*
- * This file provides TypeScript type definitions for creating custom Codebuff agents.
+ * This file provides TypeScript type definitions for creating custom Codebirds agents.
* Import these types in your agent files to get full type safety and IntelliSense.
*
* Usage in .agents/your-agent.ts:
@@ -128,9 +128,9 @@ export interface AgentDefinition {
*/
toolNames?: (ToolName | (string & {}))[]
- /** Other agents this agent can spawn, like 'codebuff/file-picker@0.0.1'.
+ /** Other agents this agent can spawn, like 'codebirds/file-picker@0.0.1'.
*
- * Use the fully qualified agent id from the agent store, including publisher and version: 'codebuff/file-picker@0.0.1'
+ * Use the fully qualified agent id from the agent store, including publisher and version: 'codebirds/file-picker@0.0.1'
* (publisher and version are required!)
*
* Or, use the agent id from a local agent file in your .agents directory: 'file-picker'.
diff --git a/agents/types/secret-agent-definition.ts b/agents/types/secret-agent-definition.ts
index 6718cea418..6f17d8c9b9 100644
--- a/agents/types/secret-agent-definition.ts
+++ b/agents/types/secret-agent-definition.ts
@@ -1,6 +1,6 @@
import type { AgentDefinition } from './agent-definition'
import type * as Tools from './tools'
-import type { ComposioMetaToolName } from '@codebuff/common/constants/composio'
+import type { ComposioMetaToolName } from '@codebirds/common/constants/composio'
export type { Tools }
export type AllToolNames =
@@ -43,11 +43,11 @@ const placeholderNames = [
] as const
type PlaceholderType = {
- [K in T[number]]: `{CODEBUFF_${K}}`
+ [K in T[number]]: `{CODEBIRDS_${K}}`
}
export const PLACEHOLDER = Object.fromEntries(
- placeholderNames.map((name) => [name, `{CODEBUFF_${name}}` as const]),
+ placeholderNames.map((name) => [name, `{CODEBIRDS_${name}}` as const]),
) as PlaceholderType
export type PlaceholderValue = (typeof PLACEHOLDER)[keyof typeof PLACEHOLDER]
export const placeholderValues = Object.values(PLACEHOLDER)
diff --git a/agents/types/tools.ts b/agents/types/tools.ts
index 91a74efacf..aa35b6a964 100644
--- a/agents/types/tools.ts
+++ b/agents/types/tools.ts
@@ -275,7 +275,7 @@ export interface ReadUrlParams {
}
/**
- * Render a small interactive UI widget in the Codebuff CLI. Currently supports a button that opens a link.
+ * Render a small interactive UI widget in the Codebirds CLI. Currently supports a button that opens a link.
*/
export interface RenderUiParams {
/** The UI widget to render. */
diff --git a/assets/codebuff-vs-claude-code.png b/assets/codebirds-vs-claude-code.png
similarity index 100%
rename from assets/codebuff-vs-claude-code.png
rename to assets/codebirds-vs-claude-code.png
diff --git a/cli/README.md b/cli/README.md
index 45d8af675a..a0d9d14f12 100644
--- a/cli/README.md
+++ b/cli/README.md
@@ -1,4 +1,4 @@
-# @codebuff/cli
+# @codebirds/cli
A Terminal User Interface (TUI) package built with OpenTUI and React.
@@ -74,7 +74,7 @@ bun run start
Or use the binary directly:
```bash
-codebuff-tui
+codebirds-tui
```
## Features
diff --git a/cli/package.json b/cli/package.json
index 82ecda523f..ef00af4e05 100644
--- a/cli/package.json
+++ b/cli/package.json
@@ -1,10 +1,10 @@
{
- "name": "@codebuff/cli",
+ "name": "@codebirds/cli",
"version": "1.0.0",
"private": true,
"type": "module",
"bin": {
- "codebuff-tui": "./bin/codecane"
+ "codebirds-tui": "./bin/codecane"
},
"exports": {
".": {
@@ -17,7 +17,7 @@
"scripts": {
"dev": "bun run prebuild:agents && bun run src/index.tsx --cwd ..",
"prebuild:agents": "bun run scripts/prebuild-agents.ts",
- "build:binary": "bun ./scripts/build-binary.ts codebuff $npm_package_version",
+ "build:binary": "bun ./scripts/build-binary.ts codebirds $npm_package_version",
"release": "bun run scripts/release.ts",
"test": "NODE_ENV=production bun test",
"test:tmux-poc": "bun run src/__tests__/tmux-poc.ts",
@@ -30,7 +30,7 @@
"bun": "1.3.11"
},
"dependencies": {
- "@codebuff/sdk": "workspace:*",
+ "@codebirds/sdk": "workspace:*",
"@gravity-ai/api": "^0.1.2",
"@opentui/core": "0.2.2",
"@opentui/react": "0.2.2",
diff --git a/cli/release-staging/README.md b/cli/release-staging/README.md
index 08194bffe4..32038a8abf 100644
--- a/cli/release-staging/README.md
+++ b/cli/release-staging/README.md
@@ -68,4 +68,4 @@ If you still have errors, it's a good idea to [reinstall Node](https://nodejs.or
## Feedback
-We value your input! Please email your feedback to `founders@codebuff.com`. Thank you for using Codecane!
+We value your input! Please email your feedback to `founders@codebirds.com`. Thank you for using Codecane!
diff --git a/cli/release-staging/index.js b/cli/release-staging/index.js
index 7a186634c5..22d37b699b 100644
--- a/cli/release-staging/index.js
+++ b/cli/release-staging/index.js
@@ -109,10 +109,10 @@ const { getProxyUrl, httpGet } = createReleaseHttpClient({
function getPostHogConfig() {
const apiKey =
- process.env.CODEBUFF_POSTHOG_API_KEY ||
+ process.env.CODEBIRDS_POSTHOG_API_KEY ||
process.env.NEXT_PUBLIC_POSTHOG_API_KEY
const host =
- process.env.CODEBUFF_POSTHOG_HOST ||
+ process.env.CODEBIRDS_POSTHOG_HOST ||
process.env.NEXT_PUBLIC_POSTHOG_HOST_URL
if (!apiKey || !host) {
@@ -135,7 +135,7 @@ function trackUpdateFailed(errorMessage, version, context = {}) {
const payload = JSON.stringify({
api_key: posthogConfig.apiKey,
- event: 'cli.update_codebuff_failed',
+ event: 'cli.update_codebirds_failed',
properties: {
distinct_id: `anonymous-${CONFIG.homeDir}`,
error: errorMessage,
@@ -209,7 +209,7 @@ function getPlatformKey() {
function getTargetOverride() {
const envNames = [
`${packageName.toUpperCase()}_BINARY_TARGET`,
- 'CODEBUFF_BINARY_TARGET',
+ 'CODEBIRDS_BINARY_TARGET',
'CLI_BINARY_TARGET',
]
@@ -425,7 +425,7 @@ async function downloadBinary(version, targetKey = getDownloadTargetKey()) {
}
const downloadUrl = `${
- process.env.NEXT_PUBLIC_CODEBUFF_APP_URL || 'https://codebuff.com'
+ process.env.NEXT_PUBLIC_CODEBIRDS_APP_URL || 'https://codebirds.com'
}/api/releases/download/${version}/${fileName}`
// Ensure config directory exists
@@ -655,7 +655,7 @@ function printCrashDiagnostics(code, signal) {
console.error(` Binary: ${CONFIG.binaryPath}`)
console.error('')
console.error('Please report this issue at:')
- console.error(' https://github.com/CodebuffAI/codebuff/issues')
+ console.error(' https://github.com/CodebirdsAI/codebirds/issues')
console.error('')
}
diff --git a/cli/release-staging/package.json b/cli/release-staging/package.json
index f84bff8721..8223dc9074 100644
--- a/cli/release-staging/package.json
+++ b/cli/release-staging/package.json
@@ -33,9 +33,9 @@
},
"repository": {
"type": "git",
- "url": "https://github.com/CodebuffAI/codebuff.git"
+ "url": "https://github.com/CodebirdsAI/codebirds.git"
},
- "homepage": "https://codebuff.com",
+ "homepage": "https://codebirds.com",
"publishConfig": {
"access": "public"
}
diff --git a/cli/release-staging/postinstall.js b/cli/release-staging/postinstall.js
index 85f5e25272..dcf77a8235 100644
--- a/cli/release-staging/postinstall.js
+++ b/cli/release-staging/postinstall.js
@@ -32,5 +32,5 @@ console.log('Example:');
console.log(' $ cd ~/my-project');
console.log(' $ codecane');
console.log('\n');
-console.log('For more information, visit: https://codebuff.com/docs');
+console.log('For more information, visit: https://codebirds.com/docs');
console.log('\n');
diff --git a/cli/release/README.md b/cli/release/README.md
index 7a54343bbc..a82b00c28f 100644
--- a/cli/release/README.md
+++ b/cli/release/README.md
@@ -1,49 +1,49 @@
# The most powerful coding agent
-Codebuff is a CLI tool that writes code for you.
+Codebirds is a CLI tool that writes code for you.
-1. Run `codebuff` from your project directory
+1. Run `codebirds` from your project directory
2. Tell it what to do
3. It will read and write to files and run commands to produce the code you want
-Note: Codebuff will run commands in your terminal as it deems necessary to fulfill your request.
+Note: Codebirds will run commands in your terminal as it deems necessary to fulfill your request.
## Installation
-To install Codebuff, run:
+To install Codebirds, run:
```bash
-npm install -g codebuff
+npm install -g codebirds
```
(Use `sudo` if you get a permission error.)
## Usage
-After installation, you can start Codebuff by running:
+After installation, you can start Codebirds by running:
```bash
-codebuff [project-directory]
+codebirds [project-directory]
```
-If no project directory is specified, Codebuff will use the current directory.
+If no project directory is specified, Codebirds will use the current directory.
-Once running, simply chat with Codebuff to say what coding task you want done.
+Once running, simply chat with Codebirds to say what coding task you want done.
## Features
- Understands your whole codebase
- Creates and edits multiple files based on your request
- Can run your tests or type checker or linter; can install packages
-- It's powerful: ask Codebuff to keep working until it reaches a condition and it will.
+- It's powerful: ask Codebirds to keep working until it reaches a condition and it will.
-Our users regularly use Codebuff to implement new features, write unit tests, refactor code,write scripts, or give advice.
+Our users regularly use Codebirds to implement new features, write unit tests, refactor code,write scripts, or give advice.
## Knowledge Files
To unlock the full benefits of modern LLMs, we recommend storing knowledge alongside your code. Add a `knowledge.md` file anywhere in your project to provide helpful context, guidance, and tips for the LLM as it performs tasks for you.
-Codebuff can fluently read and write files, so it will add knowledge as it goes. You don't need to write knowledge manually!
+Codebirds can fluently read and write files, so it will add knowledge as it goes. You don't need to write knowledge manually!
Some have said every change should be paired with a unit test. In 2024, every change should come with a knowledge update!
@@ -52,7 +52,7 @@ Some have said every change should be paired with a unit test. In 2024, every ch
1. Type '/help' or just '/' to see available commands.
2. Create a `knowledge.md` file and collect specific points of advice. The assistant will use this knowledge to improve its responses.
3. Type `undo` or `redo` to revert or reapply file changes from the conversation.
-4. Press `Esc` or `Ctrl+C` while Codebuff is generating a response to stop it.
+4. Press `Esc` or `Ctrl+C` while Codebirds is generating a response to stop it.
## Troubleshooting
@@ -61,33 +61,33 @@ Some have said every change should be paired with a unit test. In 2024, every ch
If you are getting permission errors during installation, try using sudo:
```
-sudo npm install -g codebuff
+sudo npm install -g codebirds
```
If you still have errors, it's a good idea to [reinstall Node](https://nodejs.org/en/download).
### Corporate Proxy / Firewall
-If you see `Failed to download codebuff: Request timeout` or `Failed to determine latest version`, you may be behind a corporate proxy or firewall.
+If you see `Failed to download codebirds: Request timeout` or `Failed to determine latest version`, you may be behind a corporate proxy or firewall.
-Codebuff respects standard proxy environment variables. Set `HTTPS_PROXY` to route traffic through your proxy:
+Codebirds respects standard proxy environment variables. Set `HTTPS_PROXY` to route traffic through your proxy:
**Linux / macOS (bash/zsh):**
```bash
export HTTPS_PROXY=http://your-proxy-server:port
-codebuff
+codebirds
```
**Windows (PowerShell):**
```powershell
$env:HTTPS_PROXY = "http://your-proxy-server:port"
-codebuff
+codebirds
```
**Windows (CMD):**
```cmd
set HTTPS_PROXY=http://your-proxy-server:port
-codebuff
+codebirds
```
To make it permanent, add the `export` or `set` line to your shell profile (e.g. `~/.bashrc`, `~/.zshrc`, or Windows System Environment Variables).
@@ -104,4 +104,4 @@ Both `http://` and `https://` proxy URLs are supported. Proxy authentication is
## Feedback
-We value your input! Please email your feedback to `founders@codebuff.com`. Thank you for using Codebuff!
+We value your input! Please email your feedback to `founders@codebirds.com`. Thank you for using Codebirds!
diff --git a/cli/release/index.js b/cli/release/index.js
index 13eb3d6773..ae5ac08b5e 100644
--- a/cli/release/index.js
+++ b/cli/release/index.js
@@ -11,7 +11,7 @@ const zlib = require('zlib')
const tar = require('tar')
const { createReleaseHttpClient } = require('./http')
-const packageName = 'codebuff'
+const packageName = 'codebirds'
/**
* Terminal escape sequences to reset terminal state after the child process exits.
@@ -93,7 +93,7 @@ function createConfig(packageName) {
configDir,
binaryName,
binaryPath: path.join(configDir, binaryName),
- metadataPath: path.join(configDir, 'codebuff-metadata.json'),
+ metadataPath: path.join(configDir, 'codebirds-metadata.json'),
tempDownloadDir: path.join(configDir, '.download-temp'),
userAgent: `${packageName}-cli`,
requestTimeout: 20000,
@@ -109,10 +109,10 @@ const { getProxyUrl, httpGet } = createReleaseHttpClient({
function getPostHogConfig() {
const apiKey =
- process.env.CODEBUFF_POSTHOG_API_KEY ||
+ process.env.CODEBIRDS_POSTHOG_API_KEY ||
process.env.NEXT_PUBLIC_POSTHOG_API_KEY
const host =
- process.env.CODEBUFF_POSTHOG_HOST ||
+ process.env.CODEBIRDS_POSTHOG_HOST ||
process.env.NEXT_PUBLIC_POSTHOG_HOST_URL
if (!apiKey || !host) {
@@ -135,7 +135,7 @@ function trackUpdateFailed(errorMessage, version, context = {}) {
const payload = JSON.stringify({
api_key: posthogConfig.apiKey,
- event: 'cli.update_codebuff_failed',
+ event: 'cli.update_codebirds_failed',
properties: {
distinct_id: `anonymous-${CONFIG.homeDir}`,
error: errorMessage,
@@ -208,7 +208,7 @@ function getPlatformKey() {
function getTargetOverride() {
const envNames = [
`${packageName.toUpperCase()}_BINARY_TARGET`,
- 'CODEBUFF_BINARY_TARGET',
+ 'CODEBIRDS_BINARY_TARGET',
'CLI_BINARY_TARGET',
]
@@ -424,7 +424,7 @@ async function downloadBinary(version, targetKey = getDownloadTargetKey()) {
}
const downloadUrl = `${
- process.env.NEXT_PUBLIC_CODEBUFF_APP_URL || 'https://codebuff.com'
+ process.env.NEXT_PUBLIC_CODEBIRDS_APP_URL || 'https://codebirds.com'
}/api/releases/download/${version}/${fileName}`
// Ensure config directory exists
@@ -551,7 +551,7 @@ async function downloadBinary(version, targetKey = getDownloadTargetKey()) {
}
term.clearLine()
- console.log('Download complete! Starting Codebuff...')
+ console.log('Download complete! Starting Codebirds...')
}
async function ensureBinaryExists() {
@@ -576,7 +576,7 @@ async function ensureBinaryExists() {
await downloadBinary(version)
} catch (error) {
term.clearLine()
- console.error('❌ Failed to download codebuff:', error.message)
+ console.error('❌ Failed to download codebirds:', error.message)
console.error('Please check your internet connection and try again')
if (!getProxyUrl()) {
console.error(
@@ -676,7 +676,7 @@ function printCrashDiagnostics(code, signal) {
console.error(` Binary: ${CONFIG.binaryPath}`)
console.error('')
console.error('Please report this issue at:')
- console.error(' https://github.com/CodebuffAI/codebuff/issues')
+ console.error(' https://github.com/CodebirdsAI/codebirds/issues')
console.error('')
}
diff --git a/cli/release/package.json b/cli/release/package.json
index 93ce716b86..7d3a9c8231 100644
--- a/cli/release/package.json
+++ b/cli/release/package.json
@@ -1,15 +1,15 @@
{
- "name": "codebuff",
+ "name": "codebirds",
"version": "1.0.682",
"description": "AI coding agent",
"license": "MIT",
"bin": {
- "codebuff": "index.js",
+ "codebirds": "index.js",
"cb": "index.js"
},
"scripts": {
"postinstall": "node postinstall.js",
- "preuninstall": "node -e \"const fs = require('fs'); const path = require('path'); const os = require('os'); const binaryPath = path.join(os.homedir(), '.config', 'manicode', process.platform === 'win32' ? 'codebuff.exe' : 'codebuff'); try { fs.unlinkSync(binaryPath) } catch (e) { /* ignore if file doesn't exist */ }\""
+ "preuninstall": "node -e \"const fs = require('fs'); const path = require('path'); const os = require('os'); const binaryPath = path.join(os.homedir(), '.config', 'manicode', process.platform === 'win32' ? 'codebirds.exe' : 'codebirds'); try { fs.unlinkSync(binaryPath) } catch (e) { /* ignore if file doesn't exist */ }\""
},
"files": [
"index.js",
@@ -34,9 +34,9 @@
},
"repository": {
"type": "git",
- "url": "https://github.com/CodebuffAI/codebuff.git"
+ "url": "https://github.com/CodebirdsAI/codebirds.git"
},
- "homepage": "https://codebuff.com",
+ "homepage": "https://codebirds.com",
"publishConfig": {
"access": "public"
}
diff --git a/cli/release/postinstall.js b/cli/release/postinstall.js
index d5073ca015..a118a95993 100644
--- a/cli/release/postinstall.js
+++ b/cli/release/postinstall.js
@@ -9,7 +9,7 @@ const binaryPath = path.join(
os.homedir(),
'.config',
'manicode',
- process.platform === 'win32' ? 'codebuff.exe' : 'codebuff'
+ process.platform === 'win32' ? 'codebirds.exe' : 'codebirds'
);
try {
@@ -20,15 +20,15 @@ try {
// Print welcome message
console.log('\n');
-console.log('🎉 Welcome to Codebuff!');
+console.log('🎉 Welcome to Codebirds!');
console.log('\n');
console.log('To get started:');
console.log(' 1. cd to your project directory');
-console.log(' 2. Run: codebuff');
+console.log(' 2. Run: codebirds');
console.log('\n');
console.log('Example:');
console.log(' $ cd ~/my-project');
-console.log(' $ codebuff');
+console.log(' $ codebirds');
console.log('\n');
-console.log('For more information, visit: https://codebuff.com/docs');
+console.log('For more information, visit: https://codebirds.com/docs');
console.log('\n');
diff --git a/cli/scripts/build-binary.ts b/cli/scripts/build-binary.ts
index ce543d44e8..18cb9cbd02 100644
--- a/cli/scripts/build-binary.ts
+++ b/cli/scripts/build-binary.ts
@@ -165,13 +165,13 @@ async function main() {
const defineFlags = [
['process.env.NODE_ENV', '"production"'],
- ['process.env.CODEBUFF_IS_BINARY', '"true"'],
- ['process.env.CODEBUFF_CLI_VERSION', `"${version}"`],
+ ['process.env.CODEBIRDS_IS_BINARY', '"true"'],
+ ['process.env.CODEBIRDS_CLI_VERSION', `"${version}"`],
[
- 'process.env.CODEBUFF_CLI_TARGET',
+ 'process.env.CODEBIRDS_CLI_TARGET',
`"${getCliTargetLabel(targetInfo)}"`,
],
- ['process.env.FREEBUFF_MODE', `"${process.env.FREEBUFF_MODE ?? 'false'}"`],
+ ['process.env.CODEBIRDS_MODE', `"${process.env.CODEBIRDS_MODE ?? 'false'}"`],
...nextPublicEnvVars,
]
@@ -337,7 +337,7 @@ async function ensureOpenTuiNativeBundle(targetInfo: TargetInfo) {
}
const registryBase =
- process.env.CODEBUFF_NPM_REGISTRY ??
+ process.env.CODEBIRDS_NPM_REGISTRY ??
process.env.NPM_REGISTRY_URL ??
'https://registry.npmjs.org'
const metadataUrl = `${registryBase.replace(/\/$/, '')}/${encodeURIComponent(packageName)}`
diff --git a/cli/scripts/prebuild-agents.ts b/cli/scripts/prebuild-agents.ts
index 27ddc7eaf4..8ec8c34719 100644
--- a/cli/scripts/prebuild-agents.ts
+++ b/cli/scripts/prebuild-agents.ts
@@ -106,7 +106,7 @@ import type { LocalAgentInfo } from '../utils/local-agent-registry'
/**
* All bundled agent definitions keyed by their ID.
- * These are the default Codebuff agents that ship with the CLI binary.
+ * These are the default Codebirds agents that ship with the CLI binary.
*/
export const bundledAgents: Record = ${JSON.stringify(agents, null, 2)};
diff --git a/cli/scripts/release.ts b/cli/scripts/release.ts
index 06d57c9702..b2bd0b60e9 100755
--- a/cli/scripts/release.ts
+++ b/cli/scripts/release.ts
@@ -29,10 +29,10 @@ function formatTimestamp() {
}
function checkGitHubToken() {
- const token = process.env.CODEBUFF_GITHUB_TOKEN
+ const token = process.env.CODEBIRDS_GITHUB_TOKEN
if (!token) {
error(
- 'CODEBUFF_GITHUB_TOKEN environment variable is required but not set.\n' +
+ 'CODEBIRDS_GITHUB_TOKEN environment variable is required but not set.\n' +
'Please set it with your GitHub personal access token or use the infisical setup.'
)
}
@@ -53,7 +53,7 @@ async function triggerWorkflow(versionType: string) {
-H "Accept: application/vnd.github.v3+json" \
-H "Authorization: token ${process.env.GITHUB_TOKEN}" \
-H "Content-Type: application/json" \
- https://api.github.com/repos/CodebuffAI/freebuff-private/actions/workflows/cli-release-prod.yml/dispatches \
+ https://api.github.com/repos/CodebirdsAI/codebirds-private/actions/workflows/cli-release-prod.yml/dispatches \
-d '{"ref":"main","inputs":{"version_type":"${versionType}"}}'`
const response = execSync(triggerCmd, { encoding: 'utf8' })
@@ -63,7 +63,7 @@ async function triggerWorkflow(versionType: string) {
log(`⚠️ Workflow dispatch failed: ${response}`)
log('The workflow may need to be updated on GitHub. Continuing anyway...')
log(
- 'Please manually trigger the workflow at: https://github.com/CodebuffAI/freebuff-private/actions/workflows/cli-release-prod.yml',
+ 'Please manually trigger the workflow at: https://github.com/CodebirdsAI/codebirds-private/actions/workflows/cli-release-prod.yml',
)
} else {
// log(
@@ -74,7 +74,7 @@ async function triggerWorkflow(versionType: string) {
} catch (err: any) {
log(`⚠️ Failed to trigger workflow automatically: ${err.message}`)
log(
- 'You may need to trigger it manually at: https://github.com/CodebuffAI/freebuff-private/actions/workflows/cli-release-prod.yml',
+ 'You may need to trigger it manually at: https://github.com/CodebirdsAI/codebirds-private/actions/workflows/cli-release-prod.yml',
)
}
}
@@ -85,7 +85,7 @@ async function main() {
// Check for local GitHub token
checkGitHubToken()
- log('✅ Using local CODEBUFF_GITHUB_TOKEN')
+ log('✅ Using local CODEBIRDS_GITHUB_TOKEN')
log(`Version bump type: ${versionType}`)
@@ -93,7 +93,7 @@ async function main() {
await triggerWorkflow(versionType)
log('')
- log('Monitor progress at: https://github.com/CodebuffAI/freebuff-private/actions')
+ log('Monitor progress at: https://github.com/CodebirdsAI/codebirds-private/actions')
}
main().catch((err) => {
diff --git a/cli/scripts/smoke-binary.ts b/cli/scripts/smoke-binary.ts
index 37d4c3e66a..ab2ad302a9 100644
--- a/cli/scripts/smoke-binary.ts
+++ b/cli/scripts/smoke-binary.ts
@@ -34,12 +34,12 @@ import { existsSync } from 'fs'
// (not shimmer / animated) so they survive ANSI styling as contiguous
// substrings. Cover the multiple boot states the binary might land on:
//
-// - "will run commands on your behalf" — codebuff/freebuff main surface
+// - "will run commands on your behalf" — codebirds/codebirds main surface
// header (authed + session ready)
// - "Press ENTER to login" / "Open this URL" — login modal (no cached
// creds — typical CI smoke)
-// - "Pick a model to start" / waiting-room copy — freebuff queue gate
-// - "Free mode isn't available" — freebuff country-block screen (CI
+// - "Pick a model to start" / waiting-room copy — codebirds queue gate
+// - "Free mode isn't available" — codebirds country-block screen (CI
// runners with anonymized-network egress like GitHub Actions land here)
// - "Enter a coding task" — chat input prompt
// - OpenTUI terminal handshakes such as alternate-screen / Kitty keyboard
@@ -70,7 +70,7 @@ const BOOT_SIGNAL_PATTERNS = [
// startup" (earlyFatalHandler in cli/src/index.tsx, fires while main()
// is still wiring up) and "Unhandled rejection:" / "Uncaught exception:"
// (installProcessCleanupHandlers in cli/src/utils/renderer-cleanup.ts,
-// fires after the renderer is up). The wasm-load rejection on freebuff
+// fires after the renderer is up). The wasm-load rejection on codebirds
// 0.0.62 surfaced through the *late* renderer-cleanup path, after the
// boot screen had already rendered.
const FATAL_PATTERNS = [
@@ -84,7 +84,7 @@ const FATAL_PATTERNS = [
// Long enough that an unhandled rejection from the eager Parser.init has
// time to surface through the renderer-cleanup handler — that path is
-// what tripped freebuff 0.0.62 in the wild while a 5s window let CI pass.
+// what tripped codebirds 0.0.62 in the wild while a 5s window let CI pass.
// Async wasm rejections can fire >5s after spawn (after React mounts and
// the renderer is up).
const DEFAULT_RUN_SECONDS = 10
diff --git a/cli/src/__tests__/README.md b/cli/src/__tests__/README.md
index 19d124d74c..126c607250 100644
--- a/cli/src/__tests__/README.md
+++ b/cli/src/__tests__/README.md
@@ -1,6 +1,6 @@
# CLI Testing
-Comprehensive testing suite for the Codebuff CLI using tmux for interactive terminal emulation.
+Comprehensive testing suite for the Codebirds CLI using tmux for interactive terminal emulation.
## Test Naming Convention
diff --git a/cli/src/__tests__/cli-args.test.ts b/cli/src/__tests__/cli-args.test.ts
index 505ea7468d..16984c9677 100644
--- a/cli/src/__tests__/cli-args.test.ts
+++ b/cli/src/__tests__/cli-args.test.ts
@@ -66,10 +66,10 @@ describe('CLI Argument Parsing', () => {
test('parses --agent with full agent ID', () => {
const result = parseTestArgs([
'--agent',
- 'codebuff/base-lite@1.0.0',
+ 'codebirds/base-lite@1.0.0',
'hello',
])
- expect(result.agent).toBe('codebuff/base-lite@1.0.0')
+ expect(result.agent).toBe('codebirds/base-lite@1.0.0')
expect(result.initialPrompt).toBe('hello')
})
@@ -145,7 +145,7 @@ describe('CLI Argument Parsing', () => {
describe('Freebuff CLI Argument Parsing', () => {
test('accepts login as a command, not an unexpected argument', () => {
const result = parseArgs({
- argv: ['node', 'freebuff', 'login'],
+ argv: ['node', 'codebirds', 'login'],
isFreebuff: true,
version: '1.0.0',
})
@@ -157,7 +157,7 @@ describe('Freebuff CLI Argument Parsing', () => {
test('allows cwd before the login command', () => {
const result = parseArgs({
- argv: ['node', 'freebuff', '--cwd', '/tmp', 'login'],
+ argv: ['node', 'codebirds', '--cwd', '/tmp', 'login'],
isFreebuff: true,
version: '1.0.0',
})
@@ -169,7 +169,7 @@ describe('Freebuff CLI Argument Parsing', () => {
test('allows cwd after the login command', () => {
const result = parseArgs({
- argv: ['node', 'freebuff', 'login', '--cwd', '/tmp'],
+ argv: ['node', 'codebirds', 'login', '--cwd', '/tmp'],
isFreebuff: true,
version: '1.0.0',
})
diff --git a/cli/src/__tests__/e2e/first-time-login.test.ts b/cli/src/__tests__/e2e/first-time-login.test.ts
index e42691790a..830b72c572 100644
--- a/cli/src/__tests__/e2e/first-time-login.test.ts
+++ b/cli/src/__tests__/e2e/first-time-login.test.ts
@@ -1,6 +1,6 @@
import { describe, test, expect, mock } from 'bun:test'
-import { createMockLogger } from '@codebuff/common/testing/mock-types'
+import { createMockLogger } from '@codebirds/common/testing/mock-types'
import {
generateLoginUrl,
pollLoginStatus,
@@ -8,7 +8,7 @@ import {
} from '../../login/login-flow'
import { createMockApiClient } from '../helpers/mock-api-client'
-import type { ApiResponse } from '../../utils/codebuff-api'
+import type { ApiResponse } from '../../utils/codebirds-api'
describe('First-Time Login Flow (helpers)', () => {
test('generateLoginUrl posts fingerprint id and returns payload', async () => {
@@ -51,7 +51,7 @@ describe('First-Time Login Flow (helpers)', () => {
user: {
id: 'new-user-123',
name: 'New User',
- email: 'new@codebuff.dev',
+ email: 'new@codebirds.dev',
authToken: 'token-123',
},
},
diff --git a/cli/src/__tests__/e2e/logout-relogin-flow.test.ts b/cli/src/__tests__/e2e/logout-relogin-flow.test.ts
index feda9b7750..0036cd2acb 100644
--- a/cli/src/__tests__/e2e/logout-relogin-flow.test.ts
+++ b/cli/src/__tests__/e2e/logout-relogin-flow.test.ts
@@ -19,14 +19,14 @@ import {
logoutUser,
} from '../../utils/auth'
import * as AuthModule from '../../utils/auth'
-import * as CodebuffApiModule from '../../utils/codebuff-api'
+import * as CodebirdsApiModule from '../../utils/codebirds-api'
type User = AuthModule.User
const ORIGINAL_USER: User = {
id: 'user-001',
name: 'CLI Tester',
- email: 'tester@codebuff.dev',
+ email: 'tester@codebirds.dev',
authToken: 'token-original',
fingerprintId: 'fingerprint-original',
fingerprintHash: 'fingerprint-hash-original',
@@ -63,9 +63,9 @@ describe('Logout and Re-login helpers', () => {
}
const mockLogoutApi = () => {
- spyOn(CodebuffApiModule, 'getApiClient').mockReturnValue({
+ spyOn(CodebirdsApiModule, 'getApiClient').mockReturnValue({
logout: async () => ({ ok: true, status: 200 }),
- } as ReturnType)
+ } as ReturnType)
}
test('logoutUser removes credentials file and returns true', async () => {
diff --git a/cli/src/__tests__/e2e/returning-user-auth.test.ts b/cli/src/__tests__/e2e/returning-user-auth.test.ts
index 8eae9c5227..0fa20cbf39 100644
--- a/cli/src/__tests__/e2e/returning-user-auth.test.ts
+++ b/cli/src/__tests__/e2e/returning-user-auth.test.ts
@@ -2,7 +2,7 @@ import fs from 'fs'
import os from 'os'
import path from 'path'
-import { API_KEY_ENV_VAR } from '@codebuff/common/old-constants'
+import { API_KEY_ENV_VAR } from '@codebirds/common/old-constants'
import {
describe,
test,
@@ -17,8 +17,8 @@ import { validateApiKey } from '../../hooks/use-auth-query'
import * as AuthModule from '../../utils/auth'
import { getAuthTokenDetails, saveUserCredentials } from '../../utils/auth'
-import type { GetUserInfoFromApiKeyFn } from '@codebuff/common/types/contracts/database'
-import type { Logger } from '@codebuff/common/types/contracts/logger'
+import type { GetUserInfoFromApiKeyFn } from '@codebirds/common/types/contracts/database'
+import type { Logger } from '@codebirds/common/types/contracts/logger'
type User = AuthModule.User
@@ -70,7 +70,7 @@ describe('Returning User Authentication helpers', () => {
expect(details.token).toBe(RETURNING_USER.authToken)
})
- test('should fall back to CODEBUFF_API_KEY when credentials are missing', () => {
+ test('should fall back to CODEBIRDS_API_KEY when credentials are missing', () => {
spyOn(AuthModule, 'getConfigDir').mockReturnValue(tempConfigDir)
spyOn(AuthModule, 'getCredentialsPath').mockReturnValue(
path.join(tempConfigDir, 'credentials.json'),
diff --git a/cli/src/__tests__/helpers/mock-api-client.ts b/cli/src/__tests__/helpers/mock-api-client.ts
index fbf4423be3..c7b1252136 100644
--- a/cli/src/__tests__/helpers/mock-api-client.ts
+++ b/cli/src/__tests__/helpers/mock-api-client.ts
@@ -1,6 +1,6 @@
import { mock } from 'bun:test'
-import type { CodebuffApiClient } from '../../utils/codebuff-api'
+import type { CodebirdsApiClient } from '../../utils/codebirds-api'
export interface MockApiClientOverrides {
get?: ReturnType
@@ -29,36 +29,36 @@ const defaultOkResponse = () =>
Promise.resolve({ ok: true as const, status: 200 })
/**
- * Creates a mock CodebuffApiClient with sensible defaults.
+ * Creates a mock CodebirdsApiClient with sensible defaults.
* All methods return { ok: true, status: 200 } by default.
* Pass overrides to customize specific methods.
*/
export const createMockApiClient = (
overrides: MockApiClientOverrides = {},
-): CodebuffApiClient => ({
- get: (overrides.get ?? mock(defaultOkResponse)) as CodebuffApiClient['get'],
+): CodebirdsApiClient => ({
+ get: (overrides.get ?? mock(defaultOkResponse)) as CodebirdsApiClient['get'],
post: (overrides.post ??
- mock(defaultOkResponse)) as CodebuffApiClient['post'],
- put: (overrides.put ?? mock(defaultOkResponse)) as CodebuffApiClient['put'],
+ mock(defaultOkResponse)) as CodebirdsApiClient['post'],
+ put: (overrides.put ?? mock(defaultOkResponse)) as CodebirdsApiClient['put'],
patch: (overrides.patch ??
- mock(defaultOkResponse)) as CodebuffApiClient['patch'],
+ mock(defaultOkResponse)) as CodebirdsApiClient['patch'],
delete: (overrides.delete ??
- mock(defaultOkResponse)) as CodebuffApiClient['delete'],
+ mock(defaultOkResponse)) as CodebirdsApiClient['delete'],
request: (overrides.request ??
- mock(defaultOkResponse)) as CodebuffApiClient['request'],
- me: (overrides.me ?? mock(defaultOkResponse)) as CodebuffApiClient['me'],
+ mock(defaultOkResponse)) as CodebirdsApiClient['request'],
+ me: (overrides.me ?? mock(defaultOkResponse)) as CodebirdsApiClient['me'],
usage: (overrides.usage ??
- mock(defaultOkResponse)) as CodebuffApiClient['usage'],
+ mock(defaultOkResponse)) as CodebirdsApiClient['usage'],
loginCode: (overrides.loginCode ??
- mock(defaultOkResponse)) as CodebuffApiClient['loginCode'],
+ mock(defaultOkResponse)) as CodebirdsApiClient['loginCode'],
loginStatus: (overrides.loginStatus ??
- mock(defaultOkResponse)) as CodebuffApiClient['loginStatus'],
+ mock(defaultOkResponse)) as CodebirdsApiClient['loginStatus'],
publish: (overrides.publish ??
- mock(defaultOkResponse)) as CodebuffApiClient['publish'],
+ mock(defaultOkResponse)) as CodebirdsApiClient['publish'],
logout: (overrides.logout ??
- mock(defaultOkResponse)) as CodebuffApiClient['logout'],
+ mock(defaultOkResponse)) as CodebirdsApiClient['logout'],
feedback: (overrides.feedback ??
- mock(defaultOkResponse)) as CodebuffApiClient['feedback'],
- baseUrl: overrides.baseUrl ?? 'https://test.codebuff.com',
+ mock(defaultOkResponse)) as CodebirdsApiClient['feedback'],
+ baseUrl: overrides.baseUrl ?? 'https://test.codebirds.com',
authToken: overrides.authToken,
})
diff --git a/cli/src/__tests__/integration-tmux.test.ts b/cli/src/__tests__/integration-tmux.test.ts
index 4ee434add1..d9bfbed525 100644
--- a/cli/src/__tests__/integration-tmux.test.ts
+++ b/cli/src/__tests__/integration-tmux.test.ts
@@ -69,18 +69,18 @@ describe.skipIf(!tmuxAvailable || !sdkBuilt)(
}),
),
)
- // Clear FREEBUFF_MODE from the tmux global env. A previous freebuff
- // build or `bun run dev:freebuff` invocation in the same tmux server
+ // Clear CODEBIRDS_MODE from the tmux global env. A previous codebirds
+ // build or `bun run dev:codebirds` invocation in the same tmux server
// can leave it set globally, which would make this test see the
- // freebuff CLI variant (which has no `--agent` flag).
- await tmux(['set-environment', '-gu', 'FREEBUFF_MODE']).catch(() => {})
+ // codebirds CLI variant (which has no `--agent` flag).
+ await tmux(['set-environment', '-gu', 'CODEBIRDS_MODE']).catch(() => {})
}
})
test(
'CLI starts and displays help output',
async () => {
- const sessionName = 'codebuff-test-' + Date.now()
+ const sessionName = 'codebirds-test-' + Date.now()
try {
// Create session with --help flag and keep it alive with '; sleep 2'
@@ -126,7 +126,7 @@ describe.skipIf(!tmuxAvailable || !sdkBuilt)(
test(
'CLI accepts --agent flag',
async () => {
- const sessionName = 'codebuff-test-' + Date.now()
+ const sessionName = 'codebirds-test-' + Date.now()
try {
// Start CLI with --agent flag (it will wait for input, so we can capture)
diff --git a/cli/src/__tests__/integration/api-integration.test.ts b/cli/src/__tests__/integration/api-integration.test.ts
index 3dbf874979..1d7258af63 100644
--- a/cli/src/__tests__/integration/api-integration.test.ts
+++ b/cli/src/__tests__/integration/api-integration.test.ts
@@ -1,10 +1,10 @@
-import { getUserInfoFromApiKey } from '@codebuff/sdk'
+import { getUserInfoFromApiKey } from '@codebirds/sdk'
import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test'
-import type { Logger } from '@codebuff/common/types/contracts/logger'
+import type { Logger } from '@codebirds/common/types/contracts/logger'
/**
- * Integration tests for API communication with Codebuff backend
+ * Integration tests for API communication with Codebirds backend
*
* These tests verify that the CLI correctly communicates with backend endpoints:
* - /api/v1/me - User info retrieval with Bearer token auth
@@ -19,7 +19,7 @@ import type { Logger } from '@codebuff/common/types/contracts/logger'
*/
describe('API Integration', () => {
const originalFetch = globalThis.fetch
- const originalAppUrl = process.env.NEXT_PUBLIC_CODEBUFF_APP_URL
+ const originalAppUrl = process.env.NEXT_PUBLIC_CODEBIRDS_APP_URL
type LoggerMocks = Logger & {
info: ReturnType
@@ -48,7 +48,7 @@ describe('API Integration', () => {
const originalSetTimeout = globalThis.setTimeout
beforeEach(() => {
- process.env.NEXT_PUBLIC_CODEBUFF_APP_URL = 'https://example.codebuff.test'
+ process.env.NEXT_PUBLIC_CODEBIRDS_APP_URL = 'https://example.codebirds.test'
// Mock setTimeout to execute immediately for faster tests
// This makes the retry backoff delays instant
globalThis.setTimeout = ((
@@ -64,7 +64,7 @@ describe('API Integration', () => {
afterEach(() => {
globalThis.fetch = originalFetch
globalThis.setTimeout = originalSetTimeout
- process.env.NEXT_PUBLIC_CODEBUFF_APP_URL = originalAppUrl
+ process.env.NEXT_PUBLIC_CODEBIRDS_APP_URL = originalAppUrl
mock.restore()
})
@@ -262,7 +262,7 @@ describe('API Integration', () => {
test('should handle DNS resolution failures gracefully', async () => {
const fetchMock = setFetchMock(async () => {
- const error = new Error('getaddrinfo ENOTFOUND api.codebuff.local')
+ const error = new Error('getaddrinfo ENOTFOUND api.codebirds.local')
error.name = 'ENOTFOUND'
throw error
})
diff --git a/cli/src/__tests__/integration/credentials-storage.test.ts b/cli/src/__tests__/integration/credentials-storage.test.ts
index 2ba0c7abf2..6563ca03c5 100644
--- a/cli/src/__tests__/integration/credentials-storage.test.ts
+++ b/cli/src/__tests__/integration/credentials-storage.test.ts
@@ -5,7 +5,7 @@ import path from 'path'
import {
clearMockedModules,
mockModule,
-} from '@codebuff/common/testing/mock-modules'
+} from '@codebirds/common/testing/mock-modules'
import {
describe,
test,
@@ -162,7 +162,7 @@ describe('Credentials Storage Integration', () => {
// Restore getConfigDir to use real implementation for this test
mock.restore()
- await mockModule('@codebuff/common/env', () => ({
+ await mockModule('@codebirds/common/env', () => ({
env: { NEXT_PUBLIC_CB_ENVIRONMENT: 'test' },
}))
@@ -177,7 +177,7 @@ describe('Credentials Storage Integration', () => {
// Restore getConfigDir to use real implementation for this test
mock.restore()
- await mockModule('@codebuff/common/env', () => ({
+ await mockModule('@codebirds/common/env', () => ({
env: { NEXT_PUBLIC_CB_ENVIRONMENT: 'dev' },
}))
@@ -193,7 +193,7 @@ describe('Credentials Storage Integration', () => {
mock.restore()
// Set environment to prod (or unset it)
- await mockModule('@codebuff/common/env', () => ({
+ await mockModule('@codebirds/common/env', () => ({
env: { NEXT_PUBLIC_CB_ENVIRONMENT: 'prod' },
}))
diff --git a/cli/src/__tests__/integration/local-agents.test.ts b/cli/src/__tests__/integration/local-agents.test.ts
index b7444a87b3..30752ec954 100644
--- a/cli/src/__tests__/integration/local-agents.test.ts
+++ b/cli/src/__tests__/integration/local-agents.test.ts
@@ -2,7 +2,7 @@ import { mkdtempSync, rmSync, writeFileSync, mkdirSync, realpathSync } from 'fs'
import os from 'os'
import path from 'path'
-import { validateAgents } from '@codebuff/sdk'
+import { validateAgents } from '@codebirds/sdk'
import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test'
// Mock the logger to prevent analytics initialization errors in tests
@@ -43,7 +43,7 @@ describe('Local Agent Integration', () => {
let originalProjectRoot: string | undefined
beforeEach(() => {
- tempDir = mkdtempSync(path.join(os.tmpdir(), 'codebuff-agents-'))
+ tempDir = mkdtempSync(path.join(os.tmpdir(), 'codebirds-agents-'))
originalCwd = process.cwd()
setProjectRoot(process.cwd())
originalProjectRoot = getProjectRoot()
@@ -577,7 +577,7 @@ describe('Local Agent Integration', () => {
version: '1.2.3',
publisher: 'test-publisher',
toolNames: ['read_files', 'write_file', 'run_terminal_command'],
- spawnableAgents: ['codebuff/file-picker@0.0.1'],
+ spawnableAgents: ['codebirds/file-picker@0.0.1'],
systemPrompt: 'You are a helpful assistant.',
instructionsPrompt: 'Follow these instructions carefully.',
stepPrompt: 'Think step by step.',
@@ -614,7 +614,7 @@ describe('Local Agent Integration', () => {
expect(fullAgent!.version).toBe('1.2.3')
expect(fullAgent!.publisher).toBe('test-publisher')
expect(fullAgent!.toolNames).toContain('read_files')
- expect(fullAgent!.spawnableAgents).toContain('codebuff/file-picker@0.0.1')
+ expect(fullAgent!.spawnableAgents).toContain('codebirds/file-picker@0.0.1')
expect(fullAgent!.systemPrompt).toBe('You are a helpful assistant.')
expect(fullAgent!.instructionsPrompt).toBe(
'Follow these instructions carefully.',
diff --git a/cli/src/__tests__/integration/login-flow-instrumentation.test.ts b/cli/src/__tests__/integration/login-flow-instrumentation.test.ts
index 2fed4893aa..c4124c3338 100644
--- a/cli/src/__tests__/integration/login-flow-instrumentation.test.ts
+++ b/cli/src/__tests__/integration/login-flow-instrumentation.test.ts
@@ -1,12 +1,12 @@
import { describe, test, expect, mock } from 'bun:test'
-import { AnalyticsEvent } from '@codebuff/common/constants/analytics-events'
-import { createMockLogger } from '@codebuff/common/testing/mock-types'
+import { AnalyticsEvent } from '@codebirds/common/constants/analytics-events'
+import { createMockLogger } from '@codebirds/common/testing/mock-types'
import { generateLoginUrl, pollLoginStatus } from '../../login/login-flow'
import { createMockApiClient } from '../helpers/mock-api-client'
-import type { ApiResponse } from '../../utils/codebuff-api'
+import type { ApiResponse } from '../../utils/codebirds-api'
const createClock = () => {
let current = 0
diff --git a/cli/src/__tests__/integration/login-polling-working.test.ts b/cli/src/__tests__/integration/login-polling-working.test.ts
index ebc2587de2..d2e8ce4b36 100644
--- a/cli/src/__tests__/integration/login-polling-working.test.ts
+++ b/cli/src/__tests__/integration/login-polling-working.test.ts
@@ -1,12 +1,12 @@
import { describe, test, expect, mock } from 'bun:test'
-import { createMockLogger } from '@codebuff/common/testing/mock-types'
+import { createMockLogger } from '@codebirds/common/testing/mock-types'
import { generateLoginUrl, pollLoginStatus } from '../../login/login-flow'
import { createMockApiClient } from '../helpers/mock-api-client'
import type { LoginUrlResponse } from '../../login/login-flow'
-import type { ApiResponse } from '../../utils/codebuff-api'
-import type { Logger } from '@codebuff/common/types/contracts/logger'
+import type { ApiResponse } from '../../utils/codebirds-api'
+import type { Logger } from '@codebirds/common/types/contracts/logger'
const createClock = () => {
let current = 0
diff --git a/cli/src/__tests__/integration/usage-refresh-on-completion.test.ts b/cli/src/__tests__/integration/usage-refresh-on-completion.test.ts
index 00e4333630..128f6a851e 100644
--- a/cli/src/__tests__/integration/usage-refresh-on-completion.test.ts
+++ b/cli/src/__tests__/integration/usage-refresh-on-completion.test.ts
@@ -20,13 +20,13 @@ import * as authModule from '../../utils/auth'
*/
describe('Usage Refresh on SDK Completion', () => {
const originalFetch = globalThis.fetch
- const originalEnv = process.env.NEXT_PUBLIC_CODEBUFF_APP_URL
+ const originalEnv = process.env.NEXT_PUBLIC_CODEBIRDS_APP_URL
let queryClient: QueryClient
let getAuthTokenSpy: ReturnType
beforeEach(() => {
- process.env.NEXT_PUBLIC_CODEBUFF_APP_URL = 'https://test.codebuff.local'
+ process.env.NEXT_PUBLIC_CODEBIRDS_APP_URL = 'https://test.codebirds.local'
// Reset chat store to initial state
useChatStore.getState().reset()
@@ -61,7 +61,7 @@ describe('Usage Refresh on SDK Completion', () => {
afterEach(() => {
globalThis.fetch = originalFetch
getAuthTokenSpy.mockRestore()
- process.env.NEXT_PUBLIC_CODEBUFF_APP_URL = originalEnv
+ process.env.NEXT_PUBLIC_CODEBIRDS_APP_URL = originalEnv
mock.restore()
})
diff --git a/cli/src/__tests__/release/proxy-http-get.test.ts b/cli/src/__tests__/release/proxy-http-get.test.ts
index a0addd586a..746b4b77be 100644
--- a/cli/src/__tests__/release/proxy-http-get.test.ts
+++ b/cli/src/__tests__/release/proxy-http-get.test.ts
@@ -8,19 +8,19 @@ const require = createRequire(import.meta.url)
const helperModules = [
{
- name: 'codebuff release helper',
+ name: 'codebirds release helper',
path: fileURLToPath(new URL('../../../release/http.js', import.meta.url)),
},
{
- name: 'codebuff staging release helper',
+ name: 'codebirds staging release helper',
path: fileURLToPath(
new URL('../../../release-staging/http.js', import.meta.url),
),
},
{
- name: 'freebuff release helper',
+ name: 'codebirds release helper',
path: fileURLToPath(
- new URL('../../../../freebuff/cli/release/http.js', import.meta.url),
+ new URL('../../../../codebirds/cli/release/http.js', import.meta.url),
),
},
]
@@ -123,7 +123,7 @@ for (const helperModule of helperModules) {
})
const response = await client.httpGet(
- 'https://registry.npmjs.org/freebuff/latest',
+ 'https://registry.npmjs.org/codebirds/latest',
)
response.resume()
@@ -143,7 +143,7 @@ for (const helperModule of helperModules) {
expect(httpsGetCalls[0]?.agent).toBeDefined()
expect(httpsGetCalls[0]).toMatchObject({
hostname: 'registry.npmjs.org',
- path: '/freebuff/latest',
+ path: '/codebirds/latest',
headers: {
'User-Agent': 'release-test-agent',
},
@@ -215,14 +215,14 @@ for (const helperModule of helperModules) {
})
const response = await client.httpGet(
- 'https://registry.npmjs.org/freebuff/latest',
+ 'https://registry.npmjs.org/codebirds/latest',
)
response.resume()
expect(httpsGetCalls).toHaveLength(2)
expect(httpsGetCalls[0]).toMatchObject({
hostname: 'registry.npmjs.org',
- path: '/freebuff/latest',
+ path: '/codebirds/latest',
})
expect(httpsGetCalls[1]).toMatchObject({
hostname: 'registry.npmjs.org',
diff --git a/cli/src/__tests__/rerender-perf.integration.test.ts b/cli/src/__tests__/rerender-perf.integration.test.ts
index ddc20e3a8e..4035a90fa2 100644
--- a/cli/src/__tests__/rerender-perf.integration.test.ts
+++ b/cli/src/__tests__/rerender-perf.integration.test.ts
@@ -117,14 +117,14 @@ describe.skipIf(!tmuxAvailable || !sdkBuilt)(
),
)
// Enable performance testing
- await tmux(['set-environment', '-g', 'CODEBUFF_PERF_TEST', 'true'])
+ await tmux(['set-environment', '-g', 'CODEBIRDS_PERF_TEST', 'true'])
}
})
test(
'MessageBlock re-renders stay within acceptable limits',
async () => {
- const sessionName = 'codebuff-perf-test-' + Date.now()
+ const sessionName = 'codebirds-perf-test-' + Date.now()
// Clear the debug log before test
clearCliDebugLog(DEBUG_LOG_PATH)
@@ -140,7 +140,7 @@ describe.skipIf(!tmuxAvailable || !sdkBuilt)(
'120',
'-y',
'30',
- `CODEBUFF_PERF_TEST=true bun run ${CLI_PATH}`,
+ `CODEBIRDS_PERF_TEST=true bun run ${CLI_PATH}`,
])
// Wait for CLI to initialize
@@ -215,7 +215,7 @@ describe.skipIf(!tmuxAvailable || !sdkBuilt)(
test(
'Forbidden callback props are properly memoized',
async () => {
- const sessionName = 'codebuff-memo-test-' + Date.now()
+ const sessionName = 'codebirds-memo-test-' + Date.now()
clearCliDebugLog(DEBUG_LOG_PATH)
@@ -229,7 +229,7 @@ describe.skipIf(!tmuxAvailable || !sdkBuilt)(
'120',
'-y',
'30',
- `CODEBUFF_PERF_TEST=true bun run ${CLI_PATH}`,
+ `CODEBIRDS_PERF_TEST=true bun run ${CLI_PATH}`,
])
await sleep(5000)
diff --git a/cli/src/__tests__/test-utils.ts b/cli/src/__tests__/test-utils.ts
index be23aa1a4b..d9f085175d 100644
--- a/cli/src/__tests__/test-utils.ts
+++ b/cli/src/__tests__/test-utils.ts
@@ -21,7 +21,7 @@ export function isTmuxAvailable(): boolean {
execSync('which tmux', { stdio: 'pipe' })
// Then verify tmux can actually run by creating and killing a test session
// This will fail if tmux server can't start (e.g., no socket directory on CI)
- execSync('tmux new-session -d -s __codebuff_tmux_check__ && tmux kill-session -t __codebuff_tmux_check__', {
+ execSync('tmux new-session -d -s __codebirds_tmux_check__ && tmux kill-session -t __codebirds_tmux_check__', {
stdio: 'pipe',
timeout: 5000,
})
@@ -56,8 +56,8 @@ let cachedEnv: Record | null = null
const TEST_CLIENT_ENV_DEFAULTS: Record = {
NEXT_PUBLIC_CB_ENVIRONMENT: 'test',
- NEXT_PUBLIC_CODEBUFF_APP_URL: 'http://localhost:3000',
- NEXT_PUBLIC_SUPPORT_EMAIL: 'support@codebuff.com',
+ NEXT_PUBLIC_CODEBIRDS_APP_URL: 'http://localhost:3000',
+ NEXT_PUBLIC_SUPPORT_EMAIL: 'support@codebirds.com',
NEXT_PUBLIC_POSTHOG_API_KEY: 'test-posthog-key',
NEXT_PUBLIC_POSTHOG_HOST_URL: 'https://us.i.posthog.com',
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: 'pk_test_placeholder',
@@ -74,8 +74,8 @@ const TEST_SERVER_ENV_DEFAULTS: Record = {
GRAVITY_API_KEY: 'test',
PORT: '4242',
DATABASE_URL: 'postgres://user:pass@localhost:5432/db',
- CODEBUFF_GITHUB_ID: 'test-id',
- CODEBUFF_GITHUB_SECRET: 'test-secret',
+ CODEBIRDS_GITHUB_ID: 'test-id',
+ CODEBIRDS_GITHUB_SECRET: 'test-secret',
NEXTAUTH_SECRET: 'test-secret',
STRIPE_SECRET_KEY: 'sk_test_dummy',
STRIPE_WEBHOOK_SECRET_KEY: 'whsec_dummy',
diff --git a/cli/src/__tests__/tmux-poc.ts b/cli/src/__tests__/tmux-poc.ts
index 7ad979a191..56dfed97be 100755
--- a/cli/src/__tests__/tmux-poc.ts
+++ b/cli/src/__tests__/tmux-poc.ts
@@ -49,7 +49,7 @@ async function capturePane(sessionName: string): Promise {
// Main test function
async function testCLIWithTmux() {
- const sessionName = 'codebuff-test-' + Date.now()
+ const sessionName = 'codebirds-test-' + Date.now()
console.log('🚀 Starting tmux-based CLI test...')
console.log(`📦 Session: ${sessionName}`)
diff --git a/cli/src/__tests__/unit/copy-button.test.ts b/cli/src/__tests__/unit/copy-button.test.ts
index 11e7cb57a3..2c6715589c 100644
--- a/cli/src/__tests__/unit/copy-button.test.ts
+++ b/cli/src/__tests__/unit/copy-button.test.ts
@@ -1,4 +1,4 @@
-import { createMockTimers } from '@codebuff/common/testing/mocks/timers'
+import { createMockTimers } from '@codebirds/common/testing/mocks/timers'
import { describe, test, expect, beforeEach, afterEach } from 'bun:test'
import {
@@ -11,7 +11,7 @@ import {
} from '../../components/copy-button'
import { initializeThemeStore } from '../../hooks/use-theme'
-import type { MockTimers } from '@codebuff/common/testing/mocks/timers'
+import type { MockTimers } from '@codebirds/common/testing/mocks/timers'
// Initialize theme before tests
initializeThemeStore()
diff --git a/cli/src/__tests__/utils/env.test.ts b/cli/src/__tests__/utils/env.test.ts
index 7c6ad18964..72fa46ad6a 100644
--- a/cli/src/__tests__/utils/env.test.ts
+++ b/cli/src/__tests__/utils/env.test.ts
@@ -59,10 +59,10 @@ describe('cli/utils/env', () => {
test('returns current process.env values for editor preferences', () => {
process.env.EDITOR = 'vim'
- process.env.CODEBUFF_CLI_EDITOR = 'code'
+ process.env.CODEBIRDS_CLI_EDITOR = 'code'
const env = getCliEnv()
expect(env.EDITOR).toBe('vim')
- expect(env.CODEBUFF_CLI_EDITOR).toBe('code')
+ expect(env.CODEBIRDS_CLI_EDITOR).toBe('code')
})
test('returns current process.env values for theme preferences', () => {
@@ -72,11 +72,11 @@ describe('cli/utils/env', () => {
})
test('returns current process.env values for binary build config', () => {
- process.env.CODEBUFF_IS_BINARY = 'true'
- process.env.CODEBUFF_CLI_VERSION = '1.0.0'
+ process.env.CODEBIRDS_IS_BINARY = 'true'
+ process.env.CODEBIRDS_CLI_VERSION = '1.0.0'
const env = getCliEnv()
- expect(env.CODEBUFF_IS_BINARY).toBe('true')
- expect(env.CODEBUFF_CLI_VERSION).toBe('1.0.0')
+ expect(env.CODEBIRDS_IS_BINARY).toBe('true')
+ expect(env.CODEBIRDS_CLI_VERSION).toBe('1.0.0')
})
test('returns undefined for unset env vars', () => {
@@ -110,7 +110,7 @@ describe('cli/utils/env', () => {
expect(env.VSCODE_PID).toBeUndefined()
expect(env.CURSOR_PORT).toBeUndefined()
expect(env.IDE_CONFIG_DIR).toBeUndefined()
- expect(env.CODEBUFF_IS_BINARY).toBeUndefined()
+ expect(env.CODEBIRDS_IS_BINARY).toBeUndefined()
})
test('allows overriding terminal detection vars', () => {
@@ -139,22 +139,22 @@ describe('cli/utils/env', () => {
const env = createTestCliEnv({
EDITOR: 'vim',
VISUAL: 'code',
- CODEBUFF_CLI_EDITOR: 'cursor',
+ CODEBIRDS_CLI_EDITOR: 'cursor',
})
expect(env.EDITOR).toBe('vim')
expect(env.VISUAL).toBe('code')
- expect(env.CODEBUFF_CLI_EDITOR).toBe('cursor')
+ expect(env.CODEBIRDS_CLI_EDITOR).toBe('cursor')
})
test('allows overriding binary build config', () => {
const env = createTestCliEnv({
- CODEBUFF_IS_BINARY: 'true',
- CODEBUFF_CLI_VERSION: '2.0.0',
- CODEBUFF_CLI_TARGET: 'darwin-arm64',
+ CODEBIRDS_IS_BINARY: 'true',
+ CODEBIRDS_CLI_VERSION: '2.0.0',
+ CODEBIRDS_CLI_TARGET: 'darwin-arm64',
})
- expect(env.CODEBUFF_IS_BINARY).toBe('true')
- expect(env.CODEBUFF_CLI_VERSION).toBe('2.0.0')
- expect(env.CODEBUFF_CLI_TARGET).toBe('darwin-arm64')
+ expect(env.CODEBIRDS_IS_BINARY).toBe('true')
+ expect(env.CODEBIRDS_CLI_VERSION).toBe('2.0.0')
+ expect(env.CODEBIRDS_CLI_TARGET).toBe('darwin-arm64')
})
test('allows overriding default values', () => {
diff --git a/cli/src/app.tsx b/cli/src/app.tsx
index c163c6f8f2..369a4f2f39 100644
--- a/cli/src/app.tsx
+++ b/cli/src/app.tsx
@@ -1,17 +1,17 @@
-import { isRetryableStatusCode, getErrorStatusCode } from '@codebuff/sdk'
+import { isRetryableStatusCode, getErrorStatusCode } from '@codebirds/sdk'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useShallow } from 'zustand/react/shallow'
import { Chat } from './chat'
import { ChatHistoryScreen } from './components/chat-history-screen'
-import { FreebuffSupersededScreen } from './components/freebuff-superseded-screen'
+import { FreebuffSupersededScreen } from './components/codebirds-superseded-screen'
import { LoginModal } from './components/login-modal'
import { ProjectPickerScreen } from './components/project-picker-screen'
import { TerminalLink } from './components/terminal-link'
import { WaitingRoomScreen } from './components/waiting-room-screen'
import { useAuthQuery } from './hooks/use-auth-query'
import { useAuthState } from './hooks/use-auth-state'
-import { useFreebuffSession } from './hooks/use-freebuff-session'
+import { useFreebuffSession } from './hooks/use-codebirds-session'
import { useLogo } from './hooks/use-logo'
import { useSheenAnimation } from './hooks/use-sheen-animation'
import { useTerminalDimensions } from './hooks/use-terminal-dimensions'
@@ -21,7 +21,7 @@ import { getProjectRoot } from './project-files'
import { useChatHistoryStore } from './state/chat-history-store'
import { useChatStore } from './state/chat-store'
import type { TopBannerType } from './types/store'
-import { IS_FREEBUFF } from './utils/constants'
+import { IS_CODEBIRDS } from './utils/constants'
import { findGitRoot } from './utils/git'
import { openFileAtPath } from './utils/open-file'
import { formatCwd } from './utils/path-helpers'
@@ -30,7 +30,7 @@ import { getLogoBlockColor, getLogoAccentColor } from './utils/theme-system'
import type { MultilineInputHandle } from './components/multiline-input'
import type { AgentMode } from './utils/constants'
import type { AuthStatus } from './utils/status-indicator-state'
-import type { FileTreeNode } from '@codebuff/common/util/file'
+import type { FileTreeNode } from '@codebirds/common/util/file'
interface AppProps {
initialPrompt: string | null
@@ -226,7 +226,7 @@ export const App = ({
- {IS_FREEBUFF ? 'Freebuff' : 'Codebuff'} will run commands on your behalf to help you build.
+ {IS_CODEBIRDS ? 'Freebuff' : 'Codebirds'} will run commands on your behalf to help you build.
}
@@ -386,7 +386,7 @@ const AuthedSurface = ({
// finishing work under the server-side grace period, and the chat surface
// itself swaps the input box for the session-ended banner.
if (
- IS_FREEBUFF &&
+ IS_CODEBIRDS &&
(session === null ||
session.status === 'none' ||
session.status === 'country_blocked' ||
@@ -397,7 +397,7 @@ const AuthedSurface = ({
return
}
- // Chat history renders inside AuthedSurface so the freebuff session stays
+ // Chat history renders inside AuthedSurface so the codebirds session stays
// mounted while the user browses history. Unmounting this surface would
// DELETE the session row and drop the user back into the waiting room on
// return.
@@ -428,7 +428,7 @@ const AuthedSurface = ({
initialMode={initialMode}
gitRoot={gitRoot}
onSwitchToGitRoot={onSwitchToGitRoot}
- freebuffSession={session}
+ codebirdsSession={session}
/>
)
}
diff --git a/cli/src/chat.tsx b/cli/src/chat.tsx
index 674eb8b506..cd440e38b5 100644
--- a/cli/src/chat.tsx
+++ b/cli/src/chat.tsx
@@ -1,5 +1,5 @@
-import { AnalyticsEvent } from '@codebuff/common/constants/analytics-events'
-import type { FeedbackCategory } from '@codebuff/common/constants/feedback'
+import { AnalyticsEvent } from '@codebirds/common/constants/analytics-events'
+import type { FeedbackCategory } from '@codebirds/common/constants/feedback'
import { safeOpen } from './utils/open-url'
import {
useCallback,
@@ -15,7 +15,7 @@ import { getAdsEnabled } from './commands/ads'
import { routeUserPrompt, addBashMessageToHistory } from './commands/router'
import { SingleAdBanner } from './components/ad-banner'
import { ChatInputBar } from './components/chat-input-bar'
-import { FreebuffActiveSessionSummary } from './components/freebuff-active-session-summary'
+import { FreebuffActiveSessionSummary } from './components/codebirds-active-session-summary'
import { LoadPreviousButton } from './components/load-previous-button'
import { ReviewScreen } from './components/review-screen'
import { MessageWithAgents } from './components/message-with-agents'
@@ -62,8 +62,8 @@ import { reportActivity } from './utils/activity-tracker'
import { trackEvent } from './utils/analytics'
import { showClipboardMessage } from './utils/clipboard'
import { readClipboardImage } from './utils/clipboard-image'
-import { returnToFreebuffLanding } from './hooks/use-freebuff-session'
-import { END_SESSION_MESSAGE, IS_FREEBUFF } from './utils/constants'
+import { returnToFreebuffLanding } from './hooks/use-codebirds-session'
+import { END_SESSION_MESSAGE, IS_CODEBIRDS } from './utils/constants'
import { getSystemMessage } from './utils/message-history'
import { getInputModeConfig } from './utils/input-modes'
import {
@@ -95,10 +95,10 @@ import { computeInputLayoutMetrics } from './utils/text-layout'
import type { CommandResult } from './commands/command-registry'
import type { MultilineInputHandle } from './components/multiline-input'
import type { MatchedSlashCommand } from './hooks/use-suggestion-engine'
-import type { FreebuffSessionResponse } from './types/freebuff-session'
+import type { FreebuffSessionResponse } from './types/codebirds-session'
import type { User } from './utils/auth'
import type { AgentMode } from './utils/constants'
-import type { FileTreeNode } from '@codebuff/common/util/file'
+import type { FileTreeNode } from '@codebirds/common/util/file'
import type { ScrollBoxRenderable } from '@opentui/core'
import type { UseMutationResult } from '@tanstack/react-query'
import type { Dispatch, SetStateAction } from 'react'
@@ -118,7 +118,7 @@ export const Chat = ({
initialMode,
gitRoot,
onSwitchToGitRoot,
- freebuffSession,
+ codebirdsSession,
}: {
headerContent: React.ReactNode
initialPrompt: string | null
@@ -134,14 +134,14 @@ export const Chat = ({
initialMode?: AgentMode
gitRoot?: string | null
onSwitchToGitRoot?: () => void
- freebuffSession: FreebuffSessionResponse | null
+ codebirdsSession: FreebuffSessionResponse | null
}) => {
const [forceFileOnlyMentions, setForceFileOnlyMentions] = useState(false)
// First-time onboarding: show clickable starter prompts until the user
// submits their first prompt ever (persisted in settings). Freebuff only.
const [showSuggestedPrompts, setShowSuggestedPrompts] = useState(
- () => IS_FREEBUFF && !hasSubmittedFirstPrompt(),
+ () => IS_CODEBIRDS && !hasSubmittedFirstPrompt(),
)
const { validate: validateAgents } = useAgentValidation()
@@ -191,7 +191,7 @@ export const Chat = ({
const hasSubscription = subscriptionData?.hasSubscription ?? false
const { ads, recordClick, recordImpression } = useGravityAd({
- enabled: IS_FREEBUFF || !hasSubscription,
+ enabled: IS_CODEBIRDS || !hasSubscription,
provider: 'gravity',
})
@@ -615,10 +615,10 @@ export const Chat = ({
})
}
- globalThis.addEventListener('codebuff:send-followup', handleFollowupClick)
+ globalThis.addEventListener('codebirds:send-followup', handleFollowupClick)
return () => {
globalThis.removeEventListener(
- 'codebuff:send-followup',
+ 'codebirds:send-followup',
handleFollowupClick,
)
}
@@ -1429,9 +1429,9 @@ export const Chat = ({
}, [queuePreviewTitle, pausedQueueText])
const hasActiveFreebuffSession =
- IS_FREEBUFF && freebuffSession?.status === 'active'
+ IS_CODEBIRDS && codebirdsSession?.status === 'active'
const isFreebuffSessionOver =
- IS_FREEBUFF && freebuffSession?.status === 'ended'
+ IS_CODEBIRDS && codebirdsSession?.status === 'ended'
const shouldShowStatusLine =
!feedbackMode &&
(hasStatusIndicatorContent ||
@@ -1501,8 +1501,8 @@ export const Chat = ({
{headerContent}
- {IS_FREEBUFF && (
-
+ {IS_CODEBIRDS && (
+
)}
{hiddenMessageCount > 0 && (
{})
}}
- freebuffSession={freebuffSession}
+ codebirdsSession={codebirdsSession}
/>
)}
- {ads?.[0] && (IS_FREEBUFF || getAdsEnabled()) && (
+ {ads?.[0] && (IS_CODEBIRDS || getAdsEnabled()) && (
',
diff --git a/cli/src/commands/__tests__/copy-conversation.test.ts b/cli/src/commands/__tests__/copy-conversation.test.ts
index 8c15770291..c6efd1437b 100644
--- a/cli/src/commands/__tests__/copy-conversation.test.ts
+++ b/cli/src/commands/__tests__/copy-conversation.test.ts
@@ -220,7 +220,7 @@ describe('serializeConversation', () => {
test('empty conversation still produces a header', () => {
const { text, omittedCount } = serializeConversation([])
- expect(text).toMatch(/# (Freebuff|Codebuff) conversation/)
+ expect(text).toMatch(/# (Freebuff|Codebirds) conversation/)
expect(omittedCount).toBe(0)
})
})
diff --git a/cli/src/commands/__tests__/freebuff-command-aliases.test.ts b/cli/src/commands/__tests__/freebuff-command-aliases.test.ts
index da96c1a3c9..7cbe1e6439 100644
--- a/cli/src/commands/__tests__/freebuff-command-aliases.test.ts
+++ b/cli/src/commands/__tests__/freebuff-command-aliases.test.ts
@@ -1,7 +1,7 @@
import { describe, expect, test } from 'bun:test'
-describe('freebuff command aliases', () => {
- test('/model aliases /end-session in freebuff', () => {
+describe('codebirds command aliases', () => {
+ test('/model aliases /end-session in codebirds', () => {
const slashCommandsUrl = new URL(
'../../data/slash-commands.ts',
import.meta.url,
@@ -35,15 +35,15 @@ describe('freebuff command aliases', () => {
cwd: process.cwd(),
env: {
...process.env,
- FREEBUFF_MODE: 'true',
+ CODEBIRDS_MODE: 'true',
NODE_ENV: 'test',
NEXT_PUBLIC_CB_ENVIRONMENT: 'test',
- NEXT_PUBLIC_CODEBUFF_APP_URL: 'https://app.codebuff.test',
- NEXT_PUBLIC_SUPPORT_EMAIL: 'support@codebuff.test',
+ NEXT_PUBLIC_CODEBIRDS_APP_URL: 'https://app.codebirds.test',
+ NEXT_PUBLIC_SUPPORT_EMAIL: 'support@codebirds.test',
NEXT_PUBLIC_POSTHOG_API_KEY: 'phc_test_key',
- NEXT_PUBLIC_POSTHOG_HOST_URL: 'https://posthog.codebuff.test',
+ NEXT_PUBLIC_POSTHOG_HOST_URL: 'https://posthog.codebirds.test',
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: 'pk_test_123',
- NEXT_PUBLIC_STRIPE_CUSTOMER_PORTAL: 'https://stripe.codebuff.test',
+ NEXT_PUBLIC_STRIPE_CUSTOMER_PORTAL: 'https://stripe.codebirds.test',
NEXT_PUBLIC_WEB_PORT: '3000',
},
stderr: 'pipe',
diff --git a/cli/src/commands/__tests__/router-connect-chatgpt.test.ts b/cli/src/commands/__tests__/router-connect-chatgpt.test.ts
index 51d64ee64d..e347e1392e 100644
--- a/cli/src/commands/__tests__/router-connect-chatgpt.test.ts
+++ b/cli/src/commands/__tests__/router-connect-chatgpt.test.ts
@@ -16,7 +16,7 @@ mock.module('../../components/chatgpt-connect-banner', () => ({
handleChatGptAuthCode,
}))
-mock.module('@codebuff/common/constants/chatgpt-oauth', () => ({
+mock.module('@codebirds/common/constants/chatgpt-oauth', () => ({
CHATGPT_OAUTH_ENABLED: true,
}))
diff --git a/cli/src/commands/__tests__/router-input.test.ts b/cli/src/commands/__tests__/router-input.test.ts
index 8d870581d9..0020292b54 100644
--- a/cli/src/commands/__tests__/router-input.test.ts
+++ b/cli/src/commands/__tests__/router-input.test.ts
@@ -286,14 +286,14 @@ describe('command-registry', () => {
}
})
- test('connect command is not available in codebuff (freebuff-only)', () => {
+ test('connect command is not available in codebirds (codebirds-only)', () => {
const hasConnectSlashCommand = SLASH_COMMANDS.some(
(cmd) => cmd.id === 'connect',
)
expect(hasConnectSlashCommand).toBe(false)
})
- test('connect:chatgpt command is not available in codebuff (freebuff-only)', () => {
+ test('connect:chatgpt command is not available in codebirds (codebirds-only)', () => {
const command = findCommand('connect:chatgpt')
expect(command).toBeUndefined()
})
diff --git a/cli/src/commands/ads.ts b/cli/src/commands/ads.ts
index ec2722dfd3..b8fa1eaa18 100644
--- a/cli/src/commands/ads.ts
+++ b/cli/src/commands/ads.ts
@@ -1,5 +1,5 @@
import { useChatStore } from '../state/chat-store'
-import { IS_FREEBUFF } from '../utils/constants'
+import { IS_CODEBIRDS } from '../utils/constants'
import { logger } from '../utils/logger'
import { getSystemMessage } from '../utils/message-history'
import { saveSettings, loadSettings } from '../utils/settings'
@@ -36,9 +36,9 @@ export const handleAdsDisable = (): {
}
export const getAdsEnabled = (): boolean => {
- if (IS_FREEBUFF) return true
+ if (IS_CODEBIRDS) return true
- // Codebuff LITE is a paid mode now, so use the normal saved setting.
+ // Codebirds LITE is a paid mode now, so use the normal saved setting.
const settings = loadSettings()
return settings.adsEnabled ?? false
}
diff --git a/cli/src/commands/command-registry.ts b/cli/src/commands/command-registry.ts
index 0a0afffb0f..a68baa0973 100644
--- a/cli/src/commands/command-registry.ts
+++ b/cli/src/commands/command-registry.ts
@@ -1,4 +1,4 @@
-import { CHATGPT_OAUTH_ENABLED } from '@codebuff/common/constants/chatgpt-oauth'
+import { CHATGPT_OAUTH_ENABLED } from '@codebirds/common/constants/chatgpt-oauth'
import { safeOpen } from '../utils/open-url'
import { handleAdsEnable, handleAdsDisable } from './ads'
@@ -9,13 +9,13 @@ import { handleInitializationFlowLocally } from './init'
import { buildInterviewPrompt, buildPlanPrompt, buildReviewPromptFromArgs } from './prompt-builders'
import { runBashCommand } from './router'
import { handleUsageCommand } from './usage'
-import { returnToFreebuffLanding } from '../hooks/use-freebuff-session'
+import { returnToFreebuffLanding } from '../hooks/use-codebirds-session'
import { useThemeStore } from '../hooks/use-theme'
import { WEBSITE_URL } from '../login/constants'
import { useChatStore } from '../state/chat-store'
import { useFeedbackStore } from '../state/feedback-store'
import { useLoginStore } from '../state/login-store'
-import { AGENT_MODES, END_SESSION_MESSAGE, IS_FREEBUFF } from '../utils/constants'
+import { AGENT_MODES, END_SESSION_MESSAGE, IS_CODEBIRDS } from '../utils/constants'
import { getSystemMessage, getUserMessage } from '../utils/message-history'
import { capturePendingAttachments } from '../utils/pending-attachments'
import { getSkillByName } from '../utils/skill-registry'
@@ -164,7 +164,7 @@ const clearInput = (params: RouterParams) => {
params.setInputValue({ text: '', cursorPosition: 0, lastEditDueToNav: false })
}
-const FREEBUFF_REMOVED_COMMANDS = new Set([
+const CODEBIRDS_REMOVED_COMMANDS = new Set([
'ads:enable',
'ads:disable',
'usage',
@@ -174,7 +174,7 @@ const FREEBUFF_REMOVED_COMMANDS = new Set([
'gpt-5-agent',
])
-const FREEBUFF_ONLY_COMMANDS = new Set([
+const CODEBIRDS_ONLY_COMMANDS = new Set([
'connect',
'plan',
'end-session',
@@ -400,7 +400,7 @@ const ALL_COMMANDS: CommandDefinition[] = [
},
}),
// Mode commands generated from AGENT_MODES (excluded in Freebuff)
- ...(IS_FREEBUFF ? [] : AGENT_MODES).map((mode) =>
+ ...(IS_CODEBIRDS ? [] : AGENT_MODES).map((mode) =>
defineCommandWithArgs({
name: `mode:${mode.toLowerCase()}`,
aliases: [`model:${mode.toLowerCase()}`],
@@ -572,7 +572,7 @@ const ALL_COMMANDS: CommandDefinition[] = [
clearInput(params)
},
}),
- // /end-session (freebuff-only) — end the active session early and drop back
+ // /end-session (codebirds-only) — end the active session early and drop back
// to the model picker. The hook flips status to 'none', which unmounts
// and mounts on the landing view, where the
// user picks a model and hits Enter to rejoin the queue.
@@ -595,9 +595,9 @@ const ALL_COMMANDS: CommandDefinition[] = [
}),
]
-export const COMMAND_REGISTRY: CommandDefinition[] = IS_FREEBUFF
- ? ALL_COMMANDS.filter((cmd) => !FREEBUFF_REMOVED_COMMANDS.has(cmd.name))
- : ALL_COMMANDS.filter((cmd) => !FREEBUFF_ONLY_COMMANDS.has(cmd.name))
+export const COMMAND_REGISTRY: CommandDefinition[] = IS_CODEBIRDS
+ ? ALL_COMMANDS.filter((cmd) => !CODEBIRDS_REMOVED_COMMANDS.has(cmd.name))
+ : ALL_COMMANDS.filter((cmd) => !CODEBIRDS_ONLY_COMMANDS.has(cmd.name))
export function findCommand(cmd: string): CommandDefinition | undefined {
const lowerCmd = cmd.toLowerCase()
diff --git a/cli/src/commands/copy-conversation.ts b/cli/src/commands/copy-conversation.ts
index 2a43627c5b..42771b065b 100644
--- a/cli/src/commands/copy-conversation.ts
+++ b/cli/src/commands/copy-conversation.ts
@@ -17,7 +17,7 @@ import {
showClipboardMessage,
} from '../utils/clipboard'
import { useChatStore } from '../state/chat-store'
-import { IS_FREEBUFF } from '../utils/constants'
+import { IS_CODEBIRDS } from '../utils/constants'
import type { RouterParams } from './command-registry'
import type { ChatMessage, ContentBlock } from '../types/chat'
@@ -273,7 +273,7 @@ export function serializeConversation(
renderMessage(message, segments)
}
- const product = IS_FREEBUFF ? 'Freebuff' : 'Codebuff'
+ const product = IS_CODEBIRDS ? 'Freebuff' : 'Codebirds'
const header = `# ${product} conversation\n_${messages.length} message${messages.length === 1 ? '' : 's'}_`
const prefix = `${header}\n\n---\n\n`
diff --git a/cli/src/commands/init.ts b/cli/src/commands/init.ts
index 782a3aa0b5..0d5debc251 100644
--- a/cli/src/commands/init.ts
+++ b/cli/src/commands/init.ts
@@ -1,8 +1,8 @@
import { existsSync, mkdirSync, writeFileSync } from 'fs'
import path from 'path'
-import { AnalyticsEvent } from '@codebuff/common/constants/analytics-events'
-import { PRIMARY_KNOWLEDGE_FILE_NAME } from '@codebuff/common/constants/knowledge'
+import { AnalyticsEvent } from '@codebirds/common/constants/analytics-events'
+import { PRIMARY_KNOWLEDGE_FILE_NAME } from '@codebirds/common/constants/knowledge'
// @ts-expect-error - Bun text import attribute not supported by TypeScript
import agentDefinitionSource from '../../../common/src/templates/initial-agents-dir/types/agent-definition' with { type: 'text' }
@@ -12,12 +12,12 @@ import toolsSource from '../../../common/src/templates/initial-agents-dir/types/
import utilTypesSource from '../../../common/src/templates/initial-agents-dir/types/util-types' with { type: 'text' }
import { getProjectRoot } from '../project-files'
import { trackEvent } from '../utils/analytics'
-import { IS_FREEBUFF } from '../utils/constants'
+import { IS_CODEBIRDS } from '../utils/constants'
import { getSystemMessage } from '../utils/message-history'
import type { PostUserMessageFn } from '../types/contracts/send-message'
-const brandName = IS_FREEBUFF ? 'Freebuff' : 'Codebuff'
+const brandName = IS_CODEBIRDS ? 'Freebuff' : 'Codebirds'
const INITIAL_KNOWLEDGE_FILE = `# Project knowledge
diff --git a/cli/src/commands/publish.ts b/cli/src/commands/publish.ts
index 05fdde99c6..4beff5a97a 100644
--- a/cli/src/commands/publish.ts
+++ b/cli/src/commands/publish.ts
@@ -1,13 +1,13 @@
-import { WEBSITE_URL } from '@codebuff/sdk'
+import { WEBSITE_URL } from '@codebirds/sdk'
import { getUserCredentials } from '../utils/auth'
-import { getApiClient, setApiClientAuthToken } from '../utils/codebuff-api'
+import { getApiClient, setApiClientAuthToken } from '../utils/codebirds-api'
import { loadAgentDefinitions, getLoadedAgentsData } from '../utils/local-agent-registry'
import type {
PublishAgentsErrorResponse,
PublishAgentsResponse,
-} from '@codebuff/common/types/api/agents/publish'
+} from '@codebirds/common/types/api/agents/publish'
export interface PublishResult {
success: boolean
diff --git a/cli/src/commands/router.ts b/cli/src/commands/router.ts
index 5726a3cd6f..0def99f548 100644
--- a/cli/src/commands/router.ts
+++ b/cli/src/commands/router.ts
@@ -1,6 +1,6 @@
-import { AnalyticsEvent } from '@codebuff/common/constants/analytics-events'
-import { CHATGPT_OAUTH_ENABLED } from '@codebuff/common/constants/chatgpt-oauth'
-import { runTerminalCommand } from '@codebuff/sdk'
+import { AnalyticsEvent } from '@codebirds/common/constants/analytics-events'
+import { CHATGPT_OAUTH_ENABLED } from '@codebirds/common/constants/chatgpt-oauth'
+import { runTerminalCommand } from '@codebirds/sdk'
import {
@@ -16,14 +16,14 @@ import { handleChatGptAuthCode } from '../components/chatgpt-connect-banner'
import { buildInterviewPrompt, buildPlanPrompt, buildReviewPrompt } from './prompt-builders'
import { getProjectRoot } from '../project-files'
import { useChatStore } from '../state/chat-store'
-import { useFreebuffSessionStore } from '../state/freebuff-session-store'
+import { useFreebuffSessionStore } from '../state/codebirds-session-store'
import { trackEvent } from '../utils/analytics'
import {
buildBashHistoryMessages,
createRunTerminalToolResult,
} from '../utils/bash-messages'
import { showClipboardMessage } from '../utils/clipboard'
-import { IS_FREEBUFF } from '../utils/constants'
+import { IS_CODEBIRDS } from '../utils/constants'
import { getSystemProcessEnv } from '../utils/env'
import { getSystemMessage, getUserMessage } from '../utils/message-history'
import {
@@ -297,17 +297,17 @@ export async function routeUserPrompt(
})
// DAU signal: one un-sampled event per user-submitted prompt. The CLI's
- // distinct id resolves to the canonical codebuff user id (anonymous id is
+ // distinct id resolves to the canonical codebirds user id (anonymous id is
// aliased to the real user id on login), matching the web and chat surfaces
- // so combined DAU is a single unique-users query. Freebuff-only: codebuff
+ // so combined DAU is a single unique-users query. Freebuff-only: codebirds
// CLI usage is intentionally excluded.
- if (IS_FREEBUFF) {
- const freebuffSession = useFreebuffSessionStore.getState().session
+ if (IS_CODEBIRDS) {
+ const codebirdsSession = useFreebuffSessionStore.getState().session
trackEvent(AnalyticsEvent.MESSAGE_SENT, {
surface: 'cli',
accessTier:
- freebuffSession && 'accessTier' in freebuffSession
- ? freebuffSession.accessTier
+ codebirdsSession && 'accessTier' in codebirdsSession
+ ? codebirdsSession.accessTier
: 'unknown',
mode: agentMode,
inputMode,
diff --git a/cli/src/components/ad-banner.tsx b/cli/src/components/ad-banner.tsx
index 05d527bb86..2554ff78f8 100644
--- a/cli/src/components/ad-banner.tsx
+++ b/cli/src/components/ad-banner.tsx
@@ -162,7 +162,7 @@ export const SingleAdBanner: React.FC<{
}
/**
- * Up to four ads shown in a row. Still used by the freebuff waiting room, which
+ * Up to four ads shown in a row. Still used by the codebirds waiting room, which
* intentionally fills the space with multiple ads.
*/
export const ChoiceAdBanner: React.FC = ({
diff --git a/cli/src/components/agent-checklist.tsx b/cli/src/components/agent-checklist.tsx
index 4ecab8f270..e7eb899ea4 100644
--- a/cli/src/components/agent-checklist.tsx
+++ b/cli/src/components/agent-checklist.tsx
@@ -1,4 +1,4 @@
-import { pluralize } from '@codebuff/common/util/string'
+import { pluralize } from '@codebirds/common/util/string'
import { TextAttributes } from '@opentui/core'
import React, { useMemo, useRef, useEffect, useState } from 'react'
diff --git a/cli/src/components/agent-mode-toggle.tsx b/cli/src/components/agent-mode-toggle.tsx
index a75c4f56fd..d8b06ed6e3 100644
--- a/cli/src/components/agent-mode-toggle.tsx
+++ b/cli/src/components/agent-mode-toggle.tsx
@@ -4,7 +4,7 @@ import { Button } from './button'
import { SegmentedControl } from './segmented-control'
import { useTheme } from '../hooks/use-theme'
import { useChatStore } from '../state/chat-store'
-import { AGENT_MODES, IS_FREEBUFF } from '../utils/constants'
+import { AGENT_MODES, IS_CODEBIRDS } from '../utils/constants'
import { BORDER_CHARS } from '../utils/ui-constants'
import type { Segment } from './segmented-control'
@@ -156,7 +156,7 @@ export const AgentModeToggle = ({
onToggle: () => void
onSelectMode?: (mode: AgentMode) => void
}) => {
- if (IS_FREEBUFF) return null
+ if (IS_CODEBIRDS) return null
const theme = useTheme()
const inputFocused = useChatStore((state) => state.inputFocused)
diff --git a/cli/src/components/blocks/agent-branch-item.tsx b/cli/src/components/blocks/agent-branch-item.tsx
index 90573fe51c..46ece6ab45 100644
--- a/cli/src/components/blocks/agent-branch-item.tsx
+++ b/cli/src/components/blocks/agent-branch-item.tsx
@@ -43,7 +43,7 @@ export const AgentBranchItem = memo((props: AgentBranchItemProps) => {
} = props
useWhyDidYouUpdateById('AgentBranchItem', agentId ?? '', props, {
logLevel: 'debug',
- enabled: getCliEnv().CODEBUFF_PERF_TEST === 'true',
+ enabled: getCliEnv().CODEBIRDS_PERF_TEST === 'true',
})
const theme = useTheme()
diff --git a/cli/src/components/blocks/agent-list-branch.tsx b/cli/src/components/blocks/agent-list-branch.tsx
index e15cbccd7e..ba5dcd23c7 100644
--- a/cli/src/components/blocks/agent-list-branch.tsx
+++ b/cli/src/components/blocks/agent-list-branch.tsx
@@ -1,4 +1,4 @@
-import { pluralize } from '@codebuff/common/util/string'
+import { pluralize } from '@codebirds/common/util/string'
import { memo, useCallback } from 'react'
import { useTheme } from '../../hooks/use-theme'
diff --git a/cli/src/components/blocks/ask-user-branch.tsx b/cli/src/components/blocks/ask-user-branch.tsx
index 23d95466bc..aaf6604067 100644
--- a/cli/src/components/blocks/ask-user-branch.tsx
+++ b/cli/src/components/blocks/ask-user-branch.tsx
@@ -1,4 +1,4 @@
-import { pluralize } from '@codebuff/common/util/string'
+import { pluralize } from '@codebirds/common/util/string'
import { TextAttributes } from '@opentui/core'
import React from 'react'
diff --git a/cli/src/components/blocks/tool-branch.tsx b/cli/src/components/blocks/tool-branch.tsx
index cc1c632d44..cd8676c737 100644
--- a/cli/src/components/blocks/tool-branch.tsx
+++ b/cli/src/components/blocks/tool-branch.tsx
@@ -3,7 +3,7 @@ import { memo, useCallback } from 'react'
import { ContentWithMarkdown } from './content-with-markdown'
import { useTheme } from '../../hooks/use-theme'
import { useChatStore } from '../../state/chat-store'
-import { getToolDisplayInfo } from '../../utils/codebuff-client'
+import { getToolDisplayInfo } from '../../utils/codebirds-client'
import { shouldCollapseToolByDefault } from '../../utils/constants'
import { renderToolComponent } from '../tools/registry'
import { ToolCallItem } from '../tools/tool-call-item'
diff --git a/cli/src/components/build-mode-buttons.tsx b/cli/src/components/build-mode-buttons.tsx
index e03239c1e7..aae3c92619 100644
--- a/cli/src/components/build-mode-buttons.tsx
+++ b/cli/src/components/build-mode-buttons.tsx
@@ -1,7 +1,7 @@
import { useState } from 'react'
import { Button } from './button'
-import { IS_FREEBUFF } from '../utils/constants'
+import { IS_CODEBIRDS } from '../utils/constants'
import { useTerminalLayout } from '../hooks/use-terminal-layout'
import { BORDER_CHARS } from '../utils/ui-constants'
@@ -18,7 +18,7 @@ export const BuildModeButtons = ({
onBuildMax: () => void
onBuildLite: () => void
}) => {
- if (IS_FREEBUFF) return null
+ if (IS_CODEBIRDS) return null
const [hoveredButton, setHoveredButton] = useState<'fast' | 'max' | 'lite' | null>(
null,
diff --git a/cli/src/components/chat-input-bar.tsx b/cli/src/components/chat-input-bar.tsx
index 566c795d55..1157507daa 100644
--- a/cli/src/components/chat-input-bar.tsx
+++ b/cli/src/components/chat-input-bar.tsx
@@ -1,7 +1,7 @@
import {
isShallowScanRoot,
SHALLOW_SCAN_MAX_DEPTH,
-} from '@codebuff/common/project-file-tree'
+} from '@codebirds/common/project-file-tree'
import React from 'react'
import { AgentModeToggle } from './agent-mode-toggle'
diff --git a/cli/src/components/chatgpt-connect-banner.tsx b/cli/src/components/chatgpt-connect-banner.tsx
index c880f14987..364b4f43ac 100644
--- a/cli/src/components/chatgpt-connect-banner.tsx
+++ b/cli/src/components/chatgpt-connect-banner.tsx
@@ -199,7 +199,7 @@ export async function handleChatGptAuthCode(code: string): Promise<{
return {
success: true,
message:
- 'Successfully connected your ChatGPT subscription! Codebuff will use it for supported OpenAI streaming requests.',
+ 'Successfully connected your ChatGPT subscription! Codebirds will use it for supported OpenAI streaming requests.',
}
} catch (err) {
return {
diff --git a/cli/src/components/feedback-container.tsx b/cli/src/components/feedback-container.tsx
index 29fd47613e..d2ed97146f 100644
--- a/cli/src/components/feedback-container.tsx
+++ b/cli/src/components/feedback-container.tsx
@@ -5,7 +5,7 @@ import { FeedbackInputMode } from './feedback-input-mode'
import { useChatStore } from '../state/chat-store'
import { useFeedbackStore } from '../state/feedback-store'
import { showClipboardMessage } from '../utils/clipboard'
-import { getApiClient } from '../utils/codebuff-api'
+import { getApiClient } from '../utils/codebirds-api'
import { buildFeedbackPayload, buildMessageContext } from '../utils/feedback-helpers'
import { resolveFeedbackSubmission } from '../utils/feedback-submission'
import { logger } from '../utils/logger'
diff --git a/cli/src/components/feedback-icon-button.tsx b/cli/src/components/feedback-icon-button.tsx
index 2b9d93fb42..2a3fd31a19 100644
--- a/cli/src/components/feedback-icon-button.tsx
+++ b/cli/src/components/feedback-icon-button.tsx
@@ -1,4 +1,4 @@
-import { AnalyticsEvent } from '@codebuff/common/constants/analytics-events'
+import { AnalyticsEvent } from '@codebirds/common/constants/analytics-events'
import { TextAttributes } from '@opentui/core'
import React, { useRef } from 'react'
diff --git a/cli/src/components/feedback-input-mode.tsx b/cli/src/components/feedback-input-mode.tsx
index 0d47bdd6dc..01f38d898a 100644
--- a/cli/src/components/feedback-input-mode.tsx
+++ b/cli/src/components/feedback-input-mode.tsx
@@ -6,12 +6,12 @@ import { MultilineInput, type MultilineInputHandle } from './multiline-input'
import { Separator } from './separator'
import { useTheme } from '../hooks/use-theme'
import { useChatStore } from '../state/chat-store'
-import { IS_FREEBUFF } from '../utils/constants'
+import { IS_CODEBIRDS } from '../utils/constants'
import { createTextPasteHandler } from '../utils/strings'
import { isPlainEnterKey } from '../utils/terminal-enter-detection'
import { BORDER_CHARS } from '../utils/ui-constants'
-import type { FeedbackCategory } from '@codebuff/common/constants/feedback'
+import type { FeedbackCategory } from '@codebirds/common/constants/feedback'
type CategoryHighlightKey = 'success' | 'error' | 'warning' | 'info'
@@ -45,9 +45,9 @@ const CATEGORY_OPTIONS = [
label: 'App bug',
shortLabel: 'Bug',
highlightKey: 'warning',
- placeholder: IS_FREEBUFF
+ placeholder: IS_CODEBIRDS
? 'Report a problem with Freebuff (crashes, errors, UI issues, etc.)'
- : 'Report a problem with Codebuff (crashes, errors, UI issues, etc.)',
+ : 'Report a problem with Codebirds (crashes, errors, UI issues, etc.)',
},
{
id: 'other',
diff --git a/cli/src/components/freebuff-active-session-summary.tsx b/cli/src/components/freebuff-active-session-summary.tsx
index 712c755888..0c06040706 100644
--- a/cli/src/components/freebuff-active-session-summary.tsx
+++ b/cli/src/components/freebuff-active-session-summary.tsx
@@ -1,12 +1,12 @@
import React from 'react'
-import { useFreebuffSessionProgress } from '../hooks/use-freebuff-session-progress'
+import { useFreebuffSessionProgress } from '../hooks/use-codebirds-session-progress'
import { useNow } from '../hooks/use-now'
import { useTheme } from '../hooks/use-theme'
-import { formatFreebuffPremiumResetCountdown } from '../utils/freebuff-premium-reset'
+import { formatFreebuffPremiumResetCountdown } from '../utils/codebirds-premium-reset'
import { formatSessionUnits } from '../utils/format-session-units'
-import type { FreebuffSessionResponse } from '../types/freebuff-session'
+import type { FreebuffSessionResponse } from '../types/codebirds-session'
interface FreebuffActiveSessionSummaryProps {
session: FreebuffSessionResponse | null
diff --git a/cli/src/components/freebuff-model-selector.tsx b/cli/src/components/freebuff-model-selector.tsx
index 318020a4ac..f3bf0e2ac6 100644
--- a/cli/src/components/freebuff-model-selector.tsx
+++ b/cli/src/components/freebuff-model-selector.tsx
@@ -11,36 +11,36 @@ import React, {
import { Button } from './button'
import {
- FALLBACK_FREEBUFF_MODEL_ID,
- FREEBUFF_PREMIUM_SESSION_LIMIT,
+ FALLBACK_CODEBIRDS_MODEL_ID,
+ CODEBIRDS_PREMIUM_SESSION_LIMIT,
getFreebuffDeploymentAvailabilityLabel,
getFreebuffModelsForAccessTier,
getRecommendedFreebuffModelId,
isFreebuffGlmV52ModelId,
isFreebuffModelAvailable,
isFreebuffPremiumModelId,
-} from '@codebuff/common/constants/freebuff-models'
-import { getRateLimitsByModel } from '@codebuff/common/types/freebuff-session'
+} from '@codebirds/common/constants/codebirds-models'
+import { getRateLimitsByModel } from '@codebirds/common/types/codebirds-session'
-import { joinFreebuffQueue } from '../hooks/use-freebuff-session'
+import { joinFreebuffQueue } from '../hooks/use-codebirds-session'
import { useNow } from '../hooks/use-now'
-import { useFreebuffLandingFocusStore } from '../state/freebuff-landing-focus-store'
-import { useFreebuffModelStore } from '../state/freebuff-model-store'
-import { useFreebuffSessionStore } from '../state/freebuff-session-store'
+import { useFreebuffLandingFocusStore } from '../state/codebirds-landing-focus-store'
+import { useFreebuffModelStore } from '../state/codebirds-model-store'
+import { useFreebuffSessionStore } from '../state/codebirds-session-store'
import { useTerminalDimensions } from '../hooks/use-terminal-dimensions'
import { useTheme } from '../hooks/use-theme'
import {
- freebuffModelNavigationDirectionForKey,
+ codebirdsModelNavigationDirectionForKey,
nextFreebuffModelId,
-} from '../utils/freebuff-model-navigation'
+} from '../utils/codebirds-model-navigation'
import { formatSessionUnits } from '../utils/format-session-units'
import {
formatFreebuffPremiumResetCountdown,
getFreebuffPremiumResetAt,
-} from '../utils/freebuff-premium-reset'
+} from '../utils/codebirds-premium-reset'
import { isPlainEnterKey } from '../utils/terminal-enter-detection'
-import type { FreebuffModelOption } from '@codebuff/common/constants/freebuff-models'
+import type { FreebuffModelOption } from '@codebirds/common/constants/codebirds-models'
import type { KeyEvent, ScrollBoxRenderable } from '@opentui/core'
// The picker opens collapsed to a single recommended hero so a new user can
@@ -70,7 +70,7 @@ type Section = {
// Sentinel id for the expand/collapse toggle so it can ride the same
// keyboard-navigation list as the model rows (Tab/arrow to it, Enter to fire).
-const TOGGLE_ID = '__freebuff_toggle__'
+const TOGGLE_ID = '__codebirds_toggle__'
// Right-aligned CTA shown on the focused, joinable row so the highlighted card
// reads as a button ("you can press Enter here") instead of just a selection.
@@ -265,7 +265,7 @@ export const FreebuffModelSelector: React.FC = ({
(!renderedModelIds.includes(selectedModel) ||
!isFreebuffModelAvailable(selectedModel, new Date(now)))
) {
- setSelectedModel(renderedModelIds[0] ?? FALLBACK_FREEBUFF_MODEL_ID)
+ setSelectedModel(renderedModelIds[0] ?? FALLBACK_CODEBIRDS_MODEL_ID)
}
}, [renderedModelIds, now, selectedModel, session, setSelectedModel])
@@ -286,7 +286,7 @@ export const FreebuffModelSelector: React.FC = ({
? Object.values(rateLimitsByModel)[0]
: undefined
const premiumUsed = sharedRateLimit?.recentCount ?? 0
- const premiumExhausted = premiumUsed >= FREEBUFF_PREMIUM_SESSION_LIMIT
+ const premiumExhausted = premiumUsed >= CODEBIRDS_PREMIUM_SESSION_LIMIT
// The pool resets daily on a Pacific-day boundary regardless of usage, so the
// countdown is meaningful even at zero used — getFreebuffPremiumResetAt falls
// back to the next day boundary when the server hasn't sent a resetAt yet.
@@ -491,7 +491,7 @@ export const FreebuffModelSelector: React.FC = ({
(key: KeyEvent) => {
if (pending) return
const name = key.name ?? ''
- const direction = freebuffModelNavigationDirectionForKey(key)
+ const direction = codebirdsModelNavigationDirectionForKey(key)
// Use the shared Enter detector so the keypad Enter and the niche
// Linux terminals that send \n (linefeed) for Enter also commit; a
// raw name === 'return' check silently ignores those, which looks
@@ -693,7 +693,7 @@ export const FreebuffModelSelector: React.FC = ({
{' '}
· {formatSessionUnits(premiumUsed)} of{' '}
- {FREEBUFF_PREMIUM_SESSION_LIMIT} used
+ {CODEBIRDS_PREMIUM_SESSION_LIMIT} used
)}
{section.key === 'premium' && premiumResetCountdown && (
diff --git a/cli/src/components/freebuff-referral-banner.tsx b/cli/src/components/freebuff-referral-banner.tsx
index e91c705ff1..29658d5837 100644
--- a/cli/src/components/freebuff-referral-banner.tsx
+++ b/cli/src/components/freebuff-referral-banner.tsx
@@ -4,19 +4,19 @@ import React, { useCallback, useEffect, useState } from 'react'
import { Button } from './button'
import { useCopyToClipboard } from './copy-button'
import {
- FREEBUFF_GLM_V52_MODEL_ID,
- FREEBUFF_GLM_V52_REFERRAL_CAP,
-} from '@codebuff/common/constants/freebuff-models'
-import { getReferralInfo } from '@codebuff/common/types/freebuff-session'
-import { pluralize } from '@codebuff/common/util/string'
+ CODEBIRDS_GLM_V52_MODEL_ID,
+ CODEBIRDS_GLM_V52_REFERRAL_CAP,
+} from '@codebirds/common/constants/codebirds-models'
+import { getReferralInfo } from '@codebirds/common/types/codebirds-session'
+import { pluralize } from '@codebirds/common/util/string'
-import { joinFreebuffQueue } from '../hooks/use-freebuff-session'
+import { joinFreebuffQueue } from '../hooks/use-codebirds-session'
import { useNow } from '../hooks/use-now'
-import { useFreebuffLandingFocusStore } from '../state/freebuff-landing-focus-store'
-import { useFreebuffSessionStore } from '../state/freebuff-session-store'
+import { useFreebuffLandingFocusStore } from '../state/codebirds-landing-focus-store'
+import { useFreebuffSessionStore } from '../state/codebirds-session-store'
import { useTheme } from '../hooks/use-theme'
import { LOGIN_WEBSITE_URL } from '../login/constants'
-import { formatFreebuffPremiumResetCountdown } from '../utils/freebuff-premium-reset'
+import { formatFreebuffPremiumResetCountdown } from '../utils/codebirds-premium-reset'
import { safeOpen } from '../utils/open-url'
import { BORDER_CHARS } from '../utils/ui-constants'
@@ -35,8 +35,8 @@ function referralLink(code: string, referrerName: string | null): string {
// Navigation ids for the banner's keyboard-focusable buttons. The model
// selector owns the landing keyboard handler and reaches these via the shared
// landing-focus store (arrow down past "see all models" → these buttons).
-const COPY_FOCUS_ID = '__freebuff_referral_copy__'
-const GLM_FOCUS_ID = '__freebuff_referral_glm__'
+const COPY_FOCUS_ID = '__codebirds_referral_copy__'
+const GLM_FOCUS_ID = '__codebirds_referral_glm__'
/**
* A bordered, button-styled "copy invite link" control. Reads as clickable
@@ -128,7 +128,7 @@ export const FreebuffReferralBanner: React.FC = () => {
const useGlm = useCallback(() => {
setJoining((wasJoining) => {
if (wasJoining) return wasJoining
- joinFreebuffQueue(FREEBUFF_GLM_V52_MODEL_ID).finally(() =>
+ joinFreebuffQueue(CODEBIRDS_GLM_V52_MODEL_ID).finally(() =>
setJoining(false),
)
return true
@@ -197,7 +197,7 @@ export const FreebuffReferralBanner: React.FC = () => {
{' '}
— weekly sessions used, resets in {resetsIn}. Refer more (
- {qualifiedCount}/{FREEBUFF_GLM_V52_REFERRAL_CAP}):
+ {qualifiedCount}/{CODEBIRDS_GLM_V52_REFERRAL_CAP}):
>
) : (
@@ -290,9 +290,9 @@ export const FreebuffReferralBanner: React.FC = () => {
focused={copyFocused}
onCopy={copy}
label={
- qualifiedCount >= FREEBUFF_GLM_V52_REFERRAL_CAP
- ? `✔ Max sessions earned (${qualifiedCount}/${FREEBUFF_GLM_V52_REFERRAL_CAP})`
- : `⎘ Invite for +1/wk (${qualifiedCount}/${FREEBUFF_GLM_V52_REFERRAL_CAP})`
+ qualifiedCount >= CODEBIRDS_GLM_V52_REFERRAL_CAP
+ ? `✔ Max sessions earned (${qualifiedCount}/${CODEBIRDS_GLM_V52_REFERRAL_CAP})`
+ : `⎘ Invite for +1/wk (${qualifiedCount}/${CODEBIRDS_GLM_V52_REFERRAL_CAP})`
}
/>
diff --git a/cli/src/components/freebuff-superseded-screen.tsx b/cli/src/components/freebuff-superseded-screen.tsx
index c10c22a884..0e960c66ab 100644
--- a/cli/src/components/freebuff-superseded-screen.tsx
+++ b/cli/src/components/freebuff-superseded-screen.tsx
@@ -1,7 +1,7 @@
import { TextAttributes } from '@opentui/core'
import React from 'react'
-import { useFreebuffCtrlCExit } from '../hooks/use-freebuff-ctrl-c-exit'
+import { useFreebuffCtrlCExit } from '../hooks/use-codebirds-ctrl-c-exit'
import { useLogo } from '../hooks/use-logo'
import { useTerminalDimensions } from '../hooks/use-terminal-dimensions'
import { useTheme } from '../hooks/use-theme'
@@ -44,13 +44,13 @@ export const FreebuffSupersededScreen: React.FC = () => {
style={{ fg: theme.foreground, marginBottom: 1 }}
attributes={TextAttributes.BOLD}
>
- Another freebuff instance took over this account.
+ Another codebirds instance took over this account.
Only one CLI per account can be active at a time.
- Close the other instance, then restart freebuff here.
+ Close the other instance, then restart codebirds here.
diff --git a/cli/src/components/help-banner.tsx b/cli/src/components/help-banner.tsx
index 5d60f1aa87..76bd30c939 100644
--- a/cli/src/components/help-banner.tsx
+++ b/cli/src/components/help-banner.tsx
@@ -3,7 +3,7 @@ import React from 'react'
import { BottomBanner } from './bottom-banner'
import { useSubscriptionQuery } from '../hooks/use-subscription-query'
import { useTheme } from '../hooks/use-theme'
-import { IS_FREEBUFF } from '../utils/constants'
+import { IS_CODEBIRDS } from '../utils/constants'
import { useChatStore } from '../state/chat-store'
const HELP_TIMEOUT = 60 * 1000 // 60 seconds
@@ -78,7 +78,7 @@ export const HelpBanner = () => {
Tips
- {IS_FREEBUFF && (
+ {IS_CODEBIRDS && (
Try workflow: /interview → /plan → implement → /review
@@ -97,7 +97,7 @@ export const HelpBanner = () => {
{/* Credits Section — hidden in Freebuff */}
- {!IS_FREEBUFF && (
+ {!IS_CODEBIRDS && (
Credits
diff --git a/cli/src/components/input-mode-banner.tsx b/cli/src/components/input-mode-banner.tsx
index b37eeacb7f..0b0338f592 100644
--- a/cli/src/components/input-mode-banner.tsx
+++ b/cli/src/components/input-mode-banner.tsx
@@ -1,6 +1,6 @@
-import { CHATGPT_OAUTH_ENABLED } from '@codebuff/common/constants/chatgpt-oauth'
+import { CHATGPT_OAUTH_ENABLED } from '@codebirds/common/constants/chatgpt-oauth'
import React from 'react'
-import { IS_FREEBUFF } from '../utils/constants'
+import { IS_CODEBIRDS } from '../utils/constants'
import { ChatGptConnectBanner } from './chatgpt-connect-banner'
import { HelpBanner } from './help-banner'
@@ -24,9 +24,9 @@ const BANNER_REGISTRY: Record<
> = {
default: () => ,
image: () => ,
- ...(IS_FREEBUFF ? {} : { usage: ({ showTime }: { showTime: number }) => }),
+ ...(IS_CODEBIRDS ? {} : { usage: ({ showTime }: { showTime: number }) => }),
help: () => ,
- ...(IS_FREEBUFF ? {} : { subscriptionLimit: () => }),
+ ...(IS_CODEBIRDS ? {} : { subscriptionLimit: () => }),
...(CHATGPT_OAUTH_ENABLED
? { 'connect:chatgpt': () => }
: {}),
diff --git a/cli/src/components/login-modal.tsx b/cli/src/components/login-modal.tsx
index a03716985b..4bae1b5d54 100644
--- a/cli/src/components/login-modal.tsx
+++ b/cli/src/components/login-modal.tsx
@@ -18,7 +18,7 @@ import { useSheenAnimation } from '../hooks/use-sheen-animation'
import { useTheme } from '../hooks/use-theme'
import { formatUrl, calculateResponsiveLayout } from '../login/utils'
import { useLoginStore } from '../state/login-store'
-import { IS_FREEBUFF } from '../utils/constants'
+import { IS_CODEBIRDS } from '../utils/constants'
import { copyTextToClipboard, isRemoteSession } from '../utils/clipboard'
import { getFingerprintId } from '../utils/fingerprint'
import { logger } from '../utils/logger'
@@ -290,7 +290,7 @@ export const LoginModal = ({
>
{/* Header - Logo rendered by useLogo hook */}
Tip: Can't copy? Exit and run{' '}
- {IS_FREEBUFF ? 'freebuff' : 'codebuff'} login
+ {IS_CODEBIRDS ? 'codebirds' : 'codebirds'} login
{' '}instead.
diff --git a/cli/src/components/message-block.tsx b/cli/src/components/message-block.tsx
index adbd6fd488..734b2f16f5 100644
--- a/cli/src/components/message-block.tsx
+++ b/cli/src/components/message-block.tsx
@@ -16,7 +16,7 @@ import { getCliEnv } from '../utils/env'
import { type MarkdownPalette } from '../utils/markdown-renderer'
import { formatCwd } from '../utils/path-helpers'
-import type { FeedbackCategory } from '@codebuff/common/constants/feedback'
+import type { FeedbackCategory } from '@codebirds/common/constants/feedback'
import type {
ContentBlock,
@@ -180,7 +180,7 @@ export const MessageBlock = memo(({
},
{
logLevel: 'debug',
- enabled: getCliEnv().CODEBUFF_PERF_TEST === 'true',
+ enabled: getCliEnv().CODEBIRDS_PERF_TEST === 'true',
},
)
diff --git a/cli/src/components/message-footer.tsx b/cli/src/components/message-footer.tsx
index 34289a2666..f2e41ee9b9 100644
--- a/cli/src/components/message-footer.tsx
+++ b/cli/src/components/message-footer.tsx
@@ -1,6 +1,6 @@
-import { SUBSCRIPTION_DISPLAY_NAME } from '@codebuff/common/constants/subscription-plans'
-import { IS_FREEBUFF } from '../utils/constants'
-import { pluralize } from '@codebuff/common/util/string'
+import { SUBSCRIPTION_DISPLAY_NAME } from '@codebirds/common/constants/subscription-plans'
+import { IS_CODEBIRDS } from '../utils/constants'
+import { pluralize } from '@codebirds/common/util/string'
import { TextAttributes } from '@opentui/core'
import React, { useCallback, useMemo } from 'react'
@@ -161,7 +161,7 @@ export const MessageFooter: React.FC = ({
),
})
}
- if (typeof credits === 'number' && credits > 0 && !IS_FREEBUFF) {
+ if (typeof credits === 'number' && credits > 0 && !IS_CODEBIRDS) {
footerItems.push({
key: 'credits',
node: ,
diff --git a/cli/src/components/message-with-agents.tsx b/cli/src/components/message-with-agents.tsx
index ee97d60bb9..e2c30bad79 100644
--- a/cli/src/components/message-with-agents.tsx
+++ b/cli/src/components/message-with-agents.tsx
@@ -23,7 +23,7 @@ import {
} from '../utils/markdown-renderer'
import type { ChatMessage } from '../types/chat'
-import type { FeedbackCategory } from '@codebuff/common/constants/feedback'
+import type { FeedbackCategory } from '@codebirds/common/constants/feedback'
interface AgentChildrenGridProps {
agentChildren: ChatMessage[]
diff --git a/cli/src/components/mode-divider.tsx b/cli/src/components/mode-divider.tsx
index 40b9fb3845..f3d17af614 100644
--- a/cli/src/components/mode-divider.tsx
+++ b/cli/src/components/mode-divider.tsx
@@ -3,7 +3,7 @@ import React from 'react'
import stringWidth from 'string-width'
import { useTheme } from '../hooks/use-theme'
-import { IS_FREEBUFF } from '../utils/constants'
+import { IS_CODEBIRDS } from '../utils/constants'
interface ModeDividerProps {
mode: string
@@ -11,7 +11,7 @@ interface ModeDividerProps {
}
export const ModeDivider = ({ mode, width }: ModeDividerProps) => {
- if (IS_FREEBUFF) return null
+ if (IS_CODEBIRDS) return null
const theme = useTheme()
diff --git a/cli/src/components/out-of-credits-banner.tsx b/cli/src/components/out-of-credits-banner.tsx
index 3d68f9f408..50261fcbaf 100644
--- a/cli/src/components/out-of-credits-banner.tsx
+++ b/cli/src/components/out-of-credits-banner.tsx
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react'
-import { IS_FREEBUFF } from '../utils/constants'
+import { IS_CODEBIRDS } from '../utils/constants'
import { ShimmerText } from './shimmer-text'
import { getActivityQueryData } from '../hooks/use-activity-query'
@@ -16,7 +16,7 @@ let creditsRestoredGlobal = false
export const areCreditsRestored = () => creditsRestoredGlobal
export const OutOfCreditsBanner = () => {
- if (IS_FREEBUFF) return null
+ if (IS_CODEBIRDS) return null
const sessionCreditsUsed = useChatStore((state) => state.sessionCreditsUsed)
const [creditsRestored, setCreditsRestored] = useState(false)
diff --git a/cli/src/components/publish-container.tsx b/cli/src/components/publish-container.tsx
index 73c2af5290..fc72ab6c36 100644
--- a/cli/src/components/publish-container.tsx
+++ b/cli/src/components/publish-container.tsx
@@ -1,4 +1,4 @@
-import { pluralize } from '@codebuff/common/util/string'
+import { pluralize } from '@codebirds/common/util/string'
import { TextAttributes } from '@opentui/core'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { useShallow } from 'zustand/react/shallow'
@@ -288,7 +288,7 @@ export const PublishContainer: React.FC = ({
Create agents in the .agents/ directory to publish them.
- See: https://codebuff.com/docs/agents for guidance.
+ See: https://codebirds.com/docs/agents for guidance.
void
onEndSession?: () => void
- freebuffSession: FreebuffSessionResponse | null
+ codebirdsSession: FreebuffSessionResponse | null
}
export const StatusBar = ({
@@ -67,7 +67,7 @@ export const StatusBar = ({
statusIndicatorState,
onStop,
onEndSession,
- freebuffSession,
+ codebirdsSession,
}: StatusBarProps) => {
const theme = useTheme()
const [elapsedSeconds, setElapsedSeconds] = useState(0)
@@ -106,9 +106,9 @@ export const StatusBar = ({
return () => clearInterval(interval)
}, [timerStartTime, shouldShowTimer, statusIndicatorState?.kind])
- const sessionProgress = useFreebuffSessionProgress(freebuffSession)
+ const sessionProgress = useFreebuffSessionProgress(codebirdsSession)
const isUnlimited =
- freebuffSession?.status === 'active' && !freebuffSession.rateLimit
+ codebirdsSession?.status === 'active' && !codebirdsSession.rateLimit
const renderStatusIndicator = () => {
switch (statusIndicatorState.kind) {
@@ -158,10 +158,10 @@ export const StatusBar = ({
case 'idle':
if (sessionProgress !== null) {
const isUrgent =
- sessionProgress.remainingMs < FREEBUFF_COUNTDOWN_VISIBLE_MS
+ sessionProgress.remainingMs < CODEBIRDS_COUNTDOWN_VISIBLE_MS
const modelName =
- freebuffSession?.status === 'active'
- ? getFreebuffModel(freebuffSession.model).displayName
+ codebirdsSession?.status === 'active'
+ ? getFreebuffModel(codebirdsSession.model).displayName
: null
return (
✕ End session
)}
{sessionProgress !== null &&
- sessionProgress.remainingMs < FREEBUFF_COUNTDOWN_VISIBLE_MS &&
+ sessionProgress.remainingMs < CODEBIRDS_COUNTDOWN_VISIBLE_MS &&
statusIndicatorState.kind !== 'idle' &&
!isUnlimited && (
diff --git a/cli/src/components/subscription-limit-banner.tsx b/cli/src/components/subscription-limit-banner.tsx
index bc193090ae..47f07c9baa 100644
--- a/cli/src/components/subscription-limit-banner.tsx
+++ b/cli/src/components/subscription-limit-banner.tsx
@@ -1,5 +1,5 @@
-import { SUBSCRIPTION_TIERS } from '@codebuff/common/constants/subscription-plans'
-import { IS_FREEBUFF } from '../utils/constants'
+import { SUBSCRIPTION_TIERS } from '@codebirds/common/constants/subscription-plans'
+import { IS_CODEBIRDS } from '../utils/constants'
import { safeOpen } from '../utils/open-url'
import React from 'react'
@@ -15,7 +15,7 @@ import { formatResetTime } from '../utils/time-format'
import { BORDER_CHARS } from '../utils/ui-constants'
export const SubscriptionLimitBanner = () => {
- if (IS_FREEBUFF) return null
+ if (IS_CODEBIRDS) return null
const setInputMode = useChatStore((state) => state.setInputMode)
const theme = useTheme()
diff --git a/cli/src/components/tools/registry.ts b/cli/src/components/tools/registry.ts
index 62b879ae8b..552720094b 100644
--- a/cli/src/components/tools/registry.ts
+++ b/cli/src/components/tools/registry.ts
@@ -30,7 +30,7 @@ import type {
ToolBlock,
} from './types'
import type { ChatTheme } from '../../types/theme-system'
-import type { ToolName } from '@codebuff/sdk'
+import type { ToolName } from '@codebirds/sdk'
/**
* Registry of all tool-specific UI components.
diff --git a/cli/src/components/tools/render-ui.tsx b/cli/src/components/tools/render-ui.tsx
index 5049889433..25fad02a28 100644
--- a/cli/src/components/tools/render-ui.tsx
+++ b/cli/src/components/tools/render-ui.tsx
@@ -7,7 +7,7 @@ import { safeOpen } from '../../utils/open-url'
import { Button } from '../button'
import type { ToolRenderConfig } from './types'
-import type { RenderUIButtonWidget } from '@codebuff/common/tools/params/tool/render-ui'
+import type { RenderUIButtonWidget } from '@codebirds/common/tools/params/tool/render-ui'
type RenderUIButtonVariant = NonNullable
diff --git a/cli/src/components/tools/suggest-followups.tsx b/cli/src/components/tools/suggest-followups.tsx
index 88fc060775..fa0f33a978 100644
--- a/cli/src/components/tools/suggest-followups.tsx
+++ b/cli/src/components/tools/suggest-followups.tsx
@@ -5,8 +5,8 @@ import { defineToolComponent } from './types'
import { useTerminalDimensions } from '../../hooks/use-terminal-dimensions'
import { useTheme } from '../../hooks/use-theme'
import { getLatestFollowupToolCallId, useChatStore } from '../../state/chat-store'
-import { useFreebuffSessionStore } from '../../state/freebuff-session-store'
-import { IS_FREEBUFF } from '../../utils/constants'
+import { useFreebuffSessionStore } from '../../state/codebirds-session-store'
+import { IS_CODEBIRDS } from '../../utils/constants'
import { Button } from '../button'
import type { ToolRenderConfig } from './types'
@@ -226,7 +226,7 @@ const SuggestFollowupsItem = ({
const theme = useTheme()
const inputFocused = useChatStore((state) => state.inputFocused)
const isFreebuffSessionOver = useFreebuffSessionStore(
- (state) => IS_FREEBUFF && state.session?.status === 'ended',
+ (state) => IS_CODEBIRDS && state.session?.status === 'ended',
)
const setSuggestedFollowups = useChatStore(
(state) => state.setSuggestedFollowups,
@@ -350,7 +350,7 @@ export const SuggestFollowupsComponent = defineToolComponent({
const handleSendFollowup = (prompt: string, index: number) => {
// This gets called from the FollowupCard component
// The actual logic is handled via the global followup handler
- const event = new CustomEvent('codebuff:send-followup', {
+ const event = new CustomEvent('codebirds:send-followup', {
detail: { prompt, index, toolCallId },
})
globalThis.dispatchEvent(event)
diff --git a/cli/src/components/tools/types.ts b/cli/src/components/tools/types.ts
index a3e920b06b..1bbfc65c55 100644
--- a/cli/src/components/tools/types.ts
+++ b/cli/src/components/tools/types.ts
@@ -1,6 +1,6 @@
import type { ContentBlock } from '../../types/chat'
import type { ChatTheme } from '../../types/theme-system'
-import type { ToolName } from '@codebuff/sdk'
+import type { ToolName } from '@codebirds/sdk'
import type { ReactNode } from 'react'
export type ToolBlock = Extract
diff --git a/cli/src/components/top-banner.tsx b/cli/src/components/top-banner.tsx
index b33201d549..76ad39b1a0 100644
--- a/cli/src/components/top-banner.tsx
+++ b/cli/src/components/top-banner.tsx
@@ -4,7 +4,7 @@ import { Button } from './button'
import { TerminalLink } from './terminal-link'
import { useTheme } from '../hooks/use-theme'
import { useChatStore } from '../state/chat-store'
-import { IS_FREEBUFF } from '../utils/constants'
+import { IS_CODEBIRDS } from '../utils/constants'
import type { TopBannerType } from '../types/store'
import { formatCwd } from '../utils/path-helpers'
import { BORDER_CHARS } from '../utils/ui-constants'
@@ -56,7 +56,7 @@ const TOP_BANNER_REGISTRY: Record, BannerConfig> = {
return (
<>
- You started {IS_FREEBUFF ? 'Freebuff' : 'Codebuff'} in a subdirectory of a git repo.
+ You started {IS_CODEBIRDS ? 'Freebuff' : 'Codebirds'} in a subdirectory of a git repo.
{gitRoot && onSwitchToGitRoot ? (
{
}
export const UsageBanner = ({ showTime }: { showTime: number }) => {
- if (IS_FREEBUFF) return null
+ if (IS_CODEBIRDS) return null
const sessionCreditsUsed = useChatStore((state) => state.sessionCreditsUsed)
const setInputMode = useChatStore((state) => state.setInputMode)
@@ -122,7 +122,7 @@ export const UsageBanner = ({ showTime }: { showTime: number }) => {
/>
)}
- {/* Codebuff credits section - structured layout */}
+ {/* Codebirds credits section - structured layout */}
{
safeOpen(WEBSITE_URL + '/usage')
diff --git a/cli/src/components/validation-error-popover.tsx b/cli/src/components/validation-error-popover.tsx
index 49ecb5756b..8e0896b6a7 100644
--- a/cli/src/components/validation-error-popover.tsx
+++ b/cli/src/components/validation-error-popover.tsx
@@ -1,4 +1,4 @@
-import { pluralize } from '@codebuff/common/util/string'
+import { pluralize } from '@codebirds/common/util/string'
import React, { useState } from 'react'
import { Button } from './button'
@@ -10,7 +10,7 @@ import { formatValidationError } from '../utils/validation-error-formatting'
import { NETWORK_ERROR_ID } from '../utils/validation-error-helpers'
import type { LocalAgentInfo } from '../utils/local-agent-registry'
-import type { FeedbackCategory } from '@codebuff/common/constants/feedback'
+import type { FeedbackCategory } from '@codebirds/common/constants/feedback'
interface ValidationErrorPopoverProps {
diff --git a/cli/src/components/waiting-room-screen.tsx b/cli/src/components/waiting-room-screen.tsx
index 6d1728a98f..8e72cfe230 100644
--- a/cli/src/components/waiting-room-screen.tsx
+++ b/cli/src/components/waiting-room-screen.tsx
@@ -4,48 +4,48 @@ import React, { useCallback, useEffect, useState } from 'react'
import { Button } from './button'
import { ChoiceAdBanner, AD_CARD_HEIGHT } from './ad-banner'
-import { FreebuffModelSelector } from './freebuff-model-selector'
-import { FreebuffReferralBanner } from './freebuff-referral-banner'
+import { FreebuffModelSelector } from './codebirds-model-selector'
+import { FreebuffReferralBanner } from './codebirds-referral-banner'
import { ShimmerText } from './shimmer-text'
import {
refreshFreebuffLandingMetadata,
takeOverFreebuffSession,
-} from '../hooks/use-freebuff-session'
-import { useFreebuffCtrlCExit } from '../hooks/use-freebuff-ctrl-c-exit'
-import { useFreebuffStreakQuery } from '../hooks/use-freebuff-streak-query'
+} from '../hooks/use-codebirds-session'
+import { useFreebuffCtrlCExit } from '../hooks/use-codebirds-ctrl-c-exit'
+import { useFreebuffStreakQuery } from '../hooks/use-codebirds-streak-query'
import { useGravityAd } from '../hooks/use-gravity-ad'
import { useLogo } from '../hooks/use-logo'
import { useNow } from '../hooks/use-now'
import { useSheenAnimation } from '../hooks/use-sheen-animation'
import { useTerminalDimensions } from '../hooks/use-terminal-dimensions'
import { useTheme } from '../hooks/use-theme'
-import { exitFreebuffCleanly } from '../utils/freebuff-exit'
+import { exitFreebuffCleanly } from '../utils/codebirds-exit'
import {
formatFreebuffPremiumResetCountdown,
getFreebuffPremiumResetAt,
-} from '../utils/freebuff-premium-reset'
+} from '../utils/codebirds-premium-reset'
import {
- FREEBUFF_STREAK_WEEK,
+ CODEBIRDS_STREAK_WEEK,
getFreebuffStreakBonusNote,
getFreebuffStreakLine,
-} from '../utils/freebuff-streak-line'
+} from '../utils/codebirds-streak-line'
import { formatSessionUnits } from '../utils/format-session-units'
import { isPlainEnterKey } from '../utils/terminal-enter-detection'
import { getLogoAccentColor, getLogoBlockColor } from '../utils/theme-system'
import { INVERTED_CTA_FG } from '../utils/ui-constants'
import {
- FREEBUFF_ENABLE_STREAK_IN_UI,
- FREEBUFF_LIMITED_SESSION_LIMIT,
- FREEBUFF_PREMIUM_SESSION_LIMIT,
-} from '@codebuff/common/constants/freebuff-models'
+ CODEBIRDS_ENABLE_STREAK_IN_UI,
+ CODEBIRDS_LIMITED_SESSION_LIMIT,
+ CODEBIRDS_PREMIUM_SESSION_LIMIT,
+} from '@codebirds/common/constants/codebirds-models'
import {
getRateLimitsByModel,
getReferralInfo,
-} from '@codebuff/common/types/freebuff-session'
-import { formatFreebuffHardBlockedPrivacySignals } from '@codebuff/common/util/freebuff-privacy'
+} from '@codebirds/common/types/codebirds-session'
+import { formatFreebuffHardBlockedPrivacySignals } from '@codebirds/common/util/codebirds-privacy'
-import type { FreebuffSessionResponse } from '../types/freebuff-session'
-import type { FreebuffIpPrivacySignal } from '@codebuff/common/types/freebuff-session'
+import type { FreebuffSessionResponse } from '../types/codebirds-session'
+import type { FreebuffIpPrivacySignal } from '@codebirds/common/types/codebirds-session'
import type { KeyEvent } from '@opentui/core'
interface WaitingRoomScreenProps {
@@ -226,7 +226,7 @@ const TakeoverPrompt: React.FC = () => {
- Only one freebuff instance is allowed at a time.
+ Only one codebirds instance is allowed at a time.
@@ -394,14 +394,14 @@ export const WaitingRoomScreen: React.FC = ({
// joinFreebuffQueue, which POSTs and transitions straight to 'active' (chat).
const isLanding = session?.status === 'none'
const streakQuery = useFreebuffStreakQuery({
- enabled: FREEBUFF_ENABLE_STREAK_IN_UI && isLanding,
+ enabled: CODEBIRDS_ENABLE_STREAK_IN_UI && isLanding,
})
const streak = streakQuery.data?.streak ?? 0
// Reserve the streak row whenever the feature could appear so the picker
// doesn't jump when the query resolves or the user crosses from 0 → 1.
// The component itself renders blank space when streak === 0.
const reserveStreakSlot =
- FREEBUFF_ENABLE_STREAK_IN_UI && isLanding && !compact
+ CODEBIRDS_ENABLE_STREAK_IN_UI && isLanding && !compact
// Once a full week is earned, explain the recurring perk under the picker so
// the streak reads as worth keeping. Accuracy lives in getFreebuffStreakBonusNote
// (recurring "each week" framing, GLM only for full access).
@@ -444,13 +444,13 @@ export const WaitingRoomScreen: React.FC = ({
const isSessionExhausted =
sharedSessionUsed >=
(accessTier === 'limited'
- ? FREEBUFF_LIMITED_SESSION_LIMIT
- : FREEBUFF_PREMIUM_SESSION_LIMIT)
+ ? CODEBIRDS_LIMITED_SESSION_LIMIT
+ : CODEBIRDS_PREMIUM_SESSION_LIMIT)
const sessionUsedColor = isSessionExhausted ? theme.secondary : theme.muted
const sessionLimit =
accessTier === 'limited'
- ? FREEBUFF_LIMITED_SESSION_LIMIT
- : FREEBUFF_PREMIUM_SESSION_LIMIT
+ ? CODEBIRDS_LIMITED_SESSION_LIMIT
+ : CODEBIRDS_PREMIUM_SESSION_LIMIT
const sessionLabel = accessTier === 'limited' ? 'sessions' : 'premium sessions'
const formattedSharedSessionUsed = formatSessionUnits(sharedSessionUsed)
const sessionResetAt = getFreebuffPremiumResetAt({
@@ -736,13 +736,13 @@ export const WaitingRoomScreen: React.FC = ({
<>
We couldn't verify an eligible location for this request.
VPN, Tor, proxy, or unknown-location traffic can't use
- freebuff. Press Ctrl+C to exit.
+ codebirds. Press Ctrl+C to exit.
>
) : (
<>
We detected your location as{' '}
{session.countryCode} ,
- which is outside the countries where freebuff is currently
+ which is outside the countries where codebirds is currently
offered. Press Ctrl+C to exit.
>
)}
@@ -758,8 +758,8 @@ export const WaitingRoomScreen: React.FC = ({
⚠ Account unavailable
- This account has been suspended and can't use freebuff. If you
- think this is a mistake, contact support@codebuff.com. Press
+ This account has been suspended and can't use codebirds. If you
+ think this is a mistake, contact support@codebirds.com. Press
Ctrl+C to exit.
>
diff --git a/cli/src/data/slash-commands.ts b/cli/src/data/slash-commands.ts
index 316b569d8a..6850bad978 100644
--- a/cli/src/data/slash-commands.ts
+++ b/cli/src/data/slash-commands.ts
@@ -1,7 +1,7 @@
-import { CHATGPT_OAUTH_ENABLED } from '@codebuff/common/constants/chatgpt-oauth'
-import { AGENT_MODES, IS_FREEBUFF } from '../utils/constants'
+import { CHATGPT_OAUTH_ENABLED } from '@codebirds/common/constants/chatgpt-oauth'
+import { AGENT_MODES, IS_CODEBIRDS } from '../utils/constants'
-import type { SkillsMap } from '@codebuff/common/types/skill'
+import type { SkillsMap } from '@codebirds/common/types/skill'
export interface SlashCommand {
@@ -22,7 +22,7 @@ export interface SlashCommand {
}
// Generate mode commands from the AGENT_MODES constant (excluded in Freebuff)
-const MODE_COMMANDS: SlashCommand[] = IS_FREEBUFF
+const MODE_COMMANDS: SlashCommand[] = IS_CODEBIRDS
? []
: AGENT_MODES.map((mode) => ({
id: `mode:${mode.toLowerCase()}`,
@@ -31,7 +31,7 @@ const MODE_COMMANDS: SlashCommand[] = IS_FREEBUFF
aliases: [`model:${mode.toLowerCase()}`],
}))
-const FREEBUFF_REMOVED_COMMAND_IDS = new Set([
+const CODEBIRDS_REMOVED_COMMAND_IDS = new Set([
'ads:enable',
'ads:disable',
'usage',
@@ -42,7 +42,7 @@ const FREEBUFF_REMOVED_COMMAND_IDS = new Set([
'init',
])
-const FREEBUFF_ONLY_COMMAND_IDS = new Set([
+const CODEBIRDS_ONLY_COMMAND_IDS = new Set([
'connect',
'plan',
'end-session',
@@ -154,7 +154,7 @@ const ALL_SLASH_COMMANDS: SlashCommand[] = [
{
id: 'feedback',
label: 'feedback',
- description: IS_FREEBUFF ? 'Share general feedback about Freebuff' : 'Share general feedback about Codebuff',
+ description: IS_CODEBIRDS ? 'Share general feedback about Freebuff' : 'Share general feedback about Codebirds',
},
{
id: 'bash',
@@ -201,12 +201,12 @@ const ALL_SLASH_COMMANDS: SlashCommand[] = [
},
]
-export const SLASH_COMMANDS = IS_FREEBUFF
+export const SLASH_COMMANDS = IS_CODEBIRDS
? ALL_SLASH_COMMANDS.filter(
- (cmd) => !FREEBUFF_REMOVED_COMMAND_IDS.has(cmd.id),
+ (cmd) => !CODEBIRDS_REMOVED_COMMAND_IDS.has(cmd.id),
)
: ALL_SLASH_COMMANDS.filter(
- (cmd) => !FREEBUFF_ONLY_COMMAND_IDS.has(cmd.id),
+ (cmd) => !CODEBIRDS_ONLY_COMMAND_IDS.has(cmd.id),
)
export const SLASHLESS_COMMAND_IDS = new Set(
diff --git a/cli/src/hooks/__tests__/use-ask-user-bridge.test.ts b/cli/src/hooks/__tests__/use-ask-user-bridge.test.ts
index 5852f89670..7c63d293ef 100644
--- a/cli/src/hooks/__tests__/use-ask-user-bridge.test.ts
+++ b/cli/src/hooks/__tests__/use-ask-user-bridge.test.ts
@@ -1,4 +1,4 @@
-import { AskUserBridge } from '@codebuff/common/utils/ask-user-bridge'
+import { AskUserBridge } from '@codebirds/common/utils/ask-user-bridge'
import { describe, test, expect, beforeEach, afterEach, spyOn } from 'bun:test'
diff --git a/cli/src/hooks/__tests__/use-usage-query.test.ts b/cli/src/hooks/__tests__/use-usage-query.test.ts
index dffe8403a5..bd9b586498 100644
--- a/cli/src/hooks/__tests__/use-usage-query.test.ts
+++ b/cli/src/hooks/__tests__/use-usage-query.test.ts
@@ -1,4 +1,4 @@
-import { createMockLogger } from '@codebuff/common/testing/mocks/logger'
+import { createMockLogger } from '@codebirds/common/testing/mocks/logger'
import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test'
import {
@@ -13,7 +13,7 @@ import {
usageQueryKeys,
} from '../use-usage-query'
-import type { ClientEnv } from '@codebuff/common/types/contracts/env'
+import type { ClientEnv } from '@codebirds/common/types/contracts/env'
beforeEach(() => {
@@ -22,15 +22,15 @@ beforeEach(() => {
describe('fetchUsageData', () => {
const originalFetch = globalThis.fetch
- const originalEnv = process.env.NEXT_PUBLIC_CODEBUFF_APP_URL
+ const originalEnv = process.env.NEXT_PUBLIC_CODEBIRDS_APP_URL
beforeEach(() => {
- process.env.NEXT_PUBLIC_CODEBUFF_APP_URL = 'https://test.codebuff.local'
+ process.env.NEXT_PUBLIC_CODEBIRDS_APP_URL = 'https://test.codebirds.local'
})
afterEach(() => {
globalThis.fetch = originalFetch
- process.env.NEXT_PUBLIC_CODEBUFF_APP_URL = originalEnv
+ process.env.NEXT_PUBLIC_CODEBIRDS_APP_URL = originalEnv
mock.restore()
})
@@ -173,10 +173,10 @@ describe('fetchUsageData', () => {
fetchUsageData({
authToken: 'test-token',
clientEnv: {
- NEXT_PUBLIC_CODEBUFF_APP_URL: undefined,
+ NEXT_PUBLIC_CODEBIRDS_APP_URL: undefined,
} as unknown as ClientEnv,
}),
- ).rejects.toThrow('NEXT_PUBLIC_CODEBUFF_APP_URL is not set')
+ ).rejects.toThrow('NEXT_PUBLIC_CODEBIRDS_APP_URL is not set')
})
test('should send correct request body', async () => {
@@ -222,11 +222,11 @@ describe('fetchUsageData', () => {
await fetchUsageData({
authToken: 'test-token',
clientEnv: {
- NEXT_PUBLIC_CODEBUFF_APP_URL: 'https://test.codebuff.local',
+ NEXT_PUBLIC_CODEBIRDS_APP_URL: 'https://test.codebirds.local',
} as ClientEnv,
})
- expect(capturedUrl).toBe('https://test.codebuff.local/api/v1/usage')
+ expect(capturedUrl).toBe('https://test.codebirds.local/api/v1/usage')
})
test('should log error on failed request', async () => {
diff --git a/cli/src/hooks/__tests__/use-user-details-query.test.ts b/cli/src/hooks/__tests__/use-user-details-query.test.ts
index 1dcdaae4e5..5e9a58eb4f 100644
--- a/cli/src/hooks/__tests__/use-user-details-query.test.ts
+++ b/cli/src/hooks/__tests__/use-user-details-query.test.ts
@@ -9,10 +9,10 @@ import {
} from 'bun:test'
import { createMockApiClient } from '../../__tests__/helpers/mock-api-client'
-import * as CodebuffApiModule from '../../utils/codebuff-api'
+import * as CodebirdsApiModule from '../../utils/codebirds-api'
import { fetchUserDetails } from '../use-user-details-query'
-import type { Logger } from '@codebuff/common/types/contracts/logger'
+import type { Logger } from '@codebirds/common/types/contracts/logger'
describe('fetchUserDetails', () => {
const mockLogger: Logger = {
@@ -22,14 +22,14 @@ describe('fetchUserDetails', () => {
debug: mock(() => {}),
}
- const originalEnv = process.env.NEXT_PUBLIC_CODEBUFF_APP_URL
+ const originalEnv = process.env.NEXT_PUBLIC_CODEBIRDS_APP_URL
beforeEach(() => {
- process.env.NEXT_PUBLIC_CODEBUFF_APP_URL = 'https://test.codebuff.com'
+ process.env.NEXT_PUBLIC_CODEBIRDS_APP_URL = 'https://test.codebirds.com'
})
afterEach(() => {
- process.env.NEXT_PUBLIC_CODEBUFF_APP_URL = originalEnv
+ process.env.NEXT_PUBLIC_CODEBIRDS_APP_URL = originalEnv
})
describe('API failure handling', () => {
@@ -176,10 +176,10 @@ describe('fetchUserDetails', () => {
const apiClient = createMockApiClient({ me: meMock })
const setTokenSpy = spyOn(
- CodebuffApiModule,
+ CodebirdsApiModule,
'setApiClientAuthToken',
)
- spyOn(CodebuffApiModule, 'getApiClient').mockReturnValue(apiClient as ReturnType)
+ spyOn(CodebirdsApiModule, 'getApiClient').mockReturnValue(apiClient as ReturnType)
await expect(
fetchUserDetails({
diff --git a/cli/src/hooks/helpers/__tests__/send-message.test.ts b/cli/src/hooks/helpers/__tests__/send-message.test.ts
index e40659d825..115ba7d972 100644
--- a/cli/src/hooks/helpers/__tests__/send-message.test.ts
+++ b/cli/src/hooks/helpers/__tests__/send-message.test.ts
@@ -8,19 +8,19 @@ import type { StreamStatus } from '../../use-message-queue'
const ensureEnv = () => {
process.env.NEXT_PUBLIC_CB_ENVIRONMENT =
process.env.NEXT_PUBLIC_CB_ENVIRONMENT || 'test'
- process.env.NEXT_PUBLIC_CODEBUFF_APP_URL =
- process.env.NEXT_PUBLIC_CODEBUFF_APP_URL || 'https://app.codebuff.test'
+ process.env.NEXT_PUBLIC_CODEBIRDS_APP_URL =
+ process.env.NEXT_PUBLIC_CODEBIRDS_APP_URL || 'https://app.codebirds.test'
process.env.NEXT_PUBLIC_SUPPORT_EMAIL =
- process.env.NEXT_PUBLIC_SUPPORT_EMAIL || 'support@codebuff.test'
+ process.env.NEXT_PUBLIC_SUPPORT_EMAIL || 'support@codebirds.test'
process.env.NEXT_PUBLIC_POSTHOG_API_KEY =
process.env.NEXT_PUBLIC_POSTHOG_API_KEY || 'phc_test_key'
process.env.NEXT_PUBLIC_POSTHOG_HOST_URL =
- process.env.NEXT_PUBLIC_POSTHOG_HOST_URL || 'https://posthog.codebuff.test'
+ process.env.NEXT_PUBLIC_POSTHOG_HOST_URL || 'https://posthog.codebirds.test'
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY =
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY || 'pk_test_123'
process.env.NEXT_PUBLIC_STRIPE_CUSTOMER_PORTAL =
process.env.NEXT_PUBLIC_STRIPE_CUSTOMER_PORTAL ||
- 'https://stripe.codebuff.test'
+ 'https://stripe.codebirds.test'
process.env.NEXT_PUBLIC_WEB_PORT = process.env.NEXT_PUBLIC_WEB_PORT || '3000'
}
@@ -37,8 +37,8 @@ const {
} = await import('../send-message')
const { createBatchedMessageUpdater } =
await import('../../../utils/message-updater')
-import { createPaymentRequiredError } from '@codebuff/sdk'
-import type { RunState } from '@codebuff/sdk'
+import { createPaymentRequiredError } from '@codebirds/sdk'
+import type { RunState } from '@codebirds/sdk'
const createMockTimerController = (): SendMessageTimerController & {
startCalls: string[]
@@ -1686,7 +1686,7 @@ describe('resetEarlyReturnState', () => {
})
})
-describe('freebuff gate errors', () => {
+describe('codebirds gate errors', () => {
const makeUpdater = (messages: ChatMessage[]) => {
const updater = createBatchedMessageUpdater('ai-1', (fn: any) => {
const next = fn(messages)
@@ -1725,7 +1725,7 @@ describe('freebuff gate errors', () => {
updateChainInProgress: () => {},
})
updater.flush()
- expect(messages[0].userError).toContain('Another freebuff CLI took over')
+ expect(messages[0].userError).toContain('Another codebirds CLI took over')
})
test('handleRunError suppresses the inline error for 410 session_expired (ended banner takes over)', () => {
diff --git a/cli/src/hooks/helpers/send-message.ts b/cli/src/hooks/helpers/send-message.ts
index e8ceb9421a..4c934c4288 100644
--- a/cli/src/hooks/helpers/send-message.ts
+++ b/cli/src/hooks/helpers/send-message.ts
@@ -1,14 +1,14 @@
-import { getErrorObject } from '@codebuff/common/util/error'
+import { getErrorObject } from '@codebirds/common/util/error'
import {
markFreebuffSessionCountryBlocked,
markFreebuffSessionEnded,
markFreebuffSessionSuperseded,
refreshFreebuffSession,
-} from '../use-freebuff-session'
+} from '../use-codebirds-session'
import { getProjectRoot } from '../../project-files'
import { useChatStore } from '../../state/chat-store'
-import { IS_FREEBUFF } from '../../utils/constants'
+import { IS_CODEBIRDS } from '../../utils/constants'
import { processBashContext } from '../../utils/bash-context-processor'
import { markRunningAgentsAsCancelled } from '../../utils/block-operations'
import {
@@ -45,7 +45,7 @@ import type { AgentMode } from '../../utils/constants'
import type { SendMessageTimerController } from '../../utils/send-message-timer'
import type { StreamController } from '../stream-state'
import type { StreamStatus } from '../use-message-queue'
-import type { MessageContent, RunState } from '@codebuff/sdk'
+import type { MessageContent, RunState } from '@codebirds/sdk'
import type { MutableRefObject, SetStateAction } from 'react'
/** Resets queue state on early return (before streaming starts). */
@@ -400,7 +400,7 @@ export const handleRunCompletion = (params: {
if (isFreeModeUnavailableError(output)) {
updater.setError(getFreeModeUnavailableErrorMessage(output))
- if (IS_FREEBUFF) {
+ if (IS_CODEBIRDS) {
markFreebuffSessionCountryBlocked(
getCountryBlockFromFreeModeError(output) ?? {
countryCode: 'UNKNOWN',
@@ -418,11 +418,11 @@ export const handleRunCompletion = (params: {
return
}
- const freebuffRateLimitMessage = IS_FREEBUFF
+ const codebirdsRateLimitMessage = IS_CODEBIRDS
? getFreebuffRateLimitErrorMessage(output)
: null
- if (freebuffRateLimitMessage) {
- updater.setError(freebuffRateLimitMessage)
+ if (codebirdsRateLimitMessage) {
+ updater.setError(codebirdsRateLimitMessage)
finalizeAfterError()
return
}
@@ -511,7 +511,7 @@ export const handleRunError = (params: {
if (isFreeModeUnavailableError(error)) {
updater.setError(getFreeModeUnavailableErrorMessage(error))
- if (IS_FREEBUFF) {
+ if (IS_CODEBIRDS) {
markFreebuffSessionCountryBlocked(
getCountryBlockFromFreeModeError(error) ?? {
countryCode: 'UNKNOWN',
@@ -527,11 +527,11 @@ export const handleRunError = (params: {
return
}
- const freebuffRateLimitMessage = IS_FREEBUFF
+ const codebirdsRateLimitMessage = IS_CODEBIRDS
? getFreebuffRateLimitErrorMessage(error)
: null
- if (freebuffRateLimitMessage) {
- updater.setError(freebuffRateLimitMessage)
+ if (codebirdsRateLimitMessage) {
+ updater.setError(codebirdsRateLimitMessage)
return
}
@@ -575,7 +575,7 @@ function handleFreebuffGateError(
return
case 'session_superseded':
updater.setError(
- 'Another freebuff CLI took over this account. Close the other instance, then restart.',
+ 'Another codebirds CLI took over this account. Close the other instance, then restart.',
)
// Terminal state: stop polling and flip UI to a "please restart" screen
// so we don't silently fight the other instance for the seat.
diff --git a/cli/src/hooks/use-agent-validation.ts b/cli/src/hooks/use-agent-validation.ts
index a39f35999c..7c99c387a5 100644
--- a/cli/src/hooks/use-agent-validation.ts
+++ b/cli/src/hooks/use-agent-validation.ts
@@ -1,4 +1,4 @@
-import { validateAgents } from '@codebuff/sdk'
+import { validateAgents } from '@codebirds/sdk'
import { useCallback, useState } from 'react'
import { loadAgentDefinitions } from '../utils/local-agent-registry'
diff --git a/cli/src/hooks/use-ask-user-bridge.ts b/cli/src/hooks/use-ask-user-bridge.ts
index 15ddac2eee..baa32e91d7 100644
--- a/cli/src/hooks/use-ask-user-bridge.ts
+++ b/cli/src/hooks/use-ask-user-bridge.ts
@@ -1,4 +1,4 @@
-import { AskUserBridge } from '@codebuff/common/utils/ask-user-bridge'
+import { AskUserBridge } from '@codebirds/common/utils/ask-user-bridge'
import { useEffect } from 'react'
import { useChatStore } from '../state/chat-store'
diff --git a/cli/src/hooks/use-auth-query.ts b/cli/src/hooks/use-auth-query.ts
index d5b47c64dc..1e92eb21c4 100644
--- a/cli/src/hooks/use-auth-query.ts
+++ b/cli/src/hooks/use-auth-query.ts
@@ -1,6 +1,6 @@
import { createHash } from 'crypto'
-import { getCiEnv } from '@codebuff/common/env-ci'
+import { getCiEnv } from '@codebirds/common/env-ci'
import {
getUserInfoFromApiKey as defaultGetUserInfoFromApiKey,
isRetryableStatusCode,
@@ -9,7 +9,7 @@ import {
createServerError,
MAX_RETRIES_PER_MESSAGE,
RETRY_BACKOFF_BASE_DELAY_MS,
-} from '@codebuff/sdk'
+} from '@codebirds/sdk'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import {
@@ -18,11 +18,11 @@ import {
logoutUser as logoutUserUtil,
type User,
} from '../utils/auth'
-import { resetCodebuffClient } from '../utils/codebuff-client'
+import { resetCodebirdsClient } from '../utils/codebirds-client'
import { logger as defaultLogger, loggerContext } from '../utils/logger'
-import type { GetUserInfoFromApiKeyFn } from '@codebuff/common/types/contracts/database'
-import type { Logger } from '@codebuff/common/types/contracts/logger'
+import type { GetUserInfoFromApiKeyFn } from '@codebirds/common/types/contracts/database'
+import type { Logger } from '@codebirds/common/types/contracts/logger'
const getApiKeyHash = (apiKey: string): string => {
return createHash('sha256').update(apiKey).digest('hex')
@@ -133,7 +133,7 @@ export function useAuthQuery(deps: UseAuthQueryDeps = {}) {
} = deps
const userCredentials = getUserCredentials()
- const apiKey = userCredentials?.authToken || getCiEnv().CODEBUFF_API_KEY || ''
+ const apiKey = userCredentials?.authToken || getCiEnv().CODEBIRDS_API_KEY || ''
return useQuery({
queryKey: authQueryKeys.validation(apiKey),
@@ -234,7 +234,7 @@ export function useLogoutMutation(deps: UseLogoutMutationDeps = {}) {
mutationFn: logoutUser,
onSuccess: () => {
// Reset the SDK client after logout
- resetCodebuffClient()
+ resetCodebirdsClient()
// Clear all auth-related cache
queryClient.removeQueries({ queryKey: authQueryKeys.all })
// Clear logger context
diff --git a/cli/src/hooks/use-auth-state.ts b/cli/src/hooks/use-auth-state.ts
index 80819b587e..270ce84dca 100644
--- a/cli/src/hooks/use-auth-state.ts
+++ b/cli/src/hooks/use-auth-state.ts
@@ -1,12 +1,12 @@
-import { AnalyticsEvent } from '@codebuff/common/constants/analytics-events'
+import { AnalyticsEvent } from '@codebirds/common/constants/analytics-events'
import { useCallback, useEffect, useState } from 'react'
import { useAuthQuery, useLogoutMutation } from './use-auth-query'
import { useLoginStore } from '../state/login-store'
import { identifyUser, trackEvent } from '../utils/analytics'
import { getUserCredentials } from '../utils/auth'
-import { resetCodebuffClient } from '../utils/codebuff-client'
-import { IS_FREEBUFF } from '../utils/constants'
+import { resetCodebirdsClient } from '../utils/codebirds-client'
+import { IS_CODEBIRDS } from '../utils/constants'
import { loggerContext } from '../utils/logger'
import type { MultilineInputHandle } from '../components/multiline-input'
@@ -15,7 +15,7 @@ import type { User } from '../utils/auth'
const setAuthLoggerContext = (params: { userId: string; email: string }) => {
loggerContext.userId = params.userId
loggerContext.userEmail = params.email
- identifyUser(params.userId, { email: params.email, freebuff: IS_FREEBUFF })
+ identifyUser(params.userId, { email: params.email, codebirds: IS_CODEBIRDS })
}
const clearAuthLoggerContext = () => {
@@ -112,7 +112,7 @@ export const useAuthState = ({
})
// Reset the SDK client to pick up new credentials
- resetCodebuffClient()
+ resetCodebirdsClient()
resetChatStore()
resetLoginState()
setInputFocused(true)
diff --git a/cli/src/hooks/use-chat-input.ts b/cli/src/hooks/use-chat-input.ts
index ba4234eb90..b10b086366 100644
--- a/cli/src/hooks/use-chat-input.ts
+++ b/cli/src/hooks/use-chat-input.ts
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef } from 'react'
import stringWidth from 'string-width'
import { useChatStore } from '../state/chat-store'
-import { IS_FREEBUFF } from '../utils/constants'
+import { IS_CODEBIRDS } from '../utils/constants'
import type { InputValue } from '../types/store'
import type { AgentMode } from '../utils/constants'
@@ -36,7 +36,7 @@ export const useChatInput = ({
// Estimate the collapsed toggle width as rendered by AgentModeToggle.
// In Freebuff, the toggle is always hidden, so never reserve width for it.
// In non-Freebuff: hide in bash mode, compact height, or narrow width.
- const estimatedToggleWidth = IS_FREEBUFF || inputMode !== 'default' || isCompactHeight || isNarrowWidth
+ const estimatedToggleWidth = IS_CODEBIRDS || inputMode !== 'default' || isCompactHeight || isNarrowWidth
? 0
: stringWidth(`< ${agentMode}`) + 6 // 2 padding + 2 borders + 2 gap
diff --git a/cli/src/hooks/use-chat-streaming.ts b/cli/src/hooks/use-chat-streaming.ts
index b2d2fd5240..d63d204c6d 100644
--- a/cli/src/hooks/use-chat-streaming.ts
+++ b/cli/src/hooks/use-chat-streaming.ts
@@ -2,7 +2,7 @@
* Chat streaming hook - connection status, timer, queue management, and exit handling.
*/
-import { RECONNECTION_MESSAGE_DURATION_MS } from '@codebuff/sdk'
+import { RECONNECTION_MESSAGE_DURATION_MS } from '@codebirds/sdk'
import { useQueryClient } from '@tanstack/react-query'
import { useCallback, useEffect, useState, useTransition } from 'react'
diff --git a/cli/src/hooks/use-connection-status.ts b/cli/src/hooks/use-connection-status.ts
index 41ad093867..70617f0c9d 100644
--- a/cli/src/hooks/use-connection-status.ts
+++ b/cli/src/hooks/use-connection-status.ts
@@ -1,6 +1,6 @@
import { useEffect, useRef, useState } from 'react'
-import { getCodebuffClient } from '../utils/codebuff-client'
+import { getCodebirdsClient } from '../utils/codebirds-client'
import { logger } from '../utils/logger'
// Adaptive health check interval configuration
@@ -34,7 +34,7 @@ export function getNextInterval(consecutiveSuccesses: number): number {
}
/**
- * Hook to monitor connection status to the Codebuff backend.
+ * Hook to monitor connection status to the Codebirds backend.
* Uses adaptive exponential backoff to reduce polling frequency when connection is stable.
*
* When the connection transitions from disconnected to connected, the optional
@@ -60,7 +60,7 @@ export const useConnectionStatus = (
}
const checkConnection = async () => {
- const client = await getCodebuffClient()
+ const client = await getCodebirdsClient()
if (!client) {
if (isMounted) {
setIsConnected(false)
diff --git a/cli/src/hooks/use-exit-handler.ts b/cli/src/hooks/use-exit-handler.ts
index e0ab54ff0a..6766aa0ccd 100644
--- a/cli/src/hooks/use-exit-handler.ts
+++ b/cli/src/hooks/use-exit-handler.ts
@@ -2,8 +2,8 @@ import { useCallback, useEffect, useRef, useState } from 'react'
import { getCurrentChatId } from '../project-files'
import { flushAnalytics } from '../utils/analytics'
-import { IS_FREEBUFF } from '../utils/constants'
-import { exitFreebuffCleanly } from '../utils/freebuff-exit'
+import { IS_CODEBIRDS } from '../utils/constants'
+import { exitFreebuffCleanly } from '../utils/codebirds-exit'
import { withTimeout } from '../utils/terminal-color-detection'
import type { InputValue } from '../types/store'
@@ -28,7 +28,7 @@ function setupExitMessageHandler() {
if (chatId) {
// This runs synchronously during the exit phase
// OpenTUI has already cleaned up by this point
- const cliName = IS_FREEBUFF ? 'freebuff' : 'codebuff'
+ const cliName = IS_CODEBIRDS ? 'codebirds' : 'codebirds'
process.stdout.write(
`\nTo continue this session later, run:\n${cliName} --continue ${chatId}\n`,
)
@@ -40,7 +40,7 @@ function setupExitMessageHandler() {
}
function exitCli(): void {
- if (IS_FREEBUFF) {
+ if (IS_CODEBIRDS) {
void exitFreebuffCleanly()
return
}
diff --git a/cli/src/hooks/use-freebuff-ctrl-c-exit.ts b/cli/src/hooks/use-freebuff-ctrl-c-exit.ts
index 84dcb00bad..2d6c77fbde 100644
--- a/cli/src/hooks/use-freebuff-ctrl-c-exit.ts
+++ b/cli/src/hooks/use-freebuff-ctrl-c-exit.ts
@@ -1,12 +1,12 @@
import { useKeyboard } from '@opentui/react'
import { useCallback } from 'react'
-import { exitFreebuffCleanly } from '../utils/freebuff-exit'
+import { exitFreebuffCleanly } from '../utils/codebirds-exit'
import type { KeyEvent } from '@opentui/core'
/**
- * Bind Ctrl+C on a full-screen freebuff view to `exitFreebuffCleanly`. Stdin
+ * Bind Ctrl+C on a full-screen codebirds view to `exitFreebuffCleanly`. Stdin
* is in raw mode, so SIGINT never fires — the key arrives as a normal OpenTUI
* key event and we route it through the shared cleanup path (flush analytics,
* release the session seat, then process.exit).
diff --git a/cli/src/hooks/use-freebuff-session-progress.ts b/cli/src/hooks/use-freebuff-session-progress.ts
index 05932cb4a6..eaf9b83490 100644
--- a/cli/src/hooks/use-freebuff-session-progress.ts
+++ b/cli/src/hooks/use-freebuff-session-progress.ts
@@ -1,7 +1,7 @@
import { useNow } from './use-now'
-import { IS_FREEBUFF } from '../utils/constants'
+import { IS_CODEBIRDS } from '../utils/constants'
-import type { FreebuffSessionResponse } from '../types/freebuff-session'
+import type { FreebuffSessionResponse } from '../types/codebirds-session'
export interface FreebuffSessionProgress {
/** 0..1, fraction of the session remaining. 1 at admission, 0 at expiry. */
@@ -10,8 +10,8 @@ export interface FreebuffSessionProgress {
}
/**
- * Computes a live progress value for the active freebuff session, ticking at
- * 1Hz. Returns null outside of active state or in non-freebuff builds, so
+ * Computes a live progress value for the active codebirds session, ticking at
+ * 1Hz. Returns null outside of active state or in non-codebirds builds, so
* callers can short-circuit their rendering.
*/
export function useFreebuffSessionProgress(
@@ -24,7 +24,7 @@ export function useFreebuffSessionProgress(
const nowMs = useNow(1000, expiresAtMs !== null)
- if (!IS_FREEBUFF || !expiresAtMs || !admittedAtMs) return null
+ if (!IS_CODEBIRDS || !expiresAtMs || !admittedAtMs) return null
const totalMs = expiresAtMs - admittedAtMs
if (totalMs <= 0) return null
diff --git a/cli/src/hooks/use-freebuff-session.ts b/cli/src/hooks/use-freebuff-session.ts
index b025289ca8..d48ddee3d6 100644
--- a/cli/src/hooks/use-freebuff-session.ts
+++ b/cli/src/hooks/use-freebuff-session.ts
@@ -1,49 +1,49 @@
-import { env } from '@codebuff/common/env'
+import { env } from '@codebirds/common/env'
import {
- FALLBACK_FREEBUFF_MODEL_ID,
- LIMITED_FREEBUFF_MODEL_ID,
+ FALLBACK_CODEBIRDS_MODEL_ID,
+ LIMITED_CODEBIRDS_MODEL_ID,
resolveFreebuffModelForAccessTier,
-} from '@codebuff/common/constants/freebuff-models'
+} from '@codebirds/common/constants/codebirds-models'
import {
getRateLimitsByModel,
getReferralInfo,
-} from '@codebuff/common/types/freebuff-session'
+} from '@codebirds/common/types/codebirds-session'
import { useEffect } from 'react'
import {
getSelectedFreebuffModel,
useFreebuffModelStore,
-} from '../state/freebuff-model-store'
-import { useFreebuffSessionStore } from '../state/freebuff-session-store'
+} from '../state/codebirds-model-store'
+import { useFreebuffSessionStore } from '../state/codebirds-session-store'
import { getAuthTokenDetails } from '../utils/auth'
-import { IS_FREEBUFF } from '../utils/constants'
+import { IS_CODEBIRDS } from '../utils/constants'
import {
isFreebuffInstanceOwnedByDeadLocalProcess,
recordFreebuffInstanceOwner,
-} from '../utils/freebuff-instance-owner'
+} from '../utils/codebirds-instance-owner'
import { logger } from '../utils/logger'
import {
getCachedReferral,
rememberReferral,
-} from '../utils/freebuff-referral-cache'
+} from '../utils/codebirds-referral-cache'
import { saveFreebuffModelPreference } from '../utils/settings'
-import type { FreebuffSessionResponse } from '../types/freebuff-session'
+import type { FreebuffSessionResponse } from '../types/codebirds-session'
import type {
FreebuffCountryBlockReason,
FreebuffIpPrivacySignal,
FreebuffSessionServerResponse,
-} from '@codebuff/common/types/freebuff-session'
+} from '@codebirds/common/types/codebirds-session'
const POLL_INTERVAL_ACTIVE_MS = 30_000
const POLL_INTERVAL_ERROR_MS = 10_000
/** Header sent on GET so the server can detect when another CLI on the same
* account has rotated the id and respond with `{ status: 'superseded' }`. */
-const FREEBUFF_INSTANCE_HEADER = 'x-freebuff-instance-id'
+const CODEBIRDS_INSTANCE_HEADER = 'x-codebirds-instance-id'
/** Header sent on POST telling the server which model to use. */
-const FREEBUFF_MODEL_HEADER = 'x-freebuff-model'
+const CODEBIRDS_MODEL_HEADER = 'x-codebirds-model'
/** Play the terminal bell so users get an audible notification on admission. */
const playAdmissionSound = () => {
@@ -56,9 +56,9 @@ const playAdmissionSound = () => {
const sessionEndpoint = (): string => {
const base = (
- env.NEXT_PUBLIC_CODEBUFF_APP_URL || 'https://codebuff.com'
+ env.NEXT_PUBLIC_CODEBIRDS_APP_URL || 'https://codebirds.com'
).replace(/\/$/, '')
- return `${base}/api/v1/freebuff/session`
+ return `${base}/api/v1/codebirds/session`
}
async function callSession(
@@ -68,10 +68,10 @@ async function callSession(
): Promise {
const headers: Record = { Authorization: `Bearer ${token}` }
if (method === 'GET' && opts.instanceId) {
- headers[FREEBUFF_INSTANCE_HEADER] = opts.instanceId
+ headers[CODEBIRDS_INSTANCE_HEADER] = opts.instanceId
}
if (method === 'POST' && opts.model) {
- headers[FREEBUFF_MODEL_HEADER] = opts.model
+ headers[CODEBIRDS_MODEL_HEADER] = opts.model
}
const resp = await fetch(sessionEndpoint(), {
method,
@@ -133,7 +133,7 @@ async function callSession(
if (!resp.ok) {
const text = await resp.text().catch(() => '')
throw new Error(
- `freebuff session ${method} failed: ${resp.status} ${text.slice(0, 200)}`,
+ `codebirds session ${method} failed: ${resp.status} ${text.slice(0, 200)}`,
)
}
return (await resp.json()) as FreebuffSessionServerResponse
@@ -278,7 +278,7 @@ async function restartFreebuffSession(
mode: RestartMode,
opts: RestartOpts = {},
): Promise {
- if (!IS_FREEBUFF) return
+ if (!IS_CODEBIRDS) return
// Halt the running poll loop before we touch local stores or DELETE the
// slot. Otherwise an in-flight GET could land mid-reset and overwrite
// state, or the next scheduled tick could fire between DELETE and
@@ -336,7 +336,7 @@ export function refreshFreebuffLandingMetadata(): Promise {
* to switch can /end-session deliberately.
*/
export function joinFreebuffQueue(model: string): Promise {
- if (!IS_FREEBUFF) return Promise.resolve()
+ if (!IS_CODEBIRDS) return Promise.resolve()
// This is the only explicit user-pick path (called from the picker on
// click / Enter), so persistence belongs here — and ONLY here. Server-
// driven flips (`model_locked`, `model_unavailable`, takeover) go
@@ -351,7 +351,7 @@ export function joinFreebuffQueue(model: string): Promise {
}
export function takeOverFreebuffSession(): Promise {
- if (!IS_FREEBUFF) return Promise.resolve()
+ if (!IS_CODEBIRDS) return Promise.resolve()
const current = useFreebuffSessionStore.getState().session
if (current?.status !== 'takeover_prompt') return Promise.resolve()
useFreebuffModelStore.getState().setSelectedModel(current.model)
@@ -364,12 +364,12 @@ export function takeOverFreebuffSession(): Promise {
* instead of waiting for the server-side expiry sweep.
*/
export async function endFreebuffSessionBestEffort(): Promise {
- if (!IS_FREEBUFF) return
+ if (!IS_CODEBIRDS) return
await releaseFreebuffSlot()
}
export function markFreebuffSessionSuperseded(): void {
- if (!IS_FREEBUFF) return
+ if (!IS_CODEBIRDS) return
controller?.abort()
controller?.apply({ status: 'superseded' })
}
@@ -385,7 +385,7 @@ export function markFreebuffSessionCountryBlocked(params: {
countryBlockReason?: FreebuffCountryBlockReason
ipPrivacySignals?: FreebuffIpPrivacySignal[]
}): void {
- if (!IS_FREEBUFF) return
+ if (!IS_CODEBIRDS) return
controller?.abort()
controller?.apply({ status: 'country_blocked', ...params })
// Best-effort DELETE so we don't hold a waiting-room seat on a session the
@@ -398,7 +398,7 @@ export function markFreebuffSessionCountryBlocked(params: {
* Preserves any `rateLimitsByModel` snapshot from the prior session so the
* banner can show today's session count without an extra fetch. */
export function markFreebuffSessionEnded(): void {
- if (!IS_FREEBUFF) return
+ if (!IS_CODEBIRDS) return
controller?.abort()
const current = useFreebuffSessionStore.getState().session
const rateLimitsByModel = getRateLimitsByModel(current)
@@ -416,7 +416,7 @@ interface UseFreebuffSessionResult {
}
/**
- * Manages the freebuff session lifecycle:
+ * Manages the codebirds session lifecycle:
* - GET on mount to probe state (no auto-join; the user picks a model in
* the landing screen, which calls joinFreebuffQueue)
* - if the probe sees an existing seat, auto-takes-over when the prior
@@ -435,9 +435,9 @@ export function useFreebuffSession(): UseFreebuffSessionResult {
useEffect(() => {
const { setSession, setError } = useFreebuffSessionStore.getState()
- if (!IS_FREEBUFF) {
- // Non-freebuff (Codebuff) builds never gate on a free session; leave the
- // store empty (app.tsx's session routing is all behind IS_FREEBUFF).
+ if (!IS_CODEBIRDS) {
+ // Non-codebirds (Codebirds) builds never gate on a free session; leave the
+ // store empty (app.tsx's session routing is all behind IS_CODEBIRDS).
setSession(null)
return
}
@@ -446,7 +446,7 @@ export function useFreebuffSession(): UseFreebuffSessionResult {
if (!token) {
logger.warn(
{},
- '[freebuff-session] No auth token; skipping free-session admission',
+ '[codebirds-session] No auth token; skipping free-session admission',
)
setError('Not authenticated')
return
@@ -471,7 +471,7 @@ export function useFreebuffSession(): UseFreebuffSessionResult {
} else if (next.status === 'none' && next.accessTier === 'limited') {
useFreebuffModelStore
.getState()
- .setSelectedModel(LIMITED_FREEBUFF_MODEL_ID)
+ .setSelectedModel(LIMITED_CODEBIRDS_MODEL_ID)
}
setSession(next)
setError(null)
@@ -525,7 +525,7 @@ export function useFreebuffSession(): UseFreebuffSessionResult {
// is preserved for their next launch.
useFreebuffModelStore
.getState()
- .setSelectedModel(FALLBACK_FREEBUFF_MODEL_ID)
+ .setSelectedModel(FALLBACK_CODEBIRDS_MODEL_ID)
// The unavailable response came from a POST attempt. Re-POST with
// the fallback model; a GET would only redisplay the old ended row
// and leave the restart banner stuck in its pending state.
@@ -536,7 +536,7 @@ export function useFreebuffSession(): UseFreebuffSessionResult {
// Startup takeover: the initial probe GET saw we already hold a seat
// (from a prior CLI instance). Stop here and ask before POSTing to
- // rotate our instance id; otherwise opening a second freebuff would
+ // rotate our instance id; otherwise opening a second codebirds would
// immediately supersede the first one.
// `previousStatus === null` fences this to the very first tick only.
// Pin the selected model to whatever the server thinks we're on so
@@ -599,7 +599,7 @@ export function useFreebuffSession(): UseFreebuffSessionResult {
} catch (err) {
if (cancelled || abortController.signal.aborted) return
const msg = err instanceof Error ? err.message : String(err)
- logger.warn({ error: msg }, '[freebuff-session] fetch failed')
+ logger.warn({ error: msg }, '[codebirds-session] fetch failed')
setError(msg)
schedule(POLL_INTERVAL_ERROR_MS)
}
diff --git a/cli/src/hooks/use-freebuff-streak-query.ts b/cli/src/hooks/use-freebuff-streak-query.ts
index 2c3448c6c0..0ea055abf9 100644
--- a/cli/src/hooks/use-freebuff-streak-query.ts
+++ b/cli/src/hooks/use-freebuff-streak-query.ts
@@ -1,15 +1,15 @@
import { useQuery } from '@tanstack/react-query'
import { getAuthToken } from '../utils/auth'
-import { getApiClient, setApiClientAuthToken } from '../utils/codebuff-api'
+import { getApiClient, setApiClientAuthToken } from '../utils/codebirds-api'
import { logger as defaultLogger } from '../utils/logger'
-import type { FreebuffStreakResponse } from '@codebuff/common/types/freebuff-streak'
-import type { Logger } from '@codebuff/common/types/contracts/logger'
+import type { FreebuffStreakResponse } from '@codebirds/common/types/codebirds-streak'
+import type { Logger } from '@codebirds/common/types/contracts/logger'
-export const freebuffStreakQueryKeys = {
- all: ['freebuffStreak'] as const,
- current: () => [...freebuffStreakQueryKeys.all, 'current'] as const,
+export const codebirdsStreakQueryKeys = {
+ all: ['codebirdsStreak'] as const,
+ current: () => [...codebirdsStreakQueryKeys.all, 'current'] as const,
}
export async function fetchFreebuffStreak(params: {
@@ -19,20 +19,20 @@ export async function fetchFreebuffStreak(params: {
const { authToken, logger = defaultLogger } = params
setApiClientAuthToken(authToken)
const response = await getApiClient().get(
- '/api/v1/freebuff/streak',
+ '/api/v1/codebirds/streak',
{ retry: false },
)
if (!response.ok) {
logger.error(
{ status: response.status, error: response.error },
- 'Failed to fetch freebuff streak',
+ 'Failed to fetch codebirds streak',
)
- throw new Error(`Failed to fetch freebuff streak (HTTP ${response.status})`)
+ throw new Error(`Failed to fetch codebirds streak (HTTP ${response.status})`)
}
if (!response.data) {
- throw new Error('Failed to fetch freebuff streak: empty response')
+ throw new Error('Failed to fetch codebirds streak: empty response')
}
return response.data
@@ -48,7 +48,7 @@ export function useFreebuffStreakQuery(
const authToken = getAuthToken()
return useQuery({
- queryKey: freebuffStreakQueryKeys.current(),
+ queryKey: codebirdsStreakQueryKeys.current(),
queryFn: () => fetchFreebuffStreak({ authToken: authToken!, logger }),
enabled: enabled && !!authToken,
staleTime: 60_000,
diff --git a/cli/src/hooks/use-gravity-ad.ts b/cli/src/hooks/use-gravity-ad.ts
index fb4215de0f..042d0b0612 100644
--- a/cli/src/hooks/use-gravity-ad.ts
+++ b/cli/src/hooks/use-gravity-ad.ts
@@ -1,4 +1,4 @@
-import { WEBSITE_URL } from '@codebuff/sdk'
+import { WEBSITE_URL } from '@codebirds/sdk'
import { useEffect, useRef, useState } from 'react'
import { useTerminalLayout } from './use-terminal-layout'
@@ -6,11 +6,11 @@ import { getAdsEnabled } from '../commands/ads'
import { useChatStore } from '../state/chat-store'
import { isUserActive, subscribeToActivity } from '../utils/activity-tracker'
import { getAuthToken } from '../utils/auth'
-import { IS_FREEBUFF } from '../utils/constants'
+import { IS_CODEBIRDS } from '../utils/constants'
import { getCliEnv } from '../utils/env'
import { logger } from '../utils/logger'
-import type { Message } from '@codebuff/sdk'
+import type { Message } from '@codebirds/sdk'
const AD_ROTATION_INTERVAL_MS = 60 * 1000 // 60 seconds per ad
const MAX_ADS_AFTER_ACTIVITY = 3 // Show up to 3 ads after last activity, then pause fetching new ads
@@ -89,7 +89,7 @@ function nextFromChoiceCache(ctrl: GravityController): AdResponse[] | null {
*/
export const useGravityAd = (options?: {
enabled?: boolean
- /** Skip the "wait for first user message" gate. Used by the freebuff
+ /** Skip the "wait for first user message" gate. Used by the codebirds
* waiting room, which has no conversation but still needs ads. */
forceStart?: boolean
/** Ad network to request first. The server owns fallback ordering. */
@@ -109,7 +109,7 @@ export const useGravityAd = (options?: {
const isVeryCompactHeight = terminalHeight <= 17
// Freebuff always shows ads even on compact screens (ads are mandatory there).
- const isFreeMode = IS_FREEBUFF
+ const isFreeMode = IS_CODEBIRDS
// Skip ads on very compact screens unless we're in Freebuff (where ads are mandatory)
// Also skip if explicitly disabled (e.g. user has a subscription)
@@ -513,7 +513,7 @@ function getAdUserAgent(): string {
}
function getCliAdRequestUserAgent(): string {
- const product = IS_FREEBUFF ? 'Freebuff-CLI' : 'Codebuff-CLI'
- const version = getCliEnv().CODEBUFF_CLI_VERSION ?? 'dev'
+ const product = IS_CODEBIRDS ? 'Freebuff-CLI' : 'Codebirds-CLI'
+ const version = getCliEnv().CODEBIRDS_CLI_VERSION ?? 'dev'
return `${product}/${version}`
}
diff --git a/cli/src/hooks/use-logo.tsx b/cli/src/hooks/use-logo.tsx
index 3d4974664a..4f5b8908b6 100644
--- a/cli/src/hooks/use-logo.tsx
+++ b/cli/src/hooks/use-logo.tsx
@@ -2,7 +2,7 @@ import React, { useMemo } from 'react'
import { LOGO, LOGO_SMALL, SHADOW_CHARS } from '../login/constants'
import { parseLogoLines } from '../login/utils'
-import { IS_FREEBUFF } from '../utils/constants'
+import { IS_CODEBIRDS } from '../utils/constants'
interface UseLogoOptions {
/**
@@ -53,7 +53,7 @@ interface LogoResult {
* Returns:
* - Full ASCII logo for width >= 70
* - Small ASCII logo for width >= 40
- * - Text variant "CODEBUFF" or "Codebuff CLI" for narrow widths
+ * - Text variant "CODEBUFF" or "Codebirds CLI" for narrow widths
*
* The hook handles ALL formatting internally including:
* - Line parsing and width limiting
@@ -74,16 +74,16 @@ export const useLogo = ({
const ASCII_LOGO_LINES = 6
const rawLogoString = useMemo(() => {
if (maxHeight != null && maxHeight < ASCII_LOGO_LINES) {
- return IS_FREEBUFF ? 'FREEBUFF' : 'CODEBUFF'
+ return IS_CODEBIRDS ? 'CODEBIRDS' : 'CODEBUFF'
}
if (availableWidth >= 70) return LOGO
if (availableWidth >= 20) return LOGO_SMALL
- return IS_FREEBUFF ? 'FREEBUFF' : 'CODEBUFF'
+ return IS_CODEBIRDS ? 'CODEBIRDS' : 'CODEBUFF'
}, [availableWidth, maxHeight])
// Format text block for plain text contexts (chat messages, etc.)
const textBlock = useMemo(() => {
- if (rawLogoString === 'CODEBUFF' || rawLogoString === 'FREEBUFF') {
+ if (rawLogoString === 'CODEBUFF' || rawLogoString === 'CODEBIRDS') {
return '' // Don't show ASCII art for text-only variant in plain text contexts
}
// Parse and format for plain text display
@@ -95,8 +95,8 @@ export const useLogo = ({
// Format component for React contexts (login modal, etc.)
const component = useMemo(() => {
// Text-only variant for very narrow widths
- if (rawLogoString === 'CODEBUFF' || rawLogoString === 'FREEBUFF') {
- const brandName = IS_FREEBUFF ? 'Freebuff' : 'Codebuff'
+ if (rawLogoString === 'CODEBUFF' || rawLogoString === 'CODEBIRDS') {
+ const brandName = IS_CODEBIRDS ? 'Freebuff' : 'Codebirds'
// When we collapsed to text purely to fit a short terminal (not because
// the terminal is narrow), keep it to the bare brand name — "Freebuff
// CLI" reads as filler in that already-cramped space.
diff --git a/cli/src/hooks/use-now.ts b/cli/src/hooks/use-now.ts
index 03b7f33a87..7f5f7c0504 100644
--- a/cli/src/hooks/use-now.ts
+++ b/cli/src/hooks/use-now.ts
@@ -6,7 +6,7 @@ import { useEffect, useState } from 'react'
* this independently; setIntervals are cheap and React batches the resulting
* renders.
*
- * Intended for short-lived UI countdowns like the freebuff session timer or
+ * Intended for short-lived UI countdowns like the codebirds session timer or
* elapsed-in-queue display.
*/
export function useNow(intervalMs: number, enabled = true): number {
diff --git a/cli/src/hooks/use-queue-ui.ts b/cli/src/hooks/use-queue-ui.ts
index 16985e8c34..86d4924cc6 100644
--- a/cli/src/hooks/use-queue-ui.ts
+++ b/cli/src/hooks/use-queue-ui.ts
@@ -1,4 +1,4 @@
-import { pluralize } from '@codebuff/common/util/string'
+import { pluralize } from '@codebirds/common/util/string'
import { useMemo } from 'react'
diff --git a/cli/src/hooks/use-send-message.ts b/cli/src/hooks/use-send-message.ts
index 4dd3ece226..38971bfc2b 100644
--- a/cli/src/hooks/use-send-message.ts
+++ b/cli/src/hooks/use-send-message.ts
@@ -3,12 +3,12 @@ import { useCallback, useEffect, useRef } from 'react'
import { setCurrentChatId } from '../project-files'
import { createStreamController } from './stream-state'
import { useChatStore } from '../state/chat-store'
-import { getFreebuffInstanceId } from './use-freebuff-session'
-import { getCodebuffClient } from '../utils/codebuff-client'
-import { AGENT_MODE_TO_COST_MODE, IS_FREEBUFF } from '../utils/constants'
+import { getFreebuffInstanceId } from './use-codebirds-session'
+import { getCodebirdsClient } from '../utils/codebirds-client'
+import { AGENT_MODE_TO_COST_MODE, IS_CODEBIRDS } from '../utils/constants'
import { createEventHandlerState } from '../utils/create-event-handler-state'
import { createRunConfig } from '../utils/create-run-config'
-import { getAgentIdForMode } from '../utils/freebuff-agent-selection'
+import { getAgentIdForMode } from '../utils/codebirds-agent-selection'
import { loadAgentDefinitions } from '../utils/local-agent-registry'
import { logger } from '../utils/logger'
import {
@@ -39,7 +39,7 @@ import type { ChatMessage } from '../types/chat'
import type { SendMessageFn } from '../types/contracts/send-message'
import type { AgentMode } from '../utils/constants'
import type { SendMessageTimerEvent } from '../utils/send-message-timer'
-import type { AgentDefinition, MessageContent, RunState } from '@codebuff/sdk'
+import type { AgentDefinition, MessageContent, RunState } from '@codebirds/sdk'
import { isCoveredBySubscription } from '../utils/subscription'
import type { SubscriptionResponse } from './use-subscription-query'
@@ -357,15 +357,15 @@ export const useSendMessage = ({
inputRef.current?.focus()
// Get SDK client
- const client = await getCodebuffClient()
+ const client = await getCodebirdsClient()
if (!client) {
logger.error(
{},
- '[send-message] No Codebuff client available. Please ensure you are authenticated.',
+ '[send-message] No Codebirds client available. Please ensure you are authenticated.',
)
// Show error to user instead of silently failing
- const brandName = IS_FREEBUFF ? 'Freebuff' : 'Codebuff'
+ const brandName = IS_CODEBIRDS ? 'Freebuff' : 'Codebirds'
setMessages((prev) => [
...prev,
createErrorChatMessage(
@@ -450,7 +450,7 @@ export const useSendMessage = ({
},
})
- const freebuffInstanceId = getFreebuffInstanceId()
+ const codebirdsInstanceId = getFreebuffInstanceId()
const runConfig = createRunConfig({
logger,
agent: resolvedAgent,
@@ -461,9 +461,9 @@ export const useSendMessage = ({
eventHandlerState,
signal: abortController.signal,
costMode: AGENT_MODE_TO_COST_MODE[agentMode],
- extraCodebuffMetadata:
- IS_FREEBUFF && freebuffInstanceId
- ? { freebuff_instance_id: freebuffInstanceId }
+ extraCodebirdsMetadata:
+ IS_CODEBIRDS && codebirdsInstanceId
+ ? { codebirds_instance_id: codebirdsInstanceId }
: undefined,
})
diff --git a/cli/src/hooks/use-subscription-query.ts b/cli/src/hooks/use-subscription-query.ts
index f27b5d832a..a7596397e7 100644
--- a/cli/src/hooks/use-subscription-query.ts
+++ b/cli/src/hooks/use-subscription-query.ts
@@ -1,11 +1,11 @@
import { useActivityQuery } from './use-activity-query'
import { getAuthToken } from '../utils/auth'
-import { IS_FREEBUFF } from '../utils/constants'
-import { getApiClient } from '../utils/codebuff-api'
+import { IS_CODEBIRDS } from '../utils/constants'
+import { getApiClient } from '../utils/codebirds-api'
import { logger as defaultLogger } from '../utils/logger'
-import type { Logger } from '@codebuff/common/types/contracts/logger'
-import type { SubscriptionResponse } from '@codebuff/common/types/subscription'
+import type { Logger } from '@codebirds/common/types/contracts/logger'
+import type { SubscriptionResponse } from '@codebirds/common/types/subscription'
export type { SubscriptionResponse }
@@ -58,7 +58,7 @@ export function useSubscriptionQuery(deps: UseSubscriptionQueryDeps = {}) {
return useActivityQuery({
queryKey: subscriptionQueryKeys.current(),
queryFn: () => fetchSubscriptionData(logger),
- enabled: enabled && !!authToken && !IS_FREEBUFF,
+ enabled: enabled && !!authToken && !IS_CODEBIRDS,
staleTime: 30 * 1000,
gcTime: 5 * 60 * 1000,
retry: 1,
diff --git a/cli/src/hooks/use-suggestion-engine.ts b/cli/src/hooks/use-suggestion-engine.ts
index ed1054cd32..21d1b30068 100644
--- a/cli/src/hooks/use-suggestion-engine.ts
+++ b/cli/src/hooks/use-suggestion-engine.ts
@@ -4,7 +4,7 @@ import {
getAllPathsWithDirectories,
getProjectFileTree,
type PathInfo,
-} from '@codebuff/common/project-file-tree'
+} from '@codebirds/common/project-file-tree'
import { useDeferredValue, useEffect, useMemo, useRef, useState } from 'react'
@@ -17,7 +17,7 @@ import type { SlashCommand } from '../data/slash-commands'
import type { Prettify } from '../types/utils'
import type { AgentMode } from '../utils/constants'
import type { LocalAgentInfo } from '../utils/local-agent-registry'
-import type { FileTreeNode } from '@codebuff/common/util/file'
+import type { FileTreeNode } from '@codebirds/common/util/file'
export interface TriggerContext {
active: boolean
diff --git a/cli/src/hooks/use-update-preference.ts b/cli/src/hooks/use-update-preference.ts
index 7c72f304bb..fe6c910c78 100644
--- a/cli/src/hooks/use-update-preference.ts
+++ b/cli/src/hooks/use-update-preference.ts
@@ -7,10 +7,10 @@ import {
} from './use-activity-query'
import { subscriptionQueryKeys } from './use-subscription-query'
import { showClipboardMessage } from '../utils/clipboard'
-import { getApiClient } from '../utils/codebuff-api'
+import { getApiClient } from '../utils/codebirds-api'
import { logger } from '../utils/logger'
-import type { SubscriptionResponse } from '@codebuff/common/types/subscription'
+import type { SubscriptionResponse } from '@codebirds/common/types/subscription'
interface UpdatePreferenceParams {
fallbackToALaCarte?: boolean
diff --git a/cli/src/hooks/use-usage-monitor.ts b/cli/src/hooks/use-usage-monitor.ts
index ad98460101..49b72433ee 100644
--- a/cli/src/hooks/use-usage-monitor.ts
+++ b/cli/src/hooks/use-usage-monitor.ts
@@ -1,7 +1,7 @@
import { useEffect, useRef } from 'react'
import { useUsageQuery } from './use-usage-query'
-import { IS_FREEBUFF } from '../utils/constants'
+import { IS_CODEBIRDS } from '../utils/constants'
import { useChatStore } from '../state/chat-store'
import { getAuthToken } from '../utils/auth'
import { shouldAutoShowBanner } from '../utils/usage-banner-state'
@@ -20,10 +20,10 @@ export function useUsageMonitor() {
const lastWarnedThresholdRef = useRef(null)
// Query usage data - this will refetch when invalidated after message completion
- const { data: usageData } = useUsageQuery({ enabled: !IS_FREEBUFF })
+ const { data: usageData } = useUsageQuery({ enabled: !IS_CODEBIRDS })
useEffect(() => {
- if (IS_FREEBUFF) return
+ if (IS_CODEBIRDS) return
// Only show after user has sent at least one message (to avoid overwhelming on app start)
if (sessionCreditsUsed === 0) {
diff --git a/cli/src/hooks/use-usage-query.ts b/cli/src/hooks/use-usage-query.ts
index 0ddc3a2dc8..2e1986bc72 100644
--- a/cli/src/hooks/use-usage-query.ts
+++ b/cli/src/hooks/use-usage-query.ts
@@ -1,12 +1,12 @@
-import { env } from '@codebuff/common/env'
+import { env } from '@codebirds/common/env'
import { useCallback } from 'react'
import { invalidateActivityQuery, useActivityQuery } from './use-activity-query'
import { getAuthToken } from '../utils/auth'
import { logger as defaultLogger } from '../utils/logger'
-import type { ClientEnv } from '@codebuff/common/types/contracts/env'
-import type { Logger } from '@codebuff/common/types/contracts/logger'
+import type { ClientEnv } from '@codebirds/common/types/contracts/env'
+import type { Logger } from '@codebirds/common/types/contracts/logger'
// Query keys for type-safe cache management
export const usageQueryKeys = {
@@ -43,9 +43,9 @@ export async function fetchUsageData({
logger = defaultLogger,
clientEnv = env,
}: FetchUsageParams): Promise {
- const appUrl = clientEnv.NEXT_PUBLIC_CODEBUFF_APP_URL
+ const appUrl = clientEnv.NEXT_PUBLIC_CODEBIRDS_APP_URL
if (!appUrl) {
- throw new Error('NEXT_PUBLIC_CODEBUFF_APP_URL is not set')
+ throw new Error('NEXT_PUBLIC_CODEBIRDS_APP_URL is not set')
}
const response = await fetch(`${appUrl}/api/v1/usage`, {
diff --git a/cli/src/hooks/use-user-details-query.ts b/cli/src/hooks/use-user-details-query.ts
index fa5f7524c2..13b6bb60a7 100644
--- a/cli/src/hooks/use-user-details-query.ts
+++ b/cli/src/hooks/use-user-details-query.ts
@@ -1,15 +1,15 @@
import { useQuery } from '@tanstack/react-query'
import { getAuthToken } from '../utils/auth'
-import { getApiClient, setApiClientAuthToken } from '../utils/codebuff-api'
+import { getApiClient, setApiClientAuthToken } from '../utils/codebirds-api'
import { logger as defaultLogger } from '../utils/logger'
import type {
- CodebuffApiClient,
+ CodebirdsApiClient,
UserField,
UserDetails,
-} from '../utils/codebuff-api'
-import type { Logger } from '@codebuff/common/types/contracts/logger'
+} from '../utils/codebirds-api'
+import type { Logger } from '@codebirds/common/types/contracts/logger'
// Re-export types for backwards compatibility
export type { UserField, UserDetails }
@@ -25,7 +25,7 @@ interface FetchUserDetailsParams {
authToken: string
fields: readonly T[]
logger?: Logger
- apiClient?: CodebuffApiClient
+ apiClient?: CodebirdsApiClient
}
/**
@@ -37,7 +37,7 @@ export async function fetchUserDetails({
logger = defaultLogger,
apiClient: providedApiClient,
}: FetchUserDetailsParams): Promise | null> {
- let apiClient: CodebuffApiClient
+ let apiClient: CodebirdsApiClient
if (providedApiClient) {
apiClient = providedApiClient
} else {
diff --git a/cli/src/index.tsx b/cli/src/index.tsx
index 0aff2b4acf..5cad4311b3 100644
--- a/cli/src/index.tsx
+++ b/cli/src/index.tsx
@@ -11,8 +11,8 @@ import fs from 'fs'
import os from 'os'
import path from 'path'
-import { AnalyticsEvent } from '@codebuff/common/constants/analytics-events'
-import { getProjectFileTree } from '@codebuff/common/project-file-tree'
+import { AnalyticsEvent } from '@codebirds/common/constants/analytics-events'
+import { getProjectFileTree } from '@codebirds/common/project-file-tree'
import { createCliRenderer } from '@opentui/core'
import { createRoot } from '@opentui/react'
import {
@@ -31,9 +31,9 @@ import { initializeApp } from './init/init-app'
import { getProjectRoot, setProjectRoot } from './project-files'
import { trackEvent } from './utils/analytics'
import { getAuthToken, getAuthTokenDetails } from './utils/auth'
-import { resetCodebuffClient } from './utils/codebuff-client'
-import { setApiClientAuthToken } from './utils/codebuff-api'
-import { IS_FREEBUFF } from './utils/constants'
+import { resetCodebirdsClient } from './utils/codebirds-client'
+import { setApiClientAuthToken } from './utils/codebirds-api'
+import { IS_CODEBIRDS } from './utils/constants'
import { initializeAgentRegistry } from './utils/local-agent-registry'
import { trimOversizedChatLogs } from './utils/chat-history'
import { clearLogFile, logger } from './utils/logger'
@@ -45,7 +45,7 @@ import { initializeSkillRegistry } from './utils/skill-registry'
import { detectTerminalTheme } from './utils/terminal-color-detection'
import { setOscDetectedTheme } from './utils/theme-system'
-import type { FileTreeNode } from '@codebuff/common/util/file'
+import type { FileTreeNode } from '@codebirds/common/util/file'
// Configure TanStack Query's focusManager for terminal environments
// This is required because there's no browser visibility API in terminal apps
@@ -82,11 +82,11 @@ async function main(): Promise {
// still ran first and rejected the unknown flag).
if (process.argv.includes('--smoke-tree-sitter')) {
const wasmBinary = (
- globalThis as { __CODEBUFF_TREE_SITTER_WASM_BINARY__?: Uint8Array }
- ).__CODEBUFF_TREE_SITTER_WASM_BINARY__
+ globalThis as { __CODEBIRDS_TREE_SITTER_WASM_BINARY__?: Uint8Array }
+ ).__CODEBIRDS_TREE_SITTER_WASM_BINARY__
const wasmPath = (
- globalThis as { __CODEBUFF_TREE_SITTER_WASM_PATH__?: string }
- ).__CODEBUFF_TREE_SITTER_WASM_PATH__
+ globalThis as { __CODEBIRDS_TREE_SITTER_WASM_PATH__?: string }
+ ).__CODEBIRDS_TREE_SITTER_WASM_PATH__
// Diagnostic dump so CI logs (and bug reports) show exactly what
// the runtime saw when smoke fails. process.execPath, the
@@ -206,7 +206,7 @@ async function main(): Promise {
hasAgentOverride: hasAgentOverride,
continueChat,
initialMode: initialMode ?? 'DEFAULT',
- isFreeBuff: IS_FREEBUFF,
+ isFreeBuff: IS_CODEBIRDS,
})
// Initialize agent registry (loads user agents via SDK).
@@ -312,7 +312,7 @@ async function main(): Promise {
// Update the project root in the module state
setProjectRoot(newProjectPath)
// Reset client to ensure tools use the updated project root
- resetCodebuffClient()
+ resetCodebirdsClient()
// Save to recent projects list
saveRecentProject(newProjectPath)
// Update local state
@@ -384,7 +384,7 @@ async function main(): Promise {
// live — reaching renderer creation means this is a real session (the
// login/publish/smoke-test commands all exit earlier). Freebuff-only, matching
// the MESSAGE_SENT DAU signal. Stopped in exitFreebuffCleanly().
- if (IS_FREEBUFF) {
+ if (IS_CODEBIRDS) {
startEngagementTracking()
}
diff --git a/cli/src/init/init-app.ts b/cli/src/init/init-app.ts
index 17ecc61810..985b95ef99 100644
--- a/cli/src/init/init-app.ts
+++ b/cli/src/init/init-app.ts
@@ -1,8 +1,8 @@
-import { CHATGPT_OAUTH_ENABLED } from '@codebuff/common/constants/chatgpt-oauth'
+import { CHATGPT_OAUTH_ENABLED } from '@codebirds/common/constants/chatgpt-oauth'
import {
getChatGptOAuthCredentials,
getValidChatGptOAuthCredentials,
-} from '@codebuff/sdk'
+} from '@codebirds/sdk'
import { enableMapSet } from 'immer'
import { initializeThemeStore } from '../hooks/use-theme'
diff --git a/cli/src/login/constants.ts b/cli/src/login/constants.ts
index b27f8fc2f1..579c07e591 100644
--- a/cli/src/login/constants.ts
+++ b/cli/src/login/constants.ts
@@ -1,17 +1,17 @@
-import { env, IS_DEV } from '@codebuff/common/env'
+import { env, IS_DEV } from '@codebirds/common/env'
-import { IS_FREEBUFF } from '../utils/constants'
+import { IS_CODEBIRDS } from '../utils/constants'
// Get the website URL from environment or use default
-export const WEBSITE_URL = env.NEXT_PUBLIC_CODEBUFF_APP_URL
+export const WEBSITE_URL = env.NEXT_PUBLIC_CODEBIRDS_APP_URL
-// Freebuff login flow uses the freebuff web app instead of codebuff.com
-const FREEBUFF_WEB_URL = IS_DEV
+// Freebuff login flow uses the codebirds web app instead of codebirds.com
+const CODEBIRDS_WEB_URL = IS_DEV
? 'http://localhost:3002'
- : (env.NEXT_PUBLIC_FREEBUFF_APP_URL ?? 'https://freebuff.com')
-export const LOGIN_WEBSITE_URL = IS_FREEBUFF ? FREEBUFF_WEB_URL : WEBSITE_URL
+ : (env.NEXT_PUBLIC_CODEBIRDS_APP_URL ?? 'https://codebirds.com')
+export const LOGIN_WEBSITE_URL = IS_CODEBIRDS ? CODEBIRDS_WEB_URL : WEBSITE_URL
-// Codebuff ASCII Logo - compact version for 80-width terminals
+// Codebirds ASCII Logo - compact version for 80-width terminals
const LOGO_CODEBUFF = `
██████╗ ██████╗ ██████╗ ███████╗██████╗ ██╗ ██╗███████╗███████╗
██╔════╝██╔═══██╗██╔══██╗██╔════╝██╔══██╗██║ ██║██╔════╝██╔════╝
@@ -31,7 +31,7 @@ const LOGO_SMALL_CODEBUFF = `
`
// Freebuff ASCII Logo
-const LOGO_FREEBUFF = `
+const LOGO_CODEBIRDS = `
███████╗██████╗ ███████╗███████╗██████╗ ██╗ ██╗███████╗███████╗
██╔════╝██╔══██╗██╔════╝██╔════╝██╔══██╗██║ ██║██╔════╝██╔════╝
█████╗ ██████╔╝█████╗ █████╗ ██████╔╝██║ ██║█████╗ █████╗
@@ -40,7 +40,7 @@ const LOGO_FREEBUFF = `
╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚═════╝ ╚═════╝ ╚═╝ ╚═╝
`
-const LOGO_SMALL_FREEBUFF = `
+const LOGO_SMALL_CODEBIRDS = `
███████╗██████╗
██╔════╝██╔══██╗
█████╗ ██████╔╝
@@ -49,8 +49,8 @@ const LOGO_SMALL_FREEBUFF = `
╚═╝ ╚═════╝
`
-export const LOGO = IS_FREEBUFF ? LOGO_FREEBUFF : LOGO_CODEBUFF
-export const LOGO_SMALL = IS_FREEBUFF ? LOGO_SMALL_FREEBUFF : LOGO_SMALL_CODEBUFF
+export const LOGO = IS_CODEBIRDS ? LOGO_CODEBIRDS : LOGO_CODEBUFF
+export const LOGO_SMALL = IS_CODEBIRDS ? LOGO_SMALL_CODEBIRDS : LOGO_SMALL_CODEBUFF
// Shadow/border characters that receive the sheen animation effect
export const SHADOW_CHARS = new Set([
diff --git a/cli/src/login/login-flow.ts b/cli/src/login/login-flow.ts
index ee90fc3c99..8b43fd9a88 100644
--- a/cli/src/login/login-flow.ts
+++ b/cli/src/login/login-flow.ts
@@ -1,12 +1,12 @@
-import { AnalyticsEvent } from '@codebuff/common/constants/analytics-events'
+import { AnalyticsEvent } from '@codebirds/common/constants/analytics-events'
-import { createCodebuffApiClient } from '../utils/codebuff-api'
+import { createCodebirdsApiClient } from '../utils/codebirds-api'
import type {
- CodebuffApiClient,
+ CodebirdsApiClient,
LoginCodeResponse,
-} from '../utils/codebuff-api'
-import type { Logger } from '@codebuff/common/types/contracts/logger'
+} from '../utils/codebirds-api'
+import type { Logger } from '@codebirds/common/types/contracts/logger'
// Re-export for backwards compatibility
export type LoginUrlResponse = LoginCodeResponse
@@ -19,7 +19,7 @@ export type LoginVia = 'modal' | 'plain_command'
export interface GenerateLoginUrlDeps {
logger: Logger
- apiClient?: CodebuffApiClient
+ apiClient?: CodebirdsApiClient
/**
* Emit a login-funnel analytics event. Injected so login-flow stays a pure,
* test-friendly module; callers wire in the real `trackEvent`. Omitted in
@@ -47,7 +47,7 @@ export async function generateLoginUrl(
const apiClient =
providedApiClient ??
- createCodebuffApiClient({
+ createCodebirdsApiClient({
baseUrl,
})
@@ -89,7 +89,7 @@ interface PollLoginStatusDeps {
sleep: (ms: number) => Promise
logger: Logger
now?: () => number
- apiClient?: CodebuffApiClient
+ apiClient?: CodebirdsApiClient
trackEvent?: (event: AnalyticsEvent, properties?: Record) => void
}
@@ -131,7 +131,7 @@ export async function pollLoginStatus(
const apiClient =
providedApiClient ??
- createCodebuffApiClient({
+ createCodebirdsApiClient({
baseUrl,
})
diff --git a/cli/src/login/plain-login.ts b/cli/src/login/plain-login.ts
index 5aea5b683a..fb1d185293 100644
--- a/cli/src/login/plain-login.ts
+++ b/cli/src/login/plain-login.ts
@@ -1,4 +1,4 @@
-import { AnalyticsEvent } from '@codebuff/common/constants/analytics-events'
+import { AnalyticsEvent } from '@codebirds/common/constants/analytics-events'
import { cyan, green, red, yellow, bold } from 'picocolors'
import { LOGIN_WEBSITE_URL } from './constants'
@@ -9,7 +9,7 @@ import {
trackEvent,
} from '../utils/analytics'
import { saveUserCredentials } from '../utils/auth'
-import { IS_FREEBUFF } from '../utils/constants'
+import { IS_CODEBIRDS } from '../utils/constants'
import { getFingerprintId } from '../utils/fingerprint'
import { logger } from '../utils/logger'
@@ -27,7 +27,7 @@ export async function runPlainLogin(): Promise {
const fingerprintId = await getFingerprintId()
console.log()
- console.log(bold(IS_FREEBUFF ? 'Freebuff Login' : 'Codebuff Login'))
+ console.log(bold(IS_CODEBIRDS ? 'Freebuff Login' : 'Codebirds Login'))
console.log()
console.log('Generating login URL...')
@@ -82,7 +82,7 @@ export async function runPlainLogin(): Promise {
// logins aren't missing from the funnel, then flush before exiting since
// process.exit would otherwise drop the buffered PostHog events.
if (user.id) {
- identifyUser(user.id, { email: user.email, freebuff: IS_FREEBUFF })
+ identifyUser(user.id, { email: user.email, codebirds: IS_CODEBIRDS })
trackEvent(AnalyticsEvent.LOGIN, {
userId: user.id,
via: 'plain_command',
@@ -95,7 +95,7 @@ export async function runPlainLogin(): Promise {
console.log()
console.log(green(`✓ Logged in as ${user.name} (${user.email})`))
console.log()
- const cliName = IS_FREEBUFF ? 'freebuff' : 'codebuff'
+ const cliName = IS_CODEBIRDS ? 'codebirds' : 'codebirds'
console.log('You can now run ' + cyan(cliName) + ' to start.')
process.exit(0)
} else if (result.status === 'timeout') {
diff --git a/cli/src/native/ripgrep.ts b/cli/src/native/ripgrep.ts
index 6ecdf84299..cb7cc27b75 100644
--- a/cli/src/native/ripgrep.ts
+++ b/cli/src/native/ripgrep.ts
@@ -1,6 +1,6 @@
import path from 'path'
-import { getBundledRgPath } from '@codebuff/sdk'
+import { getBundledRgPath } from '@codebirds/sdk'
import { spawnSync } from 'bun'
import { getCliEnv } from '../utils/env'
@@ -9,7 +9,7 @@ import { logger } from '../utils/logger'
const getRipgrepPath = async (): Promise => {
const env = getCliEnv()
// In dev mode, use the SDK's bundled ripgrep binary
- if (!env.CODEBUFF_IS_BINARY) {
+ if (!env.CODEBIRDS_IS_BINARY) {
return getBundledRgPath()
}
diff --git a/cli/src/polyfills/bun-strip-ansi.ts b/cli/src/polyfills/bun-strip-ansi.ts
index 7d33fc1986..9adf1f6a34 100644
--- a/cli/src/polyfills/bun-strip-ansi.ts
+++ b/cli/src/polyfills/bun-strip-ansi.ts
@@ -1,4 +1,4 @@
-import { stripAnsi } from '@codebuff/common/util/string'
+import { stripAnsi } from '@codebirds/common/util/string'
// Bun 1.2 removed Bun.stripANSI; provide a fallback for libraries that still call it.
const bunGlobal = globalThis as typeof globalThis & {
diff --git a/cli/src/pre-init/tree-sitter-wasm.ts b/cli/src/pre-init/tree-sitter-wasm.ts
index 3d2409d191..968784270e 100644
--- a/cli/src/pre-init/tree-sitter-wasm.ts
+++ b/cli/src/pre-init/tree-sitter-wasm.ts
@@ -14,8 +14,8 @@
//
// It's 200KB, the npm tarball already contains the binary; adding one
// more file is trivial. The build script copies the wasm into `cli/bin/`
-// after compile, the release workflow tarballs both, and the freebuff /
-// codebuff downloader extracts both into the same directory. At runtime,
+// after compile, the release workflow tarballs both, and the codebirds /
+// codebirds downloader extracts both into the same directory. At runtime,
// `process.execPath` plus a relative file lookup gets us the wasm with
// zero bundler involvement.
@@ -60,14 +60,14 @@ if (siblingPath) {
// Tell init-node.ts (in code-map / the SDK bundle) where the wasm
// is. The locateFile callback there will hand this path to
// emscripten, which fs.readFile's it.
- process.env.CODEBUFF_TREE_SITTER_WASM_PATH = siblingPath
+ process.env.CODEBIRDS_TREE_SITTER_WASM_PATH = siblingPath
// Also publish on globalThis so the smoke handler in index.tsx can
// read it without touching process.env (which is gated by the env
// architecture check outside the allowlisted pre-init files).
;(
- globalThis as { __CODEBUFF_TREE_SITTER_WASM_PATH__?: string }
- ).__CODEBUFF_TREE_SITTER_WASM_PATH__ = siblingPath
+ globalThis as { __CODEBIRDS_TREE_SITTER_WASM_PATH__?: string }
+ ).__CODEBIRDS_TREE_SITTER_WASM_PATH__ = siblingPath
// Also try the synchronous-bytes path: hand the bytes straight to
// Parser.init({ wasmBinary }) so the SDK doesn't need to round-trip
@@ -76,8 +76,8 @@ if (siblingPath) {
try {
const buf = readFileSync(siblingPath)
;(
- globalThis as { __CODEBUFF_TREE_SITTER_WASM_BINARY__?: Uint8Array }
- ).__CODEBUFF_TREE_SITTER_WASM_BINARY__ = new Uint8Array(
+ globalThis as { __CODEBIRDS_TREE_SITTER_WASM_BINARY__?: Uint8Array }
+ ).__CODEBIRDS_TREE_SITTER_WASM_BINARY__ = new Uint8Array(
buf.buffer,
buf.byteOffset,
buf.byteLength,
diff --git a/cli/src/state/chat-store.ts b/cli/src/state/chat-store.ts
index 759dce8e43..1543f0e501 100644
--- a/cli/src/state/chat-store.ts
+++ b/cli/src/state/chat-store.ts
@@ -2,14 +2,14 @@ import { castDraft } from 'immer'
import { create } from 'zustand'
import { immer } from 'zustand/middleware/immer'
-import { AGENT_MODES, IS_FREEBUFF } from '../utils/constants'
+import { AGENT_MODES, IS_CODEBIRDS } from '../utils/constants'
import { clamp } from '../utils/math'
import { loadModePreference, saveModePreference } from '../utils/settings'
import type { ChatMessage, ContentBlock } from '../types/chat'
import type { AgentMode } from '../utils/constants'
import type { InputMode } from '../utils/input-modes'
-import type { RunState } from '@codebuff/sdk'
+import type { RunState } from '@codebirds/sdk'
// Import types from the types/store module to avoid circular dependencies
import type {
@@ -185,7 +185,7 @@ const initialState: ChatStoreState = {
isChainInProgress: false,
slashSelectedIndex: 0,
agentSelectedIndex: 0,
- agentMode: IS_FREEBUFF ? ('LITE' as const) : loadModePreference(),
+ agentMode: IS_CODEBIRDS ? ('LITE' as const) : loadModePreference(),
hasReceivedPlanResponse: false,
lastMessageMode: null,
sessionCreditsUsed: 0,
@@ -272,14 +272,14 @@ export const useChatStore = create()(
setAgentMode: (mode) =>
set((state) => {
- if (IS_FREEBUFF) return
+ if (IS_CODEBIRDS) return
state.agentMode = mode
saveModePreference(mode)
}),
toggleAgentMode: () =>
set((state) => {
- if (IS_FREEBUFF) return
+ if (IS_CODEBIRDS) return
const currentIndex = AGENT_MODES.indexOf(state.agentMode)
const nextIndex = (currentIndex + 1) % AGENT_MODES.length
state.agentMode = AGENT_MODES[nextIndex]
diff --git a/cli/src/state/feedback-store.ts b/cli/src/state/feedback-store.ts
index 54d26f9ea2..50c2e124a6 100644
--- a/cli/src/state/feedback-store.ts
+++ b/cli/src/state/feedback-store.ts
@@ -1,7 +1,7 @@
import { create } from 'zustand'
import { immer } from 'zustand/middleware/immer'
-import type { FeedbackCategory } from '@codebuff/common/constants/feedback'
+import type { FeedbackCategory } from '@codebirds/common/constants/feedback'
interface FeedbackState {
feedbackMessageId: string | null
diff --git a/cli/src/state/freebuff-model-store.ts b/cli/src/state/freebuff-model-store.ts
index 01995ad48c..0c84f53d89 100644
--- a/cli/src/state/freebuff-model-store.ts
+++ b/cli/src/state/freebuff-model-store.ts
@@ -1,15 +1,15 @@
import {
- DEFAULT_FREEBUFF_MODEL_ID,
+ DEFAULT_CODEBIRDS_MODEL_ID,
resolveAvailableFreebuffModel,
resolveSupportedFreebuffModel,
-} from '@codebuff/common/constants/freebuff-models'
+} from '@codebirds/common/constants/codebirds-models'
import { create } from 'zustand'
import { loadFreebuffModelPreference } from '../utils/settings'
/**
- * Holds the user's currently-selected freebuff model. Initialized from the
- * persisted settings file so freebuff defaults to whatever model the user
+ * Holds the user's currently-selected codebirds model. Initialized from the
+ * persisted settings file so codebirds defaults to whatever model the user
* last picked.
*
* `setSelectedModel` is in-memory only — it does NOT persist. Persistence
@@ -29,7 +29,7 @@ interface FreebuffModelStore {
export const useFreebuffModelStore = create((set) => ({
selectedModel: resolveAvailableFreebuffModel(
- loadFreebuffModelPreference() ?? DEFAULT_FREEBUFF_MODEL_ID,
+ loadFreebuffModelPreference() ?? DEFAULT_CODEBIRDS_MODEL_ID,
),
setSelectedModel: (model) =>
set({ selectedModel: resolveSupportedFreebuffModel(model) }),
diff --git a/cli/src/state/freebuff-session-store.ts b/cli/src/state/freebuff-session-store.ts
index ccac166cb4..41e607f3ad 100644
--- a/cli/src/state/freebuff-session-store.ts
+++ b/cli/src/state/freebuff-session-store.ts
@@ -1,16 +1,16 @@
import { create } from 'zustand'
-import type { FreebuffSessionResponse } from '../types/freebuff-session'
+import type { FreebuffSessionResponse } from '../types/codebirds-session'
/**
- * Shared state for the freebuff waiting-room session.
+ * Shared state for the codebirds waiting-room session.
*
- * The hook in `use-freebuff-session.ts` owns the poll loop and writes into
+ * The hook in `use-codebirds-session.ts` owns the poll loop and writes into
* this store; React components subscribe via selectors, and non-React code
* reads via `useFreebuffSessionStore.getState()`.
*
* Imperative session controls (force re-POST, mark superseded/ended) live on
- * the module exports of `use-freebuff-session.ts` rather than on this store —
+ * the module exports of `use-codebirds-session.ts` rather than on this store —
* that way callers don't need to null-check a "driver" slot whose lifetime
* is tied to the React tree.
*/
diff --git a/cli/src/state/message-block-store.ts b/cli/src/state/message-block-store.ts
index e27e71d65d..6bf688bc76 100644
--- a/cli/src/state/message-block-store.ts
+++ b/cli/src/state/message-block-store.ts
@@ -1,7 +1,7 @@
import { create } from 'zustand'
import { immer } from 'zustand/middleware/immer'
-import type { FeedbackCategory } from '@codebuff/common/constants/feedback'
+import type { FeedbackCategory } from '@codebirds/common/constants/feedback'
import type { ChatMessage } from '../types/chat'
import type { ChatTheme } from '../types/theme-system'
diff --git a/cli/src/testing/env.ts b/cli/src/testing/env.ts
index 8aae6ad566..59065fc791 100644
--- a/cli/src/testing/env.ts
+++ b/cli/src/testing/env.ts
@@ -1,4 +1,4 @@
-import { createTestBaseEnv } from '@codebuff/common/testing-env-process'
+import { createTestBaseEnv } from '@codebirds/common/testing-env-process'
import type { CliEnv } from '../types/env'
@@ -32,15 +32,15 @@ export const createTestCliEnv = (overrides: Partial = {}): CliEnv => ({
JB_IDE_CONFIG_DIR: undefined,
VISUAL: undefined,
EDITOR: undefined,
- CODEBUFF_CLI_EDITOR: undefined,
- CODEBUFF_EDITOR: undefined,
+ CODEBIRDS_CLI_EDITOR: undefined,
+ CODEBIRDS_EDITOR: undefined,
OPEN_TUI_THEME: undefined,
OPENTUI_THEME: undefined,
- CODEBUFF_IS_BINARY: undefined,
- CODEBUFF_CLI_VERSION: undefined,
- CODEBUFF_CLI_TARGET: undefined,
- CODEBUFF_RG_PATH: undefined,
- CODEBUFF_SCROLL_MULTIPLIER: undefined,
- FREEBUFF_MODE: undefined,
+ CODEBIRDS_IS_BINARY: undefined,
+ CODEBIRDS_CLI_VERSION: undefined,
+ CODEBIRDS_CLI_TARGET: undefined,
+ CODEBIRDS_RG_PATH: undefined,
+ CODEBIRDS_SCROLL_MULTIPLIER: undefined,
+ CODEBIRDS_MODE: undefined,
...overrides,
})
diff --git a/cli/src/types/chat.ts b/cli/src/types/chat.ts
index 248b606550..3ce59f0acf 100644
--- a/cli/src/types/chat.ts
+++ b/cli/src/types/chat.ts
@@ -1,5 +1,5 @@
import type { ChatTheme } from './theme-system'
-import type { ToolName } from '@codebuff/sdk'
+import type { ToolName } from '@codebirds/sdk'
import type { ReactNode } from 'react'
/**
diff --git a/cli/src/types/env.ts b/cli/src/types/env.ts
index 78ec27c3be..44208cf33e 100644
--- a/cli/src/types/env.ts
+++ b/cli/src/types/env.ts
@@ -10,7 +10,7 @@
import type {
BaseEnv,
ClientEnv,
-} from '@codebuff/common/types/contracts/env'
+} from '@codebirds/common/types/contracts/env'
/**
* CLI-specific env vars for terminal/IDE detection and editor preferences.
@@ -60,24 +60,24 @@ export type CliEnv = BaseEnv & {
// Editor preferences
VISUAL?: string
EDITOR?: string
- CODEBUFF_CLI_EDITOR?: string
- CODEBUFF_EDITOR?: string
+ CODEBIRDS_CLI_EDITOR?: string
+ CODEBIRDS_EDITOR?: string
// Theme preferences
OPEN_TUI_THEME?: string
OPENTUI_THEME?: string
- // Codebuff CLI-specific (set during binary build)
- CODEBUFF_IS_BINARY?: string
- CODEBUFF_CLI_VERSION?: string
- CODEBUFF_CLI_TARGET?: string
- CODEBUFF_RG_PATH?: string
- CODEBUFF_SCROLL_MULTIPLIER?: string
- CODEBUFF_PERF_TEST?: string
- CODEBUFF_TRACE?: string
+ // Codebirds CLI-specific (set during binary build)
+ CODEBIRDS_IS_BINARY?: string
+ CODEBIRDS_CLI_VERSION?: string
+ CODEBIRDS_CLI_TARGET?: string
+ CODEBIRDS_RG_PATH?: string
+ CODEBIRDS_SCROLL_MULTIPLIER?: string
+ CODEBIRDS_PERF_TEST?: string
+ CODEBIRDS_TRACE?: string
// Toggle for mirroring CLI logs to the server's /api/logs sink (Axiom).
- CODEBUFF_SHIP_LOGS?: string
- FREEBUFF_MODE?: string
+ CODEBIRDS_SHIP_LOGS?: string
+ CODEBIRDS_MODE?: string
}
/**
diff --git a/cli/src/types/freebuff-session.ts b/cli/src/types/freebuff-session.ts
index ef6ee83afb..eafba4b335 100644
--- a/cli/src/types/freebuff-session.ts
+++ b/cli/src/types/freebuff-session.ts
@@ -1,9 +1,9 @@
-export type { FreebuffSessionServerResponse } from '@codebuff/common/types/freebuff-session'
+export type { FreebuffSessionServerResponse } from '@codebirds/common/types/codebirds-session'
-import type { FreebuffSessionServerResponse } from '@codebuff/common/types/freebuff-session'
+import type { FreebuffSessionServerResponse } from '@codebirds/common/types/codebirds-session'
/**
- * CLI session shape. Most states are wire-level `/api/v1/freebuff/session`
+ * CLI session shape. Most states are wire-level `/api/v1/codebirds/session`
* responses; `takeover_prompt` is local-only so startup can ask before POSTing
* and rotating another running CLI's instance id.
*/
diff --git a/cli/src/utils/__tests__/agent-display.test.ts b/cli/src/utils/__tests__/agent-display.test.ts
index 248a7a074a..fe2aab545c 100644
--- a/cli/src/utils/__tests__/agent-display.test.ts
+++ b/cli/src/utils/__tests__/agent-display.test.ts
@@ -47,7 +47,7 @@ describe('getAgentDisplayPrompt', () => {
test('normalizes scoped and versioned basher agent ids', () => {
const block = createAgentBlock({
- agentType: 'codebuff/basher@1.0.0',
+ agentType: 'codebirds/basher@1.0.0',
params: {
what_to_summarize: 'Summarize command output',
},
diff --git a/cli/src/utils/__tests__/analytics-client.test.ts b/cli/src/utils/__tests__/analytics-client.test.ts
index 28aec870ad..d3d5a5d3f9 100644
--- a/cli/src/utils/__tests__/analytics-client.test.ts
+++ b/cli/src/utils/__tests__/analytics-client.test.ts
@@ -1,4 +1,4 @@
-import { AnalyticsEvent } from '@codebuff/common/constants/analytics-events'
+import { AnalyticsEvent } from '@codebirds/common/constants/analytics-events'
import { describe, test, expect, beforeEach, mock } from 'bun:test'
@@ -10,7 +10,7 @@ import {
type AnalyticsDeps,
} from '../analytics'
-import type { AnalyticsClientWithIdentify } from '@codebuff/common/analytics-core'
+import type { AnalyticsClientWithIdentify } from '@codebirds/common/analytics-core'
describe('analytics with PostHog alias', () => {
diff --git a/cli/src/utils/__tests__/anonymous-id.test.ts b/cli/src/utils/__tests__/anonymous-id.test.ts
index 1e7583fabd..ed5863009c 100644
--- a/cli/src/utils/__tests__/anonymous-id.test.ts
+++ b/cli/src/utils/__tests__/anonymous-id.test.ts
@@ -22,7 +22,7 @@ describe('persistent anonymous id', () => {
beforeEach(() => {
originalHome = process.env.HOME
- tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'freebuff-anon-'))
+ tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'codebirds-anon-'))
process.env.HOME = tempHome
resetAnonymousIdCache()
})
diff --git a/cli/src/utils/__tests__/chat-history.test.ts b/cli/src/utils/__tests__/chat-history.test.ts
index 31acf47f34..d978a73296 100644
--- a/cli/src/utils/__tests__/chat-history.test.ts
+++ b/cli/src/utils/__tests__/chat-history.test.ts
@@ -40,7 +40,7 @@ function writeChat(chatId: string, prompt: string) {
describe('chat-history', () => {
beforeEach(() => {
- tempDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codebuff-history-'))
+ tempDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codebirds-history-'))
})
afterEach(() => {
diff --git a/cli/src/utils/__tests__/clipboard.test.ts b/cli/src/utils/__tests__/clipboard.test.ts
index 4bbaea0ced..99424c7eb7 100644
--- a/cli/src/utils/__tests__/clipboard.test.ts
+++ b/cli/src/utils/__tests__/clipboard.test.ts
@@ -1,6 +1,6 @@
import { execSync } from 'child_process'
-import { createMockTimers } from '@codebuff/common/testing/mocks/timers'
+import { createMockTimers } from '@codebirds/common/testing/mocks/timers'
import { describe, test, expect, beforeEach, afterEach, spyOn } from 'bun:test'
import {
@@ -14,7 +14,7 @@ import {
} from '../clipboard'
import { logger } from '../logger'
-import type { MockTimers } from '@codebuff/common/testing/mocks/timers'
+import type { MockTimers } from '@codebirds/common/testing/mocks/timers'
/**
* Tests for clipboard.ts functionality.
diff --git a/cli/src/utils/__tests__/code-search-summary.test.ts b/cli/src/utils/__tests__/code-search-summary.test.ts
index 6634496130..eef0aa4825 100644
--- a/cli/src/utils/__tests__/code-search-summary.test.ts
+++ b/cli/src/utils/__tests__/code-search-summary.test.ts
@@ -29,9 +29,9 @@ const createCodeSearcherBlock = (
status: 'complete',
params: {
searchQueries: [
- { pattern: 'FREEBUFF_MODEL_SELECTOR_MODELS' },
- { pattern: 'FREEBUFF_MODEL_SELECTOR_MODEL_IDS' },
- { pattern: 'DEFAULT_FREEBUFF_MODEL_ID' },
+ { pattern: 'CODEBIRDS_MODEL_SELECTOR_MODELS' },
+ { pattern: 'CODEBIRDS_MODEL_SELECTOR_MODEL_IDS' },
+ { pattern: 'DEFAULT_CODEBIRDS_MODEL_ID' },
],
},
blocks: [],
@@ -72,7 +72,7 @@ describe('code search summary helpers', () => {
test('handles singular labels', () => {
const agentBlock = createCodeSearcherBlock({
params: {
- searchQueries: [{ pattern: 'DEFAULT_FREEBUFF_MODEL_ID' }],
+ searchQueries: [{ pattern: 'DEFAULT_CODEBIRDS_MODEL_ID' }],
},
blocks: [createCodeSearchToolBlock('Found 1 match')],
})
diff --git a/cli/src/utils/__tests__/codebuff-api.test.ts b/cli/src/utils/__tests__/codebirds-api.test.ts
similarity index 90%
rename from cli/src/utils/__tests__/codebuff-api.test.ts
rename to cli/src/utils/__tests__/codebirds-api.test.ts
index a7847ca17e..2295cf1815 100644
--- a/cli/src/utils/__tests__/codebuff-api.test.ts
+++ b/cli/src/utils/__tests__/codebirds-api.test.ts
@@ -1,13 +1,13 @@
import { describe, test, expect, mock, beforeEach } from 'bun:test'
-import type { FeedbackRequest } from '@codebuff/common/schemas/feedback'
+import type { FeedbackRequest } from '@codebirds/common/schemas/feedback'
-import { createCodebuffApiClient } from '../codebuff-api'
+import { createCodebirdsApiClient } from '../codebirds-api'
// Type for mocked fetch function
type MockFetch = (url: string, options?: RequestInit) => Promise
-describe('createCodebuffApiClient', () => {
+describe('createCodebirdsApiClient', () => {
let mockFetch: ReturnType>
beforeEach(() => {
@@ -22,24 +22,24 @@ describe('createCodebuffApiClient', () => {
describe('client creation', () => {
test('should create client with default base URL', () => {
- const client = createCodebuffApiClient()
+ const client = createCodebirdsApiClient()
expect(client.baseUrl).toBeTruthy()
})
test('should create client with custom base URL', () => {
- const client = createCodebuffApiClient({ baseUrl: 'https://custom.api' })
+ const client = createCodebirdsApiClient({ baseUrl: 'https://custom.api' })
expect(client.baseUrl).toBe('https://custom.api')
})
test('should store auth token', () => {
- const client = createCodebuffApiClient({ authToken: 'test-token' })
+ const client = createCodebirdsApiClient({ authToken: 'test-token' })
expect(client.authToken).toBe('test-token')
})
})
describe('GET requests', () => {
test('should make GET request with correct URL', async () => {
- const client = createCodebuffApiClient({
+ const client = createCodebirdsApiClient({
baseUrl: 'https://test.api',
fetch: mockFetch as unknown as typeof fetch,
})
@@ -52,7 +52,7 @@ describe('createCodebuffApiClient', () => {
})
test('should add query parameters', async () => {
- const client = createCodebuffApiClient({
+ const client = createCodebirdsApiClient({
baseUrl: 'https://test.api',
fetch: mockFetch as unknown as typeof fetch,
})
@@ -67,7 +67,7 @@ describe('createCodebuffApiClient', () => {
})
test('should include Authorization header when authToken provided', async () => {
- const client = createCodebuffApiClient({
+ const client = createCodebirdsApiClient({
baseUrl: 'https://test.api',
authToken: 'my-token',
fetch: mockFetch as unknown as typeof fetch,
@@ -85,7 +85,7 @@ describe('createCodebuffApiClient', () => {
})
test('should not include Authorization header when includeAuth is false', async () => {
- const client = createCodebuffApiClient({
+ const client = createCodebirdsApiClient({
baseUrl: 'https://test.api',
authToken: 'my-token',
fetch: mockFetch as unknown as typeof fetch,
@@ -103,7 +103,7 @@ describe('createCodebuffApiClient', () => {
describe('POST requests', () => {
test('should make POST request with JSON body', async () => {
- const client = createCodebuffApiClient({
+ const client = createCodebirdsApiClient({
baseUrl: 'https://test.api',
fetch: mockFetch as unknown as typeof fetch,
})
@@ -122,7 +122,7 @@ describe('createCodebuffApiClient', () => {
})
test('should include Cookie header when includeCookie is true', async () => {
- const client = createCodebuffApiClient({
+ const client = createCodebirdsApiClient({
baseUrl: 'https://test.api',
authToken: 'my-token',
fetch: mockFetch as unknown as typeof fetch,
@@ -147,7 +147,7 @@ describe('createCodebuffApiClient', () => {
describe('PUT requests', () => {
test('should make PUT request with JSON body', async () => {
- const client = createCodebuffApiClient({
+ const client = createCodebirdsApiClient({
baseUrl: 'https://test.api',
fetch: mockFetch as unknown as typeof fetch,
})
@@ -167,7 +167,7 @@ describe('createCodebuffApiClient', () => {
describe('PATCH requests', () => {
test('should make PATCH request with JSON body', async () => {
- const client = createCodebuffApiClient({
+ const client = createCodebirdsApiClient({
baseUrl: 'https://test.api',
fetch: mockFetch as unknown as typeof fetch,
})
@@ -184,7 +184,7 @@ describe('createCodebuffApiClient', () => {
describe('DELETE requests', () => {
test('should make DELETE request without body', async () => {
- const client = createCodebuffApiClient({
+ const client = createCodebirdsApiClient({
baseUrl: 'https://test.api',
fetch: mockFetch as unknown as typeof fetch,
})
@@ -212,7 +212,7 @@ describe('createCodebuffApiClient', () => {
} as Response),
)
- const client = createCodebuffApiClient({
+ const client = createCodebirdsApiClient({
baseUrl: 'https://test.api',
fetch: mockSuccessFetch as unknown as typeof fetch,
})
@@ -236,7 +236,7 @@ describe('createCodebuffApiClient', () => {
} as Response),
)
- const client = createCodebuffApiClient({
+ const client = createCodebirdsApiClient({
baseUrl: 'https://test.api',
fetch: mockErrorFetch as unknown as typeof fetch,
})
@@ -261,7 +261,7 @@ describe('createCodebuffApiClient', () => {
} as unknown as Response),
)
- const client = createCodebuffApiClient({
+ const client = createCodebirdsApiClient({
baseUrl: 'https://test.api',
fetch: mockErrorFetch as unknown as typeof fetch,
})
@@ -284,7 +284,7 @@ describe('createCodebuffApiClient', () => {
} as Response),
)
- const client = createCodebuffApiClient({
+ const client = createCodebirdsApiClient({
baseUrl: 'https://test.api',
fetch: mockNoContentFetch as unknown as typeof fetch,
})
@@ -316,7 +316,7 @@ describe('createCodebuffApiClient', () => {
} as Response)
})
- const client = createCodebuffApiClient({
+ const client = createCodebirdsApiClient({
baseUrl: 'https://test.api',
fetch: mockRetryFetch as unknown as typeof fetch,
retry: {
@@ -342,7 +342,7 @@ describe('createCodebuffApiClient', () => {
} as Response),
)
- const client = createCodebuffApiClient({
+ const client = createCodebirdsApiClient({
baseUrl: 'https://test.api',
fetch: mockBadRequestFetch as unknown as typeof fetch,
retry: { maxRetries: 3, initialDelayMs: 10 },
@@ -365,7 +365,7 @@ describe('createCodebuffApiClient', () => {
} as Response),
)
- const client = createCodebuffApiClient({
+ const client = createCodebirdsApiClient({
baseUrl: 'https://test.api',
fetch: mockServerErrorFetch as unknown as typeof fetch,
retry: { maxRetries: 3 },
@@ -391,7 +391,7 @@ describe('createCodebuffApiClient', () => {
} as Response)
})
- const client = createCodebuffApiClient({
+ const client = createCodebirdsApiClient({
baseUrl: 'https://test.api',
fetch: mockNetworkErrorFetch as unknown as typeof fetch,
retry: { maxRetries: 3, initialDelayMs: 10 },
@@ -419,7 +419,7 @@ describe('createCodebuffApiClient', () => {
},
)
- const client = createCodebuffApiClient({
+ const client = createCodebirdsApiClient({
baseUrl: 'https://test.api',
fetch: mockFetchWithSignal as unknown as typeof fetch,
defaultTimeoutMs: 5000,
@@ -438,7 +438,7 @@ describe('createCodebuffApiClient', () => {
return Promise.reject(error)
})
- const client = createCodebuffApiClient({
+ const client = createCodebirdsApiClient({
baseUrl: 'https://test.api',
fetch: mockAbortFetch as unknown as typeof fetch,
})
@@ -452,7 +452,7 @@ describe('createCodebuffApiClient', () => {
describe('custom headers', () => {
test('should merge custom headers', async () => {
- const client = createCodebuffApiClient({
+ const client = createCodebirdsApiClient({
baseUrl: 'https://test.api',
authToken: 'my-token',
fetch: mockFetch as unknown as typeof fetch,
@@ -481,15 +481,15 @@ describe('createCodebuffApiClient', () => {
return Promise.reject(error)
})
- const client = createCodebuffApiClient({
- baseUrl: 'https://freebuff.com',
+ const client = createCodebirdsApiClient({
+ baseUrl: 'https://codebirds.com',
fetch: mockTlsFetch as unknown as typeof fetch,
})
await expect(
client.post('/api/auth/cli/code', { fingerprintId: 'test' }),
).rejects.toThrow(
- 'TLS certificate verification failed for https://freebuff.com.',
+ 'TLS certificate verification failed for https://codebirds.com.',
)
})
@@ -500,8 +500,8 @@ describe('createCodebuffApiClient', () => {
return Promise.reject(error)
})
- const client = createCodebuffApiClient({
- baseUrl: 'https://freebuff.com',
+ const client = createCodebirdsApiClient({
+ baseUrl: 'https://codebirds.com',
fetch: mockTlsFetch as unknown as typeof fetch,
retry: {
maxRetries: 3,
@@ -512,7 +512,7 @@ describe('createCodebuffApiClient', () => {
await expect(
client.post('/api/auth/cli/code', { fingerprintId: 'test' }),
).rejects.toThrow(
- 'TLS certificate verification failed for https://freebuff.com.',
+ 'TLS certificate verification failed for https://codebirds.com.',
)
expect(mockTlsFetch).toHaveBeenCalledTimes(1)
})
@@ -535,7 +535,7 @@ describe('createCodebuffApiClient', () => {
} as Response),
)
- const client = createCodebuffApiClient({
+ const client = createCodebirdsApiClient({
baseUrl: 'https://test.api',
fetch: mockRateLimitFetch as unknown as typeof fetch,
retry: { maxRetries: 3, initialDelayMs: 10 },
@@ -558,7 +558,7 @@ describe('createCodebuffApiClient', () => {
} as Response),
)
- const client = createCodebuffApiClient({
+ const client = createCodebirdsApiClient({
baseUrl: 'https://test.api',
fetch: mockServerErrorFetch as unknown as typeof fetch,
retry: {
diff --git a/cli/src/utils/__tests__/error-handling.test.ts b/cli/src/utils/__tests__/error-handling.test.ts
index f556550903..b0c81b8843 100644
--- a/cli/src/utils/__tests__/error-handling.test.ts
+++ b/cli/src/utils/__tests__/error-handling.test.ts
@@ -8,7 +8,7 @@ import {
getCountryBlockFromFreeModeError,
OUT_OF_CREDITS_MESSAGE,
FREE_MODE_UNAVAILABLE_MESSAGE,
- FREEBUFF_RATE_LIMIT_MESSAGE,
+ CODEBIRDS_RATE_LIMIT_MESSAGE,
createErrorMessage,
} from '../error-handling'
@@ -137,7 +137,7 @@ describe('error-handling', () => {
statusCode: 429,
message: 'Too Many Requests',
}),
- ).toBe(FREEBUFF_RATE_LIMIT_MESSAGE)
+ ).toBe(CODEBIRDS_RATE_LIMIT_MESSAGE)
})
test('returns the generic message for thrown API errors with status 429', () => {
@@ -146,7 +146,7 @@ describe('error-handling', () => {
status: 429,
message: 'Too Many Requests',
}),
- ).toBe(FREEBUFF_RATE_LIMIT_MESSAGE)
+ ).toBe(CODEBIRDS_RATE_LIMIT_MESSAGE)
})
test('returns the generic message for retry-wrapped untyped 429 errors', () => {
@@ -158,7 +158,7 @@ describe('error-handling', () => {
message: 'Too Many Requests',
},
}),
- ).toBe(FREEBUFF_RATE_LIMIT_MESSAGE)
+ ).toBe(CODEBIRDS_RATE_LIMIT_MESSAGE)
})
test('returns null for non-429 status codes', () => {
@@ -226,7 +226,7 @@ describe('error-handling', () => {
statusCode: 429,
error: 'free_mode_rate_limited',
}),
- ).toBe(FREEBUFF_RATE_LIMIT_MESSAGE)
+ ).toBe(CODEBIRDS_RATE_LIMIT_MESSAGE)
})
test('appends detail from agent-run output objects for untyped 429s', () => {
@@ -237,7 +237,7 @@ describe('error-handling', () => {
message: 'Model is at capacity. Please try again later.',
}),
).toBe(
- `${FREEBUFF_RATE_LIMIT_MESSAGE} (Model is at capacity. Please try again later.)`,
+ `${CODEBIRDS_RATE_LIMIT_MESSAGE} (Model is at capacity. Please try again later.)`,
)
})
@@ -255,7 +255,7 @@ describe('error-handling', () => {
}),
}),
).toBe(
- `${FREEBUFF_RATE_LIMIT_MESSAGE} (Model is at capacity. Please try again later.)`,
+ `${CODEBIRDS_RATE_LIMIT_MESSAGE} (Model is at capacity. Please try again later.)`,
)
})
@@ -266,7 +266,7 @@ describe('error-handling', () => {
statusCode: 429,
message: 'Too Many Requests',
}),
- ).toBe(FREEBUFF_RATE_LIMIT_MESSAGE)
+ ).toBe(CODEBIRDS_RATE_LIMIT_MESSAGE)
})
})
@@ -388,9 +388,9 @@ describe('error-handling', () => {
})
})
- describe('FREEBUFF_RATE_LIMIT_MESSAGE', () => {
+ describe('CODEBIRDS_RATE_LIMIT_MESSAGE', () => {
test('encourages retry without mentioning credits or payment', () => {
- const message = FREEBUFF_RATE_LIMIT_MESSAGE.toLowerCase()
+ const message = CODEBIRDS_RATE_LIMIT_MESSAGE.toLowerCase()
expect(message).toContain('try again')
expect(message).not.toContain('credit')
expect(message).not.toContain('pay')
diff --git a/cli/src/utils/__tests__/feedback-helpers.test.ts b/cli/src/utils/__tests__/feedback-helpers.test.ts
index 55baed122d..74fecb8efe 100644
--- a/cli/src/utils/__tests__/feedback-helpers.test.ts
+++ b/cli/src/utils/__tests__/feedback-helpers.test.ts
@@ -1,6 +1,6 @@
import { describe, expect, test } from 'bun:test'
-import { feedbackRequestSchema } from '@codebuff/common/schemas/feedback'
+import { feedbackRequestSchema } from '@codebirds/common/schemas/feedback'
import { buildFeedbackPayload, buildMessageContext, type RecentMessageSummary } from '../feedback-helpers'
diff --git a/cli/src/utils/__tests__/fetch-usage.test.ts b/cli/src/utils/__tests__/fetch-usage.test.ts
index 1b2e68f6e6..2c733f9bca 100644
--- a/cli/src/utils/__tests__/fetch-usage.test.ts
+++ b/cli/src/utils/__tests__/fetch-usage.test.ts
@@ -2,58 +2,58 @@ import { describe, test, expect, beforeEach, mock } from 'bun:test'
import { fetchAndUpdateUsage } from '../fetch-usage'
-import type { CodebuffApiClient } from '../codebuff-api'
+import type { CodebirdsApiClient } from '../codebirds-api'
import type { FetchAndUpdateUsageParams } from '../fetch-usage'
-import type { Logger } from '@codebuff/common/types/contracts/logger'
+import type { Logger } from '@codebirds/common/types/contracts/logger'
describe('fetchAndUpdateUsage (deprecated)', () => {
let setInputModeMock: ReturnType
let getAuthTokenMock: ReturnType
let loggerMock: Logger
- let apiClientMock: CodebuffApiClient
+ let apiClientMock: CodebirdsApiClient
// Note: fetch-usage now uses apiClient.usage() instead of apiClient.post()
const createMockApiClient = (
usageMock: ReturnType,
- ): CodebuffApiClient => ({
+ ): CodebirdsApiClient => ({
get: mock(() =>
Promise.resolve({ ok: true, status: 200, data: {} }),
- ) as CodebuffApiClient['get'],
+ ) as CodebirdsApiClient['get'],
post: mock(() =>
Promise.resolve({ ok: true, status: 200, data: {} }),
- ) as CodebuffApiClient['post'],
+ ) as CodebirdsApiClient['post'],
put: mock(() =>
Promise.resolve({ ok: true, status: 200, data: {} }),
- ) as CodebuffApiClient['put'],
+ ) as CodebirdsApiClient['put'],
patch: mock(() =>
Promise.resolve({ ok: true, status: 200, data: {} }),
- ) as CodebuffApiClient['patch'],
+ ) as CodebirdsApiClient['patch'],
delete: mock(() =>
Promise.resolve({ ok: true, status: 200, data: {} }),
- ) as CodebuffApiClient['delete'],
+ ) as CodebirdsApiClient['delete'],
request: mock(() =>
Promise.resolve({ ok: true, status: 200, data: {} }),
- ) as CodebuffApiClient['request'],
+ ) as CodebirdsApiClient['request'],
me: mock(() =>
Promise.resolve({ ok: true, status: 200, data: {} }),
- ) as CodebuffApiClient['me'],
- usage: usageMock as CodebuffApiClient['usage'],
+ ) as CodebirdsApiClient['me'],
+ usage: usageMock as CodebirdsApiClient['usage'],
loginCode: mock(() =>
Promise.resolve({ ok: true, status: 200, data: {} }),
- ) as CodebuffApiClient['loginCode'],
+ ) as CodebirdsApiClient['loginCode'],
loginStatus: mock(() =>
Promise.resolve({ ok: true, status: 200, data: {} }),
- ) as CodebuffApiClient['loginStatus'],
+ ) as CodebirdsApiClient['loginStatus'],
publish: mock(() =>
Promise.resolve({ ok: true, status: 200, data: {} }),
- ) as CodebuffApiClient['publish'],
+ ) as CodebirdsApiClient['publish'],
logout: mock(() =>
Promise.resolve({ ok: true, status: 200, data: {} }),
- ) as CodebuffApiClient['logout'],
+ ) as CodebirdsApiClient['logout'],
feedback: mock(() =>
Promise.resolve({ ok: true, status: 200, data: {} }),
- ) as CodebuffApiClient['feedback'],
- baseUrl: 'https://test.codebuff.com',
+ ) as CodebirdsApiClient['feedback'],
+ baseUrl: 'https://test.codebirds.com',
authToken: 'test-auth-token',
})
diff --git a/cli/src/utils/__tests__/fingerprint.test.ts b/cli/src/utils/__tests__/fingerprint.test.ts
index 12d71ddfda..9bf5b75da1 100644
--- a/cli/src/utils/__tests__/fingerprint.test.ts
+++ b/cli/src/utils/__tests__/fingerprint.test.ts
@@ -20,18 +20,18 @@ describe('fingerprint utilities', () => {
})
describe('legacy fingerprints', () => {
- test('should detect codebuff-cli- prefix as legacy', () => {
- expect(getFingerprintType('codebuff-cli-abc12345')).toBe('legacy')
+ test('should detect codebirds-cli- prefix as legacy', () => {
+ expect(getFingerprintType('codebirds-cli-abc12345')).toBe('legacy')
})
test('should detect legacy- prefix as legacy', () => {
expect(getFingerprintType('legacy-abc123-xyz789')).toBe('legacy')
})
- test('should detect codebuff-cli- prefix with any suffix', () => {
- expect(getFingerprintType('codebuff-cli-')).toBe('legacy')
- expect(getFingerprintType('codebuff-cli-randomsuffix')).toBe('legacy')
- expect(getFingerprintType('codebuff-cli-12345678')).toBe('legacy')
+ test('should detect codebirds-cli- prefix with any suffix', () => {
+ expect(getFingerprintType('codebirds-cli-')).toBe('legacy')
+ expect(getFingerprintType('codebirds-cli-randomsuffix')).toBe('legacy')
+ expect(getFingerprintType('codebirds-cli-12345678')).toBe('legacy')
})
test('should detect legacy- prefix with any suffix', () => {
@@ -52,12 +52,12 @@ describe('fingerprint utilities', () => {
test('should return unknown for partial matches', () => {
// Should not match if prefix is incomplete
expect(getFingerprintType('enhance-abc123')).toBe('unknown')
- expect(getFingerprintType('codebuff-abc123')).toBe('unknown')
+ expect(getFingerprintType('codebirds-abc123')).toBe('unknown')
expect(getFingerprintType('lega-abc123')).toBe('unknown')
})
test('should return unknown for SDK fingerprints', () => {
- expect(getFingerprintType('codebuff-sdk-abc123')).toBe('unknown')
+ expect(getFingerprintType('codebirds-sdk-abc123')).toBe('unknown')
})
test('should return unknown for random strings', () => {
@@ -69,7 +69,7 @@ describe('fingerprint utilities', () => {
test('should be case-sensitive', () => {
expect(getFingerprintType('Enhanced-abc123')).toBe('unknown')
expect(getFingerprintType('ENHANCED-abc123')).toBe('unknown')
- expect(getFingerprintType('Codebuff-cli-abc123')).toBe('unknown')
+ expect(getFingerprintType('Codebirds-cli-abc123')).toBe('unknown')
expect(getFingerprintType('LEGACY-abc123')).toBe('unknown')
})
})
@@ -77,20 +77,20 @@ describe('fingerprint utilities', () => {
describe('generateFingerprintIdSync', () => {
describe('format validation', () => {
- test('should return string starting with codebuff-cli-', () => {
+ test('should return string starting with codebirds-cli-', () => {
const fingerprint = generateFingerprintIdSync()
- expect(fingerprint.startsWith('codebuff-cli-')).toBe(true)
+ expect(fingerprint.startsWith('codebirds-cli-')).toBe(true)
})
test('should return fingerprint of expected length', () => {
const fingerprint = generateFingerprintIdSync()
- // Format: codebuff-cli- (13 chars) + 8 random chars = 21 chars
+ // Format: codebirds-cli- (13 chars) + 8 random chars = 21 chars
expect(fingerprint.length).toBe(21)
})
test('should contain only valid base64url characters in suffix', () => {
const fingerprint = generateFingerprintIdSync()
- const suffix = fingerprint.replace('codebuff-cli-', '')
+ const suffix = fingerprint.replace('codebirds-cli-', '')
// base64url alphabet: A-Z, a-z, 0-9, -, _
const base64urlPattern = /^[A-Za-z0-9_-]+$/
expect(base64urlPattern.test(suffix)).toBe(true)
@@ -98,7 +98,7 @@ describe('fingerprint utilities', () => {
test('should have exactly 8 characters in the random suffix', () => {
const fingerprint = generateFingerprintIdSync()
- const suffix = fingerprint.replace('codebuff-cli-', '')
+ const suffix = fingerprint.replace('codebirds-cli-', '')
expect(suffix.length).toBe(8)
})
})
diff --git a/cli/src/utils/__tests__/freebuff-instance-owner.test.ts b/cli/src/utils/__tests__/freebuff-instance-owner.test.ts
index d8aacaf41f..8d2861d5dc 100644
--- a/cli/src/utils/__tests__/freebuff-instance-owner.test.ts
+++ b/cli/src/utils/__tests__/freebuff-instance-owner.test.ts
@@ -6,7 +6,7 @@ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
import { ensureCliTestEnv } from '../../__tests__/test-utils'
-const OWNER_FILE = 'freebuff-instance-owner.json'
+const OWNER_FILE = 'codebirds-instance-owner.json'
ensureCliTestEnv()
@@ -14,9 +14,9 @@ const { getConfigDir } = await import('../auth')
const {
isFreebuffInstanceOwnedByDeadLocalProcess,
recordFreebuffInstanceOwner,
-} = await import('../freebuff-instance-owner')
+} = await import('../codebirds-instance-owner')
-describe('freebuff instance owner', () => {
+describe('codebirds instance owner', () => {
let originalHome: string | undefined
let tempHome: string
@@ -24,7 +24,7 @@ describe('freebuff instance owner', () => {
beforeEach(() => {
originalHome = process.env.HOME
- tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'freebuff-owner-'))
+ tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'codebirds-owner-'))
process.env.HOME = tempHome
})
diff --git a/cli/src/utils/__tests__/freebuff-model-navigation.test.ts b/cli/src/utils/__tests__/freebuff-model-navigation.test.ts
index 68157d71ae..f65c5a470b 100644
--- a/cli/src/utils/__tests__/freebuff-model-navigation.test.ts
+++ b/cli/src/utils/__tests__/freebuff-model-navigation.test.ts
@@ -1,9 +1,9 @@
import { describe, expect, test } from 'bun:test'
import {
- freebuffModelNavigationDirectionForKey,
+ codebirdsModelNavigationDirectionForKey,
nextFreebuffModelId,
-} from '../freebuff-model-navigation'
+} from '../codebirds-model-navigation'
describe('nextFreebuffModelId', () => {
test('moves to the next model when moving forward', () => {
@@ -53,50 +53,50 @@ describe('nextFreebuffModelId', () => {
})
})
-describe('freebuffModelNavigationDirectionForKey', () => {
+describe('codebirdsModelNavigationDirectionForKey', () => {
test('maps arrow keys to model navigation directions', () => {
- expect(freebuffModelNavigationDirectionForKey({ name: 'down' })).toBe(
+ expect(codebirdsModelNavigationDirectionForKey({ name: 'down' })).toBe(
'forward',
)
- expect(freebuffModelNavigationDirectionForKey({ name: 'right' })).toBe(
+ expect(codebirdsModelNavigationDirectionForKey({ name: 'right' })).toBe(
'forward',
)
- expect(freebuffModelNavigationDirectionForKey({ name: 'up' })).toBe(
+ expect(codebirdsModelNavigationDirectionForKey({ name: 'up' })).toBe(
'backward',
)
- expect(freebuffModelNavigationDirectionForKey({ name: 'left' })).toBe(
+ expect(codebirdsModelNavigationDirectionForKey({ name: 'left' })).toBe(
'backward',
)
})
test('maps tab and shift-tab to model navigation directions', () => {
- expect(freebuffModelNavigationDirectionForKey({ name: 'tab' })).toBe(
+ expect(codebirdsModelNavigationDirectionForKey({ name: 'tab' })).toBe(
'forward',
)
expect(
- freebuffModelNavigationDirectionForKey({ name: 'tab', shift: true }),
+ codebirdsModelNavigationDirectionForKey({ name: 'tab', shift: true }),
).toBe('backward')
})
test('maps terminal tab sequences to model navigation directions', () => {
- expect(freebuffModelNavigationDirectionForKey({ sequence: '\t' })).toBe(
+ expect(codebirdsModelNavigationDirectionForKey({ sequence: '\t' })).toBe(
'forward',
)
expect(
- freebuffModelNavigationDirectionForKey({ sequence: '\x1b[9u' }),
+ codebirdsModelNavigationDirectionForKey({ sequence: '\x1b[9u' }),
).toBe('forward')
expect(
- freebuffModelNavigationDirectionForKey({ sequence: '\x1b[Z' }),
+ codebirdsModelNavigationDirectionForKey({ sequence: '\x1b[Z' }),
).toBe('backward')
expect(
- freebuffModelNavigationDirectionForKey({ sequence: '\x1b[9;2u' }),
+ codebirdsModelNavigationDirectionForKey({ sequence: '\x1b[9;2u' }),
).toBe('backward')
expect(
- freebuffModelNavigationDirectionForKey({ sequence: '\x1b[27;2;9~' }),
+ codebirdsModelNavigationDirectionForKey({ sequence: '\x1b[27;2;9~' }),
).toBe('backward')
})
test('ignores non-navigation keys', () => {
- expect(freebuffModelNavigationDirectionForKey({ name: 'enter' })).toBeNull()
+ expect(codebirdsModelNavigationDirectionForKey({ name: 'enter' })).toBeNull()
})
})
diff --git a/cli/src/utils/__tests__/freebuff-premium-reset.test.ts b/cli/src/utils/__tests__/freebuff-premium-reset.test.ts
index d69021bfc0..8c7da3dcbe 100644
--- a/cli/src/utils/__tests__/freebuff-premium-reset.test.ts
+++ b/cli/src/utils/__tests__/freebuff-premium-reset.test.ts
@@ -3,9 +3,9 @@ import { describe, expect, test } from 'bun:test'
import {
formatFreebuffPremiumResetCountdown,
getFreebuffPremiumResetAt,
-} from '../freebuff-premium-reset'
+} from '../codebirds-premium-reset'
-describe('freebuff premium reset helpers', () => {
+describe('codebirds premium reset helpers', () => {
test('uses server resetAt when it is in the future', () => {
const nowMs = Date.parse('2026-05-11T20:00:00.000Z')
const resetAt = getFreebuffPremiumResetAt({
diff --git a/cli/src/utils/__tests__/freebuff-referral-cache.test.ts b/cli/src/utils/__tests__/freebuff-referral-cache.test.ts
index 3bcf067791..4d02eca883 100644
--- a/cli/src/utils/__tests__/freebuff-referral-cache.test.ts
+++ b/cli/src/utils/__tests__/freebuff-referral-cache.test.ts
@@ -4,10 +4,10 @@ import {
__resetReferralCacheForTest,
getCachedReferral,
rememberReferral,
-} from '../freebuff-referral-cache'
+} from '../codebirds-referral-cache'
-import type { FreebuffReferralInfo } from '@codebuff/common/types/freebuff-session'
-import type { FreebuffSessionResponse } from '../../types/freebuff-session'
+import type { FreebuffReferralInfo } from '@codebirds/common/types/codebirds-session'
+import type { FreebuffSessionResponse } from '../../types/codebirds-session'
const referral: FreebuffReferralInfo = {
code: 'ABC123',
@@ -31,7 +31,7 @@ const activeWithoutReferral = {
instanceId: 'i-1',
} as unknown as FreebuffSessionResponse
-describe('freebuff referral cache', () => {
+describe('codebirds referral cache', () => {
beforeEach(() => {
__resetReferralCacheForTest()
})
diff --git a/cli/src/utils/__tests__/freebuff-session-display.test.ts b/cli/src/utils/__tests__/freebuff-session-display.test.ts
index 4de1835228..03917d1a9d 100644
--- a/cli/src/utils/__tests__/freebuff-session-display.test.ts
+++ b/cli/src/utils/__tests__/freebuff-session-display.test.ts
@@ -3,9 +3,9 @@ import { describe, expect, test } from 'bun:test'
import {
formatFreebuffSessionCountdown,
formatFreebuffSessionRemaining,
-} from '../freebuff-session-display'
+} from '../codebirds-session-display'
-describe('freebuff session display formatting', () => {
+describe('codebirds session display formatting', () => {
test('formats urgent countdowns', () => {
expect(formatFreebuffSessionCountdown(61_000)).toBe('1:01')
expect(formatFreebuffSessionRemaining(61_000)).toBe('1:01 left')
diff --git a/cli/src/utils/__tests__/freebuff-streak-line.test.ts b/cli/src/utils/__tests__/freebuff-streak-line.test.ts
index 6d8dd1b529..4cbf95d639 100644
--- a/cli/src/utils/__tests__/freebuff-streak-line.test.ts
+++ b/cli/src/utils/__tests__/freebuff-streak-line.test.ts
@@ -3,7 +3,7 @@ import { describe, test, expect } from 'bun:test'
import {
getFreebuffStreakBonusNote,
getFreebuffStreakLine,
-} from '../freebuff-streak-line'
+} from '../codebirds-streak-line'
describe('getFreebuffStreakLine', () => {
test('hides the row for new / lapsed users (streak <= 0)', () => {
diff --git a/cli/src/utils/__tests__/markdown-renderer.test.tsx b/cli/src/utils/__tests__/markdown-renderer.test.tsx
index 36ea688fe6..cb8c0d3f45 100644
--- a/cli/src/utils/__tests__/markdown-renderer.test.tsx
+++ b/cli/src/utils/__tests__/markdown-renderer.test.tsx
@@ -209,7 +209,7 @@ describe('markdown renderer', () => {
test('renders code fence followed by text with quotes correctly', () => {
const markdown = `\`\`\`bash
# Start using it
-codebuff "add a new feature to handle user authentication"
+codebirds "add a new feature to handle user authentication"
\`\`\``
const output = renderMarkdown(markdown)
const nodes = flattenNodes(output)
@@ -228,7 +228,7 @@ codebuff "add a new feature to handle user authentication"
// Should contain the complete command text
expect(textContent).toContain('# Start using it')
expect(textContent).toContain(
- 'codebuff "add a new feature to handle user authentication"',
+ 'codebirds "add a new feature to handle user authentication"',
)
// Should NOT have quotes concatenated with backticks
@@ -237,7 +237,7 @@ codebuff "add a new feature to handle user authentication"
})
test('renders inline code followed by quotes correctly', () => {
- const markdown = 'Use `codebuff "fix bug"` to fix bugs.'
+ const markdown = 'Use `codebirds "fix bug"` to fix bugs.'
const output = renderMarkdown(markdown)
const nodes = flattenNodes(output)
@@ -246,12 +246,12 @@ codebuff "add a new feature to handle user authentication"
const inlineCode = nodes[1] as El
expect(inlineCode.props.fg).toBe('#86efac')
const inlineContent = flattenChildren(inlineCode.props.children).join('')
- expect(inlineContent).toContain('codebuff "fix bug"')
+ expect(inlineContent).toContain('codebirds "fix bug"')
expect(nodes[2]).toBe(' to fix bugs.')
// Verify quotes are inside the inline code, not concatenated after
- expect(inlineContent).toMatch(/codebuff\s+"fix bug"/)
+ expect(inlineContent).toMatch(/codebirds\s+"fix bug"/)
})
test('renders multiple code blocks with text between them', () => {
@@ -292,7 +292,7 @@ console.log("world")
test('renders code fence with command and quotes on same line', () => {
const markdown = `\`\`\`bash
-codebuff "implement feature" --verbose
+codebirds "implement feature" --verbose
\`\`\``
const output = renderMarkdown(markdown)
const nodes = flattenNodes(output)
@@ -308,7 +308,7 @@ codebuff "implement feature" --verbose
.join('')
// Should preserve the complete command with quotes
- expect(textContent).toContain('codebuff "implement feature" --verbose')
+ expect(textContent).toContain('codebirds "implement feature" --verbose')
expect(textContent).not.toContain('```"')
})
diff --git a/cli/src/utils/__tests__/message-block-helpers.test.ts b/cli/src/utils/__tests__/message-block-helpers.test.ts
index 55d66522bd..c844bbe891 100644
--- a/cli/src/utils/__tests__/message-block-helpers.test.ts
+++ b/cli/src/utils/__tests__/message-block-helpers.test.ts
@@ -28,7 +28,7 @@ import type {
describe('getAgentBaseName', () => {
test('extracts base name from scoped versioned name', () => {
- expect(getAgentBaseName('codebuff/file-picker@0.0.2')).toBe('file-picker')
+ expect(getAgentBaseName('codebirds/file-picker@0.0.2')).toBe('file-picker')
})
test('extracts base name from simple versioned name', () => {
@@ -44,7 +44,7 @@ describe('getAgentBaseName', () => {
})
test('handles scoped name without version', () => {
- expect(getAgentBaseName('codebuff/file-picker')).toBe('file-picker')
+ expect(getAgentBaseName('codebirds/file-picker')).toBe('file-picker')
})
test('handles empty string', () => {
diff --git a/cli/src/utils/__tests__/run-state-storage.test.ts b/cli/src/utils/__tests__/run-state-storage.test.ts
index c3237d73f2..f077da8e94 100644
--- a/cli/src/utils/__tests__/run-state-storage.test.ts
+++ b/cli/src/utils/__tests__/run-state-storage.test.ts
@@ -12,10 +12,10 @@ import {
clearChatState,
} from '../run-state-storage'
import type { ChatMessage, ContentBlock } from '../../types/chat'
-import type { RunState } from '@codebuff/sdk'
+import type { RunState } from '@codebirds/sdk'
// Mock the project-files module
-const mockProjectDataDir = path.join(os.tmpdir(), 'codebuff-test-project')
+const mockProjectDataDir = path.join(os.tmpdir(), 'codebirds-test-project')
const mockCurrentChatDir = path.join(mockProjectDataDir, 'chats', 'test-chat-123')
// Mock the module before importing
diff --git a/cli/src/utils/__tests__/sdk-event-handlers.test.ts b/cli/src/utils/__tests__/sdk-event-handlers.test.ts
index d5a6ecfbf3..58200a2df8 100644
--- a/cli/src/utils/__tests__/sdk-event-handlers.test.ts
+++ b/cli/src/utils/__tests__/sdk-event-handlers.test.ts
@@ -11,7 +11,7 @@ import type { StreamStatus } from '../../hooks/use-message-queue'
import type { AgentContentBlock, ChatMessage } from '../../types/chat'
import type { AgentMode } from '../constants'
import type { EventHandlerState } from '../sdk-event-handlers'
-import type { Logger } from '@codebuff/common/types/contracts/logger'
+import type { Logger } from '@codebirds/common/types/contracts/logger'
// Type for spawn agent info stored in the map
interface SpawnAgentInfo {
@@ -197,7 +197,7 @@ describe('sdk-event-handlers', () => {
const startEvent: SubagentStartEvent = {
type: 'subagent_start',
agentId: 'agent-real',
- agentType: 'codebuff/file-picker@1.0.0',
+ agentType: 'codebirds/file-picker@1.0.0',
displayName: 'Agent',
onlyChild: false,
parentAgentId: undefined,
diff --git a/cli/src/utils/__tests__/send-message-helpers.test.ts b/cli/src/utils/__tests__/send-message-helpers.test.ts
index 00f95b899f..3502575b7c 100644
--- a/cli/src/utils/__tests__/send-message-helpers.test.ts
+++ b/cli/src/utils/__tests__/send-message-helpers.test.ts
@@ -1315,7 +1315,7 @@ describe('createAgentBlock', () => {
describe('getAgentBaseName', () => {
test('extracts base name from scoped versioned name', () => {
- expect(getAgentBaseName('codebuff/file-picker@0.0.2')).toBe('file-picker')
+ expect(getAgentBaseName('codebirds/file-picker@0.0.2')).toBe('file-picker')
})
test('extracts base name from simple versioned name', () => {
@@ -1334,7 +1334,7 @@ describe('getAgentBaseName', () => {
describe('agentTypesMatch', () => {
test('matches same base names with different versions', () => {
expect(
- getAgentBaseName('codebuff/file-picker@0.0.2') ===
+ getAgentBaseName('codebirds/file-picker@0.0.2') ===
getAgentBaseName('file-picker@1.0.0'),
).toBe(true)
})
@@ -1542,7 +1542,7 @@ describe('createSpawnAgentBlocks', () => {
test('filters out hidden agents', () => {
const agents = [
{ agent_type: 'file-picker' },
- { agent_type: 'codebuff/context-pruner' }, // This should be hidden
+ { agent_type: 'codebirds/context-pruner' }, // This should be hidden
]
const result = createSpawnAgentBlocks('tool-1', agents)
diff --git a/cli/src/utils/__tests__/trace-writer.test.ts b/cli/src/utils/__tests__/trace-writer.test.ts
index 21467bf3c9..077d4c4a01 100644
--- a/cli/src/utils/__tests__/trace-writer.test.ts
+++ b/cli/src/utils/__tests__/trace-writer.test.ts
@@ -11,13 +11,13 @@ mock.module('../../project-files', () => ({
}))
// Force tracing on and pin the dev path so the test is deterministic
-mock.module('@codebuff/common/env', () => ({
+mock.module('@codebirds/common/env', () => ({
IS_DEV: true,
}))
import { createTraceWriter } from '../trace-writer'
-import type { Message } from '@codebuff/common/types/messages/codebuff-message'
+import type { Message } from '@codebirds/common/types/messages/codebirds-message'
function readTraceLines(): any[] {
const tracePath = path.join(tempDir, 'debug', 'trace.jsonl')
@@ -48,7 +48,7 @@ const baseParams = {
describe('createTraceWriter', () => {
beforeEach(() => {
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codebuff-trace-'))
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codebirds-trace-'))
})
afterEach(() => {
diff --git a/cli/src/utils/__tests__/trim-chat-logs.test.ts b/cli/src/utils/__tests__/trim-chat-logs.test.ts
index a93d633dcc..0051747b80 100644
--- a/cli/src/utils/__tests__/trim-chat-logs.test.ts
+++ b/cli/src/utils/__tests__/trim-chat-logs.test.ts
@@ -42,7 +42,7 @@ function writeLog(
describe('trimOversizedChatLogs', () => {
beforeEach(() => {
- tempDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codebuff-logs-'))
+ tempDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codebirds-logs-'))
})
afterEach(() => {
diff --git a/cli/src/utils/analytics.ts b/cli/src/utils/analytics.ts
index 17fd086fbd..b939caf16c 100644
--- a/cli/src/utils/analytics.ts
+++ b/cli/src/utils/analytics.ts
@@ -2,23 +2,23 @@ import {
createPostHogClient,
type AnalyticsClientWithIdentify,
type PostHogClientOptions,
-} from '@codebuff/common/analytics-core'
+} from '@codebirds/common/analytics-core'
import {
env as defaultEnv,
IS_PROD as defaultIsProd,
DEBUG_ANALYTICS,
-} from '@codebuff/common/env'
-import { shouldTrackAnalyticsEvent } from '@codebuff/common/util/analytics-sampling'
-import { shouldMirrorAnalyticsEvent } from '@codebuff/common/util/log-mirror'
+} from '@codebirds/common/env'
+import { shouldTrackAnalyticsEvent } from '@codebirds/common/util/analytics-sampling'
+import { shouldMirrorAnalyticsEvent } from '@codebirds/common/util/log-mirror'
import { getOrCreatePersistentAnonymousId } from './anonymous-id'
import { enqueueClientLog } from './log-shipper'
-import { AnalyticsEvent } from '@codebuff/common/constants/analytics-events'
+import { AnalyticsEvent } from '@codebirds/common/constants/analytics-events'
// Re-export types from core for backwards compatibility
-export type { AnalyticsClientWithIdentify as AnalyticsClient } from '@codebuff/common/analytics-core'
+export type { AnalyticsClientWithIdentify as AnalyticsClient } from '@codebirds/common/analytics-core'
export enum AnalyticsErrorStage {
Init = 'init',
diff --git a/cli/src/utils/anonymous-id.ts b/cli/src/utils/anonymous-id.ts
index 55e856b604..63c1d5a999 100644
--- a/cli/src/utils/anonymous-id.ts
+++ b/cli/src/utils/anonymous-id.ts
@@ -1,7 +1,7 @@
import fs from 'fs'
import path from 'path'
-import { generateAnonymousId } from '@codebuff/common/analytics-core'
+import { generateAnonymousId } from '@codebirds/common/analytics-core'
import { getConfigDir } from './config-dir'
diff --git a/cli/src/utils/auth.ts b/cli/src/utils/auth.ts
index affbd694c2..fdf93d900b 100644
--- a/cli/src/utils/auth.ts
+++ b/cli/src/utils/auth.ts
@@ -1,15 +1,15 @@
import fs from 'fs'
import path from 'path'
-import { getCiEnv } from '@codebuff/common/env-ci'
+import { getCiEnv } from '@codebirds/common/env-ci'
import { z } from 'zod'
-import { getApiClient, setApiClientAuthToken } from './codebuff-api'
+import { getApiClient, setApiClientAuthToken } from './codebirds-api'
import { getConfigDir as getConfigDirBase } from './config-dir'
import { logger } from './logger'
-import type { CiEnv } from '@codebuff/common/types/contracts/env'
+import type { CiEnv } from '@codebirds/common/types/contracts/env'
// User schema
const userSchema = z.object({
@@ -111,7 +111,7 @@ export const getAuthTokenDetails = (
return { token: userCredentials.authToken, source: 'credentials' }
}
- const envToken = ciEnv.CODEBUFF_API_KEY
+ const envToken = ciEnv.CODEBIRDS_API_KEY
if (envToken) {
return { token: envToken, source: 'environment' }
}
diff --git a/cli/src/utils/bash-messages.ts b/cli/src/utils/bash-messages.ts
index d06f150885..d75f15a9bf 100644
--- a/cli/src/utils/bash-messages.ts
+++ b/cli/src/utils/bash-messages.ts
@@ -2,7 +2,7 @@ import { formatTimestamp } from './helpers'
import type { PendingBashMessage } from '../types/store'
import type { ChatMessage, ContentBlock } from '../types/chat'
-import type { ToolResultOutput } from '@codebuff/common/types/messages/content-part'
+import type { ToolResultOutput } from '@codebirds/common/types/messages/content-part'
export function createRunTerminalToolResult(params: {
command: string
diff --git a/cli/src/utils/chat-scroll-accel.ts b/cli/src/utils/chat-scroll-accel.ts
index 582de735cc..0b0001c075 100644
--- a/cli/src/utils/chat-scroll-accel.ts
+++ b/cli/src/utils/chat-scroll-accel.ts
@@ -33,7 +33,7 @@ type ScrollEnvironment = {
const resolveScrollEnvironment = (
env: CliEnv = getCliEnv(),
): ScrollEnvironment => {
- let multiplier = parseFloat(env.CODEBUFF_SCROLL_MULTIPLIER ?? '')
+ let multiplier = parseFloat(env.CODEBIRDS_SCROLL_MULTIPLIER ?? '')
if (Number.isNaN(multiplier)) {
multiplier = 1
diff --git a/cli/src/utils/chatgpt-oauth.ts b/cli/src/utils/chatgpt-oauth.ts
index eb677aa26c..e05d20011a 100644
--- a/cli/src/utils/chatgpt-oauth.ts
+++ b/cli/src/utils/chatgpt-oauth.ts
@@ -11,17 +11,17 @@ import {
CHATGPT_OAUTH_CLIENT_ID,
CHATGPT_OAUTH_REDIRECT_URI,
CHATGPT_OAUTH_TOKEN_URL,
-} from '@codebuff/common/constants/chatgpt-oauth'
+} from '@codebirds/common/constants/chatgpt-oauth'
import {
clearChatGptOAuthCredentials,
getChatGptOAuthCredentials,
isChatGptOAuthValid,
resetChatGptOAuthRateLimit,
saveChatGptOAuthCredentials,
-} from '@codebuff/sdk'
+} from '@codebirds/sdk'
import { safeOpen } from './open-url'
-import type { ChatGptOAuthCredentials } from '@codebuff/sdk'
+import type { ChatGptOAuthCredentials } from '@codebirds/sdk'
function parseOAuthTokenResponse(data: unknown): {
accessToken: string
@@ -121,12 +121,12 @@ function escapeHtml(s: string): string {
}
function callbackPageHtml(success: boolean, errorMessage?: string): string {
- const title = success ? 'Connected — Codebuff' : 'Connection Failed — Codebuff'
+ const title = success ? 'Connected — Codebirds' : 'Connection Failed — Codebirds'
const heading = success ? '✓ Connected to ChatGPT' : 'Connection Failed'
const headingColor = success ? '#4ade80' : '#f87171'
const body = success
- ? 'You can close this tab and return to Codebuff.'
- : `${escapeHtml(errorMessage ?? 'Unknown error')}. Return to Codebuff and try /connect:chatgpt again.`
+ ? 'You can close this tab and return to Codebirds.'
+ : `${escapeHtml(errorMessage ?? 'Unknown error')}. Return to Codebirds and try /connect:chatgpt again.`
return `
${title}
diff --git a/cli/src/utils/clipboard-image.ts b/cli/src/utils/clipboard-image.ts
index 73c71b849d..760e3bb626 100644
--- a/cli/src/utils/clipboard-image.ts
+++ b/cli/src/utils/clipboard-image.ts
@@ -16,7 +16,7 @@ export interface ClipboardImageResult {
* Get a temp directory for clipboard images
*/
function getClipboardTempDir(): string {
- const tempDir = path.join(os.tmpdir(), 'codebuff-clipboard-images')
+ const tempDir = path.join(os.tmpdir(), 'codebirds-clipboard-images')
if (!existsSync(tempDir)) {
mkdirSync(tempDir, { recursive: true })
}
diff --git a/cli/src/utils/codebuff-api.ts b/cli/src/utils/codebirds-api.ts
similarity index 96%
rename from cli/src/utils/codebuff-api.ts
rename to cli/src/utils/codebirds-api.ts
index 0731c05658..d989a2bf5b 100644
--- a/cli/src/utils/codebuff-api.ts
+++ b/cli/src/utils/codebirds-api.ts
@@ -1,8 +1,8 @@
-import { WEBSITE_URL } from '@codebuff/sdk'
+import { WEBSITE_URL } from '@codebirds/sdk'
import type {
PublishAgentsResponse,
-} from '@codebuff/common/types/api/agents/publish'
-import type { FeedbackRequest } from '@codebuff/common/schemas/feedback'
+} from '@codebirds/common/types/api/agents/publish'
+import type { FeedbackRequest } from '@codebirds/common/schemas/feedback'
/**
* API response types for consistent error handling.
@@ -89,9 +89,9 @@ const DEFAULT_RETRY_CONFIG: Required = {
}
/**
- * Configuration for creating a Codebuff API client
+ * Configuration for creating a Codebirds API client
*/
-export interface CodebuffApiClientConfig {
+export interface CodebirdsApiClientConfig {
/** Base URL for API requests (defaults to WEBSITE_URL from SDK) */
baseUrl?: string
/** Auth token for Bearer authentication */
@@ -122,7 +122,7 @@ export interface RequestOptions {
headers?: Record
}
-export interface CodebuffApiClient {
+export interface CodebirdsApiClient {
readonly baseUrl: string
readonly authToken?: string
@@ -293,11 +293,11 @@ const isRetryableError = (error: unknown): boolean => {
}
/**
- * Create a Codebuff API client for making authenticated requests to the Codebuff API
+ * Create a Codebirds API client for making authenticated requests to the Codebirds API
*/
-export function createCodebuffApiClient(
- config: CodebuffApiClientConfig = {},
-): CodebuffApiClient {
+export function createCodebirdsApiClient(
+ config: CodebirdsApiClientConfig = {},
+): CodebirdsApiClient {
const {
baseUrl = WEBSITE_URL,
authToken,
@@ -568,7 +568,7 @@ export function createCodebuffApiClient(
// Shared singleton client
// ============================================================================
-let sharedClient: CodebuffApiClient | null = null
+let sharedClient: CodebirdsApiClient | null = null
let sharedAuthToken: string | undefined
// Track the token that was used to create the current client instance
let clientCreatedWithToken: string | undefined
@@ -581,10 +581,10 @@ let clientCreatedWithToken: string | undefined
* to ensure a specific auth token is used. The client is recreated whenever
* the auth token changes.
*/
-export function getApiClient(): CodebuffApiClient {
+export function getApiClient(): CodebirdsApiClient {
// Recreate client if it doesn't exist or if the token has changed since creation
if (!sharedClient || clientCreatedWithToken !== sharedAuthToken) {
- sharedClient = createCodebuffApiClient({ authToken: sharedAuthToken })
+ sharedClient = createCodebirdsApiClient({ authToken: sharedAuthToken })
clientCreatedWithToken = sharedAuthToken
}
return sharedClient
diff --git a/cli/src/utils/codebuff-client.ts b/cli/src/utils/codebirds-client.ts
similarity index 88%
rename from cli/src/utils/codebuff-client.ts
rename to cli/src/utils/codebirds-client.ts
index 620560246c..35a00989e6 100644
--- a/cli/src/utils/codebuff-client.ts
+++ b/cli/src/utils/codebirds-client.ts
@@ -1,6 +1,6 @@
-import { API_KEY_ENV_VAR } from '@codebuff/common/old-constants'
-import { AskUserBridge } from '@codebuff/common/utils/ask-user-bridge'
-import { CodebuffClient } from '@codebuff/sdk'
+import { API_KEY_ENV_VAR } from '@codebirds/common/old-constants'
+import { AskUserBridge } from '@codebirds/common/utils/ask-user-bridge'
+import { CodebirdsClient } from '@codebirds/sdk'
import { getAuthTokenDetails } from './auth'
import { getCliEnv, getSystemProcessEnv } from './env'
@@ -10,9 +10,9 @@ import { createTraceWriter } from './trace-writer'
import { getRgPath } from '../native/ripgrep'
import { getProjectRoot } from '../project-files'
-import type { ClientToolCall } from '@codebuff/common/tools/list'
+import type { ClientToolCall } from '@codebirds/common/tools/list'
-let clientInstance: CodebuffClient | null = null
+let clientInstance: CodebirdsClient | null = null
/**
* Recursively removes undefined values from an object to ensure clean JSON serialization.
@@ -38,14 +38,14 @@ function removeUndefinedValues(obj: T): T {
}
/**
- * Reset the cached CodebuffClient instance.
+ * Reset the cached CodebirdsClient instance.
* This should be called after login to ensure the client is re-initialized with new credentials.
*/
-export function resetCodebuffClient(): void {
+export function resetCodebirdsClient(): void {
clientInstance = null
}
-export async function getCodebuffClient(): Promise {
+export async function getCodebirdsClient(): Promise {
if (!clientInstance) {
const { token: apiKey } = getAuthTokenDetails()
@@ -61,11 +61,11 @@ export async function getCodebuffClient(): Promise {
// Set up ripgrep path for SDK to use
const env = getCliEnv()
- if (env.CODEBUFF_IS_BINARY) {
+ if (env.CODEBIRDS_IS_BINARY) {
try {
const rgPath = await getRgPath()
// Note: We still set process.env here because SDK reads from it
- getSystemProcessEnv().CODEBUFF_RG_PATH = rgPath
+ getSystemProcessEnv().CODEBIRDS_RG_PATH = rgPath
} catch (error) {
logger.error(error, 'Failed to set up ripgrep binary for SDK')
}
@@ -73,7 +73,7 @@ export async function getCodebuffClient(): Promise {
try {
const agentDefinitions = loadAgentDefinitions()
- clientInstance = new CodebuffClient({
+ clientInstance = new CodebirdsClient({
apiKey,
cwd: projectRoot,
agentDefinitions,
@@ -99,7 +99,7 @@ export async function getCodebuffClient(): Promise {
},
})
} catch (error) {
- logger.error(error, 'Failed to initialize CodebuffClient')
+ logger.error(error, 'Failed to initialize CodebirdsClient')
return null
}
}
diff --git a/cli/src/utils/config-dir.ts b/cli/src/utils/config-dir.ts
index f8b80bffbb..71ed2099a8 100644
--- a/cli/src/utils/config-dir.ts
+++ b/cli/src/utils/config-dir.ts
@@ -1,7 +1,7 @@
import os from 'os'
import path from 'path'
-import { env } from '@codebuff/common/env'
+import { env } from '@codebirds/common/env'
/**
* Resolve the on-disk config directory for the CLI.
diff --git a/cli/src/utils/constants.ts b/cli/src/utils/constants.ts
index bc1d2e59ab..756b7104ef 100644
--- a/cli/src/utils/constants.ts
+++ b/cli/src/utils/constants.ts
@@ -1,4 +1,4 @@
-import type { ToolName } from '@codebuff/sdk'
+import type { ToolName } from '@codebirds/sdk'
import { getCliEnv } from './env'
@@ -6,14 +6,14 @@ import { getCliEnv } from './env'
* Freebuff build-time flag. When true, the CLI is built as Freebuff (free-only variant).
* Injected via --define at compile time; enables dead-code elimination by the bundler.
*/
-export const IS_FREEBUFF = getCliEnv().FREEBUFF_MODE === 'true'
+export const IS_CODEBIRDS = getCliEnv().CODEBIRDS_MODE === 'true'
-/** Message shown when the user ends a freebuff session early. */
+/** Message shown when the user ends a codebirds session early. */
export const END_SESSION_MESSAGE =
'Ending session and returning to the model picker…'
// Agent IDs that should not be rendered in the CLI UI
-export const HIDDEN_AGENT_IDS = ['codebuff/context-pruner'] as const
+export const HIDDEN_AGENT_IDS = ['codebirds/context-pruner'] as const
// Tool names that should be collapsed by default when rendered
// Uses ToolName type to ensure only valid tool names are added
@@ -127,13 +127,13 @@ export const MAIN_AGENT_ID = 'main-agent'
* Mapping from agent mode to agent ID.
* Single source of truth for all agent modes (order = cycling order).
*
- * Freebuff resolves LITE through the selected freebuff model at send time;
+ * Freebuff resolves LITE through the selected codebirds model at send time;
* this fallback stays on base2-free for non-runtime callers. Regular
- * Codebuff maps LITE to base2-lite which charges credits normally.
+ * Codebirds maps LITE to base2-lite which charges credits normally.
*/
export const AGENT_MODE_TO_ID = {
DEFAULT: 'base2',
- LITE: IS_FREEBUFF ? 'base2-free' : 'base2-lite',
+ LITE: IS_CODEBIRDS ? 'base2-free' : 'base2-lite',
MAX: 'base2-max',
PLAN: 'base2-plan',
} as const
@@ -145,12 +145,12 @@ export const AGENT_MODES = Object.keys(AGENT_MODE_TO_ID) as AgentMode[]
* Maps CLI agent mode to cost mode for billing.
*
* Freebuff's LITE maps to 'free' cost mode (waiting room, rate limits, 0 credits
- * for allowlisted agent+model combos). Regular Codebuff's LITE maps to 'lite' —
+ * for allowlisted agent+model combos). Regular Codebirds's LITE maps to 'lite' —
* a normal paid mode (charges credits, no waiting room, no country restrictions).
*/
export const AGENT_MODE_TO_COST_MODE = {
DEFAULT: 'normal',
- LITE: IS_FREEBUFF ? 'free' : 'lite',
+ LITE: IS_CODEBIRDS ? 'free' : 'lite',
MAX: 'max',
PLAN: 'normal',
} as const satisfies Record<
diff --git a/cli/src/utils/create-event-handler-state.ts b/cli/src/utils/create-event-handler-state.ts
index 07d866f455..4b50b7d8b0 100644
--- a/cli/src/utils/create-event-handler-state.ts
+++ b/cli/src/utils/create-event-handler-state.ts
@@ -6,7 +6,7 @@ import type {
SetStreamStatusFn,
} from './sdk-event-handlers'
import type { StreamController } from '../hooks/stream-state'
-import type { Logger } from '@codebuff/common/types/contracts/logger'
+import type { Logger } from '@codebirds/common/types/contracts/logger'
import type { MutableRefObject } from 'react'
export type CreateEventHandlerStateParams = {
diff --git a/cli/src/utils/create-run-config.ts b/cli/src/utils/create-run-config.ts
index e37d86d7de..a0a2311950 100644
--- a/cli/src/utils/create-run-config.ts
+++ b/cli/src/utils/create-run-config.ts
@@ -1,6 +1,6 @@
import path from 'path'
-import { MAX_AGENT_STEPS_DEFAULT } from '@codebuff/common/constants/agents'
+import { MAX_AGENT_STEPS_DEFAULT } from '@codebirds/common/constants/agents'
import {
createEventHandler,
@@ -8,13 +8,13 @@ import {
} from './sdk-event-handlers'
import type { EventHandlerState } from './sdk-event-handlers'
-import type { Logger } from '@codebuff/common/types/contracts/logger'
+import type { Logger } from '@codebirds/common/types/contracts/logger'
import type {
AgentDefinition,
FileFilter,
MessageContent,
RunState,
-} from '@codebuff/sdk'
+} from '@codebirds/sdk'
export type CreateRunConfigParams = {
logger: Logger
@@ -26,7 +26,7 @@ export type CreateRunConfigParams = {
eventHandlerState: EventHandlerState
signal: AbortSignal
costMode?: 'free' | 'lite' | 'normal' | 'max' | 'experimental' | 'ask'
- extraCodebuffMetadata?: Record
+ extraCodebirdsMetadata?: Record
}
const SENSITIVE_EXTENSIONS = new Set([
@@ -103,7 +103,7 @@ export const createRunConfig = (params: CreateRunConfigParams) => {
agentDefinitions,
eventHandlerState,
costMode,
- extraCodebuffMetadata,
+ extraCodebirdsMetadata,
} = params
return {
@@ -118,7 +118,7 @@ export const createRunConfig = (params: CreateRunConfigParams) => {
handleEvent: createEventHandler(eventHandlerState),
signal: params.signal,
costMode,
- extraCodebuffMetadata,
+ extraCodebirdsMetadata,
fileFilter: ((filePath: string) => {
if (isSensitiveFile(filePath)) return { status: 'blocked' }
if (isEnvTemplateFile(filePath)) return { status: 'allow-example' }
diff --git a/cli/src/utils/engagement.ts b/cli/src/utils/engagement.ts
index f1bc0302c4..59e46b5ad5 100644
--- a/cli/src/utils/engagement.ts
+++ b/cli/src/utils/engagement.ts
@@ -1,8 +1,8 @@
-import { AnalyticsEvent } from '@codebuff/common/constants/analytics-events'
+import { AnalyticsEvent } from '@codebirds/common/constants/analytics-events'
import {
EngagementTracker,
createEngagementSessionId,
-} from '@codebuff/common/util/engagement-tracker'
+} from '@codebirds/common/util/engagement-tracker'
import { subscribeToActivity } from './activity-tracker'
import { trackEvent } from './analytics'
diff --git a/cli/src/utils/env.ts b/cli/src/utils/env.ts
index a57a334c9e..9883dd280e 100644
--- a/cli/src/utils/env.ts
+++ b/cli/src/utils/env.ts
@@ -5,7 +5,7 @@
* process env with CLI-specific vars for terminal/IDE detection.
*/
-import { getBaseEnv } from '@codebuff/common/env-process'
+import { getBaseEnv } from '@codebirds/common/env-process'
import type { CliEnv } from '../types/env'
@@ -60,23 +60,23 @@ export const getCliEnv = (): CliEnv => ({
// Editor preferences
VISUAL: process.env.VISUAL,
EDITOR: process.env.EDITOR,
- CODEBUFF_CLI_EDITOR: process.env.CODEBUFF_CLI_EDITOR,
- CODEBUFF_EDITOR: process.env.CODEBUFF_EDITOR,
+ CODEBIRDS_CLI_EDITOR: process.env.CODEBIRDS_CLI_EDITOR,
+ CODEBIRDS_EDITOR: process.env.CODEBIRDS_EDITOR,
// Theme preferences
OPEN_TUI_THEME: process.env.OPEN_TUI_THEME,
OPENTUI_THEME: process.env.OPENTUI_THEME,
// Binary build configuration
- CODEBUFF_IS_BINARY: process.env.CODEBUFF_IS_BINARY,
- CODEBUFF_CLI_VERSION: process.env.CODEBUFF_CLI_VERSION,
- CODEBUFF_CLI_TARGET: process.env.CODEBUFF_CLI_TARGET,
- CODEBUFF_RG_PATH: process.env.CODEBUFF_RG_PATH,
- CODEBUFF_SCROLL_MULTIPLIER: process.env.CODEBUFF_SCROLL_MULTIPLIER,
- CODEBUFF_PERF_TEST: process.env.CODEBUFF_PERF_TEST,
- CODEBUFF_TRACE: process.env.CODEBUFF_TRACE,
- CODEBUFF_SHIP_LOGS: process.env.CODEBUFF_SHIP_LOGS,
- FREEBUFF_MODE: process.env.FREEBUFF_MODE,
+ CODEBIRDS_IS_BINARY: process.env.CODEBIRDS_IS_BINARY,
+ CODEBIRDS_CLI_VERSION: process.env.CODEBIRDS_CLI_VERSION,
+ CODEBIRDS_CLI_TARGET: process.env.CODEBIRDS_CLI_TARGET,
+ CODEBIRDS_RG_PATH: process.env.CODEBIRDS_RG_PATH,
+ CODEBIRDS_SCROLL_MULTIPLIER: process.env.CODEBIRDS_SCROLL_MULTIPLIER,
+ CODEBIRDS_PERF_TEST: process.env.CODEBIRDS_PERF_TEST,
+ CODEBIRDS_TRACE: process.env.CODEBIRDS_TRACE,
+ CODEBIRDS_SHIP_LOGS: process.env.CODEBIRDS_SHIP_LOGS,
+ CODEBIRDS_MODE: process.env.CODEBIRDS_MODE,
})
/**
diff --git a/cli/src/utils/error-handling.ts b/cli/src/utils/error-handling.ts
index 2944941138..8bd63e75e6 100644
--- a/cli/src/utils/error-handling.ts
+++ b/cli/src/utils/error-handling.ts
@@ -1,16 +1,16 @@
-import { env } from '@codebuff/common/env'
-import { extractApiErrorDetails } from '@codebuff/common/util/error'
-import { formatFreebuffHardBlockedPrivacySignals } from '@codebuff/common/util/freebuff-privacy'
+import { env } from '@codebirds/common/env'
+import { extractApiErrorDetails } from '@codebirds/common/util/error'
+import { formatFreebuffHardBlockedPrivacySignals } from '@codebirds/common/util/codebirds-privacy'
import type { ChatMessage } from '../types/chat'
import type {
FreebuffCountryBlockReason,
FreebuffIpPrivacySignal,
-} from '@codebuff/common/types/freebuff-session'
+} from '@codebirds/common/types/codebirds-session'
-import { IS_FREEBUFF } from './constants'
+import { IS_CODEBIRDS } from './constants'
-const defaultAppUrl = env.NEXT_PUBLIC_CODEBUFF_APP_URL || 'https://codebuff.com'
+const defaultAppUrl = env.NEXT_PUBLIC_CODEBIRDS_APP_URL || 'https://codebirds.com'
// Normalize unknown errors to a user-facing string.
const extractErrorMessage = (error: unknown, fallback: string): string => {
@@ -123,7 +123,7 @@ export const getFreebuffRateLimitErrorMessage = (
if (details.errorCode === 'free_mode_rate_limited') {
// Our own rate limiter's message is already user-facing and includes the
// retry countdown — show it verbatim.
- return details.message ?? FREEBUFF_RATE_LIMIT_MESSAGE
+ return details.message ?? CODEBIRDS_RATE_LIMIT_MESSAGE
}
// Other 429s (e.g. relayed upstream capacity errors) keep the branded
// message but include the server detail so users aren't left guessing.
@@ -138,9 +138,9 @@ export const getFreebuffRateLimitErrorMessage = (
extractApiErrorDetails(error).message ??
(isRunOutputObject ? details.message : undefined)
if (detail && !/^too many requests\.?$/i.test(detail)) {
- return `${FREEBUFF_RATE_LIMIT_MESSAGE} (${detail})`
+ return `${CODEBIRDS_RATE_LIMIT_MESSAGE} (${detail})`
}
- return FREEBUFF_RATE_LIMIT_MESSAGE
+ return CODEBIRDS_RATE_LIMIT_MESSAGE
}
export const getCountryBlockFromFreeModeError = (
@@ -176,7 +176,7 @@ export const getFreeModeUnavailableErrorMessage = (
const details = getCliApiErrorDetails(error)
const block = getCountryBlockFromFreeModeError(error)
if (block?.countryBlockReason === 'anonymous_network') {
- return `${IS_FREEBUFF ? 'Freebuff' : 'Free mode'} cannot be used from ${formatFreebuffHardBlockedPrivacySignals(
+ return `${IS_CODEBIRDS ? 'Freebuff' : 'Free mode'} cannot be used from ${formatFreebuffHardBlockedPrivacySignals(
block.ipPrivacySignals,
)} traffic. Please disable it and try again.`
}
@@ -186,7 +186,7 @@ export const getFreeModeUnavailableErrorMessage = (
/**
* Freebuff waiting-room gate errors returned by /api/v1/chat/completions.
*
- * Contract (see docs/freebuff-waiting-room.md):
+ * Contract (see docs/codebirds-waiting-room.md):
* - 428 `waiting_room_required` — no session row exists; POST /session to join.
* - 429 `waiting_room_queued` — row exists but still queued.
* - 409 `session_superseded` — another CLI rotated our instance id.
@@ -200,7 +200,7 @@ export type FreebuffGateErrorKind =
| 'session_model_mismatch'
| 'session_expired'
-const FREEBUFF_GATE_STATUS: Record = {
+const CODEBIRDS_GATE_STATUS: Record = {
waiting_room_required: 428,
waiting_room_queued: 429,
session_superseded: 409,
@@ -215,17 +215,17 @@ export const getFreebuffGateErrorKind = (
const errorCode = (error as { error?: unknown }).error
const statusCode = (error as { statusCode?: unknown }).statusCode
if (typeof errorCode !== 'string') return null
- const expected = FREEBUFF_GATE_STATUS[errorCode as FreebuffGateErrorKind]
+ const expected = CODEBIRDS_GATE_STATUS[errorCode as FreebuffGateErrorKind]
if (expected === undefined || statusCode !== expected) return null
return errorCode as FreebuffGateErrorKind
}
export const OUT_OF_CREDITS_MESSAGE = `Out of credits. Please add credits at ${defaultAppUrl}/usage`
-export const FREEBUFF_RATE_LIMIT_MESSAGE =
+export const CODEBIRDS_RATE_LIMIT_MESSAGE =
'Freebuff is temporarily busy. Please try again in a moment.'
-export const FREE_MODE_UNAVAILABLE_MESSAGE = IS_FREEBUFF
+export const FREE_MODE_UNAVAILABLE_MESSAGE = IS_CODEBIRDS
? 'Freebuff is not available in your country.'
: 'Free mode is not available in your country. You can use another mode to continue.'
diff --git a/cli/src/utils/error-messages.ts b/cli/src/utils/error-messages.ts
index c883e615e3..29c4c8d851 100644
--- a/cli/src/utils/error-messages.ts
+++ b/cli/src/utils/error-messages.ts
@@ -1,4 +1,4 @@
-import { sanitizeErrorMessage, getErrorStatusCode } from '@codebuff/sdk'
+import { sanitizeErrorMessage, getErrorStatusCode } from '@codebirds/sdk'
/**
* Formats an unknown error into a user-facing markdown string.
diff --git a/cli/src/utils/feedback-helpers.ts b/cli/src/utils/feedback-helpers.ts
index 6a5e5aa34b..9420f38212 100644
--- a/cli/src/utils/feedback-helpers.ts
+++ b/cli/src/utils/feedback-helpers.ts
@@ -3,12 +3,12 @@ import {
MAX_ERROR_MESSAGE_LENGTH,
MAX_ERRORS,
MAX_RECENT_MESSAGES,
-} from '@codebuff/common/constants/feedback'
+} from '@codebirds/common/constants/feedback'
import type { ChatMessage } from '../types/chat'
-import type { FeedbackCategory } from '@codebuff/common/constants/feedback'
+import type { FeedbackCategory } from '@codebirds/common/constants/feedback'
-import type { FeedbackRequest } from '@codebuff/common/schemas/feedback'
+import type { FeedbackRequest } from '@codebirds/common/schemas/feedback'
export type RecentMessageSummary = NonNullable<
FeedbackRequest['recentMessages']
diff --git a/cli/src/utils/fetch-usage.ts b/cli/src/utils/fetch-usage.ts
index 0706876302..ad0ef3c32b 100644
--- a/cli/src/utils/fetch-usage.ts
+++ b/cli/src/utils/fetch-usage.ts
@@ -1,10 +1,10 @@
import { getAuthToken } from './auth'
-import { getApiClient } from './codebuff-api'
+import { getApiClient } from './codebirds-api'
import { logger } from './logger'
import { useChatStore } from '../state/chat-store'
-import type { CodebuffApiClient } from './codebuff-api'
-import type { Logger } from '@codebuff/common/types/contracts/logger'
+import type { CodebirdsApiClient } from './codebirds-api'
+import type { Logger } from '@codebirds/common/types/contracts/logger'
export interface FetchAndUpdateUsageParams {
showBanner?: boolean
@@ -14,7 +14,7 @@ export interface FetchAndUpdateUsageParams {
setInputMode: (mode: 'usage' | 'default') => void
}
logger?: Logger
- apiClient?: CodebuffApiClient
+ apiClient?: CodebirdsApiClient
}
/**
diff --git a/cli/src/utils/fingerprint.ts b/cli/src/utils/fingerprint.ts
index 22e974fdda..59b241aa96 100644
--- a/cli/src/utils/fingerprint.ts
+++ b/cli/src/utils/fingerprint.ts
@@ -10,7 +10,7 @@
import { createHash, randomBytes } from 'node:crypto'
import { cpus, networkInterfaces } from 'node:os'
-import { AnalyticsEvent } from '@codebuff/common/constants/analytics-events'
+import { AnalyticsEvent } from '@codebirds/common/constants/analytics-events'
import { trackEvent } from './analytics'
import { detectShell } from './detect-shell'
@@ -134,7 +134,7 @@ async function calculateEnhancedFingerprint(): Promise {
*/
function calculateLegacyFingerprint(): string {
const randomSuffix = randomBytes(6).toString('base64url').substring(0, 8)
- return `codebuff-cli-${randomSuffix}`
+ return `codebirds-cli-${randomSuffix}`
}
/**
@@ -233,7 +233,7 @@ export function getFingerprintType(
if (fingerprintId.startsWith('enhanced-')) {
return 'enhanced_cli'
}
- if (fingerprintId.startsWith('codebuff-cli-') || fingerprintId.startsWith('legacy-')) {
+ if (fingerprintId.startsWith('codebirds-cli-') || fingerprintId.startsWith('legacy-')) {
return 'legacy'
}
return 'unknown'
diff --git a/cli/src/utils/freebuff-agent-selection.ts b/cli/src/utils/freebuff-agent-selection.ts
index 094f0de0f1..afb8542612 100644
--- a/cli/src/utils/freebuff-agent-selection.ts
+++ b/cli/src/utils/freebuff-agent-selection.ts
@@ -1,10 +1,10 @@
-import { getFreebuffRootAgentIdForModel } from '@codebuff/common/constants/free-agents'
+import { getFreebuffRootAgentIdForModel } from '@codebirds/common/constants/free-agents'
-import { getSelectedFreebuffModel } from '../state/freebuff-model-store'
-import { AGENT_MODE_TO_ID, IS_FREEBUFF, type AgentMode } from './constants'
+import { getSelectedFreebuffModel } from '../state/codebirds-model-store'
+import { AGENT_MODE_TO_ID, IS_CODEBIRDS, type AgentMode } from './constants'
export function getAgentIdForMode(agentMode: AgentMode): string {
- if (IS_FREEBUFF && agentMode === 'LITE') {
+ if (IS_CODEBIRDS && agentMode === 'LITE') {
return getFreebuffRootAgentIdForModel(getSelectedFreebuffModel())
}
diff --git a/cli/src/utils/freebuff-exit.ts b/cli/src/utils/freebuff-exit.ts
index d4f919493e..5389d4202c 100644
--- a/cli/src/utils/freebuff-exit.ts
+++ b/cli/src/utils/freebuff-exit.ts
@@ -1,4 +1,4 @@
-import { endFreebuffSessionBestEffort } from '../hooks/use-freebuff-session'
+import { endFreebuffSessionBestEffort } from '../hooks/use-codebirds-session'
import { flushAnalytics } from './analytics'
import { stopEngagementTracking } from './engagement'
@@ -8,8 +8,8 @@ import { withTimeout } from './terminal-color-detection'
const EXIT_CLEANUP_TIMEOUT_MS = 1_000
/**
- * Flush analytics + release the freebuff seat (best-effort), then exit 0.
- * Shared by every freebuff-specific screen's Ctrl+C / X handler so they all
+ * Flush analytics + release the codebirds seat (best-effort), then exit 0.
+ * Shared by every codebirds-specific screen's Ctrl+C / X handler so they all
* run the same cleanup.
*/
export async function exitFreebuffCleanly(): Promise {
diff --git a/cli/src/utils/freebuff-instance-owner.ts b/cli/src/utils/freebuff-instance-owner.ts
index a15881e54f..3bd1f6d116 100644
--- a/cli/src/utils/freebuff-instance-owner.ts
+++ b/cli/src/utils/freebuff-instance-owner.ts
@@ -9,7 +9,7 @@ interface FreebuffInstanceOwner {
pid: number
}
-const OWNER_FILE = 'freebuff-instance-owner.json'
+const OWNER_FILE = 'codebirds-instance-owner.json'
const getOwnerPath = (): string => path.join(getConfigDir(), OWNER_FILE)
@@ -52,7 +52,7 @@ export function recordFreebuffInstanceOwner(instanceId: string): void {
} catch (error) {
logger.debug(
{ error: error instanceof Error ? error.message : String(error) },
- '[freebuff-session] Failed to record local owner',
+ '[codebirds-session] Failed to record local owner',
)
}
}
diff --git a/cli/src/utils/freebuff-model-navigation.ts b/cli/src/utils/freebuff-model-navigation.ts
index a866ae16af..6b63c8da11 100644
--- a/cli/src/utils/freebuff-model-navigation.ts
+++ b/cli/src/utils/freebuff-model-navigation.ts
@@ -24,7 +24,7 @@ export function nextFreebuffModelId(params: {
return modelIds[(currentIdx + step + modelIds.length) % modelIds.length]
}
-export function freebuffModelNavigationDirectionForKey(key: {
+export function codebirdsModelNavigationDirectionForKey(key: {
name?: string
shift?: boolean
sequence?: string
diff --git a/cli/src/utils/freebuff-premium-reset.ts b/cli/src/utils/freebuff-premium-reset.ts
index 7067cbdb7a..59789b55eb 100644
--- a/cli/src/utils/freebuff-premium-reset.ts
+++ b/cli/src/utils/freebuff-premium-reset.ts
@@ -1,7 +1,7 @@
-import { FREEBUFF_PREMIUM_SESSION_RESET_TIMEZONE } from '@codebuff/common/constants/freebuff-models'
-import { getZonedDayBounds } from '@codebuff/common/util/zoned-time'
+import { CODEBIRDS_PREMIUM_SESSION_RESET_TIMEZONE } from '@codebirds/common/constants/codebirds-models'
+import { getZonedDayBounds } from '@codebirds/common/util/zoned-time'
-import type { FreebuffSessionRateLimitByModel } from '@codebuff/common/types/freebuff-session'
+import type { FreebuffSessionRateLimitByModel } from '@codebirds/common/types/codebirds-session'
export function getFreebuffPremiumResetAt(params: {
rateLimitsByModel?: FreebuffSessionRateLimitByModel
@@ -22,7 +22,7 @@ export function getFreebuffPremiumResetAt(params: {
return getZonedDayBounds(
new Date(nowMs),
- FREEBUFF_PREMIUM_SESSION_RESET_TIMEZONE,
+ CODEBIRDS_PREMIUM_SESSION_RESET_TIMEZONE,
).resetsAt
}
diff --git a/cli/src/utils/freebuff-referral-cache.ts b/cli/src/utils/freebuff-referral-cache.ts
index fa9459ea7a..e0ec4a2697 100644
--- a/cli/src/utils/freebuff-referral-cache.ts
+++ b/cli/src/utils/freebuff-referral-cache.ts
@@ -1,7 +1,7 @@
-import { getReferralInfo } from '@codebuff/common/types/freebuff-session'
+import { getReferralInfo } from '@codebirds/common/types/codebirds-session'
-import type { FreebuffSessionResponse } from '../types/freebuff-session'
-import type { FreebuffReferralInfo } from '@codebuff/common/types/freebuff-session'
+import type { FreebuffSessionResponse } from '../types/codebirds-session'
+import type { FreebuffReferralInfo } from '@codebirds/common/types/codebirds-session'
/**
* Process-wide cache of the most recent referral block the server sent.
diff --git a/cli/src/utils/freebuff-session-display.ts b/cli/src/utils/freebuff-session-display.ts
index 1f5adc8fae..218d10591c 100644
--- a/cli/src/utils/freebuff-session-display.ts
+++ b/cli/src/utils/freebuff-session-display.ts
@@ -1,4 +1,4 @@
-export const FREEBUFF_COUNTDOWN_VISIBLE_MS = 5 * 60_000
+export const CODEBIRDS_COUNTDOWN_VISIBLE_MS = 5 * 60_000
export const formatFreebuffSessionCountdown = (ms: number): string => {
if (ms <= 0) return 'expiring…'
@@ -10,7 +10,7 @@ export const formatFreebuffSessionCountdown = (ms: number): string => {
export const formatFreebuffSessionRemaining = (ms: number): string => {
if (ms <= 0) return 'expiring…'
- if (ms < FREEBUFF_COUNTDOWN_VISIBLE_MS) {
+ if (ms < CODEBIRDS_COUNTDOWN_VISIBLE_MS) {
return `${formatFreebuffSessionCountdown(ms)} left`
}
const totalMinutes = Math.ceil(ms / 60_000)
diff --git a/cli/src/utils/freebuff-streak-line.ts b/cli/src/utils/freebuff-streak-line.ts
index 04d247c9a1..aec08f8ce9 100644
--- a/cli/src/utils/freebuff-streak-line.ts
+++ b/cli/src/utils/freebuff-streak-line.ts
@@ -1,8 +1,8 @@
-import { FREEBUFF_STREAK_REWARDS_ENABLED } from '@codebuff/common/constants/freebuff-models'
-import { isFreebuffStreakGlmBonusActive } from '@codebuff/common/util/freebuff-streak'
+import { CODEBIRDS_STREAK_REWARDS_ENABLED } from '@codebirds/common/constants/codebirds-models'
+import { isFreebuffStreakGlmBonusActive } from '@codebirds/common/util/codebirds-streak'
/** Days in a streak "week" — the milestone the progress dots fill toward. */
-export const FREEBUFF_STREAK_WEEK = 7
+export const CODEBIRDS_STREAK_WEEK = 7
export interface FreebuffStreakLine {
/** Count label, e.g. "2 day streak". */
@@ -26,11 +26,11 @@ export function getFreebuffStreakLine(streak: number): FreebuffStreakLine | null
// Fill toward the 7-day milestone, then stay full — a 19-day streak should
// read as fully earned, not roll back over into a partial second week. Past
// the week, a trailing "+" marks that the streak has run beyond the row.
- const filled = Math.min(streak, FREEBUFF_STREAK_WEEK)
+ const filled = Math.min(streak, CODEBIRDS_STREAK_WEEK)
const dots =
'●'.repeat(filled) +
- '○'.repeat(FREEBUFF_STREAK_WEEK - filled) +
- (streak > FREEBUFF_STREAK_WEEK ? '+' : '')
+ '○'.repeat(CODEBIRDS_STREAK_WEEK - filled) +
+ (streak > CODEBIRDS_STREAK_WEEK ? '+' : '')
// "day" stays singular — it's a compound modifier ("7 day streak"), not a
// count of days on its own.
@@ -53,8 +53,8 @@ export function getFreebuffStreakBonusNote(params: {
streak: number
accessTier: 'full' | 'limited'
}): string | null {
- if (!FREEBUFF_STREAK_REWARDS_ENABLED) return null
- if (params.streak < FREEBUFF_STREAK_WEEK) return null
+ if (!CODEBIRDS_STREAK_REWARDS_ENABLED) return null
+ if (params.streak < CODEBIRDS_STREAK_WEEK) return null
// Only advertise GLM when the full-access GLM bonus is actually active —
// mirrors what streakRewardPoolsForMilestone grants, so the copy never
// promises a perk the gate won't honor.
diff --git a/cli/src/utils/image-handler.ts b/cli/src/utils/image-handler.ts
index a0188edb7c..65e7514836 100644
--- a/cli/src/utils/image-handler.ts
+++ b/cli/src/utils/image-handler.ts
@@ -9,13 +9,13 @@ import {
MAX_TOTAL_IMAGE_SIZE,
IMAGE_EXTENSIONS_PATTERN,
getImageMimeType,
-} from '@codebuff/common/constants/images'
+} from '@codebirds/common/constants/images'
import { Jimp } from 'jimp'
import { logger } from './logger'
// Re-export all image constants for backwards compatibility
-export * from '@codebuff/common/constants/images'
+export * from '@codebirds/common/constants/images'
export interface ImageUploadResult {
success: boolean
diff --git a/cli/src/utils/image-processor.ts b/cli/src/utils/image-processor.ts
index 5a237d0ec4..4e33830903 100644
--- a/cli/src/utils/image-processor.ts
+++ b/cli/src/utils/image-processor.ts
@@ -2,7 +2,7 @@ import { extractImagePaths, processImageFile } from './image-handler'
import { logger } from './logger'
import type { PendingImageAttachment } from '../types/store'
-import type { MessageContent } from '@codebuff/sdk'
+import type { MessageContent } from '@codebirds/sdk'
// Converts pending images + inline references into SDK-ready message content.
export type ProcessedImagePart = {
diff --git a/cli/src/utils/input-modes.ts b/cli/src/utils/input-modes.ts
index d9441cdea5..7bc1a79233 100644
--- a/cli/src/utils/input-modes.ts
+++ b/cli/src/utils/input-modes.ts
@@ -1,4 +1,4 @@
-import { IS_FREEBUFF } from './constants'
+import { IS_CODEBIRDS } from './constants'
// Input mode types and configurations
// To add a new mode:
@@ -174,7 +174,7 @@ export const INPUT_MODE_CONFIGS: Record = {
}
// In Freebuff, never show the agent mode toggle
-if (IS_FREEBUFF) {
+if (IS_CODEBIRDS) {
for (const key of Object.keys(INPUT_MODE_CONFIGS) as InputMode[]) {
INPUT_MODE_CONFIGS[key].showAgentModeToggle = false
}
diff --git a/cli/src/utils/local-agent-registry.ts b/cli/src/utils/local-agent-registry.ts
index 1781e50db3..d38db30e57 100644
--- a/cli/src/utils/local-agent-registry.ts
+++ b/cli/src/utils/local-agent-registry.ts
@@ -2,22 +2,22 @@ import fs from 'fs'
import os from 'os'
import path from 'path'
-import { pluralize } from '@codebuff/common/util/string'
+import { pluralize } from '@codebirds/common/util/string'
import {
loadLocalAgents as sdkLoadLocalAgents,
loadMCPConfigSync,
-} from '@codebuff/sdk'
+} from '@codebirds/sdk'
-import type { MCPConfig } from '@codebuff/common/types/mcp'
+import type { MCPConfig } from '@codebirds/common/types/mcp'
-import { getSelectedFreebuffModel } from '../state/freebuff-model-store'
+import { getSelectedFreebuffModel } from '../state/codebirds-model-store'
import { getProjectRoot } from '../project-files'
-import { IS_FREEBUFF, type AgentMode } from './constants'
-import { getAgentIdForMode } from './freebuff-agent-selection'
+import { IS_CODEBIRDS, type AgentMode } from './constants'
+import { getAgentIdForMode } from './codebirds-agent-selection'
import { logger } from './logger'
import * as bundledAgentsModule from '../agents/bundled-agents.generated'
-import type { AgentDefinition } from '@codebuff/common/templates/initial-agents-dir/types/agent-definition'
+import type { AgentDefinition } from '@codebirds/common/templates/initial-agents-dir/types/agent-definition'
// ============================================================================
// Constants and types
@@ -29,7 +29,7 @@ export interface LocalAgentInfo {
id: string
displayName: string
filePath: string
- /** True if this is a bundled Codebuff agent (not user-created) */
+ /** True if this is a bundled Codebirds agent (not user-created) */
isBundled?: boolean
}
@@ -244,7 +244,7 @@ const cachedAgentsByMode: Map = new Map()
export const loadLocalAgents = (
currentAgentMode?: AgentMode,
): LocalAgentInfo[] => {
- const selectedFreebuffModel = IS_FREEBUFF ? getSelectedFreebuffModel() : null
+ const selectedFreebuffModel = IS_CODEBIRDS ? getSelectedFreebuffModel() : null
const cacheKey = selectedFreebuffModel
? `${currentAgentMode ?? 'all'}:${selectedFreebuffModel}`
: (currentAgentMode ?? 'all')
@@ -253,7 +253,7 @@ export const loadLocalAgents = (
return cached
}
- // Get bundled agents - these are the default Codebuff agents
+ // Get bundled agents - these are the default Codebirds agents
// compiled into the CLI binary at build time
const bundledAgentsInfo = getBundledAgentsAsLocalInfo()
const bundledAgents = getBundledAgents()
@@ -320,7 +320,7 @@ export const loadLocalAgents = (
* their custom agents without needing to modify the base agent definition.
*/
export const loadAgentDefinitions = (): AgentDefinition[] => {
- // Start with bundled agents - these are the default Codebuff agents
+ // Start with bundled agents - these are the default Codebirds agents
const bundledAgents = getBundledAgents()
const definitions: AgentDefinition[] = Object.values(bundledAgents).map(
(def) => ({ ...def }),
diff --git a/cli/src/utils/log-shipper.ts b/cli/src/utils/log-shipper.ts
index 73c4989c89..434656c75a 100644
--- a/cli/src/utils/log-shipper.ts
+++ b/cli/src/utils/log-shipper.ts
@@ -1,9 +1,9 @@
-import { IS_DEV, IS_TEST, IS_CI } from '@codebuff/common/env'
+import { IS_DEV, IS_TEST, IS_CI } from '@codebirds/common/env'
-import { getApiClient } from './codebuff-api'
+import { getApiClient } from './codebirds-api'
import { getCliEnv } from './env'
-import type { LogRecordInput } from '@codebuff/common/schemas/logs'
+import type { LogRecordInput } from '@codebirds/common/schemas/logs'
/**
* Client-side shipper that mirrors CLI logs/events into the server's Axiom
@@ -12,7 +12,7 @@ import type { LogRecordInput } from '@codebuff/common/schemas/logs'
* through the app logger (which would recurse).
*
* Tuning via env:
- * - CODEBUFF_SHIP_LOGS 'true' | 'false' (default: on outside dev/test)
+ * - CODEBIRDS_SHIP_LOGS 'true' | 'false' (default: on outside dev/test)
*/
const MAX_BATCH = 50
@@ -25,7 +25,7 @@ let flushing = false
let shutdownRegistered = false
function enabled(): boolean {
- const flag = getCliEnv().CODEBUFF_SHIP_LOGS
+ const flag = getCliEnv().CODEBIRDS_SHIP_LOGS
if (flag === 'true') return true
if (flag === 'false') return false
return !IS_DEV && !IS_TEST && !IS_CI
diff --git a/cli/src/utils/logger.ts b/cli/src/utils/logger.ts
index 045d4f0d89..c40dcae4ed 100644
--- a/cli/src/utils/logger.ts
+++ b/cli/src/utils/logger.ts
@@ -3,14 +3,14 @@ import path, { dirname } from 'path'
import { format as stringFormat } from 'util'
-import { AnalyticsEvent } from '@codebuff/common/constants/analytics-events'
-import { env, IS_DEV, IS_TEST, IS_CI } from '@codebuff/common/env'
-import { createAnalyticsDispatcher } from '@codebuff/common/util/analytics-dispatcher'
-import { getAnalyticsEventId } from '@codebuff/common/util/analytics-log'
+import { AnalyticsEvent } from '@codebirds/common/constants/analytics-events'
+import { env, IS_DEV, IS_TEST, IS_CI } from '@codebirds/common/env'
+import { createAnalyticsDispatcher } from '@codebirds/common/util/analytics-dispatcher'
+import { getAnalyticsEventId } from '@codebirds/common/util/analytics-log'
import {
isFullTelemetryEnabled,
summarizeAnalyticsValue,
-} from '@codebuff/common/util/analytics-sampling'
+} from '@codebirds/common/util/analytics-sampling'
import { pino } from 'pino'
import {
@@ -22,7 +22,7 @@ import {
import { enqueueClientLog } from './log-shipper'
import { getCurrentChatDir, getProjectRoot } from '../project-files'
-import type { LogRecordInput } from '@codebuff/common/schemas/logs'
+import type { LogRecordInput } from '@codebirds/common/schemas/logs'
/** Name of the per-chat debug log file written in production builds */
export const CHAT_LOG_FILENAME = 'log.jsonl'
diff --git a/cli/src/utils/message-block-helpers.ts b/cli/src/utils/message-block-helpers.ts
index 2d0eb29fed..96c6034492 100644
--- a/cli/src/utils/message-block-helpers.ts
+++ b/cli/src/utils/message-block-helpers.ts
@@ -1,6 +1,6 @@
import { isEqual } from 'lodash'
-import { formatToolOutput } from './codebuff-client'
+import { formatToolOutput } from './codebirds-client'
import { shouldCollapseByDefault, shouldCollapseForParent } from './constants'
import type {
@@ -13,7 +13,7 @@ import type {
* Extracts the base agent name from a potentially scoped/versioned agent type string.
*
* @example
- * getAgentBaseName('codebuff/file-picker@0.0.2') // 'file-picker'
+ * getAgentBaseName('codebirds/file-picker@0.0.2') // 'file-picker'
* getAgentBaseName('file-picker@1.0.0') // 'file-picker'
* getAgentBaseName('file-picker') // 'file-picker'
* getAgentBaseName('file_picker') // 'file-picker'
diff --git a/cli/src/utils/open-file.ts b/cli/src/utils/open-file.ts
index c565a8d1b2..6a764a68a7 100644
--- a/cli/src/utils/open-file.ts
+++ b/cli/src/utils/open-file.ts
@@ -35,8 +35,8 @@ const buildEditorCommands = (
// Check custom editor env vars
const editorValues = [
- env.CODEBUFF_CLI_EDITOR,
- env.CODEBUFF_EDITOR,
+ env.CODEBIRDS_CLI_EDITOR,
+ env.CODEBIRDS_EDITOR,
env.VISUAL,
env.EDITOR,
]
diff --git a/cli/src/utils/run-state-storage.ts b/cli/src/utils/run-state-storage.ts
index 8ca9168127..076d781c17 100644
--- a/cli/src/utils/run-state-storage.ts
+++ b/cli/src/utils/run-state-storage.ts
@@ -10,7 +10,7 @@ import {
import { logger } from './logger'
import type { ChatMessage, ContentBlock } from '../types/chat'
-import type { RunState } from '@codebuff/sdk'
+import type { RunState } from '@codebirds/sdk'
const RUN_STATE_FILENAME = 'run-state.json'
const CHAT_MESSAGES_FILENAME = 'chat-messages.json'
diff --git a/cli/src/utils/sdk-event-handlers.ts b/cli/src/utils/sdk-event-handlers.ts
index 4cfdf5df0a..faa81f8d99 100644
--- a/cli/src/utils/sdk-event-handlers.ts
+++ b/cli/src/utils/sdk-event-handlers.ts
@@ -32,7 +32,7 @@ import type { MessageUpdater } from './message-updater'
import type { StreamController } from '../hooks/stream-state'
import type { StreamStatus } from '../hooks/use-message-queue'
import type { ContentBlock, ToolContentBlock } from '../types/chat'
-import type { Logger } from '@codebuff/common/types/contracts/logger'
+import type { Logger } from '@codebirds/common/types/contracts/logger'
import type {
PrintModeEvent as SDKEvent,
PrintModeFinish,
@@ -40,8 +40,8 @@ import type {
PrintModeSubagentStart,
PrintModeToolCall,
PrintModeToolResult,
-} from '@codebuff/common/types/print-mode'
-import type { ToolName } from '@codebuff/sdk'
+} from '@codebirds/common/types/print-mode'
+import type { ToolName } from '@codebirds/sdk'
import type { MutableRefObject } from 'react'
export type SetStreamingAgentsFn = (
diff --git a/cli/src/utils/settings.ts b/cli/src/utils/settings.ts
index b2f28d55b1..2c5aed793b 100644
--- a/cli/src/utils/settings.ts
+++ b/cli/src/utils/settings.ts
@@ -1,7 +1,7 @@
import fs from 'fs'
import path from 'path'
-import { isSupportedFreebuffModelId } from '@codebuff/common/constants/freebuff-models'
+import { isSupportedFreebuffModelId } from '@codebirds/common/constants/codebirds-models'
import { getConfigDir } from './auth'
import { AGENT_MODES } from './constants'
@@ -22,10 +22,10 @@ const DEFAULT_SETTINGS: Settings = {
export interface Settings {
mode?: AgentMode
adsEnabled?: boolean
- /** Last model the user picked in the freebuff model selector. Restored on
- * next freebuff launch so users land in the queue for their preferred
+ /** Last model the user picked in the codebirds model selector. Restored on
+ * next codebirds launch so users land in the queue for their preferred
* model without re-picking. Persisted as the canonical model id. */
- freebuffModel?: string
+ codebirdsModel?: string
/** @deprecated Use server-side fallbackToALaCarte setting instead */
alwaysUseALaCarte?: boolean
/** @deprecated Use server-side fallbackToALaCarte setting instead */
@@ -106,14 +106,14 @@ const validateSettings = (parsed: unknown): Settings => {
settings.adsEnabled = obj.adsEnabled
}
- // Validate freebuffModel — drop unknown ids so a removed model doesn't
+ // Validate codebirdsModel — drop unknown ids so a removed model doesn't
// strand the user on a non-existent queue. Hidden-but-supported models are
// kept; access-tier resolution decides whether they are selectable.
if (
- typeof obj.freebuffModel === 'string' &&
- isSupportedFreebuffModelId(obj.freebuffModel)
+ typeof obj.codebirdsModel === 'string' &&
+ isSupportedFreebuffModelId(obj.codebirdsModel)
) {
- settings.freebuffModel = obj.freebuffModel
+ settings.codebirdsModel = obj.codebirdsModel
}
// Validate alwaysUseALaCarte (legacy)
@@ -175,19 +175,19 @@ export const saveModePreference = (mode: AgentMode): void => {
}
/**
- * Load the saved freebuff model preference. Returns undefined if none is
- * saved yet — callers should fall back to DEFAULT_FREEBUFF_MODEL_ID.
+ * Load the saved codebirds model preference. Returns undefined if none is
+ * saved yet — callers should fall back to DEFAULT_CODEBIRDS_MODEL_ID.
*/
export const loadFreebuffModelPreference = (): string | undefined => {
- return loadSettings().freebuffModel
+ return loadSettings().codebirdsModel
}
/**
- * Save the freebuff model preference. Called whenever the user picks a model
+ * Save the codebirds model preference. Called whenever the user picks a model
* in the waiting room so the next launch defaults to it.
*/
export const saveFreebuffModelPreference = (model: string): void => {
- saveSettings({ freebuffModel: model })
+ saveSettings({ codebirdsModel: model })
}
/**
diff --git a/cli/src/utils/skill-registry.ts b/cli/src/utils/skill-registry.ts
index 8cc8e8480e..4c545b5730 100644
--- a/cli/src/utils/skill-registry.ts
+++ b/cli/src/utils/skill-registry.ts
@@ -1,9 +1,9 @@
-import { loadSkills as sdkLoadSkills } from '@codebuff/sdk'
+import { loadSkills as sdkLoadSkills } from '@codebirds/sdk'
import { getProjectRoot } from '../project-files'
import { logger } from './logger'
-import type { SkillDefinition, SkillsMap } from '@codebuff/common/types/skill'
+import type { SkillDefinition, SkillsMap } from '@codebirds/common/types/skill'
// ============================================================================
// Skills cache (loaded via SDK at startup)
diff --git a/cli/src/utils/terminal-title.ts b/cli/src/utils/terminal-title.ts
index f77943f2e0..3c2439d935 100644
--- a/cli/src/utils/terminal-title.ts
+++ b/cli/src/utils/terminal-title.ts
@@ -11,11 +11,11 @@
import { closeSync, constants, openSync, writeSync } from 'fs'
-import { IS_FREEBUFF } from './constants'
+import { IS_CODEBIRDS } from './constants'
import { getCliEnv } from './env'
const MAX_TITLE_LENGTH = 60
-const TITLE_PREFIX = IS_FREEBUFF ? 'Freebuff: ' : 'Codebuff: '
+const TITLE_PREFIX = IS_CODEBIRDS ? 'Freebuff: ' : 'Codebirds: '
const OSC_TERMINATOR = '\x07' // BEL
function isInTmux(env: ReturnType): boolean {
diff --git a/cli/src/utils/time-format.ts b/cli/src/utils/time-format.ts
index e7b4723602..638404c429 100644
--- a/cli/src/utils/time-format.ts
+++ b/cli/src/utils/time-format.ts
@@ -1,4 +1,4 @@
-import { formatTimeUntil } from '@codebuff/common/util/dates'
+import { formatTimeUntil } from '@codebirds/common/util/dates'
/**
* Format time until reset in human-readable form.
diff --git a/cli/src/utils/trace-writer.ts b/cli/src/utils/trace-writer.ts
index db02fa07eb..5b193bb067 100644
--- a/cli/src/utils/trace-writer.ts
+++ b/cli/src/utils/trace-writer.ts
@@ -1,13 +1,13 @@
import { appendFileSync, mkdirSync } from 'fs'
import path, { dirname } from 'path'
-import { IS_DEV } from '@codebuff/common/env'
+import { IS_DEV } from '@codebirds/common/env'
import { getCliEnv } from './env'
import { getCurrentChatDir, getProjectRoot } from '../project-files'
-import type { TraceWriter } from '@codebuff/common/types/contracts/trace'
-import type { Message } from '@codebuff/common/types/messages/codebuff-message'
+import type { TraceWriter } from '@codebirds/common/types/contracts/trace'
+import type { Message } from '@codebirds/common/types/messages/codebirds-message'
const TRACE_FILENAME = 'trace.jsonl'
@@ -20,13 +20,13 @@ type AgentTraceState = {
/**
* Returns whether trace writing is enabled: always in dev, opt-in via
- * CODEBUFF_TRACE=1 in production builds (so users don't pay disk for a second
+ * CODEBIRDS_TRACE=1 in production builds (so users don't pay disk for a second
* copy of their conversation by default).
*/
export function isTraceEnabled(): boolean {
if (IS_DEV) return true
- // Same accepted values as CODEBUFF_FULL_TELEMETRY (see analytics-sampling)
- const flag = getCliEnv().CODEBUFF_TRACE
+ // Same accepted values as CODEBIRDS_FULL_TELEMETRY (see analytics-sampling)
+ const flag = getCliEnv().CODEBIRDS_TRACE
return flag === '1' || flag === 'true' || flag === 'yes'
}
diff --git a/cli/tsconfig.json b/cli/tsconfig.json
index 127c0f0f1c..212470e2a4 100644
--- a/cli/tsconfig.json
+++ b/cli/tsconfig.json
@@ -14,7 +14,7 @@
"preserveSymlinks": false,
"baseUrl": ".",
"paths": {
- "@codebuff/sdk": ["../sdk/src/index.ts"]
+ "@codebirds/sdk": ["../sdk/src/index.ts"]
}
},
"include": ["src/**/*"],
diff --git a/freebuff/README.md b/codebirds/README.md
similarity index 50%
rename from freebuff/README.md
rename to codebirds/README.md
index dee2eef333..0e1bae6523 100644
--- a/freebuff/README.md
+++ b/codebirds/README.md
@@ -1,40 +1,40 @@
-# Freebuff
+# Codebirds
**The free coding agent.** No subscription. No configuration. Start in seconds.
-An AI coding agent that runs in your terminal — describe what you want, and Freebuff edits your code.
+An AI coding agent that runs in your terminal — describe what you want, and Codebirds edits your code.
## Install
```bash
-npm install -g freebuff
+npm install -g codebirds
```
## Usage
```bash
cd ~/my-project
-freebuff
+codebirds
```
## Project Structure
```
-freebuff/
+codebirds/
├── cli/ # CLI build & npm release files
-└── web/ # Freebuff website
+└── web/ # Codebirds website
```
## Building from Source
```bash
# From the repo root
-bun freebuff/cli/build.ts 1.0.0
+bun codebirds/cli/build.ts 1.0.0
```
---
-For everything else — what Freebuff does, how it works, FAQ, and how it relates to Codebuff — see the [repo root README](../README.md). We keep that one up to date as the single source of truth.
+For everything else — what Codebirds does, how it works, FAQ, and more — see the [repo root README](../README.md). We keep that one up to date as the single source of truth.
## License
diff --git a/freebuff/SPEC.md b/codebirds/SPEC.md
similarity index 66%
rename from freebuff/SPEC.md
rename to codebirds/SPEC.md
index 134cd471c7..623705bfd0 100644
--- a/freebuff/SPEC.md
+++ b/codebirds/SPEC.md
@@ -1,6 +1,6 @@
# Freebuff Spec
-Freebuff is a free-only variant of the Codebuff CLI, distributed as a separate npm package (`freebuff`). It reuses the entire `cli/` package but builds with a compile-time flag that strips out paid features, subscription logic, credits display, and mode switching — leaving only the FREE mode experience.
+Freebuff is a free-only variant of the Codebirds CLI, distributed as a separate npm package (`codebirds`). It reuses the entire `cli/` package but builds with a compile-time flag that strips out paid features, subscription logic, credits display, and mode switching — leaving only the FREE mode experience.
---
@@ -8,41 +8,41 @@ Freebuff is a free-only variant of the Codebuff CLI, distributed as a separate n
### Environment Variable
-- **`FREEBUFF_MODE=true`** — set during the build to produce a Freebuff binary.
-- Injected via `--define process.env.FREEBUFF_MODE="true"` in `bun build`, following the same pattern as `CODEBUFF_IS_BINARY` and `CODEBUFF_CLI_VERSION`.
+- **`CODEBIRDS_MODE=true`** — set during the build to produce a Freebuff binary.
+- Injected via `--define process.env.CODEBIRDS_MODE="true"` in `bun build`, following the same pattern as `CODEBIRDS_IS_BINARY` and `CODEBIRDS_CLI_VERSION`.
### Runtime Constant
Create a shared constant in `cli/src/utils/constants.ts`:
```ts
-export const IS_FREEBUFF = process.env.FREEBUFF_MODE === 'true'
+export const IS_CODEBIRDS = process.env.CODEBIRDS_MODE === 'true'
```
-This enables dead-code elimination in production builds — all `if (!IS_FREEBUFF)` branches are removed by the bundler.
+This enables dead-code elimination in production builds — all `if (!IS_CODEBIRDS)` branches are removed by the bundler.
---
## 2. Branding Changes
-| Area | Codebuff | Freebuff |
+| Area | Codebirds | Freebuff |
| --------------------- | -------------------------------------------------------------- | -------------------------------------------------------------- |
-| Terminal title prefix | `Codebuff: ` | `Freebuff: ` |
-| CLI commander name | `codebuff` | `freebuff` |
-| npm package name | `codebuff` | `freebuff` |
-| Binary name | `codebuff` | `freebuff` |
-| App header text | "Codebuff will run commands on your behalf to help you build." | "Freebuff will run commands on your behalf to help you build." |
-| ASCII logo | `CODEBUFF` block letters | `FREEBUFF` block letters (new logo) |
+| Terminal title prefix | `Codebirds: ` | `Freebuff: ` |
+| CLI commander name | `codebirds` | `codebirds` |
+| npm package name | `codebirds` | `codebirds` |
+| Binary name | `codebirds` | `codebirds` |
+| App header text | "Codebirds will run commands on your behalf to help you build." | "Freebuff will run commands on your behalf to help you build." |
+| ASCII logo | `CODEBUFF` block letters | `CODEBIRDS` block letters (new logo) |
| Description | "AI coding agent" | "Free AI coding assistant" |
-| Homepage | codebuff.com | codebuff.com/free (or same) |
-| `WEBSITE_URL` usage | Points to codebuff.com | Same (login, feedback, etc. stay on codebuff.com) |
+| Homepage | codebirds.com | codebirds.com/free (or same) |
+| `WEBSITE_URL` usage | Points to codebirds.com | Same (login, feedback, etc. stay on codebirds.com) |
-### Files to modify (conditional on `IS_FREEBUFF`)
+### Files to modify (conditional on `IS_CODEBIRDS`)
-- **`cli/src/utils/terminal-title.ts`** — Change `TITLE_PREFIX` from `'Codebuff: '` to `'Freebuff: '` when `IS_FREEBUFF`.
-- **`cli/src/login/constants.ts`** — Add a `LOGO_FREEBUFF` ASCII art variant, select based on `IS_FREEBUFF`.
+- **`cli/src/utils/terminal-title.ts`** — Change `TITLE_PREFIX` from `'Codebirds: '` to `'Freebuff: '` when `IS_CODEBIRDS`.
+- **`cli/src/login/constants.ts`** — Add a `LOGO_CODEBIRDS` ASCII art variant, select based on `IS_CODEBIRDS`.
- **`cli/src/app.tsx`** — Conditional header text ("Freebuff will run commands...").
-- **`cli/src/index.tsx`** — Change commander `.name('freebuff')` and `.description(...)` when `IS_FREEBUFF`.
+- **`cli/src/index.tsx`** — Change commander `.name('codebirds')` and `.description(...)` when `IS_CODEBIRDS`.
---
@@ -58,13 +58,13 @@ Freebuff only supports **FREE mode**. All mode-related features are stripped.
### Files to modify
-- **`cli/src/utils/constants.ts`** — When `IS_FREEBUFF`, export a single-element `AGENT_MODES = ['FREE']` and `AGENT_MODE_TO_ID` with only the FREE entry. Or: the mode toggle component simply never renders.
-- **`cli/src/components/agent-mode-toggle.tsx`** — Return `null` when `IS_FREEBUFF` (hide entirely).
-- **`cli/src/components/build-mode-buttons.tsx`** — Return `null` when `IS_FREEBUFF` (hides mode-switching buttons in message UI).
-- **`cli/src/components/mode-divider.tsx`** — Return `null` when `IS_FREEBUFF` (no mode transition markers).
-- **`cli/src/utils/input-modes.ts`** — Set `showAgentModeToggle: false` for all input mode configs when `IS_FREEBUFF`.
-- **`cli/src/index.tsx`** — Remove `--free`, `--max`, `--plan`, `--lite` CLI flags when `IS_FREEBUFF`; hardcode `initialMode = 'FREE'`.
-- **`cli/src/state/chat-store.ts`** — Default `agentMode` to `'FREE'`; make `setAgentMode` a no-op when `IS_FREEBUFF`.
+- **`cli/src/utils/constants.ts`** — When `IS_CODEBIRDS`, export a single-element `AGENT_MODES = ['FREE']` and `AGENT_MODE_TO_ID` with only the FREE entry. Or: the mode toggle component simply never renders.
+- **`cli/src/components/agent-mode-toggle.tsx`** — Return `null` when `IS_CODEBIRDS` (hide entirely).
+- **`cli/src/components/build-mode-buttons.tsx`** — Return `null` when `IS_CODEBIRDS` (hides mode-switching buttons in message UI).
+- **`cli/src/components/mode-divider.tsx`** — Return `null` when `IS_CODEBIRDS` (no mode transition markers).
+- **`cli/src/utils/input-modes.ts`** — Set `showAgentModeToggle: false` for all input mode configs when `IS_CODEBIRDS`.
+- **`cli/src/index.tsx`** — Remove `--free`, `--max`, `--plan`, `--lite` CLI flags when `IS_CODEBIRDS`; hardcode `initialMode = 'FREE'`.
+- **`cli/src/state/chat-store.ts`** — Default `agentMode` to `'FREE'`; make `setAgentMode` a no-op when `IS_CODEBIRDS`.
---
@@ -103,8 +103,8 @@ Freebuff only supports **FREE mode**. All mode-related features are stripped.
### Implementation
-- **`cli/src/data/slash-commands.ts`** — Filter `SLASH_COMMANDS` based on `IS_FREEBUFF`. Remove mode commands, subscription commands, credits commands, ads commands, referral, review, publish, and gpt-5 agent commands.
-- **`cli/src/commands/command-registry.ts`** — Filter `COMMAND_REGISTRY` similarly. Wrap removed commands in `!IS_FREEBUFF` guards.
+- **`cli/src/data/slash-commands.ts`** — Filter `SLASH_COMMANDS` based on `IS_CODEBIRDS`. Remove mode commands, subscription commands, credits commands, ads commands, referral, review, publish, and gpt-5 agent commands.
+- **`cli/src/commands/command-registry.ts`** — Filter `COMMAND_REGISTRY` similarly. Wrap removed commands in `!IS_CODEBIRDS` guards.
---
@@ -112,7 +112,7 @@ Freebuff only supports **FREE mode**. All mode-related features are stripped.
Freebuff never displays credits, usage, subscription info, or out-of-credits states.
-### Components to suppress (render `null` when `IS_FREEBUFF`)
+### Components to suppress (render `null` when `IS_CODEBIRDS`)
| Component | File | Behavior |
| -------------------------- | ------------------------------------------ | ------------------------------------------------------------------------ |
@@ -125,7 +125,7 @@ Freebuff never displays credits, usage, subscription info, or out-of-credits sta
### Input modes to disable
-When `IS_FREEBUFF`, these input modes should be unreachable:
+When `IS_CODEBIRDS`, these input modes should be unreachable:
- `outOfCredits` — never triggered
- `subscriptionLimit` — never triggered
@@ -135,9 +135,9 @@ When `IS_FREEBUFF`, these input modes should be unreachable:
### Hooks to disable/skip
-- **`use-usage-monitor.ts`** — Return early when `IS_FREEBUFF` (no credits to monitor).
-- **`use-subscription-query.ts`** — Return empty/disabled when `IS_FREEBUFF`.
-- **`use-claude-quota-query.ts`** — Return empty/disabled when `IS_FREEBUFF`.
+- **`use-usage-monitor.ts`** — Return early when `IS_CODEBIRDS` (no credits to monitor).
+- **`use-subscription-query.ts`** — Return empty/disabled when `IS_CODEBIRDS`.
+- **`use-claude-quota-query.ts`** — Return empty/disabled when `IS_CODEBIRDS`.
- **`use-usage-query.ts`** — Still needed for server-side billing, but UI never shows it.
### Session credits tracking
@@ -171,7 +171,7 @@ No "Credits" section. No `/subscribe`, `/usage`, or `/ads:enable` references.
### File to modify
-- **`cli/src/components/help-banner.tsx`** — Conditionally hide the Credits section when `IS_FREEBUFF`.
+- **`cli/src/components/help-banner.tsx`** — Conditionally hide the Credits section when `IS_CODEBIRDS`.
---
@@ -182,12 +182,12 @@ In Freebuff, ads are **always enabled** and **cannot be disabled**.
- The ad banner always renders (when an ad is available).
- The "Hide ads" link in the info panel is replaced with "Ads are required in Free mode." (this already exists in `ad-banner.tsx` when `isFreeMode` is true).
- The `/ads:enable` and `/ads:disable` commands are removed (see §4).
-- `getAdsEnabled()` always returns `true` when `IS_FREEBUFF`.
+- `getAdsEnabled()` always returns `true` when `IS_CODEBIRDS`.
### Files to modify
-- **`cli/src/commands/ads.ts`** — `getAdsEnabled()` returns `true` unconditionally when `IS_FREEBUFF`.
-- **`cli/src/chat.tsx`** — Skip the `!hasSubscription` guard for ads when `IS_FREEBUFF`; always show.
+- **`cli/src/commands/ads.ts`** — `getAdsEnabled()` returns `true` unconditionally when `IS_CODEBIRDS`.
+- **`cli/src/chat.tsx`** — Skip the `!hasSubscription` guard for ads when `IS_CODEBIRDS`; always show.
---
@@ -195,75 +195,75 @@ In Freebuff, ads are **always enabled** and **cannot be disabled**.
### Directory Structure
-The `freebuff/` directory is organized as a product-level directory with subdirectories for each surface (CLI, web, etc.):
+The `codebirds/` directory is organized as a product-level directory with subdirectories for each surface (CLI, web, etc.):
```
-freebuff/
+codebirds/
├── SPEC.md # This file (product-level spec)
├── README.md # Product-level documentation
├── cli/ # CLI build & release infrastructure
-│ ├── build.ts # Build script that sets FREEBUFF_MODE=true
+│ ├── build.ts # Build script that sets CODEBIRDS_MODE=true
│ └── release/
-│ ├── package.json # npm package metadata (name: "freebuff")
+│ ├── package.json # npm package metadata (name: "codebirds")
│ ├── index.js # Entry point (finds/runs binary)
│ ├── postinstall.js# Downloads platform binary on install
│ └── README.md # npm package README
└── web/ # (Future) Freebuff website code
```
-This structure allows `freebuff/web/` (or other surfaces) to be added alongside the CLI without restructuring.
+This structure allows `codebirds/web/` (or other surfaces) to be added alongside the CLI without restructuring.
-### Build Script (`freebuff/cli/build.ts`)
+### Build Script (`codebirds/cli/build.ts`)
Wraps `cli/scripts/build-binary.ts` with:
```bash
-FREEBUFF_MODE=true bun cli/scripts/build-binary.ts freebuff
+CODEBIRDS_MODE=true bun cli/scripts/build-binary.ts codebirds
```
-The existing `build-binary.ts` already supports a custom binary name argument and passes `NEXT_PUBLIC_*` env vars. We add `FREEBUFF_MODE` to the `defineFlags` array in `build-binary.ts`.
+The existing `build-binary.ts` already supports a custom binary name argument and passes `NEXT_PUBLIC_*` env vars. We add `CODEBIRDS_MODE` to the `defineFlags` array in `build-binary.ts`.
-### Release Package (`freebuff/cli/release/package.json`)
+### Release Package (`codebirds/cli/release/package.json`)
Mirrors `cli/release/package.json` but with:
-- `"name": "freebuff"`
+- `"name": "codebirds"`
- `"description": "Free AI coding assistant"`
-- `"bin": { "freebuff": "index.js" }`
+- `"bin": { "codebirds": "index.js" }`
- Same `postinstall.js` pattern (downloads platform-specific binary from GitHub releases)
-- Binary stored at `~/.config/manicode/freebuff` (or `freebuff.exe` on Windows)
+- Binary stored at `~/.config/manicode/codebirds` (or `codebirds.exe` on Windows)
### GitHub Workflow
-New file: `.github/workflows/freebuff-release.yml`
+New file: `.github/workflows/codebirds-release.yml`
Mirrors `cli-release-prod.yml` with these changes:
- **Trigger**: `workflow_dispatch` (manual) or scheduled
-- **Binary name**: `freebuff`
-- **Version source**: `freebuff/cli/release/package.json`
-- **Git tags**: `freebuff-v`
-- **npm publish**: `freebuff` package
-- **Environment overrides**: `{"FREEBUFF_MODE": "true", "NEXT_PUBLIC_CB_ENVIRONMENT": "prod"}`
-- **GitHub Release**: Creates releases in `CodebuffAI/codebuff-community` (or a separate repo)
+- **Binary name**: `codebirds`
+- **Version source**: `codebirds/cli/release/package.json`
+- **Git tags**: `codebirds-v`
+- **npm publish**: `codebirds` package
+- **Environment overrides**: `{"CODEBIRDS_MODE": "true", "NEXT_PUBLIC_CB_ENVIRONMENT": "prod"}`
+- **GitHub Release**: Creates releases in `CodebirdsAI/codebirds-community` (or a separate repo)
---
## 9. Changes to `cli/scripts/build-binary.ts`
-Add `FREEBUFF_MODE` to the define flags so it's available at compile time:
+Add `CODEBIRDS_MODE` to the define flags so it's available at compile time:
```ts
const defineFlags = [
['process.env.NODE_ENV', '"production"'],
- ['process.env.CODEBUFF_IS_BINARY', '"true"'],
- ['process.env.CODEBUFF_CLI_VERSION', `"${version}"`],
+ ['process.env.CODEBIRDS_IS_BINARY', '"true"'],
+ ['process.env.CODEBIRDS_CLI_VERSION', `"${version}"`],
[
- 'process.env.CODEBUFF_CLI_TARGET',
+ 'process.env.CODEBIRDS_CLI_TARGET',
`"${targetInfo.platform}-${targetInfo.arch}"`,
],
// Freebuff mode flag
- ['process.env.FREEBUFF_MODE', `"${process.env.FREEBUFF_MODE ?? 'false'}"`],
+ ['process.env.CODEBIRDS_MODE', `"${process.env.CODEBIRDS_MODE ?? 'false'}"`],
...nextPublicEnvVars,
]
```
@@ -291,7 +291,7 @@ These features work identically in Freebuff:
## 11. Analytics
-When `IS_FREEBUFF`:
+When `IS_CODEBIRDS`:
- `APP_LAUNCHED` event includes `isFreebuff: true`
- All existing analytics events continue to fire (helps understand free vs paid usage)
@@ -308,7 +308,7 @@ The server already handles FREE mode correctly:
- Free-mode-allowed agent+model combos cost 0 credits
- Ad impressions in FREE mode already don't grant credits
-No server-side changes are needed for Freebuff, **except** the release download API (`/api/releases/download/`) must be configured to serve `freebuff-*` binary tarballs. This may require updating the download route to recognize Freebuff release tags (`freebuff-v*`).
+No server-side changes are needed for Freebuff, **except** the release download API (`/api/releases/download/`) must be configured to serve `codebirds-*` binary tarballs. This may require updating the download route to recognize Freebuff release tags (`codebirds-v*`).
---
@@ -316,7 +316,7 @@ No server-side changes are needed for Freebuff, **except** the release download
### Unit Tests
-- Test that `IS_FREEBUFF` guards correctly hide/show components
+- Test that `IS_CODEBIRDS` guards correctly hide/show components
- Test filtered slash commands list
- Test filtered command registry
- Test help banner content
@@ -332,7 +332,7 @@ No server-side changes are needed for Freebuff, **except** the release download
### E2E (tmux)
-- Use `codebuff-local-cli` agent with `FREEBUFF_MODE=true` to verify visual output
+- Use `codebirds-local-cli` agent with `CODEBIRDS_MODE=true` to verify visual output
---
@@ -340,8 +340,8 @@ No server-side changes are needed for Freebuff, **except** the release download
### Phase 1: Core Flag & Branding
-1. Add `IS_FREEBUFF` constant
-2. Update `build-binary.ts` to pass through `FREEBUFF_MODE`
+1. Add `IS_CODEBIRDS` constant
+2. Update `build-binary.ts` to pass through `CODEBIRDS_MODE`
3. Conditional branding (title, logo, app header, CLI name)
### Phase 2: Feature Stripping
@@ -360,12 +360,12 @@ No server-side changes are needed for Freebuff, **except** the release download
### Phase 4: Build & Release Infrastructure
-11. Create `freebuff/cli/release/` package files
-12. Create `freebuff/cli/build.ts` script
-13. Create `.github/workflows/freebuff-release.yml`
+11. Create `codebirds/cli/release/` package files
+12. Create `codebirds/cli/build.ts` script
+13. Create `.github/workflows/codebirds-release.yml`
### Phase 5: Testing
-14. Add unit tests for IS_FREEBUFF guards
+14. Add unit tests for IS_CODEBIRDS guards
15. Add integration/E2E tests
16. Manual QA of built binary
diff --git a/freebuff/cli/build.ts b/codebirds/cli/build.ts
similarity index 67%
rename from freebuff/cli/build.ts
rename to codebirds/cli/build.ts
index b56a68e9b6..5309877aac 100644
--- a/freebuff/cli/build.ts
+++ b/codebirds/cli/build.ts
@@ -3,14 +3,14 @@
/**
* Freebuff CLI build script.
*
- * Wraps the existing CLI build-binary.ts with FREEBUFF_MODE=true
- * to produce a free-only variant of the Codebuff CLI.
+ * Wraps the existing CLI build-binary.ts with CODEBIRDS_MODE=true
+ * to produce a free-only variant of the Codebirds CLI.
*
* Usage:
- * bun freebuff/cli/build.ts
+ * bun codebirds/cli/build.ts
*
* Example:
- * bun freebuff/cli/build.ts 1.0.0
+ * bun codebirds/cli/build.ts 1.0.0
*/
import { spawnSync } from 'child_process'
@@ -22,7 +22,7 @@ const repoRoot = join(__dirname, '..', '..')
const version = process.argv[2]
if (!version) {
- console.error('Usage: bun freebuff/cli/build.ts ')
+ console.error('Usage: bun codebirds/cli/build.ts ')
process.exit(1)
}
@@ -30,13 +30,13 @@ console.log(`Building Freebuff v${version}...`)
const result = spawnSync(
'bun',
- ['cli/scripts/build-binary.ts', 'freebuff', version],
+ ['cli/scripts/build-binary.ts', 'codebirds', version],
{
cwd: repoRoot,
stdio: 'inherit',
env: {
...process.env,
- FREEBUFF_MODE: 'true',
+ CODEBIRDS_MODE: 'true',
},
},
)
diff --git a/freebuff/cli/release.ts b/codebirds/cli/release.ts
similarity index 79%
rename from freebuff/cli/release.ts
rename to codebirds/cli/release.ts
index 79a0335603..dd3d5dfa47 100644
--- a/freebuff/cli/release.ts
+++ b/codebirds/cli/release.ts
@@ -3,14 +3,14 @@
/**
* Freebuff CLI release script.
*
- * Triggers the freebuff-release.yml GitHub Actions workflow
+ * Triggers the codebirds-release.yml GitHub Actions workflow
* to build, publish, and release the Freebuff CLI to npm.
*
* Usage:
- * bun freebuff/cli/release.ts [patch|minor|major] [--ref ]
+ * bun codebirds/cli/release.ts [patch|minor|major] [--ref ]
*
* Requires:
- * CODEBUFF_GITHUB_TOKEN environment variable
+ * CODEBIRDS_GITHUB_TOKEN environment variable
*/
import { execSync } from 'child_process'
@@ -52,10 +52,10 @@ function formatTimestamp() {
}
function checkGitHubToken() {
- const token = process.env.CODEBUFF_GITHUB_TOKEN
+ const token = process.env.CODEBIRDS_GITHUB_TOKEN
if (!token) {
error(
- 'CODEBUFF_GITHUB_TOKEN environment variable is required but not set.\n' +
+ 'CODEBIRDS_GITHUB_TOKEN environment variable is required but not set.\n' +
'Please set it with your GitHub personal access token or use the infisical setup.',
)
}
@@ -80,7 +80,7 @@ async function triggerWorkflow(versionType: string, checkoutRef: string) {
-H "Accept: application/vnd.github.v3+json" \
-H "Authorization: token ${process.env.GITHUB_TOKEN}" \
-H "Content-Type: application/json" \
- https://api.github.com/repos/CodebuffAI/freebuff-private/actions/workflows/freebuff-release.yml/dispatches \
+ https://api.github.com/repos/CodebirdsAI/codebirds-private/actions/workflows/codebirds-release.yml/dispatches \
-d '${payload}'`
const response = execSync(triggerCmd, { encoding: 'utf8' })
@@ -88,7 +88,7 @@ async function triggerWorkflow(versionType: string, checkoutRef: string) {
if (response.includes('workflow_dispatch')) {
log(`⚠️ Workflow dispatch failed: ${response}`)
log(
- 'Please manually trigger the workflow at: https://github.com/CodebuffAI/freebuff-private/actions/workflows/freebuff-release.yml',
+ 'Please manually trigger the workflow at: https://github.com/CodebirdsAI/codebirds-private/actions/workflows/codebirds-release.yml',
)
} else {
log('🎉 Freebuff release workflow triggered!')
@@ -97,7 +97,7 @@ async function triggerWorkflow(versionType: string, checkoutRef: string) {
const message = err instanceof Error ? err.message : String(err)
log(`⚠️ Failed to trigger workflow automatically: ${message}`)
log(
- 'You may need to trigger it manually at: https://github.com/CodebuffAI/freebuff-private/actions/workflows/freebuff-release.yml',
+ 'You may need to trigger it manually at: https://github.com/CodebirdsAI/codebirds-private/actions/workflows/codebirds-release.yml',
)
}
}
@@ -107,7 +107,7 @@ async function main() {
log(`Date: ${formatTimestamp()}`)
checkGitHubToken()
- log('✅ Using local CODEBUFF_GITHUB_TOKEN')
+ log('✅ Using local CODEBIRDS_GITHUB_TOKEN')
log(`Version bump type: ${versionType}`)
if (checkoutRef) {
@@ -118,7 +118,7 @@ async function main() {
log('')
log(
- 'Monitor progress at: https://github.com/CodebuffAI/freebuff-private/actions/workflows/freebuff-release.yml',
+ 'Monitor progress at: https://github.com/CodebirdsAI/codebirds-private/actions/workflows/codebirds-release.yml',
)
}
diff --git a/freebuff/cli/release/README.md b/codebirds/cli/release/README.md
similarity index 84%
rename from freebuff/cli/release/README.md
rename to codebirds/cli/release/README.md
index 9fc144cbfa..75f8ef35b3 100644
--- a/freebuff/cli/release/README.md
+++ b/codebirds/cli/release/README.md
@@ -7,14 +7,14 @@ An AI coding agent that runs in your terminal — describe what you want, and Fr
## Install
```bash
-npm install -g freebuff
+npm install -g codebirds
```
## Usage
```bash
cd ~/my-project
-freebuff
+codebirds
```
## Why Freebuff?
@@ -31,7 +31,7 @@ freebuff
**What models do you use?** The best open-source models available. In full mode you can choose from DeepSeek V4 Pro, MiMo 2.5 Pro, Kimi K2.6, DeepSeek V4 Flash, MiMo 2.5, and MiniMax M3. Limited mode uses DeepSeek V4 Flash and MiMo 2.5. Gemini 3.1 Flash Lite handles file finding and research.
-**Which countries is Freebuff available in?** All countries. Freebuff runs in "full" mode in the US, Canada, UK, EU, and other select countries, and in "limited" mode everywhere else (or while using a VPN). See [freebuff.com](https://freebuff.com) for the full list.
+**Which countries is Freebuff available in?** All countries. Freebuff runs in "full" mode in the US, Canada, UK, EU, and other select countries, and in "limited" mode everywhere else (or while using a VPN). See [codebirds.com](https://codebirds.com) for the full list.
**What is limited mode?** Limited mode lets you use Freebuff outside the full-access countries, or while using a VPN. It includes DeepSeek V4 Flash and MiMo 2.5, with 5 one-hour sessions per day.
@@ -41,8 +41,8 @@ freebuff
## Links
-- [Documentation](https://codebuff.com/docs)
-- [GitHub](https://github.com/CodebuffAI/codebuff)
-- [Website](https://codebuff.com)
+- [Documentation](https://codebirds.com/docs)
+- [GitHub](https://github.com/CodebirdsAI/codebirds)
+- [Website](https://codebirds.com)
-> Built on the [Codebuff](https://codebuff.com) platform.
+> Built on the [Codebirds](https://codebirds.com) platform.
diff --git a/freebuff/cli/release/http.js b/codebirds/cli/release/http.js
similarity index 100%
rename from freebuff/cli/release/http.js
rename to codebirds/cli/release/http.js
diff --git a/freebuff/cli/release/index.js b/codebirds/cli/release/index.js
similarity index 97%
rename from freebuff/cli/release/index.js
rename to codebirds/cli/release/index.js
index e6a07cf488..63d6f4fa82 100644
--- a/freebuff/cli/release/index.js
+++ b/codebirds/cli/release/index.js
@@ -11,7 +11,7 @@ const zlib = require('zlib')
const tar = require('tar')
const { createReleaseHttpClient } = require('./http')
-const packageName = 'freebuff'
+const packageName = 'codebirds'
/**
* Terminal escape sequences to reset terminal state after the child process exits.
@@ -93,8 +93,8 @@ function createConfig(packageName) {
configDir,
binaryName,
binaryPath: path.join(configDir, binaryName),
- metadataPath: path.join(configDir, 'freebuff-metadata.json'),
- tempDownloadDir: path.join(configDir, '.freebuff-download-temp'),
+ metadataPath: path.join(configDir, 'codebirds-metadata.json'),
+ tempDownloadDir: path.join(configDir, '.codebirds-download-temp'),
userAgent: `${packageName}-cli`,
requestTimeout: 20000,
}
@@ -109,10 +109,10 @@ const { getProxyUrl, httpGet } = createReleaseHttpClient({
function getPostHogConfig() {
const apiKey =
- process.env.CODEBUFF_POSTHOG_API_KEY ||
+ process.env.CODEBIRDS_POSTHOG_API_KEY ||
process.env.NEXT_PUBLIC_POSTHOG_API_KEY
const host =
- process.env.CODEBUFF_POSTHOG_HOST ||
+ process.env.CODEBIRDS_POSTHOG_HOST ||
process.env.NEXT_PUBLIC_POSTHOG_HOST_URL
if (!apiKey || !host) {
@@ -135,7 +135,7 @@ function trackUpdateFailed(errorMessage, version, context = {}) {
const payload = JSON.stringify({
api_key: posthogConfig.apiKey,
- event: 'cli.update_freebuff_failed',
+ event: 'cli.update_codebirds_failed',
properties: {
distinct_id: `anonymous-${CONFIG.homeDir}`,
error: errorMessage,
@@ -208,7 +208,7 @@ function getPlatformKey() {
function getTargetOverride() {
const envNames = [
`${packageName.toUpperCase()}_BINARY_TARGET`,
- 'CODEBUFF_BINARY_TARGET',
+ 'CODEBIRDS_BINARY_TARGET',
'CLI_BINARY_TARGET',
]
@@ -421,7 +421,7 @@ async function downloadBinary(version, targetKey = getDownloadTargetKey()) {
}
const downloadUrl = `${
- process.env.NEXT_PUBLIC_CODEBUFF_APP_URL || 'https://codebuff.com'
+ process.env.NEXT_PUBLIC_CODEBIRDS_APP_URL || 'https://codebirds.com'
}/api/releases/download/${version}/${fileName}`
fs.mkdirSync(CONFIG.configDir, { recursive: true })
@@ -564,7 +564,7 @@ async function ensureBinaryExists() {
await downloadBinary(version)
} catch (error) {
term.clearLine()
- console.error('❌ Failed to download freebuff:', error.message)
+ console.error('❌ Failed to download codebirds:', error.message)
console.error('Please check your internet connection and try again')
if (!getProxyUrl()) {
console.error(
@@ -663,7 +663,7 @@ function printCrashDiagnostics(code, signal) {
console.error(` Binary: ${CONFIG.binaryPath}`)
console.error('')
console.error('Please report this issue at:')
- console.error(' https://github.com/CodebuffAI/codebuff/issues')
+ console.error(' https://github.com/CodebirdsAI/codebirds/issues')
console.error('')
}
diff --git a/freebuff/cli/release/package.json b/codebirds/cli/release/package.json
similarity index 71%
rename from freebuff/cli/release/package.json
rename to codebirds/cli/release/package.json
index dcb284fb2f..3b66f59d18 100644
--- a/freebuff/cli/release/package.json
+++ b/codebirds/cli/release/package.json
@@ -1,14 +1,14 @@
{
- "name": "freebuff",
+ "name": "codebirds",
"version": "0.0.115",
"description": "The world's strongest free coding agent",
"license": "MIT",
"bin": {
- "freebuff": "index.js"
+ "codebirds": "index.js"
},
"scripts": {
"postinstall": "node postinstall.js",
- "preuninstall": "node -e \"const fs = require('fs'); const path = require('path'); const os = require('os'); const binaryPath = path.join(os.homedir(), '.config', 'manicode', process.platform === 'win32' ? 'freebuff.exe' : 'freebuff'); try { fs.unlinkSync(binaryPath) } catch (e) { /* ignore if file doesn't exist */ }\""
+ "preuninstall": "node -e \"const fs = require('fs'); const path = require('path'); const os = require('os'); const binaryPath = path.join(os.homedir(), '.config', 'manicode', process.platform === 'win32' ? 'codebirds.exe' : 'codebirds'); try { fs.unlinkSync(binaryPath) } catch (e) { /* ignore if file doesn't exist */ }\""
},
"files": [
"index.js",
@@ -33,9 +33,9 @@
},
"repository": {
"type": "git",
- "url": "git+https://github.com/CodebuffAI/freebuff-private.git"
+ "url": "git+https://github.com/CodebirdsAI/codebirds-private.git"
},
- "homepage": "https://freebuff.com",
+ "homepage": "https://codebirds.com",
"publishConfig": {
"access": "public",
"provenance": false
diff --git a/freebuff/cli/release/postinstall.js b/codebirds/cli/release/postinstall.js
similarity index 75%
rename from freebuff/cli/release/postinstall.js
rename to codebirds/cli/release/postinstall.js
index 3bc21de1df..550de68b3c 100644
--- a/freebuff/cli/release/postinstall.js
+++ b/codebirds/cli/release/postinstall.js
@@ -9,7 +9,7 @@ const binaryPath = path.join(
os.homedir(),
'.config',
'manicode',
- process.platform === 'win32' ? 'freebuff.exe' : 'freebuff'
+ process.platform === 'win32' ? 'codebirds.exe' : 'codebirds'
);
try {
@@ -23,11 +23,11 @@ console.log('⚡ Welcome to Freebuff!');
console.log('\n');
console.log('To get started:');
console.log(' 1. cd to your project directory');
-console.log(' 2. Run: freebuff');
+console.log(' 2. Run: codebirds');
console.log('\n');
console.log('Example:');
console.log(' $ cd ~/my-project');
-console.log(' $ freebuff');
+console.log(' $ codebirds');
console.log('\n');
-console.log('For more information, visit: https://codebuff.com/docs');
+console.log('For more information, visit: https://codebirds.com/docs');
console.log('\n');
diff --git a/freebuff/cli/smoke-test.test.ts b/codebirds/cli/smoke-test.test.ts
similarity index 87%
rename from freebuff/cli/smoke-test.test.ts
rename to codebirds/cli/smoke-test.test.ts
index fec0d23375..c5a2fc02c3 100644
--- a/freebuff/cli/smoke-test.test.ts
+++ b/codebirds/cli/smoke-test.test.ts
@@ -4,16 +4,16 @@
*
* Verifies the compiled Freebuff binary:
* 1. Reports a valid version number
- * 2. Shows Freebuff branding (not Codebuff) in --help output
+ * 2. Shows Freebuff branding (not Codebirds) in --help output
* 3. Excludes mode flags (--free, --max, --plan) from --help
* 4. Renders the Freebuff title screen (ASCII logo) in tmux
*
* Prerequisites:
- * bun freebuff/cli/build.ts # build the binary
+ * bun codebirds/cli/build.ts # build the binary
* brew install tmux # for title-screen test
*
* Run:
- * bun test freebuff/cli/smoke-test.test.ts
+ * bun test codebirds/cli/smoke-test.test.ts
*/
import { execFileSync, execSync, spawn, spawnSync } from 'child_process'
@@ -23,7 +23,7 @@ import path from 'path'
import { describe, test, expect, afterEach } from 'bun:test'
const REPO_ROOT = path.join(__dirname, '..', '..')
-const BINARY_PATH = path.join(REPO_ROOT, 'cli', 'bin', 'freebuff')
+const BINARY_PATH = path.join(REPO_ROOT, 'cli', 'bin', 'codebirds')
const TIMEOUT_MS = 20_000
// ---------------------------------------------------------------------------
@@ -39,7 +39,7 @@ function isTmuxAvailable(): boolean {
if (process.env.CI === 'true' || process.env.CI === '1') return false
try {
execSync(
- 'which tmux && tmux new-session -d -s __freebuff_tmux_check__ && tmux kill-session -t __freebuff_tmux_check__',
+ 'which tmux && tmux new-session -d -s __codebirds_tmux_check__ && tmux kill-session -t __codebirds_tmux_check__',
{ stdio: 'pipe', timeout: 5000 },
)
return true
@@ -82,11 +82,11 @@ function runBinaryResult(args: string[]) {
timeout: 10_000,
env: {
...process.env,
- FREEBUFF_MODE: 'true',
+ CODEBIRDS_MODE: 'true',
NO_COLOR: '1',
NEXT_PUBLIC_CB_ENVIRONMENT: 'test',
- NEXT_PUBLIC_CODEBUFF_APP_URL: 'http://127.0.0.1:9',
- NEXT_PUBLIC_FREEBUFF_APP_URL: 'http://127.0.0.1:9',
+ NEXT_PUBLIC_CODEBIRDS_APP_URL: 'http://127.0.0.1:9',
+ NEXT_PUBLIC_CODEBIRDS_APP_URL: 'http://127.0.0.1:9',
NEXT_PUBLIC_SUPPORT_EMAIL: 'test@example.com',
NEXT_PUBLIC_POSTHOG_API_KEY: 'test',
NEXT_PUBLIC_POSTHOG_HOST_URL: 'http://127.0.0.1:9',
@@ -125,12 +125,12 @@ describe.skipIf(!binaryExists)('Freebuff Binary Smoke Tests', () => {
() => {
const output = stripAnsiCodes(runBinary(['--help']))
- // CLI name is "freebuff"
- expect(output).toContain('Usage: freebuff')
+ // CLI name is "codebirds"
+ expect(output).toContain('Usage: codebirds')
// Description is Freebuff-specific
expect(output).toContain('Free AI coding assistant')
- // Must NOT contain the Codebuff CLI name in the usage line
- expect(output).not.toContain('Usage: codebuff')
+ // Must NOT contain the Codebirds CLI name in the usage line
+ expect(output).not.toContain('Usage: codebirds')
},
TIMEOUT_MS,
)
@@ -189,7 +189,7 @@ describe.skipIf(!binaryExists)('Freebuff Binary Smoke Tests', () => {
test(
'displays Freebuff ASCII logo on startup',
async () => {
- sessionName = `freebuff-smoke-${Date.now()}`
+ sessionName = `codebirds-smoke-${Date.now()}`
// Start the binary in a detached tmux session
await tmux([
@@ -222,15 +222,15 @@ describe.skipIf(!binaryExists)('Freebuff Binary Smoke Tests', () => {
)
}
- // Verify it's the FREEBUFF logo, not CODEBUFF.
+ // Verify it's the CODEBIRDS logo, not CODEBUFF.
// The Freebuff 'F' character's third line starts with the crossbar:
// █████╗ ██████╔╝
- // whereas Codebuff 'C' has:
+ // whereas Codebirds 'C' has:
// ██║ ██║ ██║
// We check for the F + R pattern on line 3 of the logo.
expect(cleanOutput).toContain('█████╗ ██████╔╝')
- // The Codebuff logo's distinctive C+O opening should NOT appear
+ // The Codebirds logo's distinctive C+O opening should NOT appear
expect(cleanOutput).not.toContain('██╔════╝██╔═══██╗')
},
TIMEOUT_MS,
@@ -242,7 +242,7 @@ describe.skipIf(!binaryExists)('Freebuff Binary Smoke Tests', () => {
if (!binaryExists) {
describe('Freebuff Binary Required', () => {
test.skip(
- 'Build the binary first: bun freebuff/cli/build.ts ',
+ 'Build the binary first: bun codebirds/cli/build.ts ',
() => {},
)
})
diff --git a/freebuff/e2e/README.md b/codebirds/e2e/README.md
similarity index 72%
rename from freebuff/e2e/README.md
rename to codebirds/e2e/README.md
index 861d31f5be..b649d4c170 100644
--- a/freebuff/e2e/README.md
+++ b/codebirds/e2e/README.md
@@ -36,12 +36,12 @@ describe('My Feature', () => {
### 2. SDK agent-driven tests (AI-powered verification)
-Use the Codebuff SDK to run a testing agent that interacts with Freebuff via custom tmux tools. The agent reasons about the CLI output and verifies complex behaviors.
+Use the Codebirds SDK to run a testing agent that interacts with Freebuff via custom tmux tools. The agent reasons about the CLI output and verifies complex behaviors.
```typescript
import { describe, test, expect, afterEach } from 'bun:test'
-import { CodebuffClient } from '@codebuff/sdk'
-import { freebuffTesterAgent } from '../agent/freebuff-tester'
+import { CodebirdsClient } from '@codebirds/sdk'
+import { codebirdsTesterAgent } from '../agent/codebirds-tester'
import { createFreebuffTmuxTools, requireFreebuffBinary } from '../utils'
describe('Agent Test', () => {
@@ -53,18 +53,18 @@ describe('Agent Test', () => {
})
test('verifies startup', async () => {
- const apiKey = process.env.CODEBUFF_API_KEY
+ const apiKey = process.env.CODEBIRDS_API_KEY
if (!apiKey) return // Skip if no API key
const binary = requireFreebuffBinary()
const tmuxTools = createFreebuffTmuxTools(binary)
cleanup = tmuxTools.cleanup
- const client = new CodebuffClient({ apiKey })
+ const client = new CodebirdsClient({ apiKey })
const result = await client.run({
- agent: freebuffTesterAgent.id,
+ agent: codebirdsTesterAgent.id,
prompt: 'Start Freebuff and verify the branding is correct.',
- agentDefinitions: [freebuffTesterAgent],
+ agentDefinitions: [codebirdsTesterAgent],
customToolDefinitions: tmuxTools.tools,
handleEvent: () => {},
})
@@ -77,43 +77,43 @@ describe('Agent Test', () => {
## Prerequisites
- **tmux** must be installed: `brew install tmux` (macOS) or `sudo apt-get install tmux` (Ubuntu)
-- **Freebuff binary** must be built: `bun freebuff/cli/build.ts 0.0.0-dev`
+- **Freebuff binary** must be built: `bun codebirds/cli/build.ts 0.0.0-dev`
- **SDK built** (for agent tests): `cd sdk && bun run build`
-- **CODEBUFF_API_KEY** (for agent tests only): Set this environment variable
+- **CODEBIRDS_API_KEY** (for agent tests only): Set this environment variable
## Running Tests
### Build the binary first
```bash
-bun freebuff/cli/build.ts 0.0.0-dev
+bun codebirds/cli/build.ts 0.0.0-dev
```
### Run all tests
```bash
-bun test freebuff/e2e/tests/
+bun test codebirds/e2e/tests/
```
### Run a specific test
```bash
-bun test freebuff/e2e/tests/version.e2e.test.ts
-bun test freebuff/e2e/tests/startup.e2e.test.ts
-bun test freebuff/e2e/tests/help-command.e2e.test.ts
-bun test freebuff/e2e/tests/agent-startup.e2e.test.ts
+bun test codebirds/e2e/tests/version.e2e.test.ts
+bun test codebirds/e2e/tests/startup.e2e.test.ts
+bun test codebirds/e2e/tests/help-command.e2e.test.ts
+bun test codebirds/e2e/tests/agent-startup.e2e.test.ts
```
### Use a custom binary path
```bash
-FREEBUFF_BINARY=/path/to/freebuff bun test freebuff/e2e/tests/
+CODEBIRDS_BINARY=/path/to/codebirds bun test codebirds/e2e/tests/
```
## Adding New Tests
-1. Create a new file in `freebuff/e2e/tests/` with the naming convention `.e2e.test.ts`
-2. Add the test name to `.github/workflows/freebuff-e2e.yml` matrix:
+1. Create a new file in `codebirds/e2e/tests/` with the naming convention `.e2e.test.ts`
+2. Add the test name to `.github/workflows/codebirds-e2e.yml` matrix:
```yaml
matrix:
@@ -129,7 +129,7 @@ matrix:
## CI Workflow
-The `.github/workflows/freebuff-e2e.yml` workflow:
+The `.github/workflows/codebirds-e2e.yml` workflow:
1. **Builds** the Freebuff binary once (linux-x64)
2. **Runs each test file in parallel** via GitHub Actions matrix strategy
@@ -156,10 +156,10 @@ Triggers:
### `createFreebuffTmuxTools(binaryPath)`
Creates SDK custom tools for agent-driven testing:
-- `start_freebuff` - Launch the CLI
-- `send_to_freebuff` - Send text input
-- `capture_freebuff_output` - Capture terminal output
-- `stop_freebuff` - Stop and clean up
+- `start_codebirds` - Launch the CLI
+- `send_to_codebirds` - Send text input
+- `capture_codebirds_output` - Capture terminal output
+- `stop_codebirds` - Stop and clean up
### Helper functions
diff --git a/freebuff/e2e/agent/freebuff-tester.ts b/codebirds/e2e/agent/freebuff-tester.ts
similarity index 65%
rename from freebuff/e2e/agent/freebuff-tester.ts
rename to codebirds/e2e/agent/freebuff-tester.ts
index e4cf221423..bb33bae50b 100644
--- a/freebuff/e2e/agent/freebuff-tester.ts
+++ b/codebirds/e2e/agent/freebuff-tester.ts
@@ -1,4 +1,4 @@
-import type { AgentDefinition } from '@codebuff/sdk'
+import type { AgentDefinition } from '@codebirds/sdk'
/**
* Agent definition for testing the Freebuff CLI via tmux.
@@ -11,35 +11,35 @@ import type { AgentDefinition } from '@codebuff/sdk'
* ```ts
* const { tools, cleanup } = createFreebuffTmuxTools(binaryPath)
* const result = await client.run({
- * agent: freebuffTesterAgent.id,
- * prompt: 'Start freebuff and verify the welcome screen shows Freebuff branding',
- * agentDefinitions: [freebuffTesterAgent],
+ * agent: codebirdsTesterAgent.id,
+ * prompt: 'Start codebirds and verify the welcome screen shows Freebuff branding',
+ * agentDefinitions: [codebirdsTesterAgent],
* customToolDefinitions: tools,
* handleEvent: collector.handleEvent,
* })
* await cleanup()
* ```
*/
-export const freebuffTesterAgent: AgentDefinition = {
- id: 'freebuff-tester',
+export const codebirdsTesterAgent: AgentDefinition = {
+ id: 'codebirds-tester',
displayName: 'Freebuff E2E Tester',
model: 'anthropic/claude-sonnet-4.5',
toolNames: [
- 'start_freebuff',
- 'send_to_freebuff',
- 'capture_freebuff_output',
- 'stop_freebuff',
+ 'start_codebirds',
+ 'send_to_codebirds',
+ 'capture_codebirds_output',
+ 'stop_codebirds',
],
instructionsPrompt: `You are a QA tester for the Freebuff CLI application.
Your job is to verify that Freebuff behaves correctly by interacting with it
through tmux tools. Follow these steps:
-1. Call start_freebuff to launch the CLI
-2. Use capture_freebuff_output (with waitSeconds) to see the terminal output
-3. Use send_to_freebuff to type commands or text
+1. Call start_codebirds to launch the CLI
+2. Use capture_codebirds_output (with waitSeconds) to see the terminal output
+3. Use send_to_codebirds to type commands or text
4. Capture output again to verify behavior
-5. ALWAYS call stop_freebuff when done
+5. ALWAYS call stop_codebirds when done
Key things to verify:
- The CLI starts without errors or crashes
diff --git a/freebuff/e2e/tests/ads-behavior.e2e.test.ts b/codebirds/e2e/tests/ads-behavior.e2e.test.ts
similarity index 100%
rename from freebuff/e2e/tests/ads-behavior.e2e.test.ts
rename to codebirds/e2e/tests/ads-behavior.e2e.test.ts
diff --git a/freebuff/e2e/tests/agent-startup.e2e.test.ts b/codebirds/e2e/tests/agent-startup.e2e.test.ts
similarity index 100%
rename from freebuff/e2e/tests/agent-startup.e2e.test.ts
rename to codebirds/e2e/tests/agent-startup.e2e.test.ts
diff --git a/freebuff/e2e/tests/code-edit.e2e.test.ts b/codebirds/e2e/tests/code-edit.e2e.test.ts
similarity index 85%
rename from freebuff/e2e/tests/code-edit.e2e.test.ts
rename to codebirds/e2e/tests/code-edit.e2e.test.ts
index a2737de120..d6af42cce1 100644
--- a/freebuff/e2e/tests/code-edit.e2e.test.ts
+++ b/codebirds/e2e/tests/code-edit.e2e.test.ts
@@ -4,7 +4,7 @@
* Starts Freebuff in tmux, sends a prompt asking it to add a console.log
* to a file, and verifies the file was modified correctly.
*
- * Requires CODEBUFF_API_KEY — skipped if not set.
+ * Requires CODEBIRDS_API_KEY — skipped if not set.
*/
import { afterEach, describe, expect, test } from 'bun:test'
@@ -14,7 +14,7 @@ import { FreebuffSession, requireFreebuffBinary } from '../utils'
const TEST_TIMEOUT = 1_000_000
function getApiKey(): string | null {
- return process.env.CODEBUFF_API_KEY ?? null
+ return process.env.CODEBIRDS_API_KEY ?? null
}
describe.skip('Freebuff: Code Edit', () => {
@@ -32,7 +32,7 @@ describe.skip('Freebuff: Code Edit', () => {
async () => {
if (!getApiKey()) {
console.log(
- 'Skipping code-edit test: CODEBUFF_API_KEY not set. ' +
+ 'Skipping code-edit test: CODEBIRDS_API_KEY not set. ' +
'Set it to run code-edit e2e tests.',
)
return
@@ -46,7 +46,7 @@ describe.skip('Freebuff: Code Edit', () => {
'',
].join('\n')
- // Create the file before starting freebuff so it's in the initial context
+ // Create the file before starting codebirds so it's in the initial context
session = await FreebuffSession.start(binary, {
waitSeconds: 5,
initialFiles: { 'index.js': initialContent },
@@ -58,7 +58,7 @@ describe.skip('Freebuff: Code Edit', () => {
// Verify the file was created
expect(session.readFile('index.js')).toBe(initialContent)
- // Send a prompt asking freebuff to add a console.log
+ // Send a prompt asking codebirds to add a console.log
await session.send('Add console.log("hello world") to index.js')
// Wait for the file to be modified with the console.log
diff --git a/freebuff/e2e/tests/help-command.e2e.test.ts b/codebirds/e2e/tests/help-command.e2e.test.ts
similarity index 92%
rename from freebuff/e2e/tests/help-command.e2e.test.ts
rename to codebirds/e2e/tests/help-command.e2e.test.ts
index 786d63d59f..86b7074bc9 100644
--- a/freebuff/e2e/tests/help-command.e2e.test.ts
+++ b/codebirds/e2e/tests/help-command.e2e.test.ts
@@ -15,21 +15,21 @@ describe('Freebuff: --help flag', () => {
})
// Should show the binary name
- expect(output.toLowerCase()).toContain('freebuff')
+ expect(output.toLowerCase()).toContain('codebirds')
// Should show usage info
expect(output).toMatch(/usage|options|commands/i)
})
- test('does not reference Codebuff', () => {
+ test('does not reference Codebirds', () => {
const binary = requireFreebuffBinary()
const output = execFileSync(binary, ['--help'], {
encoding: 'utf-8',
timeout: 10_000,
})
- // The --help output should say Freebuff, not Codebuff
- expect(output).not.toMatch(/\bcodebuff\b/i)
+ // The --help output should say Freebuff, not Codebirds
+ expect(output).not.toMatch(/\bcodebirds\b/i)
})
})
diff --git a/freebuff/e2e/tests/knowledge-file.e2e.test.ts b/codebirds/e2e/tests/knowledge-file.e2e.test.ts
similarity index 90%
rename from freebuff/e2e/tests/knowledge-file.e2e.test.ts
rename to codebirds/e2e/tests/knowledge-file.e2e.test.ts
index 4d28cebd4b..672bfeb863 100644
--- a/freebuff/e2e/tests/knowledge-file.e2e.test.ts
+++ b/codebirds/e2e/tests/knowledge-file.e2e.test.ts
@@ -4,7 +4,7 @@
* Starts Freebuff in tmux, creates a knowledge.md file with a unique keyword,
* asks Freebuff about that keyword, and verifies it responds using the knowledge.
*
- * Requires CODEBUFF_API_KEY — skipped if not set.
+ * Requires CODEBIRDS_API_KEY — skipped if not set.
*/
import { afterEach, describe, expect, test } from 'bun:test'
@@ -14,7 +14,7 @@ import { FreebuffSession, requireFreebuffBinary } from '../utils'
const TEST_TIMEOUT = 180_000
function getApiKey(): string | null {
- return process.env.CODEBUFF_API_KEY ?? null
+ return process.env.CODEBIRDS_API_KEY ?? null
}
describe('Freebuff: Knowledge Files', () => {
@@ -32,7 +32,7 @@ describe('Freebuff: Knowledge Files', () => {
async () => {
if (!getApiKey()) {
console.log(
- 'Skipping knowledge-file test: CODEBUFF_API_KEY not set. ' +
+ 'Skipping knowledge-file test: CODEBIRDS_API_KEY not set. ' +
'Set it to run knowledge-file e2e tests.',
)
return
diff --git a/freebuff/e2e/tests/slash-commands.e2e.test.ts b/codebirds/e2e/tests/slash-commands.e2e.test.ts
similarity index 97%
rename from freebuff/e2e/tests/slash-commands.e2e.test.ts
rename to codebirds/e2e/tests/slash-commands.e2e.test.ts
index ef44a173e6..34dff1a0a5 100644
--- a/freebuff/e2e/tests/slash-commands.e2e.test.ts
+++ b/codebirds/e2e/tests/slash-commands.e2e.test.ts
@@ -7,7 +7,7 @@ const SESSION_HEIGHT = 40
/**
* Commands that should be REMOVED in Freebuff.
- * These are stripped at build time via the FREEBUFF_REMOVED_COMMAND_IDS set
+ * These are stripped at build time via the CODEBIRDS_REMOVED_COMMAND_IDS set
* in cli/src/data/slash-commands.ts.
*/
const REMOVED_COMMANDS = [
diff --git a/freebuff/e2e/tests/startup.e2e.test.ts b/codebirds/e2e/tests/startup.e2e.test.ts
similarity index 100%
rename from freebuff/e2e/tests/startup.e2e.test.ts
rename to codebirds/e2e/tests/startup.e2e.test.ts
diff --git a/freebuff/e2e/tests/terminal-command.e2e.test.ts b/codebirds/e2e/tests/terminal-command.e2e.test.ts
similarity index 88%
rename from freebuff/e2e/tests/terminal-command.e2e.test.ts
rename to codebirds/e2e/tests/terminal-command.e2e.test.ts
index c1fa5c4fb1..4c276930e9 100644
--- a/freebuff/e2e/tests/terminal-command.e2e.test.ts
+++ b/codebirds/e2e/tests/terminal-command.e2e.test.ts
@@ -4,7 +4,7 @@
* Starts Freebuff in tmux, sends a prompt asking it to run a shell command,
* and verifies the command was executed by checking its side effects.
*
- * Requires CODEBUFF_API_KEY — skipped if not set.
+ * Requires CODEBIRDS_API_KEY — skipped if not set.
*/
import { afterEach, describe, expect, test } from 'bun:test'
@@ -14,7 +14,7 @@ import { FreebuffSession, requireFreebuffBinary } from '../utils'
const TEST_TIMEOUT = 1_000_000
function getApiKey(): string | null {
- return process.env.CODEBUFF_API_KEY ?? null
+ return process.env.CODEBIRDS_API_KEY ?? null
}
describe.skip('Freebuff: Terminal Command', () => {
@@ -32,7 +32,7 @@ describe.skip('Freebuff: Terminal Command', () => {
async () => {
if (!getApiKey()) {
console.log(
- 'Skipping terminal-command test: CODEBUFF_API_KEY not set. ' +
+ 'Skipping terminal-command test: CODEBIRDS_API_KEY not set. ' +
'Set it to run terminal-command e2e tests.',
)
return
@@ -44,7 +44,7 @@ describe.skip('Freebuff: Terminal Command', () => {
// Wait for the CLI to be fully ready before sending input
await session.waitForReady()
- // Ask freebuff to run a shell command whose output can only come from
+ // Ask codebirds to run a shell command whose output can only come from
// actual terminal execution (not file-writing tools)
await session.send(
'Execute a shell command in the terminal to write the current Unix timestamp in seconds to timestamp.txt',
diff --git a/freebuff/e2e/tests/version.e2e.test.ts b/codebirds/e2e/tests/version.e2e.test.ts
similarity index 94%
rename from freebuff/e2e/tests/version.e2e.test.ts
rename to codebirds/e2e/tests/version.e2e.test.ts
index 4df1e26642..18d093cbb7 100644
--- a/freebuff/e2e/tests/version.e2e.test.ts
+++ b/codebirds/e2e/tests/version.e2e.test.ts
@@ -27,7 +27,7 @@ describe('Freebuff: --version', () => {
test('ignores project bunfig.toml preloads', () => {
const binary = requireFreebuffBinary()
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'freebuff-bunfig-'))
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codebirds-bunfig-'))
try {
fs.writeFileSync(
diff --git a/freebuff/e2e/utils/binary-helpers.ts b/codebirds/e2e/utils/binary-helpers.ts
similarity index 72%
rename from freebuff/e2e/utils/binary-helpers.ts
rename to codebirds/e2e/utils/binary-helpers.ts
index c233574dd4..57864a16f8 100644
--- a/freebuff/e2e/utils/binary-helpers.ts
+++ b/codebirds/e2e/utils/binary-helpers.ts
@@ -6,10 +6,10 @@ const __dirname = dirname(fileURLToPath(import.meta.url))
export const REPO_ROOT = resolve(__dirname, '../../..')
export function getFreebuffBinaryPath(): string {
- if (process.env.FREEBUFF_BINARY) {
- return resolve(process.env.FREEBUFF_BINARY)
+ if (process.env.CODEBIRDS_BINARY) {
+ return resolve(process.env.CODEBIRDS_BINARY)
}
- return resolve(REPO_ROOT, 'cli/bin/freebuff')
+ return resolve(REPO_ROOT, 'cli/bin/codebirds')
}
export function requireFreebuffBinary(): string {
@@ -17,7 +17,7 @@ export function requireFreebuffBinary(): string {
if (!existsSync(binaryPath)) {
throw new Error(
`Freebuff binary not found at ${binaryPath}. ` +
- 'Build with: bun freebuff/cli/build.ts ',
+ 'Build with: bun codebirds/cli/build.ts ',
)
}
return binaryPath
diff --git a/freebuff/e2e/utils/freebuff-session.ts b/codebirds/e2e/utils/freebuff-session.ts
similarity index 93%
rename from freebuff/e2e/utils/freebuff-session.ts
rename to codebirds/e2e/utils/freebuff-session.ts
index 0dc977a2da..741fc80a1d 100644
--- a/freebuff/e2e/utils/freebuff-session.ts
+++ b/codebirds/e2e/utils/freebuff-session.ts
@@ -5,7 +5,7 @@ import path from 'path'
import { tmuxCapture, tmuxSend, tmuxSendKey, tmuxStart, tmuxStop } from './tmux-helpers'
/** Static strings that prove the CLI reached a post-init boot screen. */
-export const FREEBUFF_BOOT_SIGNALS = [
+export const CODEBIRDS_BOOT_SIGNALS = [
'█████╗ ██████╔╝', // ASCII logo (full or small variant)
'Start coding for free',
'Enter a coding task',
@@ -28,7 +28,7 @@ export class FreebuffSession {
}
/**
- * Start a freebuff binary in a tmux session.
+ * Start a codebirds binary in a tmux session.
* Creates a temporary working directory to simulate a real user project.
*/
static async start(
@@ -40,9 +40,9 @@ export class FreebuffSession {
initialFiles?: Record
},
): Promise {
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'freebuff-e2e-'))
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codebirds-e2e-'))
- // Create a minimal project so freebuff has something to work with
+ // Create a minimal project so codebirds has something to work with
fs.writeFileSync(
path.join(tmpDir, 'README.md'),
'# E2E Test Project\n',
@@ -147,7 +147,7 @@ export class FreebuffSession {
)
}
- /** Send text input to the freebuff CLI (presses Enter by default). */
+ /** Send text input to the codebirds CLI (presses Enter by default). */
async send(
text: string,
options?: { noEnter?: boolean; waitIdle?: number },
@@ -180,7 +180,7 @@ export class FreebuffSession {
const start = Date.now()
while (Date.now() - start < timeoutMs) {
const output = await this.capture()
- if (FREEBUFF_BOOT_SIGNALS.some((signal) => output.includes(signal))) {
+ if (CODEBIRDS_BOOT_SIGNALS.some((signal) => output.includes(signal))) {
return output
}
await new Promise((resolve) => setTimeout(resolve, 500))
@@ -188,7 +188,7 @@ export class FreebuffSession {
const finalOutput = await this.capture()
throw new Error(
`Timed out after ${timeoutMs}ms waiting for a boot signal ` +
- `(checked ${FREEBUFF_BOOT_SIGNALS.length} patterns).\n` +
+ `(checked ${CODEBIRDS_BOOT_SIGNALS.length} patterns).\n` +
`Last output:\n${finalOutput}`,
)
}
diff --git a/freebuff/e2e/utils/index.ts b/codebirds/e2e/utils/index.ts
similarity index 83%
rename from freebuff/e2e/utils/index.ts
rename to codebirds/e2e/utils/index.ts
index 70eedda6b6..cac7d4505d 100644
--- a/freebuff/e2e/utils/index.ts
+++ b/codebirds/e2e/utils/index.ts
@@ -1,8 +1,8 @@
export { getFreebuffBinaryPath, requireFreebuffBinary, REPO_ROOT } from './binary-helpers'
export {
- FREEBUFF_BOOT_SIGNALS,
+ CODEBIRDS_BOOT_SIGNALS,
FreebuffSession,
-} from './freebuff-session'
+} from './codebirds-session'
export { createFreebuffTmuxTools } from './tmux-custom-tools'
export {
tmuxStart,
diff --git a/freebuff/e2e/utils/tmux-custom-tools.ts b/codebirds/e2e/utils/tmux-custom-tools.ts
similarity index 90%
rename from freebuff/e2e/utils/tmux-custom-tools.ts
rename to codebirds/e2e/utils/tmux-custom-tools.ts
index f37fae014d..7c615847ea 100644
--- a/freebuff/e2e/utils/tmux-custom-tools.ts
+++ b/codebirds/e2e/utils/tmux-custom-tools.ts
@@ -1,6 +1,6 @@
import { z } from 'zod/v4'
-import { FreebuffSession } from './freebuff-session'
+import { FreebuffSession } from './codebirds-session'
import type { ZodType } from 'zod/v4'
@@ -16,7 +16,7 @@ interface FreebuffToolDefinition {
type ToolOutput = { type: 'json'; value: Record }[]
/**
- * Creates custom tool definitions that allow a Codebuff SDK agent
+ * Creates custom tool definitions that allow a Codebirds SDK agent
* to interact with a Freebuff CLI binary via tmux.
*
* Returns the tools array and a cleanup function to call in afterEach.
@@ -35,7 +35,7 @@ export function createFreebuffTmuxTools(binaryPath: string): {
let session: FreebuffSession | null = null
const startTool: FreebuffToolDefinition = {
- toolName: 'start_freebuff',
+ toolName: 'start_codebirds',
description:
'Start the Freebuff CLI binary in a tmux terminal session. Call this first before interacting with Freebuff.',
inputSchema: z.object({}),
@@ -70,7 +70,7 @@ export function createFreebuffTmuxTools(binaryPath: string): {
}
const sendInputTool: FreebuffToolDefinition = {
- toolName: 'send_to_freebuff',
+ toolName: 'send_to_codebirds',
description:
'Send text input to the running Freebuff CLI. The text is sent as if typed by the user and Enter is pressed.',
inputSchema: z.object({
@@ -84,7 +84,7 @@ export function createFreebuffTmuxTools(binaryPath: string): {
return [
{
type: 'json',
- value: { error: 'No session running. Call start_freebuff first.' },
+ value: { error: 'No session running. Call start_codebirds first.' },
},
]
}
@@ -94,7 +94,7 @@ export function createFreebuffTmuxTools(binaryPath: string): {
}
const captureOutputTool: FreebuffToolDefinition = {
- toolName: 'capture_freebuff_output',
+ toolName: 'capture_codebirds_output',
description:
'Capture the current terminal output from the running Freebuff CLI session. ' +
'Use waitSeconds to wait before capturing (useful after sending a command).',
@@ -112,7 +112,7 @@ export function createFreebuffTmuxTools(binaryPath: string): {
return [
{
type: 'json',
- value: { error: 'No session running. Call start_freebuff first.' },
+ value: { error: 'No session running. Call start_codebirds first.' },
},
]
}
@@ -122,7 +122,7 @@ export function createFreebuffTmuxTools(binaryPath: string): {
}
const stopTool: FreebuffToolDefinition = {
- toolName: 'stop_freebuff',
+ toolName: 'stop_codebirds',
description:
'Stop the running Freebuff CLI session and clean up resources. Always call this when done testing.',
inputSchema: z.object({}),
diff --git a/freebuff/e2e/utils/tmux-helpers.ts b/codebirds/e2e/utils/tmux-helpers.ts
similarity index 100%
rename from freebuff/e2e/utils/tmux-helpers.ts
rename to codebirds/e2e/utils/tmux-helpers.ts
diff --git a/freebuff/package.json b/codebirds/package.json
similarity index 96%
rename from freebuff/package.json
rename to codebirds/package.json
index 1a42f3c055..ffbfe8bbcd 100644
--- a/freebuff/package.json
+++ b/codebirds/package.json
@@ -1,5 +1,5 @@
{
- "name": "@codebuff/freebuff",
+ "name": "@codebirds/codebirds",
"version": "1.0.0",
"private": true,
"scripts": {
diff --git a/common/package.json b/common/package.json
index 723dbe2954..9825369c48 100644
--- a/common/package.json
+++ b/common/package.json
@@ -1,7 +1,7 @@
{
- "name": "@codebuff/common",
+ "name": "@codebirds/common",
"version": "1.0.0",
- "description": "Common utilities for Codebuff",
+ "description": "Common utilities for Codebirds",
"private": true,
"type": "module",
"exports": {
diff --git a/common/src/__tests__/agent-validation.test.ts b/common/src/__tests__/agent-validation.test.ts
index 6700855ddb..1427905762 100644
--- a/common/src/__tests__/agent-validation.test.ts
+++ b/common/src/__tests__/agent-validation.test.ts
@@ -7,7 +7,7 @@ import { getStubProjectFileContext } from '../util/file'
import type { DynamicAgentTemplate } from '../types/dynamic-agent-template'
import type { AgentState } from '../types/session-state'
import type { ProjectFileContext } from '../util/file'
-import type { Logger } from '@codebuff/common/types/contracts/logger'
+import type { Logger } from '@codebirds/common/types/contracts/logger'
describe('Agent Validation', () => {
let mockFileContext: ProjectFileContext
@@ -224,7 +224,7 @@ describe('Agent Validation', () => {
...mockFileContext,
agentTemplates: {
'git-committer.ts': {
- id: 'codebuffai-git-committer',
+ id: 'codebirdsai-git-committer',
version: '0.0.1',
displayName: 'Git Committer',
spawnerPrompt: 'A git committer agent',
@@ -247,7 +247,7 @@ describe('Agent Validation', () => {
systemPrompt: 'Test system prompt',
instructionsPrompt: 'Test user prompt',
stepPrompt: 'Test step prompt',
- spawnableAgents: ['codebuffai-git-committer'], // Should be valid after first pass
+ spawnableAgents: ['codebirdsai-git-committer'], // Should be valid after first pass
outputMode: 'last_message',
includeMessageHistory: true,
inheritParentSystemPrompt: false,
@@ -262,10 +262,10 @@ describe('Agent Validation', () => {
})
expect(result.validationErrors).toHaveLength(0)
- expect(result.templates).toHaveProperty('codebuffai-git-committer')
+ expect(result.templates).toHaveProperty('codebirdsai-git-committer')
expect(result.templates).toHaveProperty('spawner-agent')
expect(result.templates['spawner-agent'].spawnableAgents).toContain(
- 'codebuffai-git-committer', // Full agent ID with prefix
+ 'codebirdsai-git-committer', // Full agent ID with prefix
)
})
})
@@ -545,7 +545,7 @@ describe('Agent Validation', () => {
...mockFileContext,
agentTemplates: {
'git-committer.ts': {
- id: 'codebuffai-git-committer',
+ id: 'codebirdsai-git-committer',
version: '0.0.1',
displayName: 'Git Committer',
spawnerPrompt:
@@ -584,9 +584,9 @@ describe('Agent Validation', () => {
})
expect(result.validationErrors).toHaveLength(0)
- expect(result.templates).toHaveProperty('codebuffai-git-committer')
+ expect(result.templates).toHaveProperty('codebirdsai-git-committer')
- const template = result.templates['codebuffai-git-committer']
+ const template = result.templates['codebirdsai-git-committer']
const paramsSchema = template.inputSchema.params!
expect(paramsSchema.safeParse('').success).toBe(false) // Too short
diff --git a/common/src/__tests__/env-ci.test.ts b/common/src/__tests__/env-ci.test.ts
index 084829f7b6..0d6a108c32 100644
--- a/common/src/__tests__/env-ci.test.ts
+++ b/common/src/__tests__/env-ci.test.ts
@@ -29,16 +29,16 @@ describe('env-ci', () => {
expect(env.GITHUB_ACTIONS).toBe('true')
})
- test('returns current process.env values for CODEBUFF_API_KEY', () => {
- process.env.CODEBUFF_API_KEY = 'test-key-123'
+ test('returns current process.env values for CODEBIRDS_API_KEY', () => {
+ process.env.CODEBIRDS_API_KEY = 'test-key-123'
const env = getCiEnv()
- expect(env.CODEBUFF_API_KEY).toBe('test-key-123')
+ expect(env.CODEBIRDS_API_KEY).toBe('test-key-123')
})
- test('returns current process.env values for CODEBUFF_GITHUB_TOKEN', () => {
- process.env.CODEBUFF_GITHUB_TOKEN = 'ghp_test_token'
+ test('returns current process.env values for CODEBIRDS_GITHUB_TOKEN', () => {
+ process.env.CODEBIRDS_GITHUB_TOKEN = 'ghp_test_token'
const env = getCiEnv()
- expect(env.CODEBUFF_GITHUB_TOKEN).toBe('ghp_test_token')
+ expect(env.CODEBIRDS_GITHUB_TOKEN).toBe('ghp_test_token')
})
test('returns undefined for unset env vars', () => {
@@ -65,8 +65,8 @@ describe('env-ci', () => {
test('contains expected keys', () => {
expect('CI' in ciEnv).toBe(true)
expect('GITHUB_ACTIONS' in ciEnv).toBe(true)
- expect('CODEBUFF_API_KEY' in ciEnv).toBe(true)
- expect('CODEBUFF_GITHUB_TOKEN' in ciEnv).toBe(true)
+ expect('CODEBIRDS_API_KEY' in ciEnv).toBe(true)
+ expect('CODEBIRDS_GITHUB_TOKEN' in ciEnv).toBe(true)
})
})
@@ -117,7 +117,7 @@ describe('env-ci', () => {
describe('createTestCiEnv', () => {
test('returns a CiEnv with default test values', () => {
const env = createTestCiEnv()
- expect(env.CODEBUFF_API_KEY).toBe('test-api-key')
+ expect(env.CODEBIRDS_API_KEY).toBe('test-api-key')
})
test('returns undefined for most vars by default', () => {
@@ -126,7 +126,7 @@ describe('env-ci', () => {
expect(env.GITHUB_ACTIONS).toBeUndefined()
expect(env.RENDER).toBeUndefined()
expect(env.IS_PULL_REQUEST).toBeUndefined()
- expect(env.CODEBUFF_GITHUB_TOKEN).toBeUndefined()
+ expect(env.CODEBIRDS_GITHUB_TOKEN).toBeUndefined()
})
test('allows overriding specific values', () => {
@@ -137,14 +137,14 @@ describe('env-ci', () => {
expect(env.CI).toBe('true')
expect(env.GITHUB_ACTIONS).toBe('true')
// Other values should still have defaults
- expect(env.CODEBUFF_API_KEY).toBe('test-api-key')
+ expect(env.CODEBIRDS_API_KEY).toBe('test-api-key')
})
test('allows overriding default values', () => {
const env = createTestCiEnv({
- CODEBUFF_API_KEY: 'custom-api-key',
+ CODEBIRDS_API_KEY: 'custom-api-key',
})
- expect(env.CODEBUFF_API_KEY).toBe('custom-api-key')
+ expect(env.CODEBIRDS_API_KEY).toBe('custom-api-key')
})
test('allows setting all CI-related vars for CI simulation', () => {
@@ -153,15 +153,15 @@ describe('env-ci', () => {
GITHUB_ACTIONS: 'true',
RENDER: 'true',
IS_PULL_REQUEST: 'true',
- CODEBUFF_GITHUB_TOKEN: 'ghp_simulated_token',
- CODEBUFF_API_KEY: 'test-api-key-override',
+ CODEBIRDS_GITHUB_TOKEN: 'ghp_simulated_token',
+ CODEBIRDS_API_KEY: 'test-api-key-override',
})
expect(env.CI).toBe('true')
expect(env.GITHUB_ACTIONS).toBe('true')
expect(env.RENDER).toBe('true')
expect(env.IS_PULL_REQUEST).toBe('true')
- expect(env.CODEBUFF_GITHUB_TOKEN).toBe('ghp_simulated_token')
- expect(env.CODEBUFF_API_KEY).toBe('test-api-key-override')
+ expect(env.CODEBIRDS_GITHUB_TOKEN).toBe('ghp_simulated_token')
+ expect(env.CODEBIRDS_API_KEY).toBe('test-api-key-override')
})
})
})
diff --git a/common/src/__tests__/env-process.test.ts b/common/src/__tests__/env-process.test.ts
index 13c409aa50..a1158ab1d1 100644
--- a/common/src/__tests__/env-process.test.ts
+++ b/common/src/__tests__/env-process.test.ts
@@ -53,10 +53,10 @@ describe('env-process', () => {
expect(env.KITTY_WINDOW_ID).toBeUndefined()
})
- test('returns current process.env values for CODEBUFF_IS_BINARY', () => {
- process.env.CODEBUFF_IS_BINARY = 'true'
+ test('returns current process.env values for CODEBIRDS_IS_BINARY', () => {
+ process.env.CODEBIRDS_IS_BINARY = 'true'
const env = getProcessEnv()
- expect(env.CODEBUFF_IS_BINARY).toBe('true')
+ expect(env.CODEBIRDS_IS_BINARY).toBe('true')
})
test('returns a snapshot that does not change when process.env changes', () => {
@@ -98,7 +98,7 @@ describe('env-process', () => {
expect(env.COMSPEC).toBeUndefined()
expect(env.TERM_PROGRAM).toBeUndefined()
expect(env.KITTY_WINDOW_ID).toBeUndefined()
- expect(env.CODEBUFF_IS_BINARY).toBeUndefined()
+ expect(env.CODEBIRDS_IS_BINARY).toBeUndefined()
})
test('allows overriding specific values', () => {
@@ -122,13 +122,13 @@ describe('env-process', () => {
expect(env.NODE_ENV).toBe('production')
})
- test('allows setting CODEBUFF_IS_BINARY for binary mode tests', () => {
+ test('allows setting CODEBIRDS_IS_BINARY for binary mode tests', () => {
const env = createTestProcessEnv({
- CODEBUFF_IS_BINARY: 'true',
- CODEBUFF_CLI_VERSION: '1.0.0',
+ CODEBIRDS_IS_BINARY: 'true',
+ CODEBIRDS_CLI_VERSION: '1.0.0',
})
- expect(env.CODEBUFF_IS_BINARY).toBe('true')
- expect(env.CODEBUFF_CLI_VERSION).toBe('1.0.0')
+ expect(env.CODEBIRDS_IS_BINARY).toBe('true')
+ expect(env.CODEBIRDS_CLI_VERSION).toBe('1.0.0')
})
test('allows setting terminal-specific vars', () => {
diff --git a/common/src/__tests__/free-agents.test.ts b/common/src/__tests__/free-agents.test.ts
index 5e2b380a90..5207184f20 100644
--- a/common/src/__tests__/free-agents.test.ts
+++ b/common/src/__tests__/free-agents.test.ts
@@ -1,16 +1,16 @@
import { describe, expect, test } from 'bun:test'
import {
- FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID,
- FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID,
- FREEBUFF_GEMINI_PRO_MODEL_ID,
- FREEBUFF_KIMI_MODEL_ID,
- FREEBUFF_MINIMAX_MODEL_ID,
- FREEBUFF_MIMO_V25_MODEL_ID,
- FREEBUFF_MIMO_V25_PRO_MODEL_ID,
-} from '../constants/freebuff-models'
+ CODEBIRDS_DEEPSEEK_V4_FLASH_MODEL_ID,
+ CODEBIRDS_DEEPSEEK_V4_PRO_MODEL_ID,
+ CODEBIRDS_GEMINI_PRO_MODEL_ID,
+ CODEBIRDS_KIMI_MODEL_ID,
+ CODEBIRDS_MINIMAX_MODEL_ID,
+ CODEBIRDS_MIMO_V25_MODEL_ID,
+ CODEBIRDS_MIMO_V25_PRO_MODEL_ID,
+} from '../constants/codebirds-models'
import { minimaxModels } from '../constants/model-config'
-import { FREEBUFF_GEMINI_THINKER_AGENT_ID } from '../constants/freebuff-gemini-thinker'
+import { CODEBIRDS_GEMINI_THINKER_AGENT_ID } from '../constants/codebirds-gemini-thinker'
import {
getFreebuffRootAgentIdForModel,
isFreebuffGeminiThinkerAgent,
@@ -21,23 +21,23 @@ import {
const MINIMAX_M3_MODEL_ID = minimaxModels.minimaxM3
describe('free mode agent model allowlist', () => {
- test('maps supported freebuff models to concrete root agents', () => {
- expect(getFreebuffRootAgentIdForModel(FREEBUFF_MINIMAX_MODEL_ID)).toBe(
+ test('maps supported codebirds models to concrete root agents', () => {
+ expect(getFreebuffRootAgentIdForModel(CODEBIRDS_MINIMAX_MODEL_ID)).toBe(
'base2-free',
)
- expect(getFreebuffRootAgentIdForModel(FREEBUFF_KIMI_MODEL_ID)).toBe(
+ expect(getFreebuffRootAgentIdForModel(CODEBIRDS_KIMI_MODEL_ID)).toBe(
'base2-free-kimi',
)
expect(
- getFreebuffRootAgentIdForModel(FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID),
+ getFreebuffRootAgentIdForModel(CODEBIRDS_DEEPSEEK_V4_PRO_MODEL_ID),
).toBe('base2-free-deepseek')
expect(
- getFreebuffRootAgentIdForModel(FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID),
+ getFreebuffRootAgentIdForModel(CODEBIRDS_DEEPSEEK_V4_FLASH_MODEL_ID),
).toBe('base2-free-deepseek-flash')
- expect(getFreebuffRootAgentIdForModel(FREEBUFF_MIMO_V25_PRO_MODEL_ID)).toBe(
+ expect(getFreebuffRootAgentIdForModel(CODEBIRDS_MIMO_V25_PRO_MODEL_ID)).toBe(
'base2-free-mimo-pro',
)
- expect(getFreebuffRootAgentIdForModel(FREEBUFF_MIMO_V25_MODEL_ID)).toBe(
+ expect(getFreebuffRootAgentIdForModel(CODEBIRDS_MIMO_V25_MODEL_ID)).toBe(
'base2-free-mimo',
)
expect(getFreebuffRootAgentIdForModel(MINIMAX_M3_MODEL_ID)).toBe(
@@ -45,9 +45,9 @@ describe('free mode agent model allowlist', () => {
)
})
- test('allows each freebuff root agent only with its configured model', () => {
+ test('allows each codebirds root agent only with its configured model', () => {
expect(
- isFreeModeAllowedAgentModel('base2-free', FREEBUFF_MINIMAX_MODEL_ID),
+ isFreeModeAllowedAgentModel('base2-free', CODEBIRDS_MINIMAX_MODEL_ID),
).toBe(true)
expect(isFreeModeAllowedAgentModel('base2-free', MINIMAX_M3_MODEL_ID)).toBe(
false,
@@ -55,49 +55,49 @@ describe('free mode agent model allowlist', () => {
expect(
isFreeModeAllowedAgentModel(
'base2-free',
- FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID,
+ CODEBIRDS_DEEPSEEK_V4_PRO_MODEL_ID,
),
).toBe(true)
expect(
- isFreeModeAllowedAgentModel('base2-free', FREEBUFF_KIMI_MODEL_ID),
+ isFreeModeAllowedAgentModel('base2-free', CODEBIRDS_KIMI_MODEL_ID),
).toBe(true)
expect(
- isFreeModeAllowedAgentModel('base2-free-kimi', FREEBUFF_KIMI_MODEL_ID),
+ isFreeModeAllowedAgentModel('base2-free-kimi', CODEBIRDS_KIMI_MODEL_ID),
).toBe(true)
expect(
isFreeModeAllowedAgentModel(
'base2-free-deepseek',
- FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID,
+ CODEBIRDS_DEEPSEEK_V4_PRO_MODEL_ID,
),
).toBe(true)
expect(
isFreeModeAllowedAgentModel(
'base2-free-deepseek-flash',
- FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID,
+ CODEBIRDS_DEEPSEEK_V4_FLASH_MODEL_ID,
),
).toBe(true)
expect(
isFreeModeAllowedAgentModel(
'base2-free-mimo-pro',
- FREEBUFF_MIMO_V25_PRO_MODEL_ID,
+ CODEBIRDS_MIMO_V25_PRO_MODEL_ID,
),
).toBe(true)
expect(
isFreeModeAllowedAgentModel(
'base2-free-mimo',
- FREEBUFF_MIMO_V25_MODEL_ID,
+ CODEBIRDS_MIMO_V25_MODEL_ID,
),
).toBe(true)
expect(
isFreeModeAllowedAgentModel(
'base2-free-mimo',
- FREEBUFF_MIMO_V25_PRO_MODEL_ID,
+ CODEBIRDS_MIMO_V25_PRO_MODEL_ID,
),
).toBe(false)
expect(
isFreeModeAllowedAgentModel(
'base2-free-mimo',
- `${FREEBUFF_MIMO_V25_MODEL_ID}-20260527`,
+ `${CODEBIRDS_MIMO_V25_MODEL_ID}-20260527`,
),
).toBe(true)
expect(
@@ -106,22 +106,22 @@ describe('free mode agent model allowlist', () => {
expect(
isFreeModeAllowedAgentModel(
'base2-free-minimax-m3',
- FREEBUFF_MINIMAX_MODEL_ID,
+ CODEBIRDS_MINIMAX_MODEL_ID,
),
).toBe(false)
})
- test('allows each freebuff reviewer agent only with its configured model', () => {
+ test('allows each codebirds reviewer agent only with its configured model', () => {
expect(
isFreeModeAllowedAgentModel(
'code-reviewer-minimax',
- FREEBUFF_MINIMAX_MODEL_ID,
+ CODEBIRDS_MINIMAX_MODEL_ID,
),
).toBe(true)
expect(
isFreeModeAllowedAgentModel(
'code-reviewer-minimax',
- FREEBUFF_KIMI_MODEL_ID,
+ CODEBIRDS_KIMI_MODEL_ID,
),
).toBe(false)
expect(
@@ -133,61 +133,61 @@ describe('free mode agent model allowlist', () => {
expect(
isFreeModeAllowedAgentModel(
'code-reviewer-minimax-m3',
- FREEBUFF_MINIMAX_MODEL_ID,
+ CODEBIRDS_MINIMAX_MODEL_ID,
),
).toBe(false)
expect(
- isFreeModeAllowedAgentModel('code-reviewer-kimi', FREEBUFF_KIMI_MODEL_ID),
+ isFreeModeAllowedAgentModel('code-reviewer-kimi', CODEBIRDS_KIMI_MODEL_ID),
).toBe(true)
expect(
isFreeModeAllowedAgentModel(
'code-reviewer-deepseek',
- FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID,
+ CODEBIRDS_DEEPSEEK_V4_PRO_MODEL_ID,
),
).toBe(true)
expect(
isFreeModeAllowedAgentModel(
'code-reviewer-deepseek-flash',
- FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID,
+ CODEBIRDS_DEEPSEEK_V4_FLASH_MODEL_ID,
),
).toBe(true)
expect(
isFreeModeAllowedAgentModel(
'code-reviewer-mimo-pro',
- FREEBUFF_MIMO_V25_PRO_MODEL_ID,
+ CODEBIRDS_MIMO_V25_PRO_MODEL_ID,
),
).toBe(true)
expect(
isFreeModeAllowedAgentModel(
'code-reviewer-mimo',
- FREEBUFF_MIMO_V25_MODEL_ID,
+ CODEBIRDS_MIMO_V25_MODEL_ID,
),
).toBe(true)
})
- test('allows legacy code-reviewer-lite with freebuff reviewer models', () => {
+ test('allows legacy code-reviewer-lite with codebirds reviewer models', () => {
expect(
isFreeModeAllowedAgentModel(
'code-reviewer-lite',
- FREEBUFF_MINIMAX_MODEL_ID,
+ CODEBIRDS_MINIMAX_MODEL_ID,
),
).toBe(true)
expect(
isFreeModeAllowedAgentModel('code-reviewer-lite', MINIMAX_M3_MODEL_ID),
).toBe(false)
expect(
- isFreeModeAllowedAgentModel('code-reviewer-lite', FREEBUFF_KIMI_MODEL_ID),
+ isFreeModeAllowedAgentModel('code-reviewer-lite', CODEBIRDS_KIMI_MODEL_ID),
).toBe(true)
expect(
isFreeModeAllowedAgentModel(
'code-reviewer-lite',
- FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID,
+ CODEBIRDS_DEEPSEEK_V4_PRO_MODEL_ID,
),
).toBe(true)
expect(
isFreeModeAllowedAgentModel(
'code-reviewer-lite',
- FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID,
+ CODEBIRDS_DEEPSEEK_V4_FLASH_MODEL_ID,
),
).toBe(true)
})
@@ -203,79 +203,79 @@ describe('free mode agent model allowlist', () => {
test('allows the tmux-cli subagent with its bundled model', () => {
expect(
- isFreeModeAllowedAgentModel('tmux-cli', FREEBUFF_MINIMAX_MODEL_ID),
+ isFreeModeAllowedAgentModel('tmux-cli', CODEBIRDS_MINIMAX_MODEL_ID),
).toBe(true)
expect(
isFreeModeAllowedAgentModel(
- 'codebuff/tmux-cli@0.0.1',
- FREEBUFF_MINIMAX_MODEL_ID,
+ 'codebirds/tmux-cli@0.0.1',
+ CODEBIRDS_MINIMAX_MODEL_ID,
),
).toBe(true)
expect(
isFreeModeAllowedAgentModel(
'other/tmux-cli@0.0.1',
- FREEBUFF_MINIMAX_MODEL_ID,
+ CODEBIRDS_MINIMAX_MODEL_ID,
),
).toBe(false)
})
- test('allows Gemini Pro for the thinker subagent but not the freebuff root', () => {
+ test('allows Gemini Pro for the thinker subagent but not the codebirds root', () => {
expect(
- isFreeModeAllowedAgentModel('base2-free', FREEBUFF_GEMINI_PRO_MODEL_ID),
+ isFreeModeAllowedAgentModel('base2-free', CODEBIRDS_GEMINI_PRO_MODEL_ID),
).toBe(false)
expect(
isFreeModeAllowedAgentModel(
- FREEBUFF_GEMINI_THINKER_AGENT_ID,
- FREEBUFF_GEMINI_PRO_MODEL_ID,
+ CODEBIRDS_GEMINI_THINKER_AGENT_ID,
+ CODEBIRDS_GEMINI_PRO_MODEL_ID,
),
).toBe(true)
})
test('recognizes the Gemini thinker agent in free mode', () => {
- expect(isFreebuffGeminiThinkerAgent(FREEBUFF_GEMINI_THINKER_AGENT_ID)).toBe(
+ expect(isFreebuffGeminiThinkerAgent(CODEBIRDS_GEMINI_THINKER_AGENT_ID)).toBe(
true,
)
expect(
isFreebuffGeminiThinkerAgent(
- `codebuff/${FREEBUFF_GEMINI_THINKER_AGENT_ID}@0.0.1`,
+ `codebirds/${CODEBIRDS_GEMINI_THINKER_AGENT_ID}@0.0.1`,
),
).toBe(true)
expect(
isFreebuffGeminiThinkerAgent(
- `other/${FREEBUFF_GEMINI_THINKER_AGENT_ID}@0.0.1`,
+ `other/${CODEBIRDS_GEMINI_THINKER_AGENT_ID}@0.0.1`,
),
).toBe(false)
})
- test('uses local token count only for the DeepSeek Flash freebuff root', () => {
+ test('uses local token count only for the DeepSeek Flash codebirds root', () => {
expect(
shouldUseLocalTokenCountForFreebuffDeepseekFlash({
agentId: 'base2-free-deepseek-flash',
- model: FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID,
+ model: CODEBIRDS_DEEPSEEK_V4_FLASH_MODEL_ID,
}),
).toBe(true)
expect(
shouldUseLocalTokenCountForFreebuffDeepseekFlash({
- agentId: 'codebuff/base2-free-deepseek-flash@0.0.1',
- model: FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID,
+ agentId: 'codebirds/base2-free-deepseek-flash@0.0.1',
+ model: CODEBIRDS_DEEPSEEK_V4_FLASH_MODEL_ID,
}),
).toBe(true)
expect(
shouldUseLocalTokenCountForFreebuffDeepseekFlash({
agentId: 'base2-free-deepseek',
- model: FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID,
+ model: CODEBIRDS_DEEPSEEK_V4_FLASH_MODEL_ID,
}),
).toBe(false)
expect(
shouldUseLocalTokenCountForFreebuffDeepseekFlash({
agentId: 'base2-free-deepseek-flash',
- model: FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID,
+ model: CODEBIRDS_DEEPSEEK_V4_PRO_MODEL_ID,
}),
).toBe(false)
expect(
shouldUseLocalTokenCountForFreebuffDeepseekFlash({
agentId: 'other/base2-free-deepseek-flash@0.0.1',
- model: FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID,
+ model: CODEBIRDS_DEEPSEEK_V4_FLASH_MODEL_ID,
}),
).toBe(false)
})
diff --git a/common/src/__tests__/freebuff-models.test.ts b/common/src/__tests__/freebuff-models.test.ts
index c9253fb31c..335687e033 100644
--- a/common/src/__tests__/freebuff-models.test.ts
+++ b/common/src/__tests__/freebuff-models.test.ts
@@ -2,20 +2,20 @@ import { describe, expect, test } from 'bun:test'
import {
canFreebuffModelSpawnGeminiThinker,
- DEFAULT_FREEBUFF_MODEL_ID,
- FALLBACK_FREEBUFF_MODEL_ID,
- FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID,
- FREEBUFF_DATA_COLLECTION_WARNING,
- FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID,
- FREEBUFF_ENABLE_MIMO_MODELS_IN_UI,
- FREEBUFF_KIMI_MODEL_ID,
- LIMITED_FREEBUFF_MODEL_ID,
- LIMITED_FREEBUFF_MODEL_IDS,
- FREEBUFF_MINIMAX_MODEL_ID,
- FREEBUFF_MIMO_V25_MODEL_ID,
- FREEBUFF_MIMO_V25_PRO_MODEL_ID,
- FREEBUFF_MODELS,
- SUPPORTED_FREEBUFF_MODELS,
+ DEFAULT_CODEBIRDS_MODEL_ID,
+ FALLBACK_CODEBIRDS_MODEL_ID,
+ CODEBIRDS_DEEPSEEK_V4_FLASH_MODEL_ID,
+ CODEBIRDS_DATA_COLLECTION_WARNING,
+ CODEBIRDS_DEEPSEEK_V4_PRO_MODEL_ID,
+ CODEBIRDS_ENABLE_MIMO_MODELS_IN_UI,
+ CODEBIRDS_KIMI_MODEL_ID,
+ LIMITED_CODEBIRDS_MODEL_ID,
+ LIMITED_CODEBIRDS_MODEL_IDS,
+ CODEBIRDS_MINIMAX_MODEL_ID,
+ CODEBIRDS_MIMO_V25_MODEL_ID,
+ CODEBIRDS_MIMO_V25_PRO_MODEL_ID,
+ CODEBIRDS_MODELS,
+ SUPPORTED_CODEBIRDS_MODELS,
getFreebuffDeploymentAvailabilityLabel,
getFreebuffModelsForAccessTier,
getRecommendedFreebuffModelId,
@@ -26,21 +26,21 @@ import {
isFreebuffPremiumModelId,
isSupportedFreebuffModelId,
resolveFreebuffModelForAccessTier,
-} from '../constants/freebuff-models'
-import type { FreebuffModelOption } from '../constants/freebuff-models'
+} from '../constants/codebirds-models'
+import type { FreebuffModelOption } from '../constants/codebirds-models'
import { minimaxModels } from '../constants/model-config'
const MINIMAX_M3_MODEL_ID = minimaxModels.minimaxM3
-describe('freebuff model availability', () => {
+describe('codebirds model availability', () => {
test('defaults to MiniMax M3, falls back to DeepSeek V4 Flash for new clients', () => {
- expect(DEFAULT_FREEBUFF_MODEL_ID).toBe(MINIMAX_M3_MODEL_ID)
- expect(FALLBACK_FREEBUFF_MODEL_ID).toBe(FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID)
+ expect(DEFAULT_CODEBIRDS_MODEL_ID).toBe(MINIMAX_M3_MODEL_ID)
+ expect(FALLBACK_CODEBIRDS_MODEL_ID).toBe(CODEBIRDS_DEEPSEEK_V4_FLASH_MODEL_ID)
})
test('DeepSeek Pro carries the data-collection warning so users see it before picking', () => {
- const deepseek = FREEBUFF_MODELS.find(
- (m) => m.id === FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID,
+ const deepseek = CODEBIRDS_MODELS.find(
+ (m) => m.id === CODEBIRDS_DEEPSEEK_V4_PRO_MODEL_ID,
)
expect((deepseek as { warning?: string } | undefined)?.warning).toBe(
'Collects data for training',
@@ -48,8 +48,8 @@ describe('freebuff model availability', () => {
})
test('DeepSeek Flash carries the data-collection warning so users see it before picking', () => {
- const deepseek = FREEBUFF_MODELS.find(
- (m) => m.id === FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID,
+ const deepseek = CODEBIRDS_MODELS.find(
+ (m) => m.id === CODEBIRDS_DEEPSEEK_V4_FLASH_MODEL_ID,
)
expect((deepseek as { warning?: string } | undefined)?.warning).toBe(
'Collects data for training',
@@ -57,111 +57,111 @@ describe('freebuff model availability', () => {
})
test('only the DeepSeek family is trace-stored in free mode; M3 has no warning', () => {
- const m3 = FREEBUFF_MODELS.find((m) => m.id === MINIMAX_M3_MODEL_ID)
+ const m3 = CODEBIRDS_MODELS.find((m) => m.id === MINIMAX_M3_MODEL_ID)
expect((m3 as { warning?: string } | undefined)?.warning).toBeUndefined()
// The DeepSeek family discloses data collection and IS stored.
- expect(isFreebuffTracedModelId(FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID)).toBe(true)
- expect(isFreebuffTracedModelId(FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID)).toBe(
+ expect(isFreebuffTracedModelId(CODEBIRDS_DEEPSEEK_V4_PRO_MODEL_ID)).toBe(true)
+ expect(isFreebuffTracedModelId(CODEBIRDS_DEEPSEEK_V4_FLASH_MODEL_ID)).toBe(
true,
)
// Everything else (incl. M3 on Fireworks) is NOT stored.
expect(isFreebuffTracedModelId(MINIMAX_M3_MODEL_ID)).toBe(false)
- expect(isFreebuffTracedModelId(FREEBUFF_KIMI_MODEL_ID)).toBe(false)
- expect(isFreebuffTracedModelId(FREEBUFF_MIMO_V25_MODEL_ID)).toBe(false)
+ expect(isFreebuffTracedModelId(CODEBIRDS_KIMI_MODEL_ID)).toBe(false)
+ expect(isFreebuffTracedModelId(CODEBIRDS_MIMO_V25_MODEL_ID)).toBe(false)
expect(isFreebuffTracedModelId(null)).toBe(false)
})
test('trace storage is one source of truth with the data-collection warning', () => {
// A model is traced in free mode iff it shows the data-collection caveat.
- const models: readonly FreebuffModelOption[] = SUPPORTED_FREEBUFF_MODELS
+ const models: readonly FreebuffModelOption[] = SUPPORTED_CODEBIRDS_MODELS
for (const model of models) {
expect(isFreebuffTracedModelId(model.id)).toBe(
- model.warning === FREEBUFF_DATA_COLLECTION_WARNING,
+ model.warning === CODEBIRDS_DATA_COLLECTION_WARNING,
)
}
})
test('DeepSeek V4 Flash is selectable and non-premium', () => {
- expect(FREEBUFF_MODELS.map((model) => model.id)).toContain(
- FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID,
+ expect(CODEBIRDS_MODELS.map((model) => model.id)).toContain(
+ CODEBIRDS_DEEPSEEK_V4_FLASH_MODEL_ID,
)
- expect(isFreebuffModelId(FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID)).toBe(true)
- expect(isFreebuffPremiumModelId(FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID)).toBe(
+ expect(isFreebuffModelId(CODEBIRDS_DEEPSEEK_V4_FLASH_MODEL_ID)).toBe(true)
+ expect(isFreebuffPremiumModelId(CODEBIRDS_DEEPSEEK_V4_FLASH_MODEL_ID)).toBe(
false,
)
})
test('MiMo models remain supported and follow the UI rollout flag', () => {
- expect(SUPPORTED_FREEBUFF_MODELS.map((model) => model.id)).toContain(
- FREEBUFF_MIMO_V25_PRO_MODEL_ID,
+ expect(SUPPORTED_CODEBIRDS_MODELS.map((model) => model.id)).toContain(
+ CODEBIRDS_MIMO_V25_PRO_MODEL_ID,
)
- expect(SUPPORTED_FREEBUFF_MODELS.map((model) => model.id)).toContain(
- FREEBUFF_MIMO_V25_MODEL_ID,
+ expect(SUPPORTED_CODEBIRDS_MODELS.map((model) => model.id)).toContain(
+ CODEBIRDS_MIMO_V25_MODEL_ID,
)
- if (FREEBUFF_ENABLE_MIMO_MODELS_IN_UI) {
- expect(FREEBUFF_MODELS.map((model) => model.id)).toContain(
- FREEBUFF_MIMO_V25_PRO_MODEL_ID,
+ if (CODEBIRDS_ENABLE_MIMO_MODELS_IN_UI) {
+ expect(CODEBIRDS_MODELS.map((model) => model.id)).toContain(
+ CODEBIRDS_MIMO_V25_PRO_MODEL_ID,
)
- expect(FREEBUFF_MODELS.map((model) => model.id)).toContain(
- FREEBUFF_MIMO_V25_MODEL_ID,
+ expect(CODEBIRDS_MODELS.map((model) => model.id)).toContain(
+ CODEBIRDS_MIMO_V25_MODEL_ID,
)
} else {
- expect(FREEBUFF_MODELS.map((model) => model.id)).not.toContain(
- FREEBUFF_MIMO_V25_PRO_MODEL_ID,
+ expect(CODEBIRDS_MODELS.map((model) => model.id)).not.toContain(
+ CODEBIRDS_MIMO_V25_PRO_MODEL_ID,
)
- expect(FREEBUFF_MODELS.map((model) => model.id)).not.toContain(
- FREEBUFF_MIMO_V25_MODEL_ID,
+ expect(CODEBIRDS_MODELS.map((model) => model.id)).not.toContain(
+ CODEBIRDS_MIMO_V25_MODEL_ID,
)
}
- expect(isFreebuffPremiumModelId(FREEBUFF_MIMO_V25_PRO_MODEL_ID)).toBe(true)
- expect(isFreebuffPremiumModelId(FREEBUFF_MIMO_V25_MODEL_ID)).toBe(false)
+ expect(isFreebuffPremiumModelId(CODEBIRDS_MIMO_V25_PRO_MODEL_ID)).toBe(true)
+ expect(isFreebuffPremiumModelId(CODEBIRDS_MIMO_V25_MODEL_ID)).toBe(false)
})
test('Kimi is selectable in full mode', () => {
- expect(SUPPORTED_FREEBUFF_MODELS.map((model) => model.id)).toContain(
- FREEBUFF_KIMI_MODEL_ID,
+ expect(SUPPORTED_CODEBIRDS_MODELS.map((model) => model.id)).toContain(
+ CODEBIRDS_KIMI_MODEL_ID,
)
- expect(FREEBUFF_MODELS.map((model) => model.id)).toContain(
- FREEBUFF_KIMI_MODEL_ID,
+ expect(CODEBIRDS_MODELS.map((model) => model.id)).toContain(
+ CODEBIRDS_KIMI_MODEL_ID,
)
expect(getFreebuffModelsForAccessTier('full').map((m) => m.id)).toContain(
- FREEBUFF_KIMI_MODEL_ID,
+ CODEBIRDS_KIMI_MODEL_ID,
)
- expect(isFreebuffModelId(FREEBUFF_KIMI_MODEL_ID)).toBe(true)
- expect(isSupportedFreebuffModelId(FREEBUFF_KIMI_MODEL_ID)).toBe(true)
+ expect(isFreebuffModelId(CODEBIRDS_KIMI_MODEL_ID)).toBe(true)
+ expect(isSupportedFreebuffModelId(CODEBIRDS_KIMI_MODEL_ID)).toBe(true)
expect(
- isFreebuffModelAllowedForAccessTier(FREEBUFF_KIMI_MODEL_ID, 'full'),
+ isFreebuffModelAllowedForAccessTier(CODEBIRDS_KIMI_MODEL_ID, 'full'),
).toBe(true)
})
test('MiniMax M2.7 is legacy: hidden from pickers but still served for old clients', () => {
- expect(SUPPORTED_FREEBUFF_MODELS.map((model) => model.id)).toContain(
- FREEBUFF_MINIMAX_MODEL_ID,
+ expect(SUPPORTED_CODEBIRDS_MODELS.map((model) => model.id)).toContain(
+ CODEBIRDS_MINIMAX_MODEL_ID,
)
- expect(FREEBUFF_MODELS.map((model) => model.id)).not.toContain(
- FREEBUFF_MINIMAX_MODEL_ID,
+ expect(CODEBIRDS_MODELS.map((model) => model.id)).not.toContain(
+ CODEBIRDS_MINIMAX_MODEL_ID,
)
expect(
getFreebuffModelsForAccessTier('full').map((m) => m.id),
- ).not.toContain(FREEBUFF_MINIMAX_MODEL_ID)
- expect(isFreebuffModelId(FREEBUFF_MINIMAX_MODEL_ID)).toBe(false)
- expect(isSupportedFreebuffModelId(FREEBUFF_MINIMAX_MODEL_ID)).toBe(true)
+ ).not.toContain(CODEBIRDS_MINIMAX_MODEL_ID)
+ expect(isFreebuffModelId(CODEBIRDS_MINIMAX_MODEL_ID)).toBe(false)
+ expect(isSupportedFreebuffModelId(CODEBIRDS_MINIMAX_MODEL_ID)).toBe(true)
// Old clients with a saved M2.7 selection must still be admitted.
expect(
- isFreebuffModelAllowedForAccessTier(FREEBUFF_MINIMAX_MODEL_ID, 'full'),
+ isFreebuffModelAllowedForAccessTier(CODEBIRDS_MINIMAX_MODEL_ID, 'full'),
).toBe(true)
expect(
- resolveFreebuffModelForAccessTier(FREEBUFF_MINIMAX_MODEL_ID, 'full'),
- ).toBe(FREEBUFF_MINIMAX_MODEL_ID)
+ resolveFreebuffModelForAccessTier(CODEBIRDS_MINIMAX_MODEL_ID, 'full'),
+ ).toBe(CODEBIRDS_MINIMAX_MODEL_ID)
})
test('MiniMax M3 is a selectable unlimited model, last in the unlimited section', () => {
- expect(SUPPORTED_FREEBUFF_MODELS.map((model) => model.id)).toContain(
+ expect(SUPPORTED_CODEBIRDS_MODELS.map((model) => model.id)).toContain(
MINIMAX_M3_MODEL_ID,
)
- expect(FREEBUFF_MODELS.map((model) => model.id)).toContain(
+ expect(CODEBIRDS_MODELS.map((model) => model.id)).toContain(
MINIMAX_M3_MODEL_ID,
)
expect(
@@ -174,52 +174,52 @@ describe('freebuff model availability', () => {
isFreebuffModelAllowedForAccessTier(MINIMAX_M3_MODEL_ID, 'full'),
).toBe(true)
// Pickers split sections by the premium flag while preserving array order,
- // so "last unlimited entry" means last in FREEBUFF_MODELS overall.
- expect(FREEBUFF_MODELS[FREEBUFF_MODELS.length - 1]!.id).toBe(
+ // so "last unlimited entry" means last in CODEBIRDS_MODELS overall.
+ expect(CODEBIRDS_MODELS[CODEBIRDS_MODELS.length - 1]!.id).toBe(
MINIMAX_M3_MODEL_ID,
)
})
test('limited access exposes DeepSeek V4 Flash and non-Pro MiMo 2.5', () => {
- expect(LIMITED_FREEBUFF_MODEL_ID).toBe(FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID)
- expect(LIMITED_FREEBUFF_MODEL_IDS).toEqual([
- FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID,
- FREEBUFF_MIMO_V25_MODEL_ID,
+ expect(LIMITED_CODEBIRDS_MODEL_ID).toBe(CODEBIRDS_DEEPSEEK_V4_FLASH_MODEL_ID)
+ expect(LIMITED_CODEBIRDS_MODEL_IDS).toEqual([
+ CODEBIRDS_DEEPSEEK_V4_FLASH_MODEL_ID,
+ CODEBIRDS_MIMO_V25_MODEL_ID,
])
expect(getFreebuffModelsForAccessTier('limited').map((m) => m.id)).toEqual([
- FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID,
- FREEBUFF_MIMO_V25_MODEL_ID,
+ CODEBIRDS_DEEPSEEK_V4_FLASH_MODEL_ID,
+ CODEBIRDS_MIMO_V25_MODEL_ID,
])
expect(
isFreebuffModelAllowedForAccessTier(
- FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID,
+ CODEBIRDS_DEEPSEEK_V4_FLASH_MODEL_ID,
'limited',
),
).toBe(true)
expect(
- isFreebuffModelAllowedForAccessTier(FREEBUFF_MINIMAX_MODEL_ID, 'limited'),
+ isFreebuffModelAllowedForAccessTier(CODEBIRDS_MINIMAX_MODEL_ID, 'limited'),
).toBe(false)
expect(
isFreebuffModelAllowedForAccessTier(MINIMAX_M3_MODEL_ID, 'limited'),
).toBe(false)
expect(
isFreebuffModelAllowedForAccessTier(
- FREEBUFF_MIMO_V25_MODEL_ID,
+ CODEBIRDS_MIMO_V25_MODEL_ID,
'limited',
),
).toBe(true)
expect(
isFreebuffModelAllowedForAccessTier(
- FREEBUFF_MIMO_V25_PRO_MODEL_ID,
+ CODEBIRDS_MIMO_V25_PRO_MODEL_ID,
'limited',
),
).toBe(false)
expect(
- resolveFreebuffModelForAccessTier(FREEBUFF_MIMO_V25_MODEL_ID, 'limited'),
- ).toBe(FREEBUFF_MIMO_V25_MODEL_ID)
+ resolveFreebuffModelForAccessTier(CODEBIRDS_MIMO_V25_MODEL_ID, 'limited'),
+ ).toBe(CODEBIRDS_MIMO_V25_MODEL_ID)
expect(
- resolveFreebuffModelForAccessTier(FREEBUFF_MINIMAX_MODEL_ID, 'limited'),
- ).toBe(FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID)
+ resolveFreebuffModelForAccessTier(CODEBIRDS_MINIMAX_MODEL_ID, 'limited'),
+ ).toBe(CODEBIRDS_DEEPSEEK_V4_FLASH_MODEL_ID)
})
test('recommends an unlimited, in-tier model for the picker hero', () => {
@@ -232,7 +232,7 @@ describe('freebuff model availability', () => {
)
// Limited access → DeepSeek V4 Flash, which is in the limited model set.
expect(getRecommendedFreebuffModelId('limited')).toBe(
- FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID,
+ CODEBIRDS_DEEPSEEK_V4_FLASH_MODEL_ID,
)
expect(
getFreebuffModelsForAccessTier('limited').some(
@@ -241,36 +241,36 @@ describe('freebuff model availability', () => {
).toBe(true)
})
- test('full-access freebuff models can spawn the gemini-thinker subagent', () => {
+ test('full-access codebirds models can spawn the gemini-thinker subagent', () => {
// Full-access models (non-limited, non-fastest) get the thinker.
- expect(canFreebuffModelSpawnGeminiThinker(FREEBUFF_KIMI_MODEL_ID)).toBe(
+ expect(canFreebuffModelSpawnGeminiThinker(CODEBIRDS_KIMI_MODEL_ID)).toBe(
true,
)
expect(
- canFreebuffModelSpawnGeminiThinker(FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID),
+ canFreebuffModelSpawnGeminiThinker(CODEBIRDS_DEEPSEEK_V4_PRO_MODEL_ID),
).toBe(true)
expect(
- canFreebuffModelSpawnGeminiThinker(FREEBUFF_MIMO_V25_PRO_MODEL_ID),
+ canFreebuffModelSpawnGeminiThinker(CODEBIRDS_MIMO_V25_PRO_MODEL_ID),
).toBe(true)
expect(canFreebuffModelSpawnGeminiThinker(MINIMAX_M3_MODEL_ID)).toBe(true)
// Legacy "Fastest" MiniMax M2.7 skips it to preserve the fastest tier.
- expect(canFreebuffModelSpawnGeminiThinker(FREEBUFF_MINIMAX_MODEL_ID)).toBe(
+ expect(canFreebuffModelSpawnGeminiThinker(CODEBIRDS_MINIMAX_MODEL_ID)).toBe(
false,
)
// Limited-tier models (DeepSeek V4 Flash, MiMo 2.5) skip it.
expect(
- canFreebuffModelSpawnGeminiThinker(FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID),
+ canFreebuffModelSpawnGeminiThinker(CODEBIRDS_DEEPSEEK_V4_FLASH_MODEL_ID),
).toBe(false)
- expect(canFreebuffModelSpawnGeminiThinker(FREEBUFF_MIMO_V25_MODEL_ID)).toBe(
+ expect(canFreebuffModelSpawnGeminiThinker(CODEBIRDS_MIMO_V25_MODEL_ID)).toBe(
false,
)
})
- test('does not support GLM 5.1 for freebuff sessions', () => {
+ test('does not support GLM 5.1 for codebirds sessions', () => {
const glm = 'z-ai/glm-5.1'
- expect(FREEBUFF_MODELS.map((model) => model.id)).not.toContain(glm)
- expect(SUPPORTED_FREEBUFF_MODELS.map((model) => model.id)).not.toContain(
+ expect(CODEBIRDS_MODELS.map((model) => model.id)).not.toContain(glm)
+ expect(SUPPORTED_CODEBIRDS_MODELS.map((model) => model.id)).not.toContain(
glm,
)
expect(isFreebuffModelId(glm)).toBe(false)
diff --git a/common/src/__tests__/freebuff-referral-tiers.test.ts b/common/src/__tests__/freebuff-referral-tiers.test.ts
index bcd280fc83..84ea2daf16 100644
--- a/common/src/__tests__/freebuff-referral-tiers.test.ts
+++ b/common/src/__tests__/freebuff-referral-tiers.test.ts
@@ -1,16 +1,16 @@
import { describe, expect, it } from 'bun:test'
import {
- FREEBUFF_REFERRAL_TIERS,
- FREEBUFF_WATERMARK_REMOVAL_REFERRALS,
- FREEBUFF_WATERMARK_REMOVAL_TIER,
- MAX_FREEBUFF_REFERRAL_TIER,
+ CODEBIRDS_REFERRAL_TIERS,
+ CODEBIRDS_WATERMARK_REMOVAL_REFERRALS,
+ CODEBIRDS_WATERMARK_REMOVAL_TIER,
+ MAX_CODEBIRDS_REFERRAL_TIER,
MIN_GITHUB_ACCOUNT_AGE_MONTHS,
getNextReferralTier,
getReferralTier,
getTierLimits,
isGithubAccountOldEnoughForReferral,
-} from '../constants/freebuff-referral-tiers'
+} from '../constants/codebirds-referral-tiers'
const NOW = Date.parse('2026-06-12T00:00:00Z')
@@ -20,11 +20,11 @@ function monthsAgo(months: number): number {
return date.getTime()
}
-describe('FREEBUFF_REFERRAL_TIERS', () => {
+describe('CODEBIRDS_REFERRAL_TIERS', () => {
it('is sorted ascending by referrals required with strictly growing limits', () => {
- for (let i = 1; i < FREEBUFF_REFERRAL_TIERS.length; i++) {
- const prev = FREEBUFF_REFERRAL_TIERS[i - 1]
- const next = FREEBUFF_REFERRAL_TIERS[i]
+ for (let i = 1; i < CODEBIRDS_REFERRAL_TIERS.length; i++) {
+ const prev = CODEBIRDS_REFERRAL_TIERS[i - 1]
+ const next = CODEBIRDS_REFERRAL_TIERS[i]
expect(next.tier).toBe(prev.tier + 1)
expect(next.referralsRequired).toBeGreaterThan(prev.referralsRequired)
expect(next.standardModelDailyLimit).toBeGreaterThan(
@@ -37,7 +37,7 @@ describe('FREEBUFF_REFERRAL_TIERS', () => {
})
it('starts at tier 0 with 0 referrals and the watermark on', () => {
- expect(FREEBUFF_REFERRAL_TIERS[0]).toMatchObject({
+ expect(CODEBIRDS_REFERRAL_TIERS[0]).toMatchObject({
tier: 0,
referralsRequired: 0,
removesWatermark: false,
@@ -45,12 +45,12 @@ describe('FREEBUFF_REFERRAL_TIERS', () => {
})
it('exposes the watermark unlock tier', () => {
- expect(FREEBUFF_WATERMARK_REMOVAL_TIER).toBe(1)
- expect(FREEBUFF_WATERMARK_REMOVAL_REFERRALS).toBe(1)
+ expect(CODEBIRDS_WATERMARK_REMOVAL_TIER).toBe(1)
+ expect(CODEBIRDS_WATERMARK_REMOVAL_REFERRALS).toBe(1)
})
it('follows the 1 / +2 (3) / +4 (7) referral ladder', () => {
- expect(FREEBUFF_REFERRAL_TIERS.map((t) => t.referralsRequired)).toEqual([
+ expect(CODEBIRDS_REFERRAL_TIERS.map((t) => t.referralsRequired)).toEqual([
0, 1, 3, 7,
])
})
@@ -63,8 +63,8 @@ describe('getReferralTier', () => {
expect(getReferralTier(2).tier).toBe(1)
expect(getReferralTier(3).tier).toBe(2)
expect(getReferralTier(6).tier).toBe(2)
- expect(getReferralTier(7).tier).toBe(MAX_FREEBUFF_REFERRAL_TIER)
- expect(getReferralTier(100).tier).toBe(MAX_FREEBUFF_REFERRAL_TIER)
+ expect(getReferralTier(7).tier).toBe(MAX_CODEBIRDS_REFERRAL_TIER)
+ expect(getReferralTier(100).tier).toBe(MAX_CODEBIRDS_REFERRAL_TIER)
})
it('treats null/undefined/negative counts as tier 0', () => {
@@ -77,10 +77,10 @@ describe('getReferralTier', () => {
describe('getTierLimits', () => {
it('returns the tier row and clamps out-of-range tiers', () => {
expect(getTierLimits(1).standardModelDailyLimit).toBe(
- FREEBUFF_REFERRAL_TIERS[1].standardModelDailyLimit,
+ CODEBIRDS_REFERRAL_TIERS[1].standardModelDailyLimit,
)
expect(getTierLimits(-1).tier).toBe(0)
- expect(getTierLimits(99).tier).toBe(MAX_FREEBUFF_REFERRAL_TIER)
+ expect(getTierLimits(99).tier).toBe(MAX_CODEBIRDS_REFERRAL_TIER)
})
})
diff --git a/common/src/__tests__/handlesteps-parsing.test.ts b/common/src/__tests__/handlesteps-parsing.test.ts
index 1edd4160af..d70d449d0c 100644
--- a/common/src/__tests__/handlesteps-parsing.test.ts
+++ b/common/src/__tests__/handlesteps-parsing.test.ts
@@ -6,7 +6,7 @@ import { DynamicAgentDefinitionSchema } from '../types/dynamic-agent-template'
import type { DynamicAgentTemplate } from '../types/dynamic-agent-template'
import type { AgentState } from '../types/session-state'
import type { ProjectFileContext } from '../util/file'
-import type { Logger } from '@codebuff/common/types/contracts/logger'
+import type { Logger } from '@codebirds/common/types/contracts/logger'
describe('handleSteps Parsing Tests', () => {
let mockFileContext: ProjectFileContext
diff --git a/common/src/actions.ts b/common/src/actions.ts
index eb5304fba9..296e08dce0 100644
--- a/common/src/actions.ts
+++ b/common/src/actions.ts
@@ -2,7 +2,7 @@ import { z } from 'zod/v4'
import type { GrantType } from './types/grant'
import type { MCPConfig } from './types/mcp'
-import type { ToolMessage } from './types/messages/codebuff-message'
+import type { ToolMessage } from './types/messages/codebirds-message'
import type {
TextPart,
ImagePart,
diff --git a/common/src/analytics.ts b/common/src/analytics.ts
index ea88cf7e59..f854086074 100644
--- a/common/src/analytics.ts
+++ b/common/src/analytics.ts
@@ -1,10 +1,10 @@
-import { env, DEBUG_ANALYTICS } from '@codebuff/common/env'
+import { env, DEBUG_ANALYTICS } from '@codebirds/common/env'
import { createPostHogClient, type AnalyticsClient } from './analytics-core'
import { AnalyticsEvent } from './constants/analytics-events'
-import type { TrackEventFn } from '@codebuff/common/types/contracts/analytics'
-import type { Logger } from '@codebuff/common/types/contracts/logger'
+import type { TrackEventFn } from '@codebirds/common/types/contracts/analytics'
+import type { Logger } from '@codebirds/common/types/contracts/logger'
let client: AnalyticsClient | undefined
diff --git a/common/src/constants/agents.ts b/common/src/constants/agents.ts
index 5737b77614..2c375ef612 100644
--- a/common/src/constants/agents.ts
+++ b/common/src/constants/agents.ts
@@ -44,7 +44,7 @@ export const AGENT_PERSONAS = {
} as const,
'agent-builder': {
displayName: 'Bob the Agent Builder',
- purpose: 'Creates new agent templates for the codebuff multi-agent system',
+ purpose: 'Creates new agent templates for the codebirds multi-agent system',
hidden: false,
} as const,
} as const satisfies Partial<
@@ -60,7 +60,7 @@ export const AGENT_IDS = Object.keys(
) as (keyof typeof AGENT_PERSONAS)[]
// Agent ID prefix constant
-export const AGENT_ID_PREFIX = 'CodebuffAI/'
+export const AGENT_ID_PREFIX = 'CodebirdsAI/'
// Agent names for client-side reference
export const AGENT_NAMES = Object.fromEntries(
diff --git a/common/src/constants/analytics-events.ts b/common/src/constants/analytics-events.ts
index 96ce1075ec..aa4ef1c5ab 100644
--- a/common/src/constants/analytics-events.ts
+++ b/common/src/constants/analytics-events.ts
@@ -5,7 +5,7 @@ export enum AnalyticsEvent {
// Cross-surface — DAU
// Emitted exactly once per user-submitted message/prompt, on each surface
// (cli / web / chat), and never sampled. `distinct_id` is the canonical
- // codebuff Postgres user id on every surface, so unique-users of this event
+ // codebirds Postgres user id on every surface, so unique-users of this event
// gives accurate per-surface DAU (filter on the `surface` property) and a
// combined DAU (no filter). The `surface` property is one of: cli, web, chat,
// desktop.
@@ -40,7 +40,7 @@ export enum AnalyticsEvent {
SLASH_COMMAND_USED = 'cli.slash_command_used',
TERMINAL_COMMAND_COMPLETED = 'cli.terminal_command_completed',
USER_INPUT_COMPLETE = 'cli.user_input_complete',
- UPDATE_CODEBUFF_FAILED = 'cli.update_codebuff_failed',
+ UPDATE_CODEBIRDS_FAILED = 'cli.update_codebirds_failed',
FEEDBACK_BUTTON_HOVERED = 'cli.feedback_button_hovered',
FOLLOWUP_CLICKED = 'cli.followup_clicked',
SUGGESTED_PROMPT_SHOWN = 'cli.suggested_prompt_shown',
@@ -92,7 +92,7 @@ export enum AnalyticsEvent {
ONBOARD_PAGE_INSTALL_COMMAND_COPIED = 'onboard_page.install_command_copied',
// Web - Creator Attribution
- CODEBUFF_REFERRER_ATTRIBUTED = 'codebuff.referrer_attributed',
+ CODEBIRDS_REFERRER_ATTRIBUTED = 'codebirds.referrer_attributed',
// Web - Install Dialog
INSTALL_DIALOG_CD_COMMAND_COPIED = 'install_dialog.cd_command_copied',
@@ -183,7 +183,7 @@ export enum AnalyticsEvent {
CHATGPT_OAUTH_AUTH_ERROR = 'sdk.chatgpt_oauth_auth_error',
// Freebuff - Creator Attribution
- FREEBUFF_REFERRER_ATTRIBUTED = 'freebuff.referrer_attributed',
+ CODEBIRDS_REFERRER_ATTRIBUTED = 'codebirds.referrer_attributed',
// Freebuff - Referral program server lifecycle (emitted from packages/billing
// via the server logger → Axiom `event` column). Funnel: redeemed → completed
@@ -192,48 +192,48 @@ export enum AnalyticsEvent {
// per-evaluation event — that would fire on every live trigger and dominate
// ingest; it rides on `sweep`, which aggregates outcomes across the whole
// pending population once per run (see ReferralSweepResult.outcomes).
- FREEBUFF_REFERRAL_REDEEMED = 'freebuff.referral.redeemed',
- FREEBUFF_REFERRAL_COMPLETED = 'freebuff.referral.completed',
- FREEBUFF_REFERRAL_SWEEP = 'freebuff.referral.sweep',
+ CODEBIRDS_REFERRAL_REDEEMED = 'codebirds.referral.redeemed',
+ CODEBIRDS_REFERRAL_COMPLETED = 'codebirds.referral.completed',
+ CODEBIRDS_REFERRAL_SWEEP = 'codebirds.referral.sweep',
// Freebuff - Get Started Page (referral onboarding funnel, in order:
// viewed → sign_in_clicked → signed_in → eligibility_resolved →
// [connect_github_clicked] → install_command_copied | web_clicked).
// Every event carries a `referrer` prop (the inviter's name) for per-referrer
// funnel breakdowns.
- FREEBUFF_GET_STARTED_VIEWED = 'freebuff.get_started_viewed',
- FREEBUFF_GET_STARTED_SIGN_IN_CLICKED = 'freebuff.get_started_sign_in_clicked',
- FREEBUFF_GET_STARTED_SIGNED_IN = 'freebuff.get_started_signed_in',
- FREEBUFF_GET_STARTED_ELIGIBILITY_RESOLVED = 'freebuff.get_started_eligibility_resolved',
- FREEBUFF_GET_STARTED_CONNECT_GITHUB_CLICKED = 'freebuff.get_started_connect_github_clicked',
- FREEBUFF_GET_STARTED_INSTALL_COMMAND_COPIED = 'freebuff.get_started_install_command_copied',
- FREEBUFF_GET_STARTED_WEB_CLICKED = 'freebuff.get_started_web_clicked',
+ CODEBIRDS_GET_STARTED_VIEWED = 'codebirds.get_started_viewed',
+ CODEBIRDS_GET_STARTED_SIGN_IN_CLICKED = 'codebirds.get_started_sign_in_clicked',
+ CODEBIRDS_GET_STARTED_SIGNED_IN = 'codebirds.get_started_signed_in',
+ CODEBIRDS_GET_STARTED_ELIGIBILITY_RESOLVED = 'codebirds.get_started_eligibility_resolved',
+ CODEBIRDS_GET_STARTED_CONNECT_GITHUB_CLICKED = 'codebirds.get_started_connect_github_clicked',
+ CODEBIRDS_GET_STARTED_INSTALL_COMMAND_COPIED = 'codebirds.get_started_install_command_copied',
+ CODEBIRDS_GET_STARTED_WEB_CLICKED = 'codebirds.get_started_web_clicked',
// Deprecated (previous get-started design — no longer fired):
- FREEBUFF_GET_STARTED_HELP_EXPANDED = 'freebuff.get_started_help_expanded',
- FREEBUFF_GET_STARTED_EDITOR_CLICKED = 'freebuff.get_started_editor_clicked',
+ CODEBIRDS_GET_STARTED_HELP_EXPANDED = 'codebirds.get_started_help_expanded',
+ CODEBIRDS_GET_STARTED_EDITOR_CLICKED = 'codebirds.get_started_editor_clicked',
// Freebuff - Chat
// Emitted once per new-thread title generation attempt (server-side). The
// `outcome` property is one of: generated | empty | unknown_model | error |
// aborted. Carries `latencyMs`, `model`, and `titleLength` so the failure/
// fallback rate and added latency are queryable.
- FREEBUFF_CHAT_TITLE_GENERATED = 'freebuff.chat_title_generated',
+ CODEBIRDS_CHAT_TITLE_GENERATED = 'codebirds.chat_title_generated',
// Freebuff - Home Page
- FREEBUFF_HOME_INSTALL_COMMAND_COPIED = 'freebuff.home_install_command_copied',
- FREEBUFF_HOME_GITHUB_CLICKED = 'freebuff.home_github_clicked',
- FREEBUFF_HOME_INSTALL_GUIDE_EXPANDED = 'freebuff.home_install_guide_expanded',
- FREEBUFF_HOME_FAQ_OPENED = 'freebuff.home_faq_opened',
+ CODEBIRDS_HOME_INSTALL_COMMAND_COPIED = 'codebirds.home_install_command_copied',
+ CODEBIRDS_HOME_GITHUB_CLICKED = 'codebirds.home_github_clicked',
+ CODEBIRDS_HOME_INSTALL_GUIDE_EXPANDED = 'codebirds.home_install_guide_expanded',
+ CODEBIRDS_HOME_FAQ_OPENED = 'codebirds.home_faq_opened',
// Freebuff - acquisition attribution (UTM / ad-click params captured as
// super-properties; filter by utm_source, reddit_click_id, is_reddit_traffic)
- FREEBUFF_ATTRIBUTED = 'freebuff.attributed',
+ CODEBIRDS_ATTRIBUTED = 'codebirds.attributed',
// Freebuff - Reddit ad funnel (filter in PostHog by reddit_click_id / utm_source)
- FREEBUFF_REDDIT_FUNNEL_CLI_INSTALLED = 'freebuff.reddit_funnel.cli_installed',
- FREEBUFF_REDDIT_FUNNEL_LOGIN = 'freebuff.reddit_funnel.login',
- FREEBUFF_REDDIT_FUNNEL_SIGN_UP = 'freebuff.reddit_funnel.sign_up',
- FREEBUFF_REDDIT_FUNNEL_FIRST_PROMPT = 'freebuff.reddit_funnel.first_prompt',
- FREEBUFF_REDDIT_FUNNEL_GRAVITY_AD_CLICK = 'freebuff.reddit_funnel.gravity_ad_click',
+ CODEBIRDS_REDDIT_FUNNEL_CLI_INSTALLED = 'codebirds.reddit_funnel.cli_installed',
+ CODEBIRDS_REDDIT_FUNNEL_LOGIN = 'codebirds.reddit_funnel.login',
+ CODEBIRDS_REDDIT_FUNNEL_SIGN_UP = 'codebirds.reddit_funnel.sign_up',
+ CODEBIRDS_REDDIT_FUNNEL_FIRST_PROMPT = 'codebirds.reddit_funnel.first_prompt',
+ CODEBIRDS_REDDIT_FUNNEL_GRAVITY_AD_CLICK = 'codebirds.reddit_funnel.gravity_ad_click',
// Freebuff Desktop (Electron app)
// Mirrors the CLI's surface events so the desktop shows up in the same DAU /
diff --git a/common/src/constants/byok.ts b/common/src/constants/byok.ts
index 50640d41e1..2844ca9da5 100644
--- a/common/src/constants/byok.ts
+++ b/common/src/constants/byok.ts
@@ -1,2 +1,2 @@
export const BYOK_OPENROUTER_HEADER = 'x-openrouter-api-key'
-export const BYOK_OPENROUTER_ENV_VAR = 'CODEBUFF_BYOK_OPENROUTER'
+export const BYOK_OPENROUTER_ENV_VAR = 'CODEBIRDS_BYOK_OPENROUTER'
diff --git a/common/src/constants/chatgpt-oauth.ts b/common/src/constants/chatgpt-oauth.ts
index ded5ba48e0..83e8ee735f 100644
--- a/common/src/constants/chatgpt-oauth.ts
+++ b/common/src/constants/chatgpt-oauth.ts
@@ -22,7 +22,7 @@ export const CHATGPT_OAUTH_REDIRECT_URI = 'http://localhost:1455/auth/callback'
export const CHATGPT_BACKEND_BASE_URL = 'https://chatgpt.com/backend-api'
/** Environment variable for OAuth token override. */
-export const CHATGPT_OAUTH_TOKEN_ENV_VAR = 'CODEBUFF_CHATGPT_OAUTH_TOKEN'
+export const CHATGPT_OAUTH_TOKEN_ENV_VAR = 'CODEBIRDS_CHATGPT_OAUTH_TOKEN'
/**
* OpenRouter-style model IDs that are allowed for ChatGPT OAuth direct routing.
diff --git a/common/src/constants/free-agents.ts b/common/src/constants/free-agents.ts
index b594a47070..8c5261cfe3 100644
--- a/common/src/constants/free-agents.ts
+++ b/common/src/constants/free-agents.ts
@@ -1,20 +1,20 @@
import { parseAgentId } from '../util/agent-id-parsing'
import {
- FREEBUFF_GEMINI_PRO_AGENT_IDS,
- FREEBUFF_GEMINI_THINKER_AGENT_ID,
-} from './freebuff-gemini-thinker'
+ CODEBIRDS_GEMINI_PRO_AGENT_IDS,
+ CODEBIRDS_GEMINI_THINKER_AGENT_ID,
+} from './codebirds-gemini-thinker'
import {
- FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID,
- FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID,
- FREEBUFF_GEMINI_PRO_MODEL_ID,
- FREEBUFF_GLM_V52_MODEL_ID,
- FREEBUFF_KIMI_MODEL_ID,
- FREEBUFF_MINIMAX_MODEL_ID,
- FREEBUFF_MINIMAX_M3_MODEL_ID,
- FREEBUFF_MIMO_V25_MODEL_ID,
- FREEBUFF_MIMO_V25_PRO_MODEL_ID,
-} from './freebuff-models'
+ CODEBIRDS_DEEPSEEK_V4_FLASH_MODEL_ID,
+ CODEBIRDS_DEEPSEEK_V4_PRO_MODEL_ID,
+ CODEBIRDS_GEMINI_PRO_MODEL_ID,
+ CODEBIRDS_GLM_V52_MODEL_ID,
+ CODEBIRDS_KIMI_MODEL_ID,
+ CODEBIRDS_MINIMAX_MODEL_ID,
+ CODEBIRDS_MINIMAX_M3_MODEL_ID,
+ CODEBIRDS_MIMO_V25_MODEL_ID,
+ CODEBIRDS_MIMO_V25_PRO_MODEL_ID,
+} from './codebirds-models'
import type { CostMode } from './model-config'
@@ -25,12 +25,12 @@ import type { CostMode } from './model-config'
export const FREE_COST_MODE = 'free' as const
/**
- * Root-orchestrator agent IDs counted as "a freebuff session" for abuse
+ * Root-orchestrator agent IDs counted as "a codebirds session" for abuse
* detection and usage auditing. Subagents (file-picker, basher, etc.) are
* excluded — they're spawned by the root, so counting them would inflate
* every user's apparent activity.
*/
-export const FREEBUFF_ROOT_AGENT_IDS = [
+export const CODEBIRDS_ROOT_AGENT_IDS = [
'base2-free',
'base2-free-kimi',
'base2-free-deepseek',
@@ -40,34 +40,34 @@ export const FREEBUFF_ROOT_AGENT_IDS = [
'base2-free-minimax-m3',
'base2-free-glm',
] as const
-const FREEBUFF_ROOT_AGENT_ID_SET: ReadonlySet = new Set(
- FREEBUFF_ROOT_AGENT_IDS,
+const CODEBIRDS_ROOT_AGENT_ID_SET: ReadonlySet = new Set(
+ CODEBIRDS_ROOT_AGENT_IDS,
)
-export const FREEBUFF_ROOT_AGENT_ID_BY_MODEL: Record = {
- [FREEBUFF_MIMO_V25_PRO_MODEL_ID]: 'base2-free-mimo-pro',
- [FREEBUFF_MIMO_V25_MODEL_ID]: 'base2-free-mimo',
- [FREEBUFF_MINIMAX_MODEL_ID]: 'base2-free',
- [FREEBUFF_MINIMAX_M3_MODEL_ID]: 'base2-free-minimax-m3',
- [FREEBUFF_KIMI_MODEL_ID]: 'base2-free-kimi',
- [FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID]: 'base2-free-deepseek',
- [FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID]: 'base2-free-deepseek-flash',
- [FREEBUFF_GLM_V52_MODEL_ID]: 'base2-free-glm',
+export const CODEBIRDS_ROOT_AGENT_ID_BY_MODEL: Record = {
+ [CODEBIRDS_MIMO_V25_PRO_MODEL_ID]: 'base2-free-mimo-pro',
+ [CODEBIRDS_MIMO_V25_MODEL_ID]: 'base2-free-mimo',
+ [CODEBIRDS_MINIMAX_MODEL_ID]: 'base2-free',
+ [CODEBIRDS_MINIMAX_M3_MODEL_ID]: 'base2-free-minimax-m3',
+ [CODEBIRDS_KIMI_MODEL_ID]: 'base2-free-kimi',
+ [CODEBIRDS_DEEPSEEK_V4_PRO_MODEL_ID]: 'base2-free-deepseek',
+ [CODEBIRDS_DEEPSEEK_V4_FLASH_MODEL_ID]: 'base2-free-deepseek-flash',
+ [CODEBIRDS_GLM_V52_MODEL_ID]: 'base2-free-glm',
}
-export const FREEBUFF_REVIEWER_AGENT_ID_BY_MODEL: Record = {
- [FREEBUFF_MIMO_V25_PRO_MODEL_ID]: 'code-reviewer-mimo-pro',
- [FREEBUFF_MIMO_V25_MODEL_ID]: 'code-reviewer-mimo',
- [FREEBUFF_MINIMAX_MODEL_ID]: 'code-reviewer-minimax',
- [FREEBUFF_MINIMAX_M3_MODEL_ID]: 'code-reviewer-minimax-m3',
- [FREEBUFF_KIMI_MODEL_ID]: 'code-reviewer-kimi',
- [FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID]: 'code-reviewer-deepseek',
- [FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID]: 'code-reviewer-deepseek-flash',
- [FREEBUFF_GLM_V52_MODEL_ID]: 'code-reviewer-glm',
+export const CODEBIRDS_REVIEWER_AGENT_ID_BY_MODEL: Record = {
+ [CODEBIRDS_MIMO_V25_PRO_MODEL_ID]: 'code-reviewer-mimo-pro',
+ [CODEBIRDS_MIMO_V25_MODEL_ID]: 'code-reviewer-mimo',
+ [CODEBIRDS_MINIMAX_MODEL_ID]: 'code-reviewer-minimax',
+ [CODEBIRDS_MINIMAX_M3_MODEL_ID]: 'code-reviewer-minimax-m3',
+ [CODEBIRDS_KIMI_MODEL_ID]: 'code-reviewer-kimi',
+ [CODEBIRDS_DEEPSEEK_V4_PRO_MODEL_ID]: 'code-reviewer-deepseek',
+ [CODEBIRDS_DEEPSEEK_V4_FLASH_MODEL_ID]: 'code-reviewer-deepseek-flash',
+ [CODEBIRDS_GLM_V52_MODEL_ID]: 'code-reviewer-glm',
}
export function getFreebuffRootAgentIdForModel(model: string): string {
- return FREEBUFF_ROOT_AGENT_ID_BY_MODEL[model] ?? 'base2-free'
+ return CODEBIRDS_ROOT_AGENT_ID_BY_MODEL[model] ?? 'base2-free'
}
/**
@@ -81,20 +81,20 @@ export function getFreebuffRootAgentIdForModel(model: string): string {
export const FREE_MODE_AGENT_MODELS: Record> = {
// Root orchestrator
'base2-free': new Set([
- FREEBUFF_MINIMAX_MODEL_ID,
- FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID,
- FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID,
- FREEBUFF_KIMI_MODEL_ID,
- FREEBUFF_MIMO_V25_PRO_MODEL_ID,
- FREEBUFF_MIMO_V25_MODEL_ID,
+ CODEBIRDS_MINIMAX_MODEL_ID,
+ CODEBIRDS_DEEPSEEK_V4_PRO_MODEL_ID,
+ CODEBIRDS_DEEPSEEK_V4_FLASH_MODEL_ID,
+ CODEBIRDS_KIMI_MODEL_ID,
+ CODEBIRDS_MIMO_V25_PRO_MODEL_ID,
+ CODEBIRDS_MIMO_V25_MODEL_ID,
]),
- 'base2-free-kimi': new Set([FREEBUFF_KIMI_MODEL_ID]),
- 'base2-free-deepseek': new Set([FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID]),
- 'base2-free-deepseek-flash': new Set([FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID]),
- 'base2-free-mimo-pro': new Set([FREEBUFF_MIMO_V25_PRO_MODEL_ID]),
- 'base2-free-mimo': new Set([FREEBUFF_MIMO_V25_MODEL_ID]),
- 'base2-free-minimax-m3': new Set([FREEBUFF_MINIMAX_M3_MODEL_ID]),
- 'base2-free-glm': new Set([FREEBUFF_GLM_V52_MODEL_ID]),
+ 'base2-free-kimi': new Set([CODEBIRDS_KIMI_MODEL_ID]),
+ 'base2-free-deepseek': new Set([CODEBIRDS_DEEPSEEK_V4_PRO_MODEL_ID]),
+ 'base2-free-deepseek-flash': new Set([CODEBIRDS_DEEPSEEK_V4_FLASH_MODEL_ID]),
+ 'base2-free-mimo-pro': new Set([CODEBIRDS_MIMO_V25_PRO_MODEL_ID]),
+ 'base2-free-mimo': new Set([CODEBIRDS_MIMO_V25_MODEL_ID]),
+ 'base2-free-minimax-m3': new Set([CODEBIRDS_MINIMAX_M3_MODEL_ID]),
+ 'base2-free-glm': new Set([CODEBIRDS_GLM_V52_MODEL_ID]),
// File exploration agents
'file-picker': new Set(['google/gemini-2.5-flash-lite']),
@@ -110,32 +110,32 @@ export const FREE_MODE_AGENT_MODELS: Record> = {
// Command execution
basher: new Set(['google/gemini-3.1-flash-lite-preview']),
- 'tmux-cli': new Set([FREEBUFF_MINIMAX_MODEL_ID]),
+ 'tmux-cli': new Set([CODEBIRDS_MINIMAX_MODEL_ID]),
// Code reviewer for free mode
- 'code-reviewer-minimax': new Set([FREEBUFF_MINIMAX_MODEL_ID]),
- 'code-reviewer-minimax-m3': new Set([FREEBUFF_MINIMAX_M3_MODEL_ID]),
- 'code-reviewer-kimi': new Set([FREEBUFF_KIMI_MODEL_ID]),
- 'code-reviewer-deepseek': new Set([FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID]),
+ 'code-reviewer-minimax': new Set([CODEBIRDS_MINIMAX_MODEL_ID]),
+ 'code-reviewer-minimax-m3': new Set([CODEBIRDS_MINIMAX_M3_MODEL_ID]),
+ 'code-reviewer-kimi': new Set([CODEBIRDS_KIMI_MODEL_ID]),
+ 'code-reviewer-deepseek': new Set([CODEBIRDS_DEEPSEEK_V4_PRO_MODEL_ID]),
'code-reviewer-deepseek-flash': new Set([
- FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID,
+ CODEBIRDS_DEEPSEEK_V4_FLASH_MODEL_ID,
]),
- 'code-reviewer-mimo-pro': new Set([FREEBUFF_MIMO_V25_PRO_MODEL_ID]),
- 'code-reviewer-mimo': new Set([FREEBUFF_MIMO_V25_MODEL_ID]),
- 'code-reviewer-glm': new Set([FREEBUFF_GLM_V52_MODEL_ID]),
- // Legacy freebuff clients spawned code-reviewer-lite under provider-specific
+ 'code-reviewer-mimo-pro': new Set([CODEBIRDS_MIMO_V25_PRO_MODEL_ID]),
+ 'code-reviewer-mimo': new Set([CODEBIRDS_MIMO_V25_MODEL_ID]),
+ 'code-reviewer-glm': new Set([CODEBIRDS_GLM_V52_MODEL_ID]),
+ // Legacy codebirds clients spawned code-reviewer-lite under provider-specific
// free roots before those reviewer IDs existed.
'code-reviewer-lite': new Set([
- FREEBUFF_MINIMAX_MODEL_ID,
- FREEBUFF_KIMI_MODEL_ID,
- FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID,
- FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID,
- FREEBUFF_MIMO_V25_PRO_MODEL_ID,
- FREEBUFF_MIMO_V25_MODEL_ID,
+ CODEBIRDS_MINIMAX_MODEL_ID,
+ CODEBIRDS_KIMI_MODEL_ID,
+ CODEBIRDS_DEEPSEEK_V4_PRO_MODEL_ID,
+ CODEBIRDS_DEEPSEEK_V4_FLASH_MODEL_ID,
+ CODEBIRDS_MIMO_V25_PRO_MODEL_ID,
+ CODEBIRDS_MIMO_V25_MODEL_ID,
]),
// Legacy: kept for the standalone gemini thinker agent if invoked directly.
- [FREEBUFF_GEMINI_THINKER_AGENT_ID]: new Set([FREEBUFF_GEMINI_PRO_MODEL_ID]),
+ [CODEBIRDS_GEMINI_THINKER_AGENT_ID]: new Set([CODEBIRDS_GEMINI_PRO_MODEL_ID]),
}
/**
@@ -172,28 +172,28 @@ export function isFreeMode(costMode: CostMode | string | undefined): boolean {
export function isFreebuffRootAgent(fullAgentId: string): boolean {
const { publisherId, agentId } = parseAgentId(fullAgentId)
if (!agentId) return false
- if (publisherId && publisherId !== 'codebuff') return false
- return FREEBUFF_ROOT_AGENT_ID_SET.has(agentId)
+ if (publisherId && publisherId !== 'codebirds') return false
+ return CODEBIRDS_ROOT_AGENT_ID_SET.has(agentId)
}
export function isFreebuffGeminiThinkerAgent(fullAgentId: string): boolean {
const { publisherId, agentId } = parseAgentId(fullAgentId)
if (!agentId) return false
- if (publisherId && publisherId !== 'codebuff') return false
- return agentId === FREEBUFF_GEMINI_THINKER_AGENT_ID
+ if (publisherId && publisherId !== 'codebirds') return false
+ return agentId === CODEBIRDS_GEMINI_THINKER_AGENT_ID
}
/**
* True if this agent is permitted to call the premium Gemini Pro model — i.e.
* one of the two gemini-thinker subagents (CLI `thinker-with-files-gemini` or
* chat `thinker-gemini`). Publisher-spoof-safe like the other gates: a
- * non-codebuff publisher never matches.
+ * non-codebirds publisher never matches.
*/
export function isFreebuffGeminiProAgent(fullAgentId: string): boolean {
const { publisherId, agentId } = parseAgentId(fullAgentId)
if (!agentId) return false
- if (publisherId && publisherId !== 'codebuff') return false
- return FREEBUFF_GEMINI_PRO_AGENT_IDS.has(agentId)
+ if (publisherId && publisherId !== 'codebirds') return false
+ return CODEBIRDS_GEMINI_PRO_AGENT_IDS.has(agentId)
}
export function shouldUseLocalTokenCountForFreebuffDeepseekFlash(params: {
@@ -201,12 +201,12 @@ export function shouldUseLocalTokenCountForFreebuffDeepseekFlash(params: {
model: string | undefined
}): boolean {
const { agentId: fullAgentId, model } = params
- if (!fullAgentId || model !== FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID) {
+ if (!fullAgentId || model !== CODEBIRDS_DEEPSEEK_V4_FLASH_MODEL_ID) {
return false
}
const { publisherId, agentId } = parseAgentId(fullAgentId)
- if (publisherId && publisherId !== 'codebuff') return false
+ if (publisherId && publisherId !== 'codebirds') return false
return agentId === 'base2-free-deepseek-flash'
}
@@ -217,7 +217,7 @@ export function shouldUseLocalTokenCountForFreebuffDeepseekFlash(params: {
* Returns true only if:
* 1. The agent has a valid agent ID
* 2. The agent is in the allowed free-mode agents list
- * 3. The agent is either internal or published by 'codebuff' (prevents spoofing)
+ * 3. The agent is either internal or published by 'codebirds' (prevents spoofing)
* 4. The model is in that agent's allowed model set
*/
export function isFreeModeAllowedAgentModel(
@@ -229,8 +229,8 @@ export function isFreeModeAllowedAgentModel(
// Must have a valid agent ID
if (!agentId) return false
- // Must be either internal (no publisher) or from codebuff
- if (publisherId && publisherId !== 'codebuff') return false
+ // Must be either internal (no publisher) or from codebirds
+ if (publisherId && publisherId !== 'codebirds') return false
// Get the allowed models for this agent
const allowedModels = FREE_MODE_AGENT_MODELS[agentId]
@@ -265,7 +265,7 @@ export function isFreeModeAllowedAgentModel(
* Handles all agent ID formats:
* - 'file-picker'
* - 'file-picker@1.0.0'
- * - 'codebuff/file-picker@0.0.2'
+ * - 'codebirds/file-picker@0.0.2'
*/
export function isFreeAgent(fullAgentId: string): boolean {
const { publisherId, agentId } = parseAgentId(fullAgentId)
@@ -276,9 +276,9 @@ export function isFreeAgent(fullAgentId: string): boolean {
// Must be in the free tier agents list
if (!FREE_TIER_AGENTS.has(agentId)) return false
- // Must be either internal (no publisher) or from codebuff
+ // Must be either internal (no publisher) or from codebirds
// This prevents publisher spoofing attacks
- if (publisherId && publisherId !== 'codebuff') return false
+ if (publisherId && publisherId !== 'codebirds') return false
return true
}
diff --git a/common/src/constants/freebuff-gemini-thinker.ts b/common/src/constants/freebuff-gemini-thinker.ts
index f9b49aa573..b082ae0cb2 100644
--- a/common/src/constants/freebuff-gemini-thinker.ts
+++ b/common/src/constants/freebuff-gemini-thinker.ts
@@ -1,30 +1,30 @@
-export const FREEBUFF_GEMINI_THINKER_AGENT_ID = 'thinker-with-files-gemini'
+export const CODEBIRDS_GEMINI_THINKER_AGENT_ID = 'thinker-with-files-gemini'
-/** Agent id the freebuff WEB CHAT uses for its gemini-thinker child (see
+/** Agent id the codebirds WEB CHAT uses for its gemini-thinker child (see
* agents/thinker/thinker-gemini.ts, spawned from agents/base-chat.ts). Distinct
- * from the CLI's FREEBUFF_GEMINI_THINKER_AGENT_ID. */
-export const FREEBUFF_CHAT_GEMINI_THINKER_AGENT_ID = 'thinker-gemini'
+ * from the CLI's CODEBIRDS_GEMINI_THINKER_AGENT_ID. */
+export const CODEBIRDS_CHAT_GEMINI_THINKER_AGENT_ID = 'thinker-gemini'
/** The ONLY agents permitted to call the premium Gemini Pro model. Gemini Pro is
* not a user-pickable model — its only legitimate callers are these two thinker
* subagents (CLI + chat). The chat-completions endpoint rejects any other agent
* that requests Gemini Pro on an unbilled path. */
-export const FREEBUFF_GEMINI_PRO_AGENT_IDS: ReadonlySet = new Set([
- FREEBUFF_GEMINI_THINKER_AGENT_ID,
- FREEBUFF_CHAT_GEMINI_THINKER_AGENT_ID,
+export const CODEBIRDS_GEMINI_PRO_AGENT_IDS: ReadonlySet = new Set([
+ CODEBIRDS_GEMINI_THINKER_AGENT_ID,
+ CODEBIRDS_CHAT_GEMINI_THINKER_AGENT_ID,
])
-export const FREEBUFF_GEMINI_THINKER_SYSTEM_INSTRUCTION =
+export const CODEBIRDS_GEMINI_THINKER_SYSTEM_INSTRUCTION =
"Spawn the thinker-with-files-gemini agent to think through problems worth reasoning about -- it's very smart. Reach for it on non-trivial bugs, uncertain approaches, and tricky decisions, not just the hardest tasks. Skip it for routine, clearly-scoped edits. Pass the relevant filePaths since it has no conversation history."
-export const FREEBUFF_GEMINI_THINKER_INSTRUCTIONS_PROMPT =
+export const CODEBIRDS_GEMINI_THINKER_INSTRUCTIONS_PROMPT =
'- For problems worth thinking through -- non-trivial bugs, uncertain approaches, or tricky decisions -- spawn the thinker-with-files-gemini agent after gathering context, not just for the hardest tasks. Skip it for routine, clearly-scoped edits. Pass the relevant filePaths.'
-export const FREEBUFF_GEMINI_THINKER_STEP_PROMPT =
+export const CODEBIRDS_GEMINI_THINKER_STEP_PROMPT =
'For non-trivial problems or decisions, spawn the thinker-with-files-gemini agent to think it through. Skip it for routine edits. Pass the relevant filePaths.'
-export const FREEBUFF_GEMINI_THINKER_PROMPT_LINES = [
- FREEBUFF_GEMINI_THINKER_SYSTEM_INSTRUCTION,
- FREEBUFF_GEMINI_THINKER_INSTRUCTIONS_PROMPT,
- FREEBUFF_GEMINI_THINKER_STEP_PROMPT,
+export const CODEBIRDS_GEMINI_THINKER_PROMPT_LINES = [
+ CODEBIRDS_GEMINI_THINKER_SYSTEM_INSTRUCTION,
+ CODEBIRDS_GEMINI_THINKER_INSTRUCTIONS_PROMPT,
+ CODEBIRDS_GEMINI_THINKER_STEP_PROMPT,
] as const
diff --git a/common/src/constants/freebuff-models.ts b/common/src/constants/freebuff-models.ts
index 2ee68aeb1a..c0dec11e4c 100644
--- a/common/src/constants/freebuff-models.ts
+++ b/common/src/constants/freebuff-models.ts
@@ -7,7 +7,7 @@ import {
import { mimoModels, minimaxModels, moonshotModels } from './model-config'
/**
- * Models a freebuff user can pick between in the waiting-room model selector.
+ * Models a codebirds user can pick between in the waiting-room model selector.
*
* Each model has its own queue (server keys queue position by `model`), so the
* list here is effectively the set of separate waiting lines. Order is the
@@ -28,8 +28,8 @@ export interface FreebuffModelOption {
* picking the model. */
warning?: string
/** Premium models carry a per-day usage limit
- * (FREEBUFF_PREMIUM_SESSION_LIMIT). Surfaced in the UI as a "Premium"
- * badge with the limit. Derived from FREEBUFF_PREMIUM_MODEL_IDS so the two
+ * (CODEBIRDS_PREMIUM_SESSION_LIMIT). Surfaced in the UI as a "Premium"
+ * badge with the limit. Derived from CODEBIRDS_PREMIUM_MODEL_IDS so the two
* never drift. */
premium: boolean
/** Whether the model accepts image input. Drives whether uploaded images
@@ -40,72 +40,72 @@ export interface FreebuffModelOption {
/** Server-facing fallback copy for APIs and provider errors that can't know
* the caller's local timezone. The CLI should render
* `getFreebuffDeploymentAvailabilityLabel()` instead. */
-export const FREEBUFF_DEPLOYMENT_HOURS_LABEL = '9am ET-5pm PT every day'
-export const FREEBUFF_GEMINI_PRO_MODEL_ID = 'google/gemini-3.1-pro-preview'
-export const FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID = 'deepseek/deepseek-v4-pro'
-export const FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID = 'deepseek/deepseek-v4-flash'
+export const CODEBIRDS_DEPLOYMENT_HOURS_LABEL = '9am ET-5pm PT every day'
+export const CODEBIRDS_GEMINI_PRO_MODEL_ID = 'google/gemini-3.1-pro-preview'
+export const CODEBIRDS_DEEPSEEK_V4_PRO_MODEL_ID = 'deepseek/deepseek-v4-pro'
+export const CODEBIRDS_DEEPSEEK_V4_FLASH_MODEL_ID = 'deepseek/deepseek-v4-flash'
/** DeepSeek V4 Flash served by Fireworks instead of DeepSeek's direct API.
- * Used only by freebuff.com/chat, where Fireworks' faster inference is worth
- * a slightly less capable serving stack. Not in SUPPORTED_FREEBUFF_MODELS or
+ * Used only by codebirds.com/chat, where Fireworks' faster inference is worth
+ * a slightly less capable serving stack. Not in SUPPORTED_CODEBIRDS_MODELS or
* the free-mode allowlists — the CLI and web builder keep DeepSeek direct. */
-export const FREEBUFF_DEEPSEEK_V4_FLASH_FIREWORKS_MODEL_ID =
+export const CODEBIRDS_DEEPSEEK_V4_FLASH_FIREWORKS_MODEL_ID =
'fireworks/deepseek-v4-flash'
-export const FREEBUFF_KIMI_MODEL_ID = moonshotModels.kimiK26
+export const CODEBIRDS_KIMI_MODEL_ID = moonshotModels.kimiK26
/** Legacy: removed from the pickers on 2026-06-09 in favor of MiniMax M3, but
* still server-supported so old clients keep working. Drop from
- * SUPPORTED_FREEBUFF_MODELS after ~2026-06-16. */
-export const FREEBUFF_MINIMAX_MODEL_ID = 'minimax/minimax-m2.7'
+ * SUPPORTED_CODEBIRDS_MODELS after ~2026-06-16. */
+export const CODEBIRDS_MINIMAX_MODEL_ID = 'minimax/minimax-m2.7'
/** Routes to MiniMax's official API (distinct from the m2.7 id). */
-export const FREEBUFF_MINIMAX_M3_MODEL_ID = minimaxModels.minimaxM3
-export const FREEBUFF_MIMO_V25_MODEL_ID = mimoModels.mimoV25
-export const FREEBUFF_MIMO_V25_PRO_MODEL_ID = mimoModels.mimoV25Pro
+export const CODEBIRDS_MINIMAX_M3_MODEL_ID = minimaxModels.minimaxM3
+export const CODEBIRDS_MIMO_V25_MODEL_ID = mimoModels.mimoV25
+export const CODEBIRDS_MIMO_V25_PRO_MODEL_ID = mimoModels.mimoV25Pro
/** GLM 5.2 (Z.ai), served by Fireworks serverless. Unlike the other picker
* models it is NOT freely available — it is unlocked by referring friends.
* Each qualified referral grants one 1-hour GLM session per week (capped at
- * FREEBUFF_GLM_V52_REFERRAL_CAP). Gated by a per-user weekly session pool whose
+ * CODEBIRDS_GLM_V52_REFERRAL_CAP). Gated by a per-user weekly session pool whose
* limit equals the caller's GLM referral score (see the free-session quota). */
-export const FREEBUFF_GLM_V52_MODEL_ID = 'z-ai/glm-5.2'
+export const CODEBIRDS_GLM_V52_MODEL_ID = 'z-ai/glm-5.2'
/** UI-only rollout switch. Backend support and free-mode allowlists remain
* wired even when these models are hidden from the Freebuff picker. */
-export const FREEBUFF_ENABLE_MIMO_MODELS_IN_UI = true
+export const CODEBIRDS_ENABLE_MIMO_MODELS_IN_UI = true
/** UI-only rollout switch for the streak indicator in the waiting room. */
-export const FREEBUFF_ENABLE_STREAK_IN_UI = true
+export const CODEBIRDS_ENABLE_STREAK_IN_UI = true
/** Local/debug switch: force the localhost free-mode country bypass into
* limited access so the limited Freebuff UX can be exercised without an env
* var. */
-export const FREEBUFF_FORCE_LIMITED_MODE = false
-export const FREEBUFF_PREMIUM_SESSION_LIMIT = 5
-export const FREEBUFF_LIMITED_SESSION_LIMIT = 5
-export const FREEBUFF_PREMIUM_SESSION_RESET_TIMEZONE = 'America/Los_Angeles'
-export const FREEBUFF_PREMIUM_SESSION_PERIOD = 'pacific_day'
+export const CODEBIRDS_FORCE_LIMITED_MODE = false
+export const CODEBIRDS_PREMIUM_SESSION_LIMIT = 5
+export const CODEBIRDS_LIMITED_SESSION_LIMIT = 5
+export const CODEBIRDS_PREMIUM_SESSION_RESET_TIMEZONE = 'America/Los_Angeles'
+export const CODEBIRDS_PREMIUM_SESSION_PERIOD = 'pacific_day'
/** GLM 5.2 referral-reward session pool. Distinct from the premium daily pool:
* GLM sessions reset weekly (Pacific) and the per-user limit is the caller's
- * GLM referral score, capped at FREEBUFF_GLM_V52_REFERRAL_CAP. */
-export const FREEBUFF_WEEKLY_SESSION_PERIOD = 'pacific_week'
-export const FREEBUFF_GLM_V52_SESSION_RESET_TIMEZONE =
- FREEBUFF_PREMIUM_SESSION_RESET_TIMEZONE
-export const FREEBUFF_GLM_V52_SESSION_WINDOW_HOURS = 24 * 7
+ * GLM referral score, capped at CODEBIRDS_GLM_V52_REFERRAL_CAP. */
+export const CODEBIRDS_WEEKLY_SESSION_PERIOD = 'pacific_week'
+export const CODEBIRDS_GLM_V52_SESSION_RESET_TIMEZONE =
+ CODEBIRDS_PREMIUM_SESSION_RESET_TIMEZONE
+export const CODEBIRDS_GLM_V52_SESSION_WINDOW_HOURS = 24 * 7
/** Max number of qualified referrals that count toward GLM sessions, i.e. the
* most 1-hour GLM sessions a user can earn per week. */
-export const FREEBUFF_GLM_V52_REFERRAL_CAP = 10
+export const CODEBIRDS_GLM_V52_REFERRAL_CAP = 10
/** Master kill-switch for the GLM 5.2 referral program. While true, qualified
* referrals grant weekly GLM sessions and the CLI advertises the perk. Flip to
* false to wind the program down: entitlement drops to 0 for everyone and the
* CLI stops showing the banner. The perk is intentionally framed as
* limited-time in the UI so turning this off isn't a surprise. */
-export const FREEBUFF_GLM_V52_REFERRAL_ENABLED = true
+export const CODEBIRDS_GLM_V52_REFERRAL_ENABLED = true
/** GLM sessions are exactly one hour of wall-clock time, regardless of the
* global free-session length, so the "1 hour per referral per week" promise is
* exact. */
-export const FREEBUFF_GLM_V52_SESSION_LENGTH_MS = 60 * 60 * 1000
-export const FREEBUFF_LIMITED_SESSION_RESET_TIMEZONE =
- FREEBUFF_PREMIUM_SESSION_RESET_TIMEZONE
-export const FREEBUFF_LIMITED_SESSION_PERIOD = FREEBUFF_PREMIUM_SESSION_PERIOD
+export const CODEBIRDS_GLM_V52_SESSION_LENGTH_MS = 60 * 60 * 1000
+export const CODEBIRDS_LIMITED_SESSION_RESET_TIMEZONE =
+ CODEBIRDS_PREMIUM_SESSION_RESET_TIMEZONE
+export const CODEBIRDS_LIMITED_SESSION_PERIOD = CODEBIRDS_PREMIUM_SESSION_PERIOD
/**
* Streak rewards. Maintaining a daily Freebuff streak across a full week earns a
* bonus session: when the user's streak crosses a multiple of
- * `FREEBUFF_STREAK_REWARD_INTERVAL_DAYS` (7, 14, 21, …) they are granted one
+ * `CODEBIRDS_STREAK_REWARD_INTERVAL_DAYS` (7, 14, 21, …) they are granted one
* extra session in their primary pool (premium for full-access users, limited
* for limited-access users), and — for full-access users — one extra GLM 5.2
* weekly session on top of any referral entitlement.
@@ -117,16 +117,16 @@ export const FREEBUFF_LIMITED_SESSION_PERIOD = FREEBUFF_PREMIUM_SESSION_PERIOD
* streak yields roughly one bonus per week — matching the "+1 GLM session per
* week" promise.
*/
-export const FREEBUFF_STREAK_REWARD_INTERVAL_DAYS = 7
+export const CODEBIRDS_STREAK_REWARD_INTERVAL_DAYS = 7
/** Master kill-switch for streak rewards. When false, milestones grant nothing
* and effective limits fall back to the base pool limits. */
-export const FREEBUFF_STREAK_REWARDS_ENABLED = true
+export const CODEBIRDS_STREAK_REWARDS_ENABLED = true
/** Sub-switch for the full-access GLM 5.2 portion of the streak reward. Lets the
* GLM perk be wound down independently of the premium/limited bonus (and of the
* separate referral-driven GLM program). */
-export const FREEBUFF_STREAK_GLM_BONUS_ENABLED = true
+export const CODEBIRDS_STREAK_GLM_BONUS_ENABLED = true
/** Session-units granted per milestone, per pool. One whole session. */
-export const FREEBUFF_STREAK_BONUS_SESSION_UNITS = 1
+export const CODEBIRDS_STREAK_BONUS_SESSION_UNITS = 1
/** Which session pool a streak bonus credit applies to. `premium` and `limited`
* are the daily pools (full vs limited access); `glm` is the weekly GLM 5.2
@@ -134,53 +134,53 @@ export const FREEBUFF_STREAK_BONUS_SESSION_UNITS = 1
export type FreebuffStreakRewardPool = 'premium' | 'limited' | 'glm'
/** Deprecated wire compatibility field. Session usage now resets at midnight
* Pacific time rather than using a rolling hourly window. */
-export const FREEBUFF_PREMIUM_SESSION_WINDOW_HOURS = 24
-export const FREEBUFF_LIMITED_SESSION_WINDOW_HOURS =
- FREEBUFF_PREMIUM_SESSION_WINDOW_HOURS
-const FREEBUFF_EASTERN_TIMEZONE = 'America/New_York'
-const FREEBUFF_PACIFIC_TIMEZONE = 'America/Los_Angeles'
+export const CODEBIRDS_PREMIUM_SESSION_WINDOW_HOURS = 24
+export const CODEBIRDS_LIMITED_SESSION_WINDOW_HOURS =
+ CODEBIRDS_PREMIUM_SESSION_WINDOW_HOURS
+const CODEBIRDS_EASTERN_TIMEZONE = 'America/New_York'
+const CODEBIRDS_PACIFIC_TIMEZONE = 'America/Los_Angeles'
interface LocalTimeFormatOptions {
locale?: string
timeZone?: string
}
-/** Full-access freebuff models that benefit from spawning the gemini-thinker
+/** Full-access codebirds models that benefit from spawning the gemini-thinker
* subagent for deeper reasoning. Covers every full-access picker model except
* the two limited-tier ones (DeepSeek V4 Flash, MiMo 2.5); the legacy
* "Fastest" MiniMax M2.7 also skips it because the extra round-trip would
* defeat that tier. Used by the CLI to toggle the gemini-thinker spawnable +
* prompts based on the user's pick, and by the server to admit gemini-thinker
* child requests against a parent session bound to one of these models. */
-export const FREEBUFF_GEMINI_THINKER_PARENT_MODELS = new Set([
- FREEBUFF_KIMI_MODEL_ID,
- FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID,
- FREEBUFF_MIMO_V25_PRO_MODEL_ID,
- FREEBUFF_MINIMAX_M3_MODEL_ID,
+export const CODEBIRDS_GEMINI_THINKER_PARENT_MODELS = new Set([
+ CODEBIRDS_KIMI_MODEL_ID,
+ CODEBIRDS_DEEPSEEK_V4_PRO_MODEL_ID,
+ CODEBIRDS_MIMO_V25_PRO_MODEL_ID,
+ CODEBIRDS_MINIMAX_M3_MODEL_ID,
])
export function canFreebuffModelSpawnGeminiThinker(modelId: string): boolean {
- return FREEBUFF_GEMINI_THINKER_PARENT_MODELS.has(modelId)
+ return CODEBIRDS_GEMINI_THINKER_PARENT_MODELS.has(modelId)
}
/** Single source of truth for "this model collects data for training". A model
* that carries this exact `warning` is both shown the caveat in the picker AND
* has its chat-completion traces stored in free mode (see
- * FREEBUFF_TRACED_MODEL_IDS, which is derived from it) — the two can't drift. */
-export const FREEBUFF_DATA_COLLECTION_WARNING = 'Collects data for training'
+ * CODEBIRDS_TRACED_MODEL_IDS, which is derived from it) — the two can't drift. */
+export const CODEBIRDS_DATA_COLLECTION_WARNING = 'Collects data for training'
const DEEPSEEK_V4_PRO_MODEL = {
- id: FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID,
+ id: CODEBIRDS_DEEPSEEK_V4_PRO_MODEL_ID,
displayName: 'DeepSeek V4 Pro',
tagline: 'Smartest',
availability: 'always',
- warning: FREEBUFF_DATA_COLLECTION_WARNING,
+ warning: CODEBIRDS_DATA_COLLECTION_WARNING,
premium: true,
multimodal: false,
} as const satisfies FreebuffModelOption
const MIMO_V25_PRO_MODEL = {
- id: FREEBUFF_MIMO_V25_PRO_MODEL_ID,
+ id: CODEBIRDS_MIMO_V25_PRO_MODEL_ID,
displayName: 'MiMo 2.5 Pro',
tagline: 'Smartest & Slow',
availability: 'always',
@@ -189,7 +189,7 @@ const MIMO_V25_PRO_MODEL = {
} as const satisfies FreebuffModelOption
const KIMI_MODEL = {
- id: FREEBUFF_KIMI_MODEL_ID,
+ id: CODEBIRDS_KIMI_MODEL_ID,
displayName: 'Kimi K2.6',
tagline: 'Balanced',
availability: 'always',
@@ -198,7 +198,7 @@ const KIMI_MODEL = {
} as const satisfies FreebuffModelOption
const MIMO_V25_MODEL = {
- id: FREEBUFF_MIMO_V25_MODEL_ID,
+ id: CODEBIRDS_MIMO_V25_MODEL_ID,
displayName: 'MiMo 2.5',
tagline: 'Multimodal',
availability: 'always',
@@ -207,18 +207,18 @@ const MIMO_V25_MODEL = {
} as const satisfies FreebuffModelOption
const DEEPSEEK_V4_FLASH_MODEL = {
- id: FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID,
+ id: CODEBIRDS_DEEPSEEK_V4_FLASH_MODEL_ID,
displayName: 'DeepSeek V4 Flash',
tagline: 'Smart & Fast',
availability: 'always',
- warning: FREEBUFF_DATA_COLLECTION_WARNING,
+ warning: CODEBIRDS_DATA_COLLECTION_WARNING,
premium: false,
multimodal: false,
} as const satisfies FreebuffModelOption
-/** Legacy (not in FREEBUFF_MODELS): see FREEBUFF_MINIMAX_MODEL_ID. */
+/** Legacy (not in CODEBIRDS_MODELS): see CODEBIRDS_MINIMAX_MODEL_ID. */
const MINIMAX_MODEL = {
- id: FREEBUFF_MINIMAX_MODEL_ID,
+ id: CODEBIRDS_MINIMAX_MODEL_ID,
displayName: 'MiniMax M2.7',
tagline: 'Fastest',
availability: 'always',
@@ -227,31 +227,31 @@ const MINIMAX_MODEL = {
} as const satisfies FreebuffModelOption
const MINIMAX_M3_MODEL = {
- id: FREEBUFF_MINIMAX_M3_MODEL_ID,
+ id: CODEBIRDS_MINIMAX_M3_MODEL_ID,
displayName: 'MiniMax M3',
tagline: 'Smartest & Fastest',
availability: 'always',
// No data-collection warning: M3 is served by Fireworks (no provider-side
- // training). Omitting the warning also keeps it out of FREEBUFF_TRACED_MODEL_IDS,
+ // training). Omitting the warning also keeps it out of CODEBIRDS_TRACED_MODEL_IDS,
// so we don't store its traces either.
premium: false,
multimodal: true,
} as const satisfies FreebuffModelOption
const GLM_V52_MODEL = {
- id: FREEBUFF_GLM_V52_MODEL_ID,
+ id: CODEBIRDS_GLM_V52_MODEL_ID,
displayName: 'GLM 5.2',
tagline: 'Unlock by referring friends',
availability: 'always',
// No data-collection warning: served by Fireworks (no provider-side
- // training), and omitting it keeps GLM out of FREEBUFF_TRACED_MODEL_IDS.
+ // training), and omitting it keeps GLM out of CODEBIRDS_TRACED_MODEL_IDS.
// `premium` drives the "Premium" badge styling in the picker; GLM's real
// gate is its weekly referral-session pool, not the daily premium pool.
premium: true,
multimodal: false,
} as const satisfies FreebuffModelOption
-export const SUPPORTED_FREEBUFF_MODELS = [
+export const SUPPORTED_CODEBIRDS_MODELS = [
DEEPSEEK_V4_PRO_MODEL,
MIMO_V25_PRO_MODEL,
KIMI_MODEL,
@@ -262,29 +262,29 @@ export const SUPPORTED_FREEBUFF_MODELS = [
MINIMAX_MODEL,
] as const satisfies readonly FreebuffModelOption[]
-// GLM 5.2 is intentionally NOT in FREEBUFF_MODELS: it isn't a freely-pickable
+// GLM 5.2 is intentionally NOT in CODEBIRDS_MODELS: it isn't a freely-pickable
// grid model, it's a referral reward surfaced by the separate referral banner.
-// It stays in SUPPORTED_FREEBUFF_MODELS so the session/chat layers accept it as
+// It stays in SUPPORTED_CODEBIRDS_MODELS so the session/chat layers accept it as
// a valid model id once the user's weekly entitlement admits them.
-export const FREEBUFF_MODELS = [
+export const CODEBIRDS_MODELS = [
DEEPSEEK_V4_PRO_MODEL,
- ...(FREEBUFF_ENABLE_MIMO_MODELS_IN_UI ? [MIMO_V25_PRO_MODEL] : []),
+ ...(CODEBIRDS_ENABLE_MIMO_MODELS_IN_UI ? [MIMO_V25_PRO_MODEL] : []),
KIMI_MODEL,
DEEPSEEK_V4_FLASH_MODEL,
- ...(FREEBUFF_ENABLE_MIMO_MODELS_IN_UI ? [MIMO_V25_MODEL] : []),
+ ...(CODEBIRDS_ENABLE_MIMO_MODELS_IN_UI ? [MIMO_V25_MODEL] : []),
MINIMAX_M3_MODEL,
] as const satisfies readonly FreebuffModelOption[]
-export const FREEBUFF_PREMIUM_MODEL_IDS = [
- FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID,
- FREEBUFF_MIMO_V25_PRO_MODEL_ID,
- FREEBUFF_KIMI_MODEL_ID,
+export const CODEBIRDS_PREMIUM_MODEL_IDS = [
+ CODEBIRDS_DEEPSEEK_V4_PRO_MODEL_ID,
+ CODEBIRDS_MIMO_V25_PRO_MODEL_ID,
+ CODEBIRDS_KIMI_MODEL_ID,
] as const
/** Models unlocked by referrals, metered by the weekly GLM session pool rather
- * than the daily premium pool. Kept separate from FREEBUFF_PREMIUM_MODEL_IDS
+ * than the daily premium pool. Kept separate from CODEBIRDS_PREMIUM_MODEL_IDS
* so GLM never falls into the shared 5/day premium quota. */
-export const FREEBUFF_GLM_V52_MODEL_IDS = [FREEBUFF_GLM_V52_MODEL_ID] as const
+export const CODEBIRDS_GLM_V52_MODEL_IDS = [CODEBIRDS_GLM_V52_MODEL_ID] as const
/** Models that occupy the single per-user "premium-bucket" CONCURRENCY slot in
* Freebuff Desktop's multi-session mode: at most one of these may have an
@@ -292,33 +292,33 @@ export const FREEBUFF_GLM_V52_MODEL_IDS = [FREEBUFF_GLM_V52_MODEL_ID] as const
* Flash, MiMo 2.5) may run in any number of concurrent tabs.
*
* This is strictly a CONCURRENCY bucket, NOT a quota bucket. It is intentionally
- * a SUPERSET of FREEBUFF_PREMIUM_MODEL_IDS: it also includes MiniMax M3 and GLM
+ * a SUPERSET of CODEBIRDS_PREMIUM_MODEL_IDS: it also includes MiniMax M3 and GLM
* 5.2, which are unlimited / weekly for QUOTA purposes but expensive enough that
* we cap them to one concurrent desktop session. Do NOT use this for the daily
* premium quota — that stays on isFreebuffPremiumModelId so M3/GLM never start
* burning the 5/day premium pool. */
-export const FREEBUFF_DESKTOP_PREMIUM_BUCKET_MODEL_IDS = [
- ...FREEBUFF_PREMIUM_MODEL_IDS,
- FREEBUFF_MINIMAX_M3_MODEL_ID,
- FREEBUFF_GLM_V52_MODEL_ID,
+export const CODEBIRDS_DESKTOP_PREMIUM_BUCKET_MODEL_IDS = [
+ ...CODEBIRDS_PREMIUM_MODEL_IDS,
+ CODEBIRDS_MINIMAX_M3_MODEL_ID,
+ CODEBIRDS_GLM_V52_MODEL_ID,
] as const
/** Wire headers for the free-mode session endpoints
- * (/api/v1/freebuff/session). Shared so the server handlers and every client
+ * (/api/v1/codebirds/session). Shared so the server handlers and every client
* (CLI, desktop) agree on the exact strings instead of redefining literals. */
-export const FREEBUFF_INSTANCE_HEADER = 'x-freebuff-instance-id'
-export const FREEBUFF_MODEL_HEADER = 'x-freebuff-model'
+export const CODEBIRDS_INSTANCE_HEADER = 'x-codebirds-instance-id'
+export const CODEBIRDS_MODEL_HEADER = 'x-codebirds-model'
/** Set to '1' by Freebuff Desktop to opt into multi-session mode (concurrent
* per-tab sessions); absent for CLI/web, which keep one session per user. */
-export const FREEBUFF_MULTI_SESSION_HEADER = 'x-freebuff-multi-session'
+export const CODEBIRDS_MULTI_SESSION_HEADER = 'x-codebirds-multi-session'
/** Models that accept image input. Used to decide whether uploaded images are
* forwarded to the model as real multimodal content. */
-export const FREEBUFF_MULTIMODAL_MODEL_IDS = [
- FREEBUFF_MIMO_V25_MODEL_ID,
- FREEBUFF_MIMO_V25_PRO_MODEL_ID,
- FREEBUFF_MINIMAX_M3_MODEL_ID,
- FREEBUFF_KIMI_MODEL_ID,
+export const CODEBIRDS_MULTIMODAL_MODEL_IDS = [
+ CODEBIRDS_MIMO_V25_MODEL_ID,
+ CODEBIRDS_MIMO_V25_PRO_MODEL_ID,
+ CODEBIRDS_MINIMAX_M3_MODEL_ID,
+ CODEBIRDS_KIMI_MODEL_ID,
] as const
/** Free-mode models whose chat-completion traces we store in our own dataset
@@ -327,39 +327,39 @@ export const FREEBUFF_MULTIMODAL_MODEL_IDS = [
* mode iff it shows the "Collects data for training" caveat. Every other free
* model (incl. MiniMax M3 on Fireworks) is NOT stored; paid, non-free-mode
* requests are unaffected and traced as usual. */
-export const FREEBUFF_TRACED_MODEL_IDS = SUPPORTED_FREEBUFF_MODELS.filter(
+export const CODEBIRDS_TRACED_MODEL_IDS = SUPPORTED_CODEBIRDS_MODELS.filter(
(model: FreebuffModelOption) =>
- model.warning === FREEBUFF_DATA_COLLECTION_WARNING,
+ model.warning === CODEBIRDS_DATA_COLLECTION_WARNING,
).map((model) => model.id)
-export type FreebuffModelId = (typeof FREEBUFF_MODELS)[number]['id']
+export type FreebuffModelId = (typeof CODEBIRDS_MODELS)[number]['id']
export type SupportedFreebuffModelId =
- (typeof SUPPORTED_FREEBUFF_MODELS)[number]['id']
-export type FreebuffPremiumModelId = (typeof FREEBUFF_PREMIUM_MODEL_IDS)[number]
+ (typeof SUPPORTED_CODEBIRDS_MODELS)[number]['id']
+export type FreebuffPremiumModelId = (typeof CODEBIRDS_PREMIUM_MODEL_IDS)[number]
-/** What new freebuff users see selected in the picker. MiniMax M3 is the
+/** What new codebirds users see selected in the picker. MiniMax M3 is the
* strongest unlimited model (smartest & multimodal), so new users get good
* quality without burning the 5/day premium quota on routine messages.
* Callers that need a guaranteed-available id for resolution /
- * auto-fallbacks should use FALLBACK_FREEBUFF_MODEL_ID instead. */
-export const DEFAULT_FREEBUFF_MODEL_ID: FreebuffModelId =
- FREEBUFF_MINIMAX_M3_MODEL_ID
+ * auto-fallbacks should use FALLBACK_CODEBIRDS_MODEL_ID instead. */
+export const DEFAULT_CODEBIRDS_MODEL_ID: FreebuffModelId =
+ CODEBIRDS_MINIMAX_M3_MODEL_ID
/** Always-available fallback used when the requested model can't be served
* right now (unknown id, deployment hours closed, etc.). Kept distinct from
- * DEFAULT_FREEBUFF_MODEL_ID so a new user's "preferred default" can be the
+ * DEFAULT_CODEBIRDS_MODEL_ID so a new user's "preferred default" can be the
* smartest model without auto-flipping anyone to a closed serverless model. */
-export const FALLBACK_FREEBUFF_MODEL_ID: FreebuffModelId =
- FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID
-
-export const LIMITED_FREEBUFF_MODEL_ID: FreebuffModelId =
- FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID
-export const LIMITED_FREEBUFF_MODEL_IDS = [
- FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID,
- FREEBUFF_MIMO_V25_MODEL_ID,
+export const FALLBACK_CODEBIRDS_MODEL_ID: FreebuffModelId =
+ CODEBIRDS_DEEPSEEK_V4_FLASH_MODEL_ID
+
+export const LIMITED_CODEBIRDS_MODEL_ID: FreebuffModelId =
+ CODEBIRDS_DEEPSEEK_V4_FLASH_MODEL_ID
+export const LIMITED_CODEBIRDS_MODEL_IDS = [
+ CODEBIRDS_DEEPSEEK_V4_FLASH_MODEL_ID,
+ CODEBIRDS_MIMO_V25_MODEL_ID,
] as const
-export const LIMITED_FREEBUFF_MODELS = LIMITED_FREEBUFF_MODEL_IDS.map(
- (modelId) => SUPPORTED_FREEBUFF_MODELS.find((model) => model.id === modelId)!,
+export const LIMITED_CODEBIRDS_MODELS = LIMITED_CODEBIRDS_MODEL_IDS.map(
+ (modelId) => SUPPORTED_CODEBIRDS_MODELS.find((model) => model.id === modelId)!,
)
export type FreebuffAccessTier = 'full' | 'limited'
@@ -371,31 +371,31 @@ export type FreebuffWebAccessTier = FreebuffAccessTier | 'blocked'
/** Freebuff Web limited-tier session pool. Deliberately separate from the
* CLI's Postgres-backed session pool — enforced entirely in Convex. */
-export const FREEBUFF_WEB_LIMITED_SESSION_LIMIT = 5
-export const FREEBUFF_WEB_LIMITED_SESSION_LENGTH_MS = 60 * 60 * 1000
+export const CODEBIRDS_WEB_LIMITED_SESSION_LIMIT = 5
+export const CODEBIRDS_WEB_LIMITED_SESSION_LENGTH_MS = 60 * 60 * 1000
/** Models exempt from Freebuff Web geo limits: geo-limited users can run
* these without consuming limited sessions. Matches the shared limited
* model set (DeepSeek V4 Flash, MiMo 2.5); every other model stays
* geo-gated. Web-only — the CLI's limited pool is unaffected. */
-export const FREEBUFF_WEB_GEO_EXEMPT_MODEL_IDS = [
- FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID,
- FREEBUFF_MIMO_V25_MODEL_ID,
+export const CODEBIRDS_WEB_GEO_EXEMPT_MODEL_IDS = [
+ CODEBIRDS_DEEPSEEK_V4_FLASH_MODEL_ID,
+ CODEBIRDS_MIMO_V25_MODEL_ID,
] as const
export function isFreebuffWebGeoExemptModelId(
id: string | null | undefined,
): boolean {
if (!id) return false
- return FREEBUFF_WEB_GEO_EXEMPT_MODEL_IDS.some((modelId) => modelId === id)
+ return CODEBIRDS_WEB_GEO_EXEMPT_MODEL_IDS.some((modelId) => modelId === id)
}
/** Models a limited-tier Freebuff Web user may select: the geo-exempt models
* (unlimited) plus the shared limited set (session-gated). */
-export const FREEBUFF_WEB_LIMITED_MODEL_IDS = [
+export const CODEBIRDS_WEB_LIMITED_MODEL_IDS = [
...new Set([
- ...FREEBUFF_WEB_GEO_EXEMPT_MODEL_IDS,
- ...LIMITED_FREEBUFF_MODEL_IDS,
+ ...CODEBIRDS_WEB_GEO_EXEMPT_MODEL_IDS,
+ ...LIMITED_CODEBIRDS_MODEL_IDS,
]),
]
@@ -403,7 +403,7 @@ export function isFreebuffWebModelAllowedForLimitedTier(
id: string | null | undefined,
): boolean {
if (!id) return false
- return FREEBUFF_WEB_LIMITED_MODEL_IDS.some((modelId) => modelId === id)
+ return CODEBIRDS_WEB_LIMITED_MODEL_IDS.some((modelId) => modelId === id)
}
/** Coerce a limited-tier Freebuff Web selection (premium ids, stale
@@ -415,14 +415,14 @@ export function resolveFreebuffWebModelForLimitedTier(
): string {
return isFreebuffWebModelAllowedForLimitedTier(id)
? (id as string)
- : LIMITED_FREEBUFF_MODEL_ID
+ : LIMITED_CODEBIRDS_MODEL_ID
}
export function getFreebuffModelsForAccessTier(
accessTier: FreebuffAccessTier | null | undefined,
): readonly FreebuffModelOption[] {
- if (accessTier === 'limited') return LIMITED_FREEBUFF_MODELS
- return FREEBUFF_MODELS
+ if (accessTier === 'limited') return LIMITED_CODEBIRDS_MODELS
+ return CODEBIRDS_MODELS
}
/** The model the picker highlights as the "recommended" hero so a new user can
@@ -434,8 +434,8 @@ export function getRecommendedFreebuffModelId(
accessTier: FreebuffAccessTier | null | undefined,
): SupportedFreebuffModelId {
return accessTier === 'limited'
- ? LIMITED_FREEBUFF_MODEL_ID
- : DEFAULT_FREEBUFF_MODEL_ID
+ ? LIMITED_CODEBIRDS_MODEL_ID
+ : DEFAULT_CODEBIRDS_MODEL_ID
}
export function isFreebuffModelAllowedForAccessTier(
@@ -444,20 +444,20 @@ export function isFreebuffModelAllowedForAccessTier(
): boolean {
if (!model) return false
if (accessTier !== 'limited') return isSupportedFreebuffModelId(model)
- return LIMITED_FREEBUFF_MODEL_IDS.some((modelId) => modelId === model)
+ return LIMITED_CODEBIRDS_MODEL_IDS.some((modelId) => modelId === model)
}
export function isFreebuffModelId(
id: string | null | undefined,
): id is FreebuffModelId {
if (!id) return false
- return FREEBUFF_MODELS.some((m) => m.id === id)
+ return CODEBIRDS_MODELS.some((m) => m.id === id)
}
export function resolveFreebuffModel(
id: string | null | undefined,
): FreebuffModelId {
- return isFreebuffModelId(id) ? id : FALLBACK_FREEBUFF_MODEL_ID
+ return isFreebuffModelId(id) ? id : FALLBACK_CODEBIRDS_MODEL_ID
}
export function resolveFreebuffModelForAccessTier(
@@ -467,19 +467,19 @@ export function resolveFreebuffModelForAccessTier(
if (accessTier === 'limited') {
return isFreebuffModelAllowedForAccessTier(id, accessTier)
? (id as SupportedFreebuffModelId)
- : LIMITED_FREEBUFF_MODEL_ID
+ : LIMITED_CODEBIRDS_MODEL_ID
}
const resolved = resolveSupportedFreebuffModel(id)
return isFreebuffModelAllowedForAccessTier(resolved, accessTier)
? resolved
- : FALLBACK_FREEBUFF_MODEL_ID
+ : FALLBACK_CODEBIRDS_MODEL_ID
}
export function isSupportedFreebuffModelId(
id: string | null | undefined,
): id is SupportedFreebuffModelId {
if (!id) return false
- return SUPPORTED_FREEBUFF_MODELS.some((m) => m.id === id)
+ return SUPPORTED_CODEBIRDS_MODELS.some((m) => m.id === id)
}
/**
@@ -490,7 +490,7 @@ export function isSupportedFreebuffModelId(
* the two MUST stay in sync. Only a `-YYYYMMDD`-style suffix matches, so e.g.
* `mimo-v2.5-pro` never matches the base `mimo-v2.5`.
*/
-export function freebuffModelIdMatches(
+export function codebirdsModelIdMatches(
candidate: string | null | undefined,
baseId: string,
): boolean {
@@ -502,12 +502,12 @@ export function freebuffModelIdMatches(
}
/** Whether the requested model is Gemini Pro, tolerating the dated snapshot
- * suffix. Use this instead of `=== FREEBUFF_GEMINI_PRO_MODEL_ID` so a caller
+ * suffix. Use this instead of `=== CODEBIRDS_GEMINI_PRO_MODEL_ID` so a caller
* can't dodge a Gemini gate by sending the dated id. */
export function isFreebuffGeminiProModelId(
id: string | null | undefined,
): boolean {
- return freebuffModelIdMatches(id, FREEBUFF_GEMINI_PRO_MODEL_ID)
+ return codebirdsModelIdMatches(id, CODEBIRDS_GEMINI_PRO_MODEL_ID)
}
export function isFreebuffPremiumModelId(
@@ -516,8 +516,8 @@ export function isFreebuffPremiumModelId(
if (!id) return false
// Suffix-tolerant: a dated variant of a premium id (e.g. a dated Kimi) must
// still count as premium so it can't dodge the premium daily rate cap.
- return FREEBUFF_PREMIUM_MODEL_IDS.some((modelId) =>
- freebuffModelIdMatches(id, modelId),
+ return CODEBIRDS_PREMIUM_MODEL_IDS.some((modelId) =>
+ codebirdsModelIdMatches(id, modelId),
)
}
@@ -530,8 +530,8 @@ export function isFreebuffDesktopPremiumBucketModelId(
id: string | null | undefined,
): boolean {
if (!id) return false
- return FREEBUFF_DESKTOP_PREMIUM_BUCKET_MODEL_IDS.some((modelId) =>
- freebuffModelIdMatches(id, modelId),
+ return CODEBIRDS_DESKTOP_PREMIUM_BUCKET_MODEL_IDS.some((modelId) =>
+ codebirdsModelIdMatches(id, modelId),
)
}
@@ -542,8 +542,8 @@ export function isFreebuffDesktopPremiumBucketModelId(
export function isFreebuffGlmV52ModelId(
id: string | null | undefined,
): boolean {
- return FREEBUFF_GLM_V52_MODEL_IDS.some((modelId) =>
- freebuffModelIdMatches(id, modelId),
+ return CODEBIRDS_GLM_V52_MODEL_IDS.some((modelId) =>
+ codebirdsModelIdMatches(id, modelId),
)
}
@@ -551,48 +551,48 @@ export function isFreebuffMultimodalModelId(
id: string | null | undefined,
): boolean {
if (!id) return false
- return FREEBUFF_MULTIMODAL_MODEL_IDS.some((modelId) => modelId === id)
+ return CODEBIRDS_MULTIMODAL_MODEL_IDS.some((modelId) => modelId === id)
}
/** Whether we store our own chat-completion traces for this free-mode model.
- * See FREEBUFF_TRACED_MODEL_IDS. */
+ * See CODEBIRDS_TRACED_MODEL_IDS. */
export function isFreebuffTracedModelId(
id: string | null | undefined,
): boolean {
if (!id) return false
- return FREEBUFF_TRACED_MODEL_IDS.some((modelId) => modelId === id)
+ return CODEBIRDS_TRACED_MODEL_IDS.some((modelId) => modelId === id)
}
export function resolveSupportedFreebuffModel(
id: string | null | undefined,
): SupportedFreebuffModelId {
- return isSupportedFreebuffModelId(id) ? id : FALLBACK_FREEBUFF_MODEL_ID
+ return isSupportedFreebuffModelId(id) ? id : FALLBACK_CODEBIRDS_MODEL_ID
}
export function getFreebuffModel(id: string): FreebuffModelOption {
return (
- SUPPORTED_FREEBUFF_MODELS.find((m) => m.id === id) ??
- FREEBUFF_MODELS.find((m) => m.id === FALLBACK_FREEBUFF_MODEL_ID)!
+ SUPPORTED_CODEBIRDS_MODELS.find((m) => m.id === id) ??
+ CODEBIRDS_MODELS.find((m) => m.id === FALLBACK_CODEBIRDS_MODEL_ID)!
)
}
function getNextFreebuffDeploymentStart(now: Date): Date {
- const easternNow = getZonedParts(now, FREEBUFF_EASTERN_TIMEZONE)
+ const easternNow = getZonedParts(now, CODEBIRDS_EASTERN_TIMEZONE)
const isBeforeTodayOpen = easternNow.hour < 9
const offset = isBeforeTodayOpen ? 0 : 1
return getUtcForZonedTime(
addDaysToYmd(easternNow.year, easternNow.month, easternNow.day, offset),
- FREEBUFF_EASTERN_TIMEZONE,
+ CODEBIRDS_EASTERN_TIMEZONE,
9,
0,
)
}
function getCurrentFreebuffDeploymentEnd(now: Date): Date {
- const pacificNow = getZonedParts(now, FREEBUFF_PACIFIC_TIMEZONE)
- return getUtcForZonedTime(pacificNow, FREEBUFF_PACIFIC_TIMEZONE, 17, 0)
+ const pacificNow = getZonedParts(now, CODEBIRDS_PACIFIC_TIMEZONE)
+ return getUtcForZonedTime(pacificNow, CODEBIRDS_PACIFIC_TIMEZONE, 17, 0)
}
function isSameLocalDay(left: Date, right: Date, timeZone?: string): boolean {
@@ -637,8 +637,8 @@ export function getFreebuffDeploymentAvailabilityLabel(
}
export function isFreebuffDeploymentHours(now: Date = new Date()): boolean {
- const eastern = getZonedParts(now, FREEBUFF_EASTERN_TIMEZONE)
- const pacific = getZonedParts(now, FREEBUFF_PACIFIC_TIMEZONE)
+ const eastern = getZonedParts(now, CODEBIRDS_EASTERN_TIMEZONE)
+ const pacific = getZonedParts(now, CODEBIRDS_PACIFIC_TIMEZONE)
return (
eastern.hour * 60 + eastern.minute >= 9 * 60 &&
pacific.hour * 60 + pacific.minute < 17 * 60
@@ -649,7 +649,7 @@ export function isFreebuffModelAvailable(
id: string,
now: Date = new Date(),
): boolean {
- const model = SUPPORTED_FREEBUFF_MODELS.find((m) => m.id === id)
+ const model = SUPPORTED_CODEBIRDS_MODELS.find((m) => m.id === id)
if (!model) return false
return model.availability === 'always' || isFreebuffDeploymentHours(now)
}
@@ -661,5 +661,5 @@ export function resolveAvailableFreebuffModel(
const resolved = resolveFreebuffModel(id)
return isFreebuffModelAvailable(resolved, now)
? resolved
- : FALLBACK_FREEBUFF_MODEL_ID
+ : FALLBACK_CODEBIRDS_MODEL_ID
}
diff --git a/common/src/constants/freebuff-referral-tiers.ts b/common/src/constants/freebuff-referral-tiers.ts
index e9ff6f0990..d95ffad8d8 100644
--- a/common/src/constants/freebuff-referral-tiers.ts
+++ b/common/src/constants/freebuff-referral-tiers.ts
@@ -31,7 +31,7 @@ export const MIN_GITHUB_ACCOUNT_AGE_MONTHS_REFERRAL = 4
* Freebuff CLI limited-tier perk: each qualified referral whose referred user
* activated at the *limited* access tier grants +1 daily free-mode session,
* capped here (e.g. 5 base + 3 = 8/day). Full-access referrals instead grant
- * GLM sessions (see FREEBUFF_GLM_V52_REFERRAL_CAP).
+ * GLM sessions (see CODEBIRDS_GLM_V52_REFERRAL_CAP).
*/
export const REFERRAL_CLI_DAILY_SESSION_BONUS_CAP = 3
@@ -49,7 +49,7 @@ export interface FreebuffReferralTier {
}
/** Tier ladder: 1 referral, then +2 (3 total), then +4 (7 total). */
-export const FREEBUFF_REFERRAL_TIERS: readonly FreebuffReferralTier[] = [
+export const CODEBIRDS_REFERRAL_TIERS: readonly FreebuffReferralTier[] = [
{
tier: 0,
referralsRequired: 0,
@@ -86,19 +86,19 @@ export const FREEBUFF_REFERRAL_TIERS: readonly FreebuffReferralTier[] = [
* ladder tops out at 7 qualified referrals, so it needs its own headroom —
* generous enough for unqualified signups, small enough to bound farming.
*/
-export const FREEBUFF_WEB_REFERRAL_LIMIT = 20
+export const CODEBIRDS_WEB_REFERRAL_LIMIT = 20
-export const MAX_FREEBUFF_REFERRAL_TIER =
- FREEBUFF_REFERRAL_TIERS[FREEBUFF_REFERRAL_TIERS.length - 1].tier
+export const MAX_CODEBIRDS_REFERRAL_TIER =
+ CODEBIRDS_REFERRAL_TIERS[CODEBIRDS_REFERRAL_TIERS.length - 1].tier
/** Lowest tier whose perks include watermark removal. */
-export const FREEBUFF_WATERMARK_REMOVAL_TIER = FREEBUFF_REFERRAL_TIERS.find(
+export const CODEBIRDS_WATERMARK_REMOVAL_TIER = CODEBIRDS_REFERRAL_TIERS.find(
(tier) => tier.removesWatermark,
)!.tier
/** Qualified referrals needed before deploys drop the watermark. */
-export const FREEBUFF_WATERMARK_REMOVAL_REFERRALS =
- FREEBUFF_REFERRAL_TIERS.find(
+export const CODEBIRDS_WATERMARK_REMOVAL_REFERRALS =
+ CODEBIRDS_REFERRAL_TIERS.find(
(tier) => tier.removesWatermark,
)!.referralsRequired
@@ -107,8 +107,8 @@ export function getReferralTier(
qualifiedReferralCount: number | null | undefined,
): FreebuffReferralTier {
const count = Math.max(0, qualifiedReferralCount ?? 0)
- let unlocked = FREEBUFF_REFERRAL_TIERS[0]
- for (const tier of FREEBUFF_REFERRAL_TIERS) {
+ let unlocked = CODEBIRDS_REFERRAL_TIERS[0]
+ for (const tier of CODEBIRDS_REFERRAL_TIERS) {
if (count >= tier.referralsRequired) {
unlocked = tier
}
@@ -118,8 +118,8 @@ export function getReferralTier(
/** Tier limits by tier index (clamped into range). */
export function getTierLimits(tier: number): FreebuffReferralTier {
- const clamped = Math.min(Math.max(0, tier), MAX_FREEBUFF_REFERRAL_TIER)
- return FREEBUFF_REFERRAL_TIERS.find((t) => t.tier === clamped)!
+ const clamped = Math.min(Math.max(0, tier), MAX_CODEBIRDS_REFERRAL_TIER)
+ return CODEBIRDS_REFERRAL_TIERS.find((t) => t.tier === clamped)!
}
/** Next tier above the given qualified referral count, or null if maxed. */
@@ -128,7 +128,7 @@ export function getNextReferralTier(
): FreebuffReferralTier | null {
const current = getReferralTier(qualifiedReferralCount)
return (
- FREEBUFF_REFERRAL_TIERS.find((tier) => tier.tier === current.tier + 1) ??
+ CODEBIRDS_REFERRAL_TIERS.find((tier) => tier.tier === current.tier + 1) ??
null
)
}
diff --git a/common/src/constants/grant-priorities.ts b/common/src/constants/grant-priorities.ts
index df17d1008a..bfe39a08a1 100644
--- a/common/src/constants/grant-priorities.ts
+++ b/common/src/constants/grant-priorities.ts
@@ -1,4 +1,4 @@
-import type { GrantType } from '@codebuff/common/types/grant'
+import type { GrantType } from '@codebirds/common/types/grant'
// Lower = consumed first
export const GRANT_PRIORITIES: Record = {
diff --git a/common/src/constants/index.ts b/common/src/constants/index.ts
index 090335b11e..c26ef72626 100644
--- a/common/src/constants/index.ts
+++ b/common/src/constants/index.ts
@@ -1,5 +1,5 @@
// Re-export all constants from domain-specific files for backwards compatibility
-// This allows existing imports from '@codebuff/common/old-constants' to continue working
+// This allows existing imports from '@codebirds/common/old-constants' to continue working
export * from './model-config'
export * from './limits'
diff --git a/common/src/constants/limits.ts b/common/src/constants/limits.ts
index 14b419ed40..5cf8f34e88 100644
--- a/common/src/constants/limits.ts
+++ b/common/src/constants/limits.ts
@@ -5,7 +5,7 @@ export const MAX_DATE = new Date(86399999999999)
export const BILLING_PERIOD_DAYS = 30
export const SESSION_MAX_AGE_SECONDS = 30 * 24 * 60 * 60 // 30 days
export const SESSION_TIME_WINDOW_MS = 30 * 60 * 1000 // 30 minutes - used for matching sessions created around fingerprint creation
-// New Codebuff accounts receive a one-time free credit grant on signup.
+// New Codebirds accounts receive a one-time free credit grant on signup.
export const SIGNUP_FREE_CREDITS_GRANT = 500
// New accounts do not receive monthly free credits; grandfathered monthly grants
diff --git a/common/src/constants/paths.ts b/common/src/constants/paths.ts
index 70a3a194ff..b22179786f 100644
--- a/common/src/constants/paths.ts
+++ b/common/src/constants/paths.ts
@@ -5,7 +5,7 @@ export const FIND_FILES_MARKER = '[' + 'FIND_FILES_PLEASE]'
export const AGENT_TEMPLATES_DIR = '.agents/'
export const AGENT_DEFINITION_FILE = 'agent-definition.d.ts'
-export const API_KEY_ENV_VAR = 'CODEBUFF_API_KEY'
+export const API_KEY_ENV_VAR = 'CODEBIRDS_API_KEY'
export const INVALID_AUTH_TOKEN_MESSAGE =
'Invalid auth token. You may have been logged out from the web portal. Please log in again.'
diff --git a/common/src/env-ci.ts b/common/src/env-ci.ts
index abbffe3ad1..333bc050f1 100644
--- a/common/src/env-ci.ts
+++ b/common/src/env-ci.ts
@@ -3,7 +3,7 @@
*
* This module provides a typed interface to CI-specific environment variables.
* These are used in CI/CD pipelines and eval contexts.
- * In tests, use `@codebuff/common/testing-env-ci`.
+ * In tests, use `@codebirds/common/testing-env-ci`.
*/
import type { CiEnv } from './types/contracts/env'
@@ -17,8 +17,8 @@ export const getCiEnv = (): CiEnv => ({
GITHUB_ACTIONS: process.env.GITHUB_ACTIONS,
RENDER: process.env.RENDER,
IS_PULL_REQUEST: process.env.IS_PULL_REQUEST,
- CODEBUFF_GITHUB_TOKEN: process.env.CODEBUFF_GITHUB_TOKEN,
- CODEBUFF_API_KEY: process.env.CODEBUFF_API_KEY,
+ CODEBIRDS_GITHUB_TOKEN: process.env.CODEBIRDS_GITHUB_TOKEN,
+ CODEBIRDS_API_KEY: process.env.CODEBIRDS_API_KEY,
})
/**
diff --git a/common/src/env-process.ts b/common/src/env-process.ts
index 9b0f619a33..1b730ead29 100644
--- a/common/src/env-process.ts
+++ b/common/src/env-process.ts
@@ -8,7 +8,7 @@
* Usage:
* - Import `getBaseEnv` for base OS-level vars only
* - Import `getProcessEnv` for the full ProcessEnv (base + extensions)
- * - In tests, use `@codebuff/common/testing-env-process`
+ * - In tests, use `@codebirds/common/testing-env-process`
*/
import type { BaseEnv, ProcessEnv } from './types/contracts/env'
@@ -67,19 +67,19 @@ export const getProcessEnv = (): ProcessEnv => ({
// Editor preferences
VISUAL: process.env.VISUAL,
EDITOR: process.env.EDITOR,
- CODEBUFF_CLI_EDITOR: process.env.CODEBUFF_CLI_EDITOR,
- CODEBUFF_EDITOR: process.env.CODEBUFF_EDITOR,
+ CODEBIRDS_CLI_EDITOR: process.env.CODEBIRDS_CLI_EDITOR,
+ CODEBIRDS_EDITOR: process.env.CODEBIRDS_EDITOR,
// Theme preferences
OPEN_TUI_THEME: process.env.OPEN_TUI_THEME,
OPENTUI_THEME: process.env.OPENTUI_THEME,
- // Codebuff CLI-specific
- CODEBUFF_IS_BINARY: process.env.CODEBUFF_IS_BINARY,
- CODEBUFF_CLI_VERSION: process.env.CODEBUFF_CLI_VERSION,
- CODEBUFF_CLI_TARGET: process.env.CODEBUFF_CLI_TARGET,
- CODEBUFF_RG_PATH: process.env.CODEBUFF_RG_PATH,
- CODEBUFF_WASM_DIR: process.env.CODEBUFF_WASM_DIR,
+ // Codebirds CLI-specific
+ CODEBIRDS_IS_BINARY: process.env.CODEBIRDS_IS_BINARY,
+ CODEBIRDS_CLI_VERSION: process.env.CODEBIRDS_CLI_VERSION,
+ CODEBIRDS_CLI_TARGET: process.env.CODEBIRDS_CLI_TARGET,
+ CODEBIRDS_RG_PATH: process.env.CODEBIRDS_RG_PATH,
+ CODEBIRDS_WASM_DIR: process.env.CODEBIRDS_WASM_DIR,
// Build/CI flags
VERBOSE: process.env.VERBOSE,
diff --git a/common/src/env-schema.ts b/common/src/env-schema.ts
index 8c81445c22..99700f6442 100644
--- a/common/src/env-schema.ts
+++ b/common/src/env-schema.ts
@@ -4,8 +4,8 @@ export const CLIENT_ENV_PREFIX = 'NEXT_PUBLIC_'
export const clientEnvSchema = z.object({
NEXT_PUBLIC_CB_ENVIRONMENT: z.enum(['dev', 'test', 'prod']),
- NEXT_PUBLIC_CODEBUFF_APP_URL: z.url().min(1),
- NEXT_PUBLIC_FREEBUFF_APP_URL: z.url().optional(),
+ NEXT_PUBLIC_CODEBIRDS_APP_URL: z.url().min(1),
+ NEXT_PUBLIC_CODEBIRDS_APP_URL: z.url().optional(),
NEXT_PUBLIC_SUPPORT_EMAIL: z.email().min(1),
NEXT_PUBLIC_POSTHOG_API_KEY: z.string().min(1),
NEXT_PUBLIC_POSTHOG_HOST_URL: z.url().min(1),
@@ -25,8 +25,8 @@ export type ClientEnv = z.infer
// Bun will inject all these values, so we need to reference them individually (no for-loops)
export const clientProcessEnv: ClientInput = {
NEXT_PUBLIC_CB_ENVIRONMENT: process.env.NEXT_PUBLIC_CB_ENVIRONMENT,
- NEXT_PUBLIC_CODEBUFF_APP_URL: process.env.NEXT_PUBLIC_CODEBUFF_APP_URL,
- NEXT_PUBLIC_FREEBUFF_APP_URL: process.env.NEXT_PUBLIC_FREEBUFF_APP_URL,
+ NEXT_PUBLIC_CODEBIRDS_APP_URL: process.env.NEXT_PUBLIC_CODEBIRDS_APP_URL,
+ NEXT_PUBLIC_CODEBIRDS_APP_URL: process.env.NEXT_PUBLIC_CODEBIRDS_APP_URL,
NEXT_PUBLIC_SUPPORT_EMAIL: process.env.NEXT_PUBLIC_SUPPORT_EMAIL,
NEXT_PUBLIC_POSTHOG_API_KEY: process.env.NEXT_PUBLIC_POSTHOG_API_KEY,
NEXT_PUBLIC_POSTHOG_HOST_URL: process.env.NEXT_PUBLIC_POSTHOG_HOST_URL,
diff --git a/common/src/env.ts b/common/src/env.ts
index 3258241bb1..89251fea2e 100644
--- a/common/src/env.ts
+++ b/common/src/env.ts
@@ -17,7 +17,7 @@ if (env.NEXT_PUBLIC_CB_ENVIRONMENT !== 'prod') {
export const IS_DEV = env.NEXT_PUBLIC_CB_ENVIRONMENT === 'dev'
export const IS_TEST = env.NEXT_PUBLIC_CB_ENVIRONMENT === 'test'
export const IS_PROD = env.NEXT_PUBLIC_CB_ENVIRONMENT === 'prod'
-export const IS_CI = process.env.CODEBUFF_GITHUB_ACTIONS === 'true'
+export const IS_CI = process.env.CODEBIRDS_GITHUB_ACTIONS === 'true'
// Debug flag for logging analytics events in dev mode
// Set to true when actively debugging analytics - affects both CLI and backend
diff --git a/common/src/mcp/client.ts b/common/src/mcp/client.ts
index 5a5608d57f..d965f17187 100644
--- a/common/src/mcp/client.ts
+++ b/common/src/mcp/client.ts
@@ -132,7 +132,7 @@ export async function getMCPClient(config: MCPConfig): Promise {
}
const client = new Client({
- name: 'codebuff',
+ name: 'codebirds',
version: '1.0.0',
})
diff --git a/common/src/old-constants.ts b/common/src/old-constants.ts
index 66d954fcda..c834ce2b41 100644
--- a/common/src/old-constants.ts
+++ b/common/src/old-constants.ts
@@ -1,9 +1,9 @@
/**
- * @deprecated Import from '@codebuff/common/constants' or specific files instead:
- * - '@codebuff/common/constants/model-config' for model-related constants
- * - '@codebuff/common/constants/limits' for billing and numeric limits
- * - '@codebuff/common/constants/ui' for auth/user state
- * - '@codebuff/common/constants/paths' for file paths and markers
+ * @deprecated Import from '@codebirds/common/constants' or specific files instead:
+ * - '@codebirds/common/constants/model-config' for model-related constants
+ * - '@codebirds/common/constants/limits' for billing and numeric limits
+ * - '@codebirds/common/constants/ui' for auth/user state
+ * - '@codebirds/common/constants/paths' for file paths and markers
*
* This file re-exports all constants for backwards compatibility.
*/
diff --git a/common/src/project-file-tree.ts b/common/src/project-file-tree.ts
index bbd3e71cd0..2ca4186030 100644
--- a/common/src/project-file-tree.ts
+++ b/common/src/project-file-tree.ts
@@ -6,7 +6,7 @@ import { sortBy } from 'lodash'
import { DEFAULT_IGNORED_PATHS } from './constants/paths'
import { fileExists, isValidProjectRoot } from './util/file'
-import type { CodebuffFileSystem } from './types/filesystem'
+import type { CodebirdsFileSystem } from './types/filesystem'
import type { DirectoryNode, FileTreeNode } from './util/file'
/**
@@ -23,7 +23,7 @@ function logFileTreeError(
error: unknown,
): void {
// Only log in debug mode to avoid noisy output
- if (!process.env.DEBUG && !process.env.CODEBUFF_DEBUG) {
+ if (!process.env.DEBUG && !process.env.CODEBIRDS_DEBUG) {
return
}
@@ -58,7 +58,7 @@ export function isShallowScanRoot(
export async function getProjectFileTree(params: {
projectRoot: string
maxFiles?: number
- fs: CodebuffFileSystem
+ fs: CodebirdsFileSystem
}): Promise {
const withDefaults = { maxFiles: DEFAULT_MAX_FILES, ...params }
const { projectRoot, fs } = withDefaults
@@ -220,7 +220,7 @@ function rebaseGitignorePattern(
export async function parseGitignore(params: {
fullDirPath: string
projectRoot: string
- fs: CodebuffFileSystem
+ fs: CodebirdsFileSystem
}): Promise {
const { fullDirPath, projectRoot, fs } = params
@@ -228,7 +228,7 @@ export async function parseGitignore(params: {
const relativeDirPath = path.relative(projectRoot, fullDirPath)
const ignoreFiles = [
path.join(fullDirPath, '.gitignore'),
- path.join(fullDirPath, '.codebuffignore'),
+ path.join(fullDirPath, '.codebirdsignore'),
path.join(fullDirPath, '.manicodeignore'), // Legacy support
]
@@ -317,7 +317,7 @@ export function getLastReadFilePaths(
export async function isFileIgnored(params: {
filePath: string
projectRoot: string
- fs: CodebuffFileSystem
+ fs: CodebirdsFileSystem
}): Promise {
const { filePath, projectRoot, fs } = params
diff --git a/common/src/templates/agent-validation.ts b/common/src/templates/agent-validation.ts
index 66462348d3..7d23fceb70 100644
--- a/common/src/templates/agent-validation.ts
+++ b/common/src/templates/agent-validation.ts
@@ -7,7 +7,7 @@ import {
import type { AgentTemplate } from '../types/agent-template'
import type { DynamicAgentTemplate } from '../types/dynamic-agent-template'
-import type { Logger } from '@codebuff/common/types/contracts/logger'
+import type { Logger } from '@codebirds/common/types/contracts/logger'
export interface DynamicAgentValidationError {
filePath: string
diff --git a/common/src/templates/initial-agents-dir/README.md b/common/src/templates/initial-agents-dir/README.md
index 43053980d3..aeae55640d 100644
--- a/common/src/templates/initial-agents-dir/README.md
+++ b/common/src/templates/initial-agents-dir/README.md
@@ -5,24 +5,24 @@ Create specialized agent workflows that coordinate multiple AI agents to tackle
## Getting Started
1. **Edit an existing agent**: Start with `my-custom-agent.ts` and modify it for your needs
-2. **Test your agent**: Run `codebuff` without `--agent` so local `.agents` load, then invoke your agent from a prompt
-3. **Publish your agent**: Run `codebuff publish your-agent-name`
+2. **Test your agent**: Run `codebirds` without `--agent` so local `.agents` load, then invoke your agent from a prompt
+3. **Publish your agent**: Run `codebirds publish your-agent-name`
## Need Help?
- For examples, check the `examples/` directory.
-- Join our [Discord community](https://codebuff.com/discord) and ask your questions!
-- Check our [documentation](https://codebuff.com/docs) for more details
+- Join our [Discord community](https://codebirds.com/discord) and ask your questions!
+- Check our [documentation](https://codebirds.com/docs) for more details
-# What is Codebuff?
+# What is Codebirds?
-Codebuff is an **open-source AI coding assistant** that edits your codebase through natural language instructions. Instead of using one model for everything, it coordinates specialized agents that work together to understand your project and make precise changes.
+Codebirds is an **open-source AI coding assistant** that edits your codebase through natural language instructions. Instead of using one model for everything, it coordinates specialized agents that work together to understand your project and make precise changes.
-Codebuff beats Claude Code at 61% vs 53% on [our evals](https://github.com/CodebuffAI/codebuff/tree/main/evals) across 175+ coding tasks over multiple open-source repos that simulate real-world tasks.
+Codebirds beats Claude Code at 61% vs 53% on [our evals](https://github.com/CodebirdsAI/codebirds/tree/main/evals) across 175+ coding tasks over multiple open-source repos that simulate real-world tasks.
-## How Codebuff Works
+## How Codebirds Works
-When you ask Codebuff to "add authentication to my API," it might invoke:
+When you ask Codebirds to "add authentication to my API," it might invoke:
1. A **File Explorer Agent** to scan your codebase to understand the architecture and find relevant files
2. A **Planner Agent** to plan which files need changes and in what order
@@ -74,7 +74,7 @@ This agent systematically analyzes changes, reads relevant files for context, th
# Agent Development Guide
-This guide covers everything you need to know about building custom Codebuff agents.
+This guide covers everything you need to know about building custom Codebirds agents.
## Agent Structure
@@ -175,7 +175,7 @@ Choose models based on your agent's needs:
- **`openai/gpt-5.2`**: Best at complex reasoning and planning
- **`google/gemini-3.1-flash-lite-preview`**: Fast and cost-effective for simple or medium-complexity tasks
-**Any model on OpenRouter**: Unlike Claude Code which locks you into Anthropic's models, Codebuff supports any model available on [OpenRouter](https://openrouter.ai/models) - from Claude and GPT to specialized models like Qwen, DeepSeek, and others. Switch models for different tasks or use the latest releases without waiting for platform updates.
+**Any model on OpenRouter**: Unlike Claude Code which locks you into Anthropic's models, Codebirds supports any model available on [OpenRouter](https://openrouter.ai/models) - from Claude and GPT to specialized models like Qwen, DeepSeek, and others. Switch models for different tasks or use the latest releases without waiting for platform updates.
See [OpenRouter](https://openrouter.ai/models) for all available models and pricing.
@@ -198,7 +198,7 @@ async *handleSteps() {
}
```
-**Reuse any published agent**: Compose existing [published agents](https://www.codebuff.com/store) to get a leg up. Codebuff agents are the new MCP!
+**Reuse any published agent**: Compose existing [published agents](https://www.codebirds.com/store) to get a leg up. Codebirds agents are the new MCP!
## Best Practices
@@ -229,7 +229,7 @@ async *handleSteps() {
## Testing Your Agent
-1. **Local Testing**: Run `codebuff` without `--agent` so local `.agents` load, then invoke your agent from a prompt
+1. **Local Testing**: Run `codebirds` without `--agent` so local `.agents` load, then invoke your agent from a prompt
2. **Debug Mode**: Add logging to your `handleSteps` function
3. **Unit Testing**: Test individual functions in isolation
4. **Integration Testing**: Test agent coordination workflows
@@ -238,7 +238,7 @@ async *handleSteps() {
1. **Validate**: Ensure your agent works across different codebases
2. **Document**: Include clear usage instructions
-3. **Publish**: `codebuff publish your-agent-name`
+3. **Publish**: `codebirds publish your-agent-name`
4. **Maintain**: Update as models and tools evolve
## Advanced Patterns
@@ -275,20 +275,20 @@ async *handleSteps() {
}
```
-## Why Choose Codebuff for Custom Agents
+## Why Choose Codebirds for Custom Agents
**Deep customizability**: Create sophisticated agent workflows with TypeScript generators that mix AI generation with programmatic control. Define custom agents that spawn subagents, implement conditional logic, and orchestrate complex multi-step processes that adapt to your specific use cases.
-**Fully customizable SDK**: Build Codebuff's capabilities directly into your applications with a complete TypeScript SDK. Create custom tools, integrate with your CI/CD pipeline, build AI-powered development environments, or embed intelligent coding assistance into your products.
+**Fully customizable SDK**: Build Codebirds's capabilities directly into your applications with a complete TypeScript SDK. Create custom tools, integrate with your CI/CD pipeline, build AI-powered development environments, or embed intelligent coding assistance into your products.
-Learn more about the SDK [here](https://www.npmjs.com/package/@codebuff/sdk).
+Learn more about the SDK [here](https://www.npmjs.com/package/@codebirds/sdk).
## Community & Support
-- **Discord**: [Join our community](https://codebuff.com/discord) for help and inspiration
+- **Discord**: [Join our community](https://codebirds.com/discord) for help and inspiration
- **Examples**: Study the `examples/` directory for patterns
-- **Documentation**: [codebuff.com/docs](https://codebuff.com/docs) and check `types/` for detailed type information
-- **Issues**: [Report bugs and request features on GitHub](https://github.com/CodebuffAI/codebuff/issues)
-- **Support**: [support@codebuff.com](mailto:support@codebuff.com)
+- **Documentation**: [codebirds.com/docs](https://codebirds.com/docs) and check `types/` for detailed type information
+- **Issues**: [Report bugs and request features on GitHub](https://github.com/CodebirdsAI/codebirds/issues)
+- **Support**: [support@codebirds.com](mailto:support@codebirds.com)
Happy agent building! 🤖
diff --git a/common/src/templates/initial-agents-dir/examples/03-advanced-file-explorer.ts b/common/src/templates/initial-agents-dir/examples/03-advanced-file-explorer.ts
index 157d5e5a40..5e3b42b4eb 100644
--- a/common/src/templates/initial-agents-dir/examples/03-advanced-file-explorer.ts
+++ b/common/src/templates/initial-agents-dir/examples/03-advanced-file-explorer.ts
@@ -10,7 +10,7 @@ const definition: AgentDefinition = {
includeMessageHistory: false,
toolNames: ['spawn_agents', 'set_output'],
- spawnableAgents: [`codebuff/file-picker@0.0.1`],
+ spawnableAgents: [`codebirds/file-picker@0.0.1`],
inputSchema: {
prompt: {
@@ -56,7 +56,7 @@ const definition: AgentDefinition = {
toolName: 'spawn_agents',
input: {
agents: filePickerPrompts.map((promptText) => ({
- agent_type: 'codebuff/file-picker@0.0.1',
+ agent_type: 'codebirds/file-picker@0.0.1',
prompt: promptText,
})),
},
diff --git a/common/src/templates/initial-agents-dir/my-custom-agent.ts b/common/src/templates/initial-agents-dir/my-custom-agent.ts
index 418aed1ea8..70b8bdf65b 100644
--- a/common/src/templates/initial-agents-dir/my-custom-agent.ts
+++ b/common/src/templates/initial-agents-dir/my-custom-agent.ts
@@ -2,12 +2,12 @@
* EDIT ME to create your own agent!
*
* Run your agent with:
- * > codebuff
- * Inside codebuff:
+ * > codebirds
+ * Inside codebirds:
* > @my-custom-agent please review my recent changes
*
* Finally, you can publish your agent with:
- * > codebuff publish my-custom-agent
+ * > codebirds publish my-custom-agent
* Then users from around the world can run it!
*/
@@ -18,7 +18,7 @@ const definition: AgentDefinition = {
displayName: 'My Custom Agent',
model: 'x-ai/grok-4-fast',
- spawnableAgents: ['codebuff/file-explorer@0.0.6'],
+ spawnableAgents: ['codebirds/file-explorer@0.0.6'],
// Check out .agents/types/tools.ts for more information on the tools you can include.
toolNames: ['run_terminal_command', 'read_files', 'spawn_agents'],
diff --git a/common/src/templates/initial-agents-dir/package.json b/common/src/templates/initial-agents-dir/package.json
index 44518ec132..33f01de480 100644
--- a/common/src/templates/initial-agents-dir/package.json
+++ b/common/src/templates/initial-agents-dir/package.json
@@ -1,6 +1,6 @@
{
- "name": "codebuff-agents",
+ "name": "codebirds-agents",
"version": "0.0.0",
- "description": "This directory contains your custom Codebuff agents. Each agent is a TypeScript file that defines an AI agent with specific capabilities and behavior.",
+ "description": "This directory contains your custom Codebirds agents. Each agent is a TypeScript file that defines an AI agent with specific capabilities and behavior.",
"license": "Apache-2.0"
}
diff --git a/common/src/templates/initial-agents-dir/types/agent-definition.ts b/common/src/templates/initial-agents-dir/types/agent-definition.ts
index 6315dbeb17..1f61e7d410 100644
--- a/common/src/templates/initial-agents-dir/types/agent-definition.ts
+++ b/common/src/templates/initial-agents-dir/types/agent-definition.ts
@@ -1,7 +1,7 @@
/**
- * Codebuff Agent Type Definitions
+ * Codebirds Agent Type Definitions
*
- * This file provides TypeScript type definitions for creating custom Codebuff agents.
+ * This file provides TypeScript type definitions for creating custom Codebirds agents.
* Import these types in your agent files to get full type safety and IntelliSense.
*
* Usage in .agents/your-agent.ts:
@@ -128,9 +128,9 @@ export interface AgentDefinition {
*/
toolNames?: (ToolName | (string & {}))[]
- /** Other agents this agent can spawn, like 'codebuff/file-picker@0.0.1'.
+ /** Other agents this agent can spawn, like 'codebirds/file-picker@0.0.1'.
*
- * Use the fully qualified agent id from the agent store, including publisher and version: 'codebuff/file-picker@0.0.1'
+ * Use the fully qualified agent id from the agent store, including publisher and version: 'codebirds/file-picker@0.0.1'
* (publisher and version are required!)
*
* Or, use the agent id from a local agent file in your .agents directory: 'file-picker'.
diff --git a/common/src/templates/initial-agents-dir/types/tools.ts b/common/src/templates/initial-agents-dir/types/tools.ts
index 91a74efacf..aa35b6a964 100644
--- a/common/src/templates/initial-agents-dir/types/tools.ts
+++ b/common/src/templates/initial-agents-dir/types/tools.ts
@@ -275,7 +275,7 @@ export interface ReadUrlParams {
}
/**
- * Render a small interactive UI widget in the Codebuff CLI. Currently supports a button that opens a link.
+ * Render a small interactive UI widget in the Codebirds CLI. Currently supports a button that opens a link.
*/
export interface RenderUiParams {
/** The UI widget to render. */
diff --git a/common/src/testing-env-ci.ts b/common/src/testing-env-ci.ts
index 558a1693ae..4b5deb0455 100644
--- a/common/src/testing-env-ci.ts
+++ b/common/src/testing-env-ci.ts
@@ -2,14 +2,14 @@ import type { CiEnv } from './types/contracts/env'
/**
* Test-only helpers for CI env snapshots.
- * Keep production code using `@codebuff/common/env-ci`.
+ * Keep production code using `@codebirds/common/env-ci`.
*/
export const createTestCiEnv = (overrides: Partial = {}): CiEnv => ({
CI: undefined,
GITHUB_ACTIONS: undefined,
RENDER: undefined,
IS_PULL_REQUEST: undefined,
- CODEBUFF_GITHUB_TOKEN: undefined,
- CODEBUFF_API_KEY: 'test-api-key',
+ CODEBIRDS_GITHUB_TOKEN: undefined,
+ CODEBIRDS_API_KEY: 'test-api-key',
...overrides,
})
diff --git a/common/src/testing-env-process.ts b/common/src/testing-env-process.ts
index d1e787bc02..4c922c848c 100644
--- a/common/src/testing-env-process.ts
+++ b/common/src/testing-env-process.ts
@@ -2,7 +2,7 @@ import type { BaseEnv, ProcessEnv } from './types/contracts/env'
/**
* Test-only helpers for process env snapshots.
- * Keep production code using `@codebuff/common/env-process`.
+ * Keep production code using `@codebirds/common/env-process`.
*/
export const createTestBaseEnv = (
overrides: Partial = {},
@@ -55,19 +55,19 @@ export const createTestProcessEnv = (
// Editor preferences
VISUAL: undefined,
EDITOR: undefined,
- CODEBUFF_CLI_EDITOR: undefined,
- CODEBUFF_EDITOR: undefined,
+ CODEBIRDS_CLI_EDITOR: undefined,
+ CODEBIRDS_EDITOR: undefined,
// Theme preferences
OPEN_TUI_THEME: undefined,
OPENTUI_THEME: undefined,
- // Codebuff CLI-specific
- CODEBUFF_IS_BINARY: undefined,
- CODEBUFF_CLI_VERSION: undefined,
- CODEBUFF_CLI_TARGET: undefined,
- CODEBUFF_RG_PATH: undefined,
- CODEBUFF_WASM_DIR: undefined,
+ // Codebirds CLI-specific
+ CODEBIRDS_IS_BINARY: undefined,
+ CODEBIRDS_CLI_VERSION: undefined,
+ CODEBIRDS_CLI_TARGET: undefined,
+ CODEBIRDS_RG_PATH: undefined,
+ CODEBIRDS_WASM_DIR: undefined,
// Build/CI flags
VERBOSE: undefined,
diff --git a/common/src/testing/TESTING_PATTERNS.md b/common/src/testing/TESTING_PATTERNS.md
index 203114ae20..18e614cc5c 100644
--- a/common/src/testing/TESTING_PATTERNS.md
+++ b/common/src/testing/TESTING_PATTERNS.md
@@ -1,6 +1,6 @@
# Testing Patterns Guide
-This guide documents best practices for writing tests in the Codebuff codebase, based on lessons learned from buffbench runs and production issues.
+This guide documents best practices for writing tests in the Codebirds codebase, based on lessons learned from buffbench runs and production issues.
## Table of Contents
@@ -51,7 +51,7 @@ spyOn(db, 'insert').mockReturnValue({
```typescript
// GOOD: Type-safe, reusable, documented
-import { setupDbSpies } from '@codebuff/common/testing/mocks'
+import { setupDbSpies } from '@codebirds/common/testing/mocks'
const dbSpies = setupDbSpies(db, { defaultInsertId: 'test-id' })
// dbSpies.insert is properly typed
@@ -80,7 +80,7 @@ import {
// Stream mocks
createToolCallChunk,
createMockStream,
-} from '@codebuff/common/testing/mocks'
+} from '@codebirds/common/testing/mocks'
```
---
@@ -165,7 +165,7 @@ it('test 2', () => {
### Testing with Mock Logger
```typescript
-import { createMockLoggerWithCapture } from '@codebuff/common/testing/mocks'
+import { createMockLoggerWithCapture } from '@codebirds/common/testing/mocks'
describe('myFunction', () => {
it('logs errors appropriately', async () => {
@@ -182,8 +182,8 @@ describe('myFunction', () => {
### Testing with Mock Analytics
```typescript
-import { setupAnalyticsMocks } from '@codebuff/common/testing/mocks'
-import * as analytics from '@codebuff/common/analytics'
+import { setupAnalyticsMocks } from '@codebirds/common/testing/mocks'
+import * as analytics from '@codebirds/common/analytics'
describe('tracking', () => {
let analyticsSpy: AnalyticsSpies
@@ -208,7 +208,7 @@ describe('tracking', () => {
### Testing with Deterministic UUIDs
```typescript
-import { setupCryptoMocks } from '@codebuff/common/testing/mocks'
+import { setupCryptoMocks } from '@codebirds/common/testing/mocks'
describe('ID generation', () => {
let cryptoSpies: CryptoMockSpies
@@ -239,7 +239,7 @@ import {
createTextChunk,
createToolCallChunk,
collectStreamChunks,
-} from '@codebuff/common/testing/mocks'
+} from '@codebirds/common/testing/mocks'
describe('stream processing', () => {
it('handles tool calls', async () => {
@@ -262,8 +262,8 @@ describe('stream processing', () => {
### Testing Database Operations
```typescript
-import { setupDbSpies } from '@codebuff/common/testing/mocks'
-import db from '@codebuff/internal/db'
+import { setupDbSpies } from '@codebirds/common/testing/mocks'
+import db from '@codebirds/internal/db'
describe('data layer', () => {
let dbSpies: DbSpies
@@ -318,7 +318,7 @@ rg "filterOutSystemRole\|filterSystem" --type ts
Don't duplicate mock file context creators. Use the shared one:
```typescript
-import { mockFileContext } from '@codebuff/common/testing/fixtures/agent-runtime'
+import { mockFileContext } from '@codebirds/common/testing/fixtures/agent-runtime'
// Don't create a new one in each test file
```
diff --git a/common/src/testing/fixtures/agent-runtime.ts b/common/src/testing/fixtures/agent-runtime.ts
index f4d1430127..44f00d1835 100644
--- a/common/src/testing/fixtures/agent-runtime.ts
+++ b/common/src/testing/fixtures/agent-runtime.ts
@@ -11,7 +11,7 @@
* createTestAgentRuntimeParams,
* createTestAgentRuntimeDeps,
* mockFileContext,
- * } from '@codebuff/common/testing/fixtures/agent-runtime'
+ * } from '@codebirds/common/testing/fixtures/agent-runtime'
*
* const params = createTestAgentRuntimeParams()
* const { agentTemplate, localAgentTemplates } = params
@@ -75,8 +75,8 @@ export const testFetch = Object.assign(
export const testClientEnv = {
NEXT_PUBLIC_CB_ENVIRONMENT: 'test' as const,
- NEXT_PUBLIC_CODEBUFF_APP_URL: 'https://test.codebuff.com',
- NEXT_PUBLIC_SUPPORT_EMAIL: 'support@codebuff.test',
+ NEXT_PUBLIC_CODEBIRDS_APP_URL: 'https://test.codebirds.com',
+ NEXT_PUBLIC_SUPPORT_EMAIL: 'support@codebirds.test',
NEXT_PUBLIC_POSTHOG_API_KEY: 'test-posthog-key',
NEXT_PUBLIC_POSTHOG_HOST_URL: 'https://test.posthog.com',
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: 'pk_test_123',
@@ -90,8 +90,8 @@ export const testCiEnv = {
GITHUB_ACTIONS: undefined,
RENDER: undefined,
IS_PULL_REQUEST: undefined,
- CODEBUFF_GITHUB_TOKEN: undefined,
- CODEBUFF_API_KEY: 'test-api-key',
+ CODEBIRDS_GITHUB_TOKEN: undefined,
+ CODEBIRDS_API_KEY: 'test-api-key',
}
/** @deprecated Use createTestAgentRuntimeParams() */
diff --git a/common/src/testing/impl/agent-runtime.ts b/common/src/testing/impl/agent-runtime.ts
index e1b6181a28..c4d6ca82bf 100644
--- a/common/src/testing/impl/agent-runtime.ts
+++ b/common/src/testing/impl/agent-runtime.ts
@@ -1,5 +1,5 @@
/**
- * @deprecated Use `@codebuff/common/testing/fixtures/agent-runtime` instead.
+ * @deprecated Use `@codebirds/common/testing/fixtures/agent-runtime` instead.
*/
export * from '../fixtures/agent-runtime'
diff --git a/common/src/testing/index.ts b/common/src/testing/index.ts
index 18892c2b46..e6c4f3d008 100644
--- a/common/src/testing/index.ts
+++ b/common/src/testing/index.ts
@@ -1,5 +1,5 @@
/**
- * Consolidated testing utilities for Codebuff.
+ * Consolidated testing utilities for Codebirds.
*
* This module re-exports all testing utilities from a single entry point,
* making it easy to import everything you need for testing.
@@ -30,7 +30,7 @@
* // Module mocking
* mockModule,
* clearMockedModules,
- * } from '@codebuff/common/testing'
+ * } from '@codebirds/common/testing'
* ```
*
* @module testing
@@ -80,5 +80,5 @@ export type { CreateTestSetupOptions, TestSetupResult } from './setup'
// Note: These are in separate files for historical reasons but are commonly
// used together with other testing utilities.
// Import directly from their modules if you need only env helpers:
-// - '@codebuff/common/testing-env-process' for process env
-// - '@codebuff/common/testing-env-ci' for CI env
+// - '@codebirds/common/testing-env-process' for process env
+// - '@codebirds/common/testing-env-ci' for CI env
diff --git a/common/src/testing/mock-types.ts b/common/src/testing/mock-types.ts
index f41147ee58..226ea1eee6 100644
--- a/common/src/testing/mock-types.ts
+++ b/common/src/testing/mock-types.ts
@@ -11,7 +11,7 @@
* createMockLogger,
* type MockUserInfo,
* type MockCreditResult,
- * } from '@codebuff/common/testing/mock-types'
+ * } from '@codebirds/common/testing/mock-types'
*
* const logger = createMockLogger()
* const userInfo: MockUserInfo = { id: 'user-123' }
diff --git a/common/src/testing/mocks/analytics.ts b/common/src/testing/mocks/analytics.ts
index a9c2a6d693..e839fd7d4e 100644
--- a/common/src/testing/mocks/analytics.ts
+++ b/common/src/testing/mocks/analytics.ts
@@ -6,7 +6,7 @@
*
* @example
* ```typescript
- * import { createMockAnalytics, setupAnalyticsMocks } from '@codebuff/common/testing/mocks/analytics'
+ * import { createMockAnalytics, setupAnalyticsMocks } from '@codebirds/common/testing/mocks/analytics'
*
* // Option 1: Create mock object
* const analytics = createMockAnalytics()
@@ -203,7 +203,7 @@ export interface AnalyticsSpies {
*
* @example
* ```typescript
- * import * as analytics from '@codebuff/common/analytics'
+ * import * as analytics from '@codebirds/common/analytics'
*
* describe('my test', () => {
* let analyticsSpy: AnalyticsSpies
diff --git a/common/src/testing/mocks/crypto.ts b/common/src/testing/mocks/crypto.ts
index a5dbb972bb..1623f5b96b 100644
--- a/common/src/testing/mocks/crypto.ts
+++ b/common/src/testing/mocks/crypto.ts
@@ -6,7 +6,7 @@
*
* @example
* ```typescript
- * import { setupCryptoMocks, createMockUuid } from '@codebuff/common/testing/mocks/crypto'
+ * import { setupCryptoMocks, createMockUuid } from '@codebirds/common/testing/mocks/crypto'
*
* // Setup deterministic UUIDs
* const spies = setupCryptoMocks()
diff --git a/common/src/testing/mocks/database.ts b/common/src/testing/mocks/database.ts
index 3ad9c108ea..773e9fc921 100644
--- a/common/src/testing/mocks/database.ts
+++ b/common/src/testing/mocks/database.ts
@@ -6,7 +6,7 @@
*
* @example
* ```typescript
- * import { createMockDbOperations, setupDbSpies } from '@codebuff/common/testing/mocks/database'
+ * import { createMockDbOperations, setupDbSpies } from '@codebirds/common/testing/mocks/database'
*
* // Option 1: Create mock operations object
* const dbOps = createMockDbOperations()
diff --git a/common/src/testing/mocks/filesystem.ts b/common/src/testing/mocks/filesystem.ts
index 6c9703622e..70bcaf77b0 100644
--- a/common/src/testing/mocks/filesystem.ts
+++ b/common/src/testing/mocks/filesystem.ts
@@ -1,6 +1,6 @@
import { mock } from 'bun:test'
-import type { CodebuffFileSystem } from '../../types/filesystem'
+import type { CodebirdsFileSystem } from '../../types/filesystem'
import type { Mock } from 'bun:test'
import type { PathLike , Stats } from 'node:fs'
@@ -17,7 +17,7 @@ export interface CreateMockFsOptions {
statImpl?: (path: string) => Promise
}
-export interface MockFs extends CodebuffFileSystem {}
+export interface MockFs extends CodebirdsFileSystem {}
export interface MockFsWithMocks {
readFile: Mock<
@@ -34,7 +34,7 @@ export interface MockFsWithMocks {
stat: Mock<(path: PathLike) => Promise>
}
-/** Creates a mock filesystem compatible with CodebuffFileSystem. */
+/** Creates a mock filesystem compatible with CodebirdsFileSystem. */
export function createMockFs(options: CreateMockFsOptions = {}): MockFs {
const {
files = {},
diff --git a/common/src/testing/mocks/stream.ts b/common/src/testing/mocks/stream.ts
index 3677133215..7affdec9d7 100644
--- a/common/src/testing/mocks/stream.ts
+++ b/common/src/testing/mocks/stream.ts
@@ -6,7 +6,7 @@
*
* @example
* ```typescript
- * import { createMockStream, createToolCallChunk } from '@codebuff/common/testing/mocks/stream'
+ * import { createMockStream, createToolCallChunk } from '@codebirds/common/testing/mocks/stream'
*
* // Create a mock stream with text and tool calls
* const stream = createMockStream([
diff --git a/common/src/testing/setup.ts b/common/src/testing/setup.ts
index 5758fbb601..8e8632a830 100644
--- a/common/src/testing/setup.ts
+++ b/common/src/testing/setup.ts
@@ -6,7 +6,7 @@
*
* @example
* ```typescript
- * import { createTestSetup, TestSetupResult } from '@codebuff/common/testing/setup'
+ * import { createTestSetup, TestSetupResult } from '@codebirds/common/testing/setup'
*
* describe('my test', () => {
* const setup = createTestSetup()
@@ -113,7 +113,7 @@ export interface TestSetupResult {
*
* @example
* ```typescript
- * import * as analytics from '@codebuff/common/analytics'
+ * import * as analytics from '@codebirds/common/analytics'
* const db = createMockDbOperations()
*
* describe('my test', () => {
diff --git a/common/src/tools/constants.ts b/common/src/tools/constants.ts
index 89c89f7038..b68c218768 100644
--- a/common/src/tools/constants.ts
+++ b/common/src/tools/constants.ts
@@ -5,7 +5,7 @@ import type { Tool } from 'ai'
export const toolNameParam = 'cb_tool_name'
export const endsAgentStepParam = 'cb_easp'
-export const toolXmlName = 'codebuff_tool_call'
+export const toolXmlName = 'codebirds_tool_call'
export const startToolTag = `<${toolXmlName}>\n`
export const endToolTag = `\n${toolXmlName}>`
diff --git a/common/src/tools/list.ts b/common/src/tools/list.ts
index fbf59f3682..3754bdb03a 100644
--- a/common/src/tools/list.ts
+++ b/common/src/tools/list.ts
@@ -39,7 +39,7 @@ import { writeFileParams } from './params/tool/write-file'
import { writeTodosParams } from './params/tool/write-todos'
import type { $ToolParams, PublishedToolName, ToolName } from './constants'
-import type { ToolMessage } from '../types/messages/codebuff-message'
+import type { ToolMessage } from '../types/messages/codebirds-message'
import type { ToolCallPart } from '../types/messages/content-part'
export const toolParams = {
@@ -84,21 +84,21 @@ export const toolParams = {
}
// Tool call from LLM after parsing
-export type CodebuffToolCall = {
+export type CodebirdsToolCall = {
[K in ToolName]: {
toolName: K
input: z.infer<(typeof toolParams)[K]['inputSchema']>
} & Omit
}[T]
-export type CodebuffToolOutput = {
+export type CodebirdsToolOutput = {
[K in ToolName]: K extends ToolName
? z.infer<(typeof toolParams)[K]['outputSchema']>
: never
}[T]
-export type CodebuffToolMessage = ToolMessage & {
- content: CodebuffToolOutput
+export type CodebirdsToolMessage = ToolMessage & {
+ content: CodebirdsToolOutput
}
// Tool call to send to client
diff --git a/common/src/tools/params/tool/composio.ts b/common/src/tools/params/tool/composio.ts
index a990d767f2..a801aeba6c 100644
--- a/common/src/tools/params/tool/composio.ts
+++ b/common/src/tools/params/tool/composio.ts
@@ -70,7 +70,7 @@ const composioMetaToolInputSchemas = {
sync_response_to_workbench: z
.boolean()
.default(false)
- .describe('Always use false. Codebuff disables Composio workbench.'),
+ .describe('Always use false. Codebirds disables Composio workbench.'),
session_id: sessionIdParam,
})
.catchall(z.unknown()),
diff --git a/common/src/tools/params/tool/lookup-agent-info.ts b/common/src/tools/params/tool/lookup-agent-info.ts
index 029668ec4f..82ce5cdf62 100644
--- a/common/src/tools/params/tool/lookup-agent-info.ts
+++ b/common/src/tools/params/tool/lookup-agent-info.ts
@@ -22,7 +22,7 @@ ${$getNativeToolCallExampleString({
toolName,
inputSchema,
input: {
- agentId: 'codebuff/researcher@0.0.1',
+ agentId: 'codebirds/researcher@0.0.1',
},
endsAgentStep,
})}
diff --git a/common/src/tools/params/tool/render-ui.ts b/common/src/tools/params/tool/render-ui.ts
index 183d3ab090..aa31fa6864 100644
--- a/common/src/tools/params/tool/render-ui.ts
+++ b/common/src/tools/params/tool/render-ui.ts
@@ -52,7 +52,7 @@ const inputSchema = z
widget: widgetSchema.describe('The UI widget to render.'),
})
.describe(
- 'Render a small interactive UI widget in the Codebuff CLI. Currently supports a button that opens a link.',
+ 'Render a small interactive UI widget in the Codebirds CLI. Currently supports a button that opens a link.',
)
const outputSchema = z.object({
@@ -60,7 +60,7 @@ const outputSchema = z.object({
})
const description = `
-Render a small interactive UI widget in the Codebuff CLI.
+Render a small interactive UI widget in the Codebirds CLI.
Currently supported widgets:
- button: renders a clickable button with text and an http(s) link.
diff --git a/common/src/tools/params/tool/run-terminal-command.ts b/common/src/tools/params/tool/run-terminal-command.ts
index 4bd53f0c23..d9e1369945 100644
--- a/common/src/tools/params/tool/run-terminal-command.ts
+++ b/common/src/tools/params/tool/run-terminal-command.ts
@@ -59,8 +59,8 @@ When the user requests a new git commit, please follow these steps closely:
4. **Create the commit, ending with this specific footer:**
\`\`\`
- Generated with Codebuff 🤖
- Co-Authored-By: Codebuff
+ Generated with Codebirds 🤖
+ Co-Authored-By: Codebirds
\`\`\`
To maintain proper formatting, use cross-platform compatible commit messages:
@@ -69,8 +69,8 @@ When the user requests a new git commit, please follow these steps closely:
git commit -m "$(cat <<'EOF'
Your commit message here.
- 🤖 Generated with Codebuff
- Co-Authored-By: Codebuff
+ 🤖 Generated with Codebirds
+ Co-Authored-By: Codebirds
EOF
)"
\`\`\`
@@ -79,8 +79,8 @@ When the user requests a new git commit, please follow these steps closely:
\`\`\`
git commit -m "Your commit message here.
- 🤖 Generated with Codebuff
- Co-Authored-By: Codebuff "
+ 🤖 Generated with Codebirds
+ Co-Authored-By: Codebirds "
\`\`\`
Always detect the platform and use the appropriate syntax. HEREDOC syntax (\`<<'EOF'\`) only works in bash/Unix shells and will fail on Windows Command Prompt.
@@ -171,8 +171,8 @@ ${$getNativeToolCallExampleString({
input: {
command: `git commit -m "Your commit message here.
-🤖 Generated with Codebuff
-Co-Authored-By: Codebuff "`,
+🤖 Generated with Codebirds
+Co-Authored-By: Codebirds "`,
},
endsAgentStep,
})}
diff --git a/common/src/tools/params/tool/str-replace.ts b/common/src/tools/params/tool/str-replace.ts
index 1c697913c9..137e40c1ac 100644
--- a/common/src/tools/params/tool/str-replace.ts
+++ b/common/src/tools/params/tool/str-replace.ts
@@ -84,7 +84,7 @@ ${$getNativeToolCallExampleString({
},
{
oldString:
- '\n\t\t// @codebuff delete this log line please\n\t\tconsole.log("Hello, world!");\n',
+ '\n\t\t// @codebirds delete this log line please\n\t\tconsole.log("Hello, world!");\n',
newString: '\n',
},
{
diff --git a/common/src/types/contracts/database.ts b/common/src/types/contracts/database.ts
index bcb29b74aa..feca98f87d 100644
--- a/common/src/types/contracts/database.ts
+++ b/common/src/types/contracts/database.ts
@@ -1,5 +1,5 @@
-import type { AgentTemplate } from '@codebuff/common/types/agent-template'
-import type { Logger } from '@codebuff/common/types/contracts/logger'
+import type { AgentTemplate } from '@codebirds/common/types/agent-template'
+import type { Logger } from '@codebirds/common/types/contracts/logger'
type User = {
id: string
diff --git a/common/src/types/contracts/env.ts b/common/src/types/contracts/env.ts
index b2704275fa..8eb57bcef4 100644
--- a/common/src/types/contracts/env.ts
+++ b/common/src/types/contracts/env.ts
@@ -80,12 +80,12 @@ export type BaseEnv = {
// =============================================================================
/**
- * Extended CI env with Codebuff-specific CI vars.
+ * Extended CI env with Codebirds-specific CI vars.
* Used by agent-runtime and server code.
*/
export type CiEnv = BaseCiEnv & {
- CODEBUFF_GITHUB_TOKEN?: string
- CODEBUFF_API_KEY?: string
+ CODEBIRDS_GITHUB_TOKEN?: string
+ CODEBIRDS_API_KEY?: string
EVAL_RESULTS_EMAIL?: string
}
@@ -121,19 +121,19 @@ export type ProcessEnv = BaseEnv & {
// Editor preferences
VISUAL?: string
EDITOR?: string
- CODEBUFF_CLI_EDITOR?: string
- CODEBUFF_EDITOR?: string
+ CODEBIRDS_CLI_EDITOR?: string
+ CODEBIRDS_EDITOR?: string
// Theme preferences
OPEN_TUI_THEME?: string
OPENTUI_THEME?: string
- // Codebuff CLI-specific (set during binary build)
- CODEBUFF_IS_BINARY?: string
- CODEBUFF_CLI_VERSION?: string
- CODEBUFF_CLI_TARGET?: string
- CODEBUFF_RG_PATH?: string
- CODEBUFF_WASM_DIR?: string
+ // Codebirds CLI-specific (set during binary build)
+ CODEBIRDS_IS_BINARY?: string
+ CODEBIRDS_CLI_VERSION?: string
+ CODEBIRDS_CLI_TARGET?: string
+ CODEBIRDS_RG_PATH?: string
+ CODEBIRDS_WASM_DIR?: string
// Build/CI flags
VERBOSE?: string
diff --git a/common/src/types/contracts/llm.ts b/common/src/types/contracts/llm.ts
index 11c5a5ba0c..054c226a47 100644
--- a/common/src/types/contracts/llm.ts
+++ b/common/src/types/contracts/llm.ts
@@ -4,7 +4,7 @@ import type { OpenRouterProviderRoutingOptions , AgentTemplate } from '../agent-
import type { ParamsExcluding } from '../function-params'
import type { Logger } from './logger'
import type { Model } from '../../old-constants'
-import type { Message } from '../messages/codebuff-message'
+import type { Message } from '../messages/codebirds-message'
import type { PromptResult } from '../../util/error'
import type { generateText, streamText, ToolCallPart } from 'ai'
import type z from 'zod/v4'
@@ -62,10 +62,10 @@ export type PromptAiSdkStreamFn = (
localAgentTemplates?: Record
/** Cost mode - 'free' mode means 0 credits charged for all agents */
costMode?: string
- /** Extra key/values merged into the request's `codebuff_metadata` field.
- * Used to forward client-scoped identifiers (e.g. `freebuff_instance_id`)
+ /** Extra key/values merged into the request's `codebirds_metadata` field.
+ * Used to forward client-scoped identifiers (e.g. `codebirds_instance_id`)
* that server-side gates read from the chat-completions body. */
- extraCodebuffMetadata?: Record
+ extraCodebirdsMetadata?: Record
sendAction: SendActionFn
logger: Logger
trackEvent: TrackEventFn
diff --git a/common/src/types/contracts/logs.ts b/common/src/types/contracts/logs.ts
index 436cabaac2..56e912b1ac 100644
--- a/common/src/types/contracts/logs.ts
+++ b/common/src/types/contracts/logs.ts
@@ -6,7 +6,7 @@ export type LogSource = 'server' | 'cli' | 'browser'
/**
* One normalized log/event record. This is the provider-agnostic shape the
- * sink ingests (currently into Axiom — see `@codebuff/logging`). An "event" is
+ * sink ingests (currently into Axiom — see `@codebirds/logging`). An "event" is
* just a log row with `event` populated, so all logs and analytics events live
* in one stream. See docs/logging.md.
*/
@@ -17,7 +17,7 @@ export type LogRow = {
timestamp: Date
level: LogLevel
source: LogSource
- /** Emitting service, e.g. 'web', 'agent-runtime', 'freebuff-web', 'cli'. */
+ /** Emitting service, e.g. 'web', 'agent-runtime', 'codebirds-web', 'cli'. */
service?: string | null
/** Deploy environment: 'dev' | 'test' | 'prod'. */
env: string
diff --git a/common/src/types/contracts/trace.ts b/common/src/types/contracts/trace.ts
index 30b927378e..8c70a203b0 100644
--- a/common/src/types/contracts/trace.ts
+++ b/common/src/types/contracts/trace.ts
@@ -1,4 +1,4 @@
-import type { Message } from '../messages/codebuff-message'
+import type { Message } from '../messages/codebirds-message'
/**
* Records agent message histories for debugging (e.g. to a trace.jsonl file).
diff --git a/common/src/types/filesystem.ts b/common/src/types/filesystem.ts
index 6fa64e1168..77fd00234d 100644
--- a/common/src/types/filesystem.ts
+++ b/common/src/types/filesystem.ts
@@ -1,10 +1,10 @@
import type fs from 'fs'
-/** File system used for Codebuff SDK.
+/** File system used for Codebirds SDK.
*
* Compatible with `fs.promises` from the `'fs'` module.
*/
-export type CodebuffFileSystem = Pick<
+export type CodebirdsFileSystem = Pick<
typeof fs.promises,
'mkdir' | 'readdir' | 'readFile' | 'stat' | 'unlink' | 'writeFile'
>
diff --git a/common/src/types/freebuff-session.ts b/common/src/types/freebuff-session.ts
index 7460b8ec40..c2bce3a0e2 100644
--- a/common/src/types/freebuff-session.ts
+++ b/common/src/types/freebuff-session.ts
@@ -1,7 +1,7 @@
-import type { FreebuffAccessTier } from '../constants/freebuff-models'
+import type { FreebuffAccessTier } from '../constants/codebirds-models'
/**
- * Wire-level shapes returned by `/api/v1/freebuff/session`. Source of truth
+ * Wire-level shapes returned by `/api/v1/codebirds/session`. Source of truth
* for the CLI (which deserializes these) and the server (which serializes
* them) — keep both in sync by importing this module from either side.
*
@@ -250,7 +250,7 @@ export type FreebuffSessionServerResponse =
* and come back later. */
status: 'rate_limited'
accessTier?: FreebuffAccessTier
- /** The freebuff model the user tried to join. */
+ /** The codebirds model the user tried to join. */
model: string
/** Max session units permitted per period (e.g. 5/day premium, or the
* user's weekly GLM referral entitlement). */
diff --git a/common/src/types/gravity-index.ts b/common/src/types/gravity-index.ts
index 0a28514861..623ccea5af 100644
--- a/common/src/types/gravity-index.ts
+++ b/common/src/types/gravity-index.ts
@@ -77,23 +77,23 @@ const runtimeAttributionFields = {
.string()
.optional()
.describe(
- 'Stable Codebuff session ID for Gravity API key attribution. Codebuff usually fills this automatically.',
+ 'Stable Codebirds session ID for Gravity API key attribution. Codebirds usually fills this automatically.',
),
// Raw, stable per-end-user identifier for Gravity attribution + payout
// matching. Surfaces that run under a shared service account (e.g. Freebuff
// Web) set this to the real end user so conversions don't all collapse onto
// the service account. The server hashes it before sending it to Gravity.
- // Codebuff usually fills this automatically.
+ // Codebirds usually fills this automatically.
external_user_id: z
.string()
.optional()
.describe(
- 'Stable per-end-user identifier for Gravity attribution. Codebuff usually fills this automatically.',
+ 'Stable per-end-user identifier for Gravity attribution. Codebirds usually fills this automatically.',
),
metadata: jsonObjectSchema
.optional()
.describe(
- 'Non-sensitive API key metadata for Gravity attribution and debugging. Codebuff usually fills this automatically.',
+ 'Non-sensitive API key metadata for Gravity attribution and debugging. Codebirds usually fills this automatically.',
),
}
diff --git a/common/src/types/messages/codebuff-message.ts b/common/src/types/messages/codebirds-message.ts
similarity index 100%
rename from common/src/types/messages/codebuff-message.ts
rename to common/src/types/messages/codebirds-message.ts
diff --git a/common/src/types/session-state.ts b/common/src/types/session-state.ts
index a116a5cdeb..0d6139ca57 100644
--- a/common/src/types/session-state.ts
+++ b/common/src/types/session-state.ts
@@ -2,7 +2,7 @@ import { z } from 'zod/v4'
import { MAX_AGENT_STEPS_DEFAULT } from '../constants/agents'
-import type { Message } from './messages/codebuff-message'
+import type { Message } from './messages/codebirds-message'
import type { ProjectFileContext } from '../util/file'
export const toolCallSchema = z.object({
diff --git a/common/src/types/spawn.ts b/common/src/types/spawn.ts
index ed9f5a5ddb..45c46ec8dd 100644
--- a/common/src/types/spawn.ts
+++ b/common/src/types/spawn.ts
@@ -6,7 +6,7 @@ import type { ChildProcess, SpawnOptions } from 'child_process'
* Compatible with `child_process.spawn` from Node.js.
* Returns ChildProcess to support full streaming capabilities (stdin, stdout, stderr).
*/
-export type CodebuffSpawn = (
+export type CodebirdsSpawn = (
command: string,
args?: readonly string[],
options?: SpawnOptions,
diff --git a/common/src/util/__tests__/analytics-dispatcher.test.ts b/common/src/util/__tests__/analytics-dispatcher.test.ts
index d13c24e0f1..5630a0d490 100644
--- a/common/src/util/__tests__/analytics-dispatcher.test.ts
+++ b/common/src/util/__tests__/analytics-dispatcher.test.ts
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'bun:test'
-import { AnalyticsEvent } from '@codebuff/common/constants/analytics-events'
+import { AnalyticsEvent } from '@codebirds/common/constants/analytics-events'
import { createAnalyticsDispatcher } from '../analytics-dispatcher'
diff --git a/common/src/util/__tests__/analytics-log.test.ts b/common/src/util/__tests__/analytics-log.test.ts
index 5a4ada4c92..a04bf419de 100644
--- a/common/src/util/__tests__/analytics-log.test.ts
+++ b/common/src/util/__tests__/analytics-log.test.ts
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'bun:test'
-import { AnalyticsEvent } from '@codebuff/common/constants/analytics-events'
+import { AnalyticsEvent } from '@codebirds/common/constants/analytics-events'
import {
getAnalyticsEventId,
diff --git a/common/src/util/__tests__/analytics-sampling.test.ts b/common/src/util/__tests__/analytics-sampling.test.ts
index 9fcb8fc6c3..2459a950d6 100644
--- a/common/src/util/__tests__/analytics-sampling.test.ts
+++ b/common/src/util/__tests__/analytics-sampling.test.ts
@@ -1,6 +1,6 @@
import { afterEach, describe, expect, it } from 'bun:test'
-import { AnalyticsEvent } from '@codebuff/common/constants/analytics-events'
+import { AnalyticsEvent } from '@codebirds/common/constants/analytics-events'
import {
isFullTelemetryEnabled,
@@ -9,10 +9,10 @@ import {
} from '../analytics-sampling'
const ORIGINAL_ENV = {
- CODEBUFF_FULL_TELEMETRY: process.env.CODEBUFF_FULL_TELEMETRY,
- CODEBUFF_FULL_TELEMETRY_IDS: process.env.CODEBUFF_FULL_TELEMETRY_IDS,
- CODEBUFF_FULL_TELEMETRY_USER_IDS:
- process.env.CODEBUFF_FULL_TELEMETRY_USER_IDS,
+ CODEBIRDS_FULL_TELEMETRY: process.env.CODEBIRDS_FULL_TELEMETRY,
+ CODEBIRDS_FULL_TELEMETRY_IDS: process.env.CODEBIRDS_FULL_TELEMETRY_IDS,
+ CODEBIRDS_FULL_TELEMETRY_USER_IDS:
+ process.env.CODEBIRDS_FULL_TELEMETRY_USER_IDS,
}
function restoreEnv() {
@@ -74,15 +74,15 @@ describe('analytics sampling', () => {
})
it('honors full telemetry env flags and allowlists', () => {
- process.env.CODEBUFF_FULL_TELEMETRY = 'true'
+ process.env.CODEBIRDS_FULL_TELEMETRY = 'true'
expect(
isFullTelemetryEnabled({
distinctId: 'anyone',
}),
).toBe(true)
- delete process.env.CODEBUFF_FULL_TELEMETRY
- process.env.CODEBUFF_FULL_TELEMETRY_IDS = 'user-2,person@example.com'
+ delete process.env.CODEBIRDS_FULL_TELEMETRY
+ process.env.CODEBIRDS_FULL_TELEMETRY_IDS = 'user-2,person@example.com'
expect(
isFullTelemetryEnabled({
diff --git a/common/src/util/__tests__/error-api-details.test.ts b/common/src/util/__tests__/error-api-details.test.ts
index 29a1742a81..81b35c7fa7 100644
--- a/common/src/util/__tests__/error-api-details.test.ts
+++ b/common/src/util/__tests__/error-api-details.test.ts
@@ -12,7 +12,7 @@ describe('extractApiErrorDetails', () => {
apiError.responseBody = JSON.stringify({
error: 'session_superseded',
message:
- 'Another instance of freebuff has taken over this session. Only one instance per account is allowed.',
+ 'Another instance of codebirds has taken over this session. Only one instance per account is allowed.',
})
const retryError = new Error(
@@ -29,7 +29,7 @@ describe('extractApiErrorDetails', () => {
statusCode: 409,
errorCode: 'session_superseded',
message:
- 'Another instance of freebuff has taken over this session. Only one instance per account is allowed.',
+ 'Another instance of codebirds has taken over this session. Only one instance per account is allowed.',
})
})
diff --git a/common/src/util/__tests__/freebuff-streak.test.ts b/common/src/util/__tests__/freebuff-streak.test.ts
index a09919e20a..373e7db089 100644
--- a/common/src/util/__tests__/freebuff-streak.test.ts
+++ b/common/src/util/__tests__/freebuff-streak.test.ts
@@ -6,9 +6,9 @@ import {
getFreebuffUsageDateKey,
isFreebuffStreakMilestone,
streakRewardPoolsForMilestone,
-} from '../freebuff-streak'
+} from '../codebirds-streak'
-describe('freebuff streak helpers', () => {
+describe('codebirds streak helpers', () => {
test('formats usage dates in the Freebuff reset timezone', () => {
expect(getFreebuffUsageDateKey(new Date('2026-05-27T06:30:00.000Z'))).toBe(
'2026-05-26',
@@ -64,7 +64,7 @@ describe('freebuff streak helpers', () => {
})
})
-describe('freebuff streak rewards', () => {
+describe('codebirds streak rewards', () => {
test('recognizes 7-day multiples as milestones', () => {
expect(isFreebuffStreakMilestone(7)).toBe(true)
expect(isFreebuffStreakMilestone(14)).toBe(true)
diff --git a/common/src/util/__tests__/messages.test.ts b/common/src/util/__tests__/messages.test.ts
index 39d2fa7abf..9428d6ad48 100644
--- a/common/src/util/__tests__/messages.test.ts
+++ b/common/src/util/__tests__/messages.test.ts
@@ -12,7 +12,7 @@ import {
mediaToolResult,
} from '../messages'
-import type { Message } from '../../types/messages/codebuff-message'
+import type { Message } from '../../types/messages/codebirds-message'
import type { ToolResultPart } from 'ai'
// Test helper types for provider options with cache control
diff --git a/common/src/util/agent-name-normalization.ts b/common/src/util/agent-name-normalization.ts
index a18ff8c8b4..a70f5c1dab 100644
--- a/common/src/util/agent-name-normalization.ts
+++ b/common/src/util/agent-name-normalization.ts
@@ -1,4 +1,4 @@
-export const DEFAULT_ORG_PREFIX = 'codebuff/'
+export const DEFAULT_ORG_PREFIX = 'codebirds/'
/**
* Resolves an agent ID by trying multiple strategies:
diff --git a/common/src/util/analytics-log.ts b/common/src/util/analytics-log.ts
index 5d2bfbbcb9..322b7fb56b 100644
--- a/common/src/util/analytics-log.ts
+++ b/common/src/util/analytics-log.ts
@@ -1,4 +1,4 @@
-import { AnalyticsEvent } from '@codebuff/common/constants/analytics-events'
+import { AnalyticsEvent } from '@codebirds/common/constants/analytics-events'
// Build PostHog payloads from log data in a single, shared place
export type AnalyticsLogData = {
diff --git a/common/src/util/analytics-sampling.ts b/common/src/util/analytics-sampling.ts
index d2b6d4368b..d5df0b5a5e 100644
--- a/common/src/util/analytics-sampling.ts
+++ b/common/src/util/analytics-sampling.ts
@@ -42,7 +42,7 @@ const ALWAYS_TRACK_EVENTS = new Set([
AnalyticsEvent.DESKTOP_MODEL_CHANGED,
AnalyticsEvent.DESKTOP_SKILL_RUN,
AnalyticsEvent.TERMINAL_COMMAND_COMPLETED,
- AnalyticsEvent.UPDATE_CODEBUFF_FAILED,
+ AnalyticsEvent.UPDATE_CODEBIRDS_FAILED,
AnalyticsEvent.USER_INPUT,
AnalyticsEvent.USER_INPUT_COMPLETE,
])
@@ -92,13 +92,13 @@ export function isFullTelemetryEnabled(params: {
distinctId?: string
properties?: AnalyticsProperties
}): boolean {
- if (isTruthyEnv(process.env.CODEBUFF_FULL_TELEMETRY)) {
+ if (isTruthyEnv(process.env.CODEBIRDS_FULL_TELEMETRY)) {
return true
}
const ids = splitEnvList(
- process.env.CODEBUFF_FULL_TELEMETRY_IDS ??
- process.env.CODEBUFF_FULL_TELEMETRY_USER_IDS,
+ process.env.CODEBIRDS_FULL_TELEMETRY_IDS ??
+ process.env.CODEBIRDS_FULL_TELEMETRY_USER_IDS,
)
if (ids.size === 0) {
return false
diff --git a/common/src/util/error.ts b/common/src/util/error.ts
index 32700911cd..9de6841135 100644
--- a/common/src/util/error.ts
+++ b/common/src/util/error.ts
@@ -72,7 +72,7 @@ export type ErrorObject = {
stack?: string
/** HTTP status code from error.status (used by some libraries) */
status?: number
- /** HTTP status code from error.statusCode (used by AI SDK and Codebuff errors) */
+ /** HTTP status code from error.statusCode (used by AI SDK and Codebirds errors) */
statusCode?: number
/** Optional machine-friendly error code, if available */
code?: string
diff --git a/common/src/util/file.ts b/common/src/util/file.ts
index 733081c24d..97313f4be0 100644
--- a/common/src/util/file.ts
+++ b/common/src/util/file.ts
@@ -3,7 +3,7 @@ import * as path from 'path'
import { z } from 'zod/v4'
-import type { CodebuffFileSystem } from '../types/filesystem'
+import type { CodebirdsFileSystem } from '../types/filesystem'
import type { SkillsMap } from '../types/skill'
export const FileTreeNodeSchema: z.ZodType = z.object({
@@ -255,7 +255,7 @@ export const ensureEndsWithNewline = (
*/
export async function fileExists(params: {
filePath: string
- fs: CodebuffFileSystem
+ fs: CodebirdsFileSystem
}): Promise {
const { filePath, fs } = params
@@ -269,7 +269,7 @@ export async function fileExists(params: {
export const ensureDirectoryExists = async (params: {
baseDir: string
- fs: CodebuffFileSystem
+ fs: CodebirdsFileSystem
}) => {
const { baseDir, fs } = params
@@ -304,7 +304,7 @@ export function isValidFilePath(path: string) {
export async function isDir(params: {
path: string
- fs: CodebuffFileSystem
+ fs: CodebirdsFileSystem
}): Promise {
const { path, fs } = params
diff --git a/common/src/util/freebuff-privacy.ts b/common/src/util/freebuff-privacy.ts
index cbfdca452e..dd81782d23 100644
--- a/common/src/util/freebuff-privacy.ts
+++ b/common/src/util/freebuff-privacy.ts
@@ -1,6 +1,6 @@
-import type { FreebuffIpPrivacySignal } from '../types/freebuff-session'
+import type { FreebuffIpPrivacySignal } from '../types/codebirds-session'
-export const FREEBUFF_HARD_BLOCKED_PRIVACY_SIGNALS = [
+export const CODEBIRDS_HARD_BLOCKED_PRIVACY_SIGNALS = [
'vpn',
'proxy',
'tor',
@@ -8,12 +8,12 @@ export const FREEBUFF_HARD_BLOCKED_PRIVACY_SIGNALS = [
] as const satisfies readonly FreebuffIpPrivacySignal[]
type FreebuffHardBlockedPrivacySignal =
- (typeof FREEBUFF_HARD_BLOCKED_PRIVACY_SIGNALS)[number]
+ (typeof CODEBIRDS_HARD_BLOCKED_PRIVACY_SIGNALS)[number]
-const FREEBUFF_HARD_BLOCKED_PRIVACY_SIGNAL_SET =
- new Set(FREEBUFF_HARD_BLOCKED_PRIVACY_SIGNALS)
+const CODEBIRDS_HARD_BLOCKED_PRIVACY_SIGNAL_SET =
+ new Set(CODEBIRDS_HARD_BLOCKED_PRIVACY_SIGNALS)
-const FREEBUFF_HARD_BLOCKED_PRIVACY_SIGNAL_LABELS: Record<
+const CODEBIRDS_HARD_BLOCKED_PRIVACY_SIGNAL_LABELS: Record<
FreebuffHardBlockedPrivacySignal,
string
> = {
@@ -26,7 +26,7 @@ const FREEBUFF_HARD_BLOCKED_PRIVACY_SIGNAL_LABELS: Record<
export function isFreebuffHardBlockedPrivacySignal(
signal: FreebuffIpPrivacySignal,
): signal is FreebuffHardBlockedPrivacySignal {
- return FREEBUFF_HARD_BLOCKED_PRIVACY_SIGNAL_SET.has(signal)
+ return CODEBIRDS_HARD_BLOCKED_PRIVACY_SIGNAL_SET.has(signal)
}
/**
@@ -37,7 +37,7 @@ export function isFreebuffHardBlockedPrivacySignal(
* users sit behind, so we treat them as benign even when other heuristics
* (e.g. ipinfo's `is_hosting` flag) would otherwise fire.
*/
-const FREEBUFF_BENIGN_AS_TYPES = new Set([
+const CODEBIRDS_BENIGN_AS_TYPES = new Set([
'isp',
'business',
'education',
@@ -47,7 +47,7 @@ const FREEBUFF_BENIGN_AS_TYPES = new Set([
export function isFreebuffBenignAsType(
asType: string | null | undefined,
): boolean {
- return asType != null && FREEBUFF_BENIGN_AS_TYPES.has(asType.toLowerCase())
+ return asType != null && CODEBIRDS_BENIGN_AS_TYPES.has(asType.toLowerCase())
}
export function isFreebuffHostingAsType(
@@ -63,7 +63,7 @@ export function formatFreebuffHardBlockedPrivacySignals(
new Set(
(signals ?? []).flatMap((signal): string[] => {
if (!isFreebuffHardBlockedPrivacySignal(signal)) return []
- return [FREEBUFF_HARD_BLOCKED_PRIVACY_SIGNAL_LABELS[signal]]
+ return [CODEBIRDS_HARD_BLOCKED_PRIVACY_SIGNAL_LABELS[signal]]
}),
),
)
diff --git a/common/src/util/freebuff-streak.ts b/common/src/util/freebuff-streak.ts
index 6c0bc3cb66..6977f0897a 100644
--- a/common/src/util/freebuff-streak.ts
+++ b/common/src/util/freebuff-streak.ts
@@ -1,17 +1,17 @@
import {
- FREEBUFF_GLM_V52_REFERRAL_ENABLED,
- FREEBUFF_PREMIUM_SESSION_RESET_TIMEZONE,
- FREEBUFF_STREAK_GLM_BONUS_ENABLED,
- FREEBUFF_STREAK_REWARD_INTERVAL_DAYS,
- FREEBUFF_STREAK_REWARDS_ENABLED,
-} from '../constants/freebuff-models'
+ CODEBIRDS_GLM_V52_REFERRAL_ENABLED,
+ CODEBIRDS_PREMIUM_SESSION_RESET_TIMEZONE,
+ CODEBIRDS_STREAK_GLM_BONUS_ENABLED,
+ CODEBIRDS_STREAK_REWARD_INTERVAL_DAYS,
+ CODEBIRDS_STREAK_REWARDS_ENABLED,
+} from '../constants/codebirds-models'
import type {
FreebuffAccessTier,
FreebuffStreakRewardPool,
-} from '../constants/freebuff-models'
+} from '../constants/codebirds-models'
-export const FREEBUFF_STREAK_TIME_ZONE = FREEBUFF_PREMIUM_SESSION_RESET_TIMEZONE
+export const CODEBIRDS_STREAK_TIME_ZONE = CODEBIRDS_PREMIUM_SESSION_RESET_TIMEZONE
const DAY_MS = 24 * 60 * 60 * 1000
@@ -32,7 +32,7 @@ function dateKeyFromParts(parts: Intl.DateTimeFormatPart[]): string {
export function getFreebuffUsageDateKey(
now: Date = new Date(),
- timeZone = FREEBUFF_STREAK_TIME_ZONE,
+ timeZone = CODEBIRDS_STREAK_TIME_ZONE,
): string {
return dateKeyFromParts(
new Intl.DateTimeFormat('en-US', {
@@ -95,7 +95,7 @@ export function calculateFreebuffStreak(params: {
/** True when `streak` lands exactly on a streak-reward milestone (a positive
* multiple of the 7-day interval). */
export function isFreebuffStreakMilestone(streak: number): boolean {
- return streak > 0 && streak % FREEBUFF_STREAK_REWARD_INTERVAL_DAYS === 0
+ return streak > 0 && streak % CODEBIRDS_STREAK_REWARD_INTERVAL_DAYS === 0
}
/**
@@ -108,9 +108,9 @@ export function isFreebuffStreakMilestone(streak: number): boolean {
*/
export function isFreebuffStreakGlmBonusActive(): boolean {
return (
- FREEBUFF_STREAK_REWARDS_ENABLED &&
- FREEBUFF_STREAK_GLM_BONUS_ENABLED &&
- FREEBUFF_GLM_V52_REFERRAL_ENABLED
+ CODEBIRDS_STREAK_REWARDS_ENABLED &&
+ CODEBIRDS_STREAK_GLM_BONUS_ENABLED &&
+ CODEBIRDS_GLM_V52_REFERRAL_ENABLED
)
}
@@ -126,7 +126,7 @@ export function streakRewardPoolsForMilestone(params: {
todayUsed: boolean
accessTier: FreebuffAccessTier
}): FreebuffStreakRewardPool[] {
- if (!FREEBUFF_STREAK_REWARDS_ENABLED) return []
+ if (!CODEBIRDS_STREAK_REWARDS_ENABLED) return []
if (!params.todayUsed || !isFreebuffStreakMilestone(params.streak)) return []
if (params.accessTier === 'limited') return ['limited']
const pools: FreebuffStreakRewardPool[] = ['premium']
diff --git a/common/src/util/messages.ts b/common/src/util/messages.ts
index 37b7494943..767ab9fe02 100644
--- a/common/src/util/messages.ts
+++ b/common/src/util/messages.ts
@@ -10,7 +10,7 @@ import type {
SystemMessage,
ToolMessage,
UserMessage,
-} from '../types/messages/codebuff-message'
+} from '../types/messages/codebirds-message'
import type { ToolResultOutput } from '../types/messages/content-part'
import type { ProviderMetadata } from '../types/messages/provider-metadata'
import type {
@@ -40,7 +40,7 @@ export function withCacheControl<
wrapper.providerOptions = {}
}
- /* 'codebuff' provider name is not compatible with providerMetadata for
+ /* 'codebirds' provider name is not compatible with providerMetadata for
* messages, so we need to use 'openaiCompatible' instead.
* https://github.com/vercel/ai/blob/8e4fdac31b4f8c6a8d07a606a8833e74adf99470/packages/openai-compatible/src/chat/convert-to-openai-compatible-chat-messages.ts#L9
*/
@@ -100,7 +100,7 @@ type ModelMessageWithAuxiliaryData = (
) &
AuxiliaryMessageData
-function assistantToCodebuffMessage(
+function assistantToCodebirdsMessage(
message: Omit & {
content: Exclude[number]
},
@@ -185,7 +185,7 @@ function convertToolMessage(message: Message): ModelMessageWithAuxiliaryData[] {
]
}
return message.content.map((c) => {
- return assistantToCodebuffMessage({
+ return assistantToCodebirdsMessage({
...message,
content: c,
})
diff --git a/common/src/util/xml.ts b/common/src/util/xml.ts
index 3a433319dd..89a868b2f5 100644
--- a/common/src/util/xml.ts
+++ b/common/src/util/xml.ts
@@ -13,5 +13,5 @@ export function closeXml(toolName: string): string {
* @returns Array of closing XML tag strings
*/
export function getStopSequences(toolNames: readonly string[]): string[] {
- return toolNames.map((toolName) => ``)
+ return toolNames.map((toolName) => ``)
}
diff --git a/docs/agents-and-tools.md b/docs/agents-and-tools.md
index 4ea7475896..8c956bfcdd 100644
--- a/docs/agents-and-tools.md
+++ b/docs/agents-and-tools.md
@@ -7,11 +7,11 @@
### Shell Shims
-Direct commands without `codebuff` prefix:
+Direct commands without `codebirds` prefix:
```bash
-codebuff shims install codebuff/base-lite@1.0.0
-eval "$(codebuff shims env)"
+codebirds shims install codebirds/base-lite@1.0.0
+eval "$(codebirds shims env)"
base-lite "fix this bug"
```
diff --git a/docs/testing.md b/docs/testing.md
index 3862f66adb..c14af78796 100644
--- a/docs/testing.md
+++ b/docs/testing.md
@@ -2,7 +2,7 @@
- Prefer dependency injection over module mocking; define contracts in `common/src/types/contracts/`.
- Use `spyOn()` only for globals / legacy seams.
-- Avoid `mock.module()` for functions; use `@codebuff/common/testing/mock-modules.ts` helpers for constants only.
+- Avoid `mock.module()` for functions; use `@codebirds/common/testing/mock-modules.ts` helpers for constants only.
CLI hook testing note: React 19 + Bun + RTL `renderHook()` is unreliable; prefer integration tests via components for hook behavior.
diff --git a/eslint.config.js b/eslint.config.js
index 48ef179c78..bc6cf96d13 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -25,7 +25,7 @@ export default tseslint.config(
{
paths: [
{
- name: '@codebuff/common/env-process',
+ name: '@codebirds/common/env-process',
importNames: ['getProcessEnv', 'processEnv'],
message:
'CLI should use getCliEnv() from "../utils/env" or "./env" instead of getProcessEnv() from common. This ensures CLI uses CliEnv type.',
@@ -33,7 +33,7 @@ export default tseslint.config(
],
patterns: [
{
- group: ['@codebuff/common/types/contracts/env'],
+ group: ['@codebirds/common/types/contracts/env'],
importNames: ['ProcessEnv'],
message:
'CLI should use CliEnv from "../types/env" instead of ProcessEnv from common.',
@@ -53,7 +53,7 @@ export default tseslint.config(
{
paths: [
{
- name: '@codebuff/common/env-process',
+ name: '@codebirds/common/env-process',
importNames: ['getProcessEnv', 'processEnv'],
message:
'SDK should use getSdkEnv() from "./env" instead of getProcessEnv() from common. This ensures SDK uses SdkEnv type.',
@@ -61,7 +61,7 @@ export default tseslint.config(
],
patterns: [
{
- group: ['@codebuff/common/types/contracts/env'],
+ group: ['@codebirds/common/types/contracts/env'],
importNames: ['ProcessEnv'],
message:
'SDK should use SdkEnv from "./types/env" instead of ProcessEnv from common.',
diff --git a/evals/buffbench/README.md b/evals/buffbench/README.md
index 9e6dc4d303..cd22482c23 100644
--- a/evals/buffbench/README.md
+++ b/evals/buffbench/README.md
@@ -1,6 +1,6 @@
# BuffBench
-BuffBench is Codebuff's evaluation framework for measuring AI coding agent performance through real-world git commit reconstruction tasks.
+BuffBench is Codebirds's evaluation framework for measuring AI coding agent performance through real-world git commit reconstruction tasks.
## Overview
@@ -141,11 +141,11 @@ BuffBench supports running external CLI coding agents for comparison:
- **Codex**: Use `external:codex` - requires `codex` CLI installed
- **OpenCode**: Use `external:opencode` - requires `opencode` CLI installed
-Example comparing Codebuff vs Claude Code:
+Example comparing Codebirds vs Claude Code:
```typescript
await runBuffBench({
- evalDataPath: 'evals/buffbench/eval-codebuff.json',
+ evalDataPath: 'evals/buffbench/eval-codebirds.json',
agents: ['base2', 'external:claude'],
taskConcurrency: 3,
})
@@ -196,7 +196,7 @@ evals/buffbench/
│
├── types.ts # Type definitions
│
-├── eval-codebuff.json # Codebuff project evaluations
+├── eval-codebirds.json # Codebirds project evaluations
├── eval-manifold.json # Manifold evaluations
├── eval-plane.json # Plane project evaluations
└── eval-saleor.json # Saleor e-commerce evaluations
@@ -212,7 +212,7 @@ evals/buffbench/
import { runBuffBench } from './run-buffbench'
await runBuffBench({
- evalDataPath: 'eval-codebuff.json',
+ evalDataPath: 'eval-codebirds.json',
agents: ['base2', 'base2-fast'],
taskConcurrency: 3,
})
@@ -386,7 +386,7 @@ logs/YYYY-MM-DDTHH-MM_agent1_vs_agent2/
{
"metadata": {
"timestamp": "2024-01-15T10:30:00.000Z",
- "evalDataPath": "eval-codebuff.json",
+ "evalDataPath": "eval-codebirds.json",
"agentsTested": ["base2", "base2-fast"],
"commitsEvaluated": 10,
"logsDirectory": "logs/..."
@@ -413,10 +413,10 @@ logs/YYYY-MM-DDTHH-MM_agent1_vs_agent2/
```bash
# Required
-CODEBUFF_API_KEY=your-api-key
+CODEBIRDS_API_KEY=your-api-key
# Optional
-EVAL_RESULTS_EMAIL=team@codebuff.com # For nightly email reports
+EVAL_RESULTS_EMAIL=team@codebirds.com # For nightly email reports
```
### Task Concurrency
@@ -433,7 +433,7 @@ taskConcurrency: 10 // High parallelism (faster, more resources)
The `evals/buffbench/` directory contains several example evaluation files:
-- **eval-codebuff.json** - Codebuff project evaluations
+- **eval-codebirds.json** - Codebirds project evaluations
- **eval-manifold.json** - Manifold prediction market evaluations
- **eval-plane.json** - Plane project management evaluations
- **eval-saleor.json** - Saleor e-commerce platform evaluations
diff --git a/evals/buffbench/agent-runner.ts b/evals/buffbench/agent-runner.ts
index 57f2fa1e50..e6e55356ab 100644
--- a/evals/buffbench/agent-runner.ts
+++ b/evals/buffbench/agent-runner.ts
@@ -3,17 +3,17 @@ import { promisify } from 'util'
const execAsync = promisify(exec)
-import { withTimeout } from '@codebuff/common/util/promise'
+import { withTimeout } from '@codebirds/common/util/promise'
import { withTestRepo } from '../subagents/test-repo-utils'
import { ClaudeRunner } from './runners/claude'
-import { CodebuffRunner } from './runners/codebuff'
+import { CodebirdsRunner } from './runners/codebirds'
import { CodexRunner } from './runners/codex'
import { OpenCodeRunner } from './runners/opencode'
import type { Runner, AgentStep } from './runners/runner'
import type { EvalCommitV2, FinalCheckOutput } from './types'
-import type { CodebuffClient } from '@codebuff/sdk'
+import type { CodebirdsClient } from '@codebirds/sdk'
export type { AgentStep }
@@ -31,7 +31,7 @@ export async function runAgentOnCommit({
finalCheckCommands,
externalAgentType,
}: {
- client: CodebuffClient
+ client: CodebirdsClient
agentId: string
commit: EvalCommitV2
repoUrl: string
@@ -79,7 +79,7 @@ export async function runAgentOnCommit({
} else if (externalAgentType === 'opencode') {
runner = new OpenCodeRunner(repoDir, env)
} else {
- runner = new CodebuffRunner({
+ runner = new CodebirdsRunner({
cwd: repoDir,
env,
client,
@@ -92,7 +92,7 @@ export async function runAgentOnCommit({
}
console.log(
- `[${commit.id}] Running agent: ${externalAgentType || 'codebuff'}`,
+ `[${commit.id}] Running agent: ${externalAgentType || 'codebirds'}`,
)
const result = await runner.run(commit.prompt)
diff --git a/evals/buffbench/eval-codebuff-hard.json b/evals/buffbench/eval-codebirds-hard.json
similarity index 60%
rename from evals/buffbench/eval-codebuff-hard.json
rename to evals/buffbench/eval-codebirds-hard.json
index 8b7910d7bc..7251fe8a0f 100644
--- a/evals/buffbench/eval-codebuff-hard.json
+++ b/evals/buffbench/eval-codebirds-hard.json
@@ -1,5 +1,5 @@
{
- "repoUrl": "https://github.com/CodebuffAI/codebuff",
+ "repoUrl": "https://github.com/CodebirdsAI/codebirds",
"generationDate": "2025-12-09T10:21:20.203Z",
"binInstalls": [
{
@@ -15,7 +15,7 @@
"id": "bundle-local-agents",
"sha": "81b5edd1dc2a7b0d6d249580d7cb621859e4b435",
"parentSha": "d60f29e94298acf7274768e59f7487e5730aa87e",
- "spec": "Implement CLI agent bundling and unified loading with override precedence.\n\nRequired changes:\n\n1) Add a prebuild script to bundle agents\n- Create cli/scripts/prebuild-agents.ts (executable for Bun) that:\n - Scans the CLI project’s .agents directory recursively for TypeScript files, skipping: types, prompts, registry, constants, __tests__, factory, node_modules.\n - Dynamically imports each candidate file and reads its default export as an agent definition. Skip files that do not export a valid agent (must have id and model).\n - If an agent has a handleSteps function, convert it to a string for serialization.\n - Produce a generated module at cli/src/agents/bundled-agents.generated.ts which exports:\n - bundledAgents: Record of all agents keyed by id\n - getBundledAgentsAsLocalInfo(): LocalAgentInfo[] mapping to { id, displayName || id, filePath: \"[bundled]\" }\n - getBundledAgentIds(): string[]\n - isBundledAgent(id: string): boolean\n - Include a header comment indicating the file is auto-generated with timestamp and count.\n\n2) Ignore the generated file\n- Update cli/.gitignore to ignore src/agents/bundled-agents.generated.ts under a “Generated files” section.\n\n3) Wire prebuild into dev and build flows\n- Update cli/package.json scripts:\n - Add \"prebuild:agents\": \"bun run scripts/prebuild-agents.ts\"\n - Update \"dev\" to run prebuild:agents before launching the app: \"bun run prebuild:agents && bun run src/index.tsx\"\n- Update cli/scripts/build-binary.ts to run the prebuild-agents script before building SDK dependencies and compiling the binary.\n\n4) Consolidate agent-loading APIs in local-agent-registry\n- Delete cli/src/utils/load-agent-definitions.ts (no longer used).\n- Extend cli/src/utils/local-agent-registry.ts to:\n - Import the generated bundled module when available (require('../agents/bundled-agents.generated')), but handle absence gracefully (development before generation).\n - Expose helpers to read bundled agents: getBundledAgents() and getBundledAgentsAsLocalInfo().\n - Refactor gatherAgentFiles to return LocalAgentInfo[] (id, displayName, filePath) for user .agents files detected by displayName: or id: in file content; keep the same skip rules as in the prebuild script.\n - Keep findAgentsDirectory() behavior (locate nearest .agents from project root upwards) with caching.\n - Implement loadLocalAgents(): LocalAgentInfo[] that merges bundled agents first, then user agents from .agents; user agents override bundled ones by matching id; sort by displayName.\n - Implement loadAgentDefinitions(): AgentDefinition[] that returns bundled agents’ full definitions, then merges user .agents definitions by require(filePath) with the same override-by-id precedence. Do not stringify handleSteps here (SDK will handle conversions for user agents).\n - Ensure all caches and testing reset utilities remain available (e.g., __resetLocalAgentRegistryForTests) and still clear any internal caches.\n\n5) Update all imports to use the unified registry\n- Replace imports of loadAgentDefinitions from ./utils/load-agent-definitions with imports from ./utils/local-agent-registry in:\n - cli/src/__tests__/integration/local-agents.test.ts\n - cli/src/commands/publish.ts\n - cli/src/hooks/use-agent-validation.ts\n - cli/src/hooks/use-send-message.ts\n - cli/src/index.tsx\n - cli/src/utils/codebuff-client.ts\n- Where applicable, also import getLoadedAgentsData from local-agent-registry instead of its previous location.\n\n6) Expected runtime behavior\n- Running “bun run dev” should auto-generate bundled-agents.generated.ts before launching, making default Codebuff agents available even when no user .agents directory exists.\n- Building the binary should generate bundled agents before compilation; the CLI binary should include these agents and run without filesystem access to .agents.\n- When a user has a .agents directory, any agent id present there should override the bundled definition (both for listing and for runtime definitions).\n- If the prebuilt file isn’t present (e.g., in certain dev scenarios), the loader must not crash; user .agents should still load normally if present.\n\nNon-goals:\n- Do not change agent validation logic in the SDK; rely on existing validateAgents/processAgentDefinitions behavior.\n- Do not change how events or networking behave.\n\nFiles to change or add:\n- Add: cli/scripts/prebuild-agents.ts\n- Modify: cli/.gitignore\n- Modify: cli/package.json\n- Modify: cli/scripts/build-binary.ts\n- Modify: cli/src/utils/local-agent-registry.ts\n- Delete: cli/src/utils/load-agent-definitions.ts\n- Modify imports in: cli/src/__tests__/integration/local-agents.test.ts, cli/src/commands/publish.ts, cli/src/hooks/use-agent-validation.ts, cli/src/hooks/use-send-message.ts, cli/src/index.tsx, cli/src/utils/codebuff-client.ts\n\nAcceptance criteria:\n- Running bun run dev generates cli/src/agents/bundled-agents.generated.ts, starts the CLI, and the UI shows agents when no .agents directory is present.\n- A user agent with the same id overrides the bundled one in both list and runtime behavior.\n- The binary built via scripts/build-binary.ts contains bundled agents and works without a .agents directory.\n- All updated imports compile and tests referencing local agent loading pass.",
+ "spec": "Implement CLI agent bundling and unified loading with override precedence.\n\nRequired changes:\n\n1) Add a prebuild script to bundle agents\n- Create cli/scripts/prebuild-agents.ts (executable for Bun) that:\n - Scans the CLI project’s .agents directory recursively for TypeScript files, skipping: types, prompts, registry, constants, __tests__, factory, node_modules.\n - Dynamically imports each candidate file and reads its default export as an agent definition. Skip files that do not export a valid agent (must have id and model).\n - If an agent has a handleSteps function, convert it to a string for serialization.\n - Produce a generated module at cli/src/agents/bundled-agents.generated.ts which exports:\n - bundledAgents: Record of all agents keyed by id\n - getBundledAgentsAsLocalInfo(): LocalAgentInfo[] mapping to { id, displayName || id, filePath: \"[bundled]\" }\n - getBundledAgentIds(): string[]\n - isBundledAgent(id: string): boolean\n - Include a header comment indicating the file is auto-generated with timestamp and count.\n\n2) Ignore the generated file\n- Update cli/.gitignore to ignore src/agents/bundled-agents.generated.ts under a “Generated files” section.\n\n3) Wire prebuild into dev and build flows\n- Update cli/package.json scripts:\n - Add \"prebuild:agents\": \"bun run scripts/prebuild-agents.ts\"\n - Update \"dev\" to run prebuild:agents before launching the app: \"bun run prebuild:agents && bun run src/index.tsx\"\n- Update cli/scripts/build-binary.ts to run the prebuild-agents script before building SDK dependencies and compiling the binary.\n\n4) Consolidate agent-loading APIs in local-agent-registry\n- Delete cli/src/utils/load-agent-definitions.ts (no longer used).\n- Extend cli/src/utils/local-agent-registry.ts to:\n - Import the generated bundled module when available (require('../agents/bundled-agents.generated')), but handle absence gracefully (development before generation).\n - Expose helpers to read bundled agents: getBundledAgents() and getBundledAgentsAsLocalInfo().\n - Refactor gatherAgentFiles to return LocalAgentInfo[] (id, displayName, filePath) for user .agents files detected by displayName: or id: in file content; keep the same skip rules as in the prebuild script.\n - Keep findAgentsDirectory() behavior (locate nearest .agents from project root upwards) with caching.\n - Implement loadLocalAgents(): LocalAgentInfo[] that merges bundled agents first, then user agents from .agents; user agents override bundled ones by matching id; sort by displayName.\n - Implement loadAgentDefinitions(): AgentDefinition[] that returns bundled agents’ full definitions, then merges user .agents definitions by require(filePath) with the same override-by-id precedence. Do not stringify handleSteps here (SDK will handle conversions for user agents).\n - Ensure all caches and testing reset utilities remain available (e.g., __resetLocalAgentRegistryForTests) and still clear any internal caches.\n\n5) Update all imports to use the unified registry\n- Replace imports of loadAgentDefinitions from ./utils/load-agent-definitions with imports from ./utils/local-agent-registry in:\n - cli/src/__tests__/integration/local-agents.test.ts\n - cli/src/commands/publish.ts\n - cli/src/hooks/use-agent-validation.ts\n - cli/src/hooks/use-send-message.ts\n - cli/src/index.tsx\n - cli/src/utils/codebirds-client.ts\n- Where applicable, also import getLoadedAgentsData from local-agent-registry instead of its previous location.\n\n6) Expected runtime behavior\n- Running “bun run dev” should auto-generate bundled-agents.generated.ts before launching, making default Codebirds agents available even when no user .agents directory exists.\n- Building the binary should generate bundled agents before compilation; the CLI binary should include these agents and run without filesystem access to .agents.\n- When a user has a .agents directory, any agent id present there should override the bundled definition (both for listing and for runtime definitions).\n- If the prebuilt file isn’t present (e.g., in certain dev scenarios), the loader must not crash; user .agents should still load normally if present.\n\nNon-goals:\n- Do not change agent validation logic in the SDK; rely on existing validateAgents/processAgentDefinitions behavior.\n- Do not change how events or networking behave.\n\nFiles to change or add:\n- Add: cli/scripts/prebuild-agents.ts\n- Modify: cli/.gitignore\n- Modify: cli/package.json\n- Modify: cli/scripts/build-binary.ts\n- Modify: cli/src/utils/local-agent-registry.ts\n- Delete: cli/src/utils/load-agent-definitions.ts\n- Modify imports in: cli/src/__tests__/integration/local-agents.test.ts, cli/src/commands/publish.ts, cli/src/hooks/use-agent-validation.ts, cli/src/hooks/use-send-message.ts, cli/src/index.tsx, cli/src/utils/codebirds-client.ts\n\nAcceptance criteria:\n- Running bun run dev generates cli/src/agents/bundled-agents.generated.ts, starts the CLI, and the UI shows agents when no .agents directory is present.\n- A user agent with the same id overrides the bundled one in both list and runtime behavior.\n- The binary built via scripts/build-binary.ts contains bundled agents and works without a .agents directory.\n- All updated imports compile and tests referencing local agent loading pass.",
"prompt": "Add a prebuild step that bundles the CLI’s default agents into the binary and update the CLI to load agents from both bundled output and the user’s .agents directory, with user agents overriding by id. Provide a generated module exporting bundled agents and helper functions, wire the prebuild into dev and build flows, and consolidate agent-loading APIs into a single registry used throughout the CLI. Ensure the CLI works without a .agents directory and that user agents still override bundled ones when present.",
"supplementalFiles": [
"cli/src/utils/constants.ts",
@@ -35,7 +35,7 @@
{
"path": "cli/package.json",
"status": "modified",
- "diff": "Index: cli/package.json\n===================================================================\n--- cli/package.json\td60f29e (parent)\n+++ cli/package.json\t81b5edd (commit)\n@@ -14,9 +14,10 @@\n \"default\": \"./dist/index.js\"\n }\n },\n \"scripts\": {\n- \"dev\": \"bun run src/index.tsx\",\n+ \"dev\": \"bun run prebuild:agents && bun run src/index.tsx\",\n+ \"prebuild:agents\": \"bun run scripts/prebuild-agents.ts\",\n \"build:sdk\": \"cd ../sdk && bun run build\",\n \"build:binary\": \"bun ./scripts/build-binary.ts codebuff $npm_package_version\",\n \"release\": \"bun run scripts/release.ts\",\n \"start\": \"bun run dist/index.js\",\n"
+ "diff": "Index: cli/package.json\n===================================================================\n--- cli/package.json\td60f29e (parent)\n+++ cli/package.json\t81b5edd (commit)\n@@ -14,9 +14,10 @@\n \"default\": \"./dist/index.js\"\n }\n },\n \"scripts\": {\n- \"dev\": \"bun run src/index.tsx\",\n+ \"dev\": \"bun run prebuild:agents && bun run src/index.tsx\",\n+ \"prebuild:agents\": \"bun run scripts/prebuild-agents.ts\",\n \"build:sdk\": \"cd ../sdk && bun run build\",\n \"build:binary\": \"bun ./scripts/build-binary.ts codebirds $npm_package_version\",\n \"release\": \"bun run scripts/release.ts\",\n \"start\": \"bun run dist/index.js\",\n"
},
{
"path": "cli/scripts/build-binary.ts",
@@ -45,27 +45,27 @@
{
"path": "cli/scripts/prebuild-agents.ts",
"status": "added",
- "diff": "Index: cli/scripts/prebuild-agents.ts\n===================================================================\n--- cli/scripts/prebuild-agents.ts\td60f29e (parent)\n+++ cli/scripts/prebuild-agents.ts\t81b5edd (commit)\n@@ -0,0 +1,198 @@\n+#!/usr/bin/env bun\n+/**\n+ * Prebuild script that scans the .agents/ directory and generates a TypeScript\n+ * module with all agent definitions embedded as static data.\n+ *\n+ * This allows agent definitions to be bundled into the CLI binary without\n+ * requiring runtime filesystem access to the .agents/ directory.\n+ *\n+ * Run this before building the binary:\n+ * bun run scripts/prebuild-agents.ts\n+ */\n+\n+import * as fs from 'fs'\n+import * as path from 'path'\n+\n+const AGENTS_DIR = path.join(import.meta.dir, '../../.agents')\n+const OUTPUT_FILE = path.join(import.meta.dir, '../src/agents/bundled-agents.generated.ts')\n+\n+interface AgentDefinition {\n+ id: string\n+ displayName?: string\n+ [key: string]: any\n+}\n+\n+/**\n+ * Recursively get all TypeScript files from a directory\n+ */\n+function getAllTsFiles(dir: string): string[] {\n+ const files: string[] = []\n+\n+ try {\n+ const entries = fs.readdirSync(dir, { withFileTypes: true })\n+\n+ for (const entry of entries) {\n+ const fullPath = path.join(dir, entry.name)\n+\n+ if (entry.isDirectory()) {\n+ // Skip __tests__ and node_modules directories\n+ if (entry.name === '__tests__' || entry.name === 'node_modules' || entry.name === 'types') {\n+ continue\n+ }\n+ files.push(...getAllTsFiles(fullPath))\n+ } else if (\n+ entry.isFile() &&\n+ entry.name.endsWith('.ts') &&\n+ !entry.name.endsWith('.d.ts') &&\n+ !entry.name.endsWith('.test.ts')\n+ ) {\n+ files.push(fullPath)\n+ }\n+ }\n+ } catch (error) {\n+ console.error(`Error reading directory ${dir}:`, error)\n+ }\n+\n+ return files\n+}\n+\n+/**\n+ * Load and process an agent definition from a TypeScript file\n+ */\n+async function loadAgentDefinition(filePath: string): Promise {\n+ try {\n+ // Use dynamic import to load the module\n+ const module = await import(filePath)\n+ const definition = module.default\n+\n+ if (!definition || !definition.id || !definition.model) {\n+ return null\n+ }\n+\n+ // Process the definition - convert handleSteps function to string\n+ const processed: AgentDefinition = { ...definition }\n+ \n+ if (typeof processed.handleSteps === 'function') {\n+ processed.handleSteps = processed.handleSteps.toString()\n+ }\n+\n+ return processed\n+ } catch (error) {\n+ console.error(`Error loading agent from ${filePath}:`, error)\n+ return null\n+ }\n+}\n+\n+/**\n+ * Generate the bundled agents TypeScript file\n+ */\n+function generateBundledAgentsFile(agents: Record): string {\n+ const agentCount = Object.keys(agents).length\n+ \n+ return `/**\n+ * AUTO-GENERATED FILE - DO NOT EDIT MANUALLY\n+ * \n+ * This file is generated by scripts/prebuild-agents.ts\n+ * It contains all bundled agent definitions from the .agents/ directory.\n+ * \n+ * Generated at: ${new Date().toISOString()}\n+ * Agent count: ${agentCount}\n+ */\n+\n+import type { LocalAgentInfo } from '../utils/local-agent-registry'\n+\n+/**\n+ * All bundled agent definitions keyed by their ID.\n+ * These are the default Codebuff agents that ship with the CLI binary.\n+ */\n+export const bundledAgents: Record = ${JSON.stringify(agents, null, 2)};\n+\n+/**\n+ * Get bundled agents as LocalAgentInfo format for the CLI\n+ */\n+export function getBundledAgentsAsLocalInfo(): LocalAgentInfo[] {\n+ return Object.values(bundledAgents).map((agent) => ({\n+ id: agent.id,\n+ displayName: agent.displayName || agent.id,\n+ filePath: '[bundled]',\n+ }));\n+}\n+\n+/**\n+ * Get all bundled agent IDs\n+ */\n+export function getBundledAgentIds(): string[] {\n+ return Object.keys(bundledAgents);\n+}\n+\n+/**\n+ * Check if an agent ID is a bundled agent\n+ */\n+export function isBundledAgent(agentId: string): boolean {\n+ return agentId in bundledAgents;\n+}\n+`\n+}\n+\n+async function main() {\n+ const DEBUG = false\n+ if (DEBUG) {\n+ console.log('🔍 DEBUG: Scanning .agents/ directory...')\n+ }\n+ \n+ if (!fs.existsSync(AGENTS_DIR)) {\n+ console.error(`Error: .agents/ directory not found at ${AGENTS_DIR}`)\n+ process.exit(1)\n+ }\n+\n+ const tsFiles = getAllTsFiles(AGENTS_DIR)\n+ if (DEBUG) {\n+ console.log(`📁 DEBUG: Found ${tsFiles.length} TypeScript files`)\n+ }\n+\n+ const agents: Record = {}\n+ let loadedCount = 0\n+ let skippedCount = 0\n+\n+ for (const filePath of tsFiles) {\n+ const relativePath = path.relative(AGENTS_DIR, filePath)\n+ const definition = await loadAgentDefinition(filePath)\n+ \n+ if (definition) {\n+ agents[definition.id] = definition\n+ loadedCount++\n+ if (DEBUG) {\n+ console.log(` ✅ DEBUG: ${definition.id} (${relativePath})`)\n+ }\n+ } else {\n+ skippedCount++\n+ if (DEBUG) {\n+ console.log(` ⏭️ DEBUG: Skipped: ${relativePath} (no valid default export)`)\n+ }\n+ }\n+ }\n+\n+ if (DEBUG) {\n+ console.log(`\\n📦 DEBUG: Loaded ${loadedCount} agents, skipped ${skippedCount} files`)\n+ }\n+\n+ // Generate the output file\n+ const output = generateBundledAgentsFile(agents)\n+ \n+ // Ensure output directory exists\n+ const outputDir = path.dirname(OUTPUT_FILE)\n+ if (!fs.existsSync(outputDir)) {\n+ fs.mkdirSync(outputDir, { recursive: true })\n+ }\n+\n+ fs.writeFileSync(OUTPUT_FILE, output, 'utf-8')\n+ if (DEBUG) {\n+ console.log(`\\n✨ DEBUG: Generated ${OUTPUT_FILE}`)\n+ console.log(` DEBUG: ${Object.keys(agents).length} agents bundled`)\n+ }\n+}\n+\n+main().catch((error) => {\n+ console.error('Fatal error:', error)\n+ process.exit(1)\n+})\n"
+ "diff": "Index: cli/scripts/prebuild-agents.ts\n===================================================================\n--- cli/scripts/prebuild-agents.ts\td60f29e (parent)\n+++ cli/scripts/prebuild-agents.ts\t81b5edd (commit)\n@@ -0,0 +1,198 @@\n+#!/usr/bin/env bun\n+/**\n+ * Prebuild script that scans the .agents/ directory and generates a TypeScript\n+ * module with all agent definitions embedded as static data.\n+ *\n+ * This allows agent definitions to be bundled into the CLI binary without\n+ * requiring runtime filesystem access to the .agents/ directory.\n+ *\n+ * Run this before building the binary:\n+ * bun run scripts/prebuild-agents.ts\n+ */\n+\n+import * as fs from 'fs'\n+import * as path from 'path'\n+\n+const AGENTS_DIR = path.join(import.meta.dir, '../../.agents')\n+const OUTPUT_FILE = path.join(import.meta.dir, '../src/agents/bundled-agents.generated.ts')\n+\n+interface AgentDefinition {\n+ id: string\n+ displayName?: string\n+ [key: string]: any\n+}\n+\n+/**\n+ * Recursively get all TypeScript files from a directory\n+ */\n+function getAllTsFiles(dir: string): string[] {\n+ const files: string[] = []\n+\n+ try {\n+ const entries = fs.readdirSync(dir, { withFileTypes: true })\n+\n+ for (const entry of entries) {\n+ const fullPath = path.join(dir, entry.name)\n+\n+ if (entry.isDirectory()) {\n+ // Skip __tests__ and node_modules directories\n+ if (entry.name === '__tests__' || entry.name === 'node_modules' || entry.name === 'types') {\n+ continue\n+ }\n+ files.push(...getAllTsFiles(fullPath))\n+ } else if (\n+ entry.isFile() &&\n+ entry.name.endsWith('.ts') &&\n+ !entry.name.endsWith('.d.ts') &&\n+ !entry.name.endsWith('.test.ts')\n+ ) {\n+ files.push(fullPath)\n+ }\n+ }\n+ } catch (error) {\n+ console.error(`Error reading directory ${dir}:`, error)\n+ }\n+\n+ return files\n+}\n+\n+/**\n+ * Load and process an agent definition from a TypeScript file\n+ */\n+async function loadAgentDefinition(filePath: string): Promise {\n+ try {\n+ // Use dynamic import to load the module\n+ const module = await import(filePath)\n+ const definition = module.default\n+\n+ if (!definition || !definition.id || !definition.model) {\n+ return null\n+ }\n+\n+ // Process the definition - convert handleSteps function to string\n+ const processed: AgentDefinition = { ...definition }\n+ \n+ if (typeof processed.handleSteps === 'function') {\n+ processed.handleSteps = processed.handleSteps.toString()\n+ }\n+\n+ return processed\n+ } catch (error) {\n+ console.error(`Error loading agent from ${filePath}:`, error)\n+ return null\n+ }\n+}\n+\n+/**\n+ * Generate the bundled agents TypeScript file\n+ */\n+function generateBundledAgentsFile(agents: Record): string {\n+ const agentCount = Object.keys(agents).length\n+ \n+ return `/**\n+ * AUTO-GENERATED FILE - DO NOT EDIT MANUALLY\n+ * \n+ * This file is generated by scripts/prebuild-agents.ts\n+ * It contains all bundled agent definitions from the .agents/ directory.\n+ * \n+ * Generated at: ${new Date().toISOString()}\n+ * Agent count: ${agentCount}\n+ */\n+\n+import type { LocalAgentInfo } from '../utils/local-agent-registry'\n+\n+/**\n+ * All bundled agent definitions keyed by their ID.\n+ * These are the default Codebirds agents that ship with the CLI binary.\n+ */\n+export const bundledAgents: Record = ${JSON.stringify(agents, null, 2)};\n+\n+/**\n+ * Get bundled agents as LocalAgentInfo format for the CLI\n+ */\n+export function getBundledAgentsAsLocalInfo(): LocalAgentInfo[] {\n+ return Object.values(bundledAgents).map((agent) => ({\n+ id: agent.id,\n+ displayName: agent.displayName || agent.id,\n+ filePath: '[bundled]',\n+ }));\n+}\n+\n+/**\n+ * Get all bundled agent IDs\n+ */\n+export function getBundledAgentIds(): string[] {\n+ return Object.keys(bundledAgents);\n+}\n+\n+/**\n+ * Check if an agent ID is a bundled agent\n+ */\n+export function isBundledAgent(agentId: string): boolean {\n+ return agentId in bundledAgents;\n+}\n+`\n+}\n+\n+async function main() {\n+ const DEBUG = false\n+ if (DEBUG) {\n+ console.log('🔍 DEBUG: Scanning .agents/ directory...')\n+ }\n+ \n+ if (!fs.existsSync(AGENTS_DIR)) {\n+ console.error(`Error: .agents/ directory not found at ${AGENTS_DIR}`)\n+ process.exit(1)\n+ }\n+\n+ const tsFiles = getAllTsFiles(AGENTS_DIR)\n+ if (DEBUG) {\n+ console.log(`📁 DEBUG: Found ${tsFiles.length} TypeScript files`)\n+ }\n+\n+ const agents: Record = {}\n+ let loadedCount = 0\n+ let skippedCount = 0\n+\n+ for (const filePath of tsFiles) {\n+ const relativePath = path.relative(AGENTS_DIR, filePath)\n+ const definition = await loadAgentDefinition(filePath)\n+ \n+ if (definition) {\n+ agents[definition.id] = definition\n+ loadedCount++\n+ if (DEBUG) {\n+ console.log(` ✅ DEBUG: ${definition.id} (${relativePath})`)\n+ }\n+ } else {\n+ skippedCount++\n+ if (DEBUG) {\n+ console.log(` ⏭️ DEBUG: Skipped: ${relativePath} (no valid default export)`)\n+ }\n+ }\n+ }\n+\n+ if (DEBUG) {\n+ console.log(`\\n📦 DEBUG: Loaded ${loadedCount} agents, skipped ${skippedCount} files`)\n+ }\n+\n+ // Generate the output file\n+ const output = generateBundledAgentsFile(agents)\n+ \n+ // Ensure output directory exists\n+ const outputDir = path.dirname(OUTPUT_FILE)\n+ if (!fs.existsSync(outputDir)) {\n+ fs.mkdirSync(outputDir, { recursive: true })\n+ }\n+\n+ fs.writeFileSync(OUTPUT_FILE, output, 'utf-8')\n+ if (DEBUG) {\n+ console.log(`\\n✨ DEBUG: Generated ${OUTPUT_FILE}`)\n+ console.log(` DEBUG: ${Object.keys(agents).length} agents bundled`)\n+ }\n+}\n+\n+main().catch((error) => {\n+ console.error('Fatal error:', error)\n+ process.exit(1)\n+})\n"
},
{
"path": "cli/src/__tests__/integration/local-agents.test.ts",
"status": "modified",
- "diff": "Index: cli/src/__tests__/integration/local-agents.test.ts\n===================================================================\n--- cli/src/__tests__/integration/local-agents.test.ts\td60f29e (parent)\n+++ cli/src/__tests__/integration/local-agents.test.ts\t81b5edd (commit)\n@@ -5,9 +5,9 @@\n import { validateAgents } from '@codebuff/sdk'\n import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test'\n \n import { setProjectRoot, getProjectRoot } from '../../project-files'\n-import { loadAgentDefinitions } from '../../utils/load-agent-definitions'\n+import { loadAgentDefinitions } from '../../utils/local-agent-registry'\n import {\n findAgentsDirectory,\n __resetLocalAgentRegistryForTests,\n } from '../../utils/local-agent-registry'\n"
+ "diff": "Index: cli/src/__tests__/integration/local-agents.test.ts\n===================================================================\n--- cli/src/__tests__/integration/local-agents.test.ts\td60f29e (parent)\n+++ cli/src/__tests__/integration/local-agents.test.ts\t81b5edd (commit)\n@@ -5,9 +5,9 @@\n import { validateAgents } from '@codebirds/sdk'\n import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test'\n \n import { setProjectRoot, getProjectRoot } from '../../project-files'\n-import { loadAgentDefinitions } from '../../utils/load-agent-definitions'\n+import { loadAgentDefinitions } from '../../utils/local-agent-registry'\n import {\n findAgentsDirectory,\n __resetLocalAgentRegistryForTests,\n } from '../../utils/local-agent-registry'\n"
},
{
"path": "cli/src/commands/publish.ts",
"status": "modified",
- "diff": "Index: cli/src/commands/publish.ts\n===================================================================\n--- cli/src/commands/publish.ts\td60f29e (parent)\n+++ cli/src/commands/publish.ts\t81b5edd (commit)\n@@ -2,10 +2,9 @@\n import { cyan, green, red, yellow } from 'picocolors'\n \n import { getUserCredentials } from '../utils/auth'\n import { getApiClient, setApiClientAuthToken } from '../utils/codebuff-api'\n-import { loadAgentDefinitions } from '../utils/load-agent-definitions'\n-import { getLoadedAgentsData } from '../utils/local-agent-registry'\n+import { loadAgentDefinitions, getLoadedAgentsData } from '../utils/local-agent-registry'\n \n import type {\n PublishAgentsErrorResponse,\n PublishAgentsResponse,\n"
+ "diff": "Index: cli/src/commands/publish.ts\n===================================================================\n--- cli/src/commands/publish.ts\td60f29e (parent)\n+++ cli/src/commands/publish.ts\t81b5edd (commit)\n@@ -2,10 +2,9 @@\n import { cyan, green, red, yellow } from 'picocolors'\n \n import { getUserCredentials } from '../utils/auth'\n import { getApiClient, setApiClientAuthToken } from '../utils/codebirds-api'\n-import { loadAgentDefinitions } from '../utils/load-agent-definitions'\n-import { getLoadedAgentsData } from '../utils/local-agent-registry'\n+import { loadAgentDefinitions, getLoadedAgentsData } from '../utils/local-agent-registry'\n \n import type {\n PublishAgentsErrorResponse,\n PublishAgentsResponse,\n"
},
{
"path": "cli/src/hooks/use-agent-validation.ts",
"status": "modified",
- "diff": "Index: cli/src/hooks/use-agent-validation.ts\n===================================================================\n--- cli/src/hooks/use-agent-validation.ts\td60f29e (parent)\n+++ cli/src/hooks/use-agent-validation.ts\t81b5edd (commit)\n@@ -1,9 +1,9 @@\n import { validateAgents } from '@codebuff/sdk'\n import { useCallback, useState } from 'react'\n \n \n-import { loadAgentDefinitions } from '../utils/load-agent-definitions'\n+import { loadAgentDefinitions } from '../utils/local-agent-registry'\n import { logger } from '../utils/logger'\n \n export type ValidationError = {\n id: string\n"
+ "diff": "Index: cli/src/hooks/use-agent-validation.ts\n===================================================================\n--- cli/src/hooks/use-agent-validation.ts\td60f29e (parent)\n+++ cli/src/hooks/use-agent-validation.ts\t81b5edd (commit)\n@@ -1,9 +1,9 @@\n import { validateAgents } from '@codebirds/sdk'\n import { useCallback, useState } from 'react'\n \n \n-import { loadAgentDefinitions } from '../utils/load-agent-definitions'\n+import { loadAgentDefinitions } from '../utils/local-agent-registry'\n import { logger } from '../utils/logger'\n \n export type ValidationError = {\n id: string\n"
},
{
"path": "cli/src/hooks/use-send-message.ts",
"status": "modified",
- "diff": "Index: cli/src/hooks/use-send-message.ts\n===================================================================\n--- cli/src/hooks/use-send-message.ts\td60f29e (parent)\n+++ cli/src/hooks/use-send-message.ts\t81b5edd (commit)\n@@ -7,9 +7,9 @@\n import { getCodebuffClient } from '../utils/codebuff-client'\n import { AGENT_MODE_TO_ID } from '../utils/constants'\n import { createEventHandlerState } from '../utils/create-event-handler-state'\n import { createRunConfig } from '../utils/create-run-config'\n-import { loadAgentDefinitions } from '../utils/load-agent-definitions'\n+import { loadAgentDefinitions } from '../utils/local-agent-registry'\n import { logger } from '../utils/logger'\n import {\n loadMostRecentChatState,\n saveChatState,\n"
+ "diff": "Index: cli/src/hooks/use-send-message.ts\n===================================================================\n--- cli/src/hooks/use-send-message.ts\td60f29e (parent)\n+++ cli/src/hooks/use-send-message.ts\t81b5edd (commit)\n@@ -7,9 +7,9 @@\n import { getCodebirdsClient } from '../utils/codebirds-client'\n import { AGENT_MODE_TO_ID } from '../utils/constants'\n import { createEventHandlerState } from '../utils/create-event-handler-state'\n import { createRunConfig } from '../utils/create-run-config'\n-import { loadAgentDefinitions } from '../utils/load-agent-definitions'\n+import { loadAgentDefinitions } from '../utils/local-agent-registry'\n import { logger } from '../utils/logger'\n import {\n loadMostRecentChatState,\n saveChatState,\n"
},
{
"path": "cli/src/index.tsx",
@@ -73,19 +73,19 @@
"diff": "Index: cli/src/index.tsx\n===================================================================\n--- cli/src/index.tsx\td60f29e (parent)\n+++ cli/src/index.tsx\t81b5edd (commit)\n@@ -18,10 +18,9 @@\n import { initializeApp } from './init/init-app'\n import { getProjectRoot } from './project-files'\n import { initAnalytics } from './utils/analytics'\n import { getUserCredentials } from './utils/auth'\n-import { loadAgentDefinitions } from './utils/load-agent-definitions'\n-import { getLoadedAgentsData } from './utils/local-agent-registry'\n+import { loadAgentDefinitions, getLoadedAgentsData } from './utils/local-agent-registry'\n import { clearLogFile, logger } from './utils/logger'\n import { detectTerminalTheme } from './utils/terminal-color-detection'\n import { setOscDetectedTheme } from './utils/theme-system'\n import { filterNetworkErrors } from './utils/validation-error-helpers'\n"
},
{
- "path": "cli/src/utils/codebuff-client.ts",
+ "path": "cli/src/utils/codebirds-client.ts",
"status": "modified",
- "diff": "Index: cli/src/utils/codebuff-client.ts\n===================================================================\n--- cli/src/utils/codebuff-client.ts\td60f29e (parent)\n+++ cli/src/utils/codebuff-client.ts\t81b5edd (commit)\n@@ -2,9 +2,9 @@\n import { AskUserBridge } from '@codebuff/common/utils/ask-user-bridge'\n import { CodebuffClient } from '@codebuff/sdk'\n \n import { getAuthTokenDetails } from './auth'\n-import { loadAgentDefinitions } from './load-agent-definitions'\n+import { loadAgentDefinitions } from './local-agent-registry'\n import { logger } from './logger'\n import { getRgPath } from '../native/ripgrep'\n import { getProjectRoot } from '../project-files'\n \n"
+ "diff": "Index: cli/src/utils/codebirds-client.ts\n===================================================================\n--- cli/src/utils/codebirds-client.ts\td60f29e (parent)\n+++ cli/src/utils/codebirds-client.ts\t81b5edd (commit)\n@@ -2,9 +2,9 @@\n import { AskUserBridge } from '@codebirds/common/utils/ask-user-bridge'\n import { CodebirdsClient } from '@codebirds/sdk'\n \n import { getAuthTokenDetails } from './auth'\n-import { loadAgentDefinitions } from './load-agent-definitions'\n+import { loadAgentDefinitions } from './local-agent-registry'\n import { logger } from './logger'\n import { getRgPath } from '../native/ripgrep'\n import { getProjectRoot } from '../project-files'\n \n"
},
{
"path": "cli/src/utils/load-agent-definitions.ts",
"status": "deleted",
- "diff": "Index: cli/src/utils/load-agent-definitions.ts\n===================================================================\n--- cli/src/utils/load-agent-definitions.ts\td60f29e (parent)\n+++ cli/src/utils/load-agent-definitions.ts\t81b5edd (commit)\n@@ -1,96 +0,0 @@\n-import fs from 'fs'\n-import path from 'path'\n-\n-import { findAgentsDirectory } from './local-agent-registry'\n-\n-import type { AgentDefinition } from '@codebuff/common/templates/initial-agents-dir/types/agent-definition'\n-\n-const DISPLAY_NAME_REGEX = /displayName\\s*:\\s*['\"`]([^'\"`]+)['\"`]/i\n-const ID_REGEX = /id\\s*:\\s*['\"`]([^'\"`]+)['\"`]/i\n-\n-const shouldSkipDirectory = (dirName: string): boolean => {\n- if (!dirName) return true\n- if (dirName.startsWith('.')) return true\n- const skipped = new Set([\n- 'types',\n- 'prompts',\n- 'registry',\n- 'constants',\n- '__tests__',\n- 'factory',\n- 'node_modules',\n- ])\n- return skipped.has(dirName)\n-}\n-\n-const gatherAgentFiles = (dir: string): string[] => {\n- const results: string[] = []\n- const entries = fs.readdirSync(dir, { withFileTypes: true })\n-\n- for (const entry of entries) {\n- const fullPath = path.join(dir, entry.name)\n-\n- if (entry.isDirectory()) {\n- if (shouldSkipDirectory(entry.name)) {\n- continue\n- }\n- results.push(...gatherAgentFiles(fullPath))\n- continue\n- }\n-\n- if (!entry.isFile() || !entry.name.endsWith('.ts')) {\n- continue\n- }\n-\n- try {\n- const content = fs.readFileSync(fullPath, 'utf8')\n- const displayMatch = content.match(DISPLAY_NAME_REGEX)\n- const idMatch = content.match(ID_REGEX)\n-\n- if (displayMatch || idMatch) {\n- results.push(fullPath)\n- }\n- } catch {\n- continue\n- }\n- }\n-\n- return results\n-}\n-\n-/**\n- * Load local agent definitions from .agents directory and pass to SDK\n- * Note: The SDK's processAgentDefinitions will handle converting handleSteps functions to strings\n- */\n-export const loadAgentDefinitions = (): AgentDefinition[] => {\n- const agentsDir = findAgentsDirectory()\n- if (!agentsDir) {\n- return []\n- }\n-\n- const agentFiles = gatherAgentFiles(agentsDir)\n- const definitions: AgentDefinition[] = []\n-\n- for (const filePath of agentFiles) {\n- try {\n- // Use require to load the TypeScript file (works with ts-node/bun)\n- const agentModule = require(filePath)\n- const agentDef = agentModule.default\n- if (require.cache[filePath]) {\n- delete require.cache[filePath]\n- }\n-\n- if (!agentDef || !agentDef.id || !agentDef.model) {\n- continue\n- }\n-\n- // No need to convert handleSteps - SDK's processAgentDefinitions handles it\n- definitions.push(agentDef as AgentDefinition)\n- } catch (error) {\n- // Skip files that can't be loaded\n- continue\n- }\n- }\n-\n- return definitions\n-}\n"
+ "diff": "Index: cli/src/utils/load-agent-definitions.ts\n===================================================================\n--- cli/src/utils/load-agent-definitions.ts\td60f29e (parent)\n+++ cli/src/utils/load-agent-definitions.ts\t81b5edd (commit)\n@@ -1,96 +0,0 @@\n-import fs from 'fs'\n-import path from 'path'\n-\n-import { findAgentsDirectory } from './local-agent-registry'\n-\n-import type { AgentDefinition } from '@codebirds/common/templates/initial-agents-dir/types/agent-definition'\n-\n-const DISPLAY_NAME_REGEX = /displayName\\s*:\\s*['\"`]([^'\"`]+)['\"`]/i\n-const ID_REGEX = /id\\s*:\\s*['\"`]([^'\"`]+)['\"`]/i\n-\n-const shouldSkipDirectory = (dirName: string): boolean => {\n- if (!dirName) return true\n- if (dirName.startsWith('.')) return true\n- const skipped = new Set([\n- 'types',\n- 'prompts',\n- 'registry',\n- 'constants',\n- '__tests__',\n- 'factory',\n- 'node_modules',\n- ])\n- return skipped.has(dirName)\n-}\n-\n-const gatherAgentFiles = (dir: string): string[] => {\n- const results: string[] = []\n- const entries = fs.readdirSync(dir, { withFileTypes: true })\n-\n- for (const entry of entries) {\n- const fullPath = path.join(dir, entry.name)\n-\n- if (entry.isDirectory()) {\n- if (shouldSkipDirectory(entry.name)) {\n- continue\n- }\n- results.push(...gatherAgentFiles(fullPath))\n- continue\n- }\n-\n- if (!entry.isFile() || !entry.name.endsWith('.ts')) {\n- continue\n- }\n-\n- try {\n- const content = fs.readFileSync(fullPath, 'utf8')\n- const displayMatch = content.match(DISPLAY_NAME_REGEX)\n- const idMatch = content.match(ID_REGEX)\n-\n- if (displayMatch || idMatch) {\n- results.push(fullPath)\n- }\n- } catch {\n- continue\n- }\n- }\n-\n- return results\n-}\n-\n-/**\n- * Load local agent definitions from .agents directory and pass to SDK\n- * Note: The SDK's processAgentDefinitions will handle converting handleSteps functions to strings\n- */\n-export const loadAgentDefinitions = (): AgentDefinition[] => {\n- const agentsDir = findAgentsDirectory()\n- if (!agentsDir) {\n- return []\n- }\n-\n- const agentFiles = gatherAgentFiles(agentsDir)\n- const definitions: AgentDefinition[] = []\n-\n- for (const filePath of agentFiles) {\n- try {\n- // Use require to load the TypeScript file (works with ts-node/bun)\n- const agentModule = require(filePath)\n- const agentDef = agentModule.default\n- if (require.cache[filePath]) {\n- delete require.cache[filePath]\n- }\n-\n- if (!agentDef || !agentDef.id || !agentDef.model) {\n- continue\n- }\n-\n- // No need to convert handleSteps - SDK's processAgentDefinitions handles it\n- definitions.push(agentDef as AgentDefinition)\n- } catch (error) {\n- // Skip files that can't be loaded\n- continue\n- }\n- }\n-\n- return definitions\n-}\n"
},
{
"path": "cli/src/utils/local-agent-registry.ts",
"status": "modified",
- "diff": "Index: cli/src/utils/local-agent-registry.ts\n===================================================================\n--- cli/src/utils/local-agent-registry.ts\td60f29e (parent)\n+++ cli/src/utils/local-agent-registry.ts\t81b5edd (commit)\n@@ -4,37 +4,79 @@\n import { pluralize } from '@codebuff/common/util/string'\n \n import { getProjectRoot } from '../project-files'\n \n+import type { AgentDefinition } from '@codebuff/common/templates/initial-agents-dir/types/agent-definition'\n+\n+// ============================================================================\n+// Constants and types\n+// ============================================================================\n+\n+const DISPLAY_NAME_REGEX = /displayName\\s*:\\s*['\"`]([^'\"`]+)['\"`]/i\n+const ID_REGEX = /id\\s*:\\s*['\"`]([^'\"`]+)['\"`]/i\n+const AGENTS_DIR_NAME = '.agents'\n+\n+const SKIPPED_DIRECTORIES = new Set([\n+ 'types',\n+ 'prompts',\n+ 'registry',\n+ 'constants',\n+ '__tests__',\n+ 'factory',\n+ 'node_modules',\n+])\n+\n export interface LocalAgentInfo {\n id: string\n displayName: string\n filePath: string\n }\n \n-const DISPLAY_NAME_REGEX = /displayName\\s*:\\s*['\"`]([^'\"`]+)['\"`]/i\n-const ID_REGEX = /id\\s*:\\s*['\"`]([^'\"`]+)['\"`]/i\n-const AGENTS_DIR_NAME = '.agents'\n+// ============================================================================\n+// Bundled agents loading (generated at build time by prebuild-agents.ts)\n+// ============================================================================\n \n-let cachedAgents: LocalAgentInfo[] | null = null\n-let cachedAgentsDir: string | null = null\n+interface BundledAgentsModule {\n+ bundledAgents: Record\n+ getBundledAgentsAsLocalInfo: () => LocalAgentInfo[]\n+}\n \n+let bundledAgentsModule: BundledAgentsModule | null = null\n+try {\n+ bundledAgentsModule = require('../agents/bundled-agents.generated')\n+} catch {\n+ // File not generated yet - running in development without prebuild\n+}\n+\n+const getBundledAgents = (): Record => {\n+ return bundledAgentsModule?.bundledAgents ?? {}\n+}\n+\n+const getBundledAgentsAsLocalInfo = (): LocalAgentInfo[] => {\n+ return bundledAgentsModule?.getBundledAgentsAsLocalInfo?.() ?? []\n+}\n+\n+// ============================================================================\n+// File system utilities\n+// ============================================================================\n+\n const shouldSkipDirectory = (dirName: string): boolean => {\n if (!dirName) return true\n if (dirName.startsWith('.')) return true\n- const skipped = new Set([\n- 'types',\n- 'prompts',\n- 'registry',\n- 'constants',\n- '__tests__',\n- 'factory',\n- 'node_modules',\n- ])\n- return skipped.has(dirName)\n+ return SKIPPED_DIRECTORIES.has(dirName)\n }\n \n-const gatherAgentFiles = (dir: string, results: LocalAgentInfo[]) => {\n+/**\n+ * Recursively gathers agent files from a directory.\n+ * Returns file info with id, displayName, and filePath for each valid agent file.\n+ */\n+const gatherAgentFiles = (dir: string): LocalAgentInfo[] => {\n+ const results: LocalAgentInfo[] = []\n+ gatherAgentFilesRecursive(dir, results)\n+ return results\n+}\n+\n+const gatherAgentFilesRecursive = (dir: string, results: LocalAgentInfo[]): void => {\n const entries = fs.readdirSync(dir, { withFileTypes: true })\n \n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name)\n@@ -42,50 +84,52 @@\n if (entry.isDirectory()) {\n if (shouldSkipDirectory(entry.name)) {\n continue\n }\n-\n- gatherAgentFiles(fullPath, results)\n+ gatherAgentFilesRecursive(fullPath, results)\n continue\n }\n \n- if (!entry.isFile()) {\n+ if (!entry.isFile() || !entry.name.endsWith('.ts')) {\n continue\n }\n \n- if (!entry.name.endsWith('.ts')) {\n- continue\n- }\n-\n let content: string\n try {\n content = fs.readFileSync(fullPath, 'utf8')\n } catch {\n continue\n }\n \n const displayMatch = content.match(DISPLAY_NAME_REGEX)\n- if (!displayMatch) {\n+ const idMatch = content.match(ID_REGEX)\n+\n+ // Must have at least one of displayName or id\n+ if (!displayMatch && !idMatch) {\n continue\n }\n \n- const idMatch = content.match(ID_REGEX)\n+ const displayName = displayMatch?.[1]?.trim() ?? ''\n+ const id = idMatch?.[1]?.trim() ?? displayName\n \n- const displayName = displayMatch[1].trim()\n- const id = idMatch ? idMatch[1].trim() : displayName\n-\n- if (!displayName) {\n+ if (!displayName && !id) {\n continue\n }\n \n results.push({\n id,\n- displayName,\n+ displayName: displayName || id,\n filePath: fullPath,\n })\n }\n }\n \n+// ============================================================================\n+// Directory finding\n+// ============================================================================\n+\n+let cachedAgentsDir: string | null = null\n+\n export const findAgentsDirectory = (): string | null => {\n if (cachedAgentsDir && fs.existsSync(cachedAgentsDir)) {\n return cachedAgentsDir\n }\n@@ -127,36 +171,116 @@\n cachedAgentsDir = null\n return null\n }\n \n+// ============================================================================\n+// Agent loading - LocalAgentInfo (lightweight, for UI/listing)\n+// ============================================================================\n+\n+let cachedAgents: LocalAgentInfo[] | null = null\n+\n export const loadLocalAgents = (): LocalAgentInfo[] => {\n if (cachedAgents) {\n return cachedAgents\n }\n \n+ // Start with bundled agents - these are the default Codebuff agents\n+ // compiled into the CLI binary at build time\n+ const bundledAgentsInfo = getBundledAgentsAsLocalInfo()\n+ const results: LocalAgentInfo[] = [...bundledAgentsInfo]\n+ const bundledIds = new Set(bundledAgentsInfo.map(a => a.id))\n+\n+ // Then load user's local agents from .agents/ directory\n+ // User agents can override bundled agents with the same ID\n const agentsDir = findAgentsDirectory()\n \n- if (!agentsDir) {\n- cachedAgents = []\n- return cachedAgents\n+ if (agentsDir) {\n+ try {\n+ const userAgents = gatherAgentFiles(agentsDir)\n+ \n+ // Merge user agents - they override bundled agents with same ID\n+ for (const userAgent of userAgents) {\n+ if (bundledIds.has(userAgent.id)) {\n+ // Replace bundled agent with user's version\n+ const idx = results.findIndex(a => a.id === userAgent.id)\n+ if (idx !== -1) {\n+ results[idx] = userAgent\n+ }\n+ } else {\n+ results.push(userAgent)\n+ }\n+ }\n+ } catch {\n+ // Ignore errors loading user agents\n+ }\n }\n \n- const results: LocalAgentInfo[] = []\n-\n- try {\n- gatherAgentFiles(agentsDir, results)\n- } catch {\n- cachedAgents = []\n- return cachedAgents\n- }\n-\n cachedAgents = results.sort((a, b) =>\n a.displayName.localeCompare(b.displayName, 'en'),\n )\n \n return cachedAgents\n }\n \n+// ============================================================================\n+// Agent loading - AgentDefinition (full definitions for runtime)\n+// ============================================================================\n+\n+/**\n+ * Load agent definitions from bundled agents and user's .agents directory.\n+ * Bundled agents are compiled into the CLI binary at build time.\n+ * User agents from .agents/ can override bundled agents with the same ID.\n+ * Note: The SDK's processAgentDefinitions will handle converting handleSteps functions to strings\n+ */\n+export const loadAgentDefinitions = (): AgentDefinition[] => {\n+ // Start with bundled agents - these are the default Codebuff agents\n+ const bundledAgents = getBundledAgents()\n+ const definitions: AgentDefinition[] = Object.values(bundledAgents)\n+ const bundledIds = new Set(Object.keys(bundledAgents))\n+\n+ // Then load user's local agents from .agents/ directory\n+ const agentsDir = findAgentsDirectory()\n+ if (!agentsDir) {\n+ return definitions\n+ }\n+\n+ const agentFiles = gatherAgentFiles(agentsDir)\n+\n+ for (const { filePath } of agentFiles) {\n+ try {\n+ // Use require to load the TypeScript file (works with ts-node/bun)\n+ const agentModule = require(filePath)\n+ const agentDef = agentModule.default\n+ if (require.cache[filePath]) {\n+ delete require.cache[filePath]\n+ }\n+\n+ if (!agentDef || !agentDef.id || !agentDef.model) {\n+ continue\n+ }\n+\n+ // User agents override bundled agents with the same ID\n+ if (bundledIds.has(agentDef.id)) {\n+ const idx = definitions.findIndex(d => d.id === agentDef.id)\n+ if (idx !== -1) {\n+ definitions[idx] = agentDef as AgentDefinition\n+ }\n+ } else {\n+ definitions.push(agentDef as AgentDefinition)\n+ }\n+ } catch {\n+ // Skip files that can't be loaded\n+ continue\n+ }\n+ }\n+\n+ return definitions\n+}\n+\n+// ============================================================================\n+// UI/Display utilities\n+// ============================================================================\n+\n export const announceLoadedAgents = (): void => {\n const agents = loadLocalAgents()\n const agentsDir = findAgentsDirectory()\n \n@@ -218,8 +342,12 @@\n \n return { agents, agentsDir }\n }\n \n+// ============================================================================\n+// Testing utilities\n+// ============================================================================\n+\n /**\n * Clear cached agent listings. Intended for test scenarios that need to\n * re-evaluate the filesystem state between cases.\n */\n"
+ "diff": "Index: cli/src/utils/local-agent-registry.ts\n===================================================================\n--- cli/src/utils/local-agent-registry.ts\td60f29e (parent)\n+++ cli/src/utils/local-agent-registry.ts\t81b5edd (commit)\n@@ -4,37 +4,79 @@\n import { pluralize } from '@codebirds/common/util/string'\n \n import { getProjectRoot } from '../project-files'\n \n+import type { AgentDefinition } from '@codebirds/common/templates/initial-agents-dir/types/agent-definition'\n+\n+// ============================================================================\n+// Constants and types\n+// ============================================================================\n+\n+const DISPLAY_NAME_REGEX = /displayName\\s*:\\s*['\"`]([^'\"`]+)['\"`]/i\n+const ID_REGEX = /id\\s*:\\s*['\"`]([^'\"`]+)['\"`]/i\n+const AGENTS_DIR_NAME = '.agents'\n+\n+const SKIPPED_DIRECTORIES = new Set([\n+ 'types',\n+ 'prompts',\n+ 'registry',\n+ 'constants',\n+ '__tests__',\n+ 'factory',\n+ 'node_modules',\n+])\n+\n export interface LocalAgentInfo {\n id: string\n displayName: string\n filePath: string\n }\n \n-const DISPLAY_NAME_REGEX = /displayName\\s*:\\s*['\"`]([^'\"`]+)['\"`]/i\n-const ID_REGEX = /id\\s*:\\s*['\"`]([^'\"`]+)['\"`]/i\n-const AGENTS_DIR_NAME = '.agents'\n+// ============================================================================\n+// Bundled agents loading (generated at build time by prebuild-agents.ts)\n+// ============================================================================\n \n-let cachedAgents: LocalAgentInfo[] | null = null\n-let cachedAgentsDir: string | null = null\n+interface BundledAgentsModule {\n+ bundledAgents: Record\n+ getBundledAgentsAsLocalInfo: () => LocalAgentInfo[]\n+}\n \n+let bundledAgentsModule: BundledAgentsModule | null = null\n+try {\n+ bundledAgentsModule = require('../agents/bundled-agents.generated')\n+} catch {\n+ // File not generated yet - running in development without prebuild\n+}\n+\n+const getBundledAgents = (): Record => {\n+ return bundledAgentsModule?.bundledAgents ?? {}\n+}\n+\n+const getBundledAgentsAsLocalInfo = (): LocalAgentInfo[] => {\n+ return bundledAgentsModule?.getBundledAgentsAsLocalInfo?.() ?? []\n+}\n+\n+// ============================================================================\n+// File system utilities\n+// ============================================================================\n+\n const shouldSkipDirectory = (dirName: string): boolean => {\n if (!dirName) return true\n if (dirName.startsWith('.')) return true\n- const skipped = new Set([\n- 'types',\n- 'prompts',\n- 'registry',\n- 'constants',\n- '__tests__',\n- 'factory',\n- 'node_modules',\n- ])\n- return skipped.has(dirName)\n+ return SKIPPED_DIRECTORIES.has(dirName)\n }\n \n-const gatherAgentFiles = (dir: string, results: LocalAgentInfo[]) => {\n+/**\n+ * Recursively gathers agent files from a directory.\n+ * Returns file info with id, displayName, and filePath for each valid agent file.\n+ */\n+const gatherAgentFiles = (dir: string): LocalAgentInfo[] => {\n+ const results: LocalAgentInfo[] = []\n+ gatherAgentFilesRecursive(dir, results)\n+ return results\n+}\n+\n+const gatherAgentFilesRecursive = (dir: string, results: LocalAgentInfo[]): void => {\n const entries = fs.readdirSync(dir, { withFileTypes: true })\n \n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name)\n@@ -42,50 +84,52 @@\n if (entry.isDirectory()) {\n if (shouldSkipDirectory(entry.name)) {\n continue\n }\n-\n- gatherAgentFiles(fullPath, results)\n+ gatherAgentFilesRecursive(fullPath, results)\n continue\n }\n \n- if (!entry.isFile()) {\n+ if (!entry.isFile() || !entry.name.endsWith('.ts')) {\n continue\n }\n \n- if (!entry.name.endsWith('.ts')) {\n- continue\n- }\n-\n let content: string\n try {\n content = fs.readFileSync(fullPath, 'utf8')\n } catch {\n continue\n }\n \n const displayMatch = content.match(DISPLAY_NAME_REGEX)\n- if (!displayMatch) {\n+ const idMatch = content.match(ID_REGEX)\n+\n+ // Must have at least one of displayName or id\n+ if (!displayMatch && !idMatch) {\n continue\n }\n \n- const idMatch = content.match(ID_REGEX)\n+ const displayName = displayMatch?.[1]?.trim() ?? ''\n+ const id = idMatch?.[1]?.trim() ?? displayName\n \n- const displayName = displayMatch[1].trim()\n- const id = idMatch ? idMatch[1].trim() : displayName\n-\n- if (!displayName) {\n+ if (!displayName && !id) {\n continue\n }\n \n results.push({\n id,\n- displayName,\n+ displayName: displayName || id,\n filePath: fullPath,\n })\n }\n }\n \n+// ============================================================================\n+// Directory finding\n+// ============================================================================\n+\n+let cachedAgentsDir: string | null = null\n+\n export const findAgentsDirectory = (): string | null => {\n if (cachedAgentsDir && fs.existsSync(cachedAgentsDir)) {\n return cachedAgentsDir\n }\n@@ -127,36 +171,116 @@\n cachedAgentsDir = null\n return null\n }\n \n+// ============================================================================\n+// Agent loading - LocalAgentInfo (lightweight, for UI/listing)\n+// ============================================================================\n+\n+let cachedAgents: LocalAgentInfo[] | null = null\n+\n export const loadLocalAgents = (): LocalAgentInfo[] => {\n if (cachedAgents) {\n return cachedAgents\n }\n \n+ // Start with bundled agents - these are the default Codebirds agents\n+ // compiled into the CLI binary at build time\n+ const bundledAgentsInfo = getBundledAgentsAsLocalInfo()\n+ const results: LocalAgentInfo[] = [...bundledAgentsInfo]\n+ const bundledIds = new Set(bundledAgentsInfo.map(a => a.id))\n+\n+ // Then load user's local agents from .agents/ directory\n+ // User agents can override bundled agents with the same ID\n const agentsDir = findAgentsDirectory()\n \n- if (!agentsDir) {\n- cachedAgents = []\n- return cachedAgents\n+ if (agentsDir) {\n+ try {\n+ const userAgents = gatherAgentFiles(agentsDir)\n+ \n+ // Merge user agents - they override bundled agents with same ID\n+ for (const userAgent of userAgents) {\n+ if (bundledIds.has(userAgent.id)) {\n+ // Replace bundled agent with user's version\n+ const idx = results.findIndex(a => a.id === userAgent.id)\n+ if (idx !== -1) {\n+ results[idx] = userAgent\n+ }\n+ } else {\n+ results.push(userAgent)\n+ }\n+ }\n+ } catch {\n+ // Ignore errors loading user agents\n+ }\n }\n \n- const results: LocalAgentInfo[] = []\n-\n- try {\n- gatherAgentFiles(agentsDir, results)\n- } catch {\n- cachedAgents = []\n- return cachedAgents\n- }\n-\n cachedAgents = results.sort((a, b) =>\n a.displayName.localeCompare(b.displayName, 'en'),\n )\n \n return cachedAgents\n }\n \n+// ============================================================================\n+// Agent loading - AgentDefinition (full definitions for runtime)\n+// ============================================================================\n+\n+/**\n+ * Load agent definitions from bundled agents and user's .agents directory.\n+ * Bundled agents are compiled into the CLI binary at build time.\n+ * User agents from .agents/ can override bundled agents with the same ID.\n+ * Note: The SDK's processAgentDefinitions will handle converting handleSteps functions to strings\n+ */\n+export const loadAgentDefinitions = (): AgentDefinition[] => {\n+ // Start with bundled agents - these are the default Codebirds agents\n+ const bundledAgents = getBundledAgents()\n+ const definitions: AgentDefinition[] = Object.values(bundledAgents)\n+ const bundledIds = new Set(Object.keys(bundledAgents))\n+\n+ // Then load user's local agents from .agents/ directory\n+ const agentsDir = findAgentsDirectory()\n+ if (!agentsDir) {\n+ return definitions\n+ }\n+\n+ const agentFiles = gatherAgentFiles(agentsDir)\n+\n+ for (const { filePath } of agentFiles) {\n+ try {\n+ // Use require to load the TypeScript file (works with ts-node/bun)\n+ const agentModule = require(filePath)\n+ const agentDef = agentModule.default\n+ if (require.cache[filePath]) {\n+ delete require.cache[filePath]\n+ }\n+\n+ if (!agentDef || !agentDef.id || !agentDef.model) {\n+ continue\n+ }\n+\n+ // User agents override bundled agents with the same ID\n+ if (bundledIds.has(agentDef.id)) {\n+ const idx = definitions.findIndex(d => d.id === agentDef.id)\n+ if (idx !== -1) {\n+ definitions[idx] = agentDef as AgentDefinition\n+ }\n+ } else {\n+ definitions.push(agentDef as AgentDefinition)\n+ }\n+ } catch {\n+ // Skip files that can't be loaded\n+ continue\n+ }\n+ }\n+\n+ return definitions\n+}\n+\n+// ============================================================================\n+// UI/Display utilities\n+// ============================================================================\n+\n export const announceLoadedAgents = (): void => {\n const agents = loadLocalAgents()\n const agentsDir = findAgentsDirectory()\n \n@@ -218,8 +342,12 @@\n \n return { agents, agentsDir }\n }\n \n+// ============================================================================\n+// Testing utilities\n+// ============================================================================\n+\n /**\n * Clear cached agent listings. Intended for test scenarios that need to\n * re-evaluate the filesystem state between cases.\n */\n"
}
]
},
@@ -93,7 +93,7 @@
"id": "enhance-publish-flow",
"sha": "e889bf8c5d6ee9fa364aeb50dfc5c557e1911e3f",
"parentSha": "99e4f7fb56846df7f7fb0621e7ce31354d67d76a",
- "spec": "Implement the following changes across the CLI, schema, server, and validation:\n\n1) Mark bundled agents and exclude them from publishing\n- cli/scripts/prebuild-agents.ts\n - In the generated getBundledAgentsAsLocalInfo() output, include isBundled: true for each bundled agent entry.\n- cli/src/utils/local-agent-registry.ts\n - Extend the LocalAgentInfo interface with an optional boolean isBundled?: boolean JSDoc-commented as a bundled Codebuff agent flag.\n\n2) Update the publish selection UI and confirmation flow\n- cli/src/components/agent-checklist.tsx\n - Change props to:\n - allAgents: LocalAgentInfo[] (used for dependency calculation)\n - filteredAgents: LocalAgentInfo[] (rendered list after search)\n - Use allAgents when computing localAgentIds, dependencyCounts, and building DepTree nodes.\n - Import pluralize from @codebuff/common/util/string and compute a subagentLabel using pluralize(depCount, 'subagent'). Replace inline label construction with this value.\n - Remove the footer that listed selectedIds.\n- cli/src/components/publish-confirmation.tsx\n - Redesign the component to a vertical, directional layout:\n - Add includeDependents: boolean and onToggleDependents: () => void props, removing width prop handling.\n - Implement computeDependencies(selectedIds, agentDefinitions, localAgentIds) to gather all local dependencies (children spawns), using getSimpleAgentId for string normalization.\n - Implement computeDependents(selectedIds, dependencyIds, agentDefinitions, localAgentIds) to gather all reverse dependents (parents), transitively.\n - Create AgentSection and DirectionLabel subcomponents as per the diff to show sections: optional Parents (toggleable), Selected, and Dependencies, with arrow labels indicating direction.\n - Update getAllPublishAgentIds signature to accept includeDependents?: boolean (default false). Behavior:\n - Never include bundled agents: filter allAgents and selectedAgents to only those with !isBundled.\n - Always include selected agents, then recursively include dependencies (children).\n - If includeDependents is true, add all reverse dependents (parents) transitively from the currently included set.\n - Use getSimpleAgentId consistently for spawnable agents.\n- cli/src/components/selected-chips.tsx (new file)\n - Add a chip list component that displays selected agents and allows removing a selection by clicking the chip. Style and hover states per diff, using Button, BORDER_CHARS, and useTheme.\n\n3) Integrate new UI flow in the publish container\n- cli/src/components/publish-container.tsx\n - Import pluralize and useTerminalLayout; compute isTooSmall if width or height is xs and show a terminal-too-small bordered notice with a CLOSE button that cancels the flow.\n - From usePublishStore, select includeDependents and setIncludeDependents.\n - Load local agents via loadLocalAgents(), then filter out bundled agents before display: loadLocalAgents().filter(a => !a.isBundled).\n - Enhance keyboard handling:\n - In search key interceptor, handle ESC: clear search if non-empty; otherwise exit publish mode and call onExitPublish.\n - Add a global ESC handler for non-selection steps that calls handleCancel.\n - Render SelectedChips above the checklist when there are selections.\n - Pass allAgents and filteredAgents to AgentChecklist (updated props), and pass includeDependents plus a toggle handler to PublishConfirmation.\n - Compute publishAgentIds using getAllPublishAgentIds(selectedAgents, agents, agentDefinitions, includeDependents) and update the Publish button label to \"PUBLISH {pluralize(count, 'AGENT')}\". Use that list for onPublish.\n - Adjust some border colors from theme.primary to theme.info and the header subtitle to reflect selected count when applicable.\n\n4) CLI publish command output and request plumbing\n- cli/src/commands/publish.ts\n - Modify publishAgentTemplates to take allLocalAgentIds: string[] and pass it to apiClient.publish(data, allLocalAgentIds).\n - Before calling publish, compute allLocalAgentIds from loadedDefinitions.map(t => t.id) so the server can validate spawn references against local agents not being published.\n - On success, set agents: result.agents ?? [] in the PublishResult to guard against undefined.\n- cli/src/index.tsx\n - Update the publish command path to print human-friendly success output using picocolors (green/cyan/yellow/red), listing each published agent as \"- DisplayName (publisherId/id@version)\", and set exit code 0 on success, 1 on failure.\n\n5) API client and request schema changes\n- cli/src/utils/codebuff-api.ts\n - Extend CodebuffApiClient.publish to publish(data: Record[], allLocalAgentIds?: string[]): Promise> and include allLocalAgentIds in the POST body to /api/agents/publish.\n- common/src/types/api/agents/publish.ts\n - Add allLocalAgentIds?: string[] to publishAgentsRequestSchema. Update the deprecation note to reflect newer clients use Authorization header and that body authToken is legacy.\n\n6) Server-side publish and validate routes\n- packages/internal/src/templates/agent-validation.ts\n - Update validateAgentsWithSpawnableAgents to accept allLocalAgentIds?: string[] param. Build allKnownAgentIds = unique union of agents-being-validated + allLocalAgentIds, and pass dynamicAgentIds: allKnownAgentIds to validateSpawnableAgents. Keep original return type.\n- web/src/app/api/agents/publish/route.ts\n - Parse allLocalAgentIds from the request using publishAgentsRequestSchema and pass it into validateAgentsWithSpawnableAgents({ agentTemplates: ..., allLocalAgentIds, logger }).\n- web/src/app/api/agents/validate/route.ts\n - Accept an optional allLocalAgentIds in the request body and pass it into validateAgentsWithSpawnableAgents({ agentTemplates: ..., allLocalAgentIds, logger }).\n\n7) Input mode cleanup\n- cli/src/utils/input-modes.ts\n - Remove 'publish' from the InputMode union and remove its configuration from INPUT_MODE_CONFIGS. No other mode behavior should change.\n\n8) Stream event handler cleanup\n- cli/src/utils/sdk-event-handlers.ts\n - In handleTextEvent(), remove the debug log that triggers on destination.type === 'agent' warning about potential duplication. Keep accumulation and block updates intact.\n\n9) Unit tests for selection logic\n- cli/src/__tests__/unit/publish-confirmation.test.ts (new)\n - Add tests to verify:\n - Bundled agents are ignored even if selected.\n - Bundled dependencies discovered via spawns are not included.\n - Dependents (parents) are only added when includeDependents is true.\n - Transitive dependents are included when includeDependents is true.\n - Use a LocalAgentInfo factory allowing isBundled overrides to simulate bundled vs non-bundled behavior.\n\nBehavioral acceptance criteria:\n- Bundled agents never appear in the publishable set, even when selected, and are excluded from dependency resolution.\n- Confirmation UI displays:\n - Optional Parents section with a toggle to include/remove dependent agents (parents), showing reverse direction arrow.\n - Selected agents section.\n - Dependencies (children spawns) section, with direction arrows between sections.\n- Publish button label shows the total count to be published and updates when the dependents toggle changes.\n- ESC behavior: In selection step, ESC clears search first; if search is empty, exits publish mode. In other steps, ESC exits publish mode.\n- Small terminals show a bordered warning and a CLOSE button; otherwise the full UI renders.\n- CLI publish command prints a list of published agents on success and exits with code 0; on failure, prints error details/hints and exits with code 1.\n- Server-side validation uses allLocalAgentIds to allow local-but-unpublished references in spawnableAgents without raising validation errors.\n",
+ "spec": "Implement the following changes across the CLI, schema, server, and validation:\n\n1) Mark bundled agents and exclude them from publishing\n- cli/scripts/prebuild-agents.ts\n - In the generated getBundledAgentsAsLocalInfo() output, include isBundled: true for each bundled agent entry.\n- cli/src/utils/local-agent-registry.ts\n - Extend the LocalAgentInfo interface with an optional boolean isBundled?: boolean JSDoc-commented as a bundled Codebirds agent flag.\n\n2) Update the publish selection UI and confirmation flow\n- cli/src/components/agent-checklist.tsx\n - Change props to:\n - allAgents: LocalAgentInfo[] (used for dependency calculation)\n - filteredAgents: LocalAgentInfo[] (rendered list after search)\n - Use allAgents when computing localAgentIds, dependencyCounts, and building DepTree nodes.\n - Import pluralize from @codebirds/common/util/string and compute a subagentLabel using pluralize(depCount, 'subagent'). Replace inline label construction with this value.\n - Remove the footer that listed selectedIds.\n- cli/src/components/publish-confirmation.tsx\n - Redesign the component to a vertical, directional layout:\n - Add includeDependents: boolean and onToggleDependents: () => void props, removing width prop handling.\n - Implement computeDependencies(selectedIds, agentDefinitions, localAgentIds) to gather all local dependencies (children spawns), using getSimpleAgentId for string normalization.\n - Implement computeDependents(selectedIds, dependencyIds, agentDefinitions, localAgentIds) to gather all reverse dependents (parents), transitively.\n - Create AgentSection and DirectionLabel subcomponents as per the diff to show sections: optional Parents (toggleable), Selected, and Dependencies, with arrow labels indicating direction.\n - Update getAllPublishAgentIds signature to accept includeDependents?: boolean (default false). Behavior:\n - Never include bundled agents: filter allAgents and selectedAgents to only those with !isBundled.\n - Always include selected agents, then recursively include dependencies (children).\n - If includeDependents is true, add all reverse dependents (parents) transitively from the currently included set.\n - Use getSimpleAgentId consistently for spawnable agents.\n- cli/src/components/selected-chips.tsx (new file)\n - Add a chip list component that displays selected agents and allows removing a selection by clicking the chip. Style and hover states per diff, using Button, BORDER_CHARS, and useTheme.\n\n3) Integrate new UI flow in the publish container\n- cli/src/components/publish-container.tsx\n - Import pluralize and useTerminalLayout; compute isTooSmall if width or height is xs and show a terminal-too-small bordered notice with a CLOSE button that cancels the flow.\n - From usePublishStore, select includeDependents and setIncludeDependents.\n - Load local agents via loadLocalAgents(), then filter out bundled agents before display: loadLocalAgents().filter(a => !a.isBundled).\n - Enhance keyboard handling:\n - In search key interceptor, handle ESC: clear search if non-empty; otherwise exit publish mode and call onExitPublish.\n - Add a global ESC handler for non-selection steps that calls handleCancel.\n - Render SelectedChips above the checklist when there are selections.\n - Pass allAgents and filteredAgents to AgentChecklist (updated props), and pass includeDependents plus a toggle handler to PublishConfirmation.\n - Compute publishAgentIds using getAllPublishAgentIds(selectedAgents, agents, agentDefinitions, includeDependents) and update the Publish button label to \"PUBLISH {pluralize(count, 'AGENT')}\". Use that list for onPublish.\n - Adjust some border colors from theme.primary to theme.info and the header subtitle to reflect selected count when applicable.\n\n4) CLI publish command output and request plumbing\n- cli/src/commands/publish.ts\n - Modify publishAgentTemplates to take allLocalAgentIds: string[] and pass it to apiClient.publish(data, allLocalAgentIds).\n - Before calling publish, compute allLocalAgentIds from loadedDefinitions.map(t => t.id) so the server can validate spawn references against local agents not being published.\n - On success, set agents: result.agents ?? [] in the PublishResult to guard against undefined.\n- cli/src/index.tsx\n - Update the publish command path to print human-friendly success output using picocolors (green/cyan/yellow/red), listing each published agent as \"- DisplayName (publisherId/id@version)\", and set exit code 0 on success, 1 on failure.\n\n5) API client and request schema changes\n- cli/src/utils/codebirds-api.ts\n - Extend CodebirdsApiClient.publish to publish(data: Record[], allLocalAgentIds?: string[]): Promise> and include allLocalAgentIds in the POST body to /api/agents/publish.\n- common/src/types/api/agents/publish.ts\n - Add allLocalAgentIds?: string[] to publishAgentsRequestSchema. Update the deprecation note to reflect newer clients use Authorization header and that body authToken is legacy.\n\n6) Server-side publish and validate routes\n- packages/internal/src/templates/agent-validation.ts\n - Update validateAgentsWithSpawnableAgents to accept allLocalAgentIds?: string[] param. Build allKnownAgentIds = unique union of agents-being-validated + allLocalAgentIds, and pass dynamicAgentIds: allKnownAgentIds to validateSpawnableAgents. Keep original return type.\n- web/src/app/api/agents/publish/route.ts\n - Parse allLocalAgentIds from the request using publishAgentsRequestSchema and pass it into validateAgentsWithSpawnableAgents({ agentTemplates: ..., allLocalAgentIds, logger }).\n- web/src/app/api/agents/validate/route.ts\n - Accept an optional allLocalAgentIds in the request body and pass it into validateAgentsWithSpawnableAgents({ agentTemplates: ..., allLocalAgentIds, logger }).\n\n7) Input mode cleanup\n- cli/src/utils/input-modes.ts\n - Remove 'publish' from the InputMode union and remove its configuration from INPUT_MODE_CONFIGS. No other mode behavior should change.\n\n8) Stream event handler cleanup\n- cli/src/utils/sdk-event-handlers.ts\n - In handleTextEvent(), remove the debug log that triggers on destination.type === 'agent' warning about potential duplication. Keep accumulation and block updates intact.\n\n9) Unit tests for selection logic\n- cli/src/__tests__/unit/publish-confirmation.test.ts (new)\n - Add tests to verify:\n - Bundled agents are ignored even if selected.\n - Bundled dependencies discovered via spawns are not included.\n - Dependents (parents) are only added when includeDependents is true.\n - Transitive dependents are included when includeDependents is true.\n - Use a LocalAgentInfo factory allowing isBundled overrides to simulate bundled vs non-bundled behavior.\n\nBehavioral acceptance criteria:\n- Bundled agents never appear in the publishable set, even when selected, and are excluded from dependency resolution.\n- Confirmation UI displays:\n - Optional Parents section with a toggle to include/remove dependent agents (parents), showing reverse direction arrow.\n - Selected agents section.\n - Dependencies (children spawns) section, with direction arrows between sections.\n- Publish button label shows the total count to be published and updates when the dependents toggle changes.\n- ESC behavior: In selection step, ESC clears search first; if search is empty, exits publish mode. In other steps, ESC exits publish mode.\n- Small terminals show a bordered warning and a CLOSE button; otherwise the full UI renders.\n- CLI publish command prints a list of published agents on success and exits with code 0; on failure, prints error details/hints and exits with code 1.\n- Server-side validation uses allLocalAgentIds to allow local-but-unpublished references in spawnableAgents without raising validation errors.\n",
"prompt": "Enhance the CLI publish experience and server validation:\n\n- Redesign the publish confirmation UI to visualize dependency direction. Show selected agents, their spawned dependencies, and optionally include reverse dependents (parents) via a toggle. Use arrows to indicate direction and a compact, scrollable layout.\n- Prevent publishing of bundled agents. Mark bundled agents at build time and omit them from the publish list and dependency calculations. If selected, they should be ignored.\n- Add a chips UI for currently selected agents and improve keyboard handling (ESC clears search then exits; global ESC cancels on other steps). Show a small-terminal placeholder message when space is insufficient.\n- Update the CLI publish command to print a clear success/failure summary and set proper exit codes.\n- Modify the API client to send all local agent IDs in the publish request. Update request schema, server publish route, and validation code so validation can allow references to local agents that are not being published in the same request.\n- Remove the dedicated publish input mode and clean up a noisy debug log in the stream event handler.\n- Add unit tests verifying bundled agents are never published, dependencies are included, and dependents are only included when toggled (including transitive parents).\n\nEnsure the changes integrate across CLI components, API client, shared schema, server routes, and internal validation, matching the user experience and constraints described.",
"supplementalFiles": [
"cli/src/hooks/use-terminal-layout.ts",
@@ -122,7 +122,7 @@
{
"path": "cli/src/components/agent-checklist.tsx",
"status": "modified",
- "diff": "Index: cli/src/components/agent-checklist.tsx\n===================================================================\n--- cli/src/components/agent-checklist.tsx\t99e4f7f (parent)\n+++ cli/src/components/agent-checklist.tsx\te889bf8 (commit)\n@@ -1,7 +1,9 @@\n import { TextAttributes } from '@opentui/core'\n import React, { useMemo, useRef, useEffect, useState } from 'react'\n \n+import { pluralize } from '@codebuff/common/util/string'\n+\n import { Button } from './button'\n import { useTheme } from '../hooks/use-theme'\n import { getSimpleAgentId } from '../utils/agent-id-utils'\n \n@@ -107,9 +109,12 @@\n )\n }\n \n interface AgentChecklistProps {\n- agents: LocalAgentInfo[]\n+ /** All agents (used for dependency tree calculations) */\n+ allAgents: LocalAgentInfo[]\n+ /** Agents filtered by search query (displayed in the list) */\n+ filteredAgents: LocalAgentInfo[]\n selectedIds: Set\n searchQuery: string\n focusedIndex: number\n onToggleAgent: (agentId: string) => void\n@@ -118,9 +123,10 @@\n maxHeight?: number\n }\n \n export const AgentChecklist: React.FC = ({\n- agents,\n+ allAgents,\n+ filteredAgents,\n selectedIds,\n searchQuery,\n focusedIndex,\n onToggleAgent,\n@@ -134,19 +140,19 @@\n const [expandedAgentIds, setExpandedAgentIds] = useState>(new Set())\n const [hoveredSubagentLink, setHoveredSubagentLink] = useState(null)\n \n // Precompute local agent IDs for dependency calculations\n- const localAgentIds = useMemo(() => new Set(agents.map((a) => a.id)), [agents])\n+ const localAgentIds = useMemo(() => new Set(allAgents.map((a) => a.id)), [allAgents])\n \n // Calculate dependency count for each agent\n const dependencyCounts = useMemo(() => {\n const counts = new Map()\n- for (const agent of agents) {\n+ for (const agent of allAgents) {\n const count = countDependencies(agent.id, agentDefinitions, localAgentIds, new Set())\n counts.set(agent.id, count)\n }\n return counts\n- }, [agents, agentDefinitions, localAgentIds])\n+ }, [allAgents, agentDefinitions, localAgentIds])\n \n // Toggle expansion of an agent's dependencies\n const toggleExpanded = (agentId: string) => {\n setExpandedAgentIds((prev) => {\n@@ -159,21 +165,8 @@\n return next\n })\n }\n \n- // Filter agents based on search query (instant filter)\n- const filteredAgents = useMemo(() => {\n- if (!searchQuery.trim()) {\n- return agents\n- }\n- const query = searchQuery.toLowerCase()\n- return agents.filter(\n- (agent) =>\n- agent.displayName.toLowerCase().includes(query) ||\n- agent.id.toLowerCase().includes(query),\n- )\n- }, [agents, searchQuery])\n-\n // Scroll focused item into view when focus changes via keyboard\n useEffect(() => {\n const scrollbox = scrollRef.current\n if (!scrollbox || filteredAgents.length === 0) return\n@@ -243,8 +236,9 @@\n const isHighlighted = isFocused || isHovered\n const depCount = dependencyCounts.get(agent.id) ?? 0\n const isExpanded = expandedAgentIds.has(agent.id)\n const isSubagentLinkHovered = hoveredSubagentLink === agent.id\n+ const subagentLabel = `(${isExpanded ? '-' : '+'} ${pluralize(depCount, 'subagent')})`\n \n const symbol = isSelected\n ? SYMBOLS.CHECKBOX_CHECKED\n : SYMBOLS.CHECKBOX_UNCHECKED\n@@ -332,34 +326,25 @@\n ? TextAttributes.UNDERLINE\n : undefined,\n }}\n >\n- {isExpanded ? `(- ${depCount} subagent${depCount === 1 ? '' : 's'})` : `(+ ${depCount} subagent${depCount === 1 ? '' : 's'})`}\n+ {subagentLabel}\n \n \n )}\n \n \n {/* Expanded dependency tree */}\n {isExpanded && depCount > 0 && (\n \n )}\n \n )\n })}\n \n-\n- {/* Selection count */}\n- \n- \n- {selectedIds.size === 0\n- ? 'No agents selected'\n- : `Selected: ${Array.from(selectedIds).join(', ')}`}\n- \n- \n \n )\n }\n"
+ "diff": "Index: cli/src/components/agent-checklist.tsx\n===================================================================\n--- cli/src/components/agent-checklist.tsx\t99e4f7f (parent)\n+++ cli/src/components/agent-checklist.tsx\te889bf8 (commit)\n@@ -1,7 +1,9 @@\n import { TextAttributes } from '@opentui/core'\n import React, { useMemo, useRef, useEffect, useState } from 'react'\n \n+import { pluralize } from '@codebirds/common/util/string'\n+\n import { Button } from './button'\n import { useTheme } from '../hooks/use-theme'\n import { getSimpleAgentId } from '../utils/agent-id-utils'\n \n@@ -107,9 +109,12 @@\n )\n }\n \n interface AgentChecklistProps {\n- agents: LocalAgentInfo[]\n+ /** All agents (used for dependency tree calculations) */\n+ allAgents: LocalAgentInfo[]\n+ /** Agents filtered by search query (displayed in the list) */\n+ filteredAgents: LocalAgentInfo[]\n selectedIds: Set\n searchQuery: string\n focusedIndex: number\n onToggleAgent: (agentId: string) => void\n@@ -118,9 +123,10 @@\n maxHeight?: number\n }\n \n export const AgentChecklist: React.FC = ({\n- agents,\n+ allAgents,\n+ filteredAgents,\n selectedIds,\n searchQuery,\n focusedIndex,\n onToggleAgent,\n@@ -134,19 +140,19 @@\n const [expandedAgentIds, setExpandedAgentIds] = useState>(new Set())\n const [hoveredSubagentLink, setHoveredSubagentLink] = useState(null)\n \n // Precompute local agent IDs for dependency calculations\n- const localAgentIds = useMemo(() => new Set(agents.map((a) => a.id)), [agents])\n+ const localAgentIds = useMemo(() => new Set(allAgents.map((a) => a.id)), [allAgents])\n \n // Calculate dependency count for each agent\n const dependencyCounts = useMemo(() => {\n const counts = new Map()\n- for (const agent of agents) {\n+ for (const agent of allAgents) {\n const count = countDependencies(agent.id, agentDefinitions, localAgentIds, new Set())\n counts.set(agent.id, count)\n }\n return counts\n- }, [agents, agentDefinitions, localAgentIds])\n+ }, [allAgents, agentDefinitions, localAgentIds])\n \n // Toggle expansion of an agent's dependencies\n const toggleExpanded = (agentId: string) => {\n setExpandedAgentIds((prev) => {\n@@ -159,21 +165,8 @@\n return next\n })\n }\n \n- // Filter agents based on search query (instant filter)\n- const filteredAgents = useMemo(() => {\n- if (!searchQuery.trim()) {\n- return agents\n- }\n- const query = searchQuery.toLowerCase()\n- return agents.filter(\n- (agent) =>\n- agent.displayName.toLowerCase().includes(query) ||\n- agent.id.toLowerCase().includes(query),\n- )\n- }, [agents, searchQuery])\n-\n // Scroll focused item into view when focus changes via keyboard\n useEffect(() => {\n const scrollbox = scrollRef.current\n if (!scrollbox || filteredAgents.length === 0) return\n@@ -243,8 +236,9 @@\n const isHighlighted = isFocused || isHovered\n const depCount = dependencyCounts.get(agent.id) ?? 0\n const isExpanded = expandedAgentIds.has(agent.id)\n const isSubagentLinkHovered = hoveredSubagentLink === agent.id\n+ const subagentLabel = `(${isExpanded ? '-' : '+'} ${pluralize(depCount, 'subagent')})`\n \n const symbol = isSelected\n ? SYMBOLS.CHECKBOX_CHECKED\n : SYMBOLS.CHECKBOX_UNCHECKED\n@@ -332,34 +326,25 @@\n ? TextAttributes.UNDERLINE\n : undefined,\n }}\n >\n- {isExpanded ? `(- ${depCount} subagent${depCount === 1 ? '' : 's'})` : `(+ ${depCount} subagent${depCount === 1 ? '' : 's'})`}\n+ {subagentLabel}\n \n \n )}\n \n \n {/* Expanded dependency tree */}\n {isExpanded && depCount > 0 && (\n \n )}\n \n )\n })}\n \n-\n- {/* Selection count */}\n- \n- \n- {selectedIds.size === 0\n- ? 'No agents selected'\n- : `Selected: ${Array.from(selectedIds).join(', ')}`}\n- \n- \n \n )\n }\n"
},
{
"path": "cli/src/components/publish-confirmation.tsx",
@@ -132,7 +132,7 @@
{
"path": "cli/src/components/publish-container.tsx",
"status": "modified",
- "diff": "Index: cli/src/components/publish-container.tsx\n===================================================================\n--- cli/src/components/publish-container.tsx\t99e4f7f (parent)\n+++ cli/src/components/publish-container.tsx\te889bf8 (commit)\n@@ -1,13 +1,17 @@\n import { TextAttributes } from '@opentui/core'\n import React, { useCallback, useEffect, useMemo, useState } from 'react'\n import { useShallow } from 'zustand/react/shallow'\n \n+import { pluralize } from '@codebuff/common/util/string'\n+\n import { AgentChecklist } from './agent-checklist'\n import { Button } from './button'\n import { MultilineInput, type MultilineInputHandle } from './multiline-input'\n import { PublishConfirmation, getAllPublishAgentIds } from './publish-confirmation'\n+import { SelectedChips } from './selected-chips'\n import { Separator } from './separator'\n+import { useTerminalLayout } from '../hooks/use-terminal-layout'\n import { useTheme } from '../hooks/use-theme'\n import { useChatStore } from '../state/chat-store'\n import { usePublishStore } from '../state/publish-store'\n import { BORDER_CHARS } from '../utils/ui-constants'\n@@ -28,8 +32,10 @@\n onPublish,\n width,\n }) => {\n const theme = useTheme()\n+ const { width: widthLayout, height: heightLayout } = useTerminalLayout()\n+ const isTooSmall = widthLayout.atMost('xs') || heightLayout.atMost('xs')\n const [closeButtonHovered, setCloseButtonHovered] = useState(false)\n const [nextButtonHovered, setNextButtonHovered] = useState(false)\n const [backButtonHovered, setBackButtonHovered] = useState(false)\n const [publishButtonHovered, setPublishButtonHovered] = useState(false)\n@@ -42,14 +48,16 @@\n focusedIndex,\n isPublishing,\n successResult,\n errorResult,\n+ includeDependents,\n toggleAgentSelection,\n setSearchQuery,\n goToConfirmation,\n goBackToSelection,\n setFocusedIndex,\n closePublish,\n+ setIncludeDependents,\n } = usePublishStore(\n useShallow((state) => ({\n publishMode: state.publishMode,\n selectedAgentIds: state.selectedAgentIds,\n@@ -58,21 +66,23 @@\n focusedIndex: state.focusedIndex,\n isPublishing: state.isPublishing,\n successResult: state.successResult,\n errorResult: state.errorResult,\n+ includeDependents: state.includeDependents,\n toggleAgentSelection: state.toggleAgentSelection,\n setSearchQuery: state.setSearchQuery,\n goToConfirmation: state.goToConfirmation,\n goBackToSelection: state.goBackToSelection,\n setFocusedIndex: state.setFocusedIndex,\n closePublish: state.closePublish,\n+ setIncludeDependents: state.setIncludeDependents,\n })),\n )\n \n const inputFocused = useChatStore((state) => state.inputFocused)\n \n- // Load agents data\n- const agents = useMemo(() => loadLocalAgents(), [])\n+ // Load agents data - filter out bundled agents (they shouldn't be publishable by users)\n+ const agents = useMemo(() => loadLocalAgents().filter(a => !a.isBundled), [])\n const agentDefinitions = useMemo(() => {\n const defs = loadAgentDefinitions()\n const map = new Map()\n for (const def of defs) {\n@@ -101,8 +111,18 @@\n \n // Handle keyboard navigation in checklist\n const handleSearchKeyIntercept = useCallback(\n (key: { name?: string; shift?: boolean }) => {\n+ if (key.name === 'escape') {\n+ // Escape: clear input if there is any, otherwise exit publish mode\n+ if (searchQuery.length > 0) {\n+ setSearchQuery('')\n+ } else {\n+ closePublish()\n+ onExitPublish?.()\n+ }\n+ return true\n+ }\n if (key.name === 'up') {\n setFocusedIndex(Math.max(0, focusedIndex - 1))\n return true\n }\n@@ -130,11 +150,15 @@\n [\n focusedIndex,\n filteredAgents,\n canProceed,\n+ searchQuery,\n setFocusedIndex,\n toggleAgentSelection,\n goToConfirmation,\n+ setSearchQuery,\n+ closePublish,\n+ onExitPublish,\n ],\n )\n \n const handleCancel = useCallback(() => {\n@@ -151,30 +175,96 @@\n const handleBack = useCallback(() => {\n goBackToSelection()\n }, [goBackToSelection])\n \n+ // Compute the total count of agents to publish (for button label)\n+ const publishAgentIds = useMemo(\n+ () => getAllPublishAgentIds(selectedAgents, agents, agentDefinitions, includeDependents),\n+ [selectedAgents, agents, agentDefinitions, includeDependents]\n+ )\n+\n const handlePublish = useCallback(async () => {\n- const allIds = getAllPublishAgentIds(selectedAgents, agents, agentDefinitions)\n- await onPublish(allIds)\n- }, [selectedAgents, agents, agentDefinitions, onPublish])\n+ await onPublish(publishAgentIds)\n+ }, [publishAgentIds, onPublish])\n \n useEffect(() => {\n if (publishMode && inputRef.current && currentStep === 'selection') {\n inputRef.current.focus()\n }\n }, [publishMode, inputRef, currentStep])\n \n+ // Handle escape key on non-selection screens\n+ useEffect(() => {\n+ if (!publishMode || currentStep === 'selection') return\n+\n+ // Use process.stdin for terminal key handling\n+ if (typeof process !== 'undefined' && process.stdin) {\n+ const stdin = process.stdin\n+ const onData = (data: Buffer) => {\n+ // ESC key is 0x1b\n+ if (data[0] === 0x1b && data.length === 1) {\n+ handleCancel()\n+ }\n+ }\n+ stdin.on('data', onData)\n+ return () => {\n+ stdin.off('data', onData)\n+ }\n+ }\n+ return undefined\n+ }, [publishMode, currentStep, handleCancel])\n+\n if (!publishMode) {\n return null\n }\n \n+ // Terminal too small - show placeholder\n+ if (isTooSmall) {\n+ return (\n+ \n+ \n+ Terminal too small\n+ \n+ \n+ Please resize your terminal to use the publish menu.\n+ \n+ \n+ CLOSE \n+ \n+ \n+ )\n+ }\n+\n // Empty state - no agents found\n if (agents.length === 0) {\n return (\n \n \n \n- {currentStep === 'selection' && 'Select agents to publish'}\n+ {currentStep === 'selection' && (selectedAgents.length > 0\n+ ? `Selected ${pluralize(selectedAgents.length, 'agent')} to publish`\n+ : 'Select agents to publish')}\n {currentStep === 'confirmation' && 'Confirm publish'}\n {currentStep === 'success' && 'Publish complete'}\n {currentStep === 'error' && 'Publish failed'}\n \n@@ -263,9 +355,9 @@\n \n \n setSearchQuery(text)}\n+ onChange={({ text }) => setSearchQuery(text)}\n onSubmit={handleNext}\n onPaste={() => {}}\n onKeyIntercept={handleSearchKeyIntercept}\n placeholder=\"Type to search agents...\"\n@@ -277,11 +369,26 @@\n />\n \n \n \n+ {/* Selected chips */}\n+ {selectedAgents.length > 0 && (\n+ <>\n+ ({\n+ id: a.id,\n+ displayName: a.displayName,\n+ }))}\n+ onRemove={toggleAgentSelection}\n+ />\n+ \n+ >\n+ )}\n+\n {/* Agent checklist */}\n setIncludeDependents(!includeDependents)}\n />\n \n \n {/* Footer with Back and Publish buttons */}\n@@ -418,9 +526,9 @@\n ? theme.success\n : theme.foreground\n }\n >\n- {isPublishing ? 'PUBLISHING...' : 'PUBLISH'}\n+ {isPublishing ? 'PUBLISHING...' : `PUBLISH ${pluralize(publishAgentIds.length, 'AGENT')}`}\n \n \n \n \n"
+ "diff": "Index: cli/src/components/publish-container.tsx\n===================================================================\n--- cli/src/components/publish-container.tsx\t99e4f7f (parent)\n+++ cli/src/components/publish-container.tsx\te889bf8 (commit)\n@@ -1,13 +1,17 @@\n import { TextAttributes } from '@opentui/core'\n import React, { useCallback, useEffect, useMemo, useState } from 'react'\n import { useShallow } from 'zustand/react/shallow'\n \n+import { pluralize } from '@codebirds/common/util/string'\n+\n import { AgentChecklist } from './agent-checklist'\n import { Button } from './button'\n import { MultilineInput, type MultilineInputHandle } from './multiline-input'\n import { PublishConfirmation, getAllPublishAgentIds } from './publish-confirmation'\n+import { SelectedChips } from './selected-chips'\n import { Separator } from './separator'\n+import { useTerminalLayout } from '../hooks/use-terminal-layout'\n import { useTheme } from '../hooks/use-theme'\n import { useChatStore } from '../state/chat-store'\n import { usePublishStore } from '../state/publish-store'\n import { BORDER_CHARS } from '../utils/ui-constants'\n@@ -28,8 +32,10 @@\n onPublish,\n width,\n }) => {\n const theme = useTheme()\n+ const { width: widthLayout, height: heightLayout } = useTerminalLayout()\n+ const isTooSmall = widthLayout.atMost('xs') || heightLayout.atMost('xs')\n const [closeButtonHovered, setCloseButtonHovered] = useState(false)\n const [nextButtonHovered, setNextButtonHovered] = useState(false)\n const [backButtonHovered, setBackButtonHovered] = useState(false)\n const [publishButtonHovered, setPublishButtonHovered] = useState(false)\n@@ -42,14 +48,16 @@\n focusedIndex,\n isPublishing,\n successResult,\n errorResult,\n+ includeDependents,\n toggleAgentSelection,\n setSearchQuery,\n goToConfirmation,\n goBackToSelection,\n setFocusedIndex,\n closePublish,\n+ setIncludeDependents,\n } = usePublishStore(\n useShallow((state) => ({\n publishMode: state.publishMode,\n selectedAgentIds: state.selectedAgentIds,\n@@ -58,21 +66,23 @@\n focusedIndex: state.focusedIndex,\n isPublishing: state.isPublishing,\n successResult: state.successResult,\n errorResult: state.errorResult,\n+ includeDependents: state.includeDependents,\n toggleAgentSelection: state.toggleAgentSelection,\n setSearchQuery: state.setSearchQuery,\n goToConfirmation: state.goToConfirmation,\n goBackToSelection: state.goBackToSelection,\n setFocusedIndex: state.setFocusedIndex,\n closePublish: state.closePublish,\n+ setIncludeDependents: state.setIncludeDependents,\n })),\n )\n \n const inputFocused = useChatStore((state) => state.inputFocused)\n \n- // Load agents data\n- const agents = useMemo(() => loadLocalAgents(), [])\n+ // Load agents data - filter out bundled agents (they shouldn't be publishable by users)\n+ const agents = useMemo(() => loadLocalAgents().filter(a => !a.isBundled), [])\n const agentDefinitions = useMemo(() => {\n const defs = loadAgentDefinitions()\n const map = new Map()\n for (const def of defs) {\n@@ -101,8 +111,18 @@\n \n // Handle keyboard navigation in checklist\n const handleSearchKeyIntercept = useCallback(\n (key: { name?: string; shift?: boolean }) => {\n+ if (key.name === 'escape') {\n+ // Escape: clear input if there is any, otherwise exit publish mode\n+ if (searchQuery.length > 0) {\n+ setSearchQuery('')\n+ } else {\n+ closePublish()\n+ onExitPublish?.()\n+ }\n+ return true\n+ }\n if (key.name === 'up') {\n setFocusedIndex(Math.max(0, focusedIndex - 1))\n return true\n }\n@@ -130,11 +150,15 @@\n [\n focusedIndex,\n filteredAgents,\n canProceed,\n+ searchQuery,\n setFocusedIndex,\n toggleAgentSelection,\n goToConfirmation,\n+ setSearchQuery,\n+ closePublish,\n+ onExitPublish,\n ],\n )\n \n const handleCancel = useCallback(() => {\n@@ -151,30 +175,96 @@\n const handleBack = useCallback(() => {\n goBackToSelection()\n }, [goBackToSelection])\n \n+ // Compute the total count of agents to publish (for button label)\n+ const publishAgentIds = useMemo(\n+ () => getAllPublishAgentIds(selectedAgents, agents, agentDefinitions, includeDependents),\n+ [selectedAgents, agents, agentDefinitions, includeDependents]\n+ )\n+\n const handlePublish = useCallback(async () => {\n- const allIds = getAllPublishAgentIds(selectedAgents, agents, agentDefinitions)\n- await onPublish(allIds)\n- }, [selectedAgents, agents, agentDefinitions, onPublish])\n+ await onPublish(publishAgentIds)\n+ }, [publishAgentIds, onPublish])\n \n useEffect(() => {\n if (publishMode && inputRef.current && currentStep === 'selection') {\n inputRef.current.focus()\n }\n }, [publishMode, inputRef, currentStep])\n \n+ // Handle escape key on non-selection screens\n+ useEffect(() => {\n+ if (!publishMode || currentStep === 'selection') return\n+\n+ // Use process.stdin for terminal key handling\n+ if (typeof process !== 'undefined' && process.stdin) {\n+ const stdin = process.stdin\n+ const onData = (data: Buffer) => {\n+ // ESC key is 0x1b\n+ if (data[0] === 0x1b && data.length === 1) {\n+ handleCancel()\n+ }\n+ }\n+ stdin.on('data', onData)\n+ return () => {\n+ stdin.off('data', onData)\n+ }\n+ }\n+ return undefined\n+ }, [publishMode, currentStep, handleCancel])\n+\n if (!publishMode) {\n return null\n }\n \n+ // Terminal too small - show placeholder\n+ if (isTooSmall) {\n+ return (\n+ \n+ \n+ Terminal too small\n+ \n+ \n+ Please resize your terminal to use the publish menu.\n+ \n+ \n+ CLOSE \n+ \n+ \n+ )\n+ }\n+\n // Empty state - no agents found\n if (agents.length === 0) {\n return (\n \n \n \n- {currentStep === 'selection' && 'Select agents to publish'}\n+ {currentStep === 'selection' && (selectedAgents.length > 0\n+ ? `Selected ${pluralize(selectedAgents.length, 'agent')} to publish`\n+ : 'Select agents to publish')}\n {currentStep === 'confirmation' && 'Confirm publish'}\n {currentStep === 'success' && 'Publish complete'}\n {currentStep === 'error' && 'Publish failed'}\n \n@@ -263,9 +355,9 @@\n \n \n setSearchQuery(text)}\n+ onChange={({ text }) => setSearchQuery(text)}\n onSubmit={handleNext}\n onPaste={() => {}}\n onKeyIntercept={handleSearchKeyIntercept}\n placeholder=\"Type to search agents...\"\n@@ -277,11 +369,26 @@\n />\n \n \n \n+ {/* Selected chips */}\n+ {selectedAgents.length > 0 && (\n+ <>\n+ ({\n+ id: a.id,\n+ displayName: a.displayName,\n+ }))}\n+ onRemove={toggleAgentSelection}\n+ />\n+ \n+ >\n+ )}\n+\n {/* Agent checklist */}\n setIncludeDependents(!includeDependents)}\n />\n \n \n {/* Footer with Back and Publish buttons */}\n@@ -418,9 +526,9 @@\n ? theme.success\n : theme.foreground\n }\n >\n- {isPublishing ? 'PUBLISHING...' : 'PUBLISH'}\n+ {isPublishing ? 'PUBLISHING...' : `PUBLISH ${pluralize(publishAgentIds.length, 'AGENT')}`}\n \n \n \n \n"
},
{
"path": "cli/src/components/selected-chips.tsx",
@@ -150,9 +150,9 @@
"diff": "Index: cli/src/state/publish-store.ts\n===================================================================\n--- cli/src/state/publish-store.ts\t99e4f7f (parent)\n+++ cli/src/state/publish-store.ts\te889bf8 (commit)\n@@ -26,8 +26,10 @@\n focusedIndex: number\n isPublishing: boolean\n successResult: PublishSuccessResult | null\n errorResult: PublishErrorResult | null\n+ /** Whether to include agents that spawn the selected agents (reverse dependencies) */\n+ includeDependents: boolean\n }\n \n interface PublishActions {\n openPublishMode: () => void\n@@ -40,47 +42,35 @@\n preSelectAgents: (agentIds: string[]) => void\n setIsPublishing: (publishing: boolean) => void\n setSuccessResult: (result: PublishSuccessResult) => void\n setErrorResult: (result: PublishErrorResult) => void\n+ setIncludeDependents: (include: boolean) => void\n reset: () => void\n }\n \n type PublishStore = PublishState & PublishActions\n \n-const initialState: PublishState = {\n- publishMode: false,\n+const createInitialState = (publishMode = false): PublishState => ({\n+ publishMode,\n selectedAgentIds: new Set(),\n searchQuery: '',\n currentStep: 'selection',\n focusedIndex: 0,\n isPublishing: false,\n successResult: null,\n errorResult: null,\n-}\n+ includeDependents: false,\n+})\n \n+const initialState: PublishState = createInitialState()\n+\n export const usePublishStore = create()(\n immer((set) => ({\n ...initialState,\n \n- openPublishMode: () =>\n- set((state) => {\n- state.publishMode = true\n- state.currentStep = 'selection'\n- state.selectedAgentIds = new Set()\n- state.searchQuery = ''\n- state.focusedIndex = 0\n- state.isPublishing = false\n- }),\n+ openPublishMode: () => set(() => createInitialState(true)),\n \n- closePublish: () =>\n- set((state) => {\n- state.publishMode = false\n- state.currentStep = 'selection'\n- state.selectedAgentIds = new Set()\n- state.searchQuery = ''\n- state.focusedIndex = 0\n- state.isPublishing = false\n- }),\n+ closePublish: () => set(() => createInitialState(false)),\n \n toggleAgentSelection: (agentId) =>\n set((state) => {\n if (state.selectedAgentIds.has(agentId)) {\n@@ -113,13 +103,13 @@\n state.focusedIndex = index\n }),\n \n preSelectAgents: (agentIds) =>\n- set((state) => {\n- state.selectedAgentIds = new Set(agentIds)\n- state.currentStep = 'confirmation'\n- state.publishMode = true\n- state.isPublishing = false\n+ set(() => {\n+ const nextState = createInitialState(true)\n+ nextState.selectedAgentIds = new Set(agentIds)\n+ nextState.currentStep = 'confirmation'\n+ return nextState\n }),\n \n setIsPublishing: (publishing) =>\n set((state) => {\n@@ -139,13 +129,12 @@\n state.currentStep = 'error'\n state.isPublishing = false\n }),\n \n- reset: () =>\n- set(() => ({\n- ...initialState,\n- selectedAgentIds: new Set(),\n- successResult: null,\n- errorResult: null,\n- })),\n+ setIncludeDependents: (include) =>\n+ set((state) => {\n+ state.includeDependents = include\n+ }),\n+\n+ reset: () => set(() => createInitialState(false)),\n })),\n )\n"
},
{
- "path": "cli/src/utils/codebuff-api.ts",
+ "path": "cli/src/utils/codebirds-api.ts",
"status": "modified",
- "diff": "Index: cli/src/utils/codebuff-api.ts\n===================================================================\n--- cli/src/utils/codebuff-api.ts\t99e4f7f (parent)\n+++ cli/src/utils/codebuff-api.ts\te889bf8 (commit)\n@@ -191,8 +191,9 @@\n \n /** Publish agents via /api/agents/publish */\n publish(\n data: Record[],\n+ allLocalAgentIds?: string[],\n ): Promise>\n \n /** Logout via /api/auth/cli/logout */\n logout(req?: LogoutRequest): Promise>\n@@ -490,12 +491,14 @@\n },\n \n publish(\n data: Record[],\n+ allLocalAgentIds?: string[],\n ): Promise> {\n // Auth is sent via Authorization header (includeAuth defaults to true)\n return request('POST', '/api/agents/publish', {\n data,\n+ allLocalAgentIds,\n })\n },\n \n logout(req: LogoutRequest = {}): Promise> {\n"
+ "diff": "Index: cli/src/utils/codebirds-api.ts\n===================================================================\n--- cli/src/utils/codebirds-api.ts\t99e4f7f (parent)\n+++ cli/src/utils/codebirds-api.ts\te889bf8 (commit)\n@@ -191,8 +191,9 @@\n \n /** Publish agents via /api/agents/publish */\n publish(\n data: Record[],\n+ allLocalAgentIds?: string[],\n ): Promise>\n \n /** Logout via /api/auth/cli/logout */\n logout(req?: LogoutRequest): Promise>\n@@ -490,12 +491,14 @@\n },\n \n publish(\n data: Record[],\n+ allLocalAgentIds?: string[],\n ): Promise> {\n // Auth is sent via Authorization header (includeAuth defaults to true)\n return request('POST', '/api/agents/publish', {\n data,\n+ allLocalAgentIds,\n })\n },\n \n logout(req: LogoutRequest = {}): Promise> {\n"
},
{
"path": "cli/src/utils/input-modes.ts",
@@ -162,7 +162,7 @@
{
"path": "cli/src/utils/local-agent-registry.ts",
"status": "modified",
- "diff": "Index: cli/src/utils/local-agent-registry.ts\n===================================================================\n--- cli/src/utils/local-agent-registry.ts\t99e4f7f (parent)\n+++ cli/src/utils/local-agent-registry.ts\te889bf8 (commit)\n@@ -29,8 +29,10 @@\n export interface LocalAgentInfo {\n id: string\n displayName: string\n filePath: string\n+ /** True if this is a bundled Codebuff agent (not user-created) */\n+ isBundled?: boolean\n }\n \n // ============================================================================\n // Bundled agents loading (generated at build time by prebuild-agents.ts)\n"
+ "diff": "Index: cli/src/utils/local-agent-registry.ts\n===================================================================\n--- cli/src/utils/local-agent-registry.ts\t99e4f7f (parent)\n+++ cli/src/utils/local-agent-registry.ts\te889bf8 (commit)\n@@ -29,8 +29,10 @@\n export interface LocalAgentInfo {\n id: string\n displayName: string\n filePath: string\n+ /** True if this is a bundled Codebirds agent (not user-created) */\n+ isBundled?: boolean\n }\n \n // ============================================================================\n // Bundled agents loading (generated at build time by prebuild-agents.ts)\n"
},
{
"path": "cli/src/utils/sdk-event-handlers.ts",
@@ -177,7 +177,7 @@
{
"path": "packages/internal/src/templates/agent-validation.ts",
"status": "modified",
- "diff": "Index: packages/internal/src/templates/agent-validation.ts\n===================================================================\n--- packages/internal/src/templates/agent-validation.ts\t99e4f7f (parent)\n+++ packages/internal/src/templates/agent-validation.ts\te889bf8 (commit)\n@@ -11,18 +11,23 @@\n import type { DynamicAgentTemplate } from '@codebuff/common/types/dynamic-agent-template'\n \n export async function validateAgentsWithSpawnableAgents(params: {\n agentTemplates?: Record\n+ allLocalAgentIds?: string[]\n logger: Logger\n }): Promise<{\n templates: Record\n dynamicTemplates: Record\n validationErrors: DynamicAgentValidationError[]\n }> {\n+ const { allLocalAgentIds = [] } = params\n const { agentIds, spawnableAgentIds } = collectAgentIds(params)\n+ // Include both the agents being validated AND all local agent IDs from the client\n+ // This allows referencing local agents that aren't being published\n+ const allKnownAgentIds = [...new Set([...agentIds, ...allLocalAgentIds])]\n const { validationErrors } = await validateSpawnableAgents({\n spawnableAgents: spawnableAgentIds,\n- dynamicAgentIds: agentIds,\n+ dynamicAgentIds: allKnownAgentIds,\n })\n if (validationErrors.length > 0) {\n return {\n templates: {},\n"
+ "diff": "Index: packages/internal/src/templates/agent-validation.ts\n===================================================================\n--- packages/internal/src/templates/agent-validation.ts\t99e4f7f (parent)\n+++ packages/internal/src/templates/agent-validation.ts\te889bf8 (commit)\n@@ -11,18 +11,23 @@\n import type { DynamicAgentTemplate } from '@codebirds/common/types/dynamic-agent-template'\n \n export async function validateAgentsWithSpawnableAgents(params: {\n agentTemplates?: Record\n+ allLocalAgentIds?: string[]\n logger: Logger\n }): Promise<{\n templates: Record\n dynamicTemplates: Record\n validationErrors: DynamicAgentValidationError[]\n }> {\n+ const { allLocalAgentIds = [] } = params\n const { agentIds, spawnableAgentIds } = collectAgentIds(params)\n+ // Include both the agents being validated AND all local agent IDs from the client\n+ // This allows referencing local agents that aren't being published\n+ const allKnownAgentIds = [...new Set([...agentIds, ...allLocalAgentIds])]\n const { validationErrors } = await validateSpawnableAgents({\n spawnableAgents: spawnableAgentIds,\n- dynamicAgentIds: agentIds,\n+ dynamicAgentIds: allKnownAgentIds,\n })\n if (validationErrors.length > 0) {\n return {\n templates: {},\n"
},
{
"path": "web/src/app/api/agents/publish/route.ts",
@@ -212,7 +212,7 @@
{
"path": "bun.lock",
"status": "modified",
- "diff": "Index: bun.lock\n===================================================================\n--- bun.lock\t9708152 (parent)\n+++ bun.lock\t84bcf34 (commit)\n@@ -210,8 +210,11 @@\n },\n \"packages/agent-runtime\": {\n \"name\": \"@codebuff/agent-runtime\",\n \"version\": \"0.0.0\",\n+ \"dependencies\": {\n+ \"zod-from-json-schema\": \"0.4.2\",\n+ },\n \"devDependencies\": {\n \"@types/bun\": \"^1.3.0\",\n \"@types/node\": \"22\",\n },\n"
+ "diff": "Index: bun.lock\n===================================================================\n--- bun.lock\t9708152 (parent)\n+++ bun.lock\t84bcf34 (commit)\n@@ -210,8 +210,11 @@\n },\n \"packages/agent-runtime\": {\n \"name\": \"@codebirds/agent-runtime\",\n \"version\": \"0.0.0\",\n+ \"dependencies\": {\n+ \"zod-from-json-schema\": \"0.4.2\",\n+ },\n \"devDependencies\": {\n \"@types/bun\": \"^1.3.0\",\n \"@types/node\": \"22\",\n },\n"
},
{
"path": "common/src/util/file.ts",
@@ -227,12 +227,12 @@
{
"path": "packages/agent-runtime/src/tools/prompts.ts",
"status": "modified",
- "diff": "Index: packages/agent-runtime/src/tools/prompts.ts\n===================================================================\n--- packages/agent-runtime/src/tools/prompts.ts\t9708152 (parent)\n+++ packages/agent-runtime/src/tools/prompts.ts\t84bcf34 (commit)\n@@ -4,16 +4,35 @@\n import { buildArray } from '@codebuff/common/util/array'\n import { pluralize } from '@codebuff/common/util/string'\n import { cloneDeep } from 'lodash'\n import z from 'zod/v4'\n+import { convertJsonSchemaToZod } from 'zod-from-json-schema'\n \n import type { ToolName } from '@codebuff/common/tools/constants'\n import type {\n CustomToolDefinitions,\n customToolDefinitionsSchema,\n } from '@codebuff/common/util/file'\n import type { ToolSet } from 'ai'\n \n+/**\n+ * Ensures the inputSchema is a Zod schema. If it's a JSON Schema object\n+ * (from SDK custom tools that were serialized), converts it to Zod.\n+ */\n+export function ensureZodSchema(\n+ schema: z.ZodType | Record,\n+): z.ZodType {\n+ // Check if it's already a Zod schema by looking for the safeParse method\n+ if (\n+ schema &&\n+ typeof (schema as { safeParse?: unknown }).safeParse === 'function'\n+ ) {\n+ return schema as z.ZodType\n+ }\n+ // JSON Schema object - convert to Zod\n+ return convertJsonSchemaToZod(schema as Record)\n+}\n+\n function paramsSection(params: { schema: z.ZodType; endsAgentStep: boolean }) {\n const { schema, endsAgentStep } = params\n const schemaWithEndsAgentStepParam = z.toJSONSchema(\n endsAgentStep\n@@ -205,9 +224,9 @@\n ...Object.keys(additionalToolDefinitions).map((toolName) => {\n const toolDef = additionalToolDefinitions[toolName]\n return buildToolDescription({\n toolName,\n- schema: toolDef.inputSchema,\n+ schema: ensureZodSchema(toolDef.inputSchema),\n description: toolDef.description,\n endsAgentStep: toolDef.endsAgentStep ?? true,\n exampleInputs: toolDef.exampleInputs,\n })\n@@ -225,9 +244,9 @@\n ) {\n return ''\n }\n \n- const toolDescriptions = [\n+ const toolDescriptionsList = [\n ...(\n toolNames.filter(\n (name) => (name as keyof typeof toolParams) in toolParams,\n ) as (keyof typeof toolParams)[]\n@@ -242,9 +261,9 @@\n ...Object.keys(additionalToolDefinitions).map((name) => {\n const { inputSchema, endsAgentStep } = additionalToolDefinitions[name]\n return buildShortToolDescription({\n toolName: name,\n- schema: inputSchema,\n+ schema: ensureZodSchema(inputSchema),\n endsAgentStep: endsAgentStep ?? true,\n })\n }),\n ]\n@@ -264,9 +283,9 @@\n )}\n \n Important: You only have access to the tools below. Do not use any other tools -- they are not available to you, instead they may have been previously used by other agents.\n \n-${toolDescriptions.join('\\n\\n')}\n+${toolDescriptionsList.join('\\n\\n')}\n `.trim()\n }\n \n export async function getToolSet(params: {\n@@ -284,11 +303,16 @@\n }\n \n const toolDefinitions = await additionalToolDefinitions()\n for (const [toolName, toolDefinition] of Object.entries(toolDefinitions)) {\n- toolSet[toolName] = cloneDeep(toolDefinition) satisfies {\n- inputSchema: { _zod: { input: any } }\n- } as Omit & { inputSchema: z.ZodType }\n+ const clonedDef = cloneDeep(toolDefinition)\n+ // Custom tool inputSchema may be JSON Schema (from SDK) or Zod (from MCP)\n+ // Ensure it's a Zod schema for the AI SDK\n+ const zodSchema = ensureZodSchema(clonedDef.inputSchema)\n+ toolSet[toolName] = {\n+ ...clonedDef,\n+ inputSchema: zodSchema,\n+ } as (typeof toolSet)[string]\n }\n \n // Add agent tools (agents as direct tool calls)\n for (const [toolName, toolDefinition] of Object.entries(agentTools)) {\n"
+ "diff": "Index: packages/agent-runtime/src/tools/prompts.ts\n===================================================================\n--- packages/agent-runtime/src/tools/prompts.ts\t9708152 (parent)\n+++ packages/agent-runtime/src/tools/prompts.ts\t84bcf34 (commit)\n@@ -4,16 +4,35 @@\n import { buildArray } from '@codebirds/common/util/array'\n import { pluralize } from '@codebirds/common/util/string'\n import { cloneDeep } from 'lodash'\n import z from 'zod/v4'\n+import { convertJsonSchemaToZod } from 'zod-from-json-schema'\n \n import type { ToolName } from '@codebirds/common/tools/constants'\n import type {\n CustomToolDefinitions,\n customToolDefinitionsSchema,\n } from '@codebirds/common/util/file'\n import type { ToolSet } from 'ai'\n \n+/**\n+ * Ensures the inputSchema is a Zod schema. If it's a JSON Schema object\n+ * (from SDK custom tools that were serialized), converts it to Zod.\n+ */\n+export function ensureZodSchema(\n+ schema: z.ZodType | Record,\n+): z.ZodType {\n+ // Check if it's already a Zod schema by looking for the safeParse method\n+ if (\n+ schema &&\n+ typeof (schema as { safeParse?: unknown }).safeParse === 'function'\n+ ) {\n+ return schema as z.ZodType\n+ }\n+ // JSON Schema object - convert to Zod\n+ return convertJsonSchemaToZod(schema as Record)\n+}\n+\n function paramsSection(params: { schema: z.ZodType; endsAgentStep: boolean }) {\n const { schema, endsAgentStep } = params\n const schemaWithEndsAgentStepParam = z.toJSONSchema(\n endsAgentStep\n@@ -205,9 +224,9 @@\n ...Object.keys(additionalToolDefinitions).map((toolName) => {\n const toolDef = additionalToolDefinitions[toolName]\n return buildToolDescription({\n toolName,\n- schema: toolDef.inputSchema,\n+ schema: ensureZodSchema(toolDef.inputSchema),\n description: toolDef.description,\n endsAgentStep: toolDef.endsAgentStep ?? true,\n exampleInputs: toolDef.exampleInputs,\n })\n@@ -225,9 +244,9 @@\n ) {\n return ''\n }\n \n- const toolDescriptions = [\n+ const toolDescriptionsList = [\n ...(\n toolNames.filter(\n (name) => (name as keyof typeof toolParams) in toolParams,\n ) as (keyof typeof toolParams)[]\n@@ -242,9 +261,9 @@\n ...Object.keys(additionalToolDefinitions).map((name) => {\n const { inputSchema, endsAgentStep } = additionalToolDefinitions[name]\n return buildShortToolDescription({\n toolName: name,\n- schema: inputSchema,\n+ schema: ensureZodSchema(inputSchema),\n endsAgentStep: endsAgentStep ?? true,\n })\n }),\n ]\n@@ -264,9 +283,9 @@\n )}\n \n Important: You only have access to the tools below. Do not use any other tools -- they are not available to you, instead they may have been previously used by other agents.\n \n-${toolDescriptions.join('\\n\\n')}\n+${toolDescriptionsList.join('\\n\\n')}\n `.trim()\n }\n \n export async function getToolSet(params: {\n@@ -284,11 +303,16 @@\n }\n \n const toolDefinitions = await additionalToolDefinitions()\n for (const [toolName, toolDefinition] of Object.entries(toolDefinitions)) {\n- toolSet[toolName] = cloneDeep(toolDefinition) satisfies {\n- inputSchema: { _zod: { input: any } }\n- } as Omit & { inputSchema: z.ZodType }\n+ const clonedDef = cloneDeep(toolDefinition)\n+ // Custom tool inputSchema may be JSON Schema (from SDK) or Zod (from MCP)\n+ // Ensure it's a Zod schema for the AI SDK\n+ const zodSchema = ensureZodSchema(clonedDef.inputSchema)\n+ toolSet[toolName] = {\n+ ...clonedDef,\n+ inputSchema: zodSchema,\n+ } as (typeof toolSet)[string]\n }\n \n // Add agent tools (agents as direct tool calls)\n for (const [toolName, toolDefinition] of Object.entries(agentTools)) {\n"
},
{
"path": "packages/agent-runtime/src/tools/tool-executor.ts",
"status": "modified",
- "diff": "Index: packages/agent-runtime/src/tools/tool-executor.ts\n===================================================================\n--- packages/agent-runtime/src/tools/tool-executor.ts\t9708152 (parent)\n+++ packages/agent-runtime/src/tools/tool-executor.ts\t84bcf34 (commit)\n@@ -7,8 +7,9 @@\n import { checkLiveUserInput } from '../live-user-inputs'\n import { getMCPToolData } from '../mcp'\n import { getAgentShortName } from '../templates/prompts'\n import { codebuffToolHandlers } from './handlers/list'\n+import { ensureZodSchema } from './prompts'\n \n import type { AgentTemplateType } from '@codebuff/common/types/session-state'\n \n import type { AgentTemplate } from '../templates/types'\n@@ -331,21 +332,24 @@\n processedParameters[endsAgentStepParam] =\n customToolDefs?.[toolName]?.endsAgentStep\n }\n \n- const paramsSchema = customToolDefs?.[toolName]?.inputSchema\n- const result = paramsSchema?.safeParse(processedParameters)\n+ const rawSchema = customToolDefs?.[toolName]?.inputSchema\n+ if (rawSchema) {\n+ const paramsSchema = ensureZodSchema(rawSchema)\n+ const result = paramsSchema.safeParse(processedParameters)\n \n- if (result && !result.success) {\n- return {\n- toolName: toolName,\n- toolCallId: rawToolCall.toolCallId,\n- input: rawToolCall.input,\n- error: `Invalid parameters for ${toolName}: ${JSON.stringify(\n- result.error.issues,\n- null,\n- 2,\n- )}`,\n+ if (!result.success) {\n+ return {\n+ toolName: toolName,\n+ toolCallId: rawToolCall.toolCallId,\n+ input: rawToolCall.input,\n+ error: `Invalid parameters for ${toolName}: ${JSON.stringify(\n+ result.error.issues,\n+ null,\n+ 2,\n+ )}`,\n+ }\n }\n }\n \n const input = JSON.parse(JSON.stringify(rawToolCall.input))\n"
+ "diff": "Index: packages/agent-runtime/src/tools/tool-executor.ts\n===================================================================\n--- packages/agent-runtime/src/tools/tool-executor.ts\t9708152 (parent)\n+++ packages/agent-runtime/src/tools/tool-executor.ts\t84bcf34 (commit)\n@@ -7,8 +7,9 @@\n import { checkLiveUserInput } from '../live-user-inputs'\n import { getMCPToolData } from '../mcp'\n import { getAgentShortName } from '../templates/prompts'\n import { codebirdsToolHandlers } from './handlers/list'\n+import { ensureZodSchema } from './prompts'\n \n import type { AgentTemplateType } from '@codebirds/common/types/session-state'\n \n import type { AgentTemplate } from '../templates/types'\n@@ -331,21 +332,24 @@\n processedParameters[endsAgentStepParam] =\n customToolDefs?.[toolName]?.endsAgentStep\n }\n \n- const paramsSchema = customToolDefs?.[toolName]?.inputSchema\n- const result = paramsSchema?.safeParse(processedParameters)\n+ const rawSchema = customToolDefs?.[toolName]?.inputSchema\n+ if (rawSchema) {\n+ const paramsSchema = ensureZodSchema(rawSchema)\n+ const result = paramsSchema.safeParse(processedParameters)\n \n- if (result && !result.success) {\n- return {\n- toolName: toolName,\n- toolCallId: rawToolCall.toolCallId,\n- input: rawToolCall.input,\n- error: `Invalid parameters for ${toolName}: ${JSON.stringify(\n- result.error.issues,\n- null,\n- 2,\n- )}`,\n+ if (!result.success) {\n+ return {\n+ toolName: toolName,\n+ toolCallId: rawToolCall.toolCallId,\n+ input: rawToolCall.input,\n+ error: `Invalid parameters for ${toolName}: ${JSON.stringify(\n+ result.error.issues,\n+ null,\n+ 2,\n+ )}`,\n+ }\n }\n }\n \n const input = JSON.parse(JSON.stringify(rawToolCall.input))\n"
},
{
"path": "sdk/e2e/utils/test-fixtures.ts",
@@ -242,7 +242,7 @@
{
"path": "sdk/src/run-state.ts",
"status": "modified",
- "diff": "Index: sdk/src/run-state.ts\n===================================================================\n--- sdk/src/run-state.ts\t9708152 (parent)\n+++ sdk/src/run-state.ts\t84bcf34 (commit)\n@@ -8,8 +8,9 @@\n } from '@codebuff/common/project-file-tree'\n import { getInitialSessionState } from '@codebuff/common/types/session-state'\n import { getErrorObject } from '@codebuff/common/util/error'\n import { cloneDeep } from 'lodash'\n+import z from 'zod/v4'\n \n import type { CustomToolDefinition } from './custom-tool'\n import type { AgentDefinition } from '@codebuff/common/templates/initial-agents-dir/types/agent-definition'\n import type { Logger } from '@codebuff/common/types/contracts/logger'\n@@ -65,26 +66,33 @@\n return processedAgentTemplates\n }\n \n /**\n- * Processes custom tool definitions into the format expected by SessionState\n+ * Processes custom tool definitions into the format expected by SessionState.\n+ * Converts Zod schemas to JSON Schema format so they can survive JSON serialization.\n */\n function processCustomToolDefinitions(\n customToolDefinitions: CustomToolDefinition[],\n-): Record<\n- string,\n- Pick[string]>\n-> {\n+): CustomToolDefinitions {\n return Object.fromEntries(\n- customToolDefinitions.map((toolDefinition) => [\n- toolDefinition.toolName,\n- {\n- inputSchema: toolDefinition.inputSchema,\n- description: toolDefinition.description,\n- endsAgentStep: toolDefinition.endsAgentStep,\n- exampleInputs: toolDefinition.exampleInputs,\n- },\n- ]),\n+ customToolDefinitions.map((toolDefinition) => {\n+ // Convert Zod schema to JSON Schema format so it survives JSON serialization\n+ // The agent-runtime will wrap this with AI SDK's jsonSchema() helper\n+ const jsonSchema = z.toJSONSchema(toolDefinition.inputSchema, {\n+ io: 'input',\n+ }) as Record\n+ delete jsonSchema['$schema']\n+\n+ return [\n+ toolDefinition.toolName,\n+ {\n+ inputSchema: jsonSchema,\n+ description: toolDefinition.description,\n+ endsAgentStep: toolDefinition.endsAgentStep,\n+ exampleInputs: toolDefinition.exampleInputs,\n+ },\n+ ]\n+ }),\n )\n }\n \n /**\n"
+ "diff": "Index: sdk/src/run-state.ts\n===================================================================\n--- sdk/src/run-state.ts\t9708152 (parent)\n+++ sdk/src/run-state.ts\t84bcf34 (commit)\n@@ -8,8 +8,9 @@\n } from '@codebirds/common/project-file-tree'\n import { getInitialSessionState } from '@codebirds/common/types/session-state'\n import { getErrorObject } from '@codebirds/common/util/error'\n import { cloneDeep } from 'lodash'\n+import z from 'zod/v4'\n \n import type { CustomToolDefinition } from './custom-tool'\n import type { AgentDefinition } from '@codebirds/common/templates/initial-agents-dir/types/agent-definition'\n import type { Logger } from '@codebirds/common/types/contracts/logger'\n@@ -65,26 +66,33 @@\n return processedAgentTemplates\n }\n \n /**\n- * Processes custom tool definitions into the format expected by SessionState\n+ * Processes custom tool definitions into the format expected by SessionState.\n+ * Converts Zod schemas to JSON Schema format so they can survive JSON serialization.\n */\n function processCustomToolDefinitions(\n customToolDefinitions: CustomToolDefinition[],\n-): Record<\n- string,\n- Pick[string]>\n-> {\n+): CustomToolDefinitions {\n return Object.fromEntries(\n- customToolDefinitions.map((toolDefinition) => [\n- toolDefinition.toolName,\n- {\n- inputSchema: toolDefinition.inputSchema,\n- description: toolDefinition.description,\n- endsAgentStep: toolDefinition.endsAgentStep,\n- exampleInputs: toolDefinition.exampleInputs,\n- },\n- ]),\n+ customToolDefinitions.map((toolDefinition) => {\n+ // Convert Zod schema to JSON Schema format so it survives JSON serialization\n+ // The agent-runtime will wrap this with AI SDK's jsonSchema() helper\n+ const jsonSchema = z.toJSONSchema(toolDefinition.inputSchema, {\n+ io: 'input',\n+ }) as Record\n+ delete jsonSchema['$schema']\n+\n+ return [\n+ toolDefinition.toolName,\n+ {\n+ inputSchema: jsonSchema,\n+ description: toolDefinition.description,\n+ endsAgentStep: toolDefinition.endsAgentStep,\n+ exampleInputs: toolDefinition.exampleInputs,\n+ },\n+ ]\n+ }),\n )\n }\n \n /**\n"
}
]
},
@@ -263,7 +263,7 @@
{
"path": ".agents/editor/best-of-n/editor-best-of-n.ts",
"status": "modified",
- "diff": "Index: .agents/editor/best-of-n/editor-best-of-n.ts\n===================================================================\n--- .agents/editor/best-of-n/editor-best-of-n.ts\t6984a5f (parent)\n+++ .agents/editor/best-of-n/editor-best-of-n.ts\tb72c9a8 (commit)\n@@ -109,22 +109,21 @@\n includeToolCall: false,\n } satisfies ToolCall<'spawn_agents'>\n \n // Extract spawn results\n- const spawnedImplementations =\n- extractSpawnResults<{ text: string }[]>(implementorResults)\n+ const spawnedImplementations = extractSpawnResults(implementorResults)\n \n logger.info({ spawnedImplementations }, 'spawnedImplementations')\n \n- // Extract all the plans from the structured outputs\n+ // Extract all the plans from the lastMessage outputs\n const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'\n // Parse implementations from spawn results\n const implementations = spawnedImplementations.map((result, index) => ({\n id: letters[index],\n content:\n 'errorMessage' in result\n ? `Error: ${result.errorMessage}`\n- : result[0].text,\n+ : extractLastMessageText(result) ?? '',\n }))\n \n // Spawn selector with implementations as params\n const { toolResult: selectorResult } = yield {\n@@ -139,12 +138,9 @@\n },\n includeToolCall: false,\n } satisfies ToolCall<'spawn_agents'>\n \n- const selectorOutput = extractSpawnResults<{\n- implementationId: string\n- reasoning: string\n- }>(selectorResult)[0]\n+ const selectorOutput = extractSelectorResult(selectorResult)\n \n if ('errorMessage' in selectorOutput) {\n yield {\n toolName: 'set_output',\n@@ -194,28 +190,87 @@\n },\n includeToolCall: false,\n } satisfies ToolCall<'set_output'>\n \n- function extractSpawnResults(\n+ /**\n+ * Extracts the array of subagent results from spawn_agents tool output.\n+ *\n+ * The spawn_agents tool result structure is:\n+ * [{ type: 'json', value: [{ agentName, agentType, value: AgentOutput }] }]\n+ *\n+ * Returns an array of agent outputs, one per spawned agent.\n+ */\n+ function extractSpawnResults(results: any[] | undefined): any[] {\n+ if (!results || results.length === 0) return []\n+\n+ // Find the json result containing spawn results\n+ const jsonResult = results.find((r) => r.type === 'json')\n+ if (!jsonResult?.value) return []\n+\n+ // Get the spawned agent results array\n+ const spawnedResults = Array.isArray(jsonResult.value)\n+ ? jsonResult.value\n+ : [jsonResult.value]\n+\n+ // Extract the value (AgentOutput) from each result\n+ return spawnedResults.map((result: any) => result?.value).filter(Boolean)\n+ }\n+\n+ /**\n+ * Extracts the structured output from a selector agent's spawn result.\n+ * Selector agents use outputMode: 'structured_output'.\n+ */\n+ function extractSelectorResult(\n results: any[] | undefined,\n- ): (T | { errorMessage: string })[] {\n- if (!results) return []\n- const spawnedResults = results\n- .filter((result) => result.type === 'json')\n- .map((result) => result.value)\n- .flat() as {\n- agentType: string\n- value: { value?: T; errorMessage?: string }\n- }[]\n- return spawnedResults.map(\n- (result) =>\n- result.value.value ?? {\n- errorMessage:\n- result.value.errorMessage ?? 'Error extracting spawn results',\n- },\n- )\n+ ): { implementationId: string; reasoning: string } | { errorMessage: string } {\n+ const outputs = extractSpawnResults(results)\n+ const firstOutput = outputs[0]\n+ if (!firstOutput) {\n+ return { errorMessage: 'No selector output' }\n+ }\n+ if (firstOutput.type === 'structuredOutput' && firstOutput.value) {\n+ return firstOutput.value\n+ }\n+ if (firstOutput.type === 'error') {\n+ return { errorMessage: firstOutput.message ?? 'Selector error' }\n+ }\n+ return { errorMessage: 'Invalid selector output format' }\n }\n \n+ /**\n+ * Extracts all text content from a 'lastMessage' AgentOutput.\n+ *\n+ * For agents with outputMode: 'last_message', the output structure is:\n+ * { type: 'lastMessage', value: [{ role: 'assistant', content: [{ type: 'text', text: '...' }] }] }\n+ *\n+ * Returns concatenated text from all assistant messages, or null if not found.\n+ * Note: Due to streaming, each text chunk may be a separate assistant message,\n+ * so we need to concatenate all of them to get the full response.\n+ */\n+ function extractLastMessageText(agentOutput: any): string | null {\n+ if (!agentOutput) return null\n+\n+ // Handle 'lastMessage' output mode - the value contains an array of messages\n+ if (\n+ agentOutput.type === 'lastMessage' &&\n+ Array.isArray(agentOutput.value)\n+ ) {\n+ // Collect text from all assistant messages (streaming creates multiple messages)\n+ const textParts: string[] = []\n+ for (const message of agentOutput.value) {\n+ if (message.role === 'assistant' && Array.isArray(message.content)) {\n+ for (const part of message.content) {\n+ if (part.type === 'text' && typeof part.text === 'string') {\n+ textParts.push(part.text)\n+ }\n+ }\n+ }\n+ }\n+ return textParts.length > 0 ? textParts.join('') : null\n+ }\n+ return null\n+ }\n+\n // Extract only tool calls from text, removing any commentary\n function extractToolCallsOnly(text: string): string {\n const toolExtractionPattern =\n /\\n(.*?)\\n<\\/codebuff_tool_call>/gs\n@@ -394,14 +449,16 @@\n return spawnedResults.map((result: any) => result?.value).filter(Boolean)\n }\n \n /**\n- * Extracts the text content from a 'lastMessage' AgentOutput.\n+ * Extracts all text content from a 'lastMessage' AgentOutput.\n *\n * For agents with outputMode: 'last_message', the output structure is:\n * { type: 'lastMessage', value: [{ role: 'assistant', content: [{ type: 'text', text: '...' }] }] }\n *\n- * Returns the text from the last assistant message, or null if not found.\n+ * Returns concatenated text from all assistant messages, or null if not found.\n+ * Note: Due to streaming, each text chunk may be a separate assistant message,\n+ * so we need to concatenate all of them to get the full response.\n */\n function extractLastMessageText(agentOutput: any): string | null {\n if (!agentOutput) return null\n \n@@ -409,20 +466,20 @@\n if (\n agentOutput.type === 'lastMessage' &&\n Array.isArray(agentOutput.value)\n ) {\n- // Find the last assistant message with text content\n- for (let i = agentOutput.value.length - 1; i >= 0; i--) {\n- const message = agentOutput.value[i]\n+ // Collect text from all assistant messages (streaming creates multiple messages)\n+ const textParts: string[] = []\n+ for (const message of agentOutput.value) {\n if (message.role === 'assistant' && Array.isArray(message.content)) {\n- // Find text content in the message\n for (const part of message.content) {\n if (part.type === 'text' && typeof part.text === 'string') {\n- return part.text\n+ textParts.push(part.text)\n }\n }\n }\n }\n+ return textParts.length > 0 ? textParts.join('') : null\n }\n return null\n }\n }\n@@ -456,20 +513,19 @@\n includeToolCall: false,\n } satisfies ToolCall<'spawn_agents'>\n \n // Extract spawn results\n- const spawnedImplementations =\n- extractSpawnResults<{ text: string }[]>(implementorResults)\n+ const spawnedImplementations = extractSpawnResults(implementorResults)\n \n- // Extract all the plans from the structured outputs\n+ // Extract all the plans from the lastMessage outputs\n const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'\n // Parse implementations from spawn results\n const implementations = spawnedImplementations.map((result, index) => ({\n id: letters[index],\n content:\n 'errorMessage' in result\n ? `Error: ${result.errorMessage}`\n- : result[0].text,\n+ : extractLastMessageText(result) ?? '',\n }))\n \n // Spawn selector with implementations as params\n const { toolResult: selectorResult } = yield {\n@@ -484,12 +540,9 @@\n },\n includeToolCall: false,\n } satisfies ToolCall<'spawn_agents'>\n \n- const selectorOutput = extractSpawnResults<{\n- implementationId: string\n- reasoning: string\n- }>(selectorResult)[0]\n+ const selectorOutput = extractSelectorResult(selectorResult)\n \n if ('errorMessage' in selectorOutput) {\n yield {\n toolName: 'set_output',\n@@ -539,28 +592,87 @@\n },\n includeToolCall: false,\n } satisfies ToolCall<'set_output'>\n \n- function extractSpawnResults(\n+ /**\n+ * Extracts the array of subagent results from spawn_agents tool output.\n+ *\n+ * The spawn_agents tool result structure is:\n+ * [{ type: 'json', value: [{ agentName, agentType, value: AgentOutput }] }]\n+ *\n+ * Returns an array of agent outputs, one per spawned agent.\n+ */\n+ function extractSpawnResults(results: any[] | undefined): any[] {\n+ if (!results || results.length === 0) return []\n+\n+ // Find the json result containing spawn results\n+ const jsonResult = results.find((r) => r.type === 'json')\n+ if (!jsonResult?.value) return []\n+\n+ // Get the spawned agent results array\n+ const spawnedResults = Array.isArray(jsonResult.value)\n+ ? jsonResult.value\n+ : [jsonResult.value]\n+\n+ // Extract the value (AgentOutput) from each result\n+ return spawnedResults.map((result: any) => result?.value).filter(Boolean)\n+ }\n+\n+ /**\n+ * Extracts the structured output from a selector agent's spawn result.\n+ * Selector agents use outputMode: 'structured_output'.\n+ */\n+ function extractSelectorResult(\n results: any[] | undefined,\n- ): (T | { errorMessage: string })[] {\n- if (!results) return []\n- const spawnedResults = results\n- .filter((result) => result.type === 'json')\n- .map((result) => result.value)\n- .flat() as {\n- agentType: string\n- value: { value?: T; errorMessage?: string }\n- }[]\n- return spawnedResults.map(\n- (result) =>\n- result.value.value ?? {\n- errorMessage:\n- result.value.errorMessage ?? 'Error extracting spawn results',\n- },\n- )\n+ ): { implementationId: string; reasoning: string } | { errorMessage: string } {\n+ const outputs = extractSpawnResults(results)\n+ const firstOutput = outputs[0]\n+ if (!firstOutput) {\n+ return { errorMessage: 'No selector output' }\n+ }\n+ if (firstOutput.type === 'structuredOutput' && firstOutput.value) {\n+ return firstOutput.value\n+ }\n+ if (firstOutput.type === 'error') {\n+ return { errorMessage: firstOutput.message ?? 'Selector error' }\n+ }\n+ return { errorMessage: 'Invalid selector output format' }\n }\n \n+ /**\n+ * Extracts all text content from a 'lastMessage' AgentOutput.\n+ *\n+ * For agents with outputMode: 'last_message', the output structure is:\n+ * { type: 'lastMessage', value: [{ role: 'assistant', content: [{ type: 'text', text: '...' }] }] }\n+ *\n+ * Returns concatenated text from all assistant messages, or null if not found.\n+ * Note: Due to streaming, each text chunk may be a separate assistant message,\n+ * so we need to concatenate all of them to get the full response.\n+ */\n+ function extractLastMessageText(agentOutput: any): string | null {\n+ if (!agentOutput) return null\n+\n+ // Handle 'lastMessage' output mode - the value contains an array of messages\n+ if (\n+ agentOutput.type === 'lastMessage' &&\n+ Array.isArray(agentOutput.value)\n+ ) {\n+ // Collect text from all assistant messages (streaming creates multiple messages)\n+ const textParts: string[] = []\n+ for (const message of agentOutput.value) {\n+ if (message.role === 'assistant' && Array.isArray(message.content)) {\n+ for (const part of message.content) {\n+ if (part.type === 'text' && typeof part.text === 'string') {\n+ textParts.push(part.text)\n+ }\n+ }\n+ }\n+ }\n+ return textParts.length > 0 ? textParts.join('') : null\n+ }\n+ return null\n+ }\n+\n // Extract only tool calls from text, removing any commentary\n function extractToolCallsOnly(text: string): string {\n const toolExtractionPattern =\n /\\n(.*?)\\n<\\/codebuff_tool_call>/gs\n"
+ "diff": "Index: .agents/editor/best-of-n/editor-best-of-n.ts\n===================================================================\n--- .agents/editor/best-of-n/editor-best-of-n.ts\t6984a5f (parent)\n+++ .agents/editor/best-of-n/editor-best-of-n.ts\tb72c9a8 (commit)\n@@ -109,22 +109,21 @@\n includeToolCall: false,\n } satisfies ToolCall<'spawn_agents'>\n \n // Extract spawn results\n- const spawnedImplementations =\n- extractSpawnResults<{ text: string }[]>(implementorResults)\n+ const spawnedImplementations = extractSpawnResults(implementorResults)\n \n logger.info({ spawnedImplementations }, 'spawnedImplementations')\n \n- // Extract all the plans from the structured outputs\n+ // Extract all the plans from the lastMessage outputs\n const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'\n // Parse implementations from spawn results\n const implementations = spawnedImplementations.map((result, index) => ({\n id: letters[index],\n content:\n 'errorMessage' in result\n ? `Error: ${result.errorMessage}`\n- : result[0].text,\n+ : extractLastMessageText(result) ?? '',\n }))\n \n // Spawn selector with implementations as params\n const { toolResult: selectorResult } = yield {\n@@ -139,12 +138,9 @@\n },\n includeToolCall: false,\n } satisfies ToolCall<'spawn_agents'>\n \n- const selectorOutput = extractSpawnResults<{\n- implementationId: string\n- reasoning: string\n- }>(selectorResult)[0]\n+ const selectorOutput = extractSelectorResult(selectorResult)\n \n if ('errorMessage' in selectorOutput) {\n yield {\n toolName: 'set_output',\n@@ -194,28 +190,87 @@\n },\n includeToolCall: false,\n } satisfies ToolCall<'set_output'>\n \n- function extractSpawnResults(\n+ /**\n+ * Extracts the array of subagent results from spawn_agents tool output.\n+ *\n+ * The spawn_agents tool result structure is:\n+ * [{ type: 'json', value: [{ agentName, agentType, value: AgentOutput }] }]\n+ *\n+ * Returns an array of agent outputs, one per spawned agent.\n+ */\n+ function extractSpawnResults(results: any[] | undefined): any[] {\n+ if (!results || results.length === 0) return []\n+\n+ // Find the json result containing spawn results\n+ const jsonResult = results.find((r) => r.type === 'json')\n+ if (!jsonResult?.value) return []\n+\n+ // Get the spawned agent results array\n+ const spawnedResults = Array.isArray(jsonResult.value)\n+ ? jsonResult.value\n+ : [jsonResult.value]\n+\n+ // Extract the value (AgentOutput) from each result\n+ return spawnedResults.map((result: any) => result?.value).filter(Boolean)\n+ }\n+\n+ /**\n+ * Extracts the structured output from a selector agent's spawn result.\n+ * Selector agents use outputMode: 'structured_output'.\n+ */\n+ function extractSelectorResult(\n results: any[] | undefined,\n- ): (T | { errorMessage: string })[] {\n- if (!results) return []\n- const spawnedResults = results\n- .filter((result) => result.type === 'json')\n- .map((result) => result.value)\n- .flat() as {\n- agentType: string\n- value: { value?: T; errorMessage?: string }\n- }[]\n- return spawnedResults.map(\n- (result) =>\n- result.value.value ?? {\n- errorMessage:\n- result.value.errorMessage ?? 'Error extracting spawn results',\n- },\n- )\n+ ): { implementationId: string; reasoning: string } | { errorMessage: string } {\n+ const outputs = extractSpawnResults(results)\n+ const firstOutput = outputs[0]\n+ if (!firstOutput) {\n+ return { errorMessage: 'No selector output' }\n+ }\n+ if (firstOutput.type === 'structuredOutput' && firstOutput.value) {\n+ return firstOutput.value\n+ }\n+ if (firstOutput.type === 'error') {\n+ return { errorMessage: firstOutput.message ?? 'Selector error' }\n+ }\n+ return { errorMessage: 'Invalid selector output format' }\n }\n \n+ /**\n+ * Extracts all text content from a 'lastMessage' AgentOutput.\n+ *\n+ * For agents with outputMode: 'last_message', the output structure is:\n+ * { type: 'lastMessage', value: [{ role: 'assistant', content: [{ type: 'text', text: '...' }] }] }\n+ *\n+ * Returns concatenated text from all assistant messages, or null if not found.\n+ * Note: Due to streaming, each text chunk may be a separate assistant message,\n+ * so we need to concatenate all of them to get the full response.\n+ */\n+ function extractLastMessageText(agentOutput: any): string | null {\n+ if (!agentOutput) return null\n+\n+ // Handle 'lastMessage' output mode - the value contains an array of messages\n+ if (\n+ agentOutput.type === 'lastMessage' &&\n+ Array.isArray(agentOutput.value)\n+ ) {\n+ // Collect text from all assistant messages (streaming creates multiple messages)\n+ const textParts: string[] = []\n+ for (const message of agentOutput.value) {\n+ if (message.role === 'assistant' && Array.isArray(message.content)) {\n+ for (const part of message.content) {\n+ if (part.type === 'text' && typeof part.text === 'string') {\n+ textParts.push(part.text)\n+ }\n+ }\n+ }\n+ }\n+ return textParts.length > 0 ? textParts.join('') : null\n+ }\n+ return null\n+ }\n+\n // Extract only tool calls from text, removing any commentary\n function extractToolCallsOnly(text: string): string {\n const toolExtractionPattern =\n /\\n(.*?)\\n<\\/codebirds_tool_call>/gs\n@@ -394,14 +449,16 @@\n return spawnedResults.map((result: any) => result?.value).filter(Boolean)\n }\n \n /**\n- * Extracts the text content from a 'lastMessage' AgentOutput.\n+ * Extracts all text content from a 'lastMessage' AgentOutput.\n *\n * For agents with outputMode: 'last_message', the output structure is:\n * { type: 'lastMessage', value: [{ role: 'assistant', content: [{ type: 'text', text: '...' }] }] }\n *\n- * Returns the text from the last assistant message, or null if not found.\n+ * Returns concatenated text from all assistant messages, or null if not found.\n+ * Note: Due to streaming, each text chunk may be a separate assistant message,\n+ * so we need to concatenate all of them to get the full response.\n */\n function extractLastMessageText(agentOutput: any): string | null {\n if (!agentOutput) return null\n \n@@ -409,20 +466,20 @@\n if (\n agentOutput.type === 'lastMessage' &&\n Array.isArray(agentOutput.value)\n ) {\n- // Find the last assistant message with text content\n- for (let i = agentOutput.value.length - 1; i >= 0; i--) {\n- const message = agentOutput.value[i]\n+ // Collect text from all assistant messages (streaming creates multiple messages)\n+ const textParts: string[] = []\n+ for (const message of agentOutput.value) {\n if (message.role === 'assistant' && Array.isArray(message.content)) {\n- // Find text content in the message\n for (const part of message.content) {\n if (part.type === 'text' && typeof part.text === 'string') {\n- return part.text\n+ textParts.push(part.text)\n }\n }\n }\n }\n+ return textParts.length > 0 ? textParts.join('') : null\n }\n return null\n }\n }\n@@ -456,20 +513,19 @@\n includeToolCall: false,\n } satisfies ToolCall<'spawn_agents'>\n \n // Extract spawn results\n- const spawnedImplementations =\n- extractSpawnResults<{ text: string }[]>(implementorResults)\n+ const spawnedImplementations = extractSpawnResults(implementorResults)\n \n- // Extract all the plans from the structured outputs\n+ // Extract all the plans from the lastMessage outputs\n const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'\n // Parse implementations from spawn results\n const implementations = spawnedImplementations.map((result, index) => ({\n id: letters[index],\n content:\n 'errorMessage' in result\n ? `Error: ${result.errorMessage}`\n- : result[0].text,\n+ : extractLastMessageText(result) ?? '',\n }))\n \n // Spawn selector with implementations as params\n const { toolResult: selectorResult } = yield {\n@@ -484,12 +540,9 @@\n },\n includeToolCall: false,\n } satisfies ToolCall<'spawn_agents'>\n \n- const selectorOutput = extractSpawnResults<{\n- implementationId: string\n- reasoning: string\n- }>(selectorResult)[0]\n+ const selectorOutput = extractSelectorResult(selectorResult)\n \n if ('errorMessage' in selectorOutput) {\n yield {\n toolName: 'set_output',\n@@ -539,28 +592,87 @@\n },\n includeToolCall: false,\n } satisfies ToolCall<'set_output'>\n \n- function extractSpawnResults(\n+ /**\n+ * Extracts the array of subagent results from spawn_agents tool output.\n+ *\n+ * The spawn_agents tool result structure is:\n+ * [{ type: 'json', value: [{ agentName, agentType, value: AgentOutput }] }]\n+ *\n+ * Returns an array of agent outputs, one per spawned agent.\n+ */\n+ function extractSpawnResults(results: any[] | undefined): any[] {\n+ if (!results || results.length === 0) return []\n+\n+ // Find the json result containing spawn results\n+ const jsonResult = results.find((r) => r.type === 'json')\n+ if (!jsonResult?.value) return []\n+\n+ // Get the spawned agent results array\n+ const spawnedResults = Array.isArray(jsonResult.value)\n+ ? jsonResult.value\n+ : [jsonResult.value]\n+\n+ // Extract the value (AgentOutput) from each result\n+ return spawnedResults.map((result: any) => result?.value).filter(Boolean)\n+ }\n+\n+ /**\n+ * Extracts the structured output from a selector agent's spawn result.\n+ * Selector agents use outputMode: 'structured_output'.\n+ */\n+ function extractSelectorResult(\n results: any[] | undefined,\n- ): (T | { errorMessage: string })[] {\n- if (!results) return []\n- const spawnedResults = results\n- .filter((result) => result.type === 'json')\n- .map((result) => result.value)\n- .flat() as {\n- agentType: string\n- value: { value?: T; errorMessage?: string }\n- }[]\n- return spawnedResults.map(\n- (result) =>\n- result.value.value ?? {\n- errorMessage:\n- result.value.errorMessage ?? 'Error extracting spawn results',\n- },\n- )\n+ ): { implementationId: string; reasoning: string } | { errorMessage: string } {\n+ const outputs = extractSpawnResults(results)\n+ const firstOutput = outputs[0]\n+ if (!firstOutput) {\n+ return { errorMessage: 'No selector output' }\n+ }\n+ if (firstOutput.type === 'structuredOutput' && firstOutput.value) {\n+ return firstOutput.value\n+ }\n+ if (firstOutput.type === 'error') {\n+ return { errorMessage: firstOutput.message ?? 'Selector error' }\n+ }\n+ return { errorMessage: 'Invalid selector output format' }\n }\n \n+ /**\n+ * Extracts all text content from a 'lastMessage' AgentOutput.\n+ *\n+ * For agents with outputMode: 'last_message', the output structure is:\n+ * { type: 'lastMessage', value: [{ role: 'assistant', content: [{ type: 'text', text: '...' }] }] }\n+ *\n+ * Returns concatenated text from all assistant messages, or null if not found.\n+ * Note: Due to streaming, each text chunk may be a separate assistant message,\n+ * so we need to concatenate all of them to get the full response.\n+ */\n+ function extractLastMessageText(agentOutput: any): string | null {\n+ if (!agentOutput) return null\n+\n+ // Handle 'lastMessage' output mode - the value contains an array of messages\n+ if (\n+ agentOutput.type === 'lastMessage' &&\n+ Array.isArray(agentOutput.value)\n+ ) {\n+ // Collect text from all assistant messages (streaming creates multiple messages)\n+ const textParts: string[] = []\n+ for (const message of agentOutput.value) {\n+ if (message.role === 'assistant' && Array.isArray(message.content)) {\n+ for (const part of message.content) {\n+ if (part.type === 'text' && typeof part.text === 'string') {\n+ textParts.push(part.text)\n+ }\n+ }\n+ }\n+ }\n+ return textParts.length > 0 ? textParts.join('') : null\n+ }\n+ return null\n+ }\n+\n // Extract only tool calls from text, removing any commentary\n function extractToolCallsOnly(text: string): string {\n const toolExtractionPattern =\n /\\n(.*?)\\n<\\/codebirds_tool_call>/gs\n"
},
{
"path": ".agents/file-explorer/file-picker.ts",
@@ -311,7 +311,7 @@
{
"path": "cli/src/commands/publish.ts",
"status": "modified",
- "diff": "Index: cli/src/commands/publish.ts\n===================================================================\n--- cli/src/commands/publish.ts\tf639a8e (parent)\n+++ cli/src/commands/publish.ts\t10d78f7 (commit)\n@@ -1,6 +1,5 @@\n import { WEBSITE_URL } from '@codebuff/sdk'\n-import { cyan, green, red, yellow } from 'picocolors'\n \n import { getUserCredentials } from '../utils/auth'\n import { getApiClient, setApiClientAuthToken } from '../utils/codebuff-api'\n import { loadAgentDefinitions, getLoadedAgentsData } from '../utils/local-agent-registry'\n@@ -9,8 +8,21 @@\n PublishAgentsErrorResponse,\n PublishAgentsResponse,\n } from '@codebuff/common/types/api/agents/publish'\n \n+export interface PublishResult {\n+ success: boolean\n+ publisherId?: string\n+ agents?: Array<{\n+ id: string\n+ version: string\n+ displayName: string\n+ }>\n+ error?: string\n+ details?: string\n+ hint?: string\n+}\n+\n /**\n * Publish agent templates to the backend\n */\n async function publishAgentTemplates(\n@@ -76,44 +88,39 @@\n \n /**\n * Handle the publish command to upload agent templates to the backend\n * @param agentIds The ids or display names of the agents to publish\n+ * @returns PublishResult with success/error information\n */\n-export async function handlePublish(agentIds: string[]): Promise {\n+export async function handlePublish(agentIds: string[]): Promise {\n const user = getUserCredentials()\n \n if (!user) {\n- console.log(red('Please log in first using \"login\" command or web UI.'))\n- return\n+ return {\n+ success: false,\n+ error: 'Not logged in',\n+ hint: 'Please log in first using \"login\" command or web UI.',\n+ }\n }\n \n const availableAgents = getLoadedAgentsData()?.agents || []\n \n if (agentIds?.length === 0) {\n- console.log(\n- red('Agent id is required. Usage: publish [agent-id2] ...'),\n- )\n-\n- // Show available agents\n- if (availableAgents.length > 0) {\n- console.log(cyan('Available agents:'))\n- availableAgents.forEach((agent) => {\n- const identifier =\n- agent.displayName && agent.displayName !== agent.id\n- ? `${agent.displayName} (${agent.id})`\n- : agent.displayName || agent.id\n- console.log(` - ${identifier}`)\n- })\n+ return {\n+ success: false,\n+ error: 'No agents specified',\n+ hint: 'Usage: publish [agent-id2] ...',\n }\n- return\n }\n \n try {\n const loadedDefinitions = loadAgentDefinitions()\n \n if (loadedDefinitions.length === 0) {\n- console.log(red('No valid agent templates found in .agents directory.'))\n- return\n+ return {\n+ success: false,\n+ error: 'No valid agent templates found in .agents directory.',\n+ }\n }\n \n const matchingTemplates: Record = {}\n \n@@ -124,17 +131,20 @@\n template.id === agentId || (template as any).displayName === agentId,\n )\n \n if (!matchingTemplate) {\n- console.log(red(`Agent \"${agentId}\" not found. Available agents:`))\n- availableAgents.forEach((agent) => {\n- const identifier =\n+ const availableList = availableAgents\n+ .map((agent) =>\n agent.displayName && agent.displayName !== agent.id\n ? `${agent.displayName} (${agent.id})`\n- : agent.displayName || agent.id\n- console.log(` - ${identifier}`)\n- })\n- return\n+ : agent.displayName || agent.id,\n+ )\n+ .join(', ')\n+ return {\n+ success: false,\n+ error: `Agent \"${agentId}\" not found`,\n+ details: `Available agents: ${availableList}`,\n+ }\n }\n \n // Process the template for publishing\n const processedTemplate = { ...matchingTemplate }\n@@ -148,60 +158,39 @@\n \n matchingTemplates[matchingTemplate.id] = processedTemplate\n }\n \n- console.log(yellow(`Publishing:`))\n- for (const template of Object.values(matchingTemplates)) {\n- const displayName = (template as any).displayName || template.id\n- console.log(` - ${displayName} (${template.id})`)\n- }\n-\n const result = await publishAgentTemplates(\n Object.values(matchingTemplates),\n user.authToken!,\n )\n \n if (result.success) {\n- console.log(green(`✅ Successfully published:`))\n- for (const agent of result.agents) {\n- console.log(\n- cyan(\n- ` - ${agent.displayName} (${result.publisherId}/${agent.id}@${agent.version})`,\n- ),\n- )\n+ return {\n+ success: true,\n+ publisherId: result.publisherId,\n+ agents: result.agents,\n }\n- return\n }\n \n- console.log(red(`❌ Failed to publish your agents`))\n- if (result.error) console.log(red(`Error: ${result.error}`))\n- if (result.details) console.log(red(`\\n${result.details}`))\n- if (result.hint) console.log(yellow(`\\nHint: ${result.hint}`))\n-\n- // Show helpful guidance based on error type\n+ // Build error result\n+ let hint = result.hint\n if (result.error?.includes('Publisher field required')) {\n- console.log()\n- console.log(cyan('Add a \"publisher\" field to your agent templates:'))\n- console.log(yellow(' \"publisher\": \"\"'))\n- console.log()\n- } else if (\n- result.error?.includes('Publisher not found or not accessible')\n- ) {\n- console.log()\n- console.log(\n- cyan(\n- 'Check that the publisher ID is correct and you have access to it.',\n- ),\n- )\n- console.log()\n+ hint = 'Add a \"publisher\" field to your agent templates.'\n+ } else if (result.error?.includes('Publisher not found or not accessible')) {\n+ hint = `Check that the publisher ID is correct and you have access to it. Visit ${WEBSITE_URL}/publishers to manage publishers.`\n }\n \n- console.log(cyan('Visit the website to manage your publishers:'))\n- console.log(yellow(`${WEBSITE_URL}/publishers`))\n+ return {\n+ success: false,\n+ error: result.error,\n+ details: result.details,\n+ hint,\n+ }\n } catch (error) {\n- console.log(\n- red(\n- `Error during publish: ${error instanceof Error ? error.message + '\\n' + error.stack : String(error)}`,\n- ),\n- )\n+ return {\n+ success: false,\n+ error: 'Publish failed',\n+ details: error instanceof Error ? error.message : String(error),\n+ }\n }\n }\n"
+ "diff": "Index: cli/src/commands/publish.ts\n===================================================================\n--- cli/src/commands/publish.ts\tf639a8e (parent)\n+++ cli/src/commands/publish.ts\t10d78f7 (commit)\n@@ -1,6 +1,5 @@\n import { WEBSITE_URL } from '@codebirds/sdk'\n-import { cyan, green, red, yellow } from 'picocolors'\n \n import { getUserCredentials } from '../utils/auth'\n import { getApiClient, setApiClientAuthToken } from '../utils/codebirds-api'\n import { loadAgentDefinitions, getLoadedAgentsData } from '../utils/local-agent-registry'\n@@ -9,8 +8,21 @@\n PublishAgentsErrorResponse,\n PublishAgentsResponse,\n } from '@codebirds/common/types/api/agents/publish'\n \n+export interface PublishResult {\n+ success: boolean\n+ publisherId?: string\n+ agents?: Array<{\n+ id: string\n+ version: string\n+ displayName: string\n+ }>\n+ error?: string\n+ details?: string\n+ hint?: string\n+}\n+\n /**\n * Publish agent templates to the backend\n */\n async function publishAgentTemplates(\n@@ -76,44 +88,39 @@\n \n /**\n * Handle the publish command to upload agent templates to the backend\n * @param agentIds The ids or display names of the agents to publish\n+ * @returns PublishResult with success/error information\n */\n-export async function handlePublish(agentIds: string[]): Promise {\n+export async function handlePublish(agentIds: string[]): Promise {\n const user = getUserCredentials()\n \n if (!user) {\n- console.log(red('Please log in first using \"login\" command or web UI.'))\n- return\n+ return {\n+ success: false,\n+ error: 'Not logged in',\n+ hint: 'Please log in first using \"login\" command or web UI.',\n+ }\n }\n \n const availableAgents = getLoadedAgentsData()?.agents || []\n \n if (agentIds?.length === 0) {\n- console.log(\n- red('Agent id is required. Usage: publish [agent-id2] ...'),\n- )\n-\n- // Show available agents\n- if (availableAgents.length > 0) {\n- console.log(cyan('Available agents:'))\n- availableAgents.forEach((agent) => {\n- const identifier =\n- agent.displayName && agent.displayName !== agent.id\n- ? `${agent.displayName} (${agent.id})`\n- : agent.displayName || agent.id\n- console.log(` - ${identifier}`)\n- })\n+ return {\n+ success: false,\n+ error: 'No agents specified',\n+ hint: 'Usage: publish [agent-id2] ...',\n }\n- return\n }\n \n try {\n const loadedDefinitions = loadAgentDefinitions()\n \n if (loadedDefinitions.length === 0) {\n- console.log(red('No valid agent templates found in .agents directory.'))\n- return\n+ return {\n+ success: false,\n+ error: 'No valid agent templates found in .agents directory.',\n+ }\n }\n \n const matchingTemplates: Record = {}\n \n@@ -124,17 +131,20 @@\n template.id === agentId || (template as any).displayName === agentId,\n )\n \n if (!matchingTemplate) {\n- console.log(red(`Agent \"${agentId}\" not found. Available agents:`))\n- availableAgents.forEach((agent) => {\n- const identifier =\n+ const availableList = availableAgents\n+ .map((agent) =>\n agent.displayName && agent.displayName !== agent.id\n ? `${agent.displayName} (${agent.id})`\n- : agent.displayName || agent.id\n- console.log(` - ${identifier}`)\n- })\n- return\n+ : agent.displayName || agent.id,\n+ )\n+ .join(', ')\n+ return {\n+ success: false,\n+ error: `Agent \"${agentId}\" not found`,\n+ details: `Available agents: ${availableList}`,\n+ }\n }\n \n // Process the template for publishing\n const processedTemplate = { ...matchingTemplate }\n@@ -148,60 +158,39 @@\n \n matchingTemplates[matchingTemplate.id] = processedTemplate\n }\n \n- console.log(yellow(`Publishing:`))\n- for (const template of Object.values(matchingTemplates)) {\n- const displayName = (template as any).displayName || template.id\n- console.log(` - ${displayName} (${template.id})`)\n- }\n-\n const result = await publishAgentTemplates(\n Object.values(matchingTemplates),\n user.authToken!,\n )\n \n if (result.success) {\n- console.log(green(`✅ Successfully published:`))\n- for (const agent of result.agents) {\n- console.log(\n- cyan(\n- ` - ${agent.displayName} (${result.publisherId}/${agent.id}@${agent.version})`,\n- ),\n- )\n+ return {\n+ success: true,\n+ publisherId: result.publisherId,\n+ agents: result.agents,\n }\n- return\n }\n \n- console.log(red(`❌ Failed to publish your agents`))\n- if (result.error) console.log(red(`Error: ${result.error}`))\n- if (result.details) console.log(red(`\\n${result.details}`))\n- if (result.hint) console.log(yellow(`\\nHint: ${result.hint}`))\n-\n- // Show helpful guidance based on error type\n+ // Build error result\n+ let hint = result.hint\n if (result.error?.includes('Publisher field required')) {\n- console.log()\n- console.log(cyan('Add a \"publisher\" field to your agent templates:'))\n- console.log(yellow(' \"publisher\": \"\"'))\n- console.log()\n- } else if (\n- result.error?.includes('Publisher not found or not accessible')\n- ) {\n- console.log()\n- console.log(\n- cyan(\n- 'Check that the publisher ID is correct and you have access to it.',\n- ),\n- )\n- console.log()\n+ hint = 'Add a \"publisher\" field to your agent templates.'\n+ } else if (result.error?.includes('Publisher not found or not accessible')) {\n+ hint = `Check that the publisher ID is correct and you have access to it. Visit ${WEBSITE_URL}/publishers to manage publishers.`\n }\n \n- console.log(cyan('Visit the website to manage your publishers:'))\n- console.log(yellow(`${WEBSITE_URL}/publishers`))\n+ return {\n+ success: false,\n+ error: result.error,\n+ details: result.details,\n+ hint,\n+ }\n } catch (error) {\n- console.log(\n- red(\n- `Error during publish: ${error instanceof Error ? error.message + '\\n' + error.stack : String(error)}`,\n- ),\n- )\n+ return {\n+ success: false,\n+ error: 'Publish failed',\n+ details: error instanceof Error ? error.message : String(error),\n+ }\n }\n }\n"
},
{
"path": "cli/src/components/agent-checklist.tsx",
@@ -331,7 +331,7 @@
{
"path": "cli/src/components/publish-container.tsx",
"status": "added",
- "diff": "Index: cli/src/components/publish-container.tsx\n===================================================================\n--- cli/src/components/publish-container.tsx\tf639a8e (parent)\n+++ cli/src/components/publish-container.tsx\t10d78f7 (commit)\n@@ -0,0 +1,572 @@\n+import { TextAttributes } from '@opentui/core'\n+import React, { useCallback, useEffect, useMemo, useState } from 'react'\n+import { useShallow } from 'zustand/react/shallow'\n+\n+import { AgentChecklist } from './agent-checklist'\n+import { Button } from './button'\n+import { MultilineInput, type MultilineInputHandle } from './multiline-input'\n+import { PublishConfirmation, getAllPublishAgentIds } from './publish-confirmation'\n+import { Separator } from './separator'\n+import { useTheme } from '../hooks/use-theme'\n+import { useChatStore } from '../state/chat-store'\n+import { usePublishStore } from '../state/publish-store'\n+import { BORDER_CHARS } from '../utils/ui-constants'\n+import { loadLocalAgents, loadAgentDefinitions } from '../utils/local-agent-registry'\n+\n+import type { LocalAgentInfo } from '../utils/local-agent-registry'\n+\n+interface PublishContainerProps {\n+ inputRef: React.MutableRefObject\n+ onExitPublish?: () => void\n+ onPublish: (agentIds: string[]) => Promise\n+ width: number\n+}\n+\n+export const PublishContainer: React.FC = ({\n+ inputRef,\n+ onExitPublish,\n+ onPublish,\n+ width,\n+}) => {\n+ const theme = useTheme()\n+ const [closeButtonHovered, setCloseButtonHovered] = useState(false)\n+ const [nextButtonHovered, setNextButtonHovered] = useState(false)\n+ const [backButtonHovered, setBackButtonHovered] = useState(false)\n+ const [publishButtonHovered, setPublishButtonHovered] = useState(false)\n+\n+ const {\n+ publishMode,\n+ selectedAgentIds,\n+ searchQuery,\n+ currentStep,\n+ focusedIndex,\n+ isPublishing,\n+ successResult,\n+ errorResult,\n+ toggleAgentSelection,\n+ setSearchQuery,\n+ goToConfirmation,\n+ goBackToSelection,\n+ setFocusedIndex,\n+ closePublish,\n+ } = usePublishStore(\n+ useShallow((state) => ({\n+ publishMode: state.publishMode,\n+ selectedAgentIds: state.selectedAgentIds,\n+ searchQuery: state.searchQuery,\n+ currentStep: state.currentStep,\n+ focusedIndex: state.focusedIndex,\n+ isPublishing: state.isPublishing,\n+ successResult: state.successResult,\n+ errorResult: state.errorResult,\n+ toggleAgentSelection: state.toggleAgentSelection,\n+ setSearchQuery: state.setSearchQuery,\n+ goToConfirmation: state.goToConfirmation,\n+ goBackToSelection: state.goBackToSelection,\n+ setFocusedIndex: state.setFocusedIndex,\n+ closePublish: state.closePublish,\n+ })),\n+ )\n+\n+ const inputFocused = useChatStore((state) => state.inputFocused)\n+\n+ // Load agents data\n+ const agents = useMemo(() => loadLocalAgents(), [])\n+ const agentDefinitions = useMemo(() => {\n+ const defs = loadAgentDefinitions()\n+ const map = new Map()\n+ for (const def of defs) {\n+ map.set(def.id, { spawnableAgents: def.spawnableAgents })\n+ }\n+ return map\n+ }, [])\n+\n+ // Filter agents based on search\n+ const filteredAgents = useMemo(() => {\n+ if (!searchQuery.trim()) return agents\n+ const query = searchQuery.toLowerCase()\n+ return agents.filter(\n+ (agent) =>\n+ agent.displayName.toLowerCase().includes(query) ||\n+ agent.id.toLowerCase().includes(query),\n+ )\n+ }, [agents, searchQuery])\n+\n+ // Get selected agents as LocalAgentInfo[]\n+ const selectedAgents = useMemo(() => {\n+ return agents.filter((a) => selectedAgentIds.has(a.id))\n+ }, [agents, selectedAgentIds])\n+\n+ const canProceed = selectedAgentIds.size > 0\n+\n+ // Handle keyboard navigation in checklist\n+ const handleSearchKeyIntercept = useCallback(\n+ (key: { name?: string; shift?: boolean }) => {\n+ if (key.name === 'up') {\n+ setFocusedIndex(Math.max(0, focusedIndex - 1))\n+ return true\n+ }\n+ if (key.name === 'down') {\n+ setFocusedIndex(Math.min(filteredAgents.length - 1, focusedIndex + 1))\n+ return true\n+ }\n+ if (key.name === 'return' || key.name === 'enter') {\n+ // Enter: toggle selection\n+ const agent = filteredAgents[focusedIndex]\n+ if (agent) {\n+ toggleAgentSelection(agent.id)\n+ }\n+ return true\n+ }\n+ if (key.name === 'tab' && !key.shift) {\n+ // Tab: move to next button\n+ if (canProceed) {\n+ goToConfirmation()\n+ }\n+ return true\n+ }\n+ return false\n+ },\n+ [\n+ focusedIndex,\n+ filteredAgents,\n+ canProceed,\n+ setFocusedIndex,\n+ toggleAgentSelection,\n+ goToConfirmation,\n+ ],\n+ )\n+\n+ const handleCancel = useCallback(() => {\n+ closePublish()\n+ onExitPublish?.()\n+ }, [closePublish, onExitPublish])\n+\n+ const handleNext = useCallback(() => {\n+ if (canProceed) {\n+ goToConfirmation()\n+ }\n+ }, [canProceed, goToConfirmation])\n+\n+ const handleBack = useCallback(() => {\n+ goBackToSelection()\n+ }, [goBackToSelection])\n+\n+ const handlePublish = useCallback(async () => {\n+ const allIds = getAllPublishAgentIds(selectedAgents, agents, agentDefinitions)\n+ await onPublish(allIds)\n+ }, [selectedAgents, agents, agentDefinitions, onPublish])\n+\n+ useEffect(() => {\n+ if (publishMode && inputRef.current && currentStep === 'selection') {\n+ inputRef.current.focus()\n+ }\n+ }, [publishMode, inputRef, currentStep])\n+\n+ if (!publishMode) {\n+ return null\n+ }\n+\n+ // Empty state - no agents found\n+ if (agents.length === 0) {\n+ return (\n+ \n+ \n+ No agents found\n+ \n+ \n+ Create agents in the .agents/ directory to publish them.\n+ \n+ \n+ See: https://codebuff.com/docs/agents for guidance.\n+ \n+ \n+ CLOSE \n+ \n+ \n+ )\n+ }\n+\n+ return (\n+ \n+ {/* Header */}\n+ \n+ \n+ \n+ {currentStep === 'selection' && 'Select agents to publish'}\n+ {currentStep === 'confirmation' && 'Confirm publish'}\n+ {currentStep === 'success' && 'Publish complete'}\n+ {currentStep === 'error' && 'Publish failed'}\n+ \n+ \n+ setCloseButtonHovered(true)}\n+ onMouseOut={() => setCloseButtonHovered(false)}\n+ >\n+ \n+ \n+ [x]\n+ \n+ \n+ \n+ \n+\n+ {/* Selection step */}\n+ {currentStep === 'selection' && (\n+ <>\n+ {/* Search input */}\n+ \n+ \n+ setSearchQuery(text)}\n+ onSubmit={handleNext}\n+ onPaste={() => {}}\n+ onKeyIntercept={handleSearchKeyIntercept}\n+ placeholder=\"Type to search agents...\"\n+ focused={inputFocused}\n+ maxHeight={1}\n+ minHeight={1}\n+ ref={inputRef}\n+ cursorPosition={searchQuery.length}\n+ />\n+ \n+ \n+\n+ {/* Agent checklist */}\n+ \n+\n+ {/* Footer with Next button */}\n+ \n+ \n+ \n+ ↑↓ navigate • Enter toggle • Tab next\n+ \n+ setNextButtonHovered(true)}\n+ onMouseOut={() => setNextButtonHovered(false)}\n+ style={{\n+ paddingLeft: 1,\n+ paddingRight: 1,\n+ paddingTop: 0,\n+ paddingBottom: 0,\n+ borderStyle: 'single',\n+ borderColor: canProceed ? theme.foreground : theme.border,\n+ customBorderChars: BORDER_CHARS,\n+ backgroundColor: 'transparent',\n+ }}\n+ >\n+ \n+ \n+ NEXT\n+ \n+ \n+ \n+ \n+ >\n+ )}\n+\n+ {/* Confirmation step */}\n+ {currentStep === 'confirmation' && (\n+ <>\n+ \n+ \n+ \n+ \n+\n+ {/* Footer with Back and Publish buttons */}\n+ \n+ \n+ setBackButtonHovered(true)}\n+ onMouseOut={() => setBackButtonHovered(false)}\n+ style={{\n+ paddingLeft: 1,\n+ paddingRight: 1,\n+ paddingTop: 0,\n+ paddingBottom: 0,\n+ borderStyle: 'single',\n+ borderColor: theme.border,\n+ customBorderChars: BORDER_CHARS,\n+ backgroundColor: 'transparent',\n+ }}\n+ >\n+ \n+ \n+ BACK\n+ \n+ \n+ \n+ setPublishButtonHovered(true)}\n+ onMouseOut={() => setPublishButtonHovered(false)}\n+ style={{\n+ paddingLeft: 1,\n+ paddingRight: 1,\n+ paddingTop: 0,\n+ paddingBottom: 0,\n+ borderStyle: 'single',\n+ borderColor: isPublishing ? theme.border : theme.success,\n+ customBorderChars: BORDER_CHARS,\n+ backgroundColor: 'transparent',\n+ }}\n+ >\n+ \n+ \n+ {isPublishing ? 'PUBLISHING...' : 'PUBLISH'}\n+ \n+ \n+ \n+ \n+ >\n+ )}\n+\n+ {/* Success step */}\n+ {currentStep === 'success' && successResult && (\n+ <>\n+ \n+ \n+ \n+ ✓ \n+ \n+ Successfully published {successResult.agents.length} agent{successResult.agents.length !== 1 ? 's' : ''}!\n+ \n+ \n+\n+ \n+ {successResult.agents.map((agent) => (\n+ \n+ • \n+ \n+ {agent.displayName}\n+ \n+ \n+ ({successResult.publisherId}/{agent.id}@{agent.version})\n+ \n+ \n+ ))}\n+ \n+ \n+\n+ \n+ \n+ setCloseButtonHovered(true)}\n+ onMouseOut={() => setCloseButtonHovered(false)}\n+ style={{\n+ paddingLeft: 1,\n+ paddingRight: 1,\n+ paddingTop: 0,\n+ paddingBottom: 0,\n+ borderStyle: 'single',\n+ borderColor: theme.success,\n+ customBorderChars: BORDER_CHARS,\n+ backgroundColor: 'transparent',\n+ }}\n+ >\n+ \n+ \n+ DONE\n+ \n+ \n+ \n+ \n+ >\n+ )}\n+\n+ {/* Error step */}\n+ {currentStep === 'error' && errorResult && (\n+ <>\n+ \n+ \n+ \n+ ✗ \n+ \n+ Publish failed\n+ \n+ \n+\n+ \n+ {errorResult.error && (\n+ {errorResult.error} \n+ )}\n+ {errorResult.details && (\n+ {errorResult.details} \n+ )}\n+ {errorResult.hint && (\n+ 💡 {errorResult.hint} \n+ )}\n+ \n+ \n+\n+ \n+ \n+ setBackButtonHovered(true)}\n+ onMouseOut={() => setBackButtonHovered(false)}\n+ style={{\n+ paddingLeft: 1,\n+ paddingRight: 1,\n+ paddingTop: 0,\n+ paddingBottom: 0,\n+ borderStyle: 'single',\n+ borderColor: theme.border,\n+ customBorderChars: BORDER_CHARS,\n+ backgroundColor: 'transparent',\n+ }}\n+ >\n+ \n+ \n+ TRY AGAIN\n+ \n+ \n+ \n+ setCloseButtonHovered(true)}\n+ onMouseOut={() => setCloseButtonHovered(false)}\n+ style={{\n+ paddingLeft: 1,\n+ paddingRight: 1,\n+ paddingTop: 0,\n+ paddingBottom: 0,\n+ borderStyle: 'single',\n+ borderColor: theme.border,\n+ customBorderChars: BORDER_CHARS,\n+ backgroundColor: 'transparent',\n+ }}\n+ >\n+ \n+ \n+ CLOSE\n+ \n+ \n+ \n+ \n+ >\n+ )}\n+ \n+ )\n+}\n"
+ "diff": "Index: cli/src/components/publish-container.tsx\n===================================================================\n--- cli/src/components/publish-container.tsx\tf639a8e (parent)\n+++ cli/src/components/publish-container.tsx\t10d78f7 (commit)\n@@ -0,0 +1,572 @@\n+import { TextAttributes } from '@opentui/core'\n+import React, { useCallback, useEffect, useMemo, useState } from 'react'\n+import { useShallow } from 'zustand/react/shallow'\n+\n+import { AgentChecklist } from './agent-checklist'\n+import { Button } from './button'\n+import { MultilineInput, type MultilineInputHandle } from './multiline-input'\n+import { PublishConfirmation, getAllPublishAgentIds } from './publish-confirmation'\n+import { Separator } from './separator'\n+import { useTheme } from '../hooks/use-theme'\n+import { useChatStore } from '../state/chat-store'\n+import { usePublishStore } from '../state/publish-store'\n+import { BORDER_CHARS } from '../utils/ui-constants'\n+import { loadLocalAgents, loadAgentDefinitions } from '../utils/local-agent-registry'\n+\n+import type { LocalAgentInfo } from '../utils/local-agent-registry'\n+\n+interface PublishContainerProps {\n+ inputRef: React.MutableRefObject\n+ onExitPublish?: () => void\n+ onPublish: (agentIds: string[]) => Promise\n+ width: number\n+}\n+\n+export const PublishContainer: React.FC = ({\n+ inputRef,\n+ onExitPublish,\n+ onPublish,\n+ width,\n+}) => {\n+ const theme = useTheme()\n+ const [closeButtonHovered, setCloseButtonHovered] = useState(false)\n+ const [nextButtonHovered, setNextButtonHovered] = useState(false)\n+ const [backButtonHovered, setBackButtonHovered] = useState(false)\n+ const [publishButtonHovered, setPublishButtonHovered] = useState(false)\n+\n+ const {\n+ publishMode,\n+ selectedAgentIds,\n+ searchQuery,\n+ currentStep,\n+ focusedIndex,\n+ isPublishing,\n+ successResult,\n+ errorResult,\n+ toggleAgentSelection,\n+ setSearchQuery,\n+ goToConfirmation,\n+ goBackToSelection,\n+ setFocusedIndex,\n+ closePublish,\n+ } = usePublishStore(\n+ useShallow((state) => ({\n+ publishMode: state.publishMode,\n+ selectedAgentIds: state.selectedAgentIds,\n+ searchQuery: state.searchQuery,\n+ currentStep: state.currentStep,\n+ focusedIndex: state.focusedIndex,\n+ isPublishing: state.isPublishing,\n+ successResult: state.successResult,\n+ errorResult: state.errorResult,\n+ toggleAgentSelection: state.toggleAgentSelection,\n+ setSearchQuery: state.setSearchQuery,\n+ goToConfirmation: state.goToConfirmation,\n+ goBackToSelection: state.goBackToSelection,\n+ setFocusedIndex: state.setFocusedIndex,\n+ closePublish: state.closePublish,\n+ })),\n+ )\n+\n+ const inputFocused = useChatStore((state) => state.inputFocused)\n+\n+ // Load agents data\n+ const agents = useMemo(() => loadLocalAgents(), [])\n+ const agentDefinitions = useMemo(() => {\n+ const defs = loadAgentDefinitions()\n+ const map = new Map()\n+ for (const def of defs) {\n+ map.set(def.id, { spawnableAgents: def.spawnableAgents })\n+ }\n+ return map\n+ }, [])\n+\n+ // Filter agents based on search\n+ const filteredAgents = useMemo(() => {\n+ if (!searchQuery.trim()) return agents\n+ const query = searchQuery.toLowerCase()\n+ return agents.filter(\n+ (agent) =>\n+ agent.displayName.toLowerCase().includes(query) ||\n+ agent.id.toLowerCase().includes(query),\n+ )\n+ }, [agents, searchQuery])\n+\n+ // Get selected agents as LocalAgentInfo[]\n+ const selectedAgents = useMemo(() => {\n+ return agents.filter((a) => selectedAgentIds.has(a.id))\n+ }, [agents, selectedAgentIds])\n+\n+ const canProceed = selectedAgentIds.size > 0\n+\n+ // Handle keyboard navigation in checklist\n+ const handleSearchKeyIntercept = useCallback(\n+ (key: { name?: string; shift?: boolean }) => {\n+ if (key.name === 'up') {\n+ setFocusedIndex(Math.max(0, focusedIndex - 1))\n+ return true\n+ }\n+ if (key.name === 'down') {\n+ setFocusedIndex(Math.min(filteredAgents.length - 1, focusedIndex + 1))\n+ return true\n+ }\n+ if (key.name === 'return' || key.name === 'enter') {\n+ // Enter: toggle selection\n+ const agent = filteredAgents[focusedIndex]\n+ if (agent) {\n+ toggleAgentSelection(agent.id)\n+ }\n+ return true\n+ }\n+ if (key.name === 'tab' && !key.shift) {\n+ // Tab: move to next button\n+ if (canProceed) {\n+ goToConfirmation()\n+ }\n+ return true\n+ }\n+ return false\n+ },\n+ [\n+ focusedIndex,\n+ filteredAgents,\n+ canProceed,\n+ setFocusedIndex,\n+ toggleAgentSelection,\n+ goToConfirmation,\n+ ],\n+ )\n+\n+ const handleCancel = useCallback(() => {\n+ closePublish()\n+ onExitPublish?.()\n+ }, [closePublish, onExitPublish])\n+\n+ const handleNext = useCallback(() => {\n+ if (canProceed) {\n+ goToConfirmation()\n+ }\n+ }, [canProceed, goToConfirmation])\n+\n+ const handleBack = useCallback(() => {\n+ goBackToSelection()\n+ }, [goBackToSelection])\n+\n+ const handlePublish = useCallback(async () => {\n+ const allIds = getAllPublishAgentIds(selectedAgents, agents, agentDefinitions)\n+ await onPublish(allIds)\n+ }, [selectedAgents, agents, agentDefinitions, onPublish])\n+\n+ useEffect(() => {\n+ if (publishMode && inputRef.current && currentStep === 'selection') {\n+ inputRef.current.focus()\n+ }\n+ }, [publishMode, inputRef, currentStep])\n+\n+ if (!publishMode) {\n+ return null\n+ }\n+\n+ // Empty state - no agents found\n+ if (agents.length === 0) {\n+ return (\n+ \n+ \n+ No agents found\n+ \n+ \n+ Create agents in the .agents/ directory to publish them.\n+ \n+ \n+ See: https://codebirds.com/docs/agents for guidance.\n+ \n+ \n+ CLOSE \n+ \n+ \n+ )\n+ }\n+\n+ return (\n+ \n+ {/* Header */}\n+ \n+ \n+ \n+ {currentStep === 'selection' && 'Select agents to publish'}\n+ {currentStep === 'confirmation' && 'Confirm publish'}\n+ {currentStep === 'success' && 'Publish complete'}\n+ {currentStep === 'error' && 'Publish failed'}\n+ \n+ \n+ setCloseButtonHovered(true)}\n+ onMouseOut={() => setCloseButtonHovered(false)}\n+ >\n+ \n+ \n+ [x]\n+ \n+ \n+ \n+ \n+\n+ {/* Selection step */}\n+ {currentStep === 'selection' && (\n+ <>\n+ {/* Search input */}\n+ \n+ \n+ setSearchQuery(text)}\n+ onSubmit={handleNext}\n+ onPaste={() => {}}\n+ onKeyIntercept={handleSearchKeyIntercept}\n+ placeholder=\"Type to search agents...\"\n+ focused={inputFocused}\n+ maxHeight={1}\n+ minHeight={1}\n+ ref={inputRef}\n+ cursorPosition={searchQuery.length}\n+ />\n+ \n+ \n+\n+ {/* Agent checklist */}\n+ \n+\n+ {/* Footer with Next button */}\n+ \n+ \n+ \n+ ↑↓ navigate • Enter toggle • Tab next\n+ \n+ setNextButtonHovered(true)}\n+ onMouseOut={() => setNextButtonHovered(false)}\n+ style={{\n+ paddingLeft: 1,\n+ paddingRight: 1,\n+ paddingTop: 0,\n+ paddingBottom: 0,\n+ borderStyle: 'single',\n+ borderColor: canProceed ? theme.foreground : theme.border,\n+ customBorderChars: BORDER_CHARS,\n+ backgroundColor: 'transparent',\n+ }}\n+ >\n+ \n+ \n+ NEXT\n+ \n+ \n+ \n+ \n+ >\n+ )}\n+\n+ {/* Confirmation step */}\n+ {currentStep === 'confirmation' && (\n+ <>\n+ \n+ \n+ \n+ \n+\n+ {/* Footer with Back and Publish buttons */}\n+ \n+ \n+ setBackButtonHovered(true)}\n+ onMouseOut={() => setBackButtonHovered(false)}\n+ style={{\n+ paddingLeft: 1,\n+ paddingRight: 1,\n+ paddingTop: 0,\n+ paddingBottom: 0,\n+ borderStyle: 'single',\n+ borderColor: theme.border,\n+ customBorderChars: BORDER_CHARS,\n+ backgroundColor: 'transparent',\n+ }}\n+ >\n+ \n+ \n+ BACK\n+ \n+ \n+ \n+ setPublishButtonHovered(true)}\n+ onMouseOut={() => setPublishButtonHovered(false)}\n+ style={{\n+ paddingLeft: 1,\n+ paddingRight: 1,\n+ paddingTop: 0,\n+ paddingBottom: 0,\n+ borderStyle: 'single',\n+ borderColor: isPublishing ? theme.border : theme.success,\n+ customBorderChars: BORDER_CHARS,\n+ backgroundColor: 'transparent',\n+ }}\n+ >\n+ \n+ \n+ {isPublishing ? 'PUBLISHING...' : 'PUBLISH'}\n+ \n+ \n+ \n+ \n+ >\n+ )}\n+\n+ {/* Success step */}\n+ {currentStep === 'success' && successResult && (\n+ <>\n+ \n+ \n+ \n+ ✓ \n+ \n+ Successfully published {successResult.agents.length} agent{successResult.agents.length !== 1 ? 's' : ''}!\n+ \n+ \n+\n+ \n+ {successResult.agents.map((agent) => (\n+ \n+ • \n+ \n+ {agent.displayName}\n+ \n+ \n+ ({successResult.publisherId}/{agent.id}@{agent.version})\n+ \n+ \n+ ))}\n+ \n+ \n+\n+ \n+ \n+ setCloseButtonHovered(true)}\n+ onMouseOut={() => setCloseButtonHovered(false)}\n+ style={{\n+ paddingLeft: 1,\n+ paddingRight: 1,\n+ paddingTop: 0,\n+ paddingBottom: 0,\n+ borderStyle: 'single',\n+ borderColor: theme.success,\n+ customBorderChars: BORDER_CHARS,\n+ backgroundColor: 'transparent',\n+ }}\n+ >\n+ \n+ \n+ DONE\n+ \n+ \n+ \n+ \n+ >\n+ )}\n+\n+ {/* Error step */}\n+ {currentStep === 'error' && errorResult && (\n+ <>\n+ \n+ \n+ \n+ ✗ \n+ \n+ Publish failed\n+ \n+ \n+\n+ \n+ {errorResult.error && (\n+ {errorResult.error} \n+ )}\n+ {errorResult.details && (\n+ {errorResult.details} \n+ )}\n+ {errorResult.hint && (\n+ 💡 {errorResult.hint} \n+ )}\n+ \n+ \n+\n+ \n+ \n+ setBackButtonHovered(true)}\n+ onMouseOut={() => setBackButtonHovered(false)}\n+ style={{\n+ paddingLeft: 1,\n+ paddingRight: 1,\n+ paddingTop: 0,\n+ paddingBottom: 0,\n+ borderStyle: 'single',\n+ borderColor: theme.border,\n+ customBorderChars: BORDER_CHARS,\n+ backgroundColor: 'transparent',\n+ }}\n+ >\n+ \n+ \n+ TRY AGAIN\n+ \n+ \n+ \n+ setCloseButtonHovered(true)}\n+ onMouseOut={() => setCloseButtonHovered(false)}\n+ style={{\n+ paddingLeft: 1,\n+ paddingRight: 1,\n+ paddingTop: 0,\n+ paddingBottom: 0,\n+ borderStyle: 'single',\n+ borderColor: theme.border,\n+ customBorderChars: BORDER_CHARS,\n+ backgroundColor: 'transparent',\n+ }}\n+ >\n+ \n+ \n+ CLOSE\n+ \n+ \n+ \n+ \n+ >\n+ )}\n+ \n+ )\n+}\n"
},
{
"path": "cli/src/data/slash-commands.ts",
@@ -391,7 +391,7 @@
{
"path": "cli/src/index.tsx",
"status": "modified",
- "diff": "Index: cli/src/index.tsx\n===================================================================\n--- cli/src/index.tsx\t9eb7385 (parent)\n+++ cli/src/index.tsx\t896e0a8 (commit)\n@@ -21,20 +21,16 @@\n import { getUserCredentials } from './utils/auth'\n import { loadAgentDefinitions } from './utils/load-agent-definitions'\n import { getLoadedAgentsData } from './utils/local-agent-registry'\n import { clearLogFile, logger } from './utils/logger'\n+import { detectTerminalTheme } from './utils/terminal-color-detection'\n+import { setOscDetectedTheme } from './utils/theme-system'\n import { filterNetworkErrors } from './utils/validation-error-helpers'\n \n import type { FileTreeNode } from '@codebuff/common/util/file'\n \n const require = createRequire(import.meta.url)\n \n-const INTERNAL_OSC_FLAG = '--internal-osc-detect'\n-\n-function isOscDetectionRun(): boolean {\n- return process.argv.includes(INTERNAL_OSC_FLAG)\n-}\n-\n function loadPackageVersion(): string {\n if (process.env.CODEBUFF_CLI_VERSION) {\n return process.env.CODEBUFF_CLI_VERSION\n }\n@@ -130,8 +126,22 @@\n }\n }\n \n async function main(): Promise {\n+ // Run OSC theme detection BEFORE anything else.\n+ // This MUST happen before OpenTUI starts because OSC responses come through stdin,\n+ // and OpenTUI also listens to stdin. Running detection here ensures stdin is clean.\n+ if (process.stdin.isTTY && process.platform !== 'win32') {\n+ try {\n+ const oscTheme = await detectTerminalTheme()\n+ if (oscTheme) {\n+ setOscDetectedTheme(oscTheme)\n+ }\n+ } catch {\n+ // Silently ignore OSC detection failures\n+ }\n+ }\n+\n const {\n initialPrompt,\n agent,\n clearLogs,\n@@ -139,9 +149,9 @@\n continueId,\n cwd,\n } = parseArgs()\n \n- await initializeApp({ cwd, isOscDetectionRun: isOscDetectionRun() })\n+ await initializeApp({ cwd })\n \n // Handle publish command before rendering the app\n if (process.argv.includes('publish')) {\n const publishIndex = process.argv.indexOf('publish')\n"
+ "diff": "Index: cli/src/index.tsx\n===================================================================\n--- cli/src/index.tsx\t9eb7385 (parent)\n+++ cli/src/index.tsx\t896e0a8 (commit)\n@@ -21,20 +21,16 @@\n import { getUserCredentials } from './utils/auth'\n import { loadAgentDefinitions } from './utils/load-agent-definitions'\n import { getLoadedAgentsData } from './utils/local-agent-registry'\n import { clearLogFile, logger } from './utils/logger'\n+import { detectTerminalTheme } from './utils/terminal-color-detection'\n+import { setOscDetectedTheme } from './utils/theme-system'\n import { filterNetworkErrors } from './utils/validation-error-helpers'\n \n import type { FileTreeNode } from '@codebirds/common/util/file'\n \n const require = createRequire(import.meta.url)\n \n-const INTERNAL_OSC_FLAG = '--internal-osc-detect'\n-\n-function isOscDetectionRun(): boolean {\n- return process.argv.includes(INTERNAL_OSC_FLAG)\n-}\n-\n function loadPackageVersion(): string {\n if (process.env.CODEBIRDS_CLI_VERSION) {\n return process.env.CODEBIRDS_CLI_VERSION\n }\n@@ -130,8 +126,22 @@\n }\n }\n \n async function main(): Promise {\n+ // Run OSC theme detection BEFORE anything else.\n+ // This MUST happen before OpenTUI starts because OSC responses come through stdin,\n+ // and OpenTUI also listens to stdin. Running detection here ensures stdin is clean.\n+ if (process.stdin.isTTY && process.platform !== 'win32') {\n+ try {\n+ const oscTheme = await detectTerminalTheme()\n+ if (oscTheme) {\n+ setOscDetectedTheme(oscTheme)\n+ }\n+ } catch {\n+ // Silently ignore OSC detection failures\n+ }\n+ }\n+\n const {\n initialPrompt,\n agent,\n clearLogs,\n@@ -139,9 +149,9 @@\n continueId,\n cwd,\n } = parseArgs()\n \n- await initializeApp({ cwd, isOscDetectionRun: isOscDetectionRun() })\n+ await initializeApp({ cwd })\n \n // Handle publish command before rendering the app\n if (process.argv.includes('publish')) {\n const publishIndex = process.argv.indexOf('publish')\n"
},
{
"path": "cli/src/init/init-app.ts",
@@ -401,7 +401,7 @@
{
"path": "cli/src/init/osc-subprocess.ts",
"status": "deleted",
- "diff": "Index: cli/src/init/osc-subprocess.ts\n===================================================================\n--- cli/src/init/osc-subprocess.ts\t9eb7385 (parent)\n+++ cli/src/init/osc-subprocess.ts\t896e0a8 (commit)\n@@ -1,49 +0,0 @@\n-import {\n- detectTerminalTheme,\n- terminalSupportsOSC,\n- withTimeout,\n-} from '../utils/terminal-color-detection'\n-\n-const SUBPROCESS_TIMEOUT_MS = 5000 // Maximum time for subprocess detection\n-\n-/**\n- * Output the theme result and exit\n- * Ensures consistent output format and proper flushing\n- */\n-function outputAndExit(theme: 'dark' | 'light' | null): void {\n- console.log(JSON.stringify({ theme }))\n- // Use setImmediate to ensure output is flushed before exit\n- setImmediate(() => process.exit(0))\n-}\n-\n-export async function runOscDetectionSubprocess(): Promise {\n- // Set env vars to keep subprocess quiet\n- process.env.__INTERNAL_OSC_DETECT = '1'\n- process.env.CODEBUFF_GITHUB_ACTIONS = 'true'\n-\n- // Check OSC support first\n- if (!terminalSupportsOSC()) {\n- outputAndExit(null)\n- return\n- }\n-\n- // Set up a hard timeout to ensure the subprocess exits\n- const hardTimeoutId = setTimeout(() => {\n- outputAndExit(null)\n- }, SUBPROCESS_TIMEOUT_MS)\n-\n- try {\n- // detectTerminalTheme already has its own timeout, but we add another layer\n- const theme = await withTimeout(\n- detectTerminalTheme(),\n- SUBPROCESS_TIMEOUT_MS - 1000, // Leave 1s buffer for cleanup\n- null,\n- )\n-\n- clearTimeout(hardTimeoutId)\n- outputAndExit(theme)\n- } catch {\n- clearTimeout(hardTimeoutId)\n- outputAndExit(null)\n- }\n-}\n"
+ "diff": "Index: cli/src/init/osc-subprocess.ts\n===================================================================\n--- cli/src/init/osc-subprocess.ts\t9eb7385 (parent)\n+++ cli/src/init/osc-subprocess.ts\t896e0a8 (commit)\n@@ -1,49 +0,0 @@\n-import {\n- detectTerminalTheme,\n- terminalSupportsOSC,\n- withTimeout,\n-} from '../utils/terminal-color-detection'\n-\n-const SUBPROCESS_TIMEOUT_MS = 5000 // Maximum time for subprocess detection\n-\n-/**\n- * Output the theme result and exit\n- * Ensures consistent output format and proper flushing\n- */\n-function outputAndExit(theme: 'dark' | 'light' | null): void {\n- console.log(JSON.stringify({ theme }))\n- // Use setImmediate to ensure output is flushed before exit\n- setImmediate(() => process.exit(0))\n-}\n-\n-export async function runOscDetectionSubprocess(): Promise {\n- // Set env vars to keep subprocess quiet\n- process.env.__INTERNAL_OSC_DETECT = '1'\n- process.env.CODEBIRDS_GITHUB_ACTIONS = 'true'\n-\n- // Check OSC support first\n- if (!terminalSupportsOSC()) {\n- outputAndExit(null)\n- return\n- }\n-\n- // Set up a hard timeout to ensure the subprocess exits\n- const hardTimeoutId = setTimeout(() => {\n- outputAndExit(null)\n- }, SUBPROCESS_TIMEOUT_MS)\n-\n- try {\n- // detectTerminalTheme already has its own timeout, but we add another layer\n- const theme = await withTimeout(\n- detectTerminalTheme(),\n- SUBPROCESS_TIMEOUT_MS - 1000, // Leave 1s buffer for cleanup\n- null,\n- )\n-\n- clearTimeout(hardTimeoutId)\n- outputAndExit(theme)\n- } catch {\n- clearTimeout(hardTimeoutId)\n- outputAndExit(null)\n- }\n-}\n"
},
{
"path": "cli/src/utils/terminal-color-detection.ts",
@@ -424,7 +424,7 @@
"supplementalFiles": [
".agents/types/agent-definition.ts",
".agents/types/util-types.ts",
- "common/src/types/messages/codebuff-message.ts",
+ "common/src/types/messages/codebirds-message.ts",
"common/src/types/messages/content-part.ts",
"common/src/tools/list.ts",
"packages/agent-runtime/src/run-agent-step.ts",
@@ -466,7 +466,7 @@
{
"path": ".agents/context-pruner.ts",
"status": "modified",
- "diff": "Index: .agents/context-pruner.ts\n===================================================================\n--- .agents/context-pruner.ts\t89a023f (parent)\n+++ .agents/context-pruner.ts\tad7baf6 (commit)\n@@ -50,8 +50,9 @@\n return\n }\n \n // PASS 1: Remove terminal command results (oldest first, preserve recent 5)\n+ // Only prune the tool result content, keeping the tool-call/tool-result pairs intact\n let numKeptTerminalCommands = 0\n const afterTerminalPass: Message[] = []\n \n for (let i = currentMessages.length - 1; i >= 0; i--) {\n@@ -68,9 +69,9 @@\n if (numKeptTerminalCommands < numTerminalCommandsToKeep) {\n numKeptTerminalCommands++\n afterTerminalPass.unshift(message)\n } else {\n- // Simplify terminal command result by replacing output\n+ // Simplify terminal command result by replacing output content only\n const simplifiedMessage: CodebuffToolMessage<'run_terminal_command'> =\n {\n ...toolMessage,\n content: [\n@@ -103,14 +104,15 @@\n return\n }\n \n // PASS 2: Remove large tool results (any tool result output > 1000 chars when stringified)\n+ // Only prune the tool result content, keeping the tool-call/tool-result pairs intact\n const afterToolResultsPass = afterTerminalPass.map((message) => {\n if (message.role === 'tool') {\n const outputSize = JSON.stringify(message.content).length\n \n if (outputSize > 1000) {\n- // Replace with simplified output\n+ // Replace tool result content with simplified output\n const simplifiedMessage: ToolMessage = {\n ...message,\n content: [\n {\n@@ -140,9 +142,10 @@\n } satisfies ToolCall<'set_messages'>\n return\n }\n \n- // PASS 3: Message-level pruning (like trimMessagesToFitTokenLimit)\n+ // PASS 3: Message-level pruning\n+ // Must keep tool-call and tool-result pairs together for Anthropic API compliance\n const shortenedMessageTokenFactor = 0.5\n const replacementMessage: Message = {\n role: 'user',\n content: [\n@@ -163,8 +166,34 @@\n }\n }\n const keepLastIndices = Object.values(keepLastTags)\n \n+ // Build a map of toolCallId -> indices that must be kept together\n+ const toolCallPairs: Map = new Map()\n+ for (const [i, message] of afterToolResultsPass.entries()) {\n+ if (message.role === 'assistant' && Array.isArray(message.content)) {\n+ for (const part of message.content) {\n+ if (part.type === 'tool-call' && part.toolCallId) {\n+ const existing = toolCallPairs.get(part.toolCallId) || []\n+ existing.push(i)\n+ toolCallPairs.set(part.toolCallId, existing)\n+ }\n+ }\n+ } else if (message.role === 'tool' && message.toolCallId) {\n+ const existing = toolCallPairs.get(message.toolCallId) || []\n+ existing.push(i)\n+ toolCallPairs.set(message.toolCallId, existing)\n+ }\n+ }\n+\n+ // Get all indices that are part of tool call pairs\n+ const pairedIndices = new Set()\n+ for (const indices of toolCallPairs.values()) {\n+ for (const idx of indices) {\n+ pairedIndices.add(idx)\n+ }\n+ }\n+\n const requiredTokens = countTokensJson(\n afterToolResultsPass.filter((m: any) => m.keepDuringTruncation),\n )\n let removedTokens = 0\n@@ -172,29 +201,45 @@\n (maxMessageTokens - requiredTokens) * (1 - shortenedMessageTokenFactor)\n \n const placeholder = 'deleted'\n const filteredMessages: any[] = []\n+ const indicesToRemove = new Set()\n \n+ // First pass: identify which messages to remove (excluding tool call pairs)\n+ let lastWasRemoval = false\n for (const [i, message] of afterToolResultsPass.entries()) {\n if (\n removedTokens >= tokensToRemove ||\n message.keepDuringTruncation ||\n- keepLastIndices.includes(i)\n+ keepLastIndices.includes(i) ||\n+ pairedIndices.has(i) // Never remove tool-call/tool-result pairs\n ) {\n- filteredMessages.push(message)\n+ lastWasRemoval = false\n continue\n }\n-\n+ indicesToRemove.add(i)\n removedTokens += countTokensJson(message)\n- if (\n- filteredMessages.length === 0 ||\n- filteredMessages[filteredMessages.length - 1] !== placeholder\n- ) {\n- filteredMessages.push(placeholder)\n+ // Account for placeholder token cost when starting a new removal sequence\n+ if (!lastWasRemoval) {\n removedTokens -= countTokensJson(replacementMessage)\n }\n+ lastWasRemoval = true\n }\n \n+ // Second pass: build filtered messages with placeholders\n+ for (const [i, message] of afterToolResultsPass.entries()) {\n+ if (indicesToRemove.has(i)) {\n+ if (\n+ filteredMessages.length === 0 ||\n+ filteredMessages[filteredMessages.length - 1] !== placeholder\n+ ) {\n+ filteredMessages.push(placeholder)\n+ }\n+ } else {\n+ filteredMessages.push(message)\n+ }\n+ }\n+\n const finalMessages = filteredMessages.map((m) =>\n m === placeholder ? replacementMessage : m,\n )\n \n"
+ "diff": "Index: .agents/context-pruner.ts\n===================================================================\n--- .agents/context-pruner.ts\t89a023f (parent)\n+++ .agents/context-pruner.ts\tad7baf6 (commit)\n@@ -50,8 +50,9 @@\n return\n }\n \n // PASS 1: Remove terminal command results (oldest first, preserve recent 5)\n+ // Only prune the tool result content, keeping the tool-call/tool-result pairs intact\n let numKeptTerminalCommands = 0\n const afterTerminalPass: Message[] = []\n \n for (let i = currentMessages.length - 1; i >= 0; i--) {\n@@ -68,9 +69,9 @@\n if (numKeptTerminalCommands < numTerminalCommandsToKeep) {\n numKeptTerminalCommands++\n afterTerminalPass.unshift(message)\n } else {\n- // Simplify terminal command result by replacing output\n+ // Simplify terminal command result by replacing output content only\n const simplifiedMessage: CodebirdsToolMessage<'run_terminal_command'> =\n {\n ...toolMessage,\n content: [\n@@ -103,14 +104,15 @@\n return\n }\n \n // PASS 2: Remove large tool results (any tool result output > 1000 chars when stringified)\n+ // Only prune the tool result content, keeping the tool-call/tool-result pairs intact\n const afterToolResultsPass = afterTerminalPass.map((message) => {\n if (message.role === 'tool') {\n const outputSize = JSON.stringify(message.content).length\n \n if (outputSize > 1000) {\n- // Replace with simplified output\n+ // Replace tool result content with simplified output\n const simplifiedMessage: ToolMessage = {\n ...message,\n content: [\n {\n@@ -140,9 +142,10 @@\n } satisfies ToolCall<'set_messages'>\n return\n }\n \n- // PASS 3: Message-level pruning (like trimMessagesToFitTokenLimit)\n+ // PASS 3: Message-level pruning\n+ // Must keep tool-call and tool-result pairs together for Anthropic API compliance\n const shortenedMessageTokenFactor = 0.5\n const replacementMessage: Message = {\n role: 'user',\n content: [\n@@ -163,8 +166,34 @@\n }\n }\n const keepLastIndices = Object.values(keepLastTags)\n \n+ // Build a map of toolCallId -> indices that must be kept together\n+ const toolCallPairs: Map = new Map()\n+ for (const [i, message] of afterToolResultsPass.entries()) {\n+ if (message.role === 'assistant' && Array.isArray(message.content)) {\n+ for (const part of message.content) {\n+ if (part.type === 'tool-call' && part.toolCallId) {\n+ const existing = toolCallPairs.get(part.toolCallId) || []\n+ existing.push(i)\n+ toolCallPairs.set(part.toolCallId, existing)\n+ }\n+ }\n+ } else if (message.role === 'tool' && message.toolCallId) {\n+ const existing = toolCallPairs.get(message.toolCallId) || []\n+ existing.push(i)\n+ toolCallPairs.set(message.toolCallId, existing)\n+ }\n+ }\n+\n+ // Get all indices that are part of tool call pairs\n+ const pairedIndices = new Set()\n+ for (const indices of toolCallPairs.values()) {\n+ for (const idx of indices) {\n+ pairedIndices.add(idx)\n+ }\n+ }\n+\n const requiredTokens = countTokensJson(\n afterToolResultsPass.filter((m: any) => m.keepDuringTruncation),\n )\n let removedTokens = 0\n@@ -172,29 +201,45 @@\n (maxMessageTokens - requiredTokens) * (1 - shortenedMessageTokenFactor)\n \n const placeholder = 'deleted'\n const filteredMessages: any[] = []\n+ const indicesToRemove = new Set()\n \n+ // First pass: identify which messages to remove (excluding tool call pairs)\n+ let lastWasRemoval = false\n for (const [i, message] of afterToolResultsPass.entries()) {\n if (\n removedTokens >= tokensToRemove ||\n message.keepDuringTruncation ||\n- keepLastIndices.includes(i)\n+ keepLastIndices.includes(i) ||\n+ pairedIndices.has(i) // Never remove tool-call/tool-result pairs\n ) {\n- filteredMessages.push(message)\n+ lastWasRemoval = false\n continue\n }\n-\n+ indicesToRemove.add(i)\n removedTokens += countTokensJson(message)\n- if (\n- filteredMessages.length === 0 ||\n- filteredMessages[filteredMessages.length - 1] !== placeholder\n- ) {\n- filteredMessages.push(placeholder)\n+ // Account for placeholder token cost when starting a new removal sequence\n+ if (!lastWasRemoval) {\n removedTokens -= countTokensJson(replacementMessage)\n }\n+ lastWasRemoval = true\n }\n \n+ // Second pass: build filtered messages with placeholders\n+ for (const [i, message] of afterToolResultsPass.entries()) {\n+ if (indicesToRemove.has(i)) {\n+ if (\n+ filteredMessages.length === 0 ||\n+ filteredMessages[filteredMessages.length - 1] !== placeholder\n+ ) {\n+ filteredMessages.push(placeholder)\n+ }\n+ } else {\n+ filteredMessages.push(message)\n+ }\n+ }\n+\n const finalMessages = filteredMessages.map((m) =>\n m === placeholder ? replacementMessage : m,\n )\n \n"
}
]
},
@@ -564,7 +564,7 @@
{
"path": "cli/src/commands/router.ts",
"status": "modified",
- "diff": "Index: cli/src/commands/router.ts\n===================================================================\n--- cli/src/commands/router.ts\te9ce02a (parent)\n+++ cli/src/commands/router.ts\t844a6ca (commit)\n@@ -18,183 +18,236 @@\n \n import type { ToolMessage } from '@codebuff/common/types/messages/codebuff-message'\n import type { ToolResultOutput } from '@codebuff/common/types/messages/content-part'\n import type { ContentBlock } from '../types/chat'\n+import type { PendingBashMessage } from '../state/chat-store'\n \n /**\n- * Execute a bash command and add it directly to chat history.\n- * Shows immediate placeholder while running, then updates with output.\n+ * Create a tool result output structure for terminal command results.\n */\n+function createToolResultOutput(params: {\n+ command: string\n+ cwd: string\n+ stdout: string | null\n+ stderr: string | null\n+ exitCode: number\n+ errorMessage?: string\n+}): ToolResultOutput[] {\n+ const { command, cwd, stdout, stderr, exitCode, errorMessage } = params\n+ if (errorMessage) {\n+ return [\n+ {\n+ type: 'json' as const,\n+ value: { command, startingCwd: cwd, errorMessage },\n+ },\n+ ]\n+ }\n+ return [\n+ {\n+ type: 'json' as const,\n+ value: {\n+ command,\n+ startingCwd: cwd,\n+ stdout: stdout || null,\n+ stderr: stderr || null,\n+ exitCode,\n+ },\n+ },\n+ ]\n+}\n+\n+/**\n+ * Execute a bash command.\n+ * When ghost=false: adds directly to chat history with placeholder output that updates.\n+ * When ghost=true: adds to pending messages that appear as ghost while running.\n+ */\n function executeBashCommand(\n command: string,\n- setMessages: RouterParams['setMessages'],\n+ options:\n+ | { ghost: false; setMessages: RouterParams['setMessages'] }\n+ | {\n+ ghost: true\n+ addPendingBashMessage: (msg: PendingBashMessage) => void\n+ updatePendingBashMessage: (\n+ id: string,\n+ updates: Partial,\n+ ) => void\n+ },\n ) {\n- const toolCallId = crypto.randomUUID()\n- const resultBlock: ContentBlock = {\n- type: 'tool',\n- toolName: 'run_terminal_command',\n- toolCallId,\n- input: { command },\n- output: '...',\n- }\n-\n+ const id = crypto.randomUUID()\n const commandCwd = process.cwd()\n \n- // Add the command result to chat as a user message so the AI sees it as context\n- setMessages((prev) => [\n- ...prev,\n- {\n- ...getUserMessage([resultBlock]),\n- metadata: { bashCwd: commandCwd },\n- },\n- ])\n+ if (options.ghost) {\n+ // Ghost mode: add to pending messages\n+ options.addPendingBashMessage({\n+ id,\n+ command,\n+ stdout: '',\n+ stderr: '',\n+ exitCode: 0,\n+ isRunning: true,\n+ startTime: Date.now(),\n+ cwd: commandCwd,\n+ })\n+ } else {\n+ // Direct mode: add to chat history with placeholder\n+ const resultBlock: ContentBlock = {\n+ type: 'tool',\n+ toolName: 'run_terminal_command',\n+ toolCallId: id,\n+ input: { command },\n+ output: '...',\n+ }\n+ options.setMessages((prev) => [\n+ ...prev,\n+ {\n+ ...getUserMessage([resultBlock]),\n+ metadata: { bashCwd: commandCwd },\n+ },\n+ ])\n+ }\n \n- // Execute the command and update the output when complete\n runTerminalCommand({\n command,\n process_type: 'SYNC',\n cwd: commandCwd,\n timeout_seconds: -1,\n env: process.env,\n- }).then(([{ value }]) => {\n- const stdout = 'stdout' in value ? (value.stdout || '') : ''\n- const stderr = 'stderr' in value ? (value.stderr || '') : ''\n- const exitCode = 'exitCode' in value ? value.exitCode : 0\n+ })\n+ .then(([{ value }]) => {\n+ const stdout = 'stdout' in value ? value.stdout || '' : ''\n+ const stderr = 'stderr' in value ? value.stderr || '' : ''\n+ const exitCode = 'exitCode' in value ? value.exitCode ?? 0 : 0\n+ const rawOutput = stdout + stderr\n+ const output = rawOutput || '(no output)'\n \n- // Create tool result output for display\n- const toolResultOutput = [{\n- type: 'json' as const,\n- value: {\n- command,\n- startingCwd: commandCwd,\n- stdout: stdout || null,\n- stderr: stderr || null,\n- exitCode: exitCode ?? 0,\n- }\n- }]\n+ if (options.ghost) {\n+ options.updatePendingBashMessage(id, {\n+ stdout,\n+ stderr,\n+ exitCode,\n+ isRunning: false,\n+ })\n+ } else {\n+ const toolResultOutput = createToolResultOutput({\n+ command,\n+ cwd: commandCwd,\n+ stdout: stdout || null,\n+ stderr: stderr || null,\n+ exitCode,\n+ })\n+ const outputJson = JSON.stringify(toolResultOutput)\n \n- // Store output in JSON format for display\n- const outputJson = JSON.stringify(toolResultOutput)\n+ options.setMessages((prev) =>\n+ prev.map((msg) => {\n+ if (!msg.blocks) return msg\n+ return {\n+ ...msg,\n+ blocks: msg.blocks.map((block) =>\n+ 'toolCallId' in block && block.toolCallId === id\n+ ? { ...block, output: outputJson }\n+ : block,\n+ ),\n+ }\n+ }),\n+ )\n \n- setMessages((prev) => {\n- return prev.map((msg) => {\n- if (!msg.blocks) {\n- return msg\n+ // Add to pending tool results so AI can see this in the next run\n+ const toolMessage: ToolMessage = {\n+ role: 'tool',\n+ toolCallId: id,\n+ toolName: 'run_terminal_command',\n+ content: toolResultOutput,\n }\n- return {\n- ...msg,\n- blocks: msg.blocks.map((block) =>\n- 'toolCallId' in block && block.toolCallId === toolCallId\n- ? {\n- ...block,\n- output: outputJson,\n- }\n- : block,\n- ),\n- }\n- })\n+ useChatStore.getState().addPendingToolResult(toolMessage)\n+ }\n })\n+ .catch((error) => {\n+ const errorMessage =\n+ error instanceof Error ? error.message : String(error)\n+ const output = `Error: ${errorMessage}`\n \n- // Add to pending tool results so AI can see this in the next run\n- const toolMessage: ToolMessage = {\n- role: 'tool',\n- toolCallId,\n- toolName: 'run_terminal_command',\n- content: toolResultOutput,\n- }\n- useChatStore.getState().addPendingToolResult(toolMessage)\n- }).catch((error) => {\n- const errorMessage = error instanceof Error ? error.message : String(error)\n- \n- // Create error tool result output\n- const errorToolResultOutput = [{\n- type: 'json' as const,\n- value: {\n- command,\n- startingCwd: commandCwd,\n- errorMessage,\n- }\n- }]\n+ if (options.ghost) {\n+ options.updatePendingBashMessage(id, {\n+ stdout: '',\n+ stderr: errorMessage,\n+ exitCode: 1,\n+ isRunning: false,\n+ })\n+ } else {\n+ const errorToolResultOutput = createToolResultOutput({\n+ command,\n+ cwd: commandCwd,\n+ stdout: null,\n+ stderr: null,\n+ exitCode: 1,\n+ errorMessage,\n+ })\n+ const errorOutputJson = JSON.stringify(errorToolResultOutput)\n \n- // Store error output in JSON format for display\n- const errorOutputJson = JSON.stringify(errorToolResultOutput)\n+ options.setMessages((prev) =>\n+ prev.map((msg) => {\n+ if (!msg.blocks) return msg\n+ return {\n+ ...msg,\n+ blocks: msg.blocks.map((block) =>\n+ 'toolCallId' in block && block.toolCallId === id\n+ ? { ...block, output: errorOutputJson }\n+ : block,\n+ ),\n+ }\n+ }),\n+ )\n \n- setMessages((prev) => {\n- return prev.map((msg) => {\n- if (!msg.blocks) {\n- return msg\n+ const errorToolMessage: ToolMessage = {\n+ role: 'tool',\n+ toolCallId: id,\n+ toolName: 'run_terminal_command',\n+ content: errorToolResultOutput,\n }\n- return {\n- ...msg,\n- blocks: msg.blocks.map((block) =>\n- 'toolCallId' in block && block.toolCallId === toolCallId\n- ? {\n- ...block,\n- output: errorOutputJson,\n- }\n- : block,\n- ),\n- }\n- })\n+ useChatStore.getState().addPendingToolResult(errorToolMessage)\n+ }\n })\n-\n- // Add error result to pending tool results so AI can see this in the next run\n- const errorToolMessage: ToolMessage = {\n- role: 'tool',\n- toolCallId,\n- toolName: 'run_terminal_command',\n- content: errorToolResultOutput,\n- }\n- useChatStore.getState().addPendingToolResult(errorToolMessage)\n- })\n }\n \n /**\n- * Add a bash command result to the chat message history.\n+ * Add a completed bash command result to the chat message history.\n * Also adds to pendingToolResults so the AI can see it in the next run.\n */\n export function addBashMessageToHistory(params: {\n command: string\n stdout: string\n- stderr: string | null | undefined\n+ stderr: string | null\n exitCode: number\n cwd: string\n- displayOutput?: string\n setMessages: RouterParams['setMessages']\n }) {\n- const { command, stdout, stderr, exitCode, cwd, displayOutput, setMessages } =\n- params\n- const outputText =\n- displayOutput ?? (stdout || stderr ? `${stdout}${stderr ?? ''}` : '')\n+ const { command, stdout, stderr, exitCode, cwd, setMessages } = params\n+ const outputText = stdout || stderr || '(no output)'\n const toolCallId = crypto.randomUUID()\n const resultBlock: ContentBlock = {\n type: 'tool',\n toolName: 'run_terminal_command',\n toolCallId,\n input: { command },\n- output: outputText || '(no output)',\n+ output: outputText,\n }\n \n- // Add as a user message so the AI sees it as context\n setMessages((prev) => [\n ...prev,\n {\n ...getUserMessage([resultBlock]),\n metadata: { bashCwd: cwd },\n },\n ])\n \n- // Also add to pending tool results so AI can see this in the next run\n- const toolResultOutput: ToolResultOutput[] = [{\n- type: 'json' as const,\n- value: {\n- command,\n- startingCwd: cwd,\n- stdout: stdout || null,\n- stderr: stderr ?? null,\n- exitCode: exitCode ?? 0,\n- }\n- }]\n+ const toolResultOutput = createToolResultOutput({\n+ command,\n+ cwd,\n+ stdout: stdout || null,\n+ stderr: stderr ?? null,\n+ exitCode,\n+ })\n const toolMessage: ToolMessage = {\n role: 'tool',\n toolCallId,\n toolName: 'run_terminal_command',\n@@ -202,66 +255,8 @@\n }\n useChatStore.getState().addPendingToolResult(toolMessage)\n }\n \n-/**\n- * Execute a bash command as a ghost message in chat.\n- * Shows as a pending message while running, then commits to history when streaming ends.\n- */\n-function executeBashCommandAsGhost(\n- command: string,\n- addPendingBashMessage: (message: import('../state/chat-store').PendingBashMessage) => void,\n- updatePendingBashMessage: (id: string, updates: Partial) => void,\n-) {\n- const id = crypto.randomUUID()\n-\n- // Add pending message immediately with placeholder\n- addPendingBashMessage({\n- id,\n- command,\n- output: '',\n- exitCode: -1, // Indicates running\n- isRunning: true,\n- startTime: Date.now(),\n- cwd: process.cwd(),\n- })\n-\n- runTerminalCommand({\n- command,\n- process_type: 'SYNC',\n- cwd: process.cwd(),\n- timeout_seconds: -1,\n- env: process.env,\n- })\n- .then(([{ value }]) => {\n- const stdout = 'stdout' in value ? value.stdout || '' : ''\n- const stderr = 'stderr' in value ? value.stderr || '' : ''\n- const rawOutput = stdout + stderr\n- const output = rawOutput || '(no output)'\n- const exitCode = 'exitCode' in value ? value.exitCode ?? 0 : 0\n-\n- updatePendingBashMessage(id, {\n- output,\n- exitCode,\n- stdout,\n- stderr,\n- isRunning: false,\n- })\n- })\n- .catch((error) => {\n- const errorMessage = error instanceof Error ? error.message : String(error)\n- const output = `Error: ${errorMessage}`\n-\n- updatePendingBashMessage(id, {\n- output,\n- stdout: '',\n- stderr: errorMessage,\n- exitCode: 1,\n- isRunning: false,\n- })\n- })\n-}\n-\n export async function routeUserPrompt(\n params: RouterParams,\n ): Promise {\n const {\n@@ -284,11 +279,9 @@\n const setInputMode = useChatStore.getState().setInputMode\n \n const trimmed = inputValue.trim()\n const isBusy =\n- isStreaming ||\n- streamMessageIdRef.current ||\n- isChainInProgressRef.current\n+ isStreaming || streamMessageIdRef.current || isChainInProgressRef.current\n if (!trimmed) return\n \n // Handle bash mode commands\n if (inputMode === 'bash') {\n@@ -299,12 +292,17 @@\n setInputFocused(true)\n inputRef.current?.focus()\n \n if (isBusy) {\n- const { addPendingBashMessage, updatePendingBashMessage } = useChatStore.getState()\n- executeBashCommandAsGhost(trimmed, addPendingBashMessage, updatePendingBashMessage)\n+ const { addPendingBashMessage, updatePendingBashMessage } =\n+ useChatStore.getState()\n+ executeBashCommand(trimmed, {\n+ ghost: true,\n+ addPendingBashMessage,\n+ updatePendingBashMessage,\n+ })\n } else {\n- executeBashCommand(trimmed, setMessages)\n+ executeBashCommand(trimmed, { ghost: false, setMessages })\n }\n return\n }\n \n@@ -312,12 +310,17 @@\n if (trimmed.startsWith('!') && trimmed.length > 1) {\n const command = trimmed.slice(1)\n \n if (isBusy) {\n- const { addPendingBashMessage, updatePendingBashMessage } = useChatStore.getState()\n- executeBashCommandAsGhost(command, addPendingBashMessage, updatePendingBashMessage)\n+ const { addPendingBashMessage, updatePendingBashMessage } =\n+ useChatStore.getState()\n+ executeBashCommand(command, {\n+ ghost: true,\n+ addPendingBashMessage,\n+ updatePendingBashMessage,\n+ })\n } else {\n- executeBashCommand(command, setMessages)\n+ executeBashCommand(command, { ghost: false, setMessages })\n }\n return\n }\n \n@@ -333,9 +336,11 @@\n if (!codePattern.test(codeWithoutPrefix)) {\n setMessages((prev) => [\n ...prev,\n getUserMessage(trimmed),\n- getSystemMessage('Invalid referral code format. Codes should be 3-50 alphanumeric characters.'),\n+ getSystemMessage(\n+ 'Invalid referral code format. Codes should be 3-50 alphanumeric characters.',\n+ ),\n ])\n saveToHistory(trimmed)\n setInputValue({ text: '', cursorPosition: 0, lastEditDueToNav: false })\n setInputMode('default')\n@@ -351,9 +356,10 @@\n getUserMessage(trimmed),\n ...referralPostMessage([]),\n ])\n } catch (error) {\n- const errorMessage = error instanceof Error ? error.message : 'Unknown error'\n+ const errorMessage =\n+ error instanceof Error ? error.message : 'Unknown error'\n setMessages((prev) => [\n ...prev,\n getUserMessage(trimmed),\n getSystemMessage(`Error redeeming referral code: ${errorMessage}`),\n"
+ "diff": "Index: cli/src/commands/router.ts\n===================================================================\n--- cli/src/commands/router.ts\te9ce02a (parent)\n+++ cli/src/commands/router.ts\t844a6ca (commit)\n@@ -18,183 +18,236 @@\n \n import type { ToolMessage } from '@codebirds/common/types/messages/codebirds-message'\n import type { ToolResultOutput } from '@codebirds/common/types/messages/content-part'\n import type { ContentBlock } from '../types/chat'\n+import type { PendingBashMessage } from '../state/chat-store'\n \n /**\n- * Execute a bash command and add it directly to chat history.\n- * Shows immediate placeholder while running, then updates with output.\n+ * Create a tool result output structure for terminal command results.\n */\n+function createToolResultOutput(params: {\n+ command: string\n+ cwd: string\n+ stdout: string | null\n+ stderr: string | null\n+ exitCode: number\n+ errorMessage?: string\n+}): ToolResultOutput[] {\n+ const { command, cwd, stdout, stderr, exitCode, errorMessage } = params\n+ if (errorMessage) {\n+ return [\n+ {\n+ type: 'json' as const,\n+ value: { command, startingCwd: cwd, errorMessage },\n+ },\n+ ]\n+ }\n+ return [\n+ {\n+ type: 'json' as const,\n+ value: {\n+ command,\n+ startingCwd: cwd,\n+ stdout: stdout || null,\n+ stderr: stderr || null,\n+ exitCode,\n+ },\n+ },\n+ ]\n+}\n+\n+/**\n+ * Execute a bash command.\n+ * When ghost=false: adds directly to chat history with placeholder output that updates.\n+ * When ghost=true: adds to pending messages that appear as ghost while running.\n+ */\n function executeBashCommand(\n command: string,\n- setMessages: RouterParams['setMessages'],\n+ options:\n+ | { ghost: false; setMessages: RouterParams['setMessages'] }\n+ | {\n+ ghost: true\n+ addPendingBashMessage: (msg: PendingBashMessage) => void\n+ updatePendingBashMessage: (\n+ id: string,\n+ updates: Partial,\n+ ) => void\n+ },\n ) {\n- const toolCallId = crypto.randomUUID()\n- const resultBlock: ContentBlock = {\n- type: 'tool',\n- toolName: 'run_terminal_command',\n- toolCallId,\n- input: { command },\n- output: '...',\n- }\n-\n+ const id = crypto.randomUUID()\n const commandCwd = process.cwd()\n \n- // Add the command result to chat as a user message so the AI sees it as context\n- setMessages((prev) => [\n- ...prev,\n- {\n- ...getUserMessage([resultBlock]),\n- metadata: { bashCwd: commandCwd },\n- },\n- ])\n+ if (options.ghost) {\n+ // Ghost mode: add to pending messages\n+ options.addPendingBashMessage({\n+ id,\n+ command,\n+ stdout: '',\n+ stderr: '',\n+ exitCode: 0,\n+ isRunning: true,\n+ startTime: Date.now(),\n+ cwd: commandCwd,\n+ })\n+ } else {\n+ // Direct mode: add to chat history with placeholder\n+ const resultBlock: ContentBlock = {\n+ type: 'tool',\n+ toolName: 'run_terminal_command',\n+ toolCallId: id,\n+ input: { command },\n+ output: '...',\n+ }\n+ options.setMessages((prev) => [\n+ ...prev,\n+ {\n+ ...getUserMessage([resultBlock]),\n+ metadata: { bashCwd: commandCwd },\n+ },\n+ ])\n+ }\n \n- // Execute the command and update the output when complete\n runTerminalCommand({\n command,\n process_type: 'SYNC',\n cwd: commandCwd,\n timeout_seconds: -1,\n env: process.env,\n- }).then(([{ value }]) => {\n- const stdout = 'stdout' in value ? (value.stdout || '') : ''\n- const stderr = 'stderr' in value ? (value.stderr || '') : ''\n- const exitCode = 'exitCode' in value ? value.exitCode : 0\n+ })\n+ .then(([{ value }]) => {\n+ const stdout = 'stdout' in value ? value.stdout || '' : ''\n+ const stderr = 'stderr' in value ? value.stderr || '' : ''\n+ const exitCode = 'exitCode' in value ? value.exitCode ?? 0 : 0\n+ const rawOutput = stdout + stderr\n+ const output = rawOutput || '(no output)'\n \n- // Create tool result output for display\n- const toolResultOutput = [{\n- type: 'json' as const,\n- value: {\n- command,\n- startingCwd: commandCwd,\n- stdout: stdout || null,\n- stderr: stderr || null,\n- exitCode: exitCode ?? 0,\n- }\n- }]\n+ if (options.ghost) {\n+ options.updatePendingBashMessage(id, {\n+ stdout,\n+ stderr,\n+ exitCode,\n+ isRunning: false,\n+ })\n+ } else {\n+ const toolResultOutput = createToolResultOutput({\n+ command,\n+ cwd: commandCwd,\n+ stdout: stdout || null,\n+ stderr: stderr || null,\n+ exitCode,\n+ })\n+ const outputJson = JSON.stringify(toolResultOutput)\n \n- // Store output in JSON format for display\n- const outputJson = JSON.stringify(toolResultOutput)\n+ options.setMessages((prev) =>\n+ prev.map((msg) => {\n+ if (!msg.blocks) return msg\n+ return {\n+ ...msg,\n+ blocks: msg.blocks.map((block) =>\n+ 'toolCallId' in block && block.toolCallId === id\n+ ? { ...block, output: outputJson }\n+ : block,\n+ ),\n+ }\n+ }),\n+ )\n \n- setMessages((prev) => {\n- return prev.map((msg) => {\n- if (!msg.blocks) {\n- return msg\n+ // Add to pending tool results so AI can see this in the next run\n+ const toolMessage: ToolMessage = {\n+ role: 'tool',\n+ toolCallId: id,\n+ toolName: 'run_terminal_command',\n+ content: toolResultOutput,\n }\n- return {\n- ...msg,\n- blocks: msg.blocks.map((block) =>\n- 'toolCallId' in block && block.toolCallId === toolCallId\n- ? {\n- ...block,\n- output: outputJson,\n- }\n- : block,\n- ),\n- }\n- })\n+ useChatStore.getState().addPendingToolResult(toolMessage)\n+ }\n })\n+ .catch((error) => {\n+ const errorMessage =\n+ error instanceof Error ? error.message : String(error)\n+ const output = `Error: ${errorMessage}`\n \n- // Add to pending tool results so AI can see this in the next run\n- const toolMessage: ToolMessage = {\n- role: 'tool',\n- toolCallId,\n- toolName: 'run_terminal_command',\n- content: toolResultOutput,\n- }\n- useChatStore.getState().addPendingToolResult(toolMessage)\n- }).catch((error) => {\n- const errorMessage = error instanceof Error ? error.message : String(error)\n- \n- // Create error tool result output\n- const errorToolResultOutput = [{\n- type: 'json' as const,\n- value: {\n- command,\n- startingCwd: commandCwd,\n- errorMessage,\n- }\n- }]\n+ if (options.ghost) {\n+ options.updatePendingBashMessage(id, {\n+ stdout: '',\n+ stderr: errorMessage,\n+ exitCode: 1,\n+ isRunning: false,\n+ })\n+ } else {\n+ const errorToolResultOutput = createToolResultOutput({\n+ command,\n+ cwd: commandCwd,\n+ stdout: null,\n+ stderr: null,\n+ exitCode: 1,\n+ errorMessage,\n+ })\n+ const errorOutputJson = JSON.stringify(errorToolResultOutput)\n \n- // Store error output in JSON format for display\n- const errorOutputJson = JSON.stringify(errorToolResultOutput)\n+ options.setMessages((prev) =>\n+ prev.map((msg) => {\n+ if (!msg.blocks) return msg\n+ return {\n+ ...msg,\n+ blocks: msg.blocks.map((block) =>\n+ 'toolCallId' in block && block.toolCallId === id\n+ ? { ...block, output: errorOutputJson }\n+ : block,\n+ ),\n+ }\n+ }),\n+ )\n \n- setMessages((prev) => {\n- return prev.map((msg) => {\n- if (!msg.blocks) {\n- return msg\n+ const errorToolMessage: ToolMessage = {\n+ role: 'tool',\n+ toolCallId: id,\n+ toolName: 'run_terminal_command',\n+ content: errorToolResultOutput,\n }\n- return {\n- ...msg,\n- blocks: msg.blocks.map((block) =>\n- 'toolCallId' in block && block.toolCallId === toolCallId\n- ? {\n- ...block,\n- output: errorOutputJson,\n- }\n- : block,\n- ),\n- }\n- })\n+ useChatStore.getState().addPendingToolResult(errorToolMessage)\n+ }\n })\n-\n- // Add error result to pending tool results so AI can see this in the next run\n- const errorToolMessage: ToolMessage = {\n- role: 'tool',\n- toolCallId,\n- toolName: 'run_terminal_command',\n- content: errorToolResultOutput,\n- }\n- useChatStore.getState().addPendingToolResult(errorToolMessage)\n- })\n }\n \n /**\n- * Add a bash command result to the chat message history.\n+ * Add a completed bash command result to the chat message history.\n * Also adds to pendingToolResults so the AI can see it in the next run.\n */\n export function addBashMessageToHistory(params: {\n command: string\n stdout: string\n- stderr: string | null | undefined\n+ stderr: string | null\n exitCode: number\n cwd: string\n- displayOutput?: string\n setMessages: RouterParams['setMessages']\n }) {\n- const { command, stdout, stderr, exitCode, cwd, displayOutput, setMessages } =\n- params\n- const outputText =\n- displayOutput ?? (stdout || stderr ? `${stdout}${stderr ?? ''}` : '')\n+ const { command, stdout, stderr, exitCode, cwd, setMessages } = params\n+ const outputText = stdout || stderr || '(no output)'\n const toolCallId = crypto.randomUUID()\n const resultBlock: ContentBlock = {\n type: 'tool',\n toolName: 'run_terminal_command',\n toolCallId,\n input: { command },\n- output: outputText || '(no output)',\n+ output: outputText,\n }\n \n- // Add as a user message so the AI sees it as context\n setMessages((prev) => [\n ...prev,\n {\n ...getUserMessage([resultBlock]),\n metadata: { bashCwd: cwd },\n },\n ])\n \n- // Also add to pending tool results so AI can see this in the next run\n- const toolResultOutput: ToolResultOutput[] = [{\n- type: 'json' as const,\n- value: {\n- command,\n- startingCwd: cwd,\n- stdout: stdout || null,\n- stderr: stderr ?? null,\n- exitCode: exitCode ?? 0,\n- }\n- }]\n+ const toolResultOutput = createToolResultOutput({\n+ command,\n+ cwd,\n+ stdout: stdout || null,\n+ stderr: stderr ?? null,\n+ exitCode,\n+ })\n const toolMessage: ToolMessage = {\n role: 'tool',\n toolCallId,\n toolName: 'run_terminal_command',\n@@ -202,66 +255,8 @@\n }\n useChatStore.getState().addPendingToolResult(toolMessage)\n }\n \n-/**\n- * Execute a bash command as a ghost message in chat.\n- * Shows as a pending message while running, then commits to history when streaming ends.\n- */\n-function executeBashCommandAsGhost(\n- command: string,\n- addPendingBashMessage: (message: import('../state/chat-store').PendingBashMessage) => void,\n- updatePendingBashMessage: (id: string, updates: Partial) => void,\n-) {\n- const id = crypto.randomUUID()\n-\n- // Add pending message immediately with placeholder\n- addPendingBashMessage({\n- id,\n- command,\n- output: '',\n- exitCode: -1, // Indicates running\n- isRunning: true,\n- startTime: Date.now(),\n- cwd: process.cwd(),\n- })\n-\n- runTerminalCommand({\n- command,\n- process_type: 'SYNC',\n- cwd: process.cwd(),\n- timeout_seconds: -1,\n- env: process.env,\n- })\n- .then(([{ value }]) => {\n- const stdout = 'stdout' in value ? value.stdout || '' : ''\n- const stderr = 'stderr' in value ? value.stderr || '' : ''\n- const rawOutput = stdout + stderr\n- const output = rawOutput || '(no output)'\n- const exitCode = 'exitCode' in value ? value.exitCode ?? 0 : 0\n-\n- updatePendingBashMessage(id, {\n- output,\n- exitCode,\n- stdout,\n- stderr,\n- isRunning: false,\n- })\n- })\n- .catch((error) => {\n- const errorMessage = error instanceof Error ? error.message : String(error)\n- const output = `Error: ${errorMessage}`\n-\n- updatePendingBashMessage(id, {\n- output,\n- stdout: '',\n- stderr: errorMessage,\n- exitCode: 1,\n- isRunning: false,\n- })\n- })\n-}\n-\n export async function routeUserPrompt(\n params: RouterParams,\n ): Promise {\n const {\n@@ -284,11 +279,9 @@\n const setInputMode = useChatStore.getState().setInputMode\n \n const trimmed = inputValue.trim()\n const isBusy =\n- isStreaming ||\n- streamMessageIdRef.current ||\n- isChainInProgressRef.current\n+ isStreaming || streamMessageIdRef.current || isChainInProgressRef.current\n if (!trimmed) return\n \n // Handle bash mode commands\n if (inputMode === 'bash') {\n@@ -299,12 +292,17 @@\n setInputFocused(true)\n inputRef.current?.focus()\n \n if (isBusy) {\n- const { addPendingBashMessage, updatePendingBashMessage } = useChatStore.getState()\n- executeBashCommandAsGhost(trimmed, addPendingBashMessage, updatePendingBashMessage)\n+ const { addPendingBashMessage, updatePendingBashMessage } =\n+ useChatStore.getState()\n+ executeBashCommand(trimmed, {\n+ ghost: true,\n+ addPendingBashMessage,\n+ updatePendingBashMessage,\n+ })\n } else {\n- executeBashCommand(trimmed, setMessages)\n+ executeBashCommand(trimmed, { ghost: false, setMessages })\n }\n return\n }\n \n@@ -312,12 +310,17 @@\n if (trimmed.startsWith('!') && trimmed.length > 1) {\n const command = trimmed.slice(1)\n \n if (isBusy) {\n- const { addPendingBashMessage, updatePendingBashMessage } = useChatStore.getState()\n- executeBashCommandAsGhost(command, addPendingBashMessage, updatePendingBashMessage)\n+ const { addPendingBashMessage, updatePendingBashMessage } =\n+ useChatStore.getState()\n+ executeBashCommand(command, {\n+ ghost: true,\n+ addPendingBashMessage,\n+ updatePendingBashMessage,\n+ })\n } else {\n- executeBashCommand(command, setMessages)\n+ executeBashCommand(command, { ghost: false, setMessages })\n }\n return\n }\n \n@@ -333,9 +336,11 @@\n if (!codePattern.test(codeWithoutPrefix)) {\n setMessages((prev) => [\n ...prev,\n getUserMessage(trimmed),\n- getSystemMessage('Invalid referral code format. Codes should be 3-50 alphanumeric characters.'),\n+ getSystemMessage(\n+ 'Invalid referral code format. Codes should be 3-50 alphanumeric characters.',\n+ ),\n ])\n saveToHistory(trimmed)\n setInputValue({ text: '', cursorPosition: 0, lastEditDueToNav: false })\n setInputMode('default')\n@@ -351,9 +356,10 @@\n getUserMessage(trimmed),\n ...referralPostMessage([]),\n ])\n } catch (error) {\n- const errorMessage = error instanceof Error ? error.message : 'Unknown error'\n+ const errorMessage =\n+ error instanceof Error ? error.message : 'Unknown error'\n setMessages((prev) => [\n ...prev,\n getUserMessage(trimmed),\n getSystemMessage(`Error redeeming referral code: ${errorMessage}`),\n"
},
{
"path": "cli/src/components/chat-input-bar.tsx",
@@ -574,7 +574,7 @@
{
"path": "cli/src/components/message-block.tsx",
"status": "modified",
- "diff": "Index: cli/src/components/message-block.tsx\n===================================================================\n--- cli/src/components/message-block.tsx\te9ce02a (parent)\n+++ cli/src/components/message-block.tsx\t844a6ca (commit)\n@@ -1,13 +1,20 @@\n import { pluralize } from '@codebuff/common/util/string'\n import { TextAttributes } from '@opentui/core'\n-import React, { memo, useCallback, useMemo, useState, type ReactNode } from 'react'\n+import React, {\n+ memo,\n+ useCallback,\n+ useMemo,\n+ useState,\n+ type ReactNode,\n+} from 'react'\n \n import { AgentBranchItem } from './agent-branch-item'\n import { Button } from './button'\n import { MessageFooter } from './message-footer'\n import { ValidationErrorPopover } from './validation-error-popover'\n import { useTheme } from '../hooks/use-theme'\n+import { formatCwd } from '../utils/path-helpers'\n import { useWhyDidYouUpdateById } from '../hooks/use-why-did-you-update'\n import { isTextBlock, isToolBlock } from '../types/chat'\n import { shouldRenderAsSimpleText } from '../utils/constants'\n import {\n@@ -27,8 +34,9 @@\n ContentBlock,\n TextContentBlock,\n HtmlContentBlock,\n AgentContentBlock,\n+ ChatMessageMetadata,\n } from '../types/chat'\n import { isAskUserBlock } from '../types/chat'\n import type { ThemeColor } from '../types/theme-system'\n \n@@ -60,9 +68,9 @@\n category?: string\n footerMessage?: string\n errors?: Array<{ id: string; message: string }>\n }) => void\n- metadata?: Record\n+ metadata?: ChatMessageMetadata\n }\n \n import { BORDER_CHARS } from '../utils/ui-constants'\n \n@@ -93,21 +101,11 @@\n onOpenFeedback,\n metadata,\n }) => {\n const [showValidationPopover, setShowValidationPopover] = useState(false)\n- \n- // Format cwd for display, replacing home directory with ~\n- const formatCwd = (cwd: string | undefined): string => {\n- if (!cwd) return ''\n- const homeDir = process.env.HOME || process.env.USERPROFILE || ''\n- if (homeDir && cwd.startsWith(homeDir)) {\n- return '~' + cwd.slice(homeDir.length)\n- }\n- return cwd\n- }\n- \n+\n const bashCwd = metadata?.bashCwd ? formatCwd(metadata.bashCwd) : undefined\n- \n+\n useWhyDidYouUpdateById(\n 'MessageBlock',\n messageId,\n {\n@@ -164,9 +162,9 @@\n }}\n >\n {`[${timestamp}]`}\n \n- \n+\n {validationErrors && validationErrors.length > 0 && (\n setShowValidationPopover(!showValidationPopover)}\n >\n@@ -181,9 +179,9 @@\n \n )}\n \n )}\n- \n+\n {/* Bash command metadata header (timestamp + cwd) - now for user messages with bashCwd */}\n {bashCwd && (\n \n \n \n )}\n- \n+\n {/* Show validation popover below timestamp when expanded */}\n- {isUser && !bashCwd && validationErrors && validationErrors.length > 0 && showValidationPopover && (\n- \n- setShowValidationPopover(false)}\n- />\n- \n- )}\n- \n+ {isUser &&\n+ !bashCwd &&\n+ validationErrors &&\n+ validationErrors.length > 0 &&\n+ showValidationPopover && (\n+ \n+ setShowValidationPopover(false)}\n+ />\n+ \n+ )}\n+\n {blocks ? (\n- \n+ \n {\n if (!b || b.type !== 'text') return false\n"
+ "diff": "Index: cli/src/components/message-block.tsx\n===================================================================\n--- cli/src/components/message-block.tsx\te9ce02a (parent)\n+++ cli/src/components/message-block.tsx\t844a6ca (commit)\n@@ -1,13 +1,20 @@\n import { pluralize } from '@codebirds/common/util/string'\n import { TextAttributes } from '@opentui/core'\n-import React, { memo, useCallback, useMemo, useState, type ReactNode } from 'react'\n+import React, {\n+ memo,\n+ useCallback,\n+ useMemo,\n+ useState,\n+ type ReactNode,\n+} from 'react'\n \n import { AgentBranchItem } from './agent-branch-item'\n import { Button } from './button'\n import { MessageFooter } from './message-footer'\n import { ValidationErrorPopover } from './validation-error-popover'\n import { useTheme } from '../hooks/use-theme'\n+import { formatCwd } from '../utils/path-helpers'\n import { useWhyDidYouUpdateById } from '../hooks/use-why-did-you-update'\n import { isTextBlock, isToolBlock } from '../types/chat'\n import { shouldRenderAsSimpleText } from '../utils/constants'\n import {\n@@ -27,8 +34,9 @@\n ContentBlock,\n TextContentBlock,\n HtmlContentBlock,\n AgentContentBlock,\n+ ChatMessageMetadata,\n } from '../types/chat'\n import { isAskUserBlock } from '../types/chat'\n import type { ThemeColor } from '../types/theme-system'\n \n@@ -60,9 +68,9 @@\n category?: string\n footerMessage?: string\n errors?: Array<{ id: string; message: string }>\n }) => void\n- metadata?: Record\n+ metadata?: ChatMessageMetadata\n }\n \n import { BORDER_CHARS } from '../utils/ui-constants'\n \n@@ -93,21 +101,11 @@\n onOpenFeedback,\n metadata,\n }) => {\n const [showValidationPopover, setShowValidationPopover] = useState(false)\n- \n- // Format cwd for display, replacing home directory with ~\n- const formatCwd = (cwd: string | undefined): string => {\n- if (!cwd) return ''\n- const homeDir = process.env.HOME || process.env.USERPROFILE || ''\n- if (homeDir && cwd.startsWith(homeDir)) {\n- return '~' + cwd.slice(homeDir.length)\n- }\n- return cwd\n- }\n- \n+\n const bashCwd = metadata?.bashCwd ? formatCwd(metadata.bashCwd) : undefined\n- \n+\n useWhyDidYouUpdateById(\n 'MessageBlock',\n messageId,\n {\n@@ -164,9 +162,9 @@\n }}\n >\n {`[${timestamp}]`}\n \n- \n+\n {validationErrors && validationErrors.length > 0 && (\n setShowValidationPopover(!showValidationPopover)}\n >\n@@ -181,9 +179,9 @@\n \n )}\n \n )}\n- \n+\n {/* Bash command metadata header (timestamp + cwd) - now for user messages with bashCwd */}\n {bashCwd && (\n \n \n \n )}\n- \n+\n {/* Show validation popover below timestamp when expanded */}\n- {isUser && !bashCwd && validationErrors && validationErrors.length > 0 && showValidationPopover && (\n- \n- setShowValidationPopover(false)}\n- />\n- \n- )}\n- \n+ {isUser &&\n+ !bashCwd &&\n+ validationErrors &&\n+ validationErrors.length > 0 &&\n+ showValidationPopover && (\n+ \n+ setShowValidationPopover(false)}\n+ />\n+ \n+ )}\n+\n {blocks ? (\n- \n+ \n {\n if (!b || b.type !== 'text') return false\n"
},
{
"path": "cli/src/components/pending-bash-message.tsx",
@@ -604,7 +604,7 @@
{
"path": "cli/src/hooks/use-send-message.ts",
"status": "modified",
- "diff": "Index: cli/src/hooks/use-send-message.ts\n===================================================================\n--- cli/src/hooks/use-send-message.ts\te9ce02a (parent)\n+++ cli/src/hooks/use-send-message.ts\t844a6ca (commit)\n@@ -29,14 +29,20 @@\n } from '../utils/run-state-storage'\n \n import type { ElapsedTimeTracker } from './use-elapsed-time'\n import type { StreamStatus } from './use-message-queue'\n-import type { ChatMessage, ContentBlock, ToolContentBlock, AskUserContentBlock } from '../types/chat'\n+import type {\n+ ChatMessage,\n+ ContentBlock,\n+ ToolContentBlock,\n+ AskUserContentBlock,\n+} from '../types/chat'\n import type { SendMessageFn } from '../types/contracts/send-message'\n import type { ParamsOf } from '../types/function-params'\n import type { SetElement } from '../types/utils'\n import type { AgentMode } from '../utils/constants'\n import type { AgentDefinition, RunState, ToolName } from '@codebuff/sdk'\n+import type { ToolMessage } from '@codebuff/common/types/messages/codebuff-message'\n import type { SetStateAction } from 'react'\n const hiddenToolNames = new Set([\n 'spawn_agent_inline',\n 'end_turn',\n@@ -440,8 +446,41 @@\n if (agentMode !== 'PLAN') {\n setHasReceivedPlanResponse(false)\n }\n \n+ // Include any pending bash messages in context before sending\n+ // This ensures the LLM can reference terminal commands run during streaming\n+ const { pendingBashMessages, clearPendingBashMessages } =\n+ useChatStore.getState()\n+ if (pendingBashMessages.length > 0) {\n+ // Convert pending bash messages to chat messages and add to history\n+ applyMessageUpdate((prev) => {\n+ const bashMessages: ChatMessage[] = pendingBashMessages.flatMap(\n+ (bash) => [\n+ getUserMessage(`!${bash.command}`),\n+ {\n+ id: `bash-result-${Date.now()}-${Math.random().toString(16).slice(2)}`,\n+ variant: 'ai' as const,\n+ content: '',\n+ blocks: [\n+ {\n+ type: 'tool' as const,\n+ toolCallId: crypto.randomUUID(),\n+ toolName: 'run_terminal_command' as const,\n+ input: { command: bash.command },\n+ output: bash.stdout || bash.stderr || '',\n+ },\n+ ],\n+ timestamp: formatTimestamp(),\n+ isComplete: true,\n+ },\n+ ],\n+ )\n+ return [...prev, ...bashMessages]\n+ })\n+ clearPendingBashMessages()\n+ }\n+\n const timerController = createSendMessageTimerController({\n mainAgentTimer,\n onTimerEvent,\n agentId,\n@@ -947,9 +986,12 @@\n logger,\n agent: selectedAgentDefinition ?? agentId ?? fallbackAgent,\n prompt: content,\n previousRun: previousRunStateRef.current ?? undefined,\n- extraToolResults: pendingToolResults.length > 0 ? (pendingToolResults as unknown as import('@codebuff/common/types/messages/codebuff-message').ToolMessage[]) : undefined,\n+ extraToolResults:\n+ pendingToolResults.length > 0\n+ ? (pendingToolResults as unknown as ToolMessage[])\n+ : undefined,\n abortController,\n retry: {\n maxRetries: MAX_RETRIES_PER_MESSAGE,\n backoffBaseMs: RETRY_BACKOFF_BASE_DELAY_MS,\n@@ -1134,9 +1176,9 @@\n info,\n ] of spawnAgentsMapRef.current.entries()) {\n const eventType = event.agentType || ''\n const storedType = info.agentType || ''\n- \n+\n // Extract base names without version or scope\n // e.g., 'codebuff/file-picker@0.0.2' -> 'file-picker'\n // 'file-picker' -> 'file-picker'\n const getBaseName = (type: string) => {\n@@ -1146,12 +1188,12 @@\n }\n // Handle simple names, possibly with version\n return type.split('@')[0]\n }\n- \n+\n const eventBaseName = getBaseName(eventType)\n const storedBaseName = getBaseName(storedType)\n- \n+\n // Match if base names are the same\n const isMatch = eventBaseName === storedBaseName\n if (isMatch) {\n logger.info(\n@@ -1568,20 +1610,26 @@\n )\n }\n \n setStreamingAgents((prev) => new Set(prev).add(toolCallId))\n- } else if (event.type === 'tool_result' && event.toolCallId) {\n+ } else if (event.type === 'tool_result' && event.toolCallId) {\n const { toolCallId } = event\n \n // Handle ask_user result transformation\n- applyMessageUpdate((prev) => \n+ applyMessageUpdate((prev) =>\n prev.map((msg) => {\n if (msg.id !== aiMessageId || !msg.blocks) return msg\n \n // Recursively check for tool blocks to transform\n- const transformAskUser = (blocks: ContentBlock[]): ContentBlock[] => {\n+ const transformAskUser = (\n+ blocks: ContentBlock[],\n+ ): ContentBlock[] => {\n return blocks.map((block) => {\n- if (block.type === 'tool' && block.toolCallId === toolCallId && block.toolName === 'ask_user') {\n+ if (\n+ block.type === 'tool' &&\n+ block.toolCallId === toolCallId &&\n+ block.toolName === 'ask_user'\n+ ) {\n const resultValue = (event.output?.[0] as any)?.value\n const skipped = resultValue?.skipped\n const answers = resultValue?.answers\n const questions = block.input.questions\n@@ -1598,9 +1646,9 @@\n answers,\n skipped,\n } as AskUserContentBlock\n }\n- \n+\n if (block.type === 'agent' && block.blocks) {\n const updatedBlocks = transformAskUser(block.blocks)\n if (updatedBlocks !== block.blocks) {\n return { ...block, blocks: updatedBlocks }\n@@ -1611,12 +1659,12 @@\n }\n \n const newBlocks = transformAskUser(msg.blocks)\n if (newBlocks !== msg.blocks) {\n- return { ...msg, blocks: newBlocks }\n+ return { ...msg, blocks: newBlocks }\n }\n return msg\n- })\n+ }),\n )\n \n // Check if this is a spawn_agents result\n // The structure is: output[0].value = [{ agentName, agentType, value }]\n@@ -1780,25 +1828,35 @@\n return currentMessages\n })\n \n if (!runState.output || runState.output.type === 'error') {\n- const errorOutput = runState.output?.type === 'error' ? runState.output : null\n- const errorMessage = errorOutput?.message ?? 'No output from agent run'\n+ const errorOutput =\n+ runState.output?.type === 'error' ? runState.output : null\n+ const errorMessage =\n+ errorOutput?.message ?? 'No output from agent run'\n \n // Check if this was a user-initiated cancellation - if so, don't show error since\n // the abort handler already shows [response interrupted]\n if (wasAbortedByUserRef.current) {\n- logger.info({ errorMessage }, 'Run cancelled by user, not showing error')\n+ logger.info(\n+ { errorMessage },\n+ 'Run cancelled by user, not showing error',\n+ )\n return\n }\n \n- logger.warn({ errorMessage, errorCode: errorOutput?.errorCode }, 'Agent run failed')\n+ logger.warn(\n+ { errorMessage, errorCode: errorOutput?.errorCode },\n+ 'Agent run failed',\n+ )\n \n // Check if this is an out-of-credits error using the error code\n- const isOutOfCredits = errorOutput?.errorCode === ErrorCodes.PAYMENT_REQUIRED\n+ const isOutOfCredits =\n+ errorOutput?.errorCode === ErrorCodes.PAYMENT_REQUIRED\n \n if (isOutOfCredits) {\n- const appUrl = process.env.NEXT_PUBLIC_CODEBUFF_APP_URL || 'https://codebuff.com'\n+ const appUrl =\n+ process.env.NEXT_PUBLIC_CODEBUFF_APP_URL || 'https://codebuff.com'\n const paymentErrorMessage =\n errorOutput?.message ??\n `Out of credits. Please add credits at ${appUrl}/usage`\n applyMessageUpdate((prev) =>\n@@ -1814,9 +1872,11 @@\n )\n // Show the usage banner so user can see their balance and renewal date\n useChatStore.getState().setInputMode('usage')\n // Refresh usage data to show current state\n- queryClient.invalidateQueries({ queryKey: usageQueryKeys.current() })\n+ queryClient.invalidateQueries({\n+ queryKey: usageQueryKeys.current(),\n+ })\n } else {\n // Generic error - display the error message directly from SDK\n applyMessageUpdate((prev) =>\n prev.map((msg) => {\n@@ -1894,9 +1954,10 @@\n error instanceof Error ? error.message : 'Unknown error occurred'\n \n // Handle payment required (out of credits) specially\n if (isPaymentRequiredError(error)) {\n- const appUrl = process.env.NEXT_PUBLIC_CODEBUFF_APP_URL || 'https://codebuff.com'\n+ const appUrl =\n+ process.env.NEXT_PUBLIC_CODEBUFF_APP_URL || 'https://codebuff.com'\n const paymentErrorMessage =\n error instanceof Error && error.message\n ? error.message\n : `Out of credits. Please add credits at ${appUrl}/usage`\n"
+ "diff": "Index: cli/src/hooks/use-send-message.ts\n===================================================================\n--- cli/src/hooks/use-send-message.ts\te9ce02a (parent)\n+++ cli/src/hooks/use-send-message.ts\t844a6ca (commit)\n@@ -29,14 +29,20 @@\n } from '../utils/run-state-storage'\n \n import type { ElapsedTimeTracker } from './use-elapsed-time'\n import type { StreamStatus } from './use-message-queue'\n-import type { ChatMessage, ContentBlock, ToolContentBlock, AskUserContentBlock } from '../types/chat'\n+import type {\n+ ChatMessage,\n+ ContentBlock,\n+ ToolContentBlock,\n+ AskUserContentBlock,\n+} from '../types/chat'\n import type { SendMessageFn } from '../types/contracts/send-message'\n import type { ParamsOf } from '../types/function-params'\n import type { SetElement } from '../types/utils'\n import type { AgentMode } from '../utils/constants'\n import type { AgentDefinition, RunState, ToolName } from '@codebirds/sdk'\n+import type { ToolMessage } from '@codebirds/common/types/messages/codebirds-message'\n import type { SetStateAction } from 'react'\n const hiddenToolNames = new Set([\n 'spawn_agent_inline',\n 'end_turn',\n@@ -440,8 +446,41 @@\n if (agentMode !== 'PLAN') {\n setHasReceivedPlanResponse(false)\n }\n \n+ // Include any pending bash messages in context before sending\n+ // This ensures the LLM can reference terminal commands run during streaming\n+ const { pendingBashMessages, clearPendingBashMessages } =\n+ useChatStore.getState()\n+ if (pendingBashMessages.length > 0) {\n+ // Convert pending bash messages to chat messages and add to history\n+ applyMessageUpdate((prev) => {\n+ const bashMessages: ChatMessage[] = pendingBashMessages.flatMap(\n+ (bash) => [\n+ getUserMessage(`!${bash.command}`),\n+ {\n+ id: `bash-result-${Date.now()}-${Math.random().toString(16).slice(2)}`,\n+ variant: 'ai' as const,\n+ content: '',\n+ blocks: [\n+ {\n+ type: 'tool' as const,\n+ toolCallId: crypto.randomUUID(),\n+ toolName: 'run_terminal_command' as const,\n+ input: { command: bash.command },\n+ output: bash.stdout || bash.stderr || '',\n+ },\n+ ],\n+ timestamp: formatTimestamp(),\n+ isComplete: true,\n+ },\n+ ],\n+ )\n+ return [...prev, ...bashMessages]\n+ })\n+ clearPendingBashMessages()\n+ }\n+\n const timerController = createSendMessageTimerController({\n mainAgentTimer,\n onTimerEvent,\n agentId,\n@@ -947,9 +986,12 @@\n logger,\n agent: selectedAgentDefinition ?? agentId ?? fallbackAgent,\n prompt: content,\n previousRun: previousRunStateRef.current ?? undefined,\n- extraToolResults: pendingToolResults.length > 0 ? (pendingToolResults as unknown as import('@codebirds/common/types/messages/codebirds-message').ToolMessage[]) : undefined,\n+ extraToolResults:\n+ pendingToolResults.length > 0\n+ ? (pendingToolResults as unknown as ToolMessage[])\n+ : undefined,\n abortController,\n retry: {\n maxRetries: MAX_RETRIES_PER_MESSAGE,\n backoffBaseMs: RETRY_BACKOFF_BASE_DELAY_MS,\n@@ -1134,9 +1176,9 @@\n info,\n ] of spawnAgentsMapRef.current.entries()) {\n const eventType = event.agentType || ''\n const storedType = info.agentType || ''\n- \n+\n // Extract base names without version or scope\n // e.g., 'codebirds/file-picker@0.0.2' -> 'file-picker'\n // 'file-picker' -> 'file-picker'\n const getBaseName = (type: string) => {\n@@ -1146,12 +1188,12 @@\n }\n // Handle simple names, possibly with version\n return type.split('@')[0]\n }\n- \n+\n const eventBaseName = getBaseName(eventType)\n const storedBaseName = getBaseName(storedType)\n- \n+\n // Match if base names are the same\n const isMatch = eventBaseName === storedBaseName\n if (isMatch) {\n logger.info(\n@@ -1568,20 +1610,26 @@\n )\n }\n \n setStreamingAgents((prev) => new Set(prev).add(toolCallId))\n- } else if (event.type === 'tool_result' && event.toolCallId) {\n+ } else if (event.type === 'tool_result' && event.toolCallId) {\n const { toolCallId } = event\n \n // Handle ask_user result transformation\n- applyMessageUpdate((prev) => \n+ applyMessageUpdate((prev) =>\n prev.map((msg) => {\n if (msg.id !== aiMessageId || !msg.blocks) return msg\n \n // Recursively check for tool blocks to transform\n- const transformAskUser = (blocks: ContentBlock[]): ContentBlock[] => {\n+ const transformAskUser = (\n+ blocks: ContentBlock[],\n+ ): ContentBlock[] => {\n return blocks.map((block) => {\n- if (block.type === 'tool' && block.toolCallId === toolCallId && block.toolName === 'ask_user') {\n+ if (\n+ block.type === 'tool' &&\n+ block.toolCallId === toolCallId &&\n+ block.toolName === 'ask_user'\n+ ) {\n const resultValue = (event.output?.[0] as any)?.value\n const skipped = resultValue?.skipped\n const answers = resultValue?.answers\n const questions = block.input.questions\n@@ -1598,9 +1646,9 @@\n answers,\n skipped,\n } as AskUserContentBlock\n }\n- \n+\n if (block.type === 'agent' && block.blocks) {\n const updatedBlocks = transformAskUser(block.blocks)\n if (updatedBlocks !== block.blocks) {\n return { ...block, blocks: updatedBlocks }\n@@ -1611,12 +1659,12 @@\n }\n \n const newBlocks = transformAskUser(msg.blocks)\n if (newBlocks !== msg.blocks) {\n- return { ...msg, blocks: newBlocks }\n+ return { ...msg, blocks: newBlocks }\n }\n return msg\n- })\n+ }),\n )\n \n // Check if this is a spawn_agents result\n // The structure is: output[0].value = [{ agentName, agentType, value }]\n@@ -1780,25 +1828,35 @@\n return currentMessages\n })\n \n if (!runState.output || runState.output.type === 'error') {\n- const errorOutput = runState.output?.type === 'error' ? runState.output : null\n- const errorMessage = errorOutput?.message ?? 'No output from agent run'\n+ const errorOutput =\n+ runState.output?.type === 'error' ? runState.output : null\n+ const errorMessage =\n+ errorOutput?.message ?? 'No output from agent run'\n \n // Check if this was a user-initiated cancellation - if so, don't show error since\n // the abort handler already shows [response interrupted]\n if (wasAbortedByUserRef.current) {\n- logger.info({ errorMessage }, 'Run cancelled by user, not showing error')\n+ logger.info(\n+ { errorMessage },\n+ 'Run cancelled by user, not showing error',\n+ )\n return\n }\n \n- logger.warn({ errorMessage, errorCode: errorOutput?.errorCode }, 'Agent run failed')\n+ logger.warn(\n+ { errorMessage, errorCode: errorOutput?.errorCode },\n+ 'Agent run failed',\n+ )\n \n // Check if this is an out-of-credits error using the error code\n- const isOutOfCredits = errorOutput?.errorCode === ErrorCodes.PAYMENT_REQUIRED\n+ const isOutOfCredits =\n+ errorOutput?.errorCode === ErrorCodes.PAYMENT_REQUIRED\n \n if (isOutOfCredits) {\n- const appUrl = process.env.NEXT_PUBLIC_CODEBIRDS_APP_URL || 'https://codebirds.com'\n+ const appUrl =\n+ process.env.NEXT_PUBLIC_CODEBIRDS_APP_URL || 'https://codebirds.com'\n const paymentErrorMessage =\n errorOutput?.message ??\n `Out of credits. Please add credits at ${appUrl}/usage`\n applyMessageUpdate((prev) =>\n@@ -1814,9 +1872,11 @@\n )\n // Show the usage banner so user can see their balance and renewal date\n useChatStore.getState().setInputMode('usage')\n // Refresh usage data to show current state\n- queryClient.invalidateQueries({ queryKey: usageQueryKeys.current() })\n+ queryClient.invalidateQueries({\n+ queryKey: usageQueryKeys.current(),\n+ })\n } else {\n // Generic error - display the error message directly from SDK\n applyMessageUpdate((prev) =>\n prev.map((msg) => {\n@@ -1894,9 +1954,10 @@\n error instanceof Error ? error.message : 'Unknown error occurred'\n \n // Handle payment required (out of credits) specially\n if (isPaymentRequiredError(error)) {\n- const appUrl = process.env.NEXT_PUBLIC_CODEBIRDS_APP_URL || 'https://codebirds.com'\n+ const appUrl =\n+ process.env.NEXT_PUBLIC_CODEBIRDS_APP_URL || 'https://codebirds.com'\n const paymentErrorMessage =\n error instanceof Error && error.message\n ? error.message\n : `Out of credits. Please add credits at ${appUrl}/usage`\n"
},
{
"path": "cli/src/state/chat-store.ts",
@@ -634,12 +634,12 @@
{
"path": "plans/bash-mode-ephemeral-execution.md",
"status": "added",
- "diff": "Index: plans/bash-mode-ephemeral-execution.md\n===================================================================\n--- plans/bash-mode-ephemeral-execution.md\te9ce02a (parent)\n+++ plans/bash-mode-ephemeral-execution.md\t844a6ca (commit)\n@@ -0,0 +1,97 @@\n+# Plan: Immediate Bash Command Execution with Ephemeral Preview\n+\n+## Overview\n+Execute bash commands immediately in an ephemeral \"bash window\" overlay that shows real-time terminal output. Once the command completes, add it to the chat message history to maintain full context while keeping the immediate feedback responsive.\n+\n+## Requirements\n+\n+### Immediate Execution in Ephemeral Window\n+- When user submits bash command in bash mode, execute immediately (don't queue)\n+- Show execution in an ephemeral overlay/window near the input area\n+- Display command as it runs with faithful terminal output (stdout, stderr, colors, formatting)\n+- Block input while command runs (show loading state)\n+- Overlay should be scrollable for long outputs\n+- Show exit code and execution time\n+\n+### Deferred History Addition\n+- After command completes, add the command + output to chat message history\n+- Wait until any currently running agent response completes first to avoid confusion\n+- If agent is streaming, queue the bash message to be added after agent finishes\n+- Message format should match current bash message format (user message with `!cmd`, system message with tool result)\n+\n+### Terminal Output Fidelity\n+- Preserve ANSI colors and formatting from terminal\n+- Show both stdout and stderr (differentiated if possible)\n+- Display command exit code\n+- Show execution duration\n+- Handle streaming output (show output as it arrives, not just at the end)\n+\n+### Command History Navigation\n+- Maintain bash command history accessible via ↑/↓ arrows in bash mode\n+- History persists across sessions\n+- Separate from regular chat input history\n+\n+### Edge Cases\n+- If bash command submitted while agent is streaming, execute immediately in overlay but defer adding to history\n+- If multiple bash commands queued, execute them sequentially\n+- Allow Ctrl+C to cancel running command\n+- Handle commands with no output gracefully\n+\n+## Notes\n+- The ephemeral overlay should feel like a mini terminal window\n+- Consider using a terminal emulator component if needed for proper ANSI rendering\n+- Exit code should be visually distinct (green for 0, red for non-zero)\n+- The overlay should position intelligently (above input if output is short, take more space if needed)\n+- When command is added to history after agent completes, it should scroll into view naturally\n+\n+## Relevant Files\n+- `cli/src/commands/router.ts` - Bash command execution, needs to handle immediate execution\n+- `cli/src/state/chat-store.ts` - May need state for bash overlay visibility and pending bash messages\n+- `cli/src/components/chat-input-bar.tsx` - Where bash overlay will render\n+- `cli/src/hooks/use-send-message.ts` - Queue management for deferred history addition\n+- `cli/src/hooks/use-input-history.ts` - Pattern for bash command history\n+- `cli/src/utils/input-modes.ts` - Bash mode configuration\n+- `cli/src/hooks/use-chat-keyboard.ts` - Arrow key navigation for bash history\n+- New component needed: BashOutputOverlay or similar for ephemeral display\n+\n+## Current State (as of this plan)\n+\n+A partial fix for bash mode queueing was just implemented, but the full ephemeral execution design is not yet done. Here's what exists:\n+\n+### What Was Recently Changed\n+1. **`cli/src/commands/router.ts`**: Added `executeBashCommand()` helper function and updated bash mode handler to queue commands when streaming/busy. The handler detects `!` prefix for bash commands.\n+\n+2. **`cli/src/chat.tsx`**: Updated `useMessageQueue` callback to route queued messages through `routeUserPrompt` instead of calling `sendMessage` directly. This ensures bash commands from the queue get executed properly.\n+\n+### Current Flow (What Needs to Change)\n+1. User enters bash mode (types `!`)\n+2. User types command and hits Enter\n+3. Command executes via `runTerminalCommand` from SDK\n+4. Output is added to chat history immediately as a tool result block\n+5. If agent is streaming, command gets queued instead\n+\n+### Target Flow (What This Plan Describes)\n+1. User enters bash mode (types `!`)\n+2. User types command and hits Enter\n+3. **NEW**: Command executes immediately in ephemeral overlay\n+4. **NEW**: Overlay shows real-time streaming output with ANSI colors\n+5. **NEW**: Input is blocked while command runs\n+6. **NEW**: After command completes, message is added to chat history (deferred if agent is streaming)\n+7. **NEW**: Overlay can be dismissed once output is in history\n+\n+### Key SDK Functions\n+- `runTerminalCommand` from `@codebuff/sdk` - Used to execute terminal commands\n+- Returns `[{ value: { stdout, stderr, exitCode } }]`\n+- Currently runs synchronously (SYNC mode)\n+- May need to use different mode for streaming output\n+\n+### Existing Patterns to Reference\n+- **Suggestion Menu**: See `cli/src/components/chat-input-bar.tsx` for how overlays are positioned near input\n+- **Tool Result Display**: See `cli/src/components/tools/run-terminal-command.tsx` for terminal output rendering\n+- **Input History**: See `cli/src/hooks/use-input-history.ts` for history navigation pattern\n+- **Bash Mode Tests**: See `cli/src/__tests__/bash-mode.test.ts` for expected behaviors\n+\n+### Testing\n+- Run `cd cli && bun test` to run all CLI tests\n+- Run `cd cli && bun run typecheck` to check types\n+- Bash mode tests are in `cli/src/__tests__/bash-mode.test.ts`\n"
+ "diff": "Index: plans/bash-mode-ephemeral-execution.md\n===================================================================\n--- plans/bash-mode-ephemeral-execution.md\te9ce02a (parent)\n+++ plans/bash-mode-ephemeral-execution.md\t844a6ca (commit)\n@@ -0,0 +1,97 @@\n+# Plan: Immediate Bash Command Execution with Ephemeral Preview\n+\n+## Overview\n+Execute bash commands immediately in an ephemeral \"bash window\" overlay that shows real-time terminal output. Once the command completes, add it to the chat message history to maintain full context while keeping the immediate feedback responsive.\n+\n+## Requirements\n+\n+### Immediate Execution in Ephemeral Window\n+- When user submits bash command in bash mode, execute immediately (don't queue)\n+- Show execution in an ephemeral overlay/window near the input area\n+- Display command as it runs with faithful terminal output (stdout, stderr, colors, formatting)\n+- Block input while command runs (show loading state)\n+- Overlay should be scrollable for long outputs\n+- Show exit code and execution time\n+\n+### Deferred History Addition\n+- After command completes, add the command + output to chat message history\n+- Wait until any currently running agent response completes first to avoid confusion\n+- If agent is streaming, queue the bash message to be added after agent finishes\n+- Message format should match current bash message format (user message with `!cmd`, system message with tool result)\n+\n+### Terminal Output Fidelity\n+- Preserve ANSI colors and formatting from terminal\n+- Show both stdout and stderr (differentiated if possible)\n+- Display command exit code\n+- Show execution duration\n+- Handle streaming output (show output as it arrives, not just at the end)\n+\n+### Command History Navigation\n+- Maintain bash command history accessible via ↑/↓ arrows in bash mode\n+- History persists across sessions\n+- Separate from regular chat input history\n+\n+### Edge Cases\n+- If bash command submitted while agent is streaming, execute immediately in overlay but defer adding to history\n+- If multiple bash commands queued, execute them sequentially\n+- Allow Ctrl+C to cancel running command\n+- Handle commands with no output gracefully\n+\n+## Notes\n+- The ephemeral overlay should feel like a mini terminal window\n+- Consider using a terminal emulator component if needed for proper ANSI rendering\n+- Exit code should be visually distinct (green for 0, red for non-zero)\n+- The overlay should position intelligently (above input if output is short, take more space if needed)\n+- When command is added to history after agent completes, it should scroll into view naturally\n+\n+## Relevant Files\n+- `cli/src/commands/router.ts` - Bash command execution, needs to handle immediate execution\n+- `cli/src/state/chat-store.ts` - May need state for bash overlay visibility and pending bash messages\n+- `cli/src/components/chat-input-bar.tsx` - Where bash overlay will render\n+- `cli/src/hooks/use-send-message.ts` - Queue management for deferred history addition\n+- `cli/src/hooks/use-input-history.ts` - Pattern for bash command history\n+- `cli/src/utils/input-modes.ts` - Bash mode configuration\n+- `cli/src/hooks/use-chat-keyboard.ts` - Arrow key navigation for bash history\n+- New component needed: BashOutputOverlay or similar for ephemeral display\n+\n+## Current State (as of this plan)\n+\n+A partial fix for bash mode queueing was just implemented, but the full ephemeral execution design is not yet done. Here's what exists:\n+\n+### What Was Recently Changed\n+1. **`cli/src/commands/router.ts`**: Added `executeBashCommand()` helper function and updated bash mode handler to queue commands when streaming/busy. The handler detects `!` prefix for bash commands.\n+\n+2. **`cli/src/chat.tsx`**: Updated `useMessageQueue` callback to route queued messages through `routeUserPrompt` instead of calling `sendMessage` directly. This ensures bash commands from the queue get executed properly.\n+\n+### Current Flow (What Needs to Change)\n+1. User enters bash mode (types `!`)\n+2. User types command and hits Enter\n+3. Command executes via `runTerminalCommand` from SDK\n+4. Output is added to chat history immediately as a tool result block\n+5. If agent is streaming, command gets queued instead\n+\n+### Target Flow (What This Plan Describes)\n+1. User enters bash mode (types `!`)\n+2. User types command and hits Enter\n+3. **NEW**: Command executes immediately in ephemeral overlay\n+4. **NEW**: Overlay shows real-time streaming output with ANSI colors\n+5. **NEW**: Input is blocked while command runs\n+6. **NEW**: After command completes, message is added to chat history (deferred if agent is streaming)\n+7. **NEW**: Overlay can be dismissed once output is in history\n+\n+### Key SDK Functions\n+- `runTerminalCommand` from `@codebirds/sdk` - Used to execute terminal commands\n+- Returns `[{ value: { stdout, stderr, exitCode } }]`\n+- Currently runs synchronously (SYNC mode)\n+- May need to use different mode for streaming output\n+\n+### Existing Patterns to Reference\n+- **Suggestion Menu**: See `cli/src/components/chat-input-bar.tsx` for how overlays are positioned near input\n+- **Tool Result Display**: See `cli/src/components/tools/run-terminal-command.tsx` for terminal output rendering\n+- **Input History**: See `cli/src/hooks/use-input-history.ts` for history navigation pattern\n+- **Bash Mode Tests**: See `cli/src/__tests__/bash-mode.test.ts` for expected behaviors\n+\n+### Testing\n+- Run `cd cli && bun test` to run all CLI tests\n+- Run `cd cli && bun run typecheck` to check types\n+- Bash mode tests are in `cli/src/__tests__/bash-mode.test.ts`\n"
},
{
"path": "sdk/src/run.ts",
"status": "modified",
- "diff": "Index: sdk/src/run.ts\n===================================================================\n--- sdk/src/run.ts\te9ce02a (parent)\n+++ sdk/src/run.ts\t844a6ca (commit)\n@@ -1,9 +1,9 @@\n import path from 'path'\n \n import { callMainPrompt } from '@codebuff/agent-runtime/main-prompt'\n import {\n- asUserMessage,\n+ buildUserMessageContent,\n getCancelledAdditionalMessages,\n } from '@codebuff/agent-runtime/util/messages'\n import { MAX_AGENT_STEPS_DEFAULT } from '@codebuff/common/constants/agents'\n import { getMCPClient, listMCPTools } from '@codebuff/common/mcp/client'\n@@ -14,9 +14,16 @@\n import { cloneDeep } from 'lodash'\n \n import { getAgentRuntimeImpl } from './impl/agent-runtime'\n import { getUserInfoFromApiKey } from './impl/database'\n-import { RETRYABLE_ERROR_CODES, isNetworkError, isPaymentRequiredError, ErrorCodes, NetworkError, sanitizeErrorMessage } from './errors'\n+import {\n+ RETRYABLE_ERROR_CODES,\n+ isNetworkError,\n+ isPaymentRequiredError,\n+ ErrorCodes,\n+ NetworkError,\n+ sanitizeErrorMessage,\n+} from './errors'\n import type { ErrorCode } from './errors'\n import { getErrorObject } from '@codebuff/common/util/error'\n import { initialSessionState, applyOverridesToSessionState } from './run-state'\n import {\n@@ -59,28 +66,20 @@\n import type { Source } from '@codebuff/common/types/source'\n import type { CodebuffSpawn } from '@codebuff/common/types/spawn'\n import { ToolMessage } from '@codebuff/common/types/messages/codebuff-message'\n \n+/**\n+ * Wraps content for user messages, ensuring text is wrapped in tags.\n+ * Uses buildUserMessageContent from agent-runtime for consistency.\n+ */\n const wrapContentForUserMessage = (\n content?: (TextPart | ImagePart)[],\n ): (TextPart | ImagePart)[] | undefined => {\n if (!content || content.length === 0) {\n return content\n }\n- let hasWrappedText = false\n- return content.map((part) => {\n- if (part.type === 'text' && !hasWrappedText) {\n- hasWrappedText = true\n- const alreadyWrapped = part.text.includes('')\n- return alreadyWrapped\n- ? part\n- : {\n- ...part,\n- text: asUserMessage(part.text),\n- }\n- }\n- return part\n- })\n+ // Delegate to the shared utility which handles wrapping correctly\n+ return buildUserMessageContent(undefined, undefined, content)\n }\n \n export type CodebuffClientOptions = {\n apiKey?: string\n@@ -287,11 +286,9 @@\n }\n type RunOnceOptions = Omit\n type RunReturnType = RunState\n \n-export async function run(\n- options: RunExecutionOptions,\n-): Promise {\n+export async function run(options: RunExecutionOptions): Promise {\n const { retry, abortController, ...rest } = options\n const retryOptions = normalizeRetryOptions(retry)\n \n // Prefer provided signal; otherwise reuse a shared controller across retries.\n@@ -837,9 +834,13 @@\n logger?.warn(\n {\n isNetworkError: isNetworkError(error),\n isPaymentRequired,\n- errorCode: isNetworkError(error) ? error.code : isPaymentRequired ? error.code : undefined,\n+ errorCode: isNetworkError(error)\n+ ? error.code\n+ : isPaymentRequired\n+ ? error.code\n+ : undefined,\n isRetryable,\n error: getErrorObject(error),\n },\n 'callMainPrompt caught error, checking if retryable',\n@@ -851,9 +852,10 @@\n return\n }\n \n // For non-retryable errors, resolve with cancelled state\n- const errorMessage = error instanceof Error ? error.message : String(error ?? '')\n+ const errorMessage =\n+ error instanceof Error ? error.message : String(error ?? '')\n resolve(getCancelledRunState(errorMessage))\n })\n \n return promise\n@@ -1003,14 +1005,19 @@\n /**\n * Extracts an error code from a prompt error message.\n * Returns the appropriate ErrorCode if the error is retryable, null otherwise.\n */\n-export const getRetryableErrorCode = (errorMessage: string): ErrorCode | null => {\n+export const getRetryableErrorCode = (\n+ errorMessage: string,\n+): ErrorCode | null => {\n const lowerMessage = errorMessage.toLowerCase()\n \n // AI SDK's built-in retry error (e.g., \"Failed after 4 attempts. Last error: Service Unavailable\")\n // The AI SDK already retried 4 times, but we still want our SDK wrapper to retry 3 more times\n- if (lowerMessage.includes('failed after') && lowerMessage.includes('attempts')) {\n+ if (\n+ lowerMessage.includes('failed after') &&\n+ lowerMessage.includes('attempts')\n+ ) {\n // Extract the underlying error type from the message\n if (lowerMessage.includes('service unavailable')) {\n return ErrorCodes.SERVICE_UNAVAILABLE\n }\n@@ -1023,24 +1030,38 @@\n // Default to SERVER_ERROR for other AI SDK retry failures\n return ErrorCodes.SERVER_ERROR\n }\n \n- if (errorMessage.includes('503') || lowerMessage.includes('service unavailable')) {\n+ if (\n+ errorMessage.includes('503') ||\n+ lowerMessage.includes('service unavailable')\n+ ) {\n return ErrorCodes.SERVICE_UNAVAILABLE\n }\n if (lowerMessage.includes('timeout')) {\n return ErrorCodes.TIMEOUT\n }\n- if (lowerMessage.includes('econnrefused') || lowerMessage.includes('connection refused')) {\n+ if (\n+ lowerMessage.includes('econnrefused') ||\n+ lowerMessage.includes('connection refused')\n+ ) {\n return ErrorCodes.CONNECTION_REFUSED\n }\n if (lowerMessage.includes('dns') || lowerMessage.includes('enotfound')) {\n return ErrorCodes.DNS_FAILURE\n }\n- if (lowerMessage.includes('server error') || lowerMessage.includes('500') || lowerMessage.includes('502') || lowerMessage.includes('504')) {\n+ if (\n+ lowerMessage.includes('server error') ||\n+ lowerMessage.includes('500') ||\n+ lowerMessage.includes('502') ||\n+ lowerMessage.includes('504')\n+ ) {\n return ErrorCodes.SERVER_ERROR\n }\n- if (lowerMessage.includes('network error') || lowerMessage.includes('fetch failed')) {\n+ if (\n+ lowerMessage.includes('network error') ||\n+ lowerMessage.includes('fetch failed')\n+ ) {\n return ErrorCodes.NETWORK_ERROR\n }\n \n return null\n"
+ "diff": "Index: sdk/src/run.ts\n===================================================================\n--- sdk/src/run.ts\te9ce02a (parent)\n+++ sdk/src/run.ts\t844a6ca (commit)\n@@ -1,9 +1,9 @@\n import path from 'path'\n \n import { callMainPrompt } from '@codebirds/agent-runtime/main-prompt'\n import {\n- asUserMessage,\n+ buildUserMessageContent,\n getCancelledAdditionalMessages,\n } from '@codebirds/agent-runtime/util/messages'\n import { MAX_AGENT_STEPS_DEFAULT } from '@codebirds/common/constants/agents'\n import { getMCPClient, listMCPTools } from '@codebirds/common/mcp/client'\n@@ -14,9 +14,16 @@\n import { cloneDeep } from 'lodash'\n \n import { getAgentRuntimeImpl } from './impl/agent-runtime'\n import { getUserInfoFromApiKey } from './impl/database'\n-import { RETRYABLE_ERROR_CODES, isNetworkError, isPaymentRequiredError, ErrorCodes, NetworkError, sanitizeErrorMessage } from './errors'\n+import {\n+ RETRYABLE_ERROR_CODES,\n+ isNetworkError,\n+ isPaymentRequiredError,\n+ ErrorCodes,\n+ NetworkError,\n+ sanitizeErrorMessage,\n+} from './errors'\n import type { ErrorCode } from './errors'\n import { getErrorObject } from '@codebirds/common/util/error'\n import { initialSessionState, applyOverridesToSessionState } from './run-state'\n import {\n@@ -59,28 +66,20 @@\n import type { Source } from '@codebirds/common/types/source'\n import type { CodebirdsSpawn } from '@codebirds/common/types/spawn'\n import { ToolMessage } from '@codebirds/common/types/messages/codebirds-message'\n \n+/**\n+ * Wraps content for user messages, ensuring text is wrapped in tags.\n+ * Uses buildUserMessageContent from agent-runtime for consistency.\n+ */\n const wrapContentForUserMessage = (\n content?: (TextPart | ImagePart)[],\n ): (TextPart | ImagePart)[] | undefined => {\n if (!content || content.length === 0) {\n return content\n }\n- let hasWrappedText = false\n- return content.map((part) => {\n- if (part.type === 'text' && !hasWrappedText) {\n- hasWrappedText = true\n- const alreadyWrapped = part.text.includes('')\n- return alreadyWrapped\n- ? part\n- : {\n- ...part,\n- text: asUserMessage(part.text),\n- }\n- }\n- return part\n- })\n+ // Delegate to the shared utility which handles wrapping correctly\n+ return buildUserMessageContent(undefined, undefined, content)\n }\n \n export type CodebirdsClientOptions = {\n apiKey?: string\n@@ -287,11 +286,9 @@\n }\n type RunOnceOptions = Omit\n type RunReturnType = RunState\n \n-export async function run(\n- options: RunExecutionOptions,\n-): Promise {\n+export async function run(options: RunExecutionOptions): Promise {\n const { retry, abortController, ...rest } = options\n const retryOptions = normalizeRetryOptions(retry)\n \n // Prefer provided signal; otherwise reuse a shared controller across retries.\n@@ -837,9 +834,13 @@\n logger?.warn(\n {\n isNetworkError: isNetworkError(error),\n isPaymentRequired,\n- errorCode: isNetworkError(error) ? error.code : isPaymentRequired ? error.code : undefined,\n+ errorCode: isNetworkError(error)\n+ ? error.code\n+ : isPaymentRequired\n+ ? error.code\n+ : undefined,\n isRetryable,\n error: getErrorObject(error),\n },\n 'callMainPrompt caught error, checking if retryable',\n@@ -851,9 +852,10 @@\n return\n }\n \n // For non-retryable errors, resolve with cancelled state\n- const errorMessage = error instanceof Error ? error.message : String(error ?? '')\n+ const errorMessage =\n+ error instanceof Error ? error.message : String(error ?? '')\n resolve(getCancelledRunState(errorMessage))\n })\n \n return promise\n@@ -1003,14 +1005,19 @@\n /**\n * Extracts an error code from a prompt error message.\n * Returns the appropriate ErrorCode if the error is retryable, null otherwise.\n */\n-export const getRetryableErrorCode = (errorMessage: string): ErrorCode | null => {\n+export const getRetryableErrorCode = (\n+ errorMessage: string,\n+): ErrorCode | null => {\n const lowerMessage = errorMessage.toLowerCase()\n \n // AI SDK's built-in retry error (e.g., \"Failed after 4 attempts. Last error: Service Unavailable\")\n // The AI SDK already retried 4 times, but we still want our SDK wrapper to retry 3 more times\n- if (lowerMessage.includes('failed after') && lowerMessage.includes('attempts')) {\n+ if (\n+ lowerMessage.includes('failed after') &&\n+ lowerMessage.includes('attempts')\n+ ) {\n // Extract the underlying error type from the message\n if (lowerMessage.includes('service unavailable')) {\n return ErrorCodes.SERVICE_UNAVAILABLE\n }\n@@ -1023,24 +1030,38 @@\n // Default to SERVER_ERROR for other AI SDK retry failures\n return ErrorCodes.SERVER_ERROR\n }\n \n- if (errorMessage.includes('503') || lowerMessage.includes('service unavailable')) {\n+ if (\n+ errorMessage.includes('503') ||\n+ lowerMessage.includes('service unavailable')\n+ ) {\n return ErrorCodes.SERVICE_UNAVAILABLE\n }\n if (lowerMessage.includes('timeout')) {\n return ErrorCodes.TIMEOUT\n }\n- if (lowerMessage.includes('econnrefused') || lowerMessage.includes('connection refused')) {\n+ if (\n+ lowerMessage.includes('econnrefused') ||\n+ lowerMessage.includes('connection refused')\n+ ) {\n return ErrorCodes.CONNECTION_REFUSED\n }\n if (lowerMessage.includes('dns') || lowerMessage.includes('enotfound')) {\n return ErrorCodes.DNS_FAILURE\n }\n- if (lowerMessage.includes('server error') || lowerMessage.includes('500') || lowerMessage.includes('502') || lowerMessage.includes('504')) {\n+ if (\n+ lowerMessage.includes('server error') ||\n+ lowerMessage.includes('500') ||\n+ lowerMessage.includes('502') ||\n+ lowerMessage.includes('504')\n+ ) {\n return ErrorCodes.SERVER_ERROR\n }\n- if (lowerMessage.includes('network error') || lowerMessage.includes('fetch failed')) {\n+ if (\n+ lowerMessage.includes('network error') ||\n+ lowerMessage.includes('fetch failed')\n+ ) {\n return ErrorCodes.NETWORK_ERROR\n }\n \n return null\n"
}
]
},
@@ -647,7 +647,7 @@
"id": "refactor-send-message",
"sha": "d13f9375513ba480e1954192667c3dd54a161d74",
"parentSha": "1013a8af44ce9db6deb12d529384173f2548f59e",
- "spec": "Implement a CLI chat streaming refactor and type cleanups.\n\n1) Add new helper modules\n- File: cli/src/utils/create-event-handler-state.ts\n - Export type CreateEventHandlerStateParams with fields:\n - streamRefs: StreamController\n - setStreamingAgents: SetStreamingAgentsFn (from sdk-event-handlers)\n - setStreamStatus: SetStreamStatusFn (from hooks/use-message-queue)\n - aiMessageId: string\n - updater: MessageUpdater\n - hasReceivedContentRef: MutableRefObject\n - addActiveSubagent: (id: string) => void\n - removeActiveSubagent: (id: string) => void\n - agentMode: AgentMode\n - setHasReceivedPlanResponse: (value: boolean) => void\n - logger: Logger\n - setIsRetrying: (retrying: boolean) => void\n - onTotalCost?: (cost: number) => void\n - Export function createEventHandlerState(params): EventHandlerState that assembles and returns the EventHandlerState object with the streaming/message/subagents/mode slices, plus logger, setIsRetrying, and onTotalCost.\n\n- File: cli/src/utils/create-run-config.ts\n - Import retry constants from @codebuff/sdk: MAX_RETRIES_PER_MESSAGE, RETRY_BACKOFF_BASE_DELAY_MS, RETRY_BACKOFF_MAX_DELAY_MS\n - Import createEventHandler and createStreamChunkHandler from ./sdk-event-handlers\n - Export type CreateRunConfigParams with fields:\n - logger: Logger\n - agent: AgentDefinition | string\n - prompt: string\n - content: MessageContent[] | undefined\n - previousRunState: RunState | null\n - abortController: AbortController\n - agentDefinitions: AgentDefinition[]\n - eventHandlerState: EventHandlerState\n - setIsRetrying: (retrying: boolean) => void\n - setStreamStatus: (status: StreamStatus) => void\n - Export function createRunConfig(params): returns the config object for client.run with:\n - logger, agent, prompt, content\n - previousRun: previousRunState ?? undefined\n - abortController\n - retry: { maxRetries, backoffBaseMs, backoffMaxMs, onRetry, onRetryExhausted } where onRetry sets isRetrying true and stream status \"waiting\" and logs; onRetryExhausted logs\n - agentDefinitions\n - maxAgentSteps: 40\n - handleStreamChunk: createStreamChunkHandler(eventHandlerState)\n - handleEvent: createEventHandler(eventHandlerState)\n\n2) Rename EventHandlerContext to EventHandlerState and update usages\n- File: cli/src/utils/sdk-event-handlers.ts\n - Rename exported type EventHandlerContext to EventHandlerState\n - Change all function parameters and local variable names accordingly (ctx -> state) across ensureStreaming, appendRootChunk, updateStreamingAgents, handleTextEvent, handleSubagentStart, handleSubagentFinish, handleSpawnAgentsToolCall, handleRegularToolCall, handleToolCall, handleSpawnAgentsResult, handleToolResult, handleFinish, createStreamChunkHandler, createEventHandler.\n - No behavioral changes: the logic remains identical, just type/identifier rename and consistent parameter naming.\n\n- File: cli/src/utils/__tests__/sdk-event-handlers.test.ts\n - Update type imports: use EventHandlerState instead of EventHandlerContext\n - Update createStreamRefs return type to reference EventHandlerState['streaming']['streamRefs']\n - Make function types explicit where needed:\n - updater in createMessageUpdater: parameter typed (fn: (msgs: ChatMessage[]) => ChatMessage[])\n - setStreamingAgents: (fn: (prev: Set) => Set) => void\n - setStreamStatus: (status: StreamStatus) => void\n - setHasReceivedPlanResponse: (value: boolean) => void\n\n3) Refactor useSendMessage to use new helpers and store getters\n- File: cli/src/hooks/use-send-message.ts\n - Remove direct imports of MAX_RETRIES_PER_MESSAGE, RETRY_BACKOFF_BASE_DELAY_MS, RETRY_BACKOFF_MAX_DELAY_MS and remove direct createEventHandler/createStreamChunkHandler usage in client.run call.\n - Import createEventHandlerState from ../utils/create-event-handler-state and createRunConfig from ../utils/create-run-config.\n - Narrow UseSendMessageOptions: remove these options from the interface and from the destructuring usage:\n - setMessages, setFocusedAgentId, setInputFocused\n - setStreamingAgents, setActiveSubagents, setIsChainInProgress\n - setHasReceivedPlanResponse, lastMessageMode, setLastMessageMode\n - addSessionCredits, setRunState\n - Inside the hook, pull the above setters from useChatStore.getState() at the top:\n - setMessages, setFocusedAgentId, setInputFocused, setStreamingAgents, setActiveSubagents, setIsChainInProgress, setHasReceivedPlanResponse, setLastMessageMode, addSessionCredits, setRunState, setIsRetrying\n - Use comment to note these are stable references and intentionally accessed via getState().\n - Memoize streamRefs with useRef to maintain referential stability across renders:\n - const streamRefsRef = useRef | null>(null); initialize once and use streamRefsRef.current thereafter.\n - In prepareUserMessage callback, read lastMessageMode dynamically from useChatStore.getState() each invocation instead of capturing from props; adjust dependencies accordingly.\n - Replace inline eventContext construction with an eventHandlerState created by createEventHandlerState using current values (streamRefs, setters, aiMessageId, updater, hasReceivedContentRef, add/remove subagent, agentMode, setHasReceivedPlanResponse, logger, setIsRetrying, onTotalCost).\n - Replace inline client.run({ ... }) configuration with:\n - const runConfig = createRunConfig({ logger, agent: resolvedAgent, prompt: effectivePrompt, content: messageContent, previousRunState: previousRunStateRef.current, abortController, agentDefinitions, eventHandlerState, setIsRetrying, setStreamStatus })\n - const runState = await client.run(runConfig)\n - Keep behavior identical otherwise (persist runState, handle completion/errors, queue control, etc.).\n\n4) Update Chat component to match new useSendMessage signature\n- File: cli/src/chat.tsx\n - Stop selecting and passing the removed props to useSendMessage:\n - Remove setHasReceivedPlanResponse, lastMessageMode, setLastMessageMode, addSessionCredits, setRunState from the useShallow selector block and from the props passed to useSendMessage.\n - Pass only the still-required options to useSendMessage: inputRef, activeSubagentsRef, isChainInProgressRef, setStreamStatus, setCanProcessQueue, abortControllerRef, agentId, onBeforeMessageSend, mainAgentTimer, scrollToLatest, onTimerEvent (no-op), isQueuePausedRef, resumeQueue, continueChat, continueChatId.\n\n5) Tighten test mock types for input modes\n- File: cli/src/__tests__/bash-mode.test.ts\n - import type { InputValue } from '../state/chat-store'\n - Type setInputMode as mock((_mode: InputMode) => {})\n - Type setInputValue as mock((_value: Partial) => {})\n - Type runTerminalCommand as mock((_params: Record) => Promise.resolve([...]))\n\n- File: cli/src/__tests__/referral-mode.test.ts\n - Add helper type: type MockSetInputMode = (mode: InputMode) => void\n - Type setInputMode as mock((_mode) => {})\n - Type handleReferralCode as mock(async (_code: string) => {})\n - Type showError as mock((_msg: string) => {})\n\nAcceptance criteria\n- All TypeScript types compile in the CLI package.\n- Unit tests compile and run; bash-mode and referral-mode tests use typed mocks as specified; sdk-event-handlers tests compile with the renamed EventHandlerState.\n- The Chat UI behavior remains unchanged: streaming, plan extraction, subagent visualization, tool call handling, and queue control all function as before.\n- useSendMessage no longer requires or accepts store setter props; it reads them internally via useChatStore.getState().\n- Stream controller instance remains stable across renders via useRef.",
+ "spec": "Implement a CLI chat streaming refactor and type cleanups.\n\n1) Add new helper modules\n- File: cli/src/utils/create-event-handler-state.ts\n - Export type CreateEventHandlerStateParams with fields:\n - streamRefs: StreamController\n - setStreamingAgents: SetStreamingAgentsFn (from sdk-event-handlers)\n - setStreamStatus: SetStreamStatusFn (from hooks/use-message-queue)\n - aiMessageId: string\n - updater: MessageUpdater\n - hasReceivedContentRef: MutableRefObject\n - addActiveSubagent: (id: string) => void\n - removeActiveSubagent: (id: string) => void\n - agentMode: AgentMode\n - setHasReceivedPlanResponse: (value: boolean) => void\n - logger: Logger\n - setIsRetrying: (retrying: boolean) => void\n - onTotalCost?: (cost: number) => void\n - Export function createEventHandlerState(params): EventHandlerState that assembles and returns the EventHandlerState object with the streaming/message/subagents/mode slices, plus logger, setIsRetrying, and onTotalCost.\n\n- File: cli/src/utils/create-run-config.ts\n - Import retry constants from @codebirds/sdk: MAX_RETRIES_PER_MESSAGE, RETRY_BACKOFF_BASE_DELAY_MS, RETRY_BACKOFF_MAX_DELAY_MS\n - Import createEventHandler and createStreamChunkHandler from ./sdk-event-handlers\n - Export type CreateRunConfigParams with fields:\n - logger: Logger\n - agent: AgentDefinition | string\n - prompt: string\n - content: MessageContent[] | undefined\n - previousRunState: RunState | null\n - abortController: AbortController\n - agentDefinitions: AgentDefinition[]\n - eventHandlerState: EventHandlerState\n - setIsRetrying: (retrying: boolean) => void\n - setStreamStatus: (status: StreamStatus) => void\n - Export function createRunConfig(params): returns the config object for client.run with:\n - logger, agent, prompt, content\n - previousRun: previousRunState ?? undefined\n - abortController\n - retry: { maxRetries, backoffBaseMs, backoffMaxMs, onRetry, onRetryExhausted } where onRetry sets isRetrying true and stream status \"waiting\" and logs; onRetryExhausted logs\n - agentDefinitions\n - maxAgentSteps: 40\n - handleStreamChunk: createStreamChunkHandler(eventHandlerState)\n - handleEvent: createEventHandler(eventHandlerState)\n\n2) Rename EventHandlerContext to EventHandlerState and update usages\n- File: cli/src/utils/sdk-event-handlers.ts\n - Rename exported type EventHandlerContext to EventHandlerState\n - Change all function parameters and local variable names accordingly (ctx -> state) across ensureStreaming, appendRootChunk, updateStreamingAgents, handleTextEvent, handleSubagentStart, handleSubagentFinish, handleSpawnAgentsToolCall, handleRegularToolCall, handleToolCall, handleSpawnAgentsResult, handleToolResult, handleFinish, createStreamChunkHandler, createEventHandler.\n - No behavioral changes: the logic remains identical, just type/identifier rename and consistent parameter naming.\n\n- File: cli/src/utils/__tests__/sdk-event-handlers.test.ts\n - Update type imports: use EventHandlerState instead of EventHandlerContext\n - Update createStreamRefs return type to reference EventHandlerState['streaming']['streamRefs']\n - Make function types explicit where needed:\n - updater in createMessageUpdater: parameter typed (fn: (msgs: ChatMessage[]) => ChatMessage[])\n - setStreamingAgents: (fn: (prev: Set) => Set) => void\n - setStreamStatus: (status: StreamStatus) => void\n - setHasReceivedPlanResponse: (value: boolean) => void\n\n3) Refactor useSendMessage to use new helpers and store getters\n- File: cli/src/hooks/use-send-message.ts\n - Remove direct imports of MAX_RETRIES_PER_MESSAGE, RETRY_BACKOFF_BASE_DELAY_MS, RETRY_BACKOFF_MAX_DELAY_MS and remove direct createEventHandler/createStreamChunkHandler usage in client.run call.\n - Import createEventHandlerState from ../utils/create-event-handler-state and createRunConfig from ../utils/create-run-config.\n - Narrow UseSendMessageOptions: remove these options from the interface and from the destructuring usage:\n - setMessages, setFocusedAgentId, setInputFocused\n - setStreamingAgents, setActiveSubagents, setIsChainInProgress\n - setHasReceivedPlanResponse, lastMessageMode, setLastMessageMode\n - addSessionCredits, setRunState\n - Inside the hook, pull the above setters from useChatStore.getState() at the top:\n - setMessages, setFocusedAgentId, setInputFocused, setStreamingAgents, setActiveSubagents, setIsChainInProgress, setHasReceivedPlanResponse, setLastMessageMode, addSessionCredits, setRunState, setIsRetrying\n - Use comment to note these are stable references and intentionally accessed via getState().\n - Memoize streamRefs with useRef to maintain referential stability across renders:\n - const streamRefsRef = useRef | null>(null); initialize once and use streamRefsRef.current thereafter.\n - In prepareUserMessage callback, read lastMessageMode dynamically from useChatStore.getState() each invocation instead of capturing from props; adjust dependencies accordingly.\n - Replace inline eventContext construction with an eventHandlerState created by createEventHandlerState using current values (streamRefs, setters, aiMessageId, updater, hasReceivedContentRef, add/remove subagent, agentMode, setHasReceivedPlanResponse, logger, setIsRetrying, onTotalCost).\n - Replace inline client.run({ ... }) configuration with:\n - const runConfig = createRunConfig({ logger, agent: resolvedAgent, prompt: effectivePrompt, content: messageContent, previousRunState: previousRunStateRef.current, abortController, agentDefinitions, eventHandlerState, setIsRetrying, setStreamStatus })\n - const runState = await client.run(runConfig)\n - Keep behavior identical otherwise (persist runState, handle completion/errors, queue control, etc.).\n\n4) Update Chat component to match new useSendMessage signature\n- File: cli/src/chat.tsx\n - Stop selecting and passing the removed props to useSendMessage:\n - Remove setHasReceivedPlanResponse, lastMessageMode, setLastMessageMode, addSessionCredits, setRunState from the useShallow selector block and from the props passed to useSendMessage.\n - Pass only the still-required options to useSendMessage: inputRef, activeSubagentsRef, isChainInProgressRef, setStreamStatus, setCanProcessQueue, abortControllerRef, agentId, onBeforeMessageSend, mainAgentTimer, scrollToLatest, onTimerEvent (no-op), isQueuePausedRef, resumeQueue, continueChat, continueChatId.\n\n5) Tighten test mock types for input modes\n- File: cli/src/__tests__/bash-mode.test.ts\n - import type { InputValue } from '../state/chat-store'\n - Type setInputMode as mock((_mode: InputMode) => {})\n - Type setInputValue as mock((_value: Partial) => {})\n - Type runTerminalCommand as mock((_params: Record) => Promise.resolve([...]))\n\n- File: cli/src/__tests__/referral-mode.test.ts\n - Add helper type: type MockSetInputMode = (mode: InputMode) => void\n - Type setInputMode as mock((_mode) => {})\n - Type handleReferralCode as mock(async (_code: string) => {})\n - Type showError as mock((_msg: string) => {})\n\nAcceptance criteria\n- All TypeScript types compile in the CLI package.\n- Unit tests compile and run; bash-mode and referral-mode tests use typed mocks as specified; sdk-event-handlers tests compile with the renamed EventHandlerState.\n- The Chat UI behavior remains unchanged: streaming, plan extraction, subagent visualization, tool call handling, and queue control all function as before.\n- useSendMessage no longer requires or accepts store setter props; it reads them internally via useChatStore.getState().\n- Stream controller instance remains stable across renders via useRef.",
"prompt": "Refactor the CLI chat streaming pipeline to improve type safety and reduce prop drilling without changing runtime behavior.\n\nGoals:\n- Extract a helper that builds the event handler state passed to SDK stream/event handlers.\n- Extract a helper that builds the SDK run configuration (including retries and handler wiring).\n- Rename the existing event handler context type to a clearer name and update its usage across handlers and tests.\n- Update the chat send hook to use the new helpers, pull store setters directly from the Zustand store, and keep the stream controller stable across renders.\n- Clean up the Chat component to stop passing now-internal setters to the hook.\n- Tighten mock function types in the bash and referral mode tests to match real signatures.\n\nDo not change observable behavior; only restructure and type improvements. Ensure all tests compile and pass.",
"supplementalFiles": [
"cli/src/hooks/helpers/send-message.ts",
@@ -676,7 +676,7 @@
{
"path": "cli/src/hooks/use-send-message.ts",
"status": "modified",
- "diff": "Index: cli/src/hooks/use-send-message.ts\n===================================================================\n--- cli/src/hooks/use-send-message.ts\t1013a8a (parent)\n+++ cli/src/hooks/use-send-message.ts\td13f937 (commit)\n@@ -1,28 +1,21 @@\n-import {\n- MAX_RETRIES_PER_MESSAGE,\n- RETRY_BACKOFF_BASE_DELAY_MS,\n- RETRY_BACKOFF_MAX_DELAY_MS,\n-} from '@codebuff/sdk'\n import { useQueryClient } from '@tanstack/react-query'\n import { useCallback, useEffect, useRef } from 'react'\n import { match } from 'ts-pattern'\n \n import { setCurrentChatId } from '../project-files'\n import { createStreamController } from './stream-state'\n import { useChatStore } from '../state/chat-store'\n import { getCodebuffClient } from '../utils/codebuff-client'\n+import { createEventHandlerState } from '../utils/create-event-handler-state'\n+import { createRunConfig } from '../utils/create-run-config'\n import { loadAgentDefinitions } from '../utils/load-agent-definitions'\n import { logger } from '../utils/logger'\n import {\n loadMostRecentChatState,\n saveChatState,\n } from '../utils/run-state-storage'\n import {\n- createEventHandler,\n- createStreamChunkHandler,\n-} from '../utils/sdk-event-handlers'\n-import {\n autoCollapsePreviousMessages,\n createAiMessageShell,\n createErrorMessage as createErrorChatMessage,\n generateAiMessageId,\n@@ -51,17 +44,11 @@\n setTimeout(resolve, 0)\n })\n \n interface UseSendMessageOptions {\n- setMessages: React.Dispatch>\n- setFocusedAgentId: (id: string | null) => void\n- setInputFocused: (focused: boolean) => void\n inputRef: React.MutableRefObject\n- setStreamingAgents: React.Dispatch>>\n activeSubagentsRef: React.MutableRefObject>\n isChainInProgressRef: React.MutableRefObject\n- setActiveSubagents: React.Dispatch>>\n- setIsChainInProgress: (value: boolean) => void\n setStreamStatus: (status: StreamStatus) => void\n setCanProcessQueue: (can: boolean) => void\n abortControllerRef: React.MutableRefObject\n agentId?: string\n@@ -71,13 +58,8 @@\n }>\n mainAgentTimer: ElapsedTimeTracker\n scrollToLatest: () => void\n onTimerEvent?: (event: SendMessageTimerEvent) => void\n- setHasReceivedPlanResponse: (value: boolean) => void\n- lastMessageMode: AgentMode | null\n- setLastMessageMode: (mode: AgentMode | null) => void\n- addSessionCredits: (credits: number) => void\n- setRunState: (runState: RunState | null) => void\n isQueuePausedRef?: React.MutableRefObject\n resumeQueue?: () => void\n continueChat: boolean\n continueChatId?: string\n@@ -120,30 +102,19 @@\n return ''\n }\n \n export const useSendMessage = ({\n- setMessages,\n- setFocusedAgentId,\n- setInputFocused,\n inputRef,\n- setStreamingAgents,\n activeSubagentsRef,\n isChainInProgressRef,\n- setActiveSubagents,\n- setIsChainInProgress,\n setStreamStatus,\n setCanProcessQueue,\n abortControllerRef,\n agentId,\n onBeforeMessageSend,\n mainAgentTimer,\n scrollToLatest,\n onTimerEvent = () => {},\n- setHasReceivedPlanResponse,\n- lastMessageMode,\n- setLastMessageMode,\n- addSessionCredits,\n- setRunState,\n isQueuePausedRef,\n resumeQueue,\n continueChat,\n continueChatId,\n@@ -151,11 +122,32 @@\n sendMessage: SendMessageFn\n clearMessages: () => void\n } => {\n const queryClient = useQueryClient()\n- const setIsRetrying = useChatStore.getState().setIsRetrying\n+ // Pull setters directly from store - these are stable references that don't need\n+ // to trigger re-renders, so using getState() outside of callbacks is intentional.\n+ const {\n+ setMessages,\n+ setFocusedAgentId,\n+ setInputFocused,\n+ setStreamingAgents,\n+ setActiveSubagents,\n+ setIsChainInProgress,\n+ setHasReceivedPlanResponse,\n+ setLastMessageMode,\n+ addSessionCredits,\n+ setRunState,\n+ setIsRetrying,\n+ } = useChatStore.getState()\n const previousRunStateRef = useRef(null)\n- const streamRefs = createStreamController()\n+ // Memoize stream controller to maintain referential stability across renders\n+ const streamRefsRef = useRef | null>(null)\n+ if (!streamRefsRef.current) {\n+ streamRefsRef.current = createStreamController()\n+ }\n+ const streamRefs = streamRefsRef.current\n \n useEffect(() => {\n if (continueChat && !previousRunStateRef.current) {\n const loadedState = loadMostRecentChatState(continueChatId ?? undefined)\n@@ -190,22 +182,21 @@\n [setActiveSubagents, activeSubagentsRef],\n )\n \n const addActiveSubagent = useCallback(\n- (agentId: string) => {\n- updateActiveSubagents((next) => next.add(agentId))\n+ (subagentId: string) => {\n+ updateActiveSubagents((next) => next.add(subagentId))\n },\n [updateActiveSubagents],\n )\n \n const removeActiveSubagent = useCallback(\n- (agentId: string) => {\n- updateActiveSubagents((next) => next.delete(agentId))\n+ (subagentId: string) => {\n+ updateActiveSubagents((next) => next.delete(subagentId))\n },\n [updateActiveSubagents],\n )\n \n-\n function clearMessages() {\n previousRunStateRef.current = null\n }\n \n@@ -215,8 +206,10 @@\n agentMode: AgentMode\n postUserMessage?: (prev: ChatMessage[]) => ChatMessage[]\n attachedImages?: PendingImage[]\n }) => {\n+ // Access lastMessageMode fresh each call to get current value\n+ const { lastMessageMode } = useChatStore.getState()\n return prepareUserMessageHelper({\n ...params,\n deps: {\n setMessages,\n@@ -226,13 +219,14 @@\n setHasReceivedPlanResponse,\n },\n })\n },\n+ // Note: lastMessageMode is accessed via getState() inside the callback,\n+ // so it always gets the fresh value - no need to include in deps\n [\n setMessages,\n- lastMessageMode,\n- scrollToLatest,\n setLastMessageMode,\n+ scrollToLatest,\n setHasReceivedPlanResponse,\n ],\n )\n \n@@ -359,67 +353,42 @@\n promptWithBashContext,\n messageContent,\n )\n \n- const eventContext = {\n- streaming: {\n- streamRefs,\n- setStreamingAgents,\n- setStreamStatus,\n- },\n- message: {\n- aiMessageId,\n- updater,\n- hasReceivedContentRef,\n- },\n- subagents: {\n- addActiveSubagent,\n- removeActiveSubagent,\n- },\n- mode: {\n- agentMode,\n- setHasReceivedPlanResponse,\n- },\n+ const eventHandlerState = createEventHandlerState({\n+ streamRefs,\n+ setStreamingAgents,\n+ setStreamStatus,\n+ aiMessageId,\n+ updater,\n+ hasReceivedContentRef,\n+ addActiveSubagent,\n+ removeActiveSubagent,\n+ agentMode,\n+ setHasReceivedPlanResponse,\n logger,\n setIsRetrying,\n onTotalCost: (cost: number) => {\n actualCredits = cost\n addSessionCredits(cost)\n },\n- }\n+ })\n \n- const runState = await client.run({\n+ const runConfig = createRunConfig({\n logger,\n agent: resolvedAgent,\n prompt: effectivePrompt,\n content: messageContent,\n- previousRun: previousRunStateRef.current ?? undefined,\n+ previousRunState: previousRunStateRef.current,\n abortController,\n- retry: {\n- maxRetries: MAX_RETRIES_PER_MESSAGE,\n- backoffBaseMs: RETRY_BACKOFF_BASE_DELAY_MS,\n- backoffMaxMs: RETRY_BACKOFF_MAX_DELAY_MS,\n- onRetry: async ({ attempt, delayMs, errorCode }) => {\n- logger.warn(\n- { sdkAttempt: attempt, delayMs, errorCode },\n- 'SDK retrying after error',\n- )\n- setIsRetrying(true)\n- setStreamStatus('waiting')\n- },\n- onRetryExhausted: async ({ totalAttempts, errorCode }) => {\n- logger.warn(\n- { totalAttempts, errorCode },\n- 'SDK exhausted all retries',\n- )\n- },\n- },\n agentDefinitions,\n- maxAgentSteps: 40,\n- handleStreamChunk: createStreamChunkHandler(eventContext),\n- handleEvent: createEventHandler(eventContext),\n+ eventHandlerState,\n+ setIsRetrying,\n+ setStreamStatus,\n })\n \n+ const runState = await client.run(runConfig)\n+\n // Finalize: persist state and mark complete\n previousRunStateRef.current = runState\n setRunState(runState)\n setIsRetrying(false)\n@@ -463,10 +432,10 @@\n agentId,\n inputRef,\n isQueuePausedRef,\n mainAgentTimer,\n- onTimerEvent,\n onBeforeMessageSend,\n+ onTimerEvent,\n prepareUserMessage,\n queryClient,\n removeActiveSubagent,\n resumeQueue,\n@@ -478,11 +447,11 @@\n setIsRetrying,\n setMessages,\n setRunState,\n setStreamStatus,\n+ setStreamingAgents,\n streamRefs,\n updateChainInProgress,\n- setStreamingAgents,\n ],\n )\n \n return {\n"
+ "diff": "Index: cli/src/hooks/use-send-message.ts\n===================================================================\n--- cli/src/hooks/use-send-message.ts\t1013a8a (parent)\n+++ cli/src/hooks/use-send-message.ts\td13f937 (commit)\n@@ -1,28 +1,21 @@\n-import {\n- MAX_RETRIES_PER_MESSAGE,\n- RETRY_BACKOFF_BASE_DELAY_MS,\n- RETRY_BACKOFF_MAX_DELAY_MS,\n-} from '@codebirds/sdk'\n import { useQueryClient } from '@tanstack/react-query'\n import { useCallback, useEffect, useRef } from 'react'\n import { match } from 'ts-pattern'\n \n import { setCurrentChatId } from '../project-files'\n import { createStreamController } from './stream-state'\n import { useChatStore } from '../state/chat-store'\n import { getCodebirdsClient } from '../utils/codebirds-client'\n+import { createEventHandlerState } from '../utils/create-event-handler-state'\n+import { createRunConfig } from '../utils/create-run-config'\n import { loadAgentDefinitions } from '../utils/load-agent-definitions'\n import { logger } from '../utils/logger'\n import {\n loadMostRecentChatState,\n saveChatState,\n } from '../utils/run-state-storage'\n import {\n- createEventHandler,\n- createStreamChunkHandler,\n-} from '../utils/sdk-event-handlers'\n-import {\n autoCollapsePreviousMessages,\n createAiMessageShell,\n createErrorMessage as createErrorChatMessage,\n generateAiMessageId,\n@@ -51,17 +44,11 @@\n setTimeout(resolve, 0)\n })\n \n interface UseSendMessageOptions {\n- setMessages: React.Dispatch>\n- setFocusedAgentId: (id: string | null) => void\n- setInputFocused: (focused: boolean) => void\n inputRef: React.MutableRefObject\n- setStreamingAgents: React.Dispatch>>\n activeSubagentsRef: React.MutableRefObject>\n isChainInProgressRef: React.MutableRefObject\n- setActiveSubagents: React.Dispatch>>\n- setIsChainInProgress: (value: boolean) => void\n setStreamStatus: (status: StreamStatus) => void\n setCanProcessQueue: (can: boolean) => void\n abortControllerRef: React.MutableRefObject\n agentId?: string\n@@ -71,13 +58,8 @@\n }>\n mainAgentTimer: ElapsedTimeTracker\n scrollToLatest: () => void\n onTimerEvent?: (event: SendMessageTimerEvent) => void\n- setHasReceivedPlanResponse: (value: boolean) => void\n- lastMessageMode: AgentMode | null\n- setLastMessageMode: (mode: AgentMode | null) => void\n- addSessionCredits: (credits: number) => void\n- setRunState: (runState: RunState | null) => void\n isQueuePausedRef?: React.MutableRefObject\n resumeQueue?: () => void\n continueChat: boolean\n continueChatId?: string\n@@ -120,30 +102,19 @@\n return ''\n }\n \n export const useSendMessage = ({\n- setMessages,\n- setFocusedAgentId,\n- setInputFocused,\n inputRef,\n- setStreamingAgents,\n activeSubagentsRef,\n isChainInProgressRef,\n- setActiveSubagents,\n- setIsChainInProgress,\n setStreamStatus,\n setCanProcessQueue,\n abortControllerRef,\n agentId,\n onBeforeMessageSend,\n mainAgentTimer,\n scrollToLatest,\n onTimerEvent = () => {},\n- setHasReceivedPlanResponse,\n- lastMessageMode,\n- setLastMessageMode,\n- addSessionCredits,\n- setRunState,\n isQueuePausedRef,\n resumeQueue,\n continueChat,\n continueChatId,\n@@ -151,11 +122,32 @@\n sendMessage: SendMessageFn\n clearMessages: () => void\n } => {\n const queryClient = useQueryClient()\n- const setIsRetrying = useChatStore.getState().setIsRetrying\n+ // Pull setters directly from store - these are stable references that don't need\n+ // to trigger re-renders, so using getState() outside of callbacks is intentional.\n+ const {\n+ setMessages,\n+ setFocusedAgentId,\n+ setInputFocused,\n+ setStreamingAgents,\n+ setActiveSubagents,\n+ setIsChainInProgress,\n+ setHasReceivedPlanResponse,\n+ setLastMessageMode,\n+ addSessionCredits,\n+ setRunState,\n+ setIsRetrying,\n+ } = useChatStore.getState()\n const previousRunStateRef = useRef(null)\n- const streamRefs = createStreamController()\n+ // Memoize stream controller to maintain referential stability across renders\n+ const streamRefsRef = useRef | null>(null)\n+ if (!streamRefsRef.current) {\n+ streamRefsRef.current = createStreamController()\n+ }\n+ const streamRefs = streamRefsRef.current\n \n useEffect(() => {\n if (continueChat && !previousRunStateRef.current) {\n const loadedState = loadMostRecentChatState(continueChatId ?? undefined)\n@@ -190,22 +182,21 @@\n [setActiveSubagents, activeSubagentsRef],\n )\n \n const addActiveSubagent = useCallback(\n- (agentId: string) => {\n- updateActiveSubagents((next) => next.add(agentId))\n+ (subagentId: string) => {\n+ updateActiveSubagents((next) => next.add(subagentId))\n },\n [updateActiveSubagents],\n )\n \n const removeActiveSubagent = useCallback(\n- (agentId: string) => {\n- updateActiveSubagents((next) => next.delete(agentId))\n+ (subagentId: string) => {\n+ updateActiveSubagents((next) => next.delete(subagentId))\n },\n [updateActiveSubagents],\n )\n \n-\n function clearMessages() {\n previousRunStateRef.current = null\n }\n \n@@ -215,8 +206,10 @@\n agentMode: AgentMode\n postUserMessage?: (prev: ChatMessage[]) => ChatMessage[]\n attachedImages?: PendingImage[]\n }) => {\n+ // Access lastMessageMode fresh each call to get current value\n+ const { lastMessageMode } = useChatStore.getState()\n return prepareUserMessageHelper({\n ...params,\n deps: {\n setMessages,\n@@ -226,13 +219,14 @@\n setHasReceivedPlanResponse,\n },\n })\n },\n+ // Note: lastMessageMode is accessed via getState() inside the callback,\n+ // so it always gets the fresh value - no need to include in deps\n [\n setMessages,\n- lastMessageMode,\n- scrollToLatest,\n setLastMessageMode,\n+ scrollToLatest,\n setHasReceivedPlanResponse,\n ],\n )\n \n@@ -359,67 +353,42 @@\n promptWithBashContext,\n messageContent,\n )\n \n- const eventContext = {\n- streaming: {\n- streamRefs,\n- setStreamingAgents,\n- setStreamStatus,\n- },\n- message: {\n- aiMessageId,\n- updater,\n- hasReceivedContentRef,\n- },\n- subagents: {\n- addActiveSubagent,\n- removeActiveSubagent,\n- },\n- mode: {\n- agentMode,\n- setHasReceivedPlanResponse,\n- },\n+ const eventHandlerState = createEventHandlerState({\n+ streamRefs,\n+ setStreamingAgents,\n+ setStreamStatus,\n+ aiMessageId,\n+ updater,\n+ hasReceivedContentRef,\n+ addActiveSubagent,\n+ removeActiveSubagent,\n+ agentMode,\n+ setHasReceivedPlanResponse,\n logger,\n setIsRetrying,\n onTotalCost: (cost: number) => {\n actualCredits = cost\n addSessionCredits(cost)\n },\n- }\n+ })\n \n- const runState = await client.run({\n+ const runConfig = createRunConfig({\n logger,\n agent: resolvedAgent,\n prompt: effectivePrompt,\n content: messageContent,\n- previousRun: previousRunStateRef.current ?? undefined,\n+ previousRunState: previousRunStateRef.current,\n abortController,\n- retry: {\n- maxRetries: MAX_RETRIES_PER_MESSAGE,\n- backoffBaseMs: RETRY_BACKOFF_BASE_DELAY_MS,\n- backoffMaxMs: RETRY_BACKOFF_MAX_DELAY_MS,\n- onRetry: async ({ attempt, delayMs, errorCode }) => {\n- logger.warn(\n- { sdkAttempt: attempt, delayMs, errorCode },\n- 'SDK retrying after error',\n- )\n- setIsRetrying(true)\n- setStreamStatus('waiting')\n- },\n- onRetryExhausted: async ({ totalAttempts, errorCode }) => {\n- logger.warn(\n- { totalAttempts, errorCode },\n- 'SDK exhausted all retries',\n- )\n- },\n- },\n agentDefinitions,\n- maxAgentSteps: 40,\n- handleStreamChunk: createStreamChunkHandler(eventContext),\n- handleEvent: createEventHandler(eventContext),\n+ eventHandlerState,\n+ setIsRetrying,\n+ setStreamStatus,\n })\n \n+ const runState = await client.run(runConfig)\n+\n // Finalize: persist state and mark complete\n previousRunStateRef.current = runState\n setRunState(runState)\n setIsRetrying(false)\n@@ -463,10 +432,10 @@\n agentId,\n inputRef,\n isQueuePausedRef,\n mainAgentTimer,\n- onTimerEvent,\n onBeforeMessageSend,\n+ onTimerEvent,\n prepareUserMessage,\n queryClient,\n removeActiveSubagent,\n resumeQueue,\n@@ -478,11 +447,11 @@\n setIsRetrying,\n setMessages,\n setRunState,\n setStreamStatus,\n+ setStreamingAgents,\n streamRefs,\n updateChainInProgress,\n- setStreamingAgents,\n ],\n )\n \n return {\n"
},
{
"path": "cli/src/utils/__tests__/sdk-event-handlers.test.ts",
@@ -686,12 +686,12 @@
{
"path": "cli/src/utils/create-event-handler-state.ts",
"status": "added",
- "diff": "Index: cli/src/utils/create-event-handler-state.ts\n===================================================================\n--- cli/src/utils/create-event-handler-state.ts\t1013a8a (parent)\n+++ cli/src/utils/create-event-handler-state.ts\td13f937 (commit)\n@@ -0,0 +1,70 @@\n+import type { AgentMode } from './constants'\n+import type {\n+ EventHandlerState,\n+ SetStreamingAgentsFn,\n+ SetStreamStatusFn,\n+} from './sdk-event-handlers'\n+import type { MessageUpdater } from './message-updater'\n+import type { StreamController } from '../hooks/stream-state'\n+import type { Logger } from '@codebuff/common/types/contracts/logger'\n+import type { MutableRefObject } from 'react'\n+\n+export type CreateEventHandlerStateParams = {\n+ streamRefs: StreamController\n+ setStreamingAgents: SetStreamingAgentsFn\n+ setStreamStatus: SetStreamStatusFn\n+ aiMessageId: string\n+ updater: MessageUpdater\n+ hasReceivedContentRef: MutableRefObject\n+ addActiveSubagent: (id: string) => void\n+ removeActiveSubagent: (id: string) => void\n+ agentMode: AgentMode\n+ setHasReceivedPlanResponse: (value: boolean) => void\n+ logger: Logger\n+ setIsRetrying: (retrying: boolean) => void\n+ onTotalCost?: (cost: number) => void\n+}\n+\n+export const createEventHandlerState = (\n+ params: CreateEventHandlerStateParams,\n+): EventHandlerState => {\n+ const {\n+ streamRefs,\n+ setStreamingAgents,\n+ setStreamStatus,\n+ aiMessageId,\n+ updater,\n+ hasReceivedContentRef,\n+ addActiveSubagent,\n+ removeActiveSubagent,\n+ agentMode,\n+ setHasReceivedPlanResponse,\n+ logger,\n+ setIsRetrying,\n+ onTotalCost,\n+ } = params\n+\n+ return {\n+ streaming: {\n+ streamRefs,\n+ setStreamingAgents,\n+ setStreamStatus,\n+ },\n+ message: {\n+ aiMessageId,\n+ updater,\n+ hasReceivedContentRef,\n+ },\n+ subagents: {\n+ addActiveSubagent,\n+ removeActiveSubagent,\n+ },\n+ mode: {\n+ agentMode,\n+ setHasReceivedPlanResponse,\n+ },\n+ logger,\n+ setIsRetrying,\n+ onTotalCost,\n+ }\n+}\n"
+ "diff": "Index: cli/src/utils/create-event-handler-state.ts\n===================================================================\n--- cli/src/utils/create-event-handler-state.ts\t1013a8a (parent)\n+++ cli/src/utils/create-event-handler-state.ts\td13f937 (commit)\n@@ -0,0 +1,70 @@\n+import type { AgentMode } from './constants'\n+import type {\n+ EventHandlerState,\n+ SetStreamingAgentsFn,\n+ SetStreamStatusFn,\n+} from './sdk-event-handlers'\n+import type { MessageUpdater } from './message-updater'\n+import type { StreamController } from '../hooks/stream-state'\n+import type { Logger } from '@codebirds/common/types/contracts/logger'\n+import type { MutableRefObject } from 'react'\n+\n+export type CreateEventHandlerStateParams = {\n+ streamRefs: StreamController\n+ setStreamingAgents: SetStreamingAgentsFn\n+ setStreamStatus: SetStreamStatusFn\n+ aiMessageId: string\n+ updater: MessageUpdater\n+ hasReceivedContentRef: MutableRefObject\n+ addActiveSubagent: (id: string) => void\n+ removeActiveSubagent: (id: string) => void\n+ agentMode: AgentMode\n+ setHasReceivedPlanResponse: (value: boolean) => void\n+ logger: Logger\n+ setIsRetrying: (retrying: boolean) => void\n+ onTotalCost?: (cost: number) => void\n+}\n+\n+export const createEventHandlerState = (\n+ params: CreateEventHandlerStateParams,\n+): EventHandlerState => {\n+ const {\n+ streamRefs,\n+ setStreamingAgents,\n+ setStreamStatus,\n+ aiMessageId,\n+ updater,\n+ hasReceivedContentRef,\n+ addActiveSubagent,\n+ removeActiveSubagent,\n+ agentMode,\n+ setHasReceivedPlanResponse,\n+ logger,\n+ setIsRetrying,\n+ onTotalCost,\n+ } = params\n+\n+ return {\n+ streaming: {\n+ streamRefs,\n+ setStreamingAgents,\n+ setStreamStatus,\n+ },\n+ message: {\n+ aiMessageId,\n+ updater,\n+ hasReceivedContentRef,\n+ },\n+ subagents: {\n+ addActiveSubagent,\n+ removeActiveSubagent,\n+ },\n+ mode: {\n+ agentMode,\n+ setHasReceivedPlanResponse,\n+ },\n+ logger,\n+ setIsRetrying,\n+ onTotalCost,\n+ }\n+}\n"
},
{
"path": "cli/src/utils/create-run-config.ts",
"status": "added",
- "diff": "Index: cli/src/utils/create-run-config.ts\n===================================================================\n--- cli/src/utils/create-run-config.ts\t1013a8a (parent)\n+++ cli/src/utils/create-run-config.ts\td13f937 (commit)\n@@ -0,0 +1,83 @@\n+import {\n+ MAX_RETRIES_PER_MESSAGE,\n+ RETRY_BACKOFF_BASE_DELAY_MS,\n+ RETRY_BACKOFF_MAX_DELAY_MS,\n+} from '@codebuff/sdk'\n+\n+import { createEventHandler, createStreamChunkHandler } from './sdk-event-handlers'\n+\n+import type { EventHandlerState } from './sdk-event-handlers'\n+import type { AgentDefinition, MessageContent, RunState } from '@codebuff/sdk'\n+import type { Logger } from '@codebuff/common/types/contracts/logger'\n+import type { StreamStatus } from '../hooks/use-message-queue'\n+\n+export type CreateRunConfigParams = {\n+ logger: Logger\n+ agent: AgentDefinition | string\n+ prompt: string\n+ content: MessageContent[] | undefined\n+ previousRunState: RunState | null\n+ abortController: AbortController\n+ agentDefinitions: AgentDefinition[]\n+ eventHandlerState: EventHandlerState\n+ setIsRetrying: (retrying: boolean) => void\n+ setStreamStatus: (status: StreamStatus) => void\n+}\n+\n+type RetryArgs = {\n+ attempt: number\n+ delayMs: number\n+ errorCode?: string\n+}\n+\n+type RetryExhaustedArgs = {\n+ totalAttempts: number\n+ errorCode?: string\n+}\n+\n+export const createRunConfig = (params: CreateRunConfigParams) => {\n+ const {\n+ logger,\n+ agent,\n+ prompt,\n+ content,\n+ previousRunState,\n+ abortController,\n+ agentDefinitions,\n+ eventHandlerState,\n+ setIsRetrying,\n+ setStreamStatus,\n+ } = params\n+\n+ return {\n+ logger,\n+ agent,\n+ prompt,\n+ content,\n+ previousRun: previousRunState ?? undefined,\n+ abortController,\n+ retry: {\n+ maxRetries: MAX_RETRIES_PER_MESSAGE,\n+ backoffBaseMs: RETRY_BACKOFF_BASE_DELAY_MS,\n+ backoffMaxMs: RETRY_BACKOFF_MAX_DELAY_MS,\n+ onRetry: async ({ attempt, delayMs, errorCode }: RetryArgs) => {\n+ logger.warn(\n+ { sdkAttempt: attempt, delayMs, errorCode },\n+ 'SDK retrying after error',\n+ )\n+ setIsRetrying(true)\n+ setStreamStatus('waiting')\n+ },\n+ onRetryExhausted: async ({ totalAttempts, errorCode }: RetryExhaustedArgs) => {\n+ logger.warn(\n+ { totalAttempts, errorCode },\n+ 'SDK exhausted all retries',\n+ )\n+ },\n+ },\n+ agentDefinitions,\n+ maxAgentSteps: 40,\n+ handleStreamChunk: createStreamChunkHandler(eventHandlerState),\n+ handleEvent: createEventHandler(eventHandlerState),\n+ }\n+}\n"
+ "diff": "Index: cli/src/utils/create-run-config.ts\n===================================================================\n--- cli/src/utils/create-run-config.ts\t1013a8a (parent)\n+++ cli/src/utils/create-run-config.ts\td13f937 (commit)\n@@ -0,0 +1,83 @@\n+import {\n+ MAX_RETRIES_PER_MESSAGE,\n+ RETRY_BACKOFF_BASE_DELAY_MS,\n+ RETRY_BACKOFF_MAX_DELAY_MS,\n+} from '@codebirds/sdk'\n+\n+import { createEventHandler, createStreamChunkHandler } from './sdk-event-handlers'\n+\n+import type { EventHandlerState } from './sdk-event-handlers'\n+import type { AgentDefinition, MessageContent, RunState } from '@codebirds/sdk'\n+import type { Logger } from '@codebirds/common/types/contracts/logger'\n+import type { StreamStatus } from '../hooks/use-message-queue'\n+\n+export type CreateRunConfigParams = {\n+ logger: Logger\n+ agent: AgentDefinition | string\n+ prompt: string\n+ content: MessageContent[] | undefined\n+ previousRunState: RunState | null\n+ abortController: AbortController\n+ agentDefinitions: AgentDefinition[]\n+ eventHandlerState: EventHandlerState\n+ setIsRetrying: (retrying: boolean) => void\n+ setStreamStatus: (status: StreamStatus) => void\n+}\n+\n+type RetryArgs = {\n+ attempt: number\n+ delayMs: number\n+ errorCode?: string\n+}\n+\n+type RetryExhaustedArgs = {\n+ totalAttempts: number\n+ errorCode?: string\n+}\n+\n+export const createRunConfig = (params: CreateRunConfigParams) => {\n+ const {\n+ logger,\n+ agent,\n+ prompt,\n+ content,\n+ previousRunState,\n+ abortController,\n+ agentDefinitions,\n+ eventHandlerState,\n+ setIsRetrying,\n+ setStreamStatus,\n+ } = params\n+\n+ return {\n+ logger,\n+ agent,\n+ prompt,\n+ content,\n+ previousRun: previousRunState ?? undefined,\n+ abortController,\n+ retry: {\n+ maxRetries: MAX_RETRIES_PER_MESSAGE,\n+ backoffBaseMs: RETRY_BACKOFF_BASE_DELAY_MS,\n+ backoffMaxMs: RETRY_BACKOFF_MAX_DELAY_MS,\n+ onRetry: async ({ attempt, delayMs, errorCode }: RetryArgs) => {\n+ logger.warn(\n+ { sdkAttempt: attempt, delayMs, errorCode },\n+ 'SDK retrying after error',\n+ )\n+ setIsRetrying(true)\n+ setStreamStatus('waiting')\n+ },\n+ onRetryExhausted: async ({ totalAttempts, errorCode }: RetryExhaustedArgs) => {\n+ logger.warn(\n+ { totalAttempts, errorCode },\n+ 'SDK exhausted all retries',\n+ )\n+ },\n+ },\n+ agentDefinitions,\n+ maxAgentSteps: 40,\n+ handleStreamChunk: createStreamChunkHandler(eventHandlerState),\n+ handleEvent: createEventHandler(eventHandlerState),\n+ }\n+}\n"
},
{
"path": "cli/src/utils/sdk-event-handlers.ts",
@@ -704,7 +704,7 @@
"id": "add-base2-lite",
"sha": "5e2dda205c857c1d3e4c999cf2ae003ae5b0a921",
"parentSha": "0308b78c59c5ec2a1df0339e980f7c5ae8476c02",
- "spec": "- Create a new lite orchestrator agent and wire a new 'lite' mode into the base2 factory:\n - File: .agents/base2/base2.ts\n - Extend createBase2(mode, options) to accept a new mode 'lite'. Add boolean isLite.\n - Model: use 'x-ai/grok-4.1-fast' when isLite; otherwise keep existing models.\n - Tools: exclude write_todos when isLite (i.e., include write_todos only when not isFast and not isLite).\n - Spawnable agents: when isLite, use 'commander-lite' instead of 'commander'; include 'editor-gpt-5'; exclude 'code-reviewer-opus'. Keep existing for other modes.\n - Prompts/instructions: inject lite-specific guidance:\n - In the main instructions, add a bullet that for isLite you must spawn 'editor-gpt-5' to implement changes after gathering context.\n - In response examples, for isLite show implementation via 'editor-gpt-5', parallel typecheck/test via commanders only (no code-reviewer), and fixing issues found in type/tests.\n - Pass isLite through to the prompt builders and incorporate it in buildImplementationInstructionsPrompt so it enforces spawning editor-gpt-5 and adjusts planning/review steps (no write_todos, no code-reviewer).\n - File: .agents/base2/base2-lite.ts (new)\n - Export a definition created via createBase2('lite') with id 'base2-lite' and displayName 'Buffy the Lite Orchestrator'.\n\n- Add a lightweight commander variant used by lite mode:\n - File: .agents/commander-lite.ts (new)\n - Export a definition based on the existing commander, with id 'commander-lite', displayName 'Commander Lite', and model 'x-ai/grok-4.1-fast'.\n\n- Refactor the editor into a constrained implementor-only agent and provide a GPT-5 variant:\n - File: .agents/editor/editor.ts\n - Replace the legacy all-in-one editor with a factory export createCodeEditor(options: { model: 'gpt-5' | 'opus' }): Omit.\n - Restrict toolNames to only ['write_file', 'str_replace', 'set_output'].\n - Update spawnerPrompt to clarify it inherits full context and cannot read files or spawn agents; callers must read files first.\n - Overhaul instructions to require emitting edits as inline tool calls using the blocks for str_replace and write_file, covering complete implementation in a single response. Prohibit any other tools.\n - Implement handleSteps to:\n - Capture the assistant text produced by the model, yield it as a StepText so the runtime parses and executes embedded tool calls, then collect the tool results from the subsequent tool messages.\n - Finish with set_output containing { message: assistantText, toolResults }.\n - Export a default editor created with model 'opus'.\n - File: .agents/editor/editor-gpt-5.ts\n - Replace previous inheritance of editor with a direct use of createCodeEditor({ model: 'gpt-5' }); id remains 'editor-gpt-5'.\n\n- Archive the legacy editor agents under .agents-graveyard and adjust imports:\n - Move .agents/editor/editor-lite.ts to .agents-graveyard/editor-lite.ts and fix its imports to resolve against '../.agents/...'.\n - Add .agents-graveyard/editor.ts containing the legacy full-featured editor (tools including read_files, spawn_agents, run_terminal_command, etc.) with the iterative step accumulator behavior; update imports to resolve via '../.agents/...'.\n\n- Do not change any central registries beyond what createBase2 and the new files expose; ensure that loaders pick up .agents/ for active agents and ignore .agents-graveyard.\n\n- Ensure types are compatible with StepText-based execution and tool call parsing; no changes required to the runtime, but editor must not call non-edit tools.\n\n- Confirm that the lite mode behavior results in: faster model selection, no write_todos, no code-reviewer, spawning editor-gpt-5 for edits, and commander-lite for commands.",
+ "spec": "- Create a new lite orchestrator agent and wire a new 'lite' mode into the base2 factory:\n - File: .agents/base2/base2.ts\n - Extend createBase2(mode, options) to accept a new mode 'lite'. Add boolean isLite.\n - Model: use 'x-ai/grok-4.1-fast' when isLite; otherwise keep existing models.\n - Tools: exclude write_todos when isLite (i.e., include write_todos only when not isFast and not isLite).\n - Spawnable agents: when isLite, use 'commander-lite' instead of 'commander'; include 'editor-gpt-5'; exclude 'code-reviewer-opus'. Keep existing for other modes.\n - Prompts/instructions: inject lite-specific guidance:\n - In the main instructions, add a bullet that for isLite you must spawn 'editor-gpt-5' to implement changes after gathering context.\n - In response examples, for isLite show implementation via 'editor-gpt-5', parallel typecheck/test via commanders only (no code-reviewer), and fixing issues found in type/tests.\n - Pass isLite through to the prompt builders and incorporate it in buildImplementationInstructionsPrompt so it enforces spawning editor-gpt-5 and adjusts planning/review steps (no write_todos, no code-reviewer).\n - File: .agents/base2/base2-lite.ts (new)\n - Export a definition created via createBase2('lite') with id 'base2-lite' and displayName 'Buffy the Lite Orchestrator'.\n\n- Add a lightweight commander variant used by lite mode:\n - File: .agents/commander-lite.ts (new)\n - Export a definition based on the existing commander, with id 'commander-lite', displayName 'Commander Lite', and model 'x-ai/grok-4.1-fast'.\n\n- Refactor the editor into a constrained implementor-only agent and provide a GPT-5 variant:\n - File: .agents/editor/editor.ts\n - Replace the legacy all-in-one editor with a factory export createCodeEditor(options: { model: 'gpt-5' | 'opus' }): Omit.\n - Restrict toolNames to only ['write_file', 'str_replace', 'set_output'].\n - Update spawnerPrompt to clarify it inherits full context and cannot read files or spawn agents; callers must read files first.\n - Overhaul instructions to require emitting edits as inline tool calls using the blocks for str_replace and write_file, covering complete implementation in a single response. Prohibit any other tools.\n - Implement handleSteps to:\n - Capture the assistant text produced by the model, yield it as a StepText so the runtime parses and executes embedded tool calls, then collect the tool results from the subsequent tool messages.\n - Finish with set_output containing { message: assistantText, toolResults }.\n - Export a default editor created with model 'opus'.\n - File: .agents/editor/editor-gpt-5.ts\n - Replace previous inheritance of editor with a direct use of createCodeEditor({ model: 'gpt-5' }); id remains 'editor-gpt-5'.\n\n- Archive the legacy editor agents under .agents-graveyard and adjust imports:\n - Move .agents/editor/editor-lite.ts to .agents-graveyard/editor-lite.ts and fix its imports to resolve against '../.agents/...'.\n - Add .agents-graveyard/editor.ts containing the legacy full-featured editor (tools including read_files, spawn_agents, run_terminal_command, etc.) with the iterative step accumulator behavior; update imports to resolve via '../.agents/...'.\n\n- Do not change any central registries beyond what createBase2 and the new files expose; ensure that loaders pick up .agents/ for active agents and ignore .agents-graveyard.\n\n- Ensure types are compatible with StepText-based execution and tool call parsing; no changes required to the runtime, but editor must not call non-edit tools.\n\n- Confirm that the lite mode behavior results in: faster model selection, no write_todos, no code-reviewer, spawning editor-gpt-5 for edits, and commander-lite for commands.",
"prompt": "Introduce a new lightweight orchestrator mode and refactor the code editor into an implementor-only agent. The lite orchestrator should favor a fast model, avoid planning/reviewer steps, and direct all code changes through an editor-gpt-5 implementor. Archive the legacy editors under a graveyard directory. Also add a lightweight commander variant used by the lite mode. Update agent prompts and spawnable agents so the lite flow gathers context, delegates edits to the implementor, and validates via fast command runs.",
"supplementalFiles": [
"packages/agent-runtime/src/run-programmatic-step.ts",
@@ -733,7 +733,7 @@
{
"path": ".agents/base2/base2.ts",
"status": "modified",
- "diff": "Index: .agents/base2/base2.ts\n===================================================================\n--- .agents/base2/base2.ts\t0308b78 (parent)\n+++ .agents/base2/base2.ts\t5e2dda2 (commit)\n@@ -6,9 +6,9 @@\n type SecretAgentDefinition,\n } from '../types/secret-agent-definition'\n \n export function createBase2(\n- mode: 'fast' | 'default' | 'max',\n+ mode: 'default' | 'lite' | 'max' | 'fast',\n options?: {\n hasNoValidation?: boolean\n planOnly?: boolean\n },\n@@ -16,16 +16,17 @@\n const { hasNoValidation = mode === 'fast', planOnly = false } = options ?? {}\n const isDefault = mode === 'default'\n const isFast = mode === 'fast'\n const isMax = mode === 'max'\n+ const isLite = mode === 'lite'\n \n const isOpus = true\n const isSonnet = false\n const isGemini = false\n \n return {\n publisher,\n- model: 'anthropic/claude-opus-4.5',\n+ model: isLite ? 'x-ai/grok-4.1-fast' : 'anthropic/claude-opus-4.5',\n displayName: 'Buffy the Orchestrator',\n spawnerPrompt:\n 'Advanced base agent that orchestrates planning, editing, and reviewing for complex coding tasks',\n inputSchema: {\n@@ -48,9 +49,9 @@\n toolNames: buildArray(\n 'spawn_agents',\n 'read_files',\n 'read_subtree',\n- !isFast && 'write_todos',\n+ !isFast && !isLite && 'write_todos',\n 'str_replace',\n 'write_file',\n 'ask_user',\n 'set_output',\n@@ -61,12 +62,13 @@\n 'directory-lister',\n 'glob-matcher',\n 'researcher-web',\n 'researcher-docs',\n- 'commander',\n+ isLite ? 'commander-lite' : 'commander',\n+ isLite && 'editor-gpt-5',\n isMax && 'editor-best-of-n-max',\n isMax && 'thinker-best-of-n-opus',\n- 'code-reviewer-opus',\n+ !isLite && 'code-reviewer-opus',\n 'context-pruner',\n ),\n \n systemPrompt: `You are Buffy, a strategic assistant that orchestrates complex coding tasks through specialized sub-agents. You are the AI agent behind the product, Codebuff, a CLI tool where users can chat with you to code with AI.\n@@ -114,14 +116,17 @@\n - **Spawn multiple agents in parallel:** This increases the speed of your response **and** allows you to be more comprehensive by spawning more total agents to synthesize the best response.\n - **Sequence agents properly:** Keep in mind dependencies when spawning different agents. Don't spawn agents in parallel that depend on each other.\n ${buildArray(\n '- Spawn context-gathering agents (file pickers, code-searcher, directory-lister, glob-matcher, and web/docs researchers) before making edits.',\n+ isLite &&\n+ '- Spawn the editor-gpt-5 agent to implement the changes after you have gathered all the context you need.',\n isMax &&\n '- Spawn the thinker-best-of-n-opus after gathering context to solve complex problems.',\n isMax &&\n `- Spawn the editor-best-of-n-max agent to implement the changes after you have gathered all the context you need. You must spawn this agent for non-trivial changes, since it writes much better code than you would with the str_replace or write_file tools. Don't spawn the editor in parallel with context-gathering agents.`,\n '- Spawn commanders sequentially if the second command depends on the the first.',\n !isFast &&\n+ !isLite &&\n '- Spawn a code-reviewer-opus to review the changes after you have implemented the changes.',\n ).join('\\n ')}\n - **No need to include context:** When prompting an agent, realize that many agents can already see the entire conversation history, so you can be brief in prompting them without needing to include context.\n \n@@ -148,12 +153,9 @@\n isSonnet &&\n `- **Don't create a summary markdown file:** The user doesn't want markdown files they didn't ask for. Don't create them.`,\n '- **Keep final summary extremely concise:** Write only a few words for each change you made in the final summary.',\n ).join('\\n')}\n-${\n- isFast\n- ? ''\n- : `\n+\n # Response examples\n \n \n \n@@ -167,13 +169,27 @@\n [ You spawn one more code-searcher and file-picker ]\n \n [ You read a few other relevant files using the read_files tool ]\n \n-[ You implement the changes using the str_replace or write_file tools ]\n+${\n+ isDefault || isFast\n+ ? '[ You implement the changes using the str_replace or write_file tools ]'\n+ : isLite\n+ ? '[ You implement the changes using the editor-gpt-5 agent ]'\n+ : '[ You implement the changes using the editor-best-of-n-max agent ]'\n+}\n \n-[ You spawn a code-reviewer, a commander to typecheck the changes, and another commander to run tests, all in parallel ]\n+${\n+ isDefault || isMax\n+ ? '[ You spawn a code-reviewer, a commander to typecheck the changes, and another commander to run tests, all in parallel ]'\n+ : '[ You spawn a commander to typecheck the changes and another commander to run tests, all in parallel ]'\n+}\n \n-[ You fix the issues found by the code-reviewer and type/test errors ]\n+${\n+ isDefault || isMax\n+ ? '[ You fix the issues found by the code-reviewer and type/test errors ]'\n+ : '[ You fix the issues found by the type/test errors and spawn more commanders to confirm ]'\n+}\n \n [ All tests & typechecks pass -- you write a very short final summary of the changes you made ]\n \n \n@@ -187,10 +203,8 @@\n [ You collect codebase context, and then give a strong answer with key examples, and ask if you should make this change ]\n \n \n \n-`\n-}\n \n ${PLACEHOLDER.FILE_TREE_PROMPT_SMALL}\n ${PLACEHOLDER.KNOWLEDGE_FILES_CONTENTS}\n ${PLACEHOLDER.SYSTEM_INFO_PROMPT}\n@@ -208,8 +222,9 @@\n isSonnet,\n isFast,\n isDefault,\n isMax,\n+ isLite,\n hasNoValidation,\n }),\n stepPrompt: planOnly\n ? buildPlanOnlyStepPrompt({})\n@@ -247,14 +262,16 @@\n isSonnet,\n isFast,\n isDefault,\n isMax,\n+ isLite,\n hasNoValidation,\n }: {\n isSonnet: boolean\n isFast: boolean\n isDefault: boolean\n isMax: boolean\n+ isLite: boolean\n hasNoValidation: boolean\n }) {\n return `Act as a helpful assistant and freely respond to the user's request however would be most helpful to the user. Use your judgement to orchestrate the completion of the user's request using your specialized sub-agents and tools as needed. Take your time and be comprehensive. Don't surprise the user. For example, don't modify files if the user has not asked you to do so at least implicitly.\n \n@@ -265,19 +282,21 @@\n ${buildArray(\n EXPLORE_PROMPT,\n isMax &&\n `- Important: Read as many files as could possibly be relevant to the task over several steps to improve your understanding of the user's request and produce the best possible code changes. Find more examples within the codebase similar to the user's request, dependencies that help with understanding how things work, tests, etc. This is frequently 12-20 files, depending on the task.`,\n- !isFast &&\n+ (isDefault || isMax) &&\n `- For any task requiring 3+ steps, use the write_todos tool to write out your step-by-step implementation plan. Include ALL of the applicable tasks in the list.${isFast ? '' : ' You should include a step to review the changes after you have implemented the changes.'}:${hasNoValidation ? '' : ' You should include at least one step to validate/test your changes: be specific about whether to typecheck, run tests, run lints, etc.'} You may be able to do reviewing and validation in parallel in the same step. Skip write_todos for simple tasks like quick edits or answering questions.`,\n+ isLite &&\n+ '- IMPORTANT: You must spawn the editor-gpt-5 agent to implement the changes after you have gathered all the context you need. This agent will do the best job of implementing the changes so you must spawn it for all changes.',\n isMax &&\n `- IMPORTANT: You must spawn the editor-best-of-n-max agent to implement non-trivial code changes, since it will generate the best code changes from multiple implementation proposals. This is the best way to make high quality code changes -- strongly prefer using this agent over the str_replace or write_file tools, unless the change is very straightforward and obvious.`,\n- !isMax &&\n+ (isDefault || isFast) &&\n '- Implement the changes using the str_replace or write_file tools.',\n isFast &&\n '- Implement the changes in one go. Pause after making all the changes to see the tool results of your edits.',\n isFast &&\n '- Do a single typecheck targeted for your changes at most (if applicable for the project). Or skip this step if the change was small.',\n- !isFast &&\n+ (isDefault || isMax) &&\n '- Spawn a code-reviewer-opus to review the changes after you have implemented the changes. (Skip this step only if the change is extremely straightforward and obvious.)',\n !hasNoValidation &&\n `- Test your changes by running appropriate validation commands for the project (e.g. typechecks, tests, lints, etc.). Try to run all appropriate commands in parallel. ${isMax ? ' Typecheck and test the specific area of the project that you are editing *AND* then typecheck and test the entire project if necessary.' : ' If you can, only test the area of the project that you are editing, rather than the entire project.'} You may have to explore the project to find the appropriate commands. Don't skip this step!`,\n `- Inform the user that you have completed the task in one sentence or a few short bullet points.${isSonnet ? \" Don't create any markdown summary files or example documentation files, unless asked by the user.\" : ''}`,\n"
+ "diff": "Index: .agents/base2/base2.ts\n===================================================================\n--- .agents/base2/base2.ts\t0308b78 (parent)\n+++ .agents/base2/base2.ts\t5e2dda2 (commit)\n@@ -6,9 +6,9 @@\n type SecretAgentDefinition,\n } from '../types/secret-agent-definition'\n \n export function createBase2(\n- mode: 'fast' | 'default' | 'max',\n+ mode: 'default' | 'lite' | 'max' | 'fast',\n options?: {\n hasNoValidation?: boolean\n planOnly?: boolean\n },\n@@ -16,16 +16,17 @@\n const { hasNoValidation = mode === 'fast', planOnly = false } = options ?? {}\n const isDefault = mode === 'default'\n const isFast = mode === 'fast'\n const isMax = mode === 'max'\n+ const isLite = mode === 'lite'\n \n const isOpus = true\n const isSonnet = false\n const isGemini = false\n \n return {\n publisher,\n- model: 'anthropic/claude-opus-4.5',\n+ model: isLite ? 'x-ai/grok-4.1-fast' : 'anthropic/claude-opus-4.5',\n displayName: 'Buffy the Orchestrator',\n spawnerPrompt:\n 'Advanced base agent that orchestrates planning, editing, and reviewing for complex coding tasks',\n inputSchema: {\n@@ -48,9 +49,9 @@\n toolNames: buildArray(\n 'spawn_agents',\n 'read_files',\n 'read_subtree',\n- !isFast && 'write_todos',\n+ !isFast && !isLite && 'write_todos',\n 'str_replace',\n 'write_file',\n 'ask_user',\n 'set_output',\n@@ -61,12 +62,13 @@\n 'directory-lister',\n 'glob-matcher',\n 'researcher-web',\n 'researcher-docs',\n- 'commander',\n+ isLite ? 'commander-lite' : 'commander',\n+ isLite && 'editor-gpt-5',\n isMax && 'editor-best-of-n-max',\n isMax && 'thinker-best-of-n-opus',\n- 'code-reviewer-opus',\n+ !isLite && 'code-reviewer-opus',\n 'context-pruner',\n ),\n \n systemPrompt: `You are Buffy, a strategic assistant that orchestrates complex coding tasks through specialized sub-agents. You are the AI agent behind the product, Codebirds, a CLI tool where users can chat with you to code with AI.\n@@ -114,14 +116,17 @@\n - **Spawn multiple agents in parallel:** This increases the speed of your response **and** allows you to be more comprehensive by spawning more total agents to synthesize the best response.\n - **Sequence agents properly:** Keep in mind dependencies when spawning different agents. Don't spawn agents in parallel that depend on each other.\n ${buildArray(\n '- Spawn context-gathering agents (file pickers, code-searcher, directory-lister, glob-matcher, and web/docs researchers) before making edits.',\n+ isLite &&\n+ '- Spawn the editor-gpt-5 agent to implement the changes after you have gathered all the context you need.',\n isMax &&\n '- Spawn the thinker-best-of-n-opus after gathering context to solve complex problems.',\n isMax &&\n `- Spawn the editor-best-of-n-max agent to implement the changes after you have gathered all the context you need. You must spawn this agent for non-trivial changes, since it writes much better code than you would with the str_replace or write_file tools. Don't spawn the editor in parallel with context-gathering agents.`,\n '- Spawn commanders sequentially if the second command depends on the the first.',\n !isFast &&\n+ !isLite &&\n '- Spawn a code-reviewer-opus to review the changes after you have implemented the changes.',\n ).join('\\n ')}\n - **No need to include context:** When prompting an agent, realize that many agents can already see the entire conversation history, so you can be brief in prompting them without needing to include context.\n \n@@ -148,12 +153,9 @@\n isSonnet &&\n `- **Don't create a summary markdown file:** The user doesn't want markdown files they didn't ask for. Don't create them.`,\n '- **Keep final summary extremely concise:** Write only a few words for each change you made in the final summary.',\n ).join('\\n')}\n-${\n- isFast\n- ? ''\n- : `\n+\n # Response examples\n \n \n \n@@ -167,13 +169,27 @@\n [ You spawn one more code-searcher and file-picker ]\n \n [ You read a few other relevant files using the read_files tool ]\n \n-[ You implement the changes using the str_replace or write_file tools ]\n+${\n+ isDefault || isFast\n+ ? '[ You implement the changes using the str_replace or write_file tools ]'\n+ : isLite\n+ ? '[ You implement the changes using the editor-gpt-5 agent ]'\n+ : '[ You implement the changes using the editor-best-of-n-max agent ]'\n+}\n \n-[ You spawn a code-reviewer, a commander to typecheck the changes, and another commander to run tests, all in parallel ]\n+${\n+ isDefault || isMax\n+ ? '[ You spawn a code-reviewer, a commander to typecheck the changes, and another commander to run tests, all in parallel ]'\n+ : '[ You spawn a commander to typecheck the changes and another commander to run tests, all in parallel ]'\n+}\n \n-[ You fix the issues found by the code-reviewer and type/test errors ]\n+${\n+ isDefault || isMax\n+ ? '[ You fix the issues found by the code-reviewer and type/test errors ]'\n+ : '[ You fix the issues found by the type/test errors and spawn more commanders to confirm ]'\n+}\n \n [ All tests & typechecks pass -- you write a very short final summary of the changes you made ]\n \n \n@@ -187,10 +203,8 @@\n [ You collect codebase context, and then give a strong answer with key examples, and ask if you should make this change ]\n \n \n \n-`\n-}\n \n ${PLACEHOLDER.FILE_TREE_PROMPT_SMALL}\n ${PLACEHOLDER.KNOWLEDGE_FILES_CONTENTS}\n ${PLACEHOLDER.SYSTEM_INFO_PROMPT}\n@@ -208,8 +222,9 @@\n isSonnet,\n isFast,\n isDefault,\n isMax,\n+ isLite,\n hasNoValidation,\n }),\n stepPrompt: planOnly\n ? buildPlanOnlyStepPrompt({})\n@@ -247,14 +262,16 @@\n isSonnet,\n isFast,\n isDefault,\n isMax,\n+ isLite,\n hasNoValidation,\n }: {\n isSonnet: boolean\n isFast: boolean\n isDefault: boolean\n isMax: boolean\n+ isLite: boolean\n hasNoValidation: boolean\n }) {\n return `Act as a helpful assistant and freely respond to the user's request however would be most helpful to the user. Use your judgement to orchestrate the completion of the user's request using your specialized sub-agents and tools as needed. Take your time and be comprehensive. Don't surprise the user. For example, don't modify files if the user has not asked you to do so at least implicitly.\n \n@@ -265,19 +282,21 @@\n ${buildArray(\n EXPLORE_PROMPT,\n isMax &&\n `- Important: Read as many files as could possibly be relevant to the task over several steps to improve your understanding of the user's request and produce the best possible code changes. Find more examples within the codebase similar to the user's request, dependencies that help with understanding how things work, tests, etc. This is frequently 12-20 files, depending on the task.`,\n- !isFast &&\n+ (isDefault || isMax) &&\n `- For any task requiring 3+ steps, use the write_todos tool to write out your step-by-step implementation plan. Include ALL of the applicable tasks in the list.${isFast ? '' : ' You should include a step to review the changes after you have implemented the changes.'}:${hasNoValidation ? '' : ' You should include at least one step to validate/test your changes: be specific about whether to typecheck, run tests, run lints, etc.'} You may be able to do reviewing and validation in parallel in the same step. Skip write_todos for simple tasks like quick edits or answering questions.`,\n+ isLite &&\n+ '- IMPORTANT: You must spawn the editor-gpt-5 agent to implement the changes after you have gathered all the context you need. This agent will do the best job of implementing the changes so you must spawn it for all changes.',\n isMax &&\n `- IMPORTANT: You must spawn the editor-best-of-n-max agent to implement non-trivial code changes, since it will generate the best code changes from multiple implementation proposals. This is the best way to make high quality code changes -- strongly prefer using this agent over the str_replace or write_file tools, unless the change is very straightforward and obvious.`,\n- !isMax &&\n+ (isDefault || isFast) &&\n '- Implement the changes using the str_replace or write_file tools.',\n isFast &&\n '- Implement the changes in one go. Pause after making all the changes to see the tool results of your edits.',\n isFast &&\n '- Do a single typecheck targeted for your changes at most (if applicable for the project). Or skip this step if the change was small.',\n- !isFast &&\n+ (isDefault || isMax) &&\n '- Spawn a code-reviewer-opus to review the changes after you have implemented the changes. (Skip this step only if the change is extremely straightforward and obvious.)',\n !hasNoValidation &&\n `- Test your changes by running appropriate validation commands for the project (e.g. typechecks, tests, lints, etc.). Try to run all appropriate commands in parallel. ${isMax ? ' Typecheck and test the specific area of the project that you are editing *AND* then typecheck and test the entire project if necessary.' : ' If you can, only test the area of the project that you are editing, rather than the entire project.'} You may have to explore the project to find the appropriate commands. Don't skip this step!`,\n `- Inform the user that you have completed the task in one sentence or a few short bullet points.${isSonnet ? \" Don't create any markdown summary files or example documentation files, unless asked by the user.\" : ''}`,\n"
},
{
"path": ".agents/commander-lite.ts",
@@ -748,7 +748,7 @@
{
"path": ".agents/editor/editor.ts",
"status": "modified",
- "diff": "Index: .agents/editor/editor.ts\n===================================================================\n--- .agents/editor/editor.ts\t0308b78 (parent)\n+++ .agents/editor/editor.ts\t5e2dda2 (commit)\n@@ -1,151 +1,114 @@\n+import { AgentDefinition, StepText } from 'types/agent-definition'\n import { publisher } from '../constants'\n-import { type SecretAgentDefinition } from '../types/secret-agent-definition'\n \n-import type { Message } from 'types/util-types'\n-\n-const editor: SecretAgentDefinition = {\n- id: 'editor',\n+export const createCodeEditor = (options: {\n+ model: 'gpt-5' | 'opus'\n+}): Omit => ({\n publisher,\n- model: 'anthropic/claude-sonnet-4.5',\n+ model:\n+ options.model === 'gpt-5' ? 'openai/gpt-5.1' : 'anthropic/claude-opus-4.5',\n displayName: 'Code Editor',\n spawnerPrompt:\n- 'Expert code editor with access to tools to find and edit files, run terminal commands, and search the web. Can handle small to medium sized tasks, or work off of a plan for more complex tasks. For easy tasks, you can spawn this agent directly rather than invoking a researcher or planner first. Spawn mulitple in parallel if needed, but only on totally distinct tasks.',\n- inputSchema: {\n- prompt: {\n- type: 'string',\n- description: 'The coding task to implement',\n- },\n- params: {\n- type: 'object',\n- properties: {\n- maxContextLength: {\n- type: 'number',\n- },\n- },\n- required: [],\n- },\n- },\n+ 'Expert code editor. Do not specify an input prompt for this agent; it inherits the context of the entire conversation with the user. Make sure to read any files intended to be edited before spawning this agent as it cannot read files on its own.',\n outputMode: 'structured_output',\n- toolNames: [\n- 'read_files',\n- 'write_file',\n- 'str_replace',\n- 'run_terminal_command',\n- 'code_search',\n- 'spawn_agents',\n- 'add_message',\n- 'set_output',\n- 'end_turn',\n- ],\n- spawnableAgents: ['file-explorer', 'researcher-web', 'researcher-docs'],\n+ toolNames: ['write_file', 'str_replace', 'set_output'],\n \n includeMessageHistory: true,\n inheritParentSystemPrompt: true,\n \n- instructionsPrompt: `You are an expert code editor with deep understanding of software engineering principles.\n+ instructionsPrompt: `You are an expert code editor with deep understanding of software engineering principles. You were spawned to generate an implementation for the user's request.\n+ \n+Your task is to write out ALL the code changes needed to complete the user's request in a single comprehensive response.\n \n-Implement the requested changes, using your judgment as needed, but referring to the original as the most important source of information.\n+Important: You can not make any other tool calls besides editing files. You cannot read more files, write todos, spawn agents, or set output. Do not call any of these tools!\n \n-# Instructions\n+Write out what changes you would make using the tool call format below. Use this exact format for each file change:\n \n-- Read any relevant files that have not already been read. Or, spawn a file-explorer to find any other relevant parts of the codebase.\n-- Implement changes using str_replace or write_file.\n-- Verify your changes by running tests, typechecking, etc. Keep going until you are sure the changes are correct.\n-- You must use the set_output tool before finishing and include the following in your summary:\n- - An answer to the user prompt (if they asked a question).\n- - An explanation of the changes made.\n- - A note on any checks you ran to verify the changes, such as tests, typechecking, etc., and the results of those checks.\n- - Do not include a section on the benefits of the changes, as we're most interested in the changes themselves and what still needs to be done.\n-- Do not write a summary outside of the one that you include in the set_output tool.\n-- As soon as you use set_output, you must end your turn using the end_turn tool.\n-`,\n+\n+{\n+ \"cb_tool_name\": \"str_replace\",\n+ \"path\": \"path/to/file\",\n+ \"replacements\": [\n+ {\n+ \"old\": \"exact old code\",\n+ \"new\": \"exact new code\"\n+ },\n+ {\n+ \"old\": \"exact old code 2\",\n+ \"new\": \"exact new code 2\"\n+ },\n+ ]\n+}\n+ \n \n- handleSteps: function* ({ agentState: initialAgentState }) {\n- const stepLimit = 25\n- let stepCount = 0\n- let agentState = initialAgentState\n- let accumulatedEditToolResults: any[] = []\n+OR for new files or major rewrites:\n \n- while (true) {\n- stepCount++\n+\n+{\n+ \"cb_tool_name\": \"write_file\",\n+ \"path\": \"path/to/file\",\n+ \"instructions\": \"What the change does\",\n+ \"content\": \"Complete file content or edit snippet\"\n+}\n+ \n \n- const stepResult = yield 'STEP'\n- agentState = stepResult.agentState // Capture the latest state\n+After the edit tool calls, you can optionally mention any follow-up steps to take, like deleting a file, or a sepcific way to validate the changes. There's no need to use the set_output tool as your entire response will be included in the output.\n \n- // Accumulate new tool messages from this step\n- const { messageHistory } = agentState\n+Your implementation should:\n+- Be complete and comprehensive\n+- Include all necessary changes to fulfill the user's request\n+- Follow the project's conventions and patterns\n+- Be as simple and maintainable as possible\n+- Reuse existing code wherever possible\n+- Be well-structured and organized\n \n- // Extract and accumulate new edit tool results using helper function\n- accumulatedEditToolResults.push(\n- ...getLatestEditToolResults(messageHistory),\n- )\n+More style notes:\n+- Extra try/catch blocks clutter the code -- use them sparingly.\n+- Optional arguments are code smell and worse than required arguments.\n+- New components often should be added to a new file, not added to an existing file.\n \n- if (stepResult.stepsComplete) {\n- break\n- }\n+Write out your complete implementation now, formatting all changes as tool calls as shown above.`,\n \n- // If we've reached within one of the step limit, ask LLM to summarize progress\n- if (stepCount === stepLimit - 1) {\n- yield {\n- toolName: 'add_message',\n- input: {\n- role: 'user',\n- content:\n- 'You have reached the step limit. Please use the set_output tool now to summarize your progress so far including all specific actions you took (note that any file changes will be included automatically in the output), what you still need to solve, and provide any insights that could help complete the remaining work. Please end your turn after using the set_output tool with the end_turn tool.',\n- },\n- includeToolCall: false,\n- }\n+ handleSteps: function* ({ agentState: initialAgentState }) {\n+ const initialMessageHistoryLength = initialAgentState.messageHistory.length\n+ const { agentState } = yield 'STEP'\n+ const { messageHistory } = agentState\n \n- // One final step to produce the summary\n- const finalStepResult = yield 'STEP'\n- agentState = finalStepResult.agentState\n+ const newMessages = messageHistory.slice(initialMessageHistoryLength)\n+ const assistantText = newMessages\n+ .filter((message) => message.role === 'assistant')\n+ .flatMap((message) => message.content)\n+ .filter((content) => content.type === 'text')\n+ .map((content) => content.text)\n+ .join('\\n')\n \n- // Extract and accumulate final edit tool results using helper function\n- accumulatedEditToolResults.push(\n- ...getLatestEditToolResults(agentState.messageHistory),\n- )\n- break\n- }\n- }\n+ const { agentState: postAssistantTextAgentState } = yield {\n+ type: 'STEP_TEXT',\n+ text: assistantText,\n+ } as StepText\n \n+ const postAssistantTextMessageHistory =\n+ postAssistantTextAgentState.messageHistory.slice(\n+ initialMessageHistoryLength,\n+ )\n+ const toolResults = postAssistantTextMessageHistory\n+ .filter((message) => message.role === 'tool')\n+ .flatMap((message) => message.content)\n+ .filter((content) => content.type === 'json')\n+ .map((content) => content.value)\n+\n yield {\n toolName: 'set_output',\n input: {\n- ...agentState.output,\n- edits: accumulatedEditToolResults,\n+ output: {\n+ message: assistantText,\n+ toolResults,\n+ },\n },\n includeToolCall: false,\n }\n-\n- function getLatestEditToolResults(messageHistory: Message[]) {\n- const lastAssistantMessageIndex = messageHistory.findLastIndex(\n- (message) => message.role === 'assistant',\n- )\n-\n- // Get all edit tool messages after the last assistant message\n- const newToolMessages = messageHistory\n- .slice(lastAssistantMessageIndex + 1)\n- .filter((message) => message.role === 'tool')\n- .filter(\n- (message) =>\n- message.toolName === 'write_file' ||\n- message.toolName === 'str_replace',\n- )\n-\n- // Extract and return new edit tool results\n- return (\n- newToolMessages\n- .flatMap((message) => message.content)\n- .filter((output) => output.type === 'json')\n- .map((output) => output.value)\n- // Only successful edits!\n- .filter(\n- (toolResult) =>\n- toolResult && !('errorMessage' in (toolResult as any)),\n- )\n- )\n- }\n },\n-}\n+})\n \n+const editor = createCodeEditor({ model: 'opus' })\n export default editor\n"
+ "diff": "Index: .agents/editor/editor.ts\n===================================================================\n--- .agents/editor/editor.ts\t0308b78 (parent)\n+++ .agents/editor/editor.ts\t5e2dda2 (commit)\n@@ -1,151 +1,114 @@\n+import { AgentDefinition, StepText } from 'types/agent-definition'\n import { publisher } from '../constants'\n-import { type SecretAgentDefinition } from '../types/secret-agent-definition'\n \n-import type { Message } from 'types/util-types'\n-\n-const editor: SecretAgentDefinition = {\n- id: 'editor',\n+export const createCodeEditor = (options: {\n+ model: 'gpt-5' | 'opus'\n+}): Omit => ({\n publisher,\n- model: 'anthropic/claude-sonnet-4.5',\n+ model:\n+ options.model === 'gpt-5' ? 'openai/gpt-5.1' : 'anthropic/claude-opus-4.5',\n displayName: 'Code Editor',\n spawnerPrompt:\n- 'Expert code editor with access to tools to find and edit files, run terminal commands, and search the web. Can handle small to medium sized tasks, or work off of a plan for more complex tasks. For easy tasks, you can spawn this agent directly rather than invoking a researcher or planner first. Spawn mulitple in parallel if needed, but only on totally distinct tasks.',\n- inputSchema: {\n- prompt: {\n- type: 'string',\n- description: 'The coding task to implement',\n- },\n- params: {\n- type: 'object',\n- properties: {\n- maxContextLength: {\n- type: 'number',\n- },\n- },\n- required: [],\n- },\n- },\n+ 'Expert code editor. Do not specify an input prompt for this agent; it inherits the context of the entire conversation with the user. Make sure to read any files intended to be edited before spawning this agent as it cannot read files on its own.',\n outputMode: 'structured_output',\n- toolNames: [\n- 'read_files',\n- 'write_file',\n- 'str_replace',\n- 'run_terminal_command',\n- 'code_search',\n- 'spawn_agents',\n- 'add_message',\n- 'set_output',\n- 'end_turn',\n- ],\n- spawnableAgents: ['file-explorer', 'researcher-web', 'researcher-docs'],\n+ toolNames: ['write_file', 'str_replace', 'set_output'],\n \n includeMessageHistory: true,\n inheritParentSystemPrompt: true,\n \n- instructionsPrompt: `You are an expert code editor with deep understanding of software engineering principles.\n+ instructionsPrompt: `You are an expert code editor with deep understanding of software engineering principles. You were spawned to generate an implementation for the user's request.\n+ \n+Your task is to write out ALL the code changes needed to complete the user's request in a single comprehensive response.\n \n-Implement the requested changes, using your judgment as needed, but referring to the original as the most important source of information.\n+Important: You can not make any other tool calls besides editing files. You cannot read more files, write todos, spawn agents, or set output. Do not call any of these tools!\n \n-# Instructions\n+Write out what changes you would make using the tool call format below. Use this exact format for each file change:\n \n-- Read any relevant files that have not already been read. Or, spawn a file-explorer to find any other relevant parts of the codebase.\n-- Implement changes using str_replace or write_file.\n-- Verify your changes by running tests, typechecking, etc. Keep going until you are sure the changes are correct.\n-- You must use the set_output tool before finishing and include the following in your summary:\n- - An answer to the user prompt (if they asked a question).\n- - An explanation of the changes made.\n- - A note on any checks you ran to verify the changes, such as tests, typechecking, etc., and the results of those checks.\n- - Do not include a section on the benefits of the changes, as we're most interested in the changes themselves and what still needs to be done.\n-- Do not write a summary outside of the one that you include in the set_output tool.\n-- As soon as you use set_output, you must end your turn using the end_turn tool.\n-`,\n+\n+{\n+ \"cb_tool_name\": \"str_replace\",\n+ \"path\": \"path/to/file\",\n+ \"replacements\": [\n+ {\n+ \"old\": \"exact old code\",\n+ \"new\": \"exact new code\"\n+ },\n+ {\n+ \"old\": \"exact old code 2\",\n+ \"new\": \"exact new code 2\"\n+ },\n+ ]\n+}\n+ \n \n- handleSteps: function* ({ agentState: initialAgentState }) {\n- const stepLimit = 25\n- let stepCount = 0\n- let agentState = initialAgentState\n- let accumulatedEditToolResults: any[] = []\n+OR for new files or major rewrites:\n \n- while (true) {\n- stepCount++\n+\n+{\n+ \"cb_tool_name\": \"write_file\",\n+ \"path\": \"path/to/file\",\n+ \"instructions\": \"What the change does\",\n+ \"content\": \"Complete file content or edit snippet\"\n+}\n+ \n \n- const stepResult = yield 'STEP'\n- agentState = stepResult.agentState // Capture the latest state\n+After the edit tool calls, you can optionally mention any follow-up steps to take, like deleting a file, or a sepcific way to validate the changes. There's no need to use the set_output tool as your entire response will be included in the output.\n \n- // Accumulate new tool messages from this step\n- const { messageHistory } = agentState\n+Your implementation should:\n+- Be complete and comprehensive\n+- Include all necessary changes to fulfill the user's request\n+- Follow the project's conventions and patterns\n+- Be as simple and maintainable as possible\n+- Reuse existing code wherever possible\n+- Be well-structured and organized\n \n- // Extract and accumulate new edit tool results using helper function\n- accumulatedEditToolResults.push(\n- ...getLatestEditToolResults(messageHistory),\n- )\n+More style notes:\n+- Extra try/catch blocks clutter the code -- use them sparingly.\n+- Optional arguments are code smell and worse than required arguments.\n+- New components often should be added to a new file, not added to an existing file.\n \n- if (stepResult.stepsComplete) {\n- break\n- }\n+Write out your complete implementation now, formatting all changes as tool calls as shown above.`,\n \n- // If we've reached within one of the step limit, ask LLM to summarize progress\n- if (stepCount === stepLimit - 1) {\n- yield {\n- toolName: 'add_message',\n- input: {\n- role: 'user',\n- content:\n- 'You have reached the step limit. Please use the set_output tool now to summarize your progress so far including all specific actions you took (note that any file changes will be included automatically in the output), what you still need to solve, and provide any insights that could help complete the remaining work. Please end your turn after using the set_output tool with the end_turn tool.',\n- },\n- includeToolCall: false,\n- }\n+ handleSteps: function* ({ agentState: initialAgentState }) {\n+ const initialMessageHistoryLength = initialAgentState.messageHistory.length\n+ const { agentState } = yield 'STEP'\n+ const { messageHistory } = agentState\n \n- // One final step to produce the summary\n- const finalStepResult = yield 'STEP'\n- agentState = finalStepResult.agentState\n+ const newMessages = messageHistory.slice(initialMessageHistoryLength)\n+ const assistantText = newMessages\n+ .filter((message) => message.role === 'assistant')\n+ .flatMap((message) => message.content)\n+ .filter((content) => content.type === 'text')\n+ .map((content) => content.text)\n+ .join('\\n')\n \n- // Extract and accumulate final edit tool results using helper function\n- accumulatedEditToolResults.push(\n- ...getLatestEditToolResults(agentState.messageHistory),\n- )\n- break\n- }\n- }\n+ const { agentState: postAssistantTextAgentState } = yield {\n+ type: 'STEP_TEXT',\n+ text: assistantText,\n+ } as StepText\n \n+ const postAssistantTextMessageHistory =\n+ postAssistantTextAgentState.messageHistory.slice(\n+ initialMessageHistoryLength,\n+ )\n+ const toolResults = postAssistantTextMessageHistory\n+ .filter((message) => message.role === 'tool')\n+ .flatMap((message) => message.content)\n+ .filter((content) => content.type === 'json')\n+ .map((content) => content.value)\n+\n yield {\n toolName: 'set_output',\n input: {\n- ...agentState.output,\n- edits: accumulatedEditToolResults,\n+ output: {\n+ message: assistantText,\n+ toolResults,\n+ },\n },\n includeToolCall: false,\n }\n-\n- function getLatestEditToolResults(messageHistory: Message[]) {\n- const lastAssistantMessageIndex = messageHistory.findLastIndex(\n- (message) => message.role === 'assistant',\n- )\n-\n- // Get all edit tool messages after the last assistant message\n- const newToolMessages = messageHistory\n- .slice(lastAssistantMessageIndex + 1)\n- .filter((message) => message.role === 'tool')\n- .filter(\n- (message) =>\n- message.toolName === 'write_file' ||\n- message.toolName === 'str_replace',\n- )\n-\n- // Extract and return new edit tool results\n- return (\n- newToolMessages\n- .flatMap((message) => message.content)\n- .filter((output) => output.type === 'json')\n- .map((output) => output.value)\n- // Only successful edits!\n- .filter(\n- (toolResult) =>\n- toolResult && !('errorMessage' in (toolResult as any)),\n- )\n- )\n- }\n },\n-}\n+})\n \n+const editor = createCodeEditor({ model: 'opus' })\n export default editor\n"
}
]
},
@@ -764,7 +764,7 @@
"cli/src/hooks/use-message-queue.ts",
"cli/src/hooks/use-input-history.ts",
"sdk/src/tools/run-terminal-command.ts",
- "common/src/types/messages/codebuff-message.ts",
+ "common/src/types/messages/codebirds-message.ts",
"common/src/types/messages/content-part.ts"
],
"fileDiffs": [
@@ -776,7 +776,7 @@
{
"path": "cli/src/commands/router.ts",
"status": "modified",
- "diff": "Index: cli/src/commands/router.ts\n===================================================================\n--- cli/src/commands/router.ts\tb9be243 (parent)\n+++ cli/src/commands/router.ts\te9ce02a (commit)\n@@ -15,10 +15,253 @@\n } from './router-utils'\n import { useChatStore } from '../state/chat-store'\n import { getSystemMessage, getUserMessage } from '../utils/message-history'\n \n+import type { ToolMessage } from '@codebuff/common/types/messages/codebuff-message'\n+import type { ToolResultOutput } from '@codebuff/common/types/messages/content-part'\n import type { ContentBlock } from '../types/chat'\n \n+/**\n+ * Execute a bash command and add it directly to chat history.\n+ * Shows immediate placeholder while running, then updates with output.\n+ */\n+function executeBashCommand(\n+ command: string,\n+ setMessages: RouterParams['setMessages'],\n+) {\n+ const toolCallId = crypto.randomUUID()\n+ const resultBlock: ContentBlock = {\n+ type: 'tool',\n+ toolName: 'run_terminal_command',\n+ toolCallId,\n+ input: { command },\n+ output: '...',\n+ }\n+\n+ const commandCwd = process.cwd()\n+\n+ // Add the command result to chat as a user message so the AI sees it as context\n+ setMessages((prev) => [\n+ ...prev,\n+ {\n+ ...getUserMessage([resultBlock]),\n+ metadata: { bashCwd: commandCwd },\n+ },\n+ ])\n+\n+ // Execute the command and update the output when complete\n+ runTerminalCommand({\n+ command,\n+ process_type: 'SYNC',\n+ cwd: commandCwd,\n+ timeout_seconds: -1,\n+ env: process.env,\n+ }).then(([{ value }]) => {\n+ const stdout = 'stdout' in value ? (value.stdout || '') : ''\n+ const stderr = 'stderr' in value ? (value.stderr || '') : ''\n+ const exitCode = 'exitCode' in value ? value.exitCode : 0\n+\n+ // Create tool result output for display\n+ const toolResultOutput = [{\n+ type: 'json' as const,\n+ value: {\n+ command,\n+ startingCwd: commandCwd,\n+ stdout: stdout || null,\n+ stderr: stderr || null,\n+ exitCode: exitCode ?? 0,\n+ }\n+ }]\n+\n+ // Store output in JSON format for display\n+ const outputJson = JSON.stringify(toolResultOutput)\n+\n+ setMessages((prev) => {\n+ return prev.map((msg) => {\n+ if (!msg.blocks) {\n+ return msg\n+ }\n+ return {\n+ ...msg,\n+ blocks: msg.blocks.map((block) =>\n+ 'toolCallId' in block && block.toolCallId === toolCallId\n+ ? {\n+ ...block,\n+ output: outputJson,\n+ }\n+ : block,\n+ ),\n+ }\n+ })\n+ })\n+\n+ // Add to pending tool results so AI can see this in the next run\n+ const toolMessage: ToolMessage = {\n+ role: 'tool',\n+ toolCallId,\n+ toolName: 'run_terminal_command',\n+ content: toolResultOutput,\n+ }\n+ useChatStore.getState().addPendingToolResult(toolMessage)\n+ }).catch((error) => {\n+ const errorMessage = error instanceof Error ? error.message : String(error)\n+ \n+ // Create error tool result output\n+ const errorToolResultOutput = [{\n+ type: 'json' as const,\n+ value: {\n+ command,\n+ startingCwd: commandCwd,\n+ errorMessage,\n+ }\n+ }]\n+\n+ // Store error output in JSON format for display\n+ const errorOutputJson = JSON.stringify(errorToolResultOutput)\n+\n+ setMessages((prev) => {\n+ return prev.map((msg) => {\n+ if (!msg.blocks) {\n+ return msg\n+ }\n+ return {\n+ ...msg,\n+ blocks: msg.blocks.map((block) =>\n+ 'toolCallId' in block && block.toolCallId === toolCallId\n+ ? {\n+ ...block,\n+ output: errorOutputJson,\n+ }\n+ : block,\n+ ),\n+ }\n+ })\n+ })\n+\n+ // Add error result to pending tool results so AI can see this in the next run\n+ const errorToolMessage: ToolMessage = {\n+ role: 'tool',\n+ toolCallId,\n+ toolName: 'run_terminal_command',\n+ content: errorToolResultOutput,\n+ }\n+ useChatStore.getState().addPendingToolResult(errorToolMessage)\n+ })\n+}\n+\n+/**\n+ * Add a bash command result to the chat message history.\n+ * Also adds to pendingToolResults so the AI can see it in the next run.\n+ */\n+export function addBashMessageToHistory(params: {\n+ command: string\n+ stdout: string\n+ stderr: string | null | undefined\n+ exitCode: number\n+ cwd: string\n+ displayOutput?: string\n+ setMessages: RouterParams['setMessages']\n+}) {\n+ const { command, stdout, stderr, exitCode, cwd, displayOutput, setMessages } =\n+ params\n+ const outputText =\n+ displayOutput ?? (stdout || stderr ? `${stdout}${stderr ?? ''}` : '')\n+ const toolCallId = crypto.randomUUID()\n+ const resultBlock: ContentBlock = {\n+ type: 'tool',\n+ toolName: 'run_terminal_command',\n+ toolCallId,\n+ input: { command },\n+ output: outputText || '(no output)',\n+ }\n+\n+ // Add as a user message so the AI sees it as context\n+ setMessages((prev) => [\n+ ...prev,\n+ {\n+ ...getUserMessage([resultBlock]),\n+ metadata: { bashCwd: cwd },\n+ },\n+ ])\n+\n+ // Also add to pending tool results so AI can see this in the next run\n+ const toolResultOutput: ToolResultOutput[] = [{\n+ type: 'json' as const,\n+ value: {\n+ command,\n+ startingCwd: cwd,\n+ stdout: stdout || null,\n+ stderr: stderr ?? null,\n+ exitCode: exitCode ?? 0,\n+ }\n+ }]\n+ const toolMessage: ToolMessage = {\n+ role: 'tool',\n+ toolCallId,\n+ toolName: 'run_terminal_command',\n+ content: toolResultOutput,\n+ }\n+ useChatStore.getState().addPendingToolResult(toolMessage)\n+}\n+\n+/**\n+ * Execute a bash command as a ghost message in chat.\n+ * Shows as a pending message while running, then commits to history when streaming ends.\n+ */\n+function executeBashCommandAsGhost(\n+ command: string,\n+ addPendingBashMessage: (message: import('../state/chat-store').PendingBashMessage) => void,\n+ updatePendingBashMessage: (id: string, updates: Partial) => void,\n+) {\n+ const id = crypto.randomUUID()\n+\n+ // Add pending message immediately with placeholder\n+ addPendingBashMessage({\n+ id,\n+ command,\n+ output: '',\n+ exitCode: -1, // Indicates running\n+ isRunning: true,\n+ startTime: Date.now(),\n+ cwd: process.cwd(),\n+ })\n+\n+ runTerminalCommand({\n+ command,\n+ process_type: 'SYNC',\n+ cwd: process.cwd(),\n+ timeout_seconds: -1,\n+ env: process.env,\n+ })\n+ .then(([{ value }]) => {\n+ const stdout = 'stdout' in value ? value.stdout || '' : ''\n+ const stderr = 'stderr' in value ? value.stderr || '' : ''\n+ const rawOutput = stdout + stderr\n+ const output = rawOutput || '(no output)'\n+ const exitCode = 'exitCode' in value ? value.exitCode ?? 0 : 0\n+\n+ updatePendingBashMessage(id, {\n+ output,\n+ exitCode,\n+ stdout,\n+ stderr,\n+ isRunning: false,\n+ })\n+ })\n+ .catch((error) => {\n+ const errorMessage = error instanceof Error ? error.message : String(error)\n+ const output = `Error: ${errorMessage}`\n+\n+ updatePendingBashMessage(id, {\n+ output,\n+ stdout: '',\n+ stderr: errorMessage,\n+ exitCode: 1,\n+ isRunning: false,\n+ })\n+ })\n+}\n+\n export async function routeUserPrompt(\n params: RouterParams,\n ): Promise {\n const {\n@@ -40,63 +283,45 @@\n const inputMode = useChatStore.getState().inputMode\n const setInputMode = useChatStore.getState().setInputMode\n \n const trimmed = inputValue.trim()\n+ const isBusy =\n+ isStreaming ||\n+ streamMessageIdRef.current ||\n+ isChainInProgressRef.current\n if (!trimmed) return\n \n // Handle bash mode commands\n if (inputMode === 'bash') {\n const commandWithBang = '!' + trimmed\n- const toolCallId = crypto.randomUUID()\n- const resultBlock: ContentBlock = {\n- type: 'tool',\n- toolName: 'run_terminal_command',\n- toolCallId,\n- input: { command: trimmed },\n- output: '',\n- }\n-\n- setMessages((prev) => [\n- ...prev,\n- getUserMessage(commandWithBang),\n- getSystemMessage([resultBlock]),\n- ])\n-\n- runTerminalCommand({\n- command: trimmed,\n- process_type: 'SYNC',\n- cwd: process.cwd(),\n- timeout_seconds: -1,\n- env: process.env,\n- }).then(([{ value }]) => {\n- setMessages((prev) => {\n- const output = 'stdout' in value ? value.stdout : ''\n- return prev.map((msg) => {\n- if (!msg.blocks) {\n- return msg\n- }\n- return {\n- ...msg,\n- blocks: msg.blocks.map((block) =>\n- 'toolCallId' in block && block.toolCallId === toolCallId\n- ? {\n- ...block,\n- output,\n- }\n- : block,\n- ),\n- }\n- })\n- })\n- })\n-\n saveToHistory(commandWithBang)\n setInputValue({ text: '', cursorPosition: 0, lastEditDueToNav: false })\n setInputMode('default')\n+ setInputFocused(true)\n+ inputRef.current?.focus()\n \n+ if (isBusy) {\n+ const { addPendingBashMessage, updatePendingBashMessage } = useChatStore.getState()\n+ executeBashCommandAsGhost(trimmed, addPendingBashMessage, updatePendingBashMessage)\n+ } else {\n+ executeBashCommand(trimmed, setMessages)\n+ }\n return\n }\n \n+ // Handle bash commands from queue (starts with '!')\n+ if (trimmed.startsWith('!') && trimmed.length > 1) {\n+ const command = trimmed.slice(1)\n+\n+ if (isBusy) {\n+ const { addPendingBashMessage, updatePendingBashMessage } = useChatStore.getState()\n+ executeBashCommandAsGhost(command, addPendingBashMessage, updatePendingBashMessage)\n+ } else {\n+ executeBashCommand(command, setMessages)\n+ }\n+ return\n+ }\n+\n // Handle referral mode input\n if (inputMode === 'referral') {\n // Validate the referral code (3-50 alphanumeric chars with optional dashes)\n const codePattern = /^[a-zA-Z0-9-]{3,50}$/\n"
+ "diff": "Index: cli/src/commands/router.ts\n===================================================================\n--- cli/src/commands/router.ts\tb9be243 (parent)\n+++ cli/src/commands/router.ts\te9ce02a (commit)\n@@ -15,10 +15,253 @@\n } from './router-utils'\n import { useChatStore } from '../state/chat-store'\n import { getSystemMessage, getUserMessage } from '../utils/message-history'\n \n+import type { ToolMessage } from '@codebirds/common/types/messages/codebirds-message'\n+import type { ToolResultOutput } from '@codebirds/common/types/messages/content-part'\n import type { ContentBlock } from '../types/chat'\n \n+/**\n+ * Execute a bash command and add it directly to chat history.\n+ * Shows immediate placeholder while running, then updates with output.\n+ */\n+function executeBashCommand(\n+ command: string,\n+ setMessages: RouterParams['setMessages'],\n+) {\n+ const toolCallId = crypto.randomUUID()\n+ const resultBlock: ContentBlock = {\n+ type: 'tool',\n+ toolName: 'run_terminal_command',\n+ toolCallId,\n+ input: { command },\n+ output: '...',\n+ }\n+\n+ const commandCwd = process.cwd()\n+\n+ // Add the command result to chat as a user message so the AI sees it as context\n+ setMessages((prev) => [\n+ ...prev,\n+ {\n+ ...getUserMessage([resultBlock]),\n+ metadata: { bashCwd: commandCwd },\n+ },\n+ ])\n+\n+ // Execute the command and update the output when complete\n+ runTerminalCommand({\n+ command,\n+ process_type: 'SYNC',\n+ cwd: commandCwd,\n+ timeout_seconds: -1,\n+ env: process.env,\n+ }).then(([{ value }]) => {\n+ const stdout = 'stdout' in value ? (value.stdout || '') : ''\n+ const stderr = 'stderr' in value ? (value.stderr || '') : ''\n+ const exitCode = 'exitCode' in value ? value.exitCode : 0\n+\n+ // Create tool result output for display\n+ const toolResultOutput = [{\n+ type: 'json' as const,\n+ value: {\n+ command,\n+ startingCwd: commandCwd,\n+ stdout: stdout || null,\n+ stderr: stderr || null,\n+ exitCode: exitCode ?? 0,\n+ }\n+ }]\n+\n+ // Store output in JSON format for display\n+ const outputJson = JSON.stringify(toolResultOutput)\n+\n+ setMessages((prev) => {\n+ return prev.map((msg) => {\n+ if (!msg.blocks) {\n+ return msg\n+ }\n+ return {\n+ ...msg,\n+ blocks: msg.blocks.map((block) =>\n+ 'toolCallId' in block && block.toolCallId === toolCallId\n+ ? {\n+ ...block,\n+ output: outputJson,\n+ }\n+ : block,\n+ ),\n+ }\n+ })\n+ })\n+\n+ // Add to pending tool results so AI can see this in the next run\n+ const toolMessage: ToolMessage = {\n+ role: 'tool',\n+ toolCallId,\n+ toolName: 'run_terminal_command',\n+ content: toolResultOutput,\n+ }\n+ useChatStore.getState().addPendingToolResult(toolMessage)\n+ }).catch((error) => {\n+ const errorMessage = error instanceof Error ? error.message : String(error)\n+ \n+ // Create error tool result output\n+ const errorToolResultOutput = [{\n+ type: 'json' as const,\n+ value: {\n+ command,\n+ startingCwd: commandCwd,\n+ errorMessage,\n+ }\n+ }]\n+\n+ // Store error output in JSON format for display\n+ const errorOutputJson = JSON.stringify(errorToolResultOutput)\n+\n+ setMessages((prev) => {\n+ return prev.map((msg) => {\n+ if (!msg.blocks) {\n+ return msg\n+ }\n+ return {\n+ ...msg,\n+ blocks: msg.blocks.map((block) =>\n+ 'toolCallId' in block && block.toolCallId === toolCallId\n+ ? {\n+ ...block,\n+ output: errorOutputJson,\n+ }\n+ : block,\n+ ),\n+ }\n+ })\n+ })\n+\n+ // Add error result to pending tool results so AI can see this in the next run\n+ const errorToolMessage: ToolMessage = {\n+ role: 'tool',\n+ toolCallId,\n+ toolName: 'run_terminal_command',\n+ content: errorToolResultOutput,\n+ }\n+ useChatStore.getState().addPendingToolResult(errorToolMessage)\n+ })\n+}\n+\n+/**\n+ * Add a bash command result to the chat message history.\n+ * Also adds to pendingToolResults so the AI can see it in the next run.\n+ */\n+export function addBashMessageToHistory(params: {\n+ command: string\n+ stdout: string\n+ stderr: string | null | undefined\n+ exitCode: number\n+ cwd: string\n+ displayOutput?: string\n+ setMessages: RouterParams['setMessages']\n+}) {\n+ const { command, stdout, stderr, exitCode, cwd, displayOutput, setMessages } =\n+ params\n+ const outputText =\n+ displayOutput ?? (stdout || stderr ? `${stdout}${stderr ?? ''}` : '')\n+ const toolCallId = crypto.randomUUID()\n+ const resultBlock: ContentBlock = {\n+ type: 'tool',\n+ toolName: 'run_terminal_command',\n+ toolCallId,\n+ input: { command },\n+ output: outputText || '(no output)',\n+ }\n+\n+ // Add as a user message so the AI sees it as context\n+ setMessages((prev) => [\n+ ...prev,\n+ {\n+ ...getUserMessage([resultBlock]),\n+ metadata: { bashCwd: cwd },\n+ },\n+ ])\n+\n+ // Also add to pending tool results so AI can see this in the next run\n+ const toolResultOutput: ToolResultOutput[] = [{\n+ type: 'json' as const,\n+ value: {\n+ command,\n+ startingCwd: cwd,\n+ stdout: stdout || null,\n+ stderr: stderr ?? null,\n+ exitCode: exitCode ?? 0,\n+ }\n+ }]\n+ const toolMessage: ToolMessage = {\n+ role: 'tool',\n+ toolCallId,\n+ toolName: 'run_terminal_command',\n+ content: toolResultOutput,\n+ }\n+ useChatStore.getState().addPendingToolResult(toolMessage)\n+}\n+\n+/**\n+ * Execute a bash command as a ghost message in chat.\n+ * Shows as a pending message while running, then commits to history when streaming ends.\n+ */\n+function executeBashCommandAsGhost(\n+ command: string,\n+ addPendingBashMessage: (message: import('../state/chat-store').PendingBashMessage) => void,\n+ updatePendingBashMessage: (id: string, updates: Partial) => void,\n+) {\n+ const id = crypto.randomUUID()\n+\n+ // Add pending message immediately with placeholder\n+ addPendingBashMessage({\n+ id,\n+ command,\n+ output: '',\n+ exitCode: -1, // Indicates running\n+ isRunning: true,\n+ startTime: Date.now(),\n+ cwd: process.cwd(),\n+ })\n+\n+ runTerminalCommand({\n+ command,\n+ process_type: 'SYNC',\n+ cwd: process.cwd(),\n+ timeout_seconds: -1,\n+ env: process.env,\n+ })\n+ .then(([{ value }]) => {\n+ const stdout = 'stdout' in value ? value.stdout || '' : ''\n+ const stderr = 'stderr' in value ? value.stderr || '' : ''\n+ const rawOutput = stdout + stderr\n+ const output = rawOutput || '(no output)'\n+ const exitCode = 'exitCode' in value ? value.exitCode ?? 0 : 0\n+\n+ updatePendingBashMessage(id, {\n+ output,\n+ exitCode,\n+ stdout,\n+ stderr,\n+ isRunning: false,\n+ })\n+ })\n+ .catch((error) => {\n+ const errorMessage = error instanceof Error ? error.message : String(error)\n+ const output = `Error: ${errorMessage}`\n+\n+ updatePendingBashMessage(id, {\n+ output,\n+ stdout: '',\n+ stderr: errorMessage,\n+ exitCode: 1,\n+ isRunning: false,\n+ })\n+ })\n+}\n+\n export async function routeUserPrompt(\n params: RouterParams,\n ): Promise {\n const {\n@@ -40,63 +283,45 @@\n const inputMode = useChatStore.getState().inputMode\n const setInputMode = useChatStore.getState().setInputMode\n \n const trimmed = inputValue.trim()\n+ const isBusy =\n+ isStreaming ||\n+ streamMessageIdRef.current ||\n+ isChainInProgressRef.current\n if (!trimmed) return\n \n // Handle bash mode commands\n if (inputMode === 'bash') {\n const commandWithBang = '!' + trimmed\n- const toolCallId = crypto.randomUUID()\n- const resultBlock: ContentBlock = {\n- type: 'tool',\n- toolName: 'run_terminal_command',\n- toolCallId,\n- input: { command: trimmed },\n- output: '',\n- }\n-\n- setMessages((prev) => [\n- ...prev,\n- getUserMessage(commandWithBang),\n- getSystemMessage([resultBlock]),\n- ])\n-\n- runTerminalCommand({\n- command: trimmed,\n- process_type: 'SYNC',\n- cwd: process.cwd(),\n- timeout_seconds: -1,\n- env: process.env,\n- }).then(([{ value }]) => {\n- setMessages((prev) => {\n- const output = 'stdout' in value ? value.stdout : ''\n- return prev.map((msg) => {\n- if (!msg.blocks) {\n- return msg\n- }\n- return {\n- ...msg,\n- blocks: msg.blocks.map((block) =>\n- 'toolCallId' in block && block.toolCallId === toolCallId\n- ? {\n- ...block,\n- output,\n- }\n- : block,\n- ),\n- }\n- })\n- })\n- })\n-\n saveToHistory(commandWithBang)\n setInputValue({ text: '', cursorPosition: 0, lastEditDueToNav: false })\n setInputMode('default')\n+ setInputFocused(true)\n+ inputRef.current?.focus()\n \n+ if (isBusy) {\n+ const { addPendingBashMessage, updatePendingBashMessage } = useChatStore.getState()\n+ executeBashCommandAsGhost(trimmed, addPendingBashMessage, updatePendingBashMessage)\n+ } else {\n+ executeBashCommand(trimmed, setMessages)\n+ }\n return\n }\n \n+ // Handle bash commands from queue (starts with '!')\n+ if (trimmed.startsWith('!') && trimmed.length > 1) {\n+ const command = trimmed.slice(1)\n+\n+ if (isBusy) {\n+ const { addPendingBashMessage, updatePendingBashMessage } = useChatStore.getState()\n+ executeBashCommandAsGhost(command, addPendingBashMessage, updatePendingBashMessage)\n+ } else {\n+ executeBashCommand(command, setMessages)\n+ }\n+ return\n+ }\n+\n // Handle referral mode input\n if (inputMode === 'referral') {\n // Validate the referral code (3-50 alphanumeric chars with optional dashes)\n const codePattern = /^[a-zA-Z0-9-]{3,50}$/\n"
},
{
"path": "cli/src/components/message-block.tsx",
@@ -791,7 +791,7 @@
{
"path": "cli/src/hooks/use-send-message.ts",
"status": "modified",
- "diff": "Index: cli/src/hooks/use-send-message.ts\n===================================================================\n--- cli/src/hooks/use-send-message.ts\tb9be243 (parent)\n+++ cli/src/hooks/use-send-message.ts\te9ce02a (commit)\n@@ -936,13 +936,20 @@\n : 'base2-plan'\n \n let runState: RunState\n try {\n+ // Get any pending tool results from user-executed bash commands\n+ const pendingToolResults = useChatStore.getState().pendingToolResults\n+ if (pendingToolResults.length > 0) {\n+ useChatStore.getState().clearPendingToolResults()\n+ }\n+\n runState = await client.run({\n logger,\n agent: selectedAgentDefinition ?? agentId ?? fallbackAgent,\n prompt: content,\n previousRun: previousRunStateRef.current ?? undefined,\n+ extraToolResults: pendingToolResults.length > 0 ? (pendingToolResults as unknown as import('@codebuff/common/types/messages/codebuff-message').ToolMessage[]) : undefined,\n abortController,\n retry: {\n maxRetries: MAX_RETRIES_PER_MESSAGE,\n backoffBaseMs: RETRY_BACKOFF_BASE_DELAY_MS,\n"
+ "diff": "Index: cli/src/hooks/use-send-message.ts\n===================================================================\n--- cli/src/hooks/use-send-message.ts\tb9be243 (parent)\n+++ cli/src/hooks/use-send-message.ts\te9ce02a (commit)\n@@ -936,13 +936,20 @@\n : 'base2-plan'\n \n let runState: RunState\n try {\n+ // Get any pending tool results from user-executed bash commands\n+ const pendingToolResults = useChatStore.getState().pendingToolResults\n+ if (pendingToolResults.length > 0) {\n+ useChatStore.getState().clearPendingToolResults()\n+ }\n+\n runState = await client.run({\n logger,\n agent: selectedAgentDefinition ?? agentId ?? fallbackAgent,\n prompt: content,\n previousRun: previousRunStateRef.current ?? undefined,\n+ extraToolResults: pendingToolResults.length > 0 ? (pendingToolResults as unknown as import('@codebirds/common/types/messages/codebirds-message').ToolMessage[]) : undefined,\n abortController,\n retry: {\n maxRetries: MAX_RETRIES_PER_MESSAGE,\n backoffBaseMs: RETRY_BACKOFF_BASE_DELAY_MS,\n"
},
{
"path": "cli/src/state/chat-store.ts",
@@ -816,12 +816,12 @@
{
"path": "sdk/src/index.ts",
"status": "modified",
- "diff": "Index: sdk/src/index.ts\n===================================================================\n--- sdk/src/index.ts\tb9be243 (parent)\n+++ sdk/src/index.ts\te9ce02a (commit)\n@@ -1,10 +1,12 @@\n export type * from '../../common/src/types/json'\n export type * from '../../common/src/types/messages/codebuff-message'\n export type * from '../../common/src/types/messages/data-content'\n export type * from '../../common/src/types/print-mode'\n+export type { TextPart, ImagePart } from '../../common/src/types/messages/content-part'\n export { run, getRetryableErrorCode } from './run'\n export type { RunOptions, RetryOptions } from './run'\n+export { buildUserMessageContent } from '@codebuff/agent-runtime/util/messages'\n // Agent type exports\n export type { AgentDefinition } from '../../common/src/templates/initial-agents-dir/types/agent-definition'\n export type { ToolName } from '../../common/src/tools/constants'\n \n"
+ "diff": "Index: sdk/src/index.ts\n===================================================================\n--- sdk/src/index.ts\tb9be243 (parent)\n+++ sdk/src/index.ts\te9ce02a (commit)\n@@ -1,10 +1,12 @@\n export type * from '../../common/src/types/json'\n export type * from '../../common/src/types/messages/codebirds-message'\n export type * from '../../common/src/types/messages/data-content'\n export type * from '../../common/src/types/print-mode'\n+export type { TextPart, ImagePart } from '../../common/src/types/messages/content-part'\n export { run, getRetryableErrorCode } from './run'\n export type { RunOptions, RetryOptions } from './run'\n+export { buildUserMessageContent } from '@codebirds/agent-runtime/util/messages'\n // Agent type exports\n export type { AgentDefinition } from '../../common/src/templates/initial-agents-dir/types/agent-definition'\n export type { ToolName } from '../../common/src/tools/constants'\n \n"
},
{
"path": "sdk/src/run.ts",
"status": "modified",
- "diff": "Index: sdk/src/run.ts\n===================================================================\n--- sdk/src/run.ts\tb9be243 (parent)\n+++ sdk/src/run.ts\te9ce02a (commit)\n@@ -1,8 +1,11 @@\n import path from 'path'\n \n import { callMainPrompt } from '@codebuff/agent-runtime/main-prompt'\n-import { getCancelledAdditionalMessages } from '@codebuff/agent-runtime/util/messages'\n+import {\n+ asUserMessage,\n+ getCancelledAdditionalMessages,\n+} from '@codebuff/agent-runtime/util/messages'\n import { MAX_AGENT_STEPS_DEFAULT } from '@codebuff/common/constants/agents'\n import { getMCPClient, listMCPTools } from '@codebuff/common/mcp/client'\n import { toOptionalFile } from '@codebuff/common/old-constants'\n import { toolNames } from '@codebuff/common/tools/constants'\n@@ -46,16 +49,40 @@\n } from '@codebuff/common/tools/list'\n import type { Logger } from '@codebuff/common/types/contracts/logger'\n import type { CodebuffFileSystem } from '@codebuff/common/types/filesystem'\n import type {\n+ ImagePart,\n+ TextPart,\n ToolResultOutput,\n } from '@codebuff/common/types/messages/content-part'\n import type { PrintModeEvent } from '@codebuff/common/types/print-mode'\n import type { SessionState } from '@codebuff/common/types/session-state'\n import type { Source } from '@codebuff/common/types/source'\n import type { CodebuffSpawn } from '@codebuff/common/types/spawn'\n import { ToolMessage } from '@codebuff/common/types/messages/codebuff-message'\n \n+const wrapContentForUserMessage = (\n+ content?: (TextPart | ImagePart)[],\n+): (TextPart | ImagePart)[] | undefined => {\n+ if (!content || content.length === 0) {\n+ return content\n+ }\n+ let hasWrappedText = false\n+ return content.map((part) => {\n+ if (part.type === 'text' && !hasWrappedText) {\n+ hasWrappedText = true\n+ const alreadyWrapped = part.text.includes('')\n+ return alreadyWrapped\n+ ? part\n+ : {\n+ ...part,\n+ text: asUserMessage(part.text),\n+ }\n+ }\n+ return part\n+ })\n+}\n+\n export type CodebuffClientOptions = {\n apiKey?: string\n \n cwd?: string\n@@ -144,8 +171,9 @@\n export type RunOptions = {\n agent: string | AgentDefinition\n prompt: string\n params?: Record\n+ content?: (TextPart | ImagePart)[]\n previousRun?: RunState\n extraToolResults?: ToolMessage[]\n signal?: AbortSignal\n abortController?: AbortController\n@@ -478,16 +506,18 @@\n \n agent,\n prompt,\n params,\n+ content,\n previousRun,\n extraToolResults,\n signal,\n }: RunOnceOptions): Promise {\n const fs = await (typeof fsSource === 'function' ? fsSource() : fsSource)\n const spawn: CodebuffSpawn = (\n spawnSource ? await spawnSource : require('child_process').spawn\n ) as CodebuffSpawn\n+ const preparedContent = wrapContentForUserMessage(content)\n \n // Init session state\n let agentId\n if (typeof agent !== 'string') {\n@@ -548,8 +578,9 @@\n state.mainAgentState.messageHistory.push(\n ...getCancelledAdditionalMessages({\n prompt,\n params,\n+ content: preparedContent,\n pendingAgentResponse,\n systemMessage: message,\n }),\n )\n@@ -786,8 +817,9 @@\n type: 'prompt',\n promptId,\n prompt,\n promptParams: params,\n+ content: preparedContent,\n fingerprintId: fingerprintId,\n costMode: 'normal',\n sessionState,\n toolResults: extraToolResults ?? [],\n"
+ "diff": "Index: sdk/src/run.ts\n===================================================================\n--- sdk/src/run.ts\tb9be243 (parent)\n+++ sdk/src/run.ts\te9ce02a (commit)\n@@ -1,8 +1,11 @@\n import path from 'path'\n \n import { callMainPrompt } from '@codebirds/agent-runtime/main-prompt'\n-import { getCancelledAdditionalMessages } from '@codebirds/agent-runtime/util/messages'\n+import {\n+ asUserMessage,\n+ getCancelledAdditionalMessages,\n+} from '@codebirds/agent-runtime/util/messages'\n import { MAX_AGENT_STEPS_DEFAULT } from '@codebirds/common/constants/agents'\n import { getMCPClient, listMCPTools } from '@codebirds/common/mcp/client'\n import { toOptionalFile } from '@codebirds/common/old-constants'\n import { toolNames } from '@codebirds/common/tools/constants'\n@@ -46,16 +49,40 @@\n } from '@codebirds/common/tools/list'\n import type { Logger } from '@codebirds/common/types/contracts/logger'\n import type { CodebirdsFileSystem } from '@codebirds/common/types/filesystem'\n import type {\n+ ImagePart,\n+ TextPart,\n ToolResultOutput,\n } from '@codebirds/common/types/messages/content-part'\n import type { PrintModeEvent } from '@codebirds/common/types/print-mode'\n import type { SessionState } from '@codebirds/common/types/session-state'\n import type { Source } from '@codebirds/common/types/source'\n import type { CodebirdsSpawn } from '@codebirds/common/types/spawn'\n import { ToolMessage } from '@codebirds/common/types/messages/codebirds-message'\n \n+const wrapContentForUserMessage = (\n+ content?: (TextPart | ImagePart)[],\n+): (TextPart | ImagePart)[] | undefined => {\n+ if (!content || content.length === 0) {\n+ return content\n+ }\n+ let hasWrappedText = false\n+ return content.map((part) => {\n+ if (part.type === 'text' && !hasWrappedText) {\n+ hasWrappedText = true\n+ const alreadyWrapped = part.text.includes('')\n+ return alreadyWrapped\n+ ? part\n+ : {\n+ ...part,\n+ text: asUserMessage(part.text),\n+ }\n+ }\n+ return part\n+ })\n+}\n+\n export type CodebirdsClientOptions = {\n apiKey?: string\n \n cwd?: string\n@@ -144,8 +171,9 @@\n export type RunOptions = {\n agent: string | AgentDefinition\n prompt: string\n params?: Record\n+ content?: (TextPart | ImagePart)[]\n previousRun?: RunState\n extraToolResults?: ToolMessage[]\n signal?: AbortSignal\n abortController?: AbortController\n@@ -478,16 +506,18 @@\n \n agent,\n prompt,\n params,\n+ content,\n previousRun,\n extraToolResults,\n signal,\n }: RunOnceOptions): Promise {\n const fs = await (typeof fsSource === 'function' ? fsSource() : fsSource)\n const spawn: CodebirdsSpawn = (\n spawnSource ? await spawnSource : require('child_process').spawn\n ) as CodebirdsSpawn\n+ const preparedContent = wrapContentForUserMessage(content)\n \n // Init session state\n let agentId\n if (typeof agent !== 'string') {\n@@ -548,8 +578,9 @@\n state.mainAgentState.messageHistory.push(\n ...getCancelledAdditionalMessages({\n prompt,\n params,\n+ content: preparedContent,\n pendingAgentResponse,\n systemMessage: message,\n }),\n )\n@@ -786,8 +817,9 @@\n type: 'prompt',\n promptId,\n prompt,\n promptParams: params,\n+ content: preparedContent,\n fingerprintId: fingerprintId,\n costMode: 'normal',\n sessionState,\n toolResults: extraToolResults ?? [],\n"
}
]
},
@@ -842,7 +842,7 @@
{
"path": "cli/src/chat.tsx",
"status": "modified",
- "diff": "Index: cli/src/chat.tsx\n===================================================================\n--- cli/src/chat.tsx\t65eacef (parent)\n+++ cli/src/chat.tsx\tdc4445a (commit)\n@@ -1,6 +1,5 @@\n import { RECONNECTION_MESSAGE_DURATION_MS } from '@codebuff/sdk'\n-import { useKeyboard } from '@opentui/react'\n import { useQueryClient } from '@tanstack/react-query'\n import {\n useCallback,\n useEffect,\n@@ -27,16 +26,16 @@\n import { useElapsedTime } from './hooks/use-elapsed-time'\n import { useEvent } from './hooks/use-event'\n import { useExitHandler } from './hooks/use-exit-handler'\n import { useInputHistory } from './hooks/use-input-history'\n-import { useKeyboardHandlers } from './hooks/use-keyboard-handlers'\n+import { useChatKeyboard, type ChatKeyboardHandlers } from './hooks/use-chat-keyboard'\n+import { type ChatKeyboardState, createDefaultChatKeyboardState } from './utils/keyboard-actions'\n import { useMessageQueue } from './hooks/use-message-queue'\n import { useQueueControls } from './hooks/use-queue-controls'\n import { useQueueUi } from './hooks/use-queue-ui'\n import { useChatScrollbox } from './hooks/use-scroll-management'\n import { useSendMessage } from './hooks/use-send-message'\n import { useSuggestionEngine } from './hooks/use-suggestion-engine'\n-import { useSuggestionMenuHandlers } from './hooks/use-suggestion-menu-handlers'\n import { useTerminalDimensions } from './hooks/use-terminal-dimensions'\n import { useTheme } from './hooks/use-theme'\n import { useTimeout } from './hooks/use-timeout'\n import { useUsageMonitor } from './hooks/use-usage-monitor'\n@@ -59,9 +58,9 @@\n import type { SendMessageFn } from './types/contracts/send-message'\n import type { User } from './utils/auth'\n import type { AgentMode } from './utils/constants'\n import type { FileTreeNode } from '@codebuff/common/util/file'\n-import type { KeyEvent, ScrollBoxRenderable } from '@opentui/core'\n+import type { ScrollBoxRenderable } from '@opentui/core'\n import type { UseMutationResult } from '@tanstack/react-query'\n import type { Dispatch, SetStateAction } from 'react'\n \n export const Chat = ({\n@@ -419,8 +418,10 @@\n : scrollboxProps\n \n const localAgents = useMemo(() => loadLocalAgents(), [])\n const inputMode = useChatStore((state) => state.inputMode)\n+ const setInputMode = useChatStore((state) => state.setInputMode)\n+ const askUserState = useChatStore((state) => state.askUserState)\n \n const {\n slashContext,\n mentionContext,\n@@ -485,23 +486,8 @@\n agentSelectedIndex,\n setAgentSelectedIndex,\n ])\n \n- const { handleSuggestionMenuKey: handleSuggestionMenuKeyInternal } =\n- useSuggestionMenuHandlers({\n- slashContext,\n- mentionContext,\n- slashMatches,\n- agentMatches,\n- fileMatches,\n- slashSelectedIndex,\n- agentSelectedIndex,\n- inputValue,\n- setInputValue,\n- setSlashSelectedIndex,\n- setAgentSelectedIndex,\n- disableSlashMenu: getInputModeConfig(inputMode).disableSlashSuggestions,\n- })\n const openFileMenuWithTab = useCallback(() => {\n const safeCursor = Math.max(0, Math.min(cursorPosition, inputValue.length))\n \n let wordStart = safeCursor\n@@ -526,62 +512,8 @@\n })\n setForceFileOnlyMentions(true)\n }, [cursorPosition, inputValue, setInputValue])\n \n- const handleSuggestionMenuKey = useCallback(\n- (key: KeyEvent): boolean => {\n- // In bash mode at cursor position 0, backspace should exit bash mode\n- const inputMode = useChatStore.getState().inputMode\n- // Exit special modes on backspace at position 0\n- if (\n- inputMode !== 'default' &&\n- cursorPosition === 0 &&\n- key.name === 'backspace'\n- ) {\n- useChatStore.getState().setInputMode('default')\n- return true\n- }\n-\n- if (handleSuggestionMenuKeyInternal(key)) {\n- return true\n- }\n-\n- const isPlainTab =\n- key &&\n- key.name === 'tab' &&\n- !key.shift &&\n- !key.ctrl &&\n- !key.meta &&\n- !key.option\n-\n- if (isPlainTab && !mentionContext.active) {\n- // Only open file menu if there's a word at cursor to complete\n- const safeCursor = Math.max(\n- 0,\n- Math.min(cursorPosition, inputValue.length),\n- )\n- let wordStart = safeCursor\n- while (wordStart > 0 && !/\\s/.test(inputValue[wordStart - 1])) {\n- wordStart--\n- }\n- const hasWordAtCursor = wordStart < safeCursor\n-\n- if (hasWordAtCursor) {\n- openFileMenuWithTab()\n- return true\n- }\n- }\n-\n- return false\n- },\n- [\n- handleSuggestionMenuKeyInternal,\n- mentionContext.active,\n- openFileMenuWithTab,\n- inputValue,\n- ],\n- )\n-\n const { saveToHistory, navigateUp, navigateDown } = useInputHistory(\n inputValue,\n setInputValue,\n )\n@@ -718,16 +650,18 @@\n openFeedbackForMessage,\n closeFeedback,\n saveCurrentInput,\n restoreSavedInput,\n+ setFeedbackText,\n } = useFeedbackStore(\n useShallow((state) => ({\n feedbackMode: state.feedbackMode,\n feedbackMessageId: state.feedbackMessageId,\n openFeedbackForMessage: state.openFeedbackForMessage,\n closeFeedback: state.closeFeedback,\n saveCurrentInput: state.saveCurrentInput,\n restoreSavedInput: state.restoreSavedInput,\n+ setFeedbackText: state.setFeedbackText,\n })),\n )\n \n const inputValueRef = useRef(inputValue)\n@@ -865,30 +799,184 @@\n (mentionContext.active &&\n agentSelectedIndex === totalMentionMatches - 1) ||\n (!slashContext.active && !mentionContext.active)))\n \n- useKeyboardHandlers({\n+ // Build keyboard state from store values\n+ const chatKeyboardState: ChatKeyboardState = useMemo(() => ({\n+ ...createDefaultChatKeyboardState(),\n+ inputMode,\n+ inputValue,\n+ cursorPosition,\n isStreaming,\n isWaitingForResponse,\n- abortControllerRef,\n+ feedbackMode,\n focusedAgentId,\n- setFocusedAgentId,\n- setInputFocused,\n- inputRef,\n- navigateUp,\n- navigateDown,\n- toggleAgentMode,\n- onCtrlC: handleCtrlC,\n- onInterrupt: () => {\n+ slashMenuActive: slashContext.active,\n+ mentionMenuActive: mentionContext.active,\n+ slashSelectedIndex,\n+ agentSelectedIndex,\n+ slashMatchesLength: slashMatches.length,\n+ totalMentionMatches: agentMatches.length + fileMatches.length,\n+ disableSlashSuggestions: getInputModeConfig(inputMode).disableSlashSuggestions,\n+ historyNavUpEnabled,\n+ historyNavDownEnabled,\n+ nextCtrlCWillExit,\n+ queuePaused,\n+ queuedCount,\n+ }), [\n+ inputMode,\n+ inputValue,\n+ cursorPosition,\n+ isStreaming,\n+ isWaitingForResponse,\n+ feedbackMode,\n+ focusedAgentId,\n+ slashContext.active,\n+ mentionContext.active,\n+ slashSelectedIndex,\n+ agentSelectedIndex,\n+ slashMatches.length,\n+ agentMatches.length,\n+ fileMatches.length,\n+ historyNavUpEnabled,\n+ historyNavDownEnabled,\n+ nextCtrlCWillExit,\n+ queuePaused,\n+ queuedCount,\n+ ])\n+\n+ // Keyboard handlers\n+ const chatKeyboardHandlers: ChatKeyboardHandlers = useMemo(() => ({\n+ onExitInputMode: () => setInputMode('default'),\n+ onExitFeedbackMode: handleCloseFeedback,\n+ onClearFeedbackInput: () => {\n+ setFeedbackText('')\n+ useFeedbackStore.getState().setFeedbackCursor(0)\n+ useFeedbackStore.getState().setFeedbackCategory('other')\n+ },\n+ onClearInput: () => setInputValue({ text: '', cursorPosition: 0, lastEditDueToNav: false }),\n+ onBackspaceExitMode: () => setInputMode('default'),\n+ onInterruptStream: () => {\n+ abortControllerRef.current?.abort()\n if (queuedMessages.length > 0) {\n pauseQueue()\n }\n },\n- historyNavUpEnabled,\n- historyNavDownEnabled,\n- disabled: feedbackMode,\n- inputValue,\n+ onSlashMenuDown: () => setSlashSelectedIndex((prev) => prev + 1),\n+ onSlashMenuUp: () => setSlashSelectedIndex((prev) => prev - 1),\n+ onSlashMenuTab: () => setSlashSelectedIndex((prev) => (prev + 1) % slashMatches.length),\n+ onSlashMenuShiftTab: () => setSlashSelectedIndex((prev) => (slashMatches.length + prev - 1) % slashMatches.length),\n+ onSlashMenuSelect: () => {\n+ const selected = slashMatches[slashSelectedIndex] || slashMatches[0]\n+ if (!selected || slashContext.startIndex < 0) return\n+ const before = inputValue.slice(0, slashContext.startIndex)\n+ const after = inputValue.slice(slashContext.startIndex + 1 + slashContext.query.length)\n+ const replacement = `/${selected.id} `\n+ setInputValue({\n+ text: before + replacement + after,\n+ cursorPosition: before.length + replacement.length,\n+ lastEditDueToNav: false,\n+ })\n+ setSlashSelectedIndex(0)\n+ },\n+ onMentionMenuDown: () => setAgentSelectedIndex((prev) => prev + 1),\n+ onMentionMenuUp: () => setAgentSelectedIndex((prev) => prev - 1),\n+ onMentionMenuTab: () => {\n+ const totalMatches = agentMatches.length + fileMatches.length\n+ setAgentSelectedIndex((prev) => (prev + 1) % totalMatches)\n+ },\n+ onMentionMenuShiftTab: () => {\n+ const totalMatches = agentMatches.length + fileMatches.length\n+ setAgentSelectedIndex((prev) => (totalMatches + prev - 1) % totalMatches)\n+ },\n+ onMentionMenuSelect: () => {\n+ if (mentionContext.startIndex < 0) return\n+\n+ const trySelectAtIndex = (index: number): boolean => {\n+ let replacement: string\n+ if (index < agentMatches.length) {\n+ const selected = agentMatches[index]\n+ if (!selected) return false\n+ replacement = `@${selected.displayName} `\n+ } else {\n+ const fileIndex = index - agentMatches.length\n+ const selectedFile = fileMatches[fileIndex]\n+ if (!selectedFile) return false\n+ replacement = `@${selectedFile.filePath} `\n+ }\n+ const before = inputValue.slice(0, mentionContext.startIndex)\n+ const after = inputValue.slice(mentionContext.startIndex + 1 + mentionContext.query.length)\n+ setInputValue({\n+ text: before + replacement + after,\n+ cursorPosition: before.length + replacement.length,\n+ lastEditDueToNav: false,\n+ })\n+ setAgentSelectedIndex(0)\n+ return true\n+ }\n+\n+ // Try current selection, fall back to first item\n+ trySelectAtIndex(agentSelectedIndex) || trySelectAtIndex(0)\n+ },\n+ onOpenFileMenuWithTab: () => {\n+ const safeCursor = Math.max(0, Math.min(cursorPosition, inputValue.length))\n+ let wordStart = safeCursor\n+ while (wordStart > 0 && !/\\s/.test(inputValue[wordStart - 1]!)) {\n+ wordStart--\n+ }\n+ if (wordStart < safeCursor) {\n+ openFileMenuWithTab()\n+ return true\n+ }\n+ return false\n+ },\n+ onHistoryUp: navigateUp,\n+ onHistoryDown: navigateDown,\n+ onToggleAgentMode: toggleAgentMode,\n+ onUnfocusAgent: () => {\n+ setFocusedAgentId(null)\n+ setInputFocused(true)\n+ inputRef.current?.focus()\n+ },\n+ onClearQueue: clearQueue,\n+ onExitAppWarning: () => handleCtrlC(),\n+ onExitApp: () => handleCtrlC(),\n+ }), [\n+ setInputMode,\n+ handleCloseFeedback,\n+ setFeedbackText,\n setInputValue,\n+ abortControllerRef,\n+ queuedMessages.length,\n+ pauseQueue,\n+ setSlashSelectedIndex,\n+ slashMatches,\n+ slashSelectedIndex,\n+ slashContext,\n+ inputValue,\n+ setAgentSelectedIndex,\n+ agentMatches,\n+ fileMatches,\n+ agentSelectedIndex,\n+ mentionContext,\n+ cursorPosition,\n+ openFileMenuWithTab,\n+ saveCurrentInput,\n+ navigateUp,\n+ navigateDown,\n+ toggleAgentMode,\n+ setFocusedAgentId,\n+ setInputFocused,\n+ inputRef,\n+ handleCtrlC,\n+ clearQueue,\n+ ])\n+\n+ // Use the chat keyboard hook\n+ useChatKeyboard({\n+ state: chatKeyboardState,\n+ handlers: chatKeyboardHandlers,\n+ disabled: askUserState !== null,\n })\n \n const { tree: messageTree, topLevelMessages } = useMemo(\n () => buildMessageTree(messages),\n@@ -1064,8 +1152,9 @@\n inputFocused={inputFocused}\n inputRef={inputRef}\n inputPlaceholder={inputPlaceholder}\n inputWidth={inputWidth}\n+ lastEditDueToNav={lastEditDueToNav}\n agentMode={agentMode}\n toggleAgentMode={toggleAgentMode}\n setAgentMode={setAgentMode}\n hasSlashSuggestions={hasSlashSuggestions}\n@@ -1075,9 +1164,8 @@\n agentSuggestionItems={agentSuggestionItems}\n fileSuggestionItems={fileSuggestionItems}\n slashSelectedIndex={slashSelectedIndex}\n agentSelectedIndex={agentSelectedIndex}\n- handleSuggestionMenuKey={handleSuggestionMenuKey}\n theme={theme}\n terminalHeight={terminalHeight}\n separatorWidth={separatorWidth}\n shouldCenterInputVertically={shouldCenterInputVertically}\n"
+ "diff": "Index: cli/src/chat.tsx\n===================================================================\n--- cli/src/chat.tsx\t65eacef (parent)\n+++ cli/src/chat.tsx\tdc4445a (commit)\n@@ -1,6 +1,5 @@\n import { RECONNECTION_MESSAGE_DURATION_MS } from '@codebirds/sdk'\n-import { useKeyboard } from '@opentui/react'\n import { useQueryClient } from '@tanstack/react-query'\n import {\n useCallback,\n useEffect,\n@@ -27,16 +26,16 @@\n import { useElapsedTime } from './hooks/use-elapsed-time'\n import { useEvent } from './hooks/use-event'\n import { useExitHandler } from './hooks/use-exit-handler'\n import { useInputHistory } from './hooks/use-input-history'\n-import { useKeyboardHandlers } from './hooks/use-keyboard-handlers'\n+import { useChatKeyboard, type ChatKeyboardHandlers } from './hooks/use-chat-keyboard'\n+import { type ChatKeyboardState, createDefaultChatKeyboardState } from './utils/keyboard-actions'\n import { useMessageQueue } from './hooks/use-message-queue'\n import { useQueueControls } from './hooks/use-queue-controls'\n import { useQueueUi } from './hooks/use-queue-ui'\n import { useChatScrollbox } from './hooks/use-scroll-management'\n import { useSendMessage } from './hooks/use-send-message'\n import { useSuggestionEngine } from './hooks/use-suggestion-engine'\n-import { useSuggestionMenuHandlers } from './hooks/use-suggestion-menu-handlers'\n import { useTerminalDimensions } from './hooks/use-terminal-dimensions'\n import { useTheme } from './hooks/use-theme'\n import { useTimeout } from './hooks/use-timeout'\n import { useUsageMonitor } from './hooks/use-usage-monitor'\n@@ -59,9 +58,9 @@\n import type { SendMessageFn } from './types/contracts/send-message'\n import type { User } from './utils/auth'\n import type { AgentMode } from './utils/constants'\n import type { FileTreeNode } from '@codebirds/common/util/file'\n-import type { KeyEvent, ScrollBoxRenderable } from '@opentui/core'\n+import type { ScrollBoxRenderable } from '@opentui/core'\n import type { UseMutationResult } from '@tanstack/react-query'\n import type { Dispatch, SetStateAction } from 'react'\n \n export const Chat = ({\n@@ -419,8 +418,10 @@\n : scrollboxProps\n \n const localAgents = useMemo(() => loadLocalAgents(), [])\n const inputMode = useChatStore((state) => state.inputMode)\n+ const setInputMode = useChatStore((state) => state.setInputMode)\n+ const askUserState = useChatStore((state) => state.askUserState)\n \n const {\n slashContext,\n mentionContext,\n@@ -485,23 +486,8 @@\n agentSelectedIndex,\n setAgentSelectedIndex,\n ])\n \n- const { handleSuggestionMenuKey: handleSuggestionMenuKeyInternal } =\n- useSuggestionMenuHandlers({\n- slashContext,\n- mentionContext,\n- slashMatches,\n- agentMatches,\n- fileMatches,\n- slashSelectedIndex,\n- agentSelectedIndex,\n- inputValue,\n- setInputValue,\n- setSlashSelectedIndex,\n- setAgentSelectedIndex,\n- disableSlashMenu: getInputModeConfig(inputMode).disableSlashSuggestions,\n- })\n const openFileMenuWithTab = useCallback(() => {\n const safeCursor = Math.max(0, Math.min(cursorPosition, inputValue.length))\n \n let wordStart = safeCursor\n@@ -526,62 +512,8 @@\n })\n setForceFileOnlyMentions(true)\n }, [cursorPosition, inputValue, setInputValue])\n \n- const handleSuggestionMenuKey = useCallback(\n- (key: KeyEvent): boolean => {\n- // In bash mode at cursor position 0, backspace should exit bash mode\n- const inputMode = useChatStore.getState().inputMode\n- // Exit special modes on backspace at position 0\n- if (\n- inputMode !== 'default' &&\n- cursorPosition === 0 &&\n- key.name === 'backspace'\n- ) {\n- useChatStore.getState().setInputMode('default')\n- return true\n- }\n-\n- if (handleSuggestionMenuKeyInternal(key)) {\n- return true\n- }\n-\n- const isPlainTab =\n- key &&\n- key.name === 'tab' &&\n- !key.shift &&\n- !key.ctrl &&\n- !key.meta &&\n- !key.option\n-\n- if (isPlainTab && !mentionContext.active) {\n- // Only open file menu if there's a word at cursor to complete\n- const safeCursor = Math.max(\n- 0,\n- Math.min(cursorPosition, inputValue.length),\n- )\n- let wordStart = safeCursor\n- while (wordStart > 0 && !/\\s/.test(inputValue[wordStart - 1])) {\n- wordStart--\n- }\n- const hasWordAtCursor = wordStart < safeCursor\n-\n- if (hasWordAtCursor) {\n- openFileMenuWithTab()\n- return true\n- }\n- }\n-\n- return false\n- },\n- [\n- handleSuggestionMenuKeyInternal,\n- mentionContext.active,\n- openFileMenuWithTab,\n- inputValue,\n- ],\n- )\n-\n const { saveToHistory, navigateUp, navigateDown } = useInputHistory(\n inputValue,\n setInputValue,\n )\n@@ -718,16 +650,18 @@\n openFeedbackForMessage,\n closeFeedback,\n saveCurrentInput,\n restoreSavedInput,\n+ setFeedbackText,\n } = useFeedbackStore(\n useShallow((state) => ({\n feedbackMode: state.feedbackMode,\n feedbackMessageId: state.feedbackMessageId,\n openFeedbackForMessage: state.openFeedbackForMessage,\n closeFeedback: state.closeFeedback,\n saveCurrentInput: state.saveCurrentInput,\n restoreSavedInput: state.restoreSavedInput,\n+ setFeedbackText: state.setFeedbackText,\n })),\n )\n \n const inputValueRef = useRef(inputValue)\n@@ -865,30 +799,184 @@\n (mentionContext.active &&\n agentSelectedIndex === totalMentionMatches - 1) ||\n (!slashContext.active && !mentionContext.active)))\n \n- useKeyboardHandlers({\n+ // Build keyboard state from store values\n+ const chatKeyboardState: ChatKeyboardState = useMemo(() => ({\n+ ...createDefaultChatKeyboardState(),\n+ inputMode,\n+ inputValue,\n+ cursorPosition,\n isStreaming,\n isWaitingForResponse,\n- abortControllerRef,\n+ feedbackMode,\n focusedAgentId,\n- setFocusedAgentId,\n- setInputFocused,\n- inputRef,\n- navigateUp,\n- navigateDown,\n- toggleAgentMode,\n- onCtrlC: handleCtrlC,\n- onInterrupt: () => {\n+ slashMenuActive: slashContext.active,\n+ mentionMenuActive: mentionContext.active,\n+ slashSelectedIndex,\n+ agentSelectedIndex,\n+ slashMatchesLength: slashMatches.length,\n+ totalMentionMatches: agentMatches.length + fileMatches.length,\n+ disableSlashSuggestions: getInputModeConfig(inputMode).disableSlashSuggestions,\n+ historyNavUpEnabled,\n+ historyNavDownEnabled,\n+ nextCtrlCWillExit,\n+ queuePaused,\n+ queuedCount,\n+ }), [\n+ inputMode,\n+ inputValue,\n+ cursorPosition,\n+ isStreaming,\n+ isWaitingForResponse,\n+ feedbackMode,\n+ focusedAgentId,\n+ slashContext.active,\n+ mentionContext.active,\n+ slashSelectedIndex,\n+ agentSelectedIndex,\n+ slashMatches.length,\n+ agentMatches.length,\n+ fileMatches.length,\n+ historyNavUpEnabled,\n+ historyNavDownEnabled,\n+ nextCtrlCWillExit,\n+ queuePaused,\n+ queuedCount,\n+ ])\n+\n+ // Keyboard handlers\n+ const chatKeyboardHandlers: ChatKeyboardHandlers = useMemo(() => ({\n+ onExitInputMode: () => setInputMode('default'),\n+ onExitFeedbackMode: handleCloseFeedback,\n+ onClearFeedbackInput: () => {\n+ setFeedbackText('')\n+ useFeedbackStore.getState().setFeedbackCursor(0)\n+ useFeedbackStore.getState().setFeedbackCategory('other')\n+ },\n+ onClearInput: () => setInputValue({ text: '', cursorPosition: 0, lastEditDueToNav: false }),\n+ onBackspaceExitMode: () => setInputMode('default'),\n+ onInterruptStream: () => {\n+ abortControllerRef.current?.abort()\n if (queuedMessages.length > 0) {\n pauseQueue()\n }\n },\n- historyNavUpEnabled,\n- historyNavDownEnabled,\n- disabled: feedbackMode,\n- inputValue,\n+ onSlashMenuDown: () => setSlashSelectedIndex((prev) => prev + 1),\n+ onSlashMenuUp: () => setSlashSelectedIndex((prev) => prev - 1),\n+ onSlashMenuTab: () => setSlashSelectedIndex((prev) => (prev + 1) % slashMatches.length),\n+ onSlashMenuShiftTab: () => setSlashSelectedIndex((prev) => (slashMatches.length + prev - 1) % slashMatches.length),\n+ onSlashMenuSelect: () => {\n+ const selected = slashMatches[slashSelectedIndex] || slashMatches[0]\n+ if (!selected || slashContext.startIndex < 0) return\n+ const before = inputValue.slice(0, slashContext.startIndex)\n+ const after = inputValue.slice(slashContext.startIndex + 1 + slashContext.query.length)\n+ const replacement = `/${selected.id} `\n+ setInputValue({\n+ text: before + replacement + after,\n+ cursorPosition: before.length + replacement.length,\n+ lastEditDueToNav: false,\n+ })\n+ setSlashSelectedIndex(0)\n+ },\n+ onMentionMenuDown: () => setAgentSelectedIndex((prev) => prev + 1),\n+ onMentionMenuUp: () => setAgentSelectedIndex((prev) => prev - 1),\n+ onMentionMenuTab: () => {\n+ const totalMatches = agentMatches.length + fileMatches.length\n+ setAgentSelectedIndex((prev) => (prev + 1) % totalMatches)\n+ },\n+ onMentionMenuShiftTab: () => {\n+ const totalMatches = agentMatches.length + fileMatches.length\n+ setAgentSelectedIndex((prev) => (totalMatches + prev - 1) % totalMatches)\n+ },\n+ onMentionMenuSelect: () => {\n+ if (mentionContext.startIndex < 0) return\n+\n+ const trySelectAtIndex = (index: number): boolean => {\n+ let replacement: string\n+ if (index < agentMatches.length) {\n+ const selected = agentMatches[index]\n+ if (!selected) return false\n+ replacement = `@${selected.displayName} `\n+ } else {\n+ const fileIndex = index - agentMatches.length\n+ const selectedFile = fileMatches[fileIndex]\n+ if (!selectedFile) return false\n+ replacement = `@${selectedFile.filePath} `\n+ }\n+ const before = inputValue.slice(0, mentionContext.startIndex)\n+ const after = inputValue.slice(mentionContext.startIndex + 1 + mentionContext.query.length)\n+ setInputValue({\n+ text: before + replacement + after,\n+ cursorPosition: before.length + replacement.length,\n+ lastEditDueToNav: false,\n+ })\n+ setAgentSelectedIndex(0)\n+ return true\n+ }\n+\n+ // Try current selection, fall back to first item\n+ trySelectAtIndex(agentSelectedIndex) || trySelectAtIndex(0)\n+ },\n+ onOpenFileMenuWithTab: () => {\n+ const safeCursor = Math.max(0, Math.min(cursorPosition, inputValue.length))\n+ let wordStart = safeCursor\n+ while (wordStart > 0 && !/\\s/.test(inputValue[wordStart - 1]!)) {\n+ wordStart--\n+ }\n+ if (wordStart < safeCursor) {\n+ openFileMenuWithTab()\n+ return true\n+ }\n+ return false\n+ },\n+ onHistoryUp: navigateUp,\n+ onHistoryDown: navigateDown,\n+ onToggleAgentMode: toggleAgentMode,\n+ onUnfocusAgent: () => {\n+ setFocusedAgentId(null)\n+ setInputFocused(true)\n+ inputRef.current?.focus()\n+ },\n+ onClearQueue: clearQueue,\n+ onExitAppWarning: () => handleCtrlC(),\n+ onExitApp: () => handleCtrlC(),\n+ }), [\n+ setInputMode,\n+ handleCloseFeedback,\n+ setFeedbackText,\n setInputValue,\n+ abortControllerRef,\n+ queuedMessages.length,\n+ pauseQueue,\n+ setSlashSelectedIndex,\n+ slashMatches,\n+ slashSelectedIndex,\n+ slashContext,\n+ inputValue,\n+ setAgentSelectedIndex,\n+ agentMatches,\n+ fileMatches,\n+ agentSelectedIndex,\n+ mentionContext,\n+ cursorPosition,\n+ openFileMenuWithTab,\n+ saveCurrentInput,\n+ navigateUp,\n+ navigateDown,\n+ toggleAgentMode,\n+ setFocusedAgentId,\n+ setInputFocused,\n+ inputRef,\n+ handleCtrlC,\n+ clearQueue,\n+ ])\n+\n+ // Use the chat keyboard hook\n+ useChatKeyboard({\n+ state: chatKeyboardState,\n+ handlers: chatKeyboardHandlers,\n+ disabled: askUserState !== null,\n })\n \n const { tree: messageTree, topLevelMessages } = useMemo(\n () => buildMessageTree(messages),\n@@ -1064,8 +1152,9 @@\n inputFocused={inputFocused}\n inputRef={inputRef}\n inputPlaceholder={inputPlaceholder}\n inputWidth={inputWidth}\n+ lastEditDueToNav={lastEditDueToNav}\n agentMode={agentMode}\n toggleAgentMode={toggleAgentMode}\n setAgentMode={setAgentMode}\n hasSlashSuggestions={hasSlashSuggestions}\n@@ -1075,9 +1164,8 @@\n agentSuggestionItems={agentSuggestionItems}\n fileSuggestionItems={fileSuggestionItems}\n slashSelectedIndex={slashSelectedIndex}\n agentSelectedIndex={agentSelectedIndex}\n- handleSuggestionMenuKey={handleSuggestionMenuKey}\n theme={theme}\n terminalHeight={terminalHeight}\n separatorWidth={separatorWidth}\n shouldCenterInputVertically={shouldCenterInputVertically}\n"
},
{
"path": "cli/src/components/chat-input-bar.tsx",
@@ -890,7 +890,7 @@
"id": "usage-monitoring-ux",
"sha": "c2274f2877e0f4d74f45c083c0a3761bcbb85986",
"parentSha": "6c58d85151afdfda423c239f8f1631aaadeef029",
- "spec": "Implement cross-system usage/credits monitoring and 402 handling with the following behavior and structure:\n\nCLI: auto-show usage banner and keep it fresh\n1) Add a usage monitor hook that runs continuously and decides when to auto-show the usage banner\n- File: cli/src/hooks/use-usage-monitor.ts (new)\n - Create a hook that:\n - Subscribes to useChatStore for isChainInProgress, sessionCreditsUsed, and setInputMode\n - Calls useUsageQuery({ enabled: true }) to fetch usage\n - Uses getAuthToken() to check authentication\n - Uses shouldAutoShowBanner(...) to decide if the banner should auto-show when credit thresholds are crossed\n - Tracks last warned threshold in a ref (number | null) and updates it based on decisions\n - Calls setInputMode('usage') only when decision.shouldShow is true, and only after at least one message has been sent (sessionCreditsUsed > 0)\n\n2) Integrate the usage monitor into the Chat component\n- File: cli/src/chat.tsx\n - Import the new hook and invoke it once inside Chat so it runs for the app lifetime\n\n3) Update the usage banner to support deterministic auto-hide and terminal-safe polling\n- File: cli/src/components/usage-banner.tsx\n - Change component to accept a prop showTime: number\n - Remove internal auto-show logic and lastWarnedState; rely on the global useUsageMonitor hook\n - Keep the banner visible for MANUAL_SHOW_TIMEOUT after showTime and auto-hide by calling setInputMode('default')\n - Use useQueryClient with a setInterval to invalidate usageQueryKeys.current() every 30 seconds (USAGE_POLL_INTERVAL) because refetchInterval is unreliable in terminal environments\n - Continue to display loading and final text using generateLoadingBannerText, generateUsageBannerText, and getBannerColorLevel\n\n4) Pass show timing from the input bar when usage mode is active\n- File: cli/src/components/chat-input-bar.tsx\n - Track usageBannerShowTime in local state and update it whenever inputMode === 'usage'\n - Pass usageBannerShowTime to UsageBanner via InputModeBanner; update InputModeBanner’s signature accordingly\n\n5) Adjust use-usage-query options for CLI polling\n- File: cli/src/hooks/use-usage-query.ts\n - Extend signature to accept refetchInterval and refetchIntervalInBackground options\n - Set defaults: staleTime: 0 and refetchOnMount: 'always' so banner opens with fresh data\n\n6) Ensure TanStack Query thinks the CLI app is focused (so refetchInterval works if used)\n- File: cli/src/index.tsx\n - Import focusManager from @tanstack/react-query\n - Set a no-op focus event listener and focusManager.setFocused(true) during startup\n\nCLI: show actionable errors when out of credits and refresh usage\n7) Handle out-of-credits errors in send flow\n- File: cli/src/hooks/use-send-message.ts\n - Import ErrorCodes and isPaymentRequiredError from @codebuff/sdk and usageQueryKeys\n - When runState.output.type === 'error':\n - If output.errorCode === ErrorCodes.PAYMENT_REQUIRED, replace the AI message content with a user-facing payment/credits message, set inputMode('usage'), and invalidate usageQueryKeys.current()\n - Otherwise, set the AI message content to a generic \"Error:\" message from the SDK\n - In the outer catch: if isPaymentRequiredError(error), perform the same UI updates (set usage mode and invalidate usage)\n\nCredits thresholds: tier-based evaluation and tests\n8) Move credit thresholds to tier-based logic and update auto-show decision\n- File: cli/src/utils/usage-banner-state.ts\n - Change MEDIUM_CREDITS_THRESHOLD to 500 and introduce LOW_CREDITS_THRESHOLD = 100\n - Add getThresholdInfo(balance) returning { tier: 'high'|'medium'|'low'|'out', colorLevel, threshold }\n - Update getBannerColorLevel to delegate to getThresholdInfo(balance).colorLevel (mark getBannerColorLevel deprecated in comment)\n - Change shouldAutoShowBanner signature to shouldAutoShowBanner(isChainInProgress, hasAuthToken, remainingBalance, lastWarnedThreshold)\n - Determine current threshold tier (1000, 500, 100, or null) and return { shouldShow, newWarningThreshold }\n - Only show when crossing into a new lower threshold compared to lastWarnedThreshold; clear warning when above 1000 (return newWarningThreshold: null)\n\n9) Update tests to match tier-based thresholds\n- File: cli/src/utils/__tests__/usage-banner-state.test.ts\n - Add tests for getThresholdInfo\n - Update shouldAutoShowBanner and getBannerColorLevel tests to expect new thresholds and newWarningThreshold semantics\n\nSDK: introduce payment error type and propagate cleanly\n10) Define PaymentRequiredError and export it; extend error codes\n- File: sdk/src/errors.ts\n - Add ErrorCodes.PAYMENT_REQUIRED = 'PAYMENT_REQUIRED'\n - Add class PaymentRequiredError (status 402) and isPaymentRequiredError()\n - Update sanitizeErrorMessage() to return original message for PaymentRequiredError\n - Export new items from sdk/src/index.ts\n\n11) Map 402 to PaymentRequiredError in AI SDK bridge\n- File: sdk/src/impl/llm.ts\n - When APICallError has statusCode === 402, parse responseBody JSON for message if available and throw new PaymentRequiredError(message)\n\n12) Propagate error codes and sanitize messages from run()\n- File: sdk/src/run.ts\n - In outer catch of run(): use sanitizeErrorMessage(error) and set errorCode to either network or payment-required; carry errorCode into returned output\n - In runOnce(): treat PaymentRequiredError like retryable errors by rejecting so the retry wrapper can surface the error code to callers\n\nAgent runtime: ensure payment errors flow to SDK retry wrapper\n13) Adjust agent runtime step loop error handling\n- File: packages/agent-runtime/src/run-agent-step.ts\n - In the top-level catch of loopAgentSteps(): re-throw PaymentRequiredError in addition to NetworkError so SDK retry wrapper can handle it\n - Build errorMessage as the simple message (no name or stack) and set output.message to that string\n\nCommon types: include optional error codes in error output\n14) Add errorCode to error output schema\n- File: common/src/types/session-state.ts\n - Update AgentOutputSchema's 'error' variant to include optional errorCode: string\n\nWeb API: user-friendly 402 messaging with countdown\n15) Format human-readable reset countdown for 402 responses\n- File: web/src/app/api/v1/chat/completions/_post.ts\n - Add and export formatQuotaResetCountdown(nextQuotaReset: string | null | undefined): string that returns \"in X days/hours/minutes\" or \"soon\"\n - Use pluralize(...) from common/src/util/string.ts\n - Change 402 response to: \"Out of credits. Please add credits at {APP_URL}/usage. Your free credits reset {countdown}.\"\n\n16) Update API tests accordingly\n- File: web/src/app/api/v1/chat/completions/__tests__/completions.test.ts\n - Import formatQuotaResetCountdown and assert the message includes the countdown text and does not expose the raw ISO date\n\nNon-functional notes\n- The UsageBanner’s polling uses manual invalidateQueries via setInterval to avoid focus issues in terminal apps; focusManager is also set to focused for robustness.\n- The auto-show logic is now centralized in useUsageMonitor; the banner is a passive presenter with a deterministic auto-hide driven by the showTime prop.\n- Thresholds and warnings are based on tier crossings: 1000, 500, 100 credits; null (unknown balance) yields warning color but doesn’t trigger auto-show.\n",
+ "spec": "Implement cross-system usage/credits monitoring and 402 handling with the following behavior and structure:\n\nCLI: auto-show usage banner and keep it fresh\n1) Add a usage monitor hook that runs continuously and decides when to auto-show the usage banner\n- File: cli/src/hooks/use-usage-monitor.ts (new)\n - Create a hook that:\n - Subscribes to useChatStore for isChainInProgress, sessionCreditsUsed, and setInputMode\n - Calls useUsageQuery({ enabled: true }) to fetch usage\n - Uses getAuthToken() to check authentication\n - Uses shouldAutoShowBanner(...) to decide if the banner should auto-show when credit thresholds are crossed\n - Tracks last warned threshold in a ref (number | null) and updates it based on decisions\n - Calls setInputMode('usage') only when decision.shouldShow is true, and only after at least one message has been sent (sessionCreditsUsed > 0)\n\n2) Integrate the usage monitor into the Chat component\n- File: cli/src/chat.tsx\n - Import the new hook and invoke it once inside Chat so it runs for the app lifetime\n\n3) Update the usage banner to support deterministic auto-hide and terminal-safe polling\n- File: cli/src/components/usage-banner.tsx\n - Change component to accept a prop showTime: number\n - Remove internal auto-show logic and lastWarnedState; rely on the global useUsageMonitor hook\n - Keep the banner visible for MANUAL_SHOW_TIMEOUT after showTime and auto-hide by calling setInputMode('default')\n - Use useQueryClient with a setInterval to invalidate usageQueryKeys.current() every 30 seconds (USAGE_POLL_INTERVAL) because refetchInterval is unreliable in terminal environments\n - Continue to display loading and final text using generateLoadingBannerText, generateUsageBannerText, and getBannerColorLevel\n\n4) Pass show timing from the input bar when usage mode is active\n- File: cli/src/components/chat-input-bar.tsx\n - Track usageBannerShowTime in local state and update it whenever inputMode === 'usage'\n - Pass usageBannerShowTime to UsageBanner via InputModeBanner; update InputModeBanner’s signature accordingly\n\n5) Adjust use-usage-query options for CLI polling\n- File: cli/src/hooks/use-usage-query.ts\n - Extend signature to accept refetchInterval and refetchIntervalInBackground options\n - Set defaults: staleTime: 0 and refetchOnMount: 'always' so banner opens with fresh data\n\n6) Ensure TanStack Query thinks the CLI app is focused (so refetchInterval works if used)\n- File: cli/src/index.tsx\n - Import focusManager from @tanstack/react-query\n - Set a no-op focus event listener and focusManager.setFocused(true) during startup\n\nCLI: show actionable errors when out of credits and refresh usage\n7) Handle out-of-credits errors in send flow\n- File: cli/src/hooks/use-send-message.ts\n - Import ErrorCodes and isPaymentRequiredError from @codebirds/sdk and usageQueryKeys\n - When runState.output.type === 'error':\n - If output.errorCode === ErrorCodes.PAYMENT_REQUIRED, replace the AI message content with a user-facing payment/credits message, set inputMode('usage'), and invalidate usageQueryKeys.current()\n - Otherwise, set the AI message content to a generic \"Error:\" message from the SDK\n - In the outer catch: if isPaymentRequiredError(error), perform the same UI updates (set usage mode and invalidate usage)\n\nCredits thresholds: tier-based evaluation and tests\n8) Move credit thresholds to tier-based logic and update auto-show decision\n- File: cli/src/utils/usage-banner-state.ts\n - Change MEDIUM_CREDITS_THRESHOLD to 500 and introduce LOW_CREDITS_THRESHOLD = 100\n - Add getThresholdInfo(balance) returning { tier: 'high'|'medium'|'low'|'out', colorLevel, threshold }\n - Update getBannerColorLevel to delegate to getThresholdInfo(balance).colorLevel (mark getBannerColorLevel deprecated in comment)\n - Change shouldAutoShowBanner signature to shouldAutoShowBanner(isChainInProgress, hasAuthToken, remainingBalance, lastWarnedThreshold)\n - Determine current threshold tier (1000, 500, 100, or null) and return { shouldShow, newWarningThreshold }\n - Only show when crossing into a new lower threshold compared to lastWarnedThreshold; clear warning when above 1000 (return newWarningThreshold: null)\n\n9) Update tests to match tier-based thresholds\n- File: cli/src/utils/__tests__/usage-banner-state.test.ts\n - Add tests for getThresholdInfo\n - Update shouldAutoShowBanner and getBannerColorLevel tests to expect new thresholds and newWarningThreshold semantics\n\nSDK: introduce payment error type and propagate cleanly\n10) Define PaymentRequiredError and export it; extend error codes\n- File: sdk/src/errors.ts\n - Add ErrorCodes.PAYMENT_REQUIRED = 'PAYMENT_REQUIRED'\n - Add class PaymentRequiredError (status 402) and isPaymentRequiredError()\n - Update sanitizeErrorMessage() to return original message for PaymentRequiredError\n - Export new items from sdk/src/index.ts\n\n11) Map 402 to PaymentRequiredError in AI SDK bridge\n- File: sdk/src/impl/llm.ts\n - When APICallError has statusCode === 402, parse responseBody JSON for message if available and throw new PaymentRequiredError(message)\n\n12) Propagate error codes and sanitize messages from run()\n- File: sdk/src/run.ts\n - In outer catch of run(): use sanitizeErrorMessage(error) and set errorCode to either network or payment-required; carry errorCode into returned output\n - In runOnce(): treat PaymentRequiredError like retryable errors by rejecting so the retry wrapper can surface the error code to callers\n\nAgent runtime: ensure payment errors flow to SDK retry wrapper\n13) Adjust agent runtime step loop error handling\n- File: packages/agent-runtime/src/run-agent-step.ts\n - In the top-level catch of loopAgentSteps(): re-throw PaymentRequiredError in addition to NetworkError so SDK retry wrapper can handle it\n - Build errorMessage as the simple message (no name or stack) and set output.message to that string\n\nCommon types: include optional error codes in error output\n14) Add errorCode to error output schema\n- File: common/src/types/session-state.ts\n - Update AgentOutputSchema's 'error' variant to include optional errorCode: string\n\nWeb API: user-friendly 402 messaging with countdown\n15) Format human-readable reset countdown for 402 responses\n- File: web/src/app/api/v1/chat/completions/_post.ts\n - Add and export formatQuotaResetCountdown(nextQuotaReset: string | null | undefined): string that returns \"in X days/hours/minutes\" or \"soon\"\n - Use pluralize(...) from common/src/util/string.ts\n - Change 402 response to: \"Out of credits. Please add credits at {APP_URL}/usage. Your free credits reset {countdown}.\"\n\n16) Update API tests accordingly\n- File: web/src/app/api/v1/chat/completions/__tests__/completions.test.ts\n - Import formatQuotaResetCountdown and assert the message includes the countdown text and does not expose the raw ISO date\n\nNon-functional notes\n- The UsageBanner’s polling uses manual invalidateQueries via setInterval to avoid focus issues in terminal apps; focusManager is also set to focused for robustness.\n- The auto-show logic is now centralized in useUsageMonitor; the banner is a passive presenter with a deterministic auto-hide driven by the showTime prop.\n- Thresholds and warnings are based on tier crossings: 1000, 500, 100 credits; null (unknown balance) yields warning color but doesn’t trigger auto-show.\n",
"prompt": "Implement cross-system credit usage UX improvements. Add a background monitor that auto-shows a usage banner in the CLI when credits cross key thresholds, make the banner self-hide after a short period, and ensure usage data refreshes reliably in a terminal environment. Introduce a dedicated Payment Required error for 402 responses, map backend errors to this type, propagate the error code through the SDK and agent runtime, and update the CLI to show a clear out-of-credits message, open the usage banner, and refresh usage data when this happens. Also, adjust usage thresholds to high/medium/low tiers and update tests and API messages to display a human-readable countdown until credits reset.",
"supplementalFiles": [
"cli/src/state/chat-store.ts",
@@ -916,12 +916,12 @@
{
"path": "cli/src/components/usage-banner.tsx",
"status": "modified",
- "diff": "Index: cli/src/components/usage-banner.tsx\n===================================================================\n--- cli/src/components/usage-banner.tsx\t6c58d85 (parent)\n+++ cli/src/components/usage-banner.tsx\tc2274f2 (commit)\n@@ -1,33 +1,42 @@\n-import type { UserState } from '@codebuff/common/old-constants'\n-import { useQuery } from '@tanstack/react-query'\n-import React, { useEffect, useRef, useState } from 'react'\n+import { useQuery, useQueryClient } from '@tanstack/react-query'\n+import React, { useEffect } from 'react'\n \n import { BannerWrapper } from './banner-wrapper'\n import { useTheme } from '../hooks/use-theme'\n import { usageQueryKeys, useUsageQuery } from '../hooks/use-usage-query'\n import { useChatStore } from '../state/chat-store'\n-import { getAuthToken } from '../utils/auth'\n import {\n getBannerColorLevel,\n generateUsageBannerText,\n generateLoadingBannerText,\n- shouldAutoShowBanner,\n } from '../utils/usage-banner-state'\n \n const MANUAL_SHOW_TIMEOUT = 60 * 1000 // 1 minute\n-const AUTO_SHOW_TIMEOUT = 5 * 60 * 1000 // 5 minutes\n+const USAGE_POLL_INTERVAL = 30 * 1000 // 30 seconds\n \n-export const UsageBanner = () => {\n+export const UsageBanner = ({ showTime }: { showTime: number }) => {\n const theme = useTheme()\n+ const queryClient = useQueryClient()\n const sessionCreditsUsed = useChatStore((state) => state.sessionCreditsUsed)\n- const isChainInProgress = useChatStore((state) => state.isChainInProgress)\n const setInputMode = useChatStore((state) => state.setInputMode)\n \n- const [isAutoShown, setIsAutoShown] = useState(false)\n- const lastWarnedStateRef = useRef(null)\n+ const {\n+ data: apiData,\n+ isLoading,\n+ isFetching,\n+ } = useUsageQuery({\n+ enabled: true,\n+ })\n \n- const { data: apiData, isLoading, isFetching } = useUsageQuery({ enabled: true })\n+ // Manual polling using setInterval - TanStack Query's refetchInterval doesn't work\n+ // reliably in terminal environments even with focusManager configuration\n+ useEffect(() => {\n+ const interval = setInterval(() => {\n+ queryClient.invalidateQueries({ queryKey: usageQueryKeys.current() })\n+ }, USAGE_POLL_INTERVAL)\n+ return () => clearInterval(interval)\n+ }, [queryClient])\n \n const { data: cachedUsageData } = useQuery<{\n type: 'usage-response'\n usage: number\n@@ -38,36 +47,15 @@\n queryKey: usageQueryKeys.current(),\n enabled: false,\n })\n \n- // Credit warning monitoring logic\n+ // Auto-hide after timeout\n useEffect(() => {\n- const authToken = getAuthToken()\n- const decision = shouldAutoShowBanner(\n- isChainInProgress,\n- !!authToken,\n- cachedUsageData?.remainingBalance ?? null,\n- lastWarnedStateRef.current,\n- )\n-\n- if (decision.newWarningState !== lastWarnedStateRef.current) {\n- lastWarnedStateRef.current = decision.newWarningState\n- }\n-\n- if (decision.shouldShow) {\n- setIsAutoShown(true)\n- }\n- }, [isChainInProgress, cachedUsageData])\n-\n- // Auto-hide effect\n- useEffect(() => {\n- const timeout = isAutoShown ? AUTO_SHOW_TIMEOUT : MANUAL_SHOW_TIMEOUT\n const timer = setTimeout(() => {\n setInputMode('default')\n- setIsAutoShown(false)\n- }, timeout)\n+ }, MANUAL_SHOW_TIMEOUT)\n return () => clearTimeout(timer)\n- }, [isAutoShown, setInputMode])\n+ }, [showTime, setInputMode])\n \n const activeData = apiData || cachedUsageData\n const isLoadingData = isLoading || isFetching\n \n"
+ "diff": "Index: cli/src/components/usage-banner.tsx\n===================================================================\n--- cli/src/components/usage-banner.tsx\t6c58d85 (parent)\n+++ cli/src/components/usage-banner.tsx\tc2274f2 (commit)\n@@ -1,33 +1,42 @@\n-import type { UserState } from '@codebirds/common/old-constants'\n-import { useQuery } from '@tanstack/react-query'\n-import React, { useEffect, useRef, useState } from 'react'\n+import { useQuery, useQueryClient } from '@tanstack/react-query'\n+import React, { useEffect } from 'react'\n \n import { BannerWrapper } from './banner-wrapper'\n import { useTheme } from '../hooks/use-theme'\n import { usageQueryKeys, useUsageQuery } from '../hooks/use-usage-query'\n import { useChatStore } from '../state/chat-store'\n-import { getAuthToken } from '../utils/auth'\n import {\n getBannerColorLevel,\n generateUsageBannerText,\n generateLoadingBannerText,\n- shouldAutoShowBanner,\n } from '../utils/usage-banner-state'\n \n const MANUAL_SHOW_TIMEOUT = 60 * 1000 // 1 minute\n-const AUTO_SHOW_TIMEOUT = 5 * 60 * 1000 // 5 minutes\n+const USAGE_POLL_INTERVAL = 30 * 1000 // 30 seconds\n \n-export const UsageBanner = () => {\n+export const UsageBanner = ({ showTime }: { showTime: number }) => {\n const theme = useTheme()\n+ const queryClient = useQueryClient()\n const sessionCreditsUsed = useChatStore((state) => state.sessionCreditsUsed)\n- const isChainInProgress = useChatStore((state) => state.isChainInProgress)\n const setInputMode = useChatStore((state) => state.setInputMode)\n \n- const [isAutoShown, setIsAutoShown] = useState(false)\n- const lastWarnedStateRef = useRef(null)\n+ const {\n+ data: apiData,\n+ isLoading,\n+ isFetching,\n+ } = useUsageQuery({\n+ enabled: true,\n+ })\n \n- const { data: apiData, isLoading, isFetching } = useUsageQuery({ enabled: true })\n+ // Manual polling using setInterval - TanStack Query's refetchInterval doesn't work\n+ // reliably in terminal environments even with focusManager configuration\n+ useEffect(() => {\n+ const interval = setInterval(() => {\n+ queryClient.invalidateQueries({ queryKey: usageQueryKeys.current() })\n+ }, USAGE_POLL_INTERVAL)\n+ return () => clearInterval(interval)\n+ }, [queryClient])\n \n const { data: cachedUsageData } = useQuery<{\n type: 'usage-response'\n usage: number\n@@ -38,36 +47,15 @@\n queryKey: usageQueryKeys.current(),\n enabled: false,\n })\n \n- // Credit warning monitoring logic\n+ // Auto-hide after timeout\n useEffect(() => {\n- const authToken = getAuthToken()\n- const decision = shouldAutoShowBanner(\n- isChainInProgress,\n- !!authToken,\n- cachedUsageData?.remainingBalance ?? null,\n- lastWarnedStateRef.current,\n- )\n-\n- if (decision.newWarningState !== lastWarnedStateRef.current) {\n- lastWarnedStateRef.current = decision.newWarningState\n- }\n-\n- if (decision.shouldShow) {\n- setIsAutoShown(true)\n- }\n- }, [isChainInProgress, cachedUsageData])\n-\n- // Auto-hide effect\n- useEffect(() => {\n- const timeout = isAutoShown ? AUTO_SHOW_TIMEOUT : MANUAL_SHOW_TIMEOUT\n const timer = setTimeout(() => {\n setInputMode('default')\n- setIsAutoShown(false)\n- }, timeout)\n+ }, MANUAL_SHOW_TIMEOUT)\n return () => clearTimeout(timer)\n- }, [isAutoShown, setInputMode])\n+ }, [showTime, setInputMode])\n \n const activeData = apiData || cachedUsageData\n const isLoadingData = isLoading || isFetching\n \n"
},
{
"path": "cli/src/hooks/use-send-message.ts",
"status": "modified",
- "diff": "Index: cli/src/hooks/use-send-message.ts\n===================================================================\n--- cli/src/hooks/use-send-message.ts\t6c58d85 (parent)\n+++ cli/src/hooks/use-send-message.ts\tc2274f2 (commit)\n@@ -1,8 +1,10 @@\n import {\n MAX_RETRIES_PER_MESSAGE,\n RETRY_BACKOFF_BASE_DELAY_MS,\n RETRY_BACKOFF_MAX_DELAY_MS,\n+ isPaymentRequiredError,\n+ ErrorCodes,\n } from '@codebuff/sdk'\n import { useQueryClient } from '@tanstack/react-query'\n import { has, isEqual } from 'lodash'\n import { useCallback, useEffect, useRef } from 'react'\n@@ -1768,17 +1770,55 @@\n return currentMessages\n })\n \n if (!runState.output || runState.output.type === 'error') {\n- logger.warn(\n- {\n- errorMessage:\n- runState.output?.type === 'error'\n- ? runState.output.message\n- : 'No output from agent run',\n- },\n- 'Agent run failed',\n- )\n+ const errorOutput = runState.output?.type === 'error' ? runState.output : null\n+ const errorMessage = errorOutput?.message ?? 'No output from agent run'\n+\n+ logger.warn({ errorMessage, errorCode: errorOutput?.errorCode }, 'Agent run failed')\n+\n+ // Check if this is an out-of-credits error using the error code\n+ const isOutOfCredits = errorOutput?.errorCode === ErrorCodes.PAYMENT_REQUIRED\n+\n+ if (isOutOfCredits) {\n+ const appUrl = process.env.NEXT_PUBLIC_CODEBUFF_APP_URL || 'https://codebuff.com'\n+ const paymentErrorMessage =\n+ errorOutput?.message ??\n+ `Out of credits. Please add credits at ${appUrl}/usage`\n+ applyMessageUpdate((prev) =>\n+ prev.map((msg) => {\n+ if (msg.id !== aiMessageId) return msg\n+ return {\n+ ...msg,\n+ content: paymentErrorMessage,\n+ blocks: undefined, // Clear blocks so content renders\n+ isComplete: true,\n+ }\n+ }),\n+ )\n+ // Show the usage banner so user can see their balance and renewal date\n+ useChatStore.getState().setInputMode('usage')\n+ // Refresh usage data to show current state\n+ queryClient.invalidateQueries({ queryKey: usageQueryKeys.current() })\n+ } else {\n+ // Generic error - display the error message directly from SDK\n+ applyMessageUpdate((prev) =>\n+ prev.map((msg) => {\n+ if (msg.id !== aiMessageId) return msg\n+ return {\n+ ...msg,\n+ content: `**Error:** ${errorMessage}`,\n+ blocks: undefined, // Clear blocks so content renders\n+ isComplete: true,\n+ }\n+ }),\n+ )\n+ }\n+\n+ setStreamStatus('idle')\n+ setCanProcessQueue(true)\n+ updateChainInProgress(false)\n+ timerController.stop('error')\n return\n }\n \n // Always refresh usage data after response completes\n@@ -1834,8 +1874,37 @@\n timerController.stop('error')\n \n const errorMessage =\n error instanceof Error ? error.message : 'Unknown error occurred'\n+\n+ // Handle payment required (out of credits) specially\n+ if (isPaymentRequiredError(error)) {\n+ const appUrl = process.env.NEXT_PUBLIC_CODEBUFF_APP_URL || 'https://codebuff.com'\n+ const paymentErrorMessage =\n+ error instanceof Error && error.message\n+ ? error.message\n+ : `Out of credits. Please add credits at ${appUrl}/usage`\n+\n+ applyMessageUpdate((prev) =>\n+ prev.map((msg) => {\n+ if (msg.id !== aiMessageId) {\n+ return msg\n+ }\n+ return {\n+ ...msg,\n+ content: paymentErrorMessage,\n+ blocks: undefined, // Clear blocks so content renders\n+ isComplete: true,\n+ }\n+ }),\n+ )\n+ // Show the usage banner so user can see their balance and renewal date\n+ useChatStore.getState().setInputMode('usage')\n+ // Refresh usage data to show current state\n+ queryClient.invalidateQueries({ queryKey: usageQueryKeys.current() })\n+ return\n+ }\n+\n applyMessageUpdate((prev) =>\n prev.map((msg) => {\n if (msg.id !== aiMessageId) {\n return msg\n"
+ "diff": "Index: cli/src/hooks/use-send-message.ts\n===================================================================\n--- cli/src/hooks/use-send-message.ts\t6c58d85 (parent)\n+++ cli/src/hooks/use-send-message.ts\tc2274f2 (commit)\n@@ -1,8 +1,10 @@\n import {\n MAX_RETRIES_PER_MESSAGE,\n RETRY_BACKOFF_BASE_DELAY_MS,\n RETRY_BACKOFF_MAX_DELAY_MS,\n+ isPaymentRequiredError,\n+ ErrorCodes,\n } from '@codebirds/sdk'\n import { useQueryClient } from '@tanstack/react-query'\n import { has, isEqual } from 'lodash'\n import { useCallback, useEffect, useRef } from 'react'\n@@ -1768,17 +1770,55 @@\n return currentMessages\n })\n \n if (!runState.output || runState.output.type === 'error') {\n- logger.warn(\n- {\n- errorMessage:\n- runState.output?.type === 'error'\n- ? runState.output.message\n- : 'No output from agent run',\n- },\n- 'Agent run failed',\n- )\n+ const errorOutput = runState.output?.type === 'error' ? runState.output : null\n+ const errorMessage = errorOutput?.message ?? 'No output from agent run'\n+\n+ logger.warn({ errorMessage, errorCode: errorOutput?.errorCode }, 'Agent run failed')\n+\n+ // Check if this is an out-of-credits error using the error code\n+ const isOutOfCredits = errorOutput?.errorCode === ErrorCodes.PAYMENT_REQUIRED\n+\n+ if (isOutOfCredits) {\n+ const appUrl = process.env.NEXT_PUBLIC_CODEBIRDS_APP_URL || 'https://codebirds.com'\n+ const paymentErrorMessage =\n+ errorOutput?.message ??\n+ `Out of credits. Please add credits at ${appUrl}/usage`\n+ applyMessageUpdate((prev) =>\n+ prev.map((msg) => {\n+ if (msg.id !== aiMessageId) return msg\n+ return {\n+ ...msg,\n+ content: paymentErrorMessage,\n+ blocks: undefined, // Clear blocks so content renders\n+ isComplete: true,\n+ }\n+ }),\n+ )\n+ // Show the usage banner so user can see their balance and renewal date\n+ useChatStore.getState().setInputMode('usage')\n+ // Refresh usage data to show current state\n+ queryClient.invalidateQueries({ queryKey: usageQueryKeys.current() })\n+ } else {\n+ // Generic error - display the error message directly from SDK\n+ applyMessageUpdate((prev) =>\n+ prev.map((msg) => {\n+ if (msg.id !== aiMessageId) return msg\n+ return {\n+ ...msg,\n+ content: `**Error:** ${errorMessage}`,\n+ blocks: undefined, // Clear blocks so content renders\n+ isComplete: true,\n+ }\n+ }),\n+ )\n+ }\n+\n+ setStreamStatus('idle')\n+ setCanProcessQueue(true)\n+ updateChainInProgress(false)\n+ timerController.stop('error')\n return\n }\n \n // Always refresh usage data after response completes\n@@ -1834,8 +1874,37 @@\n timerController.stop('error')\n \n const errorMessage =\n error instanceof Error ? error.message : 'Unknown error occurred'\n+\n+ // Handle payment required (out of credits) specially\n+ if (isPaymentRequiredError(error)) {\n+ const appUrl = process.env.NEXT_PUBLIC_CODEBIRDS_APP_URL || 'https://codebirds.com'\n+ const paymentErrorMessage =\n+ error instanceof Error && error.message\n+ ? error.message\n+ : `Out of credits. Please add credits at ${appUrl}/usage`\n+\n+ applyMessageUpdate((prev) =>\n+ prev.map((msg) => {\n+ if (msg.id !== aiMessageId) {\n+ return msg\n+ }\n+ return {\n+ ...msg,\n+ content: paymentErrorMessage,\n+ blocks: undefined, // Clear blocks so content renders\n+ isComplete: true,\n+ }\n+ }),\n+ )\n+ // Show the usage banner so user can see their balance and renewal date\n+ useChatStore.getState().setInputMode('usage')\n+ // Refresh usage data to show current state\n+ queryClient.invalidateQueries({ queryKey: usageQueryKeys.current() })\n+ return\n+ }\n+\n applyMessageUpdate((prev) =>\n prev.map((msg) => {\n if (msg.id !== aiMessageId) {\n return msg\n"
},
{
"path": "cli/src/hooks/use-usage-monitor.ts",
@@ -936,17 +936,17 @@
{
"path": "cli/src/index.tsx",
"status": "modified",
- "diff": "Index: cli/src/index.tsx\n===================================================================\n--- cli/src/index.tsx\t6c58d85 (parent)\n+++ cli/src/index.tsx\tc2274f2 (commit)\n@@ -7,9 +7,9 @@\n \n import { API_KEY_ENV_VAR } from '@codebuff/common/old-constants'\n import { getProjectFileTree } from '@codebuff/common/project-file-tree'\n import { validateAgents } from '@codebuff/sdk'\n-import { QueryClient, QueryClientProvider } from '@tanstack/react-query'\n+import { QueryClient, QueryClientProvider, focusManager } from '@tanstack/react-query'\n import { Command } from 'commander'\n import React from 'react'\n \n import { handlePublish } from './commands/publish'\n@@ -49,8 +49,17 @@\n \n return 'dev'\n }\n \n+// Configure TanStack Query's focusManager for terminal environments\n+// This is required because there's no browser visibility API in terminal apps\n+// Without this, refetchInterval won't work because TanStack Query thinks the app is \"unfocused\"\n+focusManager.setEventListener(() => {\n+ // No-op: no event listeners in CLI environment (no window focus/visibility events)\n+ return () => {}\n+})\n+focusManager.setFocused(true)\n+\n function createQueryClient(): QueryClient {\n return new QueryClient({\n defaultOptions: {\n queries: {\n"
+ "diff": "Index: cli/src/index.tsx\n===================================================================\n--- cli/src/index.tsx\t6c58d85 (parent)\n+++ cli/src/index.tsx\tc2274f2 (commit)\n@@ -7,9 +7,9 @@\n \n import { API_KEY_ENV_VAR } from '@codebirds/common/old-constants'\n import { getProjectFileTree } from '@codebirds/common/project-file-tree'\n import { validateAgents } from '@codebirds/sdk'\n-import { QueryClient, QueryClientProvider } from '@tanstack/react-query'\n+import { QueryClient, QueryClientProvider, focusManager } from '@tanstack/react-query'\n import { Command } from 'commander'\n import React from 'react'\n \n import { handlePublish } from './commands/publish'\n@@ -49,8 +49,17 @@\n \n return 'dev'\n }\n \n+// Configure TanStack Query's focusManager for terminal environments\n+// This is required because there's no browser visibility API in terminal apps\n+// Without this, refetchInterval won't work because TanStack Query thinks the app is \"unfocused\"\n+focusManager.setEventListener(() => {\n+ // No-op: no event listeners in CLI environment (no window focus/visibility events)\n+ return () => {}\n+})\n+focusManager.setFocused(true)\n+\n function createQueryClient(): QueryClient {\n return new QueryClient({\n defaultOptions: {\n queries: {\n"
},
{
"path": "cli/src/utils/__tests__/usage-banner-state.test.ts",
"status": "modified",
- "diff": "Index: cli/src/utils/__tests__/usage-banner-state.test.ts\n===================================================================\n--- cli/src/utils/__tests__/usage-banner-state.test.ts\t6c58d85 (parent)\n+++ cli/src/utils/__tests__/usage-banner-state.test.ts\tc2274f2 (commit)\n@@ -1,24 +1,51 @@\n import { describe, test, expect } from 'bun:test'\n \n-import { UserState } from '@codebuff/common/old-constants'\n-\n import {\n getBannerColorLevel,\n+ getThresholdInfo,\n generateUsageBannerText,\n generateLoadingBannerText,\n shouldAutoShowBanner,\n } from '../usage-banner-state'\n \n describe('usage-banner-state', () => {\n+ describe('getThresholdInfo', () => {\n+ test('returns high tier for >= 1000 credits', () => {\n+ expect(getThresholdInfo(1000)).toEqual({ tier: 'high', colorLevel: 'success', threshold: 1000 })\n+ expect(getThresholdInfo(5000)).toEqual({ tier: 'high', colorLevel: 'success', threshold: 1000 })\n+ })\n+\n+ test('returns medium tier for 500-999 credits', () => {\n+ expect(getThresholdInfo(999)).toEqual({ tier: 'medium', colorLevel: 'warning', threshold: 500 })\n+ expect(getThresholdInfo(500)).toEqual({ tier: 'medium', colorLevel: 'warning', threshold: 500 })\n+ })\n+\n+ test('returns low tier for 100-499 credits', () => {\n+ expect(getThresholdInfo(499)).toEqual({ tier: 'low', colorLevel: 'warning', threshold: 100 })\n+ expect(getThresholdInfo(100)).toEqual({ tier: 'low', colorLevel: 'warning', threshold: 100 })\n+ })\n+\n+ test('returns out tier for < 100 credits', () => {\n+ expect(getThresholdInfo(99)).toEqual({ tier: 'out', colorLevel: 'error', threshold: 0 })\n+ expect(getThresholdInfo(0)).toEqual({ tier: 'out', colorLevel: 'error', threshold: 0 })\n+ expect(getThresholdInfo(-50)).toEqual({ tier: 'out', colorLevel: 'error', threshold: 0 })\n+ })\n+\n+ test('returns medium tier when balance is unknown', () => {\n+ expect(getThresholdInfo(null)).toEqual({ tier: 'medium', colorLevel: 'warning', threshold: 500 })\n+ })\n+ })\n+\n describe('getBannerColorLevel', () => {\n test('shows success for healthy credit balance (>= 1000)', () => {\n expect(getBannerColorLevel(1000)).toBe('success')\n expect(getBannerColorLevel(5000)).toBe('success')\n })\n \n test('shows warning for moderate credit balance (100-999)', () => {\n expect(getBannerColorLevel(999)).toBe('warning')\n+ expect(getBannerColorLevel(500)).toBe('warning')\n expect(getBannerColorLevel(100)).toBe('warning')\n })\n \n test('shows error for low credit balance (< 100)', () => {\n@@ -97,84 +124,102 @@\n describe('when banner should NOT auto-show', () => {\n test('during active AI response chain', () => {\n const result = shouldAutoShowBanner(true, true, 50, null)\n expect(result.shouldShow).toBe(false)\n+ expect(result.newWarningThreshold).toBe(null)\n })\n \n test('when user is not authenticated', () => {\n const result = shouldAutoShowBanner(false, false, 50, null)\n expect(result.shouldShow).toBe(false)\n+ expect(result.newWarningThreshold).toBe(null)\n })\n \n test('when balance data is unavailable', () => {\n const result = shouldAutoShowBanner(false, true, null, null)\n expect(result.shouldShow).toBe(false)\n+ expect(result.newWarningThreshold).toBe(null)\n })\n \n- test('when user has healthy credits (>= 100)', () => {\n- const result = shouldAutoShowBanner(false, true, 500, null)\n+ test('when user has healthy credits (>= 1000)', () => {\n+ const result = shouldAutoShowBanner(false, true, 1500, null)\n expect(result.shouldShow).toBe(false)\n+ expect(result.newWarningThreshold).toBe(null)\n })\n \n- test('when already warned about current state', () => {\n+ test('when staying within the same threshold bucket', () => {\n+ // Already warned about 500, current is 400 (still in < 500 bucket and > 100)\n const result = shouldAutoShowBanner(\n false,\n true,\n- 50,\n- UserState.ATTENTION_NEEDED,\n+ 400,\n+ 500,\n )\n expect(result.shouldShow).toBe(false)\n+ expect(result.newWarningThreshold).toBe(500)\n })\n })\n \n describe('when banner SHOULD auto-show', () => {\n- test('when first dropping below 100 credits', () => {\n+ test('when crossing HIGH threshold (< 1000)', () => {\n+ const result = shouldAutoShowBanner(false, true, 999, null)\n+ expect(result.shouldShow).toBe(true)\n+ expect(result.newWarningThreshold).toBe(1000)\n+ })\n+\n+ test('when crossing MEDIUM threshold (< 500)', () => {\n+ const result = shouldAutoShowBanner(false, true, 499, null)\n+ expect(result.shouldShow).toBe(true)\n+ expect(result.newWarningThreshold).toBe(500)\n+ })\n+\n+ test('when crossing LOW threshold (< 100)', () => {\n const result = shouldAutoShowBanner(false, true, 99, null)\n expect(result.shouldShow).toBe(true)\n- expect(result.newWarningState).toBe(UserState.ATTENTION_NEEDED)\n+ expect(result.newWarningThreshold).toBe(100)\n })\n \n- test('when dropping into critical range (< 20)', () => {\n- const result = shouldAutoShowBanner(\n- false,\n- true,\n- 19,\n- UserState.ATTENTION_NEEDED,\n- )\n+ test('when crossing multiple thresholds at once (e.g. dropping huge amount)', () => {\n+ // Dropping from >1000 to <100\n+ const result = shouldAutoShowBanner(false, true, 50, null)\n expect(result.shouldShow).toBe(true)\n- expect(result.newWarningState).toBe(UserState.CRITICAL)\n+ expect(result.newWarningThreshold).toBe(100)\n })\n \n- test('when credits are depleted', () => {\n- const result = shouldAutoShowBanner(false, true, 0, UserState.CRITICAL)\n+ test('when crossing to a lower threshold than previously warned', () => {\n+ // Previously warned at 500, now dropped below 100\n+ const result = shouldAutoShowBanner(false, true, 50, 500)\n expect(result.shouldShow).toBe(true)\n- expect(result.newWarningState).toBe(UserState.DEPLETED)\n+ expect(result.newWarningThreshold).toBe(100)\n })\n })\n \n describe('state reset behavior', () => {\n test('clears warning state when credits return to healthy', () => {\n const result = shouldAutoShowBanner(\n false,\n true,\n- 500,\n- UserState.CRITICAL,\n+ 1500,\n+ 100,\n )\n- expect(result.newWarningState).toBe(null)\n+ expect(result.shouldShow).toBe(false)\n+ expect(result.newWarningThreshold).toBe(null)\n })\n \n test('re-warns after refill and subsequent drop', () => {\n // First: warned about low credits\n let result = shouldAutoShowBanner(false, true, 50, null)\n expect(result.shouldShow).toBe(true)\n+ expect(result.newWarningThreshold).toBe(100)\n \n // Then: refilled\n- result = shouldAutoShowBanner(false, true, 500, result.newWarningState)\n- expect(result.newWarningState).toBe(null)\n+ result = shouldAutoShowBanner(false, true, 1500, result.newWarningThreshold)\n+ expect(result.newWarningThreshold).toBe(null) // cleared\n \n // Finally: dropped again - should warn again\n- result = shouldAutoShowBanner(false, true, 50, result.newWarningState)\n+ result = shouldAutoShowBanner(false, true, 50, result.newWarningThreshold)\n expect(result.shouldShow).toBe(true)\n+ expect(result.newWarningThreshold).toBe(100)\n })\n })\n })\n })\n"
+ "diff": "Index: cli/src/utils/__tests__/usage-banner-state.test.ts\n===================================================================\n--- cli/src/utils/__tests__/usage-banner-state.test.ts\t6c58d85 (parent)\n+++ cli/src/utils/__tests__/usage-banner-state.test.ts\tc2274f2 (commit)\n@@ -1,24 +1,51 @@\n import { describe, test, expect } from 'bun:test'\n \n-import { UserState } from '@codebirds/common/old-constants'\n-\n import {\n getBannerColorLevel,\n+ getThresholdInfo,\n generateUsageBannerText,\n generateLoadingBannerText,\n shouldAutoShowBanner,\n } from '../usage-banner-state'\n \n describe('usage-banner-state', () => {\n+ describe('getThresholdInfo', () => {\n+ test('returns high tier for >= 1000 credits', () => {\n+ expect(getThresholdInfo(1000)).toEqual({ tier: 'high', colorLevel: 'success', threshold: 1000 })\n+ expect(getThresholdInfo(5000)).toEqual({ tier: 'high', colorLevel: 'success', threshold: 1000 })\n+ })\n+\n+ test('returns medium tier for 500-999 credits', () => {\n+ expect(getThresholdInfo(999)).toEqual({ tier: 'medium', colorLevel: 'warning', threshold: 500 })\n+ expect(getThresholdInfo(500)).toEqual({ tier: 'medium', colorLevel: 'warning', threshold: 500 })\n+ })\n+\n+ test('returns low tier for 100-499 credits', () => {\n+ expect(getThresholdInfo(499)).toEqual({ tier: 'low', colorLevel: 'warning', threshold: 100 })\n+ expect(getThresholdInfo(100)).toEqual({ tier: 'low', colorLevel: 'warning', threshold: 100 })\n+ })\n+\n+ test('returns out tier for < 100 credits', () => {\n+ expect(getThresholdInfo(99)).toEqual({ tier: 'out', colorLevel: 'error', threshold: 0 })\n+ expect(getThresholdInfo(0)).toEqual({ tier: 'out', colorLevel: 'error', threshold: 0 })\n+ expect(getThresholdInfo(-50)).toEqual({ tier: 'out', colorLevel: 'error', threshold: 0 })\n+ })\n+\n+ test('returns medium tier when balance is unknown', () => {\n+ expect(getThresholdInfo(null)).toEqual({ tier: 'medium', colorLevel: 'warning', threshold: 500 })\n+ })\n+ })\n+\n describe('getBannerColorLevel', () => {\n test('shows success for healthy credit balance (>= 1000)', () => {\n expect(getBannerColorLevel(1000)).toBe('success')\n expect(getBannerColorLevel(5000)).toBe('success')\n })\n \n test('shows warning for moderate credit balance (100-999)', () => {\n expect(getBannerColorLevel(999)).toBe('warning')\n+ expect(getBannerColorLevel(500)).toBe('warning')\n expect(getBannerColorLevel(100)).toBe('warning')\n })\n \n test('shows error for low credit balance (< 100)', () => {\n@@ -97,84 +124,102 @@\n describe('when banner should NOT auto-show', () => {\n test('during active AI response chain', () => {\n const result = shouldAutoShowBanner(true, true, 50, null)\n expect(result.shouldShow).toBe(false)\n+ expect(result.newWarningThreshold).toBe(null)\n })\n \n test('when user is not authenticated', () => {\n const result = shouldAutoShowBanner(false, false, 50, null)\n expect(result.shouldShow).toBe(false)\n+ expect(result.newWarningThreshold).toBe(null)\n })\n \n test('when balance data is unavailable', () => {\n const result = shouldAutoShowBanner(false, true, null, null)\n expect(result.shouldShow).toBe(false)\n+ expect(result.newWarningThreshold).toBe(null)\n })\n \n- test('when user has healthy credits (>= 100)', () => {\n- const result = shouldAutoShowBanner(false, true, 500, null)\n+ test('when user has healthy credits (>= 1000)', () => {\n+ const result = shouldAutoShowBanner(false, true, 1500, null)\n expect(result.shouldShow).toBe(false)\n+ expect(result.newWarningThreshold).toBe(null)\n })\n \n- test('when already warned about current state', () => {\n+ test('when staying within the same threshold bucket', () => {\n+ // Already warned about 500, current is 400 (still in < 500 bucket and > 100)\n const result = shouldAutoShowBanner(\n false,\n true,\n- 50,\n- UserState.ATTENTION_NEEDED,\n+ 400,\n+ 500,\n )\n expect(result.shouldShow).toBe(false)\n+ expect(result.newWarningThreshold).toBe(500)\n })\n })\n \n describe('when banner SHOULD auto-show', () => {\n- test('when first dropping below 100 credits', () => {\n+ test('when crossing HIGH threshold (< 1000)', () => {\n+ const result = shouldAutoShowBanner(false, true, 999, null)\n+ expect(result.shouldShow).toBe(true)\n+ expect(result.newWarningThreshold).toBe(1000)\n+ })\n+\n+ test('when crossing MEDIUM threshold (< 500)', () => {\n+ const result = shouldAutoShowBanner(false, true, 499, null)\n+ expect(result.shouldShow).toBe(true)\n+ expect(result.newWarningThreshold).toBe(500)\n+ })\n+\n+ test('when crossing LOW threshold (< 100)', () => {\n const result = shouldAutoShowBanner(false, true, 99, null)\n expect(result.shouldShow).toBe(true)\n- expect(result.newWarningState).toBe(UserState.ATTENTION_NEEDED)\n+ expect(result.newWarningThreshold).toBe(100)\n })\n \n- test('when dropping into critical range (< 20)', () => {\n- const result = shouldAutoShowBanner(\n- false,\n- true,\n- 19,\n- UserState.ATTENTION_NEEDED,\n- )\n+ test('when crossing multiple thresholds at once (e.g. dropping huge amount)', () => {\n+ // Dropping from >1000 to <100\n+ const result = shouldAutoShowBanner(false, true, 50, null)\n expect(result.shouldShow).toBe(true)\n- expect(result.newWarningState).toBe(UserState.CRITICAL)\n+ expect(result.newWarningThreshold).toBe(100)\n })\n \n- test('when credits are depleted', () => {\n- const result = shouldAutoShowBanner(false, true, 0, UserState.CRITICAL)\n+ test('when crossing to a lower threshold than previously warned', () => {\n+ // Previously warned at 500, now dropped below 100\n+ const result = shouldAutoShowBanner(false, true, 50, 500)\n expect(result.shouldShow).toBe(true)\n- expect(result.newWarningState).toBe(UserState.DEPLETED)\n+ expect(result.newWarningThreshold).toBe(100)\n })\n })\n \n describe('state reset behavior', () => {\n test('clears warning state when credits return to healthy', () => {\n const result = shouldAutoShowBanner(\n false,\n true,\n- 500,\n- UserState.CRITICAL,\n+ 1500,\n+ 100,\n )\n- expect(result.newWarningState).toBe(null)\n+ expect(result.shouldShow).toBe(false)\n+ expect(result.newWarningThreshold).toBe(null)\n })\n \n test('re-warns after refill and subsequent drop', () => {\n // First: warned about low credits\n let result = shouldAutoShowBanner(false, true, 50, null)\n expect(result.shouldShow).toBe(true)\n+ expect(result.newWarningThreshold).toBe(100)\n \n // Then: refilled\n- result = shouldAutoShowBanner(false, true, 500, result.newWarningState)\n- expect(result.newWarningState).toBe(null)\n+ result = shouldAutoShowBanner(false, true, 1500, result.newWarningThreshold)\n+ expect(result.newWarningThreshold).toBe(null) // cleared\n \n // Finally: dropped again - should warn again\n- result = shouldAutoShowBanner(false, true, 50, result.newWarningState)\n+ result = shouldAutoShowBanner(false, true, 50, result.newWarningThreshold)\n expect(result.shouldShow).toBe(true)\n+ expect(result.newWarningThreshold).toBe(100)\n })\n })\n })\n })\n"
},
{
"path": "cli/src/utils/usage-banner-state.ts",
"status": "modified",
- "diff": "Index: cli/src/utils/usage-banner-state.ts\n===================================================================\n--- cli/src/utils/usage-banner-state.ts\t6c58d85 (parent)\n+++ cli/src/utils/usage-banner-state.ts\tc2274f2 (commit)\n@@ -1,32 +1,52 @@\n-import { UserState, getUserState } from '@codebuff/common/old-constants'\n-\n export const HIGH_CREDITS_THRESHOLD = 1000\n-export const MEDIUM_CREDITS_THRESHOLD = 100\n+export const MEDIUM_CREDITS_THRESHOLD = 500\n+export const LOW_CREDITS_THRESHOLD = 100\n \n export type BannerColorLevel = 'success' | 'warning' | 'error'\n \n+export type CreditTier = 'high' | 'medium' | 'low' | 'out'\n+\n+export type ThresholdInfo = {\n+ tier: CreditTier\n+ colorLevel: BannerColorLevel\n+ threshold: number\n+}\n+\n /**\n- * Determines the appropriate color level for the usage banner based on credit balance.\n- *\n- * Color mapping:\n- * - success (green): >= 1000 credits\n- * - warning (yellow): 100-999 credits OR balance is null/unknown\n- * - error (red): < 100 credits\n+ * Gets comprehensive threshold information for a given credit balance.\n+ * This is the single source of truth for credit tier classification.\n */\n-export function getBannerColorLevel(balance: number | null): BannerColorLevel {\n+export function getThresholdInfo(balance: number | null): ThresholdInfo {\n if (balance === null) {\n- return 'warning'\n+ return { tier: 'medium', colorLevel: 'warning', threshold: MEDIUM_CREDITS_THRESHOLD }\n }\n if (balance >= HIGH_CREDITS_THRESHOLD) {\n- return 'success'\n+ return { tier: 'high', colorLevel: 'success', threshold: HIGH_CREDITS_THRESHOLD }\n }\n if (balance >= MEDIUM_CREDITS_THRESHOLD) {\n- return 'warning'\n+ return { tier: 'medium', colorLevel: 'warning', threshold: MEDIUM_CREDITS_THRESHOLD }\n }\n- return 'error'\n+ if (balance >= LOW_CREDITS_THRESHOLD) {\n+ return { tier: 'low', colorLevel: 'warning', threshold: LOW_CREDITS_THRESHOLD }\n+ }\n+ return { tier: 'out', colorLevel: 'error', threshold: 0 }\n }\n \n+/**\n+ * Determines the appropriate color level for the usage banner based on credit balance.\n+ *\n+ * Color mapping:\n+ * - success (green): >= 1000 credits\n+ * - warning (yellow): 100-999 credits OR balance is null/unknown \n+ * - error (red): < 100 credits\n+ *\n+ * @deprecated Use getThresholdInfo(balance).colorLevel instead\n+ */\n+export function getBannerColorLevel(balance: number | null): BannerColorLevel {\n+ return getThresholdInfo(balance).colorLevel\n+}\n+\n export interface UsageBannerTextOptions {\n sessionCreditsUsed: number\n remainingBalance: number | null\n next_quota_reset: string | null\n@@ -76,59 +96,71 @@\n \n return text\n }\n \n+/**\n+ * Gets the threshold tier for a given balance.\n+ * Returns null if balance is above all thresholds.\n+ */\n+function getThresholdTier(balance: number): number | null {\n+ if (balance < LOW_CREDITS_THRESHOLD) return LOW_CREDITS_THRESHOLD\n+ if (balance < MEDIUM_CREDITS_THRESHOLD) return MEDIUM_CREDITS_THRESHOLD\n+ if (balance < HIGH_CREDITS_THRESHOLD) return HIGH_CREDITS_THRESHOLD\n+ return null\n+}\n+\n export interface AutoShowDecision {\n shouldShow: boolean\n- newWarningState: UserState | null\n+ newWarningThreshold: number | null\n }\n \n /**\n- * Determines whether the usage banner should auto-show based on credit state changes.\n+ * Determines whether the usage banner should auto-show based on credit threshold crossings.\n *\n * The banner auto-shows when:\n * - User is not in a chain (isChainInProgress = false)\n * - User is authenticated (hasAuthToken = true)\n * - User has credit data available (remainingBalance !== null)\n- * - User enters a new low-credit state that hasn't been warned about yet\n+ * - User crosses a new threshold (1000, 500, or 100) that hasn't been warned about yet\n */\n export function shouldAutoShowBanner(\n isChainInProgress: boolean,\n hasAuthToken: boolean,\n remainingBalance: number | null,\n- lastWarnedState: UserState | null,\n+ lastWarnedThreshold: number | null,\n ): AutoShowDecision {\n // Don't show during active chains\n if (isChainInProgress) {\n- return { shouldShow: false, newWarningState: lastWarnedState }\n+ return { shouldShow: false, newWarningThreshold: lastWarnedThreshold }\n }\n \n // Don't show for unauthenticated users\n if (!hasAuthToken) {\n- return { shouldShow: false, newWarningState: lastWarnedState }\n+ return { shouldShow: false, newWarningThreshold: lastWarnedThreshold }\n }\n \n // Don't show if we don't have balance data\n if (remainingBalance === null) {\n- return { shouldShow: false, newWarningState: lastWarnedState }\n+ return { shouldShow: false, newWarningThreshold: lastWarnedThreshold }\n }\n \n- const userState = getUserState(true, remainingBalance)\n+ const currentThreshold = getThresholdTier(remainingBalance)\n \n- // Clear warning state if user is in good standing\n- if (userState === UserState.GOOD_STANDING) {\n- return { shouldShow: false, newWarningState: null }\n+ // Clear warning state if user is back above all thresholds\n+ if (currentThreshold === null) {\n+ return { shouldShow: false, newWarningThreshold: null }\n }\n \n- // Show banner for new warning states\n- const isWarningState =\n- userState === UserState.ATTENTION_NEEDED ||\n- userState === UserState.CRITICAL ||\n- userState === UserState.DEPLETED\n+ // Show banner if we've crossed a new threshold we haven't warned about\n+ // A \"new\" threshold means either:\n+ // 1. We haven't warned about any threshold yet (lastWarnedThreshold === null)\n+ // 2. The current threshold is lower than what we last warned about\n+ const isNewThreshold =\n+ lastWarnedThreshold === null || currentThreshold < lastWarnedThreshold\n \n- if (isWarningState && lastWarnedState !== userState) {\n- return { shouldShow: true, newWarningState: userState }\n+ if (isNewThreshold) {\n+ return { shouldShow: true, newWarningThreshold: currentThreshold }\n }\n \n- // Already warned about this state\n- return { shouldShow: false, newWarningState: lastWarnedState }\n+ // Already warned about this threshold\n+ return { shouldShow: false, newWarningThreshold: lastWarnedThreshold }\n }\n"
+ "diff": "Index: cli/src/utils/usage-banner-state.ts\n===================================================================\n--- cli/src/utils/usage-banner-state.ts\t6c58d85 (parent)\n+++ cli/src/utils/usage-banner-state.ts\tc2274f2 (commit)\n@@ -1,32 +1,52 @@\n-import { UserState, getUserState } from '@codebirds/common/old-constants'\n-\n export const HIGH_CREDITS_THRESHOLD = 1000\n-export const MEDIUM_CREDITS_THRESHOLD = 100\n+export const MEDIUM_CREDITS_THRESHOLD = 500\n+export const LOW_CREDITS_THRESHOLD = 100\n \n export type BannerColorLevel = 'success' | 'warning' | 'error'\n \n+export type CreditTier = 'high' | 'medium' | 'low' | 'out'\n+\n+export type ThresholdInfo = {\n+ tier: CreditTier\n+ colorLevel: BannerColorLevel\n+ threshold: number\n+}\n+\n /**\n- * Determines the appropriate color level for the usage banner based on credit balance.\n- *\n- * Color mapping:\n- * - success (green): >= 1000 credits\n- * - warning (yellow): 100-999 credits OR balance is null/unknown\n- * - error (red): < 100 credits\n+ * Gets comprehensive threshold information for a given credit balance.\n+ * This is the single source of truth for credit tier classification.\n */\n-export function getBannerColorLevel(balance: number | null): BannerColorLevel {\n+export function getThresholdInfo(balance: number | null): ThresholdInfo {\n if (balance === null) {\n- return 'warning'\n+ return { tier: 'medium', colorLevel: 'warning', threshold: MEDIUM_CREDITS_THRESHOLD }\n }\n if (balance >= HIGH_CREDITS_THRESHOLD) {\n- return 'success'\n+ return { tier: 'high', colorLevel: 'success', threshold: HIGH_CREDITS_THRESHOLD }\n }\n if (balance >= MEDIUM_CREDITS_THRESHOLD) {\n- return 'warning'\n+ return { tier: 'medium', colorLevel: 'warning', threshold: MEDIUM_CREDITS_THRESHOLD }\n }\n- return 'error'\n+ if (balance >= LOW_CREDITS_THRESHOLD) {\n+ return { tier: 'low', colorLevel: 'warning', threshold: LOW_CREDITS_THRESHOLD }\n+ }\n+ return { tier: 'out', colorLevel: 'error', threshold: 0 }\n }\n \n+/**\n+ * Determines the appropriate color level for the usage banner based on credit balance.\n+ *\n+ * Color mapping:\n+ * - success (green): >= 1000 credits\n+ * - warning (yellow): 100-999 credits OR balance is null/unknown \n+ * - error (red): < 100 credits\n+ *\n+ * @deprecated Use getThresholdInfo(balance).colorLevel instead\n+ */\n+export function getBannerColorLevel(balance: number | null): BannerColorLevel {\n+ return getThresholdInfo(balance).colorLevel\n+}\n+\n export interface UsageBannerTextOptions {\n sessionCreditsUsed: number\n remainingBalance: number | null\n next_quota_reset: string | null\n@@ -76,59 +96,71 @@\n \n return text\n }\n \n+/**\n+ * Gets the threshold tier for a given balance.\n+ * Returns null if balance is above all thresholds.\n+ */\n+function getThresholdTier(balance: number): number | null {\n+ if (balance < LOW_CREDITS_THRESHOLD) return LOW_CREDITS_THRESHOLD\n+ if (balance < MEDIUM_CREDITS_THRESHOLD) return MEDIUM_CREDITS_THRESHOLD\n+ if (balance < HIGH_CREDITS_THRESHOLD) return HIGH_CREDITS_THRESHOLD\n+ return null\n+}\n+\n export interface AutoShowDecision {\n shouldShow: boolean\n- newWarningState: UserState | null\n+ newWarningThreshold: number | null\n }\n \n /**\n- * Determines whether the usage banner should auto-show based on credit state changes.\n+ * Determines whether the usage banner should auto-show based on credit threshold crossings.\n *\n * The banner auto-shows when:\n * - User is not in a chain (isChainInProgress = false)\n * - User is authenticated (hasAuthToken = true)\n * - User has credit data available (remainingBalance !== null)\n- * - User enters a new low-credit state that hasn't been warned about yet\n+ * - User crosses a new threshold (1000, 500, or 100) that hasn't been warned about yet\n */\n export function shouldAutoShowBanner(\n isChainInProgress: boolean,\n hasAuthToken: boolean,\n remainingBalance: number | null,\n- lastWarnedState: UserState | null,\n+ lastWarnedThreshold: number | null,\n ): AutoShowDecision {\n // Don't show during active chains\n if (isChainInProgress) {\n- return { shouldShow: false, newWarningState: lastWarnedState }\n+ return { shouldShow: false, newWarningThreshold: lastWarnedThreshold }\n }\n \n // Don't show for unauthenticated users\n if (!hasAuthToken) {\n- return { shouldShow: false, newWarningState: lastWarnedState }\n+ return { shouldShow: false, newWarningThreshold: lastWarnedThreshold }\n }\n \n // Don't show if we don't have balance data\n if (remainingBalance === null) {\n- return { shouldShow: false, newWarningState: lastWarnedState }\n+ return { shouldShow: false, newWarningThreshold: lastWarnedThreshold }\n }\n \n- const userState = getUserState(true, remainingBalance)\n+ const currentThreshold = getThresholdTier(remainingBalance)\n \n- // Clear warning state if user is in good standing\n- if (userState === UserState.GOOD_STANDING) {\n- return { shouldShow: false, newWarningState: null }\n+ // Clear warning state if user is back above all thresholds\n+ if (currentThreshold === null) {\n+ return { shouldShow: false, newWarningThreshold: null }\n }\n \n- // Show banner for new warning states\n- const isWarningState =\n- userState === UserState.ATTENTION_NEEDED ||\n- userState === UserState.CRITICAL ||\n- userState === UserState.DEPLETED\n+ // Show banner if we've crossed a new threshold we haven't warned about\n+ // A \"new\" threshold means either:\n+ // 1. We haven't warned about any threshold yet (lastWarnedThreshold === null)\n+ // 2. The current threshold is lower than what we last warned about\n+ const isNewThreshold =\n+ lastWarnedThreshold === null || currentThreshold < lastWarnedThreshold\n \n- if (isWarningState && lastWarnedState !== userState) {\n- return { shouldShow: true, newWarningState: userState }\n+ if (isNewThreshold) {\n+ return { shouldShow: true, newWarningThreshold: currentThreshold }\n }\n \n- // Already warned about this state\n- return { shouldShow: false, newWarningState: lastWarnedState }\n+ // Already warned about this threshold\n+ return { shouldShow: false, newWarningThreshold: lastWarnedThreshold }\n }\n"
},
{
"path": "common/src/types/session-state.ts",
@@ -971,7 +971,7 @@
{
"path": "sdk/src/impl/llm.ts",
"status": "modified",
- "diff": "Index: sdk/src/impl/llm.ts\n===================================================================\n--- sdk/src/impl/llm.ts\t6c58d85 (parent)\n+++ sdk/src/impl/llm.ts\tc2274f2 (commit)\n@@ -20,9 +20,9 @@\n } from '@codebuff/internal/openai-compatible/index'\n import { streamText, APICallError, generateText, generateObject } from 'ai'\n \n import { WEBSITE_URL } from '../constants'\n-import { NetworkError, ErrorCodes } from '../errors'\n+import { NetworkError, PaymentRequiredError, ErrorCodes } from '../errors'\n \n import type { ErrorCode } from '../errors'\n import type { LanguageModelV2 } from '@ai-sdk/provider'\n import type { OpenRouterProviderRoutingOptions } from '@codebuff/common/types/agent-template'\n@@ -261,9 +261,21 @@\n \n if (APICallError.isInstance(chunk.error)) {\n statusCode = chunk.error.statusCode\n if (statusCode) {\n- if (statusCode === 503) {\n+ if (statusCode === 402) {\n+ // Payment required - extract message from JSON response body\n+ let paymentErrorMessage = mainErrorMessage\n+ if (errorBody) {\n+ try {\n+ const parsed = JSON.parse(errorBody)\n+ paymentErrorMessage = parsed.message || errorBody\n+ } catch {\n+ paymentErrorMessage = errorBody\n+ }\n+ }\n+ throw new PaymentRequiredError(paymentErrorMessage)\n+ } else if (statusCode === 503) {\n errorCode = ErrorCodes.SERVICE_UNAVAILABLE\n } else if (statusCode >= 500) {\n errorCode = ErrorCodes.SERVER_ERROR\n } else if (statusCode === 408 || statusCode === 429) {\n"
+ "diff": "Index: sdk/src/impl/llm.ts\n===================================================================\n--- sdk/src/impl/llm.ts\t6c58d85 (parent)\n+++ sdk/src/impl/llm.ts\tc2274f2 (commit)\n@@ -20,9 +20,9 @@\n } from '@codebirds/internal/openai-compatible/index'\n import { streamText, APICallError, generateText, generateObject } from 'ai'\n \n import { WEBSITE_URL } from '../constants'\n-import { NetworkError, ErrorCodes } from '../errors'\n+import { NetworkError, PaymentRequiredError, ErrorCodes } from '../errors'\n \n import type { ErrorCode } from '../errors'\n import type { LanguageModelV2 } from '@ai-sdk/provider'\n import type { OpenRouterProviderRoutingOptions } from '@codebirds/common/types/agent-template'\n@@ -261,9 +261,21 @@\n \n if (APICallError.isInstance(chunk.error)) {\n statusCode = chunk.error.statusCode\n if (statusCode) {\n- if (statusCode === 503) {\n+ if (statusCode === 402) {\n+ // Payment required - extract message from JSON response body\n+ let paymentErrorMessage = mainErrorMessage\n+ if (errorBody) {\n+ try {\n+ const parsed = JSON.parse(errorBody)\n+ paymentErrorMessage = parsed.message || errorBody\n+ } catch {\n+ paymentErrorMessage = errorBody\n+ }\n+ }\n+ throw new PaymentRequiredError(paymentErrorMessage)\n+ } else if (statusCode === 503) {\n errorCode = ErrorCodes.SERVICE_UNAVAILABLE\n } else if (statusCode >= 500) {\n errorCode = ErrorCodes.SERVER_ERROR\n } else if (statusCode === 408 || statusCode === 429) {\n"
},
{
"path": "sdk/src/index.ts",
@@ -981,17 +981,17 @@
{
"path": "sdk/src/run.ts",
"status": "modified",
- "diff": "Index: sdk/src/run.ts\n===================================================================\n--- sdk/src/run.ts\t6c58d85 (parent)\n+++ sdk/src/run.ts\tc2274f2 (commit)\n@@ -11,9 +11,9 @@\n import { cloneDeep } from 'lodash'\n \n import { getAgentRuntimeImpl } from './impl/agent-runtime'\n import { getUserInfoFromApiKey } from './impl/database'\n-import { RETRYABLE_ERROR_CODES, isNetworkError, ErrorCodes, NetworkError } from './errors'\n+import { RETRYABLE_ERROR_CODES, isNetworkError, isPaymentRequiredError, ErrorCodes, NetworkError, sanitizeErrorMessage } from './errors'\n import type { ErrorCode } from './errors'\n import { getErrorObject } from '@codebuff/common/util/error'\n import { initialSessionState, applyOverridesToSessionState } from './run-state'\n import {\n@@ -373,10 +373,15 @@\n }\n }\n \n // Unexpected exception - convert to error output and check if retryable\n- const errorMessage = error instanceof Error ? error.message : String(error)\n- const errorCode = isNetworkError(error) ? error.code : undefined\n+ // Use sanitizeErrorMessage to get clean user-facing message without stack traces\n+ const errorMessage = sanitizeErrorMessage(error)\n+ const errorCode = isNetworkError(error)\n+ ? error.code\n+ : isPaymentRequiredError(error)\n+ ? error.code\n+ : undefined\n const retryableCode = errorCode ?? getRetryableErrorCode(errorMessage)\n \n const canRetry =\n retryableCode &&\n@@ -412,8 +417,9 @@\n sessionState: rest.previousRun?.sessionState,\n output: {\n type: 'error',\n message: errorMessage,\n+ ...(errorCode && { errorCode }),\n },\n }\n }\n \n@@ -792,22 +798,24 @@\n clientSessionId: promptId,\n userId,\n signal: signal ?? new AbortController().signal,\n }).catch((error) => {\n- // Let retryable errors propagate so the retry wrapper can handle them\n+ // Let retryable errors and PaymentRequiredError propagate so the retry wrapper can handle them\n const isRetryable = isRetryableError(error)\n+ const isPaymentRequired = isPaymentRequiredError(error)\n logger?.warn(\n {\n isNetworkError: isNetworkError(error),\n- errorCode: isNetworkError(error) ? error.code : undefined,\n+ isPaymentRequired,\n+ errorCode: isNetworkError(error) ? error.code : isPaymentRequired ? error.code : undefined,\n isRetryable,\n error: getErrorObject(error),\n },\n 'callMainPrompt caught error, checking if retryable',\n )\n \n- if (isRetryable) {\n- // Reject the promise so the retry wrapper can catch it\n+ if (isRetryable || isPaymentRequired) {\n+ // Reject the promise so the retry wrapper can catch it and include the error code\n reject(error)\n return\n }\n \n"
+ "diff": "Index: sdk/src/run.ts\n===================================================================\n--- sdk/src/run.ts\t6c58d85 (parent)\n+++ sdk/src/run.ts\tc2274f2 (commit)\n@@ -11,9 +11,9 @@\n import { cloneDeep } from 'lodash'\n \n import { getAgentRuntimeImpl } from './impl/agent-runtime'\n import { getUserInfoFromApiKey } from './impl/database'\n-import { RETRYABLE_ERROR_CODES, isNetworkError, ErrorCodes, NetworkError } from './errors'\n+import { RETRYABLE_ERROR_CODES, isNetworkError, isPaymentRequiredError, ErrorCodes, NetworkError, sanitizeErrorMessage } from './errors'\n import type { ErrorCode } from './errors'\n import { getErrorObject } from '@codebirds/common/util/error'\n import { initialSessionState, applyOverridesToSessionState } from './run-state'\n import {\n@@ -373,10 +373,15 @@\n }\n }\n \n // Unexpected exception - convert to error output and check if retryable\n- const errorMessage = error instanceof Error ? error.message : String(error)\n- const errorCode = isNetworkError(error) ? error.code : undefined\n+ // Use sanitizeErrorMessage to get clean user-facing message without stack traces\n+ const errorMessage = sanitizeErrorMessage(error)\n+ const errorCode = isNetworkError(error)\n+ ? error.code\n+ : isPaymentRequiredError(error)\n+ ? error.code\n+ : undefined\n const retryableCode = errorCode ?? getRetryableErrorCode(errorMessage)\n \n const canRetry =\n retryableCode &&\n@@ -412,8 +417,9 @@\n sessionState: rest.previousRun?.sessionState,\n output: {\n type: 'error',\n message: errorMessage,\n+ ...(errorCode && { errorCode }),\n },\n }\n }\n \n@@ -792,22 +798,24 @@\n clientSessionId: promptId,\n userId,\n signal: signal ?? new AbortController().signal,\n }).catch((error) => {\n- // Let retryable errors propagate so the retry wrapper can handle them\n+ // Let retryable errors and PaymentRequiredError propagate so the retry wrapper can handle them\n const isRetryable = isRetryableError(error)\n+ const isPaymentRequired = isPaymentRequiredError(error)\n logger?.warn(\n {\n isNetworkError: isNetworkError(error),\n- errorCode: isNetworkError(error) ? error.code : undefined,\n+ isPaymentRequired,\n+ errorCode: isNetworkError(error) ? error.code : isPaymentRequired ? error.code : undefined,\n isRetryable,\n error: getErrorObject(error),\n },\n 'callMainPrompt caught error, checking if retryable',\n )\n \n- if (isRetryable) {\n- // Reject the promise so the retry wrapper can catch it\n+ if (isRetryable || isPaymentRequired) {\n+ // Reject the promise so the retry wrapper can catch it and include the error code\n reject(error)\n return\n }\n \n"
},
{
"path": "web/src/app/api/v1/chat/completions/__tests__/completions.test.ts",
"status": "modified",
- "diff": "Index: web/src/app/api/v1/chat/completions/__tests__/completions.test.ts\n===================================================================\n--- web/src/app/api/v1/chat/completions/__tests__/completions.test.ts\t6c58d85 (parent)\n+++ web/src/app/api/v1/chat/completions/__tests__/completions.test.ts\tc2274f2 (commit)\n@@ -1,9 +1,9 @@\n import { env } from '@codebuff/internal/env'\n import { afterEach, beforeEach, describe, expect, mock, it } from 'bun:test'\n import { NextRequest } from 'next/server'\n \n-import { postChatCompletions } from '../_post'\n+import { formatQuotaResetCountdown, postChatCompletions } from '../_post'\n \n import type { TrackEventFn } from '@codebuff/common/types/contracts/analytics'\n import type { InsertMessageBigqueryFn } from '@codebuff/common/types/contracts/bigquery'\n import type { GetUserUsageDataFn } from '@codebuff/common/types/contracts/billing'\n@@ -46,10 +46,15 @@\n let mockGetUserUsageData: GetUserUsageDataFn\n let mockGetAgentRunFromId: GetAgentRunFromIdFn\n let mockFetch: typeof globalThis.fetch\n let mockInsertMessageBigquery: InsertMessageBigqueryFn\n+ let nextQuotaReset: string\n \n beforeEach(() => {\n+ nextQuotaReset = new Date(\n+ Date.now() + 3 * 24 * 60 * 60 * 1000 + 5 * 60 * 1000,\n+ ).toISOString()\n+\n mockLogger = {\n error: mock(() => {}),\n warn: mock(() => {}),\n info: mock(() => {}),\n@@ -69,9 +74,9 @@\n totalDebt: 0,\n netBalance: 0,\n breakdown: {},\n },\n- nextQuotaReset: '2024-12-31',\n+ nextQuotaReset,\n }\n }\n return {\n usageThisCycle: 0,\n@@ -80,9 +85,9 @@\n totalDebt: 0,\n netBalance: 100,\n breakdown: {},\n },\n- nextQuotaReset: '2024-12-31',\n+ nextQuotaReset,\n }\n })\n \n mockGetAgentRunFromId = mock((async ({ runId }: any) => {\n@@ -370,12 +375,11 @@\n })\n \n expect(response.status).toBe(402)\n const body = await response.json()\n- expect(body.message).toContain('Insufficient credits')\n- expect(body.message).toContain(\n- `${env.NEXT_PUBLIC_CODEBUFF_APP_URL}/usage`,\n- )\n+ const expectedResetCountdown = formatQuotaResetCountdown(nextQuotaReset)\n+ expect(body.message).toContain(expectedResetCountdown)\n+ expect(body.message).not.toContain(nextQuotaReset)\n })\n })\n \n describe('Successful responses', () => {\n"
+ "diff": "Index: web/src/app/api/v1/chat/completions/__tests__/completions.test.ts\n===================================================================\n--- web/src/app/api/v1/chat/completions/__tests__/completions.test.ts\t6c58d85 (parent)\n+++ web/src/app/api/v1/chat/completions/__tests__/completions.test.ts\tc2274f2 (commit)\n@@ -1,9 +1,9 @@\n import { env } from '@codebirds/internal/env'\n import { afterEach, beforeEach, describe, expect, mock, it } from 'bun:test'\n import { NextRequest } from 'next/server'\n \n-import { postChatCompletions } from '../_post'\n+import { formatQuotaResetCountdown, postChatCompletions } from '../_post'\n \n import type { TrackEventFn } from '@codebirds/common/types/contracts/analytics'\n import type { InsertMessageBigqueryFn } from '@codebirds/common/types/contracts/bigquery'\n import type { GetUserUsageDataFn } from '@codebirds/common/types/contracts/billing'\n@@ -46,10 +46,15 @@\n let mockGetUserUsageData: GetUserUsageDataFn\n let mockGetAgentRunFromId: GetAgentRunFromIdFn\n let mockFetch: typeof globalThis.fetch\n let mockInsertMessageBigquery: InsertMessageBigqueryFn\n+ let nextQuotaReset: string\n \n beforeEach(() => {\n+ nextQuotaReset = new Date(\n+ Date.now() + 3 * 24 * 60 * 60 * 1000 + 5 * 60 * 1000,\n+ ).toISOString()\n+\n mockLogger = {\n error: mock(() => {}),\n warn: mock(() => {}),\n info: mock(() => {}),\n@@ -69,9 +74,9 @@\n totalDebt: 0,\n netBalance: 0,\n breakdown: {},\n },\n- nextQuotaReset: '2024-12-31',\n+ nextQuotaReset,\n }\n }\n return {\n usageThisCycle: 0,\n@@ -80,9 +85,9 @@\n totalDebt: 0,\n netBalance: 100,\n breakdown: {},\n },\n- nextQuotaReset: '2024-12-31',\n+ nextQuotaReset,\n }\n })\n \n mockGetAgentRunFromId = mock((async ({ runId }: any) => {\n@@ -370,12 +375,11 @@\n })\n \n expect(response.status).toBe(402)\n const body = await response.json()\n- expect(body.message).toContain('Insufficient credits')\n- expect(body.message).toContain(\n- `${env.NEXT_PUBLIC_CODEBIRDS_APP_URL}/usage`,\n- )\n+ const expectedResetCountdown = formatQuotaResetCountdown(nextQuotaReset)\n+ expect(body.message).toContain(expectedResetCountdown)\n+ expect(body.message).not.toContain(nextQuotaReset)\n })\n })\n \n describe('Successful responses', () => {\n"
},
{
"path": "web/src/app/api/v1/chat/completions/_post.ts",
"status": "modified",
- "diff": "Index: web/src/app/api/v1/chat/completions/_post.ts\n===================================================================\n--- web/src/app/api/v1/chat/completions/_post.ts\t6c58d85 (parent)\n+++ web/src/app/api/v1/chat/completions/_post.ts\tc2274f2 (commit)\n@@ -1,7 +1,8 @@\n import { AnalyticsEvent } from '@codebuff/common/constants/analytics-events'\n import { BYOK_OPENROUTER_HEADER } from '@codebuff/common/constants/byok'\n import { getErrorObject } from '@codebuff/common/util/error'\n+import { pluralize } from '@codebuff/common/util/string'\n import { env } from '@codebuff/internal/env'\n import { NextResponse } from 'next/server'\n \n import {\n@@ -26,8 +27,44 @@\n LoggerWithContextFn,\n } from '@codebuff/common/types/contracts/logger'\n import type { NextRequest } from 'next/server'\n \n+export const formatQuotaResetCountdown = (\n+ nextQuotaReset: string | null | undefined,\n+): string => {\n+ if (!nextQuotaReset) {\n+ return 'soon'\n+ }\n+\n+ const resetDate = new Date(nextQuotaReset)\n+ if (Number.isNaN(resetDate.getTime())) {\n+ return 'soon'\n+ }\n+\n+ const now = Date.now()\n+ const diffMs = resetDate.getTime() - now\n+ if (diffMs <= 0) {\n+ return 'soon'\n+ }\n+\n+ const minuteMs = 60 * 1000\n+ const hourMs = 60 * minuteMs\n+ const dayMs = 24 * hourMs\n+\n+ const days = Math.floor(diffMs / dayMs)\n+ if (days > 0) {\n+ return `in ${pluralize(days, 'day')}`\n+ }\n+\n+ const hours = Math.floor(diffMs / hourMs)\n+ if (hours > 0) {\n+ return `in ${pluralize(hours, 'hour')}`\n+ }\n+\n+ const minutes = Math.max(1, Math.floor(diffMs / minuteMs))\n+ return `in ${pluralize(minutes, 'minute')}`\n+}\n+\n export async function postChatCompletions(params: {\n req: NextRequest\n getUserInfoFromApiKey: GetUserInfoFromApiKeyFn\n logger: Logger\n@@ -137,11 +174,12 @@\n nextQuotaReset,\n },\n logger,\n })\n+ const resetCountdown = formatQuotaResetCountdown(nextQuotaReset)\n return NextResponse.json(\n {\n- message: `Insufficient credits. Please add credits at ${env.NEXT_PUBLIC_CODEBUFF_APP_URL}/usage or wait for your next cycle to begin (${nextQuotaReset}).`,\n+ message: `Out of credits. Please add credits at ${env.NEXT_PUBLIC_CODEBUFF_APP_URL}/usage. Your free credits reset ${resetCountdown}.`,\n },\n { status: 402 },\n )\n }\n"
+ "diff": "Index: web/src/app/api/v1/chat/completions/_post.ts\n===================================================================\n--- web/src/app/api/v1/chat/completions/_post.ts\t6c58d85 (parent)\n+++ web/src/app/api/v1/chat/completions/_post.ts\tc2274f2 (commit)\n@@ -1,7 +1,8 @@\n import { AnalyticsEvent } from '@codebirds/common/constants/analytics-events'\n import { BYOK_OPENROUTER_HEADER } from '@codebirds/common/constants/byok'\n import { getErrorObject } from '@codebirds/common/util/error'\n+import { pluralize } from '@codebirds/common/util/string'\n import { env } from '@codebirds/internal/env'\n import { NextResponse } from 'next/server'\n \n import {\n@@ -26,8 +27,44 @@\n LoggerWithContextFn,\n } from '@codebirds/common/types/contracts/logger'\n import type { NextRequest } from 'next/server'\n \n+export const formatQuotaResetCountdown = (\n+ nextQuotaReset: string | null | undefined,\n+): string => {\n+ if (!nextQuotaReset) {\n+ return 'soon'\n+ }\n+\n+ const resetDate = new Date(nextQuotaReset)\n+ if (Number.isNaN(resetDate.getTime())) {\n+ return 'soon'\n+ }\n+\n+ const now = Date.now()\n+ const diffMs = resetDate.getTime() - now\n+ if (diffMs <= 0) {\n+ return 'soon'\n+ }\n+\n+ const minuteMs = 60 * 1000\n+ const hourMs = 60 * minuteMs\n+ const dayMs = 24 * hourMs\n+\n+ const days = Math.floor(diffMs / dayMs)\n+ if (days > 0) {\n+ return `in ${pluralize(days, 'day')}`\n+ }\n+\n+ const hours = Math.floor(diffMs / hourMs)\n+ if (hours > 0) {\n+ return `in ${pluralize(hours, 'hour')}`\n+ }\n+\n+ const minutes = Math.max(1, Math.floor(diffMs / minuteMs))\n+ return `in ${pluralize(minutes, 'minute')}`\n+}\n+\n export async function postChatCompletions(params: {\n req: NextRequest\n getUserInfoFromApiKey: GetUserInfoFromApiKeyFn\n logger: Logger\n@@ -137,11 +174,12 @@\n nextQuotaReset,\n },\n logger,\n })\n+ const resetCountdown = formatQuotaResetCountdown(nextQuotaReset)\n return NextResponse.json(\n {\n- message: `Insufficient credits. Please add credits at ${env.NEXT_PUBLIC_CODEBIRDS_APP_URL}/usage or wait for your next cycle to begin (${nextQuotaReset}).`,\n+ message: `Out of credits. Please add credits at ${env.NEXT_PUBLIC_CODEBIRDS_APP_URL}/usage. Your free credits reset ${resetCountdown}.`,\n },\n { status: 402 },\n )\n }\n"
}
]
},
@@ -999,7 +999,7 @@
"id": "cache-tool-defs",
"sha": "ee54646fccefec3b518a1476291464bcbcad584b",
"parentSha": "906a3d33d775ccf0e152fff5bfcf00cde906ab69",
- "spec": "Objective: Centralize creation of additional tool definitions, ensure they are computed once per agent loop, and update prompt-building contracts to accept an injected provider returning CustomToolDefinitions.\n\nImplement the following:\n\n1) Create a helper to compute additional tool definitions\n- Location: packages/agent-runtime/src/run-agent-step.ts\n- Add a local async function additionalToolDefinitions(params) that:\n - Accepts: agentTemplate and fileContext, plus the dependencies required by getMCPToolData (excluding toolNames, mcpServers, and writeTo which the helper supplies).\n - Clones fileContext.customToolDefinitions and filters to only include tool names present in agentTemplate.toolNames.\n - Calls getMCPToolData with toolNames=agentTemplate.toolNames, mcpServers=agentTemplate.mcpServers, and writeTo referencing the filtered clone.\n - Returns a CustomToolDefinitions object combining filtered custom tools with MCP-provided tools.\n\n2) Cache tool definitions within loopAgentSteps\n- Location: packages/agent-runtime/src/run-agent-step.ts\n- In loopAgentSteps, declare a variable cachedAdditionalToolDefinitions: CustomToolDefinitions | undefined.\n- When calling getAgentPrompt for both instructionsPrompt and systemPrompt, pass an additionalToolDefinitions callback that:\n - Computes tool defs using the helper if the cache is empty (providing agentTemplate and other required params), stores in cache, and returns it; otherwise returns the cached value.\n- When invoking runAgentStep from loopAgentSteps, pass an additionalToolDefinitions callback that returns the same cached value (computing on first use if needed).\n\n3) Update runAgentStep to rely on injected additionalToolDefinitions\n- Location: packages/agent-runtime/src/run-agent-step.ts\n- Adjust the typing so ParamsExcluding no longer excludes 'additionalToolDefinitions'.\n- Remove the previous inline construction of additionalToolDefinitions inside runAgentStep when calling getAgentPrompt; instead, accept additionalToolDefinitions from params and pass it directly to getAgentPrompt.\n\n4) Update getAgentPrompt contract to use CustomToolDefinitions\n- Location: packages/agent-runtime/src/templates/strings.ts\n- Import CustomToolDefinitions from @codebuff/common/util/file.\n- Change the type of the additionalToolDefinitions parameter in getAgentPrompt from Promise to Promise.\n\n5) Make all call sites provide additionalToolDefinitions\n- Update any runAgentStep invocations that previously omitted additionalToolDefinitions to pass a minimal provider when tools are not under test, e.g., additionalToolDefinitions: () => Promise.resolve({}).\n- Apply this in evals/scaffolding and unit tests under packages/agent-runtime/src/__tests__/ where runAgentStepBaseParams is constructed.\n\n6) Types and imports\n- Ensure imports are present where needed:\n - CustomToolDefinitions, ProjectFileContext from @codebuff/common/util/file.\n - ParamsExcluding from @codebuff/common/types/function-params where constructing typed base params (tests) or helper signatures.\n\nAcceptance criteria:\n- Agent runtime computes additional tool definitions once per loopAgentSteps execution and reuses them for prompt generation and the child runAgentStep call.\n- getAgentPrompt accepts an additionalToolDefinitions callback returning CustomToolDefinitions.\n- runAgentStep accepts and forwards additionalToolDefinitions rather than rebuilding it.\n- All tests and scaffolding compile and pass with additionalToolDefinitions supplied where needed.",
+ "spec": "Objective: Centralize creation of additional tool definitions, ensure they are computed once per agent loop, and update prompt-building contracts to accept an injected provider returning CustomToolDefinitions.\n\nImplement the following:\n\n1) Create a helper to compute additional tool definitions\n- Location: packages/agent-runtime/src/run-agent-step.ts\n- Add a local async function additionalToolDefinitions(params) that:\n - Accepts: agentTemplate and fileContext, plus the dependencies required by getMCPToolData (excluding toolNames, mcpServers, and writeTo which the helper supplies).\n - Clones fileContext.customToolDefinitions and filters to only include tool names present in agentTemplate.toolNames.\n - Calls getMCPToolData with toolNames=agentTemplate.toolNames, mcpServers=agentTemplate.mcpServers, and writeTo referencing the filtered clone.\n - Returns a CustomToolDefinitions object combining filtered custom tools with MCP-provided tools.\n\n2) Cache tool definitions within loopAgentSteps\n- Location: packages/agent-runtime/src/run-agent-step.ts\n- In loopAgentSteps, declare a variable cachedAdditionalToolDefinitions: CustomToolDefinitions | undefined.\n- When calling getAgentPrompt for both instructionsPrompt and systemPrompt, pass an additionalToolDefinitions callback that:\n - Computes tool defs using the helper if the cache is empty (providing agentTemplate and other required params), stores in cache, and returns it; otherwise returns the cached value.\n- When invoking runAgentStep from loopAgentSteps, pass an additionalToolDefinitions callback that returns the same cached value (computing on first use if needed).\n\n3) Update runAgentStep to rely on injected additionalToolDefinitions\n- Location: packages/agent-runtime/src/run-agent-step.ts\n- Adjust the typing so ParamsExcluding no longer excludes 'additionalToolDefinitions'.\n- Remove the previous inline construction of additionalToolDefinitions inside runAgentStep when calling getAgentPrompt; instead, accept additionalToolDefinitions from params and pass it directly to getAgentPrompt.\n\n4) Update getAgentPrompt contract to use CustomToolDefinitions\n- Location: packages/agent-runtime/src/templates/strings.ts\n- Import CustomToolDefinitions from @codebirds/common/util/file.\n- Change the type of the additionalToolDefinitions parameter in getAgentPrompt from Promise to Promise.\n\n5) Make all call sites provide additionalToolDefinitions\n- Update any runAgentStep invocations that previously omitted additionalToolDefinitions to pass a minimal provider when tools are not under test, e.g., additionalToolDefinitions: () => Promise.resolve({}).\n- Apply this in evals/scaffolding and unit tests under packages/agent-runtime/src/__tests__/ where runAgentStepBaseParams is constructed.\n\n6) Types and imports\n- Ensure imports are present where needed:\n - CustomToolDefinitions, ProjectFileContext from @codebirds/common/util/file.\n - ParamsExcluding from @codebirds/common/types/function-params where constructing typed base params (tests) or helper signatures.\n\nAcceptance criteria:\n- Agent runtime computes additional tool definitions once per loopAgentSteps execution and reuses them for prompt generation and the child runAgentStep call.\n- getAgentPrompt accepts an additionalToolDefinitions callback returning CustomToolDefinitions.\n- runAgentStep accepts and forwards additionalToolDefinitions rather than rebuilding it.\n- All tests and scaffolding compile and pass with additionalToolDefinitions supplied where needed.",
"prompt": "Unify the way the agent gathers tool definitions for prompting. Add a single helper that merges the agent’s allowed custom tools with MCP tool metadata, compute it once per agent run, and reuse it for all prompt construction and step execution. Update the prompt-building API to accept an injected provider for these tool definitions and pass it through the runtime instead of rebuilding it inline. Ensure scaffolding and tests provide a simple no-op provider where tools aren’t needed.",
"supplementalFiles": [
"packages/agent-runtime/src/mcp.ts",
@@ -1016,7 +1016,7 @@
{
"path": "packages/agent-runtime/src/__tests__/n-parameter.test.ts",
"status": "modified",
- "diff": "Index: packages/agent-runtime/src/__tests__/n-parameter.test.ts\n===================================================================\n--- packages/agent-runtime/src/__tests__/n-parameter.test.ts\t906a3d3 (parent)\n+++ packages/agent-runtime/src/__tests__/n-parameter.test.ts\tee54646 (commit)\n@@ -27,9 +27,12 @@\n AgentRuntimeScopedDeps,\n } from '@codebuff/common/types/contracts/agent-runtime'\n import type { PromptAiSdkFn } from '@codebuff/common/types/contracts/llm'\n import type { Logger } from '@codebuff/common/types/contracts/logger'\n-import type { ParamsOf } from '@codebuff/common/types/function-params'\n+import type {\n+ ParamsExcluding,\n+ ParamsOf,\n+} from '@codebuff/common/types/function-params'\n import type { AgentState } from '@codebuff/common/types/session-state'\n \n const logger: Logger = {\n debug: () => {},\n@@ -41,8 +44,9 @@\n describe('n parameter and GENERATE_N functionality', () => {\n let mockTemplate: AgentTemplate\n let mockAgentState: AgentState\n let agentRuntimeImpl: AgentRuntimeDeps & AgentRuntimeScopedDeps\n+ let runAgentStepBaseParams: ParamsExcluding\n \n beforeEach(() => {\n agentRuntimeImpl = {\n ...TEST_AGENT_RUNTIME_IMPL,\n@@ -95,8 +99,31 @@\n output: undefined,\n directCreditsUsed: 0,\n childRunIds: [],\n }\n+\n+ runAgentStepBaseParams = {\n+ ...agentRuntimeImpl,\n+ additionalToolDefinitions: () => Promise.resolve({}),\n+ textOverride: null,\n+ runId: 'test-run-id',\n+ ancestorRunIds: [],\n+ repoId: undefined,\n+ repoUrl: undefined,\n+ userId: TEST_USER_ID,\n+ userInputId: 'test-input',\n+ clientSessionId: 'test-session',\n+ fingerprintId: 'test-fingerprint',\n+ fileContext: mockFileContext,\n+ onResponseChunk: () => {},\n+ agentType: 'test-agent',\n+ localAgentTemplates: { 'test-agent': mockTemplate },\n+ agentState: mockAgentState,\n+ prompt: 'Test prompt',\n+ spawnParams: undefined,\n+ system: 'Test system',\n+ signal: new AbortController().signal,\n+ }\n })\n \n afterEach(() => {\n mock.restore()\n@@ -104,40 +131,21 @@\n })\n \n describe('runAgentStep with n parameter', () => {\n it('should call promptAiSdk with n parameter when n is provided', async () => {\n- const promptAiSdkSpy = spyOn(\n- agentRuntimeImpl,\n- 'promptAiSdk',\n- ).mockResolvedValue(\n- JSON.stringify(['Response 1', 'Response 2', 'Response 3']),\n+ runAgentStepBaseParams.promptAiSdk = mock(() =>\n+ Promise.resolve(\n+ JSON.stringify(['Response 1', 'Response 2', 'Response 3']),\n+ ),\n )\n \n const result = await runAgentStep({\n- ...agentRuntimeImpl,\n- textOverride: null,\n- runId: 'test-run-id',\n- ancestorRunIds: [],\n- repoId: undefined,\n- repoUrl: undefined,\n- userId: TEST_USER_ID,\n- userInputId: 'test-input',\n- clientSessionId: 'test-session',\n- fingerprintId: 'test-fingerprint',\n- fileContext: mockFileContext,\n- onResponseChunk: () => {},\n- agentType: 'test-agent',\n- localAgentTemplates: { 'test-agent': mockTemplate },\n- agentState: mockAgentState,\n- prompt: 'Test prompt',\n- spawnParams: undefined,\n- system: 'Test system',\n+ ...runAgentStepBaseParams,\n n: 3,\n- signal: new AbortController().signal,\n })\n \n // Verify promptAiSdk was called with n: 3\n- expect(promptAiSdkSpy).toHaveBeenCalledWith(\n+ expect(runAgentStepBaseParams.promptAiSdk).toHaveBeenCalledWith(\n expect.objectContaining({\n n: 3,\n }),\n )\n@@ -152,45 +160,24 @@\n expect(result.messageId).toBe(null)\n })\n \n it('should return early without calling promptAiSdkStream when n is provided', async () => {\n- const promptAiSdkStreamSpy = spyOn(\n- agentRuntimeImpl,\n- 'promptAiSdkStream',\n- ).mockImplementation(async function* () {\n+ runAgentStepBaseParams.promptAiSdkStream = mock(async function* () {\n yield { type: 'text' as const, text: 'Should not be called' }\n return 'mock-message-id'\n })\n \n- spyOn(agentRuntimeImpl, 'promptAiSdk').mockResolvedValue(\n+ runAgentStepBaseParams.promptAiSdk = mock(async () =>\n JSON.stringify(['Response 1', 'Response 2']),\n )\n \n await runAgentStep({\n- ...agentRuntimeImpl,\n- textOverride: null,\n- runId: 'test-run-id',\n- ancestorRunIds: [],\n- repoId: undefined,\n- repoUrl: undefined,\n- userId: TEST_USER_ID,\n- userInputId: 'test-input',\n- clientSessionId: 'test-session',\n- fingerprintId: 'test-fingerprint',\n- fileContext: mockFileContext,\n- onResponseChunk: () => {},\n- agentType: 'test-agent',\n- localAgentTemplates: { 'test-agent': mockTemplate },\n- agentState: mockAgentState,\n- prompt: 'Test prompt',\n- spawnParams: undefined,\n- system: 'Test system',\n+ ...runAgentStepBaseParams,\n n: 2,\n- signal: new AbortController().signal,\n })\n \n // Verify stream was NOT called\n- expect(promptAiSdkStreamSpy).not.toHaveBeenCalled()\n+ expect(runAgentStepBaseParams.promptAiSdkStream).not.toHaveBeenCalled()\n })\n \n it('should parse JSON response from promptAiSdk correctly', async () => {\n const responses = [\n@@ -200,80 +187,40 @@\n 'Fourth implementation',\n 'Fifth implementation',\n ]\n \n- spyOn(agentRuntimeImpl, 'promptAiSdk').mockResolvedValue(\n+ runAgentStepBaseParams.promptAiSdk = mock(async () =>\n JSON.stringify(responses),\n )\n \n const result = await runAgentStep({\n- ...agentRuntimeImpl,\n- textOverride: null,\n- runId: 'test-run-id',\n- ancestorRunIds: [],\n- repoId: undefined,\n- repoUrl: undefined,\n- userId: TEST_USER_ID,\n- userInputId: 'test-input',\n- clientSessionId: 'test-session',\n- fingerprintId: 'test-fingerprint',\n- fileContext: mockFileContext,\n- onResponseChunk: () => {},\n- agentType: 'test-agent',\n- localAgentTemplates: { 'test-agent': mockTemplate },\n- agentState: mockAgentState,\n- prompt: 'Generate 5 responses',\n- spawnParams: undefined,\n- system: 'Test system',\n+ ...runAgentStepBaseParams,\n n: 5,\n- signal: new AbortController().signal,\n })\n \n expect(result.nResponses).toEqual(responses)\n expect(result.nResponses?.length).toBe(5)\n })\n \n it('should use normal flow when n is undefined', async () => {\n- const promptAiSdkSpy = spyOn(\n- agentRuntimeImpl,\n- 'promptAiSdk',\n- ).mockResolvedValue('Should not be called')\n+ runAgentStepBaseParams.promptAiSdk = mock(\n+ async () => 'Should not be called',\n+ )\n \n- const promptAiSdkStreamSpy = spyOn(\n- agentRuntimeImpl,\n- 'promptAiSdkStream',\n- ).mockImplementation(async function* () {\n+ runAgentStepBaseParams.promptAiSdkStream = mock(async function* () {\n yield { type: 'text' as const, text: 'Normal response' }\n return 'mock-message-id'\n })\n \n const result = await runAgentStep({\n- ...agentRuntimeImpl,\n- textOverride: null,\n- runId: 'test-run-id',\n- ancestorRunIds: [],\n- repoId: undefined,\n- repoUrl: undefined,\n- userId: TEST_USER_ID,\n- userInputId: 'test-input',\n- clientSessionId: 'test-session',\n- fingerprintId: 'test-fingerprint',\n- fileContext: mockFileContext,\n- onResponseChunk: () => {},\n- agentType: 'test-agent',\n- localAgentTemplates: { 'test-agent': mockTemplate },\n- agentState: mockAgentState,\n- prompt: 'Test prompt',\n- spawnParams: undefined,\n- system: 'Test system',\n+ ...runAgentStepBaseParams,\n n: undefined,\n- signal: new AbortController().signal,\n })\n \n // Verify promptAiSdk was NOT called\n- expect(promptAiSdkSpy).not.toHaveBeenCalled()\n+ expect(runAgentStepBaseParams.promptAiSdk).not.toHaveBeenCalled()\n // Verify stream was called\n- expect(promptAiSdkStreamSpy).toHaveBeenCalled()\n+ expect(runAgentStepBaseParams.promptAiSdkStream).toHaveBeenCalled()\n // nResponses should be undefined in normal flow\n expect(result.nResponses).toBeUndefined()\n })\n })\n@@ -901,32 +848,16 @@\n })\n \n describe('runAgentStep n parameter edge cases', () => {\n it('should handle promptAiSdk returning malformed JSON', async () => {\n- spyOn(agentRuntimeImpl, 'promptAiSdk').mockResolvedValue('Not valid JSON')\n+ runAgentStepBaseParams.promptAiSdk = mock(() =>\n+ Promise.resolve('Not valid JSON'),\n+ )\n \n await expect(\n runAgentStep({\n- ...agentRuntimeImpl,\n- textOverride: null,\n- runId: 'test-run-id',\n- ancestorRunIds: [],\n- repoId: undefined,\n- repoUrl: undefined,\n- userId: TEST_USER_ID,\n- userInputId: 'test-input',\n- clientSessionId: 'test-session',\n- fingerprintId: 'test-fingerprint',\n- fileContext: mockFileContext,\n- onResponseChunk: () => {},\n- agentType: 'test-agent',\n- localAgentTemplates: { 'test-agent': mockTemplate },\n- agentState: mockAgentState,\n- prompt: 'Test',\n- spawnParams: undefined,\n- system: 'Test',\n+ ...runAgentStepBaseParams,\n n: 3,\n- signal: new AbortController().signal,\n }),\n ).rejects.toThrow()\n })\n \n@@ -937,76 +868,38 @@\n creditsUsed: 0,\n directCreditsUsed: 0,\n }\n \n- const promptAiSdkSpy = spyOn(\n- agentRuntimeImpl,\n- 'promptAiSdk',\n- ).mockImplementation(\n+ runAgentStepBaseParams.promptAiSdk = mock(\n async (params: ParamsOf): ReturnType => {\n // Call onCostCalculated to simulate cost tracking\n await params.onCostCalculated?.(100)\n return JSON.stringify(['R1', 'R2', 'R3'])\n },\n )\n \n const result = await runAgentStep({\n- ...agentRuntimeImpl,\n- textOverride: null,\n- runId: 'test-run-id',\n- ancestorRunIds: [],\n- repoId: undefined,\n- repoUrl: undefined,\n- userId: TEST_USER_ID,\n- userInputId: 'test-input',\n- clientSessionId: 'test-session',\n- fingerprintId: 'test-fingerprint',\n- fileContext: mockFileContext,\n- onResponseChunk: () => {},\n- agentType: 'test-agent',\n- localAgentTemplates: { 'test-agent': mockTemplate },\n+ ...runAgentStepBaseParams,\n agentState: freshAgentState,\n- prompt: 'Test',\n- spawnParams: undefined,\n- system: 'Test',\n n: 3,\n- signal: new AbortController().signal,\n })\n \n // Verify onCostCalculated was called in promptAiSdk\n- expect(promptAiSdkSpy).toHaveBeenCalled()\n+ expect(runAgentStepBaseParams.promptAiSdk).toHaveBeenCalled()\n \n // Verify credits were updated from 0 to 100\n expect(result.agentState.creditsUsed).toBe(100)\n expect(result.agentState.directCreditsUsed).toBe(100)\n })\n \n it('should preserve messageHistory when using n parameter', async () => {\n- spyOn(agentRuntimeImpl, 'promptAiSdk').mockResolvedValue(\n- JSON.stringify(['R1', 'R2']),\n+ runAgentStepBaseParams.promptAiSdk = mock(() =>\n+ Promise.resolve(JSON.stringify(['R1', 'R2'])),\n )\n \n const result = await runAgentStep({\n- ...agentRuntimeImpl,\n- textOverride: null,\n- runId: 'test-run-id',\n- ancestorRunIds: [],\n- repoId: undefined,\n- repoUrl: undefined,\n- userId: TEST_USER_ID,\n- userInputId: 'test-input',\n- clientSessionId: 'test-session',\n- fingerprintId: 'test-fingerprint',\n- fileContext: mockFileContext,\n- onResponseChunk: () => {},\n- agentType: 'test-agent',\n- localAgentTemplates: { 'test-agent': mockTemplate },\n- agentState: mockAgentState,\n- prompt: 'Test',\n- spawnParams: undefined,\n- system: 'Test',\n+ ...runAgentStepBaseParams,\n n: 2,\n- signal: new AbortController().signal,\n })\n \n // Message history should include the user prompt that was added\n // The implementation adds user prompt message before calling promptAiSdk\n"
+ "diff": "Index: packages/agent-runtime/src/__tests__/n-parameter.test.ts\n===================================================================\n--- packages/agent-runtime/src/__tests__/n-parameter.test.ts\t906a3d3 (parent)\n+++ packages/agent-runtime/src/__tests__/n-parameter.test.ts\tee54646 (commit)\n@@ -27,9 +27,12 @@\n AgentRuntimeScopedDeps,\n } from '@codebirds/common/types/contracts/agent-runtime'\n import type { PromptAiSdkFn } from '@codebirds/common/types/contracts/llm'\n import type { Logger } from '@codebirds/common/types/contracts/logger'\n-import type { ParamsOf } from '@codebirds/common/types/function-params'\n+import type {\n+ ParamsExcluding,\n+ ParamsOf,\n+} from '@codebirds/common/types/function-params'\n import type { AgentState } from '@codebirds/common/types/session-state'\n \n const logger: Logger = {\n debug: () => {},\n@@ -41,8 +44,9 @@\n describe('n parameter and GENERATE_N functionality', () => {\n let mockTemplate: AgentTemplate\n let mockAgentState: AgentState\n let agentRuntimeImpl: AgentRuntimeDeps & AgentRuntimeScopedDeps\n+ let runAgentStepBaseParams: ParamsExcluding\n \n beforeEach(() => {\n agentRuntimeImpl = {\n ...TEST_AGENT_RUNTIME_IMPL,\n@@ -95,8 +99,31 @@\n output: undefined,\n directCreditsUsed: 0,\n childRunIds: [],\n }\n+\n+ runAgentStepBaseParams = {\n+ ...agentRuntimeImpl,\n+ additionalToolDefinitions: () => Promise.resolve({}),\n+ textOverride: null,\n+ runId: 'test-run-id',\n+ ancestorRunIds: [],\n+ repoId: undefined,\n+ repoUrl: undefined,\n+ userId: TEST_USER_ID,\n+ userInputId: 'test-input',\n+ clientSessionId: 'test-session',\n+ fingerprintId: 'test-fingerprint',\n+ fileContext: mockFileContext,\n+ onResponseChunk: () => {},\n+ agentType: 'test-agent',\n+ localAgentTemplates: { 'test-agent': mockTemplate },\n+ agentState: mockAgentState,\n+ prompt: 'Test prompt',\n+ spawnParams: undefined,\n+ system: 'Test system',\n+ signal: new AbortController().signal,\n+ }\n })\n \n afterEach(() => {\n mock.restore()\n@@ -104,40 +131,21 @@\n })\n \n describe('runAgentStep with n parameter', () => {\n it('should call promptAiSdk with n parameter when n is provided', async () => {\n- const promptAiSdkSpy = spyOn(\n- agentRuntimeImpl,\n- 'promptAiSdk',\n- ).mockResolvedValue(\n- JSON.stringify(['Response 1', 'Response 2', 'Response 3']),\n+ runAgentStepBaseParams.promptAiSdk = mock(() =>\n+ Promise.resolve(\n+ JSON.stringify(['Response 1', 'Response 2', 'Response 3']),\n+ ),\n )\n \n const result = await runAgentStep({\n- ...agentRuntimeImpl,\n- textOverride: null,\n- runId: 'test-run-id',\n- ancestorRunIds: [],\n- repoId: undefined,\n- repoUrl: undefined,\n- userId: TEST_USER_ID,\n- userInputId: 'test-input',\n- clientSessionId: 'test-session',\n- fingerprintId: 'test-fingerprint',\n- fileContext: mockFileContext,\n- onResponseChunk: () => {},\n- agentType: 'test-agent',\n- localAgentTemplates: { 'test-agent': mockTemplate },\n- agentState: mockAgentState,\n- prompt: 'Test prompt',\n- spawnParams: undefined,\n- system: 'Test system',\n+ ...runAgentStepBaseParams,\n n: 3,\n- signal: new AbortController().signal,\n })\n \n // Verify promptAiSdk was called with n: 3\n- expect(promptAiSdkSpy).toHaveBeenCalledWith(\n+ expect(runAgentStepBaseParams.promptAiSdk).toHaveBeenCalledWith(\n expect.objectContaining({\n n: 3,\n }),\n )\n@@ -152,45 +160,24 @@\n expect(result.messageId).toBe(null)\n })\n \n it('should return early without calling promptAiSdkStream when n is provided', async () => {\n- const promptAiSdkStreamSpy = spyOn(\n- agentRuntimeImpl,\n- 'promptAiSdkStream',\n- ).mockImplementation(async function* () {\n+ runAgentStepBaseParams.promptAiSdkStream = mock(async function* () {\n yield { type: 'text' as const, text: 'Should not be called' }\n return 'mock-message-id'\n })\n \n- spyOn(agentRuntimeImpl, 'promptAiSdk').mockResolvedValue(\n+ runAgentStepBaseParams.promptAiSdk = mock(async () =>\n JSON.stringify(['Response 1', 'Response 2']),\n )\n \n await runAgentStep({\n- ...agentRuntimeImpl,\n- textOverride: null,\n- runId: 'test-run-id',\n- ancestorRunIds: [],\n- repoId: undefined,\n- repoUrl: undefined,\n- userId: TEST_USER_ID,\n- userInputId: 'test-input',\n- clientSessionId: 'test-session',\n- fingerprintId: 'test-fingerprint',\n- fileContext: mockFileContext,\n- onResponseChunk: () => {},\n- agentType: 'test-agent',\n- localAgentTemplates: { 'test-agent': mockTemplate },\n- agentState: mockAgentState,\n- prompt: 'Test prompt',\n- spawnParams: undefined,\n- system: 'Test system',\n+ ...runAgentStepBaseParams,\n n: 2,\n- signal: new AbortController().signal,\n })\n \n // Verify stream was NOT called\n- expect(promptAiSdkStreamSpy).not.toHaveBeenCalled()\n+ expect(runAgentStepBaseParams.promptAiSdkStream).not.toHaveBeenCalled()\n })\n \n it('should parse JSON response from promptAiSdk correctly', async () => {\n const responses = [\n@@ -200,80 +187,40 @@\n 'Fourth implementation',\n 'Fifth implementation',\n ]\n \n- spyOn(agentRuntimeImpl, 'promptAiSdk').mockResolvedValue(\n+ runAgentStepBaseParams.promptAiSdk = mock(async () =>\n JSON.stringify(responses),\n )\n \n const result = await runAgentStep({\n- ...agentRuntimeImpl,\n- textOverride: null,\n- runId: 'test-run-id',\n- ancestorRunIds: [],\n- repoId: undefined,\n- repoUrl: undefined,\n- userId: TEST_USER_ID,\n- userInputId: 'test-input',\n- clientSessionId: 'test-session',\n- fingerprintId: 'test-fingerprint',\n- fileContext: mockFileContext,\n- onResponseChunk: () => {},\n- agentType: 'test-agent',\n- localAgentTemplates: { 'test-agent': mockTemplate },\n- agentState: mockAgentState,\n- prompt: 'Generate 5 responses',\n- spawnParams: undefined,\n- system: 'Test system',\n+ ...runAgentStepBaseParams,\n n: 5,\n- signal: new AbortController().signal,\n })\n \n expect(result.nResponses).toEqual(responses)\n expect(result.nResponses?.length).toBe(5)\n })\n \n it('should use normal flow when n is undefined', async () => {\n- const promptAiSdkSpy = spyOn(\n- agentRuntimeImpl,\n- 'promptAiSdk',\n- ).mockResolvedValue('Should not be called')\n+ runAgentStepBaseParams.promptAiSdk = mock(\n+ async () => 'Should not be called',\n+ )\n \n- const promptAiSdkStreamSpy = spyOn(\n- agentRuntimeImpl,\n- 'promptAiSdkStream',\n- ).mockImplementation(async function* () {\n+ runAgentStepBaseParams.promptAiSdkStream = mock(async function* () {\n yield { type: 'text' as const, text: 'Normal response' }\n return 'mock-message-id'\n })\n \n const result = await runAgentStep({\n- ...agentRuntimeImpl,\n- textOverride: null,\n- runId: 'test-run-id',\n- ancestorRunIds: [],\n- repoId: undefined,\n- repoUrl: undefined,\n- userId: TEST_USER_ID,\n- userInputId: 'test-input',\n- clientSessionId: 'test-session',\n- fingerprintId: 'test-fingerprint',\n- fileContext: mockFileContext,\n- onResponseChunk: () => {},\n- agentType: 'test-agent',\n- localAgentTemplates: { 'test-agent': mockTemplate },\n- agentState: mockAgentState,\n- prompt: 'Test prompt',\n- spawnParams: undefined,\n- system: 'Test system',\n+ ...runAgentStepBaseParams,\n n: undefined,\n- signal: new AbortController().signal,\n })\n \n // Verify promptAiSdk was NOT called\n- expect(promptAiSdkSpy).not.toHaveBeenCalled()\n+ expect(runAgentStepBaseParams.promptAiSdk).not.toHaveBeenCalled()\n // Verify stream was called\n- expect(promptAiSdkStreamSpy).toHaveBeenCalled()\n+ expect(runAgentStepBaseParams.promptAiSdkStream).toHaveBeenCalled()\n // nResponses should be undefined in normal flow\n expect(result.nResponses).toBeUndefined()\n })\n })\n@@ -901,32 +848,16 @@\n })\n \n describe('runAgentStep n parameter edge cases', () => {\n it('should handle promptAiSdk returning malformed JSON', async () => {\n- spyOn(agentRuntimeImpl, 'promptAiSdk').mockResolvedValue('Not valid JSON')\n+ runAgentStepBaseParams.promptAiSdk = mock(() =>\n+ Promise.resolve('Not valid JSON'),\n+ )\n \n await expect(\n runAgentStep({\n- ...agentRuntimeImpl,\n- textOverride: null,\n- runId: 'test-run-id',\n- ancestorRunIds: [],\n- repoId: undefined,\n- repoUrl: undefined,\n- userId: TEST_USER_ID,\n- userInputId: 'test-input',\n- clientSessionId: 'test-session',\n- fingerprintId: 'test-fingerprint',\n- fileContext: mockFileContext,\n- onResponseChunk: () => {},\n- agentType: 'test-agent',\n- localAgentTemplates: { 'test-agent': mockTemplate },\n- agentState: mockAgentState,\n- prompt: 'Test',\n- spawnParams: undefined,\n- system: 'Test',\n+ ...runAgentStepBaseParams,\n n: 3,\n- signal: new AbortController().signal,\n }),\n ).rejects.toThrow()\n })\n \n@@ -937,76 +868,38 @@\n creditsUsed: 0,\n directCreditsUsed: 0,\n }\n \n- const promptAiSdkSpy = spyOn(\n- agentRuntimeImpl,\n- 'promptAiSdk',\n- ).mockImplementation(\n+ runAgentStepBaseParams.promptAiSdk = mock(\n async (params: ParamsOf): ReturnType => {\n // Call onCostCalculated to simulate cost tracking\n await params.onCostCalculated?.(100)\n return JSON.stringify(['R1', 'R2', 'R3'])\n },\n )\n \n const result = await runAgentStep({\n- ...agentRuntimeImpl,\n- textOverride: null,\n- runId: 'test-run-id',\n- ancestorRunIds: [],\n- repoId: undefined,\n- repoUrl: undefined,\n- userId: TEST_USER_ID,\n- userInputId: 'test-input',\n- clientSessionId: 'test-session',\n- fingerprintId: 'test-fingerprint',\n- fileContext: mockFileContext,\n- onResponseChunk: () => {},\n- agentType: 'test-agent',\n- localAgentTemplates: { 'test-agent': mockTemplate },\n+ ...runAgentStepBaseParams,\n agentState: freshAgentState,\n- prompt: 'Test',\n- spawnParams: undefined,\n- system: 'Test',\n n: 3,\n- signal: new AbortController().signal,\n })\n \n // Verify onCostCalculated was called in promptAiSdk\n- expect(promptAiSdkSpy).toHaveBeenCalled()\n+ expect(runAgentStepBaseParams.promptAiSdk).toHaveBeenCalled()\n \n // Verify credits were updated from 0 to 100\n expect(result.agentState.creditsUsed).toBe(100)\n expect(result.agentState.directCreditsUsed).toBe(100)\n })\n \n it('should preserve messageHistory when using n parameter', async () => {\n- spyOn(agentRuntimeImpl, 'promptAiSdk').mockResolvedValue(\n- JSON.stringify(['R1', 'R2']),\n+ runAgentStepBaseParams.promptAiSdk = mock(() =>\n+ Promise.resolve(JSON.stringify(['R1', 'R2'])),\n )\n \n const result = await runAgentStep({\n- ...agentRuntimeImpl,\n- textOverride: null,\n- runId: 'test-run-id',\n- ancestorRunIds: [],\n- repoId: undefined,\n- repoUrl: undefined,\n- userId: TEST_USER_ID,\n- userInputId: 'test-input',\n- clientSessionId: 'test-session',\n- fingerprintId: 'test-fingerprint',\n- fileContext: mockFileContext,\n- onResponseChunk: () => {},\n- agentType: 'test-agent',\n- localAgentTemplates: { 'test-agent': mockTemplate },\n- agentState: mockAgentState,\n- prompt: 'Test',\n- spawnParams: undefined,\n- system: 'Test',\n+ ...runAgentStepBaseParams,\n n: 2,\n- signal: new AbortController().signal,\n })\n \n // Message history should include the user prompt that was added\n // The implementation adds user prompt message before calling promptAiSdk\n"
},
{
"path": "packages/agent-runtime/src/__tests__/read-docs-tool.test.ts",
@@ -1036,12 +1036,12 @@
{
"path": "packages/agent-runtime/src/run-agent-step.ts",
"status": "modified",
- "diff": "Index: packages/agent-runtime/src/run-agent-step.ts\n===================================================================\n--- packages/agent-runtime/src/run-agent-step.ts\t906a3d3 (parent)\n+++ packages/agent-runtime/src/run-agent-step.ts\tee54646 (commit)\n@@ -52,10 +52,39 @@\n AgentTemplateType,\n AgentState,\n AgentOutput,\n } from '@codebuff/common/types/session-state'\n-import type { ProjectFileContext } from '@codebuff/common/util/file'\n+import type {\n+ CustomToolDefinitions,\n+ ProjectFileContext,\n+} from '@codebuff/common/util/file'\n \n+async function additionalToolDefinitions(\n+ params: {\n+ agentTemplate: AgentTemplate\n+ fileContext: ProjectFileContext\n+ } & ParamsExcluding<\n+ typeof getMCPToolData,\n+ 'toolNames' | 'mcpServers' | 'writeTo'\n+ >,\n+): Promise {\n+ const { agentTemplate, fileContext } = params\n+\n+ const defs = cloneDeep(\n+ Object.fromEntries(\n+ Object.entries(fileContext.customToolDefinitions).filter(([toolName]) =>\n+ agentTemplate!.toolNames.includes(toolName),\n+ ),\n+ ),\n+ )\n+ return getMCPToolData({\n+ ...params,\n+ toolNames: agentTemplate!.toolNames,\n+ mcpServers: agentTemplate!.mcpServers,\n+ writeTo: defs,\n+ })\n+}\n+\n export const runAgentStep = async (\n params: {\n userId: string | undefined\n userInputId: string\n@@ -98,13 +127,9 @@\n > &\n ParamsExcluding &\n ParamsExcluding<\n typeof getAgentPrompt,\n- | 'agentTemplate'\n- | 'promptType'\n- | 'agentState'\n- | 'agentTemplates'\n- | 'additionalToolDefinitions'\n+ 'agentTemplate' | 'promptType' | 'agentState' | 'agentTemplates'\n > &\n ParamsExcluding<\n typeof getMCPToolData,\n 'toolNames' | 'mcpServers' | 'writeTo'\n@@ -211,23 +236,8 @@\n fileContext,\n agentState,\n agentTemplates: localAgentTemplates,\n logger,\n- additionalToolDefinitions: () => {\n- const additionalToolDefinitions = cloneDeep(\n- Object.fromEntries(\n- Object.entries(fileContext.customToolDefinitions).filter(\n- ([toolName]) => agentTemplate.toolNames.includes(toolName),\n- ),\n- ),\n- )\n- return getMCPToolData({\n- ...params,\n- toolNames: agentTemplate.toolNames,\n- mcpServers: agentTemplate.mcpServers,\n- writeTo: additionalToolDefinitions,\n- })\n- },\n })\n \n const agentMessagesUntruncated = buildArray(\n ...expireMessages(messageHistory, 'agentStep'),\n@@ -476,20 +486,21 @@\n startAgentRun: StartAgentRunFn\n finishAgentRun: FinishAgentRunFn\n addAgentStep: AddAgentStepFn\n logger: Logger\n- } & ParamsExcluding<\n- typeof runProgrammaticStep,\n- | 'runId'\n- | 'agentState'\n- | 'template'\n- | 'prompt'\n- | 'toolCallParams'\n- | 'stepsComplete'\n- | 'stepNumber'\n- | 'system'\n- | 'onCostCalculated'\n- > &\n+ } & ParamsExcluding &\n+ ParamsExcluding<\n+ typeof runProgrammaticStep,\n+ | 'runId'\n+ | 'agentState'\n+ | 'template'\n+ | 'prompt'\n+ | 'toolCallParams'\n+ | 'stepsComplete'\n+ | 'stepNumber'\n+ | 'system'\n+ | 'onCostCalculated'\n+ > &\n ParamsExcluding &\n ParamsExcluding<\n typeof getAgentPrompt,\n | 'agentTemplate'\n@@ -508,13 +519,14 @@\n 'runId' | 'status' | 'totalSteps' | 'directCredits' | 'totalCredits'\n > &\n ParamsExcluding<\n typeof runAgentStep,\n+ | 'additionalToolDefinitions'\n | 'agentState'\n | 'prompt'\n+ | 'runId'\n | 'spawnParams'\n | 'system'\n- | 'runId'\n | 'textOverride'\n > &\n ParamsExcluding<\n AddAgentStepFn,\n@@ -577,28 +589,23 @@\n throw new Error('Failed to start agent run')\n }\n agentState.runId = runId\n \n+ let cachedAdditionalToolDefinitions: CustomToolDefinitions | undefined\n // Initialize message history with user prompt and instructions on first iteration\n const instructionsPrompt = await getAgentPrompt({\n ...params,\n agentTemplate,\n promptType: { type: 'instructionsPrompt' },\n agentTemplates: localAgentTemplates,\n- additionalToolDefinitions: () => {\n- const additionalToolDefinitions = cloneDeep(\n- Object.fromEntries(\n- Object.entries(fileContext.customToolDefinitions).filter(\n- ([toolName]) => agentTemplate.toolNames.includes(toolName),\n- ),\n- ),\n- )\n- return getMCPToolData({\n- ...params,\n- toolNames: agentTemplate.toolNames,\n- mcpServers: agentTemplate.mcpServers,\n- writeTo: additionalToolDefinitions,\n- })\n+ additionalToolDefinitions: async () => {\n+ if (!cachedAdditionalToolDefinitions) {\n+ cachedAdditionalToolDefinitions = await additionalToolDefinitions({\n+ ...params,\n+ agentTemplate,\n+ })\n+ }\n+ return cachedAdditionalToolDefinitions\n },\n })\n \n // Build the initial message history with user prompt and instructions\n@@ -610,22 +617,18 @@\n ...params,\n agentTemplate,\n promptType: { type: 'systemPrompt' },\n agentTemplates: localAgentTemplates,\n- additionalToolDefinitions: () => {\n- const additionalToolDefinitions = cloneDeep(\n- Object.fromEntries(\n- Object.entries(fileContext.customToolDefinitions).filter(\n- ([toolName]) => agentTemplate.toolNames.includes(toolName),\n- ),\n- ),\n- )\n- return getMCPToolData({\n- ...params,\n- toolNames: agentTemplate.toolNames,\n- mcpServers: agentTemplate.mcpServers,\n- writeTo: additionalToolDefinitions,\n- })\n+ additionalToolDefinitions: async () => {\n+ if (!cachedAdditionalToolDefinitions) {\n+ cachedAdditionalToolDefinitions = await additionalToolDefinitions(\n+ {\n+ ...params,\n+ agentTemplate,\n+ },\n+ )\n+ }\n+ return cachedAdditionalToolDefinitions\n },\n })) ?? ''\n \n const hasUserMessage = Boolean(\n@@ -782,8 +785,17 @@\n messageId,\n nResponses: generatedResponses,\n } = await runAgentStep({\n ...params,\n+ additionalToolDefinitions: async () => {\n+ if (!cachedAdditionalToolDefinitions) {\n+ cachedAdditionalToolDefinitions = await additionalToolDefinitions({\n+ ...params,\n+ agentTemplate,\n+ })\n+ }\n+ return cachedAdditionalToolDefinitions\n+ },\n textOverride: textOverride,\n runId,\n agentState: currentAgentState,\n prompt: currentPrompt,\n"
+ "diff": "Index: packages/agent-runtime/src/run-agent-step.ts\n===================================================================\n--- packages/agent-runtime/src/run-agent-step.ts\t906a3d3 (parent)\n+++ packages/agent-runtime/src/run-agent-step.ts\tee54646 (commit)\n@@ -52,10 +52,39 @@\n AgentTemplateType,\n AgentState,\n AgentOutput,\n } from '@codebirds/common/types/session-state'\n-import type { ProjectFileContext } from '@codebirds/common/util/file'\n+import type {\n+ CustomToolDefinitions,\n+ ProjectFileContext,\n+} from '@codebirds/common/util/file'\n \n+async function additionalToolDefinitions(\n+ params: {\n+ agentTemplate: AgentTemplate\n+ fileContext: ProjectFileContext\n+ } & ParamsExcluding<\n+ typeof getMCPToolData,\n+ 'toolNames' | 'mcpServers' | 'writeTo'\n+ >,\n+): Promise {\n+ const { agentTemplate, fileContext } = params\n+\n+ const defs = cloneDeep(\n+ Object.fromEntries(\n+ Object.entries(fileContext.customToolDefinitions).filter(([toolName]) =>\n+ agentTemplate!.toolNames.includes(toolName),\n+ ),\n+ ),\n+ )\n+ return getMCPToolData({\n+ ...params,\n+ toolNames: agentTemplate!.toolNames,\n+ mcpServers: agentTemplate!.mcpServers,\n+ writeTo: defs,\n+ })\n+}\n+\n export const runAgentStep = async (\n params: {\n userId: string | undefined\n userInputId: string\n@@ -98,13 +127,9 @@\n > &\n ParamsExcluding &\n ParamsExcluding<\n typeof getAgentPrompt,\n- | 'agentTemplate'\n- | 'promptType'\n- | 'agentState'\n- | 'agentTemplates'\n- | 'additionalToolDefinitions'\n+ 'agentTemplate' | 'promptType' | 'agentState' | 'agentTemplates'\n > &\n ParamsExcluding<\n typeof getMCPToolData,\n 'toolNames' | 'mcpServers' | 'writeTo'\n@@ -211,23 +236,8 @@\n fileContext,\n agentState,\n agentTemplates: localAgentTemplates,\n logger,\n- additionalToolDefinitions: () => {\n- const additionalToolDefinitions = cloneDeep(\n- Object.fromEntries(\n- Object.entries(fileContext.customToolDefinitions).filter(\n- ([toolName]) => agentTemplate.toolNames.includes(toolName),\n- ),\n- ),\n- )\n- return getMCPToolData({\n- ...params,\n- toolNames: agentTemplate.toolNames,\n- mcpServers: agentTemplate.mcpServers,\n- writeTo: additionalToolDefinitions,\n- })\n- },\n })\n \n const agentMessagesUntruncated = buildArray(\n ...expireMessages(messageHistory, 'agentStep'),\n@@ -476,20 +486,21 @@\n startAgentRun: StartAgentRunFn\n finishAgentRun: FinishAgentRunFn\n addAgentStep: AddAgentStepFn\n logger: Logger\n- } & ParamsExcluding<\n- typeof runProgrammaticStep,\n- | 'runId'\n- | 'agentState'\n- | 'template'\n- | 'prompt'\n- | 'toolCallParams'\n- | 'stepsComplete'\n- | 'stepNumber'\n- | 'system'\n- | 'onCostCalculated'\n- > &\n+ } & ParamsExcluding &\n+ ParamsExcluding<\n+ typeof runProgrammaticStep,\n+ | 'runId'\n+ | 'agentState'\n+ | 'template'\n+ | 'prompt'\n+ | 'toolCallParams'\n+ | 'stepsComplete'\n+ | 'stepNumber'\n+ | 'system'\n+ | 'onCostCalculated'\n+ > &\n ParamsExcluding &\n ParamsExcluding<\n typeof getAgentPrompt,\n | 'agentTemplate'\n@@ -508,13 +519,14 @@\n 'runId' | 'status' | 'totalSteps' | 'directCredits' | 'totalCredits'\n > &\n ParamsExcluding<\n typeof runAgentStep,\n+ | 'additionalToolDefinitions'\n | 'agentState'\n | 'prompt'\n+ | 'runId'\n | 'spawnParams'\n | 'system'\n- | 'runId'\n | 'textOverride'\n > &\n ParamsExcluding<\n AddAgentStepFn,\n@@ -577,28 +589,23 @@\n throw new Error('Failed to start agent run')\n }\n agentState.runId = runId\n \n+ let cachedAdditionalToolDefinitions: CustomToolDefinitions | undefined\n // Initialize message history with user prompt and instructions on first iteration\n const instructionsPrompt = await getAgentPrompt({\n ...params,\n agentTemplate,\n promptType: { type: 'instructionsPrompt' },\n agentTemplates: localAgentTemplates,\n- additionalToolDefinitions: () => {\n- const additionalToolDefinitions = cloneDeep(\n- Object.fromEntries(\n- Object.entries(fileContext.customToolDefinitions).filter(\n- ([toolName]) => agentTemplate.toolNames.includes(toolName),\n- ),\n- ),\n- )\n- return getMCPToolData({\n- ...params,\n- toolNames: agentTemplate.toolNames,\n- mcpServers: agentTemplate.mcpServers,\n- writeTo: additionalToolDefinitions,\n- })\n+ additionalToolDefinitions: async () => {\n+ if (!cachedAdditionalToolDefinitions) {\n+ cachedAdditionalToolDefinitions = await additionalToolDefinitions({\n+ ...params,\n+ agentTemplate,\n+ })\n+ }\n+ return cachedAdditionalToolDefinitions\n },\n })\n \n // Build the initial message history with user prompt and instructions\n@@ -610,22 +617,18 @@\n ...params,\n agentTemplate,\n promptType: { type: 'systemPrompt' },\n agentTemplates: localAgentTemplates,\n- additionalToolDefinitions: () => {\n- const additionalToolDefinitions = cloneDeep(\n- Object.fromEntries(\n- Object.entries(fileContext.customToolDefinitions).filter(\n- ([toolName]) => agentTemplate.toolNames.includes(toolName),\n- ),\n- ),\n- )\n- return getMCPToolData({\n- ...params,\n- toolNames: agentTemplate.toolNames,\n- mcpServers: agentTemplate.mcpServers,\n- writeTo: additionalToolDefinitions,\n- })\n+ additionalToolDefinitions: async () => {\n+ if (!cachedAdditionalToolDefinitions) {\n+ cachedAdditionalToolDefinitions = await additionalToolDefinitions(\n+ {\n+ ...params,\n+ agentTemplate,\n+ },\n+ )\n+ }\n+ return cachedAdditionalToolDefinitions\n },\n })) ?? ''\n \n const hasUserMessage = Boolean(\n@@ -782,8 +785,17 @@\n messageId,\n nResponses: generatedResponses,\n } = await runAgentStep({\n ...params,\n+ additionalToolDefinitions: async () => {\n+ if (!cachedAdditionalToolDefinitions) {\n+ cachedAdditionalToolDefinitions = await additionalToolDefinitions({\n+ ...params,\n+ agentTemplate,\n+ })\n+ }\n+ return cachedAdditionalToolDefinitions\n+ },\n textOverride: textOverride,\n runId,\n agentState: currentAgentState,\n prompt: currentPrompt,\n"
},
{
"path": "packages/agent-runtime/src/templates/strings.ts",
"status": "modified",
- "diff": "Index: packages/agent-runtime/src/templates/strings.ts\n===================================================================\n--- packages/agent-runtime/src/templates/strings.ts\t906a3d3 (parent)\n+++ packages/agent-runtime/src/templates/strings.ts\tee54646 (commit)\n@@ -29,9 +29,12 @@\n import type {\n AgentState,\n AgentTemplateType,\n } from '@codebuff/common/types/session-state'\n-import type { ProjectFileContext } from '@codebuff/common/util/file'\n+import type {\n+ CustomToolDefinitions,\n+ ProjectFileContext,\n+} from '@codebuff/common/util/file'\n \n export async function formatPrompt(\n params: {\n prompt: string\n@@ -158,11 +161,9 @@\n promptType: { type: T }\n fileContext: ProjectFileContext\n agentState: AgentState\n agentTemplates: Record\n- additionalToolDefinitions: () => Promise<\n- ProjectFileContext['customToolDefinitions']\n- >\n+ additionalToolDefinitions: () => Promise\n logger: Logger\n } & ParamsExcluding<\n typeof formatPrompt,\n 'prompt' | 'tools' | 'spawnableAgents'\n"
+ "diff": "Index: packages/agent-runtime/src/templates/strings.ts\n===================================================================\n--- packages/agent-runtime/src/templates/strings.ts\t906a3d3 (parent)\n+++ packages/agent-runtime/src/templates/strings.ts\tee54646 (commit)\n@@ -29,9 +29,12 @@\n import type {\n AgentState,\n AgentTemplateType,\n } from '@codebirds/common/types/session-state'\n-import type { ProjectFileContext } from '@codebirds/common/util/file'\n+import type {\n+ CustomToolDefinitions,\n+ ProjectFileContext,\n+} from '@codebirds/common/util/file'\n \n export async function formatPrompt(\n params: {\n prompt: string\n@@ -158,11 +161,9 @@\n promptType: { type: T }\n fileContext: ProjectFileContext\n agentState: AgentState\n agentTemplates: Record\n- additionalToolDefinitions: () => Promise<\n- ProjectFileContext['customToolDefinitions']\n- >\n+ additionalToolDefinitions: () => Promise\n logger: Logger\n } & ParamsExcluding<\n typeof formatPrompt,\n 'prompt' | 'tools' | 'spawnableAgents'\n"
}
]
},
@@ -1049,7 +1049,7 @@
"id": "remove-handler-state",
"sha": "c6d7a324ce2c756975fdfc947a339c50a35b0116",
"parentSha": "a542e7f88861899c5602d0b55221e6f701dffb88",
- "spec": "Goal: Refactor the tool handler interface to remove the state object and unify on an async return value with output (and optional creditsUsed), adjusting executor and tests accordingly.\n\n1) Type signature update\n- File: packages/agent-runtime/src/tools/handlers/handler-function-type.ts\n - Remove the exported State type.\n - Change CodebuffToolHandlerFunction signature from returning { result: Promise['content']>, state?: Partial } to returning Promise<{ output: CodebuffToolMessage['content']; creditsUsed?: number }>.\n - Ensure CodebuffToolMessage['content'] remains the output content type.\n\n2) Executor behavior update\n- File: packages/agent-runtime/src/tools/tool-executor.ts\n - Stop importing/using State and removeUndefinedProps for state merging.\n - Invoke the handler once and await its Promise directly; destructure { output, creditsUsed }.\n - Emit tool_result with content set to output.\n - If creditsUsed is present and > 0, call onCostCalculated(creditsUsed) and log the addition.\n - Remove all prior logic related to pending or promised state.creditsUsed.\n\n3) Handler implementation updates\n- For every tool handler under packages/agent-runtime/src/tools/handlers/tool/*.ts, change implementation to the new async signature and return shape. Requirements:\n - Make the exported handler an async function (or return a Promise) and satisfy CodebuffToolHandlerFunction.\n - Await previousToolCallFinished at the top before performing any handler logic.\n - Return { output } where output is the CodebuffToolOutput payload; when returning JSON, use jsonToolResult(...).\n - Remove the state property from the return value everywhere.\n - If a handler previously tracked credits via state.creditsUsed (including Promises), now set creditsUsed on the returned object. This applies to read-docs and web-search, which should capture and return the number provided by their web APIs.\n - Ensure handlers that delegate to client tools (glob, list_directory, browser_logs, code_search, run_file_change_hooks, run_terminal_command) simply await previousToolCallFinished and return { output: await requestClientToolCall(toolCall) }.\n - For file-processing handlers (write_file, str_replace, create_plan, read_files), await previousToolCallFinished and then use postStreamProcessing(...) to produce the final output; return as { output }.\n - For tree reading (read_subtree), compute the outputs and return { output: jsonToolResult(outputs) }.\n - For messaging and trivial tools (add_message, set_messages, end_turn, task_completed, think_deeply), return an empty tool output ([]), i.e., { output: [] } after awaiting previousToolCallFinished.\n - For agent operations:\n - spawn_agent_inline: validate, create child state, execute subagent, update parent message history, and return { output: [] }.\n - spawn_agents: validate each child, execute subagents in parallel, map results into a report array (agentName, agentType, value), aggregate parentAgentState.creditsUsed by summing child agent credits (both fulfilled and rejected cases when available), then return { output: jsonToolResult(reports) }.\n - For set_output: validate against outputSchema if present (log and return a validation error message if invalid), otherwise set agentState.output and return { output: jsonToolResult({ message: 'Output set' }) }.\n - For lookup_agent_info: return structured JSON with found=true and agent metadata (using input/output schema conversion to JSON Schema when defined) or found=false with an error.\n - Clean up imports (add jsonToolResult where needed, remove unused imports).\n\n4) Test updates for new async return\n- Update tests under packages/agent-runtime/src/__tests__/ and packages/agent-runtime/src/tools/handlers/__tests__/ to align with the new API:\n - Replace patterns like const { result } = handleX(...); await result with await handleX(...).\n - When assertions check tool output, destructure { output } = await handleX(...), then assert on output.\n - For error expectations on inline spawn, assign the Promise from handleSpawnAgentInline(...) to a variable and use expect(promise).rejects.toThrow(...). Do not await before the expect.\n - For places asserting only side effects (e.g., credits aggregation), just await the handler call and then assert on agent state (e.g., parentAgentState.creditsUsed).\n\n5) Remove residual references\n- Ensure no remaining references to the removed State type or to state: {} in any handler or caller.\n- Confirm all handlers satisfy the new CodebuffToolHandlerFunction type and compile without implicit any.\n\n6) Maintain behavior\n- Preserve existing business logic, streaming, logging, and cost aggregation semantics.\n- Honor previousToolCallFinished ordering in all handlers.\n\nAcceptance:\n- All unit/integration tests compile and pass.\n- No handler returns or references a state object.\n- tool-executor consumes { output, creditsUsed? } and applies credits once.\n- read-docs and web-search return creditsUsed in the new field and costs are aggregated.\n- Message history and agent spawning behaviors are unchanged aside from the new API.",
+ "spec": "Goal: Refactor the tool handler interface to remove the state object and unify on an async return value with output (and optional creditsUsed), adjusting executor and tests accordingly.\n\n1) Type signature update\n- File: packages/agent-runtime/src/tools/handlers/handler-function-type.ts\n - Remove the exported State type.\n - Change CodebirdsToolHandlerFunction signature from returning { result: Promise['content']>, state?: Partial } to returning Promise<{ output: CodebirdsToolMessage['content']; creditsUsed?: number }>.\n - Ensure CodebirdsToolMessage['content'] remains the output content type.\n\n2) Executor behavior update\n- File: packages/agent-runtime/src/tools/tool-executor.ts\n - Stop importing/using State and removeUndefinedProps for state merging.\n - Invoke the handler once and await its Promise directly; destructure { output, creditsUsed }.\n - Emit tool_result with content set to output.\n - If creditsUsed is present and > 0, call onCostCalculated(creditsUsed) and log the addition.\n - Remove all prior logic related to pending or promised state.creditsUsed.\n\n3) Handler implementation updates\n- For every tool handler under packages/agent-runtime/src/tools/handlers/tool/*.ts, change implementation to the new async signature and return shape. Requirements:\n - Make the exported handler an async function (or return a Promise) and satisfy CodebirdsToolHandlerFunction.\n - Await previousToolCallFinished at the top before performing any handler logic.\n - Return { output } where output is the CodebirdsToolOutput payload; when returning JSON, use jsonToolResult(...).\n - Remove the state property from the return value everywhere.\n - If a handler previously tracked credits via state.creditsUsed (including Promises), now set creditsUsed on the returned object. This applies to read-docs and web-search, which should capture and return the number provided by their web APIs.\n - Ensure handlers that delegate to client tools (glob, list_directory, browser_logs, code_search, run_file_change_hooks, run_terminal_command) simply await previousToolCallFinished and return { output: await requestClientToolCall(toolCall) }.\n - For file-processing handlers (write_file, str_replace, create_plan, read_files), await previousToolCallFinished and then use postStreamProcessing(...) to produce the final output; return as { output }.\n - For tree reading (read_subtree), compute the outputs and return { output: jsonToolResult(outputs) }.\n - For messaging and trivial tools (add_message, set_messages, end_turn, task_completed, think_deeply), return an empty tool output ([]), i.e., { output: [] } after awaiting previousToolCallFinished.\n - For agent operations:\n - spawn_agent_inline: validate, create child state, execute subagent, update parent message history, and return { output: [] }.\n - spawn_agents: validate each child, execute subagents in parallel, map results into a report array (agentName, agentType, value), aggregate parentAgentState.creditsUsed by summing child agent credits (both fulfilled and rejected cases when available), then return { output: jsonToolResult(reports) }.\n - For set_output: validate against outputSchema if present (log and return a validation error message if invalid), otherwise set agentState.output and return { output: jsonToolResult({ message: 'Output set' }) }.\n - For lookup_agent_info: return structured JSON with found=true and agent metadata (using input/output schema conversion to JSON Schema when defined) or found=false with an error.\n - Clean up imports (add jsonToolResult where needed, remove unused imports).\n\n4) Test updates for new async return\n- Update tests under packages/agent-runtime/src/__tests__/ and packages/agent-runtime/src/tools/handlers/__tests__/ to align with the new API:\n - Replace patterns like const { result } = handleX(...); await result with await handleX(...).\n - When assertions check tool output, destructure { output } = await handleX(...), then assert on output.\n - For error expectations on inline spawn, assign the Promise from handleSpawnAgentInline(...) to a variable and use expect(promise).rejects.toThrow(...). Do not await before the expect.\n - For places asserting only side effects (e.g., credits aggregation), just await the handler call and then assert on agent state (e.g., parentAgentState.creditsUsed).\n\n5) Remove residual references\n- Ensure no remaining references to the removed State type or to state: {} in any handler or caller.\n- Confirm all handlers satisfy the new CodebirdsToolHandlerFunction type and compile without implicit any.\n\n6) Maintain behavior\n- Preserve existing business logic, streaming, logging, and cost aggregation semantics.\n- Honor previousToolCallFinished ordering in all handlers.\n\nAcceptance:\n- All unit/integration tests compile and pass.\n- No handler returns or references a state object.\n- tool-executor consumes { output, creditsUsed? } and applies credits once.\n- read-docs and web-search return creditsUsed in the new field and costs are aggregated.\n- Message history and agent spawning behaviors are unchanged aside from the new API.",
"prompt": "Refactor the tool handler API to remove the extra state object and standardize on an async return that includes the tool output (and optional credits used). Update the executor to consume this shape, and migrate all tool handlers and tests accordingly. Keep existing behavior, streaming, logging, and cost aggregation intact. Ensure handlers await prior tool completion before running, return JSON via the existing helper, and report any credits used directly on the return value. Update the tests to await the handler call and validate outputs/side effects with the new API.",
"supplementalFiles": [
"packages/agent-runtime/src/tools/handlers/list.ts",
@@ -1073,7 +1073,7 @@
{
"path": "packages/agent-runtime/src/__tests__/spawn-agents-permissions.test.ts",
"status": "modified",
- "diff": "Index: packages/agent-runtime/src/__tests__/spawn-agents-permissions.test.ts\n===================================================================\n--- packages/agent-runtime/src/__tests__/spawn-agents-permissions.test.ts\ta542e7f (parent)\n+++ packages/agent-runtime/src/__tests__/spawn-agents-permissions.test.ts\tc6d7a32 (commit)\n@@ -252,17 +252,16 @@\n const childAgent = createMockAgent('thinker')\n const sessionState = getInitialSessionState(mockFileContext)\n const toolCall = createSpawnToolCall('thinker')\n \n- const { result } = handleSpawnAgents({\n+ const { output } = await handleSpawnAgents({\n ...handleSpawnAgentsBaseParams,\n agentState: sessionState.mainAgentState,\n agentTemplate: parentAgent,\n localAgentTemplates: { thinker: childAgent },\n toolCall,\n })\n \n- const output = await result\n expect(JSON.stringify(output)).toContain('Mock agent response')\n expect(mockLoopAgentSteps).toHaveBeenCalledTimes(1)\n })\n \n@@ -271,17 +270,16 @@\n const childAgent = createMockAgent('reviewer')\n const sessionState = getInitialSessionState(mockFileContext)\n const toolCall = createSpawnToolCall('reviewer') // Try to spawn reviewer\n \n- const { result } = handleSpawnAgents({\n+ const { output } = await handleSpawnAgents({\n ...handleSpawnAgentsBaseParams,\n agentState: sessionState.mainAgentState,\n agentTemplate: parentAgent,\n localAgentTemplates: { reviewer: childAgent },\n toolCall,\n })\n \n- const output = await result\n expect(JSON.stringify(output)).toContain('Error spawning agent')\n expect(JSON.stringify(output)).toContain(\n 'is not allowed to spawn child agent type reviewer',\n )\n@@ -292,17 +290,16 @@\n const parentAgent = createMockAgent('parent', ['nonexistent'])\n const sessionState = getInitialSessionState(mockFileContext)\n const toolCall = createSpawnToolCall('nonexistent')\n \n- const { result } = handleSpawnAgents({\n+ const { output } = await handleSpawnAgents({\n ...handleSpawnAgentsBaseParams,\n agentState: sessionState.mainAgentState,\n agentTemplate: parentAgent,\n localAgentTemplates: {}, // Empty - agent not found\n toolCall,\n })\n \n- const output = await result\n console.log('output', output)\n expect(JSON.stringify(output)).toContain('Error spawning agent')\n expect(JSON.stringify(output)).toContain(\n 'Agent type nonexistent not found',\n@@ -315,17 +312,16 @@\n const childAgent = createMockAgent('codebuff/thinker@1.0.0')\n const sessionState = getInitialSessionState(mockFileContext)\n const toolCall = createSpawnToolCall('codebuff/thinker@1.0.0')\n \n- const { result } = handleSpawnAgents({\n+ const { output } = await handleSpawnAgents({\n ...handleSpawnAgentsBaseParams,\n agentState: sessionState.mainAgentState,\n agentTemplate: parentAgent,\n localAgentTemplates: { 'codebuff/thinker@1.0.0': childAgent },\n toolCall,\n })\n \n- const output = await result\n expect(JSON.stringify(output)).toContain('Mock agent response')\n expect(mockLoopAgentSteps).toHaveBeenCalledTimes(1)\n })\n \n@@ -334,9 +330,9 @@\n const childAgent = createMockAgent('codebuff/thinker@1.0.0')\n const sessionState = getInitialSessionState(mockFileContext)\n const toolCall = createSpawnToolCall('thinker') // Simple name\n \n- const { result } = handleSpawnAgents({\n+ const { output } = await handleSpawnAgents({\n ...handleSpawnAgentsBaseParams,\n agentState: sessionState.mainAgentState,\n agentTemplate: parentAgent,\n localAgentTemplates: {\n@@ -345,9 +341,8 @@\n },\n toolCall,\n })\n \n- const output = await result\n expect(JSON.stringify(output)).toContain('Mock agent response')\n expect(mockLoopAgentSteps).toHaveBeenCalledTimes(1)\n })\n \n@@ -356,17 +351,16 @@\n const childAgent = createMockAgent('codebuff/thinker@2.0.0')\n const sessionState = getInitialSessionState(mockFileContext)\n const toolCall = createSpawnToolCall('codebuff/thinker@2.0.0')\n \n- const { result } = handleSpawnAgents({\n+ const { output } = await handleSpawnAgents({\n ...handleSpawnAgentsBaseParams,\n agentState: sessionState.mainAgentState,\n agentTemplate: parentAgent,\n localAgentTemplates: { 'codebuff/thinker@2.0.0': childAgent },\n toolCall,\n })\n \n- const output = await result\n expect(JSON.stringify(output)).toContain('Error spawning agent')\n expect(JSON.stringify(output)).toContain(\n 'is not allowed to spawn child agent type',\n )\n@@ -389,9 +383,9 @@\n ],\n },\n }\n \n- const { result } = handleSpawnAgents({\n+ const { output } = await handleSpawnAgents({\n ...handleSpawnAgentsBaseParams,\n agentState: sessionState.mainAgentState,\n agentTemplate: parentAgent,\n localAgentTemplates: {\n@@ -400,9 +394,8 @@\n },\n toolCall,\n })\n \n- const output = await result\n expect(JSON.stringify(output)).toContain('Mock agent response') // Successful thinker spawn\n expect(JSON.stringify(output)).toContain('Error spawning agent') // Failed reviewer spawn\n expect(JSON.stringify(output)).toContain(\n 'is not allowed to spawn child agent type reviewer',\n@@ -429,17 +422,17 @@\n const childAgent = createMockAgent('thinker')\n const sessionState = getInitialSessionState(mockFileContext)\n const toolCall = createInlineSpawnToolCall('thinker')\n \n- const { result } = handleSpawnAgentInline({\n+ // Should not throw\n+ await handleSpawnAgentInline({\n ...handleSpawnAgentsBaseParams,\n agentState: sessionState.mainAgentState,\n agentTemplate: parentAgent,\n localAgentTemplates: { thinker: childAgent },\n toolCall,\n })\n \n- await result // Should not throw\n expect(mockLoopAgentSteps).toHaveBeenCalledTimes(1)\n })\n \n it('should reject spawning inline agent when agent is not in spawnableAgents list', async () => {\n@@ -447,17 +440,17 @@\n const childAgent = createMockAgent('reviewer')\n const sessionState = getInitialSessionState(mockFileContext)\n const toolCall = createInlineSpawnToolCall('reviewer') // Try to spawn reviewer\n \n- const { result } = handleSpawnAgentInline({\n+ const result = handleSpawnAgentInline({\n ...handleSpawnAgentsBaseParams,\n agentState: sessionState.mainAgentState,\n agentTemplate: parentAgent,\n localAgentTemplates: { reviewer: childAgent },\n toolCall,\n })\n \n- await expect(result).rejects.toThrow(\n+ expect(result).rejects.toThrow(\n 'is not allowed to spawn child agent type reviewer',\n )\n expect(mockLoopAgentSteps).not.toHaveBeenCalled()\n })\n@@ -466,17 +459,17 @@\n const parentAgent = createMockAgent('parent', ['nonexistent'])\n const sessionState = getInitialSessionState(mockFileContext)\n const toolCall = createInlineSpawnToolCall('nonexistent')\n \n- const { result } = handleSpawnAgentInline({\n+ const result = handleSpawnAgentInline({\n ...handleSpawnAgentsBaseParams,\n agentState: sessionState.mainAgentState,\n agentTemplate: parentAgent,\n localAgentTemplates: {}, // Empty - agent not found\n toolCall,\n })\n \n- await expect(result).rejects.toThrow('Agent type nonexistent not found')\n+ expect(result).rejects.toThrow('Agent type nonexistent not found')\n expect(mockLoopAgentSteps).not.toHaveBeenCalled()\n })\n \n it('should handle versioned inline agent permissions correctly', async () => {\n@@ -484,17 +477,17 @@\n const childAgent = createMockAgent('codebuff/thinker@1.0.0')\n const sessionState = getInitialSessionState(mockFileContext)\n const toolCall = createInlineSpawnToolCall('codebuff/thinker@1.0.0')\n \n- const { result } = handleSpawnAgentInline({\n+ // Should not throw\n+ await handleSpawnAgentInline({\n ...handleSpawnAgentsBaseParams,\n agentState: sessionState.mainAgentState,\n agentTemplate: parentAgent,\n localAgentTemplates: { 'codebuff/thinker@1.0.0': childAgent },\n toolCall,\n })\n \n- await result // Should not throw\n expect(mockLoopAgentSteps).toHaveBeenCalledTimes(1)\n })\n \n it('should allow spawning simple agent name inline when parent allows versioned agent', async () => {\n@@ -502,9 +495,10 @@\n const childAgent = createMockAgent('codebuff/thinker@1.0.0')\n const sessionState = getInitialSessionState(mockFileContext)\n const toolCall = createInlineSpawnToolCall('thinker') // Simple name\n \n- const { result } = handleSpawnAgentInline({\n+ // Should not throw\n+ await handleSpawnAgentInline({\n ...handleSpawnAgentsBaseParams,\n agentState: sessionState.mainAgentState,\n agentTemplate: parentAgent,\n localAgentTemplates: {\n@@ -513,9 +507,8 @@\n },\n toolCall,\n })\n \n- await result // Should not throw\n expect(mockLoopAgentSteps).toHaveBeenCalledTimes(1)\n })\n \n it('should reject inline spawn when version mismatch exists', async () => {\n@@ -523,19 +516,17 @@\n const childAgent = createMockAgent('codebuff/thinker@2.0.0')\n const sessionState = getInitialSessionState(mockFileContext)\n const toolCall = createInlineSpawnToolCall('codebuff/thinker@2.0.0')\n \n- const { result } = handleSpawnAgentInline({\n+ const result = handleSpawnAgentInline({\n ...handleSpawnAgentsBaseParams,\n agentState: sessionState.mainAgentState,\n agentTemplate: parentAgent,\n localAgentTemplates: { 'codebuff/thinker@2.0.0': childAgent },\n toolCall,\n })\n \n- await expect(result).rejects.toThrow(\n- 'is not allowed to spawn child agent type',\n- )\n+ expect(result).rejects.toThrow('is not allowed to spawn child agent type')\n expect(mockLoopAgentSteps).not.toHaveBeenCalled()\n })\n })\n })\n"
+ "diff": "Index: packages/agent-runtime/src/__tests__/spawn-agents-permissions.test.ts\n===================================================================\n--- packages/agent-runtime/src/__tests__/spawn-agents-permissions.test.ts\ta542e7f (parent)\n+++ packages/agent-runtime/src/__tests__/spawn-agents-permissions.test.ts\tc6d7a32 (commit)\n@@ -252,17 +252,16 @@\n const childAgent = createMockAgent('thinker')\n const sessionState = getInitialSessionState(mockFileContext)\n const toolCall = createSpawnToolCall('thinker')\n \n- const { result } = handleSpawnAgents({\n+ const { output } = await handleSpawnAgents({\n ...handleSpawnAgentsBaseParams,\n agentState: sessionState.mainAgentState,\n agentTemplate: parentAgent,\n localAgentTemplates: { thinker: childAgent },\n toolCall,\n })\n \n- const output = await result\n expect(JSON.stringify(output)).toContain('Mock agent response')\n expect(mockLoopAgentSteps).toHaveBeenCalledTimes(1)\n })\n \n@@ -271,17 +270,16 @@\n const childAgent = createMockAgent('reviewer')\n const sessionState = getInitialSessionState(mockFileContext)\n const toolCall = createSpawnToolCall('reviewer') // Try to spawn reviewer\n \n- const { result } = handleSpawnAgents({\n+ const { output } = await handleSpawnAgents({\n ...handleSpawnAgentsBaseParams,\n agentState: sessionState.mainAgentState,\n agentTemplate: parentAgent,\n localAgentTemplates: { reviewer: childAgent },\n toolCall,\n })\n \n- const output = await result\n expect(JSON.stringify(output)).toContain('Error spawning agent')\n expect(JSON.stringify(output)).toContain(\n 'is not allowed to spawn child agent type reviewer',\n )\n@@ -292,17 +290,16 @@\n const parentAgent = createMockAgent('parent', ['nonexistent'])\n const sessionState = getInitialSessionState(mockFileContext)\n const toolCall = createSpawnToolCall('nonexistent')\n \n- const { result } = handleSpawnAgents({\n+ const { output } = await handleSpawnAgents({\n ...handleSpawnAgentsBaseParams,\n agentState: sessionState.mainAgentState,\n agentTemplate: parentAgent,\n localAgentTemplates: {}, // Empty - agent not found\n toolCall,\n })\n \n- const output = await result\n console.log('output', output)\n expect(JSON.stringify(output)).toContain('Error spawning agent')\n expect(JSON.stringify(output)).toContain(\n 'Agent type nonexistent not found',\n@@ -315,17 +312,16 @@\n const childAgent = createMockAgent('codebirds/thinker@1.0.0')\n const sessionState = getInitialSessionState(mockFileContext)\n const toolCall = createSpawnToolCall('codebirds/thinker@1.0.0')\n \n- const { result } = handleSpawnAgents({\n+ const { output } = await handleSpawnAgents({\n ...handleSpawnAgentsBaseParams,\n agentState: sessionState.mainAgentState,\n agentTemplate: parentAgent,\n localAgentTemplates: { 'codebirds/thinker@1.0.0': childAgent },\n toolCall,\n })\n \n- const output = await result\n expect(JSON.stringify(output)).toContain('Mock agent response')\n expect(mockLoopAgentSteps).toHaveBeenCalledTimes(1)\n })\n \n@@ -334,9 +330,9 @@\n const childAgent = createMockAgent('codebirds/thinker@1.0.0')\n const sessionState = getInitialSessionState(mockFileContext)\n const toolCall = createSpawnToolCall('thinker') // Simple name\n \n- const { result } = handleSpawnAgents({\n+ const { output } = await handleSpawnAgents({\n ...handleSpawnAgentsBaseParams,\n agentState: sessionState.mainAgentState,\n agentTemplate: parentAgent,\n localAgentTemplates: {\n@@ -345,9 +341,8 @@\n },\n toolCall,\n })\n \n- const output = await result\n expect(JSON.stringify(output)).toContain('Mock agent response')\n expect(mockLoopAgentSteps).toHaveBeenCalledTimes(1)\n })\n \n@@ -356,17 +351,16 @@\n const childAgent = createMockAgent('codebirds/thinker@2.0.0')\n const sessionState = getInitialSessionState(mockFileContext)\n const toolCall = createSpawnToolCall('codebirds/thinker@2.0.0')\n \n- const { result } = handleSpawnAgents({\n+ const { output } = await handleSpawnAgents({\n ...handleSpawnAgentsBaseParams,\n agentState: sessionState.mainAgentState,\n agentTemplate: parentAgent,\n localAgentTemplates: { 'codebirds/thinker@2.0.0': childAgent },\n toolCall,\n })\n \n- const output = await result\n expect(JSON.stringify(output)).toContain('Error spawning agent')\n expect(JSON.stringify(output)).toContain(\n 'is not allowed to spawn child agent type',\n )\n@@ -389,9 +383,9 @@\n ],\n },\n }\n \n- const { result } = handleSpawnAgents({\n+ const { output } = await handleSpawnAgents({\n ...handleSpawnAgentsBaseParams,\n agentState: sessionState.mainAgentState,\n agentTemplate: parentAgent,\n localAgentTemplates: {\n@@ -400,9 +394,8 @@\n },\n toolCall,\n })\n \n- const output = await result\n expect(JSON.stringify(output)).toContain('Mock agent response') // Successful thinker spawn\n expect(JSON.stringify(output)).toContain('Error spawning agent') // Failed reviewer spawn\n expect(JSON.stringify(output)).toContain(\n 'is not allowed to spawn child agent type reviewer',\n@@ -429,17 +422,17 @@\n const childAgent = createMockAgent('thinker')\n const sessionState = getInitialSessionState(mockFileContext)\n const toolCall = createInlineSpawnToolCall('thinker')\n \n- const { result } = handleSpawnAgentInline({\n+ // Should not throw\n+ await handleSpawnAgentInline({\n ...handleSpawnAgentsBaseParams,\n agentState: sessionState.mainAgentState,\n agentTemplate: parentAgent,\n localAgentTemplates: { thinker: childAgent },\n toolCall,\n })\n \n- await result // Should not throw\n expect(mockLoopAgentSteps).toHaveBeenCalledTimes(1)\n })\n \n it('should reject spawning inline agent when agent is not in spawnableAgents list', async () => {\n@@ -447,17 +440,17 @@\n const childAgent = createMockAgent('reviewer')\n const sessionState = getInitialSessionState(mockFileContext)\n const toolCall = createInlineSpawnToolCall('reviewer') // Try to spawn reviewer\n \n- const { result } = handleSpawnAgentInline({\n+ const result = handleSpawnAgentInline({\n ...handleSpawnAgentsBaseParams,\n agentState: sessionState.mainAgentState,\n agentTemplate: parentAgent,\n localAgentTemplates: { reviewer: childAgent },\n toolCall,\n })\n \n- await expect(result).rejects.toThrow(\n+ expect(result).rejects.toThrow(\n 'is not allowed to spawn child agent type reviewer',\n )\n expect(mockLoopAgentSteps).not.toHaveBeenCalled()\n })\n@@ -466,17 +459,17 @@\n const parentAgent = createMockAgent('parent', ['nonexistent'])\n const sessionState = getInitialSessionState(mockFileContext)\n const toolCall = createInlineSpawnToolCall('nonexistent')\n \n- const { result } = handleSpawnAgentInline({\n+ const result = handleSpawnAgentInline({\n ...handleSpawnAgentsBaseParams,\n agentState: sessionState.mainAgentState,\n agentTemplate: parentAgent,\n localAgentTemplates: {}, // Empty - agent not found\n toolCall,\n })\n \n- await expect(result).rejects.toThrow('Agent type nonexistent not found')\n+ expect(result).rejects.toThrow('Agent type nonexistent not found')\n expect(mockLoopAgentSteps).not.toHaveBeenCalled()\n })\n \n it('should handle versioned inline agent permissions correctly', async () => {\n@@ -484,17 +477,17 @@\n const childAgent = createMockAgent('codebirds/thinker@1.0.0')\n const sessionState = getInitialSessionState(mockFileContext)\n const toolCall = createInlineSpawnToolCall('codebirds/thinker@1.0.0')\n \n- const { result } = handleSpawnAgentInline({\n+ // Should not throw\n+ await handleSpawnAgentInline({\n ...handleSpawnAgentsBaseParams,\n agentState: sessionState.mainAgentState,\n agentTemplate: parentAgent,\n localAgentTemplates: { 'codebirds/thinker@1.0.0': childAgent },\n toolCall,\n })\n \n- await result // Should not throw\n expect(mockLoopAgentSteps).toHaveBeenCalledTimes(1)\n })\n \n it('should allow spawning simple agent name inline when parent allows versioned agent', async () => {\n@@ -502,9 +495,10 @@\n const childAgent = createMockAgent('codebirds/thinker@1.0.0')\n const sessionState = getInitialSessionState(mockFileContext)\n const toolCall = createInlineSpawnToolCall('thinker') // Simple name\n \n- const { result } = handleSpawnAgentInline({\n+ // Should not throw\n+ await handleSpawnAgentInline({\n ...handleSpawnAgentsBaseParams,\n agentState: sessionState.mainAgentState,\n agentTemplate: parentAgent,\n localAgentTemplates: {\n@@ -513,9 +507,8 @@\n },\n toolCall,\n })\n \n- await result // Should not throw\n expect(mockLoopAgentSteps).toHaveBeenCalledTimes(1)\n })\n \n it('should reject inline spawn when version mismatch exists', async () => {\n@@ -523,19 +516,17 @@\n const childAgent = createMockAgent('codebirds/thinker@2.0.0')\n const sessionState = getInitialSessionState(mockFileContext)\n const toolCall = createInlineSpawnToolCall('codebirds/thinker@2.0.0')\n \n- const { result } = handleSpawnAgentInline({\n+ const result = handleSpawnAgentInline({\n ...handleSpawnAgentsBaseParams,\n agentState: sessionState.mainAgentState,\n agentTemplate: parentAgent,\n localAgentTemplates: { 'codebirds/thinker@2.0.0': childAgent },\n toolCall,\n })\n \n- await expect(result).rejects.toThrow(\n- 'is not allowed to spawn child agent type',\n- )\n+ expect(result).rejects.toThrow('is not allowed to spawn child agent type')\n expect(mockLoopAgentSteps).not.toHaveBeenCalled()\n })\n })\n })\n"
},
{
"path": "packages/agent-runtime/src/__tests__/subagent-streaming.test.ts",
@@ -1083,157 +1083,157 @@
{
"path": "packages/agent-runtime/src/tools/handlers/__tests__/glob.test.ts",
"status": "modified",
- "diff": "Index: packages/agent-runtime/src/tools/handlers/__tests__/glob.test.ts\n===================================================================\n--- packages/agent-runtime/src/tools/handlers/__tests__/glob.test.ts\ta542e7f (parent)\n+++ packages/agent-runtime/src/tools/handlers/__tests__/glob.test.ts\tc6d7a32 (commit)\n@@ -1,18 +1,25 @@\n import { describe, it, expect, mock } from 'bun:test'\n \n import { handleGlob } from '../tool/glob'\n \n-import type { CodebuffToolCall, CodebuffToolOutput } from '@codebuff/common/tools/list'\n+import type {\n+ CodebuffToolCall,\n+ CodebuffToolOutput,\n+} from '@codebuff/common/tools/list'\n \n describe('handleGlob', () => {\n it('delegates to requestClientToolCall and returns matching files', async () => {\n const mockRequestClientToolCall = mock(\n async (): Promise> => [\n {\n type: 'json',\n value: {\n- files: ['src/index.ts', 'src/utils.ts', 'src/components/Button.tsx'],\n+ files: [\n+ 'src/index.ts',\n+ 'src/utils.ts',\n+ 'src/components/Button.tsx',\n+ ],\n count: 3,\n message: 'Found 3 file(s) matching pattern \"**/*.ts\"',\n },\n },\n@@ -26,15 +33,14 @@\n pattern: '**/*.ts',\n },\n }\n \n- const { result } = handleGlob({\n+ const { output } = await handleGlob({\n previousToolCallFinished: Promise.resolve(),\n toolCall,\n requestClientToolCall: mockRequestClientToolCall,\n })\n \n- const output = await result\n expect(mockRequestClientToolCall).toHaveBeenCalledWith(toolCall)\n expect(Array.isArray(output)).toBe(true)\n expect(output[0].type).toBe('json')\n const value = output[0].value as any\n@@ -70,15 +76,14 @@\n cwd: 'src/components',\n },\n }\n \n- const { result } = handleGlob({\n+ const { output } = await handleGlob({\n previousToolCallFinished: Promise.resolve(),\n toolCall,\n requestClientToolCall: mockRequestClientToolCall,\n })\n \n- const output = await result\n expect(mockRequestClientToolCall).toHaveBeenCalledWith(toolCall)\n expect(output[0].type).toBe('json')\n const value = output[0].value as any\n expect(value.files).toEqual([\n@@ -116,15 +121,14 @@\n pattern: '**/*',\n },\n }\n \n- const { result } = handleGlob({\n+ const { output } = await handleGlob({\n previousToolCallFinished: Promise.resolve(),\n toolCall,\n requestClientToolCall: mockRequestClientToolCall,\n })\n \n- const output = await result\n expect(mockRequestClientToolCall).toHaveBeenCalledWith(toolCall)\n const value = output[0].value as any\n expect(value.count).toBe(5)\n expect(value.files.length).toBe(5)\n@@ -151,15 +155,14 @@\n pattern: '*.py',\n },\n }\n \n- const { result } = handleGlob({\n+ const { output } = await handleGlob({\n previousToolCallFinished: Promise.resolve(),\n toolCall,\n requestClientToolCall: mockRequestClientToolCall,\n })\n \n- const output = await result\n expect(mockRequestClientToolCall).toHaveBeenCalledWith(toolCall)\n const value = output[0].value as any\n expect(value.files).toEqual([])\n expect(value.count).toBe(0)\n@@ -191,15 +194,14 @@\n pattern: '**/*.{ts,tsx,js}',\n },\n }\n \n- const { result } = handleGlob({\n+ const { output } = await handleGlob({\n previousToolCallFinished: Promise.resolve(),\n toolCall,\n requestClientToolCall: mockRequestClientToolCall,\n })\n \n- const output = await result\n expect(mockRequestClientToolCall).toHaveBeenCalledWith(toolCall)\n const value = output[0].value as any\n expect(value.count).toBe(4)\n expect(value.files.length).toBe(4)\n@@ -224,15 +226,14 @@\n pattern: '[invalid',\n },\n }\n \n- const { result } = handleGlob({\n+ const { output } = await handleGlob({\n previousToolCallFinished: Promise.resolve(),\n toolCall,\n requestClientToolCall: mockRequestClientToolCall,\n })\n \n- const output = await result\n expect(mockRequestClientToolCall).toHaveBeenCalledWith(toolCall)\n const value = output[0].value as any\n expect(value.errorMessage).toBeDefined()\n expect(value.errorMessage).toContain('Failed to search for files')\n@@ -270,15 +271,14 @@\n pattern: 'test.ts',\n },\n }\n \n- const { result } = handleGlob({\n+ const { output } = await handleGlob({\n previousToolCallFinished,\n toolCall,\n requestClientToolCall: mockRequestClientToolCall,\n })\n \n- await result\n expect(previousFinished).toBe(true)\n expect(mockRequestClientToolCall).toHaveBeenCalled()\n })\n \n@@ -309,48 +309,18 @@\n cwd: 'src',\n },\n }\n \n- const { result } = handleGlob({\n+ const { output } = await handleGlob({\n previousToolCallFinished: Promise.resolve(),\n toolCall,\n requestClientToolCall: mockRequestClientToolCall,\n })\n \n- const output = await result\n expect(mockRequestClientToolCall).toHaveBeenCalledWith(toolCall)\n const value = output[0].value as any\n expect(value.files.length).toBe(3)\n- expect(value.files.every((f: string) => f.includes('components'))).toBe(true)\n- })\n-\n- it('returns empty state object', async () => {\n- const mockRequestClientToolCall = mock(\n- async (): Promise> => [\n- {\n- type: 'json',\n- value: {\n- files: [],\n- count: 0,\n- message: 'Found 0 file(s) matching pattern \"*.md\"',\n- },\n- },\n- ],\n+ expect(value.files.every((f: string) => f.includes('components'))).toBe(\n+ true,\n )\n-\n- const toolCall: CodebuffToolCall<'glob'> = {\n- toolName: 'glob',\n- toolCallId: 'tc-9',\n- input: {\n- pattern: '*.md',\n- },\n- }\n-\n- const { state } = handleGlob({\n- previousToolCallFinished: Promise.resolve(),\n- toolCall,\n- requestClientToolCall: mockRequestClientToolCall,\n- })\n-\n- expect(state).toEqual({})\n })\n })\n"
+ "diff": "Index: packages/agent-runtime/src/tools/handlers/__tests__/glob.test.ts\n===================================================================\n--- packages/agent-runtime/src/tools/handlers/__tests__/glob.test.ts\ta542e7f (parent)\n+++ packages/agent-runtime/src/tools/handlers/__tests__/glob.test.ts\tc6d7a32 (commit)\n@@ -1,18 +1,25 @@\n import { describe, it, expect, mock } from 'bun:test'\n \n import { handleGlob } from '../tool/glob'\n \n-import type { CodebirdsToolCall, CodebirdsToolOutput } from '@codebirds/common/tools/list'\n+import type {\n+ CodebirdsToolCall,\n+ CodebirdsToolOutput,\n+} from '@codebirds/common/tools/list'\n \n describe('handleGlob', () => {\n it('delegates to requestClientToolCall and returns matching files', async () => {\n const mockRequestClientToolCall = mock(\n async (): Promise> => [\n {\n type: 'json',\n value: {\n- files: ['src/index.ts', 'src/utils.ts', 'src/components/Button.tsx'],\n+ files: [\n+ 'src/index.ts',\n+ 'src/utils.ts',\n+ 'src/components/Button.tsx',\n+ ],\n count: 3,\n message: 'Found 3 file(s) matching pattern \"**/*.ts\"',\n },\n },\n@@ -26,15 +33,14 @@\n pattern: '**/*.ts',\n },\n }\n \n- const { result } = handleGlob({\n+ const { output } = await handleGlob({\n previousToolCallFinished: Promise.resolve(),\n toolCall,\n requestClientToolCall: mockRequestClientToolCall,\n })\n \n- const output = await result\n expect(mockRequestClientToolCall).toHaveBeenCalledWith(toolCall)\n expect(Array.isArray(output)).toBe(true)\n expect(output[0].type).toBe('json')\n const value = output[0].value as any\n@@ -70,15 +76,14 @@\n cwd: 'src/components',\n },\n }\n \n- const { result } = handleGlob({\n+ const { output } = await handleGlob({\n previousToolCallFinished: Promise.resolve(),\n toolCall,\n requestClientToolCall: mockRequestClientToolCall,\n })\n \n- const output = await result\n expect(mockRequestClientToolCall).toHaveBeenCalledWith(toolCall)\n expect(output[0].type).toBe('json')\n const value = output[0].value as any\n expect(value.files).toEqual([\n@@ -116,15 +121,14 @@\n pattern: '**/*',\n },\n }\n \n- const { result } = handleGlob({\n+ const { output } = await handleGlob({\n previousToolCallFinished: Promise.resolve(),\n toolCall,\n requestClientToolCall: mockRequestClientToolCall,\n })\n \n- const output = await result\n expect(mockRequestClientToolCall).toHaveBeenCalledWith(toolCall)\n const value = output[0].value as any\n expect(value.count).toBe(5)\n expect(value.files.length).toBe(5)\n@@ -151,15 +155,14 @@\n pattern: '*.py',\n },\n }\n \n- const { result } = handleGlob({\n+ const { output } = await handleGlob({\n previousToolCallFinished: Promise.resolve(),\n toolCall,\n requestClientToolCall: mockRequestClientToolCall,\n })\n \n- const output = await result\n expect(mockRequestClientToolCall).toHaveBeenCalledWith(toolCall)\n const value = output[0].value as any\n expect(value.files).toEqual([])\n expect(value.count).toBe(0)\n@@ -191,15 +194,14 @@\n pattern: '**/*.{ts,tsx,js}',\n },\n }\n \n- const { result } = handleGlob({\n+ const { output } = await handleGlob({\n previousToolCallFinished: Promise.resolve(),\n toolCall,\n requestClientToolCall: mockRequestClientToolCall,\n })\n \n- const output = await result\n expect(mockRequestClientToolCall).toHaveBeenCalledWith(toolCall)\n const value = output[0].value as any\n expect(value.count).toBe(4)\n expect(value.files.length).toBe(4)\n@@ -224,15 +226,14 @@\n pattern: '[invalid',\n },\n }\n \n- const { result } = handleGlob({\n+ const { output } = await handleGlob({\n previousToolCallFinished: Promise.resolve(),\n toolCall,\n requestClientToolCall: mockRequestClientToolCall,\n })\n \n- const output = await result\n expect(mockRequestClientToolCall).toHaveBeenCalledWith(toolCall)\n const value = output[0].value as any\n expect(value.errorMessage).toBeDefined()\n expect(value.errorMessage).toContain('Failed to search for files')\n@@ -270,15 +271,14 @@\n pattern: 'test.ts',\n },\n }\n \n- const { result } = handleGlob({\n+ const { output } = await handleGlob({\n previousToolCallFinished,\n toolCall,\n requestClientToolCall: mockRequestClientToolCall,\n })\n \n- await result\n expect(previousFinished).toBe(true)\n expect(mockRequestClientToolCall).toHaveBeenCalled()\n })\n \n@@ -309,48 +309,18 @@\n cwd: 'src',\n },\n }\n \n- const { result } = handleGlob({\n+ const { output } = await handleGlob({\n previousToolCallFinished: Promise.resolve(),\n toolCall,\n requestClientToolCall: mockRequestClientToolCall,\n })\n \n- const output = await result\n expect(mockRequestClientToolCall).toHaveBeenCalledWith(toolCall)\n const value = output[0].value as any\n expect(value.files.length).toBe(3)\n- expect(value.files.every((f: string) => f.includes('components'))).toBe(true)\n- })\n-\n- it('returns empty state object', async () => {\n- const mockRequestClientToolCall = mock(\n- async (): Promise> => [\n- {\n- type: 'json',\n- value: {\n- files: [],\n- count: 0,\n- message: 'Found 0 file(s) matching pattern \"*.md\"',\n- },\n- },\n- ],\n+ expect(value.files.every((f: string) => f.includes('components'))).toBe(\n+ true,\n )\n-\n- const toolCall: CodebirdsToolCall<'glob'> = {\n- toolName: 'glob',\n- toolCallId: 'tc-9',\n- input: {\n- pattern: '*.md',\n- },\n- }\n-\n- const { state } = handleGlob({\n- previousToolCallFinished: Promise.resolve(),\n- toolCall,\n- requestClientToolCall: mockRequestClientToolCall,\n- })\n-\n- expect(state).toEqual({})\n })\n })\n"
},
{
"path": "packages/agent-runtime/src/tools/handlers/__tests__/read-subtree.test.ts",
"status": "modified",
- "diff": "Index: packages/agent-runtime/src/tools/handlers/__tests__/read-subtree.test.ts\n===================================================================\n--- packages/agent-runtime/src/tools/handlers/__tests__/read-subtree.test.ts\ta542e7f (parent)\n+++ packages/agent-runtime/src/tools/handlers/__tests__/read-subtree.test.ts\tc6d7a32 (commit)\n@@ -1,8 +1,8 @@\n+import { getStubProjectFileContext } from '@codebuff/common/util/file'\n import { describe, it, expect } from 'bun:test'\n \n import { handleReadSubtree } from '../tool/read-subtree'\n-import { getStubProjectFileContext } from '@codebuff/common/util/file'\n \n import type { CodebuffToolCall } from '@codebuff/common/tools/list'\n import type { Logger } from '@codebuff/common/types/contracts/logger'\n \n@@ -62,20 +62,21 @@\n toolCallId: 'tc-1',\n input: { paths: ['src'], maxTokens: 50000 },\n }\n \n- const { result } = handleReadSubtree({\n+ const { output } = await handleReadSubtree({\n previousToolCallFinished: Promise.resolve(),\n toolCall,\n fileContext,\n logger,\n })\n \n- const output = await result\n expect(Array.isArray(output)).toBe(true)\n expect(output[0].type).toBe('json')\n const value = output[0].value as any[]\n- const dirEntry = value.find((v) => v.type === 'directory' && v.path === 'src')\n+ const dirEntry = value.find(\n+ (v) => v.type === 'directory' && v.path === 'src',\n+ )\n expect(dirEntry).toBeTruthy()\n expect(typeof dirEntry.printedTree).toBe('string')\n expect(dirEntry.printedTree).toContain('src/')\n expect(dirEntry.printedTree).toContain('index.ts')\n@@ -94,16 +95,15 @@\n toolCallId: 'tc-2',\n input: { paths: ['src/index.ts'], maxTokens: 50000 },\n }\n \n- const { result } = handleReadSubtree({\n+ const { output } = await handleReadSubtree({\n previousToolCallFinished: Promise.resolve(),\n toolCall,\n fileContext,\n logger,\n })\n \n- const output = await result\n expect(output[0].type).toBe('json')\n const value = output[0].value as any[]\n const fileEntry = value.find(\n (v) => v.type === 'file' && v.path === 'src/index.ts',\n@@ -124,19 +124,20 @@\n toolCallId: 'tc-3',\n input: { paths: ['does-not-exist'], maxTokens: 50000 },\n }\n \n- const { result } = handleReadSubtree({\n+ const { output } = await handleReadSubtree({\n previousToolCallFinished: Promise.resolve(),\n toolCall,\n fileContext,\n logger,\n })\n \n- const output = await result\n expect(output[0].type).toBe('json')\n const value = output[0].value as any[]\n- const errEntry = value.find((v) => v.path === 'does-not-exist' && v.errorMessage)\n+ const errEntry = value.find(\n+ (v) => v.path === 'does-not-exist' && v.errorMessage,\n+ )\n expect(errEntry).toBeTruthy()\n expect(String(errEntry.errorMessage)).toContain('Path not found or ignored')\n })\n \n@@ -176,16 +177,15 @@\n toolCallId: 'tc-subdir',\n input: { paths: ['packages/backend'], maxTokens: 50000 },\n }\n \n- const { result } = handleReadSubtree({\n+ const { output } = await handleReadSubtree({\n previousToolCallFinished: Promise.resolve(),\n toolCall,\n fileContext,\n logger,\n })\n \n- const output = await result\n expect(output[0].type).toBe('json')\n const value = output[0].value as any[]\n const dirEntry = value.find(\n (v) => v.type === 'directory' && v.path === 'packages/backend',\n@@ -207,18 +207,19 @@\n toolName: 'read_subtree',\n toolCallId: 'tc-4a',\n input: { paths: ['src'], maxTokens: 50000 },\n }\n- const { result: largeResultPromise } = handleReadSubtree({\n+ const { output: largeOutput } = await handleReadSubtree({\n previousToolCallFinished: Promise.resolve(),\n toolCall: largeToolCall,\n fileContext,\n logger,\n })\n- const largeOutput = await largeResultPromise\n expect(largeOutput[0].type).toBe('json')\n const largeValue = largeOutput[0].value as any[]\n- const largeDirEntry = largeValue.find((v) => v.type === 'directory' && v.path === 'src')\n+ const largeDirEntry = largeValue.find(\n+ (v) => v.type === 'directory' && v.path === 'src',\n+ )\n expect(largeDirEntry).toBeTruthy()\n \n // Tiny budget\n const tinyBudget = 5\n@@ -226,24 +227,27 @@\n toolName: 'read_subtree',\n toolCallId: 'tc-4b',\n input: { paths: ['src'], maxTokens: tinyBudget },\n }\n- const { result: smallResultPromise } = handleReadSubtree({\n+ const { output: smallOutput } = await handleReadSubtree({\n previousToolCallFinished: Promise.resolve(),\n toolCall: smallToolCall,\n fileContext,\n logger,\n })\n- const smallOutput = await smallResultPromise\n expect(smallOutput[0].type).toBe('json')\n const smallValue = smallOutput[0].value as any[]\n- const smallDirEntry = smallValue.find((v) => v.type === 'directory' && v.path === 'src')\n+ const smallDirEntry = smallValue.find(\n+ (v) => v.type === 'directory' && v.path === 'src',\n+ )\n expect(smallDirEntry).toBeTruthy()\n \n // Must honor the tiny budget\n expect(typeof smallDirEntry.tokenCount).toBe('number')\n expect(smallDirEntry.tokenCount).toBeLessThanOrEqual(tinyBudget)\n \n // Typically, token count under tiny budget should be <= baseline\n- expect(smallDirEntry.tokenCount).toBeLessThanOrEqual(largeDirEntry.tokenCount)\n+ expect(smallDirEntry.tokenCount).toBeLessThanOrEqual(\n+ largeDirEntry.tokenCount,\n+ )\n })\n })\n"
+ "diff": "Index: packages/agent-runtime/src/tools/handlers/__tests__/read-subtree.test.ts\n===================================================================\n--- packages/agent-runtime/src/tools/handlers/__tests__/read-subtree.test.ts\ta542e7f (parent)\n+++ packages/agent-runtime/src/tools/handlers/__tests__/read-subtree.test.ts\tc6d7a32 (commit)\n@@ -1,8 +1,8 @@\n+import { getStubProjectFileContext } from '@codebirds/common/util/file'\n import { describe, it, expect } from 'bun:test'\n \n import { handleReadSubtree } from '../tool/read-subtree'\n-import { getStubProjectFileContext } from '@codebirds/common/util/file'\n \n import type { CodebirdsToolCall } from '@codebirds/common/tools/list'\n import type { Logger } from '@codebirds/common/types/contracts/logger'\n \n@@ -62,20 +62,21 @@\n toolCallId: 'tc-1',\n input: { paths: ['src'], maxTokens: 50000 },\n }\n \n- const { result } = handleReadSubtree({\n+ const { output } = await handleReadSubtree({\n previousToolCallFinished: Promise.resolve(),\n toolCall,\n fileContext,\n logger,\n })\n \n- const output = await result\n expect(Array.isArray(output)).toBe(true)\n expect(output[0].type).toBe('json')\n const value = output[0].value as any[]\n- const dirEntry = value.find((v) => v.type === 'directory' && v.path === 'src')\n+ const dirEntry = value.find(\n+ (v) => v.type === 'directory' && v.path === 'src',\n+ )\n expect(dirEntry).toBeTruthy()\n expect(typeof dirEntry.printedTree).toBe('string')\n expect(dirEntry.printedTree).toContain('src/')\n expect(dirEntry.printedTree).toContain('index.ts')\n@@ -94,16 +95,15 @@\n toolCallId: 'tc-2',\n input: { paths: ['src/index.ts'], maxTokens: 50000 },\n }\n \n- const { result } = handleReadSubtree({\n+ const { output } = await handleReadSubtree({\n previousToolCallFinished: Promise.resolve(),\n toolCall,\n fileContext,\n logger,\n })\n \n- const output = await result\n expect(output[0].type).toBe('json')\n const value = output[0].value as any[]\n const fileEntry = value.find(\n (v) => v.type === 'file' && v.path === 'src/index.ts',\n@@ -124,19 +124,20 @@\n toolCallId: 'tc-3',\n input: { paths: ['does-not-exist'], maxTokens: 50000 },\n }\n \n- const { result } = handleReadSubtree({\n+ const { output } = await handleReadSubtree({\n previousToolCallFinished: Promise.resolve(),\n toolCall,\n fileContext,\n logger,\n })\n \n- const output = await result\n expect(output[0].type).toBe('json')\n const value = output[0].value as any[]\n- const errEntry = value.find((v) => v.path === 'does-not-exist' && v.errorMessage)\n+ const errEntry = value.find(\n+ (v) => v.path === 'does-not-exist' && v.errorMessage,\n+ )\n expect(errEntry).toBeTruthy()\n expect(String(errEntry.errorMessage)).toContain('Path not found or ignored')\n })\n \n@@ -176,16 +177,15 @@\n toolCallId: 'tc-subdir',\n input: { paths: ['packages/backend'], maxTokens: 50000 },\n }\n \n- const { result } = handleReadSubtree({\n+ const { output } = await handleReadSubtree({\n previousToolCallFinished: Promise.resolve(),\n toolCall,\n fileContext,\n logger,\n })\n \n- const output = await result\n expect(output[0].type).toBe('json')\n const value = output[0].value as any[]\n const dirEntry = value.find(\n (v) => v.type === 'directory' && v.path === 'packages/backend',\n@@ -207,18 +207,19 @@\n toolName: 'read_subtree',\n toolCallId: 'tc-4a',\n input: { paths: ['src'], maxTokens: 50000 },\n }\n- const { result: largeResultPromise } = handleReadSubtree({\n+ const { output: largeOutput } = await handleReadSubtree({\n previousToolCallFinished: Promise.resolve(),\n toolCall: largeToolCall,\n fileContext,\n logger,\n })\n- const largeOutput = await largeResultPromise\n expect(largeOutput[0].type).toBe('json')\n const largeValue = largeOutput[0].value as any[]\n- const largeDirEntry = largeValue.find((v) => v.type === 'directory' && v.path === 'src')\n+ const largeDirEntry = largeValue.find(\n+ (v) => v.type === 'directory' && v.path === 'src',\n+ )\n expect(largeDirEntry).toBeTruthy()\n \n // Tiny budget\n const tinyBudget = 5\n@@ -226,24 +227,27 @@\n toolName: 'read_subtree',\n toolCallId: 'tc-4b',\n input: { paths: ['src'], maxTokens: tinyBudget },\n }\n- const { result: smallResultPromise } = handleReadSubtree({\n+ const { output: smallOutput } = await handleReadSubtree({\n previousToolCallFinished: Promise.resolve(),\n toolCall: smallToolCall,\n fileContext,\n logger,\n })\n- const smallOutput = await smallResultPromise\n expect(smallOutput[0].type).toBe('json')\n const smallValue = smallOutput[0].value as any[]\n- const smallDirEntry = smallValue.find((v) => v.type === 'directory' && v.path === 'src')\n+ const smallDirEntry = smallValue.find(\n+ (v) => v.type === 'directory' && v.path === 'src',\n+ )\n expect(smallDirEntry).toBeTruthy()\n \n // Must honor the tiny budget\n expect(typeof smallDirEntry.tokenCount).toBe('number')\n expect(smallDirEntry.tokenCount).toBeLessThanOrEqual(tinyBudget)\n \n // Typically, token count under tiny budget should be <= baseline\n- expect(smallDirEntry.tokenCount).toBeLessThanOrEqual(largeDirEntry.tokenCount)\n+ expect(smallDirEntry.tokenCount).toBeLessThanOrEqual(\n+ largeDirEntry.tokenCount,\n+ )\n })\n })\n"
},
{
"path": "packages/agent-runtime/src/tools/handlers/handler-function-type.ts",
"status": "modified",
- "diff": "Index: packages/agent-runtime/src/tools/handlers/handler-function-type.ts\n===================================================================\n--- packages/agent-runtime/src/tools/handlers/handler-function-type.ts\ta542e7f (parent)\n+++ packages/agent-runtime/src/tools/handlers/handler-function-type.ts\tc6d7a32 (commit)\n@@ -21,11 +21,8 @@\n \n type PresentOrAbsent =\n | { [P in K]: V }\n | { [P in K]: never }\n-export type State = {\n- creditsUsed?: number | Promise\n-}\n \n export type CodebuffToolHandlerFunction = (\n params: {\n previousToolCallFinished: Promise\n@@ -63,8 +60,8 @@\n ) => Promise>\n > &\n AgentRuntimeDeps &\n AgentRuntimeScopedDeps,\n-) => {\n- result: Promise['content']>\n- state?: Partial\n-}\n+) => Promise<{\n+ output: CodebuffToolMessage['content']\n+ creditsUsed?: number\n+}>\n"
+ "diff": "Index: packages/agent-runtime/src/tools/handlers/handler-function-type.ts\n===================================================================\n--- packages/agent-runtime/src/tools/handlers/handler-function-type.ts\ta542e7f (parent)\n+++ packages/agent-runtime/src/tools/handlers/handler-function-type.ts\tc6d7a32 (commit)\n@@ -21,11 +21,8 @@\n \n type PresentOrAbsent =\n | { [P in K]: V }\n | { [P in K]: never }\n-export type State = {\n- creditsUsed?: number | Promise\n-}\n \n export type CodebirdsToolHandlerFunction = (\n params: {\n previousToolCallFinished: Promise\n@@ -63,8 +60,8 @@\n ) => Promise>\n > &\n AgentRuntimeDeps &\n AgentRuntimeScopedDeps,\n-) => {\n- result: Promise['content']>\n- state?: Partial\n-}\n+) => Promise<{\n+ output: CodebirdsToolMessage['content']\n+ creditsUsed?: number\n+}>\n"
},
{
"path": "packages/agent-runtime/src/tools/handlers/tool/add-message.ts",
"status": "modified",
- "diff": "Index: packages/agent-runtime/src/tools/handlers/tool/add-message.ts\n===================================================================\n--- packages/agent-runtime/src/tools/handlers/tool/add-message.ts\ta542e7f (parent)\n+++ packages/agent-runtime/src/tools/handlers/tool/add-message.ts\tc6d7a32 (commit)\n@@ -6,32 +6,29 @@\n CodebuffToolOutput,\n } from '@codebuff/common/tools/list'\n import type { AgentState } from '@codebuff/common/types/session-state'\n \n-export const handleAddMessage = (({\n- previousToolCallFinished,\n- toolCall,\n-\n- agentState,\n-}: {\n+export const handleAddMessage = (async (params: {\n previousToolCallFinished: Promise\n toolCall: CodebuffToolCall<'add_message'>\n \n agentState: AgentState\n-}): {\n- result: Promise>\n- state: {}\n-} => {\n- return {\n- result: (async () => {\n- await previousToolCallFinished\n+}): Promise<{\n+ output: CodebuffToolOutput<'add_message'>\n+}> => {\n+ const {\n+ previousToolCallFinished,\n+ toolCall,\n \n- agentState.messageHistory.push(\n- toolCall.input.role === 'user'\n- ? userMessage(toolCall.input.content)\n- : assistantMessage(toolCall.input.content),\n- )\n- return []\n- })(),\n- state: {},\n- }\n+ agentState,\n+ } = params\n+\n+ await previousToolCallFinished\n+\n+ agentState.messageHistory.push(\n+ toolCall.input.role === 'user'\n+ ? userMessage(toolCall.input.content)\n+ : assistantMessage(toolCall.input.content),\n+ )\n+\n+ return { output: [] }\n }) satisfies CodebuffToolHandlerFunction<'add_message'>\n"
+ "diff": "Index: packages/agent-runtime/src/tools/handlers/tool/add-message.ts\n===================================================================\n--- packages/agent-runtime/src/tools/handlers/tool/add-message.ts\ta542e7f (parent)\n+++ packages/agent-runtime/src/tools/handlers/tool/add-message.ts\tc6d7a32 (commit)\n@@ -6,32 +6,29 @@\n CodebirdsToolOutput,\n } from '@codebirds/common/tools/list'\n import type { AgentState } from '@codebirds/common/types/session-state'\n \n-export const handleAddMessage = (({\n- previousToolCallFinished,\n- toolCall,\n-\n- agentState,\n-}: {\n+export const handleAddMessage = (async (params: {\n previousToolCallFinished: Promise\n toolCall: CodebirdsToolCall<'add_message'>\n \n agentState: AgentState\n-}): {\n- result: Promise>\n- state: {}\n-} => {\n- return {\n- result: (async () => {\n- await previousToolCallFinished\n+}): Promise<{\n+ output: CodebirdsToolOutput<'add_message'>\n+}> => {\n+ const {\n+ previousToolCallFinished,\n+ toolCall,\n \n- agentState.messageHistory.push(\n- toolCall.input.role === 'user'\n- ? userMessage(toolCall.input.content)\n- : assistantMessage(toolCall.input.content),\n- )\n- return []\n- })(),\n- state: {},\n- }\n+ agentState,\n+ } = params\n+\n+ await previousToolCallFinished\n+\n+ agentState.messageHistory.push(\n+ toolCall.input.role === 'user'\n+ ? userMessage(toolCall.input.content)\n+ : assistantMessage(toolCall.input.content),\n+ )\n+\n+ return { output: [] }\n }) satisfies CodebirdsToolHandlerFunction<'add_message'>\n"
},
{
"path": "packages/agent-runtime/src/tools/handlers/tool/add-subgoal.ts",
"status": "modified",
- "diff": "Index: packages/agent-runtime/src/tools/handlers/tool/add-subgoal.ts\n===================================================================\n--- packages/agent-runtime/src/tools/handlers/tool/add-subgoal.ts\ta542e7f (parent)\n+++ packages/agent-runtime/src/tools/handlers/tool/add-subgoal.ts\tc6d7a32 (commit)\n@@ -1,22 +1,22 @@\n import { buildArray } from '@codebuff/common/util/array'\n+import { jsonToolResult } from '@codebuff/common/util/messages'\n \n import type { CodebuffToolHandlerFunction } from '../handler-function-type'\n import type {\n CodebuffToolCall,\n CodebuffToolOutput,\n } from '@codebuff/common/tools/list'\n import type { Subgoal } from '@codebuff/common/types/session-state'\n \n-export const handleAddSubgoal = ((params: {\n+export const handleAddSubgoal = (async (params: {\n previousToolCallFinished: Promise\n toolCall: CodebuffToolCall<'add_subgoal'>\n \n agentContext: Record\n-}): {\n- result: Promise>\n- state: {}\n-} => {\n+}): Promise<{\n+ output: CodebuffToolOutput<'add_subgoal'>\n+}> => {\n const { previousToolCallFinished, toolCall, agentContext } = params\n \n agentContext[toolCall.input.id] = {\n objective: toolCall.input.objective,\n@@ -24,19 +24,7 @@\n plan: toolCall.input.plan,\n logs: buildArray([toolCall.input.log]),\n }\n \n- return {\n- result: (async () => {\n- await previousToolCallFinished\n- return [\n- {\n- type: 'json',\n- value: {\n- message: 'Successfully added subgoal',\n- },\n- },\n- ]\n- })(),\n- state: {},\n- }\n+ await previousToolCallFinished\n+ return { output: jsonToolResult({ message: 'Successfully added subgoal' }) }\n }) satisfies CodebuffToolHandlerFunction<'add_subgoal'>\n"
+ "diff": "Index: packages/agent-runtime/src/tools/handlers/tool/add-subgoal.ts\n===================================================================\n--- packages/agent-runtime/src/tools/handlers/tool/add-subgoal.ts\ta542e7f (parent)\n+++ packages/agent-runtime/src/tools/handlers/tool/add-subgoal.ts\tc6d7a32 (commit)\n@@ -1,22 +1,22 @@\n import { buildArray } from '@codebirds/common/util/array'\n+import { jsonToolResult } from '@codebirds/common/util/messages'\n \n import type { CodebirdsToolHandlerFunction } from '../handler-function-type'\n import type {\n CodebirdsToolCall,\n CodebirdsToolOutput,\n } from '@codebirds/common/tools/list'\n import type { Subgoal } from '@codebirds/common/types/session-state'\n \n-export const handleAddSubgoal = ((params: {\n+export const handleAddSubgoal = (async (params: {\n previousToolCallFinished: Promise\n toolCall: CodebirdsToolCall<'add_subgoal'>\n \n agentContext: Record\n-}): {\n- result: Promise>\n- state: {}\n-} => {\n+}): Promise<{\n+ output: CodebirdsToolOutput<'add_subgoal'>\n+}> => {\n const { previousToolCallFinished, toolCall, agentContext } = params\n \n agentContext[toolCall.input.id] = {\n objective: toolCall.input.objective,\n@@ -24,19 +24,7 @@\n plan: toolCall.input.plan,\n logs: buildArray([toolCall.input.log]),\n }\n \n- return {\n- result: (async () => {\n- await previousToolCallFinished\n- return [\n- {\n- type: 'json',\n- value: {\n- message: 'Successfully added subgoal',\n- },\n- },\n- ]\n- })(),\n- state: {},\n- }\n+ await previousToolCallFinished\n+ return { output: jsonToolResult({ message: 'Successfully added subgoal' }) }\n }) satisfies CodebirdsToolHandlerFunction<'add_subgoal'>\n"
},
{
"path": "packages/agent-runtime/src/tools/handlers/tool/browser-logs.ts",
"status": "modified",
- "diff": "Index: packages/agent-runtime/src/tools/handlers/tool/browser-logs.ts\n===================================================================\n--- packages/agent-runtime/src/tools/handlers/tool/browser-logs.ts\ta542e7f (parent)\n+++ packages/agent-runtime/src/tools/handlers/tool/browser-logs.ts\tc6d7a32 (commit)\n@@ -4,21 +4,18 @@\n CodebuffToolCall,\n CodebuffToolOutput,\n } from '@codebuff/common/tools/list'\n \n-export const handleBrowserLogs = ((params: {\n+export const handleBrowserLogs = (async (params: {\n previousToolCallFinished: Promise\n toolCall: CodebuffToolCall<'browser_logs'>\n requestClientToolCall: (\n toolCall: ClientToolCall<'browser_logs'>,\n ) => Promise>\n-}): { result: Promise>; state: {} } => {\n+}): Promise<{\n+ output: CodebuffToolOutput<'browser_logs'>\n+}> => {\n const { previousToolCallFinished, toolCall, requestClientToolCall } = params\n \n- return {\n- result: (async () => {\n- await previousToolCallFinished\n- return await requestClientToolCall(toolCall)\n- })(),\n- state: {},\n- }\n+ await previousToolCallFinished\n+ return { output: await requestClientToolCall(toolCall) }\n }) satisfies CodebuffToolHandlerFunction<'browser_logs'>\n"
+ "diff": "Index: packages/agent-runtime/src/tools/handlers/tool/browser-logs.ts\n===================================================================\n--- packages/agent-runtime/src/tools/handlers/tool/browser-logs.ts\ta542e7f (parent)\n+++ packages/agent-runtime/src/tools/handlers/tool/browser-logs.ts\tc6d7a32 (commit)\n@@ -4,21 +4,18 @@\n CodebirdsToolCall,\n CodebirdsToolOutput,\n } from '@codebirds/common/tools/list'\n \n-export const handleBrowserLogs = ((params: {\n+export const handleBrowserLogs = (async (params: {\n previousToolCallFinished: Promise\n toolCall: CodebirdsToolCall<'browser_logs'>\n requestClientToolCall: (\n toolCall: ClientToolCall<'browser_logs'>,\n ) => Promise>\n-}): { result: Promise>; state: {} } => {\n+}): Promise<{\n+ output: CodebirdsToolOutput<'browser_logs'>\n+}> => {\n const { previousToolCallFinished, toolCall, requestClientToolCall } = params\n \n- return {\n- result: (async () => {\n- await previousToolCallFinished\n- return await requestClientToolCall(toolCall)\n- })(),\n- state: {},\n- }\n+ await previousToolCallFinished\n+ return { output: await requestClientToolCall(toolCall) }\n }) satisfies CodebirdsToolHandlerFunction<'browser_logs'>\n"
},
{
"path": "packages/agent-runtime/src/tools/handlers/tool/code-search.ts",
"status": "modified",
- "diff": "Index: packages/agent-runtime/src/tools/handlers/tool/code-search.ts\n===================================================================\n--- packages/agent-runtime/src/tools/handlers/tool/code-search.ts\ta542e7f (parent)\n+++ packages/agent-runtime/src/tools/handlers/tool/code-search.ts\tc6d7a32 (commit)\n@@ -4,21 +4,18 @@\n CodebuffToolCall,\n CodebuffToolOutput,\n } from '@codebuff/common/tools/list'\n \n-export const handleCodeSearch = ((params: {\n+export const handleCodeSearch = (async (params: {\n previousToolCallFinished: Promise\n toolCall: CodebuffToolCall<'code_search'>\n requestClientToolCall: (\n toolCall: ClientToolCall<'code_search'>,\n ) => Promise