init claude-code

This commit is contained in:
2026-04-01 17:32:37 +02:00
commit 73b208c009
1902 changed files with 513237 additions and 0 deletions
+204
View File
@@ -0,0 +1,204 @@
import { feature } from 'bun:bundle'
import { z } from 'zod/v4'
import { getKairosActive, getUserMsgOptIn } from '../../bootstrap/state.js'
import { getFeatureValue_CACHED_WITH_REFRESH } from '../../services/analytics/growthbook.js'
import { logEvent } from '../../services/analytics/index.js'
import type { ValidationResult } from '../../Tool.js'
import { buildTool, type ToolDef } from '../../Tool.js'
import { isEnvTruthy } from '../../utils/envUtils.js'
import { lazySchema } from '../../utils/lazySchema.js'
import { plural } from '../../utils/stringUtils.js'
import { resolveAttachments, validateAttachmentPaths } from './attachments.js'
import {
BRIEF_TOOL_NAME,
BRIEF_TOOL_PROMPT,
DESCRIPTION,
LEGACY_BRIEF_TOOL_NAME,
} from './prompt.js'
import { renderToolResultMessage, renderToolUseMessage } from './UI.js'
const inputSchema = lazySchema(() =>
z.strictObject({
message: z
.string()
.describe('The message for the user. Supports markdown formatting.'),
attachments: z
.array(z.string())
.optional()
.describe(
'Optional file paths (absolute or relative to cwd) to attach. Use for photos, screenshots, diffs, logs, or any file the user should see alongside your message.',
),
status: z
.enum(['normal', 'proactive'])
.describe(
"Use 'proactive' when you're surfacing something the user hasn't asked for and needs to see now — task completion while they're away, a blocker you hit, an unsolicited status update. Use 'normal' when replying to something the user just said.",
),
}),
)
type InputSchema = ReturnType<typeof inputSchema>
// attachments MUST remain optional — resumed sessions replay pre-attachment
// outputs verbatim and a required field would crash the UI renderer on resume.
const outputSchema = lazySchema(() =>
z.object({
message: z.string().describe('The message'),
attachments: z
.array(
z.object({
path: z.string(),
size: z.number(),
isImage: z.boolean(),
file_uuid: z.string().optional(),
}),
)
.optional()
.describe('Resolved attachment metadata'),
sentAt: z
.string()
.optional()
.describe(
'ISO timestamp captured at tool execution on the emitting process. Optional — resumed sessions replay pre-sentAt outputs verbatim.',
),
}),
)
type OutputSchema = ReturnType<typeof outputSchema>
export type Output = z.infer<OutputSchema>
const KAIROS_BRIEF_REFRESH_MS = 5 * 60 * 1000
/**
* Entitlement check — is the user ALLOWED to use Brief? Combines build-time
* flags with runtime GB gate + assistant-mode passthrough. No opt-in check
* here — this decides whether opt-in should be HONORED, not whether the user
* has opted in.
*
* Build-time OR-gated on KAIROS || KAIROS_BRIEF (same pattern as
* PROACTIVE || KAIROS): assistant mode depends on Brief, so KAIROS alone
* must bundle it. KAIROS_BRIEF lets Brief ship independently.
*
* Use this to decide whether `--brief` / `defaultView: 'chat'` / `--tools`
* listing should be honored. Use `isBriefEnabled()` to decide whether the
* tool is actually active in the current session.
*
* CLAUDE_CODE_BRIEF env var force-grants entitlement for dev/testing —
* bypasses the GB gate so you can test without being enrolled. Still
* requires an opt-in action to activate (--brief, defaultView, etc.), but
* the env var alone also sets userMsgOptIn via maybeActivateBrief().
*/
export function isBriefEntitled(): boolean {
// Positive ternary — see docs/feature-gating.md. Negative early-return
// would not eliminate the GB gate string from external builds.
return feature('KAIROS') || feature('KAIROS_BRIEF')
? getKairosActive() ||
isEnvTruthy(process.env.CLAUDE_CODE_BRIEF) ||
getFeatureValue_CACHED_WITH_REFRESH(
'tengu_kairos_brief',
false,
KAIROS_BRIEF_REFRESH_MS,
)
: false
}
/**
* Unified activation gate for the Brief tool. Governs model-facing behavior
* as a unit: tool availability, system prompt section (getBriefSection),
* tool-deferral bypass (isDeferredTool), and todo-nag suppression.
*
* Activation requires explicit opt-in (userMsgOptIn) set by one of:
* - `--brief` CLI flag (maybeActivateBrief in main.tsx)
* - `defaultView: 'chat'` in settings (main.tsx init)
* - `/brief` slash command (brief.ts)
* - `/config` defaultView picker (Config.tsx)
* - SendUserMessage in `--tools` / SDK `tools` option (main.tsx)
* - CLAUDE_CODE_BRIEF env var (maybeActivateBrief — dev/testing bypass)
* Assistant mode (kairosActive) bypasses opt-in since its system prompt
* hard-codes "you MUST use SendUserMessage" (systemPrompt.md:14).
*
* The GB gate is re-checked here as a kill-switch AND — flipping
* tengu_kairos_brief off mid-session disables the tool on the next 5-min
* refresh even for opted-in sessions. No opt-in → always false regardless
* of GB (this is the fix for "brief defaults on for enrolled ants").
*
* Called from Tool.isEnabled() (lazy, post-init), never at module scope.
* getKairosActive() and getUserMsgOptIn() are set in main.tsx before any
* caller reaches here.
*/
export function isBriefEnabled(): boolean {
// Top-level feature() guard is load-bearing for DCE: Bun can constant-fold
// the ternary to `false` in external builds and then dead-code the BriefTool
// object. Composing isBriefEntitled() alone (which has its own guard) is
// semantically equivalent but defeats constant-folding across the boundary.
return feature('KAIROS') || feature('KAIROS_BRIEF')
? (getKairosActive() || getUserMsgOptIn()) && isBriefEntitled()
: false
}
export const BriefTool = buildTool({
name: BRIEF_TOOL_NAME,
aliases: [LEGACY_BRIEF_TOOL_NAME],
searchHint:
'send a message to the user — your primary visible output channel',
maxResultSizeChars: 100_000,
userFacingName() {
return ''
},
get inputSchema(): InputSchema {
return inputSchema()
},
get outputSchema(): OutputSchema {
return outputSchema()
},
isEnabled() {
return isBriefEnabled()
},
isConcurrencySafe() {
return true
},
isReadOnly() {
return true
},
toAutoClassifierInput(input) {
return input.message
},
async validateInput({ attachments }, _context): Promise<ValidationResult> {
if (!attachments || attachments.length === 0) {
return { result: true }
}
return validateAttachmentPaths(attachments)
},
async description() {
return DESCRIPTION
},
async prompt() {
return BRIEF_TOOL_PROMPT
},
mapToolResultToToolResultBlockParam(output, toolUseID) {
const n = output.attachments?.length ?? 0
const suffix = n === 0 ? '' : ` (${n} ${plural(n, 'attachment')} included)`
return {
tool_use_id: toolUseID,
type: 'tool_result',
content: `Message delivered to user.${suffix}`,
}
},
renderToolUseMessage,
renderToolResultMessage,
async call({ message, attachments, status }, context) {
const sentAt = new Date().toISOString()
logEvent('tengu_brief_send', {
proactive: status === 'proactive',
attachment_count: attachments?.length ?? 0,
})
if (!attachments || attachments.length === 0) {
return { data: { message, sentAt } }
}
const appState = context.getAppState()
const resolved = await resolveAttachments(attachments, {
replBridgeEnabled: appState.replBridgeEnabled,
signal: context.abortController.signal,
})
return {
data: { message, attachments: resolved, sentAt },
}
},
} satisfies ToolDef<InputSchema, Output>)
File diff suppressed because one or more lines are too long
+110
View File
@@ -0,0 +1,110 @@
/**
* Shared attachment validation + resolution for SendUserMessage and
* SendUserFile. Lives in BriefTool/ so the dynamic `./upload.js` import
* inside the feature('BRIDGE_MODE') guard stays relative and upload.ts
* (axios, crypto, auth utils) remains tree-shakeable from non-bridge builds.
*/
import { feature } from 'bun:bundle'
import { stat } from 'fs/promises'
import type { ValidationResult } from '../../Tool.js'
import { getCwd } from '../../utils/cwd.js'
import { isEnvTruthy } from '../../utils/envUtils.js'
import { getErrnoCode } from '../../utils/errors.js'
import { IMAGE_EXTENSION_REGEX } from '../../utils/imagePaste.js'
import { expandPath } from '../../utils/path.js'
export type ResolvedAttachment = {
path: string
size: number
isImage: boolean
file_uuid?: string
}
export async function validateAttachmentPaths(
rawPaths: string[],
): Promise<ValidationResult> {
const cwd = getCwd()
for (const rawPath of rawPaths) {
const fullPath = expandPath(rawPath)
try {
const stats = await stat(fullPath)
if (!stats.isFile()) {
return {
result: false,
message: `Attachment "${rawPath}" is not a regular file.`,
errorCode: 1,
}
}
} catch (e) {
const code = getErrnoCode(e)
if (code === 'ENOENT') {
return {
result: false,
message: `Attachment "${rawPath}" does not exist. Current working directory: ${cwd}.`,
errorCode: 1,
}
}
if (code === 'EACCES' || code === 'EPERM') {
return {
result: false,
message: `Attachment "${rawPath}" is not accessible (permission denied).`,
errorCode: 1,
}
}
throw e
}
}
return { result: true }
}
export async function resolveAttachments(
rawPaths: string[],
uploadCtx: { replBridgeEnabled: boolean; signal?: AbortSignal },
): Promise<ResolvedAttachment[]> {
// Stat serially (local, fast) to keep ordering deterministic, then upload
// in parallel (network, slow). Upload failures resolve undefined — the
// attachment still carries {path, size, isImage} for local renderers.
const stated: ResolvedAttachment[] = []
for (const rawPath of rawPaths) {
const fullPath = expandPath(rawPath)
// Single stat — we need size, so this is the operation, not a guard.
// validateInput ran before us, but the file could have moved since
// (TOCTOU); if it did, let the error propagate so the model sees it.
const stats = await stat(fullPath)
stated.push({
path: fullPath,
size: stats.size,
isImage: IMAGE_EXTENSION_REGEX.test(fullPath),
})
}
// Dynamic import inside the feature() guard so upload.ts (axios, crypto,
// zod, auth utils, MIME map) is fully eliminated from non-BRIDGE_MODE
// builds. A static import would force module-scope evaluation regardless
// of the guard inside uploadBriefAttachment — CLAUDE.md: "helpers defined
// outside remain in the build even if never called".
if (feature('BRIDGE_MODE')) {
// Headless/SDK callers never set appState.replBridgeEnabled (only the TTY
// REPL does, at main.tsx init). CLAUDE_CODE_BRIEF_UPLOAD lets a host that
// runs the CLI as a subprocess opt in — e.g. the cowork desktop bridge,
// which already passes CLAUDE_CODE_OAUTH_TOKEN for auth.
const shouldUpload =
uploadCtx.replBridgeEnabled ||
isEnvTruthy(process.env.CLAUDE_CODE_BRIEF_UPLOAD)
const { uploadBriefAttachment } = await import('./upload.js')
const uuids = await Promise.all(
stated.map(a =>
uploadBriefAttachment(a.path, a.size, {
replBridgeEnabled: shouldUpload,
signal: uploadCtx.signal,
}),
),
)
return stated.map((a, i) =>
uuids[i] === undefined ? a : { ...a, file_uuid: uuids[i] },
)
}
return stated
}
+22
View File
@@ -0,0 +1,22 @@
export const BRIEF_TOOL_NAME = 'SendUserMessage'
export const LEGACY_BRIEF_TOOL_NAME = 'Brief'
export const DESCRIPTION = 'Send a message to the user'
export const BRIEF_TOOL_PROMPT = `Send a message the user will read. Text outside this tool is visible in the detail view, but most won't open it — the answer lives here.
\`message\` supports markdown. \`attachments\` takes file paths (absolute or cwd-relative) for images, diffs, logs.
\`status\` labels intent: 'normal' when replying to what they just asked; 'proactive' when you're initiating — a scheduled task finished, a blocker surfaced during background work, you need input on something they haven't asked about. Set it honestly; downstream routing uses it.`
export const BRIEF_PROACTIVE_SECTION = `## Talking to the user
${BRIEF_TOOL_NAME} is where your replies go. Text outside it is visible if the user expands the detail view, but most won't — assume unread. Anything you want them to actually see goes through ${BRIEF_TOOL_NAME}. The failure mode: the real answer lives in plain text while ${BRIEF_TOOL_NAME} just says "done!" — they see "done!" and miss everything.
So: every time the user says something, the reply they actually read comes through ${BRIEF_TOOL_NAME}. Even for "hi". Even for "thanks".
If you can answer right away, send the answer. If you need to go look — run a command, read files, check something — ack first in one line ("On it — checking the test output"), then work, then send the result. Without the ack they're staring at a spinner.
For longer work: ack → work → result. Between those, send a checkpoint when something useful happened — a decision you made, a surprise you hit, a phase boundary. Skip the filler ("running tests...") — a checkpoint earns its place by carrying information.
Keep messages tight — the decision, the file:line, the PR number. Second person always ("your config"), never third.`
+174
View File
@@ -0,0 +1,174 @@
/**
* Upload BriefTool attachments to private_api so web viewers can preview them.
*
* When the repl bridge is active, attachment paths are meaningless to a web
* viewer (they're on Claude's machine). We upload to /api/oauth/file_upload —
* the same store MessageComposer/SpaceMessage render from — and stash the
* returned file_uuid alongside the path. Web resolves file_uuid → preview;
* desktop/local try path first.
*
* Best-effort: any failure (no token, bridge off, network error, 4xx) logs
* debug and returns undefined. The attachment still carries {path, size,
* isImage}, so local-terminal and same-machine-desktop render unaffected.
*/
import { feature } from 'bun:bundle'
import axios from 'axios'
import { randomUUID } from 'crypto'
import { readFile } from 'fs/promises'
import { basename, extname } from 'path'
import { z } from 'zod/v4'
import {
getBridgeAccessToken,
getBridgeBaseUrlOverride,
} from '../../bridge/bridgeConfig.js'
import { getOauthConfig } from '../../constants/oauth.js'
import { logForDebugging } from '../../utils/debug.js'
import { lazySchema } from '../../utils/lazySchema.js'
import { jsonStringify } from '../../utils/slowOperations.js'
// Matches the private_api backend limit
const MAX_UPLOAD_BYTES = 30 * 1024 * 1024
const UPLOAD_TIMEOUT_MS = 30_000
// Backend dispatches on mime: image/* → upload_image_wrapped (writes
// PREVIEW/THUMBNAIL, no ORIGINAL), everything else → upload_generic_file
// (ORIGINAL only, no preview). Only whitelist raster formats the
// transcoder reliably handles — svg/bmp/ico risk a 400, and pdf routes
// to upload_pdf_file_wrapped which also skips ORIGINAL. Dispatch
// viewers use /preview for images and /contents for everything else,
// so images go image/* and the rest go octet-stream.
const MIME_BY_EXT: Record<string, string> = {
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.webp': 'image/webp',
}
function guessMimeType(filename: string): string {
const ext = extname(filename).toLowerCase()
return MIME_BY_EXT[ext] ?? 'application/octet-stream'
}
function debug(msg: string): void {
logForDebugging(`[brief:upload] ${msg}`)
}
/**
* Base URL for uploads. Must match the host the token is valid for.
*
* Subprocess hosts (cowork) pass ANTHROPIC_BASE_URL alongside
* CLAUDE_CODE_OAUTH_TOKEN — prefer that since getOauthConfig() only
* returns staging when USE_STAGING_OAUTH is set, which such hosts don't
* set. Without this a staging token hits api.anthropic.com → 401 → silent
* skip → web viewer sees inert cards with no file_uuid.
*/
function getBridgeBaseUrl(): string {
return (
getBridgeBaseUrlOverride() ??
process.env.ANTHROPIC_BASE_URL ??
getOauthConfig().BASE_API_URL
)
}
// /api/oauth/file_upload returns one of ChatMessage{Image,Blob,Document}FileSchema.
// All share file_uuid; that's the only field we need.
const uploadResponseSchema = lazySchema(() =>
z.object({ file_uuid: z.string() }),
)
export type BriefUploadContext = {
replBridgeEnabled: boolean
signal?: AbortSignal
}
/**
* Upload a single attachment. Returns file_uuid on success, undefined otherwise.
* Every early-return is intentional graceful degradation.
*/
export async function uploadBriefAttachment(
fullPath: string,
size: number,
ctx: BriefUploadContext,
): Promise<string | undefined> {
// Positive pattern so bun:bundle eliminates the entire body from
// non-BRIDGE_MODE builds (negative `if (!feature(...)) return` does not).
if (feature('BRIDGE_MODE')) {
if (!ctx.replBridgeEnabled) return undefined
if (size > MAX_UPLOAD_BYTES) {
debug(`skip ${fullPath}: ${size} bytes exceeds ${MAX_UPLOAD_BYTES} limit`)
return undefined
}
const token = getBridgeAccessToken()
if (!token) {
debug('skip: no oauth token')
return undefined
}
let content: Buffer
try {
content = await readFile(fullPath)
} catch (e) {
debug(`read failed for ${fullPath}: ${e}`)
return undefined
}
const baseUrl = getBridgeBaseUrl()
const url = `${baseUrl}/api/oauth/file_upload`
const filename = basename(fullPath)
const mimeType = guessMimeType(filename)
const boundary = `----FormBoundary${randomUUID()}`
// Manual multipart — same pattern as filesApi.ts. The oauth endpoint takes
// a single "file" part (no "purpose" field like the public Files API).
const body = Buffer.concat([
Buffer.from(
`--${boundary}\r\n` +
`Content-Disposition: form-data; name="file"; filename="${filename}"\r\n` +
`Content-Type: ${mimeType}\r\n\r\n`,
),
content,
Buffer.from(`\r\n--${boundary}--\r\n`),
])
try {
const response = await axios.post(url, body, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': `multipart/form-data; boundary=${boundary}`,
'Content-Length': body.length.toString(),
},
timeout: UPLOAD_TIMEOUT_MS,
signal: ctx.signal,
validateStatus: () => true,
})
if (response.status !== 201) {
debug(
`upload failed for ${fullPath}: status=${response.status} body=${jsonStringify(response.data).slice(0, 200)}`,
)
return undefined
}
const parsed = uploadResponseSchema().safeParse(response.data)
if (!parsed.success) {
debug(
`unexpected response shape for ${fullPath}: ${parsed.error.message}`,
)
return undefined
}
debug(`uploaded ${fullPath}${parsed.data.file_uuid} (${size} bytes)`)
return parsed.data.file_uuid
} catch (e) {
debug(`upload threw for ${fullPath}: ${e}`)
return undefined
}
}
return undefined
}