Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions agents/__tests__/base3.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,4 +160,10 @@ describe('base3 CLI roots', () => {
// Codebuff's paid modes explain credits; Freebuff has none to explain.
expect(base3.systemPrompt).toContain('/usage')
})

test('ships the optimization rules for long-running commands and anti-looping', () => {
expect(base3FreeDeepseek.systemPrompt).toContain('LONG-RUNNING COMMANDS (PREVENT HANGS)')
expect(base3FreeDeepseek.systemPrompt).toContain('ANTI-LOOPING')
expect(base3FreeDeepseek.systemPrompt).toContain('write_todos')
})
})
13 changes: 13 additions & 0 deletions agents/base3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,19 @@ ${
}

${PLACEHOLDER.SYSTEM_INFO_PROMPT}

# LONG-RUNNING COMMANDS (PREVENT HANGS)
- Commands like "bun run build:freebuff", compilations, or dev servers MUST use process_type: "BACKGROUND".
- NEVER run a compilation in SYNC mode, as it will deadlock the terminal.
- To wait for a background build, pipe its output (e.g., "> build.log 2>&1 < NUL") and then use "sleep 2" or "sleep 5" in SYNC mode repeatedly as a biological timer, then read the log to check if it finished.

# ANTI-LOOPING
- If you find yourself repeatedly executing the exact same tool calls without progress (e.g. reading the same file continuously), you MUST STOP. Change your approach, write to a different file, run a different search, or ask the user for help. Do NOT loop blindly.

# METAS E SUBMETAS (write_todos)
- Você DEVE EXECUTAR A FERRAMENTA/TOOL \`write_todos\` no início de qualquer tarefa para quebrar o objetivo em etapas hiper-granulares.
- Conforme você avança, chame a tool \`write_todos\` novamente para atualizar o progresso.
- NUNCA escreva os seus TODOs no texto normal do chat como markdown. Use APENAS a tool \`write_todos\` para que a UI possa ler os dados.
`
}

Expand Down
32 changes: 32 additions & 0 deletions agents/mcp-enabled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { FREEBUFF_MIMO_V25_MODEL_ID } from '@codebuff/common/constants/freebuff-models'

import { createBase3CliRoot } from './base3'

const definition = {
...createBase3CliRoot({
model: FREEBUFF_MIMO_V25_MODEL_ID,
isFreebuff: true,
}),
id: 'mcp-enabled',
displayName: 'Buffy with MCP',
mcpServers: {
filesystem: {
command: 'npx',
args: ['-y', '@modelcontextprotocol/server-filesystem'],
},
memory: {
command: 'npx',
args: ['-y', '@modelcontextprotocol/server-memory'],
},
'sequential-thinking': {
command: 'npx',
args: ['-y', '@modelcontextprotocol/server-sequential-thinking'],
},
puppeteer: {
command: 'npx',
args: ['-y', '@modelcontextprotocol/server-puppeteer'],
},
},
}

export default definition
20 changes: 18 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions cli/scripts/build-binary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,17 @@ async function main() {
['process.env.CODEBUFF_CLI_VERSION', `"${version}"`],
['process.env.CODEBUFF_CLI_TARGET', `"${getCliTargetLabel(targetInfo)}"`],
['process.env.FREEBUFF_MODE', `"${process.env.FREEBUFF_MODE ?? 'false'}"`],
['process.env.NEXT_PUBLIC_API_URL', '"https://www.codebuff.com"'],
['process.env.NEXT_PUBLIC_CODEBUFF_APP_URL', '"https://www.codebuff.com"'],
['process.env.NEXT_PUBLIC_SUPERTOKENS_APP_URL', '"https://www.codebuff.com"'],
['process.env.NEXT_PUBLIC_CB_ENVIRONMENT', '"prod"'],
['process.env.NEXT_PUBLIC_SUPPORT_EMAIL', '"support@codebuff.com"'],
['process.env.NEXT_PUBLIC_POSTHOG_API_KEY', '"dummy"'],
['process.env.NEXT_PUBLIC_POSTHOG_HOST_URL', '"https://app.posthog.com"'],
['process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY', '"pk_dummy"'],
['process.env.NEXT_PUBLIC_STRIPE_CUSTOMER_PORTAL', '"https://stripe.com"'],
['process.env.NEXT_PUBLIC_WEB_PORT', '"3000"'],
['process.env.NEXT_PUBLIC_IS_FREEBUFF', '"true"'],
...nextPublicEnvVars,
]

Expand Down
19 changes: 19 additions & 0 deletions cli/src/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,14 @@ import {
useState,
} from 'react'
import { useShallow } from 'zustand/react/shallow'
import fs from 'node:fs'
import path from 'node:path'

import { getAdsEnabled } from './commands/ads'
import { routeUserPrompt, addBashMessageToHistory } from './commands/router'
import { MissionTodosTracker } from './components/mission-todos-tracker'
import { SingleAdBanner } from './components/ad-banner'
import { getMissionPath } from './missions/mission-store'
import { ChatInputBar } from './components/chat-input-bar'
import { ChatHeader } from './components/chat-header'
import { FreebuffActiveSessionSummary } from './components/freebuff-active-session-summary'
Expand Down Expand Up @@ -1628,6 +1632,8 @@ export const Chat = ({
/>
)}

