This document was produced by reviewing the BigBrotherBot codebase. It covers every meaningful way the project uses
@cline/sdk.
BigBrotherBot is a production AI agent for 1984 Ventures. It processes inbound email, enriches a Notion CRM, scores applications, runs daily meeting prep, and provides a chatbot interface over Discord and WhatsApp.
Every LLM agent run — regardless of channel — flows through a single function: runClineSkill() in src/cline/harness.ts. There are no direct generateText calls anywhere in the agent path. @cline/sdk v0.0.41 is the sole framework.
"@cline/sdk": "^0.0.41"
src/cline/harness.ts
let _cline: ClineCore | null = null;
async function getCline(): Promise<ClineCore> {
if (!_cline) {
_cline = await ClineCore.create({
clientName: "bigbrother",
backendMode: "local",
});
}
return _cline;
}
export async function disposeCline(reason = "bigbrother_shutdown"): Promise<void> {
if (!_cline) return;
const cline = _cline;
_cline = null;
await cline.dispose(reason);
}One ClineCore instance held for daemon lifetime. disposeCline() on shutdown. abortClineSession(sessionId) wired to Discord canceller.
buildSystemPrompt(profile, agentContext, skillBody) layers:
[agentContext] <- ASSOCIATE_PERSONA or CRM_PERSONA (.md files)
[rules block] <- CHAT_RULES | EMAIL_RULES | CRON_RULES
<env> <- date [+ cwd for chat]
[skillBody] <- SKILL.md Markdown content
CHAT_RULES: "Always gather context. Do not ask permission. Tool calls until done. Show planning."
EMAIL_RULES: "Complete without asking. Keep using tools until done."
CRON_RULES: "You work autonomously. Show planning. Call submit_and_exit when done. Verify work."
| Profile | SDK mode |
enableTools |
reasoningEffort |
|---|---|---|---|
chat |
act |
false |
medium |
email |
act |
false |
-- |
cron |
yolo |
false |
-- |
cron also injects submit_and_exit tool and asserts it was called.
const submitAndExitTool = createTool({
name: "submit_and_exit",
lifecycle: { completesRun: true },
retryable: false,
maxRetries: 0,
inputSchema: {
type: "object",
properties: {
summary: { type: "string" },
verified: { type: "boolean" },
},
required: ["summary", "verified"],
},
});cline.start({
config: {
providerId: "openrouter",
modelId,
apiKey: env.OPENROUTER_API_KEY,
reasoningEffort,
cwd: process.cwd(),
mode,
enableTools: false,
enableSpawnAgent: false,
enableAgentTeams: false,
disableMcpSettingsTools: true,
extensions: [bigBrotherRuntimeExtension],
extensionContext: {
workspace: { rootPath: process.cwd(), cwd: process.cwd() },
},
systemPrompt,
extraTools: toolsForProfile(profile, input.extraTools),
},
prompt: input.userMessage,
initialMessages: input.initialMessages,
interactive: false,
});Key: enableTools: false, disableMcpSettingsTools: true, backendMode: "local".
Fields on ClineSkillInput:
| Field | Purpose |
|---|---|
skillBody |
SKILL.md Markdown — agent instructions |
userMessage |
The turn prompt |
extraTools |
AgentTool[] — full tool belt |
profile |
"chat" | "email" | "cron" |
agentContext |
Persona text prepended to system prompt |
initialMessages |
Prior thread messages |
modelId |
Override model (default: OpenRouter anthropic/claude-sonnet-4-6) |
mode / enableTools / reasoningEffort |
Per-run overrides |
verbose / onToolEvent |
Tool-call streaming |
abortSignal |
Cancel via AbortController |
telemetry |
{ functionId, userId?, properties? } — OTel span wrapping |
Gmail poller / Discord / WhatsApp / Cron scheduler
|
v
runClineSkill(input: ClineSkillInput)
|
+- buildSystemPrompt() <- persona + rules + <env> + skill body
+- (optional) subscribe() <- real-time tool event stream
+- (optional) withAiSpan() <- OpenTelemetry + PostHog tracing
+- cline.start({ config, prompt, initialMessages })
ClineCore handles multi-step tool use internally
src/cline/mcp-manager.ts
BigBrother uses @ai-sdk/mcp (createMCPClient, HTTP transport) to connect to MCP servers.
getAllMcpTools() is the tool registry. On first call it:
- Connects to every MCP server lazily
- Calls
client.listTools()for JSON Schema definitions - Calls
client.tools()to get AI SDKexecute()wrappers - Wraps each as a Cline
AgentToolviacreateTool()with a prefixed name - Caches the result for daemon lifetime
Tool name prefixes:
| Prefix | Server | Examples |
|---|---|---|
goog_* |
google-workspace | goog_gmail_send, goog_calendar_list_events |
notion_* |
notion + notion-generic | notion_create_company, notion_update_contact |
db84_* |
84db (applications) | db84_apply_list, db84_apply_update |
research_* |
research MCP | research_enrich_company, research_web_search |
posthog_* |
posthog | analytics tools |
buildToolsForNames(toolNames, lightToolRegistry, threadId?) resolves a skill's declared tools:
- Matches against the MCP tool cache
- Falls back to the local light-tool registry
- Wraps
goog_gmail_sendwith inboundthread_idfor email reply threading
Four tools defined in src/cline/tools/, not via MCP:
Delegates Notion work to a nested runClineSkill() with CRM persona + Notion + research tools.
execute: async (input) => {
const { task, context } = input;
const crmTools = allMcp.filter((t) => CRM_TOOLS.has(t.name));
return await runClineSkill({
skillBody: CRM_PERSONA,
userMessage: context ? `${task}\n\n--- Context ---\n${context}` : task,
extraTools: crmTools,
telemetry: { functionId: "ask-crm" },
});
}Lets chat agents trigger any enabled scheduled task with params. Runs synchronously.
Pulls a skill body on demand from the registry.
Registered at startup. Uses GEMINI_FLASH (RESEARCH_MODEL) rather than default Claude.
export const bigBrotherRuntimeExtension: AgentPlugin = {
name: "bigbrother-runtime",
manifest: { capabilities: ["rules", "hooks"] },
setup(api) {
api.registerRule({
id: "bigbrother-tool-discipline",
source: "bigbrother",
content: "Use tool names exactly as provided. Custom BigBrother tools use snake_case names such as ask_crm, load_skill, run_task, and submit_and_exit.",
});
},
hooks: {
afterTool({ tool, result }) {
if (isStructuredToolError(result.output)) {
console.warn(`[cline/tool] ${tool.name} returned structured error: ${result.output.error}`);
}
## 11. Observability
### Event Subscription
```typescript
unsubscribe = cline.subscribe((event) => {
const sessionId = sessionIdFromEvent(event);
if (sessionId) captureSessionId(sessionId);
if (event.type !== "agent_event") return;
const e = event.payload.event;
if (e.type === "content_start" && e.contentType === "tool") {
input.onToolEvent?.({ phase: "start", toolName: e.toolName, input: e.input });
} else if (e.type === "content_end" && e.contentType === "tool") {
input.onToolEvent?.({ phase: "end", toolName: e.toolName, output: e.output, error: e.error });
}
});Discord uses this for live status messages during tool calls.
When telemetry is provided:
- Creates an OTel span named
cline.<functionId> - Calls
cline.getAccumulatedUsage(sessionId)after the run - Attaches
{ inputTokens, outputTokens }to the span NormalizingPostHogProcessorrewrites OpenRouter model aliases to canonical IDs
| Event | Trigger |
|---|---|
agent.llm_error |
exception inside withAiSpan |
agent.step_limit |
finishReason === "max_iterations" |
cron.job_failed |
task throws in scheduler |
email.process_failed |
email thread handler throws |
email.attachment_ocr_failed |
Mistral OCR fails |
Gmail unread -> getThread -> enrichAttachments (OCR)
-> MessageRouter.handle()
-> resolveContext(msg) -> EntryProfile
-> runClineEmailSkill()
-> buildToolsForNames(skill.tools, ...)
-> session history (<= 50 messages)
-> runClineSkill({ profile: "email", agentContext: ASSOCIATE_PERSONA, ... })
@mention / WhatsApp message
-> resolvePartner() -> gate on allowlist
-> assemble: chat skill body + skill menu + all MCP tools + light tools
-> session history (last ~8h, <= 10 messages)
-> runClineSkill({ profile: "chat", ... })
AgentScheduler (croner) / pnpm cli <task-id> / runTask tool
-> task.run(params, agents)
## 14. Key Design Decisions
1. **Single ClineCore instance** -- lazy singleton held for daemon lifetime; `dispose()` on shutdown
2. **No built-in tools** -- `enableTools: false` on every run; all tools flow through `extraTools`
3. **Profile-based system prompt** -- `chat`/`email`/`cron` selects rules block, mode, and reasoning effort
4. **Recursive runs** -- `ask_crm` and `run_research_subagent` call `runClineSkill()` inside tool `execute()`
5. **Thread-ID injection** -- `goog_gmail_send` is wrapped with inbound thread ID so agents never supply it
6. **Structured error handling** -- all tool errors return `{ ok: false, ... }`; extension hook logs as warnings
7. **Autonomy gate** -- `cron` runs require `submit_and_exit` (`lifecycle: { completesRun: true }`) to complete
8. **Custom extension** -- `bigbrother-runtime` plugin adds a tool-naming rule and `afterTool` error hook
9. **MCP via `@ai-sdk/mcp`** -- not Cline's built-in MCP; BigBrother manages its own connections
10. **Telemetry** -- `cline.getAccumulatedUsage()` feeds PostHog/OTel token counts; events drive Discord live-status
-> skill.tools -> buildToolsForNames(...)
-> runClineSkill({ profile: "cron", skillBody, userMessage, agentContext: ASSOCIATE_PERSONA, ... })
Plain Markdown files:
-
src/agents/associate/AGENTS.md-- the Associate persona (email + task runs) -
src/agents/crm/AGENTS.md-- the CRM specialist persona (used byask_crmtool)}, }, };
The `afterTool` hook logs structured errors as warnings so the agent loop continues past recoverable failures.
---
## 10. Tool Error Structure
**`src/cline/tool-errors.ts`**
```typescript
interface StructuredToolError {
ok: false;
tool: string;
error: string;
errorName: string;
retryable: boolean;
}
All tool execute() bodies catch and wrap errors in this structure.