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
+216
View File
@@ -0,0 +1,216 @@
import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
import type { UUID } from 'crypto'
import type { CanUseToolFn } from '../hooks/useCanUseTool.js'
import type { CompactionResult } from '../services/compact/compact.js'
import type { ScopedMcpServerConfig } from '../services/mcp/types.js'
import type { ToolUseContext } from '../Tool.js'
import type { EffortValue } from '../utils/effort.js'
import type { IDEExtensionInstallationStatus, IdeType } from '../utils/ide.js'
import type { SettingSource } from '../utils/settings/constants.js'
import type { HooksSettings } from '../utils/settings/types.js'
import type { ThemeName } from '../utils/theme.js'
import type { LogOption } from './logs.js'
import type { Message } from './message.js'
import type { PluginManifest } from './plugin.js'
export type LocalCommandResult =
| { type: 'text'; value: string }
| {
type: 'compact'
compactionResult: CompactionResult
displayText?: string
}
| { type: 'skip' } // Skip messages
export type PromptCommand = {
type: 'prompt'
progressMessage: string
contentLength: number // Length of command content in characters (used for token estimation)
argNames?: string[]
allowedTools?: string[]
model?: string
source: SettingSource | 'builtin' | 'mcp' | 'plugin' | 'bundled'
pluginInfo?: {
pluginManifest: PluginManifest
repository: string
}
disableNonInteractive?: boolean
// Hooks to register when this skill is invoked
hooks?: HooksSettings
// Base directory for skill resources (used to set CLAUDE_PLUGIN_ROOT environment variable for skill hooks)
skillRoot?: string
// Execution context: 'inline' (default) or 'fork' (run as sub-agent)
// 'inline' = skill content expands into the current conversation
// 'fork' = skill runs in a sub-agent with separate context and token budget
context?: 'inline' | 'fork'
// Agent type to use when forked (e.g., 'Bash', 'general-purpose')
// Only applicable when context is 'fork'
agent?: string
effort?: EffortValue
// Glob patterns for file paths this skill applies to
// When set, the skill is only visible after the model touches matching files
paths?: string[]
getPromptForCommand(
args: string,
context: ToolUseContext,
): Promise<ContentBlockParam[]>
}
/**
* The call signature for a local command implementation.
*/
export type LocalCommandCall = (
args: string,
context: LocalJSXCommandContext,
) => Promise<LocalCommandResult>
/**
* Module shape returned by load() for lazy-loaded local commands.
*/
export type LocalCommandModule = {
call: LocalCommandCall
}
type LocalCommand = {
type: 'local'
supportsNonInteractive: boolean
load: () => Promise<LocalCommandModule>
}
export type LocalJSXCommandContext = ToolUseContext & {
canUseTool?: CanUseToolFn
setMessages: (updater: (prev: Message[]) => Message[]) => void
options: {
dynamicMcpConfig?: Record<string, ScopedMcpServerConfig>
ideInstallationStatus: IDEExtensionInstallationStatus | null
theme: ThemeName
}
onChangeAPIKey: () => void
onChangeDynamicMcpConfig?: (
config: Record<string, ScopedMcpServerConfig>,
) => void
onInstallIDEExtension?: (ide: IdeType) => void
resume?: (
sessionId: UUID,
log: LogOption,
entrypoint: ResumeEntrypoint,
) => Promise<void>
}
export type ResumeEntrypoint =
| 'cli_flag'
| 'slash_command_picker'
| 'slash_command_session_id'
| 'slash_command_title'
| 'fork'
export type CommandResultDisplay = 'skip' | 'system' | 'user'
/**
* Callback when a command completes.
* @param result - Optional user-visible message to display
* @param options - Optional configuration for command completion
* @param options.display - How to display the result: 'skip' | 'system' | 'user' (default)
* @param options.shouldQuery - If true, send messages to the model after command completes
* @param options.metaMessages - Additional messages to insert as isMeta (model-visible but hidden)
*/
export type LocalJSXCommandOnDone = (
result?: string,
options?: {
display?: CommandResultDisplay
shouldQuery?: boolean
metaMessages?: string[]
nextInput?: string
submitNextInput?: boolean
},
) => void
/**
* The call signature for a local JSX command implementation.
*/
export type LocalJSXCommandCall = (
onDone: LocalJSXCommandOnDone,
context: ToolUseContext & LocalJSXCommandContext,
args: string,
) => Promise<React.ReactNode>
/**
* Module shape returned by load() for lazy-loaded commands.
*/
export type LocalJSXCommandModule = {
call: LocalJSXCommandCall
}
type LocalJSXCommand = {
type: 'local-jsx'
/**
* Lazy-load the command implementation.
* Returns a module with a call() function.
* This defers loading heavy dependencies until the command is invoked.
*/
load: () => Promise<LocalJSXCommandModule>
}
/**
* Declares which auth/provider environments a command is available in.
*
* This is separate from `isEnabled()`:
* - `availability` = who can use this (auth/provider requirement, static)
* - `isEnabled()` = is this turned on right now (GrowthBook, platform, env vars)
*
* Commands without `availability` are available everywhere.
* Commands with `availability` are only shown if the user matches at least one
* of the listed auth types. See meetsAvailabilityRequirement() in commands.ts.
*
* Example: `availability: ['claude-ai', 'console']` shows the command to
* claude.ai subscribers and direct Console API key users (api.anthropic.com),
* but hides it from Bedrock/Vertex/Foundry users and custom base URL users.
*/
export type CommandAvailability =
// claude.ai OAuth subscriber (Pro/Max/Team/Enterprise via claude.ai)
| 'claude-ai'
// Console API key user (direct api.anthropic.com, not via claude.ai OAuth)
| 'console'
export type CommandBase = {
availability?: CommandAvailability[]
description: string
hasUserSpecifiedDescription?: boolean
/** Defaults to true. Only set when the command has conditional enablement (feature flags, env checks, etc). */
isEnabled?: () => boolean
/** Defaults to false. Only set when the command should be hidden from typeahead/help. */
isHidden?: boolean
name: string
aliases?: string[]
isMcp?: boolean
argumentHint?: string // Hint text for command arguments (displayed in gray after command)
whenToUse?: string // From the "Skill" spec. Detailed usage scenarios for when to use this command
version?: string // Version of the command/skill
disableModelInvocation?: boolean // Whether to disable this command from being invoked by models
userInvocable?: boolean // Whether users can invoke this skill by typing /skill-name
loadedFrom?:
| 'commands_DEPRECATED'
| 'skills'
| 'plugin'
| 'managed'
| 'bundled'
| 'mcp' // Where the command was loaded from
kind?: 'workflow' // Distinguishes workflow-backed commands (badged in autocomplete)
immediate?: boolean // If true, command executes immediately without waiting for a stop point (bypasses queue)
isSensitive?: boolean // If true, args are redacted from the conversation history
/** Defaults to `name`. Only override when the displayed name differs (e.g. plugin prefix stripping). */
userFacingName?: () => string
}
export type Command = CommandBase &
(PromptCommand | LocalCommand | LocalJSXCommand)
/** Resolves the user-visible name, falling back to `cmd.name` when not overridden. */
export function getCommandName(cmd: CommandBase): string {
return cmd.userFacingName?.() ?? cmd.name
}
/** Resolves whether the command is enabled, defaulting to true. */
export function isCommandEnabled(cmd: CommandBase): boolean {
return cmd.isEnabled?.() ?? true
}
@@ -0,0 +1,865 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
// protoc-gen-ts_proto v2.6.1
// protoc unknown
// source: events_mono/claude_code/v1/claude_code_internal_event.proto
/* eslint-disable */
import { Timestamp } from '../../../google/protobuf/timestamp.js'
import { PublicApiAuth } from '../../common/v1/auth.js'
/** GitHubActionsMetadata contains GitHub Actions-specific environment information */
export interface GitHubActionsMetadata {
actor_id?: string | undefined
repository_id?: string | undefined
repository_owner_id?: string | undefined
}
/**
* EnvironmentMetadata contains environment and runtime information
* See claude-cli-internal/src/services/statsig.ts for the source of these fields
*/
export interface EnvironmentMetadata {
platform?: string | undefined
node_version?: string | undefined
terminal?: string | undefined
package_managers?: string | undefined
runtimes?: string | undefined
is_running_with_bun?: boolean | undefined
is_ci?: boolean | undefined
is_claubbit?: boolean | undefined
is_github_action?: boolean | undefined
is_claude_code_action?: boolean | undefined
is_claude_ai_auth?: boolean | undefined
version?: string | undefined
/** GitHub Actions specific fields (only present when is_github_action is true) */
github_event_name?: string | undefined
github_actions_runner_environment?: string | undefined
github_actions_runner_os?: string | undefined
github_action_ref?: string | undefined
/** WSL specific field */
wsl_version?: string | undefined
/** GitHub metadata (only present when is_github_action is true) */
github_actions_metadata?: GitHubActionsMetadata | undefined
arch?: string | undefined
is_claude_code_remote?: boolean | undefined
remote_environment_type?: string | undefined
claude_code_container_id?: string | undefined
claude_code_remote_session_id?: string | undefined
tags?: string[] | undefined
deployment_environment?: string | undefined
is_conductor?: boolean | undefined
version_base?: string | undefined
coworker_type?: string | undefined
build_time?: string | undefined
is_local_agent_mode?: boolean | undefined
linux_distro_id?: string | undefined
linux_distro_version?: string | undefined
linux_kernel?: string | undefined
vcs?: string | undefined
platform_raw?: string | undefined
}
/**
* SlackContext contains context fields present on every Claude-in-Slack (CIS) event.
* Event-specific fields (errorType, durationMs, httpStatus, etc.) go in
* ClaudeCodeInternalEvent.additional_metadata as JSON.
*/
export interface SlackContext {
slack_team_id?: string | undefined
is_enterprise_install?: boolean | undefined
trigger?: string | undefined
creation_method?: string | undefined
}
/**
* ClaudeCodeInternalEvent represents events logged from Claude Code via Statsig
* This schema matches the structure in claude-cli-internal/src/services/statsig.ts
* Source table: proj-product-data-nhme.raw_statsig_internal_tools.events
*/
export interface ClaudeCodeInternalEvent {
/** Event name (e.g., "tengu_binary_feedback", "tengu_api_success") */
event_name?: string | undefined
/** Event timestamp */
client_timestamp?: Date | undefined
model?: string | undefined
session_id?: string | undefined
user_type?: string | undefined
betas?: string | undefined
/** Environment and runtime information */
env?: EnvironmentMetadata | undefined
entrypoint?: string | undefined
agent_sdk_version?: string | undefined
is_interactive?: boolean | undefined
client_type?: string | undefined
/**
* Process metrics as JSON string (ant-only)
* Contains: uptime, rss, heapTotal, heapUsed, external, arrayBuffers,
* constrainedMemory, cpuUsage
*/
process?: string | undefined
/**
* Additional metadata passed to logEvent (event-specific)
* This includes fields like msg_id_A, msg_id_B, gitBranch, gitHead, etc.
* that vary per event type
*/
additional_metadata?: string | undefined
/** Authentication context automatically injected by the API */
auth?: PublicApiAuth | undefined
/** Server timestamp automatically injected by the API */
server_timestamp?: Date | undefined
/** Unique identifier for this event (automatically generated by API endpoint) */
event_id?: string | undefined
/** Device identifier for the client */
device_id?: string | undefined
/** SWE-bench fields */
swe_bench_run_id?: string | undefined
swe_bench_instance_id?: string | undefined
swe_bench_task_id?: string | undefined
email?: string | undefined
/** Swarm/team agent identification for analytics attribution */
agent_id?: string | undefined
parent_session_id?: string | undefined
agent_type?: string | undefined
/** Claude-in-Slack context (only present for cis_* events) */
slack?: SlackContext | undefined
team_name?: string | undefined
skill_name?: string | undefined
plugin_name?: string | undefined
marketplace_name?: string | undefined
}
function createBaseGitHubActionsMetadata(): GitHubActionsMetadata {
return { actor_id: '', repository_id: '', repository_owner_id: '' }
}
export const GitHubActionsMetadata: MessageFns<GitHubActionsMetadata> = {
fromJSON(object: any): GitHubActionsMetadata {
return {
actor_id: isSet(object.actor_id)
? globalThis.String(object.actor_id)
: '',
repository_id: isSet(object.repository_id)
? globalThis.String(object.repository_id)
: '',
repository_owner_id: isSet(object.repository_owner_id)
? globalThis.String(object.repository_owner_id)
: '',
}
},
toJSON(message: GitHubActionsMetadata): unknown {
const obj: any = {}
if (message.actor_id !== undefined) {
obj.actor_id = message.actor_id
}
if (message.repository_id !== undefined) {
obj.repository_id = message.repository_id
}
if (message.repository_owner_id !== undefined) {
obj.repository_owner_id = message.repository_owner_id
}
return obj
},
create<I extends Exact<DeepPartial<GitHubActionsMetadata>, I>>(
base?: I,
): GitHubActionsMetadata {
return GitHubActionsMetadata.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<GitHubActionsMetadata>, I>>(
object: I,
): GitHubActionsMetadata {
const message = createBaseGitHubActionsMetadata()
message.actor_id = object.actor_id ?? ''
message.repository_id = object.repository_id ?? ''
message.repository_owner_id = object.repository_owner_id ?? ''
return message
},
}
function createBaseEnvironmentMetadata(): EnvironmentMetadata {
return {
platform: '',
node_version: '',
terminal: '',
package_managers: '',
runtimes: '',
is_running_with_bun: false,
is_ci: false,
is_claubbit: false,
is_github_action: false,
is_claude_code_action: false,
is_claude_ai_auth: false,
version: '',
github_event_name: '',
github_actions_runner_environment: '',
github_actions_runner_os: '',
github_action_ref: '',
wsl_version: '',
github_actions_metadata: undefined,
arch: '',
is_claude_code_remote: false,
remote_environment_type: '',
claude_code_container_id: '',
claude_code_remote_session_id: '',
tags: [],
deployment_environment: '',
is_conductor: false,
version_base: '',
coworker_type: '',
build_time: '',
is_local_agent_mode: false,
linux_distro_id: '',
linux_distro_version: '',
linux_kernel: '',
vcs: '',
platform_raw: '',
}
}
export const EnvironmentMetadata: MessageFns<EnvironmentMetadata> = {
fromJSON(object: any): EnvironmentMetadata {
return {
platform: isSet(object.platform)
? globalThis.String(object.platform)
: '',
node_version: isSet(object.node_version)
? globalThis.String(object.node_version)
: '',
terminal: isSet(object.terminal)
? globalThis.String(object.terminal)
: '',
package_managers: isSet(object.package_managers)
? globalThis.String(object.package_managers)
: '',
runtimes: isSet(object.runtimes)
? globalThis.String(object.runtimes)
: '',
is_running_with_bun: isSet(object.is_running_with_bun)
? globalThis.Boolean(object.is_running_with_bun)
: false,
is_ci: isSet(object.is_ci) ? globalThis.Boolean(object.is_ci) : false,
is_claubbit: isSet(object.is_claubbit)
? globalThis.Boolean(object.is_claubbit)
: false,
is_github_action: isSet(object.is_github_action)
? globalThis.Boolean(object.is_github_action)
: false,
is_claude_code_action: isSet(object.is_claude_code_action)
? globalThis.Boolean(object.is_claude_code_action)
: false,
is_claude_ai_auth: isSet(object.is_claude_ai_auth)
? globalThis.Boolean(object.is_claude_ai_auth)
: false,
version: isSet(object.version) ? globalThis.String(object.version) : '',
github_event_name: isSet(object.github_event_name)
? globalThis.String(object.github_event_name)
: '',
github_actions_runner_environment: isSet(
object.github_actions_runner_environment,
)
? globalThis.String(object.github_actions_runner_environment)
: '',
github_actions_runner_os: isSet(object.github_actions_runner_os)
? globalThis.String(object.github_actions_runner_os)
: '',
github_action_ref: isSet(object.github_action_ref)
? globalThis.String(object.github_action_ref)
: '',
wsl_version: isSet(object.wsl_version)
? globalThis.String(object.wsl_version)
: '',
github_actions_metadata: isSet(object.github_actions_metadata)
? GitHubActionsMetadata.fromJSON(object.github_actions_metadata)
: undefined,
arch: isSet(object.arch) ? globalThis.String(object.arch) : '',
is_claude_code_remote: isSet(object.is_claude_code_remote)
? globalThis.Boolean(object.is_claude_code_remote)
: false,
remote_environment_type: isSet(object.remote_environment_type)
? globalThis.String(object.remote_environment_type)
: '',
claude_code_container_id: isSet(object.claude_code_container_id)
? globalThis.String(object.claude_code_container_id)
: '',
claude_code_remote_session_id: isSet(object.claude_code_remote_session_id)
? globalThis.String(object.claude_code_remote_session_id)
: '',
tags: globalThis.Array.isArray(object?.tags)
? object.tags.map((e: any) => globalThis.String(e))
: [],
deployment_environment: isSet(object.deployment_environment)
? globalThis.String(object.deployment_environment)
: '',
is_conductor: isSet(object.is_conductor)
? globalThis.Boolean(object.is_conductor)
: false,
version_base: isSet(object.version_base)
? globalThis.String(object.version_base)
: '',
coworker_type: isSet(object.coworker_type)
? globalThis.String(object.coworker_type)
: '',
build_time: isSet(object.build_time)
? globalThis.String(object.build_time)
: '',
is_local_agent_mode: isSet(object.is_local_agent_mode)
? globalThis.Boolean(object.is_local_agent_mode)
: false,
linux_distro_id: isSet(object.linux_distro_id)
? globalThis.String(object.linux_distro_id)
: '',
linux_distro_version: isSet(object.linux_distro_version)
? globalThis.String(object.linux_distro_version)
: '',
linux_kernel: isSet(object.linux_kernel)
? globalThis.String(object.linux_kernel)
: '',
vcs: isSet(object.vcs) ? globalThis.String(object.vcs) : '',
platform_raw: isSet(object.platform_raw)
? globalThis.String(object.platform_raw)
: '',
}
},
toJSON(message: EnvironmentMetadata): unknown {
const obj: any = {}
if (message.platform !== undefined) {
obj.platform = message.platform
}
if (message.node_version !== undefined) {
obj.node_version = message.node_version
}
if (message.terminal !== undefined) {
obj.terminal = message.terminal
}
if (message.package_managers !== undefined) {
obj.package_managers = message.package_managers
}
if (message.runtimes !== undefined) {
obj.runtimes = message.runtimes
}
if (message.is_running_with_bun !== undefined) {
obj.is_running_with_bun = message.is_running_with_bun
}
if (message.is_ci !== undefined) {
obj.is_ci = message.is_ci
}
if (message.is_claubbit !== undefined) {
obj.is_claubbit = message.is_claubbit
}
if (message.is_github_action !== undefined) {
obj.is_github_action = message.is_github_action
}
if (message.is_claude_code_action !== undefined) {
obj.is_claude_code_action = message.is_claude_code_action
}
if (message.is_claude_ai_auth !== undefined) {
obj.is_claude_ai_auth = message.is_claude_ai_auth
}
if (message.version !== undefined) {
obj.version = message.version
}
if (message.github_event_name !== undefined) {
obj.github_event_name = message.github_event_name
}
if (message.github_actions_runner_environment !== undefined) {
obj.github_actions_runner_environment =
message.github_actions_runner_environment
}
if (message.github_actions_runner_os !== undefined) {
obj.github_actions_runner_os = message.github_actions_runner_os
}
if (message.github_action_ref !== undefined) {
obj.github_action_ref = message.github_action_ref
}
if (message.wsl_version !== undefined) {
obj.wsl_version = message.wsl_version
}
if (message.github_actions_metadata !== undefined) {
obj.github_actions_metadata = GitHubActionsMetadata.toJSON(
message.github_actions_metadata,
)
}
if (message.arch !== undefined) {
obj.arch = message.arch
}
if (message.is_claude_code_remote !== undefined) {
obj.is_claude_code_remote = message.is_claude_code_remote
}
if (message.remote_environment_type !== undefined) {
obj.remote_environment_type = message.remote_environment_type
}
if (message.claude_code_container_id !== undefined) {
obj.claude_code_container_id = message.claude_code_container_id
}
if (message.claude_code_remote_session_id !== undefined) {
obj.claude_code_remote_session_id = message.claude_code_remote_session_id
}
if (message.tags?.length) {
obj.tags = message.tags
}
if (message.deployment_environment !== undefined) {
obj.deployment_environment = message.deployment_environment
}
if (message.is_conductor !== undefined) {
obj.is_conductor = message.is_conductor
}
if (message.version_base !== undefined) {
obj.version_base = message.version_base
}
if (message.coworker_type !== undefined) {
obj.coworker_type = message.coworker_type
}
if (message.build_time !== undefined) {
obj.build_time = message.build_time
}
if (message.is_local_agent_mode !== undefined) {
obj.is_local_agent_mode = message.is_local_agent_mode
}
if (message.linux_distro_id !== undefined) {
obj.linux_distro_id = message.linux_distro_id
}
if (message.linux_distro_version !== undefined) {
obj.linux_distro_version = message.linux_distro_version
}
if (message.linux_kernel !== undefined) {
obj.linux_kernel = message.linux_kernel
}
if (message.vcs !== undefined) {
obj.vcs = message.vcs
}
if (message.platform_raw !== undefined) {
obj.platform_raw = message.platform_raw
}
return obj
},
create<I extends Exact<DeepPartial<EnvironmentMetadata>, I>>(
base?: I,
): EnvironmentMetadata {
return EnvironmentMetadata.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<EnvironmentMetadata>, I>>(
object: I,
): EnvironmentMetadata {
const message = createBaseEnvironmentMetadata()
message.platform = object.platform ?? ''
message.node_version = object.node_version ?? ''
message.terminal = object.terminal ?? ''
message.package_managers = object.package_managers ?? ''
message.runtimes = object.runtimes ?? ''
message.is_running_with_bun = object.is_running_with_bun ?? false
message.is_ci = object.is_ci ?? false
message.is_claubbit = object.is_claubbit ?? false
message.is_github_action = object.is_github_action ?? false
message.is_claude_code_action = object.is_claude_code_action ?? false
message.is_claude_ai_auth = object.is_claude_ai_auth ?? false
message.version = object.version ?? ''
message.github_event_name = object.github_event_name ?? ''
message.github_actions_runner_environment =
object.github_actions_runner_environment ?? ''
message.github_actions_runner_os = object.github_actions_runner_os ?? ''
message.github_action_ref = object.github_action_ref ?? ''
message.wsl_version = object.wsl_version ?? ''
message.github_actions_metadata =
object.github_actions_metadata !== undefined &&
object.github_actions_metadata !== null
? GitHubActionsMetadata.fromPartial(object.github_actions_metadata)
: undefined
message.arch = object.arch ?? ''
message.is_claude_code_remote = object.is_claude_code_remote ?? false
message.remote_environment_type = object.remote_environment_type ?? ''
message.claude_code_container_id = object.claude_code_container_id ?? ''
message.claude_code_remote_session_id =
object.claude_code_remote_session_id ?? ''
message.tags = object.tags?.map(e => e) || []
message.deployment_environment = object.deployment_environment ?? ''
message.is_conductor = object.is_conductor ?? false
message.version_base = object.version_base ?? ''
message.coworker_type = object.coworker_type ?? ''
message.build_time = object.build_time ?? ''
message.is_local_agent_mode = object.is_local_agent_mode ?? false
message.linux_distro_id = object.linux_distro_id ?? ''
message.linux_distro_version = object.linux_distro_version ?? ''
message.linux_kernel = object.linux_kernel ?? ''
message.vcs = object.vcs ?? ''
message.platform_raw = object.platform_raw ?? ''
return message
},
}
function createBaseSlackContext(): SlackContext {
return {
slack_team_id: '',
is_enterprise_install: false,
trigger: '',
creation_method: '',
}
}
export const SlackContext: MessageFns<SlackContext> = {
fromJSON(object: any): SlackContext {
return {
slack_team_id: isSet(object.slack_team_id)
? globalThis.String(object.slack_team_id)
: '',
is_enterprise_install: isSet(object.is_enterprise_install)
? globalThis.Boolean(object.is_enterprise_install)
: false,
trigger: isSet(object.trigger) ? globalThis.String(object.trigger) : '',
creation_method: isSet(object.creation_method)
? globalThis.String(object.creation_method)
: '',
}
},
toJSON(message: SlackContext): unknown {
const obj: any = {}
if (message.slack_team_id !== undefined) {
obj.slack_team_id = message.slack_team_id
}
if (message.is_enterprise_install !== undefined) {
obj.is_enterprise_install = message.is_enterprise_install
}
if (message.trigger !== undefined) {
obj.trigger = message.trigger
}
if (message.creation_method !== undefined) {
obj.creation_method = message.creation_method
}
return obj
},
create<I extends Exact<DeepPartial<SlackContext>, I>>(
base?: I,
): SlackContext {
return SlackContext.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<SlackContext>, I>>(
object: I,
): SlackContext {
const message = createBaseSlackContext()
message.slack_team_id = object.slack_team_id ?? ''
message.is_enterprise_install = object.is_enterprise_install ?? false
message.trigger = object.trigger ?? ''
message.creation_method = object.creation_method ?? ''
return message
},
}
function createBaseClaudeCodeInternalEvent(): ClaudeCodeInternalEvent {
return {
event_name: '',
client_timestamp: undefined,
model: '',
session_id: '',
user_type: '',
betas: '',
env: undefined,
entrypoint: '',
agent_sdk_version: '',
is_interactive: false,
client_type: '',
process: '',
additional_metadata: '',
auth: undefined,
server_timestamp: undefined,
event_id: '',
device_id: '',
swe_bench_run_id: '',
swe_bench_instance_id: '',
swe_bench_task_id: '',
email: '',
agent_id: '',
parent_session_id: '',
agent_type: '',
slack: undefined,
team_name: '',
skill_name: '',
plugin_name: '',
marketplace_name: '',
}
}
export const ClaudeCodeInternalEvent: MessageFns<ClaudeCodeInternalEvent> = {
fromJSON(object: any): ClaudeCodeInternalEvent {
return {
event_name: isSet(object.event_name)
? globalThis.String(object.event_name)
: '',
client_timestamp: isSet(object.client_timestamp)
? fromJsonTimestamp(object.client_timestamp)
: undefined,
model: isSet(object.model) ? globalThis.String(object.model) : '',
session_id: isSet(object.session_id)
? globalThis.String(object.session_id)
: '',
user_type: isSet(object.user_type)
? globalThis.String(object.user_type)
: '',
betas: isSet(object.betas) ? globalThis.String(object.betas) : '',
env: isSet(object.env)
? EnvironmentMetadata.fromJSON(object.env)
: undefined,
entrypoint: isSet(object.entrypoint)
? globalThis.String(object.entrypoint)
: '',
agent_sdk_version: isSet(object.agent_sdk_version)
? globalThis.String(object.agent_sdk_version)
: '',
is_interactive: isSet(object.is_interactive)
? globalThis.Boolean(object.is_interactive)
: false,
client_type: isSet(object.client_type)
? globalThis.String(object.client_type)
: '',
process: isSet(object.process) ? globalThis.String(object.process) : '',
additional_metadata: isSet(object.additional_metadata)
? globalThis.String(object.additional_metadata)
: '',
auth: isSet(object.auth)
? PublicApiAuth.fromJSON(object.auth)
: undefined,
server_timestamp: isSet(object.server_timestamp)
? fromJsonTimestamp(object.server_timestamp)
: undefined,
event_id: isSet(object.event_id)
? globalThis.String(object.event_id)
: '',
device_id: isSet(object.device_id)
? globalThis.String(object.device_id)
: '',
swe_bench_run_id: isSet(object.swe_bench_run_id)
? globalThis.String(object.swe_bench_run_id)
: '',
swe_bench_instance_id: isSet(object.swe_bench_instance_id)
? globalThis.String(object.swe_bench_instance_id)
: '',
swe_bench_task_id: isSet(object.swe_bench_task_id)
? globalThis.String(object.swe_bench_task_id)
: '',
email: isSet(object.email) ? globalThis.String(object.email) : '',
agent_id: isSet(object.agent_id)
? globalThis.String(object.agent_id)
: '',
parent_session_id: isSet(object.parent_session_id)
? globalThis.String(object.parent_session_id)
: '',
agent_type: isSet(object.agent_type)
? globalThis.String(object.agent_type)
: '',
slack: isSet(object.slack)
? SlackContext.fromJSON(object.slack)
: undefined,
team_name: isSet(object.team_name)
? globalThis.String(object.team_name)
: '',
skill_name: isSet(object.skill_name)
? globalThis.String(object.skill_name)
: '',
plugin_name: isSet(object.plugin_name)
? globalThis.String(object.plugin_name)
: '',
marketplace_name: isSet(object.marketplace_name)
? globalThis.String(object.marketplace_name)
: '',
}
},
toJSON(message: ClaudeCodeInternalEvent): unknown {
const obj: any = {}
if (message.event_name !== undefined) {
obj.event_name = message.event_name
}
if (message.client_timestamp !== undefined) {
obj.client_timestamp = message.client_timestamp.toISOString()
}
if (message.model !== undefined) {
obj.model = message.model
}
if (message.session_id !== undefined) {
obj.session_id = message.session_id
}
if (message.user_type !== undefined) {
obj.user_type = message.user_type
}
if (message.betas !== undefined) {
obj.betas = message.betas
}
if (message.env !== undefined) {
obj.env = EnvironmentMetadata.toJSON(message.env)
}
if (message.entrypoint !== undefined) {
obj.entrypoint = message.entrypoint
}
if (message.agent_sdk_version !== undefined) {
obj.agent_sdk_version = message.agent_sdk_version
}
if (message.is_interactive !== undefined) {
obj.is_interactive = message.is_interactive
}
if (message.client_type !== undefined) {
obj.client_type = message.client_type
}
if (message.process !== undefined) {
obj.process = message.process
}
if (message.additional_metadata !== undefined) {
obj.additional_metadata = message.additional_metadata
}
if (message.auth !== undefined) {
obj.auth = PublicApiAuth.toJSON(message.auth)
}
if (message.server_timestamp !== undefined) {
obj.server_timestamp = message.server_timestamp.toISOString()
}
if (message.event_id !== undefined) {
obj.event_id = message.event_id
}
if (message.device_id !== undefined) {
obj.device_id = message.device_id
}
if (message.swe_bench_run_id !== undefined) {
obj.swe_bench_run_id = message.swe_bench_run_id
}
if (message.swe_bench_instance_id !== undefined) {
obj.swe_bench_instance_id = message.swe_bench_instance_id
}
if (message.swe_bench_task_id !== undefined) {
obj.swe_bench_task_id = message.swe_bench_task_id
}
if (message.email !== undefined) {
obj.email = message.email
}
if (message.agent_id !== undefined) {
obj.agent_id = message.agent_id
}
if (message.parent_session_id !== undefined) {
obj.parent_session_id = message.parent_session_id
}
if (message.agent_type !== undefined) {
obj.agent_type = message.agent_type
}
if (message.slack !== undefined) {
obj.slack = SlackContext.toJSON(message.slack)
}
if (message.team_name !== undefined) {
obj.team_name = message.team_name
}
if (message.skill_name !== undefined) {
obj.skill_name = message.skill_name
}
if (message.plugin_name !== undefined) {
obj.plugin_name = message.plugin_name
}
if (message.marketplace_name !== undefined) {
obj.marketplace_name = message.marketplace_name
}
return obj
},
create<I extends Exact<DeepPartial<ClaudeCodeInternalEvent>, I>>(
base?: I,
): ClaudeCodeInternalEvent {
return ClaudeCodeInternalEvent.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<ClaudeCodeInternalEvent>, I>>(
object: I,
): ClaudeCodeInternalEvent {
const message = createBaseClaudeCodeInternalEvent()
message.event_name = object.event_name ?? ''
message.client_timestamp = object.client_timestamp ?? undefined
message.model = object.model ?? ''
message.session_id = object.session_id ?? ''
message.user_type = object.user_type ?? ''
message.betas = object.betas ?? ''
message.env =
object.env !== undefined && object.env !== null
? EnvironmentMetadata.fromPartial(object.env)
: undefined
message.entrypoint = object.entrypoint ?? ''
message.agent_sdk_version = object.agent_sdk_version ?? ''
message.is_interactive = object.is_interactive ?? false
message.client_type = object.client_type ?? ''
message.process = object.process ?? ''
message.additional_metadata = object.additional_metadata ?? ''
message.auth =
object.auth !== undefined && object.auth !== null
? PublicApiAuth.fromPartial(object.auth)
: undefined
message.server_timestamp = object.server_timestamp ?? undefined
message.event_id = object.event_id ?? ''
message.device_id = object.device_id ?? ''
message.swe_bench_run_id = object.swe_bench_run_id ?? ''
message.swe_bench_instance_id = object.swe_bench_instance_id ?? ''
message.swe_bench_task_id = object.swe_bench_task_id ?? ''
message.email = object.email ?? ''
message.agent_id = object.agent_id ?? ''
message.parent_session_id = object.parent_session_id ?? ''
message.agent_type = object.agent_type ?? ''
message.slack =
object.slack !== undefined && object.slack !== null
? SlackContext.fromPartial(object.slack)
: undefined
message.team_name = object.team_name ?? ''
message.skill_name = object.skill_name ?? ''
message.plugin_name = object.plugin_name ?? ''
message.marketplace_name = object.marketplace_name ?? ''
return message
},
}
type Builtin =
| Date
| Function
| Uint8Array
| string
| number
| boolean
| undefined
type DeepPartial<T> = T extends Builtin
? T
: T extends globalThis.Array<infer U>
? globalThis.Array<DeepPartial<U>>
: T extends ReadonlyArray<infer U>
? ReadonlyArray<DeepPartial<U>>
: T extends {}
? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>
type KeysOfUnion<T> = T extends T ? keyof T : never
type Exact<P, I extends P> = P extends Builtin
? P
: P & { [K in keyof P]: Exact<P[K], I[K]> } & {
[K in Exclude<keyof I, KeysOfUnion<P>>]: never
}
function fromTimestamp(t: Timestamp): Date {
let millis = (t.seconds || 0) * 1_000
millis += (t.nanos || 0) / 1_000_000
return new globalThis.Date(millis)
}
function fromJsonTimestamp(o: any): Date {
if (o instanceof globalThis.Date) {
return o
} else if (typeof o === 'string') {
return new globalThis.Date(o)
} else {
return fromTimestamp(Timestamp.fromJSON(o))
}
}
function isSet(value: any): boolean {
return value !== null && value !== undefined
}
interface MessageFns<T> {
fromJSON(object: any): T
toJSON(message: T): unknown
create<I extends Exact<DeepPartial<T>, I>>(base?: I): T
fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T
}
@@ -0,0 +1,100 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
// protoc-gen-ts_proto v2.6.1
// protoc unknown
// source: events_mono/common/v1/auth.proto
/* eslint-disable */
/** PublicApiAuth contains authentication context automatically injected by the API */
export interface PublicApiAuth {
account_id?: number | undefined
organization_uuid?: string | undefined
account_uuid?: string | undefined
}
function createBasePublicApiAuth(): PublicApiAuth {
return { account_id: 0, organization_uuid: '', account_uuid: '' }
}
export const PublicApiAuth: MessageFns<PublicApiAuth> = {
fromJSON(object: any): PublicApiAuth {
return {
account_id: isSet(object.account_id)
? globalThis.Number(object.account_id)
: 0,
organization_uuid: isSet(object.organization_uuid)
? globalThis.String(object.organization_uuid)
: '',
account_uuid: isSet(object.account_uuid)
? globalThis.String(object.account_uuid)
: '',
}
},
toJSON(message: PublicApiAuth): unknown {
const obj: any = {}
if (message.account_id !== undefined) {
obj.account_id = Math.round(message.account_id)
}
if (message.organization_uuid !== undefined) {
obj.organization_uuid = message.organization_uuid
}
if (message.account_uuid !== undefined) {
obj.account_uuid = message.account_uuid
}
return obj
},
create<I extends Exact<DeepPartial<PublicApiAuth>, I>>(
base?: I,
): PublicApiAuth {
return PublicApiAuth.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<PublicApiAuth>, I>>(
object: I,
): PublicApiAuth {
const message = createBasePublicApiAuth()
message.account_id = object.account_id ?? 0
message.organization_uuid = object.organization_uuid ?? ''
message.account_uuid = object.account_uuid ?? ''
return message
},
}
type Builtin =
| Date
| Function
| Uint8Array
| string
| number
| boolean
| undefined
type DeepPartial<T> = T extends Builtin
? T
: T extends globalThis.Array<infer U>
? globalThis.Array<DeepPartial<U>>
: T extends ReadonlyArray<infer U>
? ReadonlyArray<DeepPartial<U>>
: T extends {}
? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>
type KeysOfUnion<T> = T extends T ? keyof T : never
type Exact<P, I extends P> = P extends Builtin
? P
: P & { [K in keyof P]: Exact<P[K], I[K]> } & {
[K in Exclude<keyof I, KeysOfUnion<P>>]: never
}
function isSet(value: any): boolean {
return value !== null && value !== undefined
}
interface MessageFns<T> {
fromJSON(object: any): T
toJSON(message: T): unknown
create<I extends Exact<DeepPartial<T>, I>>(base?: I): T
fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T
}
@@ -0,0 +1,223 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
// protoc-gen-ts_proto v2.6.1
// protoc unknown
// source: events_mono/growthbook/v1/growthbook_experiment_event.proto
/* eslint-disable */
import { Timestamp } from '../../../google/protobuf/timestamp.js'
import { PublicApiAuth } from '../../common/v1/auth.js'
/**
* GrowthBook experiment assignment event
* This event tracks when a user is exposed to an experiment variant
* See: https://docs.growthbook.io/guide/bigquery
*/
export interface GrowthbookExperimentEvent {
/** Unique event identifier (for deduplication) */
event_id?: string | undefined
/** When user was exposed to experiment (maps to GrowthBook's timestamp column) */
timestamp?: Date | undefined
/** Experiment tracking key (maps to GrowthBook's experiment_id column) */
experiment_id?: string | undefined
/** Variation index: 0=control, 1+=variants (maps to GrowthBook's variation_id column) */
variation_id?: number | undefined
/** Environment where assignment occurred */
environment?: string | undefined
/** User attributes at time of assignment */
user_attributes?: string | undefined
/** Experiment metadata */
experiment_metadata?: string | undefined
/** Device identifier for the client */
device_id?: string | undefined
/** Authentication context automatically injected by the API */
auth?: PublicApiAuth | undefined
/** Session identifier for tracking user sessions */
session_id?: string | undefined
/** Anonymous identifier for unauthenticated users */
anonymous_id?: string | undefined
/** Event metadata variables (automatically populated by internal-tools-common event_logging library) */
event_metadata_vars?: string | undefined
}
function createBaseGrowthbookExperimentEvent(): GrowthbookExperimentEvent {
return {
event_id: '',
timestamp: undefined,
experiment_id: '',
variation_id: 0,
environment: '',
user_attributes: '',
experiment_metadata: '',
device_id: '',
auth: undefined,
session_id: '',
anonymous_id: '',
event_metadata_vars: '',
}
}
export const GrowthbookExperimentEvent: MessageFns<GrowthbookExperimentEvent> =
{
fromJSON(object: any): GrowthbookExperimentEvent {
return {
event_id: isSet(object.event_id)
? globalThis.String(object.event_id)
: '',
timestamp: isSet(object.timestamp)
? fromJsonTimestamp(object.timestamp)
: undefined,
experiment_id: isSet(object.experiment_id)
? globalThis.String(object.experiment_id)
: '',
variation_id: isSet(object.variation_id)
? globalThis.Number(object.variation_id)
: 0,
environment: isSet(object.environment)
? globalThis.String(object.environment)
: '',
user_attributes: isSet(object.user_attributes)
? globalThis.String(object.user_attributes)
: '',
experiment_metadata: isSet(object.experiment_metadata)
? globalThis.String(object.experiment_metadata)
: '',
device_id: isSet(object.device_id)
? globalThis.String(object.device_id)
: '',
auth: isSet(object.auth)
? PublicApiAuth.fromJSON(object.auth)
: undefined,
session_id: isSet(object.session_id)
? globalThis.String(object.session_id)
: '',
anonymous_id: isSet(object.anonymous_id)
? globalThis.String(object.anonymous_id)
: '',
event_metadata_vars: isSet(object.event_metadata_vars)
? globalThis.String(object.event_metadata_vars)
: '',
}
},
toJSON(message: GrowthbookExperimentEvent): unknown {
const obj: any = {}
if (message.event_id !== undefined) {
obj.event_id = message.event_id
}
if (message.timestamp !== undefined) {
obj.timestamp = message.timestamp.toISOString()
}
if (message.experiment_id !== undefined) {
obj.experiment_id = message.experiment_id
}
if (message.variation_id !== undefined) {
obj.variation_id = Math.round(message.variation_id)
}
if (message.environment !== undefined) {
obj.environment = message.environment
}
if (message.user_attributes !== undefined) {
obj.user_attributes = message.user_attributes
}
if (message.experiment_metadata !== undefined) {
obj.experiment_metadata = message.experiment_metadata
}
if (message.device_id !== undefined) {
obj.device_id = message.device_id
}
if (message.auth !== undefined) {
obj.auth = PublicApiAuth.toJSON(message.auth)
}
if (message.session_id !== undefined) {
obj.session_id = message.session_id
}
if (message.anonymous_id !== undefined) {
obj.anonymous_id = message.anonymous_id
}
if (message.event_metadata_vars !== undefined) {
obj.event_metadata_vars = message.event_metadata_vars
}
return obj
},
create<I extends Exact<DeepPartial<GrowthbookExperimentEvent>, I>>(
base?: I,
): GrowthbookExperimentEvent {
return GrowthbookExperimentEvent.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<GrowthbookExperimentEvent>, I>>(
object: I,
): GrowthbookExperimentEvent {
const message = createBaseGrowthbookExperimentEvent()
message.event_id = object.event_id ?? ''
message.timestamp = object.timestamp ?? undefined
message.experiment_id = object.experiment_id ?? ''
message.variation_id = object.variation_id ?? 0
message.environment = object.environment ?? ''
message.user_attributes = object.user_attributes ?? ''
message.experiment_metadata = object.experiment_metadata ?? ''
message.device_id = object.device_id ?? ''
message.auth =
object.auth !== undefined && object.auth !== null
? PublicApiAuth.fromPartial(object.auth)
: undefined
message.session_id = object.session_id ?? ''
message.anonymous_id = object.anonymous_id ?? ''
message.event_metadata_vars = object.event_metadata_vars ?? ''
return message
},
}
type Builtin =
| Date
| Function
| Uint8Array
| string
| number
| boolean
| undefined
type DeepPartial<T> = T extends Builtin
? T
: T extends globalThis.Array<infer U>
? globalThis.Array<DeepPartial<U>>
: T extends ReadonlyArray<infer U>
? ReadonlyArray<DeepPartial<U>>
: T extends {}
? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>
type KeysOfUnion<T> = T extends T ? keyof T : never
type Exact<P, I extends P> = P extends Builtin
? P
: P & { [K in keyof P]: Exact<P[K], I[K]> } & {
[K in Exclude<keyof I, KeysOfUnion<P>>]: never
}
function fromTimestamp(t: Timestamp): Date {
let millis = (t.seconds || 0) * 1_000
millis += (t.nanos || 0) / 1_000_000
return new globalThis.Date(millis)
}
function fromJsonTimestamp(o: any): Date {
if (o instanceof globalThis.Date) {
return o
} else if (typeof o === 'string') {
return new globalThis.Date(o)
} else {
return fromTimestamp(Timestamp.fromJSON(o))
}
}
function isSet(value: any): boolean {
return value !== null && value !== undefined
}
interface MessageFns<T> {
fromJSON(object: any): T
toJSON(message: T): unknown
create<I extends Exact<DeepPartial<T>, I>>(base?: I): T
fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T
}
@@ -0,0 +1,187 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
// protoc-gen-ts_proto v2.6.1
// protoc unknown
// source: google/protobuf/timestamp.proto
/* eslint-disable */
/**
* A Timestamp represents a point in time independent of any time zone or local
* calendar, encoded as a count of seconds and fractions of seconds at
* nanosecond resolution. The count is relative to an epoch at UTC midnight on
* January 1, 1970, in the proleptic Gregorian calendar which extends the
* Gregorian calendar backwards to year one.
*
* All minutes are 60 seconds long. Leap seconds are "smeared" so that no leap
* second table is needed for interpretation, using a [24-hour linear
* smear](https://developers.google.com/time/smear).
*
* The range is from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. By
* restricting to that range, we ensure that we can convert to and from [RFC
* 3339](https://www.ietf.org/rfc/rfc3339.txt) date strings.
*
* # Examples
*
* Example 1: Compute Timestamp from POSIX `time()`.
*
* Timestamp timestamp;
* timestamp.set_seconds(time(NULL));
* timestamp.set_nanos(0);
*
* Example 2: Compute Timestamp from POSIX `gettimeofday()`.
*
* struct timeval tv;
* gettimeofday(&tv, NULL);
*
* Timestamp timestamp;
* timestamp.set_seconds(tv.tv_sec);
* timestamp.set_nanos(tv.tv_usec * 1000);
*
* Example 3: Compute Timestamp from Win32 `GetSystemTimeAsFileTime()`.
*
* FILETIME ft;
* GetSystemTimeAsFileTime(&ft);
* UINT64 ticks = (((UINT64)ft.dwHighDateTime) << 32) | ft.dwLowDateTime;
*
* // A Windows tick is 100 nanoseconds. Windows epoch 1601-01-01T00:00:00Z
* // is 11644473600 seconds before Unix epoch 1970-01-01T00:00:00Z.
* Timestamp timestamp;
* timestamp.set_seconds((INT64) ((ticks / 10000000) - 11644473600LL));
* timestamp.set_nanos((INT32) ((ticks % 10000000) * 100));
*
* Example 4: Compute Timestamp from Java `System.currentTimeMillis()`.
*
* long millis = System.currentTimeMillis();
*
* Timestamp timestamp = Timestamp.newBuilder().setSeconds(millis / 1000)
* .setNanos((int) ((millis % 1000) * 1000000)).build();
*
* Example 5: Compute Timestamp from Java `Instant.now()`.
*
* Instant now = Instant.now();
*
* Timestamp timestamp =
* Timestamp.newBuilder().setSeconds(now.getEpochSecond())
* .setNanos(now.getNano()).build();
*
* Example 6: Compute Timestamp from current time in Python.
*
* timestamp = Timestamp()
* timestamp.GetCurrentTime()
*
* # JSON Mapping
*
* In JSON format, the Timestamp type is encoded as a string in the
* [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format. That is, the
* format is "{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z"
* where {year} is always expressed using four digits while {month}, {day},
* {hour}, {min}, and {sec} are zero-padded to two digits each. The fractional
* seconds, which can go up to 9 digits (i.e. up to 1 nanosecond resolution),
* are optional. The "Z" suffix indicates the timezone ("UTC"); the timezone
* is required. A proto3 JSON serializer should always use UTC (as indicated by
* "Z") when printing the Timestamp type and a proto3 JSON parser should be
* able to accept both UTC and other timezones (as indicated by an offset).
*
* For example, "2017-01-15T01:30:15.01Z" encodes 15.01 seconds past
* 01:30 UTC on January 15, 2017.
*
* In JavaScript, one can convert a Date object to this format using the
* standard
* [toISOString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString)
* method. In Python, a standard `datetime.datetime` object can be converted
* to this format using
* [`strftime`](https://docs.python.org/2/library/time.html#time.strftime) with
* the time format spec '%Y-%m-%dT%H:%M:%S.%fZ'. Likewise, in Java, one can use
* the Joda Time's [`ISODateTimeFormat.dateTime()`](
* http://joda-time.sourceforge.net/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTime()
* ) to obtain a formatter capable of generating timestamps in this format.
*/
export interface Timestamp {
/**
* Represents seconds of UTC time since Unix epoch
* 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to
* 9999-12-31T23:59:59Z inclusive.
*/
seconds?: number | undefined
/**
* Non-negative fractions of a second at nanosecond resolution. Negative
* second values with fractions must still have non-negative nanos values
* that count forward in time. Must be from 0 to 999,999,999
* inclusive.
*/
nanos?: number | undefined
}
function createBaseTimestamp(): Timestamp {
return { seconds: 0, nanos: 0 }
}
export const Timestamp: MessageFns<Timestamp> = {
fromJSON(object: any): Timestamp {
return {
seconds: isSet(object.seconds) ? globalThis.Number(object.seconds) : 0,
nanos: isSet(object.nanos) ? globalThis.Number(object.nanos) : 0,
}
},
toJSON(message: Timestamp): unknown {
const obj: any = {}
if (message.seconds !== undefined) {
obj.seconds = Math.round(message.seconds)
}
if (message.nanos !== undefined) {
obj.nanos = Math.round(message.nanos)
}
return obj
},
create<I extends Exact<DeepPartial<Timestamp>, I>>(base?: I): Timestamp {
return Timestamp.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<Timestamp>, I>>(
object: I,
): Timestamp {
const message = createBaseTimestamp()
message.seconds = object.seconds ?? 0
message.nanos = object.nanos ?? 0
return message
},
}
type Builtin =
| Date
| Function
| Uint8Array
| string
| number
| boolean
| undefined
type DeepPartial<T> = T extends Builtin
? T
: T extends globalThis.Array<infer U>
? globalThis.Array<DeepPartial<U>>
: T extends ReadonlyArray<infer U>
? ReadonlyArray<DeepPartial<U>>
: T extends {}
? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>
type KeysOfUnion<T> = T extends T ? keyof T : never
type Exact<P, I extends P> = P extends Builtin
? P
: P & { [K in keyof P]: Exact<P[K], I[K]> } & {
[K in Exclude<keyof I, KeysOfUnion<P>>]: never
}
function isSet(value: any): boolean {
return value !== null && value !== undefined
}
interface MessageFns<T> {
fromJSON(object: any): T
toJSON(message: T): unknown
create<I extends Exact<DeepPartial<T>, I>>(base?: I): T
fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T
}
+290
View File
@@ -0,0 +1,290 @@
// biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
import { z } from 'zod/v4'
import { lazySchema } from '../utils/lazySchema.js'
import {
type HookEvent,
HOOK_EVENTS,
type HookInput,
type PermissionUpdate,
} from 'src/entrypoints/agentSdkTypes.js'
import type {
HookJSONOutput,
AsyncHookJSONOutput,
SyncHookJSONOutput,
} from 'src/entrypoints/agentSdkTypes.js'
import type { Message } from 'src/types/message.js'
import type { PermissionResult } from 'src/utils/permissions/PermissionResult.js'
import { permissionBehaviorSchema } from 'src/utils/permissions/PermissionRule.js'
import { permissionUpdateSchema } from 'src/utils/permissions/PermissionUpdateSchema.js'
import type { AppState } from '../state/AppState.js'
import type { AttributionState } from '../utils/commitAttribution.js'
export function isHookEvent(value: string): value is HookEvent {
return HOOK_EVENTS.includes(value as HookEvent)
}
// Prompt elicitation protocol types. The `prompt` key acts as discriminator
// (mirroring the {async:true} pattern), with the id as its value.
export const promptRequestSchema = lazySchema(() =>
z.object({
prompt: z.string(), // request id
message: z.string(),
options: z.array(
z.object({
key: z.string(),
label: z.string(),
description: z.string().optional(),
}),
),
}),
)
export type PromptRequest = z.infer<ReturnType<typeof promptRequestSchema>>
export type PromptResponse = {
prompt_response: string // request id
selected: string
}
// Sync hook response schema
export const syncHookResponseSchema = lazySchema(() =>
z.object({
continue: z
.boolean()
.describe('Whether Claude should continue after hook (default: true)')
.optional(),
suppressOutput: z
.boolean()
.describe('Hide stdout from transcript (default: false)')
.optional(),
stopReason: z
.string()
.describe('Message shown when continue is false')
.optional(),
decision: z.enum(['approve', 'block']).optional(),
reason: z.string().describe('Explanation for the decision').optional(),
systemMessage: z
.string()
.describe('Warning message shown to the user')
.optional(),
hookSpecificOutput: z
.union([
z.object({
hookEventName: z.literal('PreToolUse'),
permissionDecision: permissionBehaviorSchema().optional(),
permissionDecisionReason: z.string().optional(),
updatedInput: z.record(z.string(), z.unknown()).optional(),
additionalContext: z.string().optional(),
}),
z.object({
hookEventName: z.literal('UserPromptSubmit'),
additionalContext: z.string().optional(),
}),
z.object({
hookEventName: z.literal('SessionStart'),
additionalContext: z.string().optional(),
initialUserMessage: z.string().optional(),
watchPaths: z
.array(z.string())
.describe('Absolute paths to watch for FileChanged hooks')
.optional(),
}),
z.object({
hookEventName: z.literal('Setup'),
additionalContext: z.string().optional(),
}),
z.object({
hookEventName: z.literal('SubagentStart'),
additionalContext: z.string().optional(),
}),
z.object({
hookEventName: z.literal('PostToolUse'),
additionalContext: z.string().optional(),
updatedMCPToolOutput: z
.unknown()
.describe('Updates the output for MCP tools')
.optional(),
}),
z.object({
hookEventName: z.literal('PostToolUseFailure'),
additionalContext: z.string().optional(),
}),
z.object({
hookEventName: z.literal('PermissionDenied'),
retry: z.boolean().optional(),
}),
z.object({
hookEventName: z.literal('Notification'),
additionalContext: z.string().optional(),
}),
z.object({
hookEventName: z.literal('PermissionRequest'),
decision: z.union([
z.object({
behavior: z.literal('allow'),
updatedInput: z.record(z.string(), z.unknown()).optional(),
updatedPermissions: z.array(permissionUpdateSchema()).optional(),
}),
z.object({
behavior: z.literal('deny'),
message: z.string().optional(),
interrupt: z.boolean().optional(),
}),
]),
}),
z.object({
hookEventName: z.literal('Elicitation'),
action: z.enum(['accept', 'decline', 'cancel']).optional(),
content: z.record(z.string(), z.unknown()).optional(),
}),
z.object({
hookEventName: z.literal('ElicitationResult'),
action: z.enum(['accept', 'decline', 'cancel']).optional(),
content: z.record(z.string(), z.unknown()).optional(),
}),
z.object({
hookEventName: z.literal('CwdChanged'),
watchPaths: z
.array(z.string())
.describe('Absolute paths to watch for FileChanged hooks')
.optional(),
}),
z.object({
hookEventName: z.literal('FileChanged'),
watchPaths: z
.array(z.string())
.describe('Absolute paths to watch for FileChanged hooks')
.optional(),
}),
z.object({
hookEventName: z.literal('WorktreeCreate'),
worktreePath: z.string(),
}),
])
.optional(),
}),
)
// Zod schema for hook JSON output validation
export const hookJSONOutputSchema = lazySchema(() => {
// Async hook response schema
const asyncHookResponseSchema = z.object({
async: z.literal(true),
asyncTimeout: z.number().optional(),
})
return z.union([asyncHookResponseSchema, syncHookResponseSchema()])
})
// Infer the TypeScript type from the schema
type SchemaHookJSONOutput = z.infer<ReturnType<typeof hookJSONOutputSchema>>
// Type guard function to check if response is sync
export function isSyncHookJSONOutput(
json: HookJSONOutput,
): json is SyncHookJSONOutput {
return !('async' in json && json.async === true)
}
// Type guard function to check if response is async
export function isAsyncHookJSONOutput(
json: HookJSONOutput,
): json is AsyncHookJSONOutput {
return 'async' in json && json.async === true
}
// Compile-time assertion that SDK and Zod types match
import type { IsEqual } from 'type-fest'
type Assert<T extends true> = T
type _assertSDKTypesMatch = Assert<
IsEqual<SchemaHookJSONOutput, HookJSONOutput>
>
/** Context passed to callback hooks for state access */
export type HookCallbackContext = {
getAppState: () => AppState
updateAttributionState: (
updater: (prev: AttributionState) => AttributionState,
) => void
}
/** Hook that is a callback. */
export type HookCallback = {
type: 'callback'
callback: (
input: HookInput,
toolUseID: string | null,
abort: AbortSignal | undefined,
/** Hook index for SessionStart hooks to compute CLAUDE_ENV_FILE path */
hookIndex?: number,
/** Optional context for accessing app state */
context?: HookCallbackContext,
) => Promise<HookJSONOutput>
/** Timeout in seconds for this hook */
timeout?: number
/** Internal hooks (e.g. session file access analytics) are excluded from tengu_run_hook metrics */
internal?: boolean
}
export type HookCallbackMatcher = {
matcher?: string
hooks: HookCallback[]
pluginName?: string
}
export type HookProgress = {
type: 'hook_progress'
hookEvent: HookEvent
hookName: string
command: string
promptText?: string
statusMessage?: string
}
export type HookBlockingError = {
blockingError: string
command: string
}
export type PermissionRequestResult =
| {
behavior: 'allow'
updatedInput?: Record<string, unknown>
updatedPermissions?: PermissionUpdate[]
}
| {
behavior: 'deny'
message?: string
interrupt?: boolean
}
export type HookResult = {
message?: Message
systemMessage?: Message
blockingError?: HookBlockingError
outcome: 'success' | 'blocking' | 'non_blocking_error' | 'cancelled'
preventContinuation?: boolean
stopReason?: string
permissionBehavior?: 'ask' | 'deny' | 'allow' | 'passthrough'
hookPermissionDecisionReason?: string
additionalContext?: string
initialUserMessage?: string
updatedInput?: Record<string, unknown>
updatedMCPToolOutput?: unknown
permissionRequestResult?: PermissionRequestResult
retry?: boolean
}
export type AggregatedHookResult = {
message?: Message
blockingErrors?: HookBlockingError[]
preventContinuation?: boolean
stopReason?: string
hookPermissionDecisionReason?: string
permissionBehavior?: PermissionResult['behavior']
additionalContexts?: string[]
initialUserMessage?: string
updatedInput?: Record<string, unknown>
updatedMCPToolOutput?: unknown
permissionRequestResult?: PermissionRequestResult
retry?: boolean
}
+44
View File
@@ -0,0 +1,44 @@
/**
* Branded types for session and agent IDs.
* These prevent accidentally mixing up session IDs and agent IDs at compile time.
*/
/**
* A session ID uniquely identifies a Claude Code session.
* Returned by getSessionId().
*/
export type SessionId = string & { readonly __brand: 'SessionId' }
/**
* An agent ID uniquely identifies a subagent within a session.
* Returned by createAgentId().
* When present, indicates the context is a subagent (not the main session).
*/
export type AgentId = string & { readonly __brand: 'AgentId' }
/**
* Cast a raw string to SessionId.
* Use sparingly - prefer getSessionId() when possible.
*/
export function asSessionId(id: string): SessionId {
return id as SessionId
}
/**
* Cast a raw string to AgentId.
* Use sparingly - prefer createAgentId() when possible.
*/
export function asAgentId(id: string): AgentId {
return id as AgentId
}
const AGENT_ID_PATTERN = /^a(?:.+-)?[0-9a-f]{16}$/
/**
* Validate and brand a string as AgentId.
* Matches the format produced by createAgentId(): `a` + optional `<label>-` + 16 hex chars.
* Returns null if the string doesn't match (e.g. teammate names, team-addressing).
*/
export function toAgentId(s: string): AgentId | null {
return AGENT_ID_PATTERN.test(s) ? (s as AgentId) : null
}
+330
View File
@@ -0,0 +1,330 @@
import type { UUID } from 'crypto'
import type { FileHistorySnapshot } from 'src/utils/fileHistory.js'
import type { ContentReplacementRecord } from 'src/utils/toolResultStorage.js'
import type { AgentId } from './ids.js'
import type { Message } from './message.js'
import type { QueueOperationMessage } from './messageQueueTypes.js'
export type SerializedMessage = Message & {
cwd: string
userType: string
entrypoint?: string // CLAUDE_CODE_ENTRYPOINT — distinguishes cli/sdk-ts/sdk-py/etc.
sessionId: string
timestamp: string
version: string
gitBranch?: string
slug?: string // Session slug for files like plans (used for resume)
}
export type LogOption = {
date: string
messages: SerializedMessage[]
fullPath?: string
value: number
created: Date
modified: Date
firstPrompt: string
messageCount: number
fileSize?: number // File size in bytes (for display)
isSidechain: boolean
isLite?: boolean // True for lite logs (messages not loaded)
sessionId?: string // Session ID for lite logs
teamName?: string // Team name if this is a spawned agent session
agentName?: string // Agent's custom name (from /rename or swarm)
agentColor?: string // Agent's color (from /rename or swarm)
agentSetting?: string // Agent definition used (from --agent flag or settings.agent)
isTeammate?: boolean // Whether this session was created by a swarm teammate
leafUuid?: UUID // If given, this uuid must appear in the DB
summary?: string // Optional conversation summary
customTitle?: string // Optional user-set custom title
tag?: string // Optional tag for the session (searchable in /resume)
fileHistorySnapshots?: FileHistorySnapshot[] // Optional file history snapshots
attributionSnapshots?: AttributionSnapshotMessage[] // Optional attribution snapshots
contextCollapseCommits?: ContextCollapseCommitEntry[] // Ordered — commit B may reference commit A's summary
contextCollapseSnapshot?: ContextCollapseSnapshotEntry // Last-wins — staged queue + spawn state
gitBranch?: string // Git branch at the end of the session
projectPath?: string // Original project directory path
prNumber?: number // GitHub PR number linked to this session
prUrl?: string // Full URL to the linked PR
prRepository?: string // Repository in "owner/repo" format
mode?: 'coordinator' | 'normal' // Session mode for coordinator/normal detection
worktreeSession?: PersistedWorktreeSession | null // Worktree state at session end (null = exited, undefined = never entered)
contentReplacements?: ContentReplacementRecord[] // Replacement decisions for resume reconstruction
}
export type SummaryMessage = {
type: 'summary'
leafUuid: UUID
summary: string
}
export type CustomTitleMessage = {
type: 'custom-title'
sessionId: UUID
customTitle: string
}
/**
* AI-generated session title. Distinct from CustomTitleMessage so that:
* - User renames (custom-title) always win over AI titles in read preference
* - reAppendSessionMetadata never re-appends AI titles (they're ephemeral/
* regeneratable; re-appending would clobber user renames on resume)
* - VS Code's onlyIfNoCustomTitle CAS check only matches user titles,
* allowing AI to overwrite its own previous AI title but not user titles
*/
export type AiTitleMessage = {
type: 'ai-title'
sessionId: UUID
aiTitle: string
}
export type LastPromptMessage = {
type: 'last-prompt'
sessionId: UUID
lastPrompt: string
}
/**
* Periodic fork-generated summary of what the agent is currently doing.
* Written every min(5 steps, 2min) by forking the main thread mid-turn so
* `claude ps` can show something more useful than the last user prompt
* (which is often "ok go" or "fix it").
*/
export type TaskSummaryMessage = {
type: 'task-summary'
sessionId: UUID
summary: string
timestamp: string
}
export type TagMessage = {
type: 'tag'
sessionId: UUID
tag: string
}
export type AgentNameMessage = {
type: 'agent-name'
sessionId: UUID
agentName: string
}
export type AgentColorMessage = {
type: 'agent-color'
sessionId: UUID
agentColor: string
}
export type AgentSettingMessage = {
type: 'agent-setting'
sessionId: UUID
agentSetting: string
}
/**
* PR link message stored in session transcript.
* Links a session to a GitHub pull request for tracking and navigation.
*/
export type PRLinkMessage = {
type: 'pr-link'
sessionId: UUID
prNumber: number
prUrl: string
prRepository: string // e.g., "owner/repo"
timestamp: string // ISO timestamp when linked
}
export type ModeEntry = {
type: 'mode'
sessionId: UUID
mode: 'coordinator' | 'normal'
}
/**
* Worktree session state persisted to the transcript for resume.
* Subset of WorktreeSession from utils/worktree.ts — excludes ephemeral
* fields (creationDurationMs, usedSparsePaths) that are only used for
* first-run analytics.
*/
export type PersistedWorktreeSession = {
originalCwd: string
worktreePath: string
worktreeName: string
worktreeBranch?: string
originalBranch?: string
originalHeadCommit?: string
sessionId: string
tmuxSessionName?: string
hookBased?: boolean
}
/**
* Records whether the session is currently inside a worktree created by
* EnterWorktree or --worktree. Last-wins: an enter writes the session,
* an exit writes null. On --resume, restored only if the worktreePath
* still exists on disk (the /exit dialog may have removed it).
*/
export type WorktreeStateEntry = {
type: 'worktree-state'
sessionId: UUID
worktreeSession: PersistedWorktreeSession | null
}
/**
* Records content blocks whose in-context representation was replaced with a
* smaller stub (the full content was persisted elsewhere). Replayed on resume
* for prompt cache stability. Written once per enforcement pass that replaces
* at least one block. When agentId is set, the record belongs to a subagent
* sidechain (AgentTool resume reads these); when absent, it's main-thread
* (/resume reads these).
*/
export type ContentReplacementEntry = {
type: 'content-replacement'
sessionId: UUID
agentId?: AgentId
replacements: ContentReplacementRecord[]
}
export type FileHistorySnapshotMessage = {
type: 'file-history-snapshot'
messageId: UUID
snapshot: FileHistorySnapshot
isSnapshotUpdate: boolean
}
/**
* Per-file attribution state tracking Claude's character contributions.
*/
export type FileAttributionState = {
contentHash: string // SHA-256 hash of file content
claudeContribution: number // Characters written by Claude
mtime: number // File modification time
}
/**
* Attribution snapshot message stored in session transcript.
* Tracks character-level contributions by Claude for commit attribution.
*/
export type AttributionSnapshotMessage = {
type: 'attribution-snapshot'
messageId: UUID
surface: string // Client surface (cli, ide, web, api)
fileStates: Record<string, FileAttributionState>
promptCount?: number // Total prompts in session
promptCountAtLastCommit?: number // Prompts at last commit
permissionPromptCount?: number // Total permission prompts shown
permissionPromptCountAtLastCommit?: number // Permission prompts at last commit
escapeCount?: number // Total ESC presses (cancelled permission prompts)
escapeCountAtLastCommit?: number // ESC presses at last commit
}
export type TranscriptMessage = SerializedMessage & {
parentUuid: UUID | null
logicalParentUuid?: UUID | null // Preserves logical parent when parentUuid is nullified for session breaks
isSidechain: boolean
gitBranch?: string
agentId?: string // Agent ID for sidechain transcripts to enable resuming agents
teamName?: string // Team name if this is a spawned agent session
agentName?: string // Agent's custom name (from /rename or swarm)
agentColor?: string // Agent's color (from /rename or swarm)
promptId?: string // Correlates with OTel prompt.id for user prompt messages
}
export type SpeculationAcceptMessage = {
type: 'speculation-accept'
timestamp: string
timeSavedMs: number
}
/**
* Persisted context-collapse commit. The archived messages themselves are
* NOT persisted — they're already in the transcript as ordinary user/
* assistant messages. We only persist enough to reconstruct the splice
* instruction (boundary uuids) and the summary placeholder (which is NOT
* in the transcript because it's never yielded to the REPL).
*
* On restore, the store reconstructs CommittedCollapse with archived=[];
* projectView lazily fills the archive the first time it finds the span.
*
* Discriminator is obfuscated to match the gate name. sessionStorage.ts
* isn't feature-gated (it's the generic transcript plumbing used by every
* entry type), so a descriptive string here would leak into external builds
* via the appendEntry dispatch / loadTranscriptFile parser even though
* nothing in an external build ever writes or reads this entry.
*/
export type ContextCollapseCommitEntry = {
type: 'marble-origami-commit'
sessionId: UUID
/** 16-digit collapse ID. Max across entries reseeds the ID counter. */
collapseId: string
/** The summary placeholder's uuid — registerSummary() needs it. */
summaryUuid: string
/** Full <collapsed id="...">text</collapsed> string for the placeholder. */
summaryContent: string
/** Plain summary text for ctx_inspect. */
summary: string
/** Span boundaries — projectView finds these in the resumed Message[]. */
firstArchivedUuid: string
lastArchivedUuid: string
}
/**
* Snapshot of the staged queue and spawn trigger state. Unlike commits
* (append-only, replay-all), snapshots are last-wins — only the most
* recent snapshot entry is applied on restore. Written after every
* ctx-agent spawn resolves (when staged contents may have changed).
*
* Staged boundaries are UUIDs (session-stable), not collapse IDs (which
* reset with the uuidToId bimap). Restoring a staged span issues fresh
* collapse IDs for those messages on the next decorate/display, but the
* span itself resolves correctly.
*/
export type ContextCollapseSnapshotEntry = {
type: 'marble-origami-snapshot'
sessionId: UUID
staged: Array<{
startUuid: string
endUuid: string
summary: string
risk: number
stagedAt: number
}>
/** Spawn trigger state — so the +interval clock picks up where it left off. */
armed: boolean
lastSpawnTokens: number
}
export type Entry =
| TranscriptMessage
| SummaryMessage
| CustomTitleMessage
| AiTitleMessage
| LastPromptMessage
| TaskSummaryMessage
| TagMessage
| AgentNameMessage
| AgentColorMessage
| AgentSettingMessage
| PRLinkMessage
| FileHistorySnapshotMessage
| AttributionSnapshotMessage
| QueueOperationMessage
| SpeculationAcceptMessage
| ModeEntry
| WorktreeStateEntry
| ContentReplacementEntry
| ContextCollapseCommitEntry
| ContextCollapseSnapshotEntry
export function sortLogs(logs: LogOption[]): LogOption[] {
return logs.sort((a, b) => {
// Sort by modified date (newest first)
const modifiedDiff = b.modified.getTime() - a.modified.getTime()
if (modifiedDiff !== 0) {
return modifiedDiff
}
// If modified dates are equal, sort by created date (newest first)
return b.created.getTime() - a.created.getTime()
})
}
+441
View File
@@ -0,0 +1,441 @@
/**
* Pure permission type definitions extracted to break import cycles.
*
* This file contains only type definitions and constants with no runtime dependencies.
* Implementation files remain in src/utils/permissions/ but can now import from here
* to avoid circular dependencies.
*/
import { feature } from 'bun:bundle'
import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/messages.mjs'
// ============================================================================
// Permission Modes
// ============================================================================
export const EXTERNAL_PERMISSION_MODES = [
'acceptEdits',
'bypassPermissions',
'default',
'dontAsk',
'plan',
] as const
export type ExternalPermissionMode = (typeof EXTERNAL_PERMISSION_MODES)[number]
// Exhaustive mode union for typechecking. The user-addressable runtime set
// is INTERNAL_PERMISSION_MODES below.
export type InternalPermissionMode = ExternalPermissionMode | 'auto' | 'bubble'
export type PermissionMode = InternalPermissionMode
// Runtime validation set: modes that are user-addressable (settings.json
// defaultMode, --permission-mode CLI flag, conversation recovery).
export const INTERNAL_PERMISSION_MODES = [
...EXTERNAL_PERMISSION_MODES,
...(feature('TRANSCRIPT_CLASSIFIER') ? (['auto'] as const) : ([] as const)),
] as const satisfies readonly PermissionMode[]
export const PERMISSION_MODES = INTERNAL_PERMISSION_MODES
// ============================================================================
// Permission Behaviors
// ============================================================================
export type PermissionBehavior = 'allow' | 'deny' | 'ask'
// ============================================================================
// Permission Rules
// ============================================================================
/**
* Where a permission rule originated from.
* Includes all SettingSource values plus additional rule-specific sources.
*/
export type PermissionRuleSource =
| 'userSettings'
| 'projectSettings'
| 'localSettings'
| 'flagSettings'
| 'policySettings'
| 'cliArg'
| 'command'
| 'session'
/**
* The value of a permission rule - specifies which tool and optional content
*/
export type PermissionRuleValue = {
toolName: string
ruleContent?: string
}
/**
* A permission rule with its source and behavior
*/
export type PermissionRule = {
source: PermissionRuleSource
ruleBehavior: PermissionBehavior
ruleValue: PermissionRuleValue
}
// ============================================================================
// Permission Updates
// ============================================================================
/**
* Where a permission update should be persisted
*/
export type PermissionUpdateDestination =
| 'userSettings'
| 'projectSettings'
| 'localSettings'
| 'session'
| 'cliArg'
/**
* Update operations for permission configuration
*/
export type PermissionUpdate =
| {
type: 'addRules'
destination: PermissionUpdateDestination
rules: PermissionRuleValue[]
behavior: PermissionBehavior
}
| {
type: 'replaceRules'
destination: PermissionUpdateDestination
rules: PermissionRuleValue[]
behavior: PermissionBehavior
}
| {
type: 'removeRules'
destination: PermissionUpdateDestination
rules: PermissionRuleValue[]
behavior: PermissionBehavior
}
| {
type: 'setMode'
destination: PermissionUpdateDestination
mode: ExternalPermissionMode
}
| {
type: 'addDirectories'
destination: PermissionUpdateDestination
directories: string[]
}
| {
type: 'removeDirectories'
destination: PermissionUpdateDestination
directories: string[]
}
/**
* Source of an additional working directory permission.
* Note: This is currently the same as PermissionRuleSource but kept as a
* separate type for semantic clarity and potential future divergence.
*/
export type WorkingDirectorySource = PermissionRuleSource
/**
* An additional directory included in permission scope
*/
export type AdditionalWorkingDirectory = {
path: string
source: WorkingDirectorySource
}
// ============================================================================
// Permission Decisions & Results
// ============================================================================
/**
* Minimal command shape for permission metadata.
* This is intentionally a subset of the full Command type to avoid import cycles.
* Only includes properties needed by permission-related components.
*/
export type PermissionCommandMetadata = {
name: string
description?: string
// Allow additional properties for forward compatibility
[key: string]: unknown
}
/**
* Metadata attached to permission decisions
*/
export type PermissionMetadata =
| { command: PermissionCommandMetadata }
| undefined
/**
* Result when permission is granted
*/
export type PermissionAllowDecision<
Input extends { [key: string]: unknown } = { [key: string]: unknown },
> = {
behavior: 'allow'
updatedInput?: Input
userModified?: boolean
decisionReason?: PermissionDecisionReason
toolUseID?: string
acceptFeedback?: string
contentBlocks?: ContentBlockParam[]
}
/**
* Metadata for a pending classifier check that will run asynchronously.
* Used to enable non-blocking allow classifier evaluation.
*/
export type PendingClassifierCheck = {
command: string
cwd: string
descriptions: string[]
}
/**
* Result when user should be prompted
*/
export type PermissionAskDecision<
Input extends { [key: string]: unknown } = { [key: string]: unknown },
> = {
behavior: 'ask'
message: string
updatedInput?: Input
decisionReason?: PermissionDecisionReason
suggestions?: PermissionUpdate[]
blockedPath?: string
metadata?: PermissionMetadata
/**
* If true, this ask decision was triggered by a bashCommandIsSafe_DEPRECATED security check
* for patterns that splitCommand_DEPRECATED could misparse (e.g. line continuations, shell-quote
* transformations). Used by bashToolHasPermission to block early before splitCommand_DEPRECATED
* transforms the command. Not set for simple newline compound commands.
*/
isBashSecurityCheckForMisparsing?: boolean
/**
* If set, an allow classifier check should be run asynchronously.
* The classifier may auto-approve the permission before the user responds.
*/
pendingClassifierCheck?: PendingClassifierCheck
/**
* Optional content blocks (e.g., images) to include alongside the rejection
* message in the tool result. Used when users paste images as feedback.
*/
contentBlocks?: ContentBlockParam[]
}
/**
* Result when permission is denied
*/
export type PermissionDenyDecision = {
behavior: 'deny'
message: string
decisionReason: PermissionDecisionReason
toolUseID?: string
}
/**
* A permission decision - allow, ask, or deny
*/
export type PermissionDecision<
Input extends { [key: string]: unknown } = { [key: string]: unknown },
> =
| PermissionAllowDecision<Input>
| PermissionAskDecision<Input>
| PermissionDenyDecision
/**
* Permission result with additional passthrough option
*/
export type PermissionResult<
Input extends { [key: string]: unknown } = { [key: string]: unknown },
> =
| PermissionDecision<Input>
| {
behavior: 'passthrough'
message: string
decisionReason?: PermissionDecision<Input>['decisionReason']
suggestions?: PermissionUpdate[]
blockedPath?: string
/**
* If set, an allow classifier check should be run asynchronously.
* The classifier may auto-approve the permission before the user responds.
*/
pendingClassifierCheck?: PendingClassifierCheck
}
/**
* Explanation of why a permission decision was made
*/
export type PermissionDecisionReason =
| {
type: 'rule'
rule: PermissionRule
}
| {
type: 'mode'
mode: PermissionMode
}
| {
type: 'subcommandResults'
reasons: Map<string, PermissionResult>
}
| {
type: 'permissionPromptTool'
permissionPromptToolName: string
toolResult: unknown
}
| {
type: 'hook'
hookName: string
hookSource?: string
reason?: string
}
| {
type: 'asyncAgent'
reason: string
}
| {
type: 'sandboxOverride'
reason: 'excludedCommand' | 'dangerouslyDisableSandbox'
}
| {
type: 'classifier'
classifier: string
reason: string
}
| {
type: 'workingDir'
reason: string
}
| {
type: 'safetyCheck'
reason: string
// When true, auto mode lets the classifier evaluate this instead of
// forcing a prompt. True for sensitive-file paths (.claude/, .git/,
// shell configs) — the classifier can see context and decide. False
// for Windows path bypass attempts and cross-machine bridge messages.
classifierApprovable: boolean
}
| {
type: 'other'
reason: string
}
// ============================================================================
// Bash Classifier Types
// ============================================================================
export type ClassifierResult = {
matches: boolean
matchedDescription?: string
confidence: 'high' | 'medium' | 'low'
reason: string
}
export type ClassifierBehavior = 'deny' | 'ask' | 'allow'
export type ClassifierUsage = {
inputTokens: number
outputTokens: number
cacheReadInputTokens: number
cacheCreationInputTokens: number
}
export type YoloClassifierResult = {
thinking?: string
shouldBlock: boolean
reason: string
unavailable?: boolean
/**
* API returned "prompt is too long" — the classifier transcript exceeded
* the context window. Deterministic (same transcript → same error), so
* callers should fall back to normal prompting rather than retry/fail-closed.
*/
transcriptTooLong?: boolean
/** The model used for this classifier call */
model: string
/** Token usage from the classifier API call (for overhead telemetry) */
usage?: ClassifierUsage
/** Duration of the classifier API call in ms */
durationMs?: number
/** Character lengths of the prompt components sent to the classifier */
promptLengths?: {
systemPrompt: number
toolCalls: number
userPrompts: number
}
/** Path where error prompts were dumped (only set when unavailable due to API error) */
errorDumpPath?: string
/** Which classifier stage produced the final decision (2-stage XML only) */
stage?: 'fast' | 'thinking'
/** Token usage from stage 1 (fast) when stage 2 was also run */
stage1Usage?: ClassifierUsage
/** Duration of stage 1 in ms when stage 2 was also run */
stage1DurationMs?: number
/**
* API request_id (req_xxx) for stage 1. Enables joining to server-side
* api_usage logs for cache-miss / routing attribution. Also used for the
* legacy 1-stage (tool_use) classifier — the single request goes here.
*/
stage1RequestId?: string
/**
* API message id (msg_xxx) for stage 1. Enables joining the
* tengu_auto_mode_decision analytics event to the classifier's actual
* prompt/completion in post-analysis.
*/
stage1MsgId?: string
/** Token usage from stage 2 (thinking) when stage 2 was run */
stage2Usage?: ClassifierUsage
/** Duration of stage 2 in ms when stage 2 was run */
stage2DurationMs?: number
/** API request_id for stage 2 (set whenever stage 2 ran) */
stage2RequestId?: string
/** API message id (msg_xxx) for stage 2 (set whenever stage 2 ran) */
stage2MsgId?: string
}
// ============================================================================
// Permission Explainer Types
// ============================================================================
export type RiskLevel = 'LOW' | 'MEDIUM' | 'HIGH'
export type PermissionExplanation = {
riskLevel: RiskLevel
explanation: string
reasoning: string
risk: string
}
// ============================================================================
// Tool Permission Context
// ============================================================================
/**
* Mapping of permission rules by their source
*/
export type ToolPermissionRulesBySource = {
[T in PermissionRuleSource]?: string[]
}
/**
* Context needed for permission checking in tools
* Note: Uses a simplified DeepImmutable approximation for this types-only file
*/
export type ToolPermissionContext = {
readonly mode: PermissionMode
readonly additionalWorkingDirectories: ReadonlyMap<
string,
AdditionalWorkingDirectory
>
readonly alwaysAllowRules: ToolPermissionRulesBySource
readonly alwaysDenyRules: ToolPermissionRulesBySource
readonly alwaysAskRules: ToolPermissionRulesBySource
readonly isBypassPermissionsModeAvailable: boolean
readonly strippedDangerousRules?: ToolPermissionRulesBySource
readonly shouldAvoidPermissionPrompts?: boolean
readonly awaitAutomatedChecksBeforeDialog?: boolean
readonly prePlanMode?: PermissionMode
}
+363
View File
@@ -0,0 +1,363 @@
import type { LspServerConfig } from '../services/lsp/types.js'
import type { McpServerConfig } from '../services/mcp/types.js'
import type { BundledSkillDefinition } from '../skills/bundledSkills.js'
import type {
CommandMetadata,
PluginAuthor,
PluginManifest,
} from '../utils/plugins/schemas.js'
import type { HooksSettings } from '../utils/settings/types.js'
export type { PluginAuthor, PluginManifest, CommandMetadata }
/**
* Definition for a built-in plugin that ships with the CLI.
* Built-in plugins appear in the /plugin UI and can be enabled/disabled by
* users (persisted to user settings).
*/
export type BuiltinPluginDefinition = {
/** Plugin name (used in `{name}@builtin` identifier) */
name: string
/** Description shown in the /plugin UI */
description: string
/** Optional version string */
version?: string
/** Skills provided by this plugin */
skills?: BundledSkillDefinition[]
/** Hooks provided by this plugin */
hooks?: HooksSettings
/** MCP servers provided by this plugin */
mcpServers?: Record<string, McpServerConfig>
/** Whether this plugin is available (e.g. based on system capabilities). Unavailable plugins are hidden entirely. */
isAvailable?: () => boolean
/** Default enabled state before the user sets a preference (defaults to true) */
defaultEnabled?: boolean
}
export type PluginRepository = {
url: string
branch: string
lastUpdated?: string
commitSha?: string
}
export type PluginConfig = {
repositories: Record<string, PluginRepository>
}
export type LoadedPlugin = {
name: string
manifest: PluginManifest
path: string
source: string
repository: string // Repository identifier, usually same as source
enabled?: boolean
isBuiltin?: boolean // true for built-in plugins that ship with the CLI
sha?: string // Git commit SHA for version pinning (from marketplace entry source)
commandsPath?: string
commandsPaths?: string[] // Additional command paths from manifest
commandsMetadata?: Record<string, CommandMetadata> // Metadata for named commands from object-mapping format
agentsPath?: string
agentsPaths?: string[] // Additional agent paths from manifest
skillsPath?: string
skillsPaths?: string[] // Additional skill paths from manifest
outputStylesPath?: string
outputStylesPaths?: string[] // Additional output style paths from manifest
hooksConfig?: HooksSettings
mcpServers?: Record<string, McpServerConfig>
lspServers?: Record<string, LspServerConfig>
settings?: Record<string, unknown>
}
export type PluginComponent =
| 'commands'
| 'agents'
| 'skills'
| 'hooks'
| 'output-styles'
/**
* Discriminated union of plugin error types.
* Each error type has specific contextual data for better debugging and user guidance.
*
* This replaces the previous string-based error matching approach with type-safe
* error handling that can't break when error messages change.
*
* IMPLEMENTATION STATUS:
* Currently used in production (2 types):
* - generic-error: Used for various plugin loading failures
* - plugin-not-found: Used when plugin not found in marketplace
*
* Planned for future use (10 types - see TODOs in pluginLoader.ts):
* - path-not-found, git-auth-failed, git-timeout, network-error
* - manifest-parse-error, manifest-validation-error
* - marketplace-not-found, marketplace-load-failed
* - mcp-config-invalid, hook-load-failed, component-load-failed
*
* These unused types support UI formatting and provide a clear roadmap for
* improving error specificity. They can be incrementally implemented as
* error creation sites are refactored.
*/
export type PluginError =
| {
type: 'path-not-found'
source: string
plugin?: string
path: string
component: PluginComponent
}
| {
type: 'git-auth-failed'
source: string
plugin?: string
gitUrl: string
authType: 'ssh' | 'https'
}
| {
type: 'git-timeout'
source: string
plugin?: string
gitUrl: string
operation: 'clone' | 'pull'
}
| {
type: 'network-error'
source: string
plugin?: string
url: string
details?: string
}
| {
type: 'manifest-parse-error'
source: string
plugin?: string
manifestPath: string
parseError: string
}
| {
type: 'manifest-validation-error'
source: string
plugin?: string
manifestPath: string
validationErrors: string[]
}
| {
type: 'plugin-not-found'
source: string
pluginId: string
marketplace: string
}
| {
type: 'marketplace-not-found'
source: string
marketplace: string
availableMarketplaces: string[]
}
| {
type: 'marketplace-load-failed'
source: string
marketplace: string
reason: string
}
| {
type: 'mcp-config-invalid'
source: string
plugin: string
serverName: string
validationError: string
}
| {
type: 'mcp-server-suppressed-duplicate'
source: string
plugin: string
serverName: string
duplicateOf: string
}
| {
type: 'lsp-config-invalid'
source: string
plugin: string
serverName: string
validationError: string
}
| {
type: 'hook-load-failed'
source: string
plugin: string
hookPath: string
reason: string
}
| {
type: 'component-load-failed'
source: string
plugin: string
component: PluginComponent
path: string
reason: string
}
| {
type: 'mcpb-download-failed'
source: string
plugin: string
url: string
reason: string
}
| {
type: 'mcpb-extract-failed'
source: string
plugin: string
mcpbPath: string
reason: string
}
| {
type: 'mcpb-invalid-manifest'
source: string
plugin: string
mcpbPath: string
validationError: string
}
| {
type: 'lsp-config-invalid'
source: string
plugin: string
serverName: string
validationError: string
}
| {
type: 'lsp-server-start-failed'
source: string
plugin: string
serverName: string
reason: string
}
| {
type: 'lsp-server-crashed'
source: string
plugin: string
serverName: string
exitCode: number | null
signal?: string
}
| {
type: 'lsp-request-timeout'
source: string
plugin: string
serverName: string
method: string
timeoutMs: number
}
| {
type: 'lsp-request-failed'
source: string
plugin: string
serverName: string
method: string
error: string
}
| {
type: 'marketplace-blocked-by-policy'
source: string
plugin?: string
marketplace: string
blockedByBlocklist?: boolean // true if blocked by blockedMarketplaces, false if not in strictKnownMarketplaces
allowedSources: string[] // Formatted source strings (e.g., "github:owner/repo")
}
| {
type: 'dependency-unsatisfied'
source: string
plugin: string
dependency: string
reason: 'not-enabled' | 'not-found'
}
| {
type: 'plugin-cache-miss'
source: string
plugin: string
installPath: string
}
| {
type: 'generic-error'
source: string
plugin?: string
error: string
}
export type PluginLoadResult = {
enabled: LoadedPlugin[]
disabled: LoadedPlugin[]
errors: PluginError[]
}
/**
* Helper function to get a display message from any PluginError
* Useful for logging and simple error displays
*/
export function getPluginErrorMessage(error: PluginError): string {
switch (error.type) {
case 'generic-error':
return error.error
case 'path-not-found':
return `Path not found: ${error.path} (${error.component})`
case 'git-auth-failed':
return `Git authentication failed (${error.authType}): ${error.gitUrl}`
case 'git-timeout':
return `Git ${error.operation} timeout: ${error.gitUrl}`
case 'network-error':
return `Network error: ${error.url}${error.details ? ` - ${error.details}` : ''}`
case 'manifest-parse-error':
return `Manifest parse error: ${error.parseError}`
case 'manifest-validation-error':
return `Manifest validation failed: ${error.validationErrors.join(', ')}`
case 'plugin-not-found':
return `Plugin ${error.pluginId} not found in marketplace ${error.marketplace}`
case 'marketplace-not-found':
return `Marketplace ${error.marketplace} not found`
case 'marketplace-load-failed':
return `Marketplace ${error.marketplace} failed to load: ${error.reason}`
case 'mcp-config-invalid':
return `MCP server ${error.serverName} invalid: ${error.validationError}`
case 'mcp-server-suppressed-duplicate': {
const dup = error.duplicateOf.startsWith('plugin:')
? `server provided by plugin "${error.duplicateOf.split(':')[1] ?? '?'}"`
: `already-configured "${error.duplicateOf}"`
return `MCP server "${error.serverName}" skipped — same command/URL as ${dup}`
}
case 'hook-load-failed':
return `Hook load failed: ${error.reason}`
case 'component-load-failed':
return `${error.component} load failed from ${error.path}: ${error.reason}`
case 'mcpb-download-failed':
return `Failed to download MCPB from ${error.url}: ${error.reason}`
case 'mcpb-extract-failed':
return `Failed to extract MCPB ${error.mcpbPath}: ${error.reason}`
case 'mcpb-invalid-manifest':
return `MCPB manifest invalid at ${error.mcpbPath}: ${error.validationError}`
case 'lsp-config-invalid':
return `Plugin "${error.plugin}" has invalid LSP server config for "${error.serverName}": ${error.validationError}`
case 'lsp-server-start-failed':
return `Plugin "${error.plugin}" failed to start LSP server "${error.serverName}": ${error.reason}`
case 'lsp-server-crashed':
if (error.signal) {
return `Plugin "${error.plugin}" LSP server "${error.serverName}" crashed with signal ${error.signal}`
}
return `Plugin "${error.plugin}" LSP server "${error.serverName}" crashed with exit code ${error.exitCode ?? 'unknown'}`
case 'lsp-request-timeout':
return `Plugin "${error.plugin}" LSP server "${error.serverName}" timed out on ${error.method} request after ${error.timeoutMs}ms`
case 'lsp-request-failed':
return `Plugin "${error.plugin}" LSP server "${error.serverName}" ${error.method} request failed: ${error.error}`
case 'marketplace-blocked-by-policy':
if (error.blockedByBlocklist) {
return `Marketplace '${error.marketplace}' is blocked by enterprise policy`
}
return `Marketplace '${error.marketplace}' is not in the allowed marketplace list`
case 'dependency-unsatisfied': {
const hint =
error.reason === 'not-enabled'
? 'disabled — enable it or remove the dependency'
: 'not found in any configured marketplace'
return `Dependency "${error.dependency}" is ${hint}`
}
case 'plugin-cache-miss':
return `Plugin "${error.plugin}" not cached at ${error.installPath} — run /plugins to refresh`
}
}
+387
View File
@@ -0,0 +1,387 @@
import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/messages.mjs'
import type { UUID } from 'crypto'
import type React from 'react'
import type { PermissionResult } from '../entrypoints/agentSdkTypes.js'
import type { Key } from '../ink.js'
import type { PastedContent } from '../utils/config.js'
import type { ImageDimensions } from '../utils/imageResizer.js'
import type { TextHighlight } from '../utils/textHighlighting.js'
import type { AgentId } from './ids.js'
import type { AssistantMessage, MessageOrigin } from './message.js'
/**
* Inline ghost text for mid-input command autocomplete
*/
export type InlineGhostText = {
/** The ghost text to display (e.g., "mit" for /commit) */
readonly text: string
/** The full command name (e.g., "commit") */
readonly fullCommand: string
/** Position in the input where the ghost text should appear */
readonly insertPosition: number
}
/**
* Base props for text input components
*/
export type BaseTextInputProps = {
/**
* Optional callback for handling history navigation on up arrow at start of input
*/
readonly onHistoryUp?: () => void
/**
* Optional callback for handling history navigation on down arrow at end of input
*/
readonly onHistoryDown?: () => void
/**
* Text to display when `value` is empty.
*/
readonly placeholder?: string
/**
* Allow multi-line input via line ending with backslash (default: `true`)
*/
readonly multiline?: boolean
/**
* Listen to user's input. Useful in case there are multiple input components
* at the same time and input must be "routed" to a specific component.
*/
readonly focus?: boolean
/**
* Replace all chars and mask the value. Useful for password inputs.
*/
readonly mask?: string
/**
* Whether to show cursor and allow navigation inside text input with arrow keys.
*/
readonly showCursor?: boolean
/**
* Highlight pasted text
*/
readonly highlightPastedText?: boolean
/**
* Value to display in a text input.
*/
readonly value: string
/**
* Function to call when value updates.
*/
readonly onChange: (value: string) => void
/**
* Function to call when `Enter` is pressed, where first argument is a value of the input.
*/
readonly onSubmit?: (value: string) => void
/**
* Function to call when Ctrl+C is pressed to exit.
*/
readonly onExit?: () => void
/**
* Optional callback to show exit message
*/
readonly onExitMessage?: (show: boolean, key?: string) => void
/**
* Optional callback to show custom message
*/
// readonly onMessage?: (show: boolean, message?: string) => void
/**
* Optional callback to reset history position
*/
readonly onHistoryReset?: () => void
/**
* Optional callback when input is cleared (e.g., double-escape)
*/
readonly onClearInput?: () => void
/**
* Number of columns to wrap text at
*/
readonly columns: number
/**
* Maximum visible lines for the input viewport. When the wrapped input
* exceeds this many lines, only lines around the cursor are rendered.
*/
readonly maxVisibleLines?: number
/**
* Optional callback when an image is pasted
*/
readonly onImagePaste?: (
base64Image: string,
mediaType?: string,
filename?: string,
dimensions?: ImageDimensions,
sourcePath?: string,
) => void
/**
* Optional callback when a large text (over 800 chars) is pasted
*/
readonly onPaste?: (text: string) => void
/**
* Callback when the pasting state changes
*/
readonly onIsPastingChange?: (isPasting: boolean) => void
/**
* Whether to disable cursor movement for up/down arrow keys
*/
readonly disableCursorMovementForUpDownKeys?: boolean
/**
* Skip the text-level double-press escape handler. Set this when a
* keybinding context (e.g. Autocomplete) owns escape — the keybinding's
* stopImmediatePropagation can't shield the text input because child
* effects register useInput listeners before parent effects.
*/
readonly disableEscapeDoublePress?: boolean
/**
* The offset of the cursor within the text
*/
readonly cursorOffset: number
/**
* Callback to set the offset of the cursor
*/
onChangeCursorOffset: (offset: number) => void
/**
* Optional hint text to display after command input
* Used for showing available arguments for commands
*/
readonly argumentHint?: string
/**
* Optional callback for undo functionality
*/
readonly onUndo?: () => void
/**
* Whether to render the text with dim color
*/
readonly dimColor?: boolean
/**
* Optional text highlights for search results or other highlighting
*/
readonly highlights?: TextHighlight[]
/**
* Optional custom React element to render as placeholder.
* When provided, overrides the standard `placeholder` string rendering.
*/
readonly placeholderElement?: React.ReactNode
/**
* Optional inline ghost text for mid-input command autocomplete
*/
readonly inlineGhostText?: InlineGhostText
/**
* Optional filter applied to raw input before key routing. Return the
* (possibly transformed) input string; returning '' for a non-empty
* input drops the event.
*/
readonly inputFilter?: (input: string, key: Key) => string
}
/**
* Extended props for VimTextInput
*/
export type VimTextInputProps = BaseTextInputProps & {
/**
* Initial vim mode to use
*/
readonly initialMode?: VimMode
/**
* Optional callback for mode changes
*/
readonly onModeChange?: (mode: VimMode) => void
}
/**
* Vim editor modes
*/
export type VimMode = 'INSERT' | 'NORMAL'
/**
* Common properties for input hook results
*/
export type BaseInputState = {
onInput: (input: string, key: Key) => void
renderedValue: string
offset: number
setOffset: (offset: number) => void
/** Cursor line (0-indexed) within the rendered text, accounting for wrapping. */
cursorLine: number
/** Cursor column (display-width) within the current line. */
cursorColumn: number
/** Character offset in the full text where the viewport starts (0 when no windowing). */
viewportCharOffset: number
/** Character offset in the full text where the viewport ends (text.length when no windowing). */
viewportCharEnd: number
// For paste handling
isPasting?: boolean
pasteState?: {
chunks: string[]
timeoutId: ReturnType<typeof setTimeout> | null
}
}
/**
* State for text input
*/
export type TextInputState = BaseInputState
/**
* State for vim input with mode
*/
export type VimInputState = BaseInputState & {
mode: VimMode
setMode: (mode: VimMode) => void
}
/**
* Input modes for the prompt
*/
export type PromptInputMode =
| 'bash'
| 'prompt'
| 'orphaned-permission'
| 'task-notification'
export type EditablePromptInputMode = Exclude<
PromptInputMode,
`${string}-notification`
>
/**
* Queue priority levels. Same semantics in both normal and proactive mode.
*
* - `now` — Interrupt and send immediately. Aborts any in-flight tool
* call (equivalent to Esc + send). Consumers (print.ts,
* REPL.tsx) subscribe to queue changes and abort when they
* see a 'now' command.
* - `next` — Mid-turn drain. Let the current tool call finish, then
* send this message between the tool result and the next API
* round-trip. Wakes an in-progress SleepTool call.
* - `later` — End-of-turn drain. Wait for the current turn to finish,
* then process as a new query. Wakes an in-progress SleepTool
* call (query.ts upgrades the drain threshold after sleep so
* the message is attached to the same turn).
*
* The SleepTool is only available in proactive mode, so "wakes SleepTool"
* is a no-op in normal mode.
*/
export type QueuePriority = 'now' | 'next' | 'later'
/**
* Queued command type
*/
export type QueuedCommand = {
value: string | Array<ContentBlockParam>
mode: PromptInputMode
/** Defaults to the priority implied by `mode` when enqueued. */
priority?: QueuePriority
uuid?: UUID
orphanedPermission?: OrphanedPermission
/** Raw pasted contents including images. Images are resized at execution time. */
pastedContents?: Record<number, PastedContent>
/**
* The input string before [Pasted text #N] placeholders were expanded.
* Used for ultraplan keyword detection so pasted content containing the
* keyword does not trigger a CCR session. Falls back to `value` when
* unset (bridge/UDS/MCP sources have no paste expansion).
*/
preExpansionValue?: string
/**
* When true, the input is treated as plain text even if it starts with `/`.
* Used for remotely-received messages (e.g. bridge/CCR) that should not
* trigger local slash commands or skills.
*/
skipSlashCommands?: boolean
/**
* When true, slash commands are dispatched but filtered through
* isBridgeSafeCommand() — 'local-jsx' and terminal-only commands return
* a helpful error instead of executing. Set by the Remote Control bridge
* inbound path so mobile/web clients can run skills and benign commands
* without re-exposing the PR #19134 bug (/model popping the local picker).
*/
bridgeOrigin?: boolean
/**
* When true, the resulting UserMessage gets `isMeta: true` — hidden in the
* transcript UI but visible to the model. Used by system-generated prompts
* (proactive ticks, teammate messages, resource updates) that route through
* the queue instead of calling `onQuery` directly.
*/
isMeta?: boolean
/**
* Provenance of this command. Stamped onto the resulting UserMessage so the
* transcript records origin structurally (not just via XML tags in content).
* undefined = human (keyboard).
*/
origin?: MessageOrigin
/**
* Workload tag threaded through to cc_workload= in the billing-header
* attribution block. The queue is the async boundary between the cron
* scheduler firing and the turn actually running — a user prompt can slip
* in between — so the tag rides on the QueuedCommand itself and is only
* hoisted into bootstrap state when THIS command is dequeued.
*/
workload?: string
/**
* Agent that should receive this notification. Undefined = main thread.
* Subagents run in-process and share the module-level command queue; the
* drain gate in query.ts filters by this field so a subagent's background
* task notifications don't leak into the coordinator's context (PR #18453
* unified the queue but lost the isolation the dual-queue accidentally had).
*/
agentId?: AgentId
}
/**
* Type guard for image PastedContent with non-empty data. Empty-content
* images (e.g. from a 0-byte file drag) yield empty base64 strings that
* the API rejects with `image cannot be empty`. Use this at every site
* that converts PastedContent → ImageBlockParam so the filter and the
* ID list stay in sync.
*/
export function isValidImagePaste(c: PastedContent): boolean {
return c.type === 'image' && c.content.length > 0
}
/** Extract image paste IDs from a QueuedCommand's pastedContents. */
export function getImagePasteIds(
pastedContents: Record<number, PastedContent> | undefined,
): number[] | undefined {
if (!pastedContents) {
return undefined
}
const ids = Object.values(pastedContents)
.filter(isValidImagePaste)
.map(c => c.id)
return ids.length > 0 ? ids : undefined
}
export type OrphanedPermission = {
permissionResult: PermissionResult
assistantMessage: AssistantMessage
}