<MissionTodosTracker messages={messages} />

{reviewMode ? (
// Review and ask_user take precedence over the session-ended banner:
// during the grace window the agent may still be asking to run tools
Expand All @@ -1652,6 +1658,19 @@ export const Chat = ({
) : isFreebuffSessionOver && !askUserState ? (
<SessionEndedBanner
isStreaming={isStreaming || isWaitingForResponse}
onSessionRenewed={() => {
const missionPath = getMissionPath(getProjectRoot() ?? process.cwd())
try {
if (fs.existsSync(missionPath)) {
const missionData = JSON.parse(fs.readFileSync(missionPath, 'utf8'))
if (missionData.status === 'active') {
setTimeout(() => {
onSubmitPrompt('continue', agentMode).catch(() => {})
}, 100)
}
}
} catch (e) {}
}}
/>
) : (
<>
Expand Down
36 changes: 36 additions & 0 deletions cli/src/commands/command-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { handleAdsEnable, handleAdsDisable } from './ads'
import { handleCopyConversationCommand } from './copy-conversation'
import { handleHelpCommand } from './help'
import { handleImageCommand } from './image'
import { runMCPCommand } from './mcp'
import { runMissionCommand } from './mission'
import { handleInitializationFlowLocally } from './init'
import {
collectProcessDiagnostics,
Expand Down Expand Up @@ -186,6 +188,40 @@ const FREEBUFF_ONLY_COMMANDS = new Set([
])

const ALL_COMMANDS: CommandDefinition[] = [
defineCommandWithArgs({
name: 'mcp',
handler: async (params, args) => {
const input = params.inputValue.trim()
params.saveToHistory(input)
clearInput(params)
const message = await runMCPCommand(args)
params.setMessages((prev) => [
...prev,
getUserMessage(input),
getSystemMessage(message),
])
},
}),
defineCommandWithArgs({
name: 'mission',
aliases: ['goal'],
handler: (params, args) => {
const input = params.inputValue.trim()
params.saveToHistory(input)
clearInput(params)
const result = runMissionCommand(args)
if (result.kind === 'start') {
params.sendMessage({ content: result.prompt, agentMode: params.agentMode })
setTimeout(() => params.scrollToLatest(), 0)
return
}
params.setMessages((prev) => [
...prev,
getUserMessage(input),
getSystemMessage(result.message),
])
},
}),
defineCommand({
name: 'ads:enable',
handler: (params) => {
Expand Down
71 changes: 71 additions & 0 deletions cli/src/commands/mcp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import {
closeAllMCPClients,
getMCPClient,
getMCPClientId,
getMCPClientStatuses,
listMCPTools,
} from '@codebuff/common/mcp/client'

import {
getLoadedMCPServers,
initializeAgentRegistry,
} from '../utils/local-agent-registry'

import type { MCPConfig } from '@codebuff/common/types/mcp'

function describeConfig(config: MCPConfig): string {
if (config.type === 'stdio') return `stdio · ${config.command}`
try {
const url = new URL(config.url)
return `${config.type} · ${url.origin}${url.pathname}`
} catch {
return `${config.type} · endereço inválido`
}
}

export function formatMCPStatus(): string {
const servers = getLoadedMCPServers()
const statuses = getMCPClientStatuses()
if (!Object.keys(servers).length) {
return 'Nenhum servidor MCP configurado. Use .agents/mcp.json, .mcp.json ou .cursor/mcp.json.'
}

const readyIds = new Set(
statuses.filter((status) => status.state === 'ready').map((status) => status.id),
)
return Object.entries(servers)
.map(([name, config]) => {
const active = readyIds.has(getMCPClientId(config))
? 'conectado'
: 'configurado'
return `${name} · ${describeConfig(config)} · ${active}`
})
.join('\n')
}

export async function runMCPCommand(args: string): Promise<string> {
const trimmed = args.trim()
const [verb, name] = trimmed ? trimmed.split(/\s+/, 2) : ['status']
if (verb === 'status' || verb === 'list') return formatMCPStatus()

if (verb === 'reload') {
await closeAllMCPClients()
await initializeAgentRegistry()
return `MCP recarregado.\n${formatMCPStatus()}`
}

if (verb === 'test') {
if (!name) return 'Uso: /mcp test <servidor>'
const config = getLoadedMCPServers()[name]
if (!config) return `Servidor MCP não encontrado: ${name}`
try {
const clientId = await getMCPClient(config)
const result = await listMCPTools(clientId)
return `${name} conectado · ${result.tools.length} ferramentas disponíveis`
} catch (error) {
return `${name} falhou: ${error instanceof Error ? error.message : String(error)}`
}
}

return 'Uso: /mcp [status|list|reload|test <servidor>]'
}
Loading
Loading