chore: full Biome lint cleanup — zero errors, zero warnings

- Remove 203 unused biome-ignore suppression comments (noConsole rule is off)
- Apply all FIXABLE transforms: Math.pow->**, parseInt radix,
  noUselessContinue, noUselessUndefinedInitialization, useIndexOf,
  useRegexLiterals, useShorthandFunctionType, noPrototypeBuiltins
- Add targeted suppressions for 31 intentional patterns
- Format all src/ files via Biome (quote style, import line width)
- Result: 0 errors, 0 warnings across 2649 files
This commit is contained in:
unraid
2026-04-14 19:56:13 +08:00
parent ee369549a8
commit 4c409df35d
1556 changed files with 49552 additions and 61067 deletions

View File

@@ -41,7 +41,11 @@ import { type Tools, type ToolUseContext, toolMatchesName } from './Tool.js'
import type { AgentDefinition } from '@claude-code-best/builtin-tools/tools/AgentTool/loadAgentsDir.js'
import { SYNTHETIC_OUTPUT_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/SyntheticOutputTool/SyntheticOutputTool.js'
import type { APIError } from '@anthropic-ai/sdk'
import type { CompactMetadata, Message, SystemCompactBoundaryMessage } from './types/message.js'
import type {
CompactMetadata,
Message,
SystemCompactBoundaryMessage,
} from './types/message.js'
import type { OrphanedPermission } from './types/textInputTypes.js'
import { createAbortController } from './utils/abortController.js'
import type { AttributionState } from './utils/commitAttribution.js'
@@ -708,7 +712,8 @@ export class QueryEngine {
message.subtype === 'compact_boundary'
) {
const compactMsg = message as SystemCompactBoundaryMessage
const tailUuid = compactMsg.compactMetadata?.preservedSegment?.tailUuid
const tailUuid =
compactMsg.compactMetadata?.preservedSegment?.tailUuid
if (tailUuid) {
const tailIdx = this.mutableMessages.findLastIndex(
m => m.uuid === tailUuid,
@@ -768,7 +773,10 @@ export class QueryEngine {
// streamed responses, this is null at content_block_stop time;
// the real value arrives via message_delta (handled below).
const msg = message as Message
const stopReason = msg.message?.stop_reason as string | null | undefined
const stopReason = msg.message?.stop_reason as
| string
| null
| undefined
if (stopReason != null) {
lastStopReason = stopReason
}
@@ -798,11 +806,15 @@ export class QueryEngine {
break
}
case 'stream_event': {
const event = (message as unknown as { event: Record<string, unknown> }).event
const event = (
message as unknown as { event: Record<string, unknown> }
).event
if (event.type === 'message_start') {
// Reset current message usage for new message
currentMessageUsage = EMPTY_USAGE
const eventMessage = event.message as { usage: BetaMessageDeltaUsage }
const eventMessage = event.message as {
usage: BetaMessageDeltaUsage
}
currentMessageUsage = updateUsage(
currentMessageUsage,
eventMessage.usage,
@@ -851,7 +863,15 @@ export class QueryEngine {
void recordTranscript(messages)
}
const attachment = msg.attachment as { type: string; data?: unknown; turnCount?: number; maxTurns?: number; prompt?: string; source_uuid?: string; [key: string]: unknown }
const attachment = msg.attachment as {
type: string
data?: unknown
turnCount?: number
maxTurns?: number
prompt?: string
source_uuid?: string
[key: string]: unknown
}
// Extract structured output from StructuredOutput tool calls
if (attachment.type === 'structured_output') {
@@ -892,10 +912,7 @@ export class QueryEngine {
return
}
// Yield queued_command attachments as SDK user message replays
else if (
replayUserMessages &&
attachment.type === 'queued_command'
) {
else if (replayUserMessages && attachment.type === 'queued_command') {
yield {
type: 'user',
message: {
@@ -923,10 +940,7 @@ export class QueryEngine {
// never shrinks (memory leak in long SDK sessions). The subtype
// check lives inside the injected callback so feature-gated strings
// stay out of this file (excluded-strings check).
const snipResult = this.config.snipReplay?.(
msg,
this.mutableMessages,
)
const snipResult = this.config.snipReplay?.(msg, this.mutableMessages)
if (snipResult !== undefined) {
if (snipResult.executed) {
this.mutableMessages.length = 0
@@ -936,10 +950,7 @@ export class QueryEngine {
}
this.mutableMessages.push(msg)
// Yield compact boundary messages to SDK
if (
msg.subtype === 'compact_boundary' &&
msg.compactMetadata
) {
if (msg.subtype === 'compact_boundary' && msg.compactMetadata) {
const compactMsg = msg as SystemCompactBoundaryMessage
// Release pre-compaction messages for GC. The boundary was just
// pushed so it's the last element. query.ts already uses
@@ -959,11 +970,18 @@ export class QueryEngine {
subtype: 'compact_boundary' as const,
session_id: getSessionId(),
uuid: msg.uuid,
compact_metadata: toSDKCompactMetadata(compactMsg.compactMetadata),
compact_metadata: toSDKCompactMetadata(
compactMsg.compactMetadata,
),
}
}
if (msg.subtype === 'api_error') {
const apiErrorMsg = msg as Message & { retryAttempt: number; maxRetries: number; retryInMs: number; error: APIError }
const apiErrorMsg = msg as Message & {
retryAttempt: number
maxRetries: number
retryInMs: number
error: APIError
}
yield {
type: 'system',
subtype: 'api_retry' as const,
@@ -980,7 +998,10 @@ export class QueryEngine {
break
}
case 'tool_use_summary': {
const msg = message as Message & { summary: unknown; precedingToolUseIds: unknown }
const msg = message as Message & {
summary: unknown
precedingToolUseIds: unknown
}
// Yield tool use summary messages to SDK
yield {
type: 'tool_use_summary' as const,
@@ -1089,7 +1110,10 @@ export class QueryEngine {
const edeResultType = result?.type ?? 'undefined'
const edeLastContentType =
result?.type === 'assistant'
? (last(result.message!.content as import('@anthropic-ai/sdk/resources/beta/messages/messages.js').BetaContentBlock[])?.type ?? 'none')
? (last(
result.message!
.content as import('@anthropic-ai/sdk/resources/beta/messages/messages.js').BetaContentBlock[],
)?.type ?? 'none')
: 'n/a'
// Flush buffered transcript writes before yielding result.
@@ -1147,7 +1171,10 @@ export class QueryEngine {
let isApiError = false
if (result.type === 'assistant') {
const lastContent = last(result.message!.content as import('@anthropic-ai/sdk/resources/beta/messages/messages.js').BetaContentBlock[])
const lastContent = last(
result.message!
.content as import('@anthropic-ai/sdk/resources/beta/messages/messages.js').BetaContentBlock[],
)
if (
lastContent?.type === 'text' &&
!SYNTHETIC_MESSAGES.has(lastContent.text)

View File

@@ -10,7 +10,11 @@ import {
setSystemPromptInjection,
} from '../context'
import { clearMemoryFileCaches } from '../utils/claudemd'
import { cleanupTempDir, createTempDir, writeTempFile } from '../../tests/mocks/file-system'
import {
cleanupTempDir,
createTempDir,
writeTempFile,
} from '../../tests/mocks/file-system'
let tempDir = ''
let projectClaudeMdContent = ''

View File

@@ -1,3 +1,3 @@
// Auto-generated type stub — replace with real implementation
export type HookEvent = any;
export type ModelUsage = any;
export type HookEvent = any
export type ModelUsage = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type AgentColorName = any;
export type AgentColorName = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type HookCallbackMatcher = any;
export type HookCallbackMatcher = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type SessionId = any;
export type SessionId = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type randomUUID = any;
export type randomUUID = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type ModelSetting = any;
export type ModelSetting = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type ModelStrings = any;
export type ModelStrings = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type SettingSource = any;
export type SettingSource = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type resetSettingsCache = any;
export type resetSettingsCache = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type PluginHookMatcher = any;
export type PluginHookMatcher = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type createSignal = any;
export type createSignal = any

View File

@@ -1755,4 +1755,6 @@ export function getPromptId(): string | null {
export function setPromptId(id: string | null): void {
STATE.promptId = id
}
export function isReplBridgeActive(): boolean { return false; }
export function isReplBridgeActive(): boolean {
return false
}

View File

@@ -225,7 +225,9 @@ export function createBridgeApiClient(deps: BridgeApiDeps): BridgeApiClient {
)
handleErrorStatus(response.status, response.data, 'Poll')
rcLog(`poll response: status=${response.status} hasData=${!!response.data} url=${deps.baseUrl}`)
rcLog(
`poll response: status=${response.status} hasData=${!!response.data} url=${deps.baseUrl}`,
)
// Empty body or null = no work available
if (!response.data) {

View File

@@ -448,9 +448,11 @@ export async function runBridgeLoop(
): (status: SessionDoneStatus) => void {
return (rawStatus: SessionDoneStatus): void => {
const workId = sessionWorkIds.get(sessionId)
rcLog(`session done: sessionId=${sessionId} workId=${workId ?? 'none'} status=${rawStatus}` +
` wasTimedOut=${timedOutSessions.has(sessionId)} duration=${Math.round((Date.now() - startTime) / 1000)}s` +
` stderr=${handle.lastStderr.length > 0 ? handle.lastStderr.join('\\n').slice(0, 500) : '(none)'}`)
rcLog(
`session done: sessionId=${sessionId} workId=${workId ?? 'none'} status=${rawStatus}` +
` wasTimedOut=${timedOutSessions.has(sessionId)} duration=${Math.round((Date.now() - startTime) / 1000)}s` +
` stderr=${handle.lastStderr.length > 0 ? handle.lastStderr.join('\\n').slice(0, 500) : '(none)'}`,
)
activeSessions.delete(sessionId)
sessionStartTimes.delete(sessionId)
sessionWorkIds.delete(sessionId)
@@ -609,7 +611,9 @@ export async function runBridgeLoop(
const pollConfig = getPollIntervalConfig()
try {
rcLog(`poll: envId=${environmentId} activeSessions=${activeSessions.size}`)
rcLog(
`poll: envId=${environmentId} activeSessions=${activeSessions.size}`,
)
const work = await api.pollForWork(
environmentId,
environmentSecret,
@@ -864,7 +868,9 @@ export async function runBridgeLoop(
break
case 'session': {
const sessionId = work.data.id
rcLog(`work received: type=session sessionId=${sessionId} workId=${work.id}`)
rcLog(
`work received: type=session sessionId=${sessionId} workId=${work.id}`,
)
try {
validateBridgeId(sessionId, 'session_id')
} catch {
@@ -1032,9 +1038,9 @@ export async function runBridgeLoop(
rcLog(
`spawning session: sessionId=${sessionId} sdkUrl=${sdkUrl}` +
` useCcrV2=${useCcrV2} workerEpoch=${workerEpoch}` +
` dir=${sessionDir}` +
` accessToken=${secret.session_ingress_token ? secret.session_ingress_token.slice(0, 8) + '...' : 'NONE'}`,
` useCcrV2=${useCcrV2} workerEpoch=${workerEpoch}` +
` dir=${sessionDir}` +
` accessToken=${secret.session_ingress_token ? secret.session_ingress_token.slice(0, 8) + '...' : 'NONE'}`,
)
const spawnResult = safeSpawn(
spawner,
@@ -1281,8 +1287,8 @@ export async function runBridgeLoop(
const errMsg = describeAxiosError(err)
rcLog(
`poll error: ${errMsg}` +
` isConn=${isConnectionError(err)} isServer=${isServerError(err)}` +
` activeSessions=${activeSessions.size}`,
` isConn=${isConnectionError(err)} isServer=${isServerError(err)}` +
` activeSessions=${activeSessions.size}`,
)
if (isConnectionError(err) || isServerError(err)) {
@@ -1676,7 +1682,7 @@ async function stopWorkWithRetry(
}
const errMsg = errorMessage(err)
if (attempt < MAX_ATTEMPTS) {
const delay = addJitter(baseDelayMs * Math.pow(2, attempt - 1))
const delay = addJitter(baseDelayMs * 2 ** (attempt - 1))
logger.logVerbose(
`Failed to stop work ${workId} (attempt ${attempt}/${MAX_ATTEMPTS}), retrying in ${formatDelay(delay)}: ${errMsg}`,
)
@@ -1964,7 +1970,6 @@ NOTES
- You must be logged in with a Claude account that has a subscription
- Run \`claude\` first in the directory to accept the workspace trust dialog
${serverNote}`
// biome-ignore lint/suspicious/noConsole: intentional help output
console.log(help)
}
@@ -2003,7 +2008,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
return
}
if (parsed.error) {
// biome-ignore lint/suspicious/noConsole: intentional error output
console.error(`Error: ${parsed.error}`)
// eslint-disable-next-line custom-rules/no-process-exit
process.exit(1)
@@ -2042,7 +2046,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
const { PERMISSION_MODES } = await import('../types/permissions.js')
const valid: readonly string[] = PERMISSION_MODES
if (!valid.includes(permissionMode)) {
// biome-ignore lint/suspicious/noConsole: intentional error output
console.error(
`Error: Invalid permission mode '${permissionMode}'. Valid modes: ${valid.join(', ')}`,
)
@@ -2085,7 +2088,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
Promise.all([shutdown1PEventLogging(), shutdownDatadog()]),
sleep(500, undefined, { unref: true }),
]).catch(() => {})
// biome-ignore lint/suspicious/noConsole: intentional error output
console.error(
'Error: Multi-session Remote Control is not enabled for your account yet.',
)
@@ -2102,7 +2104,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
// The bridge bypasses main.tsx (which renders the interactive TrustDialog via showSetupScreens),
// so we must verify trust was previously established by a normal `claude` session.
if (!checkHasTrustDialogAccepted()) {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.error(
`Error: Workspace not trusted. Please run \`claude\` in ${dir} first to review and accept the workspace trust dialog.`,
)
@@ -2119,7 +2120,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
const bridgeToken = getBridgeAccessToken()
if (!bridgeToken) {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.error(BRIDGE_LOGIN_ERROR)
// eslint-disable-next-line custom-rules/no-process-exit
process.exit(1)
@@ -2138,7 +2138,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
input: process.stdin,
output: process.stdout,
})
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(
'\nRemote Control lets you access this CLI session from the web (claude.ai/code)\nor the Claude app, so you can pick up where you left off on any device.\n\nYou can disconnect remote access anytime by running /remote-control again.\n',
)
@@ -2170,7 +2169,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
)
const found = await readBridgePointerAcrossWorktrees(dir)
if (!found) {
// biome-ignore lint/suspicious/noConsole: intentional error output
console.error(
`Error: No recent session found in this directory or its worktrees. Run \`claude remote-control\` to start a new one.`,
)
@@ -2181,7 +2179,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
const ageMin = Math.round(pointer.ageMs / 60_000)
const ageStr = ageMin < 60 ? `${ageMin}m` : `${Math.round(ageMin / 60)}h`
const fromWt = pointerDir !== dir ? ` from worktree ${pointerDir}` : ''
// biome-ignore lint/suspicious/noConsole: intentional info output
console.error(
`Resuming session ${pointer.sessionId} (${ageStr} ago)${fromWt}\u2026`,
)
@@ -2202,7 +2199,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
!baseUrl.includes('localhost') &&
!baseUrl.includes('127.0.0.1')
) {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.error(
'Error: Remote Control base URL uses HTTP. Only HTTPS or localhost HTTP is allowed.',
)
@@ -2238,7 +2234,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
? getCurrentProjectConfig().remoteControlSpawnMode
: undefined
if (savedSpawnMode === 'worktree' && !worktreeAvailable) {
// biome-ignore lint/suspicious/noConsole: intentional warning output
console.error(
'Warning: Saved spawn mode is worktree but this directory is not a git repository. Falling back to same-dir.',
)
@@ -2265,7 +2260,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
input: process.stdin,
output: process.stdout,
})
// biome-ignore lint/suspicious/noConsole: intentional dialog output
console.log(
`\nClaude Remote Control is launching in spawn mode which lets you create new sessions in this project from Claude Code on Web or your Mobile app. Learn more here: https://code.claude.com/docs/en/remote-control\n\n` +
`Spawn mode for this project:\n` +
@@ -2344,7 +2338,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
// Only reachable via explicit --spawn=worktree (default is same-dir);
// saved worktree pref was already guarded above.
if (spawnMode === 'worktree' && !worktreeAvailable) {
// biome-ignore lint/suspicious/noConsole: intentional error output
console.error(
`Error: Worktree mode requires a git repository or WorktreeCreate hooks configured. Use --spawn=session for single-session mode.`,
)
@@ -2379,7 +2372,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
try {
validateBridgeId(resumeSessionId, 'sessionId')
} catch {
// biome-ignore lint/suspicious/noConsole: intentional error output
console.error(
`Error: Invalid session ID "${resumeSessionId}". Session IDs must not contain unsafe characters.`,
)
@@ -2405,7 +2397,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
const { clearBridgePointer } = await import('./bridgePointer.js')
await clearBridgePointer(resumePointerDir)
}
// biome-ignore lint/suspicious/noConsole: intentional error output
console.error(
`Error: Session ${resumeSessionId} not found. It may have been archived or expired, or your login may have lapsed (run \`claude /login\`).`,
)
@@ -2417,7 +2408,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
const { clearBridgePointer } = await import('./bridgePointer.js')
await clearBridgePointer(resumePointerDir)
}
// biome-ignore lint/suspicious/noConsole: intentional error output
console.error(
`Error: Session ${resumeSessionId} has no environment_id. It may never have been attached to a bridge.`,
)
@@ -2471,7 +2461,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
status: err instanceof BridgeFatalError ? err.status : undefined,
})
// Registration failures are fatal — print a clean message instead of a stack trace.
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.error(
err instanceof BridgeFatalError && err.status === 404
? 'Remote Control environments are not available for your account.'
@@ -2496,7 +2485,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
`Bridge resume env mismatch: requested ${reuseEnvironmentId}, backend returned ${environmentId}. Falling back to fresh session.`,
),
)
// biome-ignore lint/suspicious/noConsole: intentional warning output
console.warn(
`Warning: Could not resume session ${resumeSessionId} — its environment has expired. Creating a fresh session instead.`,
)
@@ -2547,7 +2535,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
const { clearBridgePointer } = await import('./bridgePointer.js')
await clearBridgePointer(resumePointerDir)
}
// biome-ignore lint/suspicious/noConsole: intentional error output
console.error(
isFatal
? `Error: ${errorMessage(err)}`

View File

@@ -104,7 +104,8 @@ export function isEligibleBridgeMessage(m: Message): boolean {
export function extractTitleText(m: Message): string | undefined {
if (m.type !== 'user' || m.isMeta || m.toolUseResult || m.isCompactSummary)
return undefined
if (m.origin && (m.origin as { kind?: string }).kind !== 'human') return undefined
if (m.origin && (m.origin as { kind?: string }).kind !== 'human')
return undefined
const content = m.message!.content
let raw: string | undefined
if (typeof content === 'string') {
@@ -266,7 +267,13 @@ export function handleServerControlRequest(
// Outbound-only: reply error for mutable requests so claude.ai doesn't show
// false success. initialize must still succeed (server kills the connection
// if it doesn't — see comment above).
const req = request.request as { subtype: string; model?: string; max_thinking_tokens?: number | null; mode?: string; [key: string]: unknown }
const req = request.request as {
subtype: string
model?: string
max_thinking_tokens?: number | null
mode?: string
[key: string]: unknown
}
if (outboundOnly && req.subtype !== 'initialize') {
response = {
type: 'control_response',
@@ -389,8 +396,8 @@ export function handleServerControlRequest(
void transport.write(event)
rcLog(
`control_response: subtype=${req.subtype}` +
` request_id=${request.request_id}` +
` result=${(response.response as { subtype?: string }).subtype}`,
` request_id=${request.request_id}` +
` result=${(response.response as { subtype?: string }).subtype}`,
)
logForDebugging(
`[bridge:repl] Sent control_response for ${req.subtype} request_id=${request.request_id} result=${(response.response as { subtype?: string }).subtype}`,

View File

@@ -24,7 +24,9 @@ export function extractInboundMessageFields(
| { content: string | Array<ContentBlockParam>; uuid: UUID | undefined }
| undefined {
if (msg.type !== 'user') return undefined
const content = (msg.message as { content?: string | Array<ContentBlockParam> } | undefined)?.content
const content = (
msg.message as { content?: string | Array<ContentBlockParam> } | undefined
)?.content
if (!content) return undefined
if (Array.isArray(content) && content.length === 0) return undefined

View File

@@ -290,7 +290,9 @@ export async function initReplBridge(
isSyntheticMessage(msg)
)
continue
const rawContent = getContentText(msg.message!.content as string | ContentBlockParam[])
const rawContent = getContentText(
msg.message!.content as string | ContentBlockParam[],
)
if (!rawContent) continue
const derived = deriveTitle(rawContent)
if (!derived) continue

View File

@@ -20,7 +20,10 @@ export function rcLog(msg: string): void {
try {
if (!headerWritten) {
ensureLogDir()
appendFileSync(LOG_PATH, `\n===== RC-DEBUG session ${new Date().toISOString()} =====\n`)
appendFileSync(
LOG_PATH,
`\n===== RC-DEBUG session ${new Date().toISOString()} =====\n`,
)
headerWritten = true
}
const ts = new Date().toISOString().slice(11, 23) // HH:mm:ss.SSS

View File

@@ -834,7 +834,10 @@ export async function initEnvLessBridgeCore(
for (const msg of filtered) {
if (msg.uuid) recentPostedUUIDs.add(msg.uuid as string)
}
const events = filtered.map(m => ({ ...m, session_id: sessionId })) as StdoutMessage[]
const events = filtered.map(m => ({
...m,
session_id: sessionId,
})) as StdoutMessage[]
void transport.writeBatch(events)
},
sendControlRequest(request: SDKControlRequest) {
@@ -844,8 +847,14 @@ export async function initEnvLessBridgeCore(
)
return
}
const event: TransportMessage = { ...request, session_id: sessionId } as TransportMessage
if ((request as { request?: { subtype?: string } }).request?.subtype === 'can_use_tool') {
const event: TransportMessage = {
...request,
session_id: sessionId,
} as TransportMessage
if (
(request as { request?: { subtype?: string } }).request?.subtype ===
'can_use_tool'
) {
transport.reportState('requires_action')
}
void transport.write(event as StdoutMessage)
@@ -860,7 +869,10 @@ export async function initEnvLessBridgeCore(
)
return
}
const event: TransportMessage = { ...response, session_id: sessionId } as TransportMessage
const event: TransportMessage = {
...response,
session_id: sessionId,
} as TransportMessage
transport.reportState('running')
void transport.write(event as StdoutMessage)
logForDebugging('[remote-bridge] Sent control_response')

View File

@@ -452,7 +452,6 @@ export async function initBridgeCore(
// re-created after a connection loss.
let currentSessionId: string
if (reusedPriorSession && prior) {
currentSessionId = prior.sessionId
logForDebugging(
@@ -632,9 +631,9 @@ export async function initBridgeCore(
environmentRecreations++
rcLog(
`doReconnect: attempt=${environmentRecreations}/${MAX_ENVIRONMENT_RECREATIONS}` +
` envId=${environmentId}` +
` sessionId=${currentSessionId}` +
` workId=${currentWorkId}`,
` envId=${environmentId}` +
` sessionId=${currentSessionId}` +
` workId=${currentWorkId}`,
)
// Invalidate any in-flight v2 handshake — the environment is being
// recreated, so a stale transport arriving post-reconnect would be
@@ -846,7 +845,6 @@ export async function initBridgeCore(
// UUIDs are scoped per-session on the server, so re-flushing is safe.
previouslyFlushedUUIDs?.clear()
// Reset the counter so independent reconnections hours apart don't
// exhaust the limit — it guards against rapid consecutive failures,
// not lifetime total.
@@ -907,8 +905,8 @@ export async function initBridgeCore(
function handleTransportPermanentClose(closeCode: number | undefined): void {
rcLog(
`handleTransportPermanentClose: code=${closeCode}` +
` transport=${transport ? 'exists' : 'null'}` +
` pollAborted=${pollController.signal.aborted}`,
` transport=${transport ? 'exists' : 'null'}` +
` pollAborted=${pollController.signal.aborted}`,
)
logForDebugging(
`[bridge:repl] Transport permanently closed: code=${closeCode}`,
@@ -1303,7 +1301,9 @@ export async function initBridgeCore(
session_id: currentSessionId,
})) as TransportMessage[]
const dropsBefore = newTransport.droppedBatchCount
void newTransport.writeBatch(events as StdoutMessage[]).then(() => {
void newTransport
.writeBatch(events as StdoutMessage[])
.then(() => {
// If any batch was dropped during this flush (SI down for
// maxConsecutiveFailures attempts), flush() still resolved
// normally but the events were NOT delivered. Don't mark
@@ -1357,10 +1357,10 @@ export async function initBridgeCore(
const parsed = JSON.parse(data)
rcLog(
`ingress: type=${parsed.type}` +
`${parsed.type === 'control_request' ? ` subtype=${(parsed.request as Record<string, unknown>)?.subtype} request_id=${parsed.request_id}` : ''}` +
`${parsed.type === 'control_response' ? ` subtype=${(parsed.response as Record<string, unknown>)?.subtype} request_id=${(parsed.response as Record<string, unknown>)?.request_id}` : ''}` +
`${parsed.type === 'user' ? ` uuid=${parsed.uuid}` : ''}` +
`${parsed.type === 'keep_alive' ? '' : ` len=${data.length}`}`,
`${parsed.type === 'control_request' ? ` subtype=${(parsed.request as Record<string, unknown>)?.subtype} request_id=${parsed.request_id}` : ''}` +
`${parsed.type === 'control_response' ? ` subtype=${(parsed.response as Record<string, unknown>)?.subtype} request_id=${(parsed.response as Record<string, unknown>)?.request_id}` : ''}` +
`${parsed.type === 'user' ? ` uuid=${parsed.uuid}` : ''}` +
`${parsed.type === 'keep_alive' ? '' : ` len=${data.length}`}`,
)
} catch {
rcLog(`ingress (non-JSON): ${String(data).slice(0, 200)}`)
@@ -1387,9 +1387,9 @@ export async function initBridgeCore(
if (transport !== newTransport) return
rcLog(
`transport onClose: code=${closeCode}` +
` connected=${newTransport.isConnectedStatus()}` +
` state=${newTransport.getStateLabel()}` +
` seq=${newTransport.getLastSequenceNum()}`,
` connected=${newTransport.isConnectedStatus()}` +
` state=${newTransport.getStateLabel()}` +
` seq=${newTransport.getLastSequenceNum()}`,
)
handleTransportPermanentClose(closeCode)
})
@@ -1818,7 +1818,10 @@ export async function initBridgeCore(
for (const msg of filtered) {
if (msg.uuid) recentPostedUUIDs.add(msg.uuid as string)
}
const events: TransportMessage[] = filtered.map(m => ({ ...m, session_id: currentSessionId })) as TransportMessage[]
const events: TransportMessage[] = filtered.map(m => ({
...m,
session_id: currentSessionId,
})) as TransportMessage[]
void transport.writeBatch(events as StdoutMessage[])
},
sendControlRequest(request: SDKControlRequest) {
@@ -1828,7 +1831,10 @@ export async function initBridgeCore(
)
return
}
const event: TransportMessage = { ...request, session_id: currentSessionId } as TransportMessage
const event: TransportMessage = {
...request,
session_id: currentSessionId,
} as TransportMessage
void transport.write(event as StdoutMessage)
logForDebugging(
`[bridge:repl] Sent control_request request_id=${request.request_id}`,
@@ -1841,7 +1847,10 @@ export async function initBridgeCore(
)
return
}
const event: TransportMessage = { ...response, session_id: currentSessionId } as TransportMessage
const event: TransportMessage = {
...response,
session_id: currentSessionId,
} as TransportMessage
void transport.write(event as StdoutMessage)
logForDebugging('[bridge:repl] Sent control_response')
},

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type StdoutMessage = any;
export type StdoutMessage = any

View File

@@ -11,21 +11,44 @@
/** Patterns that match known secret/token formats. */
const SECRET_PATTERNS: Array<{ pattern: RegExp; replacement: string }> = [
// GitHub tokens (PAT, OAuth, App, Server-to-server)
{ pattern: /\b(ghp|gho|ghs|ghu|github_pat)_[A-Za-z0-9_]{10,}\b/g, replacement: '[REDACTED_GITHUB_TOKEN]' },
{
pattern: /\b(ghp|gho|ghs|ghu|github_pat)_[A-Za-z0-9_]{10,}\b/g,
replacement: '[REDACTED_GITHUB_TOKEN]',
},
// Anthropic API keys
{ pattern: /\bsk-ant-[A-Za-z0-9_-]{10,}\b/g, replacement: '[REDACTED_ANTHROPIC_KEY]' },
{
pattern: /\bsk-ant-[A-Za-z0-9_-]{10,}\b/g,
replacement: '[REDACTED_ANTHROPIC_KEY]',
},
// Generic Bearer tokens in headers
{ pattern: /(Bearer\s+)[A-Za-z0-9._\-/+=]{20,}/gi, replacement: '$1[REDACTED_TOKEN]' },
{
pattern: /(Bearer\s+)[A-Za-z0-9._\-/+=]{20,}/gi,
replacement: '$1[REDACTED_TOKEN]',
},
// AWS access keys
{ pattern: /\b(AKIA|ASIA)[A-Z0-9]{16}\b/g, replacement: '[REDACTED_AWS_KEY]' },
{
pattern: /\b(AKIA|ASIA)[A-Z0-9]{16}\b/g,
replacement: '[REDACTED_AWS_KEY]',
},
// AWS secret keys (40-char base64-like strings after common labels)
{ pattern: /(aws_secret_access_key|secret_key|SecretAccessKey)['":\s=]+[A-Za-z0-9/+=]{30,}/gi, replacement: '$1=[REDACTED_AWS_SECRET]' },
{
pattern:
/(aws_secret_access_key|secret_key|SecretAccessKey)['":\s=]+[A-Za-z0-9/+=]{30,}/gi,
replacement: '$1=[REDACTED_AWS_SECRET]',
},
// Generic API key patterns (key=value or "key": "value")
{ pattern: /(api[_-]?key|apikey|secret|password|token|credential)['":\s=]+["']?[A-Za-z0-9._\-/+=]{16,}["']?/gi, replacement: '$1=[REDACTED]' },
{
pattern:
/(api[_-]?key|apikey|secret|password|token|credential)['":\s=]+["']?[A-Za-z0-9._\-/+=]{16,}["']?/gi,
replacement: '$1=[REDACTED]',
},
// npm tokens
{ pattern: /\bnpm_[A-Za-z0-9]{36}\b/g, replacement: '[REDACTED_NPM_TOKEN]' },
// Slack tokens
{ pattern: /\bxox[bporas]-[A-Za-z0-9-]{10,}\b/g, replacement: '[REDACTED_SLACK_TOKEN]' },
{
pattern: /\bxox[bporas]-[A-Za-z0-9-]{10,}\b/g,
replacement: '[REDACTED_SLACK_TOKEN]',
},
]
/** Maximum content length before truncation (100KB). */

View File

@@ -1,50 +1,50 @@
import { feature } from 'bun:bundle'
import figures from 'figures'
import React, { useEffect, useRef, useState } from 'react'
import { useTerminalSize } from '../hooks/useTerminalSize.js'
import { Box, Text, stringWidth } from '@anthropic/ink'
import { useAppState, useSetAppState } from '../state/AppState.js'
import type { AppState } from '../state/AppStateStore.js'
import { getGlobalConfig } from '../utils/config.js'
import { isFullscreenActive } from '../utils/fullscreen.js'
import type { Theme } from '../utils/theme.js'
import { getCompanion } from './companion.js'
import { renderFace, renderSprite, spriteFrameCount } from './sprites.js'
import { RARITY_COLORS } from './types.js'
import { feature } from 'bun:bundle';
import figures from 'figures';
import React, { useEffect, useRef, useState } from 'react';
import { useTerminalSize } from '../hooks/useTerminalSize.js';
import { Box, Text, stringWidth } from '@anthropic/ink';
import { useAppState, useSetAppState } from '../state/AppState.js';
import type { AppState } from '../state/AppStateStore.js';
import { getGlobalConfig } from '../utils/config.js';
import { isFullscreenActive } from '../utils/fullscreen.js';
import type { Theme } from '../utils/theme.js';
import { getCompanion } from './companion.js';
import { renderFace, renderSprite, spriteFrameCount } from './sprites.js';
import { RARITY_COLORS } from './types.js';
const TICK_MS = 500
const BUBBLE_SHOW = 20 // ticks → ~10s at 500ms
const FADE_WINDOW = 6 // last ~3s the bubble dims so you know it's about to go
const PET_BURST_MS = 2500 // how long hearts float after /buddy pet
const TICK_MS = 500;
const BUBBLE_SHOW = 20; // ticks → ~10s at 500ms
const FADE_WINDOW = 6; // last ~3s the bubble dims so you know it's about to go
const PET_BURST_MS = 2500; // how long hearts float after /buddy pet
// Idle sequence: mostly rest (frame 0), occasional fidget (frames 1-2), rare blink.
// Sequence indices map to sprite frames; -1 means "blink on frame 0".
const IDLE_SEQUENCE = [0, 0, 0, 0, 1, 0, 0, 0, -1, 0, 0, 2, 0, 0, 0]
const IDLE_SEQUENCE = [0, 0, 0, 0, 1, 0, 0, 0, -1, 0, 0, 2, 0, 0, 0];
// Hearts float up-and-out over 5 ticks (~2.5s). Prepended above the sprite.
const H = figures.heart
const H = figures.heart;
const PET_HEARTS = [
` ${H} ${H} `,
` ${H} ${H} ${H} `,
` ${H} ${H} ${H} `,
`${H} ${H} ${H} `,
'· · · ',
]
];
function wrap(text: string, width: number): string[] {
const words = text.split(' ')
const lines: string[] = []
let cur = ''
const words = text.split(' ');
const lines: string[] = [];
let cur = '';
for (const w of words) {
if (cur.length + w.length + 1 > width && cur) {
lines.push(cur)
cur = w
lines.push(cur);
cur = w;
} else {
cur = cur ? `${cur} ${w}` : w
cur = cur ? `${cur} ${w}` : w;
}
}
if (cur) lines.push(cur)
return lines
if (cur) lines.push(cur);
return lines;
}
function SpeechBubble({
@@ -53,40 +53,29 @@ function SpeechBubble({
fading,
tail,
}: {
text: string
color: keyof Theme
fading: boolean
tail: 'down' | 'right'
text: string;
color: keyof Theme;
fading: boolean;
tail: 'down' | 'right';
}): React.ReactNode {
const lines = wrap(text, 30)
const borderColor = fading ? 'inactive' : color
const lines = wrap(text, 30);
const borderColor = fading ? 'inactive' : color;
const bubble = (
<Box
flexDirection="column"
borderStyle="round"
borderColor={borderColor}
paddingX={1}
width={34}
>
<Box flexDirection="column" borderStyle="round" borderColor={borderColor} paddingX={1} width={34}>
{lines.map((l, i) => (
<Text
key={i}
italic
dimColor={!fading}
color={fading ? 'inactive' : undefined}
>
<Text key={i} italic dimColor={!fading} color={fading ? 'inactive' : undefined}>
{l}
</Text>
))}
</Box>
)
);
if (tail === 'right') {
return (
<Box flexDirection="row" alignItems="center">
{bubble}
<Text color={borderColor}></Text>
</Box>
)
);
}
return (
<Box flexDirection="column" alignItems="flex-end" marginRight={1}>
@@ -96,18 +85,18 @@ function SpeechBubble({
<Text color={borderColor}></Text>
</Box>
</Box>
)
);
}
export const MIN_COLS_FOR_FULL_SPRITE = 100
const SPRITE_BODY_WIDTH = 12
const NAME_ROW_PAD = 2 // focused state wraps name in spaces: ` name `
const SPRITE_PADDING_X = 2
const BUBBLE_WIDTH = 36 // SpeechBubble box (34) + tail column
const NARROW_QUIP_CAP = 24
export const MIN_COLS_FOR_FULL_SPRITE = 100;
const SPRITE_BODY_WIDTH = 12;
const NAME_ROW_PAD = 2; // focused state wraps name in spaces: ` name `
const SPRITE_PADDING_X = 2;
const BUBBLE_WIDTH = 36; // SpeechBubble box (34) + tail column
const NARROW_QUIP_CAP = 24;
function spriteColWidth(nameWidth: number): number {
return Math.max(SPRITE_BODY_WIDTH, nameWidth + NAME_ROW_PAD)
return Math.max(SPRITE_BODY_WIDTH, nameWidth + NAME_ROW_PAD);
}
// Width the sprite area consumes. PromptInput subtracts this so text wraps
@@ -115,89 +104,73 @@ function spriteColWidth(nameWidth: number): number {
// width); in non-fullscreen it sits inline and needs BUBBLE_WIDTH more.
// Narrow terminals: 0 — REPL.tsx stacks the one-liner on its own row
// (above input in fullscreen, below in scrollback), so no reservation.
export function companionReservedColumns(
terminalColumns: number,
speaking: boolean,
): number {
if (!feature('BUDDY')) return 0
const companion = getCompanion()
if (!companion || getGlobalConfig().companionMuted) return 0
if (terminalColumns < MIN_COLS_FOR_FULL_SPRITE) return 0
const nameWidth = stringWidth(companion.name)
const bubble = speaking && !isFullscreenActive() ? BUBBLE_WIDTH : 0
return spriteColWidth(nameWidth) + SPRITE_PADDING_X + bubble
export function companionReservedColumns(terminalColumns: number, speaking: boolean): number {
if (!feature('BUDDY')) return 0;
const companion = getCompanion();
if (!companion || getGlobalConfig().companionMuted) return 0;
if (terminalColumns < MIN_COLS_FOR_FULL_SPRITE) return 0;
const nameWidth = stringWidth(companion.name);
const bubble = speaking && !isFullscreenActive() ? BUBBLE_WIDTH : 0;
return spriteColWidth(nameWidth) + SPRITE_PADDING_X + bubble;
}
export function CompanionSprite(): React.ReactNode {
const reaction = useAppState(s => s.companionReaction)
const petAt = useAppState(s => s.companionPetAt)
const focused = useAppState(s => s.footerSelection === 'companion')
const setAppState = useSetAppState()
const { columns } = useTerminalSize()
const [tick, setTick] = useState(0)
const lastSpokeTick = useRef(0)
const reaction = useAppState(s => s.companionReaction);
const petAt = useAppState(s => s.companionPetAt);
const focused = useAppState(s => s.footerSelection === 'companion');
const setAppState = useSetAppState();
const { columns } = useTerminalSize();
const [tick, setTick] = useState(0);
const lastSpokeTick = useRef(0);
// Sync-during-render (not useEffect) so the first post-pet render already
// has petStartTick=tick and petAge=0 — otherwise frame 0 is skipped.
const [{ petStartTick, forPetAt }, setPetStart] = useState({
petStartTick: 0,
forPetAt: petAt,
})
});
if (petAt !== forPetAt) {
setPetStart({ petStartTick: tick, forPetAt: petAt })
setPetStart({ petStartTick: tick, forPetAt: petAt });
}
useEffect(() => {
const timer = setInterval(
setT => setT((t: number) => t + 1),
TICK_MS,
setTick,
)
return () => clearInterval(timer)
}, [])
const timer = setInterval(setT => setT((t: number) => t + 1), TICK_MS, setTick);
return () => clearInterval(timer);
}, []);
useEffect(() => {
if (!reaction) return
lastSpokeTick.current = tick
if (!reaction) return;
lastSpokeTick.current = tick;
const timer = setTimeout(
setA =>
setA((prev: AppState) =>
prev.companionReaction === undefined
? prev
: { ...prev, companionReaction: undefined },
prev.companionReaction === undefined ? prev : { ...prev, companionReaction: undefined },
),
BUBBLE_SHOW * TICK_MS,
setAppState,
)
return () => clearTimeout(timer)
);
return () => clearTimeout(timer);
// eslint-disable-next-line react-hooks/exhaustive-deps -- tick intentionally captured at reaction-change, not tracked
}, [reaction, setAppState])
}, [reaction, setAppState]);
if (!feature('BUDDY')) return null
const companion = getCompanion()
if (!companion || getGlobalConfig().companionMuted) return null
if (!feature('BUDDY')) return null;
const companion = getCompanion();
if (!companion || getGlobalConfig().companionMuted) return null;
const color = RARITY_COLORS[companion.rarity]
const colWidth = spriteColWidth(stringWidth(companion.name))
const color = RARITY_COLORS[companion.rarity];
const colWidth = spriteColWidth(stringWidth(companion.name));
const bubbleAge = reaction ? tick - lastSpokeTick.current : 0
const fading =
reaction !== undefined && bubbleAge >= BUBBLE_SHOW - FADE_WINDOW
const bubbleAge = reaction ? tick - lastSpokeTick.current : 0;
const fading = reaction !== undefined && bubbleAge >= BUBBLE_SHOW - FADE_WINDOW;
const petAge = petAt ? tick - petStartTick : Infinity
const petting = petAge * TICK_MS < PET_BURST_MS
const petAge = petAt ? tick - petStartTick : Infinity;
const petting = petAge * TICK_MS < PET_BURST_MS;
// Narrow terminals: collapse to one-line face. When speaking, the quip
// replaces the name beside the face (no room for a bubble).
if (columns < MIN_COLS_FOR_FULL_SPRITE) {
const quip =
reaction && reaction.length > NARROW_QUIP_CAP
? reaction.slice(0, NARROW_QUIP_CAP - 1) + '…'
: reaction
const label = quip
? `"${quip}"`
: focused
? ` ${companion.name} `
: companion.name
reaction && reaction.length > NARROW_QUIP_CAP ? reaction.slice(0, NARROW_QUIP_CAP - 1) + '…' : reaction;
const label = quip ? `"${quip}"` : focused ? ` ${companion.name} ` : companion.name;
return (
<Box paddingX={1} alignSelf="flex-end">
<Text>
@@ -210,44 +183,34 @@ export function CompanionSprite(): React.ReactNode {
dimColor={!focused && !reaction}
bold={focused}
inverse={focused && !reaction}
color={
reaction
? fading
? 'inactive'
: color
: focused
? color
: undefined
}
color={reaction ? (fading ? 'inactive' : color) : focused ? color : undefined}
>
{label}
</Text>
</Text>
</Box>
)
);
}
const frameCount = spriteFrameCount(companion.species)
const heartFrame = petting ? PET_HEARTS[petAge % PET_HEARTS.length] : null
const frameCount = spriteFrameCount(companion.species);
const heartFrame = petting ? PET_HEARTS[petAge % PET_HEARTS.length] : null;
let spriteFrame: number
let blink = false
let spriteFrame: number;
let blink = false;
if (reaction || petting) {
// Excited: cycle all fidget frames fast
spriteFrame = tick % frameCount
spriteFrame = tick % frameCount;
} else {
const step = IDLE_SEQUENCE[tick % IDLE_SEQUENCE.length]!
const step = IDLE_SEQUENCE[tick % IDLE_SEQUENCE.length]!;
if (step === -1) {
spriteFrame = 0
blink = true
spriteFrame = 0;
blink = true;
} else {
spriteFrame = step % frameCount
spriteFrame = step % frameCount;
}
}
const body = renderSprite(companion, spriteFrame).map(line =>
blink ? line.replaceAll(companion.eye, '-') : line,
)
const sprite = heartFrame ? [heartFrame, ...body] : body
const body = renderSprite(companion, spriteFrame).map(line => (blink ? line.replaceAll(companion.eye, '-') : line));
const sprite = heartFrame ? [heartFrame, ...body] : body;
// Name row doubles as hint row — unfocused shows dim name + ↓ discovery,
// focused shows inverse name. The enter-to-open hint lives in
@@ -255,31 +218,20 @@ export function CompanionSprite(): React.ReactNode {
// sprite doesn't jump up when selected. flexShrink=0 stops the
// inline-bubble row wrapper from squeezing the sprite to fit.
const spriteColumn = (
<Box
flexDirection="column"
flexShrink={0}
alignItems="center"
width={colWidth}
>
<Box flexDirection="column" flexShrink={0} alignItems="center" width={colWidth}>
{sprite.map((line, i) => (
<Text key={i} color={i === 0 && heartFrame ? 'autoAccept' : color}>
{line}
</Text>
))}
<Text
italic
bold={focused}
dimColor={!focused}
color={focused ? color : undefined}
inverse={focused}
>
<Text italic bold={focused} dimColor={!focused} color={focused ? color : undefined} inverse={focused}>
{focused ? ` ${companion.name} ` : companion.name}
</Text>
</Box>
)
);
if (!reaction) {
return <Box paddingX={1}>{spriteColumn}</Box>
return <Box paddingX={1}>{spriteColumn}</Box>;
}
// Fullscreen: bubble renders separately via CompanionFloatingBubble in
@@ -288,19 +240,14 @@ export function CompanionSprite(): React.ReactNode {
// Non-fullscreen: bubble sits inline beside the sprite (input shrinks)
// because floating into Static scrollback can't be cleared.
if (isFullscreenActive()) {
return <Box paddingX={1}>{spriteColumn}</Box>
return <Box paddingX={1}>{spriteColumn}</Box>;
}
return (
<Box flexDirection="row" alignItems="flex-end" paddingX={1} flexShrink={0}>
<SpeechBubble
text={reaction}
color={color}
fading={fading}
tail="right"
/>
<SpeechBubble text={reaction} color={color} fading={fading} tail="right" />
{spriteColumn}
</Box>
)
);
}
// Floating bubble overlay for fullscreen mode. Mounted in FullscreenLayout's
@@ -308,33 +255,29 @@ export function CompanionSprite(): React.ReactNode {
// the ScrollBox region. CompanionSprite owns the clear-after-10s timer; this
// just reads companionReaction and renders the fade.
export function CompanionFloatingBubble(): React.ReactNode {
const reaction = useAppState(s => s.companionReaction)
const reaction = useAppState(s => s.companionReaction);
const [{ tick, forReaction }, setTick] = useState({
tick: 0,
forReaction: reaction,
})
});
// Reset tick synchronously when reaction changes (not in useEffect, which
// runs post-render and would show one stale-faded frame). Storing the
// reaction the tick is counting FOR alongside the tick itself means the
// fade computation never sees a tick from a previous reaction.
if (reaction !== forReaction) {
setTick({ tick: 0, forReaction: reaction })
setTick({ tick: 0, forReaction: reaction });
}
useEffect(() => {
if (!reaction) return
const timer = setInterval(
set => set(s => ({ ...s, tick: s.tick + 1 })),
TICK_MS,
setTick,
)
return () => clearInterval(timer)
}, [reaction])
if (!reaction) return;
const timer = setInterval(set => set(s => ({ ...s, tick: s.tick + 1 })), TICK_MS, setTick);
return () => clearInterval(timer);
}, [reaction]);
if (!feature('BUDDY') || !reaction) return null
const companion = getCompanion()
if (!companion || getGlobalConfig().companionMuted) return null
if (!feature('BUDDY') || !reaction) return null;
const companion = getCompanion();
if (!companion || getGlobalConfig().companionMuted) return null;
return (
<SpeechBubble
@@ -343,5 +286,5 @@ export function CompanionFloatingBubble(): React.ReactNode {
fading={tick >= BUBBLE_SHOW - FADE_WINDOW}
tail="down"
/>
)
);
}

View File

@@ -1,25 +1,23 @@
import { feature } from 'bun:bundle'
import React, { useEffect } from 'react'
import { useNotifications } from '../context/notifications.js'
import { Text } from '@anthropic/ink'
import { getGlobalConfig } from '../utils/config.js'
import { getRainbowColor } from '../utils/thinking.js'
import { feature } from 'bun:bundle';
import React, { useEffect } from 'react';
import { useNotifications } from '../context/notifications.js';
import { Text } from '@anthropic/ink';
import { getGlobalConfig } from '../utils/config.js';
import { getRainbowColor } from '../utils/thinking.js';
// Local date, not UTC — 24h rolling wave across timezones. Sustained Twitter
// buzz instead of a single UTC-midnight spike, gentler on soul-gen load.
// Teaser window: April 1-7, 2026 only. Command stays live forever after.
export function isBuddyTeaserWindow(): boolean {
if (process.env.USER_TYPE === 'ant') return true
const d = new Date()
return d.getFullYear() === 2026 && d.getMonth() === 3 && d.getDate() <= 7
if (process.env.USER_TYPE === 'ant') return true;
const d = new Date();
return d.getFullYear() === 2026 && d.getMonth() === 3 && d.getDate() <= 7;
}
export function isBuddyLive(): boolean {
if (process.env.USER_TYPE === 'ant') return true
const d = new Date()
return (
d.getFullYear() > 2026 || (d.getFullYear() === 2026 && d.getMonth() >= 3)
)
if (process.env.USER_TYPE === 'ant') return true;
const d = new Date();
return d.getFullYear() > 2026 || (d.getFullYear() === 2026 && d.getMonth() >= 3);
}
function RainbowText({ text }: { text: string }): React.ReactNode {
@@ -31,37 +29,35 @@ function RainbowText({ text }: { text: string }): React.ReactNode {
</Text>
))}
</>
)
);
}
// Rainbow /buddy teaser shown on startup when no companion hatched yet.
// Idle presence and reactions are handled by CompanionSprite directly.
export function useBuddyNotification(): void {
const { addNotification, removeNotification } = useNotifications()
const { addNotification, removeNotification } = useNotifications();
useEffect(() => {
if (!feature('BUDDY')) return
const config = getGlobalConfig()
if (config.companion || !isBuddyTeaserWindow()) return
if (!feature('BUDDY')) return;
const config = getGlobalConfig();
if (config.companion || !isBuddyTeaserWindow()) return;
addNotification({
key: 'buddy-teaser',
jsx: <RainbowText text="/buddy" />,
priority: 'immediate',
timeoutMs: 15_000,
})
return () => removeNotification('buddy-teaser')
}, [addNotification, removeNotification])
});
return () => removeNotification('buddy-teaser');
}, [addNotification, removeNotification]);
}
export function findBuddyTriggerPositions(
text: string,
): Array<{ start: number; end: number }> {
if (!feature('BUDDY')) return []
const triggers: Array<{ start: number; end: number }> = []
const re = /\/buddy\b/g
let m: RegExpExecArray | null
export function findBuddyTriggerPositions(text: string): Array<{ start: number; end: number }> {
if (!feature('BUDDY')) return [];
const triggers: Array<{ start: number; end: number }> = [];
const re = /\/buddy\b/g;
let m: RegExpExecArray | null;
while ((m = re.exec(text)) !== null) {
triggers.push({ start: m.index, end: m.index + m[0].length })
triggers.push({ start: m.index, end: m.index + m[0].length });
}
return triggers
return triggers;
}

View File

@@ -200,7 +200,9 @@ export async function attachHandler(target: string | undefined): Promise<void> {
const { TmuxEngine } = await import('./bg/engines/tmux.js')
const tmux = new TmuxEngine()
if (!(await tmux.available())) {
console.error('tmux is no longer available. Cannot attach to tmux session.')
console.error(
'tmux is no longer available. Cannot attach to tmux session.',
)
process.exitCode = 1
return
}
@@ -301,7 +303,9 @@ export async function handleBgStart(args: string[]): Promise<void> {
console.log(` Engine: ${result.engineUsed}`)
console.log(` Log: ${result.logPath}`)
console.log()
console.log(`Use \`claude daemon attach ${result.sessionName}\` to reconnect.`)
console.log(
`Use \`claude daemon attach ${result.sessionName}\` to reconnect.`,
)
console.log(`Use \`claude daemon status\` to check status.`)
console.log(`Use \`claude daemon kill ${result.sessionName}\` to stop.`)
} catch (e) {

View File

@@ -1,7 +1,12 @@
import { spawn } from 'child_process'
import { openSync, closeSync, mkdirSync } from 'fs'
import { dirname } from 'path'
import type { BgEngine, BgStartOptions, BgStartResult, SessionEntry } from '../engine.js'
import type {
BgEngine,
BgStartOptions,
BgStartResult,
SessionEntry,
} from '../engine.js'
import { tailLog } from '../tail.js'
export class DetachedEngine implements BgEngine {

View File

@@ -1,4 +1,9 @@
export type { BgEngine, BgStartOptions, BgStartResult, SessionEntry } from '../engine.js'
export type {
BgEngine,
BgStartOptions,
BgStartResult,
SessionEntry,
} from '../engine.js'
export async function selectEngine(): Promise<import('../engine.js').BgEngine> {
if (process.platform === 'win32') {

View File

@@ -1,7 +1,12 @@
import { spawnSync } from 'child_process'
import { execFileNoThrow } from '../../../utils/execFileNoThrow.js'
import { quote } from '../../../utils/bash/shellQuote.js'
import type { BgEngine, BgStartOptions, BgStartResult, SessionEntry } from '../engine.js'
import type {
BgEngine,
BgStartOptions,
BgStartResult,
SessionEntry,
} from '../engine.js'
export class TmuxEngine implements BgEngine {
readonly name = 'tmux' as const

View File

@@ -17,7 +17,6 @@
/** Write an error message to stderr (if given) and exit with code 1. */
export function cliError(msg?: string): never {
// biome-ignore lint/suspicious/noConsole: centralized CLI error output
if (msg) console.error(msg)
process.exit(1)
return undefined as never

View File

@@ -59,12 +59,9 @@ export async function agentsHandler(): Promise<void> {
}
if (lines.length === 0) {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log('No agents found.')
} else {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(`${totalActive} active agents\n`)
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(lines.join('\n').trimEnd())
}
}

View File

@@ -159,7 +159,9 @@ export async function authLogin({
const orgResult = await validateForceLoginOrg()
if (!orgResult.valid) {
process.stderr.write((orgResult as { valid: false; message: string }).message + '\n')
process.stderr.write(
(orgResult as { valid: false; message: string }).message + '\n',
)
process.exit(1)
}
@@ -209,7 +211,9 @@ export async function authLogin({
const orgResult = await validateForceLoginOrg()
if (!orgResult.valid) {
process.stderr.write((orgResult as { valid: false; message: string }).message + '\n')
process.stderr.write(
(orgResult as { valid: false; message: string }).message + '\n',
)
process.exit(1)
}

View File

@@ -3,203 +3,163 @@
* These are dynamically imported only when the corresponding `claude mcp *` command runs.
*/
import { stat } from 'fs/promises'
import pMap from 'p-map'
import { cwd } from 'process'
import React from 'react'
import { MCPServerDesktopImportDialog } from '../../components/MCPServerDesktopImportDialog.js'
import { wrappedRender as render } from '@anthropic/ink'
import { KeybindingSetup } from '../../keybindings/KeybindingProviderSetup.js'
import { stat } from 'fs/promises';
import pMap from 'p-map';
import { cwd } from 'process';
import React from 'react';
import { MCPServerDesktopImportDialog } from '../../components/MCPServerDesktopImportDialog.js';
import { wrappedRender as render } from '@anthropic/ink';
import { KeybindingSetup } from '../../keybindings/KeybindingProviderSetup.js';
import {
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
logEvent,
} from '../../services/analytics/index.js'
} from '../../services/analytics/index.js';
import {
clearMcpClientConfig,
clearServerTokensFromLocalStorage,
getMcpClientConfig,
readClientSecret,
saveMcpClientSecret,
} from '../../services/mcp/auth.js'
import {
connectToServer,
getMcpServerConnectionBatchSize,
} from '../../services/mcp/client.js'
} from '../../services/mcp/auth.js';
import { connectToServer, getMcpServerConnectionBatchSize } from '../../services/mcp/client.js';
import {
addMcpConfig,
getAllMcpConfigs,
getMcpConfigByName,
getMcpConfigsByScope,
removeMcpConfig,
} from '../../services/mcp/config.js'
import type {
ConfigScope,
ScopedMcpServerConfig,
} from '../../services/mcp/types.js'
import {
describeMcpConfigFilePath,
ensureConfigScope,
getScopeLabel,
} from '../../services/mcp/utils.js'
import { AppStateProvider } from '../../state/AppState.js'
import {
getCurrentProjectConfig,
getGlobalConfig,
saveCurrentProjectConfig,
} from '../../utils/config.js'
import { isFsInaccessible } from '../../utils/errors.js'
import { gracefulShutdown } from '../../utils/gracefulShutdown.js'
import { safeParseJSON } from '../../utils/json.js'
import { getPlatform } from '../../utils/platform.js'
import { cliError, cliOk } from '../exit.js'
} from '../../services/mcp/config.js';
import type { ConfigScope, ScopedMcpServerConfig } from '../../services/mcp/types.js';
import { describeMcpConfigFilePath, ensureConfigScope, getScopeLabel } from '../../services/mcp/utils.js';
import { AppStateProvider } from '../../state/AppState.js';
import { getCurrentProjectConfig, getGlobalConfig, saveCurrentProjectConfig } from '../../utils/config.js';
import { isFsInaccessible } from '../../utils/errors.js';
import { gracefulShutdown } from '../../utils/gracefulShutdown.js';
import { safeParseJSON } from '../../utils/json.js';
import { getPlatform } from '../../utils/platform.js';
import { cliError, cliOk } from '../exit.js';
async function checkMcpServerHealth(
name: string,
server: ScopedMcpServerConfig,
): Promise<string> {
async function checkMcpServerHealth(name: string, server: ScopedMcpServerConfig): Promise<string> {
try {
const result = await connectToServer(name, server)
const result = await connectToServer(name, server);
if (result.type === 'connected') {
return '✓ Connected'
return '✓ Connected';
} else if (result.type === 'needs-auth') {
return '! Needs authentication'
return '! Needs authentication';
} else {
return '✗ Failed to connect'
return '✗ Failed to connect';
}
} catch (_error) {
return '✗ Connection error'
return '✗ Connection error';
}
}
// mcp serve (lines 45124532)
export async function mcpServeHandler({
debug,
verbose,
}: {
debug?: boolean
verbose?: boolean
}): Promise<void> {
const providedCwd = cwd()
logEvent('tengu_mcp_start', {})
export async function mcpServeHandler({ debug, verbose }: { debug?: boolean; verbose?: boolean }): Promise<void> {
const providedCwd = cwd();
logEvent('tengu_mcp_start', {});
try {
await stat(providedCwd)
await stat(providedCwd);
} catch (error) {
if (isFsInaccessible(error)) {
cliError(`Error: Directory ${providedCwd} does not exist`)
cliError(`Error: Directory ${providedCwd} does not exist`);
}
throw error
throw error;
}
try {
const { setup } = await import('../../setup.js')
await setup(providedCwd, 'default', false, false, undefined, false)
const { startMCPServer } = await import('../../entrypoints/mcp.js')
await startMCPServer(providedCwd, debug ?? false, verbose ?? false)
const { setup } = await import('../../setup.js');
await setup(providedCwd, 'default', false, false, undefined, false);
const { startMCPServer } = await import('../../entrypoints/mcp.js');
await startMCPServer(providedCwd, debug ?? false, verbose ?? false);
} catch (error) {
cliError(`Error: Failed to start MCP server: ${error}`)
cliError(`Error: Failed to start MCP server: ${error}`);
}
}
// mcp remove (lines 45454635)
export async function mcpRemoveHandler(
name: string,
options: { scope?: string },
): Promise<void> {
export async function mcpRemoveHandler(name: string, options: { scope?: string }): Promise<void> {
// Look up config before removing so we can clean up secure storage
const serverBeforeRemoval = getMcpConfigByName(name)
const serverBeforeRemoval = getMcpConfigByName(name);
const cleanupSecureStorage = () => {
if (
serverBeforeRemoval &&
(serverBeforeRemoval.type === 'sse' ||
serverBeforeRemoval.type === 'http')
) {
clearServerTokensFromLocalStorage(name, serverBeforeRemoval)
clearMcpClientConfig(name, serverBeforeRemoval)
if (serverBeforeRemoval && (serverBeforeRemoval.type === 'sse' || serverBeforeRemoval.type === 'http')) {
clearServerTokensFromLocalStorage(name, serverBeforeRemoval);
clearMcpClientConfig(name, serverBeforeRemoval);
}
}
};
try {
if (options.scope) {
const scope = ensureConfigScope(options.scope)
const scope = ensureConfigScope(options.scope);
logEvent('tengu_mcp_delete', {
name: name as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
scope:
scope as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
scope: scope as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
});
await removeMcpConfig(name, scope)
cleanupSecureStorage()
process.stdout.write(`Removed MCP server ${name} from ${scope} config\n`)
cliOk(`File modified: ${describeMcpConfigFilePath(scope)}`)
await removeMcpConfig(name, scope);
cleanupSecureStorage();
process.stdout.write(`Removed MCP server ${name} from ${scope} config\n`);
cliOk(`File modified: ${describeMcpConfigFilePath(scope)}`);
}
// If no scope specified, check where the server exists
const projectConfig = getCurrentProjectConfig()
const globalConfig = getGlobalConfig()
const projectConfig = getCurrentProjectConfig();
const globalConfig = getGlobalConfig();
// Check if server exists in project scope (.mcp.json)
const { servers: projectServers } = getMcpConfigsByScope('project')
const mcpJsonExists = !!projectServers[name]
const { servers: projectServers } = getMcpConfigsByScope('project');
const mcpJsonExists = !!projectServers[name];
// Count how many scopes contain this server
const scopes: Array<Exclude<ConfigScope, 'dynamic'>> = []
if (projectConfig.mcpServers?.[name]) scopes.push('local')
if (mcpJsonExists) scopes.push('project')
if (globalConfig.mcpServers?.[name]) scopes.push('user')
const scopes: Array<Exclude<ConfigScope, 'dynamic'>> = [];
if (projectConfig.mcpServers?.[name]) scopes.push('local');
if (mcpJsonExists) scopes.push('project');
if (globalConfig.mcpServers?.[name]) scopes.push('user');
if (scopes.length === 0) {
cliError(`No MCP server found with name: "${name}"`)
cliError(`No MCP server found with name: "${name}"`);
} else if (scopes.length === 1) {
// Server exists in only one scope, remove it
const scope = scopes[0]!
const scope = scopes[0]!;
logEvent('tengu_mcp_delete', {
name: name as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
scope:
scope as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
scope: scope as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
});
await removeMcpConfig(name, scope)
cleanupSecureStorage()
process.stdout.write(
`Removed MCP server "${name}" from ${scope} config\n`,
)
cliOk(`File modified: ${describeMcpConfigFilePath(scope)}`)
await removeMcpConfig(name, scope);
cleanupSecureStorage();
process.stdout.write(`Removed MCP server "${name}" from ${scope} config\n`);
cliOk(`File modified: ${describeMcpConfigFilePath(scope)}`);
} else {
// Server exists in multiple scopes
process.stderr.write(`MCP server "${name}" exists in multiple scopes:\n`)
process.stderr.write(`MCP server "${name}" exists in multiple scopes:\n`);
scopes.forEach(scope => {
process.stderr.write(
` - ${getScopeLabel(scope)} (${describeMcpConfigFilePath(scope)})\n`,
)
})
process.stderr.write('\nTo remove from a specific scope, use:\n')
process.stderr.write(` - ${getScopeLabel(scope)} (${describeMcpConfigFilePath(scope)})\n`);
});
process.stderr.write('\nTo remove from a specific scope, use:\n');
scopes.forEach(scope => {
process.stderr.write(` claude mcp remove "${name}" -s ${scope}\n`)
})
cliError()
process.stderr.write(` claude mcp remove "${name}" -s ${scope}\n`);
});
cliError();
}
} catch (error) {
cliError((error as Error).message)
cliError((error as Error).message);
}
}
// mcp list (lines 46414688)
export async function mcpListHandler(): Promise<void> {
logEvent('tengu_mcp_list', {})
const { servers: configs } = await getAllMcpConfigs()
logEvent('tengu_mcp_list', {});
const { servers: configs } = await getAllMcpConfigs();
if (Object.keys(configs).length === 0) {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(
'No MCP servers configured. Use `claude mcp add` to add a server.',
)
console.log('No MCP servers configured. Use `claude mcp add` to add a server.');
} else {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log('Checking MCP server health...\n')
console.log('Checking MCP server health...\n');
// Check servers concurrently
const entries = Object.entries(configs)
const entries = Object.entries(configs);
const results = await pMap(
entries,
async ([name, server]) => ({
@@ -208,127 +168,100 @@ export async function mcpListHandler(): Promise<void> {
status: await checkMcpServerHealth(name, server),
}),
{ concurrency: getMcpServerConnectionBatchSize() },
)
);
for (const { name, server, status } of results) {
// Intentionally excluding sse-ide servers here since they're internal
if (server.type === 'sse') {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(`${name}: ${server.url} (SSE) - ${status}`)
console.log(`${name}: ${server.url} (SSE) - ${status}`);
} else if (server.type === 'http') {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(`${name}: ${server.url} (HTTP) - ${status}`)
console.log(`${name}: ${server.url} (HTTP) - ${status}`);
} else if (server.type === 'claudeai-proxy') {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(`${name}: ${server.url} - ${status}`)
console.log(`${name}: ${server.url} - ${status}`);
} else if (!server.type || server.type === 'stdio') {
const stdioServer = server as { command: string; args: string[]; type?: string }
const args = Array.isArray(stdioServer.args) ? stdioServer.args : []
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(`${name}: ${stdioServer.command} ${args.join(' ')} - ${status}`)
const stdioServer = server as { command: string; args: string[]; type?: string };
const args = Array.isArray(stdioServer.args) ? stdioServer.args : [];
console.log(`${name}: ${stdioServer.command} ${args.join(' ')} - ${status}`);
}
}
}
// Use gracefulShutdown to properly clean up MCP server connections
// (process.exit bypasses cleanup handlers, leaving child processes orphaned)
await gracefulShutdown(0)
await gracefulShutdown(0);
}
// mcp get (lines 46944786)
export async function mcpGetHandler(name: string): Promise<void> {
logEvent('tengu_mcp_get', {
name: name as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
const server = getMcpConfigByName(name)
});
const server = getMcpConfigByName(name);
if (!server) {
cliError(`No MCP server found with name: ${name}`)
cliError(`No MCP server found with name: ${name}`);
}
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(`${name}:`)
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` Scope: ${getScopeLabel(server.scope)}`)
console.log(`${name}:`);
console.log(` Scope: ${getScopeLabel(server.scope)}`);
// Check server health
const status = await checkMcpServerHealth(name, server)
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` Status: ${status}`)
const status = await checkMcpServerHealth(name, server);
console.log(` Status: ${status}`);
// Intentionally excluding sse-ide servers here since they're internal
if (server.type === 'sse') {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` Type: sse`)
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` URL: ${server.url}`)
console.log(` Type: sse`);
console.log(` URL: ${server.url}`);
if (server.headers) {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(' Headers:')
console.log(' Headers:');
for (const [key, value] of Object.entries(server.headers)) {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` ${key}: ${value}`)
console.log(` ${key}: ${value}`);
}
}
if (server.oauth?.clientId || server.oauth?.callbackPort) {
const parts: string[] = []
const parts: string[] = [];
if (server.oauth.clientId) {
parts.push('client_id configured')
const clientConfig = getMcpClientConfig(name, server)
if (clientConfig?.clientSecret) parts.push('client_secret configured')
parts.push('client_id configured');
const clientConfig = getMcpClientConfig(name, server);
if (clientConfig?.clientSecret) parts.push('client_secret configured');
}
if (server.oauth.callbackPort)
parts.push(`callback_port ${server.oauth.callbackPort}`)
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` OAuth: ${parts.join(', ')}`)
if (server.oauth.callbackPort) parts.push(`callback_port ${server.oauth.callbackPort}`);
console.log(` OAuth: ${parts.join(', ')}`);
}
} else if (server.type === 'http') {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` Type: http`)
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` URL: ${server.url}`)
console.log(` Type: http`);
console.log(` URL: ${server.url}`);
if (server.headers) {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(' Headers:')
console.log(' Headers:');
for (const [key, value] of Object.entries(server.headers)) {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` ${key}: ${value}`)
console.log(` ${key}: ${value}`);
}
}
if (server.oauth?.clientId || server.oauth?.callbackPort) {
const parts: string[] = []
const parts: string[] = [];
if (server.oauth.clientId) {
parts.push('client_id configured')
const clientConfig = getMcpClientConfig(name, server)
if (clientConfig?.clientSecret) parts.push('client_secret configured')
parts.push('client_id configured');
const clientConfig = getMcpClientConfig(name, server);
if (clientConfig?.clientSecret) parts.push('client_secret configured');
}
if (server.oauth.callbackPort)
parts.push(`callback_port ${server.oauth.callbackPort}`)
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` OAuth: ${parts.join(', ')}`)
if (server.oauth.callbackPort) parts.push(`callback_port ${server.oauth.callbackPort}`);
console.log(` OAuth: ${parts.join(', ')}`);
}
} else if (server.type === 'stdio') {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` Type: stdio`)
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` Command: ${server.command}`)
const args = Array.isArray(server.args) ? server.args : []
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` Args: ${args.join(' ')}`)
console.log(` Type: stdio`);
console.log(` Command: ${server.command}`);
const args = Array.isArray(server.args) ? server.args : [];
console.log(` Args: ${args.join(' ')}`);
if (server.env) {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(' Environment:')
console.log(' Environment:');
for (const [key, value] of Object.entries(server.env)) {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` ${key}=${value}`)
console.log(` ${key}=${value}`);
}
}
}
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(
`\nTo remove this server, run: claude mcp remove "${name}" -s ${server.scope}`,
)
console.log(`\nTo remove this server, run: claude mcp remove "${name}" -s ${server.scope}`);
// Use gracefulShutdown to properly clean up MCP server connections
// (process.exit bypasses cleanup handlers, leaving child processes orphaned)
await gracefulShutdown(0)
await gracefulShutdown(0);
}
// mcp add-json (lines 48014870)
@@ -338,8 +271,8 @@ export async function mcpAddJsonHandler(
options: { scope?: string; clientSecret?: true },
): Promise<void> {
try {
const scope = ensureConfigScope(options.scope)
const parsedJson = safeParseJSON(json)
const scope = ensureConfigScope(options.scope);
const parsedJson = safeParseJSON(json);
// Read secret before writing config so cancellation doesn't leave partial state
const needsSecret =
@@ -353,15 +286,15 @@ export async function mcpAddJsonHandler(
'oauth' in parsedJson &&
parsedJson.oauth &&
typeof parsedJson.oauth === 'object' &&
'clientId' in parsedJson.oauth
const clientSecret = needsSecret ? await readClientSecret() : undefined
'clientId' in parsedJson.oauth;
const clientSecret = needsSecret ? await readClientSecret() : undefined;
await addMcpConfig(name, parsedJson, scope)
await addMcpConfig(name, parsedJson, scope);
const transportType =
parsedJson && typeof parsedJson === 'object' && 'type' in parsedJson
? String(parsedJson.type || 'stdio')
: 'stdio'
: 'stdio';
if (
clientSecret &&
@@ -372,53 +305,38 @@ export async function mcpAddJsonHandler(
'url' in parsedJson &&
typeof parsedJson.url === 'string'
) {
saveMcpClientSecret(
name,
{ type: parsedJson.type, url: parsedJson.url },
clientSecret,
)
saveMcpClientSecret(name, { type: parsedJson.type, url: parsedJson.url }, clientSecret);
}
logEvent('tengu_mcp_add', {
scope:
scope as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
source:
'json' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
scope: scope as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
source: 'json' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
type: transportType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
});
cliOk(`Added ${transportType} MCP server ${name} to ${scope} config`)
cliOk(`Added ${transportType} MCP server ${name} to ${scope} config`);
} catch (error) {
cliError((error as Error).message)
cliError((error as Error).message);
}
}
// mcp add-from-claude-desktop (lines 48814927)
export async function mcpAddFromDesktopHandler(options: {
scope?: string
}): Promise<void> {
export async function mcpAddFromDesktopHandler(options: { scope?: string }): Promise<void> {
try {
const scope = ensureConfigScope(options.scope)
const platform = getPlatform()
const scope = ensureConfigScope(options.scope);
const platform = getPlatform();
logEvent('tengu_mcp_add', {
scope:
scope as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
platform:
platform as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
source:
'desktop' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
scope: scope as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
platform: platform as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
source: 'desktop' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
});
const { readClaudeDesktopMcpServers } = await import(
'../../utils/claudeDesktop.js'
)
const servers = await readClaudeDesktopMcpServers()
const { readClaudeDesktopMcpServers } = await import('../../utils/claudeDesktop.js');
const servers = await readClaudeDesktopMcpServers();
if (Object.keys(servers).length === 0) {
cliOk(
'No MCP servers found in Claude Desktop configuration or configuration file does not exist.',
)
cliOk('No MCP servers found in Claude Desktop configuration or configuration file does not exist.');
}
const { unmount } = await render(
@@ -428,29 +346,29 @@ export async function mcpAddFromDesktopHandler(options: {
servers={servers}
scope={scope}
onDone={() => {
unmount()
unmount();
}}
/>
</KeybindingSetup>
</AppStateProvider>,
{ exitOnCtrlC: true },
)
);
} catch (error) {
cliError((error as Error).message)
cliError((error as Error).message);
}
}
// mcp reset-project-choices (lines 49354952)
export async function mcpResetChoicesHandler(): Promise<void> {
logEvent('tengu_mcp_reset_mcpjson_choices', {})
logEvent('tengu_mcp_reset_mcpjson_choices', {});
saveCurrentProjectConfig(current => ({
...current,
enabledMcpjsonServers: [],
disabledMcpjsonServers: [],
enableAllProjectMcpServers: false,
}))
}));
cliOk(
'All project-scoped (.mcp.json) server approvals and rejections have been reset.\n' +
'You will be prompted for approval next time you start Claude Code.',
)
);
}

View File

@@ -72,27 +72,21 @@ export function handleMarketplaceError(error: unknown, action: string): never {
function printValidationResult(result: ValidationResult): void {
if (result.errors.length > 0) {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(
`${figures.cross} Found ${result.errors.length} ${plural(result.errors.length, 'error')}:\n`,
)
result.errors.forEach(error => {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` ${figures.pointer} ${error.path}: ${error.message}`)
})
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log('')
}
if (result.warnings.length > 0) {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(
`${figures.warning} Found ${result.warnings.length} ${plural(result.warnings.length, 'warning')}:\n`,
)
result.warnings.forEach(warning => {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` ${figures.pointer} ${warning.path}: ${warning.message}`)
})
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log('')
}
}
@@ -106,7 +100,6 @@ export async function pluginValidateHandler(
try {
const result = await validateManifest(manifestPath)
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(`Validating ${result.fileType} manifest: ${result.filePath}\n`)
printValidationResult(result)
@@ -120,7 +113,6 @@ export async function pluginValidateHandler(
if (basename(manifestDir) === '.claude-plugin') {
contentResults = await validatePluginContents(dirname(manifestDir))
for (const r of contentResults) {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(`Validating ${r.fileType}: ${r.filePath}\n`)
printValidationResult(r)
}
@@ -139,13 +131,11 @@ export async function pluginValidateHandler(
: `${figures.tick} Validation passed`,
)
} else {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(`${figures.cross} Validation failed`)
process.exit(1)
}
} catch (error) {
logError(error)
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.error(
`${figures.cross} Unexpected error during validation: ${errorMessage(error)}`,
)
@@ -358,7 +348,6 @@ export async function pluginListHandler(options: {
}
if (pluginIds.length > 0) {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log('Installed plugins:\n')
}
@@ -383,25 +372,18 @@ export async function pluginListHandler(options: {
const version = installation.version || 'unknown'
const scope = installation.scope
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` ${figures.pointer} ${pluginId}`)
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` Version: ${version}`)
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` Scope: ${scope}`)
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` Status: ${status}`)
for (const error of pluginErrors) {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` Error: ${getPluginErrorMessage(error)}`)
}
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log('')
}
}
if (inlinePlugins.length > 0 || inlineLoadErrors.length > 0) {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log('Session-only plugins (--plugin-dir):\n')
for (const p of inlinePlugins) {
// Same dirName≠manifestName fallback as the JSON path above — error
@@ -413,19 +395,13 @@ export async function pluginListHandler(options: {
pErrors.length > 0
? `${figures.cross} loaded with errors`
: `${figures.tick} loaded`
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` ${figures.pointer} ${p.source}`)
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` Version: ${p.manifest.version ?? 'unknown'}`)
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` Path: ${p.path}`)
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` Status: ${status}`)
for (const e of pErrors) {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` Error: ${getPluginErrorMessage(e)}`)
}
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log('')
}
// Path-level failures: no LoadedPlugin object exists. Show them so
@@ -433,7 +409,6 @@ export async function pluginListHandler(options: {
for (const e of inlineLoadErrors.filter(e =>
e.source.startsWith('inline['),
)) {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(
` ${figures.pointer} ${e.source}: ${figures.cross} ${getPluginErrorMessage(e)}\n`,
)
@@ -489,12 +464,10 @@ export async function marketplaceAddHandler(
}
}
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log('Adding marketplace...')
const { name, alreadyMaterialized, resolvedSource } =
await addMarketplaceSource(marketplaceSource, message => {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(message)
})
@@ -555,33 +528,25 @@ export async function marketplaceListHandler(options: {
cliOk('No marketplaces configured')
}
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log('Configured marketplaces:\n')
names.forEach(name => {
const marketplace = config[name]
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` ${figures.pointer} ${name}`)
if (marketplace?.source) {
const src = marketplace.source
if (src.source === 'github') {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` Source: GitHub (${src.repo})`)
} else if (src.source === 'git') {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` Source: Git (${src.url})`)
} else if (src.source === 'url') {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` Source: URL (${src.url})`)
} else if (src.source === 'directory') {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` Source: Directory (${src.path})`)
} else if (src.source === 'file') {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` Source: File (${src.path})`)
}
}
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log('')
})
@@ -620,11 +585,9 @@ export async function marketplaceUpdateHandler(
if (options.cowork) setUseCoworkPlugins(true)
try {
if (name) {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(`Updating marketplace: ${name}...`)
await refreshMarketplace(name, message => {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(message)
})
@@ -644,7 +607,6 @@ export async function marketplaceUpdateHandler(
cliOk('No marketplaces configured')
}
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(`Updating ${marketplaceNames.length} marketplace(s)...`)
await refreshAllMarketplaces()

View File

@@ -4,26 +4,24 @@
*/
/* eslint-disable custom-rules/no-process-exit -- CLI subcommand handlers intentionally exit */
import { cwd } from 'process'
import React from 'react'
import { WelcomeV2 } from '../../components/LogoV2/WelcomeV2.js'
import { useManagePlugins } from '../../hooks/useManagePlugins.js'
import type { Root } from '@anthropic/ink'
import { Box, Text } from '@anthropic/ink'
import { KeybindingSetup } from '../../keybindings/KeybindingProviderSetup.js'
import { logEvent } from '../../services/analytics/index.js'
import { MCPConnectionManager } from '../../services/mcp/MCPConnectionManager.js'
import { AppStateProvider } from '../../state/AppState.js'
import { onChangeAppState } from '../../state/onChangeAppState.js'
import { isAnthropicAuthEnabled } from '../../utils/auth.js'
import { cwd } from 'process';
import React from 'react';
import { WelcomeV2 } from '../../components/LogoV2/WelcomeV2.js';
import { useManagePlugins } from '../../hooks/useManagePlugins.js';
import type { Root } from '@anthropic/ink';
import { Box, Text } from '@anthropic/ink';
import { KeybindingSetup } from '../../keybindings/KeybindingProviderSetup.js';
import { logEvent } from '../../services/analytics/index.js';
import { MCPConnectionManager } from '../../services/mcp/MCPConnectionManager.js';
import { AppStateProvider } from '../../state/AppState.js';
import { onChangeAppState } from '../../state/onChangeAppState.js';
import { isAnthropicAuthEnabled } from '../../utils/auth.js';
export async function setupTokenHandler(root: Root): Promise<void> {
logEvent('tengu_setup_token_command', {})
logEvent('tengu_setup_token_command', {});
const showAuthWarning = !isAnthropicAuthEnabled()
const { ConsoleOAuthFlow } = await import(
'../../components/ConsoleOAuthFlow.js'
)
const showAuthWarning = !isAnthropicAuthEnabled();
const { ConsoleOAuthFlow } = await import('../../components/ConsoleOAuthFlow.js');
await new Promise<void>(resolve => {
root.render(
<AppStateProvider onChangeAppState={onChangeAppState}>
@@ -33,18 +31,16 @@ export async function setupTokenHandler(root: Root): Promise<void> {
{showAuthWarning && (
<Box flexDirection="column">
<Text color="warning">
Warning: You already have authentication configured via
environment variable or API key helper.
Warning: You already have authentication configured via environment variable or API key helper.
</Text>
<Text color="warning">
The setup-token command will create a new OAuth token which
you can use instead.
The setup-token command will create a new OAuth token which you can use instead.
</Text>
</Box>
)}
<ConsoleOAuthFlow
onDone={() => {
void resolve()
void resolve();
}}
mode="setup-token"
startingMessage="This will guide you through long-lived (1-year) auth token setup for your Claude account. Claude subscription required."
@@ -52,75 +48,63 @@ export async function setupTokenHandler(root: Root): Promise<void> {
</Box>
</KeybindingSetup>
</AppStateProvider>,
)
})
root.unmount()
process.exit(0)
);
});
root.unmount();
process.exit(0);
}
// DoctorWithPlugins wrapper + doctor handler
const DoctorLazy = React.lazy(() =>
import('../../screens/Doctor.js').then(m => ({ default: m.Doctor })),
)
const DoctorLazy = React.lazy(() => import('../../screens/Doctor.js').then(m => ({ default: m.Doctor })));
function DoctorWithPlugins({
onDone,
}: {
onDone: () => void
}): React.ReactNode {
useManagePlugins()
function DoctorWithPlugins({ onDone }: { onDone: () => void }): React.ReactNode {
useManagePlugins();
return (
<React.Suspense fallback={null}>
<DoctorLazy onDone={onDone} />
</React.Suspense>
)
);
}
export async function doctorHandler(root: Root): Promise<void> {
logEvent('tengu_doctor_command', {})
logEvent('tengu_doctor_command', {});
await new Promise<void>(resolve => {
root.render(
<AppStateProvider>
<KeybindingSetup>
<MCPConnectionManager
dynamicMcpConfig={undefined}
isStrictMcpConfig={false}
>
<MCPConnectionManager dynamicMcpConfig={undefined} isStrictMcpConfig={false}>
<DoctorWithPlugins
onDone={() => {
void resolve()
void resolve();
}}
/>
</MCPConnectionManager>
</KeybindingSetup>
</AppStateProvider>,
)
})
root.unmount()
process.exit(0)
);
});
root.unmount();
process.exit(0);
}
// install handler
export async function installHandler(
target: string | undefined,
options: { force?: boolean },
): Promise<void> {
const { setup } = await import('../../setup.js')
await setup(cwd(), 'default', false, false, undefined, false)
const { install } = await import('../../commands/install.js')
export async function installHandler(target: string | undefined, options: { force?: boolean }): Promise<void> {
const { setup } = await import('../../setup.js');
await setup(cwd(), 'default', false, false, undefined, false);
const { install } = await import('../../commands/install.js');
await new Promise<void>(resolve => {
const args: string[] = []
if (target) args.push(target)
if (options.force) args.push('--force')
const args: string[] = [];
if (target) args.push(target);
if (options.force) args.push('--force');
void install.call(
result => {
void resolve()
process.exit(result.includes('failed') ? 1 : 0)
void resolve();
process.exit(result.includes('failed') ? 1 : 0);
},
{},
args,
)
})
);
});
}

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type ask = any;
export type ask = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type installOAuthTokens = any;
export type installOAuthTokens = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type RemoteIO = any;
export type RemoteIO = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type StructuredIO = any;
export type StructuredIO = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type collectContextData = any;
export type collectContextData = any

View File

@@ -1,14 +1,14 @@
// Auto-generated type stub — replace with real implementation
export type SDKStatus = any;
export type ModelInfo = any;
export type SDKMessage = any;
export type SDKUserMessage = any;
export type SDKUserMessageReplay = any;
export type PermissionResult = any;
export type McpServerConfigForProcessTransport = any;
export type McpServerStatus = any;
export type RewindFilesResult = any;
export type HookEvent = any;
export type HookInput = any;
export type HookJSONOutput = any;
export type PermissionUpdate = any;
export type SDKStatus = any
export type ModelInfo = any
export type SDKMessage = any
export type SDKUserMessage = any
export type SDKUserMessageReplay = any
export type PermissionResult = any
export type McpServerConfigForProcessTransport = any
export type McpServerStatus = any
export type RewindFilesResult = any
export type HookEvent = any
export type HookInput = any
export type HookJSONOutput = any
export type PermissionUpdate = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type SDKControlElicitationResponseSchema = any;
export type SDKControlElicitationResponseSchema = any

View File

@@ -1,9 +1,9 @@
// Auto-generated type stub — replace with real implementation
export type StdoutMessage = any;
export type SDKControlInitializeRequest = any;
export type SDKControlInitializeResponse = any;
export type SDKControlRequest = any;
export type SDKControlResponse = any;
export type SDKControlMcpSetServersResponse = any;
export type SDKControlReloadPluginsResponse = any;
export type StdinMessage = any;
export type StdoutMessage = any
export type SDKControlInitializeRequest = any
export type SDKControlInitializeResponse = any
export type SDKControlRequest = any
export type SDKControlResponse = any
export type SDKControlMcpSetServersResponse = any
export type SDKControlReloadPluginsResponse = any
export type StdinMessage = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type CanUseToolFn = any;
export type CanUseToolFn = any

View File

@@ -1,5 +1,5 @@
// Auto-generated type stub — replace with real implementation
export type tryGenerateSuggestion = any;
export type logSuggestionOutcome = any;
export type logSuggestionSuppressed = any;
export type PromptVariant = any;
export type tryGenerateSuggestion = any
export type logSuggestionOutcome = any
export type logSuggestionSuppressed = any
export type PromptVariant = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type getFeatureValue_CACHED_MAY_BE_STALE = any;
export type getFeatureValue_CACHED_MAY_BE_STALE = any

View File

@@ -1,3 +1,3 @@
// Auto-generated type stub — replace with real implementation
export type logEvent = any;
export type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS = any;
export type logEvent = any
export type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS = any

View File

@@ -1,3 +1,3 @@
// Auto-generated type stub — replace with real implementation
export type isQualifiedForGrove = any;
export type checkGroveForNonInteractive = any;
export type isQualifiedForGrove = any
export type checkGroveForNonInteractive = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type EMPTY_USAGE = any;
export type EMPTY_USAGE = any

View File

@@ -1,3 +1,3 @@
// Auto-generated type stub — replace with real implementation
export type statusListeners = any;
export type ClaudeAILimits = any;
export type statusListeners = any
export type ClaudeAILimits = any

View File

@@ -1,3 +1,3 @@
// Auto-generated type stub — replace with real implementation
export type performMCPOAuthFlow = any;
export type revokeServerTokens = any;
export type performMCPOAuthFlow = any
export type revokeServerTokens = any

View File

@@ -1,3 +1,3 @@
// Auto-generated type stub — replace with real implementation
export type isChannelAllowlisted = any;
export type isChannelsEnabled = any;
export type isChannelAllowlisted = any
export type isChannelsEnabled = any

View File

@@ -1,5 +1,5 @@
// Auto-generated type stub — replace with real implementation
export type ChannelMessageNotificationSchema = any;
export type gateChannelServer = any;
export type wrapChannelMessage = any;
export type findChannelEntry = any;
export type ChannelMessageNotificationSchema = any
export type gateChannelServer = any
export type wrapChannelMessage = any
export type findChannelEntry = any

View File

@@ -1,7 +1,7 @@
// Auto-generated type stub — replace with real implementation
export type setupSdkMcpClients = any;
export type connectToServer = any;
export type clearServerCache = any;
export type fetchToolsForClient = any;
export type areMcpConfigsEqual = any;
export type reconnectMcpServerImpl = any;
export type setupSdkMcpClients = any
export type connectToServer = any
export type clearServerCache = any
export type fetchToolsForClient = any
export type areMcpConfigsEqual = any
export type reconnectMcpServerImpl = any

View File

@@ -1,6 +1,6 @@
// Auto-generated type stub — replace with real implementation
export type filterMcpServersByPolicy = any;
export type getMcpConfigByName = any;
export type isMcpServerDisabled = any;
export type setMcpServerEnabled = any;
export type getAllMcpConfigs = any;
export type filterMcpServersByPolicy = any
export type getMcpConfigByName = any
export type isMcpServerDisabled = any
export type setMcpServerEnabled = any
export type getAllMcpConfigs = any

View File

@@ -1,3 +1,3 @@
// Auto-generated type stub — replace with real implementation
export type runElicitationHooks = any;
export type runElicitationResultHooks = any;
export type runElicitationHooks = any
export type runElicitationResultHooks = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type getMcpPrefix = any;
export type getMcpPrefix = any

View File

@@ -1,4 +1,4 @@
// Auto-generated type stub — replace with real implementation
export type MCPServerConnection = any;
export type McpSdkServerConfig = any;
export type ScopedMcpServerConfig = any;
export type MCPServerConnection = any
export type McpSdkServerConfig = any
export type ScopedMcpServerConfig = any

View File

@@ -1,3 +1,3 @@
// Auto-generated type stub — replace with real implementation
export type commandBelongsToServer = any;
export type filterToolsByServer = any;
export type commandBelongsToServer = any
export type filterToolsByServer = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type setupVscodeSdkMcp = any;
export type setupVscodeSdkMcp = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type OAuthService = any;
export type OAuthService = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type isPolicyAllowed = any;
export type isPolicyAllowed = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type waitForRemoteManagedSettingsToLoad = any;
export type waitForRemoteManagedSettingsToLoad = any

View File

@@ -1,3 +1,3 @@
// Auto-generated type stub — replace with real implementation
export type downloadUserSettings = any;
export type redownloadUserSettings = any;
export type downloadUserSettings = any
export type redownloadUserSettings = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type AppState = any;
export type AppState = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type externalMetadataToAppState = any;
export type externalMetadataToAppState = any

View File

@@ -1,3 +1,3 @@
// Auto-generated type stub — replace with real implementation
export type assembleToolPool = any;
export type filterToolsByDenyRules = any;
export type assembleToolPool = any
export type filterToolsByDenyRules = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type createAbortController = any;
export type createAbortController = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type uniq = any;
export type uniq = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type getAccountInformation = any;
export type getAccountInformation = any

View File

@@ -1,4 +1,4 @@
// Auto-generated type stub — replace with real implementation
export type getLatestVersion = any;
export type InstallStatus = any;
export type installGlobalPackage = any;
export type getLatestVersion = any
export type InstallStatus = any
export type installGlobalPackage = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type AwsAuthStatusManager = any;
export type AwsAuthStatusManager = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type modelSupportsAutoMode = any;
export type modelSupportsAutoMode = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type registerCleanup = any;
export type registerCleanup = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type createCombinedAbortSignal = any;
export type createCombinedAbortSignal = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type notifyCommandLifecycle = any;
export type notifyCommandLifecycle = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type incrementPromptCount = any;
export type incrementPromptCount = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type regenerateCompletionCache = any;
export type regenerateCompletionCache = any

View File

@@ -1,4 +1,4 @@
// Auto-generated type stub — replace with real implementation
export type getGlobalConfig = any;
export type InstallMethod = any;
export type saveGlobalConfig = any;
export type getGlobalConfig = any
export type InstallMethod = any
export type saveGlobalConfig = any

View File

@@ -1,3 +1,3 @@
// Auto-generated type stub — replace with real implementation
export type loadConversationForResume = any;
export type TurnInterruptionState = any;
export type loadConversationForResume = any
export type TurnInterruptionState = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type getCwd = any;
export type getCwd = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type logForDebugging = any;
export type logForDebugging = any

View File

@@ -1,3 +1,3 @@
// Auto-generated type stub — replace with real implementation
export type logForDiagnosticsNoPII = any;
export type withDiagnosticsTiming = any;
export type logForDiagnosticsNoPII = any
export type withDiagnosticsTiming = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type getDoctorDiagnostic = any;
export type getDoctorDiagnostic = any

View File

@@ -1,5 +1,5 @@
// Auto-generated type stub — replace with real implementation
export type modelSupportsEffort = any;
export type modelSupportsMaxEffort = any;
export type EFFORT_LEVELS = any;
export type resolveAppliedEffort = any;
export type modelSupportsEffort = any
export type modelSupportsMaxEffort = any
export type EFFORT_LEVELS = any
export type resolveAppliedEffort = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type AbortError = any;
export type AbortError = any

View File

@@ -1,5 +1,5 @@
// Auto-generated type stub — replace with real implementation
export type isFastModeAvailable = any;
export type isFastModeEnabled = any;
export type isFastModeSupportedByModel = any;
export type getFastModeState = any;
export type isFastModeAvailable = any
export type isFastModeEnabled = any
export type isFastModeSupportedByModel = any
export type getFastModeState = any

View File

@@ -1,5 +1,5 @@
// Auto-generated type stub — replace with real implementation
export type fileHistoryRewind = any;
export type fileHistoryCanRestore = any;
export type fileHistoryEnabled = any;
export type fileHistoryGetDiffStats = any;
export type fileHistoryRewind = any
export type fileHistoryCanRestore = any
export type fileHistoryEnabled = any
export type fileHistoryGetDiffStats = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type executeFilePersistence = any;
export type executeFilePersistence = any

View File

@@ -1,4 +1,4 @@
// Auto-generated type stub — replace with real implementation
export type createFileStateCacheWithSizeLimit = any;
export type mergeFileStateCaches = any;
export type READ_FILE_STATE_CACHE_SIZE = any;
export type createFileStateCacheWithSizeLimit = any
export type mergeFileStateCaches = any
export type READ_FILE_STATE_CACHE_SIZE = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type getLastCacheSafeParams = any;
export type getLastCacheSafeParams = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type fromArray = any;
export type fromArray = any

View File

@@ -1,4 +1,4 @@
// Auto-generated type stub — replace with real implementation
export type gracefulShutdown = any;
export type gracefulShutdownSync = any;
export type isShuttingDown = any;
export type gracefulShutdown = any
export type gracefulShutdownSync = any
export type isShuttingDown = any

View File

@@ -1,4 +1,4 @@
// Auto-generated type stub — replace with real implementation
export type headlessProfilerStartTurn = any;
export type headlessProfilerCheckpoint = any;
export type logHeadlessProfilerTurn = any;
export type headlessProfilerStartTurn = any
export type headlessProfilerCheckpoint = any
export type logHeadlessProfilerTurn = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type executeNotificationHooks = any;
export type executeNotificationHooks = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type finalizePendingAsyncHooks = any;
export type finalizePendingAsyncHooks = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type registerHookEventHandler = any;
export type registerHookEventHandler = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type createIdleTimeoutManager = any;
export type createIdleTimeoutManager = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type safeParseJSON = any;
export type safeParseJSON = any

Some files were not shown because too many files have changed in this diff Show More